Algoritmer og datastrukturer
Kapittel 9Delkapittel 9.1
9.1  Generelt om balanserte trær

Til Avsnitt 9.1.2 - Fra tabell til balansert tre   9.1.1  Hva er et balansert tre?
Et perfekt binærtre
Figur 9.1.1 a) : Et perfekt binærtre
Et balansert binærtre
Figur 9.1.1 b) : Et balansert binærtre
Et høydebalansert binærtre
Figur 9.1.1 c) : Et høydebalansert binærtre
Begreper som balanse, likevekt og skjevhet brukes i forbindelse med binære trær og da spesielt binære søketrær. Men begrepene defineres og brukes litt forskjellig i faglitteraturen. Figur 9.1.1 a) til venstre viser et perfekt binærtre. Et slikt tre må kunne kalles balansert. Hver node har like mye på hver side.

For treet i Figur 9.1.1 b) er det annerledes. Ta f.eks. rotnodens venstre barn. Dens venstre subtre har høyde 2 og 5 noder, mens det høyre har høyde 1 og 3 noder. Men det er ikke mulig å lage noe «bedre» tre med tanke på balanse når vi som her har 19 noder. Her er alle nivåene, bortsett fra det nederste, fylt opp med noder. Slike trær har høyde h = ⌊log2(n)⌋ (obs: ⌊⌋ er avrunding nedover) der n er antall noder. Generelt for binærtrær gjelder ⌊log2(n)⌋ ≤ hn − 1. Uformelt kunne vi derfor si at jo nærmere høyden til et tre er ⌊log2(n)⌋, jo mer balansert er det. Obs: Høyden til et tre har to forskjellige definisjoner i faglitteraturen. En sier at det er lik antall nivåer i treet, mens en annen sier at det er største avstand mellom roten og en node (eller lengden på treets lengste gren). Forskjellen på de to er 1. Her brukes den siste og den er i henhold til NIST.

Et binærtre kalles høydebalansert hvis det for hver node er slik at høydeforskjellen mellom nodens to subtrær ikke er mer enn 1. Treet i Figur 9.1.1 b) og det i Figur 9.1.1 c) oppfyller dette. Sjekk at det stemmer! Husk at et tre med kun én node har høyde 0 og et tomt tre har høyde −1. For slike trær gjelder h < 1,44 log2(n + 2) − 1,328, dvs. god balanse. Et binært søketre av denne typen kalles et AVL-tre. Et AVL-tre har i tillegg algoritmer for å holde treet høydebalansert etter en innlegging eller en fjerning.

Et alternativ er antallbalanse eller vektbalanse (antall tolkes som vekt). Her må antallet noder i en vilkårlig nodes subtrær utgjøre en gitt minsteandel α av antallet i treet med noden som rot. Hvis p er en vilkårlig node, må antall(p.venstre) ≥ α · antall(p) og antall(p.høyre) ≥ α · antall(p). Her er det viktig å sette rett verdi på andelsfaktoren α. Hvis vi f.eks. bruker α = 1/5, vil treet i Figur 9.1.1 c) bli antallbalansert med 1/5 = 0,2 som andelsfaktor. Sjekk det!

Søking er den viktigste operasjonen i et binært søketre. Det er viktig at den er effektiv, dvs. av logaritmisk orden. Tiden det tar å finne en node er bestemt av hvor langt ned i treet den ligger. Hvis treet har en høyde av logaritmisk orden, så blir søkingen effektiv. Et annet begrep er gjennomsnittlig nodedybde. Dybden til en node er lik avstanden fra noden til roten og gjennomsnittet tas over alle nodene. Gjennomsnittlig nodedybde er alltid mindre enn treets høyde. Det optimale er et tre der alle nivåene, eventuelt bortsett fra det nederste, er fylt opp med noder. Det motsatte er et tre der ingen noder har to barn. For disse ytterpunktene finnes det formler for gjennomsnittlig nodedybde gnd(n) av antallet n. Dermed:

(9.1.1)       ((n + 1) k − 2(2k − 1)) / n  ≤  gnd(n)  ≤  (n − 1)/2,   k = ⌊log2(n + 1)⌋

