Effiziente Algorithmen und Datenstrukturen

Werbung
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) . . . .
ET(T, v) . .
PQ-S(A) . . . .
I(T, k) . . . . .
EM . . . .
BUH(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 ET(T, v)
1: werte v aus (bevor linkes Kind besucht wird)
2: if v innerer Knoten then
ET(T, linkes Kind von v)
3:
4: end if
5: werte v aus (bevor rechtes Kind besucht wird)
6: if v innerer Knoten then
ET(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] := EM;
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 (SS): 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 EM
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 (HS) 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: BUH 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 BUH(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 = BUH(S1 )
9: T2 = BUH(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: BUH 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
Herunterladen