Effiziente Algorithmen und Datenstrukturen D. S K Wintersemester 2003/2004 Mitschrift erstellt von S K Technische Universität München email: kugele @ cs.tum.edu 21. Dezember 2003 Inhaltsverzeichnis 1 Konzepte und Methoden der Algorithenanalyse 1.1 Theorie und Experiment . . . . . . . . . . . . . . 1.1.1 Experimentelle Analyse . . . . . . . . . . 1.1.2 Theoretische oder asymptotische Analyse 1.2 Komplexitätsmaße und Komplexitätsarten . . . 1.3 Amortisationsanalyse . . . . . . . . . . . . . . . . 1.4 Analyse Randomisierter Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 7 8 9 12 2 Grundlegende Datenstrukturen 2.1 Stacks und Queues . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Priority Queues und Heaps (Prioritätswarteschlange und Halde) 2.4 Wörterbücher und Hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 18 23 26 3 Suchbäume und Skiplisten 3.1 Binäre Suchbäume . . . 3.2 AVL-Bäume . . . . . . . 3.3 (a, b)-Bäume . . . . . . . 3.4 Splay Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 33 36 38 40 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Inhaltsverzeichnis 4 List of Algorithms 1 2 3 Bestimmung des Maximums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Randomisierter B-S . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Randomisierter Gleichheitstest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 13 14 4 5 6 7 8 9 10 D(T, v) . . . . . H(T, v) . . . . ET(T, v) . . PQ-S(A) . . . . I(T, k) . . . . . EM . . . . BUH(S) . . . . . . . 20 20 21 23 25 25 27 11 S(k, v) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 List of Algorithms 6 1 Konzepte und Methoden der Algorithenanalyse Ziel der Vorlesung ist es, die Entwurfsprinzipien „guter“ Algorithmen kennenzulernen. Algorithmus ≈ ein schrittweises Verfahren zur Lösung bestimmter Aufgaben (typischerweise in endlicher Zeit und endlichem Platz) Datenstruktur ≈ eine Systematik zur Organisation und Verwaltung von Daten „gut“ ≈ kerrekt und möglichst geringe Laufzeit (Effizienz), (geringer Speicherverbrauch) 1.1 Theorie und Experiment Arten der Algorithmenanalyse: • experimentelle Analyse • theoretische oder asymptotische Analyse 1.1.1 Experimentelle Analyse Bestimme den Zusammenhalg zwischen Eingabegrößen und der Laufzeit (typischerweise Millisekunden) eines imlementierten Programms auf einer Menge von Testdaten. Ein typisches Resultat ist ein Diagramm. Es treten jedoch folgende Nachteile auf: • Algorithen sind nur bei Ausführung in gleicher Hard- und Softwareumgebung vergleichbar • Algorithmus muss implementiert und ausgeführt werden • Experimente nur auf beschränkter Testmenge • Testmenge repräsentativ für das Problem? 1.1.2 Theoretische oder asymptotische Analyse Bestimme den Zusammenhang zwischen Eingabegrößen und Laufzeit (typischerweise Anzahl der Schritte) auf mathematisch deduktiver Basis. Das typische Resultat ist ein Funktion. Die Komponenten für eine theoretische Analyse sind: • Beschreibungssprache (Pseudocode, ...) • Berechnungsmodell (RAM, TM, ...) • Laufzeitmaße (Bitkomplexität, ...) • mathematische Einbettung der Laufzeitfunktionen (Asymptoik, O, o, Ω, ω, Θ, Auflösung von Rekursionen) Nachteile: 7 1 Konzepte und Methoden der Algorithenanalyse • Asymptotik gibt keine Antwort über Konstanten • Keine Auskunft, ab wann ein asymptotisch schneller Algorithmus mit großer Konstante besser ist, als ein asymptotisch langsamer Algorithmus mit kleiner Konstante • Algorithmus kann zu kompliziert sein, um aussagekräftige mathematische Analyse zuzulassen 1.2 Komplexitätsmaße und Komplexitätsarten Laufzeitfunktion stellt Beziehung zwischen Eingabegröße und Laufzeit her. Fragen: 1. Was ist die Eingabegröße? • Uniforme Eingabegröße: Anzahl der Komponenten, aus denen die Eingabe besteht; z.B. uniforme Größe von x = (x1 , . . . , xm ) ist m • Logarithmische Eingabegröße: Länge der Binärdarstellung der Eingabe (Zahl); es gilt |bin(n)| = log(n) + 1 2. Was ist die Laufzeit? • Anzahl der Elementaroperationen (im RAM-Modell) – – – – – Variablenzuweisung: 1 Schritt Methodenaufruf: 1 Schritt Vergleiche: 1 Schritt Feldindizierung: 1 Schritt arithmetische Operationen (+, ·, −, /): ? ∗ uniformes Komplexitätsmaß ≡ arithmetische Operation in konstanter Zeit unabhängig von der Größe der Zahlen ∗ logarithmische Komplexität (Bitkomplexität) ≡ Kostenarithmetische Operationen müssen auf Bitebene ausgerechnet werden, z.B. Addition von zwei Zahlen in O(n), Multiplikation in O(n2 ) Beispiel Betrachte Algorithmus 1: Bestimmung des Maximums in Array A[1 . . . n] mit n ≥ 1, n ist Eingabelänge. Algorithmus 1 Bestimmung des Maximums 1: max := A[1] 2: for i := 2 to n do 3: if A[i] > max then 4: max := A[i] 5: end if 6: end for 3. Welche Art Beziehung? Sei timeA (x) Anzahl der Elementaroperationen von A auf Eingabe x. Worst-Case-Komplexität: wc − timeA (n) =de f max timeA (x) |x|=n Interpretation: 8 1.3 Amortisationsanalyse • wc − timeA (x) ≤ f (n) ⇒ A macht auf jeder Eingabe der Länge n höchstens f (n) Schritte. • wc − timeA (x) ≥ g(n) ⇒ es gibt ein x der Länge |x| = n, auf dem A mindestens g(n) Schritte braucht. (1) (2) (3) (4) Im Beispiel: wc − timeA (n) = 2 + (n − 1)( 2 + 2 + 2 + 1) + 2 = 7n − 3 Best-case-Komplexität: bc − timeA (n) =de f min timeA (x) |x|=n Interpretation: • bc − timeA (n) ≤ f (n) ⇒ so gibt es eine Eingabe der Länge n auf der A höchstens f (n) Schritte braucht. • bc − timeA (n) ≥ g(n) ⇒ so braucht A auf jeder Eingabe der Länge n mindestens g(n) Schritte. (1) (2) (3) Im Beispiel: bc − timeA (n) = 2 + (n − 1)( 2 + 2 + 1) + 2 = 5n − 1 Average-Case-Komplexität: av − timeA (n) =de f E[TA,n ] wobei TA,n Zufallsvariable, die Laufzeit von A bei zufälliger Wahl der Eingabe der Länge n angibt; generell wird Gleichverteilung auf Eingaben der Länge n angenommen. Mit anderen Worten: X 1 av − timeA (n) = timeA (x) k{x | |x| = n}k |x|=n Im Beispiel: Wir haben ein Feld A[1 . . . n] von n verschiedenen Zahlen (z.B. {1, . . . , n}) gegeben; d.h. eine beliebige Permutation von n Zahlen. Dann gilt: P [A[i] < max{A[1], . . . , A[i − 1]}] = (i − 1)! 1 = i! i Sei X j für j ∈ {1, . . . , n} Zufallsvariable mit 1 Xj = 0 falls A[j] in A[1 . . . j] Maximum ist sonst Dann sei E[X j ] = P[A[ j] > max{A[1], . . . , A[j − 1]}] = P Mit TA,n = 2 + (n − 1)(2 + 2 + 1) + 2 + 2 nj=2 X j folgt: av − time = E[TA,n ] = 5n − 1 + 2 Pn j=2 E[X j ] 1 j = 5n − 1 + 2 n X 1 j |{z} ≤ j=2 5n − 1 + 2 ln(n) (wobei: ln(n + 1) ≤ Hn ≤ ln(n) + 1) Hn −1 n-te harmonische Zahl 1.3 Amortisationsanalyse Typische Situation bei Datenstrukturen: Es wird eine Folge von Grundoperationen (z.B. Einfügen, Löschen, Inkrementieren) ausgeführt. 9 1 Konzepte und Methoden der Algorithenanalyse Amortisationanalyse studiert (worst-case) Gesamtkomplexität bei der sequentiellen Ausführung von n Grundoperationen. Sei T(n) Gesamtkomplexität, dann ist amortisierte Komplexität der Einzeloperationen definiert als T(n) . n Methoden für Amortisationsanalyse: 1. Aggregatmethode 2. Bankkontomethode 3. Potenzmethode Beispiel Binärzähler, Grundoperation ist die Inkrementierung eines Zählers beginnend bei 0. Komplexität sei die Anzahl der geänderten Bits. Insgesamt soll n-mal inkrementiert werden. Naive worst-case Abschätzung: O(n log(n)) 1. Aggregatmethode Allgemein: Schätze T(n) direkt ab. Beispiel: Zähler Komplexität 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1 2 1 3 1 2 1 4 1 Tabelle 1.1: Beispiel: Binärzähler, Komplexität D.h. • bei jedem Zählvorgang entsteht Komplexität 1 • bei jedem zweiten Zählvorgang entsteht zusätzlich Komplexität 1 • bei jedem vierten Zählvorgang entsteht zusätzlich Komplexität 1 Insgesamt: X n n T(n) ≤ n + + + . . . = n2− j = 2n (geometrische Reihe) 2 4 ∞ j=0 2. Bankkontenmethode Allgemein: Seien t1 , t2 , . . . , tn die tatsächlichen Laufzeiten der Einzeloperationen. • ti ist Gebühr an „Komplexitätskasse“, um i-te Operation ausführen zu dürfen • vor jeder Operation erhät der Algorithmus ein „Gehalt“ G • alle Operationen müssen aus den Gehältern bezahlt werden können 10 1.3 Amortisationsanalyse D.h. gibt es geeigneten Gehaltsbetrag G und geeignete Sparstrategie, so gilt: T(n) ≤ nG und damit: amortisierte Komplexität ≤ G Beispiel: G = 2 Sparstrategie: • bei jedem Zählvorgang wird genau ein Bit auf 1 gesetzt, dies kostet 1 Euro • der andere Euro wird gespart, bis das Bit wieder auf 0 zurückgesetzt wird Damit fällt zum Zeitpunkt i k-mal Rücksetzten auf 0 und einmal Bitsetzen an. Finanziert wird dies durch k Euro aus „Sparkonto“ und 1 Euro aus Gehaltseingang. 3. Potenzialmethode Allgemein: • Operationen können potenzielle Energie erzeugen/verbrauchen • amortisierte Komplexität der i-ten Operation: ai = ti + ∆Φ = ti + Φi − Φi−1 (Φ heißt Potenzial) • Gesamtkomplexität: n X T(n) = n X ti = i=1 i=1 n X (ai + Φi−1 − Φi ) = ai + Φ0 − Φn i=1 D.h. falls Φn ≥ Φ0 , sind amortisierte Kosten obere Schranke für die Gesamtkomplexität. Problem: Definiere Φi = Anzahl der Einsen in Binärdarstellung von i • klar: Φi ≥ Φ0 ∀i ≥ 0 • Betrachte Übergang von i zu (i + 1); dabei werden ki Bits wieder auf 0 gesetzt und ein Bit von 0 auf 1: ti = ki + 1 • Φi+1 − Φi ≤ Φi − ki + 1 − Φi = 1 − ki • Damit ai = ti + Φi − Φi−1 = ki + 1 + (1 − ki ) = 2 Beispiel Dynamisches Array: • Typisches Problem bei Verwendung von Arrays: Größe N des Arrays muss deklariert werden (und ist damit fest). • Falls Anzahl zu speichernder Einträge n < N, verschwendet man Speicherplatz. • Falls n > N, ist Array zu klein Lösung: Dynamische Allokation von Speicherplätzen. Die kritische Operation wäre dann: „Füge neues Element hinzu“ in Array A bei n = N. Verfahre wie folgt: (multiplikative Strategie) 1. Initialisiere neues Array B der Größe 2N 2. Kopiere Inhalt von A nach B; d.h. 1: for i := 1 to n do 2: B[i] := A[i] 3: end for 3. Setze A auf B Komplexität der Speicherung einer Tabelle S der Größe n: • Im schlechtesten Fall dauert Einfügen Θ(n) 11 1 Konzepte und Methoden der Algorithenanalyse • D.h. insgesamt O(n2 ) Satz 1. Sei S eine Tabelle, die als dynamisches Array A implementiert ist. Die Laufzeit für das Ausführen von n Einfüge-Operationen in S für den Fall, dass am Anfang S leer ist und N = 1 für die Größe von A gilt, ist O(n). Beweis. Verwende Bankkontomethode der amortisierten Analyse. Die Komplexität des Einfügens (ohne Vergrößerung) sei 1; Komplexität für Vergrößerung von k auf 2k sei k (für kopieren). Wähle G = 3. Zu Zeigen: Es gibt eine geeignete Sparstrategie bei G = 3. • Strategie: „Für jedes Einfügen bezahle 1 Euro und spare 2 Euro“. • Vergrößerung nötig, falls S genau 2i Einträge enthält, Verdopplung der Arraygröße auf 2 · 2i = 2i+1 kostet 2i Euro. • Sparvolumen = gesparte Euro seit letzter Verdoppelung 2(2i − 2i−1 ) = 2i ⇒ T(n) ≤ 3n = O(n) Frage: Wie sieht es aus mit der additiven Vergrößerung? Satz 2. Es sei L > 0 eine additive Vergrößerungskonstante. Sei S eine Tabelle, die als dynamisches Array mit additiver Vergrößerungsstrategie implementiert ist. Die Laufzeit für das Ausführen von n Einfügeoperationen ist Ω(n2 ). notwendig, wenn N0 + Li Beweis. Sein N0 die Initialgröße von A. Dann ist eine Vergrößerung n − L0 Elemente in S enthalten sind, wobei i ∈ 0, 1, . . . , . L | {z } =:m Damit: m−1 X T(n) ≥ m−1 X (N0 + Li) = N0 m + L i=0 i = N0 m + L i=0 m(m − 1) = Ω(m2 ). 2 D.h. T(n) = Ω(n2 ), da m = Θ(n) 1.4 Analyse Randomisierter Algorithmen • Randomisierte (probabilistische, stochastische) Algorithmen verwenden zur Steuerung des Programmablaufs Zufallszahlen. • Damit: Sowohl Laufzeit, als auch Ergebnis sind Zufallszahlen (in Abhängigkeit von der Eingabe). • Setze idealen Zufallsgenerator voraus. • in Pseudocode: random X in M; mit der Interpretation: X wird zufällig, gleichverteilt (und unabhängig) aus endlicher Menge M gezogen. Beispiel Betrachte randomisierten B-S-Algorithmus 2 auf der nächsten Seite Eingabe: Array A[1 . . . n] mit ganzen Zahlen. Beachte: Nur die Laufzeit ist Zufallsvariable. 12 1.4 Analyse Randomisierter Algorithmen Algorithmus 2 Randomisierter B-S 1: repeat 2: random i in {1, 2, . . . , n-1} 3: if A[i] > A[i+1] then 4: vertausche{A[i], A[i+1]} 5: end if 6: until A ist sortiert Komplexitätsarten • Definiere: rtimeA (x) = E[timeA (x)], wobei der Erwartungswert von timeA (x) über alle im Programmablauf von A möglichen Zufallsauswahlen ermittelt wird (nicht über der Eingabe!) • worst-case: wc − timeA (x) = max rtimeA (x) |x|=n • average-case: av − timeA (x) = 1 k{x | |x|=n}k P |x|=n rtimeA (x) Beispiel Randomisierter Gleichheitstest Gegeben seine Felder A[1 . . . n] und B[1 . . . n]. Frage: A ≡ B, d.h. sind die Zahlen in A und B als Multimengen gleich. Z.B. 3 3 1 1 3 3 2 2 2 2 3 3 ≡ . 2 2 1 1 3 3 2 2 3 2 3 3 Tabelle 1.2: Beispiel: Randomisierter Gleichheitstest • Deterministischer Ansatz: Sortiere A und B und vergleiche führt zu einer Komplexität von O(n log n). • Randomisierter Ansatz liefert O(n). Idee: Ordne A und B Polynome pA und pB zu pA (x) = n Y (x − A[i]) i=1 n Y pB (x) = (x − B[i]) i=1 Setze q(x) = pA (x) − pB (x) Es gilt: – deg(pA ) = deg(pB ) = h – deg(q) ≤ n – A ≡ B ⇔ q Nullpolynom (∞-viele Nullstellen)(d.h. A . B ⇒ q hat höchstens n Nullstellen. Sei S endliche Menge mit kSk > n; S ist die Menge der Nullstellenkandidaten. Dann gilt: – A ≡ B ⇒ PS [q(x) = 0] = 1 – A . B ⇒ PS [q(x) = 0] ≤ n kSk 13 1 Konzepte und Methoden der Algorithenanalyse Algorithmus 3 Randomisierter Gleichheitstest nl mo 1: random x in n 2: a := a; b := 1; 3: for i:= 1 to n do 4: a := a·(x-A[i]); 5: b := b·(x-B[i]); 6: end for 7: if a=b then return A ≡ B 8: 9: else 10: return A . B 11: end if Betrachte dazu Algorithmus 3. Es gilt: • Laufzeit ist (für alle Komplexitätsarten) Θ(n) im uniformen Kostenmaß. • Zufallszahl wirkt sich nur auf Ergebnis aus. • Algorithmus macht nur einseitigen Fehler: – A ≡ B ⇒ Algorithmus antwortet immer korrekt – A . B ⇒ Algorithmus ist mit einer Wahrscheinlichkeit ≥ 1 − korrekt (er gibt aus: A . B). Methoden zur Herabsetzung der Fehlerwahrscheinlichkeit: 1. Vergrößerung der Menge S ⇒ lineare Verringerung der Fehlerwahrscheinlichkeit 2. Amplifikation für beidseitigen Fehler ⇒ exponentielle Verringerung der Fehlerwahrscheinlichkeit Amplifikation mit beidseitigem Fehler Angenommen wir haben einen Algorithmus ALG, der den randomisierten Gleichheitstest mit beidseitigem Fehler 0 < < 12 realisiert, d.h. A ≡ B ⇒ P[ALG gibt A . B aus ] ≤ A . B ⇒ P[ALG gibt A ≡ B aus ] ≤ Betrachte Algorithmus ALG0 für t ungerade: 1. Lasse den Algorithmus ALG t-mal laufen und notiere Ergebnisse als 0 (für Ausgabe A . B) und 1 (für Ausgabe A ≡ B) 2. Kommt im Ergebnisvektor 0 öfter vor als 1, so gebe 0 aus und umgekehrt (Majoritätsvotum). Wollen (garantierte) Fehlerwahrscheinlichkeit 0 abschätzen. O.B.d.A. sei A ≡ B, d.h. mit Wahrscheinlichkeit δ+ ≥ 1 − gibt ALG eine 1 aus und mit Wahrscheinlichkeit δ− ≤ eine 0; δ+ + δ− = 1. 14 1.4 Analyse Randomisierter Algorithmen Ausgabe von ALG0 sei 0, d.h. im Ergebnisvektor kommt maximal gilt: δ0 j k t 2 -oft eine 1 vor. Dann b 2t c ! X t ≤ · δi+ · δt−i − i i=0 b 2t c ! X t ≤ · δi+ · δt−i − · i i=0 t δ+ 2 −i δ− | {z } ≥1, da < 21 ∧i≤ 2t b 2t c ! X t = (δ+ δ− ) · i i=0 ! t X t t 2 ≤ (δ+ δ− ) · i i=0 |{z} t 2 2t p t p t = 2 δ+ δ− = 2 (1 − δ− )δ− p ≤ (2 (1 − ))t =: 0 | {z } <1, da < 21 t 1 ( ≤ (4) 2 ; für < ) 4 15 1 Konzepte und Methoden der Algorithenanalyse 16 2 Grundlegende Datenstrukturen 2.1 Stacks und Queues Stack (Keller) ist eine Datenstruktur, bei der das Einfügen und Entfernen von Elementen dem LIFO-Prinzip (last in first out) folgen. Operationen auf Stack S: • push(o): Fügt Objekt o an der Spitze von S ein • pop: Entfernt oberstes Element von S und gibt es zurück • (top: Gibt oberstes Element von S zurück, ohne es zu löschen) Implementation eines Stacks S z.B. als Feld der Größe N (beachte Überläufe). t ist die Position des Topelements in S. 1 ... 2 t ... N S Abbildung 2.1: Implementierung eines Stacks • push(o): 1: if size(s)=N then 2: „Stack voll“ 3: end if 4: t := t +1; 5: S[t] := o; • Pop: 1: if S ist leer then 2: „Stack leer“ 3: end if 4: e := S[t]; 5: t := t-1; 6: return e; Komplexität: Pop, Top in O(1); Push in O(1) amortisiert. Queue (Schlange) ist eine Datenstruktur, bei der Einfügen und Entfernen dem FIFO-Prinzip (first in first out) folgen. 17 2 Grundlegende Datenstrukturen Operationen auf Queue Q: • enqueue(o): Füge Objekt o am Ende von Q (tail) ein • dequeue: Entfernt erstes Element (head) von Q und gibt es zurück Implementierung einer Queue Q z.B. als Feld: h ist Position des Kopfes (head), t ist Position des Schwanzes (tail). 1 2 ... h ... t ... N ... h ... N Q 1 2 ... t Q Abbildung 2.2: Implementierung einer Queue • dequeue: 1: if Q leer then „Queue leer“ 2: 3: end if 4: e := Q[h]; 5: h := (h mod N)+1; 6: return e; • enqueue: 1: if size(Q) = N then 2: „Queue voll“ 3: end if 4: Q[t] := o; 5: t := (t mod N) + 1 Komplexität: dequeue, front in O(1); enqueue in O(1) amortisiert. 2.2 Bäume Aus DS1: Ein Baum ist ein gerichteter, zusammenhängender, kreisfreier Graph. hier: (gewurzelter) Baum T ist eine Menge von Knoten in einer Elter-Kind-Beziehung mit folgenden Eigenschaften: • T hat einen ausgezeichneten Knoten r (Wunzelknoten von T) • Jeder Knoten v , r hat einen Elter u • (T ist zusammenhängend) Bezeichnungen: 18 2.2 Bäume • u ist Elter von v ⇒ V ist Kind von u • Knoten ohne Kinder heißen Blätter; ein Knoten mit mindestens einem Kind heißt innerer Knoten. • Ein Nachfahre von v ist entwerder v selbst oder ein Nachfahre eines Kindes von v; Vorfahre von v sind alle Knoten, für die v ein Nachfahre ist. • Teilbaum von T mit Wurzel v besteht aus allen Nachfahren von v in T (und den zugehörigen Beziehungen). r v Teilbaum mit Wurzel v Abbildung 2.3: Teilbaum mit Wurzel v • Baum T ist geordnet, falls für alle Knoten alle Kinder total geordnet werden können • (Echter) binärer Baum T ist ein geordneter Baum, bei dem alle inneren Knoten (genau) höchstens zwei Kinder haben. Operationen auf Baum T: • Elter(v): Gibt Elterknoten von v zurück. Fehlermeldung, falls v Wurzel. • Kind(v): Gibt einen „Iterator“ der Kinder von v zurück. • Wurzel: Gibt Wurzel von T zurück. Iterator besteht aus einer Folge S, einer aktuellen Position in S und einer Methode (next) zur Bestimmung der nächsten Position und Aktualisierung der gegenwärtigen Position. Traversierung = „Ablaufen“ aller Knoten in einem Baum (und das Ausführen von Operationen auf den Knoten). Beispiel Bestimmung der Höhe eines Baumes • Die Tiefe dv eines Knotens v ist die Anzahl seiner Vorfahren ohne v; Höhe h(T) ist die maximale Tiefe eines Knotens in T, d.h. h(T) =de f max dv v∈T • Rekursiver Algorithmus zur Bestimmung der Tiefe: Komplexität: O(1 + dv ) • 1. Ansatz für Höhe: Bestimme Tiefen aller Knoten und nehme Maximum ⇒ O(n2 ) Laufzeit 2. Ansatz für Höhe: Verwende rekursive Charakterisierung; Tv sei Teilbaum von T mit Wurzel v; T = Tv a) v Blatt ⇒ h(Tv ) = 0 19 2 Grundlegende Datenstrukturen Algorithmus 4 D(T, v) 1: if v Wurzel then 2: return 0 3: else 4: return 1 + D(T, Elter(v)); 5: end if Algorithmus 5 H(T, v) 1: if v Blatt then 2: return 0 3: else 4: n := 0; 5: for w Kind von v do 6: h := max(h, H(T, w)); 7: end for 8: return 1+h; 9: end if b) v innerer Knoten ⇒ h(Tv ) = 1 + max w Kind von v Komplexität: X O( (1 + v∈T h(Tw ) X cv |{z} cv ) = O(n) )) = O(n + Anzahl Kinder von v v∈T |{z} n−1 Bekannte Traversierungsarten • preorder: 1. Werte v aus 2. Gehe zu Kindern über Bsp.: Kapitelstruktur von Hypertext Dokumenten • Postorder 1. Gehe zu Kindern 2. Werte v aus Bsp.: Berechnung des Platzverbrauchs eines Verzeichnisses • inorder 1. Gehe zu linkem Kind 2. Werte v aus 3. Gehe zu rechtem Kind • Eulertour (DS1: Eulertour in G ist ein Weg, der jede Kante genau einmal enthält und Start und Endknoten identisch sind. Zusammenhängender Graph hat Eulertour ⇔ alle Knoten geraden Grad haben. Hier: 20 2.2 Bäume • Betrachte jede Kante als Doppelkante (damit jeder Knoten geraden Grad hat) • Wir interessieren uns für Eulertour von Wurzel zu Wurzel in binären Bäumen. Beispiel einer Eulertour in Abbildung 2.4. Der entsprechende Algorithmus ist der Algorithmus 6. - / + * + 3 + - 3 1 9 * 2 5 3 6 - 7 4 Abbildung 2.4: Eulertour Algorithmus 6 ET(T, v) 1: werte v aus (bevor linkes Kind besucht wird) 2: if v innerer Knoten then ET(T, linkes Kind von v) 3: 4: end if 5: werte v aus (bevor rechtes Kind besucht wird) 6: if v innerer Knoten then ET(T, rechtes Kind von v) 7: 8: end if 9: werte v aus (bevor Rückkehr zum Elter(v)) Anwendung: Ausgabe korrekt geklammerter arithmetischer Ausdrücke. Laufzeit: O(n) Implementierung von Bäumen 1. Arrays: (Für Binärbäume) Definiere Nummerierung (Levelnummerierung): p : T → N a) v Wurzel ⇒ p(v) = 1 b) v linkes Kind von u ⇒ p(v) = 2p(u) c) v rechtes Kind von u ⇒ p(v) = 2p(u) + 1 Die Levelnummerierung ist in Abbildung 2.5 auf der nächsten Seite dargestellt. Speichere Inhalt von Knoten v an Position p(v) in einem Feld; Größe N ist max p(v). v∈T Es gilt: • p ist injektiv n+1 • N ≤ 2 2 − 1 für einen echten Binärbaum (⇒ im schlechtesten Fall exponentieller ungenützter Speicher) 21 2 Grundlegende Datenstrukturen 1 2 4 8 5 9 20 3 10 11 21 7 6 13 12 26 27 Abbildung 2.5: Levelnummerierung • Operationen: Wurzel, Elter, Kind in O(1) im uniformen Kostenmodell; Elter, Kind in O(|p(v)|) bzw. O(n) im logarithmischen Kostenmodell 2. Linkstruktur • im Binärbaum Ein Knoten v ist in Abbildung 2.6 veranschaulicht. Elter(v) Linkes Kind von v Rechtes Kind von v Inhalt Abbildung 2.6: Knoten eines Binärbaums • für allgemeine Bäume Ein Knoten v ist in Abbildung 2.7 veranschaulicht. Elter(v) Inhalt Abbildung 2.7: Knoten eines allgemeinen Baums 22 2.3 Priority Queues und Heaps (Prioritätswarteschlange und Halde) • Operationen: Wurzel, Elter in O(1), Kind in O(cv ) 2.3 Priority Queues und Heaps (Prioritätswarteschlange und Halde) • Identifizieren eines abzuspeichenden Objekts mit einem Schlüssel aus dem Universum U, z.B. U = N, Σ∗ , . . .. • Wir setzen ein total geordnetes universum voraus (≤), d.h. – Reflexivität: k ≤ k – Anti-Symmetrie: (k1 ≤ k2 ∧ k2 ≤ k1 ) ⇒ k1 = k2 – Transitivität: (k1 ≤ k2 ∧ k2 ≤ k3 ) ⇒ k1 ≤ k3 – Totalität: k1 ≤ k2 ∨ k2 ≤ k1 ⇒Jede endliche Teilmenge von U hat einen kleinsten Schlüssel • Priority Queue P ist eine Datenstruktur, die Objekte mit Schlüsseln verbindet und folgende Operationen unterstützt: – Insert(o, k): Fügt Objekt o mit Schlüssel k in P ein. – ExtractMin: Entfernt aus P Objekt mit dem kleinsten Schlüssel und gibt es zurück. – (MinObject: Gibt Objekt mit kleinstem Schlüssel zurück.) – (MinKey: Gibt kleinsten Schlüssel zurück.) Bemerkung: Priority Queues sind inhaltsbezogene Datenstrukturen im Gegensatz zu Stacks, Queues und Bäume. Sortieren mit Priority Queues • Gegeben: Array[1 . . . n] • Gesucht: Aufsteigend sortiertes Feld A[1 . . . n] • Algorithmus 7 zeigt die Arbeitsweise von PQ-S. Algorithmus 7 PQ-S(A) 1: for i = 1 to n do 2: Insert(A[i], A[i]); 3: end for 4: for i = 1 to n do 5: A[i] := EM; 6: end for • Komplexität von PQ-S: – amortisierte Komplexität tn von Insert ⇒ 1: in O(ntn ) – amortisierte Komplexität t0n von ExtractMin ⇒ 2: in O(nt0n ) 23 2 Grundlegende Datenstrukturen Einfache Implementierungen von PQ 1. Ungeordnetes Array P: Einfügen am Ende; Extrahieren durch Suche und Elimination leerer Feldeinträge: ⇒ Insert: O(1) amortisiert P ⇒ Extraction: Θ(n) n1 ni=1 i = n+1 2 amortisiert ⇒ PQ-S (SS): O(n2 ) 2. Geordnetes Array P: Einfügen an „richtiger“ Position i im Feld (links von i nur kleinere Schlüssel, rechts nur größere); Extrahieren bei kleinster Position anfangen ⇒ Insert: Θ(n) amortisiert ⇒ ExtractMin: O(1) ⇒ PQ-S (I S): O(n2 ) PQ-Implemplementierung mit Heaps Heap ist echter binärer Baum T, bei dem in den inneren Knoten des Baumes die Schlüssel gespeichert werden können und zusätzlich zwei Eigenschaften erfüllt: 1. Relationale Eigenschaft zwischen Schlüsseln in T: „Heap-Ordnung“: In einem Heap T gilt für alle inneren Knoten v: v nicht Wurzel ⇒ Schlüssel in v ist größer als Schlüssel in Elter (v) 2. Strukturelle Eigenschaft des Baumes selbst: „Vollständiger Binärbaum“: Binärbaum T der Höhe h ist vollständig falls für alle Level 0, 1, 2, . . . , h − 1 die maximal mögliche Anzahl von Knoten (d.h. 2i Knoten in Level i) vorhanden ist und sich in Level h − 1 alle inneren Knoten links von Blättern befinden. Abbildung 2.8 zeigt einen Heap. Level 0 8 16 13 17 20 Level 1 Level 2 Level 3 Abbildung 2.8: Heap Damit: Wurzel eines Heaps enthält immer das Minimum! Satz 3. Für Heap T über n Schlüssel gilt h(t) = log(n + 1) Beweis. Es gilt: Ph(T)−2 2i + 1 = 2h(T)−1 − 1 + 1 = 2h(T)−1 1. n ≥ i=0 24 2.3 Priority Queues und Heaps (Prioritätswarteschlange und Halde) 2. n ≤ Ph(T)−1 2i = 2h(T) − 1 i=0 Aus 1. folgt: h(T) ≤ 1 + log(n) < 1 + log(n + 1) Aus 2. folgt: h(T) ≥ log(n + 1) Damit: h(T) = log(n + 1) Ziel ist es, ein „Insert“ und „ExtractMin“ in O(h(T)) zu realisieren. 1. Heap-Repräsentierung als Array: Verwende Nummerierung p und Array der Größe N: Heap T hat für n Schlüssel n innere Knoten und 2n + 1 Knoten insgesamt; n + 1 davon sind Blätter (müssen nicht gespeichert werden). Damit N=n= max v innerer Knoten p(v) 2. Einfügen, d.h. aus heap T (ohne Schlüssel k) und Schlüssel k produziere Heap T0 mit Schlüssel k. Algorithmus 8 verdeutlicht dies. Abbildung 2.9 auf der nächsten Seite zeigt ein Beispiel. Komplexität: O(h(T)) im worst-case. Algorithmus 8 I(T, k) 1: Bestimme Blatt v mit p(v) minimal 2: Hänge zwei Kinder an v an 3: Setze Inhalt von v auf k 4: while v nicht Wurzel && Inhalt von v < Inhalt Elter(v) do 5: Vertausche Inhalt von v und Inhalt von Elter(v) 6: end while 7: v := Elter(v) 3. ExtractMin; d.h. Entfernen des Minimums aus Wurzel des Heaps T und Produzieren von T0 . Vergleiche Algorithmus 9. Abbildung 2.10 auf Seite 31 zeigt ein Beispiel. Komplexität: Algorithmus 9 EM 1: S := Inhalt von Wurzel v 2: Bestimme inneren Knoten v∗ mit p(v∗ ) maximal 3: Setze Inhalt von Wurzel r auf Inhalt von v∗ 4: Entferne beiden Kinder von v∗ 5: v := r 6: while v innerer Knoten && Inhalt von v > min{Inhalt linkes Kind von v, Inhalt rechtes Kind von v} do 7: Vertausche Inhalt von v mit „minimalem Kind von v“ 8: v := minimales Kind von v 9: end while 10: return s O(h(T)) im worst case. Damit: • Insert, ExtractMin in O(log n) • PQ-S mit Heaps (HS) in O(n log n) 25 2 Grundlegende Datenstrukturen 3 8 4 16 13 1 2 17 20 3 3 16 17 8 20 13 Abbildung 2.9: Insert in einem Heap Bottom-Up-Heap-Konstruktion Iteratives Einfügen von n Elementen in einen anfangs leeren Heap kostet O(n log n) im worst-case (auch amortisiert, denn: P amortisierte Komplexität ≈ ni=1 log i = log n! = Θ(n log n)). Bottom-Up-Ansatz (rekursiv) liefert Heap in O(n). Zur Vereinfachung sei n = 2n − 1, damit Heap vollständig in allen Leveln ist, n = log(n + 1). Algorithmus 10 auf der nächsten Seite zeigt das rekursive Verfahren. Beispiel eines Bottom-Up-Heaps ist in Abbildung 2.11 auf Seite 32 gezeigt. Es gilt: BUH benötigt für n Schlüssel Laufzeit O(n). Begründung: Sei T resultierender Heap, v ein innerer Knoten und sei Tv Teilbaum mit Wurzel v. Im worst-case muss Inhalt von Wurzel v von Tv bis in niedrigsten inneren Knoten verschoben werden; d.h. Formierung von Tv als Heap aus Teilbäumen der Kinder ist O(h(Tv )). Sei nun P(v) Pfad in T von v zum nächsten knoten in Inorder (einmal rechts, dann immer links); P(v) repräsentiert Absenken zum niedrigsten Knoten. P des Inhalts von v bis Damit: Komplexität = O v∈T Länge von P(v) = (Pfade kantendisjunkt) = = O(Anzahl Kanten inT) = O(n). 2.4 Wörterbücher und Hashing • Gegeben seien Objekte o1 , o2 , . . . , om mit zugehörigen Schlüsseln k1 , k2 , . . . , km . • Universum U = {0, 1, . . . , N − 1} (natürliche Zahlen als Vereinfachung) der Schlüssel; typisch N m 26 2.4 Wörterbücher und Hashing Algorithmus 10 BUH(S) 1: Eingabe: Folge S mit n Schlüsseln 2: Ausgabe: Heap T mit den Schlüsseln von S 3: if S leer then 4: return leerer Heap 5: end if 6: entferne ersten Schlüssel k aus S 7: spalte S in Folgen S1 und S2 mit jeweils n−1 2 = 8: T1 = BUH(S1 ) 9: T2 = BUH(S2 ) 10: Bilde Baum T wie folgt: 2n −2 2 = 2n−1 − 1 Elementen k T1 11: 12: T2 senke k in T ab, bis kein Widerspruch zur Heap-Ordnung auftritt return T • Wörterbuch (Dictionary) ist eine Datenstruktur, die folgende Operationen unterstützt: 1. IsMember(k): Test, ob für k ein Objekt im Wörterbuch enthalten ist, wenn „ja“ wird Objekt zurückgegeben sonst leeres Objekt (NIL) 2. Insert(o, k): Fügt ein Objekt o mit Schlüssel k in Wörterbuch ein unter Voraussetzung, dass kein anderes Objekt mit dem Schlüssel k enthalten ist. 3. Delete(k): Entfernt Objekt o mit Schlüssel k aus Wörterbuch (und gibt es als Ergebnis zurück). • Kapazität (Größe) eines Wörterbuches wird mit n bezeichnet. In der Regel: → m ≤ n N Idee des Hashing Implementiere Wörterbuch als (unsortiertes) Array der Größe n = O(m)(meist m ≤ n ≤ 2m) und lasse Position eines Objekts von Wert des Schlüssels abhängen. Funktion: h : {0, 1, . . . , N − 1} → {0, 1, . . . , n − 1} heißt Hashfunktion. Für k ∈ h ist dann h(k) Position des Objekts mit Schlüssel k im Wörterbuch. Problem Da n N kann Hashfunktion nicht injektiv sein, d.h. es gibt k1 , k2 ∈ U mit k1 , k2 und h(k1 ) = h(k2 ), so dass zwei Schlüssel auf die selbe Position abgebildet werden. man spricht dannn von einer Kollision. → Kollisionne sind nicht selten. 2 Satz 4. Es sei m = o n 3 . Dann tritt in einer Hashtabelle der Größe n mit m Objekten mit Wahr m(m−1) m2 scheinlichkeit ≈ 1 − e− 2n ≈ 1 − e− 2n mindestens eine Kollision auf, wenn für jeden Schlüssel jede Hashposition gleich wahrscheinlich ist. Beweis. ! m−1 m−1 Y j n− j Y = 1− P[Am ] = n n j=0 1−x≤e−x m−1 Y ≤ j e− n = e− j=0 Pm−1 j j=0 n = e− m(m−1) 2n j=0 h i (m−1) ⇒ Wahrscheinlichkeit für Kollision: P Am ≥ 1 − e− 2n 27 2 Grundlegende Datenstrukturen √ Korollar 1. Hat eine Hashtabelle der Größe n mindestens ω( n) Einträge und ist für jeden Schlüssel gede Hashposition gleiche wahrscheinlich, so tritt mit Wahrscheinlichkeit 1 − o(1) mindestens eine Kollision auf. Um Kollisionszahl gering zu halten, müssen Hashfunktionen gut streuen. Eine Hashfunktion h : U → {0, 1, . . . , n − 1} heißt ideal, wenn für alle j ∈ {0, 1, . . . , n − 1} gilt: X p(k) = k∈U 1 n h(k)= j Problem: • Wahrscheinlichkeiten p(k) sind im Allgemeinen nicht bekannt. • Hashfunktionen sollen leicht zu berechnen sein. • Für „gute“ Hashfunktionen: D. E. K: The Art of Computer Programming, Bd. 3: Sorting and Searching, Abschnitt 6.4 Strategien zur Kollisionsauflösung 1. Geschlossene Hashverfahren: (Schlüssel wird unter der durch die Hashfunktion bestimmten Stelle abgespeichert bzw. gefunden) ⇒ Hashing durch Verkettung chaining Idee: Kollisionen werden nicht aufgelöst, d.h. hinter jeder Hashposition steht eine verkettete Liste; neues Element wird immer am Ende der Liste eingefügt. • Im schlechtesten Fall können alle im Schlüssel auf die selbe Hashposition abgebildet werden. • Deshalb für allgemeine Strategie A zur Kollisionsauflösung: A+ = mittlere Anzahl von Zugriffen auf Hashtabelle für erfolgreiche Suche unter Verwendung von A. − A = mittlere Anzahl von Zugriffen auf Hashtabelle für erfolglose Suche unter Verwendung von A. Füllfaktor α einer Hashtabelle α =de f • A− für Chaining: mittlere Länge der Liste: ⇒ A− = 1 + α m n • A+ für Chaining: P i A+ = m1 m−1 i=0 1 + n = 1 + m n =α m(m+1) 2nm ≤1+ m 2n =1+ α 2 ⇒ Für festen Füllfaktor im Mittel O(1) Zugriffe. 2. Offene Hashverfahren: (Bei Kollisionen wird solange weitergesucht, bis freie Hashposition gefunden ist.) Beispiel Für offene Hashverfahren 28 2.4 Wörterbücher und Hashing • Linear Probing (lineares Sondieren) h(k, i) = (h(k) + 1) mod n • Double Hashing h(k, i) = (h(k) + ih0 (k)) mod n wobei n Primzahl, (h0 (k) für alle k teilerfremd zu n) Wollen offene Hashverfahren analysieren unter der Annahme des gleichverteilten hashings, d.h. Folgen (h(k, 0), . . . , h(k, n − 1)) sind gleichewahrscheinliche Permutationen von (0, 1, . . . , n − 1) A− für Open Hashing: Sei X Anzahl Sondierungen für erfolglose Suche mit A. Definiere Ai als Ereignis, dass i-e Sondierung zu belegtem Feldelement führt. P[X ≥ i] = P[A1 ∩ A2 ∩ . . . ∩ Ai−1 ] = P[A1 ] · P[A2 |A1 ] · P[A3 |A1 ∩ A2 ] · . . . · P[Ai−1 |A1 ∩ . . . ∩ Ai−2 ] m m−1 m−i+2 · · ... · n n−1 n−i+2 i−1 m = αi−1 n ∞ ∞ X X i · P[X = i] = P[X ≥ i] = m≤n ≤ A− = E[X] ≤ i=1 ∞ X ≤ i=1 αi−1 = i=1 1 1−α A+ für Open Hashing: Erfolgreiche Suche nach k im (i + 1)-ten Schritt bedeutet erfolglose Suche nach k bis n zum i-ten Schritt, d.h. im Mittel 1−1 i = n−i . n Damit: A+ = ≤ = m−1 m−1 n 1 X n n X 1 n X 1 = = m n−i m n−i m i i=0 i=0 i=n−m+1 Z n n 1 n dx = (ln n − ln(n − m)) m n−m x m n 1 1 n ln = ln m n−m α 1−α Beispiel Für offene Hashverfahren • Lineares Sondieren 1 A+ ≈ 12 1 + 1−α 1 A− ≈ 21 1 + (1−α) 2 • Double Hashing 1 A+ ≈ 12 ln 1−α 1 A− ≈ 1−α Abbildung 2.12 auf Seite 32 verdeutlicht dies: Universelles Hashing 29 2 Grundlegende Datenstrukturen Idee: Zufällige Wahl der Hashfunltion zur Laufzeit aus einer Menge von Hashfunltionen (C, W, 1979). Eina Familie H von Fashfunktionen heißt universell, wenn für alle x, y ∈ U mit x , y gilt: {h ∈ H | h(x) = h(y)} 1 = n kHk Satz 5. Es sei H eine universelle Familie von Hashfunktionen (für eine Hashtabelle der Größe n) und sei h ∈ H eine zufällig gewählte Hashfunktion, wobei alle hashfunktionen aus H gleich wahrscheinlich sind. Für eine Menge M ⊆ U von m Schlüsseln ist die erwartete Anzahl von Kollisionen eines festen Schlüssels x ∈ M mit einem anderen Element aus M kleiner als 1 (m ≤ n). m = kMk Beweis. Es sei Cx (y) definiert als Cx (y) =de f 1 0 falls h(x) = h(y) sonst Wegen H universell und h ∈ H zufällig gewählt, gilt für E[Cx (y)]: Für Cx = P E[Cx (y)] = 0 · P[h(x) , h(y)] + 1 · E[h(x) = h(y)] 1 = P[h(x) = h(y)] = n y∈M, x,y Cx (y) gilt: X E[Cc ] = E[Cx (y)] = y∈M, x,y m−1 <1 n Gibt es Familien universeller Hashfunktionen? Es sei U = {0, 1, . . . , n − 1}r+1 mit n Primzahl. Definiere H =de f {hα |hα : U → {0, 1, . . . , n − 1}, α ∈ U} P r mit hα (x0 , x1 , . . . , xr ) =de f mod n Es gilt: H ist universell. i=0 αi xi Begründung: Es seine x, y ∈ U mit x , y, o.B.d.A. x0 , y0 . Ist hα (x) = hα (y) für α ∈ U, n prim Pr ⇒ so gilt: α0 (y0 − x0 ) ≡ i=1 αi (xi − yi )( mod n) Es gibt genau ein α0 mit d.h. Z/nZ Körper P α0 = (y0 − x0 )−1 ri=1 xi − yi ( mod n) Mit anderen Worten: Sind α1 , . . . , αr beliebig, so gibt es genau ein α0 (und damit genau ein α) mit hα (x) = hα (y). Da x, y fest gewählt sind, gibt es genau nr Möglichkeiten α zu wählen mit hα (x) = hα (y). Damit: {h ∈ H|h(x) = h(y)} nr 1 = r+1 = n kHk n Bemerkung: Für U = {0, 1, . . . , N − 1} und Primzahl n ≥ 2 interpretiere Schlüssel k ∈ U als n-äre Darstellung (k0 , k : 1, . . . , kr ) d.h. r X k= i=0 (wobei N ≤ n1+r gelten sollte) 30 ki ni 2.4 Wörterbücher und Hashing 1 3 16 18 2 17 20 23 3 16 5 17 6 23 4 18 20 16 17 23 18 20 Abbildung 2.10: ExtractMin aus einem Heap 31 2 Grundlegende Datenstrukturen 20 9 16 15 4 7 18 15 16 4 20 9 7 Abbildung 2.11: BUH B A- (linear) A- (double) + A (linear) A+ (double) 1 Abbildung 2.12: Offene Hashverfahren 32 3 Suchbäume und Skiplisten Geordnete Wörterbücher = Wörterbücher + totale Ordnung auf Schlüssel Unterstützte Operationen: • IsMember(k), Insert(o, k), Delete(k) • ClosestKey≤ (k), ClosestKey≥ (k): Gebe den größten (bzw. kleinsten) Schlüssel ≤ k (bzw. ≥ k), der sich im Wörterbuch befindet, zurück. Implementierungen von geordneten Wörterbüchern 1. Tabellen (geordnet nach Schlüsseln): • IsMember: O(log n) mittels binärer Suche • Insert, Delete: O(n) • ClosestKey≤ : O(log n) 2. Suchbäume • Interne Suchbäume: Schlüssel werden in inneren Knoten abgespeichert. • Externe Suchbäume: Schlüssel (Objekte) werden in den Böättern abgespeichert. • Beispiele: binäre Suchbäume, AVL-Bäume, (a, b)-Bäume, Rot-Schwarz-Bäume (BBäume) 3.1 Binäre Suchbäume Binärer Baum ist ein echter Binärbaum T, in dem jeder innere Knoten v ein Paar (o, k) des Wörterbuchs D enthält und • Für alle Schlüssel k0 im linken Teilbaum von v gilt: k0 ≤ k • Für alle Schlüssel k0 im rechten Teilbaum von v gilt: k0 ≥ k Algorithmus 3.1 auf der nächsten Seite zeigt die binäre Suche. Komplexität von Search: O(h(T)) im worst case. Komplexität der Operationen: • IsMember, ClosestKey≤ in O(h(T)) im worst case • Insert in O(h(T)) • Delete (mit Finden des Knotens mit Schlüssel k) 1. Search gibt Blatt zurück ⇒ Schlüssel k kommt in T nicht vor 2. Search gibt Knoten n mit einem Blatt als Kind zurück 33 3 Suchbäume und Skiplisten Algorithmus 11 S(k, v) 1: Eingabe: Schlüssel k und Knoten v eines binären Suchbaums T 2: Ausgabe: Knoten w des Teilbaumes Tv , so dass entweder w innerer Knoten mit Schlüssel k ist oder w Blatt ist, das Schlüssel k enthalten müsste, falls k im Baum enthalten wäre. 3: if v Blatt then 4: return v 5: end if 6: if k = key(v) then 7: return v 8: else 9: if k < key(v) then 10: return S(k, linkes Kind von v) 11: else 12: return S(k, rechtes Kind von v) 13: end if 14: end if 25 7 13 löschen umhängen 9 11 3. Search gibt inneren Knoten v zurück dessen Kinder ebenfalls innere Knoten sind. 25 25 7 5 9 13 9 13 5 11 11 34 Hänge min. in rechtem Teilbaum um oder max. in linkem Teilbaum 3.1 Binäre Suchbäume Komplexität: O(h(T)) (für Search) (Suchbaum durch Linkstruktur realisiert) ⇒ im schlechtesten Fall (z.B. füge n Schlüssel aufsteigend in T ein) h(T) = n ⇒ O(n) worst case. Average case Analyse „Wie hoch ist ein Suchbaum im Mittel?“. D.h E max dv abschätzen. v∈T X max dv ≤ log E 2 v∈T = log E max 2dv ≤ log E 2dv Jensen E max dv v∈T Definiere: g(kTk) = E v∈T hP v∈T g(ktk) = 1 + v∈T i 2dv . Dann gilt: 1X 2 · g(kTlinkes Kind von v k) + g(kTrechtes Kind von v k) n v∈T oder in n ausgedrückt: 1X 2(g(i − 1) + g(n − i)) g(n) = 1 + n n = 1+ 4 n (3.1) i=1 n−1 X g(i) (3.2) g(i) (3.3) i=0 n−1 X ⇒ ng(n) = n + 4 i=0 Aus (3.3) folgt weiter: n−2 X (n − 1)g(n − 1) = n − 1 + 4 g(i) i=0 Damit: ng(n) − (n − 1)g(n − 1) = n − (n − 1) + 4g(n − 1) ⇒ ng(n) = 1 + (n + 3)g(n − 1) ⇒ g(n) = g(n) = = .. . n+3 1 g(n − 1) + n n n+3 1 g(n − 1) + n n n+3 n+2 n+3 1 1 · g(n − 2) + · + n n−1 n n−1 n n−2 j n−1 X Y n + 3 − i 1 Yn+4−i = g(1) + n−i n n−i i=0 j=0 i=1 (n + 3)(n + 2)(n + 1) X 1 (n + 3)(n + 2)(n + 1)n + 4·3·2 n 4·3·2·1 n−1 ≤ j=0 (n + 3)(n + 2)(n + 1) = (n + 1) ≤ (n + 1)n3 ≤ 2n4 4·3·2 35 3 Suchbäume und Skiplisten Damit gilt: E max dv v∈T X ≤ log E 2dv = log g(kTk) v∈T 4 ≤ log 2n = 4 log n + 1 = O(log n) D.h. im Mittel hat jeder binäre Suchbaum nur logarithmische Höhe ⇒ IsMember, ClosestKey, Insert, Delete in O(log n) im average case. 3.2 AVL-Bäume (1962 von A-V und L eingeführt) Ziel: Begrenze die Höhe von binären Suchbäumen. Definition Es sei T ein binärer Suchbaum. 1. Ein Knoten von T heißt höhenbalanciert, wenn sich die Höhe der an seinen Kindern hängenden Teilbäume um höchstens 1 unterscheidetn. 2. T ist ein AVL-Baum, wenn jeder Knoten höhenbalanciert ist. √ h Satz 6. Ein AVL-Baum der Höhe h hat mindestens 1+2 5 Knoten. Beweis. (Induktion über h(T)) • (I.A.) – h(T) = 0: Einziger AVL-Baum der Höhe 0 ist Baum mit genau einem Knoten. Damit: √ 0 1 = 1+2 5 . √ 1 – h(T) = 1: AVL-Baum der Höhe 1 besitzt 3 Knoten. Damit: 1+2 5 ≤ 2 ≤ 3. • (I.S.) h(T) ≥ 2 : Es sei T ein AVL-Baum der Höhe h. Sei v Wurzel von T und seien Tl bzw. Tr die Teilbäume mit linkem bzw. rechtem Kind als Wurzel. Da T echter Binärbaum, gibt es Tl und Tr stets. Die Höhen von Tl und Tr sind mindestens h − 2, ein Baum hat jedoch die Höhe h − 1. Nach Definition sind Tl und Tr wieder AVL-Bäume. Damit ergibt sich: kTk = (I.V.) ≥ ≥ = = kTl k + kTr k + 1 √ !h−1 √ !h−2 1+ 5 1+ 5 + +1 2 2 √ !h−2 √ ! 1+ 5 1+ 5 +1 2 2 √ !h−2 √ !2 1+ 5 1+ 5 2 2 √ !h 1+ 5 2 ⇒ Ist ein geordnetes Wörterbuch der Größe n als AVL-Baum implementiert, so lässt sich IsMember mit Komplexität O(log n) realisieren. √ h (Begründung: 2n + 1 ≥ 1+2 5 ⇒ h ≤ O(log n)) 36 3.2 AVL-Bäume Komplexität von Insert Es sei T ein binärer Suchbaum. Sei x ein innerer Knoten in T und seien Tl und Tr die linken bzw. rechten Teilbäume von x. Balancierungsfaktor B(X) ist definiert als B(x) =de f h(Tl ) − h(Tr ) Für AVL-Bäume gilt stets −1 ≤ B ≤ 1. Im Folgenden werden AVL-Bäume so erweitert, dass in jedem Knoten immer der zugehörige Balancierungsfaktor abgespeichert wird. (Dazu benötigen wir zusätzlich zwei Bits pro Knoten). Beim Einfügen eines Schlüssels k wird zunächst wie bei binären Suchbäumen der Knoten gesucht, bei dem IsMember erfolglos abbricht. An dieser Stelle wird ein neuer Schlüssel mit Inhalt k eingefügt. Höhenbalancierung wird durch Einfügen nur dann verletzt, wenn: • in Knoten x mit B(x) = 1 wird T − l(x) um einen Level größer • in Knoten x mit B(x) = −1 wird Tr (x) um einen Level größer Es sei x de Knoten in T, bei dem auf jedem Pfad vom neu eingefügten inneren Knoten zur Wurzel das erste mal die Höhenbalancierung verlettzt wird. Damit gibt es im Wesentlichen die zwei folgenden Fälle in Abbildung 3.1: Weitere Fälle entstehen durch Vertauschung von links x -2 k1 y h x k2 -1 y A h -1 k2 A z h -2 k1 k3 -1 (-, k1) B C D h h+1 C (k1, k2) h h-1 B (k2, +) (k3, k2) (k2, +) (k1, k3) Abbildung 3.1: Insert bei einem AVL-Baum und rechts; diese können aber analog behandelt werden. Durch Umhängen (Rotation) von Teilbäumen kann Höhenbalancierung wiederhergestellt werden. Vergleiche Abbildung 3.2 auf der nächsten Seite Beispiel Abbildung 3.3 auf der nächsten Seite zeigt eine Doppelrotation nach dem Einfügen eines neuen Schlüssels. ⇒ Ist ein geordnetes Wörterbuch der Größe n als ein AVL-Baum implementiert, so lässt sich Insert mit Komplexität O(log n) realisieren. Begründung: Bestimmung der Einfügeposition kostet O(h(T)) Zeit. Bestimmen, ob und wo die Höhenbalancierung verloren geht k9ostet ebenfalls O(h(T)) Zeit. Rotation kostet O(1) Zeit. ⇒Insgesamt: O(h(T)) Gesamtkomplexität = (Satz 7) ⇒ O(log n) Zeit. 37 3 Suchbäume und Skiplisten y x k1 0 k2 x 0 z k3 k1 C y k2 h+1 h-1 C h B A h h h A (k1, k2) h (k3, k2) (k2, +) (-, k1) D B (-, k1) Rotiere (x, y) nach links (k1, k3) (k2, +) Rotiere (z, y) nach rechts Rotiere (x, z) nach links Abbildung 3.2: Rotation bei AVL-Bäumen Abbildung 3.3: Rotation bei AVL-Bäumen Komplexität von Delete Beim Löschen des Schlüssels k wird zunächst Knoten mit Schlüssel k gesucht und wie bei binären Suchbäumen üblich vorgegangen. Höhenbalancierung wird durch Löschen nur dann verletzt, wenn: • in Knoten x mit B(x) = 1 wird Tr (x) um ein Level kleiner • in Knoten x mit B(x) = −1 wird Tl (x) um ein Level kleiner O.B.d.A. betrachen wir nur den zweiten Fall. Es sei x der Knoten, bei dem zum ersten Mal die Höhenbalancierung verletzt wird. Damit gibt es im Wesentlichen folgende zwei Fälle, die in Abbildung 3.4 gezeigt sind. (weitere Fälle entstehen durch Vertauschung). Durch Umhängen Abbildung 3.4: Rotation bei AVL-Bäumen nach der Delete-Operation (Rotation) von Teilbäumen ergibt sich Abbildung 3.5. Abbildung 3.5: Rotation bei AVL-Bäumen ⇒ Ist ein geordnetes Wörterbuch der Größe n als ein AVL-Baum implementiert, so lässt sich Delete mit Komplexität O(log n) realisieren. Begrüngung: (siehe Insert) 3.3 (a, b)-Bäume (1982 von H und M eingeführt) 38 3.3 (a, b)-Bäume Idee: Innere Knoten können größeren Grad haben und Tupel speichern; Einträge sagen uns, in welchem Teilbaum wir weiter suchen müssen. Wir betrachten dazu allgemeine Suchbäume: • Objekte werden nur in Blättern abgespeichert (in jedem Blatt nur ein Objekt) • Hat innerer Knoten m Kinder, so nehält er m − 1 Schlüssel • Schlüssel im i-ten Teilbaum sind kleiner als die Schlüssel im (i + 1)-ten Teilbaum • Enthalte innerer Knoten die Schlüssel k1 < k2 < . . . < km−1 , so kann ein Objekt mit Schlüssel k ∈ (ki−1 ; ki ] (wobei k0 = −∞, km = ∞) nur im i-ten Teilbaum enthalten sein. Abbildung 3.6 verdeutlicht dies. Abbildung 3.6: Schlüssel im (a, b)-Baum Es seien a, b ∈ N mit a ≥ 2, b ≥ 2a − 1. Ein allgemeiner Suchbaum heißt (a, b)-Baum, falls gilt: • Alle Blätter befinden sich auf dem selben Level, • jeder innere Knoten hat maximal b Kinder, • die Wurzel hat mindestens 2 Kinder, • jeder innere Knoten, der nicht die Wurzel ist, hat mindestens a Kinder. Beispiel Abbildung ?? auf Seite ?? zeigt ein Beispiel. Abbildung 3.7: Beispiel eines (2, 3)-Baums l m Lemma 3.3.1. Ein (a, b)-Baum mit n Blättern hat mindestens die Höhe log2 n und maximal die Höhe m l loga n2 + 1. Beweis. des Lemmas l m • maximale Anzahl von Blättern in (a, b)-Baum der Höhe h : bh ⇒ n < bh ⇒ logb n ≤ n • minimale Anzahl von Blättern in (a, b)-Baum der Höhe h : 2ah−1 ⇒ n ≥ 2ah−1 ⇒ loga h−1 n 2 ≥ Komplexität von IsMember Bestimme rekursiv über die inneren Knoten den jeweils nächsten gültigen Teilbaum, bis Suche im Blatt erfolgreich bzw. erfolglos endet (verwende in inneren Knoten lineare Suche bei Linkstruktur). ⇒ Ist ein geordnetes Wörterbuch der Größe n als (a, b)-Baum implementiert (a, b fest), so lässt sich IsMember mit Komplexität O(log n) realisieren. 39 3 Suchbäume und Skiplisten Komplexität von Insert(o, k) Wie bei Suchbäumen üblich gelangen wir nach erfolgloser Suche in ein Blatt y mit k , key(y), o.B.d.A. k < key(y). Sei x Elter von y (x isr innerer Knoten). Dann gibt es Schlüssel kii−1 , ki , die in x gespeichert sind mit ki−1 < k < key(y) < ki . Hänge neues Blatt y0 mit Schlüssel k zwischen ki−1 und ki in x ein. Dabei können zwei Fälle auftreten: 1. Hat x immer noch maximal ≤ b Kinder, so sind wir fertig. 2. Hat x nun genau b + 1 Kinder, so verfahren wir wie folgt: Da b + 1 ≥ 2a, teilen wir x in zwei Knoten mit mindestens ≥ a Kinder auf und verschieben den überschüssigen Schlüssel an den Vorgänger von x, dies erhöht die Anzahl der Kinder vom Elter von x um 1; führe nun Verfahren rekursiv weiter, bis es abbricht oder Wurzel erreicht. Abbildung 3.8 verdeutlicht dies. Abbildung 3.8: Einfügen in einen (a, b)-Baum ⇒ Ist ein geordnetes Wörterbuch der Größe n als (a, b)-Baum implementiert (a und b fest), so lässt sich Insert(o, k) mit Komplexität O(log n) realisieren. Beispiel Abbildung 3.9 verdeutlicht dies. Abbildung 3.9: Einfügen in einen (a, b)-Baum Komplexität von Delete Nach erfolgreicher Suche sind wir in Blatt mit Schlüssel k gelandet und entfernen dieses Blatt samt zugehörigen Schlüssel im Vorgänger. Sei x Elterknoten vom entfernten Blatt. Es trefen folgende zwei Fälle auf: 1. Hat x immer noch mindestens a Kinder, so sind wir fertig. 2. Hat x nun a − 1 Kinder, so verfahren wir wie folgt: Verschmelze x mit einem seiner Nachbarknoten zu einem neuen Knoten x0 . • Hat x0 nun ≥ b + 1 ≥ 2a Kinder, so spalte x0 in zwei Knoten mit jeweils ≥ a Kinder auf und wir sind fertig. • Hat x0 nun ≤ b Kinder, so ist die Anzahl der Kinder am Elterknoten von x0 um 1 gesunken, führe Verfahren rekursiv fort, bis es abbricht oder Wurzel erreicht. ⇒ Ist ein geordnetes Wörterbuch der Größe n als (a, b)-Baum implementiert (a und b fest), so lässt sich Delete mit Komplexität O(log n) realisieren. 3.4 Splay Trees (1985 von S und T eingeführt) • „Selbstorganisierende binäre Suchbäume“ • besitzen keine explizite Regeln zur Höhenbalancierung, sttt dessen werden nur bestimmte Operationen ausgeführt, die Ismember, Insert und Delete mit amortisierter Komplexität O(log n) garantieren. 40 3.4 Splay Trees Splay Tree ist ein binärer Suchbaum T, auf dem die Splay-Operationen Zig-Zig, Zig-Zag und Zig ausgeführt werden: Abbildung 3.10 zeigt die drei Operationen. Abbildung 3.10: Operationen bei Splay Trees Ziel: Bringe einen inneren Knoten x durch eine Folge von Splay-Operationen in die Wurzel; eine solche Folge heißt Splaying von x. Komplexität des Splaying • Zig-Zig, Zig-Zag reduzieren Tiefe von x um 2 • Zig reduziert Tiefe von x um 1 ⇒ Splaying eines Knotens x der Tiefe d benötigt falls d ungerade ist. j k d 2 Zig-Zig oder Zig-Zag und einmal Zig, ⇒ Splaying benötigt O(d) Schritte (entspricht auch der Komplexität der Suche des Knotens) Wann führen wir Splaying durch? • IsMember(k): Wird Knoten x mit Schlüssel k gefunden, so führen wir Splaying von x aus; wenn nicht gefunden (erfolglose Suche), so führen wir Splaying des Elterknotens des Blattes aus, in dem Suche erfolglos endete. • Insert(o, k): Führe Splaying des neu eingeführten Knotens aus. • Delete(k): Führe Splaying von Elter(x) aus, wenn x der Knoten ist, der gelöscht wird. Ansonsten: IsMember, Insert, Delete wie bei binären Suchbäumen. Beispiel Abbildung 3.11 zeigt das Löschen eines Schlüssels. Abbildung 3.11: Löschen eines Schlüssels im Splay-Tree Komplexität von IsMember, Insert und Delete O(h(T)) im worst-case; d.h. O(n) im worstcase (füge Objekte in aufsteigender Reihenfolge der Schlüssel ein) Amortisierte Analyse (und statistische Optimalität) Betrachte Folge von m Einzeloperationen IsMember, Insert, Delete (gemischt). Es sei T ein Splay-Tree mit n Schlüsseln. Sei v ∈ T und f (v) die Anzahl der Zugriffe (Splaying) auf den Knoten v bei Ausführung der Operationenfolge. Definiere: X S(v) =de f f (w) Gewicht von v w∈T R(v) =de f R(T) =de f log (S(v)) X R(v) Rang von v v∈T 41 3 Suchbäume und Skiplisten Lemma 3.4.1 (A). Es sei ∆R(T) (bzw. ∆R(v)) die veränderung des Ranges von T (bzw. von v) bei Ausführung einer Operation Zig-Zig, Zig-Zag, Zig auf Knoten v. Dann gilt: 1. ∆R(T) ≤ 3∆R(v) − 2 für Zig-Zig, Zig-Zag 2. ∆R(T) ≤ 3∆R(v) für Zig Beweis. des Lemmas • Zig-Zig: Mit S(v) = f (v) + S(w) + S(z), wobei w und z Kinder von v sind, gilt 42