Uttrykket på venstre side i (9.1.1) er for store verdier av n tilnærmet lik log2(n + 1) − 2 og dermed log2(n + 1) − 2 ≤ gnd(n) ≤ (n − 1)/2. I Avsnitt 5.2.4 fant vi at et gjennomsnittlig binært søketre med n forskjellige verdier (n stor) har en gjennomsnittlig nodedybde på 1,386·log2(n + 1) − 2,846. Dvs. at søking i et binært søketre i gjennomsnitt er effektivt. Men hvis søking alltid skal være effektivt, må en bruke teknikker for å balansere treet hvis det etter en innleggging eller fjerning har kommet i en eller annen form for ubalanse. En mulighet er å bruke høydebalanse og dermed et AVL-tre. En annen mulighet er «fargebalanse». I et rød/svart binærtre er en node enten «rød» eller «svart». En ny node er alltid rød og hvis forelderen også er rød, starter en balanseoperasjon. Et slikt tre har en høyde som maksimalt er det dobbelte av det optimale (⌊log2(n)⌋), men normalt en del bedre.

Ubalanse Et binærtre der ingen noder har to barn, er det mest ubalanserte av alle. Et slikt tre har høyde n − 1 og gjennomsnittelig nodedybde (n − 1)/2. Det er to ytterpunkter. Et binærtre kalles ekstremt høyreskjevt hvis ingen noder har venstre barn og ekstremt venstreskjevt hvis ingen noder har høyre barn.

Til fasit   Oppgaver til Avsnitt 9.1.1
1. Treet Figur 9.1.1 b) er et eksempel på et binærtre med 19 noder og alle nivåene bortsett fra det nederste, er fulle av noder. Hvor mange slike trær er det?
2. Gitt et binærtre med n noder. Hvis alle nivåene, eventuelt bortsett fra det nederste, er fylt opp med noder, har treet høyde ⌊log2(n)⌋. Er det motsatte sant?
3. Hvor mange forskjellige binære trær finnes det med n noder der ingen node har to barn.
4. Setning 9.1.1 har på venstre side en formel for den gjennomsnittlige nodedybden for et binærtre der alle nivåene eventuelt bortsett fra det nederste, er fulle av noder.
a) Sjekk at formelen stemmer for trærne i Figur 9.1.1 a og b).
b) Vis at formelen stemmer for alle n. Hint: Start med å lage en formel som gjelder for perfekte trær. Formel G.1.9 kommer til nytte her.

Til Avsnitt 9.1.3 - Komplette trær   9.1.2  Fra tabell til balansert tre
Anta at vi har verdier i en tabell. En aktuell problemstilling er å lage et balansert binærtre som inneholder tabellverdiene.

Vi kan bruke flg. idé: La tabellens midtverdi bli rotnodens verdi. Gå så til hver side av midtverdien. La midtverdien av tabellens venstre side bli verdien i rotnodens venstre barn og midtverdien på høyre side verdien i rotnodens høyre barn. Osv. Dette gjøres enklest ved at en rekursiv metode kalles først på den delen av tabellen som ligger til venstre for midten og deretter på den delen som ligger til høyre:

  private static <T> Node<T> balansert(T[] a, int v, int h)
  {
    if (v > h) return null;                // et tomt intervall
    int m = (v + h)/2;                     // midten
    return new Node<>(a[m], balansert(a, v, m - 1), balansert(a, m + 1, h));
  }
                Programkode 9.1.2 a)

Treet blir balansert siden vi alltid går inn på midten av intervallet. Da alle verdiene til venstre for midten havner i venstre subtre og alle til høyre i høyre subtre, vil en inorden traversering gi verdiene i tabellrekkefølgen. OBS: Hvis tabellen er sortert og uten duplikater, blir treet et binært søketre. Men ikke nødvendigvis hvis tabellen har duplikater. Se Avsnitt 5.2.5.

Metoden balansert() kan legges i enhver klasse som har en tilsvarende nodestruktur, f.eks. i klassen BinTre. Da må klassen ha en offentlig metode som kaller den private metoden:

  public static <T> BinTre<T> balansert(T[] a)
  {
    BinTre<T> tre = new BinTre<>();        // et tomt tre
    tre.rot = balansert(a,0,a.length-1);   // treet bygges opp
    tre.antall = a.length;                 // antall som i tabellen
    return tre;                            // treet returneres
  }
                Programkode 9.1.2 b)
Et balansert binærtre med 10 noder
Figur 9.1.2 a)

