Dynamische Bäume

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