Sven O. Krumke Dynamische Bäume Draft: 23. Mai 2016 ii Sven O. Krumke [email protected] Inhaltsverzeichnis 1 Suchbäume und Selbstorganisierende Datenstrukturen 1.1 Optimale statische Suchbäume . . . . . . . . . . . . . . . 1.2 Der Algorithmus von Huffman . . . . . . . . . . . . . . . 1.3 Amortisierte Analyse . . . . . . . . . . . . . . . . . . . . 1.3.1 Stack-Operationen . . . . . . . . . . . . . . . . . 1.3.2 Dynamische Verwaltung einer Tabelle . . . . . . . 1.4 Schüttelbäume . . . . . . . . . . . . . . . . . . . . . . . . 1.4.1 Rückführen der Suchbaumoperationen auf S PLAY 1.4.2 Implementierung der S PLAY-Operation . . . . . . 1.4.3 Analyse der S PLAY-Operation . . . . . . . . . . . 1.4.4 Analyse der Suchbaumoperationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 3 5 10 11 13 14 15 16 19 22 2 Dynamische Bäume 25 Literaturverzeichnis 35 1 Suchbäume und Selbstorganisierende Datenstrukturen Die sogenannte symmetrische Ordnung oder In-Order ist eine Möglichkeit, um geordnete Schlüsselwerte sortiert abzuspeichern. Sei T ein binärer Baum. Jeder Knoten in T besitzt neben der Schlüsselwertinformation key noch Zeiger left, right und p, die auf den linken und rechten Sohn und auf den Vater im Baum zeigen (siehe Abbildung 1.1). key left right p 3 2 4 5 key left right p 9 NULL 11 7 5 7 3 2 4 9 11 Abbildung 1.1: Ein binärer Baum und seine Implementierung mit Zeigern. Wir haben hier der Einfachheit halber die Elemente mit den Schlüsselwerten identifiziert. Definition 1.1 (Suchbaumeigenschaft (bzgl. der symmetrischen Ordnung)) Der binäre Baum T erfüllt die Suchbaumeigenschaft bezüglich der symmetrischen Ordnung, wenn für jeden Knoten x ∈ T folgendes gilt: Ist y ein Knoten im linken Teilbaum von x, so gilt key[y] < key[x]. Ist z ein Knoten im rechten Teilbaum von x, so gilt key[z] > key[x]. Wir haben in unserer Definition striktes <“ und >“ gefordert. Wir setzen in diesem ” ” Kapitel voraus, dass jedes Element einen eindeutigen Schlüssel besitzt, d.h., das jeder Schlüsselwert nur einmal vorkommt. Alle Ergebnisse lassen sich problemlos auch auf den Fall von mehrfach vorkommenden Schlüsseln übertragen. Im Folgenden identifizieren wir meist der Einfachheit halber die Elementen mit Ihren Schlüsselwerten. Dies erspart es uns key[x] für den Schlüssel von x zu schreiben: wir können einfach x schreiben. 2 Suchbäume und Selbstorganisierende Datenstrukturen Wie der Name bereits andeutet, können wir in einem Suchbaum T (effizient) suchen. Um ein Element mit Schlüsselwert x in T zu suchen, starten wir in der Wurzel r von T . Wenn key[r] = x, so sind wir fertig. Falls x < key[r], so suchen wir im linken Teilbaum von r weiter, ansonsten suchen wir im rechten Teilbaum weiter. Falls ein Element mit Schlüssel x in T enthalten ist, so finden wir dieses Element korrekt. Ansonsten terminiert die Suche in einem leeren Teilbaum (Implementation: mit einem NULL-Zeiger). Hier können wir korrekt feststellen, dass kein Element im Baum Schlüssel x hat. Algorithmus 1.1 zeigt den Pseudo-Code für die Suche. Die Suche nach x benötigt O(h) Zeit, falls x Tiefe h im Baum hat. Algorithmus 1.1 Algorithmus zum Suchen eines Elements mit Schlüssel x in einem Suchbaum. S EARCH -T REE -S EARCH(T , x) 1 v ← root[T ] 2 while v 6= NULL and key[v] 6= x do 3 if x < key[v] then 4 v ← left[v] 5 else 6 v ← right[v] 7 end if 8 if v 6= NULL then 9 return x wurde im Knoten v gefunden. 10 else 11 return x ist nicht im Baum enthalten. 12 end if 13 end while Die Suchbaumeigenschaft ermöglicht es uns außerdem, die Schlüsselwerte in einem Baum T sehr einfach sortiert auszugeben. Man muß dazu lediglich Algorithmus 1.2 mit der Wurzel root[T ] aufrufen. Man sieht leicht, dass die Laufzeit von Algorithmus 1.2 für einen Baum mit n Knoten Θ(n) beträgt. Algorithmus 1.2 Rekursiver Algorithmus zur Ausgabe der Schlüsselwerte in einem Suchbaum in geordneter Reihenfolge. I NORDER -T REE -T RAVERSAL(x) 1 if x 6= NULL then 2 I NORDER -T REE -T RAVERSAL(left[x]) 3 print key[x] 4 I NORDER -T REE -T RAVERSAL(right[x]) 5 end if Definition 1.2 (Direkter Vorgänger und Nachfolger in einem Suchbaum) Sei T ein Suchbaum, der eine Menge {x1 , . . . , xn } mit x1 < x2 < · · · < xn repräsentiert. Wir setzen x0 := −∞ und xn := +∞. Ist x ∈ / {x1 , . . . , xn } mit xi < x < xi+1 , so heißen x− := xi der direkte Vorgänger von x und x+ := xi+1 der direkte Nachfolger von x in T . Für die Standard-Operationen auf (balancierten) Suchbäumen verweisen wir auf [1, Kapitel 12]. Wir beschäftigen uns in diesem Kapitel mit binären Suchbäumen, die sich selbst ” organisieren“. Was dies genau heißt, wird nachher noch genauer klar werden. Wir motivieren die Selbstorganisation durch eine spezielle Anwendung für einen optimalen (statischen) Suchbaum. Bevor wir diese Anwendung im nächsten Abschnitt genauer vorstellen, notieren wir noch die dynamischen Mengenoperationen, von denen wir fordern, dass sie ein Suchbaum effizient unterstützt: 1.1 Optimale statische Suchbäume 3 S EARCH(S, k) Sucht und liefert das Element mit Schlüsselwert k in der sortierten dynamischen Menge S. Falls x nicht im Baum vorhanden ist, soll NULL ausgegeben werden. I NSERT(S, x) Fügt das Element x in die dynamische sortierte Menge S ein. D ELETE(S, x) Löscht das Element x in der Menge S. Neben diesen Standard-Operationen“ fordern wir von unserer Datenstruktur noch, dass ” auch folgende Operationen effizient unterstützt werden: J OIN(S1 , S2 ) Liefert die sortierte Menge, die aus dem Elementen von S1 ∪ S2 besteht. Diese Operation zerstört S1 und S2 und setzt voraus, dass für alle Schlüsselwerte k1 ∈ S1 und k2 ∈ S2 gilt: k1 6 k2 . S PLIT(S, x) Teilt die Menge S, die x enthalten muß, in zwei Mengen: S1 enthält alle Elemente aus S mit Schlüsselwerten kleiner oder gleich key[x] und S2 enthält alle Elemente aus S mit Schlüsselwerten größer als key[x]. Es gibt zahlreiche Klassen von balancierten Bäumen (etwa AVL-Bäume, Rot-SchwarzBäume, 2-3-Bäume, B-Bäume), die alle oben genannten Operationen in O(log n) Zeit unterstützen, wobei n die aktuelle Anzahl der Elemente in der Menge S sind, siehe [2, 1]. Wie schon erwähnt, ist unser Schwerpunkt in diesem Kapitel anders. 1.1 Optimale statische Suchbäume Unsere Motation für optimale statische Suchbäume kommt aus dem Bereich der Codierungstheorie. Angenommen, wir haben eine Datei D mit 100.000 Zeichen, wobei jedes Zeichen aus dem acht-elementigen Zeichenvorrat Σ = {a, b, c, d, e, f, g, h} stammt. Wenn wir die Zeichen binär mit fester Länge codieren, so benötigen wir drei Bits pro Zeichen: a = 000, b = 001, . . . , f = 101, g = 110, h = 111. Dies führt zu einem Platzbedarf von 300.000 Bits, um D zu speichern. Geht dies besser? In unserem ersten Ansatz haben wir einen sogenannten Code mit fester Länge benutzt. Ein Code mit variabler Länge kann eine deutliche Verbesserung der Speicherplatzausnutzung ergeben. Beim Zählen, wie oft jedes Zeichen aus Σ in der Datei D auftaucht, ergibt sich die Verteilung in Tabelle 1.1. Häufigkeit (in 1000) Codewort fester Länge Codewort variabler Länge a 40 b 13 c 12 d 5 e 18 f 6 g 3 h 3 000 001 010 011 100 101 110 111 1 010 011 00001 001 0001 000000 000001 Tabelle 1.1: Häufigkeiten der einzelnen Zeichen in der Beispieldatei und Codierung mit fester bzw. variabler Länge. Wenn wir die Zeichen gemäß des Codes in der dritten Zeile von Tabelle 1.1 codieren, so benötigen wir folgende Anzahl von Bits: 1000 · (40 · 1 + 13 · 3 + 12 · 3 + 5 · 5 + 18 · 3 + 6 · 4 + 3 · 6 + 3 · 6) = 278.000. Dies ist eine beträchtliche Ersparnis gegenüber dem Code mit fester Länge. Wie berechnet man einen Code mit variabler Länge und was hat dies mit Suchbäumen zu tun? 4 Suchbäume und Selbstorganisierende Datenstrukturen Der Code aus Tabelle 1.1 ist ein sogenannter Präfix-Code, d.h., kein Codewort ist ein Präfix eines anderen Codeworts. Wir können den Code als binären Baum darstellen, der gleichzeitig als effizienter Decodier-Mechanismus gilt. Abbildung 1.2 zeigt den Code aus Tabelle 1.1 als binären Baum. Dabei ist das Codewort für ein Zeichen aus Σ im Pfad von der Wurzel des Baums bis zum Blatt, welches das Zeichen enthält, gespeichert“: eine 0 ” steht für linker Sohn“eine 1 für rechter Sohn“. Man sieht leicht, dass jedem Präfix-Code ” ” ein Code-Baum entspricht und umgekehrt jeder Baum, dessen Blätter die Elemente aus Σ bijektiv zugeordnet sind, einen Präfix-Code impliziert. 100 0 1 60 0 a: 40 1 35 0 1 17 0 b: 13 1 c: 12 f: 6 1 0 6 g: 3 e: 18 0 1 11 0 25 d: 5 1 h: 3 Abbildung 1.2: Code-Baum für den Beispielcode mit variabler Länge. Jedes Blatt ist mit einem Zeichen aus dem Alphabet Σ und seiner relativen Häufigkeit (in Prozent) markiert. Jeder innere Knoten enthält die Summe der relativen Häufigkeiten aller Blätter in seinem Teilbaum. Mit Hilfe eines Code-Baums kann man übrigens effizient decodieren: Man startet in der Wurzel und folgt gemäß den gelesenen Bits einem Weg bis zu einem Blatt. Sobald man in einem Blatt angelangt ist, hat man das entsprechende Zeichen aus Σ gefunden. Man startet dann wieder in der Wurzel für das nächste Zeichen. Ist der Code-Baum bekannt, so kann man einen Datenstrom in linearer Zeit decodieren. Den Code-Baum zu einem Präfix-Code γ kann man auch als Suchbaum für die Elemente in Σ betrachten. Ist für z ∈ Σ die Höhe des entsprechenden Blattes im Baum dT (z) und p(z) ∈ [0, 1] die (relative) Häufigkeit von z in der zu codierenden Datei D, so benötigt die Codierung von D mittels γ genau |D| · c(T ) Bits, wobei |D| die Anzahl der Zeichen in D ist und X c(T ) := dT (z)p(z) (1.1) z∈Σ die gewichtete durchschnittliche Blatthöhe von T ist. Wir können die relative Häufigkeit p(z) von z auch als Wahrscheinlichkeit ansehen, dass z angefragt wird. Dann entspricht c(T ) dem Erwartungswert der Blatthöhe bei einer Suchanfrage. Einen optimalen Präfix-Code erhalten wir, indem wir einen Suchbaum T ∗ konstruieren, der eine kleinstmögliche erwartete Blatthöhe c(T ) besitzt. Wenn die Verteilung p bekannt ist (im Fall unseres Codierungsbeispiels können wir p durch einmaliges Durchlaufen der zu codierenden Datei D errechnen), so kann ein optimaler 1.2 Der Algorithmus von Huffman Baum mit Hilfe des Algorithmus von Huffman (siehe nächster Abschnitt) bestimmt werden. Allerdings hätten wir auch gerne für den Fall, dass die Verteilung p nicht bekannt ist, etwa wenn Daten online“ über einen Datenkanal gesendet werden sollen, einen optimalen oder ” zumindest guten “ Baum/Code, mit dem wir online“ codieren können. Wir werden in ” ” Abschnitt 1.4 eine Datenstruktur, die Schüttelbäume, kennenlernen, die dieses Problem lösen. 1.2 Der Algorithmus von Huffman Wir führen auf dem Weg zu den selbstorganisierenden Datenstrukturen unseren kurzen Ausflug in die Codierungstheorie fort. Wir zeigen, wie man einen optimalen statischen Suchbaum effizient konstruieren kann. Zum einen rundet dies unseren Ausflug ab, zum anderen werden wir sehen, wie man auch hier mit Hilfe von geeigneten Datenstrukturen eine effiziente Laufzeit erhalten kann. Im folgenden sei p : Σ → [0, 1] eine Wahrscheinlichkeitsverteilung auf Σ. Unsere Aufgabe ist es, einen Baum T ∗ zu konstruieren, der optimale Kosten c(T ∗ ) (siehe Gleichung (1.1)) besitzt. Der Algorithmus von Huffman arbeitet wie folgt: er startet mit w[z] := p(z) für alle z ∈ Σ. Dann fasst er iterativ die beiden Zeichen“ x und y (warum hier Anführungszeichen ” stehen, wird gleich klar) mit den kleinsten Werten w[x], w[y] zusammen, indem er sie zu Söhnen einer gemeinsamen Wurzel z macht, die Gewicht w[z] := w[x] + w[y] erhält. Die Zeichen x und y werden entfernt und durch z ersetzt. Das Verfahren ist in Algorithmus 1.3 genauer im Pseudo-Code beschrieben. Ein Beispiel, wie der Huffman-Algorithmus einen Code erzeugt, ist in den Abbildungen 1.3 und 1.4 zu sehen. Algorithmus 1.3 Der Algorithmus von Huffman. H UFFMAN -C ODE 1 for all z ∈ Σ do 2 w[z] ← p(z) 3 Alloziiere einen neuen Baumknoten z mit left[z] = right[z] = p[z] = NULL 4 end for 5 Q ← B UILD -H EAP(w) Konstruiere einen Minimum-Heap für die Elemente aus Σ, wobei das Element z ∈ Σ Schlüsselwert w[z] besitzt. 6 while |Q| > 1 do 7 x ← E XTRACT-M IN(Q) 8 y ← E XTRACT-M IN(Q) 9 Alloziiere einen neuen Baumknoten z. 10 left[z] ← x, p[x] ← z 11 p[y] ← z, p[x] ← z 12 right[z] ← y 13 p[z] ← NULL 14 w[z] ← w[x] + w[y] 15 I NSERT(Q, z) { Füge z in den Heap Q ein. } 16 end while 17 z ← E XTRACT-M IN(Q) { Q besteht jetzt nur noch aus einem Element. } 18 return z Bevor wir die Korrektheit des Huffman-Algorithmus beweisen, analysieren wir seine Laufzeit. Sei n := |Σ| die Größe des Alphabets, das wir codieren wollen. Das Alloziieren der n Knoten für die n Zeichen aus Σ benötigt dann O(n) Zeit. Ebenso ist für das Erstellen des Heaps Q nur O(n) Zeit nötig. Die while-Schleife in den Zeilen 6 bis 16 wird insgesamt 5 6 Suchbäume und Selbstorganisierende Datenstrukturen a: 40 b: 13 d: 5 c: 12 e: 18 g: 3 f: 6 h: 3 (a) Start: Die Knoten sind alle einzelne Zeichen aus Σ 6 a: 40 b: 13 d: 5 c: 12 e: 18 g: 3 f: 6 h: 3 (b) Situation nach Zusammenfassen der Knoten g und h mit kleinstem Gewicht 12 a: 40 b: 13 c: 12 e: 18 6 f: 6 g: 3 d: 5 h: 3 (c) 17 a: 40 b: 13 11 e: 18 c: 12 6 g: 3 f: 6 d: 5 h: 3 (d) Abbildung 1.3: Erzeugung eines Beispielcodes durch den Huffman-Algorithmus. 1.2 Der Algorithmus von Huffman 7 25 a: 40 e: 18 b: 13 17 11 c: 12 6 f: 6 d: 5 g: 3 h: 3 (a) 25 a: 40 b: 13 35 17 c: 12 11 6 g: 3 e: 18 f: 6 d: 5 h: 3 (b) 60 35 a: 40 17 11 6 g: 3 25 e: 18 b: 13 c: 12 f: 6 d: 5 h: 3 (c) 100 60 a: 40 35 17 11 6 g: 3 25 e: 18 b: 13 c: 12 f: 6 d: 5 h: 3 (d) Fertiger Code-Baum Abbildung 1.4: Fortsetzung: Erzeugung eines Beispielcodes durch den HuffmanAlgorithmus. 8 Suchbäume und Selbstorganisierende Datenstrukturen n − 1 mal durchlaufen, da wir mit n Knoten starten und in jedem Schritt zwei Knoten zu einem verschmelzen (die Anzahl der Knoten also um eins reduzieren). Bis auf die E XTRACTM IN-Operationen läuft H UFMANN -C ODE also in linearer Zeit. Jede der 2n − 2 E XTRACTM IN-Aufrufe benötigt (bei Implementierung mit einem binären Heap) O(log n) Zeit, so dass wir insgesamt eine Laufzeit von O(n log n) erhalten. Satz 1.3 Der Huffman-Algorithmus findet einen Baum T ∗ mit minimalen Kosten c(T ), bzw. einen optimalen Präfix-Code mit variabler Länge. Der Algorithmus kann so implementiert werden, dass er in O(n log n) Zeit läuft. Beweis: Die Laufzeit haben wir bereits bewiesen. Wir zeigen die Behauptung des Satzes in zwei Schritten: Behauptung 1.4 Seien x ∈ Σ und y ∈ Σ die zwei Zeichen mit geringster Häufigkeit p(x), p(y). Es existiert ein optimaler Baum, in dem x und y Blätter größter Höhe sind, die außerdem einen gemeinsamen Vater besitzen. Behauptung 1.5 Seien x ∈ Σ und y ∈ Σ die zwei Zeichen mit geringster Häufigkeit p(x), p(y). Sei Σ ′ := Σ \ {x, y} ∪ {z}, wobei z ∈ / Σ und p(z) := p(x) + p(y). Ist T ′ ein optimaler ′ Code-Baum für Σ , so ist der Baum T , der aus T ′ entsteht, indem man das Blatt für z durch den Teilbaum x y ersetzt, ein optimaler Code-Baum für Σ. Aus den Behauptungen 1.4 und 1.5 folgt dann sofort die Aussage des Satzes über die Korrektheit des Huffman-Algorithmus. Beweis: (Behauptung 1.4) Sei T ein optimaler Code-Baum für Σ. Zunächst bemerken wir, dass wir o.B.d.A. annehmen können, dass ein Blatt a maximaler Höhe in T immer einen Bruder hat. Falls a keinen Bruder besitzt, so ist a alleiniger Sohn seines Vaters p[a] (von p[a] kann kein weiterer Teilbaum abzweigen, da in diesem sonst ein Blatt mit größerer Höhe als a vorliegen würde). Daher können wir p[a] durch a ersetzen und den alten Knoten von a entfernen (siehe Abbildung 1.5). Die Kosten von T können höchstens geringer werden, da die Höhe von a um eins sinkt, alle anderen Blätter ihre Höhe aber behalten. Fortsetzung dieses Verfahrens liefert einen Baum, in dem das Blatt maximaler Höhe einen Bruder besitzt. Ersetzen von p[a] durch a p[a] a a Abbildung 1.5: Ein Blatt maximaler Höhe a besitzt o.B.d.A. in einem optimalen Codebaum einen Bruder. Ansonsten kann man den Baum ohne Kostenerhöhung modifizieren. Seien nun a und b Blätter mit maximaler Höhe in T , die einen gemeinsamen Vater besitzen. Wir nehmen o.B.d.A. an, dass p(a) 6 p(b) ist. Nach Wahl von x und y gilt dann p(x) 6 p(a) und p(y) 6 p(b). Wir vertauschen a mit x und dann in einem zweiten Schritt b mit y, so dass aus T zunächst T ′ und dann T ′′ entsteht (siehe Abbildung 1.6). 1.2 Der Algorithmus von Huffman 9 Vertauschen von x und a y y x a a b x b x y Vertauschen von y und b b a Abbildung 1.6: Illustration des Beweises von Behauptung 1.4. Durch Vertauschen der Positionen von x und y mit derer von zwei Blättern maximaler Höhe mit gemeinsamem Vater ensteht ein neuer Baum, ohne die Kosten zu erhöhen. 10 Suchbäume und Selbstorganisierende Datenstrukturen Die Kosten von T ′ unterscheiden sich von denen von T durch die Terme für a und x (alle anderen Blätter behalten ihre Höhen). Wir haben dann: c(T ′ ) = c(T ) − (dT (a)p(a) + dT (x)p(x)) + dT ′ (a)p(a) + dT ′ (x)p(x) = c(T ) − (dT (a)p(a) + dT (x)p(x)) + dT (x)p(a) + dT (a)p(x) = c(T ) + (dT (x) − dT (a)) · (p(a) − p(x)) | {z } | {z } 60 >0 6 c(T ). Dabei haben wir benutzt, dass dT (x) 6 dT (a), da a ein Blatt größter Höhe ist, und p(a) 6 p(x), da x kleinste Häufigkeit besitzt. Vollkommen analog zeigt man nun c(T ′′ ) 6 c(T ′ ). Daher ist T ′′ ebenfalls ein optimaler Baum, bei dem x und y an der gewünschten Position ✷ liegen. Dies beendet den Beweis von Behauptung 1.4. Beweis: (Behauptung 1.5) Wir müssen zeigen, dass der Baum T aus der Behauptung ein optimaler Code-Baum für Σ ist. Zunächst setzen wir die Kosten von T und T ′ in Beziehung. Der Baum T entspricht T ′ mit der Modifikation, dass z durch den dreiknotigen Teilbaum mit Blättern x und y ersetzt wird. Wir haben also dT (x) = dT (y) = dT ′ (z) + 1. Die Kosten von T errechnen sich daher wie folgt: X dT (s)p(s) c(T ) = s∈Σ = X dT (s)p(s) + dT (y)p(y) + dT (x)p(x) s∈Σ\{x,y} = X dT ′ (s)p(s) + dT (y)p(y) + dT (x)p(x) s∈Σ\{x,y} = X s∈Σ\{x,y} = X dT ′ (s)p(s) + (dT ′ (z) + 1) (p(y) + p(x)) | {z } =p(z) dT ′ (s)p(s) + (p(y) + p(x)) s∈Σ\{x,y}∪{z} = c(T ′ ) + (p(y) + p(x)) Angenommen, T̃ wäre ein Code-Baum für Σ mit c(T̃ ) < c(T ). Nach Behauptung 1.4 können wir o.B.d.A. davon ausgehen, dass x und y Blätter größter Höhe mit gemeinsamem Vater sind. Wir ersetzen in T̃ die Blätter x und y durch den Knoten z mit p(z) = p(x) + p(y). Sei T̂ der entsprechende Code-Baum für Σ ′ . Analog zum Kostenvergleich von T und T ′ errechnet man c(T̂ ) = c(T̃ ) − p(y) − p(x) < c(T ) − p(y) − p(x) = c(T ′ ). Dies widerspricht der Annahme, dass T ′ ein optimaler Code-Baum für Σ ′ ist. ✷ Wie bereits oben erwähnt, implizieren die Behauptungen 1.4 und 1.5 unmittelbar die Korrektheit des Hufmann-Algorithmus. ✷ 1.3 Amortisierte Analyse Bei der amortisierten Analyse berechnet man die durchschnittlichen Worst-Case-Kosten einer Operation über eine ganze Folge von Operationen. Ziel ist es, dass im Durchschnitt“die ” 1.3 Amortisierte Analyse 11 Ersetzen von z durch x y z x y Abbildung 1.7: Konstruktion eines neuen Code-Baums für Σ aus dem optimalen CodeBaum für Σ ′ = Σ \ {x, y} ∪ {z}. Kosten einer Operation niedrig liegen. Die Formulierung im Durchschnitt“ bedeutet hier ” das Mittel über eine Folge von Operationen im Worst-Case. Es findet hier keine probabilistische Analyse statt. In diesem Kapitel wenden wir die amortisierte Analyse auf zwei einfache Beispiele an. Unsere Analysetechnik benutzt dabei eine Potentialfunktion, die als Bankkonto“ benutzt ” wird, um teure gegen billige Operationen zu verrechnen. 1.3.1 Stack-Operationen Unser erstes (sehr einfaches) Beispiel ist ein Stack. Ein Stack ist ein Last-in-First-OutSpeicher S, auf dem die folgenden Operationen definiert sind: • P USH(S, x) legt das Objekt x oben auf den Stack. • P OP(S) liefert das oberste Objekt auf dem Stack und entfernt es vom Stack (wenn der Stack leer ist, dann bricht die Operation mit Fehler ab). Beide Operationen kosten O(1) Zeit. Wir erlauben jetzt noch eine weitere Operation M ULTIPOP(S, k), welche die obersten k Objekte des Stacks entfernt. Diese Operation benötigt O(k) Zeit. Wie groß ist die Laufzeit für eine Folge von n Operationen aus P USH-, P OP, M ULTIPOP auf einem anfangs leeren Stack? Offenbar benötigt jede M ULTIPOP-Operation höchstens O(n)-Zeit, da der Stack zu jedem Zeitpunkt höchstens n Elemente enthält. Da die P USH- und P OP-Operationen jeweils nur O(1)-Zeit benötigen, können wir den Gesamtaufwand mit n · O(n) = O(n2 ) abschätzen. Obwohl diese Abschätzung korrekt ist, liefert sie kein scharfes Resultat. Tatsächlich ist der Gesamtaufwand für n Operationen nur O(n), also deutlich weniger. Der Schlüssel ist hier die M ULTIPOP-Operation. Obwohl ein einzelnes M ULTIPOP sehr teuer sein kann, verringert es dabei doch die Stackgröße. Insgesamt kann jedes der maximal n Elemente nur einmal durch eine M ULTIPOP- oder P OP-Operation vom Stack entfernt werden, so dass der Gesamtaufwand für alle Pops in der Folge nur so groß wie die Anzahl der P USHOperationen sein kann. Somit erhält man die Gesamtlaufzeit von O(n). Wir haben eben gezeigt, dass eine Folge von n Operationen O(n) Zeit benötigt. Im amortisierten Sinne, d.h. im Durchschnitt, kostet damit jede der n Operationen O(n)/n = O(1) Zeit. 12 Suchbäume und Selbstorganisierende Datenstrukturen Im folgenden benutzen wir eine Potentialfunktion, um das gleiche Ergebnis noch einmal herzuleiten. Für das einfache Beispiel mag die Analyse nach einem zu großen Geschoß aussehen, allerdings werden hier bereits die technischen Hilfsmittel sichtbar. Eine Potentialfunktion Φ ordnet einer Datenstruktur D einen reellen Wert Φ(D), das Potential, zu, welches mißt, wie gut“ oder wie schlecht“ die aktuelle Konfiguration ist. Man ” ” kann sich Φ(D) gewissermaßen als Bankkonto vorstellen. Wir verteuern künstlich eine billige Operation, indem wir zusätzlich zu den realen Kosten noch etwas auf das Konto einzahlen. Bei real teuren Operationen entnehmen wir Geld von Konto, um die Operation amortisiert“ ebenfalls günstig zu machen. ” Wir starten mit einer Ausgangsdatenstruktur D0 , auf die n Operationen wirken. Wir bezeichnen mit ci die (realen) Kosten der iten Operation, welche auf der Datenstruktur Di−1 arbeitet und als Ergebnis Di liefert. Die amortisierten Kosten ai bei der iten Operation sind + Φ(Di ) − Φ(Di−1 ) . ai = ci |{z} | {z } reale Kosten für die ite Operation Potentialänderung Wenn die Differenz Φ(Di ) − Φ(Di−1 ) negativ ist, dann unterschätzt ai die tatsächlichen Kosten ci . Die Differenz wird durch das Entnehmen des Potentialverlustes aus dem Konto abgedeckt. Es gilt nun: n X ai = i=1 n X (ci + Φ(Di ) − Φ(Di−1 )) = i=1 Also haben wir: n X ci + Φ(Dn ) − Φ(D0 ). (1.2) i=1 n X i=1 ci = n X ai + Φ(D0 ) − Φ(Dn ). (1.3) i=1 Wenn wir ein Potential definieren können, so dass Φ(Dn ) > Φ(D0 ) gilt, dann überschätzen die amortisierten Kosten die realen Kosten, und eine obere Schranke für die amortisierten Kosten ist dann auch eine Schranke für die realen Kosten. Dieses Ergebnis ist derart wichtig, dass wir es (in Variationen) in einem Satz festhalten. Satz 1.6 Sei D0 eine Datenstruktur, auf die n Operationen wirken, wobei die ite Operation die Datenstruktur Di−1 ∈ D in die Datenstruktur Di ∈ D überführt. Sei Φ : D → R ein Potential. (i) Gilt Φ(Dn ) > Φ(D0 ), so sind die gesamten realen Kosten für die n Operationen nach oben durch die amortisierten Kosten für die Folge beschränkt. (ii) Gilt Φ(Di ) > 0 für i = 0, . . . , n, so sind die realen Kosten durch die amortisierten Kosten plus das Anfangspotential Φ(D0 ) nach oben beschränkt. Beweis: Siehe oben. ✷ Der einfachste Weg, um Φ(Dn ) > Φ(D0 ) zu erhalten, ist es, ein Potential mit Φ(Di ) > 0 und Φ(D0 ) = 0 zu finden. Wir führen dies an unserem Stack-Beispiel vor. Wir definieren Φ(S) := |S|. Offenbar erfüllt dieses Potential unsere Anforderungen. Wie groß sind nun die amortisierten Kosten? Wir können annehmen, dass die P USH- und P OP-Operation jeweils reale Kosten 1 und die M ULTIPOP-Operation reale Kosten k besitzt. Wir betrachten die ite Operation. Ist diese ein P USH, so gilt ai = 1 + |Si | − |Si−1 | = 1 + (|Si−1 | + 1 − |Si−1 |) = 2. 1.3 Amortisierte Analyse Im Falle eines M ULTIPOP (das als Spezialfall das P OP mit k = 1 enthält) gilt: ai = k + |Si | − |Si−1 | = k + (|Si−1 | − k − |Si−1 |) = 0. P Eine Folge von n Operationen besitzt somit die amortisierten Kosten n i=1 ai 6 2n ∈ O(n). Mit Hilfe von Satz 1.6 erhalten wir dann auch die Schranke O(n) für die realen Kosten der Folge. 1.3.2 Dynamische Verwaltung einer Tabelle Zur Speicherung einer dynamisch wachsenden Tabelle soll Speicherplatz alloziiert werden. Speicherplatz steht in Form von Blöcken zur Verfügung. Die Tabelle muß in aufeinanderfolgenden Speicheradressen untergebracht werden. So lange die Tabelle noch nicht voll belegt ist, können wir weitere Elemente einfügen. Sobald die Tabelle voll ist, müssen wir eine neue Tabelle erzeugen, welche mehr Einträge als die alte besitzt. Da die Tabelle immer in kontinuierlich im Speicher liegen soll, müssen wir dann neuen Speicherplatz anfordern und die gesamte alte Tabelle in die neue kopieren. Wir stellen nun einen Algorithmus zur dynamischen Verwaltung der Tabelle vor und analysieren seine Laufzeit. Dabei bezeichnen wir mit T das Tabellenobjekt und mit table[T ] einen Zeiger auf den Speicherblock, ab dem die Tabelle im Speicher steht. Mit num[T ] bezeichnen wir die Anzahl der gespeicherten Einträge und mit size[T ] die Größe der Tabelle. In unserem Beispiel ist die Tabelle anfänglich leer, num[T ] = size[T ] = 0. Letztendlich sei INSERT die elementare Funktion, welche ein neues Element in die Tabelle einfügt. Algorithmus 1.4 Algorithmus zur dynamischen Tabellenvewaltung TABLE -I NSERT(T , x) 1 if size[T ] = 0 then 2 Alloziiere eine neue Tabelle table[T ] der Größe 1. 3 size[T ] ← 1 4 end if 5 if num[T ] = size[T ] then 6 Alloziiere eine neue Tabelle newtable der Größe 2 · size[T ]. 7 Füge alle Einträge aus table[T ] mittels INSERT in newtable ein. 8 Gebe den Speicherplatz table[T ] frei. 9 table[T ] = newtable 10 size[T ] = 2 · size[T ] 11 end if 12 Füge x in table[T ] mittels INSERT ein. 13 num[T ] ← num[T ] + 1 In unserer Analyse nehmen wir an, dass die Laufzeit von TABLE -I NSERT linear in der Anzahl der elementaren INSERT-Operationen ist. Wir analysieren die Laufzeit daher in der Anzahl der INSERT-Operationen. Wie groß ist der Aufwand für n TABLE -I NSERT-Operationen, wenn man mit einer leeren Tabelle startet? Analog zum Stack-Beispiel in Abschnitt 1.3.1 kann man schnell eine grobe obere Schranke angeben. Falls noch Platz in der Tabelle ist, so kann die ite EinfügeOperation in O(1) Zeit durchgeführt werden. Bei voller Tabelle müssen hingegen i INSERTOperationen beim Kopieren der Tabelle ausgeführt werden, so dass insgesamt ein Aufwand von Θ(i) anfällt. Bei insgesamt n Operationen kommen wir bei einer Worst-Case-Zeit pro Operation von Θ(n) auf eine Gesamtzeit von O(n2 ). 13 14 Suchbäume und Selbstorganisierende Datenstrukturen Wir zeigen nun, wie man wieder mit Hilfe der amortisierten Analyse eine scharfe obere Schranke herleiten kann. Wir werden mit Hilfe einer geeigneten Potentialfunktion zeigen, dass die amortisierten Kosten jeder einzelnen Operation nur O(1) sind. Insgesamt erhalten wir dann einen Aufwand von O(n). Die Potentialfunktion benutzt die Idee des Bankkontos. Unmittelbar nach einer Expansion der Tabelle ist das Potential gleich 0. Bis zur nächsten Expansion steigt das Potential an, so dass wir damit für die teure nächste Expansion mit Hilfe der Potentialdifferenz bezahlen können. Wir benutzen folgende Potentialfunktion Φ(T ) := 2 · num[T ] − size[T ]. (1.4) Nach einer Expansion ist size[T ] = 2 · num[T ], also Φ(T ) = 0. Insbesondere ist das Potential am Anfang ebenfalls gleich 0. Im Verlaufe der Einfüge-Operationen ist die Tabelle immer mindestens halb gefüllt, so dass wir auch Φ(T ) > 0 haben. Aus Satz 1.6 ersehen wir, dass die Summe der amortisierten Kosten eine obere Schranke für die realen Kosten ist. Wir betrachten nun die ite TABLE -I NSERT-Operation. Falls keine Expansion notwendig ist, so gilt size[Ti ] = size[Ti−1 ] und daher: ai = 1 + Φ(Ti ) − Φ(Ti−1 ) = 1 + (2 · num[Ti ] − size[Ti ]) − (2 · num[Ti−1 ] − size[Ti−1 ]) = 1 + (2 · num[Ti ] − size[Ti−1 ]) − (2 · (num[Ti ] − 1) − size[Ti−1 ]) = 1 + 2 = 3. Falls expandiert wird, haben wir size[Ti ] = 2 · size[Ti−1 ], und es folgt: ai = num[Ti ] + (2 · num[Ti ] − size[Ti ]) − (2 · num[Ti−1 ] − size[Ti−1 ]) = num[Ti ] + (2 · num[Ti ] − 2size[Ti−1 ]) − (2 · (num[Ti ] − 1) − size[Ti−1 ]) = num[Ti ] + 2 − size[Ti−1 ] = num[Ti−1 ] + 1 − size[Ti−1 ] + 2 = num[Ti−1 ] + 1 − num[Ti−1 ] + 2 = 3. Hierbei haben wir benutzt, dass num[Ti−1 ] = size[Ti−1 ] gilt, da sonst nicht expandiert werden muß. Insgesamt erhalten wir also den behaupteten Aufwand von O(n) für eine Folge von n Einfüge-Operationen auf einer anfänglich leeren Tabelle. Das ist eine gute Nachricht: im Durchschnitt kostet jede der n Operationen nur konstante Zeit! 1.4 Schüttelbäume Ein Schüttelbaum (engl. Splay-Tree) ist ein binärer Suchbaum, bei dem alle Suchbaumoperationen auf die folgende Operation S PLAY ( schüttele“) zurückgeführt werden: ” S PLAY(T , x) gibt einen Baum aus, der die selbe Menge von Elementen wie T darstellt. Wenn x im Baum enthalten ist, so wird x zur Wurzel des Resultatbaums gemacht. Wenn x nicht im Baum enthalten ist, so wird entweder der unmittelbare Vorgänger x− oder der umittelbare Nachfolger x+ von x im Baum T zur Wurzel. 1.4 Schüttelbäume 1.4.1 15 Rückführen der Suchbaumoperationen auf S PLAY Bevor wir die genaue Implementierung der S PLAY-Operation beschreiben (schon einmal zur Vorwarnung: es ist wichtig, dass die S PLAY-Operation genau wie hier beschrieben ausgeführt wird, für andere Varianten gelten die gezeigten Ergebnisse nicht), zeigen wir, wie die anderen Operationen auf S PLAY zurückgeführt werden können. S EARCH Für S EARCH(T , x) führen wir S PLAY(T , x) aus und inspizieren die Wurzel. Nach Definition der S PLAY-Operation befindet sich x nach S PLAY(T , x) genau in der Wurzel, wenn x im Baum enthalten ist. J OIN Um J OIN2 (T1 , T2 ) zu implementieren, führen wir zunächst S PLAY(T1 , +∞) aus. Als Resultat steht dann das größte Element in der Wurzel des geänderten Baums T1′ . Diese Wurzel hat keinen rechten Sohn (da es kein größeres Element als +∞ gibt). Wir können nun T2 zum rechten Teilbaum der Wurzel von T1′ machen. Abbildung 1.8 veranschaulicht die Operationenfolge. z S PLAY(T1 , +∞) T1 z T2 T2 A A T2 Abbildung 1.8: Rückführen von J OIN(T1 , T2 ) auf S PLAY. S PLIT Für S PLIT(T , x) führen wir S PLAY(T , x) aus und brechen dann eine der Verbindungen von der Wurzel zu den Teilbäumen auf, je nachdem, ob die Wurzel nach dem S PLAY ein Element kleiner oder größer als x enthält, siehe Abbildung 1.9. z ∈ {x, x− , x+ } z S PLAY(T , x) x− Aufbrechen z∈ {x, x− } A B A B Abbildung 1.9: Rückführen von S PLIT(T , x) auf S PLAY. Hier ist der Fall z ∈ {x, x− } gezeigt. Der Fall z = x+ verläuft symmetrisch dazu. I NSERT Um I NSERT(T , x) auszuführen, führen wir zunächst S PLIT(T , x) durch. Als Resultat erhalten wir zwei Bäume T − und T + , wobei T − alle Elemente kleiner als x und T + alle Elemente größer als x enthält. Wir konstruieren einen neuen Baum mit Wurzel x und T − als linkem und T + als rechtem Teilbaum. Das Vorgehen ist in Abbildung 1.10 illustriert. D ELETE Zum Ausführen von D ELETE(T , x) führen wir S PLAY(T , x) aus, zerstören die Wurzel, wodurch wir zwei Teilbäume T1 und T2 erhalten. Diese beiden Bäume werden dann wieder durch J OIN2 (T1 , T2 ) zu einem neuen Baum verbunden, siehe Abbildung 1.11. 16 Suchbäume und Selbstorganisierende Datenstrukturen x S PLIT(T , x) T− T+ T− T+ Abbildung 1.10: Implementierung von I NSERT(T , x) in Schüttelbäumen. x S PLAY(T , x) J OIN(T1 , T2 ) T1 T1 T2 T2 Abbildung 1.11: Implementierung von D ELETE(T , x) in SchüttelbäumeSchüttelbäumen. 1.4.2 Implementierung der S PLAY-Operation In diesem Abschnitt beschreiben wir, wie die zentrale Operation S PLAY(T , x) in einem Schüttelbaum ausgeführt wird. Wie bereits erwähnt, gelten die in den folgenden Abschnitten gezeigten Ergebnisse nur, wenn die S PLAY-Operation wie hier beschrieben ausgeführt wird. Insbesondere ist es dabei wichtig, dass die Operationen in der angegebenen Reihenfolge ausgeführt werden. Bei der Operation S PLAY(T , x) führen wir eine Anzahl von Rotationen im Baum T aus, durch die x zur Wurzel gemacht wird. Wir starten dabei in x und unterscheiden verschiedene Fälle (im wesentlichen drei Fälle, von denen jeder zwei symmetrische Unterfälle hat), je nachdem wie die Position von x zu seinem Vater p[x] und seinem Großvater p[p[x]] ist. Sei u der Knoten, der x enthält. Diesen Knoten können wir durch die in Algorithmus 1.1 vorgestellte Suche in einem Suchbaum lokalisieren. Falls u einen Vater, aber keinen Großvater besitzt, so führen wir eine Rotation am Vater v = p[u] durch, wodurch u zur Wurzel wird. Mit diesem Schritt terminiert das Verfahren. Der eben beschriebene Fall ist in Abbildung 1.12(a) gezeichnet. Die Zeichnung in Abbildung 1.12(a) entspricht dem Fall, dass u linker Sohn seines Vaters ist. Falls u rechter Sohn ist, so funktioniert die Rotation entsprechend symmetrisch. Es sollte klar sein, dass eine Rotation in konstanter Zeit ausgeführt werden kann, da wir nur Zeiger auf die Teilbäume umhängen müssen. Details zu Rotationen in Suchbäumen, etwa zum Balancieren von Bäumen, findet man in [1]. Falls u einen Großvater besitzt, so unterscheiden wir zwei Fälle, je nach der Stellung von u zu seinem Vater und vom Vater p[u] zum Großvater w = p[v] = p[p[u]]. Je nachdem, welcher Fall vorliegt, wird u durch eine geeignete Rotation weiter nach oben im Baum befördert. Algorithmus 1.5 zeigt die Details der S PLAY-Operation. In Abbildung 1.12 sind die drei Fälle und die entsprechenden Rotationen gezeigt. Abbildung 1.13 zeigt ein Beispiel. Noch einmal soll darauf hingewiesen werden, dass die Reihenfolge der Rotationen von entscheidender Bedeutung ist. So führen die Rotationen im Zick-Zack-Fall dazu, dass mit u auch seine Teilbäume B und C näher an die Wurzel gelangen. 1.4 Schüttelbäume 17 Rotation an v v u u v C A A B B C (a) Zick: Der Knoten u wird durch Rotation zur Wurzel. Rotation an w w v v u w D u C A A B Rotation an v B C D u v A w B C D (b) Zick-Zick: Es erfolgt eine einfache Rotationen an w, gefolgt von einer einfachen Rotationen an v Doppelrotation an w w v u w v A u D B A B C D C (c) Zick-Zack: Es erfolgt eine Doppelrotation an w. Abbildung 1.12: Die drei Situationen beim Splay am Knoten u. Jeder Fall hat noch eine symmetrische Variante, die hier nicht gezeichnet ist. 18 Suchbäume und Selbstorganisierende Datenstrukturen Algorithmus 1.5 Implementierung der S PLAY-Operation. S PLAY(T , x) 1 u ← S EARCH -T REE -S EARCH(T , x) { Finde x mit Hilfe von Algorithmus 1.1. } 2 while p[u] 6= NULL do { Solange u noch nicht die Wurzel des Baums ist } 3 if u hat einen Vater v = p[u], aber keinen Großvater then { Zick“-Fall, siehe Abbildung 1.12(a) } ” 4 Führe eine einfache Rotation an v = p[u] durch. 5 return { Beende das Verfahren. } 6 end if 7 if u hat einen Vater v = p[u] und einen Großvater w = p[v] = p[p[u]] and sowohl v als auch u sind linke (rechte) Söhne ihres Vaters then { Zick-Zick“-Fall, siehe Abbildung 1.12(b) } ” 8 Führe eine einfache Rotation an w gefolgt von einer einfachen Rotation an v aus. 9 else 10 { Zick-Zack“-Fall, siehe Abbildung 1.12(c). Der Knoten u hat einen ” Vater v = p[u] und einen Großvater w = p[v] = p[p[u]] und v ist linker (rechter) Sohn seines Vaters, u aber rechter (linker) Sohn seines Vaters } 11 Führe eine Doppelrotation an w aus. 12 end if 13 end while 8 8 9 6 7 2 7 2 5 1 9 6 3 1 4 4 3 5 (a) Im Ausgangsbaum wurde 3 gesucht. Es wird jetzt am Knoten 13 geschüttelt. Es liegt der ZickZick-Fall vor (angedeutet durch die gestrichelten Kanten), bei dem zwei einfache Rotation erfolgen. (b) Nun liegt ein Zick-Zack-Fall vor. Es erfolgt eine Doppelrotation an 6. 8 3 9 3 6 2 1 5 (c) Nun liegt noch einmal der Zick-Fall vor, bei dem 3 durch eine einfache Rotation zur Wurzel wird und nach dem das Verfahren terminiert. 9 6 1 7 4 8 2 7 4 5 (d) Im Endergebnis steht 13 in der Wurzel. Abbildung 1.13: Beispiel für eine S PLAY-Operation. Hier wird S PLAY(T , 1) ausgeführt. 1.4 Schüttelbäume 1.4.3 19 Analyse der S PLAY-Operation In den nächsten beiden Abschnitten gehen wir der Frage nach, wie effizient Schüttelbäume sind. Dazu betrachten wir in diesem Abschnitt zunächst einmal die zentrale S PLAYOperation. Als Hilfsmittel dient uns die amortisierte Analyse aus Abschnit 1.3. Sei U eine Menge von Elementen ( Universum“), die wir in den Suchbaum einfügen, ” im Baum suchen und aus dem Baum löschen können. Die Menge U repräsentiert die möglichen Schlüsselwerte in unseren Bäumen. Sei g : U → R>0 eine Gewichtsfunktion auf U. Wir analysieren alle Operationen in Abhängigkeit der Gewichte der involvierten Elemente. Nachher werden wir g geeignet wählen, so dass wir eine ganze Reihe von hilfreichen Ergebnissen erhalten. Sei v ein Knoten im Baum T . Mit Tv bezeichnen wir den Teilbaum mit Wurzel v inklusive v und mit G(v) das Gewicht aller Knoten in Tv , d.h., X g(w). G(v) := w∈Tv Zur kürzeren Notation setzen wir außerdem: G(T ) := G(root[T ]), wobei root[T ] wie bisher die Wurzel von T ist. Nun definieren wir noch den Gewichts-Rang (oder einfach Rang) von v durch X (1.5) g(w) . r(v) := log2 G(v) = log2 w∈Tv Letztendlich sei das Potential Φ(T ) eines Schüttelbaums definiert durch X r(v). Φ(T ) := v∈T Wir werden nun die amortisierten Kosten der S PLAY-Operation nach oben abschätzen. Wir erinnern daran, dass die amortisierten Kosten einer Operation wie folgt definiert sind (siehe Abschnitt 1.3): a := c + Φ(T ′ ) − Φ(T ), wobei c der reale Zeitaufwand (reale Kosten) ist und Φ(T ) bzw. Φ(T ′ ) das Potential des Baums vor bzw. nach der Operation bezeichnen. Um die amortisierten Kosten der S PLAY-Operation abzuschätzen, zerlegen wir eine solche Operation in einzelne Splay-Schritte, in denen jeweils einer der drei Fälle aus Abbildung 1.12 vorliegt. Zunächst zeigen wir eine triviale, aber auch hilfreiche Eigenschaft der Ränge. Lemma 1.7 Für alle Knoten x mit p[x] 6= NULL gilt die Ungleichung r(p[x]) > r(x). Beweis: Die Ungleichung folgt sofort aus G(p[x]) = g(p[x]) + G(x) > G(x). ✷ Wir notieren noch ein hilfreiches Lemma: Lemma 1.8 Die Logarithmusfunktion log2 : R>0 → R ist konkav, erfüllt also insbesondere log2 a + log2 b a+b > log2 2 2 für alle a, b > 0. 20 Suchbäume und Selbstorganisierende Datenstrukturen Beweis: Mit elementaren Mitteln der Analysis. ✷ Lemma 1.9 Die amortisierten Kosten eines einzelnen Splay-Schrittes am Knoten u, bei dem der Zick-Fall vorliegt, betragen höchstens 1 + 3(r ′ (u) − r(u)). Beweis: Wir bezeichnen mit r ′ die Ränge der einzelnen Knoten nach dem Splay-Schritt und mit T ′ den Ergebnisbaum. Durch den Splay-Schritt ändern sich nur die Ränge von u und seinem Vater v im Baum T , so dass für die Potentialdifferenz gilt: Φ(T ′ ) − Φ(T ) = r ′ (u) + r ′ (v) − r(u) − r(v) (1.6) Weiterhin ist r ′ (u) = r(v), da beide Größen dem Logarithmus der Summe der Gewichte aller Elemente im Baum entsprechen. Die realen Kosten für den Splay-Schritt sind gleich 1. Somit erhalten wir aus (1.6) für die amortisierten Kosten die obere Schranke: 1 + Φ(T ′ ) − Φ(T ) = 1 + r ′ (v) − r(u) 6 1 + r ′ (u) − r(u) ′ 6 1 + 3(r (u) − r(u)) (nach Lemma 1.7) (da r ′ (u) = r(v) > r(u) nach Lemma 1.7) Somit folgt das Lemma. ✷ Lemma 1.10 Die amortisierten Kosten eines einzelnen Splay-Schrittes am Knoten u, bei dem der Zick-Zick-Fall oder ein Zick-Zack-Fall vorliegt, betragen höchstens 3(r ′ (u) − r(u)). Beweis: Wir betrachten als erstes den Zick-Zick-Fall. Die realen Kosten sind gleich 2 (für zwei Rotationen) Die Ränge aller Knoten außer u, v und w bleiben unverändert. Daher sind die amortisierten Kosten für den Splay-Schritt: 2 + Φ(T ′ ) − Φ(T ) = 2 + r ′ (u) + r ′ (v) + r ′ (w) − r(u) − r(v) − r(w) = 2 + r ′ (v) + r ′ (w) − r(u) − r(v) 6 2 + r ′ (v) + r ′ (w) − 2r(u) 6 2 + r ′ (u) + r ′ (w) − 2r(u) (da r ′ (u) = r(w)) (da r(u) 6 r(v)) (da r ′ (v) 6 r ′ (u)) (1.7) Weiterhin gilt G(u) + G ′ (w) 6 G ′ (u) (vgl. Abbildung 1.12(b)), also haben wir r(u) + r ′ (w) = log2 G(u) + log2 G ′ (w) G(u) + G ′ (w) 2 G ′ (u) 6 2 log2 2 = 2r ′ (u) − 2. 6 2 log2 (nach Lemma 1.8) Aus dieser Ungleichungskette erhalten wir r ′ (w) 6 2r ′ (u) − 2 − r(u). Setzt man diese Ungleichung in (1.7) ein, so erhalten wir 2 + Φ(T ′ ) − Φ(T ) 6 2 + r ′ (u) + (2r ′ (u) − 2 − r(u)) − 2r(u) = 3(r ′ (u) − r(u)). Damit haben wir die Behauptung des Lemmas für den Zick-Zick-Fall bewiesen. Wir betrachten nun den Zick-Zack-Fall (siehe Abbildung 1.12(c)). Wie beim Zick-ZickFall ändern sich höchstens die Ränge von u, v und w. Daher sind die amortisierten Kosten 1.4 Schüttelbäume 21 gegeben durch: 2 + Φ(T ′ ) − Φ(T ) = 2 + r ′ (u) + r ′ (v) + r ′ (w) − r(u) − r(v) − r(w) = 2 + r ′ (v) + r ′ (w) − r(u) − r(v) 6 2 + r ′ (v) + r ′ (w) − 2r(u) (da r ′ (u) = r(w)) (da r(v) > r(u)) (1.8) Es gilt nun G ′ (v) + G ′ (w) 6 G ′ (u) (siehe Abbildung 1.12(c)). Damit folgt analog zum Zick-Zick-Fall, dass r ′ (v) + r ′ (w) 6 2r ′ (u) − 2. Benutzt man diese Ungleichung in (1.8), so erhält man: 2 + Φ(T ′ ) − Φ(T ) 6 2 + (2r ′ (u) − 2)) − 2r(u) = 2(r ′ (u) − r(u)) 6 3(r ′ (u) − r(u)) (da r ′ (u) > r(u)) Dies beendet den Beweis des Lemmas. ✷ Korollar 1.11 Sei T ein Schüttelbaum mit Wurzel root[T ] und x ein Knoten in T . Die amortisierten Kosten für die Operation S PLAY(T , x) betragen höchstens G(T ) , (1.9) 1 + 3 · (r(root[T ]) − r(x)) = O log G(x) wobei root[T ] der Wurzelknoten von T ist. Beweis: Die Abschätzung durch den linken Term in (1.9) folgt sofort aus Lemma 1.9 und 1.10 durch Summieren der amortisierten Kosten für die einzelnen Splay-Schritte. Der rechte Term ergibt sich aus r(v) = log2 G(v) und den Rechenregeln für den Logarithmus. ✷ Aus unserer Implementierung des Suchens mittels der S PLAY-Operation ergibt sich die gleiche (amortisierte) Zeitkomplexität wie für S PLAY(T , x) auch für S EARCH(T , x). Wir werden im nächsten Abschnitt ähnliche Schranken wie in Korollar 1.11 für die anderen Suchbaumoperationen beweisen. Bevor wir dies tun, soll hier schon auf die Mächtigkeit der Aussage in Korollar 1.11 hingewiesen werden. Das Korollar gilt unabhängig von der Gewichtsfunktion g : U → R>0 . Es steht uns frei, g geeignet zu wählen. Zunächst erinnern wir nochmal daran, wie wir aus oberen Schranken für die amortisierten Kosten einer Operationenfolge auch obere Schranken für die realen Kosten dieser Folge herleiten können. Die realen Kosten entsprechen den amortisierten Kosten plus der Potentialdifferenz Φ(T ) − Φ(T ′ ), wobei T der Startbaum und T ′ der Endbaum nach der Operationenfolge ist (vgl. (1.3)). Für die erwähnte Potentialdifferenz gilt: X r(u) − r ′ (u) Φ(T ) − Φ(T ′ ) = u∈T = X log G(u) G ′ (u) log G(T ) g ′ (u) u∈T 6 X u∈T (1.10) Wir sind nun bereit, das erste wichtige Ergebnis zu zeigen. Satz 1.12 Die realen Kosten für eine Folge von m Suchzugriffen auf einen Schüttelbaum mit n Elementen sind in O((m + n) log n). 22 Suchbäume und Selbstorganisierende Datenstrukturen Beweis: Wir setzen g(u) := 1/n für alle u ∈ U. Es gilt dann G(T ) = 1 und somit sind nach Korollar 1.11 die amortisierten Kosten für einen Suchzugriff dann O(log n). Damit ergibt sich unmittelbar die obere Schranke von O(m log n) für die amortisierten Kosten einer Folge von m Suchoperationen. Die realen Kosten sind nach (1.10) in O(m log n) + O(n · log n) = O((n + m) log n). ✷ Das Ergebnis aus Satz 1.12 läßt sich informell wie folgt formulieren: auf einer genügend langen Folge von Suchzugriffen sind Schüttelbäume mindestens so effizient wie ein beliebiger statischer Suchbaum, der gleichmäßig balanciert“ ist, d.h. in dem jedes Element ” logarithmische Tiefe besitzt. Wir verschärfen dieses Ergebnis jetzt, indem wir zeigen, dass Schüttelbäume mindestens so effizient sind wie ein beliebiger statischer Suchbaum. Satz 1.13 (Statische Optimialität von Schüttelbäumen) Die realen Kosten für eine Folge von m Suchzugriffen auf einen Schüttelbaum mit n Elementen, wobei Element u genau q(u) > 1 mal gesucht wird, sind in ! X m q(u) log O m+ . q(u) u∈T Beweis: Für u ∈ U setzen wir G(u) := q(u)/m. Dann ist G(T ) = 1 und nach Korollar 1.11 sind die amortisierten Kosten für einen Suchzugriff auf u in O(log(m/q(u))). Daher sind die amortisierten Kosten für die q(u) Zugriffe auf u höchstens O(q(u) log(m/q(u))). Nach P (1.10) ist mit den gerade definierten Gewichten der Potentialverlust über die Folge ✷ u∈T log(m/q(u)). Warum liefert uns Satz 1.13 eine statische Optimalität“ der Schüttelbäume? Wir benutzen ” ein Resultat aus der Informationstheorie, welches die minimalen Suchkosten nach unten abschätzt. Satz 1.14 Die Kosten für eine Folge von m Suchoperationen auf einem statischen Suchbaum T ∗ mit n Elementen, von denen Elememt u genau q(u) > 1 mal gesucht wird, sind in ! X m Ω m+ q(u) log . q(u) ∗ u∈T ✷ Aus den Sätzen 1.13 und 1.14 folgt, dass die Schüttelbäume maximal um einen konstanten Faktor schlechter sind als ein optimaler statischer Suchbaum. Die Schüttelbäume benötigen dabei aber keinerlei Kenntnis über die Verteilung der Suchzugriffe! 1.4.4 Analyse der Suchbaumoperationen Nachdem wir in Korollar 1.11 eine obere Schranke für die amortisierten Kosten der SplayOperation (und somit auch der Such-Operation) hergeleitet haben, beschäftigen wir uns nun mit den anderen Suchbaumoperationen. Satz 1.15 Für die amortisierten Kosten der Suchbaumoperationen in einem Schüttelbaum gelten folgende Aussagen: 1.4 Schüttelbäume 23 (i) Die amortisierten Kosten für S PLAY(T , x) und S EARCH(T , x) sind in O log G(T ) falls x ∈ T G(x) G(T ) O log falls x ∈ / T. min{G(x− ),G(x+ )} (ii) Die amortisierten Kosten für J OIN(T1 , T2 ) betragen G(T1 ) + G(T2 ) , O log G(z) wobei z das größte Element im Baum T1 ist. (iii) Die amortisierten Kosten für S PLIT(T , x) sind in O log G(T ) G(x) G(T ) O log min{G(x− ),G(x+ )} falls x ∈ T falls x ∈ / T. (iv) Die amortisierten Kosten für I NSERT(T , x) sind G(T ) + g(x) , O log min{G(x− ), G(x+ ), g(x)} wobei x− und x+ der direkte Vorgänger bzw. Nachfolger von x in T sind. (v) Die amortisierten Kosten für D ELETE(T , x) sind G(T ) G(T ) − g(x) O log + O log , G(x) G(x− ) wobei x− der direkte Vorgänger von x im Baum T ist. Beweis: (i) Die Schranke für S PLAY(T , x) haben wir bereits in Korollar 1.11 gezeigt. Wir haben auch bereits argumentiert, dass die Kosten von S EARCH(T , x) mit denen von S PLAY(T , x) identisch sind, falls x ∈ T . Falls x ∈ / T , so wird nach Definition der S PLAY-Operation entweder der Vorgänger x− oder der Nachfolger x+ in die Wurzel befördert. (ii) Die amortisierten Kosten für J OIN2 (T1 , T2 ) kann man wie folgt nach oben abschätzen (vgl. Abbildung 1.8): G(T1 ) (für S PLAY(T1 , +∞)) O log G(r) +1 (für die realen Kosten,) (um T2 an r anzuhängen) + O (log(G(T1 ) + G(T1 )) − log G(T1 )) (Rangänderung von r) (beim Anhängen von T2 ) G(T1 ) G(T1 ) + G(T2 ) = O log + O log G(r) G(T1 ) G(T1 ) + G(T2 ) = O log G(r) (da G(T1 ) > G(r)) 24 Suchbäume und Selbstorganisierende Datenstrukturen (iii) Bei S PLIT(T , x) wird zunächst ein S PLAY(T , x) ausgeführt (vgl. Abbildung 1.9). Für diesen Schritt sind die amortisierten Kosten identisch mit der S PLAY-Operation. Danach werden noch zwei Links von der Wurzel aufgebrochen, was konstante reale Kosten erfordert. In diesem zweiten Schritt ändert sich maximal der Rang der Wurzel x. Da der Rang aber höchstens fällt, sind die amortisierten Kosten für den zweiten Schritt auch konstant. (iv) Die Kosten für I NSERT(T , x) entsprechen bis auf die Potentialänderung durch das Einfügen der neuen Wurzel denen von S PLIT(T , x). Durch das Einfügen der Wurzel x (siehe Abbildung 1.10) erhöht sich das Potential höchstens um G(T ) + g(x) log . g(x) so dass die behauptete Schranke folgt. (v) Die Abschätzung für D ELETE(T , x) folgt aus den Abschätzungen für S PLAY(T , x) und J OIN(T1 , T2 ), wobei T1 und T2 wie in Abbildung 1.11 sind. Man benutzt dabei, dass G(T1 ) + G(T2 ) = G(T ) − g(x). ✷ Setzt man g(u) := 1 für alle u ∈ U, so zeigt Satz 1.15, dass alle Suchbaumoperationen in einem Schüttelbaum mit n Knoten in O(log n) amortisierter Zeit ausgeführt werden können. Somit haben wir folgendes Ergebnis: Satz 1.16 Eine Folgt von m Suchbaumoperationen auf einer Menge von anfangs leeren P Schüttelbäume benötigt O(m + m i=1 log ni ) Zeit, wobei ni die Anzahl der Knoten in demjenigen Baum ist, auf den die ite Operation wirkt. ✷ Satz 1.16 zeigt, dass die Schüttelbäume nicht nur bei der Suche, sondern bei allen Suchbaumoperationen asymptotisch so gut sind wie die gebräuchlichen Klassen von balancierten Bäumen. 2 Dynamische Bäume Die Datenstruktur der dynamischen Bäume verwaltet eine Kollektion von knotendisjunkten Bäumen. Jeder Knoten v hat ein Gewicht g(v) ∈ R>0 ∪ {−∞, +∞}. Die Bäume werden als von unten nach oben gerichtet angesehen: für v ist p[v] der Vater von v im Baum, der v enthält, wobei v = NULL, falls v eine Wurzel ist. Folgende Operationen werden unterstützt: F IND -ROOT(v) liefert die Wurzel des Baums, der den Knoten v enthält. F IND -S IZE(v) liefert die Anzahl der Knoten im Baum, der v enthält. F IND -VALUE(v) liefert das Gewicht g(v). F IND -M IN(v) liefert den Knoten w auf dem eindeutigen Weg von v zur Wurzel mit minimalem Gewicht g(w). Falls es mehrere solche Knoten gibt, dann liefere den Knoten, der am dichtesten an der Wurzel ist. C HANGE -VALUE(v, x) addiert den Wert x ∈ R zum Gewicht g(w) jedes Vorfahren von v hinzu. Wir definieren (−∞) + (+∞) := 0. L INK(v, w) kombiniert die Bäume, welche die Knoten v und w enthalten, indem w zum Vaterknoten von v gemacht wird (vgl. Abbildung 2.1). Die Operation führt keine Aktionen aus, falls v und w im bereits gleichen Baum sind oder v kein Wurzelknoten ist. C UT(v) zerschneidet den Baum, der v enthält, durch Entfernen des Bogens von v zu seinem Vaterknoten in zwei Bäume (vgl. Abbildung 2.2). Die Operation führt keine Aktion aus, falls v ein Wurzelknoten ist. Wir zeigen nun, wie wir dynamische Bäume durch eine Erweiterung der Schüttelbäume aus Kapitel 1 implementieren können, so dass jede Operation auf einem dynamischen Baum der Größe n nur O(log n) amortisierte Zeit benötigt. Wir repräsentieren jeden dynamischen Baum T durch einen virtuellen Baum VT mit gleicher Knotenmenge, aber anderer Struktur. VT besteht aus einer hierarchischen Kollektion von binären Bäumen in folgender Weise: Zusätzlich zu linkem und rechten Sohn (jeder möglicherweise gleich NULL, also nichtexistent) hat jeder Knoten noch null oder mehr mittlere Kinder. Wir stellen uns Kanten zwischen mittleren Kindern und den Vätern als ≫gestrichelt≪ vor. Der virtuelle Baum zerfällt also an den gestrichelten Kanten in eine Kollektion von durchgezogenen Bäumen. (vgl. Abbildung 2.3). 26 Dynamische Bäume a a c b c b g e d d h f i L INK(x, d) e g f j x h i y x j y z Abbildung 2.1: Illustration der Operation L INK. a c b e f g a d h b C UT(e) c g e d h i i j j Abbildung 2.2: Illustration der Operation C UT. f z 27 Der Zusammenhang zwischen T und VT ist wie folgt: Der Vater von v in T ist der LWRNachfolger von v im durchgezogenen Baum von VT, der v enthält. Für den gemäß LWROrdnung letzten Knoten in einem durchgezogenen Baum ist sein Vater der Vater der Wurzel seines durchgezogenen Baums. Wir werden jeden durchgezogenen Baum mit Hilfe eines Schüttelbaums umsetzen. Um die Topologie des gesamten virtuellen Baumes VT zu speichern, genügt es wie bisher für jeden Knoten u einen Zeiger p[u] auf den Vater von x und Zeiger left[u] und right[u] auf den linken und rechten Sohn von u zu speichern. Wir können dann in konstanter Zeit feststellen, ob u ein linker, rechter oder mittlerer Sohn seines Vaters p[u] ist: dazu vergleichen wir wir einfach x mit left[p[u]] und right[p[u]]. Im Folgenden werden wir zur Restrukturierung eines virtuellen Baumes S PLAY-Operationen in den durchgezogenen Teilbäumen ausführen. Dabei werden die mittleren Kinder ≫einfach mitgenommen≪. Genauer, rotieren wir so, als ob mittlere Kinder nicht vorhanden wären in den durchgezogenen Bäumen. Da wir keine Zeiger auf mittlere Kinder, sondern nur von den mittleren Kindern zu den Vätern halten, wandern die Kinder mit dem Vater. Abbildung 2.4 zeigt eine Rotation unter Beibehaltunger der mittleren Kinder. Die zweite Restrukturierungstechnik, die wir anwenden werden, ist das Vertauschen von Söhnen. Genauer, wird dabei ein mittlerer Sohn v zum linken Sohn seines Vaters w gemacht, und der bisherige linke Sohn u wird zu einem mittleren Sohn. Abbildung 2.5 veranschaulicht diese Operation. Die Operation kann einfach durch Setzen von left[w] = v ausgeführt werden und benötigt konstante Zeit. Wir zeigen nun, wie wir die Gewichte g(u) für die Knoten im Baum speichern. Dazu werden wir g(u) nicht direkt bei u sondern ≫implizit≪ abspeichern. Die implizite Speicherung, wie wir sie gleich vorstellen, hat den Vorteil, dass Aktualisierungen des Baumes, vor allem die C HANGE -VALUE-Operation schneller vorgenommen werden können. Für einen Knoten u bezeichnen wir mit g(u) sein Gewicht und mit m(u) das minimale Gewicht eines Nachfolgers von u im durchgezogenen Teilbaum von u. Wir speichern nun für jeden Knoten u die folgenden zwei Werte: g(u) ∆g(u) := g(u) − g(p[u]) falls u Wurzel eines durchgezogenen Baums ist sonst ∆m(u) := g(u) − m(u). Bei der vorgestellten Speicherung können wir für jeden Knoten u die Werte g(u) und m(u) in O(1) Zeit bestimmen. Außerdem können wir die Werte nach einer Rotation oder einer Sohn-Vertauschung in O(1) Zeit wieder auf die korrekten Werte aktualisieren. Wir zeigen dies für eine einfache Rotation, die beim Zick-Fall auftritt (vgl. Abbildung 1.12(a)). Die anderen Rotationen lassen sich analog behandeln. Sei u der linke Sohn von v = p[u] und seien die Knoten a, b, c wie in Abbildung 2.4 gezeichnet. Nach der Rotation an v müssen die Daten für die Knoten u, v und b aktualisiert werden. Alle anderen Werte werden durch die Rotation nicht berührt. Die neuen Daten ergeben sich durch: ∆g ′ (u) = ∆g(u) + ∆g(v) ∆g ′ (v) = −∆g(u) ∆g ′ (b) = ∆g(u) + ∆g(b) ∆m ′ (v) = max{0, ∆m(b) − ∆g ′ (b), ∆m(c) − ∆g(c) } ∆m ′ (u) = max{0, ∆m(a) − ∆g(a), ∆m ′ (v) − ∆g ′ (v) } ∆m ′ (b) = ∆m(b). 28 Dynamische Bäume 10 a 3 b 2 c e 5 d 13 g 6 8 f h 15 j 1 4 i 6 l k 7 m 15 9 o p 1 n 10 r 4 12 q 2 s 12 t 8 u 3 v 3 w (a) Der Baum T . Die Zahlen in den Knoten bezeichen die Kosten g(v). f 8,6 l -2,2 q 6,0 -5,1 b p 1,0 c -1,0 i -2,0 7,0 a j 1,0 15,10 h 5,0 g r 4,0 11,0 m k -8,0 7,0 d o 2,0] -10,0 e 10,0 n v 3,1 w 0,0 9,10 t u -4,0 -10,0 s (b) Der virtuelle Baum VT. Die Zahlen in den Knoten bezeichnen ∆g und ∆m. Abbildung 2.3: Ein dynamischer Baum T und ein virtueller Baum VT, der T repräsentiert. 29 Rotation an v v u c Z a b X Y u a v X Y c b Z Abbildung 2.4: Rotation in einem Schüttelbaum unter Beibehaltung der mittleren Kinder. w u v w x v u x Abbildung 2.5: Vertauschen von einem mittleren Sohn mit den linken Sohn. Bei einer Sohn-Vertauschung wie in Abbildung 2.5 sind lediglich Werte bei den betroffenen Söhnen u und v zu aktualisieren. Die Formeln hierfür lauten: ∆g ′ (v) = ∆g(v) − ∆g(w) ∆g ′ (u) = ∆g(u) + ∆g(w) ∆m ′ (w) = max{0, ∆m(v) − ∆g ′ (v), ∆m(right[w]) − ∆g(right[w])}. Die Expose-Operation Im virtuellen Baum VT werden wir die Operationen auf dynamischen Bäumen auf eine abgewandelte Schüttel-Operation zurückführen. Um Mißverständnisse zu vermeiden, bezeichnen wir das Schütteln in einem durchgezogenen Baum, bei dem die mittleren Kinder mitgenommen werden, als S PLAY und die abgewandelte Schüttel-Operation, die wir gleich beschreiben als E XPOSE-Operation. Wir beschreiben E XPOSE im virtuellen Baum VT als einen dreiphasigen Bottom-UpProzeß (die drei Phasen lassen sich auch zu einer kombinieren, die Darstellung ist mit getrennten Phasen aber klarer). Sei x der Knoten, der exponiert werden soll. In der ersten Phase folgen wir dem Pfad von x zur Wurzel von VT. Dabei schütteln wir innerhalb jedes durchgezogenen Baums wie folgt: zunächst wird x zur Wurzel seines durchgezogenen Baum geschüttelt. Sei y der resultierende Vater von x im virtuellen Baum, der mit x durch eine gestrichelte Kante verbunden ist. Wir schütteln nun y zur Wurzel seines durchgezogenen Baums. Dieses Verfahren setzen wir fort, bis wir an der Wurzel angelangt sind. Nach der ersten Phase besteht der Pfad von x zur Wurzel des virtuellen Baums nur aus gestrichelten Kanten. 30 Dynamische Bäume In der zweiten Phase folgen wir wieder den (aktuellen) Pfad von x zur Wurzel von VT, wobei wir den aktuellen Knoten (der mittlerer Sohn seines Vaters ist) mit den linken Knoten des Vaters vertauschen. Dabei wird der alte linke Sohn zu einem mittleren Sohn. Nach der zweiten Phase befinden sich x und die Wurzel des virtuellen Baums im gleichen durchgezogenen Baum. In der dritten Phase folgen wir ein letztes Mal dem Pfad von x zur Wurzel und schütteln in der üblichen Weise x zur Wurzel. Nach der dritten Phase ist x dann Wurzel des virtuellen Baums. Zeitaufwand für E XPOSE Wir analysieren nun die amortisierte Zeit für E XPOSE(v). Dabei benutzen ein Potential analog zu Abschnitt 1.4.3. Jeder Knoten v hat Gewicht g(v) = 1, G(v) bezeichnet das Gewicht aller Knoten im Teilbaum mit Wurzel v (wobei wir hier sowohl durch gestrichelte als auch durch durchgezogene Kanten erreichbare P Knoten zählen) und r(v) = log2 G(v). Das Potential Φ(T ) eines Baums T ist dann 2 v∈T r(v). Es wird gleich klarwerden, warum wir hier die doppelten Ränge benutzen. Als reale Zeit zählen wir die Anzahl der ausgeführen Rotationen, die gleich der Ausgangstiefe des Knotens v ist. Analog zu Korollar 1.11 zeigt man nun das folgende Ergebnis: Lemma 2.1 Sei T ein durchgezogener Baum mit Wurzel root[T ] in einem virtuellen Baum und x ein Knoten in T . Die amortisierten Kosten für die erweiterte Splay-Operation S PLAY(T , x) betragen höchstens 1 + 6 · (r(root[T ]) − r(x)) (2.1) wobei root[T ] der Wurzelknoten von T ist. Falls wir jede Rotation bei den realen Kosten doppelt zählen, sind die amortisierten Kosten immer noch höchstens 2 + 6 · (r(root[T ]) − r(x)). Beweis: Der Beweis von Korollar 1.11 über Lemmas 1.9 und 1.10 bleibt gültig, auch wenn mittlere Kinder vorhanden sind. Wir führen die leicht geänderten Lemmas mit den nahezu trivialen Änderungen an den Beweisen der Vollständigkeit halber nochmals auf. Die Änderungen sind dabei durch Einkastelungen hervorgehoben. Lemma 2.2 Die amortisierten Kosten eines einzelnen Splay-Schrittes am Knoten u, bei dem der Zick-Fall vorliegt, betragen höchstens 1 + 6(r ′ (u) − r(u)). Falls wir jede Rotation doppelt bei den realen Kosten zählen, sind die amortisierten Kosten höchstens 2 + 6(r ′ (u) − r(u)). Beweis: Wir bezeichnen mit r ′ die Ränge der einzelnen Knoten nach dem Splay-Schritt und mit T ′ den Ergebnisbaum. Durch den Splay-Schritt ändern sich nur die Ränge von u und seinem Vater v im Baum T , so dass für die Potentialdifferenz gilt: Φ(T ′ ) − Φ(T ) = 2 (r ′ (u) + r ′ (v) − r(u) − r(v)) (2.2) Weiterhin ist r ′ (u) = r(v), da beide Größen dem Logarithmus der Summe der Gewichte aller Elemente im Baum entsprechen. Die realen Kosten für den Splay-Schritt sind gleich 1. Somit erhalten wir aus (2.2) für die amortisierten Kosten die obere Schranke: 1 + Φ(T ′ ) − Φ(T ) = 1 + 2 (r ′ (v) − r(u)) 6 1 + 2 (r ′ (u) − r(u)) ′ 6 1 + 6 (r (u) − r(u)) (nach Lemma 1.7) (da r ′ (u) = r(v) > r(u) nach Lemma 1.7) 31 Somit folgt der erste Teil des Lemmas. Falls wir jede Rotation doppelt in den realen Kosten zählen, so ergeben sich offensichtlich Kosten höchstens 2 + 6(r ′ (u) − r(u)). ✷ Lemma 2.3 Die amortisierten Kosten eines einzelnen Splay-Schrittes am Knoten u, bei dem der Zick-Zick-Fall oder ein Zick-Zack-Fall vorliegt, betragen höchstens 3(r ′ (u) − r(u)). Falls wir jede Rotation doppelt bei den realen Kosten zählen, sind die amortisierten Kosten höchstens 6(r ′ (u) − r(u)). Beweis: Wir betrachten als erstes den Zick-Zick-Fall. Die realen Kosten sind gleich 2 (für zwei Rotationen) Die Ränge aller Knoten außer u, v und w bleiben unverändert. Daher sind die amortisierten Kosten für den Splay-Schritt: 2 + Φ(T ′ ) − Φ(T ) = 2 + 2 (r ′ (u) + r ′ (v) + r ′ (w) − r(u) − r(v) − r(w)) = 2 + 2 (r ′ (v) + r ′ (w) − r(u) − r(v)) (da r ′ (u) = r(w)) 6 2 + 2 (r ′ (v) + r ′ (w) − 2r(u)) (da r(u) 6 r(v)) 6 2 + 2 (r ′ (u) + r ′ (w) − 2r(u)) (da r ′ (v) 6 r ′ (u)) (2.3) Weiterhin gilt G(u) + G ′ (w) 6 G ′ (u), also haben wir r(u) + r ′ (w) = log2 G(u) + log2 G ′ (w) G(u) + G ′ (w) 2 = 2r ′ (u) − 2. 6 2 log2 (nach Lemma 1.8) 6 2 log2 G ′ (u) 2 Aus dieser Ungleichungskette erhalten wir r ′ (w) 6 2r ′ (u) − 2 − r(u). Setzt man diese Ungleichung in (2.3) ein, so erhalten wir 2 + Φ(T ′ ) − Φ(T ) 6 2 + 2 (r ′ (u) + (2r ′ (u) − 2 − r(u)) − 2r(u)) = 6 (r ′ (u) − r(u)) -2 . Damit haben wir die erste Behauptung des Lemmas für den Zick-Zick-Fall bewiesen. Falls wir die Rotationen doppelt bewerten, so werden die zusätzlichen Kosten von 2 bei den realen Kosten durch die −2 in der letzten Ungleichung kompensiert. Wir betrachten nun den Zick-Zack-Fall. Wie beim Zick-Zick-Fall ändern sich höchstens die Ränge von u, v und w. Daher sind die amortisierten Kosten gegeben durch: 2 + Φ(T ′ ) − Φ(T ) = 2 + 2 (r ′ (u) + r ′ (v) + r ′ (w) − r(u) − r(v) − r(w)) = 2 + 2 (r ′ (v) + r ′ (w) − r(u) − r(v)) (da r ′ (u) = r(w)) 6 2 + 2 (r ′ (v) + r ′ (w) − 2r(u)) (da r(v) > r(u)) (2.4) (2.5) Es gilt nun G ′ (v) + G ′ (w) 6 G ′ (u). Damit folgt analog zum Zick-Zick-Fall, dass r ′ (v) + r ′ (w) 6 2r ′ (u) − 2. Benutzt man diese Ungleichung in (2.4), so erhält man: 2 + Φ(T ′ ) − Φ(T ) 6 2 + 2 ((2r ′ (u) − 2) − 2r(u)) = 4 (r ′ (u) − r(u)) -2 6 6 (r ′ (u) − r(u)) -2 (da r ′ (u) > r(u)) Wieder kompensiert das −2 die zusätzlichen Kosten von 2 beim doppelten Zählen der Rotationen. ✷ 32 Dynamische Bäume Lemma 2.1 folgt nun unmittelbar aus Lemma 2.2 und Lemma 2.3. ✷ Mit Lemma 2.1 folgt, dass die Kosten für die erste Phase von E XPOSE(v) höchstens 6 log n + k ist, wobei k die Tiefe von v nach der ersten Phase bezeichnet. Man erhält diese Schranke durch Summieren über die beteiligten durchgezogenen Bäume, in denen geschüttelt wird. Für das Schütteln im ersten Baum T1 (von unten gesehen), entstehen Kosten 1 + 6 · (r(root[T1 ]) − r(v)), für das Schütteln im zweiten Baum T2 dann 1 + 6 · (r(root[T2 ]) − r(y)), wobei y der Vater von v nach dem Schütteln in T1 ist. Da r(y) > r(root[T1 ]) ist, sind die Kosten für das Schütteln in T2 also höchstens 1 + 6 · (r(root[T1 ]) − r(root[T2 ])). Die gesamte Summe ergibt dann eine Telekopsumme, die sich auf k + 6 · (r(root[VT]) − r(v)) reduziert, wobei VT die Wurzel des virtuellen Baums ist, die Rang höchstens n besitzt. Die zweite Phase verändert das Potential des virtuellen Baums nicht und führt auch keine Rotationen aus, so dass sie amortisierte Kosten 0 hat. Man beachte, dass in der dritten Phase genau k Rotationen stattfinden. Wir schlagen k Rotationen aus der ersten Phase der dritten Phase zu, indem wir jede Rotation in der dritten Phase doppelt zählen. Damit verringern sich die gerechneten amortisierten Kosten für die erste Phase auf 6 log n. Aus Lemma 2.1 folgt, dass die amortisierten Kosten für die dritte Phase dann immer noch höchstens 6 log n + 2 betragen. Insgesamt haben wir somit für E XPOSE(v) amortisierte Kosten höchstens 6 log n + 6 log n + 2 = 12 log n + 2 = O(log n). Lemma 2.4 Die amortisierten Kosten für E XPOSE(v) in einem virtuellen Baum mit n Knoten sind O(log n). ✷ Implementierung der Operationen mit Hilfe von Expose Die einzelnen Operationen auf dynamischen Bäumen können wir folgt mit Hilfe der E X POSE -Operation implementiert werden: F IND -ROOT(v) Wir führen E XPOSE(v) durch. Dann folgen wir den Zeigern für die rechten Söhne solange, bis wir beim LWR-letzten Knoten w im durchgezogenen Baum angelangt sind. Wir führen E XPOSE(w) durch und liefern w zurück. F IND -S IZE(v) wird dadurch implementiert, dass wir uns für jeden virtuellen Baum seine Kardinalität merken. F IND -VALUE(v) Wir führen E XPOSE(v) durch. Danach wird g(v) explizit geführt und wir können den Wert g(v) zurückliefern. F IND -M IN(v) Wir führen E XPOSE(v) durch und folgen dann den ∆g und ∆m Feldern um zum letzten Nachfolger w von v im durchgezogenen Baum mit minimalen Kosten zu gelangen. Wir führen E XPOSE(w) durch und liefern w zurück. C HANGE -VALUE(v, x) Wir führen E XPOSE(v) aus, addieren x zu ∆g hinzu und subtrahieren x von ∆g(left[x]), falls left[x] 6= NULL. L INK(v, w) Wir führen E XPOSE(v) und E XPOSE(w) durch. Danach machen wir v zu einem mittleren Sohn von w, indem wir p[v] = w setzen. C UT(v) Nach E XPOSE(v) addieren wir ∆g(v) zu ∆g(right[v]) und entfernen den Link von v zu right[v], indem wir p[right[v] = NULL und right[v] = NULL setzen. Man sieht leicht, dass alle Operationen oben O(log n) amortisierte Kosten haben, da E XPOSE nur O(log n) amortisierte Zeit kostet. Damit erhalten wir folgenden Satz: 33 Satz 2.5 Startet man mit einer Kollektion von einelementigen Bäumen, so benötigt eine Folge von ℓ Operationen O(ℓ log k) Zeit, wobei k eine obere Schranke für die Größe der während der Folge auftretenden Bäume ist. ✷ Literaturverzeichnis [1] T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein, Introduction to algorithms, 2 ed., MIT Press, 2001. [2] R. E. Tarjan, Data structures and networks algorithms, CBMS-NSF Regional Conference Series in Applied Mathematics, vol. 44, Society for Industial and Applied Mathematics, 1983.