Hvis dette ligger i klassen BinTre, vil flg. eksempel vise hvordan det kan brukes. Utskriften viser at treets verdier i inorden svarer til hvordan de ligger i tabellen. Figur 9.1.2 a) viser hvordan treet blir:

  Integer[] a = {1,2,3,4,5,6,7,8,9,10};
  BinTre<Integer> tre = BinTre.balansert(a);
  tre.inorden(Oppgave.konsollutskrift());  // Se inorden
  // Utskrift: 1 2 3 4 5 6 7 8 9 10           og Oppgave 

          Programkode 9.1.2 c)

Til fasit   Oppgaver til Avsnitt 9.1.2
1. Hvordan blir treet Programkode 9.1.2 c) hvis tabellen inneholder tallene fra 1 til 19?
2. Metoden public boolean erBalansert() i klassen BinTre skal returnere true hvis alle nivåene eventuelt borsett fra det nederste, er fulle av noder og false ellers. Lag metoden og bruk den i Programkode 9.1.2 c).
3. Lag metoden public boolean erHøydeBalansert() i klassen BinTre. Den skal returnere true hvis treet er høydebalansert og false ellers.

Til Avsnitt 9.1.4 - Balansering ved hjelp av rotasjoner   9.1.3  Komplette trær
Det kan være ønskelig å bygge opp et komplett binærtre ved hjelp av verdiene i en tabell og f.eks. lage treet slik at en traversering i inorden eller eventuelt i nivåorden, gir verdiene i samme rekkefølge som de har i tabellen. Det siste er det enkleste å få til siden den første verdien da må høre til rotnoden, de to neste til rotnodens to barn, osv. Flg. private rekursive metode gjør nettopp det. En offentlig metode setter rekursjonen i gang:

  private static <T> Node<T> komplett(T[] a, int posisjon)
  {
    if (posisjon > a.length) return null;

    Node<T> venstre = komplett(a, 2*posisjon);     // venstre subtre
    Node<T> høyre = komplett(a, 2*posisjon + 1);   // høyre subtre

    return new Node<>(a[posisjon - 1], venstre, høyre);  // rotnoden
  }

  public static <T> BinTre<T> komplettNivåorden(T[] a)
  {
    BinTre<T> tre = new BinTre<>();
    tre.antall = a.length;
    tre.rot = komplett(a,1);
    return tre;
  }
                Programkode 9.1.3 a)

Vi trenger en metode som sjekker om et binærtre er komplett. Dermed kan vi få testet om Programkode 9.1.3 a) virker som den skal. Et binærtre med n noder er komplett hvis og bare hvis nodene har posisjonstall fra 1 til n. Vi lager derfor en rekursiv metode som traverserer treet og sjekker nodenes posisjonstall:

  private static <T> boolean sjekkPosisjon(Node<T> p, int pos, int n)
  {
    if (p == null) return true;                 // et tomt tre er komplett
    return pos <= n                             // sjekker posisjonstallet
      && sjekkPosisjon(p.venstre, 2*pos, n)     // sjekker venstre subtre
      && sjekkPosisjon(p.høyre, 2*pos + 1, n);  // sjekker høyre subtre
  }

  public boolean erKomplett()
  {
    return sjekkPosisjon(rot,1,antall);  // kaller den rekursive metoden
  }
                Programkode 9.1.3 b)

Hvis Programkode 9.1.3 a) og b) ligger i klassen BinTre, vil flg. eksempel virke:

  Integer[] a = {1,2,3,4,5,6,7,8,9,10};
  BinTre<Integer> tre = BinTre.komplettNivåorden(a);
  tre.nivåorden(Oppgave.konsollutskrift());
  System.out.println("  " + tre.erKomplett());

  // Utskrift: 1 2 3 4 5 6 7 8 9 10  true

                Programkode 9.1.3 c)

Det å lage et komplett binærtre der en traversering i inorden gir verdiene i samme rekkefølge som de har i tabellen, er vanskeligere å få til.

En mulig fremgangsmåte er å lage et komplett binærtre med plass til så mange verdier som det er i tabellen og så fylle det med verdier fra tabellen gjennom en inorden traversering. Det komplette treet kan f.eks. lages på samme måte som i Programkode 9.1.3 a):

  private static <T> Node<T> komplett(int n, int posisjon)  // n verdier
  {
    if (posisjon > n) return null;

    Node<T> venstre = komplett(n, 2*posisjon);       // venstre subtre
    Node<T> høyre = komplett(n, 2*posisjon + 1);     // høyre subtre

    return new Node<>(null,venstre,høyre);    // null som nodeverdi
  }
                Programkode 9.1.3 d)

