Algorithmen Und Datenstrukturen
Algorithmen Und Datenstrukturen
Algorithmen und
Datenstrukturen
Grundlagen und probabilistische
Methoden für den Entwurf
und die Analyse
2. Auflage
Algorithmen und Datenstrukturen
Helmut Knebl
Algorithmen und
Datenstrukturen
Grundlagen und probabilistische
Methoden für den Entwurf
und die Analyse
2., aktualisierte Auflage
Helmut Knebl
Fakultät Informatik
Technische Hochschule Nürnberg
Georg Simon Ohm
Nürnberg, Deutschland
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detail-
lierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019, 2021
Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Jede Verwertung, die nicht
ausdrücklich vom Urheberrechtsgesetz zugelassen ist, bedarf der vorherigen Zustimmung des Verlags.
Das gilt insbesondere für Vervielfältigungen, Bearbeitungen, Übersetzungen, Mikroverfilmungen und die
Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Wiedergabe von allgemein beschreibenden Bezeichnungen, Marken, Unternehmensnamen etc. in diesem
Werk bedeutet nicht, dass diese frei durch jedermann benutzt werden dürfen. Die Berechtigung zur Benutzung
unterliegt, auch ohne gesonderten Hinweis hierzu, den Regeln des Markenrechts. Die Rechte des jeweiligen
Zeicheninhabers sind zu beachten.
Der Verlag, die Autoren und die Herausgeber gehen davon aus, dass die Angaben und Informationen in
diesem Werk zum Zeitpunkt der Veröffentlichung vollständig und korrekt sind. Weder der Verlag, noch
die Autoren oder die Herausgeber übernehmen, ausdrücklich oder implizit, Gewähr für den Inhalt des
Werkes, etwaige Fehler oder Äußerungen. Der Verlag bleibt im Hinblick auf geografische Zuordnungen und
Gebietsbezeichnungen in veröffentlichten Karten und Institutionsadressen neutral.
Für die zweite Auflage wurde das Buch gründlich durchgesehen. Die Darstel-
lung wurde an vielen Stellen verbessert, Ungenauigkeiten und Fehler wurden
korrigiert. Die Struktur des Buches wurde im Wesentlichen beibehalten. Das
Mastertheorem wird jetzt in einem separaten Abschnitt dargestellt und der
komplette Beweis wird ausgeführt.
Mit der Überarbeitung von Algorithmen und Datenstrukturen erfolgte die
Übersetzung ins Englische. Die englischsprachige Ausgabe Algorithms and
”
Data Structures“ ist bei Springer International Publishing erschienen (siehe
[Knebl20]).
Ich danke meinen Lesern für Hinweise und Frau Sybille Thelen bei Springer
Vieweg für die angenehme Zusammenarbeit.
1. Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1 Korrektheit von Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 Laufzeit von Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2.1 Explizite Formeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2.2 O–Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3 Lineare Differenzengleichungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.3.1 Lineare Differenzengleichungen erster Ordnung . . . . . . . 14
1.3.2 Fibonacci-Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4 Die Mastermethode für Rekursionsgleichungen . . . . . . . . . . . . . 25
1.5 Entwurfsmethoden für Algorithmen . . . . . . . . . . . . . . . . . . . . . . . 32
1.5.1 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.5.2 Divide-and-Conquer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
1.5.3 Greedy-Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.5.4 Dynamisches Programmieren . . . . . . . . . . . . . . . . . . . . . . 41
1.5.5 Branch-and-Bound mit Backtracking . . . . . . . . . . . . . . . . 49
1.6 Probabilistische Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.6.1 Vergleich von Polynomen . . . . . . . . . . . . . . . . . . . . . . . . . . 59
1.6.2 Verifikation der Identität großer Zahlen . . . . . . . . . . . . . 62
1.6.3 Vergleich mulitivariater Polynome . . . . . . . . . . . . . . . . . . 64
1.6.4 Zufallszahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
1.7 Pseudocode für Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
1.8 Lehrbücher zu Algorithmen und Datenstrukturen . . . . . . . . . . . 70
3. Hashverfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
3.1 Grundlegende Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
3.2 Hashfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
3.2.1 Division und Multiplikation . . . . . . . . . . . . . . . . . . . . . . . . 113
3.2.2 Universelle Familien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
3.3 Kollisionsauflösung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.3.1 Kollisionsauflösung durch Verkettungen . . . . . . . . . . . . . 119
3.3.2 Offene Adressierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
3.4 Analyse der Hashverfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
3.4.1 Verkettungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
3.4.2 Offene Adressierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
4. Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
4.1 Wurzelbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
4.2 Binäre Suchbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
4.2.1 Suchen und Einfügen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
4.2.2 Löschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
4.3 Ausgeglichene Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
4.3.1 Einfügen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
4.3.2 Löschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
4.4 Probabilistische binäre Suchbäume . . . . . . . . . . . . . . . . . . . . . . . . 156
4.4.1 Die Datenstruktur Treap . . . . . . . . . . . . . . . . . . . . . . . . . . 158
4.4.2 Suchen, Einfügen und Löschen in Treaps . . . . . . . . . . . . 159
4.4.3 Treaps mit zufälligen Prioritäten . . . . . . . . . . . . . . . . . . . 160
4.5 B-Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
4.5.1 Pfadlängen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
4.5.2 Suchen und Einfügen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
4.5.3 Löschen eines Elementes . . . . . . . . . . . . . . . . . . . . . . . . . . 171
4.6 Codebäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
4.6.1 Eindeutig decodierbare Codes . . . . . . . . . . . . . . . . . . . . . . 176
4.6.2 Huffman-Codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
4.6.3 Arithmetische Codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
4.6.4 Lempel-Ziv-Codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
5. Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
5.1 Modellierung von Problemen durch Graphen . . . . . . . . . . . . . . . 215
5.2 Grundlegende Definitionen und Eigenschaften . . . . . . . . . . . . . . 220
5.3 Darstellung von Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Inhaltsverzeichnis XIII
A. Wahrscheinlichkeitsrechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
A.1 Endliche Wahrscheinlichkeitsräume und Zufallsvariable . . . . . . 319
A.2 Spezielle diskrete Verteilungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Symbole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
1. Einleitung
Ein Algorithmus stellt eine Lösung für ein Berechnungsproblem bereit. Ein
Beispiel für ein Berechnungsproblem ist die Berechnung des Produkts von
zwei Zahlen. Das wichtigste Merkmal eines Algorithmus ist, dass er korrekt
arbeitet. Ein mathematischer Beweis, der die Korrektheit eines Algorithmus
zeigt, gibt maximales Vertrauen in seine Korrektheit. Die Methode der Verifi-
kation geht noch einen Schritt weiter. Sie beweist nicht nur, dass ein Algorith-
mus korrekt ist, sie beweist sogar, dass eine Implementierung des Algorithmus
in einer bestimmten Programmiersprache korrekt ist.
Die Laufzeit ist nach der Korrektheit das zweitwichtigste Merkmal eines
Algorithmus. Obwohl es oft einfach ist, die Anzahl der Rechenoperationen
bei einer festen Eingabe zu zählen, erfordert die Berechnung der Laufzeit im
schlechtesten Fall und der durchschnittlichen Laufzeit erheblichen Aufwand.
Den Durchschnitt bilden wir dabei über alle Eingaben einer festen Größe.
Wir behandeln für die Laufzeitanalyse der Algorithmen notwendige mathe-
matische Methoden wie lineare Differenzengleichungen.
Orthogonal zur Einteilung der Algorithmen nach Problemstellungen – wie
sie in diesem Buch in den Kapiteln 2 – 6 vorgenommen ist – kann man Al-
gorithmen nach Algorithmentypen oder Entwurfsmethoden für Algorithmen
einteilen. In diesem Kapitel besprechen wir die Entwurfsmethoden bezie-
hungsweise Algorithmentypen Rekursion, Greedy-Algorithmen, Divide-and-
Conquer, dynamisches Programmieren und Branch-and-Bound. Im Anschluss
daran führen wir probabilistische Algorithmen ein. Probabilistische Algorith-
men haben sich in den letzten Jahren ein weites Anwendungsfeld erobert. In
diesem Kapitel studieren wir einen Monte-Carlo-Algorithmus zum Vergleich
von Polynomen und in jedem der nachfolgenden Kapitel 2 – 6 lösen wir eine
Problemstellung auch durch einen probabilistischen Algorithmus.
Das Buch behandelt viele konkrete Algorithmen. Eine für die theoretische
Betrachtung des Gebiets unerlässliche Präzisierung des Algorithmusbegriffs
ist hier nicht notwendig. Die Formulierung der Algorithmen erfolgt durch
Pseudocode, der sich an gängigen Programmiersprachen, wie zum Beispiel
Java, orientiert und die wichtigsten Elemente einer höheren Programmierspra-
che enthält. Die Darstellung durch Pseudocode abstrahiert von den Details
einer Programmiersprache. Sie ist aber hinreichend präzise, um Überlegungen
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021
H. Knebl, Algorithmen und Datenstrukturen,
https://doi.org/10.1007/978-3-658-32714-9_1
2 1. Einleitung
Ein Algorithmus ist korrekt, wenn er in Bezug auf eine gegebene Spezifikati-
on korrekt arbeitet. Algorithmen operieren auf Daten. Die Spezifikation muss
demzufolge den Zustand dieser Daten vor Ausführung des Algorithmus – die
Vorbedingung – und den erwünschten Zustand nach Ausführung des Algorith-
mus – die Nachbedingung – hinreichend genau definieren.
Dieses Vorgehen erläutern wir genauer mithilfe des Algorithmus Sortieren
durch Auswählen – SelectionSort.
SelectionSort soll ein Array a[1..n] sortieren. Vorbedingung ist, dass wir
auf die Elemente in a[1..n] den <–Operator anwenden können. Die Nachbe-
dingung lautet a[1..n] ist sortiert, d. h. a[1] ≤ a[2] ≤ . . . ≤ a[n].
Die Idee bei Sortieren durch Auswählen besteht darin, wie der Name
vermuten lässt, das kleinste Element in a[1..n] zu suchen. Angenommen, es
befindet sich an der Position k, dann vertauschen wir a[1] mit a[k] und setzen
das Verfahren rekursiv fort mit a[2..n].
Algorithmus 1.1.
SelectionSort(item a[1..n])
1 index i, j, k; item m
2 for i ← 1 to n − 1 do
3 k ← i, m ← a[i]
4 for j ← i + 1 to n do
5 if a[j] < m
6 then k ← j, m ← a[j]
7 exchange a[i] and a[k]
Der Algorithmus SelectionSort implementiert die Idee von oben iterativ
mithilfe von zwei for-Schleifen.1 Wir zeigen jetzt durch Induktion nach den
Schleifenparametern, dass der Algorithmus korrekt ist. Die Aufgabe der inne-
ren Schleife ist es, das Minimum von a[i..n] zu berechnen. Genauer gilt nach
jeder Iteration der Schleife:
Diese Bedingung bezeichnen wir als Invariante der Schleife. Die Bedingung
ist zu prüfen, nachdem die letzte Anweisung der Schleife ausgeführt ist. Im
obigen Algorithmus ist dies die Zeile 6. Die Aussage folgt durch vollständige
Induktion nach j.
Für die äußere Schleife gilt nach jeder Iteration:
1
Auf die hier verwendete Notation zur Formulierung von Algorithmen gehen wir
im Abschnitt 1.7 genauer ein.
1.1 Korrektheit von Algorithmen 3
Algorithmus 1.2.
int Col(int n)
1 while n ̸= 1 do
2 if n mod 2 = 0
3 then n ← n div 2
4 else n ← (3n + 1) div 2
5 return 1
Es wird vermutet, dass Col für jeden Aufrufparameter n ∈ N terminiert.
Diese Vermutung wird als Collatz4 Vermutung bezeichnet und ist seit über
60 Jahren ungelöst.
Satz 1.4. M terminiert für alle n ≤ 101 mit dem Rückgabewert 91.
Beweis. Wir zeigen zunächst, dass für n mit 90 ≤ n ≤ 100 die Behauptung
des Satzes gilt. Für n ≥ 90 ist n + 11 ≥ 101. Deshalb folgt
M(n) = M2 (n + 11) = . . .
= Mk+1 (n + 11k) = Mk (M(n + 11k)) = Mk (91) = 91
4
Lothar Collatz (1910 – 1990) war ein deutscher Mathematiker.
5
John McCarthy (1927 – 2011) war ein amerikanischer Mathematiker.
1.1 Korrektheit von Algorithmen 5
berechnen, betrachten wir zunächst die Suche nach dem Minimum in einem
Array.
Algorithmus 1.7.
item Min(item a[1..n])
1 index i, item m
2 m ← a[1]
3 for i ← 2 to n do
4 if a[i] < m
5 then m ← a[i]
6 return m
Wir sind jetzt an der durchschnittlichen Anzahl an der Ausführungen von
Zeile 5 interessiert. Ein Element a[i], für das Zeile 5 ausgeführt wird, heißt
Zwischenminimum.
Oft sind rekursive Funktionen einfacher zu analysieren. Bei einer rekur-
siven Funktion erhalten wir eine rekursive Gleichung für die Laufzeit der
Funktion. Deshalb programmieren wir die Minimumsuche rekursiv.
Algorithmus 1.8.
item MinRec(item a[1..n])
1 item m
2 if n > 1
3 then m ← MinRec(a[1..n − 1])
4 if m > a[n]
5 then m ← a[n]
6 return m
7 return a[1]
Sei xn die Anzahl der durchschnittlichen Ausführungen von Zeile 5 in
MinRec. Es gilt an = xn . Die Zeile 5 von MinRec wird genau dann aus-
geführt, wenn das kleinste Element an der Stelle n steht. Dieser Fall tritt mit
Wahrscheinlichkeit 1/n ein, denn es gibt (n−1)! viele Anordnungen, bei denen
das kleinste Element an der Stelle n steht und es gibt n! viele Anordnungen
insgesamt. Mit Wahrscheinlichkeit 1/n ist die Anzahl der Ausführungen von
Zeile 5 gleich der Anzahl der Ausführungen von Zeile 5 in MinRec(a[1..n − 1])
plus 1 und mit Wahrscheinlichkeit 1− 1/n gleich der Anzahl der Ausführungen
von Zeile 5 in MinRec(a[1..n−1]). Für xn erhalten wir die folgende Gleichung.
( )
1 1 1
x1 = 0, xn = (xn−1 + 1) + 1 − xn−1 = xn−1 + , n ≥ 2.
n n n
1 1 1
xn = xn−1 + = xn−2 + + = ...
n n−1 n
1 1 1
= x1 + + . . . + + = Hn − 1.
2 n−1 n
Hn ist die n–te harmonische Zahl (Definition B.4).12
Wir halten das Ergebnis der vorangehenden Rechnung im folgenden Lem-
ma fest.
Lemma 1.9. Die durchschnittliche Anzahl der Zwischenminima in einem
Array a[1..n] der Länge n ist Hn − 1.
Satz 1.10. Für die durchschnittliche Anzahl ã6 der Ausführungen von Zeile
6 in Algorithmus 1.1 gilt
∑
n−1 ∑
n
ã6 = (Hn−i+1 − 1) = Hi − (n − 1)
i=1 i=2
= (n + 1)Hn − n − 1 − (n − 1) = (n + 1)Hn − 2n.
Bei der Analyse von Algorithmus 1.1 haben wir gezählt, wie oft die einzel-
nen Zeilen im schlechtesten Fall (worst case) und im Durchschnitt (average
case) ausgeführt werden.
Die Anzahl der Operationen insgesamt erhalten wir, indem wir die Opera-
tionen einer Zeile multipliziert mit der Anzahl der Ausführungen der Zeile für
alle Zeilen aufsummieren. Unter einer Operation verstehen wir eine Elemen-
taroperation eines Rechners, auf dem wir den Algorithmus ausführen. Wir
gewichten jede Operation bei der Ermittlung der gesamten Anzahl der Ope-
rationen mit ihrer Laufzeit in Zeiteinheiten. Dadurch erhalten die Laufzeit
des Algorithmus in Zeiteinheiten.
Die Berechnung der Laufzeit eines Algorithmus orientiert sich an den
Grundkonstrukten Sequenz, Schleife und Verzweigung, aus denen ein Algo-
rithmus zusammengesetzt ist.
Die Laufzeit einer Sequenz von Anweisungen ist die Summe der Laufzeiten
der Anweisungen, die in der Sequenz auftreten.
Bei der Berechnung der Laufzeit für eine Schleife addieren wir die Laufzei-
ten für die einzelnen Schleifendurchgänge und dazu noch die Laufzeit für die
12
Die Approximation von Hn durch log2 (n) definiert eine geschlossene Form für
Hn (Anhang B (F.1)).
1.2 Laufzeit von Algorithmen 9
S : J −→ N,
S(I) := Anzahl der Operationen (oder die Anzahl der Zeiteinheiten) die A
zur Lösung von I benötigt.
1. Die Laufzeit oder (Zeit-)Komplexität von A (im schlechtesten Fall) ist
definiert durch
T : N −→ N, T (n) := max S(I).
I∈Jn
ganzen Zahlen nur durch den verfügbaren Speicher des Rechners beschränkt,
so hängt der Aufwand für eine Operation mit ganzen Zahlen von der Länge
der Zahlen ab. Wir können den Aufwand nicht mehr als konstant annehmen.
Wenn wir die Anzahl der n–elementigen Teilmengen von I mit n! multiplizie-
ren, erhalten wir die Anzahl der Eingaben von der Größe n.
Obwohl die Definition der Laufzeit und der durchschnittlichen Laufzeit
von allen Eingaben der Größe n abhängt, war es möglich Formeln für die
Laufzeit im schlechtesten Fall und für die durchschnittliche Laufzeit aufzu-
stellen. Mit einem Rechner ist es möglich die Laufzeit, die für die Berechnung
einer Instanz notwendig ist, einfach durch Ausführung des Algorithmus zu be-
stimmen, aber es ist nicht möglich, Formeln für die Laufzeit im schlechtesten
Fall und für die durchschnittliche Laufzeit zu berechnen.
Bemerkung. Analog zur Zeit-Komplexität kann man die Speicher-Komplexität
von A definieren. Für die Algorithmen, die wir untersuchen, spielt die
Speicher-Komplexität keine große Rolle. Die Algorithmen in den Kapiteln 2 –
4 kommen im Wesentlichen mit Speicher konstanter Größe aus. Der Speicher-
verbrauch der Algorithmen für Graphen ist linear in der Größe der Eingabe.
1.2.2 O–Notation
f, g : D −→ R≥0
Dies bedeutet, dass es nur endlich viele Paare (m, n) ∈ D gibt, die die
Ungleichung nicht erfüllen.
Beachte, 1 = O(nm) für D = N × N, und 1 ̸= O(nm) für D = N0 × N0 .
Wir werden die verallgemeinerte O–Notation in den Kapiteln 5 und 6
anwenden. Die Laufzeit T (n, m) von Graphalgorithmen hängt ab von n,
der Anzahl der Knoten, und von m, der Anzahl der Kanten des Graphen.
Satz 1.13. Sei f (n) ̸= 0 für n ∈ N. Dann gilt:
( )
g(n)
g(n) = O(f (n)) genau dann, wenn beschränkt ist.
f (n) n∈N
Beweis. Es gilt g(n) = O(f (n)) genau dann, wenn es ein c ∈ N gibt mit
g(n)
f (n) ≤ c für fast alle n ∈ N, d.
(
h. für alle bis auf endlich viele Ausnahmen.
)
g(n)
Dies ist äquivalent dazu, dass f (n) beschränkt ist. 2
n∈N
Bemerkung. Zur Entscheidung der Konvergenz einer Folge gibt die Analysis
Hilfsmittel. Die Konvergenz einer Folge impliziert die Beschränktheit der Fol-
ge (siehe
( [AmannEscher02,
) Kap. II.1]). Also folgt g(n) = O(f (n)), falls die
g(n)
Folge f (n) konvergiert. Insbesondere gilt:
n∈N
12 1. Einleitung
g(n)
g(n) = O(f (n)) und f (n) = O(g(n)), wenn lim = c, c ̸= 0.
f (n)
n→∞
g(n)
g(n) = O(f (n)) und f (n) ̸= O(g(n)), wenn lim = 0.
n→∞ f (n)
g(n)
f (n) = O(g(n)) und g(n) ̸= O(f (n)), wenn lim = ∞.
n→∞ f (n)
loga (n)k mk
lim = lim = 0 (nach Punkt 1).
n→∞ nℓ m→∞ (aℓ )m
∑k 2
Beispiele: Sei P (x) = aj xj , aj ∈ R und ak > 0.
j=0
( )
1. Es gilt P (n) = O(nk ) und nk = O(P (n)), denn Pn(n)k konvergiert
n∈N
gegen ak . Der Grad eines Polynoms bestimmt sein asymptotisches Wachs-
tum. ∑l
2. Sei Q(x) = j=0 aj xj , aj ∈ R, al > 0 und k < l.
Dann gilt P (n) = O(Q(n)) und Q(n) ̸= O(P (n)), denn
∑
P (n)/Q(n) = nk−l kj=0 aj nj−k/∑l a nj−l konvergiert gegen 0.
j=0 j ( )
3. Sei a ∈ R, a > 1. P (n) = O(an ), an ̸= O(P (n)), denn Pa(n) n
n∈N
konvergiert gegen 0. (1) (1)
4. Sei i ≤ k
( i ( 1 )) und a 0 = . . . = a i−1 = 0, a i > 0. P n = O ni , denn
n P n n∈N konvergiert gegen ai .
5. Sei
{1 {1
, falls n ungerade, und , falls n gerade, und
f (n) = n g(n) = n
n, falls n gerade. n, falls n ungerade.
f (n) g(n)
Dann ist weder g(n) noch f (n) beschränkt, d. h. f ̸= O(g) und g ̸= O(f ).
Bei der Berechnung des asymptotischen Wachstums der Laufzeit eines
Algorithmus folgen wir seiner Konstruktion aus Sequenzen, Verzweigungen
und Schleifen (Seite 8). Insbesondere sind dabei die folgenden Rechenregeln
hilfreich: Sei gi = O(fi ), i = 1, 2. Dann gilt:
Diese Regeln folgen unmittelbar aus der Definition der O–Notation (Definiti-
on 1.12).
Wenn eine Analyse nur die Ordnung der Laufzeit T (n) eines Algorithmus
bestimmt, so ist dies eher von theoretischem Interesse. Für eine Anwendung
wäre eine genaue Angabe der Konstanten c und n0 sehr hilfreich und falls
die Größe der Eingaben < n0 ist, natürlich auch das Verhalten von T (n) für
kleine n. Deshalb sollte eine möglichst genaue Bestimmung von T (n) das Ziel
der Laufzeitanalyse sein.
14 1. Einleitung
Manchmal verwenden wir die O–Notation als bequeme Notation zur Anga-
be der Laufzeit eines Algorithmus. Wir schreiben zum Beispiel T (n) = O(n2 ),
obwohl wir das Polynom T (n) vom Grad 2 genau bestimmen könnten. So
brauchen wir die Koeffizienten des Polynoms nicht anzugeben.
Eine wichtige Klasse von Algorithmen sind die Algorithmen polynomialer
Laufzeit T (n). Mit der eingeführten O–Notation bedeutet dies T (n) = O(nk )
für ein k ∈ N. Einen Algorithmus mit polynomialer Laufzeit bezeichnen wir
auch als effizienten Algorithmus. Wenn der Grad des Polynoms, das die Lauf-
zeit angibt, groß ist, können wir einen Algorithmus polynomialer Laufzeit
für praktische Anwendungen nicht einsetzen. Nicht effizient sind Algorith-
men exponentieller Laufzeit. Wir sagen T (n) wächst exponentiell , wenn T (n)
ε
mindestens so schnell wächst wie f (n) = 2n , ε > 0.
Bei der Angabe des asymptotischen Wachstums der Laufzeit von Algo-
rithmen treten häufig die Funktionen log(log(n)), log(n), n, n log(n), n2 oder
2n auf. Wir sagen dann, der Algorithmus hat doppelt logarithmische, logarith-
mische, lineare, quasi-lineare, quadratische oder exponentielle Laufzeit .
Die Berechnung der Laufzeit von Algorithmen kann oft mithilfe von Differen-
zengleichungen, den diskreten Analoga zu Differentialgleichungen, erfolgen.
Wir behandeln deshalb Methoden zur Lösung von linearen Differenzenglei-
chungen, die wir später zur Berechnung der Laufzeit von Algorithmen anwen-
den werden. Über Differenzengleichungen gibt es eine umfangreiche Theorie
(siehe zum Beispiel [KelPet91] oder [Elaydi03]).
Gegeben seien Folgen reeller Zahlen (an )n∈N und (bn )n∈N und eine reelle Zahl
b. Eine lineare Differenzengleichung erster Ordnung ist definiert durch
x1 = b,
xn = an xn−1 + bn , n ≥ 2.
Gesucht ist die Folge xn , die Folgen an und bn heißen Koeffizienten der Glei-
chung. Wir setzen sie als bekannt voraus. Die Gleichung ist von erster Ord-
nung, weil xn nur von seinem Vorgänger xn−1 abhängt. Die Zahl b heißt
Anfangsbedingung der Gleichung.
Ein Rechner kann die Folge x1 , x2 , . . . berechnen. Wir sind aber an einer
Formel für xn interessiert, die erlaubt xn durch Einsetzen von n zu ermitteln.
Eine solche Formel bezeichnen wir als geschlossene Lösung der Differenzen-
gleichung.
1.3 Lineare Differenzengleichungen 15
xn = πn cn , n ≥ 1.
bn bn bn−1 ∑n
bi
cn = cn−1 + = cn−2 + + = ... = b + .
πn πn πn−1 π
i=2 i
Wir erhalten ( )
∑n
bi
x n = πn b+ , n ≥ 1,
π
i=2 i
x1 = b, xn = an xn−1 + bn , n ≥ 2,
besitzt ( )
∑n
bi
x n = πn b+ , n ≥ 1,
π
i=2 i
∏i
als Lösung, wobei πi = j=2 aj für 2 ≤ i ≤ n und π1 = 1 gilt.
Es ist möglich eine Lösung in geschlossener Form anzugeben, falls uns
dies für das Produkt und die Summe gelingt, die in der allgemeinen Lösung
auftreten.
Corollar 1.16. Die lineare Differenzengleichung mit konstanten Koeffizien-
ten a und b
x1 = c, xn = axn−1 + b für n ≥ 2
besitzt die Lösung
an−1 c + b a a−1−1 , falls a ̸= 1,
n−1
xn =
c + (n − 1)b, falls a = 1.
Beweis.
( )
∑
n
b ∑
n ∑
n−2
n−1
xn = a c+ = an−1 c + b an−i = an−1 c + b ai
i=2
ai−1 i=2 i=0
an−1 c + b a a−1−1 , falls a =
n−1
̸ 1 (Anhang B (F.5)),
=
c + (n − 1)b, falls a = 1.
2
Beispiele:
1. Gegeben sei die Gleichung x1 = 2, xn = 2xn−1 + 2n−2 , n ≥ 2.
Wir erhalten
∏
n
πn = 2 = 2n−1 .
i=2
( ) ( )
∑
n
2i−2 ∑
n
1
n−1 n−1
xn = 2 2+ = 2 2+ = 2n−2 (n + 3).
i=2
2i−1 i=2
2
wobei a, b und c konstant sind. Wir lösen die Gleichung und berechnen
∏
n
i+2 (n + 1)(n + 2)
πn = = .
i=2
i 6
( )
(n + 1)(n + 2) ∑n
6(ai + b)
xn = c+
6 i=2
(i + 1)(i + 2)
( n ( ))
c ∑ 2a − b b−a
= (n + 1)(n + 2) + −
6 i=2 (i + 2) (i + 1)
( )
c ∑1
n+2 ∑1
n+1
= (n + 1)(n + 2) + (2a − b) − (b − a)
6 i=4
i i=3
i
( )
2a − b 13a b c
= (n + 1)(n + 2) aHn+1 + − + + .
n+2 6 3 6
∑n
Hn = i=1 1i ist die n–te harmonische Zahl (Definition B.4).
Bei der Summierung von rationalen Funktionen kommt die Partialbruch-
zerlegung zum Einsatz (Anhang B (F.2)):
ai + b A B
= + .
(i + 1)(i + 2) (i + 1) (i + 2)
A + B = a und 2A + B = b.
Algorithmus 1.17.
HelloWorld(int n)
1 if n > 0
2 then for i ← 1 to 2 do
3 HelloWorld(n − 1)
4 print(hello, world)
Sei xn die Anzahl der Ausgaben von hello, world“. Dann gilt:
”
x1 = 1, xn = 2xn−1 + 1 für n ≥ 2.
Wir erhalten
∏
n
πn = 2 = 2n−1 .
i=2
( )
∑n
1 ∑
n
n−1
xn = 2 1+ i−1
= 2n−1 + 2n−i
i=2
2 i=2
∑
n−2
= 2n−1 + 2i = 2n−1 + 2n−1 − 1 = 2n − 1.
i=0
∑
n−1
x1 = 1, xn = (xi + n), n ≥ 2.
i=1
Dann gilt
1.3.2 Fibonacci-Zahlen
f0 = 0, f1 = 1,
fn = fn−1 + fn−2 , n ≥ 2.
..
.
Die Anzahl der Knoten in der i–ten Ebene ist gleich der i–ten Fibonacci-
Zahl fi , denn die Knoten der (i − 1)–ten Ebene kommen auch in der i–ten
Ebene (als gefüllte Knoten) vor und die Knoten der (i − 2)–ten Ebene sind
in der (i − 1)–ten Ebene gefüllt.
Unser Ziel ist es, einen Algorithmus zur Berechnung der Fibonacci-Zahlen
zu entwickeln und eine geschlossene Formel für die Fibonacci-Zahlen herzulei-
ten. Dazu betrachten wir etwas allgemeiner die lineare Differenzengleichung
zweiter Ordnung
x0 = u0 , x 1 = u1 ,
xn = vn xn−1 + wn xn−2 + bn , wn ̸= 0, n ≥ 2.
18
Leonardo da Pisa, auch Fibonacci genannt, war ein bedeutender Rechenmeister.
Er lebte in der zweiten Hälfte des 12. und ersten Hälfte des 13. Jahrhunderts in
Pisa.
20 1. Einleitung
Gesucht ist die Folge xn , die Folgen vn , wn und bn heißen Koeffizienten der
Gleichung. Wir nehmen sie als bekannt an.
Wir führen eine Differenzengleichung zweiter Ordnung auf ein System von
Differenzengleichungen erster Ordnung zurück:
X1 = B,
Xn = An Xn−1 + Bn , n ≥ 2,
wobei
( ) ( )
u0 xn−1
B= , Xn = , n ≥ 1,
u1 xn
( ) ( )
0 1 0
An = , Bn = , n ≥ 2.
wn vn bn
folgt für n ≥ 1
( ) ( ( ) ( )) ( )
fn−1 fn 0 0 01
= An−1 An = An−1 = An .
fn fn+1 1 1 11
( )
n−1 a11 a12
Wir berechnen A = mit dem folgenden Algorithmus 1.20 zum
a21 a22
Potenzieren und erhalten fn = a22 .
Dieser Algorithmus berechnet in einem Schritt (Al )2 und eventuell Al A.
Bei der Berechnung von (Al )2 sind die Terme fl−1 2
+ fl2 , fl−1 fl + fl fl+1 und
fl2 + fl+1
2
zu berechnen. Ersetzt man fl+1 durch fl−1 + fl , so sieht man, dass
die Quadrierung aufgrund der speziellen Gestalt von Al 3 Multiplikationen
2
(fl−1 , fl−1 fl , fl2 ) und 6 Additionen erfordert. Bei der Multiplikation mit A
ist die erste Zeile von Al A die zweite Zeile von Al und zweite Zeile von Al A
ist die Summe der beiden Zeilen von Al , folglich sind nur 2 Additionen von
ganzen Zahlen erforderlich.
Algorithmus zum Potenzieren. Wegen der Formel
{ n/2 2
n (A ) , falls n gerade ist, und
A =
(A(n−1)/2 )2 A sonst,
können wir bei der Berechnung von An in einem Schritt, der aus einer Qua-
drierung und höchstens einer Multiplikation besteht, den Exponenten n hal-
bieren. Daraus resultiert ein Algorithmus, der An in der Zeit O(log2 (n)) be-
rechnet.
Um die Rekursion zu vermeiden, betrachten wir die Binärentwicklung
Diese Formel erhalten wir auch, wenn wir die rekursive Formel von oben
n (n − 1)/2
expandieren, d. h. fortgesetzt auf A /2 und A anwenden. Wir setzen
die Formel in einen Algorithmus um:
22 1. Einleitung
Algorithmus 1.20.
matrix Power(int matrix A; bitString nl−1 . . . n0 )
1 int i; matrix B ← A
2 for i ← l − 2 downto 0 do
3 B ← B 2 · An i
4 return B
Die Anzahl der Iterationen der for-Schleife ist ⌊log2 (n)⌋, also gleich der
Bitlänge von n minus 1 (siehe Lemma B.3). Die Laufzeit des Algorithmus
Power ist damit logarithmisch im Exponenten n, also linear in |n|, der
Bitlänge von n. Dies gilt, falls der Aufwand für die arithmetische Operati-
on der Addition und Multiplikation konstant ist. Man beachte aber, dass
der Aufwand bei großen Zahlen von der Länge der Zahlen abhängt und des-
wegen nicht mehr konstant ist. Dies erhöht die Komplexität, wenn wir den
Algorithmus zur Berechnung von sehr großen Fibonacci-Zahlen einsetzen.
Die iterative Lösung, die fn berechnet, indem wir für alle Fibonacci-Zahlen
fi , i = 2 . . . n, nacheinander fi−1 zu fi−2 addieren, benötigt n − 1 viele Addi-
tionen. Die Laufzeit dieses Algorithmus ist linear in n, folglich exponentiell
in |n|. Er berechnet aber auch die ersten n Fibonacci-Zahlen.
fn = q n ,
λ1 gn + λ2 ĝn , λ1 , λ2 ∈ R,
λ1 g0 + λ2 ĝ0 = 0 ,
λ1 g1 + λ2 ĝ1 = 1 .
1
fn = √ (gn − ĝn ),
5
die Lösung zur Anfangsbedingung f0 = 0, f1 = 1. 2
Bemerkungen:
1. Mit der Beweismethode können wir eine geschlossene Form der Lösung
für homogene Differenzengleichungen mit konstanten Koeffizienten be-
rechnen (nicht nur für Gleichungen von der Ordnung 2).
2. Da | √15 ĝn | < 12 für n ≥ 0, folgt, dass
( )
gn
fn = round √
5
gilt. Der letzte Bruch konvergiert gegen 1. Hieraus folgt die Behauptung.
Wir diskutieren die Lösung von linearen Differenzengleichungen zweiter
Ordnung mit konstanten Koeffizienten, die nicht homogen sind. Dazu be-
trachten wir die rekursive Berechnung der Fibonacci-Zahlen – wie in der
definierenden Gleichung.
Algorithmus 1.23.
int Fib(int n)
1 if n = 0 oder n = 1
2 then return n
3 else return Fib(n − 1) + Fib(n − 2)
Mit xn bezeichnen wir die Anzahl der Aufrufe von Fib zur Berechnung
der n–ten Fibonacci-Zahl. Dann gilt
x0 = 1, x1 = 1,
xn = xn−1 + xn−2 + 1, n ≥ 2.
Wir berechnen eine spezielle Lösung der Gleichung durch den Lösungsan-
satz φn = c, c konstant, und erhalten c = 2c + 1 oder c = −1.
Die allgemeine Lösung xn berechnet sich aus der allgemeinen Lösung der
homogenen Gleichung und der speziellen Lösung φn = −1:
xn = λ1 gn + λ2 ĝn − 1, λ1 , λ2 ∈ R.
Insbesondere gilt für eine monoton wachsende Funktion r und für Kontrak-
tionen h und g mit h(x) ≤ g(x) für alle x ∈ R≥0 auch Th (x) ≤ Tg (x) für alle
x ∈ R≥0 .
Beweis. Die Formel erhalten wir durch Expandieren der rechten Seite.
Tg (x) = aTg (g(x)) + r(x)
= a(aTg (g 2 (x)) + r(g(x))) + r(x)
= a2 Tg (g 2 (x)) + ar(g(x)) + r(x)
..
.
= ak Tg (g k (x)) + ak−1 r(g k−1 (x)) + . . . + ar(g(x)) + r(x)
∑
k−1
= ak d + ai r(g i (x)).
i=0
Die Aussage über die Monotonie folgt aus der Lösungsformel (L0). 2
Lemma 1.26. Wir betrachten für n ∈ R≥0 die Rekursionsgleichung
(n)
(R1) T( b ) (n) = d für n < b und T( b ) (n) = aT( b ) + cnl für n ≥ b,
b
wobei a ≥ 1, c, d Konstanten aus R≥0 , b > 1 und l Konstanten aus N0 sind.
Sei q = a/bl . Dann gilt
⌊logb (n)⌋
da⌊logb (n)⌋ + cnl q q−1 −1 , falls bl ̸= a,
(L1) T( b ) (n) =
da⌊logb (n)⌋ + cnl ⌊log (n)⌋,
b falls bl = a.
Für die Ordnung von T( b ) (n) folgt
O(nl ), falls l > logb (a),
T( b ) (n) = O(nl logb (n)), falls l = logb (a),
O(nlogb (a) ), falls l < logb (a).
1.4 Die Mastermethode für Rekursionsgleichungen 27
∑k−1
Beweis. Sei n = i=−∞ ni bi = nk−1 . . . n1 n0 , n−1 . . . mit nk−1 ̸= 0 die b–
adische Entwicklung von n. Dann ist k = ⌊logb (⌊n⌋)⌋ + 1 = ⌊logb (n)⌋ + 1
(Lemma B.3) und bni = nk−1 . . . ni , ni−1 . . . n0 n−1 . . .. Mit Lemma 1.25 und
g(n) = n/b, r(n) = cnl und g i (n) = n/bi folgt
∑
k−2 ( n )l ∑(
k−2
a )i
T( b ) (n) = ak−1 d + c ai = ak−1 d + cnl
i=0
bi i=0
bl
dak−1 + cnl q q−1−1 , falls bl ̸= a,
k−1
=
dak−1 + cnl (k − 1), falls bl = a
(Anhang B (F.5)). Ersetzt man k durch ⌊logb (n)⌋ + 121 , so folgt die Formel
für T( b ) (n).
Wir zeigen noch die Aussagen über die Ordnung von T( b ) (n).
( k−1 )
Für q > 1, also für logb (a) > l, gilt O q q−1−1 = O(q k−1 ) und
( ) ( )
O(ak−1 + nl q k−1 ) = O alogb (n) + nl q logb (n) =22 O nlogb (a) + nl nlogb (q) =
O(nlogb (a) + nl nlogb (a)−l ) = O(nlogb (a) ).
Für q < 1, also für logb (a) < l, konvergiert q q−1−1 für k −→ ∞ gegen
k−1
Es folgt q q−1−1 = O(1) und O(a⌊logb (n)⌋ + cnl ) = O(nl + cnl ) = O(nl ).
k−1
1
1−q .
Für logb (a) = l gilt da⌊logb (n)⌋ + cnl ⌊logb (n)⌋ = O(nl + nl logb (n)) =
O(nl logb (n)). Dies zeigt die Behauptung des Lemmas. 2
⌊ ⌋ ⌈ ⌉
in a Teilinstanzen der Größe nb oder alternativ der Größe nb . Die Lösungen
für die a Teilinstanzen berechnen wir rekursiv. Die Laufzeit, um eine Instanz
aufzuteilen und die Ergebnisse der rekursiven Aufrufe dieser Teilinstanzen zu
kombinieren sei cnl . Die Funktion T⌊ b ⌋ ist rekursiv definiert durch
(⌊ n ⌋)
(R2) T⌊ b ⌋ (n) = d für n < b und T⌊ b ⌋ (n) = aT⌊ b ⌋ + cnl für n ≥ b,
b
wobei a ≥ 1, b > 1 und c, d, l Konstanten aus N0 sind.
Die Funktion T⌈ b ⌉ ist analog definiert, indem wir in T⌊ b ⌋ die Funktion
⌊ b ⌋ durch die Funktion ⌈ b ⌉ ersetzen.
(⌈ n ⌉)
(R3) T⌈ b ⌉ (n) = d für n < b und T⌈ b ⌉ (n) = aT⌈ b ⌉ + cnl für n ≥ b.
b
Sei n = nk−1 bk−1 + . . . + n1 b + n0 = nk−1 . . . n1 n0 , nk−1 ̸= 0, die b–adische
Entwicklung von n und q = a/bl .
Die Funktion S(n, λ), die wir nur mit λ = b/(b − 1) und λ = (b − 1)/b
verwenden, ist definiert durch
⌊logb (n)⌋
d′ a⌊logb (n)⌋ + c(λn)l q q−1 −1 , falls q ̸= 1,
S(n, λ) =
d′ a⌊logb (n)⌋ + c(λn)l ⌊log (n)⌋,
b falls q = 1.
=
dak−1 + c(λn)l (k − 1), falls bl = a
(Anhang B (F.5)). Ersetzt man k durch ⌊logb (n)⌋+1, so folgt S(n, (b − 1)/b) ≤
TU (n) ≤ T⌊ b ⌋ (n). Für die letzte Abschätzung verwenden wir Lemma 1.25.
Zum Nachweis der oberen Schranke für T⌈ b ⌉ (n) führen wir folgende Notation
ein ⌈⌈n⌉ ⌉
⌈n⌉ ⌈n⌉
b i−1
:= n und := für i ≥ 1.
b 0 b i b
Zunächst zeigen wir
⌈n⌉ ⌈n⌉ b n
= ≤ für i = 0, . . . , k − 2.
b i bi b − 1 bi
Sei n = nk−1 . . . n1 n0 , nk−1 ̸= 0, die b–adische Entwicklung von n. Für
i = 0, . . . , k − 2 zeigen wir
⌈n⌉ b n ⌈n⌉ n
≤ oder äquivalent dazu (b − 1) ≤ i−1 .
bi b − 1 bi bi b
Für i = 0 oder ni−1 . . . n0 = 0 gilt die Behauptung offensichtlich. Sei i ≥ 1
und ni−1 . . . n0 ̸= 0. Dann gilt für die linke Seite l und die rechte Seite r
l = nk−1 bk−i + nk−2 bk−i−1 + . . . + ni b + b −
(nk−1 bk−i−1 + nk−2 bk−i−2 + . . . + ni + 1)
≤ nk−1 bk−i + nk−2 bk−i−1 + . . . + ni b + (b − 1) − bk−i−1
= nk−1 bk−i + (nk−2 − 1)bk−i−1 + nk−3 bk−i−2 + . . . + ni b + (b − 1)
≤ nk−1 bk−i + nk−2 bk−i−1 + . . . ni b + ni−1
≤r
23
Man beachte die Indexverschiebung, da hier die Anfangsbedingung für m0 und
nicht für m1 gegeben ist.
30 1. Einleitung
c11 = m1 + m4 − m5 + m7 ,
c12 = m3 + m5 ,
(S.2)
c21 = m2 + m4 ,
c22 = m1 − m2 + m3 + m6 .
Die Berechnung von (cij ) i=1,2 erfolgt durch 7 Multiplikationen und 18 Addi-
j=1,2
tionen. Die Gleichungen lassen sich einfach durch Nachrechnen verifizieren.
(n) ( n )2
T (n) = 7 · T + 18 für n ≥ 2 und T (1) = 1.
2 2
Wir wenden jetzt Satz 1.28 an und erhalten
(( ) )
log2 (n)
7
T (n) = 7 log2 (n)
+ 6n 2
−1
4
( )
= nlog2 (7) + 6n2 nlog2 ( 4 )) − 1
7
wobei k = log2 (n) gilt. Die Anzahl der arithmetischen Operationen ist in der
Ordnung O(nlog2 (7) ) = O(n2.807 ).
Für beliebiges n setzen wir k = ⌈log2 (n)⌉. Wir ergänzen A und B zu
2k × 2k –Matrizen, indem wir die fehlenden Koeffizienten gleich 0 setzen. An-
schließend wenden wir den Algorithmus 1.29 an. Für die Anzahl T (n) der
arithmetischen Operationen erhalten wir
(( ) )
⌈log2 (n)⌉
⌈log2 (n)⌉ 7
T (n) = 7 + 6n 2
−1
4
( ( ) )
log2 (n)
7 7
≤ 7·7 log2 (n)
+ 6n 2
−1
4 4
( )
7 log2 ( 74 ))
= 7n log2 (7)
+ 6n 2
n −1
4
35 log2 (7)
= n − 6n2 .
2
Für beliebiges n erhalten wir eine Abschätzung für die Anzahl der arith-
metischen Operationen. Da die Laufzeit proportional zur Anzahl der arith-
metischen Operationen ist, erhalten wir für alle n ∈ N einen Algorithmus mit
einer Laufzeit in der Ordnung O(nlog2 (7) ) = O(n2.807 ).26
In [CopWin90] wird ein Algorithmus entwickelt, der das Problem der Mul-
tiplikation quadratischer Matrizen der Dimension n in einer Laufzeit in der
Ordnung O(n2.375477 ) löst. Seither wurden Optimierungen publiziert, die den
Exponenten ab der dritten Stelle nach dem Komma erniedrigen.
1.5.1 Rekursion
Die Rekursion ist ein mächtiges Hilfsmittel bei der Entwicklung von Algo-
rithmen, und es ist oft einfacher, für einen rekursiven Algorithmus einen
Korrektheitsbeweis zu führen und die Laufzeit zu berechnen. Dies erläutern
wir am Beispiel der Türme von Hanoi. Es handelt sich um ein Puzzle, das
Lucas27 zugesprochen wird. Die Türme von Hanoi erschienen 1883 als Spiel-
zeug unter dem Pseudonym N. Claus de Siam“, ein Anagramm von Lucas
” ”
d’Amiens“. Das Spiel besteht aus drei aufrecht stehenden Stäben A, B und
C und aus einem Turm. Der Turm besteht aus n gelochten Scheiben von un-
terschiedlichem Durchmesser, die der Größe nach geordnet, mit der größten
Scheibe unten und der kleinsten Scheibe oben, auf dem Stab A aufgereiht
sind, siehe Figur 1.3.
Ziel des Spiels ist es, den kompletten Turm von A nach B zu versetzen.
.
A B C
1. Ein Arbeitsschritt erlaubt eine Scheibe von einem Stab zu einem anderen
zu bewegen.
2. Dabei dürfen wir eine Scheibe nie auf einer kleineren Scheibe ablegen.
3. Der Stab C dient als Zwischenablage.
Es ist nicht unmittelbar klar, dass das Problem überhaupt eine Lösung
besitzt. Mit Rekursion lässt sich das Problem jedoch recht einfach lösen.
27
François Édouard Anatole Lucas (1842 – 1891) war ein französischer Mathema-
tiker und ist für seine Arbeiten aus der Zahlentheorie bekannt.
34 1. Einleitung
Algorithmus 1.30.
TowersOfHanoi(int n; rod A, B, C)
1 if n ≥ 1
2 then TowersOfHanoi(n − 1, A, C, B)
3 move disc n from A to B
4 TowersOfHanoi(n − 1, C, B, A)
Die Funktion TowersOfHanoi gibt eine Folge von Arbeitsschritten aus,
die zum Ziel führen. Dies beweisen wir durch vollständige Induktion nach n.
Wenn es nur eine Scheibe gibt, so erreichen wir das Ziel durch die Anweisung
move disc 1 from A to B“. Dabei halten wir die Nebenbedingung ein.
”
Wir nehmen nun an, dass TowersOfHanoi für n − 1 Scheiben eine Fol-
ge von Arbeitsschritten angibt, die zum Ziel führen und die Nebenbedin-
gung einhalten. Der Aufruf TowersOfHanoi(n − 1, A, C, B) bewegt die ers-
ten n − 1 Scheiben von A nach C. B dient als Zwischenablage. Dann bewe-
gen wir die größte Scheibe von A nach B. Anschließend bewegt der Aufruf
TowersOfHanoi(n − 1, C, B, A) die n − 1 Scheiben von C nach B. Jetzt dient
A als Zwischenablage. Alle Bewegungen halten die Nebenbedingung ein.
Da sich bei jedem rekursiven Aufruf n um 1 erniedrigt, tritt n = 0 ein, der
Algorithmus terminiert. Diese Überlegungen zeigen, dass unser Algorithmus
korrekt arbeitet.
Auch jetzt, nachdem wir wissen, dass das Problem der Türme von Ha-
noi eine Lösung besitzt, scheint es nicht ganz offensichtlich, wie eine Lösung
ohne Rekursion aussehen soll. Dies zeigt, dass die Rekursion eine mächtige
Methode beim Entwurf von Algorithmen bietet. Die Arbeitsschritte die wir
bei Verwendung der Rekursion erhalten, lassen sich aber auch iterativ erzie-
len (Übungen, Aufgabe 15).
Wir analysieren jetzt TowersOfHanoi. Die Anzahl der Arbeitsschritte ist ein-
fach zu beschreiben.
Sei xn die Anzahl der Arbeitsschritte, um einen Turm aus n Scheiben von
einem Stab zu einem anderen zu bewegen. Dann gilt
x1 = 1, xn = 2xn−1 + 1, n ≥ 2.
Rekursion liefert allerdings nicht immer eine effiziente Lösung. Dies zeigt
die Funktion, die die Fibonacci-Zahlen rekursiv berechnet, analog zur definie-
renden Gleichung (Algorithmus 1.23).
Rekursion findet Anwendung bei der Definition von Bäumen (Definition
4.1), beim Traversieren von Bäumen (Algorithmus 4.5) und allgemeiner beim
Traversieren von Graphen (Algorithmus 5.12) und im Algorithmus von Kar-
ger zur Berechnung eines minimalen Schnitts (Algorithmus 5.30). Im Algo-
rithmus Quicksort (Algorithmus 2.1), bei der Suche des k–kleinsten Elements
(Algorithmus 2.30) und bei der binären Suche (Algorithmus 2.28) wenden
1.5 Entwurfsmethoden für Algorithmen 35
1.5.2 Divide-and-Conquer
c 0 d1 c 1 d1
d1
d0 + d1
(c0 + c1 )·
(d0 + d1 )
c 0 d0 c 1 d0
d0
c0 + c1
c0 c1
Der Vorteil dieser Reduktion von 4 auf 3 Multiplikationen macht sich be-
sonders bezahlt, wenn wir ihn rekursiv ausnutzen. Dies geschieht im folgenden
Algorithmus 1.31.
Algorithmus 1.31.
int Karatsuba(int p, q)
1 if p < m and q < m
2 then return p · q
3 lp ←⌈len(p), lq ←⌉ len(q)
4 l ← max(lp , lq )/2
5 lowp ← p mod bl , lowq ← q mod bl
6 hip ← rshiftl (p), hiq ← rshiftl (q)
7 z0 ← Karatsuba(lowp , lowq )
8 z1 ← Karatsuba(hip , hiq )
9 z2 ← Karatsuba(lowp + hip , lowq + hiq )
10 return z1 b2l + (z2 − z1 − z0 )bl + z0
1.5 Entwurfsmethoden für Algorithmen 37
Die Rekursion wird abgebrochen, wenn beide Faktoren kleiner einer vor-
gegebenen Schranke m sind. In diesem Fall führt der Prozessor des Rechners
die Multiplikation unmittelbar aus. Bei den Summanden der Additionen han-
delt es sich um große Zahlen. Die Funktion len(x) gibt die Anzahl der Stellen
von x zurück und rshiftl (x) führte eine Schiebeoperation um l Stellen nach
rechts durch.
Sei M (n) die Anzahl der Multiplikationen, die zur Multiplikation von zwei
n–stelligen b–adischen Zahlen notwendig ist. Dann gilt
(⌈ n ⌉)
M (n) = d für n < m und M (n) = 3M .
2
Die Multiplikationen mit Potenzen von b sind Schiebeoperationen, die wir
nicht zu den Multiplikationen zählen. Mit Satz 1.28 folgt
Wir erhalten ein Verfahren, das mit wesentlich weniger als n2 Multiplikatio-
nen auskommt.
Es gibt ein Verfahren, das für sehr große Zahlen noch schneller ist. Es
benutzt die Methode der diskreten Fourier-Transformation aus der Analysis
und wurde von Schönhage29 und Strassen30 in [SchStr71] publiziert.
Die Algorithmen StrassenMult (Algorithmus 1.29), QuickSort (Algorith-
mus 2.1), QuickSelect (Algorithmus 2.30) und BinSearch (Algorithmus 2.28)
sowie der Algorithmus KKT − MST (Algorithmus 6.50) zur Berechnung eines
minimalen aufspannenden Baumes wenden das Divide-and-Conquer-Prinzip
an.
1.5.3 Greedy-Algorithmen
ai = (ti , pi ), i = 1, . . . , n.
ti ist der Abschlusstermin für ai und pi die Prämie, die gezahlt wird, wenn ai
bis zum Abschlusstermin ti fertiggestellt ist. Die Aufgaben sind sequenziell
zu bearbeiten. Jede Aufgabe ai braucht zu ihrer Bearbeitung eine Zeiteinheit.
Gesucht ist eine Reihenfolge für die Bearbeitung der Aufgaben, die die Ge-
samtprämie maximiert. In diesem Zusammenhang wird die Reihenfolge für
die Bearbeitung mit Schedule (Zeitablaufplan) bezeichnet.
29
Arnold Schönhage (1934 – ) ist ein deutscher Mathematiker und Informatiker.
30
Volker Strassen (1936 – ) ist ein deutscher Mathematiker.
38 1. Einleitung
Beispiel. Seien Aufgaben a = (1, 7), b = (1, 9), c = (2, 5), d = (2, 2) und
e = (3, 7) gegeben. Für den ersten Schritt kommen alle Aufgaben infrage.
Für den zweiten Schritt brauchen wir die Aufgaben, die Abschlusstermin 1
haben, nicht mehr zu betrachten. Für den dritten Schritt sind es die Aufga-
ben, die Abschlusstermin 1 und 2 haben.
Ein mögliches Vorgehen zur Lösung ist, in jedem Schritt eine Aufgabe
zu wählen, die gerade optimal erscheint. Wir wählen eine Aufgabe, die die
Prämie im Augenblick maximiert. Eine lokal optimale Lösung soll eine op-
timale Lösung ergeben. Mit diesem Vorgehen erhält man im vorangehenden
Beispiel den Schedule b, c, e.
Diese Strategie heißt Greedy-Strategie und ein Algorithmus, der eine der-
artige Strategie verfolgt, heißt Greedy-Algorithmus. Wir formalisieren die Si-
tuation, in der die Greedy-Strategie zum Erfolg führt.
Definition 1.32. Sei S eine endliche Menge und τ ⊂ P(S) eine Menge von
Teilmengen von S. (S, τ ) heißt Matroid , wenn τ ̸= ∅ und
1. τ ist abgeschlossen bezüglich der Teilmengenbildung, d. h. für A ⊂ B und
B ∈ τ ist auch A ∈ τ . Dies wird mit Vererbungs-Eigenschaft bezeichnet.
2. Für A, B ∈ τ , |A| < |B| gilt, es gibt ein x ∈ B \ A mit A ∪ {x} ∈ τ . Diese
Bedingung bezeichnen wir mit Austausch-Eigenschaft .
Diese Definition ist bereits in einer Arbeit von Whitney31 aus dem Jahr
1935 enthalten ([Whitney35]). Über Matroide und Greedy-Algorithmen gibt
es umfangreiche Untersuchungen, siehe zum Beispiel Schrijvers Monographie
Combinatorial Optimization“ ([Schrijver03]).
”
Wir betrachten für ein Matroid M = (S, τ ) eine Gewichtsfunktion
w : S −→ R>0 .
∑
Sei A ∈ τ . w(A) := a∈A w(a) heißt Gewicht von A.
Das Optimierungsproblem für (S, τ ) besteht nun darin, ein à ∈ τ zu finden
mit
w(Ã) = max{w(A) | A ∈ τ }.
à heißt eine optimale Lösung des Optimierungsproblems.
Diese Notation wollen wir jetzt für das Task-scheduling Problem anwen-
den. Sei S = {a1 , . . . an } die Menge der Aufgaben und
w : S −→ R>0 , ai 7−→ pi
die Gewichtsfunktion.
31
Hassler Whitney (1907 – 1989) war ein amerikanischer Mathematiker. Er ist
berühmt für seine Beiträge zur Algebraischen- und zur Differentialtopologie und
zur Differentialgeometrie.
1.5 Entwurfsmethoden für Algorithmen 39
τ ist so zu definieren, dass die Elemente von τ eine Lösung des Task-
scheduling Problems darstellen (eine maximale Lösung ist ja gesucht). Wir
sagen A ⊂ S heißt zulässig für das Task-scheduling Problem, wenn es eine Rei-
henfolge für die Bearbeitung der Tasks von A gibt, sodass wir alle Aufgaben
in A vor dem Abschlusstermin fertigstellen, wenn wir in dieser Reihenfolge
vorgehen. Wir bezeichnen eine solche Reihenfolge als zulässige Reihenfolge.
Sei τ die Menge der zulässigen Teilmengen von S. Wir haben jetzt dem Task-
scheduling Problem das Paar (S, τ ) zugeordnet. Wir werden gleich zeigen,
dass (S, τ ) ein Matroid ist. Dann folgt: Das Task-scheduling Problem ist ein
Optimierungsproblem für ein Matroid.
Beispiel. Figur 1.5 zeigt die Potenzmenge für die Menge von Tasks S =
{a, b, c, d, e} aus dem vorangehenden Beispiel. Zwischen den zulässigen Teil-
mengen unseres obigen Task-scheduling Problems ist die Teilmengenbezie-
hung angegeben. Sie bilden ein Matroid.
. . . . . {}
.
.
. {a}
. . {b}
. . {c}
. . {d}
. . {e}
.
. .
{a,.b} {a,.c} {a,.d} {a,.e} {b,.c} {b,.d} {b,.e} {c,.d} {c,.e} {d,.e}
.
. .
{a, b,
. c}{a, b,
. d}{a, c,
. d}{a, b,
. e}{a, c,
. e} {a, d,
. e} {b, c,. d}{b, c,. e}{b, d,
. e}{c, d,
. e}
. {a, b,.c, d} . {a, b,.c, e} . {a, b,.d, e} . {a, c,.d, e} . {b, c,.d, e}
. . . . . {a, b, c,. d, e}
Beispiel. Seien Aufgaben a = (1, 7), b = (1, 9), c = (2, 5), d = (2, 2) und
e = (3, 7) gegeben. Unser Algorithmus wählt zunächst b. Der Test mit a ist
negativ. Dann wählt er e und anschließend c. {b, e, c} ist zulässig und maxi-
mal. Der Schedule für {b, e, c} ist (b, c, e).
F ib = 0, 1, 1, 2, 3, 5, . . .
Dieser Algorithmus ist zugleich ein typisches Beispiel für die Anwendung der
Methode dynamisches Programmieren. Die rekursive Methode versagt hier,
32
Richard Bellman (1920 – 1984) war ein amerikanischer Mathematiker.
42 1. Einleitung
weil wir mit dieser Methode gemeinsame Teilprobleme immer wieder neu be-
rechnen (Algorithmus 1.23). Die Methode dynamisches Programmieren löst
jedes Teilproblem genau ein mal und speichert das Ergebnis in einer Tabelle.
Wenn wir nur die n–te Fibonacci-Zahl berechnen wollen, kann dies wesentlich
effizienter mit Algorithmus 1.20 erfolgen.
.19 32 23 14 7 4 5 11 3 1 6 15 41 7 12 61
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Fig. 1.6: Das RMQ-Problem.
ta [i, i] = i,
{
ta [i, j − 1], falls a[ta [i, j − 1]] ≤ a[ta [i + 1, j] gilt, und
ta [i, j] =
ta [i + 1, j] sonst.
Die Idee des dynamischen Programmierens besteht nun darin, die Werte
ta [i, j] nacheinander zu berechnen und in einer Tabelle abzuspeichern.
1.5 Entwurfsmethoden für Algorithmen 43
Für die Elemente in der Diagonale gilt ta [i, i] = i. Ausgehend von der Diago-
nale berechnen wir die Spalten in der Reihenfolge von unten nach oben und
von links nach rechts. Für die Berechnung von ta [i, j] benötigen wir die Werte
ta [i, j − 1] und ta [i + 1, j]. Diese Werte sind bereits berechnet. Sie befinden
sich in der Tabelle und wir lesen sie einfach aus.
Algorithmus 1.36.
compTab(item a[1..n])
1 index i, j
2 for j ← 1 to n do
3 ta [j, j] ← j
4 for i ← j − 1 to 1 do
5 if a[ta [i, j − 1]] < a[ta [i + 1; j]]
6 then ta [i, j] ← ta [i, j − 1]
7 else ta [i, j] ← ta [i + 1, j]
1 2 3 4 5 6
1 1 2 2 2 5 6
2 2 2 2 5 6
3 3 3 5 6
4 4 5 6
5 5 6
6 6
Wir optimieren das Verfahren, indem wir nicht für alle Indizes i < j
eine Nachschlagetabelle berechnen, sondern nur für Indizes i < j für deren
Abstand j − i = 2k − 1 gilt. Wir berechnen eine Nachschlagetabelle t+ a für
die Indexpaare
t+
a [i, 0] = i,
{
a [i, k − 1], falls a[ta [i, k − 1]] ≤ a[ta [i + 2
t+ , k − 1]], und
+ + k−1
t+
a [i, k] = +
ta [i + 2 k−1
, k − 1] sonst.
Die Idee ist jetzt wie oben, die Spalten der Tabelle
t+ a [1, 0] t+ a [1, 1] ...
ta+ [2, 0] t+ a [2, 1]
.. .. .. .
. . . ..
ta+ [n − 4, 0] t+a [n − 4, 1] t+a [n − 4, 2]
t+a [n − 3, 0] t+a [n − 3, 1] t+a [n − 3, 2]
t+a [n − 2, 0] t+a [n − 2, 1]
t+a [n − 1, 0] t+a [n − 1, 1]
t+a [n, 0]
aus der (k − 1)–ten Spalte. Da wir die Spalten nacheinander von links nach
rechts berechnen, befinden sich diese Werte bereits in der Tabelle t+ a und wir
können sie einfach auslesen.
1.5 Entwurfsmethoden für Algorithmen 45
1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 6 7 8 9 10
1 2 2 3 5 6 6 7 9 9
2 2 5 6 6 6 6 9
3 6 6 6
viele Einträge (Lemma B.16). Deshalb können wir die Berechnung von t+ a,
analog zu Algorithmus 1.36, mit einer Laufzeit in der Ordnung O(n log2 (n))
implementieren.
Satz 1.37. Eine RMQ-Anfrage für ein Array a der Länge n kann nach ei-
ner Vorverarbeitung mit Laufzeit O(n log2 (n)) mit Laufzeit O(1) beantwortet
werden.
Bemerkungen:
1. Im Abschnitt 6.1.3 geben wir, zunächst für ein Array, in dem sich zwei
aufeinander folgende Einträge nur um +1 oder −1 unterscheiden, und an-
schließend für ein beliebiges Array, einen Algorithmus mit linearer Lauf-
zeit für das RMQ-Problem an.
2. Das RMQ-Problem ist äquivalent zum LCA-Problem. Dieses besteht dar-
in, in einem Wurzelbaum den gemeinsamen Vorfahren von zwei Knoten,
der den größten Abstand zur Wurzel hat, zu ermitteln (Abschnitt 6.1.3).
Das LCA-Problem und damit auch das RMQ-Problem ist ein grundlegen-
des algorithmisches Problem, das intensiv studiert wurde. Einen Über-
blick gibt der Artikel Lowest common ancestors in trees“ von Farach-
”
Colton in [Kao16].
3. Range-Minimum-Queries besitzen Anwendungen in vielen Situationen,
zum Beispiel bei Dokument-Retrieval, komprimierten Suffix-Bäumen,
Lempel-Ziv-Komprimierung und der Text-Indizierung (Fischer: Compres-
sed range minimum queries in [Kao16].).
46 1. Einleitung
Die Editierdistanz. Ein weiteres Beispiel für die Entwurfsmethode des dy-
namischen Programmierens ist die Berechnung der Editierdistanz zweier Zei-
chenketten. Die Editierdistanz quantifiziert, wie ähnlich zwei Zeichenketten
sind.
Definition 1.38. Die Editierdistanz von zwei Zeichenketten ist die minimale
Anzahl von Operationen, um die eine Zeichenkette in die andere Zeichenkette
zu überführen. Als Operationen sind dabei Einfügen (I), Löschen (D) und
Ersetzen (R) eines Zeichens erlaubt. Das Übernehmen eines Zeichens (T)
zählen wir nicht bei der Ermittlung der Anzahl der Operationen.
Die Definition der Editierdistanz wird Levenshtein33 zugeschrieben
([Leven65]). In der Arbeit wird kein Algorithmus zur Berechnung angege-
ben. In unserer Darstellung beziehen wir uns auf die Arbeit [WagFis74], die
einen etwas allgemeineren Ansatz verfolgt.
Beispiel. Die Editierdistanz zwischen dabei“ und bereit“ ist vier. Die Ope-
” ”
rationen ersetze d durch b, ersetze a durch e, ersetze b durch r, behalte e,
behalte i und füge t abschließend ein führen dabei“ in bereit“ über.
” ”
Satz 1.39. Seien a1 . . . an und b1 . . . bm Zeichenketten. Die Editierdistanz
d(a1 . . . an , b1 . . . bm ), kurz d(n, m), berechnet sich rekursiv
Wir können die Formel aus Satz 1.39 ohne Mühe in eine rekursive Funk-
tion umsetzen. Obwohl es nur (n + 1)(m + 1) viele Teilprobleme gibt, ist
33
Vladimir Levenshtein (1935 – 1917) war ein russischer Mathematiker.
1.5 Entwurfsmethoden für Algorithmen 47
die Laufzeit dieser Funktion exponentiell. Dies liegt daran, dass die rekursi-
ve Funktion dieselben Distanzen immer wieder berechnet. Einen effizienteren
Algorithmus liefert das folgende Vorgehen.
nacheinander und speichern sie in einer Tabelle ab. Wir beginnen mit der Be-
rechnung der ersten Zeile und der ersten Spalte. Die folgenden Spalten berech-
nen wir von oben nach unten und von links nach rechts. Bei der Berechnung
von d(i, j) benötigen wir die Werte d(i, j − 1), d(i − 1, j) und d(i − 1, j − 1).
Diese Werte sind bereits berechnet. Sie befinden sich in der Tabelle und wir
können sie einfach auslesen. Die Tabelle besteht aus n Zeilen und m Spal-
ten. Ihre Berechnung erfolgt in der Zeit O(nm). Wir fassen das Ergebnis im
folgenden Satz zusammen.
Satz 1.40. Seien a = a1 . . . an und b = b1 . . . bm . Zeichenketten der Länge
n und m. Mit der Methode dynamisches Programmieren können wir die Edi-
tierdistanz von a zu b in der Zeit O(nm) berechnen.
Wir modifizieren nun den Algorithmus Dist so, dass er die Folge der Ope-
rationen, welche die Editierdistanz bestimmt, auch berechnet. Dazu speichern
48 1. Einleitung
wir, wie der Wert von d[i, j] zustande kommt. Wir verwenden eine zweite Ma-
trix op[0..n, 0..m], in der wir für jede Zelle als initialen Wert die leere Menge
setzen. Dann erweitern wir Dist durch
op[i, 0] = {D} , 0 ≤ i ≤ n,
op[0, j] = {I} , 0 ≤ j ≤ m,
op[i, j] ∪ {D} (lösche), falls d[i, j] = d[i − 1, j] + 1,
op[i, j] ∪ {I} (füge ein), falls d[i, j] = d[i, j − 1] + 1,
op[i, j] =
op[i, j] ∪ {R} (ersetze), falls d[i, j] = d[i − 1, j − 1] + 1,
op[i, j] ∪ {T } (übernehme), falls d[i, j] = d[i − 1, j − 1].
Nach Terminierung von Dist ist es möglich eine der Editierdistanz ent-
sprechende Folge von Operationen zu gewinnen, indem wir op von op[n, m]
bis zu op[0, 0] durchlaufen. Der Pfad, der dadurch entsteht, enthält die Ope-
rationen. Wir starten in op[n, m]. Der nächste Knoten im Pfad ist durch die
Wahl eines Eintrags aus op[i, j] bestimmt. Als nächster Knoten ist möglich
ein Teilbaum mit Wurzel w keine Lösung enthält, so setzen wir die Suche im
Vaterknoten von w fort; es erfolgt ein Rückwärtsschritt (Backtracking). Wir
erläutern Branch-and-Bound mit Backtracking anhand des 8-Damenproblems
und des Rucksackproblems genauer.
.
Fig. 1.7: Eine Lösung des 8-Damenproblems.
Steht eine Dame auf einem Feld F , so erlauben es die Regeln nicht, die
zweite Dame auf einem Feld zu positionieren, das in der Zeile, der Spalte oder
einer der beiden Diagonalen enthalten ist, die durch das Feld F gehen.
Wir müssen die Damen deshalb in verschiedenen Zeilen positionieren. Ei-
ne Lösung des Problems ist demnach durch die Angabe der Spalte für jede
Zeile festgelegt, mithin ist sie ein 8-Tupel (q1 , . . . , q8 ) ∈ {1, . . . , 8}8 , wobei
qj die Spalte für die j–te Zeile angibt. Zusätzlich muss (q1 , . . . , q8 ) noch die
Bedingung der gegenseitigen Nichtbedrohung“ erfüllen.
”
Da zwei Damen in verschiedenen Spalten zu positionieren sind, muss für
eine Lösung (q1 , . . . , q8 ) auch qi ̸= qj für i ̸= j gelten. Darum handelt es sich
bei (q1 , . . . , q8 ) um eine Permutation (π(1), . . . , π(8)). Wir stellen die Menge
aller Permutationen auf (1, . . . , 8) durch einen Baum mit 9 Ebenen dar, siehe
Figur 1.8. Jeder Knoten in der Ebene i hat 8 − i viele Nachfolger. Folglich
hat der Baum 1 + 8 + 8 · 7 + 8 · 7 · 6 + . . . + 8! = 69.281 viele verschiedene
Knoten.
34
Max Friedrich Wilhelm Bezzel (1824 – 1871) war ein deutscher Schachspieler.
35
Carl Friedrich Gauß (1777 – 1855) war ein deutscher Mathematiker. Er zählt zu
den bedeutendsten Mathematikern seiner Zeit.
1.5 Entwurfsmethoden für Algorithmen 51
ε.
1 2 3 4 5 6 7 8
2 3 4 5 6 7 8 ......
3 4 5 6 7 8 ......
......
Eine Permutation q ist im Baum durch einen Pfad P von der Wurzel zu
einem Blatt gegeben. Gehört ein Knoten k in der Ebene i zu P , so gibt k die
i–te Komponente von q an.
Wir finden die Lösungen durch Traversieren des Baumes mittels Tiefensu-
che (Algorithmus 4.5). Dabei prüfen wir in jedem Knoten, ob die Permutation
(q1 , . . . , qi ) noch Teil einer Lösung ist. Falls dies der Fall ist, untersuchen wir
die Nachfolgeknoten in der nächsten Ebene. Ist (q1 , . . . , qi ) nicht Teil einer
Lösung, so setzen wir die Suche in der übergeordneten Ebene fort. Es findet
somit ein Rückwärtsschritt (Backtracking) statt.
Der folgende Algorithmus Queens implementiert diesen Lösungsweg, oh-
ne den Baum explizit darzustellen. Die Darstellung ist implizit durch die
Abstiegspfade der Rekursion gegeben.
Queens gibt eine Lösung für das n–Damenproblem an, das die Problem-
stellung für n Damen und ein Schachbrett mit n × n–Feldern verallgemeinert.
Algorithmus 1.42.
Queens(int Q[1..n], i)
1 if i = n + 1
2 then print(Q)
3 for j ← 1 to n do
4 if testPosition(Q[1..n], i, j)
5 then Q[i] ← j
6 Queens(Q[1..n], i + 1)
i–te Zeile, ob wir in der j–ten Spalte eine Dame positionieren können. Dazu
prüft sie, ob auf der j–ten Spalte oder einer der Geraden y = x + (j − i)
oder y = −x + (j + i) durch das Feld mit den Koordinaten (i, j) bereits eine
Dame steht. Dies geschieht in der Zeile 2 von testPosition für die Zeile k,
k = 1, . . . , i − 1.
∑
n ∑
n
xi wi ≤ m und xi pi ist maximal.
i i
Die Zahl m heißt Kapazität, der Vektor w Gewichtsvektor und der Vektor p
Profitvektor . Das Problem besteht darin, eine Lösung ∑n (x1 , . . . , xn ) zu berech-
nen,
∑n die bei Beachtung der Kapazitätsschranke ( i xi wi ≤ m) den Profit
( i xi pi ) maximiert. Eine Lösung, die dies erfüllt, heißt optimale Lösung.
Genauer handelt es sich bei diesem Problem um das 0,1-Rucksackproblem.
Das 0,1 Rucksackproblem gehört zu den sogenannten NP-vollständigen Pro-
blemen. Unter der Annahme, dass P ̸= NP36 gilt, gibt es zur Lösung des
Problems keinen Algorithmus polynomialer Laufzeit. Die Berechnungsproble-
me aus der Komplexitätsklasse P kann ein deterministischer Turingautomat
in Polynomzeit lösen, die aus der Komplexitätsklasse NP ein nichtdeterminis-
tischer Turingautomat. Ein deterministischer Turingautomat kann Lösungen
aus NP in Polynomzeit verifizieren. Die NP-vollständigen Probleme erweisen
sich als die schwierigsten Probleme aus NP. Jedes Problem P ′ ∈ NP lässt sich
in Polynomzeit auf ein NP-vollständiges Problem P reduzieren, d. h. es gibt
einen Algorithmus polynomialer Laufzeit zur Lösung von P ′ , der einen Algo-
rithmus zur Lösung von P als Unterprogramm benutzt. Deshalb ist es möglich
alle Probleme in NP in Polynomzeit zu lösen, wenn ein einziges NP-vollständi-
ges Problem eine polynomiale Lösung besitzt. Die Komplexitätsklassen P,
36
Zu entscheiden, ob diese Annahme richtig ist, wird als P versus NP Problem be-
zeichnet. Es ist in der Liste der Millennium-Probleme enthalten, die im Jahr 2000
vom Clay Mathematics Institute aufgestellt wurde. Die Liste enthält 7 Probleme,
zu den 6 ungelösten Problemen gehört auch das P versus NP Problem.
1.5 Entwurfsmethoden für Algorithmen 53
37
George Bernard Dantzig (1914 – 2005) war ein amerikanischer Mathematiker.
Er hat das Simplex-Verfahren, das Standardverfahren zur Lösung von linearen
Optimierungsproblemen, entwickelt.
54 1. Einleitung
Satz 1.44. Rufen wir den Algorithmus 1.43 mit dem Parameter i = 1 auf,
so berechnet er eine optimale Lösung für die Instanz (p[1..n], w[1..n], m) des
fraktalen Rucksackproblems.
Beweis.
∑n Für das Ergebnis x = (x1 , . . . , xn ) von Algorithmus 1.43 gilt
i=1 x i w i = m. Falls xi = 1 für i = 1, . . . , n gilt, ist x eine optimale Lösung.
Sonst gibt es ein j mit 1 ≤ j ≤ n, x1 = . . . = xj−1 = 1 und 0 < xj ≤ 1,
xj+1 = . . . = xn = 0.
Angenommen, x = (x1 , . . . , xn ) wäre nicht optimal. Sei k maximal, sodass
für alle optimalen Lösungen (ỹ1 , . . . , ỹn ) gilt ỹ1 = x1 , . . . , ỹk−1 = xk−1 und
ỹk ̸= xk . Für∑ndieses k gilt ∑nk ≤ j, denn für k > j gilt xk = 0 und es folgt
ỹk > 0 und i=1 wi ỹi > i=1 wi xi = m, ein Widerspruch.
Sei (y1 , . . . , yn ) eine optimale Lösung, für die y1 = x1 , . . . , yk−1 = xk−1
und yk ̸= xk gilt. Wir zeigen jetzt, dass yk < xk gilt. Für k < j gilt xk = 1,
also folgt yk < xk . Es bleibt ∑n der Fall k∑ = j zu zeigen. Angenommen, es
n
wäre yj > xj . Dann folgt w y
i=1 i i > i=1 wi xi = m, ein Widerspruch.
Demzufolge ist k ≤ j und yk < xk gezeigt.
Aus y1 = . . . = yk−1 = 1 folgt
∑
n
wk (xk − yk ) = wi αi mit αi ≤ yi .
i=k+1
Wir ändern jetzt die Lösung (y1 , . . . , yn ) an den Stellen k, . . . , n ab. Genauer
definieren wir (z1 , . . . , zn ) wie folgt: Wähle zi = yi , i = 1, . . . , k − 1, zk = xk
und zi = yi − αi , für i = k + 1, . . . , n. Dann gilt
∑
n ∑
n ∑
n
p i zi = pi yi + pk (zk − yk ) − pi α i
i=1 i=1 i=k+1
( )
∑
n ∑
n
pk
≥ pi yi + wk (zk − yk ) − wi αi
i=1
wk
i=k+1
∑n
= pi yi .
i=1
∑n ∑n
Da (y1 , . . . , yn ) optimal ist, folgt i=1 pi zi = i=1 pi yi . Daher ist (z1 , . . . , zn )
optimal, ein Widerspruch zur Wahl von k. Somit ist (x1 , . . . , xn ) optimal. 2
Der Baum besitzt 2n+1 − 1 viele Knoten, die auf n + 1 viele Ebenen verteilt
sind (nummeriert von 0 bis n).
Die Lösungen des Rucksackproblems sind in den 2n Blättern lokalisiert.
Diese befinden sich alle in der Ebene n. Aber nicht alle Blätter sind Lösun-
gen des Rucksackproblems. Einige verletzen unter Umständen die Kapazitäts-
schranke, andere erreichen nicht den maximal möglichen Profit. Den Baum
komplett zu durchlaufen und dann die Kapazitätsschranke und Optimalitäts-
bedingung in jedem Blatt zu prüfen, liefert ein korrektes Ergebnis. Da aber
die Anzahl der Knoten von B in der Anzahl der Gepäckstücke exponentiell
wächst, führt dieses Vorgehen zu einem Algorithmus exponentieller Laufzeit.
Die Idee ist, rechtzeitig zu erkennen, dass ein Teilbaum keine optimale Lösung
enthalten kann. Ist in einem Knoten v die Kapazitätsbedingung verletzt, dann
ist sie für alle Knoten im Teilbaum, der v als Wurzel besitzt, verletzt. Wenn
im Knoten v die Summe aus dem bereits erreichten Profit und dem Profit,
den wir noch erwarten, den bisher erreichten maximalen Profit nicht übert-
rifft, untersuchen wir den Teilbaum mit Wurzel v nicht weiter.
Mit dieser Strategie löst der folgende Algorithmus das 0,1-Rucksackprob-
lem. Er besteht aus der Funktion knapSack, mit der die Ausführung star-
tet und der Funktion traverse, die B mittels Tiefensuche traversiert (ver-
gleiche Algorithmus 4.5). Auch hier stellen wir, wie bei der Lösung des 8-
Damenproblems, den Lösungsbaum B nicht explizit als Datenstruktur dar.
Algorithmus 1.45.
int: p[1..n], w[1..n], x[1..n], x̃[1..n]
traverse(int x[1..j], m′ , p′ )
1 int : ∑ p̃
n
2 p̃ ← k=1 p[k]x̃[k]
3 if j ≤ n
4 then (i, p′′ ) ← greedyKnapsack(p[j..n],w[j..n],x[j..n],m’)
5 if p′ + p′′ > p̃
6 then if w[j] ≤ m′
7 then x[j] ← 1
8 traverse(x[1..j + 1], m′ − w[j], p′ + p[j])
9 x[j] ← 0, traverse(x[1..j + 1], m′ , p′ )
′
10 else if p > p̃ then x̃ ← x
56 1. Einleitung
Bemerkungen:
1. Die Funktion knapSack ruft greedyKnapsack auf (Zeile 1). Diese berech-
net eine Lösung x̃ für das fraktale Rucksackproblem. Wenn x̃ eine ganz-
zahlige Lösung ist, so ist eine ganzzahlige optimale Lösung berechnet.
Sonst setzen wir die letzte von 0 verschiedene Komponente der Lösung
x̃ auf 0 und erhalten eine Lösung in {0, 1}n . Nach Terminierung des Auf-
rufs traverse(x[1..1], m, 0) (Zeile 4 in knapSack) enthält x̃ die als erste
gefundene optimale Lösung.
2. Die Parameter von traverse sind der aktuelle Knoten (x1 , . . . , xj ), die im
Knoten (x1 , . . . , xj ) verbleibende Kapazität m′ und der bis zum Knoten
(x1 , . . . , xj−1 ) erzielte Profit p′ . Der Algorithmus greedyKnapsack berech-
net für das auf [j..n] eingeschränkte fraktale Teilproblem eine optimale
Lösung (Satz 1.44). Der Profit der 0,1-Lösung ist stets ≤ dem Profit der
optimalen fraktalen Lösung.
3. Die Lösung (x̃1 , . . . , x̃n ) initialisieren wir zunächst mit greedyKnapsack
und aktualisieren sie anschließend immer dann, wenn wir eine Lösung
mit höherem Profit entdecken (Zeile 10).
4. Die Bedingung in Zeile 5 prüft, ob der Teilbaum mit Wurzel (x1 , . . . , xj )
eine optimale Lösung enthalten kann. Dies ist der Fall, wenn folgendes
gilt: Der bis zum Knoten (x1 , . . . , xj−1 ) erzielte Profit p′ plus dem Profit
p′′ einer optimalen Lösung des auf ∑[j..n] eingeschränkten fraktalen Pro-
n
blems ist größer dem Profit p̃ = i=1 pi x̃i der momentanen optimalen
Lösung (x̃1 , . . . , x̃n ). Ist dies der Fall, so setzen wir mit xj = 0 und mit
xj = 1 die Suche in der nächsten Ebene fort, falls dies die verbleibende
Kapazität zulässt (Zeile 6: w[j] ≤ m′ ). Die Variable j gibt die Ebene des
Baumes an, in der sich der Knoten (x1 , . . . , xj ) befindet. Insbesondere ist
für j = n ein Blatt erreicht.
5. Falls die Bedingung in Zeile 5 nicht eintritt, schneiden wir den Teilbaum
unter dem Knoten (x1 , . . . , xj−1 ) ab, d. h. wir durchsuchen weder den
Teilbaum mit Wurzel (x1 , . . . , xj−1 , 0) noch den Teilbaum mit Wurzel
(x1 , . . . , xj−1 , 1) nach einer optimalen Lösung.
6. Die Notation x[1..j] übergibt beim Funktionsaufruf auch den Parameter
j. Wenn wir traverse ∑n mit j = n + 1 aufrufen, so aktualisieren wir x̃, falls
der Profit p′ = k=1 pk xk , der mit dem Knoten (x1 , . . . , xn ) gegeben ist,
größer
∑n dem Profit der in x̃ gespeicherten Lösung ist, d. h. wenn p′ >
k=1 pk x̃k gilt (Zeile 10).
Beispiel. Figur 1.9 zeigt einen Lösungsbaum für das Rucksackproblem mit 5
Gepäckstücken, p = (10, 18, 14, 12, 3), w = (2, 6, 5, 8, 3) und m = 12.
1.6 Probabilistische Algorithmen 57
ε.
1 0
11 10 01 00
Die Greedy-Lösung ist 11000 und erzielt den Profit p′ = 28. Diese wird
erstmals aktualisiert mit 11001. Der zugeordnete Profit p′ = 31. K steht
für eine verletzte Kapazitätsbedingung (Zeile 6: w[j] > m′ ) und B für eine
verletzte Bounding-Bedingung (Zeile 5: p′ + p′′ ≤ p̃). Die optimale Lösung ist
01100 und ergibt den Profit 32.
Irrfahrt auf der Geraden. Der folgende Algorithmus ist komplett vom
Zufall gesteuert. Das Ergebnis hängt nur von einer Folge von Münzwürfen
ab, bei denen mit Wahrscheinlichkeit p Kopf und mit Wahrscheinlichkeit
1 − p Zahl eintritt. Es handelt sich um eine Irrfahrt auf der Zahlengeraden Z
(random walk). Beim Start positionieren wir eine Figur F im Nullpunkt von
Z. In jedem Schritt bewegen wir F abhängig von einem Münzwurf bei Kopf
eine Einheit nach rechts (+1) und bei Zahl eine Einheit nach links (-1).
Algorithmus 1.46.
int randomWalk(int n)
1 int x ← 0
2 for i ← 1 to n do
3 choose at random z ∈ {−1, 1}
4 x←x+z
5 return x
Die Zufallsvariable X beschreibt den Rückgabewert x (den Endpunkt der
Irrfahrt). Sie kann die Werte −n, . . . , n annehmen.
Die Variable X hat den Wert x, wenn k mal 1 und n−k mal −1 eingetreten
ist und wenn x = k − (n − k) = 2k − n gilt, d. h. k = (n + x)/2. Wir erhalten
als Erzeugendenfunktion von X
∑n ( )
n
GX (z) = n+x p(x+n)/2 (1 − p)(n−x)/2 z x
x=−n 2
2n ( )
∑ n
= x px/2 (1 − p)n−x/2 z x−n
x=0 2
1 ∑ (n) x ( )x
n
= n p (1 − p)n−x z 2
z x=0 x
(pz 2 + (1 − p))n
=
zn
(siehe Definition A.15, Definition A.11 und Anhang B (F.3)). Hieraus folgt
( )
G′X (1) = n(2p − 1), G′′X (1) = 4n n (p − 1/2) − p (p − 1/2) + 1/4 .
2
Für den Erwartungswert folgt E(X) = n(2p − 1) und für die Varianz
Var(X) = 4np(1 − p) (Satz A.12). Das Nachrechnen der Formeln stellen
wir als Übungsaufgabe.
Für p = 1/2 ist der Erwartungswert von X gleich 0. Wir erwarten, dass die
Irrfahrt nach n Schritten wieder im Ursprung endet. Die Varianz Var(X) = n.
Wir wären√ daher erstaunt, wenn die Irrfahrt in vielen Fällen um mehr als
σ(X) = n Positionen vom Ursprung entfernt endet.
1.6 Probabilistische Algorithmen 59
Beispiel. Das Histogramm der Figur 1.10 zeigt die relativen Häufigkeiten
der Endpunkte von 10.000 simulierten Irrfahrten, wobei jede Irrfahrt aus 50
Schritten besteht.
Da bei gerader Anzahl der Schritte die Endpunkte gerade sind, sind nur
die geraden Koordinaten angegeben. Beim Münzwurf handelt es sich um eine
faire Münze, Kopf und Zahl treten jeweils mit Wahrscheinlichkeit 1/2 ein. Bei
68 % der durchgeführten Experimente endet die Irrfahrt, wie erwartet, im
Intervall [-7,7]. Die experimentell ermittelte Verteilung weicht kaum von der
berechneten Verteilung ab.
Seien f (X) und g(X) ∈ Z[X] Polynome mit ganzen Zahlen als Koeffizienten.
Die Aufgabe ist festzustellen, ob f (X) = g(X) gilt.
Falls die Koeffizienten von f (X) und g(X) bekannt sind, kann dies einfach
durch Vergleich der einzelnen Koeffizienten erfolgen. Es gibt aber auch Situa-
tionen, in denen die Koeffizienten nicht bekannt sind. Dies ist zum Beispiel
60 1. Einleitung
der Fall, wenn wir nur die Nullstellen von f (X) und g(X) kennen. Bekannt-
lich sind zwei normierte Polynome gleich, wenn f (X) und g(X) dieselben
Nullstellen α1 , . . . , αn und β1 , . . . , βn besitzen.
Die Produkte auszumultiplizieren und anschließend die Polynome zu ver-
gleichen, liefert keinen schnelleren Algorithmus, als die gerade angegebene
Lösung. Einen schnelleren Algorithmus erhalten wir, wenn wir die Idee mit
den Fingerabdrücken anwenden. Der Fingerabdruck von f (X) ist f (x). x ist
ein Argument, das wir in f einsetzen können. Für unser Verfahren wählen wir
das Argument x aus einer endlichen Menge A ⊂ Z zufällig und vergleichen
f (x) = g(x).
∏n
f (x) = i=1 (x − αi ) berechnen wir mit n Multiplikationen, ohne vorher die
Koeffizienten zu berechnen. Wir erhalten folgenden Algorithmus.
Algorithmus 1.47.
boolean OnePassPolyIdent(polynomial f, g)
1 choose at random x ∈ A
2 if f (x) ̸= g(x)
3 then return false
4 return true
Man sagt, x ist ein Zeuge für f (X) ̸= g(X), falls f (x) ̸= g(x) gilt.
Gilt f (X) = g(X), dann gilt auch f (x) = g(x) und das Ergebnis ist kor-
rekt. Falls f (X) ̸= g(X) ist, kann es durchaus sein, dass f (x) = g(x) gilt. In
diesem Fall irrt sich OnePassPolyIdent. Wir untersuchen jetzt die Wahrschein-
lichkeit, mit der sich OnePassPolyIdent irrt. Dazu nehmen wir der Einfachheit
halber an, dass deg(f −g) = n−1 ist und wählen A mit |A| = 2(n−1) im Vor-
hinein. Wenn f (X) ̸= g(X) ist, dann ist f (X) − g(X) ̸= 0 ein Polynom vom
Grad n−1 und besitzt somit – mit Vielfachheit gezählt – höchstens n−1 Null-
stellen. OnePassPolyIdent irrt sich genau dann, wenn in Zeile 1 eine Nullstelle
von f − g gewählt wird. Die Wahrscheinlichkeit dafür ist ≤ (n−1)/2(n−1) = 1/2.
Sei p die Wahrscheinlichkeit, dass OnePassPolyIdent korrekt rechnet. Es gilt
daher p = 1, falls f (X) = g(X) ist und p ≥ 1/2, falls f (X) ̸= g(X) ist.
Auf den ersten Blick scheint ein Algorithmus, der sich in vielen Fällen mit
Wahrscheinlichkeit bis zu 1/2 irrt, von geringem Nutzen zu sein. Die Irrtums-
wahrscheinlichkeit können wir jedoch durch unabhängige Wiederholungen –
einem Standardvorgehen bei probabilistischen Algorithmen – beliebig klein
machen.
Algorithmus 1.48.
boolean PolyIdent(polynomial f, g; int k)
1 for i = 1 to k do
2 choose at random x ∈ A
3 if f (x) ̸= g(x)
4 then return false
5 return true
1.6 Probabilistische Algorithmen 61
Wir geben ein Beispiel für einen Las-Vegas-Algorithmus an. Der Algorith-
mus PolyDif beweist, dass zwei Polynome vom Grad n verschieden sind. Er
basiert auf denselben Tatsachen wie der Algorithmus PolyIdent.
Algorithmus 1.50.
boolean PolyDif(polynomial f, g)
1 while true do
2 choose at random x ∈ A
3 if f (x) ̸= g(x)
4 then return true
Falls f (X) = g(X) terminiert der Algorithmus nicht. Wir berechnen den
Erwartungswert der Anzahl I der Iterationen der while-Schleife in PolyDif
für den Fall f (X) ̸= g(X). Wir betrachten das Ereignis E, dass die zufällige
Wahl von x ∈ A keine Nullstelle von f (X) − g(X) liefert. Sei p die relative
Häufigkeit der Nichtnullstellen, dann gilt p(E) = p. Die Zufallsvariable I ist
geometrisch verteilt mit Parameter p. Der Erwartungswert E(I) = 1/p (Satz
A.20).
Soll der Algorithmus für praktische Zwecke eingesetzt werden, so muss
man die Schleife irgendwann terminieren. In diesem Fall gibt der Algorithmus
aus: kein Ergebnis erzielt“. Wahrscheinlich sind dann die beiden Polynome
”
gleich. Für p = 1/2 ist nach k Iterationen die Irrtumswahrscheinlichkeit höchs-
tens 1/2k .
∏n
Bei der Berechnung von f (x) = i=1 (x − αi ) im Algorithmus PolyIdent
entstehen unter Umständen sehr große Zahlen. Eine Multiplikation ist dann
keine Elementaroperation. Der Aufwand hängt von der Länge der Zahlen
62 1. Einleitung
ab. Dieses Problem ist auch durch die Technik lösbar, Fingerabdrücken zu
verwenden.
h : X −→ Y
Da der Beweis der Primzahleigenschaft aufwendig ist, wählt man für Anwen-
dungen, die eine große Primzahl benötigen, eine Zufallszahl passender Größe
und prüft die Primzahleigenschaft mit einem probabilistischen Primzahltest
1.6 Probabilistische Algorithmen 63
Wir geben zunächst die Menge P von Primzahlen an, aus der wir eine
Primzahl zufällig wählen. Sei z eine Zahl und
Da π(z) divergiert, können wir für jede Konstante t die Schranke z so groß
wählen, dass π(z) ≈ tl gilt. Wir betrachten für dieses z die Menge P =
{p Primzahl | p ≤ z}.
Satz 1.51. Sei x ̸= y. Dann gilt für die Kollisionswahrscheinlichkeit für
zufällig gewähltes p ∈ P
1
p(hp (x) = hp (y)) ≤ .
t
Beweis. Sei x ̸= y und z := x − y. Es ist hp (x) = hp (y) genau dann, wenn
hp (z) = 0 ist. Dies aber ist äquivalent zu p ist ein Teiler von z. Da |z| < 2l
ist, besitzt z höchstens l Primteiler. Es folgt
l l 1
p(hp (x) = hp (y)) ≤ ≈ = .
|P | tl t
Wir erhalten den folgenden Algorithmus, der Zahlen auf Gleichheit testet.
Algorithmus 1.52.
boolean IsEqual(int x, y)
1 choose at random p ∈ P
2 if x ̸≡ y mod p
3 then return false
4 return true
38
Für einen einfachen Beweis des Primzahlsatzes siehe [Newman80].
64 1. Einleitung
Für x = y, ist auch x ≡ y mod p wahr, und das Ergebnis ist korrekt. Wenn
̸ y gilt, dann kann es durchaus sein, dass x ≡ y mod p gilt. In diesem
x =
Fall ist die Irrtumswahrscheinlichkeit ≈ 1t . Durch k unabhängige Wiederho-
lungen des Tests können wir die Irrtumswahrscheinlichkeit auf ≈ t1k senken.
Für t = 2 ist die Irrtumswahrscheinlichkeit ≈ 12 bzw. ≈ 21k .
kann modulo p erfolgen. Ebenso die Berechnung von hp (g(x)). Aus diesem
Grund bleibt die Anzahl der Stellen bei den n Multiplikationen durch log2 (p)
beschränkt.
Algorithmus 1.53.
boolean OnePassPolyIdent(polynomial f, g; int k)
1 choose at random x ∈ A
2 for i ← 1 to k do
3 choose at random p ∈ P
4 if f (x) ̸≡ g(x) mod p
5 then return false
6 return true
Falls f (X) = g(X) ist, liefert OnePassPolyIdent wie vorher ein korrektes
Ergebnis. Der Vergleich f (x) = g(x), durchgeführt in den Zeilen 2 – 4 mit
der probabilistischen Methode von Algorithmus 1.52, ist nur noch mit hoher
Wahrscheinlichkeit korrekt (≥ 1 − 1/2k ). Die Erfolgswahrscheinlichkeit von
OnePassPolyIdent sinkt dadurch im Fall f (X) ̸= g(X) etwas (≥ 1/2 − 1/2k ).
Durch unabhängige Wiederholungen kann die Erfolgswahrscheinlichkeit – wie
bei PolyIdent – wieder beliebig nahe an 1 gebracht werden.
Wir besprechen jetzt die Grundlagen, die notwendig sind, um den Algorith-
mus zum Vergleich univariater Polynome auf multivariate Polynome zu er-
weitern.
Satz 1.54. Sei F ein endlicher Körper mit q Elementen39 und sei
f (X1 , . . . , Xn ) ∈ F[X1 , . . . , Xn ] ein Polynom vom Grad d, d > 0. Sei
N (f ) = {(x1 , . . . , xn ) | f (x1 , . . . , xn ) = 0} die Menge der Nullstellen von
f . Dann gilt
|N (f )| ≤ d · q n−1 .
39
Die Anzahl der Elemente ist eine Primzahlpotenz, q = pn . Für q = p siehe
Anhang B, Corollar B.12.
1.6 Probabilistische Algorithmen 65
Beweis. Wir zeigen die Behauptung durch Induktion nach der Anzahl n der
Variablen. Für n = 1 ist die Behauptung richtig, weil ein Polynom in einer
Variablen vom Grad d über einem Körper höchstens d Nullstellen besitzt.
Wir zeigen, dass n aus n − 1 folgt. Sei
∑
k
f (X1 , . . . , Xn ) = fi (X1 , . . . , Xn−1 )Xni , fk (X1 , . . . , Xn−1 ) ̸= 0.
i=0
Wir nehmen ohne Einschränkung an, dass k ≥ 1 gilt, sonst entwickeln wir
f (X1 , . . . , Xn ) nach einer anderen Variablen. Das Polynom fk (X1 , . . . , Xn−1 )
besitzt einen Grad ≤ d − k. Nach Induktionsvoraussetzung gilt für
Bemerkungen:
1. Das vorangehende Corollar erlaubt für d ≤ q/2 einen probabilistischen
Algorithmus, analog zu Algorithmus 1.48, für multivariate Polynome zu
implementieren.
2. Corollar 1.55 wurde unabhängig von Schwartz40 und Zippel41 publiziert.
In der Literatur wird es mit Schwartz–Zippel Lemma bezeichnet.
3. Das vorangehende Corollar besitzt eine interessante Anwendung bei
Graphalgorithmen. Das Problem, ob ein gegebener bipartiter Graph ei-
ne perfekte Zuordnung besitzt, führen wir auf die Frage zurück, ob ein
bestimmtes Polynom das Nullpolynom ist (Satz 5.8). Der in Punkt 1 an-
gesprochene probabilistische Algorithmus, der multivariate Polynome auf
Gleichheit testet, kann verwendet werden, um diese Frage zu entscheiden.
40
Jacob Theodore Schwartz (1930 — 2009) war ein amerikanischer Mathematiker
und Informatiker.
41
Richard Zippel ist ein amerikanischer Informatiker.
66 1. Einleitung
1.6.4 Zufallszahlen
Die Algorithmen in diesem Abschnitt verwenden die Anweisung choose at
”
random“. Eine Implementierung dieser Anweisung erfordert Zufallszahlen.
Für echte Zufallszahlen sind physikalische Prozesse, wie Würfeln, radioaktive
Zerfallsprozesse oder Quanteneffekte notwendig. Eine gleichverteilte Zufalls-
zahl mit n Bit kann durch das n–malige Werfen einer unverfälschten Münze
gewonnen werden. Diese Methode eignet sich jedoch nicht zur Implementie-
rung in einem Rechner. Wir verwenden Pseudozufallszahlen. Pseudozufalls-
zahlen erzeugt ein Pseudozufallszahlen-Generator , kurz Generator. Ein Ge-
nerator ist ein deterministischer Algorithmus, der aus einem kurzen zufällig
gewählten Startwert, Keim oder Seed genannt, eine lange Folge von Ziffern
(Bits) erzeugt.
Es gibt zur Erzeugung von Zufallszahlen spezielle Hardware. Das Trusted
Platform Module (TPM), zum Beispiel, ist ein Chip, der neben grundlegen-
den Sicherheitsfunktionen auch die Generierung von Zufallszahlen bietet. Gu-
te Quellen für Zufall, die ohne zusätzliche Hardware zur Verfügung stehen,
sind Zeitdifferenzen zwischen Ereignissen innerhalb eines Computers, die aus
mechanisch generierten Informationen herrühren, wie zum Beispiel Timing
zwischen Netzwerkpaketen, Rotationslatenz von Festplatten und Timing von
Maus- und Tastatureingaben. Aus der Kombination dieser Ereignisse lässt
sich ein guter Startwert berechnen.
Es gibt im Hinblick auf die Qualität der Pseudozufallszahlen und auf
die Rechenzeit der Algorithmen unterschiedliche Verfahren. Kryptographi-
sche Algorithmen, zum Beispiel, benötigen Pseudozufallszahlen hoher Qua-
lität. Die Sicherheit kryptographischer Verfahren ist eng mit der Generierung
nicht vorhersagbarer Zufallszahlen verknüpft. Die theoretischen Aspekte der
Erzeugung von Pseudozufallszahlen in der Kryptographie werden umfassend
in [DelfsKnebl15, Kapitel 8] dargestellt.
Für unsere Zwecke genügt es, dass die Folge der Pseudozufallszahlen
keine offensichtliche Regularität aufweist und bestimmte statistische Tests
bestehen, wie zum Beispiel den χ2 –Test. Diese Aspekte sind ausführlich in
[Knuth98] diskutiert.
Beispiel. Wir betrachten drei 0-1-Folgen der Länge 50. Welche der drei Folgen
der Figur 1.11 ist eine Zufallsfolge?
.
0001011101 1100101010 1111001010 1100111100 0101010100
0000000000 0000000000 0000001010 1100111100 0101010100
0000000000 0000000000 0000000000 0000000000 0000000001
Obwohl jede der drei Folgen die Wahrscheinlichkeit 1/250 hat, wenn wir sie
zufällig in {0, 1}50 wählen, erscheint intuitiv die erste Folge typisch für eine
Zufallsfolge, die zweite weniger typisch und die dritte untypisch.
1.6 Probabilistische Algorithmen 67
Dies lässt sich wie folgt begründen. Falls wir eine Folge durch das Werfen
einer fairen Münze erzeugen, ist die Zufallsvariable X, die die Anzahl der
Einsen in der Folge zählt binomialverteilt mit den Parametern (50, 1/2) (De-
finition A.15).
√ Der Erwartungswert E(X) = 25 und die Standardabweichung
σ(X) = 50/2 = 3.54 (Satz A.16). Eine Folge der Länge 50, bei der die An-
zahl der Einsen (und damit auch die der Nullen) stark von der 25 abweicht,
erscheint uns nicht typisch für eine Zufallsfolge. Deshalb erscheint die erste
Folge mit 26 Einsen typisch und die dritte Folge mit einer Eins untypisch.
Eine andere Methode, um die obige Frage zu entscheiden, liefert die In-
formationstheorie. Für eine gleichverteilte Zufallsfolge der Länge 50 ist der
Informationsgehalt 50 Bit. Der Informationsgehalt hängt eng mit der Länge
einer kürzesten Codierung der Folge zusammen. Der Informationsgehalt einer
Bitfolge ≈ der Länge einer kürzesten Codierung der Bitfolge (Satz 4.39). Die
3. Folge besitzt eine kurze Codierung 0(49),1 (49 mal die Null gefolgt von der
Eins).42 Folglich ist der Informationsgehalt klein und damit auch der bei der
Generierung der Folge beteiligte Zufall. Eine Folge von 50 Bit, die wie die
erste Folge mit Münzwürfen erzeugt wurde, kann man nicht mit weniger als
50 Bit codierten, wenn man die ursprüngliche Folge wieder aus der codierten
Folge decodieren kann.
Der am meisten (für nicht kryptographische Anwendungen) genutzte
Pseudozufallsgenerator ist der lineare Kongruenzengenerator . Dieser ist durch
folgende Parameter festgelegt: m, a, c, x0 ∈ Z mit 0 < m, 0 ≤ a < m,
0 ≤ c < m und 0 ≤ x0 < m. m heißt Modulus und x0 heißt Startwert
des linearen Kongruenzengenerators. Ausgehend von x0 berechnen wir nach
der Vorschrift
xn+1 = (axn + c) mod m, n ≥ 1,
die Pseudozufallsfolge. Die Auswirkungen der Wahl der Parameter a, c und
m auf die Sicherheit der Pseudozufallsfolge und auf die Länge der Periode
werden ausführlich in [Knuth98, Chapter 3] studiert.
Dieser Abschnitt dient als Einstieg für probabilistische Verfahren, die wir
im weiteren Text behandeln. Quicksort, Quickselect, Suchbäume und Hash-
funktionen sind effizient im Durchschnitt. Dies hat als Konsequenz, dass bei
zufälliger Wahl der Eingaben, entsprechende Laufzeiten zu erwarten sind. Die
Idee ist, den Zufall in der Eingabe durch Zufall im Algorithmus zu ersetzen
(Einsatz von Zufallszahlen). Dieser Idee folgend, erhalten wir probabilistische
Verfahren zum Sortieren und Suchen in sortierten Arrays (Algorithmus 2.9
und Algorithmus 2.30), universelle Familien von Hashfunktionen (Abschnitt
3.2.2) und probabilistische binäre Suchbäume (Abschnitt 4.4). Wir lösen die
Graphenprobleme der Berechnung eines minimalen Schnittes und eines mini-
malen aufspannenden Baumes durch probabilistische Algorithmen (Abschnitt
5.7 und 6.6).
42
Die hier angewendete Methode der Codierung bezeichnen wir als Lauflängenco-
dierung.
68 1. Einleitung
Bei allen Algorithmen, außer dem Algorithmus zur Berechnung eines mi-
nimalen Schnitts handelt es sich um Las-Vegas-Algorithmen. Sie liefern somit
immer korrekte Ergebnisse. Der Algorithmus zur Berechnung eines minima-
len Schnitts ist ein Monte-Carlo-Algorithmus. Für alle Probleme gibt es auch
Lösungen durch deterministische Algorithmen. Diese sind jedoch weniger ef-
fizient.
43
Euklid von Alexandria war ein griechischer Mathematiker, der wahrscheinlich im
3. Jahrhundert v. Chr. in Alexandria gelebt hat. Euklid hat den Algorithmus in
seinem berühmten Werk Die Elemente“ in äquivalenter Form beschrieben.
”
1.7 Pseudocode für Algorithmen 69
A B C
Eine Variable start vom Typ listElem speichert eine Referenz auf das ers-
te Datenobjekt. Mit start.c kann man auf das Zeichen und mit start.next
auf .die Referenz zugreifen.
Die Variable next speichert eine Referenz auf ein Datenobjekt vom Typ
listElem oder null. Die null-Referenz zeigt an, dass das Ende der Liste
erreicht ist. Wir verwenden die null-Referenz, wenn eine Referenzvariable
kein Objekt referenziert.
7. Arrays sind wie zusammengesetzte Datentypen reference types. Der Zu-
griff auf einzelne Elemente erfolgt durch den [ ]–Operator. Bei der Defi-
nition eines Arrays geben wir den Bereich der Indizes mit an. Der Aus-
druck int a[1..n]“ definiert ein Array von ganzen Zahlen von der Dimen-
”
sion n, das von 1 bis n indiziert ist. Später ist es möglich mit a[i..j],
1 ≤ i ≤ j ≤ n, auf Teilarrays zuzugreifen. Für i = j greifen wir auf das
i–te Element zu und wir schreiben kurz a[i]. Ist ein Array a[1..n] Para-
meter in der Definition einer Funktion, so ist vereinbart, dass wir auf die
Variable n, die Länge des Arrays, in der Funktion zugreifen können.
8. Unser Pseudocode enthält Funktionen wie die Programmiersprachen Java
oder C. Parameter übergeben wir an Funktionen stets by value“, d. h.
”
die gerufene Funktion erhält eine Kopie des Parameters in einer eigenen
Variablen. Eine Änderung der Variablen wirkt sich nur in der gerufenen
70 1. Einleitung
Funktion aus. Sie ist in der rufenden Funktion nicht sichtbar. Ist x eine
Referenz und Parameter einer Funktion, so wirkt sich eine Zuweisung
x ← y nur in der gerufenen Funktion aus, eine Zuweisung x.prop ← a ist
auch in der rufenden Funktion sichtbar.
Variable, die wir innerhalb einer Funktion definieren, sind nur in der
Funktion sichtbar. Außerhalb von Funktionen definierte Variable sind
global sichtbar.
Es folgt eine Liste von Lehrbüchern, die ich bei der Vorbereitung der Vorle-
sungen, aus denen dieses Buch entstanden ist, benutzt habe. Sie waren eine
wertvolle Quelle bei der Auswahl und Darstellung der behandelten Algorith-
men. Im Text kann man sicher Spuren der referenzierten Lehrbücher finden.
Als Erstes möchte ich The Art of Computer Programming“ (Band 1–3)
”
von Knuth nennen ([Knuth97], [Knuth98], [Knuth98a]). Das Standardwerk,
stellt die behandelten Themen umfassend dar. Die entwickelte Methodik be-
sitzt sehr große Präzision. Dieses Werk bei der Vorbereitung einer Vorle-
sung über Algorithmen und Datenstrukturen nicht zu verwenden, wäre ein
Versäumnis.
Concrete Mathematics“ von Graham, Knuth und Patashnik entwickelt
”
mathematische Methoden zur Analyse von Algorithmen anhand zahlreicher
Beispiele ([GraKnuPat94]). Dieses Buch ist nicht nur für Informatiker, son-
dern auch für Freunde Konkreter Mathematik“ eine exorbitante Fundgrube.
”
Bei meinen ersten Vorlesungen zu Beginn der neunziger Jahre habe ich
auch die Bücher von Wirth Algorithmen und Datenstrukturen“
”
([Wirth83]) und Systematisches Programmieren“ ([Wirth83a]) verwendet.
”
Weiter ist der Klassiker The Design and Analysis of Computer Algorithms“
”
([AhoHopUll74]), der auch aus heutiger Sicht viel Interessantes enthält, und
Data Structures and Algorithms“ ([AhoHopUll83]), beide Werke von Aho,
”
Hopcroft und Ullman als auch Algorithms“ von Sedgewick
”
([Sedgewick88]), das in vierter Auflage ([SedWay11]) mit Wayne als zusätzli-
chem Autor erschienen ist, zu nennen.
Von den neueren Lehrbüchern ist Introduction to Algorithms“ von Cor-
”
men, Leiserson und Rivest ([CorLeiRiv89]), das in seiner dritten Auflage,
mit Stein als zusätzlichem Autor, auch in deutscher Übersetzung vorliegt
([CorLeiRivSte07]) und Algorithmen und Datenstrukturen“ von Dietzfelbin-
”
ger, Mehlhorn und Sanders ([DieMehSan15]) zu erwähnen.
Umfassend behandeln Motwani und Raghavan randomisierte oder proba-
bilistische Algorithmen in Randomized Algorithms“ ([MotRag95]).
”
Die im Text angegebenen biografischen Daten von Personen sind den je-
weiligen Wikipedia Einträgen entnommen.
Übungen 71
Übungen.
1. Zeigen Sie, dass n = 2 3−1 , wobei k ∈ N gerade ist, eine natürliche Zahl
k
ist, und dass der Algorithmus 1.2 bei Eingabe von n terminiert.
2. Wir betrachten Sortieren durch Einfügen.
Algorithmus 1.57.
InsertionSort(item a[1..n])
1 index i, j; item x
2 for i ← 2 to n do
3 x ← a[i], a[0] ← x, j ← i − 1
4 while x < a[j] do
5 a[j + 1] ← a[j], j ← j − 1
6 a[j + 1] ← x
√ ( )
6. Sei f1 (n) = n( n n − 1) und f2 (n) = nk k!.
Bestimmen Sie die Ordnungen von f1 (n) und f2 (n).
7. Ein Kapital k werde mit jährlich p % verzinst. Nach Ablauf eines Jahres
erhöht sich das Kapital um die Zinsen und um einen konstanten Betrag
c. Geben Sie eine Formel für das Kapital nach n Jahren an.
8. Lösen Sie die folgenden Differenzengleichungen:
a. x1 = 1, b. x1 = 0,
2(n−1)
xn = xn−1 + n, n ≥ 2. xn = n+1
n xn−1 + n , n ≥ 2.
10. Wie oft wird die Zeichenkette Hello!“ durch den folgenden Algorithmus
”
(in Abhängigkeit von n) ausgegeben?
Algorithmus 1.59.
DoRec(int n)
1 if n > 0
2 then for i ← 1 to 2n do
3 DoRec(n − 1)
4 k←1
5 for i ← 2 to n + 1 do
6 k ←k·i
7 for i ← 1 to k do
8 print(Hello!)
√
11. Seien n ∈ R≥0 , T (n) = T ( n) + r(n) für n > 2 und T (n) = 0 für n ≤ 2.
Berechnen Sie für r(n) = 1 und r(n) = log2 (n) eine geschlossene Lösung
für T (n). Verwenden Sie Lemma B.24.
12. Seien a ≥ 1, b > 1, d, l ≥ 0 und
Geben Sie eine geschlossene Lösung der Gleichung an. Verwenden Sie
die inverse Transformation k = logb (n) zur Berechnung einer Lösung der
Rekursionsgleichung (R1).
( )
13. Sei T (n) = aT ⌊ n2 ⌋ + nl , T (1) = 1.
Geben Sie Abschätzungen für a = 1, 2 und l = 0, 1 an.
14. Die Funktion Fib(n) zur Berechnung der n–ten Fibonacci-Zahl werde
rekursiv implementiert (analog zur definierenden Formel). Wie groß ist
der notwendige Stack in Abhängigkeit von n zu wählen?
15. Programmieren Sie TowersOfHanoi iterativ. Untersuchen Sie dazu den
Baum, der durch die rekursiven Aufrufe von TowersOfHanoi definiert ist.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021
H. Knebl, Algorithmen und Datenstrukturen,
https://doi.org/10.1007/978-3-658-32714-9_2
76 2. Sortieren und Suchen
2.1 Quicksort
Quicksort verfolgt die Divide-and-Conquer-Strategie (Abschnitt 1.5.2). Wir
teilen das Problem, eine Folge der Länge n zu sortieren, in kleinere Teilpro-
bleme auf. Ein Teilproblem besteht darin, eine kürzere Folge zu sortieren. Die
Teilprobleme sind vom selben Typ wie das Gesamtproblem. Es bietet sich an,
die Teilprobleme rekursiv zu lösen. Die Lösungen der Teilprobleme werden
dann zu einer Lösung des Gesamtproblems zusammengesetzt.
Sei F die zu sortierende Folge. Bei Quicksort besteht die Zerlegung des
Problems darin, ein Pivotelement x aus F zu wählen und F auf zwei Folgen F1
und F2 aufzuteilen. F1 enthalte nur Elemente, die ≤ x und F2 nur Elemente,
die ≥ x sind. Wir wenden dann Quicksort rekursiv auf F1 und F2 an. Im
Fall von Quicksort besteht das Zusammensetzen der Teillösungen F1 und
”
F2 sortiert“ zu der Gesamtlösung F sortiert“ einfach darin, nacheinander
”
die Elemente von F1 , in sortierter Reihenfolge, dann x und zum Schluss F2 ,
wieder in sortierter Reihenfolge, auszugeben.
Die zu sortierende Folge F speichern wir in einem Array a. Quicksort
sortiert auf der Grundlage von Vergleichen und Umstellungen im Inputarray
a.
Der Algorithmus Quicksort wurde von Hoare veröffentlicht ([Hoare62]).
Algorithmus 2.1.
QuickSort(item a[i..j])
1 item x; index l, r; boolean loop ← true
2 if i < j
3 then x ← a[j], l ← i, r ← j − 1
4 while loop do
5 while a[l] < x do l ← l + 1
6 while a[r] > x do r ← r − 1
7 if l < r
8 then exchange a[l] and a[r]
9 l = l + 1, r = r − 1
10 else loop ← false
11 exchange a[l] and a[j]
12 QuickSort(a[i..l − 1])
13 QuickSort(a[l + 1..j])
Der Aufruf QuickSort(a[1..n]) sortiert ein Array a[1..n]. Vor dem ersten
Aufruf von QuickSort setzen wir a[0] als Wächter.1 Für den Wächter muss
a[0] ≤ a[i], 1 ≤ i ≤ n, gelten.
Beispiel. Wir betrachten die Anwendung von QuickSort auf die Folge 67, 56,
10, 41, 95, 18, 6, 42. Figur 2.1 gibt die Hierarchie der Aufrufe des QuickSort-
Algorithmus an.
1
Das Wächterelement stellt sicher, dass wir auf das Array a nicht mit negativem
Index zugreifen.
2.1 Quicksort 77
.
a[1..8]
a[1..4] a[6..8]
In den Knoten des Baums sind die Teilarrays angegeben, die wir als Auf-
rufparameter übergeben. Jeder Knoten, der kein Blatt ist, hat genau zwei
Nachfolger. Wir sehen, dass rekursive Aufrufe mit einem Element oder auch
keinem Element stattfinden. Dies wäre bei einer Implementierung zu vermei-
den. Die Reihenfolge der Aufrufe erhalten wir durch die Preorder-Ausgabe
und die Reihenfolge der Terminierungen durch die Postorder-Ausgabe der
Knoten des Baumes (Definition 4.4). Der Aufruf Quicksort(a[1..1]) terminiert
als erster Aufruf. Das kleinste Element steht dann an der ersten Stelle.
Im Folgenden geben wir die Pivotelemente und die zugehörigen Zerlegun-
gen an.
Folge: 67 56 10 41 95 18 6 42
Pivotelement: 42
Zerlegung: 6 18 10 41 56 67 95
Pivotelemente: 41 95
Zerlegungen: 6 18 10 56 67
Pivotelemente: 10 67
Zerlegungen: 6 18 56
Sortierte Folge: 6 10 18 41 42 56 67 95
Nach Terminierung des Aufrufs QuickSort(a[i′ ..j ′ ]) ist der Teilbereich mit
den Indizes i′ ..j ′ sortiert.
Satz 2.2. QuickSort sortiert das Array a in aufsteigender Reihenfolge.
Beweis. Wir zeigen zunächst, dass QuickSort terminiert.
1. Die while-Schleife in Zeile 5 terminiert beim ersten Durchlauf spätestens
für l = j, denn das Pivotelement steht ganz rechts.
2. Die while-Schleife in Zeile 6 terminiert beim ersten Durchlauf spätestens
für r = i − 1 (falls das Pivotelement das kleinste Element ist). Denn an
dieser Stelle steht das Pivotelement der vorangehenden Zerlegung oder
für i = 1 das Wächterelement a[0]. Es gilt demnach a[i − 1] ≤ a[r] für
i ≤ r ≤ j.
78 2. Sortieren und Suchen
3. Für l < r vertauschen wir a[l] und a[r] (Zeile 8). Nach Inkrementie-
ren von l und Dekrementieren von r stehen links von a[l] Elemente ≤ x
und rechts von a[r] Elemente ≥ x. Alle nachfolgenden Durchläufe der bei-
den inneren while-Schleifen terminieren. Nach jeder Iteration der äußeren
while-Schleife (Zeilen 4 – 10) nimmt der Abstand r − l ab. Deshalb tritt
l ≥ r ein, d. h. die while-Schleife terminiert.
Die Korrektheit folgt jetzt unmittelbar mit vollständiger Induktion nach n,
der Anzahl der Elemente von a. Der Induktionsbeginn für n = 1 ist offen-
sichtlich richtig. Die Teilarrays, für die wir QuickSort rekursiv aufrufen, ha-
ben höchstens n − 1 viele Elemente. Deshalb nehmen wir per Induktions-
hypothese an, dass die Teilarrays in aufsteigender Reihenfolge sortiert sind.
Nach Terminierung der äußeren while-Schleife (Zeile 4) gilt: i ≤ l ≤ j und
a[i], . . . , a[l − 1] ≤ x und a[l + 1], . . . , a[j] ≥ x. Das Element x kann bei einer
sortierten Reihenfolge an der Stelle l stehen. Das ganze Array a ist demzu-
folge sortiert. 2
Bemerkungen:
1. Nach Terminierung der äußeren while-Schleife gilt: l ≥ r. Da a[l − 1] < x
ist, gilt l = r oder l = r + 1.
2. Sei n = j −(i−1) (Anzahl der Elemente). Es finden n oder n+1 viele Ver-
gleiche statt (Zeilen 5 und 6). Durch eine kleine Modifikation kann man
erreichen, dass n − 1 Vergleiche mit Elementen aus a ausreichen (Übun-
gen, Aufgabe 8). Die Vergleiche mit Elementen aus a bezeichnen wir
als wesentliche Vergleiche. Vergleiche zwischen Indizes erfordern geringe-
ren Aufwand, weil Indizes meistens mit Registervariablen implementiert
werden und Elemente im Array im Vergleich zu Indizes eine komple-
xe Struktur aufweisen können. Vergleiche zwischen Indizes zum Beispiel
sind unwesentlich. Wir zählen nur wesentliche Vergleiche.
Da bei jeder Vertauschung zwei Elemente
⌊ ⌋beteiligt sind, ist die Anzahl
der Vertauschungen in Zeile 8 durch n−1 2 beschränkt.
3. Im obigen Algorithmus finden rekursive Aufrufe für ein Array mit ei-
nem oder sogar ohne Elemente statt. In einer Implementierung wäre dies
zu vermeiden. Es wären sogar rekursive Aufrufe für kleine Arrays zu
vermeiden, denn QuickSort ist einer einfachen Sortiermethode wie zum
Beispiel Sortieren durch Einfügen nur überlegen, wenn die Anzahl der
Elemente der zu sortierenden Menge hinreichend groß ist. Deshalb wird
empfohlen, statt eines rekursiven Aufrufs von QuickSort, die Methode
Sortieren durch Einfügen zu verwenden, falls die Anzahl der zu sortieren-
den Elemente klein ist, d. h. eine gegebene Schranke unterschreitet. In
[Knuth98a] wird dies genau analysiert und eine Schranke für n berechnet.
Für den dort betrachteten Rechner ist diese Schranke 10.
2.1.1 Laufzeitanalyse
Wir nehmen an, dass das zu sortierende Array a[1..n] lauter verschiedene
Elemente enthält. Der Aufwand für die Zeilen 2 - 11 ist cn, c konstant. Wir
2.1 Quicksort 79
zerlegen a[1..n] in Arrays der Länge r − 1 und n − r. Für die Laufzeit T (n)
erhalten wir rekursiv:
T (n) = T (r − 1) + T (n − r) + cn, c konstant.
Der beste und der schlechteste Fall. Das Pivotelement bestimmt bei
der Zerlegung die beiden Teile. Es können gleich große Teile oder Teile stark
unterschiedlicher Größe entstehen. Im Extremfall ist ein Teil leer. Dieser Fall
tritt ein, falls das Pivotelement das größte oder kleinste Element ist. Wir be-
trachten die beiden Bäume, die die rekursive Aufrufhierarchie von QuickSort
in diesen beiden Fällen darstellen, siehe Figur 2.2 In den Knoten notieren
wir die Anzahl der Elemente des Arrays, das wir als Parameter übergeben.
Knoten für die keine Vergleiche stattfinden sind weggelassen. Der erste Baum
stellt den Fall der gleich großen Teile dar. Der zweite Baum den Fall, in dem
in jedem Schritt ein Teil leer ist.
n. n.
n n
2 2 n−1
n n n n
4 4 4 4 n−2
n
8
... n−3
.. ..
. .
Im ersten Fall ist die Höhe des Baumes ⌊log2 (n)⌋. Auf jeder Ebene des
Baumes befinden sich ungefähr n Elemente. Die Summe aller Vergleiche auf
derselben Rekursionsstufe, d. h. in einer Ebene des Baumes, ist ungefähr n.
Insgesamt ist die Anzahl der Vergleiche O(n log2 (n)).
Im zweiten Fall ist die Höhe des Baumes n. Auf der i–ten Ebene finden
n − (i + 1) viele Vergleiche statt. Insgesamt ist die Anzahl der Vergleiche
∑n−1 n(n−1)
i=1 i = 2 = O(n2 ).
Satz 2.26 besagt, dass O(n log2 (n)) eine untere Schranke für die Anzahl
der Vergleiche ist, die ein Algorithmus im schlechtesten Fall benötigt, der
auf der Grundlage des Vergleichs von zwei Elementen sortiert. Eine obere
Schranke für die Anzahl der Vergleiche liefert die folgende Überlegung.
Für die Anzahl V (n) der Vergleiche2 , die QuickSort zum Sortieren von n
Elementen benötigt, gilt
V (n) ≤ max V (r − 1) + V (n − r) + (n − 1).
2⌋
1≤r≤⌊ n
2
Wir betrachten eine optimierte Version von Quicksort, die mit n − 1 vielen Ver-
gleichen auskommt (siehe die Bemerkungen nach dem Beweis von Satz 2.2).
80 2. Sortieren und Suchen
Wir zeigen durch Induktion nach n, dass V (n) ≤ n(n−1)2 gilt. Für n = 1 ist
kein Vergleich notwendig, die Behauptung ist erfüllt. Aus der Induktionshy-
pothese folgt
(r − 1)(r − 2) (n − r)(n − r − 1)
V (n) ≤ max + + (n − 1).
2⌋
1≤r≤⌊ n 2 2
Die Funktion auf der rechten Seite, über die wir das Maximum bilden, ist für
r ∈ [1, ⌊ n2 ⌋] monoton fallend. Die Induktionsbehauptung folgt unmittelbar.
Dies zeigt, dass die Anzahl der Vergleiche von QuickSort nach unten durch
O(n log2 (n)) und nach oben durch n(n−1)2 = O(n2 ) beschränkt ist. Da die
Laufzeit proportional zur Anzahl der Vergleiche ist, ergeben sich analoge
Schranken für die Laufzeit.
Wir untersuchen die Laufzeit von QuickSort im besten und schlechtesten
Fall genauer.
Satz 2.3. Für die Laufzeit T (n) von QuickSort gilt im besten Fall
wobei b und c konstant sind. Insbesondere gilt T (n) = O(n log2 (n)).
Beweis. Der beste Fall tritt ein, wenn in jedem Rekursionsschritt der Zerle-
gungsprozess
⌈ ⌊ n−1 ⌋große Mengen liefert. n − 1 Elemente zerlegen sich
⌉etwa gleich
dann in n−12 und 2 Elemente.
(⌊ ⌋) (⌈ ⌉)
n−1 n−1
T (n) = T +T + cn,
2 2
T (1) = b.
⌊ n−1 ⌋ ⌊ n ⌋ ⌈ ⌉ ⌊n⌋
Es gilt 2 ≤ 2 und n−1 2 = 2 . Da T monoton wachsend ist, gilt
(⌊ n ⌋)
T (n) ≤ 2T + cn.
2
Wir ersetzen ≤ durch = und erhalten mit Satz 1.28 die Formel der Behaup-
tung. 2
QuickSort ist im besten Fall wesentlich effizienter als die einfachen Sortier-
methoden Sortieren durch Einfügen, Sortieren durch Auswählen und Bubble-
sort. Die Laufzeit dieser Methoden ist von der Ordnung O(n2 ). Im schlech-
testen Fall ist die Laufzeit von QuickSort auch von der Ordnung O(n2 ).
Satz 2.4. Für die Laufzeit T (n) von QuickSort gilt im schlechtesten Fall
c (c )
T (n) = n2 + + b n − c.
2 2
b und c sind konstant. Insbesondere gilt T (n) = O(n2 ).
2.1 Quicksort 81
Beweis. Der schlechteste Fall tritt ein, wenn in jedem Rekursionsschritt der
Zerlegungsprozess eine 0-elementige und eine (n − 1)–elementige Menge lie-
fert.
T (n) = T (n − 1) + T (0) + cn, n ≥ 2, T (0) = T (1) = b,
besitzt nach Satz 1.15 die Lösung
∑
n ( )
n(n + 1)
T (n) = b + (ci + b) = bn + c −1
i=2
2
c (c )
= n2 + + b n − c.
2 2
Dies zeigt die Behauptung. 2
Bemerkung. In Algorithmus 2.1 tritt der schlechteste Fall für ein sortiertes
Array ein.
π ist eine Permutation auf {1, . . . , i − 1} und π̃ ist eine Permutation auf
{i + 1, . . . , n}.
Wir bestimmen jetzt die Anzahl der Anordnungen von {a1 , . . . , an } die
nach der Zerlegung zur Folge (2.1) führen. Für jede Wahl von j Positionen
in (1, . . . , i − 1) und von j Positionen in (i + 1, . . . , n), j ≥ 0, ist genau eine
Folge bestimmt, die beim Partitionieren j Umstellungen benötigt und nach
dem Partitionieren die Folge (2.1) als Ergebnis hat. Es gibt deshalb
( )( )
i−1 n−i
(2.2)
j j
m (
∑ )( ) ( )
i−1 n−i n−1
l= =
j=0
j j i−1
(Lemma B.18). Die Zahl l ist unabhängig von der Anordnung der Folge (2.1).
Für alle Permutationen π auf {1, . . . , i − 1} und für alle Permutationen π̃
auf {i + 1, . . . , n} erhalten wir dieselbe Zahl l. Deshalb sind alle Anordnun-
gen auf {a1 , . . . , ai−1 } und auf {ai+1 , . . . , an−1 } gleich wahrscheinlich. Die
Gleichverteilung auf {a1 , a2 , . . . , an } führt daher zur Gleichverteilung auf
{a1 , . . . , ai−1 } und auf {ai+1 , . . . , an }.
Die Überlegungen zur durchschnittlichen Laufzeit, der durchschnittlichen
Anzahl der Vergleiche und der durchschnittlichen Anzahl der Umstellungen
basieren auf [Knuth98a].
Durchschnittliche Anzahl der Vergleiche. Unser Quicksort-Algorith-
mus führt bei n Elementen n oder auch n + 1 viele Vergleiche durch. Bei
n Elementen kommt man aber mit n − 1 vielen Vergleichen aus. Wir müssen
das Pivotelement nur einmal mit jedem der übrigen n − 1 Elemente verglei-
chen. Dazu müssen wir im Algorithmus die Indizes kontrollieren. Wir gehen
bei der Bestimmung der durchschnittlichen Anzahl der Vergleiche deshalb
von einem optimierten Algorithmus aus, der mit n − 1 vielen Vergleichen
auskommt (siehe die Bemerkungen nach dem Beweis von Satz 2.2). Die Be-
rechnung der Anzahl der durchschnittlichen Vergleiche für den Algorithmus
2.1 ist eine Übungsaufgabe (Aufgabe 4).
V (n) bezeichne die durchschnittliche Anzahl der Vergleiche und Ṽ (n, i) die
durchschnittliche Anzahl der Vergleiche, falls das i–te Element Pivotelement
ist. Wir erhalten
Ṽ (n, i) = V (i − 1) + V (n − i) + n − 1.
1∑
n
V (n) = Ṽ (n, i)
n i=1
1∑
n
= (V (i − 1) + V (n − i) + n − 1)
n i=1
2∑
n−1
= V (i) + n − 1, n ≥ 2,
n i=0
∑n−1
Ziel ist, die Rekursion V (n) = n2 i=0 V (i) + n − 1 durch eine geeignete
Substitution in eine Differenzengleichung zu transformieren. Bei Rekursionen,
bei denen das n–te Glied von der Summe aus allen Vorgängern abhängt,
gelingt dies mit der Substitution
∑
n
xn = V (i).
i=0
2.1 Quicksort 83
Dann gilt
2
xn − xn−1 = xn−1 + n − 1.
n
Wir erhalten die Differenzengleichung
x1 = V (0) + V (1) = 0,
n+2
xn = xn−1 + n − 1, n ≥ 2.
n
Diese Gleichung besitzt die Lösung
( )
3 5
xn = (n + 1)(n + 2) Hn+1 + −
n+2 2
(siehe Seite 16, Gleichung (D 1)). Wir erhalten
2
V (n) = xn − xn−1 = xn−1 + n − 1 = 2(n + 1)Hn − 4n.
n
Wir fassen das Ergebnis im folgenden Satz zusammen.
Satz 2.5. Für die durchschnittliche Anzahl V (n) der Vergleiche in Quicksort
gilt für n ≥ 1
V (n) = 2(n + 1)Hn − 4n.
Bei der Berechnung der durchschnittlichen Anzahl mitteln wir über alle An-
ordnungen des zu sortierenden Arrays.
Durchschnittliche Anzahl der Umstellungen. U (n) bezeichne die durch-
schnittliche Anzahl der Umstellungen und Ũ (n, i) die durchschnittliche An-
zahl der Umstellungen in Zeile 8, falls das i–te Element Pivotelement ist. Wir
berechnen zunächst Ũ (n, i) für n ≥ 2.
Sei L = {1, . . . , i − 1} und R = {i + 1, . . . , n}. Zur Berechnung der Wahr-
scheinlichkeit p(Ũ (n, i) = j) für j viele Umstellungen betrachten wir folgen-
des Experiment. Wir ziehen (ohne Zurücklegen) i−1 viele Zahlen z1 , . . . , zi−1
aus L ∪ R und setzen a[k] = zk , k = 1, . . . , i − 1. Das Ergebnis unseres Ex-
periments erfordert j Umstellungen, wenn j der Zahlen aus R und i − 1 − j
der Zahlen aus L gezogen wurden. Dies ist unabhängig von der Reihenfolge,
in der wir die Zahlen ziehen. Aus diesem Grund gilt
( )( )
n−i i−1
j i−1−j
p(Ũ (n, i) = j) = ( ) .
n−1
i−1
1∑
n
n+4
E(Ũ (n, i) + 1) = .
n i=1 6
1∑
n
E(Ũ (n, i) + 1)
n i=1
n ( )
1 ∑ (i − 1)(n − i)
= +1
n i=1 n−1
1 ∑ n
= ((n + 1)i − n − i2 ) + 1
(n − 1)n i=1
( )
1 n(n + 1)2 n(n + 1)(2n + 1)
= −n −2
+1
(n − 1)n 2 6
1 n+4
= (n − 1)(n − 2) + 1 = .
6(n − 1) 6
1∑
n
U (n) = Ũ (n, i)
n i=1
1∑
n
(n − i)(i − 1)
= (U (i − 1) + U (n − i) + + 1)
n i=1 n−1
2∑
n−1
n+4
= U (i) + , n ≥ 2.
n i=0 6
∑n
Analog zu oben ergibt sich mit der Substitution xn = i=0 U (i)
2 n+4
xn − xn−1 = xn−1 +
n 6
und
2.1 Quicksort 85
x1 = U (0) + U (1) = 0,
n+2 n+4
xn = xn−1 + , n ≥ 2.
n 6
Diese Gleichung besitzt die Lösung
( )
(n + 1)(n + 2) 2 5
xn = Hn+1 − −
6 n+2 6
2.1.2 Speicherplatzanalyse
Speicher auf dem Stack. Die Rekursionstiefe bezeichnet die maximale An-
zahl der aktiven Aufrufe einer Funktion. Bei der Ausführung einer rekursiven
Funktion steigt der Speicherplatzverbrauch linear mit der Rekursionstiefe.
Mit S(n) bezeichnen wir die Rekursionstiefe von QuickSort, in Abhängig-
keit von der Anzahl n der zu sortierenden Elemente.
Satz 2.8. Für die Rekursionstiefe S(n) von QuickSort gilt:
{
≤ ⌊log2 (n)⌋ + 1 im besten Fall,
S(n)
=n im schlechtesten Fall.
Wir können jede rekursive Funktion in eine Iteration umwandeln, indem wir
einen Stack explizit aufsetzen und dadurch den Stack ersetzen, den das Be-
triebssystem bereitstellt und den Funktionsaufrufe implizit verwenden. Quick-
sort jedoch können wir iterativ programmieren, ohne zusätzlich einen Stack
2.1 Quicksort 87
x
.
x y
i l j
4. Wir rufen QuickSort zuerst für den rechten Teil a[l..j] auf. Nachdem
der Aufruf von QuickSort für diesen Teil terminiert, ermitteln wir den
Anfangsindex für den linken Teil wie in Punkt 3 und verarbeiten den
linken Teil weiter.
5. QuickSort hat jetzt nur noch einen rekursiven Aufruf ganz am Ende – eine
Endrekursion. Wie oben beschrieben, ist dieser einfach zu eliminieren.
Wir haben gezeigt, dass die durchschnittliche Laufzeit von QuickSort von der
Ordnung O(n log2 (n)) ist. Dabei mitteln wir über alle Anordnungen der zu
sortierenden Folge. Dies impliziert, dass wir bei zufälliger Wahl der Eingabe
88 2. Sortieren und Suchen
eine Laufzeit von der Ordnung O(n log2 (n)) erwarten. Diese Voraussetzung
ist in der Anwendung unrealistisch. Die Anordnung der Eingabe ist vorge-
geben. Die Idee ist nun, die Annahme über die Zufälligkeit der Anordnung
durch Zufall im Algorithmus“ zu ersetzen. Diese Art von Zufall können wir
”
immer garantieren. Die naheliegende Idee, auf die Eingabe erst eine Zufalls-
permutation anzuwenden, erfordert die Berechnung einer Zufallspermutation.
Dazu müssen wir bei einem Array der Länge n mindestens n − 1 mal eine
Zufallszahl ermitteln. Unter Umständen sind n Umstellungen notwendig. We-
niger Aufwand erfordert es, das Pivotelement zufällig zu wählen. Hier müssen
wir für jeden Aufruf nur eine Zufallszahl ermitteln. In der probabilistischen
Version von Quicksort machen wir dies.
Zu jeder deterministischen Methode das Pivotelement zu bestimmen, gibt
es eine Reihenfolge der Elemente, für die der schlechteste“ Fall – d. h. die
”
Laufzeit ist von der Ordnung O(n2 ) – eintritt. In der probabilistischen Version
von Quicksort gibt es solche schlechten Eingaben nicht mehr. Die Wahrschein-
lichkeit, dass wir in jedem rekursiven Aufruf das schlechteste“ Pivotelement
”
wählen, ist 1/n!, wobei n die Anzahl der Elemente in der zu sortierenden Folge
ist. Wegen dieser kleinen Wahrscheinlichkeit erwarten wir, dass die probabi-
listische Version von QuickSort immer gutes Laufzeitverhalten aufweist.
Algorithmus 2.9.
ProbQuickSort(item a[i..j])
1 item x; index l, r
2 if i < j
3 then exchange a[j] and a[Random(i, j)]
4 x ← a[j], l ← i, r ← j − 1
5 while true do
6 while a[l] < x do l ← l + 1
7 while a[r] > x do r ← r − 1
8 if l < r
9 then exchange a[l] and a[r]
10 l ← l + 1, r ← r − 1
11 else break
12 exchange a[l] and a[j]
13 ProbQuickSort(a[i..l − 1])
14 ProbQuicksort(a[l + 1..j])
Random(i, j) gibt eine Zufallszahl p mit i ≤ p ≤ j zurück. Wir nehmen an,
dass alle Elemente im Array a verschieden sind. Wenn wir das Pivotelement
zufällig wählen, beschreibt die Zufallsvariable R, die Werte aus {1, . . . , n}
annehmen kann, die Wahl des Pivotelements. R sei gleichverteilt, d. h. p(R =
r) = n1 . Der Erwartungswert der Laufzeit Tn von ProbQuickSort berechnet
sich
∑ n
E(Tn ) = E(Tn | R = r)p(R = r)
r=1
(Lemma A.9). Aus
2.2 Heapsort 89
Tn | (R = r) = Tr−1 + Tn−r + cn
Es folgt
∑
n
E(Tn ) = E(Tn | R = r)p(R = r)
r=1
1∑
n
= (E(Tr−1 ) + E(Tn−r ) + cn)
n r=1
2∑
n−1
= cn + E(Tr ), n ≥ 2.
n r=0
(2) ∑n 1 3
wobei Hn = i=1 i2 . Die umfangreiche Herleitung der Formel benutzt
Erzeugendenfunktionen (siehe [IliPen10]).
Bei der Berechnung des Erwartungswertes der Anzahl der Umstellungen
berücksichtigen wir die Umstellung in Zeile 3. Wir erhalten
1 7 41
E(Un ) = (n + 1)Hn + n − , n ≥ 2.
3 18 18
2.2 Heapsort
Heapsort zählt zu den Sortiermethoden Sortieren durch Auswählen. Beim
Sortieren durch Auswählen suchen wir ein kleinstes Element x in der zu sor-
tierenden Folge F . Wir entfernen x aus F und wenden die Sortiermethode
3 ∑∞ 1
Im Gegensatz zur harmonischen Reihe konvergiert die Reihe i=1 i2 . Ihr Grenz-
2
wert ist π /6.
90 2. Sortieren und Suchen
rekursiv auf F ohne x an. Beim einfachen Sortieren durch Auswählen ver-
wenden wir die naive Methode zur Bestimmung des Minimums – inspiziere
nacheinander die n Elemente der Folge. Die Laufzeit dieser Methode ist von
der Ordnung O(n). Heapsort verwendet die Datenstruktur des binären Heaps.
Dadurch verbessert sich die Laufzeit der Funktion zur Bestimmung des Mini-
mums wesentlich (von der Ordnung O(n) auf die Ordnung O(log2 (n))). Der
Algorithmus Heapsort wurde nach Vorarbeiten durch Floyd4 von Williams5
in [Williams64] veröffentlicht. Wir betrachten zunächst die Datenstruktur ei-
nes binären Heaps.
Definition 2.11. In einem Array h[1..n] seien Elemente einer total geordne-
ten Menge abgespeichert.
1. h heißt (binärer) Heap, wenn gilt:
⌊n⌋ ⌊ ⌋
n−1
h[i] ≤ h[2i], 1 ≤ i ≤ , und h[i] ≤ h[2i + 1], 1 ≤ i ≤ .
2 2
2. Der Heap h[1..n] erhält die Struktur eines binären Baumes (Definition
4.3), indem wir h[1] als Wurzel erklären. Für i ≥ 1 und 2i ≤ n ist das
Element h[2i] linker und für 2i + 1 ≤ n ist h[2i + 1] rechter Nachfolger
von h[i]. Mit dieser Baumstruktur lautet die Heapbedingung für h: Sind
n1 und n2 Nachfolger von k, so gilt: ni ≥ k, i = 1, 2.
Beispiel. In Figur 2.4 unterlegen wir der Folge 6, 41, 10, 56, 95, 18, 42, 67
die Struktur eines binären Baumes.
6.
41 10
56 95 18 42
67
4
Robert W. Floyd (1936 – 2001) war ein amerikanischer Informatiker und Turing-
Preisträger.
5
John William Joseph Williams (1929 – 2012) war ein britisch-kanadischer Infor-
matiker.
2.2 Heapsort 91
Bemerkungen:
1. Alternativ beschreibt sich die Baumstruktur wie folgt: Das erste Ele-
ment h[1] ist die Wurzel. Anschließend sortieren wir die nachfolgenden
Elemente nacheinander von links nach rechts und von oben nach unten in
die Ebenen des Baumes ein. Wir erhalten einen binären Baum minimaler
Höhe, und die Blätter befinden sich auf höchstens zwei Ebenen. Die resul-
tierende Pfadlänge von der Wurzel zu einem Blatt ist durch ⌊log2 (n)⌋ − 1
beschränkt (Lemma 2.16).
2. Ist a ein beliebiges Array,⌊ so⌋ erfüllen die Blätter – die Knoten, die keine
Nachfolger besitzen (i > n2 ) – die Heapbedingung.
3. Ein Heap h[1..n] ist längs eines jeden seiner Pfade sortiert. Insbesondere
gilt h[1] ≤ h[j], 1 ≤ j ≤ n, d.h. h[1] ist das Minimum.
Der Algorithmus DownHeap (Algorithmus 2.12) ist zentraler und wesent-
licher Bestandteil für Heapsort. DownHeap beruht auf der folgenden Beob-
achtung: Ist in h die Heapbedingung nur in der Wurzel verletzt, so können wir
durch Einsickern“ – ein einfaches effizientes Verfahren – die Heapbedingung
”
für das gesamte Array herstellen. Einsickern bedeutet: Vertausche solange
mit dem kleineren Nachfolger, bis die Heapbedingung hergestellt ist.
Algorithmus 2.12.
DownHeap(item a[l..r])
1 index i, j; item x
2 i ← l, j ← 2 · i, x ← a[i]
3 while j ≤ r do
4 if j < r
5 then if a[j] > a[j + 1]
6 then j ← j + 1
7 if x ≤ a[j]
8 then break ;
9 a[i] ← a[j], i ← j, j ← 2 · i
10 a[i] ← x
Bemerkungen:
1. In Downheap folgen wir einem Pfad, der in a[l] startet. Den aktuellen
Knoten indizieren wir mit j und den Vorgänger mit i.
2. Die Zeilen 5 und 6 machen den kleineren Nachfolger zum aktuellen Kno-
ten.
3. Falls x größer als der aktuelle Knoten ist, kopieren wir den aktuellen
Knoten a[j] im durchlaufenen Pfad eine Position nach oben und machen
den aktuellen Knoten zum Vorgänger und den Nachfolger zum aktuellen
Knoten (Zeile 9). Es entsteht eine Lücke im durchlaufenen Pfad.
4. Falls x kleiner oder gleich dem aktuellen Knoten a[j] ist, kann x an der
Stelle i stehen. Die Einfügestelle i ist ermittelt. Wir weisen x an a[i] zu
92 2. Sortieren und Suchen
(Zeile 10). An der Position i befindet sich eine Lücke. Die Heapbedingung
in a ist wiederhergestellt.
Beispiel. Im linken Baum ist die Heapbedingung nur in der Wurzel verletzt.
Figur 2.5 zeigt, wie wir durch Einsickern der 60 die Heapbedingung herstellen.
Wir bewegen die 60 auf dem Pfad 60-37-45-58 solange nach unten, bis die
Heapbedingung hergestellt ist. Dies ist hier erst der Fall, nachdem 60 in
einem Blatt lokalisiert ist.
.
60 .
37
37 40 45 40
45 57 42 41 58 57 42 41
59 58 59 60
.
50
40 7
8 9 18 27
10 30 17 33
Der Heapaufbau erfolgt mit DownHeap von unten nach oben und von
rechts nach links. Bei dieser Reihenfolge ist der Knoten 40 der erste Kno-
ten, in dem die Heapbedingung verletzt ist. Wir stellen die Heapbedingung
im Teilbaum mit Wurzel 40 durch Einsickern her. Das Ergebnis ist im ers-
ten Baum der Figur 2.7 dargestellt. Jetzt ist die Heapbedingung nur noch
2.2 Heapsort 93
in der Wurzel verletzt. Der zweite Baum zeigt das Ergebnis, nachdem die
Heapbedingung im ganzen Baum hergestellt ist.
.
50 7.
8 7 8 18
10 9 18 27 10 9 50 27
40 30 17 33 40 30 17 33
Wir geben jetzt nach der Betrachtung des Beispiels zum Heapaufbau den
Algorithmus für den allgemeinen Fall an.
Algorithmus 2.13.
BuildHeap(item a[1..n])
1 index l
2 for l ← n div 2 downto 1 do
3 DownHeap(a[l..n])
Nachdem ein Heap aufgebaut ist, findet in der zweiten Phase der eigentli-
che Sortiervorgang statt. Das Minimum befindet sich nach der ersten Phase
an der ersten Position von a. Wir vertauschen jetzt das erste Element mit
dem letzten Element und betrachten nur noch die ersten n − 1 Elemente in
a. Die Heapbedingung ist jetzt nur in der Wurzel verletzt. Wir stellen die
Heapbedingung durch Einsickern der Wurzel wieder her (DownHeap). Wir
setzen das Verfahren rekursiv fort und erhalten die Elemente in umgekehrter
Reihenfolge sortiert.
Beispiel. Sortierphase von Heapsort:
6 41 10 56 95 18 42 67
67 41 10 56 95 18 42 |6
10 41 18 56 95 67 42 |6
42 41 18 56 95 67 |10 6
18 41 42 56 95 67 |10 6
..
.
Das Beispiel startet mit einem Heap und zeigt die ersten beiden Sortierschrit-
te mit anschließendem Einsickern der Wurzel.
94 2. Sortieren und Suchen
Algorithmus 2.14.
HeapSort(item a[1..n])
1 index l, r
2 for l ← n div 2 downto 1 do
3 DownHeap(a[l..n])
4 for r ← n downto 2 do
5 exchange a[1] and a[r]
6 DownHeap(a[1..r − 1])
2.2.3 Laufzeitanalyse
HeapSort (Algorithmus 2.14) besteht aus zwei for-Schleifen. In jeder der bei-
den for-Schleifen wird die Funktion DownHeap (Algorithus 2.12) aufgerufen.
Die Analyse von HeapSort erfordert demzufolge die Analyse von DownHeap.
Die Laufzeit von HeapSort hängt wesentlich von der Anzahl der Iterationen
der while-Schleife in DownHeap ab. Diese erfassen wir mit den Zählern I1
und I2 . I1 (n) gibt an, wie oft die while-Schleife in Downheap iteriert wird,
für alle Aufrufe von HeapSort in der Zeile 3. I2 zählt dasselbe Ereignis für alle
Aufrufe in Zeile 6. I1 (n) ist auch die Anzahl der Iterationen der while-Schleife
in Downheap, akkumuliert über alle Aufrufe durch BuildHeap (Algorithmus
2.13, Zeile 3). Wir geben Abschätzungen für I1 (n) und I2 (n) an.
Zur Analyse der Laufzeit von BuildHeap benötigen wir das folgende Lem-
ma.
Lemma 2.16. Sei a[1..r] Input für DownHeap (Algorithus 2.12), 1 ≤ l ≤ r,
und sei k die Anzahl der Iterationen der while-Schleife in DownHeap. Dann
gilt ⌊ ( r )⌋
k ≤ log2 .
l
Beweis. Den längsten Pfad erhalten wir mit der Folge l, 2l, 22 l, . . . , 2k̃ l, wobei
k̃ maximal mit 2k̃ l ≤ r ist. Es gilt
⌊ ( r )⌋
k̃ = log2 .
l
Da die Anzahl der Iterationen der while-Schleife in DownHeap durch die
Länge des Pfads beschränkt ist, der im Knoten a[l] beginnt, gilt die Abschätz-
ung auch für die Anzahl der Iterationen. Aus k ≤ k̃ folgt die Behauptung.
2
2.2 Heapsort 95
Satz 2.17.
1. Für die Anzahl I1 (n) der Iterationen der while-Schleife in DownHeap,
akkumuliert über alle Aufrufe von Downheap in BuildHeap, gilt
⌊n⌋ (⌊ n ⌋)
I1 (n) ≤ 3 − log2 − 2.
2 2
2. Für die Laufzeit T (n) von BuildHeap im schlechtesten Fall gilt
∑
n−1 ( )
I2 (n) ≤ ⌊log2 (r)⌋ = n⌊log2 (n − 1)⌋ − 2 2⌊log2 (n−1)⌋ − 1
r=1
≤ n⌊log2 (n − 1)⌋ − n + 2.
⌊n⌋
6
Für gerades n gilt Gleichheit. Für ungerades n lässt sich der Term durch 3 −
(⌊ ⌋) 2
log2 n2 − 1 abschätzen.
96 2. Sortieren und Suchen
Algorithmus 2.20.
index DownHeapO(item a[l..r])
1 index i, j; item x
2 i ← l, j ← 2 · i, x ← a[i]
3 while j ≤ r do
4 if j < r
5 then if a[j] > a[j + 1]
6 then j ← j + 1
7 i ← j, j ← 2 · i
8 return i
2.2 Heapsort 97
Bemerkungen:
1. In DownHeapO indizieren wir den aktuellen Knoten mit j und den
Vorgänger mit i.
2. In der while-Schleife folgen wir dem Pfad des kleineren Nachfolgers bis
zum Blatt, das nach Terminierung der Schleife von i indiziert wird.
Beispiel. Figur 2.8 zeigt den Pfad des kleineren Nachfolgers mit den Indizes
1, 3, 6, 13, 27 und 55. Die tiefer gestellte Zahl gibt den Index des jeweiligen
Elements an.
15 .| 1
12 5|3
7|6 9
13 11 | 13
17 15 | 27
50 43 | 55
Wir bezeichnen die Indizes des Pfades von der Wurzel bis zu einem Blatt
mit v1 , . . . , vℓ . Es gilt
⌊v ⌋
k
vk−1 = , 2 ≤ k ≤ ℓ, v1 = 1.
2
Wir erhalten die Binärentwicklung von vk−1 , wenn wir in der Binärentwick-
lung von vk die letzte Stelle streichen. Der Index k von vk gibt auch die
Anzahl der Binärstellen von vk an. Der k–te Knoten vk im Pfad ist durch
die k höherwertigen Bits von vℓ gegeben. Deshalb können wir auf jeden Kno-
ten des Pfades unmittelbar zugreifen, wenn der Index des letzten Knotens
bekannt ist.
Wir diskutieren zwei Varianten zur Bestimmung der Einfügestelle.
Sequenzielle Suche. Wenn die Einfügestelle im letzten Teil des Pfades, d. h.
in einer unteren Ebene des Baumes liegt, finden wir die Einfügestelle durch
sequenzielle Suche vom Ende des Pfades nach wenigen Schritten.
Diese Modifikation wirkt sich in der zweiten Phase von Heapsort positiv
aus. In der zweiten Phase kopieren wir in jedem Schritt ein großes Element
in die Wurzel. Anschließend sickert dieses Element tief in den Baum ein. Die
98 2. Sortieren und Suchen
Einfügestelle liegt im letzten Teil des Pfades. Diese Variante von DownHeap
folgt einer Idee von Wegener7 ([Wegener93]).
Algorithmus 2.21.
index SequentialSearch(item a[1..n], index v)
1 x ← a[1]
2 while x < a[v] do v ← v div 2
3 return v
SequentialSearch ermittelt ausgehend vom Endknoten des Pfads des kleineren
Nachfolgers die Einfügestelle für x durch sequenzielle Suche.
Die folgende Funktion Insert fügt x in den Pfad an der Position v ein. Der
Index v ist durch seine Binärentwicklung v = w1 . . . wk gegeben.
Algorithmus 2.22.
Insert(item a[1..n], index v = w1 . . . wk )
1 int j; item x ← a[1]
2 for j ← 2 to k do
3 a[w1 ..wj−1 ] ← a[w1 ..wj ]
4 a[v] ← x
Bemerkungen:
1. In der for-Schleife schieben wir die Elemente auf dem Pfad des kleineren
Nachfolgers, den wir mit P bezeichnen, um eine Position nach oben. Dazu
durchlaufen wir P nochmals von oben nach unten und berechnen die
Knoten auf P aus dem Index des Endknotens v. Der Index des i–ten
Knotens von P ist durch die i höherwertigen Bits von v gegeben.
2. In DownHeap finden in jedem Knoten zwei Vergleiche statt. In
DownHeapO findet nur ein Vergleich statt. Ist t die Anzahl der Kno-
ten in P und t̃ die Anzahl der Knoten bis zur Einfügestelle. Dann finden
in DownHeap 2t̃ Vergleiche und in DownHeapO mit SequentialSearch
finden zusammen t + t − t̃ = 2t − t̃ viele Vergleiche statt. Wenn die
Einfügestelle im letzten Drittel von P liegt, ist die Anzahl der Vergleiche
in DownHeapO und SequentialSearch kleiner, denn es gilt 2t − t̃ < 2t̃
genau dann, wenn t̃ > 23 t ist.
3. Heapsort erfordert im Durchschnitt 2n log2 (n) − O(n) Vergleiche. Mit
DownHeapO und SequentialSearch sind es nur n log2 (n) + O(n) Verglei-
che. Die Analysen sind jedoch kompliziert (siehe [Wegener93]).
Binäre Suche. Wir bezeichnen die Indizes des Pfades von der Wurzel bis
zum Blatt mit v1 , . . . , vℓ . Die Folge a[v2 ], . . . , a[vℓ ] ist aufsteigend sortiert. Wir
ermitteln in dieser Folge die Einfügestelle durch binäre Suche. Ausgehend von
vℓ berechnen wir vℓ−⌊ℓ/2⌋
7
Ingo Wegener (1950 – 2008) war ein deutscher Informatiker.
2.2 Heapsort 99
⌊ v ⌋
ℓ
vℓ−⌊ℓ/2⌋ = = w1 . . . wℓ−⌊ℓ/2⌋ ,
2⌊ℓ/2⌋
wenn vℓ die Binärentwicklung vℓ = w1 . . . wℓ besitzt.
Der folgende Algorithmus ermittelt mit der Methode der binären Suche
(Übungen, Algorithmus 2.36) den Index der Einfügestelle auf dem Pfad, der
von der Wurzel zum Knoten v führt. Der Knoten v ist durch seine Binärent-
wicklung v = w1 . . . wk gegeben.
Algorithmus 2.23.
index BinarySearch(item a[1..n], index v = w1 . . . wk )
1 index l, r; item x
2 l ← 2, r ← k, x ← a[w1 ]
3 while l <= r do
4 m ← (l + r) div 2
5 if a[w1 ..wm ] < x
6 then l ← m + 1
7 else r ← m − 1
8 return w1 . . . wl−1
Satz 2.24. Der Algorithmus 2.23 berechnet die Einfügestelle für x = a[1].
Beweis. Sei w1 . . . wi die Einfügestelle für x. Die Invariante der while-Schleife
ist
l − 1 ≤ i < r.
Wir zeigen durch Induktion nach der Anzahl der Iterationen von while, dass
die Invariante gilt. Die Behauptung gilt für l = 2 und r = k, also für 0
Iterationen. Wir betrachten für j ≥ 1 die j–te Iteration von while. Mit lj−1
und rj−1 bzw. lj und rj bezeichnen wir die Werte von l und r nach der
(j − 1)–ten bzw. j–ten Iteration. Mit (∗) bezeichnen wir die Bedingung l = r.
Wir betrachten zunächst den Fall, dass lj−1 und rj−1 die Bedingung
(∗) erfüllen. Nach der Induktionshypothese gilt lj−1 − 1 ≤ i < rj−1 . Aus
a[w1 ..wm ] < x folgt m ≤ i und lj = m + 1, also folgt lj − 1 ≤ i. Weiter gilt
lj = rj−1 + 1 = rj + 1. Aus a[w1 ..wm ] ≥ x folgt i < m. Wegen lj = lj−1 gilt
lj ≤ i. Wir setzen in Zeile 7 rj = m−1, also gilt lj = lj−1 = m = rj +1. Die In-
variante gilt demnach auch für die j–te Iteration von while (für a[w1 ..wm ] < x
und a[w1 ..wm ] ≥ x). Weiter gilt lj > rj und while terminiert im nächsten
Schritt mit rj = lj − 1, also folgt nach Terminierung von while i = l − 1.
Falls lj−1 < rj−1 gilt, folgt 2lj−1 < lj−1 + rj−1 < 2rj−1 und 2lj−1 ≤
rj−1 + lj−1 − 1 < 2rj−1 , falls rj−1 + lj−1 ungerade ist. Insgesamt ergibt sich
lj−1 ≤ m < rj−1 . Es folgt entweder lj = rj und in der nächsten Iteration der
Schleife tritt (∗) ein oder lj < rj und rj − lj < rj−1 − lj−1 . Da der Abstand
r − l mit jeder Iteration abnimmt, falls l ̸= r gilt, muss der Fall (∗) eintre-
ten. Aus (∗) und der Invariante der while-Schleife folgt, dass w1 . . . wl−1 die
Einfügestelle für x ist. 2
100 2. Sortieren und Suchen
Beispiel. Figur 2.9 zeigt das Ermitteln der Einfügestelle für a[1] = 15 mit
Algorithmus 2.23. Der Algorithmus terminiert mit l = 5 und w1 w2 w3 w4 =
1101 = 13.
15 .| 1
55 = 1101
| {z } 1 1
12 5|3 13
| {z }
27
7|6 9 while-Iteration 0 1 2 3
m 4 5 5
13 11 | 13 w1 . . . w m 13 27 27
a[w1 ..wm ] 11 15 15
l 2 5 5 5
17 15 | 27
r 6 6 5 4
50 43 | 55
Wir schätzen jetzt die Anzahl der wesentlichen Vergleiche bei Anwendung
von DownHeapO und binärer Suche der Einfügestelle ab. Sei V2 (n) die Anzahl
der wesentlichen Vergleiche in der Sortierphase von Heapsort. Dann gilt
∑
n−1 ∑
n−1
V2 (n) ≤ ⌊log2 (r)⌋ + (⌊log2 (⌊log2 (r)⌋)⌋ + 1)
r=2 r=2
≤ n(⌊log2 (n − 1)⌋ − 1) + 3 + (n − 2)(⌊log2 (log2 (n − 1))⌋ + 1)
= n⌊log2 (n − 1)⌋ + (n − 2)(⌊log2 (log2 (n − 1))⌋) + 1.
Die Anzahl der Elemente in einem Pfad ist ≤ ⌊log2 (r)⌋. Bei binärer Suche
benötigen wir deshalb nur ⌊log2 (⌊log2 (r)⌋)⌋ + 1 viele wesentliche Vergleiche
(Übungen, Algorithmus 2.36). Die erste Summe haben wir bereits abgeschätzt.
Die zweite Abschätzung folgt unmittelbar. Da die Anzahl der Vergleiche in
der Heapaufbauphase linear ist, schätzen wir diese nicht genauer ab. Die
Anzahl V (n) der wesentlichen Vergleiche schätzt sich durch
( ⌊n⌋ (⌊ n ⌋) )
V (n) ≤ 2 3 − log2 −2 +
2 2
n⌊log2 (n − 1)⌋ + (n − 2)(⌊log2 (log2 (n − 1))⌋) + 1
nach oben ab.
a <. b
b<c a<c
Die inneren Knoten des Baumes enthalten die Vergleiche, die Blätter
des Baumes alle möglichen Anordnungen. Jede Anordnung erfordert die
Durchführung der Vergleiche, die auf dem Pfad von der Wurzel zum jeweili-
gen Blatt liegen. Ist das Ergebnis eines Vergleichs wahr, so ist der nächste
Knoten des Pfades der linke Nachfolger, sonst ist es der rechte Nachfolger.
Ein Algorithmus, der die sortierte Reihenfolge aufgrund von Vergleichen
herstellt, muss die Vergleiche durchführen, die auf dem Pfad von der Wurzel
zum Blattknoten liegen, der die sortierte Reihenfolge angibt. Die Anzahl
der Vergleiche stimmt deswegen mit der Länge des entsprechenden Pfades im
Entscheidungsbaum überein. Eine untere Schranke für die Länge eines Pfades,
der die Wurzel mit einem Blatt im binären Entscheidungsbaum verbindet, ist
eine untere Schranke für die Anzahl der Vergleiche, die ein Sortierverfahren,
das nur eine geordnete Menge voraussetzt, durchführen muss.
Lemma 2.25. Sei B ein binärer Baum mit n Blättern. Dann ist die maxi-
male Länge eines Pfades von der Wurzel zu einem Blatt ≥ log2 (n).
Beweis. Sei tB die Länge eines längsten Pfades von der Wurzel zu einem
Blatt im Baum B. Dann gilt n ≤ 2tB . Hieraus folgt tB ≥ log2 (n). 2
Satz 2.26. Ein vergleichsbasierter Algorithmus benötigt beim Sortieren von n
Elementen im schlechtesten Fall mindestens n log2 (n)−O(n) viele Vergleiche.
Beweis. Die Anzahl der Vergleiche V ist gleich der Länge eines Pfades im
Entscheidungsbaum mit n! Blättern. Mit der Stirlingschen8 Näherungsformel
8
James Stirling (1692–1770) war ein schottischer Mathematiker.
2.4 Suchen in Arrays 103
√ ( n )n ( n )n
n! ≈ 2πn >
e e
folgt für die Anzahl V (n) der Vergleich im schlechtesten Fall
V (n) ≥ log2 (n!) > n log2 (n) − n log2 (e) = n log2 (n) − O(n).
Bemerkungen:
1. Wir setzen an der n–ten Stelle im Array a das gesuchte Element x als
Wächterelement (n ≥ 1). Das Wächterelement verhindert, dass wir mit
Indizes > n auf a zugreifen, wenn x nicht in a gespeichert ist.
2. Die Funktion SequSearch sucht x in a und ermittelt den kleinsten Index
l mit x = a[l]. SequSearch gibt n zurück, falls x nicht gespeichert ist.
104 2. Sortieren und Suchen
Der folgende Algorithmus für binären Suche, BinSearch, sucht ein Element
x in einem sortierten Array a mit n Elementen. Er folgt der Divide-and-
Conquer-Strategie (Abschnitt 1.5.2). Wir vergleichen das zu suchende Ele-
ment x mit dem mittleren Element a[i]. Falls der Vergleich a[i] = x ergibt,
ist das gesuchte Element gefunden. Falls x kleiner als a[i] ist, befindet sich
x links von a[i] und falls x größer a[i] ist, befindet es sich rechts von a[i].
Das Teilarray, in dem wir weiter suchen, ist etwa halb so groß wie das ur-
sprüngliche Array. Die Lösung des Problems reduziert sich auf die Lösung
des Teilproblems. Deshalb brauchen wir die Lösungen der Teilprobleme nicht
zusammenzusetzen.
Algorithmus 2.28.
index BinSearch(item a[0..n − 1], x)
1 index l, r, i
2 l ← 0, r ← n − 1
3 repeat
4 i ← (l + r) div 2
5 if a[i] < x
6 then l ← i + 1
7 else r ← i − 1
8 until a[i] = x or l > r
9 if a[i] = x
10 then return i
11 else return − 1
Bemerkungen:
1. In a[l..r] sind r − (l − 1) = r − l + 1 viele Elemente gespeichert. Der Index
i := (l + r) div 2 referenziert das mittlere Element“ in a[l..r]. BinSearch
”
gibt den Index für x zurück oder −1, falls x nicht in a ist.
2. Befinden sich gleiche Elemente im Array, so gibt BinSearch den Index
von irgendeinem der gleichen Elemente zurück. Wir können dann das
erste oder letzte unter gleichen Elementen einfach ermitteln.
3. In jeder Iteration der repeat-until-Schleife finden zwei Vergleiche mit Ar-
rayelementen statt. Eine andere Version der binären Suche (Übungen, Al-
gorithmus 2.36) optimiert die Anzahl der Vergleiche. Es finde nur noch
ein Vergleich pro Iteration statt. Insgesamt halbiert sich die Anzahl der
Vergleiche. Sie ist durch ⌊log2 (n)⌋ + 1 beschränkt.
2.4 Suchen in Arrays 105
Beispiel. Figur 2.12 gibt alle Zugriffspfade an, die beim Suchen der Elemente
in a[1..11] entstehen. Ein binärer Suchbaum (Definition 4.6) dient der Navi-
gation bei der Suche in a[1..11].
6.
3 9
1 4 7 10
2 5 8 11
Satz 2.29. Für die Anzahl I(n) der Iterationen der repeat-until-Schleife gilt
im schlechtesten Fall:
⌋) 2 (⌊ ≤
n n−1 n
viele Elemente.(⌊Es gilt ⌋) 2 und 2 = 2 . Da I monoton wach-
send ist, gilt I n−1
2 ≤I 2 . n
∑
n
E(T (n)) = E(T (n) | R = r)p(R = r) + cn
r=1
∑ 1 ∑ 1
= E(T (n) | R = r)
+ E(T (n) | R = r) + cn
n n
r∈I r ∈I
/
∑ ( (⌊ ⌋))
3n 1 ∑ 1
≤ E T + E(T (n)) + cn
4 n n
r∈I r ∈I
/
( (⌊ ⌋))
1 3n 1
= E T + E(T (n)) + cn.
2 4 2
Übungen.
1. a. Ein Array besteht aus Datensätzen mit einer Komponente, die nur 1
oder 2 enthält. Geben Sie einen Algorithmus an, der das Array nach
dieser Komponente in situ mit Laufzeit O(n) sortiert. Gibt es einen
Algorithmus, der das Array in situ mit Laufzeit O(n) sortiert, falls
die Elemente 1, 2 und 3 vorkommen?
b. Ein Array enthält Datensätze mit den Schlüsseln 1, 2, . . . , n. Geben
Sie einen Algorithmus an, der das Array in situ mit Laufzeit O(n)
sortiert.
2. Algorithmus 2.32.
BubbleSort(item a[1..n])
1 index i, j; item x
2 for i ← 1 to n − 1 do
3 for j ← n downto i + 1 do
4 if a[j] < a[j − 1]
5 then exchange a[j] and a[j − 1]
Zeigen Sie, dass der Algorithmus 2.32 korrekt ist und analysieren Sie die
Laufzeit und anschließend auch die Laufzeit von Sortieren durch Einfügen
(Algorithmus 1.57). Benutzen die folgende Aussage über den Mittelwert
von Inversionen.
Inversionen. (a[i], a[j]) ist eine Inversion, wenn i < j und a[j] < a[i]
gilt. In den beiden Algorithmen ist die Anzahl der Vertauschungen gleich
der Anzahl der Inversionen.
108 2. Sortieren und Suchen
Zum Beispiel besitzt (3,1,4,2) die Inversionen (3,1), (3,2) und (4,2).
Somit sind 3 Austauschoperationen notwendig. ( )
Gemittelt über alle Anordnungen gibt es 12 n2 = n(n−1) 4 viele Inversio-
nen, denn entweder ist (a[i], a[j]) eine Inversion in a oder im umgekehrt
angeordneten Array.
3. Ein Sortieralgorithmus wird als stabil bezeichnet, falls die Reihenfolge
gleicher Elemente nicht verändert wird. Welche der Algorithmen Sortie-
ren durch Einfügen, durch Auswählen, durch Austauschen, Quicksort und
Heapsort sind stabil?
4. Zeigen Sie, dass für die durchschnittliche Anzahl der Vergleiche V (n) im
Algorithmus 2.1
8n + 2
V (n) = 2(n + 1)Hn −
3
gilt.
5. Vermeiden Sie in Zeile 11 in Algorithmus 2.1 den unnötigen Aufruf von
exchange für l = j und zeigen Sie, dass für den modifizierten Algorithmus
für die durchschnittliche Anzahl der Umstellungen
1 5n + 8
U (n) = (n + 1)Hn −
3 18
gilt.
6. Zeigen Sie: Für die durchschnittliche Laufzeit T (n) von Quicksort gilt
1 1
T (n) = 2c(n + 1)Hn + (2b − 10c)n + (2b − c),
3 3
wobei b und c konstant sind. Der Durchschnitt wird über alle Anord-
nungen des zu sortierenden Arrays gebildet. Insbesondere gilt T (n) =
O(n ln(n)).
7. Wir betrachten eine Quicksort Variante, die von N. Lomuto stammt (sie-
he [CorLeiRivSte07]).
Algorithmus 2.33.
QuickSortVariant(item a[i..j])
1 item x, index l
2 if i < j
3 then x ← a[j], l ← i
4 for k ← i to j − 1 do
5 if a[k] ≤ x
6 then exchange a[k] and a[l]
7 l ←l+1
8 exchange a[l] and a[j]
9 QuickSort(a[i..l − 1])
10 QuickSort(a[l + 1..j])
Übungen 109
13. Entwickeln Sie eine Formel für die maximale Anzahl der Zuweisungen bei
Ausführung von HeapSort und bei Ausführung von HeapSort mit binärer
Suche.
14. Geben Sie ein Beispiel an, das zeigt, dass die obere Schranke
∑⌊ n2 ⌋ ⌊ ( n )⌋
l=1 log2 l in der Abschätzung für die Anzahl der Iterationen der
while-Schleife in DownHeap für alle Aufrufe in BuildHeap angenommen
wird (siehe Beweis von Satz 2.17).
15. Mergesort. Mergesort teilt das Array a[i..j] in etwa zwei gleich große
Teile und sortiert die beiden Teile rekursiv. Anschließend werden die bei-
den Teile zu einer sortierten Folge zusammengefügt.
Algorithmus 2.35.
MergeSort(item a[i..j]; index i, j)
1 index l
2 if i < j
3 then l ← (i + j) div 2
4 MergeSort(a[i..l])
5 MergeSort(a[l + 1..j])
6 Merge(a[i..j], l + 1)
Merge fügt die sortierten Teilarrays a[i..l] und a[l + 1..j] zusammen.
a. Geben Sie eine Implementierung von M erge an. Achten Sie dabei
auf den Speicherverbrauch.
b. Analysieren sie die Laufzeit.
16. Vergleichen Sie die folgende Version der binären Suche mit Algorithmus
2.28 und zeigen Sie, dass die Anzahl der wesentlichen Vergleiche durch
⌊log2 (n)⌋ + 1 beschränkt ist.
Algorithmus 2.36.
index BinSearch(item a[0..n − 1], x)
1 index l, r, i, l ← 0, r ← n − 1
2 while l < r do
3 i ← (l + r − 1) div 2
4 if a[i] < x
5 then l ← i + 1
6 else r ← i
7 return l
17. Sei a[1..n] ein Array von Zahlen. Geben Sie einen Algorithmus an, der die
k kleinsten Elemente in a[1..n] ohne zusätzlichen Speicher mit Laufzeit
O(n) bestimmt.
3. Hashverfahren
Die Anzahl der Zugriffe, um ein gespeichertes Objekt zu suchen, ist bei Ver-
wendung von sortierten Arrays oder binären Suchbäumen von der Ordnung
log2 (n). Bei Hashverfahren finden wir ein gespeichertes Objekt im Idealfall
mit einem einzigen Zugriff. Dies erreichen wir durch die Berechnung der
Adresse des Objekts.
In einer Anwendung von Hashverfahren speichern wir Objekte in einer
Hashtabelle. Ein Objekt bezeichnen wir hier als Datensatz. Im Datensatz ist
ein Schlüssel gespeichert, der dem Datensatz eindeutig zugeordnet ist, d. h.
die Abbildung von der Menge der Datensätze in die Menge der Schlüssel ist
injektiv.1 Bei der Organisation dieser Datensätze in einer Hashtabelle ist nur
der Schlüssel von Bedeutung. Wir identifizieren deshalb den Datensatz mit
seinem Schlüssel und sprechen nur noch von Schlüsseln, die wir abspeichern,
suchen oder löschen.
Wir nehmen an, dass es sich bei der Menge der Schlüssel um eine Men-
ge von Zahlen handelt. Dies bedeutet keine Einschränkung, weil wir die
Schlüsselmenge über einem Zahlensystem, zum Beispiel den binären Zahlen,
codieren können.
Die oben erwähnte Effizienz lässt sich nur erreichen, indem wir darauf
verzichten, die Hashtabelle ganz zu füllen. Eine sorgfältige Analyse zeigt,
welche Laufzeit wir in Abhängigkeit vom Füllgrad der Hashtabelle erreichen.
Umgekehrt können wir die Ergebnisse dieser Analyse dazu benutzen, um die
Hashtabelle so zu dimensionieren, dass wir eine gewünschte Laufzeit – zum
Beispiel ein Schlüssel soll mit zwei Zugriffen gefunden werden – erwarten
dürfen.
Definition 3.1. Sei X die Menge der möglichen Schlüssel. Ein Hashver-
fahren besteht aus einer Hashtabelle H mit m Zellen und einer Hashfunk-
tion h : X −→ {1, . . . , m}. Für s ∈ X ist h(s) der Tabellenindex, den wir
für die Speicherung von s verwenden, siehe Figur 3.1. Sollen zwei Schlüssel
1
Eine Abbildung f : X −→ Y heißt injektiv, wenn für x1 , x2 ∈ X aus f (x1 ) =
f (x2 ) x1 = x2 folgt.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021
H. Knebl, Algorithmen und Datenstrukturen,
https://doi.org/10.1007/978-3-658-32714-9_3
112 3. Hashverfahren
Hashtabelle
Schlüssel s
Hashfunktion
Bemerkung. Die Menge der tatsächlichen Schlüssel S ist oft nicht a priori
bekannt. Man weiß nur, dass S eine Teilmenge einer großen Menge X ist, der
Menge der möglichen Schlüssel. Eine Funktion, die X injektiv abbildet, ist
nicht praktikabel oder sogar unmöglich, wenn X eine Hashtabelle erfordert,
deren Größe den verfügbaren Speicher überschreitet. Deshalb nehmen wir
Kollisionen hin.
In [Knuth98a] wird umfangreiches Material zu Hashverfahren und in
[MotRag95] zu probabilistische Hashverfahren bereitgestellt, dass die hier
behandelten Aspekte umfasst.
3.2 Hashfunktionen
h : X −→ {1, . . . , 365}
3.2 Hashfunktionen 113
ordnet einer Person aus X den Tag zu, an dem sie Geburtstag hat (ohne
Beachtung von Schaltjahren). Wir nehmen an, dass es sich bei h um eine
Zufallsfunktion handelt, d. h. die Geburtstage erscheinen uns zufällig gewählt.
Bei einer Anzahl von mehr als 23 Personen ist die Wahrscheinlichkeit
für eine Kollision ≥ 1/2. Erstaunlich ist, dass wir schon bei einer so kleinen
Anzahl von Personen mit einer Kollision rechnen müssen.
Unter dieser Annahme – zufällige Wahl (mit Zurücklegen) des Geburtsta-
ges aus der Menge der Tage eines Jahres {1, . . . , 365} – können wir folgendes
rechnen: Wählen wir aus einer m–elementigen Menge k Elemente, so ist die
Wahrscheinlichkeit p, dass keine Kollision eintritt
1 ∏
k−1 ∏(
k−1
i
)
p = p(m, k) = (m − i) = 1 − .
mk i=0 i=1
m
Für alle reellen Zahlen x gilt 1 − x ≤ e−x (Corollar B.20) und es folgt
∏
k−1 ∑k−1
p≤ e−i/m = e−(1/m) i=1 i
= e−k(k−1)/2m .
i=1
Obwohl in der Definition von h reelle Zahlen vorkommen, die auf einem
Rechner durch Floatingpoint-Zahlen approximiert werden, lässt sich diese
Funktion einfach mit ganzzahliger Arithmetik implementieren, wenn m = 2p
mit p ≤ w gilt, wobei w die Wortbreite des Rechners bezeichnet. Es ist
nur eine Multiplikation von zwei ganzen Zahlen und eine Shift-Operation
notwendig.
h(s) = ⌊2p {s · c · 2w · 2−w }⌋ = ⌊2p {s(⌊c · 2w ⌋ + {c · 2w })2−w }⌋
= ⌊2p {s⌊c · 2w ⌋2−w + s{c · 2w }2−w }⌋.
Schreibe s⌊c · 2w ⌋ = q2w + r und setze s{c · 2w }2−w = 0. Dann gilt:
h(s) = ⌊2p {q + r · 2−w }⌋ = ⌊2p {r · 2−w }⌋.
Wenn s und c in je einem Register des Prozessors gespeichert sind, ergibt sich
h(s) aus den p signifikantesten Bits von r, dem niederwertigen Anteil von s · c.
Diese p Bits von r gewinnen wir durch eine Rechts-Shift-Operation um w − p
Stellen.
Beispiel. Sei w = 8, p = 6, m = 26 = 64 die Anzahl der Zellen der Hashta-
belle, c = 0.618 = 0.10011110 und s = 4 = 100 (binär). Dann berechnet sich
h(s) durch 10011110 · 100 = 10|01111000. Also gilt h(s) = 011110 = 30.
Beispiel. Figur 3.2 zeigt eine Zufallsfunktion, die Punkte gleichmäßig in der
Ebene verteilt.
Beispiel. Sei X = {0, 1}l und Y = {0, 1}r . Wir können eine Zufallsfunktion
durch eine Tabelle mit 2l Zeilen mit je r Bit speichern. Die Kollisionswahr-
scheinlichkeit ist 21r . Ist zum Beispiel l = 64 und r = 16, so ist die Kollisions-
wahrscheinlichkeit nur 2116 . Zum Abspeichern einer einzigen Zufallsfunktion
braucht man jedoch 16·264 Bit = 265 Byte. Zufallsfunktionen sind riesengroße
Objekte. Deshalb sind sie nicht implementierbar.
116 3. Hashverfahren
bijektiv. Folglich ist die Abbildung fa,b − fa′ ,b′ = fa−a′ ,b−b′ für a = ̸ a′
bijektiv. Für x ∈ Zp mit (fa,b − fa′ ,b′ ) (x) = [1] gilt ha,b (x) ̸= ha′ ,b′ (x).
2. Sei nun a = a′ und b > b′ (ohne Einschränkung annehmbar). Falls m die
Zahl b − b′ nicht teilt, folgt ha,b (0) ̸= ha,b′ (0). Falls m die Zahl b − b′ teilt,
gilt für y = p − b
Wir haben gezeigt, dass aus (a, b) ̸= (a′ , b′ ) folgt ha,b ̸= ha′ ,b′ . Deshalb gilt
p(p − 1)
|{(a, b) | a, b ∈ Zp , a ̸= 0, ha,b (x) = ha,b (y)}| ≤
m
gilt. Sei ( )
(a) x1
φ : Z2p −→ Z2p , (a, b) 7−→ A b wobei A =
y1
Da x ̸= y gilt, ist φ bijektiv (siehe [Fischer14, Kap. 2]).
Deshalb gilt
⌊p⌋
Da es zu jedem (festen) r höchstens m ⌋ s ̸= r und s ≡
viele ⌊s mit
p
r mod m gibt (dies sind die Elemente r + m, . . . , r + m m) und da es für r
gerade p viele Möglichkeiten gibt, gilt
⌊p⌋
|{(r, s) ∈ Z2p | r ̸= s, r ≡ s mod m}| ≤ · p.
m
⌊ p ⌋ p−1 ⌊p⌋
Aus m ≤ m folgt m · p ≤ p(p−1)
m . Mit |H| = (p − 1)p folgt die Behaup-
tung. 2
Bemerkung. Sei m die Anzahl der gewünschten Tabellenindizes und n = |X|.
Für eine universelle Familie H = {ha,b | a, b ∈ Zp , a ̸= 0} ist eine Primzahl
p ≥ n notwendig. Wir benötigen somit eine Primzahl p mit etwa ⌈log2 (n)⌉
Bit. Weiter benötigen wir zwei Zufallszahlen a und b mit je etwa ⌈log2 (n)⌉ Bit.
Die Berechnung des Hashwertes erfolgt durch einfache arithmetische Opera-
tionen.
Beispiel. Sei X = {0, 1, . . . , 100 000}, p = 100 003 und m = 10 000. p ist
eine Primzahl. Wähle a, b ∈ Zp , a ̸= 0, zufällig und verwende ha,b (x) =
((ax + b) mod 100 003) mod 10 000 als Hashfunktion.
Satz 3.8. Sei p eine Primzahl, a = (a1 , . . . , ar ) ∈ Zrp und
∑r
ha : Zrp −→ Zp , (x1 , . . . , xr ) 7−→ i=1 ai xi mod p.
Die Familie H = {ha | a ∈ Zrp } ist universell.
Beweis. Wir zeigen zunächst, dass |H| = pr gilt. Aus ha (x) = 0 für alle
x ∈ Zrp folgt ai = ha (ei ) = 0 für i = 1, . . . , r, d. h. a = 0. Sei ha (x) = ha′ (x)
für alle x ∈ Zrp . Dann
ist ha (x) − ha′ (x) = ha−a′ (x) = 0 und es folgt a = a′ .
Somit ist |H| = Zrp = pr gezeigt.
Seien x, y ∈ Zrp , x ̸= y gegeben. Zu zeigen ist
1 1
{a ∈ Zrp | ha (x) = ha (y)} ≤ .
|H| p
Aus ha (x) = ha (y) folgt ha (x − y) = 0 (und umgekehrt) d. h.
x 1 − y1
..
(a1 , . . . , ar )
.
= 0.
x r − yr
Da x − y ̸= 0 ist, hat die durch x − y definierte lineare Abbildung den Rang
1 und einen Kern der Dimension r − 12 (siehe [Fischer14, Kap. 2]). Es folgt
{a ∈ Zrp | ha (x − y) = 0} = pr−1 .
Bemerkung. Sei m die Anzahl der gewünschten Tabellenindizes. Für eine uni-
verselle Familie H = {ha | a ∈ Zrp } ist eine Primzahl p ≥ m notwendig. Wir
benötigen somit eine Primzahl p mit etwa ⌈log2 (m)⌉ Bit. Weiter benötigen
wir r Zufallszahlen a1 , . . . , ar mit je etwa ⌈log2 (m)⌉ Bit.
Um ha auf x ∈ X anwenden zu können, entwickeln wir x im p–adischen
Zahlensystem (Satz B.2). r ist so groß zu wählen, dass x ≤ pr − 1 für al-
le x ∈ X gilt (Lemma B.3). Die Berechnung des Hashwertes erfolgt durch
einfache arithmetische Operationen in Zp .
3.3 Kollisionsauflösung
Der Einsatz einer Hashfunktion, die mögliche Schlüssel eins zu eins auf Ta-
bellenindizes abbildet, ist nicht praktikabel oder sogar unmöglich, wenn es
viel mehr mögliche als tatsächlich gespeicherte Schlüssel gibt. Hashfunktionen
sind nicht injektiv. Verschiedene Schlüssel können auf den gleichen Hashwert
abgebildet werden. Wir sprechen dann von einer Kollision (siehe Definiti-
on 3.1). Es gibt effiziente Verfahren, um das durch Kollisionen verursachte
Problem zu lösen. Wir diskutieren die Methoden Verkettungen und offene
Adressierung.
Fig. 3.3:
. Primär- und Überlaufbereich.
Bei der Initialisierung des Hashverfahrens sollte bekannt sein, wie viele
Datensätze zu erwarten sind. Dann stellt sich die Frage, wie die Hashtabel-
le und eventuell der Überlaufbereich zu dimensionieren sind. Das Verfahren
separate Verkettungen ist nur durch den zur Verfügung stehenden Heapspei-
cher beschränkt. Wenn wir die Hashtabelle jedoch zu klein wählen, ergeben
sich lange verkettete Listen. Dies bedingt dann schlechtes Laufzeitverhalten.
Das Problem der Dimensionierung behandeln wir im Abschnitt 3.4.1.
Bei der offenen Adressierung verwenden wir eine Hashtabelle T mit m Zellen.
Die Einträge sind im Gegensatz zu Verkettungen ohne Zusatzinformation. Im
Falle einer Kollision suchen wir den Ersatzplatz innerhalb der Tabelle T . Der
Ersatzplatz hängt auch von der augenblicklichen Belegung der Tabelle ab.
Die Adresse steht somit nicht von vornherein fest. Deshalb bezeichnen wir
dieses Verfahren als offene Adressierung.
Definition 3.9.
1. Unter einer Sondierfolge verstehen wir eine Folge von Indizes i1 , i2 , i3 , . . .
der Hashtabelle.
2. Unter einer Sondierfolge für s ∈ X verstehen wir eine s eindeutig zuge-
ordnete Sondierfolge, d. h. eine Abbildung i : X −→ {1, . . . , m}N definiert
eine Sondierfolge i(s) für s ∈ X.
Bemerkungen:
1. Jene Zellen der Hashtabelle, die s ∈ X aufnehmen können, sind durch
die Sondierfolge für s festgelegt. Innerhalb dieser Folge ist die Adresse
offen.
2. Da die Tabelle endlich ist, kommen auch nur endliche Sondierfolgen zur
Anwendung.
Bevor wir auf verschiedene Methoden zur Berechnung von Sondierfolgen
eingehen, geben wir an, wie wir diese Sondierfolgen verwenden. Wir beschrei-
ben, wie die Algorithmen zum Einfügen, Suchen und Löschen im Prinzip
arbeiten.
Algorithmen zum Einfügen, Suchen und Löschen.
1. Einfügen: Inspiziere die Zellen mit den Indizes i1 , i2 , i3 . . ., die durch die
Sondierfolge für s vorgegebene sind, und verwende die erste leere Zelle“
”
zur Speicherung von s. Ist der Index dieser Zelle ik , so heißt i1 , i2, . . . , ik
die für s verwendete Sondierfolge.
2. Suchen: Sei s ∈ X, i1 , i2 , . . . die Sondierfolge für s. Wir inspiziere die
Zellen mit den Indizes i1 , i2 , . . . solange, bis wir s finden oder s ∈ / T
entscheiden können.
3. Löschen: Suche s. Falls s gespeichert ist, kennzeichne die Zelle, die s
enthält, als gelöscht.
122 3. Hashverfahren
Bemerkungen:
1. Wir können gelöschte Zellen wieder belegen. Die Indizes gelöschter Zel-
len treten aber unter Umständen in Sondierfolgen anderer Elemente auf.
Dies kann bei Zellen, die noch nie belegt waren, nicht eintreten. Deshalb
müssen wir zwischen gelöschten und nie belegten Zellen unterscheiden.
Gelöschte Zellen verkürzen unter Umständen die Sondierfolgen für ande-
re Einträge nicht.
2. Wir können gelöschte Zellen zu nie belegten Zellen machen, falls der Index
der Zelle in keiner verwendeten Sondierfolge der Einträge der Tabelle
auftritt.
Wir behandeln verschiedene Typen von Sondierfolgen. Alle Sondierfolgen
sollen die folgenden Anforderungen erfüllen:
1. Zur Berechnung der Sondierfolgen soll, wie bei den Hashfunktionen, ein
effizienter Algorithmus zur Verfügung stehen.
2. Die Sondierfolge soll alle freien Plätze der Tabelle enthalten.
Definition 3.10. Sei h : X −→ {0, . . . , m − 1} eine Hashfunktion.
Lineares Sondieren verwendet die Sondierfolge
i(s)j = (h(s) + j) mod m, j = 0, . . . , m − 1.
Die Sondierfolge besteht aus den Indizes h(s), (h(s) + 1) mod m, . . . , (h(s) +
m−1) mod m, wobei m gleich der Länge der Hashtabelle ist. Die Sondierfolge
erfüllt die Anforderungen von oben. Beim Löschen setzten wir eine Zelle auf
nie belegt, falls die nachfolgende Zelle frei ist. Es tritt aber das Problem der
Clusterbildung ein, das wir mit einem Beispiel erläutern.
Beispiel. Figur 3.5 zeigt Clusterbildung bei linearem Sondieren. Die belegten
Zellen sind grau hinterlegt.
.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Fig. 3.5: Clusterbildung.
Sei pi die Wahrscheinlichkeit für die Belegung von Zelle i. Wir beobachten:
1. Es gilt p14 = 5p9 , falls alle Werte gleich wahrscheinlich sind.
2. Wird Zelle 4 belegt, so vereinigen sich zwei Cluster.
Eine weitere Methode zur Berechnung von Sondierfolgen ist das quadra-
tische Sondieren.
Definition 3.11. Sei h : X −→ {0, . . . , m − 1} eine Hashfunktion.
Quadratisches Sondieren verwendet die Sondierfolge
i(s)±j = (h(s) ± j 2 ) mod m, j = 0, . . . , m − 1,
d. h. die Folge h(s), (h(s) + 1) mod m, (h(s) − 1) mod m, . . . . . ..
3.3 Kollisionsauflösung 123
Das Problem der primären Clusterbildung bei linearem Sondieren tritt bei
quadratischen Sondieren nicht mehr auf. Die Clusterbildung wird allerdings
nicht vollständig unterbunden, denn die Sondierfolge hängt deterministisch
vom Hashwert ab. Der folgenden Methode des Doppelhashing liegt das Modell
zugrunde, dass nicht nur der Hashwert, sondern auch die Schrittweite der
Sondierfolge zufällig gewählt ist.
Definition 3.13. Gegeben seien die Hashfunktionen h : X −→ {0, ..., m − 1}
und h∗ : X −→ {1, . . . , m − 1}. Doppelhashing verwendet die Sondierfolge
i(s)j = (h(s) + jh∗ (s)) mod m, j = 0, . . . , m − 1,
d. h. die Folge h(s), (h(s) + h∗ (s)) mod m, (h(s) + 2h∗ (s)) mod m, . . ..
Bemerkungen:
1. Die Formel ist der des linearen Sondierens ähnlich. Beim Doppelhashing
sondieren wir ausgehend vom Hashwert h(s) in der Schrittweite h∗ (s).
Die Schrittweite ist nicht konstant. Die Hashfunktion h∗ bestimmt die
Schrittweite für jeden Schlüssel s.
2. Es wird empfohlen die Hashfunktionen h und h∗ unabhängig zu wählen
(siehe Bemerkung nach Satz 3.23). Bei universellen Familien von Hash-
funktionen bedeutet dies, dass wir die Hashfunktionen h und h∗ zufällig
und unabhängig voneinander wählen. Für x1 ̸= x2 ∈ X folgt dann
1
p(h(x1 ) = h(x2 ) und h∗ (x1 ) = h∗ (x2 )) ≤ .
m2
Dies bedeutet, dass für x1 ̸= x2 die Wahrscheinlichkeit, dass die Hash-
werte und die Schrittweite gleich sind, kleiner oder gleich 1/m2 ist.
3. Die obige Sondierfolge ist eine lineare Kongruenzenfolge (Abschnitt 1.6.4):
xn+1 = (axn + c) mod m, a = 1, c = h∗ (s) und x0 = h(s). Lineare
Kongruenzenfolgen benutzen wir zur Erzeugung von Pseudozufallszah-
len (Abschnitt 1.6.4).
Satz 3.14. Sind h∗ (s) und m teilerfremd, so gilt
|{(h(s) + jh∗ (s)) mod m | j = 0, . . . , m − 1}| = m.
In dieser Situation kommen in einer Sondierfolge alle Tabellenindizes vor.
124 3. Hashverfahren
Beweis. Seien j, k ∈ {0, . . . , m − 1}, j > k. Aus (h(s) + jh∗ (s)) mod m =
(h(s)+kh∗ (s)) mod m folgt, dass m die Zahl (j−k)h∗ (s) teilt. Da m und h∗ (s)
teilerfremd sind, teilt m die Zahl j −k. Dies ist ein Widerspruch, da j −k < m
ist. Somit sind die Zahlen (h(s) + jh∗ (s)) mod m paarweise verschieden. 2
Bemerkung. Die Voraussetzung des Satzes ist für eine Primzahl m oder für
m = 2k und ein h∗ mit ungeraden Zahlen als Wertebereich erfüllt. Dann
kommen in einer Sondierfolge alle Tabellenindizes vor.
Das Bertrandsche4 Postulat besagt, dass es für jedes n ∈ N zwischen n und
2n eine Primzahl gibt [RemUll08]. Es findet sich somit stets eine Primzahl
passender Größe.
Bemerkung. Zur Dimensionierung der Hashverfahren sind Annahmen über
die Anzahl der zu speichernden Schlüssel notwendig. Unterschreitet die
geschätzte Anzahl stark die tatsächlich benötigte Anzahl, so entstehen Perfor-
mance-Probleme. Die gewünschte Eigenschaft der Hashverfahren, die Opera-
tionen Einfügen, Suchen und Löschen mit annähernd konstantem Zeitauf-
wand O(1) bereitzustellen, ist dann nicht mehr erfüllt. Bei Verkettungen mit
Überlaufbereich oder der offenen Adressierung versagen die Verfahren, wenn
die Anzahl der Schlüssel die geplante Kapazität übersteigt. Praktisch löst
man dieses Problem, indem man bei Erreichen eines bestimmten Füllgrads
den Inhalt der bestehenden Hashtabelle (inkl. der Elemente in den separa-
ten Ketten) in eine neue größere Hashtabelle umspeichert und die bisherige
Tabelle löscht. Diese Vorgehensweise wird oft auch als Rehashing bezeichnet.
Wir interessieren uns für die Frage, wie viele Vergleiche im Mittel notwendig
sind, um ein gespeichertes Objekt zu finden, und wie viele Vergleiche notwen-
dig sind, um zu entscheiden, dass ein Objekt nicht gespeichert ist. Die beiden
Verfahren der Kollisionsauflösung erfordern eine getrennte Betrachtung.
3.4.1 Verkettungen
Bei den Verkettungen interessiert uns neben der Frage nach der Anzahl der
Vergleiche bei erfolgreicher oder bei erfolgloser Suche auch die Anzahl der zu
erwartenden Kollisionen. Diese Anzahl muss bekannt sein, um den Überlauf-
bereich adäquat dimensionieren zu können.
Wir betrachten zunächst den Fall, dass es sich bei der Hashfunktion um ei-
ne Zufallsfunktion handelt. Das Zufallsexperiment besteht darin, n Schlüssel
mit einer Zufallsfunktion in eine leere Hashtabelle einzufügen. Sei X die Men-
ge der möglichen Schlüssel. Die Hashfunktion
4
Joseph Louis François Bertrand (1822 – 1900) war ein französischer Mathemati-
ker.
3.4 Analyse der Hashverfahren 125
h : X −→ {0, . . . , m − 1}
sei eine Zufallsfunktion, d. h. wir wählen die Werte für jedes Argument in
{0, . . . , m − 1} zufällig und gleichverteilt (Abschnitt 3.2.2).
Zufallsfunktionen sind nicht implementierbar. Universelle Familien von
Hashfunktionen sind eine gute Approximation von Zufallsfunktionen. Sie ver-
halten sich bezüglich Kollisionen wie Zufallsfunktionen (Abschnitt 3.2.2).
Mit S = {s1 , . . . , sn } ⊂ X bezeichnen wir die Menge der tatsächlich vorhan-
denen Schlüssel.
Satz 3.15. Sei h : X −→ {0, . . . , m − 1} eine Zufallsfunktion und sei
nj = |{s ∈ S | h(s) = j}|, j = 0, . . . , m − 1.
Die Zufallsvariable nj kann die Werte 0, . . . , n annehmen. Für die Wahr-
scheinlichkeit pi , die angibt, dass i Schlüssel auf einen Wert j abgebildet
werden, gilt:
( n ) ( 1 )i ( 1
)n−i
pi := pij := p(nj = i) = 1− .
i m m
1
Die Zufallsvariable nj ist binomialverteilt mit Parameter (n, p = m) (Defini-
tion A.15).
Beweis. Unter der Annahme, dass h eine Zufallsfunktion ist, ist das Einfügen
von n Schlüsseln die unabhängige Wiederholung des Experiments Einfügen
”
eines Schlüssels“. Es handelt sich um ein Bernoulli-Experiment und die Wahr-
scheinlichkeit dafür, dass der Index j genau i–mal auftritt, ist durch die Bi-
nomialverteilung gegeben. 2
n
Satz 3.16. Die Zufallsvariable nj besitzt den Erwartungswert E(nj ) = m.
wobei δnj i = 15 genau dann, wenn nj = i gilt, sonst ist δnj i = 0. Werden
auf einen Wert i Schlüssel abgebildet, so führen i − 1 viele der Schlüssel zu
Kollisionen. Für die Anzahl kol der Kollisionen gilt
∑
n
kol = wi (i − 1)
i=2
5
Das Zeichen δij wird mit Kronecker-Delta bezeichnet. Leopold Kronecker (1823
– 1891) war ein deutscher Mathematiker.
126 3. Hashverfahren
∑
m−1 ∑
m−1
= p(nj = i) = pi = mpi .
j=0 j=0
( )
∑
n ∑
n
E(kol) = E wi (i − 1) = (i − 1)E(wi )
i=2 i=2
( )
∑
n ∑
n ∑
n ∑
n
= (i − 1)mpi = m (i − 1)pi = m ipi − pi
i=2 i=2 i=2 i=2
(n )
=m − p1 − (1 − (p0 + p1 )) = n − m (1 − p0 ) .
m
∑n
Wir haben i=1 ipi = n
m verwendet. Dies folgt aus Satz 3.16. 2
1 ∑ i(i + 1)
n
V = wi .
n i=1 2
2. Bei erfolgloser Suche müssen wir die verkettete Liste ganz durchsuchen.
Die verkettete Liste hat die Länge nj . Also sind nj Vergleiche notwendig,
falls nj > 0 ist. Für nj = 0 ist ein Zugriff notwendig. Für die Anzahl V der
Vergleiche gilt V = δnj 0 + nj .
n
E(V ) = E(δnj 0 ) + E(nj ) = p0 + = e−B + B.
m
Die Behauptung des Satzes ist somit gezeigt. 2
{
1, falls h(x) = h(y), x ̸= y,
δh (x, y) =
0 sonst.
δh ist die Indikatorfunktion für Kollisionen. Die Anzahl der s ∈ S \ {x} mit
h(s) = h(x) berechnet sich
∑
δh (x, S) = δh (x, s).
s∈S\{x}
Bemerkung. Sei nh(x) = |{s ∈ S | h(s) = h(x)}| die Anzahl der Schlüssel
s ∈ S mit h(s) = h(x). Es gilt
{
nh(x) , falls x ∈
̸ S,
δh (x, S) =
nh(x) − 1, falls x ∈ S.
Beweis.
∑ 1 ∑ 1 ∑
E(δh (x, S)) = δh (x, S) = δh (x, s)
|H| |H|
h∈H h∈H s∈S\{x}
Bemerkungen:
1. Sei x ∈ S. Für den Erwartungswert von nh(x) gilt die Abschätzung
2. Sei x ̸∈ S. Sei V die Anzahl der Vergleiche bei (erfolgloser) Suche von x.
Dann ist ein Vergleich notwendig, falls sich kein Schlüssel mit Hashwert j in
S befindet, und nj viele sonst. Deshalb gilt V = δnj 0 + nj . Mit Satz 3.20 folgt
Modell. Eine Hashtabelle besitze m Zellen und n Zellen seien belegt, wobei
n < m gilt. Die Wahl einer Sondierfolge i1 , . . . , ik der Länge k entspricht der
Wahl von k − 1 belegten Plätzen und der Wahl eines freien Platzes. Dabei
nehmen wir an, dass alle Sondierfolgen der Länge k gleich wahrscheinlich sind.
Dies ist die Annahme uniformes Sondieren (uniform hashing).
130 3. Hashverfahren
Mit sln bezeichnen wir die Länge einer Sondierfolge, um den (n + 1)–ten
Schlüssel einzufügen. Die Zufallsvariable sln zählt, wie oft wir das Experiment
Wahl eines Platzes“ durchführen müssen, bis wir den ersten freien Platz
”
erreichen. Die Entnahme der Stichprobe erfolgt dabei ohne Zurücklegen des
gezogenen Elements.
Satz 3.22. Für die Wahrscheinlichkeit pk , die angibt, dass sln die Länge k
hat, gilt:
( )
n
k−1 m−n
pk = p(sln = k) = ( ) , k = 1, . . . , n + 1.
m m − (k − 1)
k−1
Beweis. Die Zufallsvariable sln ist negativ hypergeometrisch verteilt mit den
Parametern N = m, M = m − n und der Schranke r = 1 (Definition A.25
und Erläuterung danach). M ist die Anzahl der freien Zellen. sln zählt die
Wiederholungen, bis wir zum ersten Mal eine freie Zelle sondieren. 2
Satz 3.23 (uniformes Sondieren).
n
In einer Hashtabelle seien n von m Zellen belegt, wobei n < m gilt. B = m
sei der Belegungsfaktor.
1. Die mittlere( Länge
) einer Sondierfolge ist beim Suchen eines Elementes
1 1
gleich B ln 1−B .
2. Die mittlere Länge einer Sondierfolge ist beim Einfügen des n + 1–ten
1
Elementes gleich 1−B .
Beweis. Zu Punkt 2: Der Erwartungswert der negativen hypergeometrischen
Verteilung berechnet sich mit den Bezeichnungen N = m und M = m − n
von Satz A.26
m+1
N +1 m+1 1
E(sln ) = = = m
≈ .
M +1 m−n+1 m − m
m+1 n 1−B
Zu Punkt 1: In der Tabelle seien n Elemente. sl0 , . . . , sln−1 seien die Längen
der Sondierfolgen für die∑
n Elemente. Die mittlere Länge sl einer Sondierfolge
n−1
beim Suchen ist sl = n1 j=0 slj .
1∑ 1 ∑ m+1
n−1 n−1
E(sl) = E(slj ) =
n j=0 n j=0 m − j + 1
m+1
= (Hm+1 − Hm−n+1 )
n
m+1
≈ (ln(m + 1) − ln(m − n + 1))
n ( ) ( )
m+1 m+1 1 1
= ln ≈ ln .
n m+1−n B 1−B
∑n
Dabei wurde Hn = k=1 k1 durch ln(n) approximiert (siehe B.5). 2
3.4 Analyse der Hashverfahren 131
Figur 3.6 zeigt die Anzahl der Vergleiche bei erfolgreicher Suche in
Abhängigkeit vom Belegungsfaktor B.
Figur 3.7 zeigt die Anzahl der Vergleiche bei erfolgloser Suche in Abhängig-
keit vom Belegungsfaktor B.
Bemerkung. Auf den ersten Blick scheinen die Verfahren mit Verkettungen
überlegen zu sein. Diese Verfahren benötigen allerdings Speicher zum Abspei-
chern der Links. Wenn der für die Links notwendige Speicher im Vergleich
zum Speicher für die Datensätze groß ist, kann sich ein Vorteil für die Ver-
fahren mit offener Adressierung ergeben.
Übungen.
1. Sei M = {0, . . . , m − 1} und N = {0, . . . , n − 1}. Geben Sie den Prozent-
satz der injektiven Abbildungen f : M −→ N an.
2. Eine Hashtabelle besitze 2048 viele Zellen. Betrachten Sie die Multiplika-
tion mit c = 0.618 als Hashfunktion. Bestimmen Sie die Hashwerte für
alle Zahlen 2k , 0 ≤ k ≤ 10.
3. Sei p eine Primzahl, Zp der Körper mit p Elementen und a ∈ Zp .
ha : Zp × Zp −→ Zp , (x, y) 7−→ ax + y.
wobei p = 2003 und 2 ≤ m ≤ p ist. Wir nehmen an, dass sich Doppel-
hashing wie uniform hashing verhält, falls h und h∗ unabhängig gewählt
sind. Im Mittel soll ein Datensatz mit zwei Zugriffen gefunden werden.
Wie ist die Hashtabelle zu dimensionieren, um dieses Ziel zu erreichen?
Wie ist m zu wählen? Geben Sie das kleinste geeignete m an.
8. Bei der LZ77-Datenkomprimierung (Abschnitt 4.6.4) kann das Auffinden
eines übereinstimmenden Segmentes durch den Einsatz von Hashverfah-
ren beschleunigt werden. Arbeiten Sie die Details dieser Idee aus.
9. a. Das Eindeutigkeitsproblem ist, zu entscheiden, ob n gegebene Ob-
jekte paarweise verschieden sind. Geben Sie einen Algorithmus zur
Lösung dieses Problems an.
b. Gegeben seien Zahlen z1 , . . . , zn ∈ Z und s ∈ Z. Geben Sie einen
Algorithmus an, der entscheidet, ob es zwei Zahlen zi und zj gibt
mit s = zi + zj .
10. Sei h : S −→ {0, . . . , m − 1} eine Hashfunktion, S ist die Menge der
möglichen Schlüssel. h verteile die möglichen Schlüssel gleichmäßig:
|S|
|h−1 (j)| = , j = 0, . . . , m − 1.
m
Wir nehmen an, dass n Schlüssel s1 , . . . , sn ∈ S zufällig gewählt und ge-
speichert werden. Sei nj = |{si | h(si ) = j}|, j = 0, . . . , m − 1. Berechnen
Sie den Erwartungswert E(nj ).
11. Hashverfahren auf Festplattenspeichern. Hashverfahren können
auch für Daten auf dem Sekundärspeicher verwendet werden. Daten wer-
den hier in Blöcken abgespeichert. Ein Block kann mehrere Elemente
134 3. Hashverfahren
aufnehmen. Die Wertemenge der Hashfunktion ist gleich der Menge der
Blockadressen. Ein Block enthalte maximal b Elemente. Sei n die Anzahl
der Elemente und m die Anzahl der Blöcke. Dann verstehen wir unter
n
dem Belegungsfaktor β = m die Anzahl der Elemente pro Adresse. Der
n
Belegungsfaktor für den Speicher ist B = bm . Führen Sie die Analyse der
Hashverfahren für die obige Situation durch und lösen Sie die folgende
Aufgabe.
5000 Datensätze sollen in einer Datei gespeichert werden. Dabei soll ein
Hashverfahren mit Primär- und Überlaufbereich zum Einsatz kommen.
Im Primärbereich können 10000 Sätze gespeichert werden. Beantworten
Sie für die Blockgrößen 1 und 5 die folgenden Fragen:
a. Wie viele Blöcke bleiben im Primärbereich frei?
b. Wie viele Blöcke sind im Überlaufbereich bereitzustellen?
c. Wie viele Datensätze führen beim Einfügen zu Kollisionen?
12. Stapel Symboltabellen und Hashverfahren. Symboltabellen dienen
zur Verwaltung der Namen eines Quellprogramms bei der Übersetzung.
Die zu realisierenden Zugriffe auf Symboltabellen sind Einfügen, Löschen
und Suchen.
Ein Eintrag der Symboltabelle besteht aus
(a) dem Namen der Variablen (Labels, Prozedur,. . . ) und
(b) weiteren Information.
Die Organisation von Symboltabellen als Stapel unterstützt die Regeln
zur Sichtbarkeit in Sprachen mit einer Blockstruktur.
Für die Sichtbarkeit der Namen gelten folgende Regeln:
a. Ein Name ist sichtbar in dem Block, in dem er deklariert wird (und
auch in untergeordneten Blöcken).
b. Ein Name ist in einem Block eindeutig (ohne Verschachteln).
c. Wird in zwei verschachtelten Blöcken ein Name zweimal deklariert,
so wird im inneren Block auf die innere Deklaration Bezug genommen
(most closely nested rule).
Der Übersetzungsvorgang erfolgt sequentiell. Ein Block heißt aktiv, falls
der Compiler den Anfang des Blocks (begin block), aber noch nicht das
Ende des Blocks (end block), passiert hat. Daraus ergeben sich folgende
Anforderungen des Compilers bezüglich der Organisation der Symbolta-
bellen:
a. Nur auf Namen in aktiven Blöcken muss der Zugriff gegeben sein.
b. Namen sollen gemäß der Verschachtelungsstruktur angeordnet wer-
den (von innen nach außen—most closely nested first).
Da der Aufwand für Zugriffe auf Symboltabellen wesentlich ist, sind effizi-
ente Zugriffsmethoden notwendig. Der Organisation der Symboltabellen
als Stapel wird ein Hashverfahren überlagert. Überlegen Sie, welche Ope-
rationen am Blockanfang, am Blockende, zum Einfügen, zum Suchen und
zum Löschen notwendig sind. Arbeiten Sie die Details des Verfahrens aus.
4. Bäume
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021
H. Knebl, Algorithmen und Datenstrukturen,
https://doi.org/10.1007/978-3-658-32714-9_4
136 4. Bäume
4.1 Wurzelbäume
Bäume sind spezielle Graphen (Kapitel 5). Ein Graph ist ein Baum, wenn
es zwischen zwei Knoten genau einen Weg gibt, der beide Knoten verbindet.
Zeichnen wir in einem Baum einen Knoten als Wurzel aus und versehen wir
die Kanten mit einer Richtung, so entsteht ein Wurzelbaum. Bäume treten
in der Informatik häufig auf und sind uns schon in den vorangehenden Ka-
piteln 1 und 2 begegnet, siehe zum Beispiel die Branch-and-Bound-Methode
(Abschnitt 1.5.5), binäre Heaps (Abschnitt 2.2.1) oder Entscheidungsbäume
(Abschnitt 2.3). Wir präzisieren jetzt den Begriff Wurzelbaum.
Definition 4.1.
1. Ein Wurzelbaum B = (V, E, r) besteht aus einer endlichen Menge von
Knoten V , einer endlichen Menge von gerichteten Kanten E ⊂ V × V
und aus der Wurzel r ∈ V . Wir definieren rekursiv:
a. Ein Knoten r ist ein Wurzelbaum (B = ({r}, ∅, r)).
b. Sind B1 = (V1 , E1 , r1 ), . . . , Bk = (Vk , Ek , rk ) Bäume mit den Wur-
zeln r1 , . . . , rk , so erweitern wir die Knotenmenge V um eine neue
Wurzel r und die Kantenmenge E um die Kanten (r, ri ), i = 1, . . . , k,
so erhalten wir einen Baum mit Wurzel r:
Abweichend von dieser Struktur ist der leere Baum erklärt. Er besitzt
keine Knoten und keine Kanten.
2. Ist e = (v, w) ∈ E, so heißt v Vorgänger oder Vater von w und w
Nachfolger oder Sohn von v. Die Kanten in B sind gerichtet. Ein Knoten,
der keine Söhne besitzt, heißt Blatt.
3. Ein Pfad P in B ist eine Folge von Knoten v0 , . . . , vn mit: (vi , vi+1 ) ∈
E, i = 0, . . . , n − 1. n heißt Länge von P .
4. Seien v, w ∈ V . Der Knoten w heißt vom Knoten v aus erreichbar , wenn
es einen Pfad P von v nach w gibt, d. h. es gibt einen Pfad P = v0 , . . . , vn
mit v0 = v und vn = w. Ist w von v aus erreichbar, so heißt v ein Vorfahre
von w und w ein Nachfahre von v. Jeden Knoten v von B können wir als
Wurzel des Teilbaumes, der von v aus erreichbaren Knoten, betrachten.
Hat v die Nachfolger v1 , . . . , vk , so heißen die Teilbäume B1 , . . . , Bk mit
den Wurzeln v1 , . . . , vk die Teilbäume von v.
Bemerkung. In einem Baum gibt es zu jedem Knoten v genau einen Pfad,
der von der Wurzel zu v führt, jeder Knoten außer der Wurzel besitzt genau
einen Vorgänger.
Definition 4.2.
1. Die Höhe eines Knotens v ist das Maximum der Längen aller Pfade, die
in v beginnen.
4.1 Wurzelbäume 137
2. Die Tiefe eines Knotens v ist die Länge des Pfades von der Wurzel zu v.
Die Knoten der Tiefe i bilden die i–te Ebene des Baumes.
3. Die Höhe und Tiefe des Baumes ist die Höhe der Wurzel. Der leere Baum
besitzt die Höhe und die Tiefe −1.
Bemerkung. Besitzt jeder Knoten eines Baumes B der Höhe h höchstens d
Nachfolger, dann gilt für die Anzahl n der Knoten von B:
∑
h
dh+1 − 1
n≤ di = .
i=0
d−1
Bemerkung. Sei n die Anzahl der Knoten in einem binären Baum der Höhe
h. Dann ist die Anzahl der Knoten n ≤ 2h+1 − 1 oder äquivalent dazu, die
Höhe h ist mindestens log2 (n + 1) − 1, d. h. es gilt ⌈log2 (n + 1)⌉ − 1 ≤ h.
Die Schranke wird für einen binären Baum, in dem alle Ebenen vollständig
besetzt sind, angenommen.
A.
B C
D E H I
F G
Der Pfad, der im Knoten A startet und endet, gibt die Besuchsreihenfolge
der Knoten durch den Algorithmus 4.5 wieder. Der erste Eintritt des Pfades
in die Umgebung Uv eines Knotens v, die durch den gestrichelten Kreis um
v dargestellt ist, entspricht dem Aufruf von TreeDFS in v und das letzte
Verlassen der Umgebung Uv der Terminierung dieses Aufrufs.
1. Für jeden Knoten w im linken Teilbaum von v ist l(w) < l(v).
2. Für jeden Knoten w im rechten Teilbaum von v ist l(w) > l(v).
Satz 4.7. DFS mit Inorder-Ausgabe der Markierung der Knoten von B liefert
die Elemente von S in sortierter Reihenfolge.
Beweis. Für einen Knoten ist die Aussage richtig. Da die Elemente, die im
linken Teilbaum der Wurzel gespeichert sind, vor dem in der Wurzel gespei-
cherten Element und die Elemente, die im rechten Teilbaum der Wurzel ge-
speichert sind, nach dem in der Wurzel gespeicherten Element ausgegeben
werden, folgt die Aussage durch vollständige Induktion nach der Anzahl der
Knoten von B. 2
Beispiel. Die Inorder-Ausgabe gibt die Knoteninhalte sortiert aus, wie Fi-
gur 4.2 zeigt. Die Hochzahlen geben die Ausgabereihenfolge bei Inorder-
Traversieren an. Die Ausgabefolge ist 10, 18, 41, 43, 45, 56, 57, 59, 64, 66, 67,
95, 97.
45.5
182 6711
598
577 649
Das Suchen eines Elementes in einem binären Suchbaum erfolgt analog zur
binären Suche (Algorithmus 2.28). Wir prüfen zunächst, ob das zu suchende
Element e in der Wurzel gespeichert ist. Falls dies nicht der Fall ist und e
kleiner dem in der Wurzel gespeicherten Element ist, setzen wir die Suche
(rekursiv) im linken Teilbaum der Wurzel fort. Ist e größer als das in der
Wurzel gespeicherte Element, so setzen wir die Suche (rekursiv) im rechten
Teilbaum der Wurzel fort. Bei der Implementierung der Suche durch die Funk-
tion Search vermeiden wir die Rekursion. Wir ersetzen die Rekursion durch
eine Iteration. Beim Aufruf von Search ist der Baum und das zu suchende
Element zu übergeben.
140 4. Bäume
Algorithmus 4.8.
node Search(tree t, item e)
1 node a ← t
2 while a ̸= null and a.element ̸= e do
3 if e < a.element
4 then a ← a.lef t
5 else a ← a.right
6 return a
Das Einfügen eines Elementes erfolgt mit der Funktion Insert. Beim Auf-
ruf von Insert ist ein Baum und das einzufügende Element e zu übergeben.
Insert führt zunächst eine Suche nach e durch. Falls e bereits im Baum ist,
ist nichts zu tun. Ansonsten endet die Suche bei einem Blatt b. Wir fügen
bei b einen neuen Knoten für e an und speichern in diesem e ab.
Algorithmus 4.9.
Insert(tree t, item e)
1 node a ← t, b ← null
2 while a ̸= null and a.element ̸= e do
3 b←a
4 if e < a.element
5 then a ← a.lef t
6 else a ← a.right
7 if a = null
8 then a ← new(node), a.element ← e
9 a.lef t ← null, a.right ← null, a.parent ← b
10 if b = null
11 then t ← a, return
12 if e < b.element
13 then b.lef t ← a
14 else b.right ← a
4.2.2 Löschen
Teilbaum von v das größte Element ẽ. Dieses liegt im linken Teilbaum
am weitesten rechts. Der Knoten ṽ von ẽ besitzt somit höchstens einen
(linken) Nachfolger. Wir vertauschen e mit ẽ. Den Knoten ṽ entfernen
wir dann zusammen mit e nach Punkt 3 aus dem Baum. Da ẽ das größte
Element im linken Teilbaum von v war, sind jetzt die Elemente im linken
Teilbaum von v kleiner als ẽ. Die Elemente im rechten Teilbaum von v
sind größer als e und damit auch größer als ẽ. Die binäre Suchbaumei-
genschaft ist somit in v erfüllt. Der Knoten ṽ von ẽ heißt symmetrischer
Vorgänger von v.
.
45 .
41
18 67 lösche 45 18 67
=⇒
10 41 56 97 10 40 56 97
40 95 95
Wir implementieren das Löschen eines Elementes durch die folgenden Al-
gorithmen.
Algorithmus 4.10.
Delete(tree t, item e)
1 node b, a ← t
2 a ← Search(t, e)
3 if a = null then return
4 if a.right ̸= null and a.lef t ̸= null
5 then DelSymPred(a), return
6 if a.lef t = null
7 then b ← a.right
8 else b ← a.lef t
9 if t = a
10 then t ← b, return
11 if a.parent.lef t = a
12 then a.parent.lef t ← b
13 else a.parent.right ← b
14 b.parent ← a.parent
15 return
142 4. Bäume
DelSymPred(node a)
1 node b ← a
2 if a.lef t.right = null
3 then c ← a.lef t, a.lef t ← c.lef t
4 else b ← a.lef t
5 while b.right.right ̸= null do
6 b ← b.right
7 c ← b.right, b.right ← c.lef t
8 a.element ← c.element
9 c.lef t.parent ← b
Die Höhe eines binären Suchbaumes, der n Elemente speichert, liegt zwischen
log2 (n) und n. Wünschenswert ist, dass die Höhe nahe bei log2 (n) liegt. Dies
erreichen wir mit geringem zusätzlichen Aufwand beim Einfügen und Löschen
von Elementen. Genauer geht es darum, die folgende Bedingung für einen
binären Suchbaum, die auf Adel’son-Vel’skiĭ1 und Landis2 zurückgeht (siehe
[AdeLan62]), beim Einfügen und Löschen aufrechtzuerhalten.
Definition 4.11 (AVL-Bedingung). Ein binärer Baum heißt (AVL-)ausge-
glichen, wenn für jeden Knoten v gilt: Die Höhen des linken und rechten
Teilbaumes von v unterscheiden sich um höchstens 1. Ausgeglichene binäre
Suchbäume heißen auch AVL-Bäume.
Bei der Familie der Fibonacci-Bäume, die wir in der folgenden Definition
einführen, handelt es sich um ausgeglichene Bäume. Fibonacci-Bäume dienen
der Navigation bei der Fibonacci-Suche in sortierten Arrays (siehe Übungen,
Aufgabe 11)
Definition 4.12. Die Folge der Fibonacci-Bäume (FBk )k≥0 ist analog zur
Folge der Fibonacci-Zahlen (Definition 1.19) rekursiv definiert.
1. FB0 und FB1 bestehen aus dem Knoten 0.
2. Sei k ≥ 2. Wähle die k–te Fibonacci-Zahl fk als Wurzel, nehme FBk−1
als linken und FBk−2 als rechten Teilbaum.
3. Erhöhe jeden Knoten im rechten Teilbaum der Wurzel um fk .
Die Höhe von FBk ist für k ≥ 1 gleich k − 1. Deshalb sind die Fibonacci-
Bäume ausgeglichen.
Figur 4.4 zeigt FB2 − FB5 .
1
Georgy Maximovich Adel’son-Vel’skiĭ (1922 – 2014) war ein russischer und israe-
lischer Mathematiker und Informatiker
2
Evgenii Mikhailovich Landis (1921 – 1997) war ein russischer Mathematiker.
4.3 Ausgeglichene Bäume 143
2.
1.
FB2 : FB3 : 1 2
0 1
0 1
5.
3. 3 7
FB4 : 2 4 FB5 : 2 4 6 7
1 2 3 4 1 2 3 4 5 6
0 1 0 1
Mit Bk bezeichnen wir den Baum der inneren Knoten von FBk , d. h. der
Knoten, die keine Blattknoten sind. Die Folge (Bk )k≥0 ist analog zur Folge
der Fibonacci-Bäume definiert, wie Figur 4.5 zeigt. Der Induktionsbeginn ist
gegeben durch B0 = B1 = ∅.
Der Baum Bk , k ≥ 2, besitzt fk als Wurzel, Bk−1 als linken und Bk−2 als
rechten Teilbaum der Wurzel. Die Knoten von Bk−2 sind um fk zu erhöhen.
Per Rekursion erhalten wir
2.
B2 : 1. B3 :
1
5.
3.
3 7
B4 : 2 4 B5 :
2 4 6
1
1
Bk ist ein ausgeglichener Baum der Höhe k−2 mit einer minimalen Anzahl
von Knoten. Über die Anzahl der Knoten von Bk gibt der folgende Satz
Auskunft.
Satz 4.13. Für die Anzahl bh der Knoten eines ausgeglichenen Baumes der
Höhe h mit einer minimalen Anzahl von Knoten gilt:
bh = fh+3 − 1,
Beweis. Sei Th ein ausgeglichener Baum der Höhe h mit einer minimalen
Anzahl von Knoten. Der linke Teilbaum der Wurzel habe die Höhe h − 1. Der
rechte Teilbaum der Wurzel hat dann, wegen der Bedingung der minimalen
”
Anzahl von Knoten“, die Höhe h − 2.
Für die Anzahl bh der Knoten von Th gilt:
b0 = 1, b1 = 2, bh = bh−1 + bh−2 + 1, h ≥ 2.
Hier handelt es sich um eine inhomogene lineare Differenzengleichung zweiter
Ordnung mit konstanten Koeffizienten. Derartige Gleichungen behandeln wir
im Abschnitt 1.3.2.
Wir berechnen eine spezielle Lösung der Gleichung durch den Lösungsan-
satz φh = c, c konstant, und erhalten c = 2c + 1 oder c = −1. Die allgemeine
Lösung bh ergibt sich aus der allgemeinen Lösung der homogenen Gleichung
bh = λ1 gh + λ2 ĝh , λ1 , λ2 ∈ R,
die im Beweis von Satz 1.21 gelöst ist, und der speziellen Lösung φh :
bh = λ1 gh + λ2 ĝh − 1, λ1 , λ2 ∈ R,
√ √
wobei g = 1/2(1 + 5) und ĝ = 1/2(1 − 5) die Lösungen von x2 = x + 1 sind
(Abschnitt 1.3.2, Satz 1.21).
Aus den Anfangsbedingungen b0 = 1, b1 = 2 ergibt sich
λ1 g0 + λ2 ĝ0 − 1 = 1,
λ1 g1 + λ2 ĝ1 − 1 = 2.
Wir erhalten
λ2 = 2 − λ1 ,
λ1 g + (2 − λ1 )(1 − g) = 3.
Hieraus folgt:
4.3 Ausgeglichene Bäume 145
2g + 1 g3
λ1 = =√ ,
2g − 1 5
√ √
2g + 1 2g + 1 − 2 5 2(g − 5) + 1
λ2 = 2 − √ =− √ =− √
5 5 5
3
2ĝ + 1 ĝ
=− √ = −√ .
5 5
Bei der Rechnung wurde benutzt, dass für g und ĝ die Gleichung 2x + 1 =
x + x + 1 = x + x2 = x(x + 1) = xx2 = x3 gilt. Damit ergibt sich die Lösung
1 ( )
bh = √ gh+3 − ĝh+3 − 1 = fh+3 − 1.
5
2
Satz 4.14 (Adel’son-Vel’skiĭ und Landis). Für die Höhe h eines ausgegliche-
nen Baumes mit n Knoten gilt:
Da ein AVL-Baum ein binärer Suchbaum ist, verwenden wir zum Suchen
die Suchfunktion eines binären Suchbaums (Algorithmus 4.8). Wir studieren
jetzt die Algorithmen zum Einfügen und Löschen und die dabei notwendigen
Ausgleichsoperationen, um die AVL-Bedingung aufrecht zu erhalten.
4.3.1 Einfügen
Beim Einfügen verfahren wir zunächst wie in einem binären Suchbaum (Al-
gorithmus 4.9). Die Suche nach dem einzufügenden Element e endet in einem
Blatt, falls e nicht im Baum gespeichert ist. An diesem Blatt verankern wir
einen neuen Knoten und füllen ihn mit dem einzufügenden Element. Da-
bei kann die AVL-Bedingung verletzt werden. Anschließend reorganisieren
wir den Baum und stellen die AVL-Bedingung wieder her. Dazu prüfen wir
für jeden Knoten n des Suchpfades, ausgehend vom Blatt, ob der Baum
146 4. Bäume
b. a.
rechts um a
a δ =⇒ α b
α β β δ
links um b
⇐=
Die Rechtsrotation um a bringt b eine Ebene nach unten und a eine Ebene
nach oben. Die Linksrotation um b bringt a eine Ebene nach unten und b
eine Ebene nach oben. Da die Elemente in β größer als a und kleiner als b
sind, bleibt die binäre Suchbaumeigenschaft bei Rechts- und Linksrotationen
erhalten.
Definition 4.15. Sei a ein Knoten in einem binären Baum. Der Balancefak-
tor bf(a) von a ist die Differenz Höhe des rechten minus Höhe des linken
Teilbaumes von a. Wir schreiben bei Balancefaktoren − für −1, + für + 1,
−− für −2 und ++ für +2.
Beispiel. Der Baum von Figur 4.7 ist nicht AVL-ausgeglichen. Die Balance-
faktoren sind an jedem Knoten hochgestellt angegeben.
14.−
11−− 15++
9− 160
60
Bemerkungen:
1. In einem ausgeglichenen Baum gibt es nur Knoten mit den Balancefak-
toren −, 0 und +.
2. Ein negativer (positiver) Balancefaktor eines Knotens zeigt an, dass der
linke (rechte) Teilbaum eine größere Höhe besitzt.
Beispiel. Wir fügen die 6 in den linken Baum der Figur 4.8 ein:
.
14
.
14 11 15 .
14
füge 6 ein rechts um 9
11 15 =⇒ 10 12 16 =⇒ 11 15
10 12 16 9 9 12 16
9 6 6 10
a. - -
b.
b - α
rechts um b β a
β δ =⇒
δ α
Sei h die Höhe von α. Dann ist die Höhe von δ gleich h und die Höhe von
β nach Einfügen des Knotens gleich h + 1.
Nach erfolgter Rotation gilt bf(a) = 0 und bf(b) = 0. Daher erfüllt der rechte
Baum in den Knoten a und b die AVL-Bedingung. Die Höhe des betrachteten
Teilbaumes ist vor dem Einfügen und nach durchgeführter Rotation gleich
h + 2. Deshalb sind keine weiteren Ausgleichsoperationen notwendig.
Beispiel. Figur 4.10 zeigt eine Situation, in der es nicht möglich ist, die AVL-
Bedingung mit einer Rotation herzustellen.
.
14 9.
rechts um 9
9 15 =⇒ 3 14
3 12 12 15
10 10
Beispiel. In Figur 4.11 stellen wir durch eine Doppelrotation die AVL-
Bedingung her – erst eine Links-, dann eine Rechtsrotation.
.
14 .
14 .
12
links um 12 rechts um 12
9 15 =⇒ 12 15 =⇒
9 14
3 12 9 3 10 15
10 3 10
Wir betrachten jetzt den allgemeinen Fall für die Ausgleichsoperation für
bf(a) = −−, bf(b) = +. Der Höhenausgleich erfolgt durch eine Links- und
Rechtsrotation, erst links um c, dann rechts um c. Wir skizzieren diese in
Figur 4.12.
a. - -
b + α c.
β c erst links, b a
dann rechts um c
γ1 γ2 β γ1 γ2 α
=⇒
Sei h die Höhe von α. Es folgt, dass die Höhe von β gleich h ist und
dass die Höhen von γ1 und γ2 vor dem Einfügen gleich h − 1 sind. Da nach
Einfügen des Knotens die Höhe von b um eins zunimmt, ist entweder die
Höhe von γ1 oder von γ2 nach dem Einfügen gleich h. Figur 4.12 stellt den
zweiten Fall dar.
Die folgende Tabelle gibt in der ersten Spalte Balancefaktoren nach dem
Einfügen und in den weiteren Spalten Balancefaktoren nach erfolgter Reor-
ganisation an.
bf(c) bf(a) bf(b) bf(c)
+ 0 − 0
− + 0 0
Daher erfüllt der rechte Baum in den Knoten a, b und c die AVL-Bedingung.
Die Höhe des betrachteten Teilbaumes vor dem Einfügen und des Teilbaumes
nach durchgeführter Rotation ist gleich h + 2. Deshalb sind keine weiteren
Ausgleichsoperationen notwendig.
Wir erweitern die Knotenobjekte zur Darstellung der Bäume um die Kom-
ponente Balancefaktor.
type balFac = 0, −, −−, +, ++
150 4. Bäume
Algorithmus 4.16.
boolean AVLInsert(tree t, node a, item e)
1 if e < a.element
2 then b ← a.lef t
3 if b = null then insertNode(b, e), return true
4 if AVLInsert(t, b, e)
5 then if a.bf = + then a.bf ← 0, return false
6 if a.bf = 0 then a.bf ← −, return true
7 if b.bf = −
8 then R-Rot(t, b), return false
9 else c ← b.right
10 LR-Rot(t, c), return false
11 else if e > a.element
12 then b ← a.right
13 if b = null then insertNode(b, e), return true
14 if AVLInsert(t, b, e)
15 then if a.bf = − then a.bf ← 0, return false
16 if a.bf = 0 then a.bf ← +, return true
17 if b.bf = +
18 then L-Rot(t, b), return false
19 else c ← b.lef t
20 RL-Rot(t, c), return false
21 return false
Wir geben die Algorithmen für die Links-Rechts- und Rechtsrotation an.
Die Links- und die Rechts-Links-Rotation sind analog zu implementieren.
Algorithmus 4.17.
R − Rot(tree t, node b)
1 a ← b.parent, c ← a.parent
2 a.bf ← 0, b.bf ← 0
3 a.parent ← b, b.parent ← c, a.lef t ← b.right, b.right ← a
4 if c = null
5 then t ← b
6 else if c.right = a
7 then
8 c.right ← b
9 else c.lef t ← b
4.3 Ausgeglichene Bäume 151
Algorithmus 4.18.
LR − Rot(tree t, node c)
1 b ← c.parent, a ← b.parent, d ← a.parent
2 if c.bf = +
3 then b.bf ← −, a.bf ← 0
4 else b.bf ← 0, a.bf ← +
5 c.bf ← 0;
6 a.parent ← c, b.parent ← c, c.parent ← d
7 a.lef t ← c.right, b.right ← c.lef t, c.lef t ← b, c.right ← a
8 if d = null
9 then t ← c
10 else if d.right = a
11 then
12 d.right ← c
13 else d.lef t ← c
Bemerkungen:
1. AVLInsert ermittelt analog zur Tiefensuche (Algorithmus 4.5) durch re-
kursiven Abstieg den Einfügepfad und das Zielblatt. Dann fügt insertNo-
de im Zielblatt einen neuen Knoten an und speichert e in diesem. Auf
dem Rückweg zur Wurzel werden die Rotationen durchgeführt und die
Balancefaktoren richtiggestellt.
2. Bei Ausführung von AVLInsert wird der Abstiegspfad implizit auf dem
Aufrufstack gespeichert. Auf die node-Komponente parent, die R-Rot
und LR-Rot verwenden, greifen wir in AVLInsert nicht zu.
3. AVLInsert gibt in Zeile 6 oder Zeile 16 true zurück, falls die Höhe im Teil-
baum mit Wurzel b zugenommen hat. Unter Umständen ist ein Update
des Balancefaktors a.bf notwendig. Falls der Balancefaktor in a weder 0
noch + ist, gilt a.bf = −. Also gilt nach Terminierung des Aufrufs in
Zeile 4 a.bf = − −. Dann ist in diesem Teilbaum eine Rechtsrotation
(R-Rot) oder eine Linksrechtsrotation (LR-Rot) notwendig. Falls AVLIn-
sert false zurückgibt, sind auf dem Abstiegspfad keine weiteren Updates
notwendig.
4. Die auf den Aufruf von AVLInsert in Zeile 14 folgenden Zeilen 15 – 20
sind für den Abstieg rechts symmetrisch zu den Zeilen 5 – 10.
5. Befindet sich e im Baum, so ist keiner der Vergleiche in den Zeilen 1 und
11 wahr. AVLInsert gibt in Zeile 21 false zurück.
6. Bei einer alternativen iterativen Implementierung von AVLInsert fügen
wir mit dem Algorithmus Insert (Algorithmus 4.9) ein Element ein. Zur
Herstellung der AVL-Bedingung durchlaufen wir dann den Pfad, der
durch die parent-Komponente von node gegeben ist. Dabei führen wir
die notwendigen Rotationen und Aktualisierungen der Balancefaktoren
durch (vergleiche Algorithmus 4.19).
152 4. Bäume
4.3.2 Löschen
Löschen muss invariant bezüglich der AVL-Bedingung und der binären Such-
baumeigenschaft sein. Wir löschen zunächst wie in einem binären Suchbaum
(Algorithmus 4.10) und stellen dann eventuell die AVL-Bedingung durch Aus-
gleichsaktionen her.
.
14 .
14 .
11
entf. 16 rechts um 11
11 15 =⇒ 11 15 =⇒
10 14
10 12 16 10 12 9 12 15
9 9
Für die Ausgleichsaktion betrachten wir den Pfad P , der von der Wurzel
zum Vorgänger des Knotens führt, den wir aus dem Baum entfernen. Den
am weitesten von der Wurzel entfernte Knoten, in dem der Baum nicht mehr
balanciert ist, bezeichnen wir mit a. Wir betrachten den Fall bf(a) = − −.
Den symmetrische Fall bf(a) = + + kann man analog behandeln.
Sei b die Wurzel des linken Teilbaumes von a und α der rechte Teilbaum
von a. Sei h die Höhe von α vor dem Löschen des Knotens. Wegen bf(a) = − −
nimmt nach dem Löschen des Knotens die Höhe des Teilbaumes α um eins ab.
Der Teilbaum mit Wurzel b und damit auch der Balancefaktor bf(b) bleiben
unverändert.
Wir betrachten jetzt den Fall bf(a) = − −, bf(b) = − oder 0. Der Höhen-
ausgleich erfolgt durch eine Rechtsrotation um b. Wir skizzieren diese mit der
Figur 4.14.
a. - -
b.
b - α
rechts um b β1 a
β1 β2 =⇒
β2 α
Es folgt, dass die Höhe von β1 gleich h ist und dass die Höhe von β2 gleich
h − 1 oder h ist (bf(b) = − oder 0). Figur 4.14 stellt den ersten Fall dar. Die
Höhe des Teilbaumes mit der Wurzel a war vor dem Löschen des Knotens
h + 2.
Im rechten Baum gilt bf(a) = bf(b) = 0, falls vor dem Löschen bf(b) = −
war, und es gilt bf(a) = − und bf(b) = +, falls vor dem Löschen bf(b) = 0
war. Daher ist im rechten Baum in den Knoten a und b die AVL-Bedingung
erfüllt. Für bf(b) = − nimmt die Höhe des reorganisierten Baumes ab. Er
besitzt nur noch die Höhe h + 1. Deshalb sind eventuell Ausgleichsaktionen
für höher gelegene Knoten im Pfad P notwendig.
.
14 .
14 .
12
links um 12 rechts um 12
11 15 =⇒ 12 15 =⇒
11 14
10 12 11 13 10 13 15
13 10
a. - -
b + α c.
erst links,
β c b a
dann rechts um c
γ1 γ2 =⇒ β γ1 γ2 α
Der Teilbaum α hat nach dem Löschen des Knotens die Höhe h−1. Wegen
bf (b) = + ist die Höhe des rechten Teilbaumes von b gleich h und die Höhe
von β gleich h − 1. Entweder einer der Teilbäume γ1 oder γ2 besitzt die Höhe
h − 1 und der andere die Höhe h − 2 oder beide besitzen die Höhe h − 1. Der
ursprüngliche Baum hatte vor dem Löschen des Knotens die Höhe h + 2.
Die folgende Tabelle gibt in der ersten Spalte die Balancefaktoren von c
vor und in den weiteren Spalten Balancefaktoren nach erfolgter Reorganisa-
tion an.
bf(c) bf(a) bf(b) bf(c)
0 0 0 0
+ 0 − 0
− + 0 0
Daher erfüllt der rechte Baum in den Knoten a, b und c die AVL-
Bedingung und besitzt die Höhe h + 1. Eventuell erfordert dies Ausgleichsak-
tionen für höher gelegene Knoten im Pfad P .
Beispiel. Beim Löschen eines Knotens kann der Fall eintreten, wie Figur 4.17
zeigt, dass längs des Pfades bis zu Wurzel Rotationen erfolgen. Dies tritt ein
für die Bäume, die aus den inneren Knoten der Fibonacci-Bäume (Definition
4.12) bestehen, wenn wir das am weitesten rechts liegende Element löschen.
Wir löschen im Baum B8 die 12.
8. 8.
5 11 5 11
lösche 12 rechts um 10
=⇒ =⇒
3 7 10 12 3 7 10
2 4 6 9 2 4 6 9
1 1
8. 5.
5 10 3 8
rechts um 5
=⇒
3 7 9 11 2 4 7 10
2 4 6 1 6 9 11
AVL-Baumes in der Ordnung O(log(n)) ist (Satz 4.14), gilt für die Laufzeit
T (n) für das Löschen eines Elements T (n) = O(log(n)).
Wir nehmen an, dass n Elemente in einem binären Suchbaum gespeichert sind.
Die maximale Anzahl von Knoten in einem Suchpfad liegt je nach Baum zwi-
schen log2 (n) und n. Wir berechnen die durchschnittliche Anzahl der Knoten
in einem Suchpfad.
Satz 4.20. Fügen wir n Elemente in einen leeren binären Suchbaum ein, so
gilt für die durchschnittliche Anzahl P (n) von Knoten in einem Suchpfad
n+1
P (n) = 2 Hn − 3.3
n
Dabei berechnen wir den Durchschnitt über alle Suchpfade und alle möglichen
Anordnungen der n Elemente.
Beweis. Die Wahrscheinlichkeit dafür, dass das i–te Element (in der Sortier-
reihenfolge) vi erstes Element beim Einfügen ist, beträgt n1 .
Sei P̃ (n, i) die durchschnittliche Anzahl der Knoten in einem Suchpfad, falls
vi Wurzel ist. In diesem Fall erhalten wir den Baum in Figur 4.18:
v.i
l r
1
P̃ (n, i) =((P (i − 1) + 1)(i − 1) + (P (n − i) + 1)(n − i) + 1)
n
1
= (P (i − 1)(i − 1) + P (n − i)(n − i)) + 1.
n
3
Hn ist die n–te harmonische Zahl (Definition B.4).
4.4 Probabilistische binäre Suchbäume 157
1∑
n
P (n) = P̃ (n, i)
n i=1
n ( )
1∑ 1
= (P (i − 1)(i − 1) + P (n − i)(n − i)) + 1
n i=1 n
2 ∑
n−1
= 1+ iP (i).
n2 i=1
2∑ ∑
n−1 n−1
= iP (i) + iP (i) + n
n i=1 i=1
2
= xn−1 + xn−1 + n
n
n+2
= xn−1 + n, n ≥ 2, x1 = P (1) = 1 .
n
Diese Gleichung besitzt die Lösung
1
xn = (n + 1)(n + 2)(Hn+2 + − 2).
n+2
(Seite 16, Gleichung (D 1)). Für P (n) ergibt sich
2 ∑
n−1
2
P (n) = 1 + 2
iP (i) = 1 + 2 xn−1
n i=1 n
( )
2 1
= 1 + 2 n(n + 1) Hn+1 + −2
n n+1
n+1
=2 Hn − 3 .
n
Die Behauptung ist daher gezeigt. 2
Bemerkungen:
1. Für große n gilt: P (n) ≈ 2 ln(n). Die durchschnittliche Anzahl von
Knoten in einem Suchpfad im optimalen Fall beträgt ≈ log2 (n). Da
2 ln(n)
log2 (n) ≈ 1, 39 gilt, ist die durchschnittliche Anzahl der Knoten in einem
Suchpfad um höchstens 39 % größer als im optimalen Fall.
158 4. Bäume
(5,9) (11,10)
Satz 4.22. Sei S eine Menge von Elementen (k, p) mit paarweise verschie-
denen Prioritäten p. Dann gibt es genau einen Treap T , der S speichert.
4.4 Probabilistische binäre Suchbäume 159
Beweis. Wir zeigen die Behauptung durch Induktion nach n := |S|. Für
n = 0 und n = 1 ist nichts zu zeigen.
Aus n folgt n + 1: Sei n ≥ 1 und sei (k, p) ∈ S das Element minimaler
Priorität. Dieses ist Wurzel des Treap. Linker Teilbaum der Wurzel ist S1 :=
{(k̃, p̃) ∈ S | k̃ < k} und S2 := {(k̃, p̃) ∈ S | k̃ > k} ist rechter Teilbaum der
Wurzel. Dann gilt |S1 | < n und |S2 | < n. Nach Induktionsvoraussetzung gibt
es genau einen Treap T1 für S1 und genau einen Treap T2 für S2 . Mit T1 und
T2 ist auch T eindeutig bestimmt. 2
Corollar 4.23. Sei S eine Menge von Elementen (k, p). Dann hängt der
Treap T , der S speichert, nicht von der Reihenfolge ab, in der wir die Elemen-
te einfügen. Denken wir uns die Prioritäten für alle Elemente im Vorhinein
gewählt, so ergibt sich unabhängig von der Reihenfolge des Einfügens genau
ein Treap.
Beim Suchen von Elementen verwenden wir die Suchfunktion eines binären
Suchbaums (Algorithmus 4.8). Beim Einfügen gehen wir zunächst auch wie
in einem binären Suchbaum vor (Algorithmus 4.9). Im Zielknoten der Su-
che, einem Blatt, verankern wir einen neuen Knoten und füllen ihn mit dem
einzufügenden Element. Anschließend reorganisieren wir den Baum, um die
Heapbedingung herzustellen. Wir bewegen einen Knoten durch eine Links-
oder Rechtsrotation solange nach oben, bis die Heapbedingung hergestellt
ist.
Beispiel. Einfügen von (13, 7) in den Treap der Figur 4.19: Wir fügen
zunächst wie in einem binären Suchbaum ein.
.
(7,6)
(5,9) (11,10)
(13,7)
.
(7,6)
(5,9) (13,7)
(8,19)
.
Beim Entfernen eines Knotens verfährt man genau umgekehrt zum Einfü-
gen. Zunächst bewegen wir den Knoten durch Links- oder Rechtsrotationen
nach unten. Dabei rotieren wir stets mit dem Nachfolger, der kleinere Prio-
rität besitzt. In der untersten Ebene löschen wir den Knoten einfach.
Wir untersuchen Treaps deren Elemente Prioritäten besitzen, die zufällig und
paarweise verschieden gewählt sind. Wir erwarten, dass ein binärer Baum, wie
bei einer zufälligen Wahl der Schlüssel, entsteht. Es stellt sich die Frage nach
dem Aufwand, der zusätzlich zu erbringen ist, d. h. die Anzahl der Rotationen,
die beim Einfügen oder beim Löschen eines Elementes notwendig sind.
Wir betrachten das Löschen eines Knotens zunächst in einem binären
Baum, in dem alle Ebenen voll besetzt sind. Die Hälfte der Knoten liegt
in der untersten Ebene. Für diese Knoten ist beim Löschen keine Rotation
notwendig. Ein Viertel der Knoten liegt in der zweituntersten Ebene. Für
diese Knoten ist beim Löschen nur eine Rotation notwendig. Als Mittelwert
erhalten wir 12 · 0 + 14 · 1 + 18 · 2 + . . . ≤ 1.
Im Fall eines Treaps mit zufälligen Prioritäten gibt der folgende Satz auch
über den Erwartungswert der Anzahl der notwendigen Rotationen Auskunft.
Satz 4.24. Fügen wir n Elemente in einen leeren Treap ein und wählen wir
dabei die Priorität der Elemente zufällig nach der Gleichverteilung, dann gilt:
1. Für den Erwartungswert P (n) der Anzahl der Knoten in einem Suchpfad
gilt
n+1
P (n) = 2 Hn − 3.
n
2. Der Erwartungswert der Anzahl der Rotationen beim Einfügen oder Lö-
schen eines Elementes ist kleiner als 2.
4.4 Probabilistische binäre Suchbäume 161
Beweis. 1. Die Wahrscheinlichkeit dafür, dass das i–te Element (in der Sor-
tierreihenfolge) die geringste Priorität hat, d. h. Wurzel des Treaps ist, beträgt
1
n . Der Beweis ergibt sich analog zum Beweis von Satz 4.20.
2. Wir betrachten die Anzahl der Rotationen, die zum Löschen eines Kno-
tens notwendig ist. Die Anzahl der Rotationen beim Einfügen eines Knotens
ist aus Symmetriegründen gleich der Anzahl der Rotationen beim Löschen
des Knotens. Das zu löschende Element a sei das k + 1–te Element in der
Sortierreihenfolge.
Sei R der Pfad, der vom linken Nachfolger von a ausgeht und immer
dem rechten Nachfolger eines Knotens folgt und L der Pfad, der vom rechten
Nachfolger von a ausgeht und immer dem linken Nachfolger eines Knotens
folgt. Sei R : b, d, . . . , u, L : c, e, . . . , v, R′ : d, . . . , u, L′ : e, . . . , v, LTb der
linke Teilbaum von b und RTc der rechte Teilbaum von c. Wir betrachten
eine Rechtsrotation um b, wie Figur 4.22 zeigt:
a. b.
rechts um b
c =⇒ LTb a
b
LTb R′ L′ RTc R′ c
L′ RTc
Dieser Baum ist gleich dem Baum, den wir erhalten, wenn wir alle Prio-
ritäten vorweg wählen und die Elemente in aufsteigender Reihenfolge, nach
Prioritäten, einfügen. Bei dieser Reihenfolge sind keine Rotationen notwendig.
Die Elemente fügen wir unter Beachtung der binären Suchbaumbedingung als
Blatt ein. Der Treap hängt jedoch nicht von dieser Einfüge-Reihenfolge ab.
Sei P der Pfad, der von der Wurzel des linken Teilbaumes des Knotens
ausgeht, der xk+1 speichert und immer dem rechten Nachfolger eines Knotens
folgt. Sei lk der Erwartungswert der Anzahl der Knoten in P . Dann gilt l = lk .
Wir wollen lk bestimmen. Das Problem hängt nur von der Reihenfolge der
Elemente x1 , x2 , . . . , xk , xk+1 ab. Die Elemente xk+2 , . . . , xn liegen nicht auf
P . Sie sind für diese Fragestellung irrelevant.
Wir wählen das erste Element x ∈ {x1 , x2 , . . . xk , xk+1 } zufällig nach der
Gleichverteilung. Wir unterscheiden zwei Fälle:
1. x = xk+1 .
2. x = xi , 1 ≤ i ≤ k.
x.i
.
xk+1 xk+1
x1 xi+1
x2 xi+2
x3 xi+3
x4 xi+4
In Figur 4.23 ist mit der Auflistung der möglichen Schlüssel in P begonnen
worden. Diese müssen natürlich nicht alle vorkommen.
Ein Element x liegt genau dann auf P, wenn
(1) x < xk+1 , d. h. x ∈ {x1 , x2 , . . . xk } und
(2) x ist größer als alle Elemente, die vor x aus {x1 , x2 , . . . xk } gewählt wur-
den.
Wir betrachten den ersten Fall (x = xk+1 ). Sei ˜lk die Zufallsvariable, die
angibt, wie oft die Bedingung (2) eintritt. Wenn wir nach xk+1 als nächstes
xi wählen, kann für x1 , . . . , xi−1 die Bedingung (2) nicht mehr erfüllt sein.
Die Bedingung (2) kann höchstens für die k − i Elemente xi+1 , . . . , xk erfüllt
4.4 Probabilistische binäre Suchbäume 163
sein. Rekursiv ergibt sich: Bedingung (2) ist ˜lk−i + 1 mal erfüllt. Gemittelt
über alle i gilt deshalb
∑k
1 ∑˜
k−1
˜lk = 1 ˜
(1 + lk−i ) = 1 + li , k ≥ 1,
k i=1 k i=0
˜l0 = ˜lk−k = 0 (das erste Element ist xk ).
∑k ˜ Wir erhalten
Wir lösen diese Gleichung und setzen dazu xk = i=0 li .
∑
k−1
xk = ˜lk + ˜li = 1 xk−1 + xk−1 + 1.
i=0
k
Hieraus folgt:
x1 = ˜l1 = 1,
k+1
xk = xk−1 + 1, k ≥ 2.
k
Die lineare Differenzengleichung erster Ordnung∏lösen wir mit den Methoden
k
aus Abschnitt 1.3.1. Dazu berechnen wir πk = i=2 i+1 k+1
i = 2 und
( ) ( )
k+1 ∑k
2 k+1 ∑1
k+1
xk = 1+ = 1+2
2 i=2
i+1 2 i=3
i
( ( ))
k+1 3
= 1 + 2 Hk+1 −
2 2
= (k + 1)(Hk+1 − 1).
Wir erhalten
˜lk = xk − xk−1 = Hk .
1 ˜ ∑k
1 1 ˜ 1 ∑
k−1
lk = lk + lk−i = lk + li , k ≥ 1,
k+1 i=1
k+1 k+1 k + 1 i=0
l0 = 0.
∑k
Wir lösen jetzt diese Gleichung und setzen dazu xk = i=0 li . Wir erhal-
ten:
∑
k−1
1 1 ˜
xk = lk + li = xk−1 + xk−1 + lk .
i=0
k+1 k+1
Hieraus folgt:
164 4. Bäume
1˜ 1
x1 = l1 = l1 = ,
2 2
k+2 1 ˜
xk = xk−1 + lk , k ≥ 2.
k+1 k+1
Wir haben das Problem auf die Lösung einer inhomogenen Differenzenglei-
chung erster Ordnung zurückgeführt. Diese Gleichung lösen wir mit den Me-
thoden aus Abschnitt 1.3.1.
∏i
Wir berechnen πi = j=2 j+2 i+2
j+1 = 3 und
( )
k+2 1 ∑k
1 1
xk = +3 ˜li
3 2 i=2
i+1i+2
( ( ))
k+2 1 ∑k
1 1
= +3 ˜li −
3 2 i=2
i + 1 i+2
( (k+1 ))
k+2 1 ∑ 1 ∑
k+2
1
= +3 ˜li−1 − ˜li−2
3 2 i=3
i i=4
i
( ( ))
1 ˜ ∑ (˜ )1
k+1 ˜lk
k+2 1
= +3 l2 + li−1 − ˜li−2 −
3 2 3 i=4
i k+2
( ( ))
1 ∑ 1 1
k+1
k+2 1 Hk
= +3 + −
3 2 2 i=4 i − 1 i k+2
( ( ))
1 ∑ 1
k+1
k+2 1 1 Hk
= +3 + − −
3 2 2 i=4 i − 1 i k+2
( ( ))
1 ∑1 ∑1
k k+1
k+2 1 Hk
= +3 + − −
3 2 2 i=3 i i=4 i k+2
( ( ))
k+2 1 1 1 1 Hk
= +3 + − −
3 2 2 3 k+1 k+2
( )
k+2 3 Hk
= 3− −3
3 k+1 k+2
= k + 1 − Hk+1 .
Wir erhalten
1
lk = xk − xk−1 = 1 − .
k+1
Da der Erwartungswert von lk und von rk für jeden Knoten kleiner als eins
ist, erwarten wir im Mittel weniger als zwei Rotationen. 2
Bemerkung. AVL-Bäume und probabilistische binäre Suchbäume weisen ähn-
liche Performance-Merkmale auf. Bei probabilistischen binären Suchbäumen
handelt es sich um Erwartungswerte, AVL-Bäume halten die angegebenen
Schranken immer ein.
4.5 B-Bäume 165
4.5 B-Bäume
B-Bäume wurden zur Speicherung von Daten auf externen Speichermedien
von Bayer4 und McCreight5 entwickelt (siehe [BayMcC72]). Bei den externen
Speichermedien handelt es sich typischerweise um Festplattenspeicher. Diese
ermöglichen quasi wahlfreien“ Zugriff auf die Daten. Bei quasi wahlfreiem
”
Zugriff adressieren wir Datenblöcke – nicht einzelne Bytes wie bei wahlfreiem
Zugriff – und transferieren sie zwischen dem Hauptspeicher und dem exter-
nen Speicher. Im Wesentlichen beeinflusst die Anzahl der Zugriffe auf den
externen Speicher die Rechenzeit von Anwendungen zur Speicherung von Da-
ten auf externen Speichermedien. Die Zugriffszeit für zwei Bytes in einem
Datenblock ist annähernd halb so groß wie die Zugriffszeit für Bytes in un-
terschiedlichen Datenblöcken. B-Bäume minimieren die Anzahl der Zugriffe
auf das externe Speichermedium.
Große Datenmengen verwalten wir mit Datenbanken. Die Daten müssen
persistent gespeichert werden und besitzen typischerweise einen großen Um-
fang. Deshalb ist der Einsatz von Sekundärspeicher notwendig. Datenbank-
systeme organisieren die gespeicherten Daten mithilfe von B-Bäumen.
Definition 4.25. Ein Wurzelbaum heißt B-Baum der Ordnung d , wenn gilt:
1. Jeder Knoten besitzt höchstens d Nachfolger. ⌈ ⌉
2. Jeder Knoten außer der Wurzel und den Blättern besitzt mindestens d2
Nachfolger.
3. Die Wurzel enthält mindestens zwei Nachfolger, falls sie kein Blatt ist.
4. Alle Blätter liegen auf derselben Ebene.
Bemerkung. Ein B-Baum besitze die Ordnung d und die Höhe h. Die minimale
Anzahl von Knoten ist
⌈ ⌉ ⌈ ⌉h−1 ⌈ d ⌉h
d d ∑ ⌈ d ⌉i
h−1
−1
1+2+2 + ... + 2 =1+2 = 1 + 2 ⌈2d ⌉ .
2 −1
2 2 i=0
2
Wir bezeichnen die Knoten eines B-Baumes als Seiten. Der Transfer der
Daten vom Hauptspeicher auf die Festplatte erfolgt in Blöcken fester Größe.
Die Blockgröße hängt vom verwendeten externen Speichermedium ab. Wir
wählen die Größe einer Seite so, dass der Transfer vom Hauptspeicher in den
Sekundärspeicher mit einem Zugriff möglich ist.
B-Bäume von kleiner Ordnung sind wegen der geringen Seitengröße weni-
ger für die Organisation von Daten auf externen Speichermedien geeignet. Sie
sind eine weitere Methode, um Daten effizient im Hauptspeicher zu verwalten.
B-Bäume der Ordnung vier, zum Beispiel, sind eine zu Rot-Schwarz-Bäumen
äquivalente Struktur (Übungen, Aufgabe 17). Rot-Schwarz-Bäume sind eine
weitere Variante balancierter binärer Suchbäume (Übungen, Aufgabe 10).
a0 x1 a1 x2 a2 . . . al−1 xl al . . . . . .
Bemerkung. Wegen der oben definierten Anordnung sind die Elemente in sor-
tierter Reihenfolge im B-Baum gespeichert. Wenn wir den B-Baum inorder“
”
traversieren und ausgegeben, d. h.
4.5 B-Bäume 167
(1) starte mit dem ersten Element x der Wurzelseite, gebe zunächst (rekursiv)
die Elemente in S(lx ), dann x und anschließend (rekursiv) die Elemente
in der Seite S(rx ) aus,
(2) setze das Verfahren mit dem zweiten Element der Wurzelseite fort, und
so weiter.
so ist die Ausgabe aufsteigend sortiert.
Beispiel. Figur 4.24 zeigt einen B-Baum für die Menge {A,B,E,H,L,M,N,O,P,
Q,R,T,V,W}
4.5.1 Pfadlängen
Da alle Blätter eines B-Baumes auf derselben Ebene liegen, haben alle Pfade
von der Wurzel zu einem Blatt dieselbe Länge. Die Länge eines Pfades ist
durch die Höhe des B-Baumes beschränkt und bestimmt die Anzahl der not-
wendigen Seitenzugriffe beim Suchen, Einfügen und Löschen eines Elements.
Satz 4.26. Eine Menge S, |S| = n, werde in einem B-Baum der Ordnung d
gespeichert. Für die Höhe h des Baumes gilt dann:
( ) ⌊ ⌋
n+1 d−1
logd (n + 1) − 1 ≤ h ≤ logq+1 ,q= .
2 2
Insbesondere ist die Höhe h des Baumes logarithmisch in der Anzahl der im
Baum gespeicherten Elemente: h = O(log2 (n)).
Beweis. Sei min die minimale und max die maximale Anzahl von Elementen
in einem B-Baum der Höhe h. Dann gilt
min = 1 + 2q + 2(q + 1)q + . . . + 2(q + 1)h−1 q
∑
h−1
(q + 1)h − 1
= 1 + 2q (q + 1)i = 1 + 2q = 2(q + 1)h − 1 ≤ n.
i=0
q
( n+1 )
Es folgt h ≤ logq+1 2 .
max = (d − 1) + d(d − 1) + . . . + dh (d − 1)
∑
h
dh+1 − 1
= (d − 1) di = (d − 1) ≥ n.
i=0
d−1
168 4. Bäume
Die Seiten eines B-Baumes liegen auf dem Sekundärspeicher. Die Wurzel eines
B-Baumes befindet sich immer im Hauptspeicher. Weitere Seiten befinden
sich nur soweit möglich und notwendig im Hauptspeicher. Wir sprechen die
Seiten eines B-Baumes im Hauptspeicher mit dem Datentyp page an.
Die Sekundärspeicheradressen sind vom Typ address. Dabei bezeichnen
1, 2, 3, . . . gültige Seitenadressen, die Seitenadresse 0 spielt die Rolle von null
in verketteten Listen.
Wir geben zunächst die Funktion zum Suchen von Elementen an. e ist
das zu suchende Element und p ist die Wurzel des B-Baumes.
Algorithmus 4.27.
(page, index) BTreeSearch(page p, item e)
1 while true do
2 (i, adr) ← PageSearch(p, e)
3 if i > 0
4 then return (p, i)
5 else if adr ̸= 0
6 then p ← ReadPage(adr)
7 else return(p, 0)
BTreeSearch gibt die Seite, in der sich e befindet, in page zurück. Mit
dem Index i greifen wir dann in der Seite auf e zu.
Befindet sich e nicht im B-Baum, so endet die Suche in einem Blatt. BTree-
Search gibt in index 0 zurück (gültige Indizes beginnen ab 1). page ist die
Seite, in die e einzufügen wäre.
Da die Seite, die D aufnehmen soll, bereits voll ist, belegen wir eine neue Seite.
Die Elemente A, B, D verteilen wir auf zwei Seiten, das mittlere Element
C geht in die Vorgängerseite. Da die Vorgängerseite voll ist, belegen wir
nochmals eine neue Seite. Die Elemente C, E, N verteilen wir auf zwei Seiten,
das mittlere Element H geht in die Vorgängerseite. Wir erhalten den B-Baum,
den Figur 4.25 darstellt.
170 4. Bäume
a0 x1 a1 x2 a2 . . . . . . al−1 xl al
ã0 x̃1 . . . . . . x̃k−1 ãk−1 und ãk x̃k+1 . . . . . . ãl x̃l+1 ãl+1
Für den rechten Teil ãk x̃k+1 . . . . . . ãl x̃l+1 ãl+1 belegen wir eine neue Seite.
Die Adresse dieser Seite sei b̃. Füge x̃k , b̃ (sortiert) in der Vorgängerseite ein.
Der Algorithmus verändert die B-Baumeigenschaften nicht, denn die
Adresse, die links von x̃k steht, referenziert die alte Seite, die nun
ã0 x̃1 . . . . . . x̃k−1 ãk−1 enthält. Die darin gespeicherten Elemente sind klei-
ner als x̃k . Der B-Baum bleibt nach der Teilung eines Knotens sortiert. ⌊ d−1 ⌋ Nach
der Teilung eines vollen Knotens hat jeder Knoten mindestens viele
⌊ ⌋ ⌈d⌉ 2
Elemente und mindestens d−1 2 + 1 (= 2 ) viele Nachfolger. Die untere
Schranke für die Anzahl der Elemente, die eine Seite enthalten muss, beach-
ten wir bei der Teilung.
PageInsert fügt in die Seite p, die sich im Hauptspeicher befindet, das Ele-
ment e mit Adresse a ein und schreibt die Seite auf den Sekundärspeicher.
Falls die Seite p voll ist, belegt PageInsert eine neue Seite, führt die Auftei-
lung der Elemente durch und schreibt beide Seiten auf den Sekundärspeicher.
Wenn der Rückgabewert insert wahr ist, ist der Fall einer vollen Seite einge-
treten. In diesem Fall ist ein Element in die Vorgängerseite einzufügen. Dieses
Element ist (item f, adr b).
4.5 B-Bäume 171
Algorithmus 4.28.
BTreeInsert(page t, item e)
1 (p, i) ← BTreeSearch(t, e)
2 if i ̸= 0 then return
3 b←0
4 repeat
5 (insert, e, b) ← PageInsert(p, e, b)
6 if insert
7 then if p.predecessor ̸= 0
8 then p ← ReadPage(p.predecessor)
9 else p ← new page
10 until insert = false
Bemerkungen:
1. Zunächst ermittelt BTreeSearch das Blatt, in das e einzufügen ist. Pa-
geInsert fügt dann e in das Blatt ein. Falls insert wahr ist, lesen wir
anschließend die Vorgängerseite aus dem Sekundärspeicher. Das mittlere
Element (e, b) fügen wir in die Vorgängerseiten ein, solange der Fall der
Teilung einer Seite vorliegt, d. h. insert = true ist.
Der obige Algorithmus liest auf dem Pfad vom Blatt zur Wurzel nochmals
Seiten aus dem Sekundärspeicher. Es ist aber möglich, dies durch eine
sorgfältigere Implementierung zu vermeiden.
2. Falls in einen vollen Wurzelknoten ein Element einzufügen ist, belegt
PageInsert eine neue Seite und verteilt die Elemente der alten Wurzel
auf die alte Wurzel und die neue Seite. In Zeile 9 belegt BTreeSearch
eine Seite für eine neue Wurzel. Das mittlere Element fügen wir dann in
die neue Wurzel ein (Zeile 5). Die Höhe des Baumes nimmt um eins zu.
3. Die Ausgeglichenheit geht nicht verloren, da der Baum von unten nach
oben wächst.
Das Löschen eines Elements muss bezüglich der B-Baum Struktur invariant
sein. Es sind folgende Punkte zu beachten:
1. Weil für innere Seiten die Bedingung Anzahl der Adressen = Anzahl der
”
Elemente + 1“ gilt, ist es zunächst nur möglich, ein zu löschendes Element
aus einer Blattseite zu entfernen. Befindet sich das zu löschende Element
x nicht in einem Blatt, so vertausche x mit seinem Nachfolger (oder
Vorgänger) in der Sortierreihenfolge. Dieser ist in einem Blatt gespeichert.
Jetzt entfernen wir x aus dem Blatt. Nachdem x entfernt wurde, ist die
Sortierreihenfolge wieder hergestellt.
2. Entsteht durch Entfernen eines Elements Underflow, so müssen wir den
B-Baum reorganisieren. Underflow liegt vor, ⌊ falls ein
⌋ Knoten, der ver-
schieden von der Wurzel ist, weniger als (d − 1)/2 Elemente enthält.
172 4. Bäume
passen die Elemente aus der Seite mit Underflow, der Nachbarseite und
das Element aus dem Vorgänger in eine Seite.
Tritt Underflow in der Wurzel ein, d. h. die Wurzel enthält keine Elemente,
so geben wir die Seite frei.
3. Der Ausgleich zwischen direkt benachbarten Seiten S1 (linker Knoten)
und S2 (rechter Knoten) erfolgt über den Vorgängerknoten S der beiden
Seiten. Für ein Element x ∈ S bezeichnen wir mit lx die Adresse, die links
von x und mit rx die Adresse, die rechts von x steht. lx referenziert S1 und
rx die Seite S2 . Die Elemente in S1 sind kleiner als x und die Elemente
in S2 sind größer als x. Wir diskutieren den Fall, dass der Ausgleich von
S1 nach S2 erfolgt. Der umgekehrte Fall ist analog zu behandeln. Das
größte Element v in S1 (es steht am weitesten rechts) geht nach S an
die Stelle von x und x geht nach S2 und besetzt dort den Platz, der
am weitesten links liegt. Jetzt müssen wir den Baum anpassen. Die in
S2 fehlende Adresse lx ersetzen wir durch rv : lx = rv . Die Adresse rv
löschen wir in S1 . Die Elemente in S1 sind kleiner als v, die Elemente in
S2 sind größer als v. Die Elemente in S(lx ) sind größer als v, aber kleiner
als x. Der B-Baum bleibt auch nach der Anpassung sortiert.
Beispiel. Wir betrachten jetzt anhand eines Beispiels die verschiedenen Fälle,
die beim Löschen eintreten können. Wir löschen im B-Baum der Ordnung 4,
den Figur 4.26 darstellt, das Element U.
Dazu tauschen wir U mit T. Jetzt befindet sich U in einem Blatt und wir
können U löschen. Wir erhalten den B-Baum der Figur 4.27.
4.5 B-Bäume 173
Als Nächstes löschen wir Y. Jetzt tritt Underflow ein, es erfolgt ein Aus-
gleich mit dem direkten Nachbarn. Wir erhalten den B-Baum der Figur 4.28.
Schließlich löschen wir T. Es tritt erneut Underflow ein, wir fassen direkt
benachbarte Seiten zusammen. Nach nochmaligem Underflow im Vorgänger-
knoten, erfolgt ein Ausgleich mit dem direkten Nachbarn. Das Ergebnis ist
der B-Baum der Figur 4.29.
Als letztes löschen wir A. Wegen Underflow fassen wir direkt benachbarte
Seiten zusammen. Es tritt nochmals Underflow im Vorgängerknoten ein. Da
kein Ausgleich mit dem direkten Nachbarn möglich ist, fassen wir nochmals
direkt benachbarte Seiten zusammen. Wir erhalten den B-Baum der Figur
4.30.
Nach dem Löschen der Wurzel nimmt die Höhe des B-Baumes um eins ab.
174 4. Bäume
Algorithmus 4.29.
BTreeDel(item e)
1 with page S containing e
2 if S is not a leaf
3 then exchange e with the successor∗ of e in the page S̃
4 S ← S̃ (now S is a leaf and contains e)
5 delete e from the page S
6 while underflow in S do
7 if S = root
8 then free S , return
9 attempt to balance between immediate neighbors
10 if balance successful
11 then return
12 combine directly adjacent pages
13 S ← predecessor of S
∗
The successor in the sorted sequence is located in a leaf.
Bemerkungen:
1. B-Bäume sind per Definition vollständig ausgeglichen. Die Aufrechter-
haltung der B-Baumstruktur beim Einfügen und Löschen ist durch einfa-
che Algorithmen sichergestellt. Im ungünstigsten Fall sind beim Einfügen
und Löschen eines Elements alle Knoten des Suchpfades vom Blatt bis
zur Wurzel betroffen. Die (Anzahl der ) Sekundärspeicher-Operationen ist
in der Ordnung O(logq+1 (n + 1)/2 ), q = ⌊(d − 1)/2⌋, wobei d die Ord-
nung des B-Baumes ist und n die Anzahl der gespeicherten Elemente
bezeichnet (Satz 4.26).
Der Preis für die Ausgeglichenheit des Baumes besteht darin, dass die
einzelnen Seiten unter Umständen nur halb“ gefüllt sind.
”
2. Die in den Blattseiten auftretenden Adressen haben alle den Wert 0 und
wir brauchen sie deshalb nicht zu speichern. Wir vermerken aber, dass es
sich um ein Blatt handelt.
3. Beim Einfügen kann man wie beim Löschen einen Ausgleich zwischen
direkt benachbarten Seiten durchführen. Anstatt bei einer vollen Seite
sofort eine neue Seite anzulegen, überprüfen wir zuerst, ob in einer di-
rekt benachbarten Seite noch Platz ist. Eine neue Seite benötigen wir erst,
wenn zwei Seiten voll sind. Dadurch erhöht sich die Ausnutzung des Spei-
chers, jede Seite, außer der Wurzel, ist mindestens zu 2/3 gefüllt. Diese
B-Baum-Variante wird mit B∗ -Baum bezeichnet. Neben der effizienteren
Speichernutzung ergibt sich für B∗ -Bäume bei gegebener Anzahl von ge-
speicherten Elementen eine geringere Höhe im Vergleich zu B-Bäumen
(Satz 4.26).
4. Datenbankanwendungen speichern Datensätze. Diese Datensätze identi-
fizieren wir durch einen Schlüssel. Die Länge des Schlüssels ist oft klein
4.5 B-Bäume 175
4.6 Codebäume
Definition 4.30.
1. Unter einem Alphabet verstehen wir eine nicht leere endliche Menge X.
Die Elemente x ∈ X heißen Symbole.
2. Eine endliche Folge von Symbolen x = x1 . . . xn , xi ∈ X, i = 1, . . . , n,
heißt Wort oder Nachricht über X. |x| := n heißt Länge von x. ε
bezeichnet das Wort ohne Symbole, das leere Wort. Die Länge von ε ist
0, |ε| = 0.
3. X ∗ := {x | x Wort über X} heißt die Menge der Nachrichten über X.
4. Für n ∈ N0 heißt X n := {x ∈ X ∗ | |x| = n} die Menge der Nachrichten
der Länge n über X.
Beispiel. Ein wichtiges Beispiel sind binäre Nachrichten. Die Menge der
binären Nachrichten ist {0, 1}∗ und die Menge der binären Nachrichten der
Länge n ist {0, 1}n .
Definition 4.31. Seien X = {x1 , . . . , xm } und Y = {y1 , . . . , yn } Alphabete.
Eine Codierung von X über Y ist eine injektive Abbildung
C : X −→ Y ∗ \{ε}.
Mit C bezeichnen wir auch das Bild von C. Es besteht aus den Codewörtern
C(x1 ), . . . , C(xm ) und heißt ein Code über Y der Ordnung m.
Beispiel. Sei X = {a, b, c, d}. Codierungen von X über {0, 1} sind gegeben
durch: a 7−→ 0, b 7−→ 111, c 7−→ 110, d 7−→ 101 oder
a 7−→ 00, b 7−→ 01, c 7−→ 10, d 7−→ 11.
Der Codierer erzeugt bei Eingabe einer Nachricht aus X ∗ die codierte Nach-
richt, eine Zeichenkette aus Y ∗ . Dabei verwendet er zur Codierung der Sym-
bole aus X einen Code. Zur Codierung einer Zeichenkette aus X ∗ setzt der
Codierer die Codewörter für die Symbole, aus denen die Zeichenkette be-
steht, hintereinander. Die Aufgabe des Decodierers besteht darin, aus der co-
dierten Nachricht die ursprüngliche Nachricht zu rekonstruieren. Eindeutig
decodierbar bedeutet, dass eine Nachricht aus Y ∗ höchstens eine Zerlegung
in Codewörter besitzt. Die Verwendung eines eindeutig decodierbaren Codes
4.6 Codebäume 177
versetzt somit den Decodierer in die Lage, die Folge der codierten Nachrich-
ten zu ermitteln. Deshalb bezeichnen wir eindeutig decodierbare Codes auch
als verlustlose Codes. Etwas formaler definieren wir
Definition 4.32. Seien X = {x1 , . . . , xm } und Y = {y1 , . . . , yn } Alphabete
und C : X −→ Y ∗ \{ε} eine Codierung von X über Y .
1. Die Fortsetzung von C auf X ∗ ist definiert durch
C ∗ : X ∗ −→ Y ∗ , ε 7−→ ε, xi1 . . . xik 7−→ C(xi1 ) . . . C(xik ).
2. Die Codierung C oder der Code C = {C(x1 ), . . . , C(xn )} heißt eindeutig
decodierbar , wenn die Fortsetzung C ∗ : X ∗ −→ Y ∗ injektiv ist.
Die eindeutige Decodierbarkeit weisen wir zum Beispiel durch Angabe ei-
nes Algorithmus zur eindeutigen Decodierung nach. Eine codierte Zeichenket-
te muss sich eindeutig in eine Folge von Codewörtern zerlegen lassen. Durch
Angabe von zwei Zerlegungen einer Zeichenkette zeigen wir, dass ein Code
nicht eindeutig decodierbar ist.
Beispiel.
1. Der Code C = {0, 01} ist eindeutig decodierbar. Wir decodieren c =
ci1 . . . cik durch
{
0, falls c = 0 oder c = 00 . . . ,
c i1 =
01, falls c = 01 . . . ,
und setzen das Verfahren rekursiv mit ci2 . . . cik fort.
2. C = {a, c, ad, abb, bad, deb, bbcde} ist nicht eindeutig decodierbar, denn
abb|c|deb|ad = a|bbcde|bad sind zwei Zerlegungen in Codewörter.
Kriterium für die eindeutige Decodierbarkeit. Sei C = {c1 , . . . , cm } ⊂
Y ∗ \ {ε}. C ist nicht eindeutig decodierbar, falls es ein Gegenbeispiel zur
eindeutigen Decodierbarkeit gibt, d. h. für ein c ∈ Y ∗ gibt es zwei Zerlegungen
in Codewörter. Genauer, falls es (i1 , . . . , ik ) und (j1 , . . . , jl ) mit
ci1 . . . cik = cj1 . . . cjl und (i1 , . . . , ik ) ̸= (j1 , . . . , jl )
gibt. Auf der Suche nach einem Gegenbeispiel starten wir mit allen Co-
dewörtern, die ein Codewort als Präfix besitzen. Für jedes dieser Codewörter
prüfen wir, ob das zugehörige Postfix entweder ein weiteres Codewort als
Präfix abspaltet oder Präfix eines Codeworts ist. Wir definieren für einen
Code C die Folge
C0 := C,
C1 := {w ∈ Y ∗ \ {ε} | es gibt ein w′ ∈ C0 mit: w′ w ∈ C0 },
C2 := {w ∈ Y ∗ \ {ε} | es gibt ein w′ ∈ C0 mit: w′ w ∈ C1 }
∪ {w ∈ Y ∗ \ {ε} | es gibt ein w′ ∈ C1 mit: w′ w ∈ C0 },
..
.
178 4. Bäume
Wir bezeichnen die Cn definierenden Mengen mit Cn1 und Cn2 . Die Elemente
w ∈ C1 sind Postfixe von Codewörtern (das dazugehörige Präfix ist auch ein
Codewort). Diese müssen wir weiter betrachten. Es treten zwei Fälle ein:
(1) Entweder spaltet w ∈ C1 ein weiteres Codewort als Präfix ab (der Rest
von w liegt in C2 ) oder
(2) w ∈ C1 ist Präfix eines Codewortes c ∈ C und der Rest von c liegt in C2 .
Wir verarbeiten die Elemente aus C2 rekursiv weiter, d. h. wir bilden die
Mengen C3 , C4 , . . ..
Wir finden ein Gegenbeispiel zur eindeutigen Decodierbarkeit, falls
Cn ∩ C0 ̸= ∅ für ein n ∈ N.
Beispiel. Wir betrachten den Code C = {a, c, ad, abb, bad, deb, bbcde} von
oben.
C0 C1 C2 C3 C4 C5
a
c
ad d eb
abb bb cde de b ad , bcde
bad
deb
bbcde
Da ad ∈ C5 ∩C0 , erhalten wir ein Gegenbeispiel zur eindeutigen Decodierung:
abbcdebad besitzt die Zerlegungen a|bbcde|bad und abb|c|deb|ad.
Ist C eindeutig decodierbar, dann folgt Cn ∩ C = ∅ für alle n ∈ N. Der
folgende Satz, publiziert in [SarPat53], behauptet die Äquivalenz beider Aus-
sagen.
Satz 4.33. Für einen Code C = {c1 , . . . , cm } sind äquivalent:
1. C ist eindeutig decodierbar.
2. Cn ∩ C = ∅ für alle n ∈ N.
{
Beweis. Sei M = w ∈ Y ∗ | es gibt ci1 , . . . , cik , cj1 , . . . ,}cjl ∈ C mit:
ci1 . . . cik w = cj1 . . . cjl und w ist echter Postfix von cjl . Wir zeigen, dass
∪
M= Cn
n≥1
(cjl−1 endet mit cir−1 oder das Ende von cjl−1 liegt in cir ).
Sei w′ ∈ Y ∗ \ {ε} mit
und
w′ cir+1 . . . cik w = cjl ∈ C.
Ist w′ = cir , dann folgt wie oben, dass w ∈ Ck−r+1 . In diesem Fall ist die
Behauptung gezeigt.
Ist w′ ein echter Postfix von cir , dann folgt nach der Induktionsvoraus-
setzung, angewendet auf cj1 . . . cjl−1 w′ = ci1 . . . cik , dass w′ ∈ Cm für ein m.
Da w′ cir+1 . . . cik w ∈ C gilt (d. h. w′ ist Präfix eines Codewortes), folgt, dass
cir+1 . . . cik w Element von Cm+1 ist, und wie oben folgt w ∈ Cm+(k−r)+1 .
Unmittelbar aus der Definition von M folgt: ∪ C ist genau dann eindeutig
decodierbar, wenn C ∩ M = ∅ gilt. Aus M = n≥1 Cn folgt, C ∩ M = ∅
genau dann, wenn C ∩ Cn = ∅ für alle n ∈ N ist. 2
Corollar 4.34. Die eindeutige Decodierbarkeit ist entscheidbar, d. h. es gibt
einen Algorithmus, der bei Eingabe von C ⊂ Y ∗ entscheidet, ob C eindeutig
decodierbar ist.
Beweis. Sei C ⊂ Y ∗ \ {ε} ein Code. Wir entwerfen einen Algorithmus zur
Entscheidung der eindeutigen Decodierbarkeit mit dem Kriterium des voran-
gehenden Satzes. Sei m = maxc∈C |c|. Es gilt |w| ≤ m für alle w ∈ Cn , n ∈ N.
180 4. Bäume
y i1 . . . y i k y 1 y i1 . . . y i k y 2 ...... ...... y i1 . . . y i k y n .
2. Sei C ⊂ Y ∗ ein Code über Y .
Wir markieren im Baum B von Y ∗ die Wurzel, alle Codewörter aus C
und alle Pfade, die von der Wurzel zu Codewörtern führen.
Die markierten Elemente von B definieren den Codebaum von C.
Beispiel. Figur 4.32 zeigt den Codebaum für den binären Code {00, 001, 110,
111, 0001}. Die Codewörter befinden sich in den Rechteck-Knoten.
ε.
0 1
00 11
0001
Algorithmus 4.37.
Decode(code c[1..n])
1 l ← 1, m ← ε
2 while l ≤ n do
3 node ← root, j ← l
4 while node ̸= leaf do
5 if c[j] = 0 then node ← node.lef t
6 else node ← node.right
7 j ←j+1
8 m ← m||tab[c[l..j − 1]], l ← j
..
0 1
. 1
0 1
. .
1 0 1
. . 2 3
0 1
.. . .
.
0 1 0 1
4 5 6 7
7
Peter Elias (1923 – 2001) war ein amerikanischer Informationstheoretiker.
182 4. Bäume
Der Elias-Delta-Code Cδ baut auf Cγ auf. Bei Cδ stellen wir der Binärent-
wicklung von z, die Länge der Binärentwicklung von z voran, codiert mit Cγ .
Da jede Binärentwicklung einer Zahl mit 1 beginnt, lassen wir die führende 1
in der Codierung von z weg. So besitzt zum Beispiel 31 die Binärentwicklung
11111. Cγ (⌊log2 z⌋ + 1) = Cγ (5) = 00101. Wir erhalten 001011111.
4.6.2 Huffman-Codes
∑
m
l(C) := pi |C(xi )|
i=1
Die Entropie ist unabhängig von der Codierung einer Quelle definiert. Den
Zusammenhang zur Codierung der Quelle stellt der Quellencodierungssatz
von Shannon9 her.
Satz 4.39. Sei (X, p) eine Quelle. Für jeden eindeutig decodierbaren Code
C : X −→ {0, 1}∗ \{ε} gilt:
H(X) ≤ l(C).
Weiter gilt, es gibt einen Präfixcode C mit l(C) < H(X) + 1. Insbesondere
gilt dies für jeden kompakten Code C.
Ein Beweis des Satzes ist zum Beispiel in [HanHarJoh98] zu finden.
Der Huffman-Algorithmus, publiziert in [Huffman52], konstruiert für ei-
ne Quelle einen kompakten Präfixcode und den zugehörigen Codebaum.
Zunächst ordnen wir jeder Nachricht einen Knoten, genauer ein Blatt zu
und gewichten dieses mit der Auftrittswahrscheinlichkeit der Nachricht.
Die Konstruktion des Codebaumes erfolgt jetzt in zwei Phasen. In der
ersten Phase konstruieren wir, ausgehend von den Blättern, einen binären
Baum. Der Algorithmus erzeugt in jedem Schritt einen neuen Knoten n und
wählt aus den bestehenden Knoten ohne Vorgänger zwei Knoten n1 und n2
mit geringstem Gewicht als Nachfolger von n. Das Gewicht von n ist die
Summe der Gewichte von n1 und n2 . Wir ersetzen in der Quelle die n1 und
n2 entsprechenden Nachrichten durch eine Nachricht, die n entspricht. Die
Wahrscheinlichkeit dieser Nachricht ist die Summe der Wahrscheinlichkeiten,
die sie ersetzt. In jedem Schritt nimmt die Anzahl der Elemente der Quelle
um eins ab. Die erste Phase startet mit den Blättern, die den Nachrichten
zugeordnet sind und terminiert, wenn die Quelle nur noch ein Element enthält.
Dieses Element hat die Wahrscheinlichkeit 1 und steht für die Wurzel des
Codebaumes.
In der zweiten Phase berechnen wir, ausgehend von der Wurzel, die Co-
dewörter. Der Wurzel weisen wir das leere Codewort ε zu. Die Codes der
beiden Nachfolger eines Knotens n ergeben sich durch die Verlängerung
des Codes von n mit 0 und 1. Dadurch erhalten Nachrichten mit geringer
Auftrittswahrscheinlichkeit längere Codierungen und Nachrichten mit hohen
Auftrittswahrscheinlichkeiten kurze Codierungen. Der Huffman-Algorithmus
folgt damit der Greedy-Strategie. Wir betrachten zunächst ein
9
Claude Elwood Shannon (1916 – 2001) war ein amerikanischer Mathemati-
ker. Er ist der Begründer der Informationstheorie und ist berühmt für seine
grundlegenden Arbeiten zur Codierungstheorie ([Shannon48]) und Kryptogra-
phie ([Shannon49]).
184 4. Bäume
0 ε.
1
a 0.4 0 0.6
1
b 0.25 0.35
0 1
0.2 0.15
0 1 0 1
c 0.1 d 0.1 e 0.1 f 0.05
Wir beschreiben jetzt den allgemeinen Fall der Konstruktion der Huffman-
Codierung
C : X −→ {0, 1}∗ \{ε}
für eine Quelle (X, p), X = {x1 , . . . , xm }, p = (p1 , . . . , pm ). Wir setzen ohne
Einschränkung voraus, dass m ≥ 2 und pi > 0 für 1 ≤ i ≤ m.
1. m = 2:
X = {x1 , x2 }. Setze C(x1 ) := 0 und C(x2 ) := 1.
2. Sei m > 2:
Sortiere die Symbole xi der Quelle so, dass gilt: p1 ≥ p2 ≥ . . . ≥ pm .
(X̃, p̃) sei definiert durch:
X̃ = {x1 , . . . , xm−2 , x̃m−1 },
p(xi ) := pi für 1 ≤ i ≤ m − 2,
p(x̃m−1 ) := pm−1 + pm .
X̃ enthält m − 1 Symbole. Wähle eine Huffman-Codierung
C̃ : X̃ −→ {0, 1}∗ \ {ε}
und gewinne aus dieser die Huffman-Codierung
C : X −→ {0, 1}∗ \{ε}
durch die Zuordnung
C(xi ) := C̃(xi ) für 1 ≤ i ≤ m − 2,
C(xm−1 ) := C̃(x̃m−1 )0,
C(xm ) := C̃(x̃m−1 )1.
4.6 Codebäume 185
Bevor wir zeigen, dass die Konstruktion einen kompakten Code liefert,
formulieren wir zwei Lemmata.
Lemma 4.40. Sei C : X −→ {0, 1}∗ \{ε} eine kompakte Codierung von
(X, p). Ist pi > pj , so ist |C(xi )| ≤ |C(xj )|.
Beweis. Angenommen, pi > pj und |C(xi )| > |C(xj )|. Durch Vertauschen der
Codierungen von xi und xj ergibt sich ein Code kürzerer mittlerer Wortlänge.
Ein Widerspruch. 2
Lemma 4.41. Sei C = {c1 , . . . , cm } ein kompakter Präfixcode. Dann gibt es
zu jedem Codewort maximaler Länge ein Codewort, welches mit diesem bis
auf die letzte Stelle übereinstimmt.
Beweis. Falls die Aussage des Lemmas nicht gilt, ist der Code verkürzbar
und folglich nicht kompakt. 2
Satz 4.42. Das Huffman-Verfahren ergibt einen kompakten Präfixcode.
Beweis. Aus der Konstruktion ergibt sich unmittelbar, dass C ein Präfixcode
ist. Wir beweisen durch Induktion nach der Anzahl m der Elemente der
Quelle, dass C kompakt ist. Die Behauptung folgt für m = 2 unmittelbar.
Wir zeigen jetzt, dass aus m − 1 m folgt. Seien (X, p), C, (X̃, p̃) und C̃
wie oben gegeben. Nach Induktionsvoraussetzung ist C̃ kompakt. Wir zeigen,
dass C kompakt ist. Sei C ′ = {c′1 , . . . , c′m } ein kompakter Code für X. Nach
Lemma 4.40 gilt: |c′1 | ≤ |c′2 | ≤ . . . ≤ |c′m |. Nach Lemma 4.41 ordnen wir die
Codewörter maximaler Länge so an, dass c′m−1 = c̃0 und c′m = c̃1 für ein
c̃ ∈ {0, 1}∗ . C̃ ′ sei nun folgender Code für die Quelle X̃:
C̃ ′ (xi ) := C ′ (xi ) für 1 ≤ i ≤ m − 2,
C̃ ′ (x̃m−1 ) := c̃.
Da C̃ kompakt ist, gilt l(C̃) ≤ l(C̃ ′ ). Hieraus folgt:
l(C) = l(C̃) + pm−1 + pm ≤ l(C̃ ′ ) + pm−1 + pm = l(C ′ ).
Also ist l(C) = l(C ′ ) und C ist kompakt. 2
Algorithmus 4.43.
HuffmanCode(int pr[1..m])
1 if m ≥ 2
2 then p ← pr[1], p[1] ← pr[m]
3 DownHeap(pr[1..m − 1])
4 q ← pr[1], pr[1] ← p + q
5 DownHeap(pr[1..m − 1])
6 CreatePredecessor(p, q)
7 HuffmanCode(pr[1..m − 1])
Die Funktion CreatePredecessor(p, q) dient zum Aufbau des Huffman-Baumes.
Sie erzeugt einen neuen Knoten, weist die Wahrscheinlichkeit p + q zu, fügt
den Knoten mit Wahrscheinlichkeit p als linken und den Knoten mit Wahr-
scheinlichkeit q als rechten Nachfolger an und markiert die Kante zum linken
Nachfolger mit 0 und die Kante zum rechten Nachfolger mit 1. Nach der
Ausführung von DownHeap ist die Heapbedingung hergestellt, falls diese nur
in der Wurzel verletzt war (siehe Algorithmus 2.12). Daher wählen wir in den
Zeilen 2 und 4 die beiden niedrigsten Wahrscheinlichkeiten.
Satz 4.44. Algorithmus 4.43 berechnet für die Quelle (X, pr) einen Huffman-
Code mit einer Laufzeit in der Ordnung O(m log(m)).
Beweis. Die Laufzeit von BuildHeap ist in der Ordnung O(m), die Laufzeit
von DownHeap in der Ordnung von O(log(m)) (Satz 2.17 und Lemma 2.16).
CreatePredecessor lässt sich mit Laufzeit O(1) implementieren. Die Anzahl
der (rekursiven) Aufrufe von HuffmanCode ist m − 1. Die Laufzeit ist von
der Ordnung O(m + (m − 1) log(m)) = O(m log(m)). 2
Beweis. Wir zeigen die Aussage durch Induktion nach m. Für m = 1 ist
die Aussage offensichtlich. Sei T ein Codebaum mit m Blättern, m ≥ 2.
Entfernen wir zwei Blätter mit demselben Vorgänger, so erhalten wir einen
Codebaum mit m − 1 vielen Blättern. Nach Induktionsvoraussetzung besitzt
er 2(m − 1) − 1 viele Knoten. T hat darum 2(m − 1) + 1 = 2m − 1 viele
Knoten. 2
Definition 4.47. Sei T ein gewichteter Codebaum mit m Blättern und den
Knoten {n1 , . . . , n2m−1 }.
1. T ist ein Huffman-Baum, wenn es eine Instanz des Huffman-Algorithmus
gibt, welche T erzeugt.
2. n1 , . . . , n2m−1 ist eine Gallager-Ordnung der Knoten von T , wenn gilt:
a. w(n1 ) ≤ w(n2 ) ≤ . . . ≤ w(n2m−1 ).
b. n2l−1 und n2l , 1 ≤ l ≤ m − 1, sind Geschwisterknoten.
Die Knoten der gewichteten Codebäume der Figuren 4.35 und 4.36 sind
durchnummeriert: (1), (2), . . . .
Beispiel. Figur 4.35 zeigt einen gewichteten Codebaum, der mehrere Anord-
nungen der Knoten nach aufsteigenden Gewichten besitzt, die die Geschwis-
terbedingung erfüllen. Zum Beispiel erfüllen die Anordnungen 5, 6, 8, 9, 13,
14, 16, 17, 4, 7, 12, 15, 3, 10, 11, 2, 1 und 13, 14, 16, 17, 5, 6, 8, 9, 12, 15, 4, 7,
3, 10, 11, 2, 1 die Geschwisterbedingung. Der Codebaum T besitzt mehrere
Gallager-Ordnungen.
10
Newton Faller (1947 – 1996) war ein amerikanischer Informatiker.
11
Robert G. Gallager (1931 – ) ist ein amerikanischer Informationstheoretiker.
12
Donald E. Knuth (1938 – ) ist ein amerikanischer Informatiker.
188 4. Bäume
. (1)
12
8 (2) 4 (11)
Beispiel. Der gewichtete Codebaum der Figur 4.36 besitzt keine Gallager-
Ordnung. Es gibt nur zwei mögliche Anordnung der Knoten nach aufsteigen-
den Gewichten: 4, 5, 7, 8, 3, 10, 11, 6, 2, 9, 1 und 4, 5, 7, 8, 3, 10, 11, 6, 9, 2,
1. Für beide Anordnungen ist die Geschwisterbedingung verletzt.
. (1)
100
50 (2) 50 (9)
23 (3) 27 (6) D 24 E 26
(10) (11)
A 11 B 12 C 13 F 14
(4) (5) (7) (8)
Satz 4.48. Für einen gewichteten Codebaum T mit w(n) > 0 für alle Knoten
n sind äquivalent:
1. T ist ein Huffman-Baum.
2. T besitzt eine Gallager-Ordnung.
Beweis. Wir zeigen die Aussage durch Induktion nach der Anzahl m der
Blätter von T . Für m = 1 sind die Aussagen offensichtlich äquivalent. Sei
m ≥ 2 und T ein Huffman-Baum mit m Blättern und n1 und n2 die Knoten,
4.6 Codebäume 189
Figur 4.37 zeigt das Vertauschen des Knotens A 2 mit dem Teilbaum mit
Wurzel 2 und den Nachfolgern F 1 und E 1 .
.
10 .
10
4 6 4 6
A2 C2 B2 4 2 C2 B2 4
2 D2 F1 E1 A2 D2
F1 E1
Wir beschreiben jetzt den Algorithmus von Faller, Gallager und Knuth.
Der Algorithmus startet mit dem NULL-Knoten. Den NULL-Knoten stellen
wir mit
0
dar. Er repräsentiert die Nachrichten der Quelle, die bisher noch nicht gesen-
det wurden. Zu Beginn des Algorithmus werden alle Nachrichten der Quelle
durch den NULL-Knoten repräsentiert. Der NULL-Knoten ist ein Blatt, hat
das Gewicht 0, ist in jedem Codebaum vorhanden und ist der erste Knoten
in der Gallager-Ordnung.
Sendet die Quelle ein Symbol m zum ersten Mal, dann rufen wir die Funk-
tion InsertNode(m) auf. InsertNode generiert zwei neue Knoten n1 und n2
und fügt diese an den NULL-Knoten an. Zum linker Nachfolger von 0 ma-
chen wir n1 – er repräsentiert einen neuen NULL-Knoten – und zum rechten
Nachfolger den Knoten n2 . Er repräsentiert m. Der alte NULL-Knoten ist
jetzt ein innerer Knoten. Wir bezeichnen ihn mit n. InsertNode initialisiert
die Knoten n, n1 und n2 . In der Gallager-Ordnung kommt erst n1 dann n2
und zum Schluss n. Figur 4.38 zeigt die Codebäume für die leere Nachricht
und für eine Nachricht A.
4.6 Codebäume 191
1.
0
0 A1
Beispiel. Figur 4.39 zeigt das Einfügen der Nachricht F : Zunächst fügen wir
einen neuen NULL-Knoten und einen Knoten, der F repräsentiert, ein (Co-
debaum zwei).
5.
5. 2 3
2 3 A1 C1 B1 2
A1 C1 B1 2 1 D1
1 D1 1 E1
0 E1 0 F1
Wir vertauschen die beiden mit einem Pfeil markierten Knoten und erhal-
ten den ersten Codebaum der Figur 4.40. Anschließend inkrementieren wir
192 4. Bäume
die Gewichte längs des Pfades vom Knoten, der F repräsentiert, zur Wurzel.
Das Ergebnis ist der zweite Codebaum.
5. 6.
2 3 2 4
A1 C1 1 2 A1 C1 2 2
1 E1 B1 D1 1 E1 B1 D1
0 F1 0 F1
Fig. 4.40: Vertauschen von zwei Knoten und Update der Gewichte.
Die Komponente highest von ListNode verweist auf den größten Knoten
bezüglich der Gallager-Ordnung mit dem in der Komponente weight angege-
benen Gewicht und die Variable nrN odes speichert die Anzahl der Knoten
mit diesem Gewicht. Das node Element des Baumes erweitern wir um eine
Referenz auf eine Variable vom Typ ListNode. Diese referenziert den Kno-
ten in der Gewichtsliste, der das Gewicht des Baumknotens speichert. Jetzt
finden wir den größten Knoten in der Gallager-Ordnung unter den Knoten
gleichen Gewichts in konstanter Zeit. Das Update der Gewichtsliste erfordert
auch konstante Zeit.
Satz 4.52. Die Laufzeit von Algorithmus 4.50 ist proportional zur Tiefe des
Baumes. Die Tiefe ist stets ≤ Anzahl der Nachrichten - 1.
Bemerkung. Codierer und Decodierer konstruieren den Codebaum mit Algo-
rithmus 4.50 unabhängig voneinander. Der Codierer komprimiert Symbole,
die im Huffman-Baum vorhanden sind. Beim ersten Auftreten übermitteln
wir ein Symbol unkomprimiert an den Decodierer. Der Decodierer kann dem-
zufolge genauso, wie der Codierer, den Codebaum erweitern.
Beispiel. Sei X = {a, b, c, d, e}, p = (0.3, 0.3, 0.2, 0.1, 0.1). Figur 4.41 zeigt
die den Nachrichten a, b, c, d, e, ba, bb, bc, bd und be zugeordneten Intervalle.
0 .3 .6 .8 .9 1
.
a b c d e
t + I := [t + α, t + β[ ,
lI := [lα, lβ[ .
Sei I := {[α, β[| [α, β[⊂ [0, 1[} die Menge aller links abgeschlossenen und
rechts offenen Teilintervalle des Intervalls [0, 1[. Wir beschreiben jetzt die
Abbildung I rekursiv, die einer Nachricht ein Element von I zuordnet.
I : X ∗ \ {ε} −→ I
Bemerkung. Die Länge ∏nvon I(xi ) ist gleich pi und für die Länge l von
I(xi1 . . . xin ) gilt l = j=1 pij . Die Länge des Intervalls ist also gleich der
Wahrscheinlichkeit, mit der die Nachricht xi1 . . . xin auftritt. Weiter gilt:
∪
˙ ∪
˙
[0, 1[ = I(xi ) und [0, 1[ = I(xi1 . . . xin ).
1≤i≤m i1 ...in
1. Berechne I(adeeba):
Satz 4.53. Sei r(xi1 . . . xin ) die Codierung der Nachricht xi1 . . . xin über
{0, . . . , b − 1}. Dann gilt für die Länge von r(xi1 . . . xin )
196 4. Bäume
∏
n
|r(xi1 . . . xin )| ≤ logb p−1
ij
+ 1,
j=1
d. h. die Länge der Codierung einer Nachricht ist durch den Logarithmus
zur
∏n Basis b aus der reziproken Auftrittswahrscheinlichkeit p(xi1 . . . xin )−1 =
−1
j=1 pij der Nachricht plus 1, also dem Informationsgehalt der Nachricht
(Definition 4.38) plus 1, beschränkt.
(∏ )
n −1
Beweis. Sei r = r(xi1 . . . xin ). |r| ≤ logb p
j=1 ij + 1 gilt genau dann,
(∏ ) ∏
n n
wenn logb j=1 pij ≤ −(|r| − 1) gilt. Da β − α = j=1 pij gilt, genügt es
zu zeigen
β − α ≤ b−(|r|−1) . (∗)
Sei I(xi1 , . . . , xin ) = [α, β[, α = .α1 α2 α3 . . . und β = .β1 β2 β3 . . .,
α1 = β1 , α2 = β2 , . . . , αt−1 = βt−1 , αt < βt .
αt+1 = . . . = ατ −1 = b − 1, ατ < b − 1, für ein τ ≥ t + 1.
Da β − α frühestens an der t–ten Stelle nach dem Punkt eine Ziffer ̸= 0 hat,
gilt: β − α ≤ b−(t−1) .
Für α = 0 ist r = 0, also |r| = 1, und (∗) ist erfüllt.
Sei jetzt α > 0. Wir betrachten die verbleibenden Fälle zur Berechnung
von r (siehe Seite 195). Im ersten und zweiten Fall gilt |r| ≤ t und deshalb
β − α ≤ b−(t−1) ≤ b−(|r|−1) .
Im dritten Fall gilt β − α ≤ β − .α1 . . . ατ −1 = b−(τ −1) . |r| = τ − 1 oder
|r| = τ . In allen Fällen gilt β − α ≤ b−(|r|−1) . 2
Satz 4.54. Sei X eine Quelle und sei C ein arithmetischer Code für Nach-
richten aus X ∗ über {0, 1}. Dann gilt für die mittlere Codewortlänge l, ge-
mittelt über die Längen der Codierungen aller Nachrichten aus X n ,14
l ≤ nH(X) + 1,
14
X n ist mit der Produktwahrscheinlichkeit, definiert durch p(xi1 . . . xin ) = p(xi1 )·
. . . · p(xin ) = pi1 · . . . · pin , eine Quelle und wir bezeichnen sie als n–te Potenz
von X.
4.6 Codebäume 197
Hieraus folgt
∑
l= p(xi1 . . . xin )|r(xi1 . . . xin )|
(i1 ,...,in )
∑ ∏
n
≤ p(xi1 . . . xin ) log2 p−1
ij
+ 1
(i1 ,...,in ) j=1
∑ ∑
n ∑
= p(xi1 . . . xin ) log2 (p−1
ij ) + p(xi1 . . . xin )
(i1 ,...,in ) j=1 (i1 ,...,in )
∑
n ∑
m ∑
= p(xi1 . . . xin ) log2 (p−1
ij ) + 1
j=1 ij =1 (i1 ,...,iˆj ,...,in )
∑
n ∑
m
= pij log2 (p−1
ij ) + 1 = n H(X) + 1.
j=1 ij =1
∑
Für das vorletzte =“ wurde (i1 ,...,iˆj ,...in ) p(xi1 . . . xij . . . xin ) = p(xij ) = pij
”
verwendet. 2
Bemerkung. Wir erhalten für die mittlere Codewortlänge pro Symbol l/n ≤
H(X) + 1/n. Der Vergleich mit dem Quellencodierungssatz (Satz 4.39), der
H(X) ≤ l/n besagt, zeigt, dass sich die obere Schranke aus Satz 4.54 wenig
von der unteren Schranke unterscheidet, die durch den Quellencodierungssatz
gegeben ist. Dies zeigt, dass die arithmetische Codierung gute Kompressions-
eigenschaften besitzt.
Reskalierung. Besitzen die Darstellungen der Intervallendpunkte ein ge-
meinsames Präfix, so erniedrigen wir durch Reskalierung die Anzahl der Stel-
len in der Darstellung der Intervallendpunkte. Die weiteren Berechnungen
erfolgen dann mit dem durch die verkürzten Darstellungen gegebenen Inter-
vall. Sei α = .α1 α2 α3 . . . und β = .β1 β2 β3 . . .,
α1 = β1 , α2 = β2 , . . . , αt−1 = βt−1 , αt < βt .
Bei der Reskalierung ersetze [α, β[ durch
Repräsentiere [.0,.243[ durch .0 und folglich I(baaaa) durch .300. Ohne Res-
kalierung ergibt sich I(baaaa) = [0.3, 0.30243[ und r = 0.3. Das Codewort
verlängert sich bei Reskalierung.
Bemerkung. Mit und ohne Reskalierung berechnet sich in den meisten Fällen
das gleiche Codewort. Es gibt nur eine Ausnahme: Endet das zu codierende
Wort w mit dem ersten Element des Alphabets (nennen wir es wie in den
Beispielen a), so können sich bei der Reskalierung überflüssige Nullen am
Ende des Codeworts ergeben (wie im zweiten Beispiel). Ob es passiert, hängt
von den konkreten Wahrscheinlichkeiten der Symbole ab, aber es wird umso
wahrscheinlicher, je mehr a’s sich am Ende befinden. Natürlich könnte man
die überflüssigen Nullen am Ende einfach wegstreichen. Das geht aber nicht
mehr, wenn wir einen Eingabestrom codieren und die Zeichen des Codeworts
verschicken, sobald sie vorliegen (ohne auf die Codierung der ganzen Nach-
richt zu warten). Das Phänomen der überflüssigen Nullen tritt nicht auf, wenn
4.6 Codebäume 199
wir zu codierende Nachrichten mit einem Sonderzeichen EOF für das Nach-
richtenende abschließen (und EOF nicht das erste Zeichen des Alphabets ist).
Die Verwendung eines EOF-Symbols ist hilfreich für die Decodierung, ein ge-
sondertes Übertragen der Nachrichtenlänge ist dann nicht mehr nötig (siehe
unten).
Underflow. Underflow kann eintreten, wenn die Intervallendpunkte einen
geringen Abstand aufweisen und wenn Reskalieren nicht möglich ist. In die-
ser Situation nimmt die Darstellung der Intervallendpunkte mit jedem Schritt
zu. Wir sprechen von Underflow und diskutieren das Underflow-Problem
zunächst an einem
Beispiel. Sei X = {a, b, c, d, e}, p = (0.3, 0.3, 0.2, 0.1, 0.1).
I(a) = [.0, .3[, I(b) = [.3, .6[, I(c) = [.6, .8[, I(d) = [.8, .9[ und I(e) = [.9, 1[.
Wir berechnen das Intervall I(bbabacb).
Das Intervall I(bbab) = [.3981, .4062[. Es kann jetzt der Fall eintreten,
dass bei der Abarbeitung der weiteren Symbole das Intervall nach jedem
Symbol die Zahl 0.4 enthält, d. h. α = .39 . . . β = .40 . . .. Dann ist Reska-
lieren nicht möglich. Das Intervall [α, β[ wird mit jedem Schritt kürzer. Die
Anzahl der für die Darstellung von α und β notwendigen Stellen nimmt mit
jedem Schritt zu. Dies stellt ein Problem dar, falls wir mit einer endlichen
Anzahl von Stellen rechnen. Abhilfe schafft die Underflow-Behandlung, die
wir jetzt erläutern.
β1 = α1 + 1, α2 = b − 1 und β2 = 0
ist.
Bei der Underflow-Behandlung ändern sich α1 und β1 nicht. Bei der Bear-
beitung des nächsten Symbols vom Input tritt im Fall der Änderung von α1
und β1 der Fall α1 = β1 ein. Daher sind die bei der Underflow-Behandlung
aus r entfernten Ziffern alle gleich.
Beispiel. Sei X = {a, b, c, d, e}, p = (0.3, 0.3, 0.2, 0.1, 0.1).
I(a) = [.0, .3[, I(b) = [.3, .6[, I(c) = [.6, .8[, I(d) = [.8, .9[ und I(e) = [.9, 1[.
Wir berechnen das Intervall I(bbabacb).
Quelle Intervallberechnung
(1/5, 1/5, 1/5, 1/5, 1/5) I(a) = [.0, .2[
(1/3, 1/6, 1/6, 1/6, 1/6) I(ad) = .0 + .2 I(d) = [.132, .166[ −→ 1
= [.32, .66[
2 1 1 2
( /7, /7, /7, /7, /7)1 I(ade) = .32 + .34 I(e) = [.61, .64[ −→ 1
= [.2, .4[
(1/4, 1/8, 1/8, 1/4, 1/4) I(adee) = .2 + .2 I(e) = [.35, .7[
2 1 1 2
( /9, /9, /9, /9, /3)1 I(adeeb) = .35 + .35 I(b) = [.427, .4655[ −→ 1
= [.27, .655[
(1/5, 1/5, 1/10, 1/5, 3/10) I(adeeba) = .27 + .385 I(a) = [.27, .347[
Repräsentiere I(adeeba) durch .1113.
Bemerkung. Wir haben zwei Verfahren zur Quellencodierung behandelt, die
Huffman-Codierung und die arithmetische Codierung. Mit dem Huffman-
Verfahren erstellen wir Codetabellen und verwenden diese zur Codierung und
Decodierung. Die arithmetische Codierung benötigt keine Codetabellen. Sie
erfordert aber im Vergleich zur Huffman-Codierung erheblichen Rechenauf-
wand. Bei einer Implementierung des Verfahrens verwenden wir ganzzahlige
Arithmetik (bei Fließkomma-Arithmetik ist die codierte Nachricht durch die
Anzahl der Bytes des Zahlenformates eingeschränkt und somit zu klein).
4.6 Codebäume 203
4.6.4 Lempel-Ziv-Codes
0 1 0 1 1 0 1 0 1 1 1 0. 1 1 0 0 1 0 1 0 1 . . .
Text-Puffer Vorschau-Puffer
Wir erklären die Codierung rekursiv und nehmen dazu an, dass die Zei-
chen x1 . . . xi−1 bereits codiert sind. Der Text-Puffer enthält die Zeichen
xi−w . . . xi−1 und der Vorschau-Puffer die Zeichen xi xi+1 . . . xi+v−1 . Die we-
sentliche Idee ist nun eine Zeichenkette zu finden, die im Text-Puffer beginnt
und mit der Zeichenkette xi xi+1 . . . xi+v−1 im Vorschau-Puffer die längste
Übereinstimmung aufweist.
Für k mit i − w ≤ k ≤ i − 1 sei
und
15
Abraham Lempel (1936 – ) ist ein israelischer Informatiker.
16
Jacob Ziv (1931 – ) ist ein israelischer Informationstheoretiker.
204 4. Bäume
m = max{ℓk | i − w ≤ k ≤ i − 1}.
Die längste Übereinstimmung besteht aus m vielen Zeichen und beginnt im
Text-Puffer mit xi−j , wobei ℓi−j = m gilt. Sie kann sich in den Vorschau-
Puffer hinein erstrecken.
Falls m ≥ 1 gilt, codieren wir
Der aufwendige Teil bei der Codierung ist das Auffinden von längsten
übereinstimmenden Segmenten. Beim greedy parsing“ wird mit jedem Zei-
”
chen im Text-Puffer als Startpunkt die Länge der Übereinstimmung ermit-
telt. Eine längste Übereinstimmung wird anschließend verwendet. Dies stellt
jedoch nicht sicher, dass wir insgesamt die beste Kompressionsrate erzielen.
Diese ist wegen der Komplexität des Problems nicht zu erreichen.
Varianten des LZ77-Algorithmus implementieren Methoden, die es ermög-
lichen auf übereinstimmende Segmente schneller zuzugreifen. Dazu setzen wir
zur Adressierung übereinstimmender Segmente ein Hashverfahren oder einen
binären Suchbaum ein. Im Text-Puffer prüfen wir nicht mehr an jeder Stelle
auf Übereinstimmung, sondern nur noch an Stellen, an denen schon frühere
übereinstimmende Segmente gefunden wurden.
4.6 Codebäume 205
Das Wörterbuch stellen wir durch den Text-Puffer dar. Die gleitende Fens-
tertechnik und die konstante Länge des Text-Puffers gewährleisten, dass sich
nur Segmente, die in der jüngeren Vergangenheit aufgetreten sind, im Wörter-
buch befinden. Die Anpassung des Wörterbuchs an Veränderungen, in den
Mustern der zu komprimierenden Daten, erfolgt demnach automatisch.
Lempel-Ziv-Algorithmen mit digitalem Suchbaum. Wir studieren
LZ78 und eine Variante von LZ78, das LZW-Verfahren, genauer. In beiden
Algorithmen lesen wir die zu komprimierenden Daten nur einmal. Dabei kom-
primieren wir die Daten und erzeugen das Wörterbuch. Im Gegensatz zu
LZ77 erfolgt die Konstruktion des Wörterbuchs explizit. Wir implementieren
LZ78 mit einem digitalen Suchbaum. Der Codierer erzeugt diesen digitalen
Suchbaum während er den Quelltext analysiert. Der Decodierer erzeugt einen
identischen Suchbaum aus den komprimierten Daten. Wir müssen neben den
komprimierten Daten keine weiteren Daten vom Codierer zum Decodierer
übermitteln.
Der Algorithmus LZ78.
Definition 4.55. Sei X = {x1 , . . . , xn } eine Nachrichtenquelle. Sei y ∈ X n .
Eine Lempel-Ziv Zerlegung von y,
y = y0 ||y1 || . . . ||yk ,
206 4. Bäume
ε.
A T
E
A E T
L R
R O
AL AR ER TO
L M R
ALL ARM TOR
E
ALLE
den vorher angefügten Knoten, solange bis alle Zeichen von x verbraucht
sind.
In unserer Anwendung fügen wir jeweils nur ein neues Blatt an.
Beispiel. Figur 4.44 demonstriert den Algorithmus mit x = 010110101 . . .
Wir starten mit einem Baum, der nur aus einer Wurzel besteht. Die Wurzel
erhält die Nummer 0. In jedem Schritt fügen wir ein Blatt an den Baum an.
Wir nummerieren die Knoten in der Reihenfolge ihres Entstehens.
.
Eingabe: 010110101 ... .
0|10110101 ... .
0|1|0110101 ...
0 0 0
0 0 1
1 1 2
.
Eingabe: 0|1|01|10101 ... .
0|1|01|10|101 ... .
0|1|01|10|101| ...
0 0 0
0 1 0 1 0 1
1 2 1 2 1 2
1 1 0 1 0
3 3 4 3 4
1
Ausgabe: (1,1) (2,0) (4,1) 5
a(x)(log2 (a(x)) + e)
lim ℓn = H(X)
n→∞
(siehe [ZivLem78]).
Die LZW-Variante. Die LZW-Variante wurde 1984 von Welch17 publiziert.
Bei der Implementierung des LZW-Algorithmus verwenden wir, wie beim
LZ78-Algorithmus, einen digitalen Suchbaum. Hier starten wir jedoch mit
einem Baum, der neben der Wurzel in der Ebene 1 für jedes x ∈ X ein Blatt
besitzt. Wir ermitteln wieder eine Phrase durch digitale Suche ausgehend
von der Wurzel. Die Suche endet in einem Blattknoten. Jetzt erfolgen zwei
Aktionen
1. Ausgabe der Nummer dieses Blattknotens.
2. Anfügen eines neuen Blattknotens an diesen Blattknoten. Die neue Kante
beschriften wir mit dem nächsten Zeichen x aus der Eingabe.
x ist erstes Zeichen der nächsten Phrase.
Jetzt entsprechen gefundene Phrasen nicht mehr eineindeutig den Knoten im
Codebaum. Es kann Knoten geben, zu denen keine Phrasen existieren und
die gefundenen Phrasen sind nicht mehr notwendig paarweise verschieden.
17
Terry A. Welch (1939 – 1988) war ein amerikanischer Informatiker.
4.6 Codebäume 209
Der Vorteil gegenüber LZ78 besteht darin, dass die Ausgabe nur noch
aus der Nummer des angefügten Knotens besteht. Sie reduziert sich durch
Weglassen des zusätzlichen Symbols im Vergleich zur Ausgabe bei LZ78.
Beispiel. Figur 4.45 demonstriert die Arbeitsweise des Codierers mit der Ein-
gabe x = 010110101 . . . ..
.
Eingabe: 010110101 ... .
0|10110101 ... .
0|1|0110101 ...
0 0 0
0 1 0 1 0 1
1 2 1 2 1 2
1 1 0
3 3 4
Ausgabe: 1 2 3
.
Eingabe: 0|1|01|10101 ... .
0|1|01|10|1011 ... .
0|1|01|10|101|1 ...
0 0 0
0 1 0 1 0 1
1 2 1 2 1 2
1 0 1 0 1 0
3 4 3 4 3 4
1 1 1 1 1
5 5 6 5 6
1
Ausgabe: 4 6 7
Das Beispiel zeigt, dass LZ78 und LZW verschiedene Codebäume liefern.
Beim Decodieren rekonstruieren wir aus den Nummern der Knoten den
Codebaum. Dies bedarf weiterer Überlegungen, wenn der Codierer das zu-
letzt eingefügten Blatt bei der Ermittlung der Phrase verwendet, wie es im
Beispiel bei der Ermittlung der Phrase 101 mit der Ausgabe der Knotennum-
mer 6 der Fall ist.
Figur 4.46 zeigt die Codebäume, die der Decodierer konstruiert. Einga-
be ist die vom Codierer ausgegebene Nummer des Knotens. Der Decodierer
210 4. Bäume
gibt die dem Knoten entsprechende Phrase aus. Nachdem der Decodierer die
Knotennummer 6 erhält, hat er erst den Baum rekonstruiert, der im voran-
gehenden Schritt bei der Codierung entstanden ist. In diesem ist der Knoten
mit der Nummer 6 nicht enthalten.
.
Eingabe: 1 2. 3.
0 0 0
0 1 0 1 0 1
1 2 1 2 1 2
1
3
.
Eingabe: 4 6.
0 0 0.
0 1 0 1 0 1
1 2 1 2 1 2
1 0 1 0 1 0
3 4 3 4 3 4
1
1 1
5 5 6
Unter dem Ausnahmefall verstehen wir den Fall, dass der Decodierer einen
nicht im Codebaum enthaltenen Knoten decodieren muss. Er entsteht da-
durch, dass der Decodierer, die Codebäume nur mit einer Verzögerung von
einem Schritt konstruieren kann.
Wir geben jetzt ein Verfahren zur Behandlung des Ausnahmefalls an. Sei-
en y1 || . . . ||yi die Phrasen, die der Codierer gefunden hat. Der Codierer fügt
für jede Phrase einen Knoten in den Codebaum ein. Er hat somit insge-
samt i Knoten hinzugefügt. Wir nehmen an, dass der Decodierer die Phrasen
y1 || . . . ||yi−1 decodiert hat. Der Decodierer kann nur i − 1 viele Knoten rekon-
struieren. Der einzige Knoten, den der Decodierer nicht kennt, ist der Knoten,
Übungen 211
den der Codierer mit dem Auffinden der i–ten Phrase yi angefügt hat, also
den Knoten mit der Nummer i.
Nehmen wir weiter an, dass der Codierer im nächsten Schritt den Knoten
mit der Nummer i verwendet. Der Knoten ist Nachfolger des Knotens, den
der Codierer zuletzt verwendet hat, nämlich der Knoten, der zu yi−1 gehört.
Zum Knoten mit der Nummer i gehört die Phrase yi , aber auch die Phra-
se yi−1 ||x, wobei x das erste Zeichen von yi ist. Es folgt yi = yi−1 x, d. h.
yi−1 und yi stimmen im ersten Zeichen überein. Der Decodierer kann den
unbekannten“ Knoten ermitteln, indem er an den im vorangehenden Schritt
”
übermittelten Knoten ein Blatt anfügt und die Kante mit dem ersten Zeichen
von yi−1 beschriftet.
Für das Wörterbuch, das wir mit einem digitalen Suchbaum implemen-
tiert haben, steht nur begrenzter Speicher zur Verfügung. Es müssen des-
halb Strategien implementiert werden, die eine Anpassung des Wörterbuchs
bei veränderten Mustern im zu komprimierenden Text vornehmen und die
verhindern, dass das Wörterbuch voll läuft und keine Einträge mehr aufneh-
men kann. Mögliche Strategien sind, das Wörterbuch komplett zu löschen,
nachdem eine Schranke des belegten Speichers überschritten wird oder wenn
die Kompressionsrate unter eine vorgegebene Schranke fällt. Eine weitere
Möglichkeit besteht darin, die Benutzung der Einträge zu beobachten und
den Eintrag zu entfernen, dessen Benutzung am weitesten zurückliegt.
Übungen.
1. In den Knoten eines binären Baumes sind die Elemente a, b, c, d, e, f,
h, j gespeichert. Bei der Preorder-Ausgabe entsteht die Liste c, a, h, f,
b, j, d, e, bei der Postorder-Ausgabe die Liste h, f, a, d, e, j, b, c. Wie
kann man aus diesen Angaben den binären Baum konstruieren? Geben
Sie den binären Baum an. Beschreiben Sie Ihr Vorgehen und begründen
Sie die einzelnen Schritte. Unter welchen Voraussetzungen ist der Baum
eindeutig durch die Preorder- und Postorder-Ausgabe bestimmt?
2. Geben Sie alle binären Suchbäume für {1, 2, 3, 4} an.
3. Jedem arithmetischen Ausdruck mit den Operatoren + und * kann ein
binärer Baum zugeordnet werden.
Entwickeln Sie eine Funktion, die beim Traversieren einen arithmetischen
Ausdruck mit Klammern und Postfix-Notation erzeugt.
4. In einem binären Suchbaum und einem gegebenen Knoten v bezeichnen
wir mit vv denjenigen Knoten, der in der Inorder-Reihenfolge unmittelbar
vor v auftritt (sofern existent). Zeigen Sie: Wenn v einen linken Nachfol-
ger hat, so hat vv keinen rechten Nachfolger. Wie lautet die äquivalente
Aussage für den Nachfolger nv eines Knotens v?
212 4. Bäume
25. Eine Quelle besitzt die Elemente {a,b,c,d,e,f,g} und die Wahrscheinlich-
keiten (0.3, 0.14, 0.14, 0.14, 0.14, 0.07, 0.07). Zeichenketten aus {a,b,c,d,e,
f,g}∗ werden arithmetisch über {0, 1, . . . , 9} codiert.
a. Geben Sie den Code für die Nachricht acfg an.
b. Decodieren Sie 1688 bei einer Nachrichtenlänge von 6.
26. Sei k = 2l , l ∈ N, X = {x1 , . . . , xk } und pi = k1 , i = 1, . . . , k. Welche
Wortlängen treten bei Codierung durch arithmetische Codes über {0, 1}∗
für Nachrichten der Länge n auf.
27. Gegeben sei eine Nachrichtenquelle (X = {a, b}, p = (1 − 21k , 21k )). Geben
Sie einen arithmetischen Code für bn a, n ∈ N, über {0, 1}∗ an.
28. Ein LZ77-Verfahren werde mit einem Text-Puffer der Länge r implemen-
tiert. Geben Sie ein Alphabet X und eine Nachricht x ∈ X ∗ an, sodass
die Kompressionsrate |C(x)|/|x| der LZ77-Codierung C(x) von x maximal
ist.
5. Graphen
In diesem Abschnitt werden wir eine Reihe von populären Problemen mithilfe
von Graphen formulieren.
Königsberger-Brückenproblem. Wir erläutern das Königsberger-Brü-
ckenproblem mit Figur 5.1, die den Fluss Pregel in der Stadt Königsberg
zeigt. Zwischen den Ufern und den beiden Inseln sind sieben Brücken einge-
zeichnet. Eine Brücke verbindet die beiden Inseln. Eine der Inseln ist durch
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021
H. Knebl, Algorithmen und Datenstrukturen,
https://doi.org/10.1007/978-3-658-32714-9_5
216 5. Graphen
je eine Brücke mit der Vor- und Altstadt verbunden. Von der anderen Insel
führen je zwei Brücken zur Vor- und Altstadt. Die Frage, ob es einen ge-
schlossenen Weg gibt, welcher jede Brücke genau einmal überquert, soll zur
Unterhaltung der Bürger von Königsberg gedient haben.
K. W
Die Figur rechts zeigt den Sachverhalt der Skizze als Multigraphen. 1 Hier
wurde von den topographischen Gegebenheiten, die für die Problemstellung
keine Rolle spielen, abstrahiert. Wir betrachten nur noch zwei Objekttypen,
Knoten (nodes) und Beziehungen zwischen Knoten, die wir als Kanten (ed-
ges) bezeichnen. Die Menge der Knoten ist {A,K,V,W}. Zwei Knoten stehen
in Beziehung zueinander, wenn sie durch eine Brücke verbunden sind. Es gibt
somit sieben Kanten, die wir durch die beiden Knoten darstellen, die zuein-
ander in Beziehung stehen. Die Kanten sind gegeben durch {A,K}, {A,K},
{K,V}, {K,V}, {A,W}, {K,W}, {V,W}. Die mathematische Notation redu-
ziert auf das Wesentliche und erleichtert dadurch die Lösung des Problems.
Die Lösung des Problems wurde im Jahre 1736 von Euler2 publiziert. Die-
se Arbeit, in der Euler das Königsberger-Brückenproblem in mathematischer
Notation darstellte, wird als die erste Arbeit über Graphentheorie angesehen.
Zur Ehre von Euler bezeichnen wir heute einen geschlossenen Weg in einem
Graphen, der jede Kante genau einmal enthält, als Eulerkreis. Eulers Ar-
beit gibt eine notwendige und hinreichende Bedingung für die Existenz eines
Eulerkreises an. Ein Graph besitzt genau dann einen Eulerkreis, wenn alle
Knoten geraden Grad3 besitzen (Übungen, Aufgabe 1). Diese Bedingung ist
für das Königsberger-Brückenproblem nicht erfüllt.
House-Utility-Problem. Beim House-Utility-Problem gibt es drei Häuser
H1 , H2 , H3 und drei Versorgungsunternehmen, je eins für Gas (G), Wasser
(W) und Strom (S). Figur 5.2 stellt diese Situation dar, es handelt sich um
einen bipartiten Graphen. Bei einem bipartiten Graphen ist die Menge der
Knoten in zwei Partitionen (disjunkte Teilmengen) zerlegt. Die beiden End-
punkte einer Kante liegen nicht in einer der beiden Partitionen, sondern in
1
Bei Multigraphen sind mehrere Kanten zwischen zwei Knoten zugelassen.
2
Leonhard Euler (1707 – 1783) war ein schweizer Mathematiker und Physiker.
3
Der Grad eines Knotens ist die Anzahl der in ihm zusammentreffenden Kanten.
5.1 Modellierung von Problemen durch Graphen 217
G. W S G. H2 S
H1 H2 H3 H1 W H3
A. B C
A. B C
E
D
D
Der Satz von Kuratowski5 gibt ein notwendiges und hinreichendes Kri-
terium für ebene Graphen. Dieses Kriterium zeigt, dass K5 und K3,3 nicht
eben sind.
4
Ein Graph heißt vollständig, wenn jeder Knoten mit jedem anderen Knoten durch
eine Kante verbunden ist. Er ist somit durch Angabe der Anzahl der Knoten
bestimmt.
5
Kazimierz Kuratowski (1896 – 1980) war ein polnischer Mathematiker und Lo-
giker.
218 5. Graphen
Der Algorithmus von Hopcroft6 -Tarjan7 dient zum Test der Planarität
eines Graphen und zur Einbettung eines ebenen Graphen in die Ebene. Ebene
Graphen werden zum Beispiel in [Jungnickel13] oder [Gould88] behandelt.
Vierfarbenproblem. Die Problemstellung des Vierfarbenproblems ist ein-
fach zu formulieren. Sie lautet: Ist es möglich jede Landkarte mit vier Farben
zu färben, sodass benachbarte Länder verschiedene Farben besitzen?
Das Problem lässt sich mit Graphen formulieren. Die Knoten repräsen-
tieren die Hauptstädte der Länder. Die Hauptstädte benachbarter Länder
verbinden wir mit einer Kante. Da benachbarte Länder eine gemeinsame
Grenze besitzen, gibt es eine Kante, die ganz auf dem Gebiet der beiden
Länder verläuft. Es handelt sich um einen ebenen Graphen.
Mit Graphen formuliert lautet das Vierfarbenproblem: Kann jeder ebene
Graph mit vier Farben gefärbt werden, sodass benachbarte Knoten verschie-
dene Farben besitzen?
F F. E
E
A B
C
A B
D
C D
Den Graphen der Figur 5.4 können wir mit 4 Farben färben. Mit Blau
die Knoten A und E, mit Grün die Knoten F und D, mit Gelb den Knoten C
und mit Rot den Knoten B. Benachbarte Knoten sind mit unterschiedlichen
Farben gefärbt.
Das Vierfarbenproblem zählt zu den populären Problemen der Graphen-
theorie. Es weist eine lange Geschichte auf. Es soll von Francis Guthrie 8 im
Jahre 1852 formuliert worden sein. Caley9 hat das Problem im Jahre 1878
vor der Mathematical Society in London vorgetragen. Appel10 und Haken11
gelang im Jahre 1976 der Beweis, dass man jeden ebenen Graphen mit vier
Farben färben kann, sodass benachbarte Knoten verschiedene Farben besit-
zen.
6
John Edward Hopcroft (1939 – ) ist amerikanischer Informatiker und Turing-
Preisträger.
7
Robert Endre Tarjan (1948 – ) ist amerikanischer Informatiker und Turing-
Preisträger.
8
Francis Guthrie (1831 – 1899) war ein südafrikanischer Mathematiker.
9
Arthur Cayley (1821 – 1895) war ein englischer Mathematiker.
10
Kenneth Appel (1932 – 2013) war ein amerikanischer Mathematiker.
11
Wolfgang Haken (1928 – ) ist ein deutscher Mathematiker.
5.1 Modellierung von Problemen durch Graphen 219
d5 d8
v2 v.6 v7
d6 d12 d9
d7
d1 v3 v4 d13
d2 d10
d3
v1 v5 v8
d4 d11
a10 a9 a8
A10 A9 A8 A7
a1 a7
A.1 a9 a4 A6
a1 a5
A2 A3 A4 A5
a2 a3 a4
Fig. 5.6: Nicht symmetrische Beziehungen – Abhängigkeiten.
Bemerkungen:
1. |V | und |E| messen die Größe eines Graphen. Wir geben Aufwands-
abschätzungen in Abhängigkeit von |V | und |E| an.
2. Einem gerichteten Graphen kann man einen (ungerichteten) Graphen –
den unterliegenden Graphen – zuordnen, indem einer Kante (v, w) die
Kante {v, w} zugeordnet wird. Den einem Graphen zugeordneten gerich-
teten Graphen erhalten wir, wenn wir jede Kante {v, w} durch die Kanten
(v, w) und (w, v) ersetzen.
G: B D H: B D I: B D
A. A. A.
C E C E C
Pfade. Pfade oder Wege sind spezielle Teilgraphen eines Graphen. Mit Pfa-
den definieren wir wichtige Begriffe für Graphen wie Zusammenhang und
Abstand.
Definition 5.3.
1. Sei G = (V, E) ein Graph. Ein Pfad oder Weg P ist eine Folge von
Knoten v0 , v1 , . . . , vn , wobei {vi , vi+1 } ∈ E, i = 0, . . . , n − 1. v0 heißt
Anfangspunkt und vn Endpunkt von P . n heißt Länge von P .
w ist von v aus erreichbar , wenn es einen Pfad P mit Anfangspunkt v
und Endpunkt w gibt. Gibt es für je zwei Knoten v, w ∈ V einen Pfad
von v nach w, so ist G zusammenhängend .
222 5. Graphen
2 3 4
z1 = 3, 2, 1, 6, 3; z2 = 3, 4, 5, 6, 3; z3 = 3, 2, 1, 6, 3, 4, 5, 6, 3; . . ..
6 1 5 1 5
1
2 3 2 3 2 3
Beweis. Sei n := |V | und m := |E|. Wir zeigen zunächst, dass aus Punkt 1
Punkt 2 folgt. Ein Baum T ist per Definition zusammenhängend.
Wir zeigen m = n − 1 durch Induktion nach n: Wenn n = 1 ist, dann gilt
m = 0. Der Induktionsanfang ist richtig. Wir führen den Schluss von < n
auf n durch. Sei T ein Baum der Ordnung n und e = {v, w} eine Kante
in T . Sei T \ {e} der Graph der aus T entsteht, wenn wir in T die Kante
e entfernen. Da T azyklisch ist, gibt es in T \ {e} keinen Weg, der v mit
w verbindet. Nach dem Entfernen einer Kante erhöht sich die Anzahl der
Zusammenhangskomponenten um höchstens 1. Deshalb zerfällt T \ {e} in
zwei Komponenten (Bäume) T1 = (V1 , E1 ) und T2 = (V2 , E2 ). Es gilt m =
|E| = |E1 | + |E2 | + 1 = |V1 | − 1 + |V2 | − 1 + 1 = |V | − 1 = n − 1.
Aus Punkt 2 folgt auch Punkt 1. Sei T zusammenhängend und m =
n − 1. Angenommen, T besitzt einen Zyklus Z. Sei e ∈ Z. Dann ist T \ {e}
zusammenhängend und besitzt n−2 viele Kanten, ein Widerspruch, denn um
n Knoten zusammenhängend zu verbinden, braucht man mindestens n − 1
Kanten. 2
Bipartite Graphen. G = (V, E) heißt bipartit, wenn die Menge der Knoten
V eine Zerlegung in die Partitionen V1 und V2 besitzt, d. h. V = V1 ∪ V2 ,
V1 ∩ V2 = ∅, und jede Kante von G einen Endpunkt in V1 und den anderen
in V2 hat.
Eine perfekte Zuordnung für G besteht aus einer Menge von Kanten, die
eine bijektive Abbildung von V1 nach V2 definieren. Der folgende Satz gibt
für einen bipartiten Graphen mit |V1 | = |V2 | ein zur Existenz einer perfekten
Zuordnung äquivalentes Kriterium an.
Satz 5.8. Sei G = (V, E) ein bipartiter Graph, V1 = {v1 , . . . , vn }, V2 =
{w1 , . . . , wn } und seien Xij , 1 ≤ i, j ≤ n, Unbestimmte. Die Matrix A =
(aij )1≤i,j≤n sei definiert durch
{
Xij , falls {vi , wj } ∈ E,
aij =
0 sonst.
G besitzt genau dann eine perfekte Zuordnung, wenn det(A) ̸= 0 ist.
Beweis. Die Determinante von A ist definiert durch die Formel
∑
det(A) = sign(π)a1π(1) · · · anπ(n) .
π∈Sn
Bemerkungen:
1. det(A) ist ein Polynom mit den Koeffizienten ±1 in den Unbestimmten
Xij , 1 ≤ i, j ≤ n. Im Abschnitt 1.6.3 haben wir einen Monte-Carlo-
Algorithmus zum Test der Identität von zwei Polynomen entwickelt (Co-
rollar 1.55). Diesen Algorithmus können wir zum Test det(A) = 0 ver-
wenden. Die Effizienz des Verfahrens, das sich aus Satz 5.8 ergibt, domi-
niert der verwendete Algorithmus zur Berechnung der Determinante. Die
Berechnung nach der definierenden Formel, die auf Leibniz16 zurückgeht
oder nach dem Entwicklungssatz von Laplace17 , ist in der Ordnung O(n!)
und deshalb nicht geeignet. Moderne Verfahren haben eine Laufzeit in
der Ordnung O(n3 ) oder besser.
2. Sei G = (V, E) ein Graph, und Z ⊂ E. Z heißt perfekte Zuordnung, wenn
jeder Knoten aus V zu genau einer Kante aus Z inzident ist. Die Frage, ob
ein beliebiger Graph G = (V, E), V = {v1 , . . . vn }, n gerade, eine perfekte
Zuordnung besitzt, können wir mithilfe seiner Tutte 18 Matrix entscheiden.
Die Tutte Matrix A = (aij )1≤i,j≤n von G ist mit den Unbestimmten Xij
definiert durch
Xij , falls {vi , vj } ∈ E und i < j,
aij = −Xji , falls {vi , vj } ∈ E und i > j,
0, sonst.
Ein Graph besitzt genau dann eine perfekte Zuordnung, wenn die Deter-
minante seiner Tutte Matrix det(A) ̸= 0 ist. Der Beweis dieser Tatsache
ist jedoch nicht mehr so einfach wie bei bipartiten Graphen. Nicht nur
für den Test der Existenz einer perfekten Zuordnung, sondern auch für
die Berechnung einer perfekten Zuordnung gibt es probabilistische Algo-
rithmen (siehe [MotRag95, Kapitel 12.4]).
Definition 5.9.
1. Ein (gerichteter) Graph G = (V, E) heißt vollständig, wenn jeder Knoten
mit jedem anderen Knoten durch eine Kante verbunden
(n) ist.
2. Besitzt G viele Kanten (m groß im Vergleich zu 2 oder zu n(n − 1)),
dann heißt G dicht besetzt. ( )
3. Besitzt G wenige Kanten (m klein im Vergleich zu n2 oder zu n(n − 1)),
dann heißt G dünn besetzt.
2. Die Adjazenzliste adl[1..n] ist ein Array von Listen. Für jeden Knoten
j ∈ V ist die Liste adl[j] definiert durch
Bemerkungen:
1. Für einen Graphen G ist die Adjazenzmatrix adm symmetrisch. Die An-
zahl der Speicherstellen, die adm benötigt, ist n2 . Dies gilt auch, wenn
G nur wenige Kanten hat, das heißt, wenn G dünn besetzt ist.
2. In der Adjazenzliste eines Graphen gibt es 2m viele Einträge. Bei einem
gerichteten Graphen sind es m viele Einträge.
5.4 Elementare Graphalgorithmen 227
3. Ein Eintrag der Adjazenzmatrix benötigt weniger Speicher als ein Ein-
trag der Adjazenzliste. Zur Darstellung von dicht besetzten Graphen ist
deshalb eher die Adjazenzmatrix und für dünn besetzte Graphen eher
die Adjazenzliste geeignet.
Beispiel. Figur 5.11 zeigt die Adjazenzliste für
1. 4 1 : 2, 4 1. 4 1 : 2, 4
2 : 3, 4 2:3
3: 3:
4:3 4 : 2, 3
2 3 2 3
Wir implementieren die Adjazenzliste durch eine verkettete Liste von Lis-
tenelementen. Die Variable adl[j], j = 1, . . . , n, enthält eine Referenz auf das
erste Element der verketteten Liste oder null. Die null-Referenz gibt an, dass
adl[j] kein Listenelement referenziert, d. h. dem Knoten j ordnen wir die
leere Liste zu. Ein Listenelement ist definiert durch type vertex = 1..n,
type node = struct
vertex v
node next
Die Definition legt fest, dass ein Knoten (vertex) des Graphen die Werte
aus der Menge {1, . . . , n} annehmen kann. Ein Listenelement besitzt die Va-
riablen v vom Typ vertex und next vom Typ node. v speichert einen Knoten
des Graphen und next eine Referenz auf ein Listenelement vom Typ node
oder null. Die null-Referenz zeigt an, dass das Ende der Liste erreicht ist. Der
Zugriff auf v und next erfolgt mit dem Punktoperator .“ (Abschnitt 1.7).
”
Wir teilen die Knoten V des Graphen, V = {1, . . . , n}, in drei disjunkte
Gruppen ein:
VT : Besuchte Knoten.
Vad : Knoten, die zu Knoten aus VT benachbart, aber nicht in VT sind.
Diese sind für den Besuch vorgemerkt.
VR : V \ (VT ∪ Vad ).
Die Zuordnung eines Knotens zu VT , Vad und zu VR ändert sich während der
Durchführung von BFS:
Start: VT = ∅, Vad = {Startknoten}, VR = V \ {Startknoten}.
Beim Besuch von j ∈ Vad setzen wir
VT = VT ∪ {j},
Vad = (Vad \ {j}) ∪ (Uj ∩ VR ),
VR = V \ (VT ∪ Vad ),
5.4 Elementare Graphalgorithmen 229
A.1 B4 H8 I 10 A.
F C B
C3 G7
E D G
H J
D6 J9 K 12
I L K
F2 E5 L11 M 13 M
Wir implementieren VT , Vad und VR durch das Array where[1..n] und den
BFS-Wald durch das Array parent[1..n].
> 0, falls k ∈ VT ,
where[k] < 0, falls k ∈ Vad ,
= 0, falls k ∈ VR .
parent[k] = j, falls j der Vorgänger von k ist.
Visit(vertex k)
1 node no
2 ToQueue(k), where[k] ← −1
3 repeat
4 k ← FromQueue, where[k] ← nr
5 no ← adl[k]
6 while no ̸= null do
7 if where[no.v] = 0
8 then ToQueue(no.v), where[no.v] ← −1
9 parent[no.v] ← k
10 no ← no.next
11 until QueueEmpty
Bemerkungen:
1. Der Aufruf von Visit(k) in BFS (Zeile 7) bewirkt, dass wir alle bisher
nicht besuchten Knoten, die von k aus erreichbar sind, besuchen, denn
ausgehend von k besuchen wir alle Nachbarn von k und dann alle Nach-
barn der Nachbarn, und so weiter . . . , also alle von k aus erreichbaren
Knoten aus VR .
2. Die while-Schleife in Visit untersucht die Umgebung von k.
3. Nach der Terminierung von BFS enthält where[k] die Nummer der Kom-
ponente des aufspannenden Waldes, in der k liegt. Die Komponenten
werden abgezählt.
5.4 Elementare Graphalgorithmen 231
4. Ein Knoten k ist genau dann Wurzel einer Komponente des aufspannen-
den Waldes, wenn nach Terminierung parent[k] = 0 gilt.
BFS bei Darstellung durch eine Adjazenzmatrix. Wir streichen die
Variable no und ersetzen in Visit die Zeilen 5–10 durch:
for j ← 1 to n do
if where[j] = 0 and adm[k, j]
then ToQueue(j), where[j] ← −1, parent[j] ← k
Visit(vertex k)
1 node no
2 btime[k] ← time, time ← time + 1
3 visited[k] ← true
4 no ← adl[k]
5 while no ̸= null do
6 if visited[no.v] = false
7 then parent[no.v] ← k, Visit(no.v)
8 no ← no.next
9 etime[k] ← time, time ← time + 1
Bemerkungen:
1. Der Aufruf Visit(k) in DFS (Zeile 7) bewirkt, dass Visit für alle von k
aus erreichbaren Knoten, die bisher noch nicht besucht waren, rekursiv
aufgerufen wird. Insgesamt erfolgt für jeden Knoten von G genau ein
Aufruf von Visit.
2. Die Arrays btime und etime benötigen wir nur zur Analyse des Algorith-
mus.
3. Konstruktion des DFS-Waldes. Für jeden Knoten k mit dem Visit
von DFS aufgerufen wird, konstruiert Visit einen Wurzelbaum. Der Kno-
ten k ist Vorgänger des Knotens v, wenn der Aufruf von Visit(v) bei
der Inspektion der Umgebung von k erfolgt. Alle Aufrufe von Visit in
DFS stellen einen aufspannenden Wald für G im Array parent dar. Für
parent[k] = 0 ist k Wurzel für einen der Teilbäume des DFS-Waldes.
Dieser Wald hängt von der Durchführung von DFS ab. Die Freiheitsgrade
sind die Auswahl des Knotens für den Aufruf von Visit in DFS und die
Reihenfolge der Knoten in der Adjazenzliste.
Beispiel. Figur 5.15 zeigt einen gerichteten Graphen mit seinem DFS-Baum.
Die Hochzahlen, die an jedem Knoten notiert sind, geben die Besuchsreihen-
folge an.
A.
A.1 B 13 H6 I7
F B
E
C8 G5
D
G
4 9 12
D J K H C J
I L K
2 3 10 11
F E L M M
A.
A.
C B E
B E
D F C D F
Bemerkung. Eine Kante (v, w) mit tb (w) < tb (v) ist genau dann Querkante,
wenn te (w) < tb (v) gilt. Der Endknoten w liegt in einem anderen Teil des
Graphen, der bereits traversiert wurde.
5.5 Gerichtete azyklische Graphen 235
∗.
∗ +
+ c ∗
a b + +
e f
A. B H I
C G
D J K
F E L M
Fig. 5.18: Topologische Sortierungen.
5.5 Gerichtete azyklische Graphen 237
J. K L M A C G H I B F E D
Der folgende Algorithmus, eine Modifikation von DFS, sortiert die Knoten
V = {1, . . . , n} eines azyklischen gerichteten Graphen topologisch.
Algorithmus 5.17.
vertex sorted[1..n]; node adl[1..n]; boolean visited[1..n]; index j
TopSort()
1 vertex k
2 for k ← 1 to n do
3 visited[k] ← false
4 j←n
5 for k ← 1 to n do
6 if not visited[k]
7 then Visit(k)
Visit(vertex k)
1 node no
2 visited[k] ← true
3 no ← adl[k]
4 while no ̸= null do
5 if not visited[no.v]
6 then Visit(no.v)
7 no ← no.next
8 sorted[j] := k, j := j − 1
Satz 5.18. Das Array sorted enthält nach Terminierung von TopSort die
Knoten von G in einer topologischen Sortierung.
Beweis. Sei w < v. Wegen w < v gibt es einen Pfad von w nach v. Wir
zeigen, dass w vor v im Array sorted kommt. Wir betrachten zunächst den
Fall tb (w) < tb (v). Da es einen Pfad von w nach v gibt, ist I(v) ⊂ I(w) (Satz
5.13). Deshalb ist te (v) < te (w). Da wir das Array sorted vom Ende her in der
Reihenfolge der Terminierung füllen, kommt w vor v in sorted. Im anderen
Fall ist tb (v) < tb (w). Dann ist I(v)∩I(w) = ∅. Denn aus I(v)∩I(w) ̸= ∅ folgt
I(w) ⊂ I(v) (loc. cit.). Deshalb ist w von v aus erreichbar. Ein Widerspruch
zu G azyklisch. Also ist auch te (v) < te (w) und w kommt auch in diesem Fall
vor v im Array sorted. 2
238 5. Graphen
Algorithmus 5.19.
vertex component[1..n], adl[1..n]; boolean visited[1..n]
int where[1..n], df s[1..n], low[1..n], num
TarjanComponents()
1 vertex k
2 for k ← 1 to n do
3 visited[k] ← false , component[k] ← 0, where[k] = 0
4 num ← 1
5 for k ← 1 to n do
6 if not visited[k]
7 then Visit(k)
20
Sambasiva R. Kosaraju ist ein indischer und amerikanischer Informatiker.
21
Micha Sharir (1950 – ) ist an israelischer Mathematiker and Informatiker.
5.6 Die starken Zusammenhangskomponenten 239
Visit(vertex k)
1 node no
2 df s[k] ← num, low[k] ← num, num ← num + 1
3 Push(k), where[k] ← −1, visited[k] ← true
4 no ← adl[k]
5 while no ̸= null do
6 if visited[no.v] = false
7 then Visit(no.v)
8 low[k] = min(low[k], low[no.v])
9 else if where[no.v] = −1
10 then low[k] = min(low[k], df s[no.v])
11 no ← no.next
12 if low[k] = df s[k]
13 then repeat
14 k ′ ← Pop, where[k ′ ] ← 1, component[k ′ ] ← k
15 until k ′ ̸= k
Bemerkungen:
1. Visit legt in Zeile 3 den Knoten k mit Push(k) auf einen Stack. In der
Variable where[k] vermerken wir dies (Zeile 3: where[k] ← −1). Für die
Variable where[k] gilt
0, falls k noch nicht besucht wurde,
where[k] = −1, nachdem k auf den Stack gelegt wurde,
1, nachdem k aus dem Stack entfernt wurde.
definiert.
Sei v ein Knoten von G und te (v) der Terminierungszeitpunkt von Visit
bezüglich v. Seien C1 , . . . , Cl die zu diesem Zeitpunkt entdeckten starken Zu-
sammenhangskomponenten, d. h. jene Zusammenhangskomponenten, deren
Wurzeln bereits ermittelt wurden. Wir definieren zu diesem Zeitpunkt für
einen Knoten v von G und den Teilbaum Tv ⊂ T mit Wurzel v die Teilmenge
Qv der aktiven Querkanten
Qv = {(u, w) | u ∈ Tv , w ∈
/ Tv , w ∈
/ C1 ∪ . . . ∪ Cl und (u, w) ∈
/ Rv }.
Wir nummerieren die Knoten des Graphen beim DFS-Traversieren mit tb (v)
und setzen
{
min{tb (w) | (v, w) ∈ Qv ∪ Rv }, wenn Qv ∪ Rv ̸= ∅,
low(v) =
tb (v) sonst.
E H I .
A1,1 D4,4
E 11,11
A. D G C 2,1
G 5,4
I 10,9
8,6
K K
Lemma 5.20. Sei v ∈ V und Tv der Teilbaum von T mit Wurzel v. Dann
ist v genau dann Wurzel einer starken Zusammenhangskomponente von G,
wenn Qv ∪ Rv = ∅ gilt. Die Menge Qv betrachten wir, nachdem Visit(v) die
Inspektion der Umgebung Uv abgeschlossen hat.
Beweis. Falls Qv ∪ Rv = ∅ gilt, kann v nicht zu einer starken Zusammen-
hangskomponente gehören, deren Wurzel Vorfahre von v ist. Der Knoten v
ist aus diesem Grund Wurzel einer starken Zusammenhangskomponente.
Falls Rv ̸= ∅ gilt, gibt es eine Rückwärtskante von einem Nachfahren von
v zu einem Vorfahren w von v. v und w gehören zur selben starken Zusam-
menhangskomponente. Für die Wurzel z dieser starken Zusammenhangskom-
ponente gilt tb (z) ≤ tb (w) < tb (v). Der Knoten v ist aufgrund dessen nicht
Wurzel seiner starken Zusammenhangskomponente.
Falls Qv ̸= ∅ gilt, gibt es eine Querkante (u, w). Sei z die Wurzel der star-
ken Zusammenhangskomponente von w. Dann gilt tb (z) ≤ tb (w). Der Aufruf
von Visit(z) ist bei Inspektion der Querkante (u, w) noch nicht terminiert,
denn sonst wäre die starke Zusammenhangskomponente mit Wurzel z als
starke Zusammenhangskomponente entdeckt. Der Knoten z ist deshalb ein
Vorfahre von v. Der Knoten v gehört somit zur starken Zusammenhangskom-
ponente mit Wurzel z. 2
Lemma 5.21. Ein Knoten k ist genau dann Wurzel einer starken Zusam-
menhangskomponente, wenn low(k) = tb (k) gilt.
Beweis. Ein Knoten k ist genau dann Wurzel einer starken Zusammenhangs-
komponente, wenn Qk ∪ Rk = ∅ gilt (Lemma 5.20). Dies wiederum ist äqui-
valent zu low(k) = tb (k). 2
Satz 5.22. Sei G ein gerichteter Graph und bezeichne n die Anzahl der Kno-
ten und m die Anzahl der Kanten. Der Algorithmus TarjanComponents be-
rechnet die starken Zusammenhangskomponenten von G. Für die Laufzeit
T (n, m) von TarjanComponents gilt: T (n, m) = O(n + m).
Beweis. Aus den Lemmata 5.20 und 5.21 folgt, dass TarjanComponents kor-
rekt ist. Die Laufzeit von DFS ist von der Ordnung O(n + m) und die zusätz-
liche Laufzeit für die repeat-until Schleife in Visit ist, akkumuliert über alle
Aufrufe von Visit, von der Ordnung O(n). Insgesamt folgt für die Laufzeit
T (n, m) von TarjanComponents, dass T (n, m) = O(n + m) gilt. 2
Beispiel. Figur 5.21 zeigt einen Graphen G und den zugeordneten reversen
Graphen. Starte die Tiefensuche DFS im Knoten A des Graphen G. Die
Hochzahlen geben die DFS-Nummerierung an. Die starken Zusammenhangs-
komponenten sind {A, C, G}, {B}, {H, I}, {J}, {K}, {L, M } und {F, D, E}.
.
A13 B 12 H 10 I9 A. B H I
C4 G11 C G
D1 J8 K7 D J K
F3 E2 L6 M5 F E L M
Satz 5.24. Die Knoten der mit Algorithmus 5.23 berechneten Komponenten
von Gr entsprechen den Knoten der starken Zusammenhangskomponenten
von G.
Beweis. Liegen v und w in einer starken Zusammenhangskomponente in G,
so gibt es einen Pfad von w nach v und von v nach w in G und dadurch auch
in Gr . Aus diesem Grund liegen auch v und w in derselben Komponente von
Gr .
5.7 Ein probabilistischer Min-Cut-Algorithmus 243
genau dann, wenn es k Kanten zwischen i∑und j gibt. Zusätzlich ist ein Array
n
a[1..n] mit a[i] = deg(i) gegeben. Es gilt i=1 a[i] = 2m. Wir denken uns die
Kanten ausgehend von jedem der Knoten 1 bis n in dieser Reihenfolge von
1 bis 2m nummeriert. Jede Kante hat zwei Endknoten und damit auch zwei
Nummern. Der folgende Algorithmus wählt die Nummer r ∈ {1, . . . , 2m} der
Kante, die kontrahiert werden soll, zufällig und führt die Kontraktion der
Kante durch.
Algorithmus 5.27.
graph RandContract(graph G)
1 int i ← 0, j ← 0, s ← 0, t ← 0
2 choose r ∈ {1, . . . , 2m} at random
3 while s < r do
4 i ← i + 1, s ← s + a[i]
5 s ← r − s + a[i]
6 while t < s do
7 j ← j + 1, t ← t + adm[i, j]
8 s←0
9 for k ← 1 to n do
10 adm[i, k] ← adm[i, k] + adm[j, k]
11 adm[k, i] ← adm[i, k], s ← s + adm[i, k]
12 a[i] ← s − adm[i, i], a[j] ← 0, adm[i, i] ← 0
Bemerkungen:
1. In Zeile 2 wählen wir eine Zahl r ∈ {1, . . . , 2m} zufällig. Dann ermitteln
wir den Knoten, von dem die r–te Kante ausgeht (Zeilen 3 und 4).
2. Nach Ausführung von Zeile 5 enthält s die Zahl, als wievielte Kante die
r–te Kante den Knoten i verlässt.
3. In den Zeilen 6 und 7 ermitteln wir den andere Endknoten der r–ten
Kante.
4. Die i–te und j–te Zeile (und Spalte) sind zu vereinigen“ (Zeilen 9, 10
”
und 11) und a[i] und a[j] sind neu zu berechnen.
5. Die Laufzeit von RandContract ist von der Ordnung O(n).
6. a[k] = 0 bedeutet, dass der Knoten k bei der Kontraktion mit einem
anderen Knoten identifiziert wurde. Die k–te Zeile und die k–te Spalte
der Adjazenzmatrix sind ungültig. Wir denken uns die k–te Zeile und
k–te Spalte als gestrichen. RandContract kann wiederholt auf adm und
a operieren. adm beschreibt einen Multigraphen. Die gültigen Zeilen und
Spalten von adm sind in a vermerkt.
7. RandContract kann einfach für eine Darstellung des Multigraphen durch
eine Adjazenzliste adaptiert werden. Eine Vereinigung der Adjazenzlisten
der Knoten i und j ist in der Zeit der Ordnung O(n) möglich, falls diese
Listen sortiert vorliegen.
Die Idee des Algorithmus SimpleMinCut besteht darin, nacheinander Kan-
ten zu kontrahieren, und zwar solange, bis nur noch zwei Knoten v und w
5.7 Ein probabilistischer Min-Cut-Algorithmus 245
übrig bleiben. Sei C ein minimaler Schnitt von G. C überlebt die Kontrak-
tionen von SimpleMinCut, falls wir keine Kante aus C kontrahieren. Dann
bleiben die Kanten aus C als Kanten zwischen v und w übrig. Entsteht durch
wiederholte Kontraktion ein Graph mit nur zwei Knoten und kontrahieren wir
keine Kante eines minimalen Schnittes, dann berechnet SimpleMinCut einen
minimalen Schnitt von G. Die Erfolgswahrscheinlichkeit von SimpleMinCut
ist jedoch sehr klein (Satz 5.29).
Beispiel. Falls wir die Kante {C, G} nicht kontrahieren, berechnet Simple-
MinCut einen minimalen Schnitt, wie Figur 5.22 zeigt.
A. B E F
{C,G}
.
D C G
Algorithmus 5.28.
edgeset SimpleMinCut(graph G)
1 graph I ← G
2 while I has more than two nodes do
3 I ← RandContract(I)
4 return EI
Satz 5.29. Sei G ein Graph mit n Knoten. Für die Erfolgswahrscheinlichkeit
von SimpleMinCut gilt:
2
p(SimpleMinCut berechnet einen minimalen Schnitt ) ≥ .
n(n − 1)
die Folge der Multigraphen, die bei Ausführung von SimpleMinCut durch
Kontraktion entsteht. Nach der i–ten Iteration entsteht Ii .
Falls wir in den ersten i Iterationen keine Kante aus C wählen, ist die Kardi-
nalität eines minimalen Schnittes in Ii gleich k. Der Grad eines Knotens in
Ii ist daher ≥ k. Aus diesem Grund gilt |Ei | ≥ (n−i)k
2 und 1 − |Eki | ≥ 1 − n−i
2
.
Es folgt deshalb
∏(
n−3
2
) ∏
n−3
n−i−2 2
pr ≥ 1− = = .
i=0
n−i i=0
n−i n(n − 1)
Satz 5.31. Sei G ein Graph mit n Knoten. Für die Laufzeit T (n) von L gilt:
( )
T (n) = O n2 log2 (n) .
5.7 Ein probabilistischer Min-Cut-Algorithmus 247
Beweis. Die Laufzeit T (n) von L ist definiert durch die rekursive Formel
(⌈ (√ )⌉)
n
T (n) = 2T √ + 2−1 + cn2 .
2
⌈ (√ ) ⌉
Es gilt √n2 + 2 − 1 < n+2√ . Wir betrachten
2
( )
n+2
T̃ (n) = 2T̃ √ + cn2 .
2
√ (√ )
Setze α = √2 , n= 2k + α und xk = T̃ 2k + α . Dann gilt
2−1
(√ )
xk = T̃ 2k + α
(√ ) (√ )2
= 2T̃ 2k−1 + α + c 2k + α
(√ )2
= 2xk−1 + c 2k + α , x1 = b.
Als Lösung erhalten wir mit Satz 1.15 und der Formel (F.5) im Anhang B
( √ )
∑k
2i + 2α 2i + α2
k−1
xk = 2 b+c
i=2
2i−1
( ) 2αc √ k (√ k−1 )
= b2k−1 + c(k − 1)2k + cα2 2k−1 − 1 + √ 2 2 −1
2−1
k
= O(k2 ).
Satz 5.32. Sei G ein Graph mit n Knoten. Für die Erfolgswahrscheinlichkeit
von L gilt:
1
p(L berechnet einen minimalen Schnitt ) ≥ .
log2 (n)
⌈ (√ )⌉
Beweis. Sei C ein minimaler Schnitt von G, k = |C|, t = √n2 + 2−1
und sei
(Vi , Ei ) = Ii oder I˜i , i = 0, . . . , n − t,
die Folge der Multigraphen, die bei Ausführung von L durch Kontraktion
entsteht. Für die Wahrscheinlichkeit pr, dass C ein minimaler Schnitt von I
oder von I˜ ist, gilt
248 5. Graphen
∏
n−t−1
|Ei | − k ∏
n−t−1
k ∏ (
n−t−1
2
)
pr = = 1− ≥ 1−
i=0
|Ei | i=0
|Ei | i=0
n−i
∏
n−t−1
n−i−2 t(t − 1) 1
= = ≥
i=0
n−i n(n − 1) 2
1
yk = yk−1 + + 1.
yk−1
23
Dies ist eine einfache nicht lineare Differenzengleichung erster Ordnung.
Wir zeigen durch Induktion nach k, dass
gilt. Für y1 = 3 ist die Ungleichung erfüllt. Weiter gilt nach der Induktions-
hypothese für k − 1
1 1
yk = yk−1 + +1>k−1+ +1>k
yk−1 k + Hk−2 + 3
23
Es handelt sich um einen Spezialfall der logistischen Differenzengleichung , für
die (außer in Spezialfällen) keine geschlossene Lösung angegeben werden kann
(siehe [Elaydi03, Seite 13]).
5.7 Ein probabilistischer Min-Cut-Algorithmus 249
und
1 1
yk = yk−1 + + 1 < k − 1 + Hk−2 + 3 + + 1 = k + Hk−1 + 3.
yk−1 k−1
Der Schluss von k − 1 auf k ist durchgeführt.
Es folgt xk = yk4+1 > k+Hk−1
4
+4 und
4 1
prn = x2 log2 (n−α) > > .
2 log2 (n − α) + H⌈2 log2 (n−α)−1⌉ + 4 log2 (n)
2
Wir wenden jetzt ein Standardverfahren für probabilistische Algorithmen
an, um die Erfolgswahrscheinlichkeit zu erhöhen. Durch unabhängige Wie-
derholungen von L können wir die Fehlerwahrscheinlichkeit beliebig klein
machen. Wir wiederholen L l = k⌈log2 (n)⌉ mal. k ist eine Konstante und
bestimmt die Erfolgswahrscheinlichkeit (Satz 5.34).
Algorithmus 5.33.
edgeset MinCut(graph G; int l)
1 edgeset Ẽ, E ← EG
2 for i = 1 to l do
3 Ẽ ← L(G)
4 if |Ẽ| < |E|
5 then E ← Ẽ
6 return E
Satz 5.34. Die Laufzeit T (n) von MinCut ist in der Ordnung O(n2 log2 (n)2 )
und für die Erfolgswahrscheinlichkeit von MinCut gilt:
p(MinCut berechnet einen minimalen Schnitt ) > 1 − e−k .
Beweis. Die Aussage über die Laufzeit folgt unmittelbar aus Satz 5.31. Für
die Irrtumswahrscheinlichkeit perr von L gilt: perr < 1 − log 1(n) . Für die
2
Wahrscheinlichkeit pr, dass L in jeder Iteration irrt, gilt
( )k⌈log2 (n)⌉ ( )k log2 (n)
1 1
pr < 1 − ≤ 1− .
log2 (n) log2 (n)
( )n
Da die Folge 1 − n1 monoton wachsend gegen e−1 konvergiert (Satz B.19),
konvergiert
( )k log2 (n)
1
1−
log2 (n)
monoton wachsend gegen e−k . Deshalb folgt pr < e−k . L berechnet immer
einen Schnitt. Dieser muss jedoch nicht notwendig minimal sein. Falls einer
der Aufrufe von L einen minimalen Schnitt berechnet, ist es der Aufruf, der
das Ergebnis mit der geringsten Anzahl von Kanten liefert. Die Wahrschein-
lichkeit, dass mindestens eine Rechnung korrekt ist, ist > 1 − e−k . 2
250 5. Graphen
Übungen.
1. Zeigen Sie, dass ein Graph genau dann einen Eulerkreis enthält, wenn er
zusammenhängend ist und alle Knoten einen geraden Grad besitzen.
2. Sei G ein zusammenhängender ebener Graph, v die Anzahl der Knoten
und e die Anzahl der Kanten von G. Mit f bezeichnen wir die Anzahl
der Flächen, in die G die Ebene zerlegt. Zeigen Sie die Eulersche Poly-
ederformel für ebene Graphen:
v − e + f = 2.
Damit ein Element k in prioQu mit einem Zugriff erreichbar ist (und nicht
gesucht werden muss), benötigen wir das Array pos[1..n]. Die Variable pos[k]
enthält die Position des Elementes k in prioQu. Falls das Element k nicht
gespeichert ist, enthält pos[k] den Wert 0.
Eine Änderung der Priorität des Elementes k erfordert die Änderung
der Priorität des queueEntry in prioQu an der Stelle r = pos[k]. Un-
ter Umständen ist dadurch die Heapbedingung an der Stelle ⌊r/2⌋ und an
weiteren Stellen verletzt. Die Funktion UpHeap operiert auf dem Array
prioQu[1..n] und stellt die Heapbedingung wieder her.
Algorithmus 6.2.
UpHeap(index r)
1 index : i, j; item : x
2 i ← r, j ← ⌊ 2i ⌋, x ← prioQu[i]
3 while j ≥ 1 do
4 if x.prio ≥ prioQu[j].prio
5 then break
6 prioQu[i] ← prioQu[j], i ← j, j ← ⌊ 2i ⌋
7 prioQu[i] ← x
Algorithmus 6.3.
int nrElem; queueEntry prioQu[1..n]; index pos[1..n]
boolean PQUpdate(element k; int prio)
1 if pos[k] = 0
2 then nrElem ← nrElem + 1
3 prioQu[nrElem].elem ← k, prioQu[nrElem].prio ← prio
4 UpHeap(nrElem)
5 return true
6 else if prioQu[pos[k]].prio > prio
7 then prioQu[pos[k]].prio ← prio
8 UpHeap(pos[k])
9 return true
10 return false
element PQRemove()
1 element ret
2 ret ← prioQu[1].elem
3 pos[prioQu[1].elem] ← 0
4 prioQu[1] ← prioQu[nrElem]
5 pos[prioQu[nrElem].elem] ← 1
6 nrElem ← nrElem − 1
7 DownHeap(1)
8 return ret
Bemerkung. Die Laufzeit T (n) von PQUpdate und von PQRemove ist von
derselben Ordnung wie die Laufzeit von UpHeap und von DownHeap. Es
ergibt sich T (n) = O(log2 (n)).
∪
l
V = Vi , Vi ∩ Vj = ∅ für i ̸= j.
i=1
boolean Union(int i, j)
1 ret ← false
2 i ← Find(i)
3 j ← Find(j)
4 if i ̸= j
5 then parent[i] ← j, ret = true
6 return ret
Bei dieser Implementierung von Union können die Bäume degenerieren.
Im schlechtesten Fall entstehen lineare Listen. Wir diskutieren jetzt zwei
Techniken – Balancierung nach der Höhe und Pfadkomprimierung – die dies
verhindern.
Balancierung nach der Höhe. Bei Balancierung nach der Höhe machen
wir den Knoten mit der größeren Höhe zur neuen Wurzel. Im folgenden Al-
gorithmus erfolgt dies durch die Zeilen 5 – 10, die Zeile 5 von Algorithmus
6.4 ersetzen. Die Höhe nimmt nur dann um 1 zu, wenn beide Bäume die-
selbe Höhe besitzen. Wir benutzen das Array rank[1..n], um die Höhe eines
Knotens zu speichern. In FindInit ist rank[i] = 0 zu setzen, i = 1, . . . , n.
258 6. Gewichtete Graphen
Algorithmus 6.5.
boolean Union(int i, j)
1 ret ← false
2 i ← Find(i)
3 j ← Find(j)
4 if i ̸= j
5 then ret = true
6 if rank[i] > rank[j]
7 then parent[j] ← i
8 else parent[i] ← j
9 if rank[i] = rank[j]
10 then rank[j] = rank[j] + 1
11 return ret
v.0
v1 V0
v.0
v2 V1
. V2 Find(vl ) vl vl−1 . v2 v1
=⇒ V0
vl−1
Vl Vl−1 V2 V1
vl Vl−1
Vl
Algorithmus 6.6.
int Find(int i)
1 int k ← i
2 while parent[k] > 0 do
3 k ← parent[k]
4 while parent[i] > 0 do
5 m ← i, i ← parent[i], parent[m] ← k
6 return i
Bemerkung. Der Algorithmus Union benötigt nur den Rang der Wurzelkno-
ten. Wir können den Rang k der Wurzel i als −k in parent[i] speichern. Wur-
zeln erkennen wir jetzt an negativen Einträgen oder an der 0. Dadurch können
wir das Array rank einsparen. Im Folgenden schreiben wir rank[u] = rank(u)
als Funktion.
Beweis.
1. Folgt v auf u im Aufstiegspfad, so wurde v als Wurzel des Teilbaums Tu
festgelegt. Es gilt dann rank(v) > rank(u). Im weiteren Verlauf bleibt
der Rang von u konstant, während der Rang von v zunehmen kann. Da
Pfadkomprimierung den Rang eines Knotens nicht ändert und da ein
Knoten nach Pfadkomprimierung nur Kind eines Knotens mit größerem
Rang werden kann, gilt die Aussage, auch nachdem Pfadkomprimierung
durchgeführt wurde.
2. Wir zeigen die Behauptung durch Induktion nach l. Für l = 0 (nach
FindInit) gilt rank(u) = 0 und log2 (|Tu |) = 0 die Behauptung ist demzu-
folge richtig. Nach einem Aufruf von Union mit Durchführung der Verei-
nigung bleibt entweder der Rang gleich, dann gilt auch die Ungleichung
nach der Vereinigung der Bäume, oder der Rang nimmt um eins zu. Dann
gilt
rank(u)neu = rank(u) + 1 = log2 (2rank(u)+1 )
260 6. Gewichtete Graphen
für alle m, n ≥ 0.
Beweis. Sei m ≥ 0.
Wir führen jetzt eine Reihe von Funktionen ein, die wir bei der Analyse
des Union-Find-Datentyps anwenden. Sei u ein Knoten, der keine Wurzel ist.
Wir definieren
Aus Aδ(u)−2 (2) ≤ ⌊log2 (n)⌋ < n (siehe (4)) und der Definition von α folgt
unmittelbar
262 6. Gewichtete Graphen
Daher gilt
Wir betrachten den ersten Fall. Sei i < j mit δ(ui ) = δ(uj ) = k. Dann
gilt für ui , uj
6.1 Grundlegende Algorithmen 263
Die erste Abschätzung benutzt die Monotonie von rank längs Pfaden, die
zweite Abschätzung folgt aus der Definition von δ(ui ), die dritte Abschätzung
folgt aus der Monotonie von rank längs Pfaden und der Monotonie von Ak
und die vierte aus (2) und der Monotonie von Ak . Nach der Terminierung
von Find gilt parent(ui ) = v und
r(ui )+1
(6) rank(parent(ui )) ≥ Ak (rank(ui )).
Jedes Mal, wenn die erste Bedingung für ui eintritt und tui um eins erhöht
wird, nimmt der Exponent von Ak in (6) um mindestens eins zu. Tritt der
Fall r(ui ) = rank(ui ) ein, so folgt
rank(ui )+1 rank(ui )+1
rank(parent(ui )) ≥ Ak (rank(ui )) ≥ Ak (1) = Ak+1 (rank(ui )).
Die erste Abschätzung folgt aus (6), die zweite benutzt die Monotonie von
Ak . Es folgt
rank(parent(ui )) ≥ Ak+1 (rank(ui )).
Nach (1), der Definition von δ(ui ), gilt δ(ui ) ≥ k + 1. Weiter gilt δ(ui ) ≤
α(⌊log2 (n)⌋) + 2 (siehe (5)). Mithin folgt
Wir summieren über alle tu und fassen Knoten mit gleichem Rang zusammen.
Mit Lemma 6.7 und der Formel für die Ableitung der geometrischen Reihe
(Anhang B (F.8)) folgt
∑ ∞
∑ n
tu ≤ r · (α(⌊log2 (n)⌋) + 2)
u r=0
2r
∑∞
r
= n(α(⌊log2 (n)⌋) + 2) r
r=0
2
= 2n(α(⌊log2 (n)⌋) + 2).
Wir betrachten jetzt den zweiten Fall. Für die Knoten u und v zusammen
erhöhen wir tF um 2. Für jeden Knoten u gilt δ(u) ≤ α(⌊log2 (n)⌋) + 2 (siehe
(5)).
Wir betrachten ein k ≤ α(⌊log2 (n)⌋) + 2. Dann ist die Bedingung 2 (b)
nur für den letzten Knoten ũ im Pfad P mit δ(ũ) = k erfüllt (für alle vor-
angehenden Knoten tritt ja Fall 1 ein). Aus diesem Grund gibt es für jeden
Wert k ≤ α(⌊log2 (n)⌋) + 2 höchstens einen Knoten, welcher den Fall 2 (b)
erfüllt. Demnach erhöht sich tF bei einer Ausführung von Find um höchstens
α(⌊log2 (n)⌋) + 4. Für m Ausführungen folgt
264 6. Gewichtete Graphen
Wir erhalten
∑
t U + tF + tu ≤ c(m + n)α(⌊log2 (n)⌋) = O((m + n) · α(n)).
u
Algorithmus 6.11.
node adl[1..n]; boolean marked[1..n]
vertex ancestor[1..n], lca[1..n, 1..n]
LCA(vertex k)
1 node no
2 ancestor[k] ← k, no ← adl[k]
3 while no ̸= null do
4 LCA(no.v)
5 Union(k, no.v), ancestor[Find(k)] ← k
6 no ← no.next
7 marked[k] ← true
8 for each {u, k} from Q do
9 if marked[u]
10 then lca[u, k] ← ancestor[Find(u)]
Bemerkungen:
1. LCA traversiert T mittels Tiefensuche (Algorithmus 5.12). Sei k ein Kno-
ten von T und v0 , v1 , . . . , vl = k der Pfad P in T von der Wurzel v0 zum
Knoten k. P besteht aus den Vorfahren von k. Figur 6.3 hält den Zeit-
punkt t fest, zu dem im Aufruf LCA(k) der Prozessor alle Anweisungen
einschließlich Zeile 7 ausgeführt hat.
v.0
V0 v1
V1 v2
V2 .
vl−1
Vl−1 k
Vl
3. Für alle Knotenpaare {u, k} aus Q, für die der Knoten u bereits markiert
ist, bestimmen wir den letzten gemeinsamen Vorfahren (Zeilen 9 und 10).
Da lca[u, v] nur gesetzt wird, wenn u und v markiert sind, wird Zeile 10
genau einmal für jedes Knotenpaar {u, v} ∈ Q ausgeführt. Wir erhalten
als Ergebnis
Satz 6.12. Der Algorithmus LCA berechnet für jedes Knotenpaar aus Q den
letzten gemeinsamen Vorfahren.
Wir behandeln jetzt eine weitere Methode zur Lösung des LCA-Problems
und folgen mit unserer Darstellung [BeFa00]. Wir reduzieren das LCA-
Problem auf die Berechnung des Minimums in einem Array. Genauer, ein
Algorithmus für das range minimum query (RMQ) Problem (Abschnitt
1.5.4) berechnet für ein Array a[1..n] von Zahlen und Indizes i und j mit
1 ≤ i ≤ j ≤ n einen Index k mit i ≤ k ≤ j und
Algorithmus 6.13.
vertex parent[1..n], no[1..2n − 1]; node adl[1..n]; index ino[1..n]
int depth[1..n], de[1..2n − 1]
Init()
1 index i ← 1, parent[1] ← 0, depth[0] ← −1
2 Visit(1)
Visit(vertex k)
1 node no
2 depth[k] ← depth[parent[k]] + 1
3 de[i] ← depth[k], no[i] ← k, ino[k] ← i, i := i + 1
4 node ← adl[k]
5 while node ̸= null do
6 parent[node.v] ← k
7 Visit(node.v)
8 de[i] ← depth[k], no[i] ← k, i := i + 1
9 node ← node.next
Bemerkungen:
1. Der Baum T mit der Knotenmenge {1, . . . , n} ist durch die Adjazenzliste
adl gegeben. Die Liste speichert für jeden Knoten von T die Nachfolger.
Der Algorithmus Visit führt eine Tiefensuche in T durch.
6.1 Grundlegende Algorithmen 267
2. Für jeden Knoten erfolgt nach dem Aufruf von Visit ein Eintrag in die
Arrays de, no und ino. Die Variable de[i] speichert die Tiefe des Knotens
no[i] und ino[k] speichert den Index des ersten Auftretens von k in no.
Daher gilt no[ino[k]] = k. Für jeden Knoten erfolgt für jeden Nachfolger
ein weiterer Eintrag in den Arrays de und no (Zeile 8).
3. Die Arrays de, no und ino benötigen für jeden Knoten einen Eintrag und
die Arrays de und no für jeden Nachfolger einen weiteren Eintrag. Da
es n Knoten und insgesamt n − 1 Nachfolger gibt, benötigen de und no
2n − 1 viele Plätze.
Beispiel. Figur 6.4 zeigt die Reduktion des LCA-Problems auf das RMQ-
Problem mittels Tiefensuche. Der Pfad in 6.4, der im Knoten A startet und
endet, visualisiert den Ablauf von Visit. Er traversiert jede Kante zweimal.
Für jeden Durchlauf einer Kante erfolgt ein Eintrag in de und no und ein
zusätzlicher Eintrag für den Startknoten.
A.
B C
D E H I J
F G
i : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
de : 0 1 2 1 2 3 2 3 2 1 0 1 2 1 2 1 2 1 0
no : A B D B E F E G E B A C H C I C J C A
node : A B C D E F G H I J
ino : 1 2 12 3 5 6 8 13 15 17
Satz 6.14. Seien k und l Knoten von T mit ino[k] < ino[l]. Dann gilt
Beweis. Seien k und l Knoten von T mit ino[k] < ino[l]. Sei v der letzte
gemeinsame Vorfahre von k und l. Die Knoten k und l liegen in Tv , dem
268 6. Gewichtete Graphen
Teilbaum von T mit Wurzel v. Die Tiefe von v ist minimal für alle Knoten
in Tv . Da dies alle Knoten sind, die zwischen dem ersten und letzten Auf-
treten des Knotens v im Array no liegen, ist die Tiefe dv von v minimal in
[de[ino[k]..ino[l]], d. h. no[rmqde (ino[k], ino[l]) = v. 2
Satz 6.15. Sei T ein Wurzelbaum mit n Knoten. Nach einer Vorverarbeitung
mit Laufzeit in der Ordnung O(n), können m LCA-Anfragen, die sich auf T
beziehen, in der Zeit O(m) beantwortet werden. Es gibt somit zur Lösung des
LCA-Problems einen Algorithmus von der Ordnung O(n + m).
Beweis. Die Laufzeit von Algorithmus 6.13, der das RMQ-Problem auf das
LCA-Problem reduziert, ist von der Ordnung O(n). Er berechnet ein Array
der Länge 2n − 1. Zur Lösung des RMQ-Problems wenden wir den Algorith-
mus aus dem folgenden Abschnitt an. Er besitzt eine Vorverarbeitungszeit
von der Ordnung O(n) (Satz 6.16). Anschließend kann eine RMQ-Anfrage
und damit auch eine LCA-Anfrage in der Zeit O(1) beantwortet werden. Ins-
gesamt erhalten wir einen Algorithmus, der m Anfragen nach einer Vorverar-
beitungszeit von der Ordnung O(n) in der Zeit O(m) beantwortet. 2
Das Array de[1..2n − 1] hat die Eigenschaft, dass sich zwei aufeinander
folgende Einträge nur um +1 oder −1 unterscheiden. Solche Arrays heißen
inkrementelle Arrays. Wir geben einen Algorithmus zur Lösung des RMQ-
Problems für derartige Arrays an.
mi,j = min{mα , mµ , mω },
6.1 Grundlegende Algorithmen 269
wobei
mα = min aγ [i mod k..k − 1],
mω = min aδ [0..j mod k] und
δ−1
mµ = min al [0..k − 1].
l=γ+1
Tc
c
.
Ta
a
i j
Wir diskutieren jetzt die für die Berechnung von Ta und Tc benötigte
Laufzeit. Die Berechnung jeder der Komponenten Tl von Ta = (T1 , . . . , T2k−1 )
kann durch einen Algorithmus, in k log2 (k) vielen Schritten erfolgen (Satz
1.37). Für die Berechnung von Ta ergibt sich die Laufzeit
⌈ ⌉ (⌈ ⌉)
log2 (n) log2 (n)
c · 2k−1 · k · log2 (k) = c · 2⌈log2 (n)/2⌉−1 · · log2
2 2
√
< c · n log2 (n) = O(n).
2
Der Aufwand zur Berechnung von Tc ist m log2 (m), wobei m die Länge des
Arrays c ist (Satz 1.37).
⌈n⌉ n
m= =
⌈ log2 (n) ⌉
k 2
Mithin können wir auch Tc durch einen Algorithmus der Laufzeit O(n) be-
rechnen. Wir fassen das Ergebnis im folgenden Satz zusammen.
Satz 6.16. Sei a[0..n − 1] ein inkrementelles Array der Länge n. Jede RMQ-
Anfrage für a kann nach einer Vorverarbeitung mit Laufzeit O(n) in der Zeit
O(1) beantwortet werden.
Bemerkungen:
1. Der Code der Zeilen 2-4 erzeugt den Wurzelknoten und speichert a[1] im
Wurzelknoten ab.
2. Die for-Schleife iteriert über die Elemente von a. Zu Beginn der i–ten
Iteration ist der kartesische Baum für a[1..i − 1] konstruiert. In der i–ten
Iteration speichern wir im allokierten Baumknoten nd das Element a[i]
ab (Zeile 7).
3. Zur Implementierung von Upheap (Zeile 9) ist Algorithmus 6.2 an die
veränderte Darstellung des Baums anzupassen. Der Parameter des Auf-
rufs von Upheap ist der zuletzt eingefügte Knoten rnode (Zeile 4, Zeile
14). Upheap ermittelt den tiefst liegenden Knoten auf dem Pfad von
rnode bis zur Wurzel, für den rnode.element ≤ a[i] gilt. Sind alle auf
dem Pfad gespeicherten Elemente > a[i], so gibt UpHeap null zurück. In
diesem Fall wird der neue Knoten nd Wurzel des Baums (Zeile 13). Der
bisherige Baum wird linker Teilbaum der neuen Wurzel (Zeile 13).
4. Sonst fügen wir den neuen Baumknoten als rechten Nachfolger von rnode
an (Zeile 12). Der Knoten rnode.right wird linker Nachfolger von nd
(Zeile 11).
5. Nach jedem Einfügen eines Knotens in den Baum bleibt die Heapbedin-
gung erhalten. Die Bedingung für die Inorder-Ausgabe ist erfüllt, weil
wir den Knoten nd so in den Baum einfügen, dass er nach dem Einfügen
der am weitesten rechts liegende Knoten ist.
272 6. Gewichtete Graphen
Satz 6.19. Die Laufzeit von Algorithmus 6.18 ist in der Ordnung O(n).
Beweis. Die Laufzeit von BuildCartesianTree ist proportional zur Anzahl
der Vergleiche, die wir in Upheap über alle Iterationen der for-Schleife
durchführen. Wir verankern den nächsten Knoten in Zeile 12 oder 14 als letz-
ten Knoten im Pfad P , der am weitesten rechts liegenden Knoten. UpHeap
traversiert den Pfad P solange, bis die Einfügestelle gefunden ist. Für jeden
Vergleich, der in UpHeap erfolgt, fügen wir einen Knoten in P ein oder ent-
fernen wir einen Knoten aus P . Jeder Knoten wird einmal in P eingefügt und
einmal aus P entfernt. Deshalb ist die Anzahl der Vergleiche in UpHeap über
alle Iterationen der for-Schleife in der Ordnung O(n). 2
Für die Reduktion des RMQ-Problems auf das LCA-Problem ordnen wir
dem Array a einen kartesischen Baum B zu. Ein Knoten von B speichert
jetzt das Paar (i, a[i]). Die Sortierreihenfolge dieser Elemente ist durch die
Anordnung auf der zweiten Komponente definiert.
Um für einen Index i auf den Knoten k, der (i, a[i]) speichert, in konstanter
Zeit zugreifen zu können, führen wir das Array pos ein: pos[i] speichert eine
Referenz auf k. Die Implementierung von BuildCartesianTree und Upheap
muss pos aktualisieren.
Beispiel. Figur 6.7 zeigt den a = {7, 4, 5, 11, 3, 6} zugeordneten kartesischen
Baum.
.
(5,3)
(2,4) (6,6)
(1,7) (3,5)
(4,11)
Satz 6.20. Sei a[1..n] ein Array von ganzen Zahlen und seien i, j ≤ n Indizes.
B sei der a zugeordnete kartesische Baum.
1. Dann gilt
rmqa (i, j) = lca(pos[i], pos[j]),
wobei lca(pos[i],pos[j]) die erste Komponente des im letzten gemeinsamen
Vorfahren der von pos[i] und pos[j] referenzierten Knoten bezeichnet.
2. Unsere Untersuchungen zeigen, dass wir einen Algorithmus implementie-
ren können, der m RMQ-Anfragen für ein ganzzahliges Array der Länge
n nach einer Vorverarbeitung mit Laufzeit O(n) in der Zeit O(m) beant-
wortet.
6.2 Die Algorithmen von Dijkstra und Prim 273
Beweis. Der Knoten, der (k, a[k]) speichert, ist ein Vorfahre der Knoten, die
(i, a[i]) und (j, a[j]) speichern, genau dann, wenn a[k] < a[i] und a[k] < a[j]
gilt und i ≤ k ≤ j ist und er ist letzter gemeinsamer Vorfahre, genau dann,
wenn a[k] = min(a[i..j]) und i ≤ k ≤ j gilt. Dies zeigt Punkt 1.
Wir führen RMQ-Anfragen zunächst auf LCA-Anfragen zurück (Ab-
schnitt 6.1.3) und anschließend LCA-Anfragen auf inkrementelle RMQ-Anfra-
gen (Satz 6.14). Die Reduktion erfolgt jeweils in der Laufzeit O(n). Die Aus-
sage zur Laufzeit folgt jetzt aus Satz 6.19 und aus Satz 6.15. 2
B
2
D A.
1 2 1 2
1 A. 1 B D
1 1
3 4
C E C E
6
Bemerkung. Der Algorithmus von Dijkstra sucht in jedem Schritt eine Lösung,
die im Augenblick optimal erscheint. Wir wählen einen Knoten, der minima-
len Abstand zum Startknoten v besitzt. Eine lokal optimale Lösung, soll eine
optimale Lösung ergeben. Dijkstras Algorithmus nimmt Bezug auf die Kennt-
nis, die zum Zeitpunkt der Wahl vorhanden ist. Dies sind die Abstände zu den
Knoten des bereits konstruierten Baums und zu Knoten, die zu Baumknoten
benachbart sind.
Diese Strategie führt nicht immer zum Ziel. Bei Dijkstras Algorithmus
funktioniert dies, falls alle Kanten positives Gewicht besitzen. Falls auch ne-
gativ gewichtete Kanten zugelassen sind, scheitert sie (Übung 2).
Algorithmen die nach dieser Strategie arbeiten heißen Greedy-Algorithmen
(siehe Abschnitt 1.5.3). Weitere Greedy-Algorithmen sind die Algorithmen
von Kruskal und Borůvka (6.3 und 6.4) und der Algorithmus von Prim (6.2),
den wir gleich anschließend behandeln.
Satz 6.22. Sei G ein zusammenhängender gewichteter Graph und v ein Kno-
ten von G. Der Algorithmus von Dijkstra berechnet einen kürzesten Wege-
Baum für G und v.
Beweis. Wir zeigen die Behauptung durch Induktion nach der Anzahl j der
Iterationen. Sei Tj der in den ersten j Iterationen konstruierte Baum. Für
j = 0 gilt die Behauptung. Für den im j–ten Konstruktionsschritt gewählten
Pfad P minimaler Länge v = v1 , . . . , vk−1 , vk = w gilt v2 , . . . , vk−1 ∈ VTj−1 ,
denn angenommen, es existiert ein i ∈ {2, . . . , k−1} mit vi ∈/ VTj−1 , dann folgt
6.2 Die Algorithmen von Dijkstra und Prim 275
d(v, vi ) < d(v, w), ein Widerspruch zur Wahl von w. Da v2 , . . . , vk−1 ∈ VTj−1
gilt, folgt nach Induktionsvorausetzung, dass dTj−1 (v, vk−1 ) = d(v, vk−1 ) gilt.
Nach der Wahl von P , gilt dTj (v, w) = d(v, w), d.h. Tj ist ein kürzester Wege-
Baum für den Teilgraphen von G, der von den Knoten von Tj erzeugt wird.
Dies zeigt die Behauptung. 2
Bemerkung. Der Algorithmus von Dijkstra berechnet auch für einen gerich-
teten Graphen die Abstände von einem festen Knoten v zu allen anderen
Knoten, die von v aus erreichbar sind. Die Implementierung, die wir anschlie-
ßend besprechen funktioniert ebenso für gerichtete Graphen.
Der Algorithmus von Prim. Wir erklären den Begriff eines minimalen
aufspannenden Baums für einen zusammenhängenden gewichteten Graphen
G = (V, E). Dazu betrachten wir die Menge aller aufspannenden Teilgraphen
SP := {S = (V, ES ) | ES ⊂ E, S zusammenhängend}.
Gesucht ist ein S ∈ SP mit g(S) minimal für S ∈ SP . Ein solches S ist ein
Baum.
Definition 6.23. Ein Baum, der G aufspannt und minimales Gewicht hat,
heißt minimaler aufspannender Baum (minimal spanning tree, kurz MST)
für G.
Bemerkung. Figur 6.9 zeigt zwei minimale aufspannende Bäume eines Gra-
phen. Falls gleiche Gewichte auftreten, ist ein minimaler aufspannender Baum
nicht notwendig eindeutig bestimmt.
2 2
B D B D B D
1 2 1 1 2
1 A. 6 1 A. 1 A.
3 4
6 4 4
C E C E C E
Wir führen jetzt den Algorithmus am Beispielgraphen der Figur 6.10 durch.
2
B D A.
1 2 1
2 4
1 A. 6 B D E
3 4 1
C E C
6
Der gewählte Knoten ist bei beiden Algorithmen ein Knoten aus Vad , der eine
Minimalitätsbedingung erfüllt oder der Startknoten für die nächste Zusam-
menhangskomponente. Wir bezeichnen dieses Element als Element minimaler
Priorität.
In Dijkstras Algorithmus wählen wir einen Pfad P mit einem Wurzelkno-
ten v als Anfangsknoten, der für alle Knoten w ∈/ VT als Endknoten, minimale
Länge besitzt. Dann ist w ∈ Vad und d(v, w) die Priorität von w (siehe Be-
weis von Satz 6.22). In Prims Algorithmus ist das Gewicht g({u, w}) einer
Kante {u, w} mit u ∈ VT und w ∈ / VT die Priorität von w. Beide Algorithmen
wählen einen Knoten minimaler Priorität w ∈ Vad .
Wir erhalten eine Implementierung des Algorithmus, wenn wir im Al-
gorithmus 5.11 die Queue durch eine Priority-Queue ersetzen. Wir erset-
zen das Prinzip first in, first out“, durch das Prinzip priority first“. Die
” ”
Priority-Queue identifiziert zu jedem Zeitpunkt das Element minimaler Prio-
rität. Während der Durchführung des Algorithmus kann sich die Priorität für
Knoten aus Vad erniedrigen. In diesem Fall findet ein Priority-Update statt.
Während wir bei der Darstellung durch eine Adjazenzliste die Priority-
Queue aus dem Abschnitt 6.1.1 tatsächlich einsetzen, ist sie bei der Darstel-
lung durch eine Adjazenzmatrix nur konzeptionell vorhanden. Wir bestimmen
das Element minimaler Priorität explizit im Algorithmus. Dies erfolgt hier,
ohne die Effizienz des Algorithmus zu reduzieren.
Wir realisieren beide Algorithmen im Wesentlichen durch eine Implemen-
tierung. Mit unserem Pseudocode folgen wir der Darstellung in [Sedgewick88].
1. Zunächst befinden sich alle Knoten in VR . Während der Ausführung des
Algorithmus wechseln sie zuerst nach Vad und dann nach VT .
2. Konstruktionsschritt: Wähle entweder einen Knoten k minimaler Prio-
rität prio aus Vad , falls Vad ̸= ∅ ist, oder den Startknoten für die nächste
Komponente, wobei {
min{g({v, k}) | v ∈ VT } für Prim,
prio := prio(k) :=
d(v, k) (v Startknoten) für Dijkstra,
278 6. Gewichtete Graphen
Init()
1 vertex k
2 for k ← 1 to n do
3 priority[k] ← −infinite, parent[k] ← 0
4 priority[0] ← −(infinite + 1)
MatrixPriorityFirst()
1 vertex k, t, min
2 Init(), min ← 1
3 repeat
4 k ← min, priority[k] ← −priority[k], min ← 0
5 if priority[k] = infinite
6 then priority[k] = 0
7 for t ← 1 to n do
8 if priority[t] < 0
9 then if (adm[k, t] > 0) and (priority[t] < −prio)
10 then priority[t] ← −prio
11 parent[t] ← k
12 if priority[t] > priority[min]
13 then min ← t
14 until min = 0
prio := adm[k, t] (Prim) oder prio := priority[k] + adm[k, t] (Dijkstra).
Bemerkungen:
1. Init initialisiert die Arrays priority und parent. Der Wert infinite kann
nicht als echte Priorität auftreten. priority[0] dient als Marke. Wir grei-
fen auf priority[0] in Zeile 12 mit min = 0 zu. Jeder Wert im Array
priority[1..n] ist > −(infinite + 1).
6.2 Die Algorithmen von Dijkstra und Prim 279
2. Der Startknoten ist der Knoten 1 (Zeile 2: min ← 1). Für die Zusammen-
hangskomponente, die den Knoten 1 enthält, lösen wir die Problemstel-
lung als Erstes. Die nächste Zusammenhangskomponente ist durch den
ersten Knoten t festgelegt, für den priority[t] = −infinite gilt (Zeile 12).
3. repeat-until-Schleife:
In Zeile 4 setzen wir min = 0. Die Variable min indiziert das Element
minimaler Priorität, falls min ̸= 0 gilt. In jeder Iteration der repeat-until-
Schleife nehmen wir einen Knoten k in VT auf (Zeile 4: priority[k] wird
positiv). Zu diesem Zeitpunkt gilt im Fall von Dijkstra d(v, k) = dT (v, k),
wobei v die entsprechende Wurzel von T ist, d.h. T ist ein kürzester Wege-
Baum für den Teilgraphen von G, der durch die Knoten von T erzeugt
wird.
Die Zeilen 5 und 6 benötigen wir nur für den Algorithmus von Dijkstra.
In priority[k] akkumulieren wir Abstände. Deshalb setzen wir in Zeile 6
priority[k] = 0.
In Zeile 13 setzen wir min = t, solange es ein t ∈ {1, . . . , n} mit
priority[t] < 0 gibt. Falls dieser Fall nicht eintritt, terminiert die repeat-
until-Schleife. Somit durchlaufen wir sie n mal.
4. for-Schleife:
In der for-Schleife (in den Zeilen 12 und 13) ermitteln wir das Element
minimaler Priorität der Elemente aus Vad (−infinite < prio < 0) oder
VR (prio = −infinite). Das Element minimaler Priorität nehmen wir als
nächsten Knoten in VT auf. Die Marke priority[0] = −(infinite + 1), die
kleiner als jedes andere Element im Array priority ist, ergibt einfacheren
Code.
5. In der for-Schleife (Zeilen 9, 10 und 11) aktualisieren wir für einen Knoten
t, der zu k benachbart, aber nicht aus VT ist, priority und parent (Priority-
Update), falls dies notwendig ist (Zeilen 10 und 11).
6. Für das Array priority gilt nach Terminierung von MatrixPriorityFirst:
g({k, parent[k]}) für Prim,
priority[k] = d(w, k) für Dijkstra,
0 für parent[k] = 0.
ListPriorityFirst()
1 vertex k
2 for k ← 1 to n do
3 priority[k] ← −infinite
4 for k ← 1 to n do
5 if priority[k] = −infinite
6 then Visit(k)
Visit(vertex k)
1 node no
2 if PQUpdate(k, infinite)
3 then parent[k] ← 0
4 repeat
5 k ← PQRemove, priority[k] ← −priority[k]
6 if priority[k] = infinite
7 then priority[k] ← 0
8 no ← adl[k]
9 while no ̸= null do
10 if priority[no.v] < 0
11 then if PQUpdate(no.v, prio)
12 then priority[no.v] ← −prio
13 parent[no.v] ← k
14 no ← no.next
15 until PQEmpty
prio = no.weight (Prim) oder prio = priority[k] + no.weight (Dijkstra).
Bemerkungen:
1. Der Rückgabewert von PQUpdate(k, infinite) in Zeile 2 ist genau dann
true, wenn k Wurzel eines Baums ist.
2. Der Aufruf von Visit(k) erzeugt einen aufspannenden Baum T für die
Zusammenhangskomponente von k. Der erste Startknoten ist der Knoten
1. Für zusammenhängendes G gibt es nur diesen Startknoten.
3. repeat-until-Schleife: In jeder Iteration der repeat-until-Schleife nehmen
wir einen Knoten k in VT auf (Zeile 5: priority[k] wird positiv). Zu die-
sem Zeitpunkt gilt im Fall von Dijkstra d(v, k) = dT (v, k), wobei v die
entsprechende Wurzel von T ist, d.h. T ist ein kürzester Wege-Baum für
den Teilgraphen von G, der durch die Knoten von T erzeugt wird.
4. while-Schleife: In der while-Schleife (Zeilen 10-13) nehmen wir einen Kno-
ten t, der zu k benachbart, aber nicht aus VT ist, in die Priority-Queue
auf. Wir aktualisieren die Arrays priority und parent für alle adjazenten
Knoten ∈ / VT (Priority-Update), falls dies notwendig ist (Zeilen 12, 13).
5. Für das Array priority gilt nach Terminierung von ListPriorityFirst:
g({k, parent[k]}) für den Algorithmus von Prim,
priority[k] = d(w, k) für den Algorithmus von Dijkstra,
0 für parent[k] = 0,
6.3 Der Algorithmus von Kruskal 281
2
B D B D
1 2
1 2
1 A. 6 1 A.
3 4 4
C E C E
6
G mit |Emin ∩ ET | ist maximal. Wir nehmen Tmin ̸= T an. Es gibt ein
i mit 1 ≤ i ≤ n − 1, e1 , . . . , ei−1 ∈ Emin und ei ∈ / Emin . Der Graph H :=
Tmin ∪{ei } ist nicht azyklisch. Sei Z ein Zyklus von H und e ∈ Z \ET . H \{e}
ist ein Baum. Da Tmin ein minimaler aufspannender Baum ist, gilt g(H \
{e}) = g(Tmin ) + g(ei ) − g(e) ≥ g(Tmin ). Hieraus folgt g(ei ) − g(e) ≥ 0. Nach
Wahl von ei in Kruskals Algorithmus ist g(ei ) minimal mit der Eigenschaft
(V, {e1 , . . . , ei }) ist azyklisch. Da (V, {e1 , . . . , ei−1 , e}) azyklisch ist, gilt g(e) ≥
g(ei ). Insgesamt folgt g(e) = g(ei ) und g(H \ {e}) = g(Tmin ). H \ {e} ist
damit auch ein minimaler aufspannender Baum. Die Anzahl der gemeinsamen
Kanten von H\{e} und ET ist größer als |Emin ∩ET |. Dies ist ein Widerspruch
zur Annahme Tmin ̸= T . Also ist T ein minimaler aufspannender Baum für
G. 2
Implementierung von Kruskals Algorithmus. Mit Hilfe des Union-
Find-Datentyps implementieren wir in Schritt 2 von Kruskals Algorithmus,
den Test auf Zyklen, effizient. Wir wenden diesen Test auf die Kanten in einer
aufsteigenden Sortierung nach Gewichten an. Die Datenstruktur einer Kante
ist definiert durch
Algorithmus 6.29.
edge ed[1..m]
Kruskal()
1 int i
2 Sort(ed), FindInit(n)
3 for i ← 1 to m do
4 if Union(ed[i].v1 , ed[i].v2 ) = true
5 then Insert(ed[i])
Bemerkungen:
1. Wir sortieren die Kanten aufsteigend nach dem Gewicht (Zeile 2). Die
for-Schleife iteriert durch die sortierte Liste (Zeilen 4, 5).
2. Wir benutzen den Union-Find-Datentyp, um in der i-ten Iteration zu
entscheiden, ob T ∪ {e}, e = ed[i] = {v, w}, azyklisch ist. Dies gilt genau
dann, wenn die Endknoten v und w von e in unterschiedlichen Kompo-
nenten liegen. Falls dies nicht der Fall ist, verbinde die Komponenten der
Knoten v und w, mit anderen Worten bilde die Vereinigung der beiden
Komponenten. Dies leistet der Union-Find-Datentyp (Abschnitt 6.1.2).
3. Die Prozedur Insert fügt die Kante ed[i] in den Baum ein.
4. Der Aufwand für Sort ist von der Ordnung O(m log2 (m)) (Kapitel 2),
der für FindInit ist von der Ordnung O(n), der für alle Union Aufrufe
von der Ordnung O(m) (Satz 6.10) und der Aufwand für Insert ist von
6.4 Der Algorithmus von Borůvka 283
der Ordnung O(1) (bei einer geeigneten Datenstruktur für Bäume). Ins-
gesamt folgt, dass die Laufzeit T (n, m) von Kruskal von der Ordnung
O(n + m log2 (m)) ist.
gilt. Da es bei den Algorithmen in den Abschnitten 6.4 – 6.6 nicht auf die
tatsächlichen Gewichte der Kanten ankommt, können wir stets die Min-Max-
Ordnung auf E betrachten. Im Folgenden setzen wir deshalb voraus, dass
je zwei Kanten verschiedenes Gewicht besitzen. Ein minimaler aufspannen-
der Baum bezüglich der Min-Max-Ordnung ist eindeutig bestimmt und ein
aufspannender Baum mit minimalem Gewicht.
Sei v ∈ V und e = {v, w}. Die Kante e heißt die zu v minimal inzidente
Kante, wenn e die kleinste inzidente Kante ist. Die minimal inzidente Kante
von v ist eindeutig bestimmt und führt zum nächsten Nachbarn von v. Die
Kante e ∈ E heißt minimal inzidente Kante von G, wenn e minimal inzidente
Kante für ein v ∈ V ist. Mit EMI bezeichnen wir die Menge aller minimal in-
zidenten Kanten von G. EMI ist eindeutig bestimmt. Da eine Kante höchstens
für zwei Knoten minimal inzidente Kante sein kann, gilt n > |EMI | ≥ n/2.
Kontraktion der minimal inzidenten Kanten. Die grundlegende Idee
des Algorithmus von Borůvka besteht in der Kontraktion aller minimal inzi-
denten Kanten. Sei G = (V, E) ein zusammenhängender Graph mit mehr als
3
Otakar Borůvka (1899 – 1995) war ein tschechischer Mathematiker.
284 6. Gewichtete Graphen
Beispiel. Figur 6.12 zeigt einen gewichteten Graphen mit seinen minimal in-
zidenten Kanten (durchgezogen gezeichnet) und der resultierenden Kontrak-
tion.
5
5 6
9
3
1. 2 5
6 1 2 8
4 3 9 8
11 7 4
10 3
8 7 1. 2
Beispiel. Der erste Aufruf von Boruvka kontrahiert im Graphen der Figur
6.13 die (minimal inzidenten) Kanten {1, 4}, {2, 3}, {3, 7}, {5, 6} und {5, 8},
der rekursive Aufruf die Kanten {1, 2} und {6, 7}.
5 5
5 6 5 6
9
3 3
1. 2 1. 2
6 1 2 8 6 1 2 8
4 3 4 3
11 7 4 4
10
8 7 8 7
in der i–ten Ausführung von Boruvka. Das Gewicht der Kante (u, v) ∈ B
ist gB (u, v) := g(ev ).
3. Die Kanten zwischen den Ebenen i − 1 und i im Borůvka-Baum ent-
sprechen einschließlich ihrer Gewichte den in der i–ten Ausführung von
Boruvka kontrahierten minimal inzidenten Kanten. Wir erhalten eine Ab-
bildung
ε : EB −→ EG ,
die einer Kante in EB die ihr entsprechende Kante in EG zuordnet. Ist
eine kontrahierte Kante e minimal inzidente Kante für beide Endpunkte,
so gibt es in B zwei Kanten die e zugeordnet werden. Diese Abbildung
ist im Allgemeinen weder injektiv noch surjektiv.
4. Wenden wir den Borůvka-Algorithmus auf einen Baum an, so kontrahie-
ren alle Kanten. Die Abbildung ε ist surjektiv. Für die spätere Anwen-
dung (im Abschnitt 6.6) stellen wir ε durch eine Tabelle während der
Konstruktion des Borůvka-Baums dar.
Definition 6.36. Den Baum B bezeichnen wir als den G zugeordneten
Borůvka-Baum oder kurz als Borůvka-Baum von G.
Beispiel. Figur 6.14 zeigt einen gewichteten Graphen mit zugeordnetem
Borůvka-Baum.
5
5 6 A.
9
3
1. 2 3 3 8
6 1 2 8 B C D
4 3
7 4
1 1 2 2 4 5 5 6
10
8 7 1 4 2 3 7 5 6 8
B = {1, 4}, C = {2, 3, 7}, D = {5, 6, 8} und A = {1, 2, 3, 4, 5, 6, 7, 8}.
Definition 6.37. Ein Wurzelbaum T heißt voll verzweigt, wenn alle Blätter
von T in einer Ebene liegen und jeder Knoten, der kein Blatt ist, mindestens
zwei Nachfolger besitzt.
Bemerkung. Der einem Graphen zugeordneter Borůvka-Baum ist voll ver-
zweigt.
Lemma 6.38. Sei T ein voll verzweigter Baum mit den Ebenen 0, . . . , l und
sei n die Anzahl der Blätter von T . Dann gilt:
1. Für die Anzahl ni der Knoten in der i–ten Ebene gilt ni ≤ n/2l−i .
2. Die Anzahl der Knoten von T ist ≤ 2n.
3. Für die Tiefe l von T gilt l ≤ ⌊log2 (n)⌋.
6.5 Die Verifikation minimaler aufspannender Bäume 289
∑
l ∑l ∑
l−1 ( )
n 1 1
ni ≤ = n = n 2 − ≤ 2n
i=0 i=0
2l−i i=0
2i 2l
(Anhang B (F.5)). 2
(Anhang B (F.8)). 2
Beispiel. Figur 6.15 zeigt einen Baum mit zugeordnetem Borůvka-Baum.
5
5 6 A.
1. 2 3 3 8
9 1 2 8 B C D
4 3
3 4
1 1 2 2 4 5 5 9
8 7 1 4 2 3 7 5 6 8
Satz 6.40. Sei T = (V, E) ein gewichteter Baum (mit paarweise verschie-
denen Gewichten) und u, v ∈ V . Mit PT (u, v) bezeichnen wir den einfachen
Pfad von u nach v in T und mit PB (u, v) den (ungerichteten) einfachen Pfad,
der die Blätter u und v im T zugeordneten Borůvka-Baum B verbindet. Dann
gilt
max g(e) = max gB (ẽ).
e∈PT (u,v) ẽ∈PB (u,v)
die Einschränkung p|v von p auf die Ebenen 0, . . . , t(v) von T , wobei t(v) die
Tiefe von v ist. Wir bezeichnen die Menge diese Pfade mit
P [v] = {p|v | p ∈ Q und p geht durch v}.
Die Anfangsknoten dieser Pfade liegen in M [v]. Wir beschreiben zunächst
Eigenschaften der Liste L[v]v∈V und geben anschließend an, wie wir diese
Liste berechnen.
Die Liste L[v] enthält für jeden Knoten, der Vorfahre von v und An-
fangspunkt eines Pfades p|v aus P [v] ist, einen Eintrag. Dieser besteht aus
dem Endknoten einer Kante maximalen Gewichts von p|v . Die Liste L[v] ist
bezüglich folgender Anordnung auf der Menge V – definiert durch v < w
genau dann, wenn das Gewicht von (parent(v), v) kleiner dem Gewicht von
(parent(w), w) ist – absteigend sortiert. Mit parent(v) bezeichnen wir den
Vorgänger eines Knotens v in T .
Die Liste L[v]v∈V berechnen wir ausgehend von der Wurzel (zum Beispiel
mittels Breitensuche, Algorithmus 5.11), von oben nach unten. Für die Wurzel
r ist L[r] die leere Liste.
Sei u der Vorgänger von v. Wir berechnen die Liste L[v] aus L[u]. Die
Kante (u, v) ist Element von P [v], falls u Anfangspunkt eines Pfades aus
P [v] ist, der nur aus einer Kante besteht. Alle anderen Pfade in P [v] sind
die Pfade aus P [u], die im Knoten u nach v verzweigen, mit anderen Worten,
diese Pfade aus P [v] entstehen aus einem Pfad p̃ ∈ P [u] durch Verlängerung
mit der Kante (u, v). Sei P̃ [u] ⊂ P [u] die Teilmenge dieser Pfade und L̃[v] ⊂
L[u] die Liste der Endpunkte von Kanten maximalen Gewichts der Pfade
aus P̃ [u]. L̃[v] können wir anhand von M [u] und M [v] aus L[u] berechnen.
Mithilfe von M [u] und M [v] identifizieren wir die Pfade aus M [u], die durch v
gehen. Diese benötigen wir bei der Ermittlung von L̃[v]. Die Liste L̃[v] halten
wir absteigend sortiert. Wir beschreiben nun im Detail, wie die Berechnung
erfolgt.
Mittels binärer Suche ermitteln wir die Elemente in L̃[v], die kleiner v
sind. Diese repräsentieren die Endknoten von Kanten maximalen Gewichts,
deren Gewichte kleiner dem Gewicht der Kante (u, v) sind. Deshalb sind diese
durch v zu ersetzen.
Es bleiben jetzt noch die Pfade zu berücksichtigen, die nur aus einer Kante
(u, v) bestehen. Für jedes u ∈ M [v] erweitern wir L̃[v] um ein v. Das Ergebnis
ist die Liste L[v]. Mit L̃[v] ist auch L[v] absteigend sortiert.
Die Liste L[vi ], i = 1, . . . , m, enthält für jeden der Pfade aus P [vi ], der
Menge der Pfade aus Q, die in vi enden, den tiefer gelegenen Endpunkt einer
Kante maximalen Gewichts. Eine Pfad-Maximum-Anfrage können wir mit
den Listen M [v] und L[v] in der Zeit O(1) beantworten.
Beispiel. Wir berechnen die Listen M [v]v∈V und L[v]v∈V für das Beispiel in
Figur 6.16. Die Anfragepfade seine gegeben durch ihre Anfangs- und Endkno-
ten:
(A, 8), (A, 14), (A, 17), (B, 5), (C, 13), (C, 17) und (H, 17).
6.5 Die Verifikation minimaler aufspannender Bäume 293
A.
4 5
B C
3 3 8 7 3
D E F G H
1 1 2 2 4 5 5 9 11 1 8 2 4
1 4 2 3 7 5 6 8 11 14 12 13 17
Wir geben die Listenelemente von M [v]v∈V , L̃[v]v∈V und L[v]v∈V für
Knoten an, die auf Anfragepfaden liegen. Die Einträge der Liste L̃[v] rühren
von Einträgen der Liste L[parent(v)] her.
L[A] :
(A) (A) (A)
L[B] : B L[C] : C , C
(A) (A) (A)
L̃[F ] : B L̃[G] : C L̃[H] : C
( A ) (B ) (A) (A) ( C ) ( C )
L[F ] : F , F L[G] : G L[H] : C , H , H
(A) (A) (C )
L̃[5] : F L̃[8] : F L̃[13] : H
(A) (A) ( C )
L̃[14] : G L̃[17] : C , H
( ) ( A) (C )
L[5] : B F L[8] : 8 L[13] : H
(A) (A) ( C ) ( H )
L[14] : G L[17] : C , 17 , 17
Für die übrigen Knoten sind die Listenelemente leer. Die Liste L speichert hier
in der zweiten Komponente den Endknoten der Kante maximalen Gewichts
und in der ersten den Anfangsknoten des Anfragepfades.
Lemma 6.41. Für einen voll verzweigter Baum mit n Blättern und für m
Anfragen zur Ermittlung einer Kante maximalen Gewichts kann die Liste
L[v]v∈V mit O(n + m) vielen Vergleichen erzeugt werden.
Beweis. Mit Vi bezeichnen wir die Menge der Knoten v von B in der i–ten
Ebene mit L̃[v] ̸= ∅ und wir setzen ni = |Vi |, i = 0, . . . , l. Für die Anzahl Ni
294 6. Gewichtete Graphen
der Vergleiche bei Anwendung von Algorithmus 2.36 zur binären Suche für
alle Knoten der Ebene i gilt
∑ ∑ log (|L̃[v]|)
Ni ≤ (log2 (|L̃[v]|) + 1) ≤ ni + ni 2
ni
v∈Vi v∈Vi
(∑ ) ( )
v∈Vi |L̃[v]| m
≤ ni + ni log2 ≤ ni + ni log2 .
ni ni
∑
Es gilt v∈Vi n1i = |Vi | n1i = 1, daher folgt die Abschätzung in der zweiten
Zeile aus der Jensenschen Ungleichung, angewendet auf die konkave Funk-
tion log2 (Lemma B.23). Da jeder Anfragepfad durch genau einen Knoten
der i–ten Ebene führt und die Anzahl der Elemente von L̃[v] ∑ kleiner gleich
der Anzahl der Anfragepfade ist, die durch v gehen, gilt v∈Vi |L̃[v]| ≤ m.
Deshalb folgt die vierte Abschätzung.
Wir summieren anschließend über alle Ebenen i = 0, . . . , l und verwenden
dabei
∑l ∑l
n ∑l
1
ni ≤ l−i
≤n ≤ 2n
i=0 i=0
2 i=0
2l
(Lemma 6.38, Anhang B (F.8)).
∑
l ( ) ∑
l ( )
m m n
ni + ni log2 ≤ 2n + ni log2 ·
i=0
ni i=0
n ni
∑
l (m) ) (
n
= 2n + ni log2 + ni log2
i=0
n ni
(m) ∑ l ( )
n
≤ 2n + 2n log2 + ni log2
n i=0
n i
≤ 2n + 2m + 3n = O(n + m).
Die Funktion (n)
x 7→ x log2
x
ist für x ≤ n
4 monoton wachsend. Somit gilt
∑
l ( ) ( ) ( ) ∑
l−2 ( )
n n n n
ni log2 = nl log2 + nl−1 log2 + ni log2
i=0
ni nl nl−1 i=0
ni
∑
l−2
n ( )
≤ n+ log2 2l−i
i=0
2l−i
∑ i
≤ n+n ≤ 3n (Anhang B (F.8)).
2i
i≥0
Bemerkungen:
1. LCA(B, Q1 ) reduziert die Berechnung der letzten gemeinsamen Vor-
gänger der Endknoten der Kanten aus Q1 im Baum B auf die Lösung
des RMQ-Problems und führt dazu aus
a. Algorithmus 6.13 zur Reduktion des LCA-Problems auf das RMQ-
Problem.
b. Die Algorithmen zur Initialisierung der Tabellen für die Lösung des
RMQ-Problems (Seite 268).
c. LCA-Abfragen für alle {u, v} ∈ Q1 . Die Liste Q2 enthält für {u, v} ∈
Q1 den letzten gemeinsamen Vorfahren LCA(u, v) von u und v in B.
2. MaxEdgeInit berechnet die Nachschlagetabelle L aus diesem Abschnitt
(Seite 292).
3. MaxEdge ermittelt für eine Kante {u, v} ∈ Q1 die Kante maximalen Ge-
wichts des Pfades in T , der u und v verbindet. Dazu wird für die Knoten
u und v in B das maximale Gewicht, der sich ergebenden Teilpfade von u
nach LCA(u, v) und von LCA(u, v) nach v, in der Tabelle L nachgeschla-
gen und die Kante maximalen Gewichts des Gesamtpfads ermittelt. Mit
der Abbildung ε : EB −→ ET , die auf Seite 288 definiert ist, wird die der
Kante maximalen Gewichts in B entsprechende Kante in T ermittelt.
4. Wir können den Algorithmus 6.42 modifizieren und für alle Kanten e aus
Q1 aufzeichnen, ob für e der Vergleich in Zeile 6 erfüllt ist oder nicht.
296 6. Gewichtete Graphen
Die Laufzeit von Prims und Borůvkas Algorithmus ist von der Ordnung
O((n + m) log2 (n)), die Laufzeit von Kruskals Algorithmus ist von der Ord-
nung O(n + m log2 (m)). Durch Anwendung von probabilistischen Methoden
lässt sich ein Algorithmus mit einer besseren Laufzeit zur Lösung des Pro-
blems angeben.
Der Algorithmus von Karger, Klein und Tarjan – wir bezeichnen ihn mit
KKT-MST – berechnet für einen zusammenhängenden Graphen einen mini-
malen aufspannenden Baum ([KarKleTar95]). Der Erwartungswert der Lauf-
zeit des Algorithmus ist von der Ordnung O(n + m). Um dies zu erreichen,
wird die Funktion Contract aus Borůvkas Algorithmus und eine probabilis-
tische Methode angewendet, die mithilfe von Zufallsbits einen Teilgraphen
sampelt. Dieser Schritt dient dazu, Kanten zu identifizieren, die nicht in ei-
nem minimalen aufspannenden Baum vorkommen können.
Zunächst beweisen wir eine Eigenschaft minimaler aufspannender Bäume
und führen Begriffe ein, die wir anschließend benötigen.
Lemma 6.44 (Kreiseigenschaft). Sei G ein zusammenhängender Graph, Z
ein Zyklus in G und e ∈ Z eine Kante mit g(e) > g(e′ ) für alle e′ ∈ Z, e′ ̸= e.
Dann kann e nicht Kante eines minimalen aufspannenden Baums sein.
Beweis. Sei T ein minimaler aufspannender Baum von G und e = {u, v} eine
Kante maximalen Gewichts in Z. Angenommen, es gilt e ∈ T . Wenn wir e
entfernen, zerfällt T in zwei Komponenten Tu und Tv . Weil Z ein Zyklus ist,
gibt es eine Kante e′ = {u′ , v ′ } ∈ Z mit u′ ∈ Tu und v ′ ∈ Tv . Dann ist T ′ =
(T \{e})∪{e′ } ein aufspannender Baum und g(T ′ ) = g(T )−g(e)+g(e′ ) < g(T ),
ein Widerspruch. 2
Definition 6.45. Sei G = (V, E) ein gewichteter Graph, F ⊂ G ein azykli-
scher aufspannender Teilgraph. Für Knoten u, v aus derselben Komponente
6.6 Ein probabilistischer MST-Algorithmus 297
von F ist der F –Pfad zwischen u und v der (eindeutig bestimmte) Pfad
zwischen u und v, der ganz in F verläuft.
∞, falls u und v in
verschiedenen Komponenten von F liegen,
gF (u, v) :=
max g({v i−1 , vi }), wobei P : u = v0 , . . . , vl = v,
1≤i≤l
der F –Pfad zwischen u und v ist.
Eine Kante e = {u, v} ∈ E heißt F –schwer , wenn g(e) > gF (u, v) gilt und
F –leicht, wenn g(e) ≤ gF (u, v) gilt.
Bemerkung. Eine Kante e = {u, v} ist F –schwer, wenn alle Kanten des F –
Pfades, der u und v verbindet, ein Gewicht < g(e) besitzen. Kanten, die
verschiedene Komponenten von F verbinden und Kanten aus F sind alle
F –leicht.
Lemma 6.46. Sei G = (V, E) ein gewichteter zusammenhängender Graph,
F ⊂ G ein azyklischer Teilgraph und e ∈ E sei eine F –schwere Kante. Dann
kommt e nicht als Kante in einem minimalen aufspannenden Baum von G
vor.
Beweis. Sei die Kante e = {u, v} F –schwer und P : u = v0 , . . . , vl = v der
Pfad von u nach v in F . Das Gewicht g(e) ist größer als das Gewicht jeder
Kante des Pfades P . Nach Lemma 6.44 kann e nicht Kante eines minimalen
aufspannenden Baums sein. 2
Der probabilistische Anteil des Algorithmus von Karger, Klein und Tarjan
ist der Algorithmus SampleSubgraph, der einen Teilgraphen H von G =
(V, E) zufällig erzeugt.
298 6. Gewichtete Graphen
Algorithmus 6.47.
graph SampleSubgraph(graph G)
1 H ← (V, ∅)
2 for each e ∈ E do
3 if coinToss = heads
4 then H ← H ∪ {e}
5 return H
Der Teilgraph H von G ist nicht notwendig zusammenhängend. Die Erwar-
tung ist, dass der (eindeutig bestimmte) minimale aufspannende Wald F von
H eine gute Approximation eines minimalen aufspannenden Baums von G
ist, d. h. nur wenige Kanten von G, die nicht in F liegen, sind F –leicht.
Satz 6.48. Sei G = (V, E) ein Graph mit n Knoten und m Kanten, H das
Ergebnis von SampleSubgraph(G) und X die Anzahl der Kanten von H. Sei
F der minimale aufspannende Wald von H und Y die Anzahl der F –leichten
Kanten in G. Dann gilt E(X) = m/2 und E(Y ) ≤ 2n.
Beweis. Die Zufallsvariable X ist binomialverteilt mit Parameter (m, 1/2).
Deshalb gilt E(X) = m/2 (Satz A.16). Um den Erwartungswert der Zufalls-
variablen Y abzuschätzen, modifizieren wir SampleSubgraph und berechnen
mit H simultan den minimalen aufspannenden Wald von H nach Kruskals
Methode (Algorithmus 6.29). Der modifizierte Algorithmus entscheidet wie
SampleSubgraph für jede Kante e aufgrund eines Münzwurfs, ob e als Kante
für H genommen wird.
Algorithmus 6.49.
SampleSubgraphMSF(graph G)
1 H ← (V, ∅), F ← (V, ∅), Y ← 0
2 {e1 , . . . , em } ← sort(E)
3 for i ← 1 to m do
4 if ei is F –light
5 then Y ← Y + 1
6 if coinToss = heads
7 then H ← H ∪ {ei }
8 F ← F ∪ {ei }
9 else if coinToss = heads
10 then H ← H ∪ {ei }
Kruskals Algorithmus erfordert, dass die Kanten von G aufsteigend sor-
tiert sind. Für jede F –leichte Kante von G entscheiden wir aufgrund eines
Münzwurfs (mit einer 10 Cent Münze) in Zeile 6, ob wir sie für H und F
wählen. Die Variable Y zählt die Anzahl der F –leichten Kanten und enthält
nach Terminierung deren Anzahl, die mit der Anzahl der Münzwürfe mit der
10 Cent Münze übereinstimmt. Falls die Endpunkte der Kante ei , die wir im
i–ten Schritt betrachten, in der gleichen Komponenten von F liegen, so ist ei
F –schwer (da die Kanten aufsteigend sortiert sind, haben alle Kanten aus EF
6.6 Ein probabilistischer MST-Algorithmus 299
das Gewicht < g(ei )). Hat ei ihre Endpunkte in verschiedenen Komponen-
ten von F , so ist ei F –leicht (beachte gF (ei ) = ∞). Wir berechnen – analog
zur Methode von Kruskals Algorithmus (Algorithmus 6.3) – den minimalen
aufspannenden Wald F von H. In Zeile 9 entscheiden wir für eine F –schwere
Kante ei aufgrund eines Münzwurfs (mit einer 20 Cent Münze), ob wir ei für
H wählen. Eine Kante ei , die wir in Zeile 7 wählen, ist auch nach Terminie-
rung von SampleSubgraph F –leicht, da das Gewicht einer Kante, die später
zu F hinzukommen > g(ei ) ist.
Unser Zufallsexperiment besteht aus zwei Phasen. In Phase 1 führen wir
den Algorithmus SampleSubgraphMSF aus. Da F azyklisch ist und n viele
Knoten besitzt, gilt nach Terminierung von SampleSubgraphMSF |EF | ≤
n − 1. In Phase 2 fahren wir mit dem Werfen der 10 Cent Münzen fort. Die
Zufallsvariable Y zählt weiter die Anzahl der Münzwürfe. Wir beenden Phase
2, sobald das Ereignis heads“ n–mal (in Phase 1 und Phase 2) eintritt.
”
Die Zufallsvariable Y zählt die Anzahl der Wiederholungen bis das Er-
eignis heads“ n–mal eintritt. Sie ist negativ binomialverteilt mit Parameter
”
(n, 1/2). Für den Erwartungswert von Y gilt E(Y ) = 2n (Satz A.22). Da die
Anzahl der F –leichten Kanten durch Y beschränkt ist, ist der Erwartungs-
wert der Anzahl der F –leichten Kanten in G ≤ E(Y ), also auch ≤ 2n. 2
Der Input für den Algorithmus KKT-MST ist ein gewichteter (nicht not-
wendig zusammenhängender) Graph G = (V, E). Das Ergebnis ist ein mi-
nimaler aufspannender Wald für G. KKT-MST reduziert in zwei Schritten
die Größe des Graphen G. Im ersten Schritt wenden wir Contract (Algorith-
mus 6.31) dreimal hintereinander an. Das Ergebnis sei G1 = (V1 , E1 ). In
einem zweiten Schritt löschen wir Kanten in G1 , die in keinem minimalen
aufspannenden Baum vorkommen können. Das Ergebnis bezeichnen wir mit
G3 = (V3 , E3 ).
Algorithmus 6.50.
edgeset KKT-MST(graph G)
1 F1 ← ∅
2 for i ← 1 to 3 do
3 (G1 , EMI ) ← Contract(G)
4 F1 ← F1 ∪ EMI , G ← G1
5 if |V1 | = 1 then return F1
6 G2 ← SampleSubgraph(G1 )
7 F2 ← KKT-MST(G2 )
8 G3 ← DeleteHeavyEdges(F2 , G1 )
9 return F1 ∪ KKT-MST(G3 )
Algorithmus bezeichnet und sind durch einen Algorithmus für reguläre Aus-
drücke von Kleene7 aus dem Jahr 1956 inspiriert. Der Floyd-Warshall Algo-
rithmus ist nach der Entwurfsmethode dynamisches Programmieren konzi-
piert (siehe Abschnitt 1.5.4).
Definition 6.52. Sei G = (V, E) ein gerichteter Graph. Der Graph
a0 := a,
ak [i, j] := ak−1 [i, j] or (ak−1 [i, k] and ak−1 [k, j]) für k = 1, . . . , n.
Beweis. Wir zeigen die Behauptung durch Induktion nach k: Für k = 0 ist
a0 [i, j] = 1 genau dann, wenn es einen 0-Pfad (Kante) von i nach j gibt. Der
Induktionsanfang ist deswegen richtig.
Aus k − 1 folgt k: ak [i, j] = 1 genau dann, wenn ak−1 [i, j] = 1 oder wenn
ak−1 [i, k] = 1 und ak−1 [k, j] = 1 gilt. Dies wiederum ist äquivalent zu: Es
gibt einen (k − 1)–Pfad von i nach j oder es gibt einen (k − 1)–Pfad von i
nach k und von k nach j. Die letzte Aussage gilt genau dann, wenn es einen
k–Pfad von i nach j gibt.
Da die Menge der n–Pfade alle Pfade umfasst, folgt die letzte Behauptung
des Satzes. 2
Beispiel. Figur 6.17 zeigt die Berechnung des transitiven Abschlusses durch
dynamisches Programmieren mit dem Algorithmus von Warshall.
1. 2 0100 0100
0 0 1 0 0 0 1 0
a0 = , a1 = ,
0 0 0 1 0 0 0 1
4 3 1000 1100
0 1 1 0 0 1 1 1 0 1 1 1
0 0 0 1 1 1
0 1 0 1 0 1
a2 = , a3 = , a4 = .
0 0 0 1 0 0 0 1 1 1 0 1
1 1 1 0 1 1 1 0 1 1 1 0
Bemerkungen:
1. Einsen bleiben in den nachfolgenden Matrizen auch Einsen (oder Opera-
tion), deshalb betrachten wir für eine Iteration nur Nullen außerhalb der
Diagonale.
2. Die Berechnung von ak [i, j] erfolgt mit ak−1 [i, j], ak−1 [i, k] und ak−1 [k, j].
ak [i, k] = ak−1 [i, k] or (ak−1 [i, k] and ak−1 [k, k]). Deswegen gilt
ak [i, k] = ak−1 [i, k], da ak−1 [k, k] = 0 ist. Analog folgt: ak [k, j] =
ak−1 [k, j]. Deshalb können wir die Berechnung mit einer Matrix (Spei-
cher) durchführen.
Algorithmus 6.55.
Warshall(boolean a[1..n, 1..n])
1 vertex i, j, k
2 for k ← 1 to n do
3 for i ← 1 to n do
4 for j ← 1 to n do
5 a[i, j] = a[i, j] or (a[i, k] and a[k, j])
6.7 Transitiver Abschluss und Abstandsmatrix 303
Bemerkungen:
1. Wegen ak [i, k] = ak−1 [i, k] und ak [k, j] = ak−1 [k, j] können wir die Be-
rechnung mit einer Matrix (Speicher) durchführen.
2. Falls negative Gewichte zugelassen sind, aber keine Zyklen mit negativer
Länge auftreten, dann arbeitet Floyd korrekt, denn dann ist ein einfacher
Pfad, ein Pfad kürzester Länge.
Beispiel. Figur 6.18 zeigt die Berechnung der Abstandsmatrix mit dem Al-
gorithmus von Floyd nach der Methode dynamisches Programmieren.
10
0 5 10 ∞ 0 5 10 ∞
5 7 ∞ 7 0 2 ∞
2
1. 2 3 0 2
7 a0 = , a1 = ,
∞ ∞ 0 4 ∞ ∞ 0 4
12 4
3 8 3 12 8 0 3 8 8 0
4
0 5 7 ∞ 0 5 7 11 0 5 7 11
7 ∞ 7 6 7 6
0 2 0 2 0 2
a2 = , a3 = , a4 = .
∞ ∞ 0 4 ∞ ∞ 0 4 7 12 0 4
3 8 8 0 3 8 8 0 3 8 8 0
Algorithmus 6.57.
Floyd(real a[1..n, 1..n])
1 vertex i, j, k
2 for k ← 1 to n do
3 for i ← 1 to n do
4 for j ← 1 to n do
5 if a[i, k] + a[k, j] < a[i, j]
6 then a[i, j] ← a[i, k] + a[k, j]
Knoten auf einem kürzesten Pfad von i nach j. Wir gehen wieder iterativ
vor. Zur Initialisierung setzen wir P [i, j] = 0. In Pk [i, j], i, j = 1, . . . n, spei-
chern wir den größten Knoten eines kürzesten k–Pfades von i nach j. In
P [i, j] = Pn [i, j] ist der größte Knoten auf einem kürzesten Pfad von i nach
j gespeichert. Wir ersetzen die Zeile 6 in Floyd durch
Für i, j mit ad [i, j] ̸= ∞ gibt die Prozedur Path alle Knoten k eines
kürzesten Pfades von i nach j aus, die zwischen i und j liegen. Eine obere
Schranke für die Anzahl der Knoten in allen einfachen Pfaden ist n3 . Die hier
verwendete Methode zur Speicherung aller kürzesten Pfade benötigt nur eine
n × n–Matrix.
6.8 Flussnetzwerke
Wir studieren das Problem, einen maximalen Fluss in einem Flussnetzwerk
zu berechnen. Dazu behandeln wir den Algorithmus von Ford8 -Fulkerson9 in
der Variante von Edmonds10 -Karp11 . Der ursprüngliche Algorithmus wurde
in [FordFulk56] und die Optimierung in [EdmoKarp72] publiziert. Wir orien-
tieren uns mit unserer Darstellung an [CorLeiRivSte07]. Es gibt viele reale
Situationen, die sich mithilfe eines Flussnetzwerks modellieren lassen. Beispie-
le sind Netze zur Verteilung von elektrischer Energie oder das Rohrsystem
der Kanalisation einer Stadt. Beiden ist gemeinsam, dass die Kapazität der
Leitungen beschränkt ist und dass die Knoten über keine Speicher verfügen.
In einem Stromnetz besagt dies das erste Kirchhoffsche Gesetz12 . Zunächst
erläutern wir die Problemstellung mithilfe eines Beispiels genauer.
Beispiel. In der Figur 6.19 seien n1 , n2 , s1 , s2 Pumpstationen, s ist eine Ölsta-
tion, t eine Raffinerie und die Kanten sind Ölleitungen, die das Öl von der
Ölstation zur Raffinerie transportieren. Die Beschriftung der Kanten bezeich-
net die Kapazität der jeweiligen Leitung.
8
Lester Randolph Ford (1927 – 2017) war ein amerikanischer Mathematiker.
9
Delbert Ray Fulkerson (1924 – 1976) war ein amerikanischer Mathematiker.
10
Jack R. Edmonds (1934 – ) ist ein kanadischer Mathematiker und Informatiker.
11
Richard Manning Karp (1935 – ) ist ein amerikanischer Informatiker.
12
Gustav Robert Kirchhoff (1824–1887) war ein deutscher Physiker.
306 6. Gewichtete Graphen
300
n1 n2
s. t
s1 s2
540
Das zu lösende Problem lautet: Wie viel müssen wir über die einzelnen
Leitungen pumpen, damit der Transport von s nach t maximal wird?
300
n1 n2
s. t
s1 s2
540
Bemerkungen:
1. Für die Quelle s und Senke t fordern wir nicht, dass In(s) = ∅ oder
Out(t) = ∅ gilt. Wir verzichten auf die Flusserhaltung in s und t.
2. Da Knoten, die auf keinem Pfad von s nach t liegen, keinen Beitrag zum
totalen Fluss beitragen, nehmen wir an, dass jeder Knoten in N auf einem
Pfad von s nach t liegt. Insbesondere ist N zusammenhängend und t von
s aus erreichbar.
3. Ein Schnitt S definiert eine disjunkte Zerlegung der Knoten V = S ∪
(V \ S) in zwei echte Teilmengen. Der folgende Satz zeigt, dass der totale
Fluss über jeden Schnitt fließen muss.
Satz 6.60. Sei N = (V, E, s, t) ein Netzwerk mit Fluss f und S ein Schnitt
von N . Dann gilt für den totalen Fluss F bezüglich f :
∑ ∑
F = f (e) − f (e).
e∈Out(S) e∈In(S)
Beweis.
∑ ∑ ∑ ∑ ∑
F = f (e) − f (e) + f (e) − f (e)
v∈V \(S∪{t}) e∈In(v) e∈Out(v) e∈In(t) e∈Out(t)
∑ ∑ ∑ ∑
= f (e) − f (e)
v∈V \S e∈In(v) v∈V \S e∈Out(v)
∑ ∑ ∑ ∑
= f (e) − f (e) = f (e) − f (e).
e∈In(V \S) e∈Out(V \S) e∈Out(S) e∈In(S)
Wir erläutern die einzelnen Schritte der obigen Rechnung. In Zeile 2 tritt keine
Kante e = (x, y) mit x, y ∈ S auf. Für Kanten e = (x, y) ∈ E mit x, y ∈ / S
308 6. Gewichtete Graphen
gilt e ∈ Out(x) und e ∈ In(y). Die Flüsse längs dieser Kanten heben sich
infolgedessen auf und liefern keinen Beitrag bei der Summenbildung. Übrig
bleiben Kanten aus In(V \ S) und aus Out(V \ S). Sei e = (x, y) ∈ E mit
x ∈ S, y ∈/ S. Dann gilt e ∈ In(V \ S) ∩ In(y). Sei e = (x, y) ∈ E mit x ∈
/ S,
y ∈ S. Dann gilt e ∈ Out(V \ S) ∩ In(y). Die Behauptung ist damit gezeigt.
2
Corollar 6.61. Sei N = (V, E, s, t) ein Netzwerk mit Fluss f und totalem
Fluss F . Dann gilt
∑ ∑
F = f (e) − f (e).
e∈Out(s) e∈In(s)
Bemerkung. Falls F < 0 gilt, vertauschen wir s und t. Wir nehmen also stets
F ≥ 0 an.
Corollar 6.62. Sei N = (V, E, s, t) ein Netzwerk. f ein Fluss für N . F der
totale Fluss bezüglich f . S ein Schnitt, dann gilt:
F ≤ C(S).
Beweis. Es gilt
∑ ∑ ∑ ∑
F = f (e) − f (e) ≤ f (e) ≤ c(e) = C(S).
e∈Out(S) e∈In(S) e∈Out(S) e∈Out(S)
300;300
n1 n2
s. t
s1 s2
540;300
12,12 12
n1 n2 n1 n2
5
16,11 9,4 20,15 11 5 15 5
Definition 6.64. Sei N = (V, E, s, t) ein Netzwerk, f ein Fluss für N und
Gf der Restgraph bezüglich f . Sei v0 , v1 , . . . , vk ein (gerichteter) Pfad P in
Gf .
∆ := min{cf (vi , vi+1 ) | i = 0, . . . , k − 1}.
P heißt ein Pfad mit Zunahme ∆, falls ∆ > 0 gilt.
Satz 6.65. Sei P ein Pfad mit Zunahme ∆ von s nach t und e = (v, w) ∈ E.
e = (w, v). ∆e = min{c(e) − f (e), ∆}
g(e) := f (e) für e ∈
/ P,
g(e) := f (e) + ∆ für e ∈ P, e ∈ E, e ∈
/ E,
g : E −→ R≥0 , g(e) := f (e) − ∆ für e ∈ P, e ∈
/ E, e ∈ E,
g(e) := f (e) + ∆e ,
g(e) := f (e) − (∆ − ∆ ) für e ∈ P, e ∈ E, e ∈ E.
e
Es gilt: s ∈ S, t ∈
/ S. Für e ∈ Out(S) gilt f (e) = c(e) und für e ∈ In(S) gilt
f (e) = 0, denn sonst ließe sich ein Pfad um e über S hinaus verlängern. Also
folgt ∑ ∑ ∑
F = f (e) − f (e) = c(e) = C(S).
e∈Out(S) e∈In(S) e∈Out(S)
P = s, s1 , s2 , t mit ∆ = 5,
P = s, n1 , s2 , t mit ∆ = 7,
P = s, n1 , s2 , n2 , t mit ∆ = 3,
an. F = 60 ist maximaler totaler Fluss. S = {s, n1 } ist ein Schnitt minimaler
Kapazität, wie Figur 6.23 zeigt.
30;30
n1 n2
40;20,27,30 28;25,28
s. 10;10,3,0 5;0,3 t
30;25,30 32;20,25,32
5;5
s1 s2
54;30,35
Algorithmus 6.68.
real adm[1..n, 1..n], f low[1..n, 1..n]; vertex path[1..n]
312 6. Gewichtete Graphen
FordFulkerson()
1 vertex j, k; real delta, delta1
2 for k = 1 to n do
3 for j = 1 to n do
4 f low[k, j] ← 0
5 while delta = FindPath() ̸= 0 do
6 k ← n, j ← path[k]
7 while j ̸= 0 do
8 delta1 ← min(delta, adm[j, k] − f low[j, k])
9 f low[j, k] ← f low[j, k] + delta1
10 f low[k, j] ← f low[k, j] − (delta − delta1)
11 k ← j, j ← path[k]
Bemerkungen:
1. FindPath ermittelt einen Pfad P mit Zunahme von s (= 1) nach t (=
n), falls einer existiert (return value delta > 0) und speichert den Pfad
im Array path; path[k] speichert den Vorgänger von k im Pfad P . Die
while-Schleife in Zeile 7 durchläuft P von t aus und aktualisiert den Fluss
für die Kanten von P .
2. Wird die Kapazität c mit Werten in N vorausgesetzt, dann nimmt auch
die Restkapazität cf nur Werte in N an. Wenn wir für einen Fluss mit
Werten in N eine Flussvergrößerung mittels eines Pfades mit Zunahme
durchführen, hat der resultierende Fluss Werte in N. Für einen Pfad mit
Zunahme ∆ gilt ∆ ≥ 1. Da in jeder Iteration der while-Schleife in Zeile 5
der Fluss um mindestens eins erhöht wird, terminiert die while-Schleife
und damit der Algorithmus.
3. Ford und Fulkerson geben in [FordFulk62] ein (theoretisches) Beispiel
mit irrationalen Kapazitäten an, sodass für endlich viele Iterationen der
Konstruktionen eines Pfades mit Zunahme keine Terminierung erfolgt.
Die Konstruktion verwendet Potenzen von g1 , wobei g das Verhältnis
des goldenen Schnitts bezeichnet (Definition 1.22). Wird der Pfad mit
Zunahme nach der Methode von Edmonds-Karp gewählt, so terminiert
der Algorithmus stets (siehe unten).
4. Die Effizienz von FordFulkerson hängt wesentlich von der Wahl des Pfa-
des mit Zunahme ab.
c n c
s. 1 t
c m c
Wir bestimmen jetzt die Anzahl der Iterationen der while-Schleife in Zeile
5 von Algorithmus 6.68 bei Wahl eines Pfades mit Zunahme nach dem Vor-
schlag von Edmonds und Karp. Sei N = (V, E, s, t) ein Netzwerk mit Fluss
f und seien v, w ∈ V .
Da w ∈ U ist, folgt U ̸= ∅.
Sei y ∈ U mit δf ′ (s, y) ≤ δf ′ (s, u) für alle u ∈ U und sei P ′ ein kürzester
Pfad von s nach y in Gf ′ .
P ′ : s = v0 , . . . vl−1 , vl = y.
Setze x = vl−1 . Da δf ′ (s, x) = δf ′ (s, y) − 1, folgt nach der Wahl von y, dass
x∈/ U ist. Wir zeigen zunächst, dass (x, y) ∈ / Ef gilt. Angenommen, es wäre
(x, y) ∈ Ef , dann folgt
ein Widerspruch zu y ∈ U .
Es gilt somit (x, y) ∈
/ Ef . Wir zeigen jetzt, dass (y, x) ∈ Ef gilt. Dazu
betrachten wir zwei Fälle:
314 6. Gewichtete Graphen
ein Widerspruch zur Wahl von y ∈ U . Deshalb folgt U = ∅ und die Behaup-
tung des Lemmas. 2
Satz 6.70. Sei N = (V, E, s, t) ein Netzwerk, n = |V | und m = |E|. Für die
Anzahl T (n, m) der Iterationen der while-Schleife (Zeile 5) in Ford-Fulkerson
bei Verwendung von Edmonds-Karp gilt: T (n, m) = O(nm).
Beweis. Sei P ein Pfad mit Zunahme in Gf mit Zunahme ∆. Eine Kante e
von P heißt minimal bezüglich P , wenn cf (e) = ∆ gilt. Sei f ′ der Fluss, der
durch Addition von ∆ aus f entsteht. Die minimalen Kanten von P treten
in Ef ′ nicht mehr auf.
Wir schätzen ab, wie oft eine Kante e ∈ E minimal werden kann. Sei
e = (u, v) minimal für eine Iteration von Ford-Fulkerson (Konstruktion von f ′
aus f ). Da e Kante eines kürzesten Weges in Gf ist, gilt δf (s, v) = δf (s, u)+1.
Bevor (u, v) wieder Kante eines Pfades mit Zunahme werden kann, muss
(v, u) Kante eines kürzesten Pfades mit Zunahme sein, d. h. (v, u) ∈ Ef˜ mit
einem später berechneten Fluss f˜. Dann folgt mit Lemma 6.69
Falls die Kante e bis zur Berechnung von f r mal minimale Kante war, gilt
2r ≤ δf (s, u)
Es gilt δf (s, u) ≤ n − 2, denn auf einem kürzesten Weg von s nach u können
höchstens n − 1 viele Knoten liegen (u ̸= t).
Es folgt
n−2
r≤,
2
2 mal minimale Kante sein. Aus |Ef | ≤
d. h. eine Kante kann höchstens n−2
2|E| = 2m folgt, dass die Anzahl der minimalen Kanten ≤ (n − 2)m ist.
Da in jeder Iteration von Ford-Fulkerson mindestens eine minimale Kante
verschwindet, ist die Behauptung gezeigt. 2
Übungen 315
Übungen.
1. Rechnen Sie folgende Formeln für die Ackermann-Funktion nach:
a. A(1, n) = n + 2.
b. A(2, n) = 2n + 3.
c. A(3, n) = 2n+3 − 3.
2
.
..
2
d. A(4, n) = 2| 2{z } −3.
n+3 mal
1 : (2, 1), (3, 1), (4, 4), (5, 2) 4 : (1, 4), (3, 2), (5, 5)
2 : (1, 1), (3, 2), (5, −2) 5 : (1, 2), (2, −2), (4, 5)
3 : (1, 1), (2, 2), (4, 2)
Die Länge eines Pfades in G ist die Summe der Gewichte der Kanten, die
zum Pfad gehören. Der Abstand von zwei Knoten i, j ist das Minimum
der Menge der Längen von Pfaden von i nach j.
a. Wird durch die obige Definition eine Metrik auf der Menge der Kno-
ten von G erklärt? Wenn dies nicht der Fall ist, dann geben Sie alle
Axiome einer Metrik an, die verletzt sind.
b. Liefert der Algorithmus von Dijkstra einen kürzesten Pfad von Kno-
ten 4 nach Knoten 2? Stellen Sie die Einzelschritte bei der Ermittlung
des Pfades dar.
c. Liefert der Algorithmus von Kruskal einen minimalen aufspannenden
Baum von G. Wenn dies der Fall ist, gilt dies für alle Graphen mit
Kanten negativen Gewichts. Begründen Sie Ihre Aussage.
3. Sei G ein gerichteter gewichteter azyklischer Graph. Entwickeln Sie einen
Algorithmus, welcher einen längsten Pfad zwischen zwei Knoten von G
ermittelt (kritischer Pfad).
4. Entwerfen Sie einen Algorithmus, der für einen azyklischen gerichteten
Graphen G die Abstände von einem Knoten zu allen anderen Knoten mit
der Laufzeit O(n + m) berechnet.
5. Beim Algorithmus von Kruskal hängt der konstruierte MST von der Aus-
wahl einer Kante unter allen Kanten gleichen Gewichtes ab. Ist es möglich,
durch geeignete Auswahl einer Kante in jedem Schritt, jeden MST eines
Graphen zu erzeugen? Begründen Sie Ihre Antwort.
6. a. Wie sind die Prioritäten zu vergeben, damit eine Priority-Queue, wie
ein Stack oder eine Queue arbeitet.
b. Der Datentyp Priority-Queue soll neben den angegebenen Zugriffs-
funktionen die Vereinigung unterstützen. Geben Sie einen Algorith-
mus an und diskutieren Sie die Laufzeit.
7. Entwickeln Sie ein Verfahren für den Update“ eines MST für einen Gra-
”
phen G, falls gilt:
316 6. Gewichtete Graphen
Ein Eintrag ist gegeben durch (Knoten, Kapazität, Fluss). Ermitteln Sie
einen maximalen Fluss und einen Schnitt minimaler Kapazität.
16. Wir betrachten ein System bestehend aus zwei Prozessoren P, Q und n
Prozessen. Zwischen je zwei Prozessen findet Kommunikation statt. Ein
Teil der Prozesse ist fest P und ein Teil fest Q zugeordnet. Der Rest der
Prozesse soll so auf die Prozessoren verteilt werden, dass der Aufwand
für die Informationsübertragung von P nach Q minimiert wird.
a. Wie ist das Problem zu modellieren?
b. Mit welchem Algorithmus kann eine Verteilung der Prozesse ermittelt
werden?
14
Das 3-SAT-Problem ist NP-vollständig ([HopMotUll07]). Es besteht darin zu
entscheiden, ob ein boolscher Ausdruck b = b1 ∧ . . . ∧ bn mit bj = bj 1 ∨ bj 2 ∨ bj 3 ,
j = 1, . . . , n, und bj 1 , bj 2 , bj 3 ∈ L erfüllbar ist.
318 6. Gewichtete Graphen
8 8 8
S.1 N1 N2 T1
16
10
8 24 20 8
10 14
S2 M1 M2 T2
10
10
8 12 20 12 8
12
S3 L1 L2 T3
8 18 8
1 : 6, 7, 8 4 : 8, 9, 10 7 : 1, 3 10 : 2, 4
2 : 6, 9, 10 5:6 8 : 1, 4 .
3 : 6, 7 6 : 1, 2, 5, 3 9 : 2, 4
.
19. Sei G = (V1 ∪ V2 , E) ein bipartiter Graph, N = (V, E, s, t) das zugeord-
nete Netzwerk, Z eine Zuordnung in G, f der zugeordnete lokale Fluss
in N und P = s, v1 , . . . , vn , t ein Pfad mit Zunahme. Zeigen Sie:
a. P besitzt eine ungerade Anzahl von Kanten.
b. Sei ei = (vi , vi+1 ), i = 1, . . . , n − 1. Dann gilt:
e2i−1 ∈ Z, i = 1, . . . , n2 , und e2i ∈/ Z, i = 1, . . . , n−2
2 .
c. Beim Algorithmus zur Konstruktion einer maximalen Zuordnung
nimmt die Anzahl der Kanten der Zuordnung in jedem Schritt um
eins zu.
d. Bestimmen Sie die Ordnung der Anzahl der Iterationen von Ford-
Fulkerson zur Berechnung einer maximalen Zuordnung.
A. Wahrscheinlichkeitsrechnung
Wir stellen einige grundlegende Notationen und Ergebnisse aus der Wahr-
scheinlichkeitsrechnung zusammen. Diese wenden wir bei der Analyse der Al-
gorithmen an. Einführende Lehrbücher in die Wahrscheinlichkeitsrechnung
sind [Bosch06] und [Feller68].
Beispiel. Ein Standardbeispiel ist das Werfen mit einem Würfel. Ergebnis
des Experiments ist die Augenzahl, die auf der Seite des Würfels zu sehen
ist, die nach dem Werfen oben liegt. Die Ergebnismenge X = {1, . . . , 6}
besteht
(1 aus
) der Menge der Augenzahlen. Für einen fairen Würfel ist p =
6 . Ein Ereignis ist eine Teilmenge von {1, . . . , 6}. So ist zum Beispiel
1
6 , . . . ,
die Wahrscheinlichkeit für das Ereignis gerade Augenzahl“ gleich 12 .
”
Bemerkung. Für unsere Anwendungen ist das in Definition A.1 festgelegte
Modell für Zufallsexperimente ausreichend. Kolmogorow1 hat ein allgemeines
Modell definiert, das heute in der Wahrscheinlichkeitstheorie üblich ist.
Die Menge X der Elementarereignisse ist nicht notwendig endlich und
die Menge der Ereignisse ist eine Teilmenge A der Potenzmenge von X , eine
sogenannte σ–Algebra. Ein Wahrscheinlichkeitsmaß p ordnet jedem A ∈ A
eine Wahrscheinlichkeit p(A) im reellen Intervall [0, 1] zu. p ist additiv, d. h.
p(A∪B) = p(A)+p(B), falls A∩B = ∅, und es gilt p(X ) = 1. Die Eigenschaft
additiv wird sogar für abzählbare disjunkte Vereinigungen gefordert. Unser
Modell ist ein Spezialfall des allgemeinen Modells.
Definition A.2. Sei X ein Wahrscheinlichkeitsraum und A, B ⊆ X Ereignis-
se mit p(B) > 0. Die bedingte Wahrscheinlichkeit von A unter der Annahme
B ist
p(A ∩ B)
p(A|B) := .
p(B)
Insbesondere gilt {
p(x)/p(B) if x ∈ B,
p(x |B) =
0 if x ̸∈ B.
Die Standardabweichung ist das Maß für die Streuung einer Zufallsvaria-
blen.
2
Eigentlich ist man am Erwartungswert E(|X − E(X)|) interessiert. Da Absolut-
beträge schwer zu behandeln sind, wird Var(X) durch E((X − E(X))2 ) definiert.
322 A. Wahrscheinlichkeitsrechnung
2
Definition A.8. Sei X eine Zufallsvariable und E ein Ereignis im zugehören-
den Wahrscheinlichkeitsraum. Die Verteilung der Zufallsvariablen X |E ist
durch die bedingten Wahrscheinlichkeiten p(X = x |E) definiert.
Lemma A.9. Seien X und Y endliche Zufallsvariablen. Die Wertemenge
von Y sei {y1 , . . . , ym }. Dann gilt
∑
m
E(X) = E(X | Y = yi )p(Y = yi ).
i=1
∑
n
E(X) = p(X = xj )xj
j=1
(m )
∑
n ∑
= p(X = xj | Y = yi )p(Y = yi ) xj
j=1 i=1
∑
m ∑n
= p(X = xj | Y = yi )xj p(Y = yi )
i=1 j=1
∑
m
= E(X | Y = yi )p(Y = yi ).
i=1
Wir studieren in diesem Abschnitt Beispiele von Zufallsvariablen, die wir bei
der Analyse von Algorithmen anwenden. Zunächst erweitern wir unser Modell
für Zufallsexperimente. Wir betrachten die etwas allgemeinere Situation einer
diskreten Zufallsvariablen. Die Wertemenge von X besteht aus den nicht ne-
gativen ganzen Zahlen 0, 1, 2 . . .. Für eine Zufallsvariable X mit Wertemenge
W ({0, 1, 2 . . .} setzen wir p(X = m) = 0 für m ∈ / W, also sind auch endliche
Zufallsvariable eingeschlossen. Wir fordern, dass
∞
∑
p(X = i) = 1
i=0
Sätze aus dem Abschnitt A.1 sind auf die allgemeinere Situation übertragbar.
Wenn die Reihe
∞
∑
E(X) = i · p(X = i)
i=0
∑∞ 1
Beispiel. Die
( 1 )Reihe i=1 2i konvergiert gegen 1 (Anhang B (F.8)). Folglich
ist durch 2i i≥1 die Verteilung einer diskreten Zufallsvariablen X definiert.
∑∞
Die Reihe i=1 i · 21i konvergiert gegen 2, also besitzt X den Erwartungswert
2 (loc. cit.).
Definition A.11. Sei X eine Zufallsvariable, die nur nicht negative ganze
Zahlen als Werte annimmt. Die Potenzreihe
∞
∑
GX (z) = p(X = i)z i
i=0
∞
∑
GX (z) = p(X = i)z i impliziert p(X = 0) = GX (0).
i=0
∞
∑
G′X (z) = ip(X = i)z i−1 impliziert p(X = 1) = G′X (0).
i=1
∞
∑
G′′X (z) = i(i − 1)p(X = i)z i−2
i=2
1 ′′
impliziert p(X = 2) = G (0).
2 X
..
.
3
Die Zufallsvariable z X nimmt den Wert z i mit der Wahrscheinlichkeit p(X = i)
an. Somit gilt GX (z) = E(z X ).
A.2 Spezielle diskrete Verteilungen 325
∞
∑
(k)
GX (z) = i(i − 1) . . . (i − k + 1)p(X = i)z i−k
i=k
1 (k)
impliziert p(X = k) = G (0).
k! X
Oft kennt man neben der Potenzreihendarstellung eine weitere Darstel-
lung der erzeugenden Funktion, zum Beispiel als rationale Funktion (Beweis
von Satz A.16, A.20 und A.22). Aus dieser Darstellung können wir dann For-
meln für die Ableitungen und die Koeffizienten der Potenzreihe durch Diffe-
renzieren dieser Darstellung von GX (z) gewinnen. Die Erzeugendenfunktion
GX (z) enthält die komplette Verteilung von X implizit. Aus einer explizi-
ten Darstellung von GX (z), zum Beispiel als rationale Funktion, kann eine
Formel für den Erwartungswert und die Varianz von X abgeleitet werden.
Satz A.12. Sei X eine Zufallsvariable mit der erzeugenden Funktion GX (z).
Für GX (z) sollen die erste und zweite linksseitige Ableitung an der Stelle
z = 1 existieren. Dann gilt
Beweis. Die Formeln folgen unmittelbar aus der Betrachtung von oben mit
der Aussage 2 von Satz A.7, der auch für diskrete Zufallsvariable gilt. 2
Wir wenden jetzt Satz A.12 auf die Bernoulli-Verteilung, die Binomialver-
teilung, die negative Binomialverteilung, die Poisson-Verteilung, die geome-
trische Verteilung, die hypergeometrische Verteilung und die negativ hyper-
geometrisch verteilte Zufallsvariable an.
Definition A.13. Eine Zufallsvariable X heißt Bernoulli 4 -verteilt mit Para-
meter p, 0 < p < 1, wenn X die Werte 0 und 1 annimmt und
p(X = i) = pi (1 − p)1−i
gilt.
Satz A.14. Sei X eine Bernoulli-verteilte Zufallsvariable mit Parameter p.
Dann gilt:
E(X) = p, E(X 2 ) = p und Var(X) = p(1 − p).
Beweis.
GX (z) = pz + (1 − p), G′X (z) = p und G′′X (z) = 0.
Die Aussagen 1 und 2 folgen unmittelbar.
2
4
Jakob Bernoulli (1655 – 1705) war ein schweizer Mathematiker und Physiker.
326 A. Wahrscheinlichkeitsrechnung
Beispiel. Figur A.1 zeigt die Verteilung der Anzahl E der Einsen in einer
Zufalls 0-1-Folge, die wir durch eine faire Münze erzeugen (Abschnitt 1.6.4).
E ist binomialverteilt mit Parameter n = 50 und p = 1/2. Der Erwartungswert
ist 25 und die Standardabweichung ist 3.54.
Sei E ein Ereignis eines Zufalls Experiments, das mit der Wahrscheinlich-
keit p(E) = p > 0 eintritt. Wir betrachten n unabhängige Wiederholungen
des Experiments. Ein derartiges Experiment bezeichnen wir als Bernoulli -
Experiment mit Ereignis E und Erfolgswahrscheinlichkeit p.
Sei X die Zufallsvariable, welche zählt, wie oft das Ereignis E eintritt. Das
Ereignis tritt bei n unabhängigen Wiederholungen an i vorgegebenen Positio-
nen der Folge (bei den n vorgegebenen Wiederholungen) ( n ) mit der Wahrschein-
lichkeit p (1 − p)
i n−i
auf. Aus n Positionen gibt es ( i ) viele Möglichkeiten
i Position auszuwählen. Wir erhalten p(X = i) = ni pi (1 − p)n−i . X ist
binomialverteilt mit Parameter (n, p).
Satz A.16. Sei X eine binomialverteilte Zufallsvariable mit Parameter (n, p).
Dann gilt:
Beweis. Aus dem binomischen Lehrsatz (Anhang B (F.3)) folgt durch die
Entwicklung von (pz + (1 − p))n die Erzeugendenfunktion für die Binomial-
verteilung.
∑n (n)
5
i=0 i
pi (1 − p)n−i = (p + (1 − p))n = 1 (Anhang B (F.3)).
A.2 Spezielle diskrete Verteilungen 327
n ( )
∑ n
GX (z) = pi (1 − p)n−i z i = (pz + (1 − p))n ,
i=0
i
G′X (z) = np(pz + (1 − p))n−1 und
G′′X (z) = n(n − 1)p2 (pz + (1 − p))n−2 .
2
Beispiel. Figur A.2 zeigt die Verteilung der Anzahl N der Schlüssel, die durch
eine Zufalls-Hashfunktion für eine Hashtabelle mit 60 Plätzen und 50 Da-
tensätzen auf einen Hashwert abgebildet werden. Die Variable N ist binomi-
alverteilt mit den Parametern n = 50 und p = 1/60 (Satz 3.15).
Wir erwarten, dass auf einen Hashwert E(N ) = 5/6 Schlüssel abgebildet
werden. Die Varianz Var(N ) = 250/36 ≈ 6.9 und die Standardabweichung
σ(N ) ≈ 2.6.
λi −λ
p(X = i) = e
i!
∑∞ λi −λ
∑∞ λi
gilt (beachte i=0 i! e = e−λ i=0 i! = e−λ eλ = 1).
Satz A.18. Sei X eine Poisson-verteilte Zufallsvariable mit Parameter λ.
Dann gilt:
E(X) = λ, E(X 2 ) = λ2 und Var(X) = λ.
Beweis.
∞
∑ ∞
∑
λi (λ · z)i
GX (z) = e−λ z i = e−λ
i=0
i! i=0
i!
−λ λz
=e e ,
G′X (z) = λe −λ λz
e und G′′X (z) = λ2 e−λ eλz .
2
Definition A.19. Eine Zufallsvariable X heißt geometrisch verteilt mit Pa-
rameter p, 0 < p < 1, wenn X die Werte 1, 2, . . . annimmt und
gilt.7 Der Name erklärt sich aus der Tatsache, dass die Erzeugendenfunktion
von X eine geometrische Reihe ist (siehe unten).
Wir betrachten ein Bernoulli-Experiment mit Ereignis E und Erfolgswahr-
scheinlichkeit p. Sei X die Zufallsvariable, welche die notwendigen Versuche
zählt, bis zum ersten Mal E eintritt. E tritt bei der i–ten Wiederholung zum
ersten Mal ein, falls E bei der i–ten Wiederholung eintritt, bei den i − 1
vielen vorangehenden Wiederholungen jedoch nicht. Die Wahrscheinlichkeit
dafür ist p(1 − p)i−1 , d. h. X ist geometrisch verteilt mit Parameter p.
6
Siméon Denis Poisson (1781 – 1840) war ein französischer Mathematiker und
Physiker.
∑∞ ∑
7
i=1 p(1 − p)
i−1
=p ∞i=0 (1 − p) = 1 (Anhang B (F.8)).
i
A.2 Spezielle diskrete Verteilungen 329
r r2 r r r(1 − p)
E(X) = , E(X 2 ) = 2 + 2 − und Var(X) = .
p p p p p2
Beweis. Aus der Formel für die binomische Reihe (Anhang B (F.4)) folgt mit
Lemma B.18, q = 1 − p und r + i = k
A.2 Spezielle diskrete Verteilungen 331
∞ (
∑ ) ∞
∑ ( )
−r −r
(1 − qz)−r = (−qz)i = (−1)i qi zi
i=0
i i=0
i
∑∞ ( )∑∞ ( )
r+i−1 i i r+i−1
= qz = qi zi
i=0
i i=0
r − 1
∑∞ ( )
k−1
= q k−r z k−r .
r−1
k=r
Dann gilt
( )r−1 ( )
pz p(1 − qz) + pqz (pz)r
G′X (z) =r = rp und
1 − qz (1 − qz)2 (1 − qz)r+1
( )
(pz)r−2
G′′X (z) = rp2 (r − 1 + 2qz).
(1 − qz)r+2
Es folgt
r
E(X) = G′X (1) = ,
p
r r r2 r r
E(X 2 ) = G′′X (1) + G′X (1) =
2
(r + 1 − 2p) + = 2
+ 2− ,
p p p p p
( )2
r 2
r r r r(1 − p)
Var(X) = E(X 2 ) − E(X)2 = 2 + 2 − − = .
p p p p p2
2
gilt.
Die hypergeometrische Verteilung beschreibt ein Urnen-Experiment Zie-
”
hen ohne Zurücklegen“. Dabei enthält eine Urne N Kugeln. Von diesen N
Kugeln weisen M Kugeln ein bestimmtes Merkmal M auf. Die Wahrschein-
lichkeit, dass nach n Ziehungen (ohne Zurücklegen) k der gezogenen Kugeln
das Merkmal M besitzen ergibt sich aus der Anzahl der günstigen Fälle
332 A. Wahrscheinlichkeitsrechnung
( )( ) ( )
M N −M N
dividiert durch die Anzahl der möglichen Fälle . Es gilt
k n−k
∑ n
die Normierungsbedingung k p(X = k) = 1, d. h.
∑ (M ) (N − M ) (N )
=
k n−k n
k
(Lemma B.18).
Beispiel. Figur A.5 zeigt die Verteilung der Anzahl der Umstellungen U bei
der Quicksort-Zerlegung für ein Array mit 100 Elementen und mit Pivotele-
ment an der Position 60. Die Variable U ist hypergeometrisch verteilt mit
den Parametern n = 59, M = 40 und N = 99 (Abschnitt 2.1.1).
Beweis.
( )( )
M N −M
∑ k n−k
GX (z) = ( ) zk .
N
k n
( )( )
M M −1 N −M
∑ k k−1 n−k
G′X (z) = k ( ) z k−1
N N −1
k n n−1
( )( )
M −1 N −M
M∑ k−1 n−k
=n ( ) z k−1 .
N N −1
k n−1
A.2 Spezielle diskrete Verteilungen 333
( )( )
M −1 M −1 N −M
M ∑ k−1 k−1 n−k
G′′X (z) = n (k − 1) ( ) z k−2
N N −1 N −2
k n−1 n−2
( )( )
M −2 N −M
M M −1 ∑ k−2 n−k
= n(n − 1) ( ) z k−2 .
N N −1 N −2
k n−2
Aus
M M M −1
G′X (1) = n
und G′′X (1) = n(n − 1)
N N N −1
folgen die Formeln für Erwartungswert und Varianz von X. Bei den Rech-
nungen mit den Binomial-Koeffizienten wurde Lemma B.18 angewendet. 2
Bemerkung. Wir betrachten den Grenzwert für N → ∞ und p = M/N kon-
stant.
( M ) ( N −M ) ( )
/ N
k n−k
n
M! (N − M )! (N − n)!n!
= · ·
k!(M − k)! (n − k)!(N − M − n + k)! N!
(n) M M −k+1 N −M N − M − (n − k − 1)
= ... · ... .
k N N −k+1 N −k N − k − (n − k − 1)
Jeder der ersten k Brüche konvergieren für N → ∞ gegen p und
( jeder
) der letz-
(M ) N −M ( )
ten n−k Brüche gegen 1−p. Es folgt, dass der Quotient k n−k /
N für
(n) k n
N → ∞, wobei wir M/N konstant halten, gegen k p (1 − p)n−k konvergiert.
Die Binomialverteilung beschreibt die unabhängige Wiederholung des Urnen-
Experiments Ziehen mit Zurücklegen“. Für große N hat das Zurücklegen
”
nur sehr geringe Auswirkungen auf die Wahrscheinlichkeiten.
Definition A.25. Eine Zufallsvariable X heißt negativ hypergeometrisch ver-
teilt mit den Parametern (r, M, N ), r, M, N ∈ N, 0 < r ≤ M ≤ N , wenn X
die Werte r, . . . , r + N − M annimmt und
( )( )
k−1 N −k
r−1 M −r
p(X = k) = ( )
N
M
gilt.
Die negative hypergeometrische Verteilung beschreibt ein Urnen-Experi-
ment Ziehen ohne Zurücklegen“. Die Urne enthält N Kugeln. Von diesen
”
weisen M Kugeln ein bestimmtes Merkmal M auf. Sei r ≤ M . Die Zufallsva-
riable X zählt, wie oft wir das Experiment ausführen müssen, damit genau r
gezogene Kugeln das Merkmal M besitzen. Es gilt X = k, wenn bei der k–ten
Ziehung die r–te Kugel mit dem Merkmal M gezogen wird und wenn in den
334 A. Wahrscheinlichkeitsrechnung
∑
r+N −M ( )( )
r k N −k
= ( ) z k−1
N r M −r
M k=r
−M +1 (
r+N∑ )( )
r k−1 N − (k − 1)
= ( ) z k−2 .
N r M −r
M k=r+1
∑
m+N −M +1 ( )( ))
k−1 N +1−k
−2 k
m M + 1 − (m + 1)
k=m+1
( ∑
m+N −M +2 ( ) )(
m N +2−k k−1
= ( ) (m + 1)
N M + 2 − (m + 2)
m+1
M k=m+2
( ))
N +1
−2
M +1
( ( ) ( ))
m N +2 N +1
= ( ) (m + 1) −2
N M +2 M +1
M
( )
N +1 N +2
=m (m + 1) −2 .
M +1 M +2
Beispiel. Figur A.6 zeigt die Verteilung der Anzahl sln der Versuche, die
notwendig sind, um in eine Hashtabelle die n Schlüssel enthält, den (n + 1)–
ten Schlüssel einzufügen für n = 80, falls die Hashtabelle 100 Plätze besitzt.
Die Variable sln ist negativ hypergeometrisch verteilt mit den Parametern
r = 1, M = 20 und N = 100 (Satz 3.22).
336 A. Wahrscheinlichkeitsrechnung
Bei der Analyse der Algorithmen wenden wir eine Reihe von elementaren
Formeln, wie zum Beispiel Formeln für die geometrische Reihe, die Binomial-
Koeffizienten und die Exponentialfunktion öfter an. Im folgenden Abschnitt
stellen wir einige nützliche Formeln und mathematische Begriffe zusammen.
z = q 1 · a + r1 = q 2 · a + r 2
Bemerkung. Die Zahl q ist der ganzzahlige Quotient und r = z mod a ist der
Rest bei der Division von z durch a.
Wir können die natürlichen Zahlen in einem Stellensystem bezüglich jeder
Basis b ∈ N, b > 1, darstellen.
Satz B.2 (b–adische Entwicklung natürlicher Zahlen). Sei b ∈ N, b > 1.
Dann ist jede natürliche Zahl z ∈ N0 auf genau eine Weise darstellbar durch
∑
n−1
z= zi bi , zi ∈ {0, . . . , b − 1}.
i=0
Beweis. Die Existenz der Darstellung folgt durch fortgesetzte Division mit
Rest.
z = q1 b + r0 , q1 = q2 b + r1 , q2 = q3 b + r2 , . . . , qn−1 = 0 · b + rn−1 .
Setze z0 := r0 , z1 :=∑r1 , . . . , zn−1 := rn−1 . Dann gilt 0 ≤ zi < b, i =
n−1
0, . . . , n − 1, und z = i=0 zi bi .
Um die Eindeutigkeit zu zeigen, nehmen wir an, dass z zwei Darstellungen
besitzt. Dann ist die Differenz der Darstellungen eine Darstellung der 0 mit
Koeffizienten |zi | ≤ b − 1. Es genügt somit zu zeigen, dass die Darstellung
der 0 eindeutig ist. Sei
∑
n−1
z= zi bi , |zi | ≤ b − 1,
i=0
Falls z mit n Stellen dargestellt ist, dann gilt bn−1 ≤ z < bn . Hieraus folgt
n − 1 ≤ logb (z) < n, d. h. n = ⌊logb (z)⌋ + 1. 2
Harmonische Zahlen. Harmonische Zahlen treten bei der Analyse von Al-
gorithmen sehr oft auf. Es hat sich die folgende Notation etabliert.
Definition B.4. Die Zahl
∑
n
1
Hn :=
i=1
i
heißt n–te harmonische Zahl .
∑∞
Die harmonische Reihe i=1 1i divergiert. Sie divergiert jedoch sehr lang-
sam. Die folgende Abschätzung beschreibt das Wachstum der harmonischen
Zahlen genauer.
Lemma B.5. Es gilt die Abschätzung ln(n + 1) ≤ Hn ≤ ln(n) + 1.
B. Mathematische Begriffe und nützliche Formeln 339
Beweis. Aus
∑
n ∫ n+1 ∑n ∫ n
1 1 1 1
≥ dx = ln(n + 1) und ≤ dx = ln(n)
i=1
i 1 x i=2
i 1 x
Beweis.
∑
n
1 1
Hi = n + (n − 1) + . . . + (n − (n − 1)) =
i=1
2 n
∑
n
1
(n − (i − 1)) = (n + 1)Hn − n.
i=1
i
2
Der Restklassenring Zn . Neben dem Ring Z der ganzen Zahlen ist der
Ring Zn der Restklassen modulo n von großer Bedeutung.
Definition B.7.
1. Sei n ∈ N, n ≥ 2. Wir definieren auf Z eine Äquivalenzrelation: a, b ∈ Z
heißen kongruent modulo n, in Zeichen
a ≡ b mod n,
wenn gilt, n teilt a − b, d. h. a und b haben bei Division mit Rest durch
n denselben Rest.
2. Sei a ∈ Z. Die Äquivalenzklasse [a] := {x ∈ Z | x ≡ a mod n} heißt
Restklasse von a. a ist ein Repräsentant für [a].
3. Zn := {[a] | a ∈ Z} heißt Menge der Restklassen.
1
Im Gegensatz zur Eulerschen Zahl e, einer transzendenten Zahl, ist über γ nicht
einmal bekannt, ob es sich um eine rationale Zahl handelt. Das Buch [Havil07]
handelt von dieser Frage.
340 B. Mathematische Begriffe und nützliche Formeln
Bemerkungen:
1. Für jedes x ∈ [a] gilt [x] = [a].
2. Man rechnet leicht nach, dass durch die Definition unter Punkt 1 tatsäch-
lich eine Äquivalenzrelation gegeben ist. Da bei der Division mit Rest
durch n die Reste 0, . . . , n − 1 auftreten, gibt es in Zn n Restklassen,
Zn = {[0], . . . , [n − 1]}.
Die Zahlen 0, . . . , n − 1 heißen natürliche Repräsentanten.
Definition B.8. Wir führen auf Zn Addition und Multiplikation ein:
[a] + [b] = [a + b], [a] · [b] = [a · b].
Die Unabhängigkeit von der Wahl der Repräsentanten a und b ergibt eine
einfache Rechnung.
Die Ring-Axiome in Z vererben sich auf Zn . Zn wird zu einem kommuta-
tiven Ring mit dem Einselement [1]. Er heißt Restklassenring von Z modulo n.
Wir können jetzt alle Zahlen angeben, die bei quadratischem Sondieren
als Modulus geeignet sind.
Corollar B.15. Für p = 4k + 3 gilt
Zp = {±[i2 ] | i = 0, . . . , (p − 1)/2}.
∑
n ∑
n
n(n + 1)
1 = n, i= ,
i=1 i=1
2
∑
n ∑
n
2i = n(n + 1), (2i − 1) = n2 ,
i=1
∑
n
i=1
∑
n ( )2
n(n + 1)(2n + 1) n(n + 1)
i2 = , 3
i = .
i=1
6 i=1
2
Mit diesen Formeln können wir unmittelbar Formeln für endliche Summen
von Polynom-Werten für Polynome bis zum Grad 3 ableiten.
r(n)
f (n) = s(n) + mit Polynomen r(n) und s(n) und deg(r) < deg(q).
q(n)
r(n) ∑ ak
l
(F.2) = .
q(n) n − nk
k=1
1 a b
= +
i(i − 1) i i−1
4
Der Beweis dieser Aussage kann mithilfe des Legendre Symbols aus der elemen-
taren Zahlentheorie erfolgen (siehe zum Beispiel [DelfsKnebl15, Seite 422]).
B. Mathematische Begriffe und nützliche Formeln 343
ansetzen. Multiplizieren wir auf beiden Seiten mit i(i − 1), so folgt
1 = a(i − 1) + bi = −a + (a + b)i.
∑
n ∑ n ( ) ∑
n ∑
n−1
1 −1 1 1 1 1
= + =− + =1− .
i=2
i(i − 1) i=2
i i−1 i=2
i i=1
i n
∑
n+1 ( )
⌊log2 (r)⌋ = (n + 1)⌊log2 (n)⌋ − 2 2⌊log2 (n)⌋ − 1 + ⌊log2 (n + 1)⌋
r=1
( )
= (n + 2)⌊log2 (n + 1)⌋ − 2 2⌊log2 (n+1)⌋ − 1 .
344 B. Mathematische Begriffe und nützliche Formeln
Die binomische Reihe ergibt sich als Taylorreihe5 der allgemeinen Potenz-
funktion x 7→ xα mit Entwicklungspunkt 1 ([AmannEscher02, Kap. V.3]).
Die binomische Reihe wurde von Newton6 entdeckt.
Die binomische Reihe bricht für α ∈ N ab. Die Formel ergibt sich dann
aus dem binomischen
( ) Lehrsatz. Ersetzt man x durch −x so erhält man für
α = −1 wegen −1 k = (−1) k
die geometrische Reihe (Anhang B (F.8)).
5
Brook Taylor (1685 – 1731) war ein englischer Mathematiker.
6
Isaac Newton (1642 - 1726) war ein englischer Universalgelehrter. Er ist Mitbe-
gründer der Infinitesimalrechnung und ist berühmt für sein Gravitationsgesetz.
B. Mathematische Begriffe und nützliche Formeln 345
∑
m (r)(s) ( )
r+s−1
k =s .
k k r−1
k=1
Beweis.
1. Die Behauptung folgt unmittelbar aus der Definition der Binomial-
Koeffizienten.
346 B. Mathematische Begriffe und nützliche Formeln
Hieraus folgt
∑(r)( s
) (
r+s
)
= .
k n−k n
k
5. Mit 4 folgt
∑(r)( s
) ∑(r)( s
) (
r+s
) (
r+s
)
= = = .
k n+k k s−n−k s−n r+n
k k
7.
( )
−n −n(−n − 1) · . . . · (−n − k + 1)
=
k k!
n(n + 1) · . . . · (n + k − 1)
= (−1)k
( ) k!
n+k−1
= .
k
8. Wir zeigen die Formel durch Induktion nach n. Für n = 0 ist die Formel
richtig. Der Schluss von n auf n + 1 erfolgt durch
∑( k ) ∑
n+1 n (
k
) (
n+1
) (
n+1
) (
n+1
) (
n+2
)
= + = + =
m m m m+1 m m+1
k=0 k=0
Die geometrische Reihe. Wir geben für x ̸= 1 Formeln für die n–te Par-
tialsumme der geometrische Reihe und deren Ableitungen an.
∑
n
xn+1 − 1
(F.5) xi = .
i=0
x−1
gilt.
Die Exponentialfunktion. Die Exponentialfunktion wird üblicherweise
durch die auf ganz R konvergente Potenzreihe
∑∞
x xn
e :=
n=0
n!
definiert. Wir können sie aber auch als Grenzwert einer monoton wachsenden
Folge darstellen.
Satz B.19. Für alle x ∈ R konvergiert die Folge
(( x )n )
1+
n n∈N
(( )n ) ( )
Beweis. Wir berechnen ln 1 + nx = n ln 1 + nx . Es gilt
( )
ln 1 + nx
lim x = ln′ (1) = 1.
n→∞
n
ln(
( )
)x
1+ n ∆y
Die Folge x = ∆x ist die Folge der Steigungen der Sekanten für ∆x =
n
n → 0. Sie konvergiert für x > 0 streng monoton wachsend und für x <
x
0 streng
( monoton
) fallend gegen die Steigung der Tangente. Hieraus folgt
n ln 1 + nx ist streng monoton wachsend und
( x) (( x )n )
lim n ln 1 + = lim ln 1 + = x.
n→∞ n n→∞ n
( )n
Deshalb ist auch 1 + nx streng monoton wachsend und
( x )n
lim 1 + = ex .
n→∞ n
Dies zeigt die Behauptung. 2
∑
n ∑
n
ai f (xi ) ≤ ai f (x0 ) + ai λ(x0 )(xi − x0 )
i=1 i=1
( )
∑
n ∑
n ∑
n
= f (x0 ) ai + λ(x0 ) ai xi − x0 ai
i=1 i=1 i=1
( )
∑
n
=f ai xi .
i=1
Lemma B.24. Sei t : R≥0 −→ R≥0 invertierbar und x : R≥0 −→ R≥0 eine
Funktion, sodass für alle k ∈ N gilt
y(k) = x(t(k)).
9
Um die Definition einer linearen Differenzengleichung erster Ordnung aus Ab-
schnitt 1.3.1 zu erhalten, die ein Spezialfall dieser Notation ist, setzen wir
f (k, y( k − 1)) = ak yk−1 + bk
350 B. Mathematische Begriffe und nützliche Formeln
Sei Ly eine Fortsetzung der geschlossenen Lösung für y(k) mit y(k) = Ly (k)
für k ∈ R≥0 . Dann ist Lx = Ly ◦ t−1 eine geschlossene Lösung für x(n), d. h.
x(n) = Lx (n) für n ∈ R≥0 .
Sei die Transformation t monoton wachsend und Ly = O(g) für eine
Funktion g, genauer verlangen wir Ly (n) ≤ cg(n) für alle n ∈ R≥0 und
n ≥ n0 für Konstanten c und n0 , dann ist Lx = O(g ◦ t−1 ) für alle Lösungen
Lx .
Beweis. x(n) = x(t(t−1 (n))) = Ly (t−1 (n)) = Ly ◦ t−1 (n) = Lx (n), n ∈ R≥0 .
Die Aussage über die Ordnung folgt aus
Lx (n) = Lx (t(t−1 (n))) = Ly (t−1 (n)) ≤ cg(t−1 (n)) = c(g ◦ t−1 )(n)
Lehrbücher
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021
H. Knebl, Algorithmen und Datenstrukturen,
https://doi.org/10.1007/978-3-658-32714-9
352 Literatur
Zeitschriften-Artikel
[Kruskal56] J. Kruskal: On the shortest spanning subtree and the traveling sales-
man problem. Proceedings of the American Mathematical Society 7: 48–50,
1956.
[Leven65] V. Levenshtein: Binary codes capable of correcting deletions, insertions,
and reversals. Sov. Phys. Dokl. 10(8):707–710 (English translation), 1966
[Newman80] D. J. Newman: Simple analytic proof of the prime number theorem.
Am. Math. Monthly 87: 693–696, 1980.
[Pasco76] R. Pasco: Source Coding Algorithms for Fast Data Compression. Ph. D.
Thesis, Dept. of Electrical Engineering, Stanford University, 1976.
[Prim57] R. C. Prim: Shortest connection networks and some generalizations. Bell
System Technical Journal, 36(6): 1389–1401, 1957.
[Rissanen76] J. J. Rissanen: Generalized Kraft inequality and arithmetic coding.
IBM Journal of Research and Development, 20(3): 198–203, 1976.
[RobSanSeyTho97] N. Robertson, D. Sanders, P. D. Seymour, R. Thomas: The
four-colour theorem. J. Combin. Theory B70: 2–44, 1997.
[SarPat53] A. A. Sardinas, G. W. Patterson: A necessary and sufficient condition
for the unique decomposition of coded messages. IRE Internat. Conv. Rec. 8:
104–108, 1953.
[SchStr71] A. Schönhage, V. Strassen: Schnelle Multiplikation großer Zahlen. Com-
puting 7: 281–292, 1971.
[Shannon48] C. E. Shannon: A mathematical theory of communication. Bell Sys-
tems Journal, 27: 379–423, 623–656, 1948.
[Shannon49] C. E. Shannon: Communication theory of secrecy systems. Bell Sys-
tems Journal, 28: 656–715, 1949.
[Sharir81] M. Sharir: A strong-connectivity algorithm and its applications in data
flow analysis. Computers and Mathematics with Applications 7(1): 67–72, 1981.
[SieSch95] A. Siegel, J. P. Schmidt: Closed hashing is computable and optimally
randomizable with universal hash functions. Computer Science Tech. Report
687. New York: Courant Institute, 1995.
[Strassen69] V. Strassen: Gaussian Elimination is not optimal. Numerische Mathe-
matik 13: 354-356, 1969.
[Tarjan79] R. E. Tarjan: Applications of path compression on balanced trees. Jour-
nal of the ACM, 26(4): 690–715, 1979.
[Tarjan99] R. E. Tarjan: Class notes: Disjoint set union. COS 423, Princeton Uni-
versity, 1999
[WagFis74] R. Wagner, M. Fischer: The string-to-string correction problem. Jour-
nal of the ACM, 21(1): 168–173, 1974.
[Warshall62] S. Warshall: A theorem on boolean matrices. Journal of the ACM,
9(1): 11-12, 1962.
[Wegener93] I. Wegener: Bottom-up-heapsort, a new variant of heapsort beating,
on an average, Quicksort. Theoretical Computer Science 118:81–98, 1993.
[Whitney35] H. Whitney: On the abstract properties of linear dependence. Ameri-
can Journal of Mathematics 57: 509–533, 1935.
[Williams64] J. W. J. Williams: Algorithm 232: Heapsort. Communications of the
ACM, 7(6): 347–348, 1964.
[Yao85] A. C. Yao: Uniform hashing is optimal. Journal of the ACM, 32(3): 687–
693, 1985.
[ZivLem77] J. Ziv, A. Lempel: A universal algorithm for sequential data compres-
sion. IEEE Transactions on Information Theory, 23(3): 337–343, 1977.
[ZivLem78] J. Ziv, A. Lempel: Compression of individual sequences via variable-
rate encoding. IEEE Transactions on Information Theory, 24(5): 530–536, 1978.
Literatur 355
Internet
Symbole
Seite
N natürliche Zahlen: {1, 2, 3, . . .}
N0 nicht negative ganze Zahlen: {0, 1, 2, 3, . . .}
Z ganze Zahlen
Q rationale Zahlen
R reelle Zahlen
R≥0 nicht negative reelle Zahlen x ≥ 0
R>0 positive reelle Zahlen x > 0
∅ leere Menge
A∩B Durchschnitt von Mengen A und B
A∪B Vereinigung von Mengen A und B
A∪B
˙ disjunkte Vereinigung, A ∪ B, falls A ∩ B = ∅
A\B Differenzmenge, A ohne B
G ∪ {e} Erweiterung des Graphen G durch die Kante e
G \ {e} Reduktion des Graphen G durch Wegnahme der Kante e
Hn n–te harmonisch Zahl 338
fn n–te Fibonacci-Zahl 19
n! n! = 1 · 2 · . . . · n, n–Fakultät
(n)
k Binomialkoeffizient n über k 344
g◦f Komposition von Abbildungen: g ◦ f (x) = g(f (x))
idX identische Abbildung: idX (x) = x fà 14 r alle x ∈ X
f −1 inverse Abbildung einer bijektiven Abbildung f
ln(x) natürlicher Logarithmus einer reellen Zahl x > 0
log(x) Logarithmus zur Basis 10 einer reellen Zahl x > 0
log2 (x) Logarithmus zur Basis 2 einer reellen Zahl x > 0
|x| Länge eines Bitstrings x
ε leere Zeichenkette
|X| Anzahl der Elemente der Menge X
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021
H. Knebl, Algorithmen und Datenstrukturen,
https://doi.org/10.1007/978-3-658-32714-9
358 Symbole
Seite
Xn Menge der Wörter der Länge n über X
X∗ Menge der Wörter über X, X ∗ = ∪n≥0 X n
H(X) Entropie einer Quelle X 182
l(C) mittlere Codewortlänge eines Codes C 182
{0, 1}∗ Menge der Bitstrings beliebiger Länge
a||b Konkatenation von Zeichenketten a und b
Zn Restklassenring modulo n 340
a div n ganzzahliger Quotient von a durch n 337
a mod n Rest von a modulo n 337
Fq endlicher Körper mit q vielen Elementen
∏n
ai Produkt a1 · . . . · an
∑i=1n
i=1 ai Summe a1 + . . . + an
[a, b], ]a, b], [a, b[, ]a, b[ Intervalle (geschlossen, halboffen und offen)
i..j Folge i, i + 1, . . . , j
a[i..j] Teilarray von a 69
min a[i..j] Minimum im Teilarray a[i..j]
a.b Punktoperator 69
⌊x⌋ größte ganze Zahl ≤ x
⌈x⌉ kleinste ganze Zahl ≥ x
O(f (n)) O Notation 10
p(E) Wahrscheinlichkeit eines Ereignisses E 319
p(x) Wahrscheinlichkeit eines Elementar-
ereignisses x ∈ X 319
p(E | F ) bedingte Wahrscheinlichkeit von E
unter der Annahme von F 320
E(X) Erwartungswert einer Zufallsvariablen X 321
Var(X) Varianz einer Zufallsvariablen X 321
σ(X) Standardabweichung einer Zufallsvariablen X 321
GX (z) Erzeugendenfunktion einer Zufallsvariablen X 324
Index
Optimierungsproblem 38 Speicher-Komplexität 10
– optimale Lösung 38 Stackframe 85
Suchen in Arrays 103
Partialbruchzerlegung 342 – binäre Suche 104, 110
partiell geordnete Menge 236 – Fibonacci-Suche 212
perfekte Zuordnung 224 – k–kleinstes Element 105
Pfadkomprimierung, siehe Datenstruk- – sequenzielle Suche 103
turen, -Union-Find Symbol siehe Codes, -Symbol
Phrasen 206
Postorder-Ausgabe, siehe Baum, Treap 158
-binärer Baum – Suchen, Einfügen und Löschen 159
Prädikate 3 – zufällige Prioritäten 160
Preorder-Ausgabe, siehe Baum, -binärer
Baum Variable
primitiv rekursiv 5 – reference type 69
Programmverifikation 3 – value type 69
Pseudocode für Algorithmen 68 Verifikation
Pseudozufallszahlen 66 – Identität großer Zahlen 62
Pseudozufallszahlen-Generator 66 – Identität von Polynomen 59
– kürzester Pfadbaum 317
Quadratzahl 340 – minimaler aufspannender Baum 289
Quicksort 76 Vierfarbenproblem 217
– Laufzeitanalyse 78
– – bester und schlechtester Fall 79 Wahrscheinlichkeitsrechnung 319
– – durchschnittliche Anzahl der – bedingte Wahrscheinlichkeit 320
Umstellungen 83 – Ereignis 319
– – durchschnittliche Anzahl der – – unabhängige Ereignisse 320
Vergleiche 82 – Markovsche Ungleichung 323
– – durchschnittliche Laufzeit 81 – spezielle diskrete Verteilungen 323
– ohne Stack 86 – – Bernoulli-verteilt 325
– Pivotelement 76 – – binomialverteilt 326
– Speicherplatzanalyse 85 – – geometrisch verteilt 328
– – hypergeometrisch verteilt 331
Reduktion – – negativ binomialverteilt 329
– LCA-Problem auf das RMQ-Problem – – negativ hypergeometrisch verteilt
266 333
– Pfad-Maximum-Problem auf voll – – Poisson-verteilt 328
verzweigte Bäume 289 – Standardabweichung 321
– RMQ-Problem auf das LCA-Problem – Varianz 321
270 – Verteilung 319
Rekursion, siehe Algorithmen, – – seltene Ereignisse 328
-Entwurfsmethoden – Wahrscheinlichkeitsmaß 319
– Endrekursion 86 – Wahrscheinlichkeitsraum 319
Rekursionstiefe 26, 86 – Zufallsvariable 321
Seed 66 – – binäre 321
Sortieren 75 – – diskrete 323
– durch Austauschen (bubble sort) 107 – – Erwartungswert 321, 324
– durch Auswählen (selection sort) 2 – – erzeugende Funktion 324
– durch Einfügen (insertion sort) 71 – – reelle 321
– Heapsort, siehe Heapsort
Zeuge 60
– Mergesort 110
Zufallsfunktion 115
– Quicksort, siehe Quicksort
Zufallszahlen 66
– stabil 108
Zwischenminimum 7
– Vergleich von Quicksort und Heapsort
Zyklus, siehe Graphen, -Zyklus
100