Neste skritt er legge inn verdiene mens treet traverseres. Det kan gjøres rekursivt eller iterativt (se Oppgave 2 og 3 for iterative løsninger). Under traverseringen må vi holde orden på hvilket nummer noden har i inorden og så legge inn tilsvarende verdi fra tabellen. Som «teller» bruker vi en int-tabell med kun én verdi:

  private static <T> void leggInnInorden(Node<T> p, T[] a, int[] indeks)
  {
    if (p.venstre != null) leggInnInorden(p.venstre, a, indeks);
    p.verdi = a[indeks[0]];   // setter inn tabellverdi
    indeks[0]++;              // øker indeks
    if (p.høyre != null) leggInnInorden(p.høyre, a, indeks);
  }

  public static <T> BinTre<T> komplettInorden(T[] a)
  {
    BinTre<T> tre = new BinTre<>();
    int[] indeks = {0};
    tre.rot = komplett(a.length,1);
    leggInnInorden(tre.rot, a, indeks);
    tre.antall = a.length;
    return tre;
  }
                Programkode 9.1.3 e)

Flg. programbit der tabellen inneholder bokstaver, viser hvordan dette vil virke:

  String[] s = "ABCDEFGHIJKL".split("");           // = {"A","B","C", . . . }
  BinTre<String> tre = BinTre.komplettInorden(s);  // bygger opp treet

  tre.inorden(Oppgave.konsollutskrift());          // skriver ut i inorden
  System.out.println("  " + tre.erKomplett());     // sjekker om det er komplett

  // Utskrift: A B C D E F G H I J K L  true

                Programkode 9.1.3 f)

Det er mulig å lage et komplett binærtre med verdiene fra en tabell i inorden på en tilsvarende måte som i Programkode 9.1.2 a). Der var det tabellens midtverdi som ble rotnodeverdi. Men det vil ikke fungere for et komplett binærtre siden det der normalt er flere noder i rotnodens venstre subtre enn i høyre subtre. Figuren under til venstre viser et komplett binærtre med 17 noder:

Et komplett binærtre med 17 noder
Figur 9.1.3 a): 17 noder

I dette treet er det 9 noder i rotnodens venstre subtre og 7 i høyre subtre. Hvis vi har en tabell a, må derfor de 9 første verdiene i a (indekser fra 0 til 8) legges i venstre subtre, den 10-ende verdien ( a[9] ) i rotnoden og resten i høyre subtre.

Hvis vi isteden vil lage et komplett binærtre med 18 noder, blir det 10 noder i venstre subtre og dermed er det verdi nr. 11 ( a[10] ) som skal inn i rotnoden. Osv. Men for komplette trær med fra 23 og oppover til 31 noder vil antallet noder i venstre subtre være fast lik 15.

Dette kan oppsummeres slik: Gitt at verdiene i en tabell a med lengde n skal legges inn i et komplett binærtre slik at verdienes rekkefølge i inorden er den samme som i tabellen. Hvis m er antallet noder i rotnodens venstre subtre, er det verdien a[m] som skal inn i rotnoden. Oppgaven blir derfor å finne m som funksjon av n.

Treet i Figur 9.1.3 a) har 17 noder. Tallet 17 skrives som 10001 på binærform. Hvis vi «stripper» tallet, dvs. setter alle 1-ere til 0 bortsett fra den første, får vi det binære tallet k = 10000 som svarer til 16. Videre gir 16 - 1 = 15 antallet noder i det perfekte treet vi får ved å fjerne nederste rad. Dermed vil antallet noder på nederste rad være lik nk + 1. Vi ser at antallet noder i venstre subtre er avhengig av om antallet på nederste rad er mindre enn eller lik antallet på nest nederste rad eller ikke. Dvs. hvis nk + 1 ≤ k/2 (eller nk < k/2 eller nk/2 < k), blir antallet m i venstre subtre lik nk/2. Hvis ikke, blir antallet m i venstre subtre lik k − 1.

Det finnes en ferdig metode som «stripper» et positivt heltall. Den ligger i klassen Integer og heter highestOneBit(). Dermed kan vi lage flg. metode:

  private static <T> Node<T> komplett(T[] a, int v, int h)
  {
    if (v > h) return null;             // tomt intervall

    int n = h - v + 1;                  // antallet i intervallet
    int k = Integer.highestOneBit(n);   // stripper n

    int m = n - (k >> 1);               // k >> 1 svarer til k/2
    m = (m < k ? m : k - 1) + v;        // m relativt til v

    Node<T> venstre = komplett(a, v, m - 1);   // venstre subtre
    Node<T> høyre   = komplett(a, m + 1, h);   // høyre subtre

    return new Node<>(a[m], venstre, høyre);
  }
                Programkode 9.1.3 g)

Det er mulig å optimalisere Programkode 9.1.3 g) noe. Vi vet at i et komplett binærtre er minst ett av rotnodens to subtrær et perfekt binærtre. I noen tilfeller er begge perfekte. Hvis vi skal bygge opp et perfekt tre ved hjelp av verdiene i et tabellintervall, vil det være intervallets midtverdi som skal inn i rotnoden. Dermed kan vi isteden bruke metoden balansert() i Programkode 9.1.2 a). Dette innebærer en effektivisering siden det er litt mindre arbeid å finne midten i en tabell enn å utføre den beregningen som inngår i starten av Programkode 9.1.3 g). Se Oppgave 6.

Vi trenger en offentlig metode som kaller den rekursive metoden i Programkode 9.1.3 g):

  public static <T> BinTre<T> komplettInorden(T[] a)  // ny versjon
  {
    BinTre<T> tre = new BinTre<>();
    tre.antall = a.length;
    tre.rot = komplett(a,0,a.length-1);
    return tre;
  }
                Programkode 9.1.3 h)

Flg. programbit tester dette:

  Integer[] a = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};
  BinTre<Integer> tre = BinTre.komplettInorden(a);

  System.out.println(tre.erKomplett());
  tre.inorden(Oppgave.konsollutskrift());
  System.out.println();
  tre.nivåorden(Oppgave.konsollutskrift());

  // Utskrift:
  // true
  // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
  // 13 8 17 4 11 15 19 2 6 10 12 14 16 18 20 1 3 5 7 9

                Programkode 9.1.3 i)
Til fasit   Oppgaver til Avsnitt 9.1.3
1. Ta utgangspunkt i Programkode 9.1.3 c). Sett opp tallene fra 1 til 10 i en slik rekkefølge at når treet bygges med metoden komplettNivåorden(), vil en inorden traversering gi tallene i sortert rekkefølge. Sjekk svaret ved å bruke traverseringsmetoden inorden(). Gjør deretter det samme med tallene fra 1 til 20.
2. Hvor mange bladnoder har et komplett binærtre med n noder?
3. Metoden komplettNivåorden() bruker en rekursiv hjelpemetode. Lag den uten rekursjon, f.eks. ved å bruke en kø.
2. Lag metoden public static <T> BinTre<T> komplettInorden(T[] a) uten rekursjon. I koden skal det først opprettes et tomt binærtre, så skal treet bygges ved et kall på metoden i Programkode 9.1.3 d). Deretter skal det gjøres en iterativ traversering ved hjelp av en stakk (bruk samme teknikk som i Programkode 5.1.8 g ) der verdiene fra tabellen fortløpende legges inn i nodene. Til slutt returneres treet. Lag en programbit som tester det du har laget.
3. Ta utgangspunkt i Figur 9.1.3 a). Legg på noder forløpende slik at treet hele tiden er komplett. Hvor mange noder er det i rotnodens venstre subtre når treet får 18, 19, 20, 21, 22, 23, 24, 25 og 26 noder?
4. Det påstås i teksten at i et komplett binærtre er minst ett av rotnodens to subtrær et perfekt binærtre. Tegn et komplett binærtre med 20 noder. Hvilket av rotnodens to subtrær er perfekt? Gjør det samme for 23 noder og 25 noder.
5. Gjør om Programkode 9.1.3 g) slik at metoden balansert() fra Programkode 9.1.2 a) kalles hvis tabellintervallet er slik at treet vil bli perfekt.

Til Avsnitt 9.1.1 - Hva er et balansert tre?   9.1.4  Balansering ved hjelp av rotasjoner
Figur 9.1.4 a) under viser til venstre et utsnitt av et binærtre. Vi har to noder p og q med verdier x og y der q er venstre barn til p. Bokstavene a, b og c står for subtrær. Et eller flere av dem kan være tomme.

En høyrerotasjon
Figur 9.1.4 a) : En høyrerotasjon med hensyn på p

Til høyre i Figur 9.1.4 a) står resultatet av en høyrerotasjon med hensyn på p. Vi ser at noden q har «gått opp» og noden p har «gått ned» og dermed at p nå har blitt høyre barn til q. Videre har b, som opprinnelig var høyre subtre til q, nå blitt venstre subtre til p. Den krumme pilen i midten markerer at det er utført en rotasjon.

En viktig egenskap ved en høyrerotasjon er at nodene vil ha nøyaktig samme rekkefølge i inorden etter som før rotasjonen. Før rotasjonen, til venstre i figuren, er rekkefølgen i inorden lik axbyc og det er nøyaktig slik det er til høyre etter rotasjonen.

Flg. metode, som har p som parameter, gjør en høyrerotasjon med hensyn på p og returnerer en referanse til det som opprinnelig var p sitt venstre barn:

  private static <T> Node<T> høyreRotasjon(Node<T> p)
  {
    Node<T> q = p.venstre;
    p.venstre = q.høyre;
    q.høyre = p;
    return q;
  }
                Programkode 9.1.4 a)

En venstrerotasjon gjør det motsatte av en høyrerotasjon. Hvis vi gjør en venstrerotasjon med hensyn på p i treutsnittet til venstre i Figur 9.1.4 b) under, blir resultatet treutsnittet til høyre. Noden p går ned og noden q opp:

En venstrerotasjon
Figur 9.1.4 b) : En venstrerotasjon med hensyn på p

En venstrerotasjon får vi ved å bytte venstre med høyre i Programkode 9.1.4 a):

  private static <T> Node<T> venstreRotasjon(Node<T> p)
  {
    Node<T> q = p.høyre;
    p.høyre = q.venstre;
    q.venstre = p;
    return q;
  }
                Programkode 9.1.4 b)

Hvis vi kombinerer to eller flere rotasjoner, vil vi kunne oppnå ulike effekter. Hvis vi starter med utsnittet under til venstre i Figur 9.1.4 c), så gjør en venstrerotasjon med hensyn på q og så en høyrerotasjon med hensyn på p, får vi det som står under til høyre i Figur 9.1.4 c):

En venstre- og en høyrerotasjon
Figur 9.1.4 c) : En venstre- og en høyrerotasjon

Vi ser spesielt at rekkefølgen i inorden bevares. Den var lik axbyczd og det er den også etterpå. Denne kombinasjonen av rotasjoner kalles en dobbel høyrerotasjon. Selv om den kan uføres ved hjelp av de to rotasjonsmetodene vi allerede har, er det likevel vanlig å lage en egen metode for det. Se Oppgave 1.

Det er mulig å «brette ut» et hvilket som helst binærtre til et «ekstremt høyreskjevt» tre ved hjelp av en serie høyrerotasjoner. Et binærtre kalles ekstremt høyreskjevt hvis ingen noder har venstre barn. I Figur 9.1.4 d) under er det utført to høyrerotasjoner. Den første (på treet til venstre) der rotasjonen er med hensyn på rotnoden (verdien D). Det gir det midterste treet som resultat. På det treet er det på nytt utført en høyrerotasjon med hensyn på rotnoden (den har verdi B):

To høyrerotasjoner
Figur 9.1.4 d) : To høyrerotasjoner med hensyn på rotnoden

Vi kan fortsette «utbrettingen» ved å gå fra rotnoden i det høyre treet (verdien A) og nedover mot høyre. For hver node nedover som har venstre barn, gjør vi roteringer inntil det ikke lenger er venstre barn. Osv. Da vil vi ende opp med et ekstremt høyreskjevt tre:

  public void gjørEkstremtHøyreskjevt()
  {
    if (antall() <= 1) return;

    while (rot.venstre != null) rot = høyreRotasjon(rot);

    Node<T> f = rot;   // forelder
    while (f.høyre != null)
    {
      Node<T> p = f.høyre;  // roterer med hensyn på p
      while (p.venstre != null) p = høyreRotasjon(p);
      f.høyre = p;

      f = f.høyre;
    }
  }             Programkode 9.1.4 c)

Vi kan teste om «utbrettingsteknikken» virker ved f.eks. å traversere treet i både inorden og nivåorden. Hvis treet er ekstremt høyreskjevt, vil vi få samme utskrift i de to tilfellene. Men vi kan også lage en egen metode som sjekker dette. Da er det bare å starte i rotnoden og for hver node nedover til høyre må det undersøke om noden har et venstre barn. Hvis en node har det, så er treet ikke ekstremt høyreskjevt. Dette kan kodes slik:

  public boolean erEkstremtHøyreskjevt()
  {
    for (Node<T> p = rot; p != null; p = p.høyre)
      if (p.venstre != null) return false;
    return true;
  }
                Programkode 9.1.4 d)

Flg. programbit tester om «utbrettingsteknikken» i Programkode 9.1.4 c) virker som den skal. Vi starter med å bygge opp treet til venstre i Figur 9.1.4 d):

  char[] verdi = "DBHACFIEG".toCharArray();  // nodeverdier
  int[] posisjon = {1,2,3,4,5,6,7,12,13};    // nodeposisjoner
  BinTre<Character> tre = new BinTre<>();    // tomt tre

  for (int i = 0; i < verdi.length; i++)     // bygger treet
    tre.leggInn(posisjon[i], verdi[i]);

  tre.gjørEkstremtHøyreskjevt();             // rotasjoner

  tre.nivåorden(Oppgave.konsollutskrift());  // nivåorden
  System.out.print("  ");                    // mellomrom
  tre.inorden(Oppgave.konsollutskrift());    // inorden

  System.out.println("  " + tre.erEkstremtHøyreskjevt());

  // Utskrift: A B C D E F G H I   A B C D E F G H I  true

                Programkode 9.1.4 e)

Overskriften på dette avsnittet er «balansering ved hjelp av rotasjoner». Så langt har vi imidlertid kun gjort det motsatte av balansering, dvs. laget et tre som er så ubalansert som det er mulig å få til. Neste skritt imidlertid å «brette sammen» det skjeve treet til et komplett binærtre. Figur 9.1.4 e) under viser hvordan vi kan «brette sammen» et ekstremt høyreskjevt tre slik at det blir et komplett tre:

Fire stadier i sammenbrettingsprosessen av et fullstendig høyreskjevt tre
Figur 9.1.4 e) : Fire stadier i sammenbrettingsprosessen for et ekstremt høyreskjevt tre

«Sammenbrettingen» er litt mer komplisert enn «utbrettingen». Figur 9.1.4 e) viser noen av skrittene. Det komplette treet lengst til høyre har to noder på nederste rad. Dermed må det utføres to rotasjoner i første runde: Den første på roten og det gir en ny rotnode. Neste rotasjon utføres på dens høyre barn. La n være antallet noder. La videre m være det største tallet av typen 2k − 1 som er mindre enn eller lik n. Differansen n − m er antallet noder på nederste rad i det komplette treet. I eksempelet i Figur 9.1.4 e) er n = 9 og m = 23 − 1 = 7 og dermed n − m = 2. Det andre (fra venstre) treet i Figur 9.1.4 e) viser resultatet av de to første rotasjonene.

Videre i rotasjonene tar vi utgangspunkt i m = 7. I neste runde gjøres det m / 2 = 3 rotasjoner. Da får vi det tredje treet i Figur 9.1.4 e). Så settes m = 3 og det utføres m / 2 = 1 rotasjon. Dermed vi vi det siste treet i Figur 9.1.4 e).

Rotasjonene starter på rotnoden og utføres så forløpende nedover på høyre barn så mange ganger som trengs. Flg. hjelpemetode gjør dette:

  private void rotasjoner(int m)
  {
    rot = venstreRotasjon(rot);

    Node<T> p = rot;
    for (int i = 1; i < m; i++)
      p = p.høyre = venstreRotasjon(p.høyre);
  }
                Programkode 9.1.4 f)

Dette settes sammen til en private metode som «bretter sammen» et ekstremt høyreskjevt binærtre og en offentlig metode som starter med et vilkårlig tre og ender opp med et komplett binærtre der nodene har nøyaktig samme rekkefølge i inorden som de hadde i det opprinnelige treet:

  private void brettSammen()
  {
    int k = Integer.highestOneBit(antall);
    int m = (antall == (k << 1) - 1) ? antall : k - 1;

    rotasjoner(antall - m);  // første runde

    while (m > 1)            // resten av rundene
    {
      m /= 2;
      rotasjoner(m);
    }
  }

  public void gjørKomplett()
  {
    gjørEkstremtHøyreskjevt();
    brettSammen();
  }
                Programkode 9.1.4 g)

Teknikken bak metoden gjørKomplett() kalles DSW-algoritmen etter Day, Stout og Warren.

Flg. programbit demonstrerer bruken av metoden. Først lages det et fullstendig venstreskjevt tre med bokstavene fra A til Z i inorden og deretter blir treet «komplettisert»:

  BinTre<Character> tre = new BinTre<>();    // tomt tre
  for (char c = 'Z'; c >= 'A'; c--) tre.leggInn(1 << ('Z' - c), c);

  tre.gjørKomplett();

  tre.inorden(Oppgave.konsollutskrift());
  System.out.println("  " + tre.erKomplett());

  // Utskrift: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z   true

                Programkode 9.1.4 h)

Effektivitet: Hvor effektiv er metoden gjørKomplett()? Her er det naturlig å se på antallet rotasjoner, dvs. antallet ganger det utføres en venstrerotasjon eller en høyrerotasjon. I metoden gjørEkstremtHøyreskjevt() er det kun høyrerotasjoner. Hvis vi starter i roten og går mot høyre så langt det går, kalles nodene på den veien for treets høyre ryggrad (eng: backbone). Hvis treet har n noder og k av dem utgjør den høyre ryggraden, så vil det bli utført nk høyrerotasjoner. Det verste tilfellet er når treet er ekstremt venstreskjevt, dvs. at ingen noder har høyre barn. Da blir det n − 1 høyrerotasjoner. Det beste tilfellet er når treet er ekstremt høyreskjevt siden det gir ingen høyrerotasjoner. I gjennomsnitt vil k være av størrelsesorden log2(n) og dermed vil antallet høyrerotasjoner i gjennomsnitt være ikke så langt unna n.

I metoden brettSammen() utføres det kun venstrerotasjoner. Det kan vises at det der alltid blir utført n − ⌊log2(n+1)⌋ venstrerotasjoner når det er n noder. I sum får vi derfor at antallet rotasjoner i metoden gjørKomplett() alltid er mindre enn 2n både i gjennomsnitt og i det verste tilfellet. Metoden er derfor av orden n. Se også Oppgave 3.

Det er også mulig å «komplettisere» et binærtre ved å legge nodene, i inorden, i en tabell og så bygge et komplett tre ved hjelp av tabellen. Ulempen er at vi bruker ekstra plass til tabellen, men den vil bli noe raskere enn metoden over. Se Oppgave 4-6.

Referanser: Til fasit   Oppgaver til Avsnitt 9.1.4
1. Lag metoden private static <T> Node<T> dobbelHøyreRotasjon(Node<T> p). Den skal gjøre som i Figur 9.1.4 c). Inputparameter er p og den skal returnere noden r.
2. Lag metoden private static <T> Node<T> dobbelVenstreRotasjon(Node<T> p). Den skal gjøre det motsatte av metoden i Oppgave 1.
3. Lag et program som bygger opp et f.eks. balansert binærtre med 1 million noder og finn så ut hvor lang tid metoden gjørKomplett() bruker på din maskin.
4. Lag en metode f.eks. med navn tilTabell. Den skal traversere treet i inorden og fortløpende legge nodene inn i enn nodetabell a, dvs. en tabell av typen Node<T>[].
5. Lag en metode komplett maken til den i Programkode 9.1.3 g). Metoden skal sette sammen treet ved hjelp av nodene i tabellen a av typen Node<T>[]. Da skal det ikke lages noen nye noder. Metoden skal returnere roten til treet.
6. Lag en ny versjon av metoden gjørKomplett() der «komplettiseringen» utføres ved hjelp av metodene i Oppgave 4 og 5. Når det skal opprettes en nodetabell må det brukes en «rå» type (eng: raw type), dvs. uten typeparameter: Node[] a = new Node[antall]; Sammenlign så tidsbruken med den andre versjonen. Se Oppgave 3.


Valid XHTML 1.0 Strict