0% fanden dieses Dokument nützlich (0 Abstimmungen)
3K Ansichten373 Seiten

Algorithmen Und Datenstrukturen

Hochgeladen von

Ion Postolache
Copyright
© © All Rights Reserved
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen
0% fanden dieses Dokument nützlich (0 Abstimmungen)
3K Ansichten373 Seiten

Algorithmen Und Datenstrukturen

Hochgeladen von

Ion Postolache
Copyright
© © All Rights Reserved
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen

Helmut Knebl

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

ISBN 978-3-658-32713-2 ISBN 978-3-658-32714-9  (eBook)


https://doi.org/10.1007/978-3-658-32714-9

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.

Planung: Sybille Thelen


Springer Vieweg ist ein Imprint der eingetragenen Gesellschaft Springer Fachmedien Wiesbaden GmbH und ist
ein Teil von Springer Nature.
Die Anschrift der Gesellschaft ist: Abraham-Lincoln-Str. 46, 65189 Wiesbaden, Germany
Vorwort zur zweiten Auflage

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.

Nürnberg, im Dezember 2020 Helmut Knebl


Vorwort

Viele praktische Probleme können durch einen Algorithmus gelöst werden.


Deshalb sind Computeralgorithmen heute so vielfältig und allgegenwärtig.
Das Spektrum reicht von einem der ältesten aufgezeichneten Algorithmen,
dem Algorithmus von Euklid aus dem 3. Jahrhundert v. Chr., bis zu den
Algorithmen für die Untersuchung großer Datenmengen, für die Kommunika-
tion und Suche im Internet, Algorithmen für bildgebende Verfahren und zur
Diagnostik in der Medizintechnik, Algorithmen für Assistenzsysteme im Au-
to, der Motorsteuerung oder der Steuerung von Haushaltsgeräten. Algorith-
men sind Gegenstand intensiver Forschung und zählen zu den fundamentalen
Konzepten der Informatik. Der Entwurf effizienter Algorithmen und deren
Analyse im Hinblick auf den Ressourcenbedarf sind grundlegend für die Ent-
wicklung von Computerprogrammen. Deshalb ist das Fach Algorithmen und
Datenstrukturen zentraler Bestandteil eines jeden Informatik Curriculums.
Das vorliegende Buch ist aus Vorlesungen über Algorithmen und Daten-
strukturen für Studenten und Studentinnen der Informatik, Medien- und
Wirtschaftsinformatik an der Technischen Hochschule Nürnberg Georg Si-
mon Ohm entstanden. Die grundlegenden Themen des Buches werden in den
Bachelorkursen behandelt. Fortgeschrittene Teile, wie zum Beispiel die pro-
babilistischen Algorithmen, sind Masterkursen vorbehalten.
Die Algorithmen des ersten Kapitels, es handelt sich durchweg um po-
puläre Algorithmen, studieren wir, um gängige Designprinzipien für die Ent-
wicklung von Algorithmen einzuführen. Die folgenden Kapitel 2 bis 6 sind
nach Problembereichen organisiert. Wir betrachten das Problem Elemente
einer Menge abzuspeichern und wieder zu finden sowie Probleme, die sich
mit Graphen formulieren lassen. Für das erste Problem verwenden wir drei
Methoden, um diese Operationen effizient zu implementieren: Sortieren mit
binärer Suche, Suchbäume und Hashverfahren. Die ersten beiden Methoden
setzen geordnete Mengen voraus, die letzte Methode setzt voraus, dass die
Elemente der Menge eindeutig durch Schlüssel identifiziert sind.
Die Sortiermethoden Quicksort und Heapsort, binäre Suche und die Suche
nach dem k–kleinsten Element sind Gegenstand von Kapitel 2. Besonderer
Wert wird auf die Analyse der Laufzeit der Algorithmen gelegt. Über alle
Kapitel hinweg wird angestrebt, explizite Formeln oder präzise Abschätzun-
gen für die Laufzeit zu entwickeln. Dabei werden unter anderem Differen-
zengleichungen als Lösungsmethode herangezogen. Dadurch können exakte
VIII Vorwort

und nicht nur asymptotische Aussagen zu den Laufzeiten von Algorithmen


gemacht werden. Wir erhalten eine standardisierte Methode bei der Laufzeit-
berechnung: Stelle zunächst eine Differenzengleichung für die Laufzeit auf
und löse anschließend die Gleichung mit bekannten Methoden.
Hashfunktionen, insbesondere universelle Familien von Hashfunktionen,
Verfahren zur Behandlung von Kollisionen und eine detaillierte Analyse von
Hashverfahren sind Gegenstand von Kapitel 3.
In Kapitel 4 werden binäre Suchbäume, AVL-Bäume und probabilistische
binäre Suchbäume behandelt. B-Bäume dienen zum Speichern von Daten auf
dem Sekundärspeicher. Codebäume zur graphischen Darstellung von Codes
zur Datenkomprimierung runden das Kapitel ab.
Graphen spielen in vielen Gebieten der Informatik eine grundlegende Rol-
le. Für viele Graphenprobleme existieren Lösungen in Form von effizienten
Algorithmen. In Kapitel 5 werden Tiefen- und Breitensuche für Graphen stu-
diert und als Anwendung davon topologisches Sortieren und die Berechnung
der starken Zusammenhangskomponenten. Grundlegende Optimierungspro-
bleme, wie die Berechnung minimaler aufspannender Bäume und kürzester
Wege als auch das Flussproblem in Netzwerken, sind der Inhalt von Kapitel 6.
Probabilistische Methoden sind grundlegend für die Konstruktion einfa-
cher und effizienter Algorithmen. In jedem Kapitel wird mindestens ein Pro-
blem mit einem probabilistischen Algorithmus gelöst. Im Einzelnen geht es
um die Verifikation der Identität von Polynomen, die probabilistische Version
von Quicksort und Quickselect, um universelle Familien von Hashfunktionen
und um probabilistische binäre Suchbäume. Die probabilistischen Algorith-
men zur Berechnung eines minimalen Schnittes in einem Graphen und zur
Berechnung eines minimalen aufspannenden Baumes für einen gewichteten
Graphen zählen zu den hervorgehobenen Themen.
Der Schwerpunkt des Buches liegt bei den Algorithmen. Datenstrukturen
werden besprochen, soweit sie zur Implementierung der Algorithmen benötigt
werden. Die Auswahl der Themen erfolgt vor allem unter dem Gesichtspunkt,
elementare Algorithmen – die ein weites Anwendungsgebiet aufweisen –
zu behandeln. Dabei wird ein detailliertes tiefer gehendes Studium ange-
strebt.
Der Text setzt Erfahrungen in der Programmierung von Algorithmen, ins-
besondere mit elementaren Datenstrukturen – wie zum Beispiel verketteten
Listen, Queues und Stacks – im Umfang des Inhalts der Programmiervor-
lesungen der ersten beiden Semester im Informatikstudium, voraus. Ebenso
ist die Vertrautheit mit mathematischen Methoden, die in den ersten bei-
den Semestern behandelt werden, wünschenswert. Zur Bequemlichkeit der
Leser werden die zum Verständnis notwendigen mathematischen Methoden,
insbesondere elementare Lösungsmethoden für Differenzengleichungen und
spezielle Wahrscheinlichkeitsverteilungen, im ersten Kapitel und im Anhang
wiederholt.
Die Formulierung der Algorithmen durch Pseudocode fokussiert auf das
Wesentliche und macht dadurch die Idee des Algorithmus deutlich. Sie ist hin-
Vorwort IX

reichend präzise, um Überlegungen zur Korrektheit und Berechnungen der


Laufzeit durchzuführen. Über 100 Figuren machen die Algorithmen anschau-
lich. Viele Beispiele helfen, die einzelnen Schritte der Algorithmen nachzuvoll-
ziehen. Zahlreiche Übungsaufgaben schließen jedes Kapitel ab und helfen den
Stoff einzuüben und zu vertiefen. Lösungen zu den Übungsaufgaben stehen
zum Download bereit: www.in.th-nuernberg.de/Knebl/Algorithmen.
Das Buch ist aus Vorlesungen über Algorithmen und Datenstrukturen
entstanden, die ich an der Technischen Hochschule Nürnberg Georg Simon
Ohm über viele Jahre hinweg gehalten habe. In dieser Zeitspanne hat die
Hochschule zweimal den Namen geändert und ist doch die alte geblieben. Bei
der Vorbereitung der Vorlesung habe ich die Lehrbücher verwendet, die im
Abschnitt 1.8 gelistet sind.
Bei der Fertigstellung des Buches habe ich mannigfache Unterstützung er-
halten. Meine Kollegen Jens Albrecht, Christian Schiedermeier und vor allem
Alexander Kröner haben Teile sorgfältig durchgesehen, was zur Korrektur von
Fehlern und Unklarheiten geführt hat. Harald Stieber verdanke ich wertvolle
Anregungen und Diskussionen, die zur Verbesserung des Buches beigetragen
haben. Allen, die mich unterstützt haben, auch den nicht genannten, spreche
ich meinen großen Dank aus. Bei meinen Studenten, die in den vergangenen
Jahren die Vorlesung mit Engagement besucht, fleißig Übungsaufgaben bear-
beitet und beim Aufspüren von Fehlern behilflich waren, bedanke ich mich
ganz besonders.

Nürnberg, im Mai 2019 Helmut Knebl


Inhaltsverzeichnis

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

2. Sortieren und Suchen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75


2.1 Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
2.1.1 Laufzeitanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
2.1.2 Speicherplatzanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
2.1.3 Quicksort ohne Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
2.1.4 Probabilistisches Quicksort . . . . . . . . . . . . . . . . . . . . . . . . 87
2.2 Heapsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
2.2.1 Binäre Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
2.2.2 Die Sortierphase von Heapsort . . . . . . . . . . . . . . . . . . . . . 93
2.2.3 Laufzeitanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
2.2.4 Heapsort Optimierungen . . . . . . . . . . . . . . . . . . . . . . . . . . 96
XII Inhaltsverzeichnis

2.2.5 Vergleich von Quicksort und Heapsort . . . . . . . . . . . . . . . 100


2.3 Eine untere Schranke für Sortieren durch Vergleichen . . . . . . . 101
2.4 Suchen in Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
2.4.1 Sequenzielle Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
2.4.2 Binäre Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
2.4.3 Die Suche nach dem k–kleinsten Element . . . . . . . . . . . . 105

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

5.4 Elementare Graphalgorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . 227


5.4.1 Die Breitensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
5.4.2 Die Tiefensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
5.5 Gerichtete azyklische Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
5.6 Die starken Zusammenhangskomponenten . . . . . . . . . . . . . . . . . 238
5.7 Ein probabilistischer Min-Cut-Algorithmus . . . . . . . . . . . . . . . . 243

6. Gewichtete Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253


6.1 Grundlegende Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
6.1.1 Die Priority-Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
6.1.2 Der Union-Find-Datentyp . . . . . . . . . . . . . . . . . . . . . . . . . 256
6.1.3 Das LCA- und das RMQ-Problem . . . . . . . . . . . . . . . . . . 264
6.2 Die Algorithmen von Dijkstra und Prim . . . . . . . . . . . . . . . . . . . 273
6.3 Der Algorithmus von Kruskal . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
6.4 Der Algorithmus von Borůvka . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
6.5 Die Verifikation minimaler aufspannender Bäume . . . . . . . . . . . 289
6.6 Ein probabilistischer MST-Algorithmus . . . . . . . . . . . . . . . . . . . 296
6.7 Transitiver Abschluss und Abstandsmatrix . . . . . . . . . . . . . . . . . 300
6.8 Flussnetzwerke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305

A. Wahrscheinlichkeitsrechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
A.1 Endliche Wahrscheinlichkeitsräume und Zufallsvariable . . . . . . 319
A.2 Spezielle diskrete Verteilungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 323

B. Mathematische Begriffe und nützliche Formeln . . . . . . . . . . . 337

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

zur Korrektheit und Berechnungen der Laufzeit durchzuführen. Die Notation


führen wir gegen Ende des Kapitels ein (Abschnitt 1.7).

1.1 Korrektheit von Algorithmen

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:

m = min a[i..j] für j = i + 1, . . . , n und a[k] = m.

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

a[1..i] ist sortiert, i = 1, . . . , n − 1, und a[i] ≤ a[k] für k ≥ i + 1.

Diese Aussage folgt durch vollständige Induktion nach i. Da unser Al-


gorithmus terminiert und die Schleifenvariable i bei Terminierung den Wert
n − 1 hat, folgt, dass a[1..n − 1] sortiert ist. Da a[n] ≥ a[n − 1] gilt, ist die
Behauptung bewiesen.
In unserem Beweis, der zeigt, dass der Algorithmus korrekt ist, kommen
die Variablen des Algorithmus vor. Er nimmt somit auf eine konkrete Imple-
mentierung Bezug. Ein solcher Beweis, der zeigt, dass die Implementierung
eines Algorithmus A korrekt ist, bezeichnen wir als Programmverifikation.
Vor- und Nachbedingung sind als Prädikate V und N der Programmvariablen
zu spezifizieren. Ein mathematischer Beweis, der zeigt – gilt vor Ausführung
von A das Prädikat V und nach Ausführung von A das Prädikat N – be-
weist die Korrektheit von A unter der Voraussetzung, dass A terminiert.
Die Technik der Programmverifikation wurde unter anderem von Hoare2 und
Dijkstra3 entwickelt. Lehrbücher über Programmverifikation sind [Gries81]
oder [Backhouse86].
Das Vorgehen, wie man bei spezifizierter Vor- und Nachbedingung zeigt,
dass ein Codesegment die Vorbedingung in die Nachbedingung transformiert
ist formalisierbar. Ein Computerprogramm kann uns unterstützen, den Be-
weis zu führen. Theorem-Beweiser gehen noch einen Schritt weiter. Ein
Theorem-Beweiser ist ein Algorithmus, der einen Beweis findet, der zeigt,
dass aus einer Vorbedingung eine bestimmte Nachbedingung folgt. Der ge-
fundene Beweis ist konstruktiv. Er transformiert die Vorbedingung in die
Nachbedingung und stellt die Codierung des Algorithmus bereit.
Wir führen in diesem Buch Beweise, die zeigen, dass Algorithmen kor-
rekt sind. Auf die konkrete Implementierung gehen wir dabei bis auf wenige
Ausnahmen nicht ein.
Unser Beispiel SelectionSort terminiert für jede Eingabe. Ein Algorith-
mus, der nicht nur for-Schleifen, sondern auch while-Schleifen oder rekursive
Aufrufe enthält, muss nicht für jede Eingabe terminieren. Für die Korrektheit
eines Algorithmus ist dies jedoch eine notwendige Voraussetzung. Das Pro-
blem für einen beliebigen Algorithmus zu entscheiden, ob er terminiert oder
nicht, bezeichnet man als Halteproblem. Ein Algorithmus kann die Frage, ob
ein beliebiges Programm terminiert, nicht beantworten, d. h. das Haltepro-
blem ist nicht entscheidbar . Ebenso ist es nicht möglich einen Algorithmus
anzugeben, der für einen beliebigen Algorithmus A entscheidet, ob A korrekt
ist oder der die Laufzeit von A berechnet. Deshalb müssen wir diese Fragen
für jeden Algorithmus individuell beantworten.
Das folgende Beispiel zeigt, dass es nicht einmal für einen gegebenen kon-
kreten Algorithmus einfach ist zu entscheiden, ob der Algorithmus für jede
erlaubte Eingabe terminiert.
2
Tony Hoare (1934 – ) ist ein britischer Informatiker und Turing-Preisträger.
3
Edsger W. Dijkstra (1930 – 2002) war Niederländer, Turing-Preisträger und hat
grundlegende Beiträge zu mehreren Gebieten der Informatik geleistet.
4 1. Einleitung

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.

Rekursion ist eine mächtige Entwurfs- und Programmiermethode. Wir


studieren zwei interessante rekursive Funktionen.
McCarthys5 Funktion, bekannt als McCarthys 91 Funktion, weist eine
komplexe rekursive Struktur auf. Wir bezeichnen diese Funktion mit M. Für
n > 100 terminiert M mit Rückgabewert n − 10. Für n < 100 ist die Termi-
nierung nicht so offensichtlich.
Algorithmus 1.3.
int M(int n)
1 if n > 100
2 then return n − 10
3 else return M(M(n + 11))

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(M(n + 11)) = M(n + 1).

Wir erhalten für n mit 90 ≤ n ≤ 100

M(n) = M(M(n + 11)) = M(n + 1) = M(M(n + 12)) =

M(n + 2) = . . . = M(101) = 91.


Sei jetzt n ≤ 89 und k = max{j | n + 11j ≤ 100}. Dann gilt 90 ≤ n + 11k ≤
100 und wegen

M(n) = M2 (n + 11) = . . .
= Mk+1 (n + 11k) = Mk (M(n + 11k)) = Mk (91) = 91

folgt die Behauptung. 2

4
Lothar Collatz (1910 – 1990) war ein deutscher Mathematiker.
5
John McCarthy (1927 – 2011) war ein amerikanischer Mathematiker.
1.1 Korrektheit von Algorithmen 5

Die Ackermann-Funktion6 weist interessante Eigenschaften auf, die für


die Theoretische Informatik Bedeutung haben. Sie zeigt, dass es Turing7
berechenbare Funktionen gibt, die nicht primitiv rekursiv sind. Ackermann
publizierte seine Funktion in [Ackermann28] und widerlegt damit eine Ver-
mutung von Hilbert8 , dass jede berechenbare Funktion primitiv rekursiv sei.
Die Ackermann-Funktion wächst schneller, als es für primitiv rekursive Funk-
tionen möglich ist. Primitiv rekursive Funktionen sind das Resultat eines
eingeschränkten Berechnungsmodells, das nur for-Schleifen, aber keine while-
Schleifen erlaubt (Loop-Programme). Bei der Ackermann-Funktion9 handelt
es sich um eine extrem schnell wachsende Funktion, die von zwei Parametern
m, n ∈ N0 abhängt.
Algorithmus 1.5.
int A(int m, n)
1 if m = 0
2 then return n + 1
3 if n = 0
4 then return A(m − 1, 1)
5 return A(m − 1, A(m, n − 1))
Für m = 0 terminiert A unmittelbar. Um zu zeigen, dass A für alle
Eingaben terminiert betrachten wir die lexikographische Ordnung auf N0 ×
N0 : {
′ ′ m < m′ oder
(m, n) < (m , n ) genau dann, wenn
m = m′ und n < n′ .
Da
(m − 1, 1) < (m, 0), (m, n − 1) < (m, n) und (m − 1, A(m, n − 1)) < (m, n)
gilt, erfolgt der rekursive Aufruf mit einem kleineren Parameter bezüglich der
lexikographischen Anordnung. Nach dem folgenden Lemma 1.6 gibt es nur
endliche absteigende Folgen, die mit (m, n) beginnen. Deshalb terminiert die
Funktion A für alle Eingaben (m, n) mit einem Rückgabewert aus N.
Die Ackermann-Funktion findet Anwendung bei der Analyse des Union-
Find-Datentyps (Abschnitt 6.1.2).
Lemma 1.6. Bezüglich der lexikographischen Ordnung auf N0 × N0 gibt es
nur endliche absteigende Folgen.
6
Wilhelm Friedrich Ackermann (1896 – 1962) war ein deutscher Mathematiker.
Er war ein Schüler von Hilbert und beschäftigte sich mit Grundlagen der Mathe-
matik.
7
Alan Mathison Turing (1912 – 1954) war ein britischer Mathematiker und Infor-
matiker. Er hat grundlegende Beiträge zur Theoretischen Informatik und prak-
tischen Kryptoanalyse (Enigma) geleistet.
8
David Hilbert (1862 – 1943) war ein deutscher Mathematiker. Er gilt als einer
der bedeutendsten Mathematiker des ausgehenden 19. und des 20. Jahrhunderts.
9
Die folgende Funktion wird als Ackermann-Funktion bezeichnet. Es handelt sich
jedoch um eine vereinfachte Version der ursprünglichen Funktion, die 1955 von
der ungarischen Mathematikerin Rózsa Péter (1905 – 1977) definiert wurde.
6 1. Einleitung

Beweis. Angenommen, es gibt eine unendliche absteigende Folge


(m1 , n1 ) > (m2 , n2 ) > (m3 , n3 ) > . . .
Die Menge {m1 , m2 , m3 , . . .} ⊂ N0 besitzt ein kleinstes Element mℓ 10 . Es
folgt mℓ = mℓ+1 = mℓ+2 = . . .. Dann besitzt die Menge {nℓ , nℓ+1 , nℓ+2 , . . .}
kein kleinstes Element, ein Widerspruch. 2

1.2 Laufzeit von Algorithmen


Die Laufzeitanalyse – kurz Analyse – von Algorithmen spielt beim Studi-
um von Algorithmen eine wesentliche Rolle. Oft stehen zur Lösung eines
Problems mehrere Algorithmen bereit. Erst aufgrund der Analyse können
wir entschieden, welcher der Algorithmen für eine bestimmte Anwendung
am besten geeignet ist. Bei der Analyse streben wir möglichst explizite For-
meln für die Laufzeit an. Dies demonstrieren wir jetzt mit Algorithmus 1.1 –
SelectionSort.

1.2.1 Explizite Formeln


Wir analysieren, wie oft die einzelnen Zeilen von SelectionSort (Algorith-
mus 1.1) ausgeführt werden. Wir berechnen die Anzahl der Ausführungen im
schlechtesten Fall und im Durchschnitt. Den Durchschnitt bilden wir dabei
über alle möglichen Anordnungen der Elemente im Array a. Für die Analyse
nehmen wir an, dass alle Elemente in a paarweise verschieden sind.
Sei ai die Anzahl der Ausführungen von Zeile i im schlechtesten Fall und
ãi die Anzahl der Ausführungen von Zeile i im Durchschnitt – jeweils in
Abhängigkeit von n.
Zeile i ai ãi
3, 7 n−1 n−1
5 n(n − 1)/2 n(n − 1)/2
2
6 ≤ n /4 ?
a3 , a5 und a7 hängen nicht von der Anordnung der Elemente in a ab.
Deshalb gilt ai = ãi , i = 3, 5, 7. Die Anzahl der Ausführungen im schlechtes-
ten Fall von Zeile 6 (a6 ) ist offensichtlich durch n(n − 1)/2 beschränkt. Diese
2
Schranke wird durch keine Eingabe angenommen. Die bessere Schranke n /4
ist kompliziert zu ermitteln, siehe [Knuth98a, Abschnitt 5.2.3]11 . In den Zei-
len 4, 5 und 6 wird das Minimum in einem Teilarray bestimmt. Um ã6 zu
10
Hier verwenden wir die folgende Eigenschaft natürlicher Zahlen: Sei M ⊂ N0 ,
M ̸= ∅. Dann besitzt M ein kleinstes Element.
11
Donald E. Knuth (1938 – ) ist ein amerikanischer Informatiker. Er ist Autor von
TEXund Metafont und Verfasser von The Art of Computer Programming“, ein

Standardwerk über grundlegende Algorithmen und Datenstrukturen, das mitt-
lerweile vier Bände umfasst, vor fast 50 Jahren begonnen wurde und noch nicht
abgeschlossen ist ([Knuth97], [Knuth98], [Knuth98a] und [Knuth11]).
1.2 Laufzeit von Algorithmen 7

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

Gleichungen dieser Art heißen lineare Differenzengleichungen. Wir behandeln


eine allgemeine Lösungsmethode für diese Gleichungen in Abschnitt 1.3. Die
Gleichung von oben ist einfach zu lösen, indem wir xj auf der rechten Seite
fortgesetzt durch xj−1 + 1/j , j = n − 1, . . . , 2, ersetzen. Wir sagen, wir lösen
die Gleichung durch Expandieren der rechten Seite.
8 1. Einleitung

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

ã6 = (n + 1)Hn − 2n.

Beweis. Der Algorithmus SelectionSort bestimmt in den Zeilen 2 – 6 das


Minimum im Array a[i . . . n] für i = 1, . . . , n − 1. Die Länge dieses Arrays ist
n − (i − 1). Mit Lemma 1.9 und Lemma B.6 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.

Dies zeigt die Behauptung. 2

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

letztmalige Prüfung der Abbruchbedingung. Die Laufzeit für einen Schleifen-


durchgang erhalten wir durch Addition der für die Prüfung der Abbruchbe-
dingung notwendigen Laufzeit und der Laufzeit, die für den Schleifenrumpf
erforderlich ist. Ist die Laufzeit für alle Schleifendurchgänge gleich, so mul-
tiplizieren wir die Laufzeit für einen Schleifdurchgang mit der Anzahl der
Iterationen der Schleife.
Die Laufzeit für eine if-then-else Anweisung ergibt sich aus der Laufzeit
für die Prüfung der Bedingung, der Laufzeit für den if-Teil und der Laufzeit
für den else-Teil. Bei der Berechnung der Laufzeit im schlechtesten Fall ist
das Maximum der Laufzeiten für den if- und den else-Teil zu betrachten.
Bei der durchschnittlichen Laufzeit ist es die mit den Wahrscheinlichkeiten
gewichtete Summe der Laufzeiten für den if- und den else-Teil.
Definition 1.11. Sei P ein Berechnungsproblem, A ein Algorithmus für P
und J die Menge der Instanzen von P. Sei l : J −→ N eine Abbildung, die
die Größe einer Instanz definiert. Jn := {I ∈ J | l(I) = n} heißt Menge der
Instanzen der Größe n.13 Wir definieren

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

2. Ist auf Jn eine Wahrscheinlichkeitsverteilung (p(I))I∈Jn gegeben (Defini-


tion A.1), so ist Sn : Jn −→ N, Sn (I) := S(I),
∑ eine Zufallsvariable (Defi-
nition A.5). Der Erwartungswert E(Sn ) = I∈Jn p(I)S(I) von Sn heißt
durchschnittliche Laufzeit oder durchschnittliche (Zeit-)Komplexität von
A
T̃ : N −→ N, T̃ (n) := E(Sn ).
In diesem Text nehmen wir für alle Berechnungen stets die Gleichvertei-
lung an, d. h. alle Elemente in Jn treten mit der gleichen Wahrscheinlichkeit
auf. Der Erwartungswert ist dann der Mittelwert.
Bemerkung. Bei dem betrachteten Algorithmus 1.7 zur Berechnung des Mini-
mums sei die Eingabe ein Array mit ganzen Zahlen. Die Eingaben der Größe
n sind die Arrays der Länge n. Wir setzen die Elemente als paarweise ver-
schieden voraus. Für jede Menge {a1 , . . . , an } von ganzen Zahlen erhalten wir
n! viele verschiedene Arrays.
Die Anzahl der Stellen einer ganzen Zahl ist durch die Architektur des
verwendeten Rechners bestimmt. Die darstellbaren ganzen Zahlen bilden ei-
ne endliche Teilmenge I der ganzen Zahlen. Ist die Anzahl der Stellen der
13
Für unsere Anwendungen kann Jn als endlich vorausgesetzt werden.
10 1. Einleitung

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

Seien A1 und A2 Algorithmen für dasselbe Berechnungsproblem. T1 (n) und


T2 (n) seien die Laufzeiten von A1 und A2 . Wir wollen jetzt T1 (n) und T2 (n)
für große n vergleichen, d. h. wir interessieren uns für die Laufzeit bei großen
Eingaben. Angenommen es ist T2 (n) ≤ T1 (n) für große n. Dieser Unterschied
ist unwesentlich14 , wenn es eine Konstante c gibt, sodass für große n gilt
T1 (n) ≤ cT2 (n). Wenn keine Konstante c die Ungleichung erfüllt, ist die
Laufzeit von A2 wesentlich besser als die Laufzeit von A1 bei großen Einga-
ben. Dies präzisiert die O–Notation, die den Landau-Symbolen15 zugerechnet
wird.
Definition 1.12. Seien f, g : N −→ R≥0 Funktionen. g heißt in der Ordnung
von f oder g wächst asymptotisch nicht schneller als f , wenn es Konstanten
c, n0 ∈ N gibt mit
g(n) ≤ cf (n) für alle n ≥ n0 .
Wir schreiben g(n) = O(f (n)) oder g = O(f ), falls g in der Ordnung von f
ist, und andernfalls g(n) ̸= O(f (n)).

Beispiel. Sei d > 0, ℓ ≥ 1, f (n) = nℓ und g(n) = nℓ + dnℓ−1 . Dann gilt

nℓ + dnℓ−1 ≤ nℓ + nℓ = 2nℓ für n ≥ d.


14
Im Sinne der O–Notation, aber nicht beim praktischen Einsatz von Algorithmen.
15
Edmund Georg Hermann Landau (1877 – 1938) war ein deutscher Mathematiker,
der auf dem Gebiet der analytischen Zahlentheorie arbeitete. Landau hat die O–
Notation bekannt gemacht.
1.2 Laufzeit von Algorithmen 11

Für c = 2 und n0 = d gilt g(n) ≤ cf (n) für n ≥ n0 , d. h. g(n) = O(f (n)). Es


gilt auch
nℓ ≤ nℓ + dnℓ−1 für n ≥ 1.
Für c = 1 und n0 = 1 gilt f (n) ≤ cg(n) für n ≥ n0 , d. h. f (n) = O(g(n)).
Bemerkungen:
1. Unmittelbar aus der Definition folgt die Transitivität der O–Notation:
Aus f (n) = O(g(n)) und g(n) = O(h(n)) folgt f (n) = O(h(n)).
2. g(n) = O(f (n)) vergleicht f und g bezüglich des asymptotischen Wachs-
tums (g wächst asymptotisch nicht schneller als f ).
3. g(n) = O(f (n)) und f (n) = O(g(n)) bedeutet f und g haben dasselbe
asymptotische Wachstum. Es gibt dann Konstanten c1 > 0, c2 > 0 und
n0 ∈ N, sodass gilt c1 g(n) ≤ f (n) ≤ c2 g(n) für alle n ≥ n0 .
4. Wir erweitern die O–Notation auf Funktionen, die von zwei Parametern
(n, m) ∈ D ⊂ N0 × N0 abhängen. Seien

f, g : D −→ R≥0

Funktionen. Wir sagen g(n, m) = O(f (n, m)), wenn es Konstanten


c, n0 , m0 ∈ N gibt mit

g(n, m) ≤ cf (n, m) für alle n ≥ n0 oder m ≥ m0 .

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)

Beispiel. Figur 1.1 zeigt Funktionsgraphen von elementaren Funktionen, die


als Laufzeiten von Algorithmen auftreten.

Fig. 1.1: Elementare Funktionen.

Satz 1.14 (asymptotisches Wachstum elementarer Funktionen). Sei n ∈ N.


nk
1. Sei k ≥ 0 und a > 1. Dann gilt limn→∞ an = 0, also auch nk = O(an ).
loga (n)k
2. Sei k ≥ 0, ℓ > 0 und a > 1. Dann gilt limn→∞ nℓ
= 0, insbesondere
loga (n)k = O(nℓ ).
3. Sei a > 1. Dann gilt an = O(n!).
4. n! = O(nn ).
Beweis.
1. Die allgemeine Exponentialfunktion ist definiert durch an := eln(a)n .
n
Es gilt da n nk/an = 0. Nach der Re-
dn = ln(a)a . Wir zeigen limn→∞
gel von l’Hospital16 für ∞ “ konvergiert ein Quotient von Folgen, falls
”∞
der Quotient der Ableitungen von Zähler und Nenner konvergiert (sie-
he [AmannEscher02, Kap. IV.2]). Die Grenzwerte sind dann gleich. Wir
betrachten
16
Guillaume François Antoine Marquis de L’Hospital (1661 – 1704) war ein französi-
scher Mathematiker.
1.2 Laufzeit von Algorithmen 13

nk knk−1 k(k − 1)nk−2 k(k − 1) · . . . · 1


n
, n
, 2 n
,..., .
a ln(a)a ln(a) a ln(a)k an
Der letzte Quotient konvergiert gegen 0. Damit konvergieren alle Quoti-
enten gegen 0. Dies zeigt die erste Behauptung.
2. Wir setzen loga (n) = m. Dann ist n = am und Punkt 2 folgt aus

loga (n)k mk
lim = lim = 0 (nach Punkt 1).
n→∞ nℓ m→∞ (aℓ )m

3. Sei n0 := ⌈a⌉. Wegen an ≤ an0 n! gilt die dritte Behauptung.


4. Aus n! ≤ nn für n ∈ N folgt die vierte Behauptung.

∑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:

g1 + g2 = O(max(f1 , f2 )), g1 · g2 = O(f1 · f2 ).

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 .

1.3 Lineare Differenzengleichungen

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]).

1.3.1 Lineare Differenzengleichungen erster Ordnung

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

Die beiden Fälle bn = 0 und an = 1 sind einfach durch Expandieren der


rechten Seite der Gleichung zu lösen. Wir erhalten

n
xn = an xn−1 = an an−1 xn−2 = . . . = b ai und
i=2

n
xn = xn−1 + bn = xn−2 + bn−1 + bn = . . . = b + bi .
i=2

Im allgemeinen Fall betrachten wir zunächst die zugeordnete homogene


Gleichung
x1 = b, xn = an xn−1 für n ≥ 2.
Eine Lösung der homogenen Gleichung erhalten wir wie oben:

n
xn = πn b, wobei πn = ai , n ≥ 2, π1 = 1.
i=2

Sei πn ̸= 0 für alle n ∈ N. Die Lösung der inhomogenen Gleichung erhalten


wir mit dem Lösungsansatz:

xn = πn cn , n ≥ 1.

Wir setzen xn = πn cn in die Gleichung ein und erhalten

πn cn = an πn−1 cn−1 + bn = πn cn−1 + bn .

Division durch πn ergibt eine Differenzengleichung für cn


bn
cn = cn−1 + .
πn
Diese lösen wir durch Expandieren der rechten Seite der Gleichung:

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

als Lösung der ursprünglichen Gleichung.

Die diskutierte Methode zur Lösung von linearen Differenzengleichungen


erster Ordnung bezeichnen wir als Methode der Variation der Konstanten.
Insgesamt gilt
16 1. Einleitung

Satz 1.15. Die lineare Differenzengleichung

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

2. Gegeben sei die Gleichung


n+2
(D 1) x1 = c, xn = xn−1 + (an + b), n ≥ 2,
n
1.3 Lineare Differenzengleichungen 17

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)

Multiplikation mit dem Hauptnenner ergibt

ai + b = A(i + 2) + B(i + 1) = (A + B)i + 2A + B.

Ein Koeffizientenvergleich liefert das Gleichungssystem

A + B = a und 2A + B = b.

Dieses besitzt A = b − a und B = 2a − b als Lösungen.


Wir werden diese Gleichung mit (a, b, c) = (1, −1, 0) und (a, b, c) =
(1/6, 2/3, 0) bei der Berechnung der durchschnittlichen Anzahl der Ver-
gleiche und Umstellungen für den Quicksort-Algorithmus (Beweis von
Satz 2.5 und von Satz 2.7) sowie bei der Berechnung der durchschnittli-
chen Pfadlänge in einem binären Suchbaum (Beweis von Satz 4.20) mit
(a, b, c) = (1, 0, 1) verwenden.
3. Wir wenden jetzt lineare Differenzengleichungen an, um die Laufzeit von
Algorithmen zu analysieren. Wir berechnen die Anzahl der Ausgaben des
folgenden Algorithmus HelloWorld.17
17
In Anlehnung an Kernighan und Ritchies berühmtes hello, world“ ([KerRit78]).

18 1. Einleitung

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

4. Gegeben sei folgender Algorithmus:


Algorithmus 1.18.
HelloWorld2(int n)
1 if n >= 1
2 then print(hello, world)
3 for i ← 1 to n − 1 do
4 HelloWorld2(i)
5 print(hello, world)

Wir bezeichnen mit xn die Anzahl der Ausgaben hello, world“ in



Abhängigkeit von n.


n−1
x1 = 1, xn = (xi + n), n ≥ 2.
i=1

Dann gilt

xn − xn−1 = xn−1 + 1, also


xn = 2xn−1 + 1, n ≥ 2, x1 = 1.

Diese Gleichung besitzt die Lösung xn = 2n − 1.


1.3 Lineare Differenzengleichungen 19

1.3.2 Fibonacci-Zahlen

Fibonacci-Zahlen sind nach Fibonacci18 , der als Entdecker dieser Zahlen


gilt, benannt. Fibonacci-Zahlen haben viele interessante Eigenschaften. Un-
ter anderem treten sie bei der Analyse von Algorithmen auf. Wir werden die
Fibonacci-Zahlen im Abschnitt 4.3 verwenden, um eine obere Schranke für
die Höhe eines ausgeglichenen binären Suchbaumes anzugeben.
Definition 1.19. Die Fibonacci-Zahlen sind rekursiv definiert durch

f0 = 0, f1 = 1,
fn = fn−1 + fn−2 , n ≥ 2.

Figur 1.2 veranschaulicht die Entwicklung der Fibonacci-Zahlen durch


einen Wachstumsprozess. Aus einem nicht gefüllten Knoten entsteht in der
nächsten Ebene ein gefüllter Knoten. Aus einem gefüllten Knoten entstehen
in der nächsten Ebene ein nicht gefüllter und ein gefüllter Knoten.
.

..
.

Fig. 1.2: Fibonacci-Zahlen.

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

Dieses besitzt die Lösung


( )
∑n
X n = πn B + πi −1 Bi , n ≥ 1, wobei
i=2
( )
10
πi = Ai · Ai−1 · . . . · A2 , 2 ≤ i ≤ n, , π1 = .
01
Insbesondere besitzt die homogene Gleichung
x0 = u0 , x 1 = u1 ,
xn = vn xn−1 + wn xn−2 , wn ̸= 0, n ≥ 2,
die Lösung
Xn = πn B, n ≥ 1, wobei für n ≥ 2
( )
10
πn = An · An−1 · . . . · A2 , π1 = .
01
Die homogene Differenzengleichung mit konstanten Koeffizienten
x0 = u0 , x 1 = u1 ,
xn = vxn−1 + wxn−2 , w ̸= 0, n ≥ 2
besitzt die Lösung
( )
0 1
Xn = A n−1
B, n ≥ 1, wobei A = .
wv
Bemerkungen:
1. Wir geben unten einen effizienten Potenzierungsalgorithmus an, der die
Matrix An in log2 (n) Schritten berechnet.
2. Die Methode zur Lösung von Differenzengleichungen zweiter Ordnung
können wir auf Differenzengleichungen k–ter Ordnung verallgemeinern.
1.3 Lineare Differenzengleichungen 21

Algorithmus zur Berechnung der n–ten Fibonacci-Zahl. Die Fibon-


acci-Zahlen sind durch eine homogene lineare Differenzengleichung zweiter
Ordnung mit konstanten Koeffizienten definiert. Mit
( ) ( )
fn−1 01
Xn = und A =
fn 11

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

n = 2l−1 nl−1 + 2l−2 nl−2 + . . . + 21 n1 + 20 n0 (mit nl−1 = 1)


= (2l−2 nl−1 + 2l−3 nl−2 + . . . + n1 ) · 2 + n0
= (. . . ((2nl−1 + nl−2 ) · 2 + nl−3 ) · 2 + . . . + n1 ) · 2 + n0

von n. Dann ist l = ⌊log2 (n)⌋ + 1 und


l−1
+2l−2 nl−2 +...+2n1 +n0
A2 = (. . . (((A2 · Anl−2 )2 · Anl−3 )2 · . . .)2 · An1 )2 · An0 .

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.

Im Folgenden geht es um eine geschlossene Lösung für die Fibonacci-


Zahlen.
Satz 1.21. Für die Fibonacci-Zahlen fn gilt
1
fn = √ (gn − ĝn ), n ≥ 0,
5
wobei
1( √ ) 1 1( √ )
g= 1 + 5 und ĝ = 1 − g = − = 1− 5
2 g 2
die Lösungen der Gleichung x2 = x + 1 sind.
Definition 1.22. Die Zahl g heißt das Verhältnis des goldenen Schnitts.19
Beweis. Der Lösungsansatz mit der Exponentialfunktion

fn = q n ,

eingesetzt in die Gleichung xn − xn−1 − xn−2 = 0, liefert


( )
q n − q n−1 − q n−2 = q n−2 q 2 − q − 1 = 0.
19
Eine Strecke ist im Verhältnis des goldenen Schnitts geteilt, wenn das Verhältnis
des Ganzen zu seinem größeren Teil gleich dem Verhältnis des größeren zum
kleineren Teil ist. In Formeln: (x + y)/x = x/y . Dieses Verhältnis hängt nicht
von x und y ab und ist Lösung der Gleichung x2 = x + 1. In der Architektur
werden Proportionen, die dem Verhältnis des goldenen Schnitts genügen, als ideal
erachtet.
1.3 Lineare Differenzengleichungen 23

Die Basis q der Exponentialfunktion ist Nullstelle des quadratischen Poly-


noms X 2 − X − 1. Dieses besitzt die Lösungen g und ĝ. Deswegen gilt

gn − gn−1 − gn−2 = 0 und ĝn − ĝn−1 − ĝn−2 = 0,

d. h. die Funktionen gn und ĝn sind Lösungen der Gleichung xn − xn−1 −


xn−2 = 0. Da die Gleichung linear ist, ist auch

λ1 gn + λ2 ĝn , λ1 , λ2 ∈ R,

eine Lösung. Sie heißt die allgemeine Lösung der Differenzengleichung.


Mit der Anfangsbedingung f0 = 0, f1 = 1 folgt

λ1 g0 + λ2 ĝ0 = 0 ,
λ1 g1 + λ2 ĝ1 = 1 .

Das lineare Gleichungssystem hat die Lösung λ1 = −λ2 = √15 .


Einsetzen von λ1 = −λ2 = √15 in die allgemeine Lösung liefert

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, wobei round(x) zur x am nächsten gelegenen ganzen Zahl rundet.


Insbesondere folgt aus der Formel, dass die Fibonacci-Zahlen exponenti-
elles Wachstum besitzen.
3. Die Berechnung von Fibonacci-Zahlen √ mit der Formel aus Satz 1.21
√ er-
folgt im quadratischen Zahlkörper Q( 5)20 . Die Arithmetik in Q( 5) lie-
fert exakte Ergebnisse. Falls diese nicht implementiert ist, approximiert
man irrationale Zahlen üblicherweise durch Gleitkommazahlen. Diese bie-
ten nur eingeschränkte Genauigkeit und der Rechenaufwand ist im Ver-
gleich zu ganzzahliger Arithmetik höher.
20
Quadratische Zahlkörper sind Gegenstand der Algebraischen Zahlentheorie. Sie
werden auch in [RemUll08] studiert.
24 1. Einleitung

4. Der Quotient zweier aufeinander folgender Fibonacci-Zahlen konvergiert


gegen das Verhältnis g des goldenen Schnitts. Dies folgt, da
( )n+1 ( )n+1
−1
fn+1 g − ĝ
n+1 n+1 1− ĝ
g 1− g2
= =g ( )n = g ( )n
fn gn − ĝn 1− ĝ
1− −1
g g2

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.

Aus den Anfangsbedingungen x0 = x1 = 1 folgt λ1 g0 + λ2 ĝ0 − 1 = 1 und


λ1 g1 + λ2 ĝ1 − 1 = 1. Wir erhalten

2(1 − ĝ) 2g 2(g − 5) 2ĝ
λ1 = √ = √ , λ2 = − √ = −√ .
5 5 5 5
Damit ergibt sich die Lösung
2 2 ( )
xn = √ (ggn − ĝĝn ) − 1 = √ gn+1 − ĝn+1 − 1 = 2fn+1 − 1.
5 5
Die Laufzeit ist somit exponentiell in n. Der Algorithmus von oben, der die
Potenzierungsmethode verwendet, hat logarithmische Laufzeit.
1.4 Die Mastermethode für Rekursionsgleichungen 25

Bemerkung. Wir betrachten eine inhomogene lineare Differenzengleichung k–


ter Ordnung mit konstanten Koeffizienten:

(1.1) xn + ak−1 xn−1 + . . . + a1 xn−k+1 + a0 xn−k = rn , a0 ̸= 0.

Wir ordnen der Gleichung das charakteristische Polynom


P (X) := X k + ak−1 X k−1 + . . . + a1 X 1 + a0
zu.
1. Eine allgemeine Lösung berechnet sich so:
Besitzt P (X) die Nullstellen x1 , . . . , xµ mit den Vielfachheiten α1 , . . . , αµ ,
so ist
∑ µ ∑αi
xn = λij nj−1 xni , λi,j ∈ R,
i=1 j=1

eine allgemeine Lösung der zugeordneten homogenen Gleichung.


2. Der Ansatz für die rechte Seite liefert eine Lösung der inhomogenen Glei-
chung: Ist die rechte Seite rn der Gleichung (1.1) von der Form p(n)an ,
wobei p(n) ein Polynom vom Grad ν ist, so liefert ein Lösungsansatz
nℓ an φ(n) mit einem Polynom φ(n) vom Grad ν mit unbestimmten Koef-
fizienten eine spezielle Lösung. ℓ ist die Vielfachheit der Nullstelle a des
charakteristischen Polynoms.
Die in diesem Abschnitt diskutierten Methoden zur Lösung von rekur-
siven Gleichungen, insbesondere von linearen Differenzengleichungen erster
Ordnung werden wir später bei der Laufzeitberechnung von Quicksort (Ab-
schnitt 2.1.1), von Quickselect (Satz 2.31), zur Bestimmung der durchschnitt-
lichen Pfadlänge bei binären Suchbäumen (Satz 4.20) und im Abschnitt 4.4
zur Analyse der Laufzeit der Zugriffsfunktionen auf probabilistische binäre
Suchbäume (Satz 4.24) sowie bei der Analyse eines probabilistischen Algorith-
mus zur Berechnung eines minimalen Schnitts in einem Graphen (Abschnitt
5.7) anwenden. Eine obere Schranke für die Höhe eines AVL-Baumes geben
wir mithilfe einer Differenzengleichung zweiter Ordnung an (Satz 4.14).

1.4 Die Mastermethode für Rekursionsgleichungen

Der folgende Satz wird in der Literatur mit Mastertheorem“ bezeichnet. Er



ist bei Algorithmen anwendbar, die der Divide-and-Conquer-Strategie folgen
(siehe Abschnitt 1.5.2). Die Laufzeit T (n) solcher Algorithmen erfüllt eine Re-
kursionsgleichung, d.h. der Wert T (n) wird durch die Werte von Argumenten
kleiner n definiert.
Wir betrachten den Fall, dass nur ein kleinerer Wert auftritt, der durch ei-
ne Kontraktion bestimmt ist. Falls nur ganzzahlige Eingabegrößen betrachtet
werden, ist die Kontraktion um Auf- oder Abrunden zu ergänzen.
26 1. Einleitung

Die Methode, die wir zur Lösung von Rekursionsgleichungen verwenden,


erlaubt präzise Aussagen und liefert einen einfachen Beweis. Wir formulieren
zunächst den Begriff der Kontraktion und zwei vorbereitende Lemmata.
Definition 1.24. Sei g : R≥0 −→ R≥0 eine Funktion und 0 ≤ q < 1. g heißt
Kontraktion, wenn für alle x ∈ R≥0 gilt g(x) ≤ qx.
Lemma 1.25. Sei g : R≥0 −→ R≥0 eine Kontraktion, r : R≥0 −→ R≥0 eine
Funktion und b ∈ N. Wir betrachten für x ∈ R≥0 die Rekursionsgleichung
(R0) Tg (x) = d für x < b und Tg (x) = aTg (g(x)) + r(x) für x ≥ b,
wobei a ≥ 1 und d Konstanten aus R≥0 sind. Sei k die Rekursionstiefe von g
bezüglich x und b, d. h. k ist der kleinste Exponent mit g k (x) < b. Dann gilt

k−1
(L0) Tg (x) = ak d + ai r(g i (x)).
i=0

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

Aus der Lösungsmethode – Expandieren der rechten Seite – folgt unmit-


telbar die Eindeutigkeit der Lösung.
Corollar 1.27. Die (geschlossene) Lösung (L1) der Rekursionsgleichung
(R1) ist eindeutig. Sie ist durch die Parameter a, b, c, d und l der Rekur-
sionsgleichung (R1) bestimmt.
Bemerkung. Sei n = bk . Wir setzen xk = T( b ) (bk ). Dann gilt
( )k
x1 = ad + cbl und xk = axk−1 + c bl für k > 1.

Mithilfe der Substitution n = bk haben wir die Rekursionsgleichung (R1)


in eine lineare Differenzengleichung transformiert. Die Lösung dieser Diffe-
renzengleichung stellen wir als Übungsaufgabe (Aufgabe 12). Eine weitere
Methode zur Lösung der Rekursionsgleichung (R1) ergibt sich durch Anwen-
dung der inversen Transformation k = logb (n) (Lemma B.24).
Wir formulieren die Voraussetzungen für die Anwendung des Mastertheo-
rems. Wir zerlegen den Input der Größe n für einen rekursiven Algorithmus A
21
Die Rekursion bricht nach k − 1 = ⌊logb (n)⌋ Schritten ab, ⌊logb (n)⌋ ist die
Rekursionstiefe der Gleichung (R1).
22
alogb (n) = (blogb (a) )logb (n) = (blogb (n) )logb (a) = nlogb (a) .
28 1. Einleitung

⌊ ⌋ ⌈ ⌉
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.

In Abhängigkeit von λ und n ist d′ definiert durch


{
′ d, falls λ = (b − 1)/b oder λ = b/(b − 1) und n ≤ (b − 1)bk−1 ,
d =
ad + cbl , falls λ = b/(b − 1) und n > (b − 1)bk−1 .

Satz 1.28 (Mastertheorem). Sei T( b ) die Funktion aus Lemma 1.26.


1. Es gilt T⌊ b ⌋ (n) ≤ T( b ) (n) ≤ T⌈ b ⌉ (n) für n ∈ N.
2. Für n = nk−1 bk−1 ist die Ungleichung von oben eine Gleichung, d. h. für
T⌊ b ⌋ (n) und T⌈ b ⌉ (n) gelten die Formeln für T( b ) (n) von Lemma 1.26.
( )
3. Weiter ist für n ̸= nk−1 bk−1 durch S n, (b − 1)/b eine untere Schranke
von T⌊ b ⌋ (n) und durch S(n, b/(b − 1)) eine obere Schranke für T⌈ b ⌉ gege-
ben.
4. Für T⌊ b ⌋ (n) und T⌈ b ⌉ (n) gelten die asymptotischen Aussagen für T( b ) (n)
von Lemma 1.26.
Beweis. Die Ungleichung von Punkt 1 folgt aus Lemma 1.25.
Für n = nk−1 bk−1 folgt ⌊ bni ⌋ = ⌈ bni ⌉ = bni , i = 0, . . . , k − 1. Deshalb wird
aus der Ungleichung eine Gleichung.
Wir zeigen jetzt die Behauptung für die untere Schranke von T⌊ b ⌋ (n).
Dazu setzen wir
n − (b − 1)
U (n) = .
b
⌊ ⌋
Dann gilt U (n) ≤ nb . Sei
1.4 Die Mastermethode für Rekursionsgleichungen 29

TU (n) = d für n < b, TU (n) = aTU (U (n)) + cnl für n ≥ b.


Wir setzen
mi−1 + 1 − b
m0 = n und mi = für i ≥ 1.
b
Hier handelt es sich um eine lineare Differenzengleichung mit konstanten
Koeffizienten. Mit Corollar 1.16 folgt23
( )
n 1 b−1 n
mi = i + i − 1 und hieraus ≤ mi für i = 0, . . . , k − 2.
b b b bi
Es gilt U i (n) = mi . Mit Lemma 1.25 und λ = (b − 1)/b erhalten wir

k−2 ∑(
k−2
a )i
TU (n) = ak−1 d + c ai mli ≥ ak−1 d + c(λn)l
i=0 i=0
bl

 dak−1 + c(λn)l q q−1−1 , falls bl ̸= a,
k−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

Mit Lemma 1.25 folgt



k−2 (⌈ n ⌉ )l ∑(
k−2
a )i
T⌈ b ⌉ (n) = d′ ak−1 + c ai ≤ d′ ak−1 + c(nλ)l
i=0
b i i=0
bl
 ( k−1 )

 d′ ak−1 + c(λn)l
q −1
if bl ̸= a,
q−1
=


d′ ak−1 + c(λn)l (k − 1) if bl = a.
Wir ersetzen wieder k durch ⌊logb (n)⌋ + 1 und erhalten die obere Schranke
für T⌈ b ⌉ .
Punkt 4 ergibt sich aus Punkt 1 und 3 mit Lemma 1.26. 2

Bemerkung. Wir fassen das Ergebnis unserer Untersuchungen zusammen.


Die Laufzeit T (n) eines Divide-and-Conquer-Algorithmus,
⌊ ⌋ ⌈ ⌉ der Instanzen der
Größe n in Instanzen der Größe nb oder nb aufteilt, können wir für
n = nk−1 bk−1 exakt bestimmen. Im Fall n ̸= nk−1 bk−1 geben wir Funktionen
an, die T⌊ b ⌋ (n) eng nach unten und T⌈ b ⌉ (n) eng nach oben begrenzen. Die Re-
kursionstiefe der Gleichungen (R1), (R2) und (R3) beträgt für n ∈ N jeweils
⌊logb (n)⌋. Nebenbei folgen die Aussagen über die Ordnung der Laufzeit.
Im folgenden Abschnitt wenden wir das Mastertheorem an.
Der Algorithmus von Strassen zur Multiplikation von Matrizen.
Wir betrachten das Produkt aus zwei quadratischen 2 × 2–Matrizen mit Ko-
effizienten aus einem (nicht notwendig kommutativen) Ring24 :
( ) ( ) ( )
a11 a12 b11 b12 c11 c12
· = .
a21 a22 b21 b22 c21 c22
Das Standardverfahren, das das Skalarprodukt jeder Zeile der ersten mit
jeder Spalte der zweiten Matrix berechnet, erfordert 8 Multiplikation und 4
Additionen der Koeffizienten.
Die folgenden Gleichungen, die Strassen25 in [Strassen69] publiziert hat,
reduzieren die Anzahl der Multiplikationen auf 7.

m1 = f1 · j 1 := (a11 + a22 ) · (b11 + b22 ),


m2 = f2 · j 2 := (a21 + a22 ) · b11 ,
m3 = f3 · j 3 := a11 · (b12 − b22 ),
(S.1) m4 = f4 · j 4 := a22 · (b21 − b11 ),
m5 = f5 · j 5 := (a11 + a12 ) · b22 ,
m6 = f6 · j 6 := (a21 − a11 ) · (b11 + b12 ),
m7 = f7 · j 7 := (a12 − a22 ) · (b21 + b22 ).
24
In der Anwendung handelt es sich um Matrizenringe. Das Ergebnis der Multipli-
kation von zwei Matrizen hängt von der Reihenfolge der Faktoren ab. Matrizen-
ringe sind im Allgemeinen nicht kommutativ.
25
Volker Strassen (1936 – ) ist ein deutscher Mathematiker.
1.4 Die Mastermethode für Rekursionsgleichungen 31

Aus m1 , . . . , m7 berechnet sich das Produkt (cij ) i=1,2 :


j=1,2

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.

Der folgende Algorithmus berechnet das Produkt von zwei quadratischen


n × n–Matrizen A, B mit Koeffizienten aus einem Ring R für n = 2k . Wir
wenden die Reduktion der Anzahl der Multiplikationen von 8 auf 7 rekursiv
an. Dazu zerlegen wir den Input A und B in je vier n/2 × n/2–Matrizen. Jetzt
handelt es sich bei A, B und C um 2 × 2–Matrizen mit n/2 × n/2–Matrizen
als Koeffizienten:
( ) ( ) ( )
A11 A12 B11 B12 C11 C12
· = .
A21 A22 B21 B22 C21 C22

Wir betrachten 2×2–Matrizen über dem Matrizenring der n/2 × n/2–Matrizen,


wo die Gleichungen (S.1) und (S.2) zur Berechnung des Produkts von zwei 2×
2–Matrizen auch gelten. Der Algorithmus ist nach der Divide-and-Conquer-
Entwurfsmethode konzipiert (Abschnitt 1.5.2).
Algorithmus 1.29.
matrix StrassenMult(matrix A[1..n, 1..n], B[1..n, 1..n])
1 if n = 1
2 then return A[1, 1] · B[1, 1]
3 for k ← 1 to 7 do
4 (Fk , Jk ) ← divide(k, A, B)
5 Mk ← StrassenMult(Fk , Jk )
6 C ← combine(M1 , . . . , M7 )
7 return C
Für n = 1 wird das Produkt unmittelbar berechnet. Für n = 2 führt der
Algorithmus die Operationen der Gleichungen (S.1) und (S.2) mit Elementen
aus dem Ring R durch.
Die Berechnung der Inputs (F1 , J1 ), . . . , (F7 , J7 ) für die rekursiven Auf-
rufe erfolgt durch divide gemäß den Gleichungen (S.1). Sie erfordert 10 Ad-
ditionen von quadratischen Matrizen der Dimension n/2. Nach Terminierung
der rekursiven Aufrufe von StrassenMult berechnet combine aus den Teilre-
sultaten den Rückgabewert C (Gleichungen (S.2)). Dafür sind nochmals 8
Additionen von quadratischen Matrizen der Dimension n/2 notwendig.
Die Anzahl der arithmetischen Operationen (Additionen, Subtraktionen
und Multiplikationen) genügt deshalb folgender Rekursionsgleichung
32 1. Einleitung

(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

= 7nlog2 (7) − 6n2 = 7k+1 − 3 · 22k+1 ,

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 Entwurfsmethoden für Algorithmen

Der Entwurf eines Algorithmus erfolgt je nach Problemstellung individuell.


Es gibt jedoch Designprinzipien, die sich bewährt haben und gute Algorith-
men liefern. Oft ist es einfach ein Problem auf ein Problem vom gleichen
26
Die Laufzeit des Standardalgorithmus ist in der Ordnung O(n3 ).
1.5 Entwurfsmethoden für Algorithmen 33

Typ, aber von kleinerem Umfang zu reduzieren. In dieser Situation wenden


wir Rekursion an. Weitere Methoden sind Divide-and-Conquer – oft in Ver-
bindung mit Rekursion – Greedy-Algorithmen, dynamisches Programmieren
und Branch-and-Bound-Algorithmen. In diesem Abschnitt führen wir diese
Entwurfsmethoden anhand von prominenten Beispielen ein.

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

Fig. 1.3: Türme von Hanoi.

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.

Es handelt sich um eine lineare Differenzengleichung erster Ordnung. Diese


besitzt nach Corollar 1.16 die Lösung xn = 2n−1 + 2n−1 − 1 = 2n − 1.

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

wir Rekursion mit der Divide-and-Conquer-Entwurfsmethode an. Der Algo-


rithmus von Karger, Klein und Tarjan – ein probabilistischer Algorithmus
zur Berechnung eines minimalen aufspannenden Baumes in einem Graphen
– setzt Divide-and-Conquer mit Rekursion virtuos ein (Algorithmus 6.50).

1.5.2 Divide-and-Conquer

Bei der Divide-and-Conquer-Strategie zerlegen wir das Problem zunächst in


kleinere unabhängige Teilprobleme. Dabei sollen die Teilprobleme besser zu
beherrschen sein als das Gesamtproblem. Die Teilprobleme lösen wir rekursiv.
Die Lösungen der Teilprobleme setzen wir anschließend zu einer Lösung des
Gesamtproblems zusammen. Die Anwendung dieses Prinzips führt, wie im
folgenden Beispiel der Multiplikation von ganzen Zahlen, zu einem einfache-
ren Algorithmus. Damit ist auch eine wesentliche Verbesserung der Laufzeit
verbunden.
Das Produkt von positiven ganzen Zahlen

n−1 ∑
n−1
c= ci bi und d = d i bi
i=0 i=0

– dargestellt in einem b–adischen Zahlensystem – berechnet sich nach der


Schulmethode folgendermaßen

2n−1
e = cd = e i bi .
i=0

Die Koeffizienten ei berechnen sich aus



ei = c j dk .
j+k=i

Dabei müssen wir Überträge berücksichtigen, d. h. es ist modulo b zu rechnen


und der Übertrag von der vorhergehenden Stelle ist zu beachten.
Bei diesem Verfahren ist jede Ziffer von c mit jeder Ziffer von d zu multi-
pliziert, folglich sind n2 viele Multiplikationen von Ziffern notwendig. Wenn
wir mit Papier und Bleistift rechnen, verwenden wir das Dezimalsystem. Wir
haben n2 viele Ziffern nach dem Einmaleins zu multiplizieren. Falls es sich
bei c und d um große Zahlen handelt, nehmen wir einen Rechner zu Hilfe und
stellen die Zahlen zum Beispiel mit der Basis b = 232 dar, wenn es sich um
einen 32–Bit Rechner handelt. Die Multiplikationen von Ziffern führt dann
der Prozessor des Rechners unmittelbar aus.
Es gibt jedoch Verfahren zur Multiplikation von Zahlen, die schneller sind,
wenn es sich um große Zahlen handelt. Der Algorithmus von Karatsuba28
verwendet die Divide-and-Conquer-Entwurfsmethode (siehe [KarOfm62]).
28
Anatoli Alexejewitsch Karatsuba (1937 – 2008) war ein russischer Mathematiker,
der sich mit Informatik, Zahlentheorie und Analysis beschäftigte.
36 1. Einleitung

Seien c und d 2n–stellige b–adische Zahlen. Um Karatsubas Methode an-


zuwenden, schreiben wir
c = c 1 b n + c 0 , d = d 1 bn + d 0 ,
wobei c0 , c1 , d0 und d1 höchstens n–stellige Zahlen sind. Bei der Berechnung
des Produkts
cd = c1 d1 b2n + (c1 d0 + c0 d1 )bn + c0 d0
reduzieren wir mithilfe eines Tricks die 4 Multiplikationen von n–stelligen
Zahlen auf 3 Multiplikationen. Man berechnet f := (c1 + c0 )(d1 + d0 ). Dann
gilt
c 1 d0 + c 0 d1 = f − c 1 d1 − c 0 d0 .
Dabei besitzen c1 + c0 und d1 + d0 höchstens n + 1 Stellen. Die Berechnung
von f ist etwas aufwendiger als bei n–stelligen Zahlen. Diesen zusätzlichen
Aufwand vernachlässigen wir in der folgenden Betrachtung.
Wir veranschaulichen den Vorteil durch Figur 1.4. Nur die Flächen der
weißen Quadrate sind zu berechnen.

c 0 d1 c 1 d1
d1
d0 + d1

(c0 + c1 )·
(d0 + d1 )
c 0 d0 c 1 d0
d0

c0 + c1

c0 c1

. Fig. 1.4: Reduktion der Multiplikationen von 4 auf 3.

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

M (n) = O(nlog2 (3) ) = O(n1.585 ).

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

Wir betrachten ein Task-scheduling Problem. Gegeben seien Aufgaben

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}

Fig. 1.5: Das Matroid für das Task-scheduling Beispiel.

Satz 1.33. Das Task-scheduling Problem (S, τ ) ist ein Matroid.


Beweis. 1. Es gilt ∅ ∈ τ , folglich ist τ ̸= ∅. Offensichtlich ist τ bezüglich
Teilmengenbildung abgeschlossen. Ein Schedule für die Teilmenge ergibt sich
aus einem Schedule der Obermenge durch Streichen von Tasks.
2. Es bleibt die Austausch-Eigenschaft zu zeigen. Seien A, B ∈ τ und |A| <
|B| =: l. Wir schreiben die Elemente von B in einer zulässigen Reihenfolge:

b1 = (t1 , p1 ), b2 = (t2 , p2 ), . . . , bl = (tl , pl ).

Da jeder Task zu seiner Bearbeitung eine Zeiteinheit benötigt, gilt tj ≥ j,


j = 1, . . . , l.
40 1. Einleitung

Sei k maximal mit bk ̸∈ A, d. h. bk ̸∈ A und bk+1 , . . . , bl ∈ A. |A \


{bk+1 , . . . , bl }| = |A| − (l − k) = |A| − |B| + k < k.
Wir zeigen, dass A ∪ bk ∈ τ gilt und geben dazu einen Schedule für A ∪ bk
an. Wir führen zuerst die Tasks von A \ {bk+1 , . . . , bl } in der (zulässigen)
Reihenfolge von A und dann die Tasks bk , bk+1 , . . . , bl (in dieser Reihenfolge)
aus. Wir führen den Task bj zum Zeitpunkt ≤ j aus, j = k, . . . , l. Wegen
tj ≥ j, j = 1, . . . , l, erhalten wir eine zulässige Reihenfolge für A ∪ {bk }, d. h.
A ∪ {bk } ∈ τ . Dies zeigt die Austausch-Eigenschaft für τ . 2
Sei (S, τ ) ein Matroid mit Gewichtsfunktion w : S −→ R>0 . Der folgen-
de Algorithmus, eine generische Lösung für das Optimierungsproblem für
Matroide, ruft einen Algorithmus Sort zum Sortieren von S nach Gewichten
auf. Sort sortiert absteigend nach Gewichten, d. h.
w(s1 ) ≥ w(s2 ) ≥ . . . ≥ w(sn ).
Algorithmus 1.34.
set Optimum(Matroid (S, τ ))
1 {s1 , . . . , sn } ← Sort(S), O ← ∅
2 for i ← 1 to n do
3 if O ∪ {si } ∈ τ
4 then O ← O ∪ {si }
5 return O

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).

Das Matroid ist im Allgemeinen nicht explizit, sondern nur konzeptionell


gegeben. Der Test O ∪ {si } ∈ τ“ entspricht für das Task-scheduling Problem

der Prüfung: Ist O ∪{si } zulässig für das Task-scheduling Problem, d. h. kann
eine Reihenfolge für die Bearbeitung der Elemente von O ∪ {si } angegeben
werden, sodass wir alle Aufgaben vor dem Abschlusstermin fertigstellen.
Satz 1.35. Sei M = (S, τ ) ein Matroid und w : τ −→ R>0 eine Gewichts-
funktion, dann liefert der Algorithmus Optimum eine optimale Lösung des
Optimierungsproblems für (M, w).
Beweis. Der Algorithmus gibt ein Element O aus τ zurück. Seien S =
{s1 , . . . , sn } und O = {si1 , . . . , sik } nach Gewichten absteigend sortiert und
sei Õ eine optimale Lösung. Wir zeigen zunächst, dass |O| = |Õ| gilt.
Aus |O| > |Õ| folgt mit der Austausch-Eigenschaft, dass es ein s ∈ O \ Õ
gibt mit Õ ∪ {s} ∈ τ . Dies ist ein Widerspruch, da Õ optimal ist.
Aus |O| < |Õ| folgt wieder mit der Austausch-Eigenschaft, dass es ein
s ∈ Õ \ O gibt, sodass O ∪ {s} ∈ τ . Wegen der Vererbungs-Eigenschaft ist
jede Teilmenge von O∪{s} Element von τ . Dann wäre aber s beim Abarbeiten
der Liste L in Zeile 4 des Algorithmus gewählt worden. Ein Widerspruch.
1.5 Entwurfsmethoden für Algorithmen 41

Sei Õ = {sj1 , . . . , sjk } und w(sj1 ) ≥ w(sj2 ) ≥ . . . ≥ w(sjk ). Angenom-


men, es wäre w(Õ) > w(O). Dann gäbe es ein ˜l mit w(sjl̃ ) > w(sil̃ ). Mit
l bezeichnen wir den kleinsten Index mit dieser Eigenschaft. Da S nach Ge-
wichten absteigend sortiert ist, gilt jl < il . Sei A = {si1 , . . . , sil−1 } und
à = {sj1 , . . . , sjl }. Nach der Austausch-Eigenschaft gibt es ein s ∈ à \ A mit
A ∪ {s} ∈ τ . Da w(sj1 ) ≥ w(sj2 ) ≥ . . . ≥ w(sjl ) gilt, folgt w(s) ≥ w(sjl ) >
w(sil ). Ein Widerspruch zur Wahl von sil in Zeile 4 des Algorithmus. Also
gilt w(Õ) = w(O). Dies zeigt, dass O eine optimale Lösung ist. 2
Bemerkung. Für ein τ ⊂ P(S) gelte nur die Vererbungs-Eigenschaft (erste
Bedingung von Definition 1.32). Dann berechnet der Algorithmus 1.34 genau
dann eine optimale Lösung, wenn τ ein Matroid ist ([Schrijver03, Theorem
40.1]).
Greedy-Algorithmen sind der Algorithmus 1.43 zur Lösung des fraktalen
Rucksackproblems, der Huffman-Algorithmus (Abschnitt 4.6.2) zur Daten-
komprimierung, der Algorithmus von Dijkstra (Abschnitt 6.2) zur Berech-
nung von kürzesten Wegen in gewichteten Graphen, die Algorithmen von
Prim (Abschnitt 6.2), Kruskal (Abschnitt 6.3) und Borůvka (Abschnitt 6.4)
zur Berechnung von minimalen aufspannenden Bäumen in gewichteten Gra-
phen. Der LZ77-Algorithmus verwendet eine Greedy-Strategie zum Parsen
von Zeichenketten (Abschnitt 4.6.4).

1.5.4 Dynamisches Programmieren

Bei Anwendung der Methode des dynamischen Programmierens berechnen


wir eine optimale Lösung eines Problems auf einfache Weise aus den opti-
malen Lösungen von Teilproblemen. Diese Methode ist schon lange bekannt.
Eine systematische Behandlung wurde von Bellman32 durchgeführt (siehe
[Bellman57]). Der Schlüssel bei der Entwicklung einer Lösung nach der Me-
thode des dynamischen Programmierens ist, eine rekursive Lösungsformel für
eine optimale Lösung zu finden. Diese Formel bezeichnen wir als Bellmansche
Optimalitätsgleichung.
Dynamisches Programmieren bezieht sich auf eine Lösungsmethode unter
Verwendung von Tabellen und nicht auf eine Programmiertechnik. Ein einfa-
cher Algorithmus, der dieser Methode folgt, ergibt sich, wenn wir die ersten
n Fibonacci-Zahlen iterativ berechnen. Die Tabelle ist ein Array F ib[0..n],
das wir mit F ib[0] = 0 und F ib[1] = 1 initialisieren. Die weiteren Einträge
berechnen wir mittels F ib[i] = F ib[i − 1] + F ib[i − 2] für i = 2, . . . , n.

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.

Das RMQ-Problem. Wir wenden die Entwurfsmethode des dynamischen


Programmierens zur Berechnung einer Lösung für das range minimum query
(RMQ) Problem an. Die hier dargestellte Lösung folgt [BeFa00].
Das RMQ-Problem behandelt, wie der Name vermuten lässt, die Berech-
nung des Minimums in einem Teilbereich eines Arrays von Zahlen. Genauer
ist für jeden Teilbereich ein Index innerhalb des Teilbereichs zu ermitteln, der
eine Position angibt, an der sich das Minimum des Teilbereichs befindet.
Beispiel. Das Minimum des Teilbereichs a[5..12] befindet sich an der Position
10, wie Figur 1.6 zeigt.

.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.

Wir präzisieren zunächst die Problemstellung und führen Notationen ein.


Sei a[1..n] ein Array von Zahlen. Ein Algorithmus rmqa (i, j) für das RMQ-
Problem berechnet für Indizes i und j mit 1 ≤ i ≤ j ≤ n einen Index k mit
i ≤ k ≤ j und
a[k] = min{a[l] | i ≤ l ≤ j}.
Die hier entwickelte Lösung des Problems berechnet eine Nachschlageta-
belle ta [1..n, 1..n], welche für i ≤ j den Index eines Elements mit minimalem
Wert speichert:
ta [i, j] = rmqa (i, j).
Nachdem die Tabelle ta erstellt ist, können wir eine Anfrage mit einem
Tabellenzugriff beantworten. Wegen
min a[i..i] = a[i] und
min a[i..j] = min{min a[i..j − 1], min a[i + 1..j]}
folgt eine rekursive Formel für die Berechnung von ta [i, j]:

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

ta [1, 1] ta [1, 2] ta [1, 3] ...


ta [2, 2] ta [2, 3]
..
ta [3, 3] .
ta [4, 4]
.. ..
. .
ta [n, n]

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]

Die Laufzeit von compTab ist von der Ordnung O(n2 ).

Beispiel. Für a = [7, 4, 5, 11, 3, 1] erhalten wir für die Tabelle ta

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

(i, i + 2k − 1), i = 1, . . . , n und für alle k ≥ 0 mit i + 2k − 1 ≤ n.

Es gilt rmqa (i, i + 2k − 1) = t+


a [i, k] und rmqa (i − 2 + 1, i) = ta [i − 2 + 1, k].
k + k

Die folgende Überlegung zeigt, dass dies ausreicht.


44 1. Einleitung

Für das Indexpaar i < j und k = ⌊log2 (j − i + 1)⌋ gilt


j − i + 1 = 2log2 (j−i+1) < 2⌊log2 (j−i+1)⌋+1 = 2 · 2k
und somit auch
j − 2k + 1 ≤ i + 2k − 1.
Deshalb gilt für das Intervall [i, j]
[i, j] = [i, i + 2k − 1] ∪ [j − 2k + 1, j].
Also gilt mit i′ := rmqa (i, i + 2k − 1) und j ′ := rmqa (j − 2k + 1, j) auch
{
i′ , falls a[i′ ] ≤ a[j ′ ] gilt, und
rmqa (i, j) =
j ′ sonst.
Wir berechnen rmqa (i, j) mithilfe der Anfragen
rmqa (i, i + 2k − 1) und rmqa (j − 2k + 1, j).
Für beide Anfragen ist der Abstand der Indizes eine um 1 verminderte Potenz
von 2.
Wir stellen jetzt für t+
a eine rekursive Formel auf. Wegen
min a[i..i] = a[i] und min a[i..i + 2k − 1] =
min{min a[i..i + 2k−1 − 1], min a[i + 2k−1 ..i + 2k − 1]}, k ≥ 1,
erhalten wir die folgende rekursive Formel für t+
a:

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]

nacheinander zu berechnen und abzuspeichern.


Für die erste Spalte gilt t+ a [i, 0] = i, i = 1, . . . , n. Für die folgenden
Spalten benutzen wir die rekursive Formel. Bei der Berechnung der k–ten
Spalte t+a [i, k] benötigen wir die Werte ta [i, k − 1] und ta [i + 2 , k − 1]
+ + k−1

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

Beispiel. Für a = [7, 4, 5, 11, 3, 1, 14, 17, 2, 6] ist

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

die zu t+a transponierte Matrix.


Für die i–te Zeile der Tabelle ta+ gilt für den größten Spaltenindex li die
Abschätzung i + 2li − 1 ≤ n oder äquivalent dazu li = ⌊log2 (n − i + 1)⌋. Die
Tabelle besitzt daher

n ∑
n
(li + 1) = (⌊log2 (n − i + 1)⌋ + 1)
i=1 i=1

n
= n+ ⌊log2 (i)⌋
i=1

= (n + 1)(⌊log2 (n)⌋ + 1) − 2⌊log2 (n)⌋+1 + 1

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

d(0, 0) = 0, d(i, 0) = i, d(0, j) = j,


d(i, j) = min{d(i, j − 1) + 1, d(i − 1, j) + 1, d(i − 1, j − 1) + [ai ̸= bj ]}.

Dabei ist [ai ̸= bj ] = 1, wenn ai ̸= bj ist, und 0 sonst.


Beweis. Bei der Umwandlung von a1 . . . ai in b1 . . . bj unterscheiden wir die
Fälle
(1) lösche ai (am Ende) und führe a1 . . . ai−1 in b1 . . . bj über,
(2) führe a1 . . . ai in b1 . . . bj−1 über und füge bj an,
(3) führe a1 . . . ai−1 in b1 . . . bj−1 über und ersetze ai durch bj oder
(4) führe a1 . . . ai−1 in b1 . . . bj−1 über und übernehme das letzte Zeichen
unverändert.
Da d(i, j) durch einen der vier Fälle hergestellt wird, gilt d(i, j) ≥ min{d(i, j−
1) + 1, d(i − 1, j) + 1, d(i − 1, j − 1) + [ai ̸= bj ]}.
Da die Umwandlung von a1 . . . ai in b1 . . . bj durch jeden der 4 betrachte-
ten Fälle erfolgen kann und da d(i, j) der Mindestabstand ist, gilt d(i, j) ≤
min{d(i, j − 1) + 1, d(i − 1, j) + 1, d(i − 1, j − 1) + [ai ̸= bj ]}. Dies zeigt die
Behauptung. 2

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.

Wir berechnen die Werte

d(0, 0) d(0, 1) d(0, 2) . . . d(0, m)


d(1, 0) d(1, 1) d(1, 2) . . .
d(2, 0) d(2, 1) . . .
..
.

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 geben den Algorithmus in Pseudocode an.


Algorithmus 1.41.
Dist(char a[1..n], b[1..m])
1 int i, j, d[0..n, 0..m]
2 for i ← 0 to n do
3 d[i, 0] ← i
4 for j ← 1 to m do
5 d[0, j] ← j
6 for i ← 1 to n do
7 for j ← 1 to m do
8 if ai = bj
9 then k ← 0
10 else k ← 1
11 d[i, j] ← Min(d[i, j − 1] + 1,
12 d[i − 1, j] + 1, d[i − 1, j − 1] + k)
Nach der Terminierung von Dist, steht in d[n, m] die Editierdistanz von a[1..n]
und b[1..m].

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

op[i − 1, j], falls D ∈ op[i, j],


op[i, j − 1], falls I ∈ op[i, j] oder
op[i − 1, j − 1], falls R oder T ∈ op[i, j].
Der Pfad – und damit auch die Folge der Operationen – ist nicht eindeutig
bestimmt.
Beispiel. Wir berechnen die Editierdistanz zwischen dabei“ und bereit“
” ”
und eine minimale Folge von Operationen, die dabei“ in bereit“ überführt.
” ”
Wir erhalten die Abstandsmatrix d
d b e r e i t
0 1 2 3 4 5 6
d 1 1 2 3 4 5 6
a 2 2 2 3 4 5 6
b 3 2 3 3 4 5 6
e 4 3 2 3 3 4 5
i 5 4 3 3 4 3 4
und die Operationenmatrix op
op b e r e i t
I I I I I I
d D R I,R I,R I,R I,R I,R
a D D,R R I,R I,R I,R I,R
b D T D,I,R R I,R I,R I,R
e D D T I T I I
i D D D R D,I,R T I
1.5 Entwurfsmethoden für Algorithmen 49

Die Folge von Operationen ist R,R,R,T,T,I.


Bemerkungen:
1. Die berechnete Operationenfolge für ein Wortpaar (v, u) ergibt sich aus
der Operationenfolge für (u, v) durch Vertauschung der Operationen I
(Einfügen) und D (Löschen). Wir überführen mit den Operationen R, R,
R, T, T, D bereit“ in dabei“.
” ”
2. Die für das Wortpaar (a1 . . . an , b1 . . . bm ) berechnete Matrix d enthält
auch die Editierdistanzen sämtlicher Präfixpaare (a1 . . . ai , b1 . . . bj ), i ≤
n, j ≤ m. Entsprechend enthält die Matrix op die Einträge zur Rekon-
struktion einer Operationenfolge für jedes Präfixpaar.
Die Editierdistanz d(dabe, be) = d[4, 2] = 2. Beginnend mit op[4, 2] erhält
man hier die eindeutige Operationenfolge D, D, T, T.
3. Anwendung findet die Editierdistanz und deren Berechnung in der Algo-
rithmischen Biologie. Dort wird mit der Editierdistanz die Ähnlichkeit
von DNA Sequenzen gemessen. Das Verfahren findet auch im Bereich
der Mustererkennung Anwendung. Dort spricht man üblicherweise vom
Levenshtein-Abstand .
Der Algorithmus von Warshall-Floyd ist nach der Entwurfsmethode des dy-
namischen Programmierens konstruiert (Abschnitt 6.7).

1.5.5 Branch-and-Bound mit Backtracking

Branch-and-Bound ist ein grundlegendes Designprinzip für Algorithmen und


in vielen Situationen anwendbar. Die Idee von Branch-and-Bound wurde be-
reits in den 60er Jahren des letzten Jahrhunderts bei der Lösung von ganz-
zahligen Optimierungsproblemen entwickelt.
Branch-and-Bound erfordert, dass der Lösungsraum L des Berechnungs-
problems aus n–Tupeln (x1 , . . . , xn ) ∈ S1 × . . . × Sn besteht, wobei Si ,
i = 1, . . . , n, eine endliche Menge ist. Diese n–Tupeln (x1 , . . . , xn ) ∈ L
sind durch gewisse Bedingungen definiert. Der Lösungsraum wird oft mit
der Struktur eines Baumes versehen (Definition 4.1).
Zum Beispiel können wir Permutationen auf den Mengen {1, . . . , i}, i =
1, . . . , n, einen Baum zuordnen. Wir definieren π2 als Nachfolger von π1 , wenn
π2 die Permutation π1 auf einen Definitionsbereich fortsetzt, der ein Element
mehr enthält. Permutationen auf {1, . . . , n} sind in den Blättern des Baumes
lokalisiert.
Bei Anwendung von Branch-and-Bound durchsuchen wir den Lösungs-
raum nach einer Lösung. Eine Bounding-Funktion beschränkt den Suchraum,
indem sie Teilbäume abschneidet, die eine gewünschte Lösung nicht enthalten
können. Breitensuche, Tiefensuche oder die Festlegung der Reihenfolge nach
Prioritäten definieren verschiedene Besuchsreihenfolgen beim Durchmustern
des Lösungsbaumes.
Beim Backtracking erfolgt die Durchmusterung mittels Tiefensuche (Algo-
rithmus 4.5). Stellen wir durch Anwendung der Bounding-Funktion fest, dass
50 1. Einleitung

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.

Das 8-Damenproblem. Das 8-Damenproblem wurde von Bezzel34 im Jahr


1848 publiziert. Es besteht darin, 8 Damen auf einem Schachbrett so zu po-
sitionieren, dass sich je zwei Damen nicht gegenseitig bedrohen. Genauer
besteht das Problem darin, die Zahl der möglichen Lösungen des Problems
anzugeben. Sogar der berühmte Mathematiker Gauß35 hat sich mit dem Pro-
blem beschäftigt.
Figur 1.7 zeigt eine der 92 Lösungen des 8-Damenproblems.

.
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 ......

......

Fig. 1.8: Der Lösungsbaum für das 8-Damenproblem.

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)

boolean testPosition(int Q[1..n], i, j)


1 for k ← 1 to i − 1 do
2 if Q[k] = j or Q[k] = j + i − k or Q[k] = j − i + k
3 then return false
4 return true

Der Aufruf Queens(Q[1..n], 1) berechnet alle Lösungen des n–Damenprob-


lems und gibt sie mit print(Q) aus. Die Funktion testPosition prüft für die
52 1. Einleitung

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.

Ein PC kann die 92 Lösungen für das 8-Damenproblem in weniger als


einer Sekunde berechnen. Kürzlich ist es einer Arbeitsgruppe an der TU
Dresden gelungen alle Lösungen des 27-Damenproblems zu berechnen. Es
gibt 234.907.967.154.122.528 viele Lösungen. Zur Berechnung wurden Field
Programmable Gate Arrays (FPGA) eingesetzt, die ein massiv paralleles
Rechnen ermöglichen. Diese haben ein Jahr lang mit all ihrer Leerlaufzeit
gerechnet, um das Ergebnis zu erzielen ([Queens@TUD-Team16]).
Das Rucksackproblem. Beim Rucksackproblem sind ein Rucksack und
Gepäckstücke fester Größe gegeben. Das Ziel ist, mit einer geeigneten Aus-
wahl aus allen Gepäckstücken den Rucksack möglichst vollzupacken. Abstrak-
ter und allgemeiner formuliert sich das Problem folgendermaßen:
Gegeben seien m ∈ N, w = (w1 , . . . , wn ) und p = (p1 , . . . , pn ) ∈ Nn .
Gesucht ist x = (x1 , . . . , xn ) ∈ {0, 1}n mit


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

NP und weitere sind Gegenstand der Theoretischen Informatik. Die dar-


an interessierten Leser verweisen wir auf Lehrbücher über Berechenbarkeits-
und Komplexitätstheorie, wie zum Beispiel [GarJoh79], [HopMotUll07] und
[Hromkovič14].
Das Rucksackproblem weist eine lange Geschichte auf. Die wesentlichen
Methoden zur Lösung des Problems sind dynamisches Programmieren und
Branch-and-Bound. Über das Problem wurden viele theoretische und auch
anwendungsorientierte Arbeiten publiziert. Eine umfassende Übersicht ist in
[MartToth90] oder in [KelPisPfe04] zu finden.
Wir schwächen zunächst die Anforderung an die Lösungen ab und lassen
auch Vektoren x = (x1 , . . . , xn ) ∈ [0, 1]n mit Brüchen als Komponenten zu.
In diesem Fall bezeichnen wir das Problem als fraktales Rucksackproblem.
Wir lösen das fraktale Rucksackproblem durch den folgenden Algorithmus
greedyKnapsack. Er geht auf eine Arbeit von Dantzig37 aus dem Jahr 1957
zurück.
Der Algorithmus folgt zur Maximierung des Profits folgender nahelie-
gender Greedy-Strategie: Packe die Gepäckstücke in der absteigenden Rei-
henfolge ihrer Profitdichte wpii , i = 1 . . . , n, ein, genauer, beginne mit dem
Gepäckstück mit der größten Profitdichte, fahre fort die Gepäckstücke in der
absteigenden Reihenfolge ihrer Profitdichte einzupacken, solange die Schran-
ke m noch nicht erreicht ist, und fülle die Restkapazität mit einem Teil
des nächsten Gepäckstücks. Dazu sortieren wir w = (w1 , . . . , wn ) und p =
(p1 , . . . , pn ) so, dass wp11 ≥ wp22 ≥ . . . ≥ wpnn gilt. Die Funktion greedyKnapsack
hat für i = 1 als Aufrufparameter den Gewichtsvektor w[1..n], den Profit-
vektor p[1..n], den mit 0 initialisierten Vektor x[1..n], der nach der Berech-
nung die Lösung enthält, und die Kapazität m des Rucksacks. Die Funktion
greedyKnapsack gibt den größten Index j mit x[j] ̸= 0 und den erzielten
Profit P zurück.
Algorithmus 1.43.
(int, real) greedyKnapsack(int p[i..n], w[i..n], x[i..n], m)
1 index j; int c ← 0, P ← 0
2 for j ← i to n do
3 c ← c + w[j]
4 if c < m
5 then x[j] ← 1
6 P ← P + p[j]
7 else x[j] ← w[j]−(c−m)
w[j]
8 P ← P + p[j] · x[j]
9 break
10 return (j, P )

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

Wir lösen jetzt das 0,1-Rucksackproblem ∑n und nehmen ohne Einschränkung


an, dass wi ≤ m für i = 1, . . . , n und i=1 wi > m gilt. Wir wenden dazu
den Algorithmus 1.43 an und setzen deshalb voraus, dass die Anordnung von
p[1] p[2] p[n]
p[1..n] und w[1..n] eine absteigend sortierte Folge w[1] ≥ w[2] ≥ . . . ≥ w[n]
ergibt.
Wir betrachten folgenden binären Baum B (Definition 4.3). Knoten sind
die Binärvektoren (x1 , . . . , xi ), i = 0, . . . , n. Der Knoten (x′1 , . . . , x′i+1 ) ist
Nachfolger von (x1 , . . . , xi ), wenn x′j = xj für j = 1, . . . i gilt, siehe Figur 1.9.
1.5 Entwurfsmethoden für Algorithmen 55

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]

knapSack(int p[1..n], w[1..n], m)


1 (i, p′′ ) ← greedyKnapsack(p[1..n], w[1..n], x̃[1..n], m)
2 if x̃[i] < 1
3 then x̃[i] ← 0
4 traverse(x[1..1], m, 0)
5 return x̃

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

K 110 101 100 011 010 B

K 1100 K 1010 1001 1000 K 0110 B

11001 11000 B B B K 01100

Fig. 1.9: Lösungsbaum bei 5 Gepäckstücken.

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.

1.6 Probabilistische Algorithmen

Ursprünglich wurden probabilistische Algorithmen hauptsächlich in der Algo-


rithmischen Zahlentheorie und Kryptographie eingesetzt. Wichtige Beispiele
sind die Primzahltests von Solovay-Strassen und Miller-Rabin (siehe zum Bei-
spiel [DelfsKnebl15]). In der Kryptographie sind probabilistische Verfahren
die Schlüsseltechnologie für sichere kryptographische Verfahren (loc. cit.). In-
zwischen gibt es ein weites Anwendungsfeld für probabilistische Methoden.
Sie zählen zu den grundlegenden Techniken zur Konstruktion einfacher und
effizienter Algorithmen. Dieser Abschnitt orientiert sich am Buch Randomi-

zed Algorithms“ von Motwani und Raghavan, das einen guten Einblick in
das Gebiet der probabilistischen Algorithmen vermittelt ([MotRag95]). Eine
Einführung findet man in [Hromkovič04].
Der Kontrollfluss in probabilistischen Algorithmen hängt von Zufallsent-
scheidungen ab, wie zum Beispiel dem Ergebnis eines Münzwurfs. Wenden wir
einen probabilistischen Algorithmus zweimal zur Berechnung für denselben
Input an, so können unterschiedliche Resultate der Münzwürfe zu unterschied-
lichen Ergebnissen der Berechnung führen. Für theoretische Zwecke werden
probabilistische Algorithmen analog zu (deterministischen) Algorithmen mit
58 1. Einleitung

probabilistischen Turingautomaten modelliert (siehe [HopMotUll07]).

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.

Fig. 1.10: Verteilung der Endpunkte von 10.000 Irrfahrten zu je 50 Schritten.

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.

1.6.1 Vergleich von Polynomen

Wir betrachten das Problem, zu entscheiden, ob zwei endliche Folgen von


ganzen Zahlen
α1 , . . . , αn und β1 , . . . , βn
dieselben Elemente enthalten. Wenn wir die beiden Folgen sortieren und
anschließend die Folgen Element für Element vergleichen, haben wir eine
Lösung, die eine Laufzeit der Ordnung O(n log2 (n)) besitzt (siehe Kapitel 2).
Eine effizientere Lösung gewinnen wir, mit den α1 , . . . , αn und β1 , . . . , βn
zugeordneten Polynomen

n ∏
n
f (X) = (X − αi ) und g(X) = (X − βi ).
i=1 i=1

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

Die Wahrscheinlichkeit, dass sich PolyIdent k–mal irrt – bei k unabhängi-


gen Wiederholungen der Wahl von x – ist kleiner gleich 1/2k . Die Erfolgswahr-
scheinlichkeit ist dann größer gleich 1 − 1/2k . Durch geeignete Wahl von k,
erreichen wir, dass sie beliebig nahe bei 1 liegt.

Ein probabilistischer Algorithmus, heißt Monte-Carlo-Algorithmus, wenn


er ein korrektes Ergebnis mit hoher Wahrscheinlichkeit liefert und Las-Vegas-
Algorithmus, wenn er immer ein korrektes Ergebnis liefert. Genauer formu-
lieren wir

Definition 1.49. Sei P ein Berechnungsproblem. Ein probabilistischer Algo-


rithmus A für P heißt
1. Monte-Carlo-Algorithmus, wenn gilt:
a. Das Ergebnis A(x) ist mit hoher Wahrscheinlichkeit richtig.
b. Die Laufzeit ist polynomial.
2. Las-Vegas-Algorithmus, wenn gilt:
a. Das Ergebnis A(x) ist immer richtig.
b. Der Erwartungswert der Laufzeit ist polynomial.

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.

1.6.2 Verifikation der Identität großer Zahlen

Wir demonstrieren den Vorteil der probabilistischen Methode anhand der


Problemstellung Objekte auf Gleichheit zu prüfen. Wir betrachten Objekte
x1 und x2 , die Elemente einer großen Menge X sind. Die Aufgabe besteht
darin festzustellen, ob x1 = x2 gilt. Bei x1 , x2 kann es sich zum Beispiel um
große Zahlen oder um Folgen von Zahlen handeln.
Die Idee ist, eine Hashfunktion

h : X −→ Y

zu verwenden und h(x1 ) = h(x2 ) zu prüfen. Hashfunktionen führen wir im


Abschnitt 3.2 ein. Üblicherweise bilden Hashfunktionen Elemente sehr großer
Mengen X – X kann sogar unendlich viele Elemente besitzen – auf Elemen-
te von Mengen moderater Größe ab. Wir bezeichnen den Hashwert h(x) als
Fingerabdruck von x. Der Vergleich von Elementen in Y ist weniger auf-
wendig. Dies bringt natürlich nur einen Vorteil, wenn die Berechnung von
h(x1 ) und h(x2 ) und der Vergleich h(x1 ) = h(x2 ) weniger kostet als der Ver-
gleich x1 = x2 . Wir werden gleich Situationen kennenlernen, bei denen dies
der Fall ist. Ein anderes Problem besteht darin, dass h(x1 ) = h(x2 ) gelten
kann, ohne dass x1 = x2 gelten muss. In diesem Fall sagen wir (x1 , x2 ) ist
eine Kollision von h (siehe Abschnitt 3.2.2). Die Methode, den Vergleich der
Objekte x1 = x2 auf den Vergleich der Fingerabdrücke h(x1 ) = h(x2 ) zurück-
zuführen, ist nur einsetzbar, wenn für x1 ̸= x2 die Kollisionswahrscheinlich-
keit p(hp (x) = hp (y)) klein ist (siehe Definition 3.5). Zur Konstruktion von
Hashfunktion mit kleiner Kollisionswahrscheinlichkeit dienen universelle Fa-
milien von Hashfunktion (Definition 3.6). Wir können die Universellen Fami-
lien aus dem Abschnitt 3.2.2 beim Test auf Gleichheit einsetzen, wenn nur
Anforderungen an die Hashfunktion gestellt sind, die jene erfüllen.
Seien x und y natürliche Zahlen mit x, y < 2l . Wir wenden jetzt die Idee
mit den Fingerabdrücken an, um festzustellen, ob x = y gilt.
Wir benötigen für die folgende Anwendung Hashfunktionen, die mit der
Multiplikation vertauschen. Da die universellen Familien aus dem Abschnitt
3.2.2 diese Eigenschaft nicht besitzen, geben wir eine geeignete Familie von
Hashfunktionen an.
Wir wählen eine Primzahl p zufällig aus einer geeigneten Menge von Prim-
zahlen P und betrachten die Familie (hp )p∈P ,

hp : {0, 1}l −→ {0, . . . , p − 1}, x 7−→ x mod p.

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

wie dem Miller-Rabin-Test ([DelfsKnebl15, Algorithm A.93]). Dieser Test er-


gibt für zusammengesetzte Zahlen das korrekte Resultat zusammengesetzt“.

Das Resultat Primzahl“ ist jedoch nur mit hoher Wahrscheinlichkeit richtig.

Allerdings können wir durch unabhängige Wiederholungen des Tests errei-
chen, dass die Irrtumswahrscheinlichkeit beliebig klein ist. Probabilistische
Primzahltests sind auch beim Test von großen Zahlen sehr effizient.

Wir geben zunächst die Menge P von Primzahlen an, aus der wir eine
Primzahl zufällig wählen. Sei z eine Zahl und

π(z) = |{p Primzahl | p ≤ z}|.

Nach dem Primzahlsatz38 gilt für große z


z
π(z) ≈ .
ln(z)

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

Dies zeigt die Behauptung. 2

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 .

Wir benutzen jetzt diese probabilistische Methode im Algorithmus Poly-


Ident, um f (x) = g(x) zu testen. Die Berechnung von
( n ) ( n )
∏ ∏
hp (f (x)) = (x − αi ) mod p = (x − αi ) mod p mod p
i=1 i=1

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.

1.6.3 Vergleich mulitivariater Polynome

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

N (fk ) = {(x1 , . . . , xn−1 ) | fk (x1 , . . . , xn−1 ) = 0},

dass |N (fk )| ≤ (d − k) · q n−2 ist. Zu jedem (x1 , . . . , xn−1 ) ∈ N (fk ) gibt es


höchstens q Nullstellen von f (x1 , . . . , xn−1 , Xn ). Zu jedem (x1 , . . . , xn−1 ) ̸∈
N (fk ) ist f (x1 , . . . , xn−1 , Xn ) ein Polynom vom Grad k. Folglich gibt es zu
jedem (x1 , . . . , xn−1 ) ̸∈ N (fk ) höchstens k Nullstelle von f (x1 , . . . , xn−1 , Xn ).
Dann gilt mit l = |N (fk )|

|N (f )| ≤ l · q + (q n−1 − l)k ≤ (d − k) · q n−1 + k · q n−1 = d · q n−1 ,

dies zeigt die Behauptung. 2


Corollar 1.55. Sei F ein endlicher Körper und sei f (X1 , . . . , Xn ) ∈
F[X1 , . . . , Xn ] ein Polynom vom Grad d, d > 0. Dann ist die Wahrschein-
lichkeit, dass ein in Fn zufällig gewähltes Element (x1 , . . . , xn ) eine Nullstelle
von f ist, kleiner gleich d/q .

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

Fig. 1.11: 0-1-Folgen.

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.

1.7 Pseudocode für Algorithmen

Wir formulieren die Algorithmen mit Pseudocode. Pseudocode ist wesentlich


präziser und kompakter als eine umgangssprachliche Formulierung. Pseudo-
code erlaubt uns, die Algorithmen hinreichend klar zu formulieren, ohne auf
die Details einer Implementierung in einer konkreten Programmiersprache
– wie zum Beispiel C oder Java – eingehen zu müssen. Auf der Basis von
Pseudocode erfolgen Überlegungen zur Korrektheit und Berechnungen der
Laufzeit.
Wir vereinbaren folgende Notation:
1. In unserem Pseudocode gibt es Variablen und elementare Datentypen
wie in Java oder C.
2. Der Zuweisungsoperator ist ←“, der Vergleichsoperator ist =“.
” ”
3. Wir verwenden Kontrollstrukturen, wie in den Programmiersprachen Ja-
va oder C. Im einzelnen geht es um for- und while-Schleifen, bedingte
Anweisungen (if-then-else) und den Aufruf von Funktionen. Anders als
in Java oder C terminieren for-Schleifen immer. Nach dem Verlassen der
Schleife hat die Laufvariable den Wert, der zum Abbruch geführt hat.
4. Wir machen die Blockstruktur durch Einrücken sichtbar. Da die Codie-
rung der betrachteten Algorithmen stets auf eine Seite passt, verzichten
wir darauf, Blockanfang und Blockende konventionell zu markieren. So
umfasst im folgenden Algorithmus, dem Algorithmus von Euklid43 , der
den größten gemeinsamen Teiler von Zahlen a und b berechnet, der Schlei-
fenrumpf der while-Schleife die eingerückten Zeilen 2, 3 und 4.
Algorithmus 1.56.
int gcd(int a, b)
1 while b ̸= 0 do
2 r ← a mod b
3 a←b
4 b←r
5 return |a|

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

5. Elementare Datentypen sind value types, d. h. eine Variable enthält das


elementare Datenobjekt.
6. Zusammengesetzte Datentypen fassen mehrere Variablen zusammen, die
möglicherweise von unterschiedlichen Typen sind. Sie sind reference types.
Eine Variable enthält eine Referenz (Zeiger) auf das Datenobjekt. Der
Zugriff auf eine Komponente erfolgt durch den Punktoperator .“. Neben

der Deklartion eines reference types muss mit dem new-Operator die
Zuteilung von Speicher für das Datenobjekt erfolgen.
Wir zeigen die Definition von zusammengesetzte Datentypen anhand der
Definition eines Listenelements:
type listElem = struct
char c
listElem next
Ein Datenobjekt vom Typ listElem besteht aus den Komponenten c und
next. Die Variable c kann ein Zeichen und next kann eine Referenz auf ein
Datenobjekt vom Typ listElem speichern. Die Definition erlaubt Selbst-
referenzierung.
Wir benutzen jetzt Datenobjekte vom Typ ListElem um die Zeichen A,
B und C in einer verketteten Liste abzuspeichern, wie Figur 1.12 zeigt.

A B C

Fig. 1.12: Verkettete Liste.

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.

1.8 Lehrbücher zu Algorithmen und Datenstrukturen

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

a. Veranschaulichen Sie die Arbeitsweise des Algorithmus anhand ge-


eigneter Inputarrays.
b. Zeigen Sie, dass InsertionSort korrekt ist.
3. Gegeben seien Algorithmen A1 , A2 und A3 , welche die Laufzeiten

T1 (n) = c1 n, T2 (n) = c2 n3 und T3 (n) = c3 2n

haben, wobei die ci konstant sind, i = 1, 2, 3. Für jeden Algorithmus be-


zeichne mi , i = 1, 2, 3, die maximale Größe der Eingabe, die innerhalb
einer fest vorgegebenen Zeit T auf einem Rechner R verarbeitet werden
kann. Wie ändern sich die Zahlen mi , i = 1, 2, 3, wenn der Rechner R
durch einen k–mal schnelleren Rechner ersetzt wird?

4. Ordnen Sie die folgenden Funktionen in aufsteigender Reihenfolge bezüg-


lich des asymptotischen Wachstums an. Hierzu schätzen Sie das asym-
ptotische Wachstum einer jeden Funktion. Dann vergleichen Sie zwei auf-
einander folgende Glieder der Folge mithilfe einer Rechnung. Bei allen
Rechnungen sind nur die Rechenregeln für Brüche, Potenzen und Loga-
rithmen zu verwenden.
n
f1 (n) = n, f7 (n) = ,
√ log2 (n)
f2 (n) = n, √
f8 (n) = n log2 (n)2 ,
f3 (n) = log2 (n), f9 (n) = (1/3)n ,

f4 (n) = log2 ( n), f10 (n) = (3/2)n ,
f5 (n) = log2 (log2 (n)), √
f11 (n) = log2 (log2 (n)) log2 (n),
f6 (n) = log2 (n)2 , f12 (n) = 2f11 (n) .
72 1. Einleitung

5. Für welche (i, j) gilt fi = O(fj )?

f1 (n) = n2 . f2 (n) = n2 + 1000n.


{ {
n, falls n ungerade, n, falls n ≤ 100 ist,
f3 (n) = f4 (n) =
n3 sonst. n3 sonst.

√ ( )
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.

9. Bestimmen Sie mithilfe von Differenzengleichungen, wie oft die Zeichen-


kette Hello!“ durch den folgenden Algorithmus (in Abhängigkeit von n)

ausgegeben wird.
Algorithmus 1.58.
DoRec(int n)
1 if n > 0
2 then for i ← 1 to n − 1 do
3 DoRec(i)
4 print(Hello!), print(Hello!)

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!)

Setzen Sie zur Lösung Differenzengleichungen ein.


Übungen 73


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

x1 = ad + cbl , xk = axk−1 + c(bl )k für k > 1.

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.

16. Implementieren Sie den Algorithmus Optimum für das Task-scheduling


Problem.
17. Ein Betrag von n (Euro-)Cent soll ausbezahlt werden. Dabei sollen die
Münzen so gewählt werden, dass die Anzahl der Münzen minimiert wird.
Entwickeln Sie dafür einen Greedy-Algorithmus und zeigen Sie, dass die
Greedy-Strategie zum Erfolg führt.
18. Satz 1.39 enthält eine rekursive Formel zur Berechnung der Editierdi-
stanz. Setzen Sie die Formel in eine rekursive Funktion um und geben
Sie eine untere Schranke für die Laufzeit an.
19. Sei a1 . . . an eine Zeichenkette. Eine Teilkette von a1 . . . an entsteht, in-
dem wir Zeichen in der Kette a1 . . . an löschen. Entwickeln Sie einen Algo-
rithmus nach der Methode des dynamischen Programmierens, der zu zwei
Zeichenketten a1 . . . an und b1 . . . bm die Länge der größten gemeinsamen
Teilkette berechnet.
∑j
20. Sei a1 . . . an eine Folge ganzer Zahlen. f (i, j) = k=i ak . Gesucht ist

m := max f (i, j).


i,j

Entwickeln Sie einen Algorithmus nach der Methode des dynamischen


Programmierens, der m berechnet.
21. Seien a = a1 . . . an und b = b1 . . . bm , m ≤ n, Zeichenketten. Das Problem
besteht nun darin zu entscheiden, ob b ein Teilstring von a ist, und falls
dies der Fall ist, das kleinste i mit ai . . . ai+m−1 = b1 . . . bm anzugeben.
Dieses Problem wird mit pattern matching“ in Zeichenketten bezeichnet.

74 1. Einleitung

Entwickeln Sie einen probabilistischen Algorithmus zur Lösung dieses


Problems und verwenden Sie dabei die Technik der Fingerabdrücke.
2. Sortieren und Suchen

Sei a1 , . . . , an eine endliche Folge. Die Folgenglieder sollen Elemente ei-


ner geordneten Menge sein. Die Ordnungsrelation bezeichnen wir mit ≤.
Gesucht ist eine Permutation π der Indizes {1, . . . , n}, sodass die Folge
aπ(1) ≤ aπ(2) ≤ . . . ≤ aπ(n) aufsteigend angeordnet ist. Genauer sind wir an
einem Algorithmus interessiert, welcher diese Anordnung herbeiführt. Derar-
tige Algorithmen bezeichnen wir als Sortierverfahren.
Bei Sortierverfahren unterscheidet man zwischen Sortierverfahren, die Da-
ten im Hauptspeicher, und denen, die Daten auf dem Sekundärspeicher sor-
tieren. Wir betrachten in diesem Abschnitt nur Sortierverfahren für Daten im
Hauptspeicher. Die zu sortierenden Elemente sind in einem Array a abgelegt.
Die Sortierung soll im Array a (bis auf Variable möglichst ohne zusätzlichen
Speicher) durch Umstellung von Elementen aufgrund von Vergleichen erfol-
gen. Ein Maß für die Effizienz der Algorithmen ist die Anzahl der Vergleiche
und Umstellungen in Abhängigkeit von n. n bezeichnet die Länge des Arrays.
Bei Sortierverfahren für Daten im Hauptspeicher unterscheiden wir zwi-
schen den einfachen Sortierverfahren wie Sortieren durch Auswählen (selecti-
on sort, Algorithmus 1.1), Sortieren durch Einfügen (insertion sort , Algorith-
mus 1.57) und Sortieren durch Austauschen (bubble sort, Algorithmus 2.32)
und den effizienteren Verfahren Heapsort und Quicksort. Die einfachen Ver-
fahren besitzen eine Laufzeit von der Ordnung O(n2 ). Quicksort und Heapsort
besitzen eine Laufzeit von der Ordnung O(n log2 (n)), Quicksort im Mittel
und Heapsort im schlechtesten Fall. Wir behandeln sie ausführlich in diesem
Kapitel.
Eine wichtige Anwendung der Sortieralgorithmen besteht darin, durch
Sortieren die nachfolgende Suche zu vereinfachen. Unsortierte Arrays erfor-
dern die sequenzielle Suche. In sortierten Arrays können wir binär suchen.
Die Ordnung der Laufzeit verbessert sich dadurch beachtlich, nämlich von
O(n) auf O(log(n)). Neben der sequenziellen und binären Suche behandeln
wir auch das Problem, das k–kleinste Element einer endlichen Folge zu finden.
Dieses Problem können wir lösen, indem wir die Folge zuerst sortieren und
anschließend auf das k–te Element zugreifen. Quickselect jedoch stellt eine
wesentlich effizientere Lösungen bereit.

© 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]

a[1..3] a[5..4] a[6..7] a[9..8]

a[1..1] a[3..3] a[6..6] a[8..7]

Fig. 2.1: Aufrufhierarchie für Quicksort.

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
.. ..
. .

Fig. 2.2: Der beste und der schlechteste Fall.

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

T (n) ≤ b2⌊log2 (n)⌋ + cn⌊log2 (n)⌋,

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.

Die durchschnittliche Laufzeit. Die Analyse für die durchschnittliche


Laufzeit erfolgt unter der Annahme, dass alle Elemente paarweise verschieden
sind. Wir bezeichnen die zu sortierenden Elemente in sortierter Reihenfolge
mit a1 < a2 < . . . < an . Die Wahrscheinlichkeit, dass ai an der letzten Po-
sition n steht, beträgt n1 , denn es gibt (n − 1)! viele Permutationen π mit
π(i) = n und n! viele Permutationen insgesamt und (n−1)! n! = n1 .
Falls ai an der letzten Position steht, zerlegt sich {a1 , a2 , . . . , an } in
{a1 , . . . , ai−1 }, {ai+1 , . . . , an } und {ai }. Wir zeigen zunächst, dass nach dem
Partitionieren Gleichverteilung auf {a1 , . . . , ai−1 } und {ai+1 , . . . , an } vorliegt.
Nach der Zerlegung mit dem Pivotelement ai erhalten wir – vor Ausführ-
ung von Zeile 11 – die Folge

(2.1) aπ(1) , aπ(2) , . . . , aπ(i−1) , aπ̃(i+1) , aπ̃(i+2) , . . . , aπ̃(n) , ai .

π 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

viele Ausgangsfolgen, die nach dem Partitionieren mit j Vertauschungen zur


Folge (2.1) führen.
Sei m = min{i − 1, n − i}. Die Anzahl l aller Folgen die nach dem Parti-
tionieren die Folge (2.1) als Ergebnis haben ist
82 2. Sortieren und Suchen

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.

V (n) ergibt sich als Mittelwert der Ṽ (n, i), i = 1, . . . , n:

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

Die Zufallsvariable Ũ (n, i) ist hypergeometrisch verteilt. Für den Erwartungs-


wert E(Ũ (n, i)) gilt nach Satz A.24
n−i
E(Ũ (n, i)) = (i − 1) .
n−1
Mit der letzten Umstellung, die ai auf die i–te Position bringt (Zeile 11 im
Algorithmus 2.1) ergibt sich (i−1)(n−i)
n−1 + 1.
84 2. Sortieren und Suchen

Lemma 2.6. Die durchschnittliche Anzahl der Umstellungen (ohne Rekursi-


on) gemittelt über alle Pivotelemente ist für n ≥ 2

1∑
n
n+4
E(Ũ (n, i) + 1) = .
n i=1 6

Beweis. Die durchschnittliche Anzahl der Umstellungen (ohne Rekursion) ge-


mittelt über alle Pivotelemente

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

Damit ist die Behauptung gezeigt. 2

Für die durchschnittliche Anzahl der Umstellungen Ũ (n, i) bei Pivotele-


ment ai erhalten wir
(i − 1)(n − i)
Ũ (n, i) = U (i − 1) + U (n − i) + + 1.
n−1
Für die durchschnittliche Anzahl der Umstellungen U (n) ergibt sich

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

(Seite 16, Gleichung (D 1)). Wir erhalten


2 n+4 1 1 5
U (n) = xn − xn−1 = xn−1 + = (n + 1)Hn − n − .
n 6 3 9 18
Wir halten das Ergebnis im folgenden Satz fest.
Satz 2.7. Für die durchschnittliche Anzahl von Umstellungen U (n) ergibt
sich für n ≥ 2
1 1 5
U (n) = (n + 1)Hn − n − .
3 9 18
Bei der Berechnung der durchschnittlichen Anzahl mitteln wir über alle An-
ordnungen des zu sortierenden Arrays.

Bemerkung. Im Algorithmus 2.1 rufen wir in Zeile 11 auch für l = j exchange


auf. Die Berechnung der Formel für U (n) für den modifizierten Algorithmus,
der diesen unnötigen Aufruf vermeidet, ist eine Übungsaufgabe (Aufgabe 5).
Analog zu den Formeln in Satz 2.5 und Satz 2.7 erhalten wir eine Formel
für die durchschnittliche Laufzeit T (n) von QuickSort:
1 1
T (n) = 2c(n + 1)Hn + (2b − 10c)n + (2b − c).
3 3
b und c sind konstant. Den Durchschnitt bilden wir über alle Anordnungen des
zu sortierenden Arrays. Insbesondere gilt T (n) = O(n ln(n)). Die Ausführung
der Berechnung stellen wir als Übungsaufgabe (Aufgabe 6).

2.1.2 Speicherplatzanalyse

Die Implementierung von Funktionsaufrufen verwendet einen Stackframe auf


einem Speicherbereich, dem Stack , den das Betriebssystem bereitstellt. Der
Aufruf der Funktion belegt den Stackframe und bei der Terminierung der
Funktion wird er wieder freigegeben. Der Stackframe dient unter anderem
zum Speichern der Übergabeparameter, der lokalen Variablen und der Rück-
sprungadresse. Ein Funktionsaufruf ist aktiv zu einem Zeitpunkt t, wenn der
Aufruf vor dem Zeitpunkt t erfolgte und zum Zeitpunkt t noch keine Termi-
nierung stattgefunden hat. Für jeden aktiven Aufruf belegt sein Stackframe
86 2. Sortieren und Suchen

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.

Beweis. Es gilt S(1) = 1.


⌈ ⌉ ⌊ ⌋
1. Der beste Fall tritt ein, falls⌊sich ⌋die n
⌊ −⌋1 Elemente
⌈ ⌉ in ⌊n−1
2⌋ und n−1
2
Elemente aufteilen. Es gilt n−1 2 ≤ n2 und n−1 2 = n2 . Da S mono-
ton wachsend ist, gilt
(⌊ n ⌋)
S(n) ≤ S + 1.
2
Wir ersetzen ≤ durch = und erhalten
(⌊ n ⌋)
S(n) = S + 1.
2
Mit Satz 1.28 folgt, dass S(n) ≤ ⌊log2 (n)⌋ + 1 gilt .
2. Im schlechtesten Fall wird in jedem Rekursionsschritt eine einelementige
Teilmenge abgespalten. Es folgt

S(n) = S(n − 1) + 1 = . . . = S(1) + n − 1 = n.

Dies zeigt die Behauptung des Satzes. 2


Bemerkung. Der Speicherverbrauch variiert beachtlich. Wir können Quick-
Sort so implementiert, dass der beste Fall stets eintritt. Dazu erfolgt zuerst
der rekursive Aufruf für den kleineren Teil der Zerlegung. Der zweite rekur-
sive Aufruf erfolgt als letzte Anweisung in der Funktion. Es handelt sich um
eine Endrekursion. Endrekursionen werden ganz allgemein durch Sprungan-
weisungen mit Wiederverwendung des Speichers für die Variablen auf dem
Stack eliminiert (Übungen, Aufgabe 11). Wir eliminieren die Endrekursion.
Für die so gewonnene Implementierung ist die Anzahl der aktiven Aufrufe
durch ⌊log2 (n)⌋ + 1 beschränkt.

2.1.3 Quicksort ohne Stack

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

zu benutzen, d. h. wir benötigen neben lokalen Variablen keinen zusätzli-


chen Speicher. Das Problem, das dabei zu lösen ist, besteht darin, dass wir
unmittelbar nach der Zerlegung nur einen Teil verarbeiten können. Informa-
tionen über den anderen Teil müssen wir für die spätere Verarbeitung zwi-
schenspeichern. Verarbeiten wir den rechten Teil zuerst, so müssen wir den
Anfangsindex des linken Teils zwischenspeichern. Die Idee besteht nun dar-
in, diese Information im Array selbst zu hinterlegen. Die Quicksort-Variante
ohne Stack ist in [Ďurian86] publiziert.
Wir erläutern nun die Idee von Durians Algorithmus.
1. Wir betrachten eine Zerlegung, die sich geringfügig von der Zerlegung im
Algorithmus 2.1 unterscheidet (siehe die Quicksort Variante, Übungen
Aufgabe 9). Die Zerlegung für das Teilarray a[i..j] erfolgt mithilfe eines
Pivotelements x. Wir ordnen die Elemente von a so um, dass a[k] < x
für i ≤ k ≤ l − 1, a[l] = x und a[k] ≥ x für l + 1 ≤ k ≤ j.
2. Sei a[i..j] rechter Teil bei der Zerlegung von a[g..j] und x = a[i] das
Pivotelement für diese Zerlegung. Die Elemente links von der Position i
sind kleiner als x, insbesondere gilt a[i − 1] < x und die Elemente rechts
von der Position i sind ≥ x d. h. x ≤ a[k], i ≤ k ≤ j.
3. Wir zerlegen jetzt a[i..j] mit dem Pivotelement y in die Teile a[i..l−1] und
a[l..j], siehe Figur 2.3. Wir speichern das Pivotelement y in der Position
l und vertauschen a[i] mit a[l − 1]. Aus l bestimmen wir i durch Suchen.
An der Position i − 1 steht das erste Element (links von der Position
l) das kleiner als x ist (x steht an der Position l − 1). Dieses Element
ermitteln wir durch sequentielle Suche.

x
.
x y
i l j

Fig. 2.3: Zerlegung mit zusätzlicher Information.

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.

2.1.4 Probabilistisches Quicksort

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

und der Linearität des Erwartungswertes ergibt sich

E(Tn | R = r) = E(Tr−1 ) + E(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

Wir erhalten analog zur durchschnittlichen Laufzeit von Quicksort


Satz 2.10. Für den Erwartungswert der Laufzeit Tn von P robQuickSort gilt:
1 1
E(Tn ) = 2c(n + 1)Hn + (2b − 10c)n + (2b − c).
3 3
b und c sind konstant. Insbesondere gilt E(Tn ) = O(n ln(n)).
Bemerkung. Analog ergibt sich für die Zufallsvariablen Vn der Anzahl der
Vergleiche:
E(Vn ) = 2(n + 1)Hn − 4n.
Für die Varianz Var(Vn ) gilt die Formel

Var(Vn ) = 7n2 − 4(n + 1)2 H(2)


n − 2(n + 1)Hn + 13n,

(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.

2.2.1 Binäre 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

Fig. 2.4: Heap als Baum.

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

Fig. 2.5: Einsickern der Wurzel.

Heapsort arbeitet in zwei Phasen, um ein Array zu sortieren. In der ersten


Phase ordnen wir die Elemente von a so um, dass a die Heapbedingung erfüllt
(BuildHeap). In der zweiten Phase findet der eigentliche Sortiervorgang statt.
Das Minimum befindet sich nach der ersten Phase an der ersten Position von
a (in der Wurzel des Baumes). DownHeap kommt beim Heapaufbau durch
den Algorithmus BuildHeap zum Einsatz. Dies zeigen wir zunächst anhand
eines Beispiels.
Beispiel. Figur 2.6 zeigt den Heapaufbau mit der Folge 50, 40, 7, 8, 9, 18, 27,
10, 30, 17, 33.

.
50

40 7

8 9 18 27

10 30 17 33

Fig. 2.6: Die Heapbedingung ist in den Knoten 40 und 50 verletzt.

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

Fig. 2.7: Herstellung der Heapbedingung.

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])

2.2.2 Die Sortierphase von Heapsort

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])

Satz 2.15. Heapsort sortiert das Array a in umgekehrter Reihenfolge.


Beweis. Der Beweis folgt sofort per vollständiger Induktion. 2
Bemerkung. Ersetzen wir in Zeile 5 im Algorithmus 2.12 > durch < und in
Zeile 7 ≤ durch ≥, so sortieren wir a aufsteigend.

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

T (n) ≤ cI1 (n),

c konstant. Insbesondere gilt: T (n) = O(n).


Beweis. Mit Lemma B.16 folgt
⌊n/2⌋ ⌊ ( n )⌋ ⌊n/2⌋ (n)
∑ ∑
I1 (n) ≤ log2 ≤ log2
l l
l=1 l=1
⌊n⌋ ⌊n/2⌋

= log2 (n) − log2 (l)
2
l=1
⌊n⌋ ⌊n/2⌋

≤ log2 (n) − ⌊log2 (l)⌋
2
l=1
⌊n⌋ ((⌊ n ⌋ ) (⌊ n ⌋) ( ))
− 2 2⌊log2 (⌊ 2 ⌋)⌋ − 1
n
= log2 (n) − + 1 log2
2 ( ) 2 2
⌊n⌋ n (⌊ n ⌋) ⌊n⌋
≤ log2 ⌊ n ⌋ − log2 +2 −2
2 2
2 2
⌊n⌋ (⌊ n ⌋)
≈3 − log2 − 2.6
2 2
Dies zeigt Punkt 1 der Behauptung, aus dem unmittelbar Punkt 2 folgt. 2
Lemma 2.18. Sei n ≥ 2. Für die Anzahl I2 (n) der Iterationen der while-
Schleife in DownHeap, akkumuliert über alle Aufrufe von HeapSort in Zeile
6, gilt
I2 (n) ≤ n⌊log2 (n − 1)⌋ − n + 2.
Beweis. Mit Lemma B.16 und wegen n ≤ 2 · 2⌊log2 (n−1)⌋ folgt


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

Die Behauptung des Lemmas ist gezeigt. 2

Wir fassen das Ergebnis der Laufzeitanalyse für HeapSort im folgenden


Satz zusammen.
Satz 2.19.
1. Für die Laufzeit (im schlechtesten Fall) von HeapSort, die wir mit T (n)
bezeichnen, gilt

T (n) = c1 (I1 (n) + I2 (n)) + c2 (n − 1)


( ⌊n⌋ (⌊ n ⌋) )
≤ c1 3 − n + n⌊log2 (n − 1)⌋ − log2 + 2 + c2 (n − 1),
2 2
wobei c1 und c2 konstant sind. Insbesondere gilt: T (n) = O(n log2 (n)).
2. Für die Anzahl V (n) der wesentlichen Vergleiche in Heapsort gilt

V (n) = 2(I1 (n) + I2 (n))


( ⌊n⌋ (⌊ n ⌋) )
≤2 3 − n + n⌊log2 (n − 1)⌋ − log2 +2 .
2 2

2.2.4 Heapsort Optimierungen

Den Pfad, der beim Einsickern eines Elementes x in DownHeap (Algorithmus


2.12) entsteht, bilden wir, indem wir in jedem Knoten als nächsten Knoten
den kleineren Nachfolger wählen. Auf diesem Pfad des kleineren Nachfolgers,
der von der Wurzel des Baumes bis zu einem Blatt führt, liegt die Einfügestel-
le für x. Der Algorithmus DownHeap ermittelt in einem Schritt den nächsten
Knoten und prüft, ob die Einfügestelle bereits erreicht ist. In jeder Iteration
der inneren Schleife finden zwei Vergleiche statt (Zeile 5 und 7 in Algorithmus
2.12).
Die Idee, den Algorithmus zu beschleunigen, besteht nun darin, zunächst
den Pfad des kleineren Nachfolgers – von der Wurzel, bis zum Blatt – zu
berechnen (siehe [Carlson87]). Dies erfordert in jeder Iteration der inneren
Schleife nur einen Vergleich.

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

Fig. 2.8: Der Pfad des kleineren Nachfolgers.

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

Fig. 2.9: Binäre Suche der Einfügestelle.

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.

2.2.5 Vergleich von Quicksort und Heapsort


Wir haben die Anzahl der wesentlichen Vergleiche im schlechtesten Fall für
zwei Heapsort-Varianten abgeschätzt. Für Quicksort ist die durchschnittliche
2.3 Eine untere Schranke für Sortieren durch Vergleichen 101

Anzahl von Vergleichen 2(n + 1)Hn − 4n (Satz 2.5). Die Funktionsgraphen,


die Figur 2.10 zeigt, stellen die Schranken für die Anzahl der wesentlichen
Vergleiche für Heapsort und die durchschnittliche Anzahl von Vergleichen für
Quicksort dar.
Die Funktionsgraphen zeigen, dass Heapsort mit binärer Suche dem
gewöhnlichen Heapsort klar überlegen ist. Die Kurve für Quicksort ergibt die
geringste Anzahl von Vergleichen. Wir betonen aber nochmals, dass es sich
bei Heapsort um Abschätzungen im schlechtesten Fall handelt, bei Quicksort
um die exakte Lösung für die durchschnittliche Anzahl der Vergleiche.

Fig. 2.10: Vergleich der Sortiermethoden.

2.3 Eine untere Schranke für Sortieren durch


Vergleichen
Quicksort und Heapsort setzen eine geordnete Menge voraus, d. h. auf die
zu sortierenden Elemente lässt sich der ≤–Operator anwenden. Die beiden
genannten Algorithmen stellen Elemente aufgrund eines Vergleichs mit dem
≤–Operator um. Vergleichsbasierte Sortieralgorithmen verwenden nur den
≤–Operator und keine strukturellen Eigenschaften der zu sortierenden Ele-
mente. In den Übungen, Aufgabe 1, behandeln wir Situationen, die Sortieren
mit der Laufzeit O(n) ermöglichen. Die Algorithmen benutzen strukturelle
Eigenschaften der zu sortierenden Elemente.
Für vergleichsbasierte Sortieralgorithmen ist das Sortieren der Elemente
a[1], . . . , a[n], a[i] ̸= a[j] für i ̸= j, äquivalent zur Bestimmung einer Permu-
tation auf {1, . . . , n}. Diese Permutation π liefert die sortierte Reihenfolge:

a[π(1)] < a[π(2)] < . . . < a[π(n)].


102 2. Sortieren und Suchen

Wir ermitteln π mithilfe eines binären Entscheidungsbaumes. Dabei findet in


jedem Knoten ein Vergleich mit dem <– Operator statt. Ein Blatt gibt die
sortierte Reihenfolge der Elemente an. Ein binärer Entscheidungsbaum für n
paarweise verschiedene Elemente hat n! viele Blätter.
Beispiel. Figur 2.11 zeigt einen binären Entscheidungsbaum für a, b und c. Sie
zeigt die notwendigen Vergleiche, um die Reihenfolge für a, b, c zu ermitteln.

a <. b

b<c a<c

a<b<c a<c b<a<c b<c

a<c<b c<a<b b<c<a c<b<a

Fig. 2.11: Der Entscheidungsbaum für 3 Elemente.

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).

Dies zeigt die Behauptung. 2

2.4 Suchen in Arrays

In diesem Abschnitt behandeln wir das Problem, ein bestimmtes Element in


einem Array a zu suchen. Falls a unsortiert ist, müssen wir sequenziell suchen,
d. h. wir inspizieren die Elemente von a nacheinander, bis das gewünschte
Element gefunden ist. Die Laufzeit ist von der Ordnung O(n). Ein sortiertes
Array erlaubt binäre Suche. Die Laufzeit dieser Methode ist von der Ord-
nung O(log2 (n)). Eine weitere Methode mit logarithmischer Laufzeit ist die
Fibonacci-Suche (siehe Kapitel 4, Übungen, Aufgabe 11). Werden Elemente
hinreichend oft gesucht, dann macht sich der Aufwand zu sortieren bezahlt.
Neben der sequenziellen und binären Suche behandeln wir die Suche nach
dem k–kleinsten Element. Hier geben wir das Element nicht explizit an. Wir
spezifizieren das Element durch eine Eigenschaft.

2.4.1 Sequenzielle Suche

Der Algorithmus der sequenziellen Suche – SequSearch – sucht ein Element


x in einem Array a[0..n] unter den ersten n Elementen. Dabei inspizieren wir
die Elemente von a[0..n − 1] nacheinander, bis x gefunden oder das Ende von
a erreicht ist. Der Algorithmus benötigt die Variable a[n] als Wächterelement.
Algorithmus 2.27.
index SequSearch(item a[0..n], x)
1 index i
2 a[n] ← x, i ← 0
3 while a[i] ̸= x do
4 i←i+1
5 return i

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

3. Für die Anzahl I(n) der Iterationen der while-Schleife im schlechtesten


Fall gilt I(n) = n.
4. Werden alle in a gespeicherten Elemente gesucht und sind die Elemente
in a paarweise verschieden, so ist die mittlere Anzahl der Iterationen der
while-Schleife 12 (n − 1).

2.4.2 Binäre Suche

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

Fig. 2.12: Alle Zugriffspfade.

Satz 2.29. Für die Anzahl I(n) der Iterationen der repeat-until-Schleife gilt
im schlechtesten Fall:

I(n) ≤ ⌊log2 (n)⌋ + 1.


Die Anzahl der Vergleiche beträgt 2I(n) und ist somit ≤ 2(⌊log2 (n)⌋ + 1).
⌈ ⌉ ⌊ ⌋
Beweis. In jeder Iteration teilen
⌊ n−1 ⌋ ⌊wir ⌋ n − 1⌈Elemente
⌉ ⌊ auf
⌋ in n−1
2 und n−1
2

⌋) 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

Im schlechtesten Fall terminiert die repeat-until-Schleife mit l > r. I(n)


genügt daher der Rekursion:
(⌊ n ⌋)
I(n) ≤ I + 1, I(1) = 1.
2
Hieraus folgt die Behauptung mit Satz 1.28. 2

2.4.3 Die Suche nach dem k–kleinsten Element

Sei a1 , a2 , . . . , an eine endliche Folge. Ein Element a dieser Folge heißt k–


kleinstes Element oder Element vom Rang k, wenn a in einer Sortierung der
Folge an der Position k stehen kann. Falls sich mehrere gleiche Elemente in
der Folge befinden, ist das Element vom Rang k nicht eindeutig bestimmt.
Das Problem, das k–kleinste Element in einer Folge zu finden, lässt sich
natürlich dadurch lösen, dass man zunächst die Elemente sortiert und dann
auf das k–te Element zugreift. Der Aufwand zum Sortieren der Folge ist von
der Ordnung O(n ln(n)), wenn wir zum Beispiel den Algorithmus QuickSort
verwenden. Wir behandeln einen probabilistischen Algorithmus, der das k–te
Element – ohne vorher zu sortieren – bestimmt. Der Erwartungswert seiner
Laufzeit ist von der Ordnung O(n).
106 2. Sortieren und Suchen

Wir nennen diesen Algorithmus QuickSelect. Er entsteht durch Modifikati-


on von QuickSort. Der Algorithmus gibt nach dem Aufruf
QuickSelect(a[1..n], k) das Element vom Rang k zurück. Die Vorbedingung
für QuickSelect ist, dass wir auf die Elemente von a den <–Operator anwen-
den können und dass 1 ≤ k ≤ n gilt.
Algorithmus 2.30.
QuickSelect(item a[i..j], int k)
1 item x, index l, r, boolean loop ← true
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 loop 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 loop ← f alse
12 exchange a[l] and a[j]
13 if k < l
14 then return QuickSelect(a[i..l − 1], k)
15 else if k > l
16 then return QuickSelect(a[l + 1..j], k − l)
17 return a[l]
18 return a[i]
Die Laufzeit im schlechtesten Fall ist – wie bei Quicksort – von der
Ordnung O(n2 ). In QuickSelect wählen wir das Pivotelement zufällig. Die
Wahl des Pivotelement werde durch die Zufallsvariable R mit Wertebereich
{1, . . . , n} beschrieben. R ist gleichverteilt, d. h. p(R = r) = n1 .
Satz 2.31. Der Algorithmus QuickSelect gibt das k–te Element von a zurück.
Der Erwartungswert der Laufzeit ist linear.
Beweis. Die Aussage folgt mit vollständiger Induktion nach n. Für n = 1 gibt
QuickSelect a[1] zurück. Der Induktionsanfang ist richtig. Sei n ≥ 2. Nach
Terminierung der äußeren while-Schleife (Zeile 5) gilt a[1], . . . , a[l − 1] ≤ x
und a[l + 1], . . . , a[n] ≥ x. Falls k < l gilt, befindet sich das k–te Element
links von der Position l und falls k > l gilt, befindet sich das k–te Element
rechts von der Position l. Nach Induktionsvoraussetzung liefert der Aufruf
QuickSelect(a[1..l − 1], k) das k–te Element und der Aufruf QuickSelect(a[l +
1..n], k − l) das (k − l)–te Element. Es wird in beiden Fällen das k–te Element
von a berechnet. Falls l = k ist, ist a[l] das k–te Element.
Der Erwartungswert der Laufzeit T (n) von QuickSelect berechnet sich
nach Lemma A.9
Übungen 107


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

wobei I = [n/4, 3n/4] und c konstant ist. Es folgt


( ( ))
3n
E(T (n)) ≤ E T + 2cn.
4

Setze b = 43 , n = bk und xk = E(T (bk )). Dann gilt x1 = 2c und xk =


∑k
xk−1 + 2cbk = 2c + 2c i=2 bi = 8cbk − 26
3 c (Satz 1.15).
Mit k = logb (n) und Lemma B.24 folgt E(T (n)) ≤ 8cn − 26
3 c = O(n). 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

a. Vergleichen Sie QuickSortVariante mit Algorithmus 2.1. Diskutieren


Sie Vor- und Nachteile.
b. Geben Sie die Invariante der for-Schleife an und zeigen Sie, dass der
Algorithmus korrekt ist.
8. Modifizieren Sie Algorithmus 2.1 so, dass er mit n − 1 Vergleichen im
Zerlegungsprozess auskommen. Analysieren Sie die Anzahl der Vergleiche
im besten Fall.
9. Zeigen Sie, dass die folgende Variante von Quicksort korrekt ist und be-
stimmen Sie die Laufzeit.
Algorithmus 2.34.
QuickSort(item a[i..j])
1 index l, r, p, item x
2 if p ← Pivot(a[i..j]) ̸= 0
3 then x ← a[p], l ← i, r ← j
4 repeat
5 exchange a[l] and a[r]
6 while a[l] < x do l ← l + 1
7 while a[r] ≥ x do r ← r − 1
8 until l = r + 1
9 QuickSort(a[i..l − 1])
10 Quicksort(a[l..j])

index Pivot(item a[i..j])


1 index l, item x
2 x ← a[i]
3 for l ← i + 1 to j do
4 if a[l] > x
5 then return l
6 else if a[l] < x
7 then return i
8 return 0

10. Modifizieren Sie den Quicksort-Algorithmus so, dass eine logarithmisch


beschränkte Rekursionstiefe gewährleistet wird. Ersetzen Sie dazu im Al-
gorithmus 2.34 aus der vorangehenden Übungsaufgabe einen rekursiven
Aufruf durch eine Iteration.
11. Geben Sie eine Implementierung der iterativen Version von Quicksort an
und analysieren Sie diese.
12. Sei a[1..n] ein Array von Zahlen. Geben (Sie einen Algorithmus
) an, der die
ersten k Elemente in a[1..n] für kleine k k ≤ n/log2 (n) ohne zusätzlichen
Speicher mit Laufzeit O(n) sortiert.
110 2. Sortieren und Suchen

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.

3.1 Grundlegende Begriffe

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

s1 , s2 ∈ X, s1 ̸= s2 , gespeichert werden und ergibt sich der gleiche Tabellen-


index, d. h. h(s1 ) = h(s2 ), so sprechen wir von einer Kollision. Da Hashfunk-
tionen nicht injektiv sind, Hashfunktionen lassen Kollisionen zu, ist für ein
Hashverfahren neben der Hashtabelle und der Hashfunktion ein Verfahren
zur Behandlung der Kollisionen notwendig.

Hashtabelle
Schlüssel s

Hashfunktion

Fig. 3.1: Hashverfahren.


.

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

In einem Hashverfahren bildet die Hashfunktion jeden Schlüssel auf einen


Tabellenplatz ab. Dies setzt voraus, dass die Hashfunktion durch einen effi-
zienten Algorithmus berechenbar ist. Alle Beispiele von Hashfunktionen, die
wir diskutieren, lassen sich durch wenige einfache arithmetische Operationen
berechnen.
Eine zweite Anforderung an Hashfunktionen ist, dass Hashfunktionen Kol-
lisionen minimieren sollen. Bei universellen Familien von Hashfunktionen
sind quantitative Aussagen möglich. Die zugrunde liegenden Annahmen sind
in Anwendungen realisierbar.
Das folgende Beispiel zeigt, dass Kollisionen auch bei einer kleinen Anzahl
von Schlüsseln und einer im Vergleich dazu großen Tabelle möglich sind.
Beispiel. X sei eine Menge von Personen. Die Funktion h

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

Die Wahrscheinlichkeit für eine Kollision ist 1 − p und 1 − p ≥ 12 , falls k ≥


(√ ) √ √
1
2 1 + 8 ln 2 · m + 1 ≈ 1.18 m. Für m = 365 ergibt sich 1.18 365 ≈
22, 5.

3.2.1 Division und Multiplikation

Die arithmetischen Operationen Multiplikation und Division mit Rest (Satz


B.1) liefern geeignete Hashfunktionen. Die meisten Prozessoren führen die
Multiplikationen von zwei Zahlen wesentlich schneller als deren Division aus.
Definition 3.2 (Division mit Rest). Sei m ∈ N.
h : S −→ {0, . . . , m − 1}, s 7−→ s mod m.
Beispiel. Sei S = {2, 4, . . . , 200}. Für m = 100 gilt h(s) = h(s + 100). Es
finden 50 Kollisionen statt. Wählen wir für m = 101, so finden keine Kolli-
sionen statt. Dies lässt vermuten, dass Primzahlen als Teiler besser geeignet
sind.
Definition 3.3 (Multiplikation). Sei c ∈ R, 0 < c < 1.
h : S −→ {0, . . . , m − 1}, s 7−→ ⌊m{sc}⌋,
wobei für x ∈ R der Ausdruck {x} den gebrochenen Anteil von x bezeichnet,
d. h. {x} := x − ⌊x⌋.

Bemerkung. Sei g = 12 (1+ 5) das Verhältnis des goldenen Schnitts (siehe De-
finition 1.22). Für c = g1 werden die besten Resultate erzielt (vgl. [Knuth98a,
Chapter 6.4]).
114 3. Hashverfahren

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.

3.2.2 Universelle Familien


Hashfunktionen sollen Kollisionen minimieren. Zufallsfunktionen haben diese
Eigenschaft. Sie sind jedoch nicht implementierbar. Universelle Familien von
Hashfunktionen, die in [CarWeg79] eingeführt wurden, minimieren Kollisio-
nen und sind einfach zu implementieren. Wir betrachten zunächst Zufalls-
funktionen.
Sei X die Menge der möglichen Schlüssel, n := |X| und m ∈ N. Wir
definieren eine Zufallsfunktion
h : X −→ {0, . . . , m − 1}
durch die folgende Konstruktion. Für jedes Argument x wählen wir den Wert
y ∈ {0, . . . , m − 1} zufällig. Die Paare (x, y) speichern wir in einer Tabelle.
Die Zufallsfunktion h wird durch diese Tabelle beschrieben – ein Eintrag
(x, y) definiert die Zuordnung x 7−→ y. Soll für x ∈ X der Funktionswert
y = h(x) berechnet werden, so schaue in der Tabelle nach, ob x als Argument
verzeichnet ist.
1. Falls ja, verwende den Tabelleneintrag (x, y) und setze h(x) = y.
2. Sonst wähle y ∈ {0, . . . , m − 1} zufällig, setze h(x) = y und trage (x, y)
in die Tabelle ein.
Bei der Anwendung der Hashfunktionen h soll die Kollisionswahrschein-
lichkeit
p(h(x1 ) = h(x2 )), x1 , x2 ∈ X, x1 ̸= x2 ,
1
klein sein. Für Zufallsfunktion h ist dies erfüllt, es gilt p(h(x1 ) = h(x2 )) = m.
3.2 Hashfunktionen 115

Beispiel. Figur 3.2 zeigt eine Zufallsfunktion, die Punkte gleichmäßig in der
Ebene verteilt.

Fig. 3.2: Mit einer Zufallsfunktion erzeugte Koordinaten.

Das oben beschriebene Verfahren, um eine Zufallsfunktion zu konstruie-


ren, erzeugt eine bestimmte Funktion mit der Wahrscheinlichkeit m1n . Auf
F (X, Y ), der Menge aller Abbildungen von X nach Y , erhalten wir somit die
Gleichverteilung.

Eine andere Methode, eine Zufallsfunktion zu erhalten, besteht darin, eine


Funktion aus F (X, Y ) zufällig zu wählen. Beide Methoden liefern die Gleich-
verteilung auf F (X, Y ). Wir definieren Zufallsfunktionen durch
Definition 3.4. Eine Zufallsfunktion ist eine aus F (X, Y ) zufällig gewählte
Funktion.
Bemerkung. Die Kollisionswahrscheinlichkeit bezieht sich jetzt auf die zufälli-
ge Wahl von h.
|{f ∈ F (X, Y ) | f (x1 ) = f (x2 )}| mn−1 1
p(h(x1 ) = h(x2 )) = = = .
|F (X, Y )| mn m
Diese Definition der Kollisionswahrscheinlichkeit können wir für beliebige Fa-
milien von Funktionen verallgemeinern.
Definition 3.5. Sei H eine Familie von Funktionen h : X −→ Y und x1 , x2 ∈
X, x1 ̸= x2 . Für zufällig gewähltes h ∈ H ist die Kollisionswahrscheinlichkeit
|{h ∈ H | h(x1 ) = h(x2 )}|
p(h(x1 ) = h(x2 )) := .
|H|

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

Als Hashfunktion wünschen wir uns Funktionen, deren Kollisionswahr-


scheinlichkeit klein ist – wie bei Zufallsfunktionen – und die zusätzlich effi-
zient implementierbar sind. Dies leisten universelle Familien von Hashfunkti-
onen. Sie verhalten sich bezüglich Kollisionen wie Zufallsfunktionen und sind
effizient implementierbar (Satz 3.20 und Satz 3.7).
Definition 3.6. Eine Familie H von Funktionen h : X −→ Y heißt eine uni-
verselle Familie von Hashfunktionen, wenn für alle x1 , x2 ∈ X, x1 ̸= x2 , gilt:
Für zufällig gewähltes h ∈ H ist die Kollisionswahrscheinlichkeit
1
p(h(x1 ) = h(x2 )) ≤ , wobei m = |Y | gilt.
m
Eine Hashfunktionen h : X −→ Y soll die tatsächlichen Schlüssel S ⊂ X
gleich verteilen, d. h. es soll gelten
|S|
nh(y) := |{s ∈ S | h(s) = h(y)}| = , für alle y ∈ Y.
m
Der Aufwand für die Kollisionsbehandlung bezüglich eines Wertes y hängt
von der Anzahl der Schlüssel ab, die auf diesen Wert abgebildet werden. Sie
ist proportional zu nh(y) (Abschnitt 3.3). Im Idealfall tritt keine Kollision
auf. Dies ist äquivalent zu nh(y) ≤ 1 für alle y ∈ Y . Wenn wir die Menge
der Schlüssel zufällig wählen oder eine Zufallsfunktion h verwenden, dann
erwarten wir, dass nh(y) = |S| m ist (Übungen, Aufgabe 10 und Satz 3.16). Zu-
fallsfunktionen sind jedoch nicht implementierbar und auf die Verteilung der
Schlüssel können wir keinen Einfluss nehmen. Die Anwendung, in der wir das
Hashverfahren einsetzen, gibt die Verteilung vor. Eine Lösung des Problems
bietet die Verwendung einer universellen Familie von Hashfunktionen.

Universelle Familien von Hashfunktionen und Varianten davon spielen


auch in der Informationstheorie und Kryptographie eine wichtige Rolle (sie-
he [DelfsKnebl15, Chapter 10.1 und 10.3]). Es gibt universelle Familien von
Hashfunktionen, die die effiziente binäre Arithmetik verwenden. Da wir auf
die Einführung endlicher Erweiterung von F2 verzichten, geben wir jetzt zwei
Beispiele für universelle Familien an, die auf der weniger effizienten modula-
ren Arithmetik basieren. Für eine natürliche Zahl m bezeichnet
Zm = {[0], . . . , [m − 1]}
die Menge der Restklassen modulo m. Wir identifizieren Zm mit {0, . . . , m −
1}, der Menge der Reste modulo m (Definition B.8). Für eine Primzahl m = p
ist Zp ein Körper und Zrp , r ∈ N, ein Vektorraum über dem Körper Zp
(Corollar B.12).
Satz 3.7. Sei p eine Primzahl, seien a, b ∈ Zp , m ∈ N, 2 ≤ m ≤ p und
ha,b : {0, . . . , p − 1} −→ {0, . . . , m − 1}, x 7−→ ((ax + b) mod p) mod m.
Die Familie H = {ha,b | a, b ∈ Zp , a ̸= 0} ist universell.
3.2 Hashfunktionen 117

Beweis. Wir zeigen zunächst, dass |H| = |{(a, b) | a, b ∈ Zp , a ̸= 0}| = p(p−1)


gilt. Dabei unterscheiden wir:
1. Seien a, a′ , b, b′ ∈ Zp , a ̸= a′ . Wir zeigen, dass ha,b ̸= ha′ ,b′ gilt.
Für a ̸= 0 ist die Abbildung

fa,b : Zp −→ Zp , x 7−→ (ax + b) mod p

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

ha,b (a−1 y) = ((p − b + b) mod p) mod m = 0 und


ha′ ,b′ (a−1 y) = ((p − b + b′ ) mod p) mod m
= ((p − (b − b′ )) mod p) mod m
= (p − (b − b′ )) mod m
= p mod m ̸= 0.

Wir haben gezeigt, dass aus (a, b) ̸= (a′ , b′ ) folgt ha,b ̸= ha′ ,b′ . Deshalb gilt

|H| = |{(a, b) ∈ Z2p | a ̸= 0}| = p(p − 1).

Seien x, y ∈ Zp , x ̸= y gegeben. Wir zeigen jetzt, dass

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]).

φ({(a, b) ∈ Z2p | a ̸= 0}) = {(r, s) ∈ Z2p | r ̸= s}


und

φ({(a, b) ∈ Z2p | a ̸= 0, ha,b (x) = ha,b (y)})


= {(r, s) ∈ Z2p | r ̸= s, r ≡ s mod m}.

Deshalb gilt

|{(a, b) ∈ Z2p | a ̸= 0, ha,b (x) = ha,b (y)}|


= |{(r, s) ∈ Z2p | r ̸= s, r ≡ s mod m}|.
118 3. Hashverfahren

⌊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 .

Hieraus folgt sofort die Behauptung. 2


2
Die Vektoren, die in Zrp
auf einem Vektor ̸= 0 senkrecht stehen, bilden einen
Untervektoraum der Dimension r − 1.
3.3 Kollisionsauflösung 119

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 .

Verfahren bei Verwendung universeller Familien. Wenn wir eine uni-


versellen Familie H von Hashfunktionen verwenden, machen wir das Hashver-
fahren zu einem probabilistischen Verfahren. Dies erfolgt durch die zufällige
Wahl der Hashfunktion. Die zufällige Wahl der Hashfunktion h macht die
Anzahl nh(y) der Elemente aus S, die h auf y abbildet, zu einer Zufallsvaria-
blen. Ihr Erwartungswert ist ähnlich wie bei einer Zufallsfunktion |S|
m (Satz
3.16 und Satz 3.20).
Wir implementieren das Verfahren so, dass wir die Hashfunktion als Parame-
ter übergeben.
1. Bei der Initialisierung des Verfahrens wählen wir h ∈ H zufällig.
2. Diese Funktion h verwenden wir dann für die gesamte Laufzeit des Ver-
fahrens, d. h. alle Einfüge- und Suchoperationen führen wir mit dieser
Hashfunktion h durch.

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.

3.3.1 Kollisionsauflösung durch Verkettungen

Die Verfahren zur Kollisionsauflösung durch Verkettungen organisieren die


Schlüssel, die auf denselben Hashwert abgebildet werden, in einer linear ver-
ketteten Liste. Beim Suchen eines Schlüssels durchsuchen wir die verkettete
Liste.
Verkettungen mit Überlaufbereich. Wir teilen die Hashtabelle in einen
Primärbereich und einen Überlaufbereich auf. Die Hashfunktion berechnet
Indizes der Zellen im Primärbereich. Die Zellen im Überlaufbereich speichern
120 3. Hashverfahren

Schlüssel, die im Primärbereich aufgrund von Kollisionen keinen Platz finden,


siehe Figur 3.3.
Zur Verwaltung der Schlüssel im Überlaufbereich erweitern wir die Ein-
träge der Hashtabelle um einen Index für Verkettungen und organisieren alle
Schlüssel mit gleichem Hashwert in einer verketteten Liste.
Für die Zellen im Überlaufbereich ist eine Speicherverwaltung zu imple-
mentieren. Dies kann mithilfe einer verketteten Liste der freien Zellen erfol-
gen.

Fig. 3.3:
. Primär- und Überlaufbereich.

Separate Verkettungen. Alle Schlüssel speichern wir in den Knoten der


verketteten Listen. Die Hashtabelle enthält Anker für die verketteten Listen,
siehe Figur 3.4. Die Hashfunktion bildet Schlüssel auf Indizes der Hashtabelle
ab. Die Knotenelemente für die verketteten Listen werden nach Bedarf belegt
und freigegeben.

. Fig. 3.4: Separate Verkettung.

Üblicherweise unterstützt dies die zur Implementierung verwendete Pro-


grammiersprache. Höhere Programmiersprachen bieten eine dynamische Spei-
cherverwaltung auf dem sogenannten Heapspeicher .3 Der Vorteil zu Verket-
tungen mit Überlaufbereich liegt daher darin, dass keine Freispeicherverwal-
tung zu implementieren ist. Da die Speicherverwaltung auf dem Heapspeicher
Blöcke variabler Länge verwaltet, ist sie im Vergleich zu einer auf die obige
Situation angepasste Speicherverwaltung weniger effizient.
3
Das Betriebssystem stellt für ein laufendes Programm einen zusammenhängen-
den Speicherbereich, den Heapspeicher, bereit. Ein Programm kann Speicher-
blöcke aus dem Heapspeicher zur Laufzeit belegen und freigeben.
3.3 Kollisionsauflösung 121

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.

3.3.2 Offene Adressierung

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

Beim quadratischen Sondieren betrachten wir nur Indizes, deren Abstand


vom Hashwert ein Quadrat modulo m ist.
Satz 3.12. Sei m = p eine Primzahl und es gelte p ≡ 3 mod 4. Beim qua-
dratischen Sondieren modulo p treten in jeder Sondierfolge alle Indizes der
Hashtabelle auf.
Beweis. Nach Corollar B.15 gilt Zp = {±[i2 ] | i = 0, . . . , (p − 1)/2}. Die bei
der Berechnung einer Sondierfolge angewendete Translation um c = h(s) de-
finiert eine bijektive Abbildung auf Zp . Somit kommen bei quadratischem
Sondieren in einer Sondierfolge alle Tabellenindizes vor. 2

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.

3.4 Analyse der Hashverfahren

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.

Beweis. Die Behauptung folgt aus Satz A.16. 2


Bemerkung. Für die Anzahl wi der Werte mit i Urbildern gilt:

m−1
wi = |{j | nj = i}| = δnj i , i = 0, . . . , n,
j=0

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

mit den Zufallsvariablen wi und kol.

5
Das Zeichen δij wird mit Kronecker-Delta bezeichnet. Leopold Kronecker (1823
– 1891) war ein deutscher Mathematiker.
126 3. Hashverfahren

Satz 3.17. Sei H eine Hashtabelle mit m Zellen im Primärbereich und n


gespeicherten Elementen. Die Anzahl der Werte mit i Urbildern beträgt im
Mittel mpi . Die Anzahl der stattgefundenen Kollisionen beträgt im Mittel
n − m(1 − p0 ).
Beweis.
 

m−1 ∑
m−1 ∑
m−1
E(wi ) = E  δ nj i  = E(δnj i ) = p(δnj i = 1)
j=0 j=0 j=0


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

Definition 3.18. Befinden sich in einer Hashtabelle H mit m Zellen im Pri-


n
märbereich n Elemente, so heißt B := m Belegungsfaktor von H.
Wir approximieren die Binomialverteilung durch die Poisson-Verteilung
i
(Seite 328) pi ≈ ri = Bi! e−B und berechnen den Prozentsatz E(wi )/m · 100 =
pi ·100 der durch i Schlüssel belegten Werte für i = 0, 1, 2 und den Prozentsatz
E(Kol)
n · 100 der Schlüssel, die Kollisionen verursachen für B = 0.1, 0.5, 1, 1.5
und 3.
B = 0.1 B = 0.5 B = 1 B = 1.5 B = 3
p0 · 100 90.5 60.7 36.8 22.3 5.0
p1 · 100 9.0 30.3 36.8 33.5 14.9
p2 · 100 0.5 7.6 18.4 25.1 22.4
E(Kol)
n · 100 4.8 21.3 36.8 48.2 68.3

Wir können diese Tabelle benutzen, um Primär- und Überlaufbereich zu di-


mensionieren. So erwarten wir bei einem Belegungsfaktor 1, d. h. die Anzahl
der Schlüssel stimmt mit der Länge der Hashtabelle überein, dass 36,8 % der
Tabellenplätze frei bleiben und dass somit auch 36,8 % der Schlüssel zu Kol-
lisionen führen. Entsprechend ist der Überlaufbereich zu dimensionieren. Der
Belegungsfaktor beeinflusst die Performance des Verfahrens und umgekehrt.
Genaue Auskunft darüber gibt
3.4 Analyse der Hashverfahren 127

Satz 3.19. Sei h : X −→ {0, . . . , m − 1} eine Zufallsfunktion, S = {s1 , . . . , sn }


n
die Menge der gespeicherten Schlüssel und B = m der Belegungsfaktor.
1. Die Anzahl der Vergleiche bei erfolgreicher Suche eines Schlüssels beträgt
im Mittel 1 + 1/2B.
2. Die Anzahl der Vergleiche bei erfolgloser Suche eines Schlüssels beträgt
im Mittel B + e−B .
Beweis. 1. Die Anzahl der Vergleiche, falls wir alle Schlüssel einer Kette der
Länge i suchen, ist i(i + 1)/2. Da es wi viele Ketten der Länge i gibt, ist die
∑n
Anzahl der Vergleiche, falls wir alle Schlüssel suchen, gleich i=1 i(i+1)2 wi .
Wir erhalten für die Anzahl der Vergleiche pro Schlüssel

1 ∑ i(i + 1)
n
V = wi .
n i=1 2

Für den Erwartungswert E(V ) gilt


( )
1 ∑ i(i + 1) 1 ∑ i(i + 1)
n n
1 m (n − 1)n n
E(V ) = E(wi ) = mpi = +2
n i=1 2 n i=1 2 2n m2 m
( )
1 n−1 n−1 1
= +2 =1+ ≈ 1 + B.
2 m 2m 2

Dabei verwenden wir



n ∑
n ∑
n
n(n − 1) n
i(i + 1)pi = i 2 pi + ipi = 2
+2 .
i=1 i=1 i=1
m m

Das letzte =“ ergibt sich mit Satz A.16:



∑n
n ∑n
n(n − 1) n
ipi = und i 2 pi = 2
+ .
i=1
m i=1
m m

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

Statt einer Zufallsfunktion h betrachten wir jetzt ein zufällig gewähltes


h ∈ H, wobei H eine universelle Familie von Hashfunktionen ist. Wir erhalten
analog zu Satz 3.19 Aussagen für universelle Familien von Hashfunktionen
(Corollar 3.21). Sei
128 3. Hashverfahren

{
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.

Wir betrachten δh (x, S) als Zufallsvariable in Abhängigkeit von h.


Satz 3.20. Sei H = {h : X −→ {0, . . . , m − 1}} eine universelle Familie von
Hashfunktionen, S = {s1 , . . . , sn } die Menge der gespeicherten Schlüssel und
x ∈ X. Dann ist der Erwartungswert (bezüglich der gleichverteilten zufälligen
Wahl von h ∈ H) {
n
, falls x ̸∈ S,
E(δh (x, S)) ≤ m n−1
m sonst.

In allen Fällen gilt E(δh (x, S)) ≤ n


m.

Beweis.
∑ 1 ∑ 1 ∑
E(δh (x, S)) = δh (x, S) = δh (x, s)
|H| |H|
h∈H h∈H s∈S\{x}

1 ∑ ∑ ∑ |{h ∈ H | h(s) = h(x)}|


= δh (x, s) =
|H| |H|
s∈S\{x} h∈H s∈S\{x}
{
|S \ {x}| n
m, falls x ̸∈ S,
≤ = n−1
m m sonst.

In die letzte Abschätzung geht ein, dass die Kollisionswahrscheinlichkeit der


Familie H kleiner oder gleich 1/m ist. 2

Bemerkungen:
1. Sei x ∈ S. Für den Erwartungswert von nh(x) gilt die Abschätzung

E(nh(x) ) ≤ (n − 1)/m + 1 ≈ n/m + 1.


Wir erwarten, dass ein zufällig gewähltes h ∈ H die Schlüssel gleichmäßig
auf die Tabellenindizes verteilt. Dies ist unabhängig von der Verteilung
auf X, nach der wir die Schlüssel wählen.
3.4 Analyse der Hashverfahren 129

2. Der Aufwand für eine Einfügeoperation (oder Suchoperation) für ein


Element mit Hashwert h(x) ist proportional zu nh(x) . Die Abschätzung
E(nh(x) ) ≤ m n
für eine Einfügeoperation und E(nh(x) ) ≤ n−1m + 1 für
erfolgreiche Suche hängt nicht von h(x) ab.
3. Für die Wahrscheinlichkeit, dass δh (x, S) Werte oberhalb des Erwartungs-
wertes annimmt, folgt mit der Ungleichung von Markov (Satz A.10) für
jede reelle Zahl r > 0
1
p(δh (x, S) ≥ rE(δh (x, S))) ≤ .
r

Dies bedeutet |{h ∈ H | δh (x, S) ≥ rE(δh (x, S))}| ≤ |H|


r .
|H|
Sei H̃ = {h ∈ H | δh (x, S) ≥ rn/m}. Dann gilt |H̃| ≤ r .

Corollar 3.21. Sei H = {h : X −→ {0, . . . , m − 1}} eine universelle Familie


von Hashfunktionen, S = {s1 , . . . , sn } die Menge der gespeicherten Schlüssel
n
und B = m der Belegungsfaktor. Dann gilt
1. Der Erwartungswert der Anzahl der Vergleiche bei erfolgreicher Suche
eines Schlüssels ist < 1 + 1/2B.
2. Der Erwartungswert der Anzahl der Vergleiche bei erfolgloser Suche eines
Schlüssels ist ≤ 1 + B.
Der Erwartungswert bezieht sich dabei auf die zufällige Wahl von h.
Beweis. 1. Sei h ∈ H gewählt, x ∈ S und j = h(x). Die Anzahl der Vergleiche,
falls wir alle Schlüssel mit Hashwert j suchen, ist gleich (nj (nj + 1))/2. Für
die durchschnittliche Anzahl der Vergleiche pro Schlüssel ergibt sich V =
(nj + 1)/2. Aus der Linearität des Erwartungswertes und Satz 3.20 folgt
( )
n−1 1
E(V ) = E (1/2 (nj + 1)) ≤ 1/2 2 + < 1 + B.
m 2

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

E(V ) = E(δnj 0 ) + E(nj ) = p (nj = 0) + E(nj ) ≤ 1 + B.

Dies zeigt die Behauptung. 2

3.4.2 Offene Adressierung

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

Bemerkung. Werden beim Doppelhashing die Hashfunktionen h und h∗ aus


einer Familie universeller Hashfunktionen unabhängig gewählt, so sind die
mittleren Längen der Sondierfolgen ähnlich wie bei uniform hashing. Die
Analyse ist jedoch viel komplizierter. In [SieSch95] wird gezeigt, dass die
1
mittlere Länge einer Sondierfolge beim Einfügen eines Elements gleich 1−B +
1
O( m ) ist.
Satz 3.24 (lineares Sondieren).
In einer Hashtabelle seien n von m Zellen belegt. B sei der Belegungsfak-
tor.
1. Die mittlere
( Länge ) einer Sondierfolge ist beim Suchen eines Elementes
1 1
gleich 2 1 + 1−B .
2. Die mittlere
( Länge einer Sondierfolge ist beim Einfügen eines Elementes
( )2 )
1 1
gleich 2 1 + 1−B .

Beweis. Siehe [Knuth98a]. 2

Wir vergleichen jetzt die Verfahren uniform hashing (UH), Doppelhashing


mit einer universellen Familie (DU), Verkettungen mit einer universellen Fa-
milie (VU) und Verkettungen mit einer Zufallsfunktion (VZ).

Figur 3.6 zeigt die Anzahl der Vergleiche bei erfolgreicher Suche in
Abhängigkeit vom Belegungsfaktor B.

Fig. 3.6: Vergleich – erfolgreiche Suche.


132 3. Hashverfahren

Figur 3.7 zeigt die Anzahl der Vergleiche bei erfolgloser Suche in Abhängig-
keit vom Belegungsfaktor B.

Fig. 3.7: Vergleich – erfolglose Suche.

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.

Zeigen Sie: H = {ha | a ∈ Zp } ist eine universelle Familie von Hashfunk-


tionen.
4. Sei p eine Primzahl und Zp der Körper mit p Elementen. Zeigen Sie,
dass die Menge der linearen Abbildungen A : Zkp −→ Zlp eine universelle
Familie von Hashfunktionen ist.
5. Gegeben sei die Sondierfolge : i(s)j := (s + j · c) mod m, j = 0, 1, 2, . . ..
Übungen 133

a. Tragen Sie mithilfe dieser Sondierfolge in eine zunächst leere Hash-


tabelle folgende Schlüssel ein: 261, 321, 453, 781, 653, 1333,109, 235,
800. Setzen Sie dazu c = 7 und m = 21.
b. Bei der Sondierfolge unter Punkt a werden nicht alle Plätze in
der Hashtabelle sondiert. Für welche c werden bei der Sondierfol-
ge i(s)j := (s + j · c) mod m, j = 0, 1, 2, . . ., bei gegebenem m alle
Plätze in der Hashtabelle sondiert. Begründen Sie Ihre Aussage.
6. Im Hauptspeicher werden 10000 Datensätze verwaltet. Dabei kommt ein
Hashverfahren mit Primär- und Überlaufbereich zum Einsatz. Im Mittel
soll ein Datensatz mit zwei Zugriffen gefunden werden.
a. Bestimmen Sie den Belegungsfaktor.
b. Wie ist Primär- und Überlaufbereich zu dimensionieren?
7. Mit einem Hashverfahren im Hauptspeicher werden 1000 Datensätze ver-
waltet. Kollisionsauflösung erfolgt durch Doppelhashing. Wir verwenden
die Familie von Funktionen

ha,b : {0, . . . , p − 1} −→ {0, . . . , m − 1}, x 7−→ ((ax + b) mod p) mod m,

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

In sortierten Arrays finden wir ein gespeichertes Element mit maximal


O(log2 (n)) Vergleichen. Eine andere Methode, Elemente zu sortieren, stellen
binäre Suchbäume bereit. Binäre Suchbäume sind für den Fall konzipiert, dass
Elemente dynamisch eingefügt oder gelöscht werden. Dabei ist wünschens-
wert, dass durch die Einfüge- und Löschoperationen der Baum möglichst
ausgeglichen bleibt, d. h. die Anzahl der Ebenen soll klein sein.
Binäre Suchbäume sind im Mittel ausgeglichen (gemittelt über alle mögli-
chen Anordnungen der zu speichernden Elemente). Im ungünstigen Fall de-
generieren binäre Suchbäume zu linearen Listen. Dann ist die Anzahl der
notwendigen Vergleiche beim Suchen eines Elements von der Ordnung O(n)
im Vergleich zur Ordnung O(log2 (n)) im ausgeglichenen Fall.
Im Idealfall ist die Anzahl der Ebenen ⌊log2 (n)⌋ + 1 und die Blätter be-
finden sich auf ein oder zwei Ebenen, wenn n die Anzahl der gespeicherten
Elemente bezeichnet. Der Aufwand, um diesen Idealfall zu erreichen, ist nicht
vertretbar. Deshalb streben wir diesen Idealfall nicht an.
AVL-Bäume sind beinahe ausgeglichen (nur etwa 45 % schlechter als im
Idealfall). Wir erreichen dies mit geringem zusätzlichen Aufwand. Der Auf-
wand zum Suchen eines Elements ist vergleichbar mit dem Aufwand, der
beim Suchen in einem sortierten Array zu leisten ist. Eine weitere Methode
zu verhindern, dass binäre Suchbäume degenerieren, ist die Verwendung von
probabilistischen binären Suchbäumen. Die durchschnittliche Pfadlänge bei
binären Suchbäumen wird zum Erwartungswert der Pfadlänge bei probabi-
listischen binären Suchbäumen. Die Pfadlänge nimmt um höchstens 39 % im
Vergleich zum optimalen Fall zu.
Binäre Suchbäume und ihre Varianten dienen der Organisation von Daten
im Hauptspeicher. Für die Organisation von Daten auf dem Sekundärspeicher
diskutieren wir B-Bäume.
Codebäume zur graphischen Darstellung von Codes zur Datenkomprimie-
rung runden das Kapitel ab. Wir zeigen, dass das Problem der eindeutigen
Decodierbarkeit entscheidbar ist und behandeln Huffman-Codes inklusive ad-
aptiver Huffman-Codes, die arithmetische Codierung und die allgegenwärti-
gen Lempel-Ziv-Codes.

© 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:

(V1 ∪ . . . ∪ Vk ∪ {r}, {(r, ri ) | i = 1, . . . , k} ∪ E1 ∪ . . . ∪ Ek , 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

Definition 4.3. Sei B ein Wurzelbaum. Besitzt jeder Knoten v in B höchs-


tens zwei Nachfolger, so heißt B ein binärer Baum. Dabei unterscheidet man
die beiden Nachfolger in linker Nachfolger und rechter Nachfolger und die
beiden Teilbäume von v in linker Teilbaum und rechter Teilbaum.

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.

Die folgenden Algorithmen verwenden verkettete Listen von Knotenele-


menten zur Implementierung binärer Bäume. Ein Knotenelement ist definiert
durch
type node = struct
item element
node lef t, right
node parent
Die Referenz parent auf den Vaterknoten ist optional. Wir benötigen sie
nur, falls wir im Algorithmus auf den Vaterknoten zugreifen. Ein Baum ist
vom Typ tree und wird durch seinen Wurzelknoten oder eine Referenz auf
den Wurzelknoten definiert. Der Zugriff auf eine Komponente von node erfolgt
mit dem . – Operator (siehe Abschnitt 1.7).
Definition 4.4. Wir führen mit einem binären Baum Tiefensuche (depth-
first search, kurz DFS) durch (Algorithmus 4.5, Abschnitt 5.4.2). Dabei wer-
den seine Knoten in verschiedenen Reihenfolgen ausgegeben. Wir definieren
folgende Alternativen:
1. Bei Preorder-Ausgabe gebe zuerst den Knoten, dann die Knoten des lin-
ken Teilbaumes und anschließend die Knoten des rechten Teilbaumes aus.
2. Bei Inorder-Ausgabe gebe zuerst die Knoten des linken Teilbaumes, dann
den Knoten und anschließend die Knoten des rechten Teilbaumes aus.
138 4. Bäume

3. Bei Postorder-Ausgabe gebe zuerst die Knoten des linken Teilbaumes,


dann anschließend die Knoten des rechten Teilbaumes und zuletzt den
Knoten aus.
Die jeweilige Vorschrift ist ausgehend von der Wurzel rekursiv anzuwenden.
Die rekursive Definition eines Baumes ermöglicht die Tiefensuche einfach
durch einen rekursiven Algorithmus zu implementieren.
Algorithmus 4.5.
TreeDFS(node a)
1 if a.lef t ̸= null then TreeDFS(a.lef t)
2 if a.right ̸= null then TreeDFS(a.right)
3 mark node a as visited

Beispiel. Figur 4.1 zeigt die Tiefensuche in einem binären Baum.

A.

B C

D E H I

F G

Fig. 4.1: Tiefensuche.

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.

4.2 Binäre Suchbäume


Wir verwenden zur Speicherung einer geordneten Menge S einen binären
Suchbaum, falls S in einem großen Universum“ U liegt, |S| im Vergleich zu

|U | klein ist und falls die Möglichkeit Elemente hinzuzufügen oder zu löschen
gefordert ist.
Definition 4.6. Ein binärer Suchbaum für eine geordnete Menge S ist ein
binärer Baum B = (V, E) mit einer bijektiven Abbildung l : V −→ S (jeder
Knoten ist mit einem s ∈ S markiert), sodass für jeden Knoten v gilt:
4.2 Binäre Suchbäume 139

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

101 413 566 9713

434 6610 9512

598

577 649

Fig. 4.2: Tiefensuche mit Inorder-Ausgabe.

4.2.1 Suchen und Einfügen

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

Beim Löschen eines Elementes e betrachten wir folgende Fälle:


1. Es gibt keinen Knoten mit Element e: Es ist nichts zu tun.
2. Falls der Knoten mit dem Element e ein Blatt ist, können wir den Konten
einfach aus dem Baum entfernen. Wir ändern die Referenz im Vorgänger
auf null ab.
3. Wir können den Knoten v von e auch aus der verketteten Liste entfernen,
wenn v nur einen Nachfolger besitzt. Dazu muss der Vorgänger von v den
Nachfolger von v referenzieren. Die binäre Suchbaumeigenschaft ist davon
nicht betroffen.
4. Falls der Knoten v von e zwei Nachfolger besitzt, kann der Knoten v
nicht aus der verketteten Liste entfernt werden. Wir suchen im linken
4.2 Binäre Suchbäume 141

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.

Beispiel. Wir löschen die 45 im linken Baum von Figur 4.3.

.
45 .
41

18 67 lösche 45 18 67
=⇒

10 41 56 97 10 40 56 97

40 95 95

Fig. 4.3: Löschen im binären Suchbaum.

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

4.3 Ausgeglichene Bäume

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

Fig. 4.4: Fibonacci-Bäume FB2 − FB5

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

Fig. 4.5: Innere Knoten B2 − B5


144 4. Bäume

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,

wobei fh+3 die (h + 3)–te Fibonacci-Zahl ist.

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:

h < 1.45 log2 (n + 2) − 1.33.

Beweis. Für die Anzahl n der Knoten gilt:


( h+3 )
g gh+3
n ≥ bh = fh+3 − 1 = round √ − 1 > √ − 2.
5 5

Hieraus folgt 5(n + 2) > gh+3 . Es ergibt sich

h < logg ( 5(n + 2)) − 3 ≈ 1.45 log2 (n + 2) − 1.33.

Dies zeigt die Behauptung des Satzes. 2

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

in n ausgeglichen ist, d. h. ob sich die Höhen der beiden Teilbäume von n


höchstens um 1 unterscheiden. Falls dies nicht der Fall ist, erreichen wir dies
durch eine Ausgleichsoperation. Der Algorithmus benötigt nicht die Höhen
der beiden Teilbäume eines Knotens, sondern nur die Differenz aus den bei-
den Höhen, den Balancefaktor des Knotens. Die Ausgleichsoperation muss
invariant bezüglich der binären Suchbaumeigenschaft sein. Dies erreichen wir
mit Rotationen, wie Figur 4.6 zeigt.

b. a.
rechts um a
a δ =⇒ α b

α β β δ
links um b
⇐=

Fig. 4.6: Rechts-, Linksrotation.

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++

10−− 120 20−

9− 160

60

Fig. 4.7: Balancefaktoren.


4.3 Ausgeglichene Bäume 147

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

Fig. 4.8: Beispiel – Einfügen mit Rechtsrotation.

In einen ausgeglichenen Baum B werde ein Knoten eingefügt. Gilt nach


dem Einfügen bf(a) = − − oder bf(a) = + +, so ist im Knoten a die Bedin-
gung für einen ausgeglichenen Baum verletzt.
Wir diskutieren jetzt den Fall bf(a) = − −. Der hierzu symmetrische Fall
bf(a) = + + ist analog zu behandeln. Sei a ein Knoten mit bf(a) = − − und
b die Wurzel des linken von a ausgehenden Teilbaumes.
Im Allgemeinen gilt für bf(b) vor dem Einfügen und nach dem Einfügen eines
Knotens
vorher nachher
0 −, +
− −− .
+ ++
−, + 0
Wegen bf(a) = − − nahm die Höhe von b nach dem Einfügen eines Kno-
tens um eins zu. Da in den beiden Fällen der letzten Zeile die Höhe von b
unverändert bleibt, scheiden diese Fälle aus.
Für die Fälle bf(b) = − − oder bf(b) = ++ gehen wir zum Teilbaum
mit Wurzel b über. Damit bleiben die Fälle bf(a) = −−, bf(b) = − und
bf(a) = −−, bf(b) = + übrig.
Wir geben jetzt die Ausgleichsoperation für bf(a) = −−, bf(b) = − an.
Der Höhenausgleich erfolgt durch eine Rechtsrotation um b. Wir skizzieren
diese in Figur 4.9.
148 4. Bäume

a. - -
b.
b - α
rechts um b β a
β δ =⇒
δ α

Fig. 4.9: Einfügen mit Rechtsrotation.

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

Fig. 4.10: Einfachrotation gleicht nicht aus.

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

Fig. 4.11: Beispiel für eine Doppelrotation.


4.3 Ausgeglichene Bäume 149

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 α
=⇒

Fig. 4.12: Einfügen mit Doppelrotation.

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.

Die folgenden Algorithmen implementieren Einfügen in AVL-Bäume. Der


erste Parameter von AVLInsert ist der Baum, der zweite Parameter die Wur-
zel des Baums und der dritte Parameter das einzufügende Element e.

Wir erweitern die Knotenobjekte zur Darstellung der Bäume um die Kom-
ponente Balancefaktor.
type balFac = 0, −, −−, +, ++
150 4. Bäume

type node = struct


item element
node lef t, right
node parent
balFac bf

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.

Beispiel. In Figur 4.13 löschen wir die 16:

.
14 .
14 .
11
entf. 16 rechts um 11
11 15 =⇒ 11 15 =⇒
10 14

10 12 16 10 12 9 12 15

9 9

Fig. 4.13: Beispiel – Löschen mit Rechtsrotation.

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 α

Fig. 4.14: Löschen mit Rechtsrotation.


4.3 Ausgeglichene Bäume 153

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.

Es bleibt noch der Höhenausgleich beim Löschen im Fall bf(a) = − −,


bf(b) = + zu behandeln.
Beispiel. In Figur 4.15 wurde ein Nachfolger der 15 entfernt. Im Knoten, der
die 14 enthält, entsteht der Balancefaktor − −.

.
14 .
14 .
12
links um 12 rechts um 12
11 15 =⇒ 12 15 =⇒
11 14

10 12 11 13 10 13 15

13 10

Fig. 4.15: Beispiel – Löschen mit Doppelrotation.

Wir betrachten jetzt den allgemeinen Fall. Der Höhenausgleich erfolgt


durch eine Links- und Rechtsrotation, erst links um c dann rechts um c. Wir
skizzieren diese in Figur 4.16.

a. - -

b + α c.
erst links,
β c b a
dann rechts um c
γ1 γ2 =⇒ β γ1 γ2 α

Fig. 4.16: Löschen mit Doppelrotation.


154 4. Bäume

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

Fig. 4.17: Beispiel – Löschen mit mehreren Rotationen


4.3 Ausgeglichene Bäume 155

Die Herstellung der Ausgeglichenheit erfordert erst eine Rechtsrotation


um 10 und anschließend eine Rechtsrotation um 5. Der rechte Baum zeigt
das Ergebnis.

Zum Löschen eines Elements verwenden wir Delete (Algorithmus 4.10).


Delete ist so zu modifizieren, dass der Vorgänger a des Knotens b, der aus
dem Baum entfernt wurde, Rückgabewert ist. Weiter enthält der Rückgabe-
wert die Information, ob b linker (bal = +) oder rechter (bal = −) Nachfolger
von a war. Nach dem Aufruf von AVLRepair mit diesen Parametern durch-
laufen wir den Pfad P von a bis zur Wurzel. Dabei führen wir die Rotationen
zur Herstellung der AVL-Bedingung durch und aktualisieren die Balancefak-
toren. Die Rotationen, die wir beim Einfügen eines Knotens verwenden (Al-
gorithmen 4.17 und 4.18), sind bezüglich der Korrektur der Balancefaktoren
anzupassen.
Die Variable h steuert, ob in höher gelegenen Knoten Ausgleichsoperatio-
nen notwendig sind.
Algorithmus 4.19.
AVLRepair(tree t, node a, int bal)
1 node b, c; int h ← bal
2 while a ̸= null and h ̸= 0 do
3 a.bf ← a.bf + h, next ← a.parent
4 if a = next.lef t then h ← + else h ← −
5 if a.bf = −−
6 then b ← a.lef t
7 if b.bf = −
8 then R-Rot(t, b)
9 else if b.bf = 0
10 then h ← 0, R-Rot(t, b)
11 else c ← b.right, LR-Rot(t, c)
12 if a.bf = ++
13 then b ← a.right
14 if b.bf = +
15 then L-Rot(t, b)
16 else if b.bf = 0
17 then h ← 0, L-Rot(t, b)
18 else c ← b.lef t, RL-Rot(t, c)
19 if a.bf = − or a.bf = + then h ← 0
20 a ← next

Bemerkung. Einfügen eines Knotens erfordert höchstens zwei Rotationen


mit konstante Laufzeit. Zusammen mit dem Aufwand für das Auffinden der
Einfügestelle, ist der Aufwand für das Einfügen eines Elementes in der Ord-
nung O(log(n)). Die Anzahl der beim Löschen eines Elementes notwendigen
Rotationen ist durch die Höhe des Baumes beschränkt. Da die Höhe eines
156 4. Bäume

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)).

4.4 Probabilistische binäre Suchbäume

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

Fig. 4.18: Binärer Suchbaum.

Im linken Teilbaum l der Wurzel befinden sich i − 1 und im rechten Teil-


baum r befinden sich n − i viele Knoten. Deshalb gilt die folgende rekursive
Formel für die durchschnittliche Anzahl von Knoten in einem Suchpfad bei
fester Wurzel vi .

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

Ziel ist, diese Rekursionsgleichung durch eine geeignete Substitution in eine


Differenzengleichung zu transformieren. Diese Rekursionsgleichung ist der Re-
kursionsgleichung, die wir bei der Laufzeitberechnung von QuickSort gesehen
haben, ähnlich (Abschnitt 2.1.1). Wie dort hängt das n–te Glied von allen
Vorgängern ab. Eine ähnliche
∑n Substitution führt zum Erfolg.
Wir setzen xn := i=1 iP (i) und erhalten die Differenzengleichung

n−1
xn = nP (n) + iP (i)
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

2. Speichern wir die Elemente in einer zufällig gewählten Reihenfolge ab, so


erhalten wir den Durchschnittswert als Erwartungswert für die Anzahl
der Knoten eines Suchpfades. Ziel ist, den Suchbaum, wie bei zufälliger
Wahl der zu speichernden Elemente aufzubauen. Dies erreichen wir durch
folgende Konstruktion.

4.4.1 Die Datenstruktur Treap


Bei der Datenstruktur Treap (= Tr(ee) + (H)eap) überlagern wir einem
binären Suchbaum die Heapstruktur (Definition 2.11). Treaps werden in
[AragSeid89] verwendet, um randomisierte Suchbäume zu implementieren.
Die im Treap zu speichernden Elemente e = (k, p) bestehen aus der Schlüssel-
komponente k und der Prioritätskomponente p. In einem binären Suchbaum
ist die Priorität durch die zeitliche Reihenfolge definiert, in der wir die Ele-
mente in den Suchbaum einfügen. Bei einem binären Suchbaum wird das
Element Wurzel, welches wir als erstes einfügen. Jetzt soll das Element mit
der geringsten Priorität Wurzel werden. Wir weisen die Prioritäten zufällig zu
und erreichen damit, dass ein Suchbaum, wie bei zufälliger Wahl der Reihen-
folge der Elemente, entsteht. Insbesondere ist jedes Element mit der Wahr-
scheinlichkeit 1/n Wurzel.
Definition 4.21. Ein binärer Suchbaum heißt Treap, wenn für jeden Knoten
neben der Suchbaumeigenschaft auch die Heapbedingung erfüllt ist:
1. Die Schlüssel der Elemente, die im rechten Teilbaum eines Knotens v
gespeichert sind, sind größer als der Schlüssel des Elements von v. Die-
ser wiederum ist größer als die Schlüssel der Elemente, die im linken
Teilbaum von v gespeichert sind.
2. Die Priorität eines Elementes e ist kleiner als die Priorität der beiden Ele-
mente, die in den beiden Nachfolgeknoten des Knotens von e gespeichert
sind.
Figur 4.19 zeigt einen Treap.
.
(7,6)

(5,9) (11,10)

(3,11) (6,13) (9,12) (14,14)

(1,18) (8,19) (12,15) (15,17)

Fig. 4.19: Ein Treap.

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.

4.4.2 Suchen, Einfügen und Löschen in Treaps

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)

(3,11) (6,13) (9,12) (14,14)

(1,18) (8,19) (12,15) (15,17)

(13,7)

Fig. 4.20: Beispiel für Einfügen.

Anschließend wird der Knoten (13, 7) mithilfe von Rotationen solange


nach oben bewegt, bis die Heapbedingung hergestellt ist. Figur 4.21 stellt
das Ergebnis dar.
160 4. Bäume

.
(7,6)

(5,9) (13,7)

(3,11) (6,13) (11,10) (14,14)

(1,18) (9,12) (12,15) (15,17)

(8,19)
.

Fig. 4.21: Beispiel – Heapbedingung hergestellt.

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.

4.4.3 Treaps mit zufälligen Prioritäten

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

Fig. 4.22: Rechtsrotation.

Wir bringen den Knoten a durch eine Rechtsrotation um b eine Ebene


tiefer. Die Anzahl der Knoten in R vermindert sich um eins, die Anzahl der
Knoten in L ändert sich nicht. Die Situation bei Linksrotationen ist analog.
Damit a auf die Blattebene gelangt, muss a um alle Knoten aus L∪R rotieren.
Da wir bei jeder Rotation die Anzahl der Knoten in L∪R um eins vermindern
und für einen Blattknoten L∪R = ∅ gilt, ist die Anzahl der Rotationen gleich
der Summe der Knoten in den beiden Pfaden L und R.
Wir zeigen für den Erwartungswert l der Anzahl der Knoten von L und
für den Erwartungswert r der Anzahl der Knoten von R:
1 1
l =1− ,r = 1 − .
k+1 n−k
Aus diesen beiden Gleichungen folgt l+r ≤ 2 und damit auch die Behauptung
2 des Satzes. Aus Symmetriegründen genügt es, eine Formel zu zeigen. Wir
zeigen die Formel für l.
Sei x1 < x2 < . . . xk < xk+1 < . . . < xn . Wir betrachten nun den
binären Suchbaum, der entsteht, wenn wir (x1 , x2 , . . . , xn ) einer Zufallsper-
mutation unterziehen und die Elemente in der neuen Reihenfolge einfügen.
Die Zufallspermutation realisieren wir, indem wir die Elemente nacheinan-
der zufällig und gleichverteilt aus {x1 , x2 , . . . , xn } (ohne Zurücklegen) ziehen.
162 4. Bäume

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

Fig. 4.23: Linker Teilbaum – rechter Schenkel.

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 .

Im ersten Fall (x = xk+1 ) ist lk = ˜lk . Im zweiten Fall (x = xi , 1 ≤ i ≤ k)


liegen die Elemente x1 , . . . , xi−1 nicht auf P (sie liegen ja im linken Teilbaum
von xi ). Auf P liegen nur Elemente aus {xi+1 , . . . , xk }. Rekursiv folgt: Die
Anzahl der Elemente in P ist lk−i . Gemittelt über alle i ergibt sich:

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

Werden weitere Operationen gefordert, wie zum Beispiel die Vereinigung


von zwei Bäumen T1 und T2 , wobei die Schlüssel in T1 kleiner als die Schlüssel
in T2 sind, so lässt sich dies bei probabilistischen binäre Suchbäumen einfach
implementieren. Wähle eine neue Wurzel mit geeigneter Schlüssel und Prio-
ritätskomponente, mache T1 zum linken und T2 zum rechten Nachfolger der
Wurzel und lösche anschließend die Wurzel.

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

Die maximale Anzahl von Knoten ist



h
dh+1 − 1
1 + d + d2 + . . . + dh = di = .
i=0
d−1
4
Rudolf Bayer (1939 – ) ist ein deutscher Informatiker.
5
Edward Meyers McCreight ist ein amerikanischer Informatiker.
166 4. Bäume

Insgesamt folgt für die Anzahl n der Knoten


⌈ d ⌉h
−1 dh+1 − 1
1 + 2 ⌈2d ⌉ ≤n≤ .
2 −1 d−1

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).

Zur Speicherung einer geordneten Menge X verwenden wir die B-Baum-


struktur wie folgt:
1. Eine Seite enthält Elemente von X in geordneter Reihenfolge und Adres-
sen von Nachfolgeknoten. Adressen in Blattknoten werden nicht benutzt.
2. Sei d die Ordnung des B-Baumes. Für jede Seite gilt:
Anzahl der Adressen = Anzahl der Elemente + 1.
Hieraus ergeben sich folgende Bedingungen für die Anzahl der Elemente
in einer Seite.
a. Die Wurzel enthält mindestens ein Element. ⌊ ⌋ ⌈ ⌉
b. Jeder Knoten außer der Wurzel enthält mindestens d−1 2 (= d2 −1)
Elemente.
c. Jeder Knoten enthält höchstens d − 1 Elemente.
3. Die logische Datenstruktur einer Seite ist gegeben durch:

a0 x1 a1 x2 a2 . . . al−1 xl al . . . . . .

Dabei bezeichnet ai , i = 0, . . . , l, die Adresse einer Nachfolgeseite und


xi , i = 1, . . . , l, ein Element, das in der Seite gespeichert ist. Es gilt
x1 < x2 < . . . < xl < . . .. Für ein Element x bezeichnen wir mit lx die
Adresse, die links von x steht, und mit rx die Adresse, die rechts von x
steht. Für eine Adresse a bezeichnet S(a) die Seite, die a adressiert. Für
Elemente u ∈ S(lx ) und z ∈ S(rx ) gilt u < x < z.

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}

Fig. 4.24: Ein B-Baum.

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

Dies impliziert logd (n + 1) − 1 ≤ h. 2

Beispiel. Sei d = 127, n = 221 + 1(≈ 2M io.). Dann ist q = 63 und


h ≤ log64 (220 + 1) ≈ 3.3. Wir bringen die Elemente in einem B-Baum mit
vier Ebenen unter.

Wir studieren in den nächsten beiden Abschnitten Algorithmen zum Su-


chen, Einfügen und Löschen von Elementen. Die Effizienz dieser Algorithmen
ist durch die Anzahl der notwendigen Zugriffe auf den Sekundärspeicher be-
stimmt. Diese wiederum hängen von der Höhe h des B-Baumes ab. Mit Satz
4.26 folgt, dass die
( Anzahl
) der Zugriffe auf den
⌊ d−1 ⌋ Sekundärspeicher von der Ord-
nung O(logq+1 n+1 2 ) ist, wobei q = 2 , d die Ordnung des B-Baumes
und n die Anzahl der gespeicherten Elemente bezeichnet.

4.5.2 Suchen und Einfügen

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.

Die Algorithmen zum Suchen, Einfügen und Löschen verwenden Zugriffs-


funktionen, die der abstrakte Datentyp B-Baum bereitstellt. Es handelt sich
um folgende Funktionen:
1. ReadPage(address a)
liest die Seite mit der Adresse a vom Sekundärspeicher und gibt eine
Referenz auf die Seite im Hauptspeicher zurück.
2. WritePage(address a, page p)
schreibt eine Seite p in den Sekundärspeicher an die Seitenadresse a.
Ist a = 0, so belegt WritePage eine neue Seite auf dem Sekundärspeicher
und liefert deren Adresse zurück. Andernfalls gibt WritePage a zurück.
3. PageSearch(page p, item e)
sucht e in der Seite, die p referenziert und gibt ein Paar (i, adr) zurück.
Falls i > 0 gilt, ist e das i–te Element in p. Falls i = 0 ist, so befindet sich
e nicht in der Seite p. Ist in diesem Fall p ein Blatt, so gibt PageSearch
die Adresse adr = 0 zurück. Ist p kein Blatt, so gibt PageSearch die
Adresse jener Seite zurück, welche Wurzel des Teil-B-Baums ist, welcher
e enthalten könnte.
Die beiden ersten Funktionen abstrahieren von den Details über den Transfer
der Seiten vom und zum Sekundärspeicher.
4.5 B-Bäume 169

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.

Beim Einfügen eines Elementes e suchen wir zunächst mit BTreeSearch


den Knoten, welcher e nach der aufsteigend sortierten Anordnung zu spei-
chern hat. Die Suche mit BTreeSearch endet in einem Blatt des B-Baumes
erfolglos. Falls das Blatt noch nicht voll besetzt ist, fügen wir e in dieses Blatt
ein. Falls das Blatt voll ist, belegen wir eine neue Seite. Etwa die Hälfte der
Elemente der alten Seite kopieren wir in die neue Seite und fügen ein Ele-
ment zusammen mit der Adresse der neuen Seite in die Vorgängerseite ein.
Das Einfügen eines Elements ist nicht auf die Blätter beschränkt, es setzt
sich unter Umständen auf niedrigere Ebenen fort.

Wir demonstrieren das Einfügen eines Elements zunächst mit einem


Beispiel. Wir fügen D in den B-Baum der folgenden Figur ein.

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

Fig. 4.25: Einfügen eines Elements.

Wir betrachten nun den allgemeinen Fall einer vollen Seite.

a0 x1 a1 x2 a2 . . . . . . al−1 xl al

mit l = d − 1, wobei d die Ordnung des B-Baums bezeichnet. Füge e, b ein


(b = 0, falls in ein Blatt eingefügt werden soll):

a0 x1 a1 . . . ai−1 e b xi ai . . . . . . al−1 xl al = ã0 x̃1 . . . . . . ãl x̃l+1 ãl+1

Suche das mittlere Element x̃k und zerlege in

ã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.

Wir erweitern den abstrakten Datentypen B-Baum um die Funktion

(boolean insert, item f, adr b) PageInsert(page p, item e, address a).

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.

4.5.3 Löschen eines Elementes

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

Wir versuchen zunächst zwischen direkt benachbarten Seiten auszuglei-


chen. Wir bezeichnen zwei Knoten im B-Baum als direkt benachbart, falls
sie in der Ebene benachbart sind und denselben Vorgänger besitzen. Ist
es nicht möglich zwischen direkt benachbarten Seiten auszugleichen, so
fassen wir zwei direkt benachbarte Seiten zusammen und geben anschlie-
ßend eine der beiden Seiten frei. Dabei holen wir ein Element aus der
Vorgängerseite, d. h. wir setzen das Löschen rekursiv fort. Wegen
⌊ ⌋ ⌊ ⌋
d−1 d−1
−1+ +1≤d−1
2 2

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.

Fig. 4.26: Löschen von 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

Fig. 4.27: Löschen eines Elements im Blatt.

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.

Fig. 4.28: Löschen eines Elements mit Ausgleich.

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.

Fig. 4.29: Löschen eines Elements mit Ausgleich im Inneren.

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.

Fig. 4.30: Löschen eines Elements mit Reduktion der Höhe.

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

im Vergleich zur Länge des ganzen Datensatzes. Zur Beschleunigung der


Zugriffe auf die Datensätze implementieren wir einen Index. Der Index
besteht aus Suchschlüsseln und wir organisieren ihn als B-Baum.
Wegen des geringeren Speicherbedarfs für einen Suchschlüssel ist es
möglich, in den Seiten viele Suchschlüssel zu speichern. Dies erhöht den
Grad eines Knotens und bedingt eine niedere Höhe des Baumes. Die Blatt-
seiten des B-Baumes enthalten die Verweise auf die Datenseiten. Diese
Konstruktion, bestehend aus Index und Datenseiten, bezeichnen wir als
B+ -Baum.
Die Menge der Suchschlüssel kann aus Schlüsseln der Datensätze beste-
hen. Tatsächlich muss ein Suchschlüssel s in einem Blatt die Schlüssel
der nachfolgenden Datenseiten separieren, d. h. es muss gelten, dass die
Schlüssel im linken Nachfolger < s und die Schlüssel im rechten Nach-
folger > s sind. Deshalb erfordert das Löschen eines Datensatzes unter
Umständen keine Änderung der Suchschlüssel.
Wenn wir die Datenseiten in einer doppelt verketteten Liste organisieren,
dann ist es möglich, auf den Vorgänger oder Nachfolger einer Datenseite
in konstanter Zeit zuzugreifen. Als Konsequenz können wir die Bereichs-
suche von Daten, d. h. die Ausgabe von Daten, deren Schlüssel in einem
Intervall liegen, sehr effizient durchführen.
Diese Art des Zugriffs bezeichnen wir als ISAM (Indexed Sequential Ac-
cess Method) Zugriffsmethode. Sie ermöglicht sowohl sequentiellen (in
einer sortierten Reihenfolge) als auch index-basierten Zugriff auf die Da-
tensätze einer Datenbank.

Beispiel. Figur 4.31 zeigt einen B+ -Baum.

Fig. 4.31: B+ -Baum.


176 4. Bäume

4.6 Codebäume

Codebäume werden in diesem Abschnitt verwendet, um Codes für die Daten-


komprimierung graphisch darzustellen. Deshalb führen wir zunächst grundle-
gende Begriffe aus der Codierungstheorie ein. In den Abschnitten über die ad-
aptive Huffman-Codierung, arithmetische Codes und Lempel-Ziv-Codes leh-
nen wir uns an die Darstellung in [HanHarJoh98] an, eine ausgezeichnete
Einführung in die Informationstheorie und Datenkomprimierung.

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.

4.6.1 Eindeutig decodierbare Codes

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

Cn := {w ∈ Y ∗ \ {ε} | es gibt ein w′ ∈ C0 mit: w′ w ∈ Cn−1 }


∪ {w ∈ Y ∗ \ {ε} | es gibt ein w′ ∈ Cn−1 mit: w′ w ∈ C0 }.

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

gilt. Die Beziehung Cn ⊆ M folgt durch Induktion nach n. Für n = 1 folgt


die Behauptung direkt aus der Definition von C1 . Sei nun n > 1 und die
Behauptung für n − 1 bereits gezeigt, d. h. Cn−1 ⊆ M. Sei w ∈ Cn1 . Dann
4.6 Codebäume 179

gibt es ein w′ ∈ C0 mit w′ w ∈ Cn−1 . Nach Induktionsvoraussetzung gibt


es eine Darstellung ci1 . . . cik w′ w = cj1 . . . cjl , sodass w′ w und damit auch
w ein echter Postfix von cjl ist. Also ist w ∈ M. Falls w ∈ Cn2 , gibt es
ein w′ ∈ Cn−1 mit w′ w ∈ C0 . Nach Induktionsvoraussetzung gibt es eine
Darstellung ci1 . . . cik w′ = cj1 . . . cjl , sodass w′ ein echter Postfix von cjl ist.
Wir ergänzen auf beiden Seiten w am Ende und erhalten die Darstellung
ci1 . . . cik w′ w = cj1 . . . cjl w, die ∪
w ∈ M beweist.
Zu zeigen bleibt noch: M ⊆ n≥1 Cn . Für w ∈ M gibt es eine Darstellung
ci1 . . . cik w = cj1 . . . cjl , wobei w echter Postfix von cjl ist. Wir zeigen mit
Induktion nach k + l, dass w ∈ Cn für ein n ≥ 1. Für k + l = 2 muss k = l = 1
sein und ci1 w = cj1 zeigt, dass w ∈ C1 . Sei nun k + l > 2. Ist l = 1, so folgt
aus ci1 . . . cik w = cj1 , dass ci2 . . . cik w ∈ C1 und ci3 . . . cik w ∈ C2 , und so
weiter, also folgt schließlich, dass w ∈ Ck gilt.
Es bleibt der Fall l ≥ 2. Dies veranschaulichen wir mit der Skizze
c j1 cjl−1 c jl
′ w
w
.
c i1 c ir c ik
.
Es gibt ein r ≤ k mit

|ci1 . . . cir−1 | ≤ |cj1 . . . cjl−1 | < |ci1 . . . cir |

(cjl−1 endet mit cir−1 oder das Ende von cjl−1 liegt in cir ).
Sei w′ ∈ Y ∗ \ {ε} mit

cj1 . . . cjl−1 w′ = ci1 . . . cir

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

Deshalb ist Cn ⊂ Y m für alle n ∈ N. Da Y m nur endlich viele Teilmengen


besitzt, wird die Folge (Cn )n∈N periodisch. Wir müssen die Bedingung 2 von
Satz 4.33 nur für endliche viele n überprüfen. 2
Definition 4.35 (graphische Darstellung eines Codes).
1. Sei Y = {y1 , . . . , yn } ein Alphabet. Der Menge der Nachrichten Y ∗ =
∪∞ k
k=0 Y über Y ordnen wir einen Baum B zu:
a. Wurzel des Baumes ist das leere Wort {ε}.
b. Sei y = yi1 . . . yik ein Knoten in B. Der Knoten y besitzt die n
Nachfolger yi1 . . . yik y1 , . . . , yi1 . . . yik yn .

yi1 . ... yik

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

000 001 110 111

0001

Fig. 4.32: Ein Codebaum.

Definition 4.36. Ein Code C = {c1 , . . . , cm } ⊂ Y ∗ \{ε} heißt unmittelbar


oder Präfixcode 6 , falls ci kein Präfix von cj für i ̸= j ist.
Bei Präfixcodes sind die Codewörter in den Blättern des Codebaumes
lokalisiert. Deshalb sind Präfixcodes eindeutig decodierbar. Jeder Pfad im
Codebaum von der Wurzel zu einem Blatt entspricht einem Codewort. Wir
verwenden den Codebaum als Parser zum Zerlegen von c = ci1 . . . cin in
Codewörter. Die Tabelle tab ordnet einem Codewort die entsprechende Nach-
richt zu.
6
Eigentlich präfixfreier Code.
4.6 Codebäume 181

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

Präfixcodes für natürliche Zahlen – die Elias-Codes. Natürliche Zah-


len werden oft durch ihre Binärentwicklung dargestellt. Diese stellt jedoch
keinen eindeutig decodierbaren Code dar. Einen Präfixcode erhalten wir zum
Beispiel, wenn wir die Zahlen im unären Alphabet {0} codieren und die 1 für
die Markierung des Endes eines Codewortes verwenden (1 = 1, 2 = 01, 3 =
001, usw. . . . ). Für die Darstellung von z brauchen wir dann aber z Bit. Die
Elias7 -Codes sind Präfixcodes für die natürlichen Zahlen, die mit wesentlich
weniger Bit auskommen. Die Idee, der Binärentwicklung eine Codierung der
Länge der Zahl voranzustellen, liegt den Elias-Codes zugrunde und führt zu
Präfixcodes.

Beim Elias-Gamma-Code Cγ stellen wir der Binärentwicklung von z


⌊log2 z⌋ 0-Bit voran. Die Länge der Binärentwicklung berechnet sich mit der
Formel ⌊log2 z⌋ + 1. Aus den ⌊log2 z⌋ vorangestellten 0-Bit berechnen wir die
Länge des Codewortes für z. So besitzt zum Beispiel 31 die Binärentwicklung
11111. Es sind 4 0-Bit voranzustellen. Wir erhalten 000011111. Die Code-
wortlänge des Codes für z beträgt 2⌊log2 z⌋ + 1. Figur 4.33 zeigt einen Teil
des Codebaumes für Cγ .

..
0 1
. 1
0 1
. .
1 0 1
. . 2 3
0 1
.. . .
.
0 1 0 1
4 5 6 7

Fig. 4.33: Elias-Gamma-Code.

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.

Algorithmus zum Decodieren:


1. Zähle die führenden Nullen. Sei n die Anzahl der führenden Nullen. Die
ersten 2n + 1 Bit codieren die Länge l der Binärentwicklung von z.
2. Den l − 1 folgenden Bit stellen wir eine 1 voran. Dies ist die Binärent-
wicklung von z. Damit ist z decodiert.
Da Cγ ein Präfixcode ist, ist auch Cδ ein Präfixcode.
Für die Codewortlänge gilt: |Cδ (z)| = 2⌊log2 (⌊log2 z⌋ + 1)⌋ + 1 + ⌊log2 z⌋.
Für z ≥ 32 gilt |Cδ (z)| < |Cγ (z)|.

4.6.2 Huffman-Codes

Huffman8 -Codes benutzen ein statistisches Modell der zu komprimierenden


Daten. Das statistische Modell gewinnen wir aus den Auftrittshäufigkeiten
der Codewörter in der zu komprimierenden Nachricht. Aus den komprimier-
ten Daten rekonstruiert der Decodierer wieder die ursprünglichen Daten. Bei
der Codierung geht keine Information verloren. Es handelt sich um eine ver-
lustlose Codierung.
Definition 4.38. Eine Quelle (ohne Gedächtnis) (X, p) besteht aus einem
Alphabet X = {x1 , . . . , xm } und einer Wahrscheinlichkeitsverteilung
∑m p =
(p1 , . . . , pm ), d. h. pi ∈ ]0, 1], i = 1, . . . , m, und i=1 pi = 1 (siehe Definition
A.1). Die Quelle sendet das Symbol xi mit der Wahrscheinlichkeit p(xi ) = pi .
1. In der
( Informationstheorie
) wird der Informationsgehalt von xi als
log2 1/pi = − log2 (pi ) definiert. Der Informationsgehalt oder die Entro-
pie H(X) einer Quelle (X, p) ist der mittlere Informationsgehalt ihrer
Nachrichten, d. h.
∑m
H(X) := − pi log2 (pi ).
i=1

Die Maßeinheit des Informationsgehalts ist bit.


2. Sei C : X −→ Y ∗ \{ε} eine Codierung von X über Y , dann heißt


m
l(C) := pi |C(xi )|
i=1

die mittlere Codewortlänge von C.


8
David A. Huffman (1925 – 1999) war ein amerikanischer Informatiker.
4.6 Codebäume 183

3. Eine eindeutig decodierbare Codierung C : X −→ Y ∗ \{ε} heißt kompakt


oder minimal , wenn die mittlere Codewortlänge l(C) für alle eindeutig
decodierbaren Codierungen C : X −→ Y ∗ \{ε} minimal ist.

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

Beispiel. Figur 4.34 zeigt einen Huffman-Codebaum für X = {a, b, c, d, e, f },


p = (0.4, 0.25, 0.1, 0.1, 0.1, 0.05). Die Blattknoten enthalten die Nachrichten
und die Auftrittswahrscheinlichkeiten. Die Codes der Nachrichten ergeben
sich aus den Beschriftungen der Kanten der Pfade, die die Nachrichten mit
der Wurzel verbinden: C(a) = 0, C(b) = 10, C(c) = 1100, C(d) = 1101,
C(e) = 1110, C(f ) = 1111.

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

Fig. 4.34: Der Codebaum für einen Huffman-Code.

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

Bemerkung. Der Huffman-Baum ist nicht eindeutig bestimmt. Es sind nicht


einmal die Längen der Codewörter eindeutig (Übungen, Aufgabe 20). Ein-
deutig ist die mittlere Codewortlänge.
Wir beschreiben jetzt das Huffman-Verfahren durch Pseudocode. Das
Huffman-Verfahren setzt voraus, dass wir auf die Wahrscheinlichkeitsver-
teilung pr[1..m] in einer absteigenden Reihenfolge zugreifen. Dies können
wir durch die Verwendung der Datenstruktur Heap erreichen, ohne pr[1..m]
zu sortieren (Abschnitt 2.2). Der Heapaufbau erfolgt durch den Aufruf
BuildHeap(pr[1..m]) (Algorithmus 2.13) und muss vor dem Aufruf von Huff-
manCode erfolgen. Wir ordnen jeder Nachricht xi ∈ X einen Knoten zu, der
ein Blatt im Huffman-Baum darstellt, und gewichten diesen mit p(xi ).
186 4. Bäume

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

Codierung und Decodierung. Zur Codierung verwenden wir die durch


den Codebaum definierte Tabelle (xi → ci ). In dieser Tabelle schlagen wir
nach, wie die einzelnen Symbole zu codieren sind. Um x = xi1 . . . xin ∈ X ∗
zu codiert ersetzen wir xij durch cij .
Die Nachrichten der Quelle sind in den Blättern des Codebaumes lokali-
siert (Figur 4.34). Der Huffman-Code ist ein Präfixcode. Wir decodieren mit
Algorithmus 4.37. Insbesondere folgt, dass die Codierung und Decodierung
linear in der Länge der Eingabe erfolgt.
Ein adaptives Huffman-Verfahren – der Algorithmus von Faller,
Gallager und Knuth. Das Huffman-Verfahren setzt ein statistisches Mo-
dell der zu komprimierenden Daten voraus. Beim adaptiven Huffman-Ver-
fahren erfolgt die statistische Analyse gleichzeitig mit der Codierung der
Daten. Die zu komprimierenden Daten werden nur einmal gelesen. Es ist
somit möglich, aus einem Eingabestrom simultan einen komprimierten Aus-
gabestrom zu erzeugen. Nach jeder verarbeiteten Nachricht passen wir die
Quelle und mit ihr den Huffman-Code an. Die Aktualisierung der Quelle
ist einfach. Mit dem Huffman-Verfahren nach jeder Aktualisierung der Quel-
le einen Huffman-Code neu zu bestimmen, wäre ineffizient. Das adaptive
4.6 Codebäume 187

Huffman-Verfahren modifiziert mit jeder neuen Nachricht, welche die Quelle


sendet, den bestehenden Huffman-Code.
Das adaptive Huffman-Verfahren wurde von Faller10 in [Faller73] pu-
bliziert. Später wurde es von Gallager11 und Knuth12 erweitert. Zunächst
charakterisieren wir Huffman-Codes durch eine äquivalente Eigenschaft, die
Gallager-Ordnung. Mit dieser führen wir dann den Beweis, dass das Verfahren
einen kompakten Code liefert.
Definition 4.45.
1. Ein binärer Baum heißt binärer Codebaum, wenn jeder Knoten, der kein
Blatt ist, zwei Nachfolger besitzt.
2. Ein binärer Codebaum mit einer Gewichtsfunktion w auf der Menge der
Knoten heißt gewichteter Codebaum, wenn für jeden Knoten n mit den
Nachfolgern n1 und n2 gilt: w(n) = w(n1 ) + w(n2 ).
Lemma 4.46. Ein binärer Codebaum mit m Blättern besitzt 2m − 1 viele
Knoten.

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)

4 (3) A4 2 (12) 2 (15)


(10)
2 (4) 2 (7) S1 X1 Z1 W1
(13) (14) (16) (17)
L1 R1 Q1 M1
(5) (6) (8) (9)

Fig. 4.35: Codebaum mit mehreren Gallager-Ordnungen.

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)

Fig. 4.36: Ein Codebaum ohne Gallager-Ordnung.

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

die beim ersten Schritt der Konstruktion des Huffman-Baumes zusammenge-


fasst wurden. n1 und n2 sind Geschwisterknoten mit w(n1 ) ≤ w(n2 ) ≤ w(n),
wobei n ein beliebiger Knoten ist, verschieden von n1 und n2 . Entfernen
wir die beiden Blattknoten n1 und n2 , so wird der Vorgänger von n1 und
n2 ein Blattknoten und wir erhalten einen Huffman-Baum T̃ mit m − 1 vie-
len Blättern. Nach Induktionsvoraussetzung besitzt T̃ eine Gallager-Ordnung
ñ1 , . . . , ñ2(m−1)−1 . Die Anordnung n1 , n2 , ñ1 , . . . , ñ2(m−1)−1 ist eine Gallager-
Ordnung von T .
Sei nun m ≥ 2 und n1 , n2 . . . , n2m−1 eine Gallager-Ordnung von T . Dann
sind n1 und n2 Geschwisterknoten und Blätter. Sei T̃ der Baum, den wir
erhalten, nachdem wir aus T die Knoten n1 und n2 entfernen. n3 , . . . , n2m−1
ist eine Gallager-Ordnung von T̃ . Nach Induktionsvoraussetzung ist T̃ ein
Huffman-Baum. Dann ist aber auch T ein Huffman-Baum. 2
Bemerkung. Die Aussage des Satzes und der Beweis sind auch richtig unter
der schwächeren Voraussetzung, dass ein Knoten das Gewicht 0 besitzt.
Zur Repräsentation eines Codebaumes benutzen wir folgende Datenstruk-
tur:
type node = struct
string symbol
int weight
node parent, lef t, right, prev, next
Ein Knoten kann in der Variablen symbol eine Nachricht speichern. Diese
benutzen wir nur in Blattknoten. Die Variable weight speichert das Gewicht
des Knotens. Die Variable parent referenziert den Vorgänger, lef t den lin-
ken und right den rechten Nachfolger im Baum, prev den Vorgänger und
next den Nachfolger in der Gallager-Ordnung. Einen Baum repräsentieren
wir dann durch eine verkettete Struktur von Knoten.

Seien n1 und n2 Knoten in einem gewichteten Codebaum T . Mit m̃1 und


m̃2 bezeichnen wir die Vorgänger von n1 und n2 in T und mit m1 und m2
die Vorgänger von n1 und n2 bezüglich der Gallager-Ordnung. Durch Vertau-
schen der Variablen n1 .parent und n2 .parent, n1 .prev und n2 .prev, n1 .next
und n2 .next, m1 .next und m2 .next sowie m̃1 .lef t oder m̃1 .right mit m̃2 .lef t
oder m̃2 .right entsteht wieder ein Codebaum T̃ . Der Baum T̃ entsteht aus T
durch Vertauschen der Teilbäume mit den Wurzeln n1 und n2 . Wir sagen T̃
entsteht aus T durch Vertauschen der Knoten n1 und n2 . Besitzen n1 und n2
dasselbe Gewicht, d. h. n1 .weight = n2 .weight, dann ist T̃ ein gewichteter
Codebaum.

Lemma 4.49. Sei n1 , . . . , nℓ , . . . , nk , . . . , n2m−1 eine Gallager-Ordnung der


Knoten von T . nℓ und nk seien Knoten mit w(nℓ ) = w(nk ). T̃ entstehe aus
T durch Vertauschen der Knoten nℓ und nk . Dann gilt:
190 4. Bäume

n1 , . . . , nℓ−1 , nk , nℓ+1 , . . . , nk−1 , nℓ , nk+1 , . . . , n2m−1

ist eine Gallager-Ordnung der Knoten von T̃ .


Beweis. Wegen w(nℓ ) = w(nk ) sind die Gewichte der Anordnung
n1 , . . . , nℓ−1 , nk , nℓ+1 , . . . , nk−1 , nℓ , nk+1 , . . . , n2m−1 aufsteigend sortiert. Die
Bedingung bezüglich der Geschwisterknoten ist auch erfüllt. 2

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

Fig. 4.37: Vertauschung von zwei Knoten.

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

Fig. 4.38: Startzustand und Codebaum für ein Element .

Nach dem Empfang eines Symbols m erfolgt die Aktualisierung des


Huffman-Baumes durch den Algorithmus TreeUpdate.
Algorithmus 4.50.
void TreeUpdate(message m)
1 n ← leaf node corresponding to m
2 if n is the NULL node
3 then InsertNode(m)
4 n ← n.parent
5 while n ̸= root node do
6 ñ ← node of equal weight to n of the highest order
7 if n.parent ̸= ñ
8 then exchange n with ñ
9 n.weight ← n.weight + 1, n ← n.parent
10 n.weight ← n.weight + 1

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

Fig. 4.39: Einfügen von F .

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.

Satz 4.51. Das adaptive Huffman-Verfahren erzeugt einen kompakten Code.


Beweis. Es genügt zu zeigen, dass das Ergebnis des adaptiven Huffman-
Verfahren ein Code mit Gallager-Ordnung ist (Satz 4.48). Wir starten das
Verfahren mit einem Baum mit Gallager-Ordnung. Deshalb genügt es zu
zeigen, dass bei Ausführung von Algorithmus 4.50 die Gallager-Ordnung er-
halten bleibt. Nach Lemma 4.49 bleibt beim Vertauschen von zwei Knoten
mit demselben Gewicht die Gallager-Ordnung erhalten. Wenn wir das Ge-
wicht eines Knotens um eins erhöhen, ist es möglich, dass wir dadurch die
Gallager-Ordnung verletzen. Wir vertauschen deshalb den Knoten mit dem
Knoten gleichen Gewichts und höchster Gallager-Ordnung. An dieser Position
können wir das Gewicht von n um eins erhöhen, ohne die Gallager-Ordnung
zu verletzen. 2

Zur effizienten Implementierung von Zeile 6 von Algorithmus 4.50 führen


wir eine zusätzliche Datenstruktur – die Gewichtsliste – ein. Diese bewirkt,
dass wir Zeile 6 des Algorithmus in konstanter Zeit ausführen können. In der
Gewichtsliste kommen alle Gewichte vor, die im Baum vorhanden sind. Wir
organisieren die Liste als doppelt verkettete Liste, nach Gewichten sortiert.

type ListNode = struct


int weight
node highest
int nrN odes
ListNode next, prev
4.6 Codebäume 193

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.

4.6.3 Arithmetische Codes

Die arithmetische Codierung benutzt keine Codetabellen, um einzelne Sym-


bole oder Blöcke von Symbolen fester Länge zu codieren. Sie ordnet einer
kompletten Nachricht beliebiger Länge einen Code zu. Diesen Code berech-
nen wir individuell für jede Nachricht.
Das Verfahren der arithmetischen Codierung hat seinen Ursprung in den
Arbeiten [Pasco76] und [Rissanen76] aus dem Jahr 1976.
Ein arithmetischer Code ist durch die b–adische Darstellungen einer Zahl
gegeben (Satz B.2). Dazu legen wir eine Basis b ∈ N, b > 1, des Zahlensystems
fest.
Sei (X, p), X = {x1 , . . . , xm } und p = (p1 , . . . , pm ), eine Nachrichtenquelle
und sei x := xi1 . . . xin ∈ X n eine Nachricht. Zunächst ordnen wir x ein
Intervall zu:
x 7−→ [α, β[⊂ [0, 1[.
x codieren wir dann durch einen b–adischen Bruch c = .c1 c2 c3 . . . ∈ [α, β[13 .
Die Zahl c wählen wir in [α, β[ mit einer minimalen Anzahl von Stellen.
Das Codewort ist dann c1 c2 . . . ∈ {a0 , . . . , ab−1 }∗ . Für b = 2 erhalten wir Co-
des in {0, 1}∗ und für b = 10 erhalten wir Codes in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}∗ .
Intervallzuordnung. Wir beschreiben jetzt, wie die Zuordnung einer Nach-
richt zu einem Intervall erfolgt. Jedem Symbol ordnen wir ein Teilintervall
des Einheitsintervalls [0, 1[ zu. Die Länge des Teilintervalls ist die Wahrschein-
lichkeit, mit der das Symbol auftritt. Die im ersten Schritt gewonnen Teilin-
tervalle unterteilen wir nach dem gleichen Verfahren weiter, um die Intervalle
für Nachrichten der Länge 2 zu gewinnen. Wir betrachten ein
13
In diesem Abschnitt verwenden wir den Punkt .“ als Dezimaltrennzeichen

194 4. Bäume

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

.3 .39 .48 .54 .57 .6


ba bb bc bd be

Fig. 4.41: Intervallzuordnung.

Wir beschreiben jetzt den allgemeinen Fall für eine Nachrichtenquelle


(X, p), X = {x1 , . . . , xm } und p = (p1 , . . . , pm ).
Für ein Intervall I = [α, β[ führen wir folgende Notation ein:

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

sei definiert durch:


 

i−1 ∑
i
I(xi ) :=  pj , pj  ,
j=1 j=1

I(xi1 . . . xin ) := α + (β − α)I(xin ), wobei [α, β[ = I(xi1 . . . xin−1 ).

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

Ein b–adischer Bruch c liegt bei vorgegebenem n in genau einem Intervall


I(xi1 . . . xin ). Durch c und n ist deshalb xi1 . . . xin eindeutig festgelegt, d. h.
die arithmetische Codierung ist eindeutig decodierbar.
4.6 Codebäume 195

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[.

1. Berechne I(adeeba):

I(a) = [.0, .3[,


I(ad) = .0 + .3 I(d) = [.24, .27[,
I(ade) = .24 + .03 I(e) = [.267, .27[,
I(adee) = .267 + .003 I(e) = [.2697, .27[,
I(adeeb) = .2697 + .0003 I(b) = [.26979, .26988[,
I(adeeba) = .26979 + .00009 I(a) = [.26979, .269817[.

Repräsentiere I(adeeba) durch .2698.


2. Berechne I(adeebb):

I(adeebb) = .26979 + .00009 I(b) = [.269817, .269844[.

Repräsentiere I(adeebb) durch .26982.

Bemerkung. Sei (X, p) eine Nachrichtenquelle. Wir betrachten die arithmeti-


sche Codierung auf X n . Wie das obige Beispiel zeigt, erhalten wir im Allge-
meinen keine präfixfreie Codierung der Produktquelle X n .
Berechnung des Repräsentanten. Wir beschreiben jetzt den Algorith-
mus, der den Repräsentanten eines Intervalls bei gegebenen Intervallendpunk-
ten berechnet. Sei [α, β[ gegeben, α = .α1 α2 α3 . . . und β = .β1 β2 β3 . . ..
Es gelte αi = βi , für i = 1, . . . , t − 1 und αt < βt .
Für alle γ = .γ1 γ2 . . . ∈ [α, β[ gilt: γi = αi , i = 1, . . . , t − 1, d. h. jeder Re-
präsentant besitzt .α1 . . . αt−1 als Präfix.
Bei der Berechnung des Repräsentanten r mit der kürzesten b–adischen Ent-
wicklung sind folgende Fälle zu beachten:
1. Falls α = .α1 . . . αk , αk ̸= 0 und k ≤ t, so ist .α1 . . . αk der kürzeste
Repräsentant.
2. Sonst erhöhen wir αt um 1, falls .α1 . . . αt−1 (αt + 1) noch < β ist.
3. Es bleibt der Fall .α1 . . . αt−1 (αt + 1) = β, d. h. αt + 1 = βt und βt+1 =
βt+2 = . . . = 0.
τ sei die erste Stelle nach der Stelle t für die gilt ατ < b − 1, d. h. αt+1 =
αt+2 = . . . = ατ −1 = b − 1 und ατ < b − 1. Wir setzen
{
.α1 . . . ατ −1 , falls 0 = ατ = ατ +1 = . . . ,
r=
.α1 . . . (ατ + 1) sonst.

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.

β − α = β − .β1 . . . βt−1 − (α − .α1 . . . αt−1 )


= .0 . . . 0βt . . . −
.0 . . . 0αt αt+1 . . . ατ −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,

wobei H(X) die Entropie der Quelle X bezeichnet (Definition 4.38).


Beweis. Sei X = {x1 , . . . , xm } und p = (p1 , . . . , pm ). Nach Satz 4.53 gilt die
Abschätzung  
∏n
|r(xi1 . . . xin )| ≤ log2  p−1ij
 + 1.
j=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

[α′ , β ′ [ := [{bt−1 α}, {bt−1 β}[.

Dabei ist {x} := x − [x] der gebrochene Anteil von x.


α1 . . . αt−1 = β1 . . . βt−1 ist ein Präfix des zu berechnenden Codewortes, denn
jeder b–adische Bruch in [α, β[ hat α1 . . . αt−1 als Präfix.

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 adeeba zugeordnete Intervall I und den Repräsentan-
ten von I, den Code für die komprimierte Nachricht.
198 4. Bäume

I(a) = [.0, .3[,


I(ad) = .0 + .3 I(d) = [.24, .27[ −→ 2
= [.4, .7[,
I(ade) = .4 + .3 I(e) = [.67, .7[,
I(adee) = .67 + .03 I(e) = [.697, .7[,
I(adeeb) = .697 + .003 I(b) = [.6979, .6988[ −→ 69
= [.79, .88[,
I(adeeba) = .79 + .09 I(a) = [.79, .817[.

Repräsentiere [.79,.817[ durch .8 und I(adeeba) durch .2698.


Bemerkung. Bei Verwendung von Reskalierung stehen Teile des Codewortes
schon während des Codierungsprozesses zur Verfügung. Reskalierung vermin-
dert den Rechenaufwand.

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 der Nachricht baaaa zugeordnete Intervall I(baaaa).

I(b) = [.3, .6[,


I(ba) = .3 + .3 I(a) = [.3, .39[ −→ 3
= [.0, .9[,
I(baa) = .0 + .9 I(a) = [.0, .27[
I(baaa) = .0 + .27 I(a) = [.0, .081[ −→ 0
= [.0, .81[,
I(baaaa) = .0 + .81 I(a) = [.0, .243[.

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).

I(b) = [.3, .6[,


I(bb) = .3 + .3 I(b) = [.39, .48[,
I(bba) = .39 + .09 I(a) = [.39, .417[,
I(bbab) = .39 + .027 I(b) = [.3981, .4062[,
I(bbaba) = .3981 + .0081 I(a) = [.3981, .40053[,
I(bbabac) = .3981 + .00243 I(c) = [.399558, .400044[,
I(bbabacb) = ..399558 + .000464 I(b) = [.3997038, .3998496[.

Repräsentiere I(bbabacb) durch .3998.

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.

Sei α = .α1 α2 α3 . . . und β = .β1 β2 β3 . . .,


Wir sagen, es tritt Underflow ein, falls

β1 = α1 + 1, α2 = b − 1 und β2 = 0

ist.

Für r = .r1 r2 . . . ∈ [α, β[ gilt bei Vorliegen von Underflow


{
b − 1, falls r1 = α1 ,
r2 =
0, falls r1 = β1 ,
200 4. Bäume

d. h. r2 hängt funktional von r1 ab.

Wir beschreiben jetzt den Algorithmus zur Underflow-Behandlung.


Wir streichen α2 , β2 und r2 in der Darstellung von α, β und r und erhalten
α = .α1 α3 . . . , β = .β1 β3 . . . und r = .r1 r3 . . ..
Diese Operation ist die Translation um .α1 (b − 1 − α1 ) gefolgt von der
Streckung mit dem Faktor b. Für x ∈ [α, β[ gilt

.x1 x2 x3 → b(.x1 x2 x3 − α1 (b − 1 − α1 )) = .x1 x3 .

Falls Underflow vorliegt, gilt |β − α| < 1


b2 .

Wir verfahren bei der Codierung folgendermaßen:


1. Solange α1 ̸= β1
i. Solange Underflow vorliegt, führen wir die Underflow-Behandlung
durch, d. h. wir entfernen die zweite Ziffer nach dem Punkt aus α
und β und inkrementieren die Variable count (sie zählt, wie oft die
zweite Ziffer entfernt wurde).
ii. Bearbeite das nächste Symbol vom Input, d. h. wende den Algo-
rithmus zur weiteren Teilung des Intervalls auf Grund des nächsten
Symbols vom Input an.
2. Gebe r1 = α1 (hier gilt α1 = β1 ) und count mal r2 in Abhängigkeit von
r1 aus.

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).

I(b) = [.3, .6[,


I(bb) = .3 + .3 I(b) = [.39, .48[,
I(bba) = .39 + .09 I(a) = [.39, .417[,
I(bbab) = .39 + .027 I(b) = [.3981, .4062[ −→ [.381, .462[, count = 1,
I(bbaba) = .381 + .081 I(a) = [.381, .4053[,
I(bbabac) = .381 + .0243 I(c) = [.39558, .40044[ −→ [.3558, .4044[, count = 2,
I(bbabacb) = .3558 + .0486 I(b) = [.37038, .38496[
−→ 399, count = 0, [.7038 + .8496[ −→ 8.

Repräsentiere I(bbabac) durch .3998.


4.6 Codebäume 201

Reskalierung und Underflow-Behandlung verkürzen die Darstellungen der


Intervallgrenzen. Dadurch ist es möglich, die Berechnungen mit einer festen
Anzahl von Stellen durchzuführen. In der Anwendung reicht eine ganzzahlige
Arithmetik mit 32 Bit. Floatingpoint-Arithmetik ist wegen der begrenzten
Genauigkeit und des begrenzten Speichers, den Floatingpoint-Formate erlau-
ben, nicht geeignet.
Decodierung. Gegeben seien eine komprimierte Nachricht m und deren
Länge n. Gesucht ist die Nachricht xi1 . . . xin .
Wir berechnen xi1 . . . xin rekursiv:
1. i1 = j gilt genau dann, wenn m ∈ I(xj ) gilt.
2. Seien xi1 . . . xik−1 und I(xi1 . . . xik−1 ) = [α, β[ und l = β − α berechnet.
Es gilt ik = j genau dann, wenn m ∈ α + l I(xj ) ist. Dies wiederum ist
äquivalent zu (m − α)/l ∈ I(xj ).
Berechne α und l aus I(x) = [αx , βx [ und lx = βx − αx :
αneu = αalt + lalt αx ,
lneu = lalt lx .
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[.
Gegeben: .2697, 4.
α l (m − α)/l x
0 1 .2697 a
0 .3 .899 d
.24 .03 .99 e
.267 .003 .9 e
Decodierung mit Reskalierung. Bei der Decodierung einer Nachricht neh-
men wir, um die Rechnung zu vereinfachen, Reskalierung vor. Wir beschrei-
ben den Algorithmus anhand eines Beispiels.
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[.
Gegeben: .2698, 6.
m α l (m − α)/l x
.2698 .0 1 .2698 a
.0 .3 .8993333 d
.24 .03 .9933333 e
.698 .4 .3
.67 .03 .99333333 e
.98 .7 .3
.97 .03 .3333337 b
.8 .7 .3
.79 .09 .1111111 a
202 4. Bäume

Wir führen Reskalierung durch, sobald in α und β gemeinsame führende


Ziffern entstehen. Eine notwendige Bedingung dafür ist, dass in l führende
Nullen entstehen. Wir multipliziere α, l und m mit bk , wobei k gleich der
Anzahl der gemeinsamen führenden Ziffern ist.
Codierung der Länge der Nachricht. Bei der Decodierung muss die
Länge n der Nachricht bekannt sein. Die Länge n stellen wir dem Decodierer
außerhalb der codierten Nachricht zur Verfügung oder wir vereinbaren ein
spezielles Symbol EOF“ für das Ende der Nachricht. EOF sendet die Quel-

le mit einer sehr kleinen Wahrscheinlichkeit. Der Decodierer erkennt dann
das Ende der Nachricht. Natürlich verlängert sich durch dieses zusätzliche
Symbol die codierte Nachricht geringfügig.
Adaptive arithmetische Codierung. Bei der adaptiven arithmetischen
Codierung wird, wie bei der adaptiven Huffman-Codierung, das statisti-
sche Modell mit der Codierung der Nachricht entwickelt. Im Gegensatz zur
Huffman-Codierung sind die Auswirkungen der Änderung des statistischen
Modells auf den Codierer und Decodierer gering. Die weitere Teilung eines
bereits berechneten Teilintervalls erfolgt einfach nach den neu ermittelten
Wahrscheinlichkeiten. Wir machen dies mit einem Beispiel deutlich.
Beispiel. Sei X = {a, b, c, d, e}, p = (1/5, 1/5, 1/5, 1/5, 1/5).
I(a) = [.0, .2[, I(b) = [.2, .4[, I(c) = [.4, .6[, I(d) = [.6, .8[ und I(e) = [.8, 1[.

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

Lempel15 und Ziv16 führen in [ZivLem77] und [ZivLem78] neue Techniken


der Datenkomprimierung ein. Diese Methoden benötigen im Gegensatz zur
Huffman-Codierung kein statistisches Modell.
Die Methoden von Lempel und Ziv bezeichnet man als Wörterbuchme-
thoden. Die Idee besteht darin, häufig auftretende Textsegmente in einem
Wörterbuch zu speichern. Im zu komprimierenden Text ersetzen wir die Text-
segmente durch Verweise auf die Textsegmente. Benötigt der Verweis weniger
Bit als das ersetzte Textsegment, so tritt Kompression ein.
Es gibt viele Varianten der beiden ursprünglichen Algorithmen, die nach
dem Jahr der Entstehung mit LZ77 oder LZ78 bezeichnet werden. Diese
Varianten werden von populären Kompressionsapplikationen, wie GNU zip
oder Unix compress, bei der Datenübertragung, wie V.42bis, und bei der
Kompression von Graphikformaten, wie bei Graphics Interchange Format
(GIF), Portable Network Graphics (PNG) oder Adobes PDF, eingesetzt.
Lempel-Ziv-Algorithmus mit gleitendem Fenster. Beim LZ77-Algo-
rithmus lassen wir über die zu codierende Zeichenkette x1 . . . xn ein Fenster
konstanter Größe gleiten. Das Fenster besteht aus zwei Teilen, dem Text-
Puffer und dem Vorschau-Puffer, wie Figur 4.42 zeigt. Der Text-Puffer enthält
Zeichen, die bereits codiert wurden, die Zeichen aus dem Vorschau-Puffer
sind noch zu codieren. Sei w die Länge des Text-Puffers und v die Länge des
Vorschau-Puffers.

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

Fig. 4.42: LZ77-Komprimierung.

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

ℓk = max{j | j ≤ v und xk . . . xk+j−1 = xi . . . xi+j−1 }

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

xi . . . xi+m−1 7−→ (j, m).

Wir verschieben das Fenster um m Positionen nach rechts.


Falls wir keine Übereinstimmung finden, d. h. es gilt m = 0, so codieren
wir
xi 7−→ xi
und verschieben das Fenster um eine Position nach rechts.
Der Ausgabestrom enthält die beiden unterschiedlichen Datentypen Zei-
chen und Verweise, d. h. Paare (j, m) von Zahlen, j gibt den Index des ersten
Zeichens der Übereinstimmung relativ zum Ende des Text-Puffers und m
gibt die Länge der Übereinstimmung an. Wir codieren beide Datentypen mit
einer festen Anzahl von Bit und unterscheiden sie durch ein führendes Bit,
zum Beispiel 0, falls ein Zeichen folgt, und 1, falls ein Verweis folgt. Stellen
wir j mit 8 Bit und m mit 4 Bit dar, so ergibt sich ein Text-Puffer mit 256
Byte und ein Vorschau-Puffer mit 16 Byte. Zur Codierung eines Zeichens
benötigen wir 7 Bit und zur Codierung eines Verweises 13 Bit.
Insbesondere ist jetzt auch der Induktionsanfang erklärt. Zu Beginn der
Codierung ist der Text-Puffer leer. Wir können keine übereinstimmenden
Segmente finden, der Ausgabestrom enthält die einzelnen Zeichen.
Bei der Implementierung codieren wir kurze übereinstimmende Segmente
durch die einzelnen Zeichen, wenn sich dadurch ein kürzerer Ausgabestrom
ergibt. Ist m die Anzahl der übereinstimmenden Zeichen, s die Anzahl der
Bit für die Codierung eines Zeichens, t die Anzahl der Bit für die Codierung
eines Paares (j, m) und gilt m · s < t, so codieren wir die einzelnen Zeichen.

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

Beispiel. Wir führen den Algorithmus mit der Eingabe x = 01101101101,


w = 6 und v = 3 durch. Jede Zeile der folgenden Matrix entspricht einem
Schritt in der Ausführung des Algorithmus. Die 3 Zeichen des Vorschau-
Puffers sind in den ersten 5 Zeilen durch | begrenzt. In der zweiten Spalte
sind die ermittelten Codes vermerkt.
|011|01101101 0
0|110|1101101 1
01|101|101101 (1, 1)
011|011|01101 (3, 3)
011011|011|01 (3, 3)
011|011011|01 (3, 2)

Während die Kompression mit hohem Rechenaufwand verbunden ist, ist


die Dekompression einfach durchzuführen. Entweder liegen die Codes der
einzelnen Zeichen vor oder Verweise, die einfach auszuwerten sind.
Falls sich die Übereinstimmung bei der Codierung in den Vorschau-Puffer
hinein erstreckte, erweitern wir den Text-Puffer schrittweise. Diesen Punkt
erläutern wir jetzt mit einem Beispiel genauer. Sei w = 12 und v = 6 und
. . . 111110|101010 zu codieren. Die Ausgabe ist dann (2, 6). Wir nehmen an,
dass . . . 111110| bereits decodiert ist und im nächsten Schritt (2, 6) zu deco-
dieren ist. Wir erhalten nacheinander

. . . |111110|10, . . . |111110|1010, . . . |111110|101010.

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

besteht aus paarweise verschiedenen yj , 0 ≤ j ≤ k, und ist definiert durch:


1. y0 := ε.
2. Zu yi , 1 ≤ i ≤ k, gibt es genau ein yj in der Folge y0 , . . . , yi−1 und ein
x ∈ X mit yi = yj ||x.
Wir bezeichnen die Teilstrings yi als Phrasen von y.
Wir geben einen Algorithmus zur Konstruktion einer Lempel-Ziv Zerle-
gung an. Der Algorithmus benutzt einen digitalen Suchbaum. Wir erläutern
zunächst die Datenstruktur eines digitalen Suchbaums, siehe Figur 4.43.

ε.
A T
E
A E T
L R
R O
AL AR ER TO
L M R
ALL ARM TOR
E
ALLE

Fig. 4.43: Ein digitaler Suchbaum.

Einem digitalen Suchbaum liegt ein Alphabet X zugrunde. Ein Knoten


des digitalen Suchbaums kann bis zu |X| Nachfolger besitzen. Jede Kante ist
mit einem Symbol aus X beschriftet. Dabei sind keine zwei Kanten, die von
einem Knoten ausgehen, mit demselben Symbol beschriftet.
Jedem Knoten entspricht eine Zeichenkette aus X ∗ . Die Zeichenkette er-
gibt sich aus den Kantenbeschriftungen des Pfades von der Wurzel bis zum
entsprechenden Knoten. Der Wurzel entspricht das leere Wort ε. Mit dem
digitalen Suchbaum sind zwei Funktionen verbunden.
1. Die Suche nach x ∈ X ∗ erfolgt ausgehend von der Wurzel durch Vergleich
des ersten Zeichens von x mit den Kantenbeschriftungen der Kanten, die
von der Wurzel ausgehen. Bei Übereinstimmung mit einer Kantenbeschrif-
tung setzen wir die Suche rekursiv im entsprechenden Nachfolger fort.
2. Beim Einfügen von Zeichen in einen digitalen Suchbaum fügen wir neue
Knoten an. Soll eine Zeichenkette x eingefügt werden, so führen wir aus-
gehend von der Wurzel eine Suche mit x durch. Befindet sich x nicht im
Suchbaum, so endet die Suche in einem Knoten, ohne alle Zeichen von
x bei der Suche zu verbrauchen. Diesen Knoten bezeichnen wir mit k.
Für jedes Zeichen von x, das bei der Suche nicht verwendet wurde, fügen
wir einen weiteren Knoten an, die dazu notwendige Kante beschriften
wir mit dem entsprechenden Zeichen. Den ersten Knoten fügen wir an k
an, den zweiten an den ersten angefügten Knoten, und den nächsten an
4.6 Codebäume 207

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

Ausgabe: (0,0) (0,1)

.
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

Fig. 4.44: Codierung mit LZ78

In jedem Schritt ermitteln wir eine Phrase der Lempel-Ziv-Zerlegung. Sie


ergibt sich durch digitale Suche ausgehend von der Wurzel. Im letzten Knoten
k im Suchpfad erweitern wir den Baum um ein zusätzliches Blatt. Die Kante
zu diesem Blatt beschriften wir mit dem nächsten Zeichen der Eingabe. Die
gefundene Phrase entspricht dem neuen Knoten.
Die Ausgabe besteht aus dem letzten Knoten k im Suchpfad zusammen
mit der Beschriftung der neu eingefügten Kante. Die komprimierte Datei
besteht aus den Ausgaben. Die Ausgaben bestehen aus den Daten, die für
die Konstruktion des Baumes notwendig sind. Deshalb können wir aus der
komprimierten Datei den Codebaum und aus dem Codebaum anschließend
auch die ursprünglichen Daten gewinnen.
208 4. Bäume

Bei der Verarbeitung des Endes der zu komprimierenden Zeichenkette


kann die Suche in einem inneren Knoten enden. Auf die technischen Details,
die dann notwendig sind, gehen wir nicht weiter ein.
Bemerkung. Sei p eine Wahrscheinlichkeitsverteilung auf X. Mit a(x) bezeich-
nen wir die Anzahl der Phrasen die in einer Zerlegung von x ∈ X n auftreten.
Die komprimierten Daten bestehen dann aus einer Folge der Länge a(x) von
Paaren (k, c), die aus der Nummer eines Knotens im Codebaum und einem
Symbol bestehen. Die Anzahl der Knoten ist dann auch a(x). Die Darstellung
eines Knotens k benötigt log2 (a(x)) Bit und die Darstellung eines Paars (k, c)
erfordert log2 (a(x)) + e Bit (e ist eine Konstante). Insgesamt benötigen wir

a(x)(log2 (a(x)) + e)

Bit zur Codierung der Lempel-Ziv-Zerlegung.


Der Mittelwert
1 ∑
ℓn = p(x)a(x)(log2 (a(x)) + e),
n n x∈X
∏n
wobei p(x1 . . . xn ) := i=1 p(xi ), der Auftrittswahrscheinlichkeit von x1 . . . xn
ist, ergibt die mittlere Codewortlänge pro Quellsymbol aus X.
Nach dem Quellencodierungssatz (4.39) ist die Entropie einer Quelle eine
untere Schranke für die mittlere Codewortlänge eines eindeutig decodierbaren
Codes für die Quelle.
Die Lempel-Ziv-Codierung ist asymptotisch optimal, d. h.

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

Fig. 4.45: Codierung mit LZW

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

Ausgabe: 0| 0|1 0|1|01|

.
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

Ausgabe: 0|1|01|10 0|1|01|10|101

Fig. 4.46: Decodierung mit LZW

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

5. Entwickeln und implementieren Sie einen Algorithmus, der entscheidet,


ob eine gegebene Folge von Knoten eines binären Suchbaumes mit den
Schlüsseln s1 , s2 , . . . , sn = s als Suchpfad für s auftreten kann.
6. Sei T ein binärer Suchbaum, x ein Blatt von T und y der Vorgänger von
x. In x sei das Element e und in y das Element f gespeichert. Zeigen Sie,
f ist entweder das kleinste Element in T , das größer als e ist, oder das
größte Element in T , das kleiner als e ist.
7. Sei n ∈ N. Zeigen Sie, dass es einen binären Suchbaum mit n Knoten der
Höhe ⌊log2 (n)⌋ + 1 gibt.
8. Beweisen oder widerlegen Sie durch ein Gegenbeispiel die folgende Aussa-
ge: Zu jedem AVL-Baum gibt es eine Reihenfolge der Schlüssel, die beim
Einfügen ohne Rotationen zu diesem Baum führt.
9. Die Menge {1,. . . ,n} werde in der angegebenen Reihenfolge in einem lee-
ren AVL-Baum gespeichert. Bestimmen Sie die Höhe des Baumes.
10. Sei T ein binärer Suchbaum. Wir erhalten T , indem wir in T die null-
Referenzen durch externe Knoten ersetzen. In T hat jeder Knoten, der
kein Blatt ist, zwei Nachfolger und die Blätter sind die externen Knoten.
Ein Rot-Schwarz-Baum ist ein binärer Suchbaum, in dem jede Kante rot
oder schwarz gefärbt ist und in T folgende Eigenschaften erfüllt sind:
a. Die Eingangskanten von Blättern sind schwarz.
b. In einem Pfad folgt auf eine rote Kante stets eine schwarze Kante.
c. Für jeden Pfad von der Wurzel zu einem Blatt ist die Anzahl der
schwarzen Kanten gleich.
Die Rot-Schwarz-Bedingungen garantieren gute Balance-Eigenschaften.
Sei T ein Rot-Schwarz-Baum mit n Knoten. Zeigen Sie, dass die Höhe
von T durch 2 log2 (n + 1) beschränkt ist.
11. Sei (fi )i≥0 die Folge der Fibonacci-Zahlen. Bei der Fibonacci-Suche erfolgt
die Teilung des sortierten Arrays a[1..n], in dem das zu suchende Ele-
ment gefunden werden soll, mithilfe der Fibonacci-Zahlen. Wir nehmen
zunächst an, dass n = fk −1 gilt. Das Array wird dann in die Teilbereiche
a[1..fk−1 − 1], a[fk−1 ] und a[fk−1 + 1..fk − 1] der Längen fk−1 − 1, 1 und
fk−2 −1 zerlegt. Falls sich das gesuchte Element nicht an der Position fk−1
befindet, wird die Suche rekursiv mit a[1..fk−1 − 1] oder a[fk−1 + 1..fk − 1]
fortgesetzt. Falls n + 1 keine Fibonacci-Zahl ist, setze die obere Grenze
gleich der kleinsten Fibonacci-Zahl, die größer als n + 1 ist.
Arbeiten Sie die Details der Fibonacci-Suche aus. Stellen Sie die Verbin-
dung zu den Fibonacci-Bäumen (siehe Definition 4.12) her und analysie-
ren Sie die Laufzeit des Algorithmus.
12. Gegeben sei folgende Menge
S = {(1, 4), (2, 1), (3, 8), (4, 5), (5, 7), (6, 6), (8, 2), (9, 3)}
mit Schlüssel-Priorität-Paaren.
Übungen 213

a. Konstruieren Sie hierzu einen Treap durch Einfügen der Schlüssel-


Priorität-Paare in folgender Reihenfolge:
(8, 2), (9, 3), (2, 1), (5, 7), (3, 8), (1, 4), (4, 5), (6, 6).
b. Fügen Sie das neue Element (7, 0) ein und geben Sie alle relevanten
Zwischenschritte an.
c. Löschen Sie das Element (8, 2) und geben Sie alle relevanten Zwi-
schenschritte an.
d. Welcher Treap entsteht, wenn in (a) (7, 0) an Stelle von (8, 2) ein-
gefügt worden wäre?
( 2n )
n , n ≥ 0, viele binäre Bäume mit n
1
13. Zeigen Sie, dass es cn := n+1
Knoten gibt. Die Zahlen cn heißen Catalanzahlen 18 .
14. Ein B-Baum B speichert in alphabetischer Reihenfolge in der Wurzel j,
w und in der ersten Ebene a, c, f, i, o, r, u, v, y und z.
a. Stellen Sie B graphisch dar.
b. Geben Sie die Zahlen an, die als Ordnung von B möglich sind
c. Fügen Sie nacheinander die Elemente x, p, k, q, e und l ein und
skizzieren Sie nach jedem Schritt den Baum.
15. Ein B-Baum B speichert in alphabetischer Reihenfolge das Element m in
der Ebene 0, die Elemente e, h, t und x in der Ebene 1 und die Elemente
b, f, l, r, u, v, w und y in der Ebene 2.
a. Skizzieren Sie B und geben Sie die Ordnung von B an.
b. Löschen Sie nacheinander h aus B und l, b aus dem Baum den Sie er-
halten, nachdem Sie die vorangehende Löschung durchgeführt haben.
Beschreiben Sie die wesentlichen Schritte.
16. Beweisen oder widerlegen Sie die folgende Aussage: Für eine gegebene
Menge S ist die Anzahl der Knoten eines B-Baumes, der S speichert,
eindeutig.
17. Wir konstruieren zu einem B-Baum der Ordnung 4 einen binären Such-
baum. Die B-Baumseiten bilden wir wie folgt ab:
b. e.
a −→ a, bc −→ , d e f −→ .
c d f
Um einen binären Suchbaum zu erhalten, ergänzen wir mit den Kanten
des B-Baums. Wir färben die Eingangskanten von a, b, e schwarz und die
Eingangskanten von c, d, f rot.
Ordnen Sie dem B-Baum der Figur 4.24 seinen Rot-Schwarz-Baum zu
(siehe Aufgabe 10). Zeigen Sie mithilfe der definierten Abbildungsvor-
schrift, dass Rot-Schwarz-Bäume und B-Bäume der Ordnung 4 äquiva-
lente Strukturen sind. Geben Sie die inverse Transformation der obigen
Abbildungsvorschrift an, die jedem Rot-Schwarz-Baum einen B-Baum
der Ordnung 4 zuordnet.
18
Eugène Charles Catalan (1814 – 1894) war ein belgischer Mathematiker.
214 4. Bäume

18. Welcher der beiden Codes


(a) C1 = {c, bb, bbd, dea, bbaa, abbd, aacde},
(b) C2 = {c, bb, bbd, dea, abbe, baad, bbaa, aacde}
ist eindeutig decodierbar? Begründen Sie Ihre Antwort und geben Sie
gegebenenfalls ein Gegenbeispiel an.
19. Seien n1 , . . . , nℓ ∈ N und C ein Präfixcode über Y der Ordnung n. C
besitze ni Wörter der Länge i, i = 1, . . . , ℓ. Zeigen Sie, dass gilt: ni ≤
ni − n1 ni−1 − · · · − ni−1 n, i = 1, . . . , ℓ.
20. Eine Quelle besitzt die Elemente {x1 , x2 , x3 , x4 , x5 , x6 , x7 } und die Wahr-
scheinlichkeiten p1 = 27 , p2 = p3 = p4 = p5 = 17 , p6 = p7 = 14 1
.
a. Ist der Code c1 = 00, c2 = 11, c3 = 010, c4 = 100, c5 = 101, c6 =
0110, c7 = 0111 (ci codiert xi für i = 1, . . . , 7) kompakt?
b. Gibt es kompakte Codes über {0,1} für die obige Quelle mit anderen
Wortlängen? Geben Sie gegebenenfalls einen solchen Code an.
21. Der Huffman-Algorithmus kann auf beliebige Alphabete Y erweitert wer-
den. Wie ist dann bei der Konstruktion des Codes vorzugehen?
22. Gegeben sei eine Nachrichtenquelle (X, p). X = {x1 , . . . , x256 } und
pi = 2561
. Ist C = {c1 c2 c3 c4 c5 c6 c7 c8 |ci ∈ {0, 1}} ein kompakter Code
für (X, p)?
23. Gegeben sei eine Nachrichtenquelle (X, p). p = (p1 , . . . , pk ) und pi = 21νi ,
νi ∈ N, i = 1, . . . , k. Geben Sie einen kompakten Code C für (X, p) und
die mittlere Codewortlänge l(C) an.
24. Sei (X, p) eine Quelle. ℓ1 , . . . , ℓn seien die Längen der Codewörter eines
kompakten Codes über {0,1}. Zeigen Sie:

n
1 2
ℓi ≤ (n + n − 2).
i=1
2

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

Die Graphentheorie ist Teilgebiet der Diskreten Mathematik. Als mathema-


tische Disziplin hat sie einen umfangreichen, abstrakten theoretischen Anteil.
Sie weist aber auch interessante, anwendungsrelevante algorithmische Aspek-
te auf.
Viele alltägliche Probleme, wie zum Beispiel die Suche nach einem kürzes-
ten Weg in einem Verkehrs- oder Kommunikationsnetzwerk, die Darstellung
des World Wide Web (www), bestehend aus den Webseiten und ihre Vernet-
zung mithilfe von Links, Beschreibung des zeitlichen Verlaufs von Montage-
operationen durch Vorranggraphen, der Entwurf von elektronischen Schaltun-
gen, semantische Netze zur Repräsentation von Wissen oder die Modellierung
der Abhängigkeiten von Arbeitspaketen in einem Projekt führen zu einer Dar-
stellung mit Graphen. Mithilfe von Graphen lassen sich oft die strukturellen
Gegebenheiten algorithmischer Probleme beschreiben. Dies ermöglicht die
Lösung dieser Probleme durch Graphalgorithmen.
Wir studieren Graphalgorithmen in den folgenden beiden Kapiteln. Wir
behandeln die grundlegenden Algorithmen Breiten- und Tiefensuche zum sys-
tematischen Durchlaufen eines Graphen. Diese beiden Algorithmen werden
in vielen anderen Graphalgorithmen benutzt. Wir berechnen mithilfe von
Breiten- und Tiefensuche aufspannende Bäume, die Abstände von Knoten,
die Zusammenhangskomponenten eines Graphen sowie die starken Zusam-
menhangskomponenten eines gerichteten Graphen, testen auf Zyklen und
sortieren azyklische gerichtete Graphen topologisch. Ein probabilistischer Al-
gorithmus zur Berechnung eines minimalen Schnittes schließt das Kapitel
ab.

5.1 Modellierung von Problemen durch 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

Fig. 5.1: Königsberger Brückenproblem.

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

verschiedenen Partitionen. Einen bipartiten Graphen, bei dem jede Partition


3 Elemente hat und bei dem jeder Knoten aus der ersten Partition mit jedem
Knoten aus der zweiten Partition verbunden ist, bezeichnen wir mit K3,3 .

G. W S G. H2 S

H1 H2 H3 H1 W H3

Fig. 5.2: House-Utility-Problem.

In beiden skizzierten Anordnungen kreuzen sich die Versorgungsleitungen.


Mit Multiplizität gezählt, schneiden sie sich in der ersten Anordnung in neun
Punkten, in der zweiten Anordnung sind es nur noch drei Punkte. Es stellt
sich die Frage, ob es möglich ist, eine Anordnung ohne Schnittpunkte zu
finden. Diese Fragestellung führt zur Begriffsbildung eines ebenen Graphen.
Ebene Graphen. Unter einem ebenen Graphen verstehen wir einen Gra-
phen, den wir in der Ebene ohne Überschneidung von Kanten zeichnen
können. Beim vollständigen Graphen K4 mit vier Knoten gelingt dies, wie
Figur 5.3 zeigt.4 Bei K5 , dem vollständigen Graphen mit fünf Knoten, scheint
dies nicht möglich zu sein.

A. B C
A. B C

E
D
D

Fig. 5.3: K4 und K5 .

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

Fig. 5.4: Vierfarbenproblem.

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

Der Beweis verwendet eine Reduktionsmethode, die von Birkhoff12 und


Heesch13 entwickelt wurde. Diese Methode reduziert den allgemeinen Fall
auf 1482 Fälle, die mit massivem Einsatz von Rechnern gelöst wurden. Ein
neuerer Beweis, der dieselben Techniken benutzt, ist in [RobSanSeyTho97]
publiziert.
Minimale aufspannende Bäume, Abstands-, Chinese-Postman- und
Traveling-Salesman-Problem. Wir erläutern das Problem der Berech-
nung minimaler aufspannender Bäume, das Abstands-, das Chinese-Postman-
und das Traveling-Salesman-Problem mit Figur 5.5.

d5 d8
v2 v.6 v7

d6 d12 d9
d7
d1 v3 v4 d13

d2 d10
d3
v1 v5 v8
d4 d11

Fig. 5.5: Diverse Graphenprobleme.

Die Knoten vi modellieren Ortspunkte (Städte oder Straßenkreuzungen),


die Kanten modellieren Verbindungen zwischen den Ortspunkten und die
Beschriftung di an einer Kante bezeichnet die Länge oder Kosten dieser Ver-
bindung.
1. Zur Konstruktion eines minimalen aufspannenden Baums sind alle Kno-
ten mit Kanten zu verbinden (Definition 6.23). Dabei ist die Summe
der Kosten zu minimieren. Wir studieren mehrere Algorithmen für die
Berechnung eines minimalen aufspannenden Baumes (Algorithmus 6.26,
6.29, 6.34 und 6.50).
2. Beim Abstandsproblem ist ein kürzester Weg zwischen zwei Knoten vi
und vj oder allen Paaren von Knoten gesucht. Dieses Problem lösen wir
im Abschnitt 6.2 und Abschnitt 6.7.
3. Beim Chinese-Postman-Problem wird ein kürzester Weg mit Startpunkt
und Endpunkt v1 gesucht, welcher jede Verbindung mindestens einmal
durchläuft (siehe [Schrijver03, Chapter 29.1, 29.11g]).
4. Das Traveling-Salesman-Problem lautet: Gesucht ist ein kürzester Weg
mit Startpunkt und Endpunkt v1 , welcher durch jeden Knoten mindes-
tens einmal geht. Das Traveling-Salesman-Problem wird auch in
[Schrijver03, Chapter 58] behandelt.
12
George David Birkhoff (1884 – 1944) war ein amerikanischer Mathematiker.
13
Heinrich Heesch (1906 – 1995) war ein deutscher Mathematiker.
220 5. Graphen

Nicht symmetrische Beziehungen. Bei den vorangehenden Problemen


wurden zur Modellierung Graphen benutzt. Es handelt sich um symmetri-
sche Beziehungen. Bei nicht symmetrischen Beziehungen setzen wir gerichtete
Graphen ein. Wir erläutern ein derartiges Problem mit Figur 5.6.

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.

Die Knoten des Graphen modellieren Arbeitspakete in einem Projekt.


Falls das Arbeitspaket Ai das Arbeitspaket Aj voraussetzt, zeichnen wir eine
aj
gerichtete Kante: Aj −→ Ai . Hierbei ist aj der Aufwand, der zur Bearbei-
tung von Aj notwendig ist. Wir erhalten dadurch den Abhängigkeitsgraphen
der Arbeitspakete. Dieser darf natürlich keine Zyklen enthalten (Definition
5.3). Bei der Durchführung eines Projektes ist man an den längsten Pfaden
in G interessiert. Die Arbeitspakete auf einem Pfad müssen wir nacheinander
(sequentiell) bearbeiten. Eine Verzögerung bei der Bearbeitung der Arbeit-
spakete auf einem längsten Pfad verzögert die Fertigstellung des Projektes.
Deshalb heißt ein längster Pfad auch kritischer Pfad . Die Berechnung eines
kritischen Pfads stellen wir als Übungsaufgabe im Kapitel 6 (Aufgabe 3).

5.2 Grundlegende Definitionen und Eigenschaften


Die Grundbegriffe der Graphentheorie sind anschaulich und unmittelbar
verständlich. Wir stellen jetzt die grundlegenden Definitionen und Eigen-
schaften zusammen, die wir im Folgenden benötigen. Ein Graph besteht aus
Knoten (vertices oder nodes) und Kanten (edges), die Beziehungen zwischen
Knoten darstellen. Wir präzisieren die Begriffe in der folgenden Definition.
Definition 5.1.
1. Ein gerichteter Graph ist ein Paar G = (V, E), wobei V eine nicht leere
endliche Menge und E ⊂ V × V \ {(v, v) | v ∈ V } ist. V heißt die Menge
der Knoten und E heißt die Menge der (gerichteten) Kanten.
Ein Knoten w heißt benachbart oder adjazent zu v, wenn (v, w) ∈ E.
v heißt Anfangspunkt und w heißt Endpunkt der Kante (v, w).
2. Bei einem Graphen besitzen die Kanten keine Richtung. E ⊂ P2 (V ), der
Menge der zweielementigen Teilmengen von V . Die Knoten v, w ∈ V sind
benachbart oder adjazent, wenn {v, w} ∈ E, v und w heißen Endpunkte
der Kante e = {v, w}. Die Kante {v, w} heißt inzident zu v und zu w.
5.2 Grundlegende Definitionen und Eigenschaften 221

3. Sei v ∈ V . Uv := {w ∈ V | w adjazent zu v} heißt Umgebung von v.


Die Anzahl |Uv | der Elemente von Uv heißt Grad von v, kurz deg(v). Die
Anzahl |V | der Knoten heißt Ordnung von G.

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.

Definition 5.2. Sei G = (VG , EG ) ein (gerichteter) Graph.


1. Ein Graph H = (VH , EH ) heißt Teilgraph von G, wenn VH ⊂ VG und
EH ⊂ EG gilt.
2. Ein Teilgraph H von G heißt ein G aufspannender Teilgraph, wenn VH =
VG gilt.
3. Sei S ⊂ VG . Sei ES die Menge der Kanten in E, deren Endknoten (bzw.
Anfangs- und Endknoten) in S liegen. Der Graph ⟨S⟩ := (S, ES ) heißt
der von S erzeugte Teilgraph.
Beispiele: Figur 5.7 zeigt die Teilgraphen H und I von G, I ist der von {A,
B, C, D} erzeugte Teilgraph von G und H ist ein G aufspannender Teilgraph
(Teilbaum).

G: B D H: B D I: B D

A. A. A.

C E C E C

Fig. 5.7: Diverse Graphen.

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

Ein Pfad P ist einfach, falls vi ̸= vj für i ̸= j und 0 ≤ i, j ≤ n. Ein Pfad


P ist geschlossen, falls v0 = vn . Ein geschlossener Pfad P ist einfach,
falls vi ̸= vj für i ̸= j und 0 ≤ i, j ≤ n − 1.
Ein einfacher geschlossener Weg der Länge ≥ 3 heißt Zyklus oder Kreis.
Besitzt ein Graph G keine Zyklen, so ist G azyklisch.
2. Für einen gerichteten Graphen G erklären wir die Begriffe analog. In
einem Pfad P sind die Kanten gerichtet enthalten, d. h. (vi , vi+1 ) ∈
E, i = 0, . . . , n − 1. Für einen Zyklus ist eine Länge ≥ 2 gefordert.
Die Knoten v, w sind gegenseitig erreichbar , wenn v von w und w von v
aus erreichbar ist. Sind je zwei Knoten v, w ∈ V gegenseitig erreichbar,
so heißt G stark zusammenhängend .
Bemerkungen: Sei G = (V, E) ein Graph.
1. Sei v ∈ V . v ist ein (geschlossener) Weg von v nach v. Insbesondere ist v
von v aus erreichbar.
2. Die Relation erreichbar von“ definiert eine Äquivalenzrelation14 auf V .

Für einen gerichteten Graphen G = (V, E) ist die Relation gegenseitig

erreichbar“ eine Äquivalenzrelation auf V ( erreichbar von“ ist für einen

gerichteten Graphen nicht symmetrisch).
3. Sei {v, w} ∈ E. v, w, v ist ein einfacher geschlossener Weg, aber kein
Zyklus.
4. Zwei geschlossene Wege P = v0 , . . . , vn und P ′ = w0 , . . . , wn mit:

wi = v(i+j)mod(n+1) , für festes j und i = 0, . . . , n,

definieren denselben Zyklus. Die Bedingung besagt, P und P ′ unterschei-


den sich nur durch die Wahl des Anfangspunktes.
Beispiel. Figur 5.8 zeigt geschlossene Wege im Graphen
1. 6 5

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; . . ..

Fig. 5.8: Geschlossene Wege.

Mithilfe von Wegen erklären wir den Abstand von Knoten.


Definition 5.4. Sei G = (V, E) ein (gerichteter) Graph und v, w ∈ V . Der
Abstand d(v, w) zwischen den Knoten v und w ist die Länge eines kürzesten
Weges, d. h. eines Weges mit der minimalen Anzahl von Kanten, der v und w
verbindet. Falls kein Weg von v nach w existiert, so setzen wir d(v, w) = ∞.
14
Eine Relation ∼, die reflexiv (v ∼ v, v ∈ V ), symmetrisch (v ∼ w impliziert w ∼
v) und transitiv (u ∼ v und v ∼ w impliziert u ∼ w) ist, heißt Äquivalenzrelation.
5.2 Grundlegende Definitionen und Eigenschaften 223

Bemerkung. Für einen zusammenhängenden Graphen G = (V, E) ergibt sich


unmittelbar aus der Definition des Abstands von Knoten, dass d die Axio-
me einer Metrik erfüllt (Definition B.25). Bei einem stark zusammenhängen-
den gerichteten Graphen ist unter Umständen das Axiom der Symmetrie“

d(u, v) = d(v, u), u, v ∈ V , verletzt.
Definition 5.5.
1. Sei G ein Graph und sei K eine Äquivalenzklasse15 der Relation erreich-

bar von“. Der von K erzeugte Graph ⟨K⟩ heißt Zusammenhangskompo-
nente von G.
2. Sei G ein gerichteter Graph und sei K eine Äquivalenzklasse der Relation
gegenseitig erreichbar“. Der von K erzeugte Graph ⟨K⟩ heißt starke

Zusammenhangskomponente von G.
Bemerkung. Seien Ki = (Vi , Ei ), i = 1, . . . , l, die Zusammenhangskomponen-
ten von G = (V, E), dann gilt: V = ∪li=1 Vi , Vi ∩ Vj = ∅ für i ̸= j und
E = ∪li=1 Ei , Ei ∩ Ej = ∅ für i ̸= j.
Bäume. Wir haben im Kapitel 4 Wurzelbäume kennen gelernt. Bäume sind
eine spezielle Klasse von Graphen. Sie treten in den meisten Algorithmen auf,
die wir in den beiden Kapiteln über Graphen studieren.
Definition 5.6. Ein azyklischer zusammenhängender Graph G heißt (freier)
Baum. Besitzt G mehrere Zusammenhangskomponenten, so sprechen wir von
einem Wald
Bemerkung. Einem freien Baum kann ein Wurzelbaum zugeordnet werden,
wie Figur 5.9 zeigt. Dazu zeichnen wir einen Knoten als Wurzel aus und
versehen jede Kante mit einer Orientierung in der Richtung weg von der
Wurzel. Die Knoten, die den Abstand i von der Wurzel haben, bilden die
i–te Ebene des Baumes.
6.
6 5
4. 4
4.

6 1 5 1 5
1

2 3 2 3 2 3

Fig. 5.9: Diverse Bäume.

Satz 5.7. Für einen Graphen T = (V, E) sind äquivalent:


1. T ist ein Baum.
2. T ist zusammenhängend und |E| = |V | − 1.
15
Eine Äquivalenzklasse einer Äquivalenzrelation ∼ besteht aus allen zu einem
Element v äquivalenten Elementen ({w |w ∼ v}).
224 5. Graphen

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

Es wird über alle π aus der symmetrischen Gruppe Sn summiert, d. h. es sind


alle bijektiven Abbildungen auf {1, . . . , n} zu bilden ([Fischer14, Kap. 3]).
G besitze eine perfekte Zuordnung, die π definiert. Dann ist der Summand
a1π(1) · · · anπ(n) = X1π(1) · · · Xnπ(n) ̸= 0. (∗)
Da die Summanden in det(A) paarweise verschieden sind, ist auch det(A) ̸=
0. Ist umgekehrt det(A) ̸= 0, so folgt, dass mindestens ein Summand
a1π(1) · · · anπ(n) ̸= 0 ist. Es folgt die Ungleichung (∗), mit anderen Worten: π
definiert eine perfekte Zuordnung. 2
5.3 Darstellung von Graphen 225

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]).

5.3 Darstellung von Graphen

Wir bezeichnen durchweg für einen Graphen G = (V, E) mit n = |V | die


Anzahl der Knoten und mit m( =) |E| die Anzahl der Kanten von G. Für
einen Graphen gilt 0 ≤ m ≤ n2 und für einen gerichteten Graphen gilt
0 ≤ m ≤ n(n − 1). Im Folgenden nehmen wir oft an, dass V = {1, . . . , n}
ist. Damit ist für die Algorithmen keine Einschränkung der Allgemeinheit
verbunden.
16
Gottfried Wilhelm Leibniz (1646 – 1716) war ein deutscher Philosoph, Mathe-
matiker und Naturwissenschaftler. Er ist Mitbegründer der Infinitesimalrechnung
und gilt als der bedeutendste universale Gelehrte seiner Zeit.
17
Pierre-Simon Laplace (1749 – 1827) war ein französischer Mathematiker, Physi-
ker und Astronom.
18
William Thomas Tutte (1917 – 2002) war ein britisch-kanadischer Kryptologe
und Mathematiker.
226 5. Graphen

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.

( ) Graphen gilt |E| = n(n−1) im Fall eines


Bemerkung. Für einen vollständigen
gerichteten Graphen und |E| = n2 für einen Graphen.
Definition 5.10. Sei G = (V, E) ein gerichteter Graph, V = {1, . . . , n}.
1. Die Adjazenzmatrix adm ist eine n × n–Matrix,
{
1 für (i, j) ∈ E ,
adm[i, j] :=
0 sonst.

2. Die Adjazenzliste adl[1..n] ist ein Array von Listen. Für jeden Knoten
j ∈ V ist die Liste adl[j] definiert durch

i ∈ adl[j] genau dann, wenn (j, i) ∈ E gilt.

In adl[j] sind die zu j adjazenten Knoten eingetragen.


3. Das Vorgängerarray parent[1..n] wird verwendet, falls G ein Wurzelbaum
oder Wald ist. parent[i] speichert den Vorgänger von i oder den Wert 0,
falls i Wurzeln einer Zusammenhangskomponente von G ist.
4. Einen Graphen stellen wir durch die Adjazenzmatrix oder Adjazenzliste
des zugeordneten gerichteten Graphen dar.
Beispiel. Figur 5.10 zeigt die Adjazenzmatrix für
   
1. 2 0 1 0 1 1. 2 0 1 0 1
   
1 0 1 1 0 0 1 1
   
0 1 0 1 0 0 0 1
1 1 1 0 0 0 0 0
4 3 4 3

Fig. 5.10: Graph - gerichteter Graph – jeweils mit Adjazenzmatrix.

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

Fig. 5.11: Graph – gerichteter Graph – jeweils mit Adjazenzliste.

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).

5.4 Elementare Graphalgorithmen


Tiefensuche und Breitensuche stellen zwei unterschiedliche Methoden zum
Traversieren eines Graphen bereit. Traversieren bedeutet ein systematisches
Durchlaufen der Kanten des Graphen, um alle Knoten des Graphen zu besu-
chen. Tiefensuche und Breitensuche wenden wir auf Graphen und gerichtete
Graphen an. Sie sind grundlegend für viele weitere Graphalgorithmen.

5.4.1 Die Breitensuche

Wir beschreiben den Algorithmus Breitensuche (breadth-first search, kurz


BFS) zunächst informell.
(1) Wähle einen Startknoten.
(2) Besuche ausgehend von diesem Knoten alle Nachbarn n1 , n2 , . . . und dann
228 5. Graphen

(3) alle Nachbarn des ersten Nachbarn n1 und dann


(4) alle Nachbarn des zweiten Nachbarn n2 ,
(5) und so weiter . . . . . .
(6) Erreichen wir nicht alle Knoten des Graphen, so setzen wir die Breitensu-
che mit einem weiteren Startknoten, ein Knoten, der noch nicht erreicht
wurde, fort. Dies wiederholen wir solange, bis alle Knoten besucht sind.
Beispiel. Die Besuchsreihenfolge ergibt sich bei Figur 5.12 mit der Wahl des
Startknotens im Zentrum längs des gestrichelten Pfades, der im Zentrum
beginnt, dieses zweimal umrundet und im Nordosten im äußeren Knoten
endet.

Fig. 5.12: Breitensuche.

Für jeden Startknoten ergeben sich durch die Besuchsreihenfolge Ebenen.


Die Ebene 0 besteht aus dem Startknoten. Die Ebene i, i ≥ 1, besteht aus
den Knoten, die zu einem Knoten der Ebene i − 1, aber nicht zu einem Kno-
ten der Ebenen 0, . . . , i − 2 benachbart sind.

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

wobei Uj , die Umgebung von j, aus den zu j adjazenten Knoten besteht.


Konstruktion des BFS-Waldes. Für jeden Startknoten erhalten wir durch
die folgende Vorschrift einen gerichteten Baum.
Start: T = ({Startknoten}, ∅)
Sei T = (VT , ET ) konstruiert. Beim Besuch von j setzen wir

T = (VT ∪ (Uj ∩ VR ), ET ∪ {{j, k} | k ∈ Uj ∩ VR }).

Der Knoten j wird Vaterknoten für alle Knoten k ∈ Uj ∩ VR . Diese wechseln


von VR nach Vad . Da für jede Zusammenhangskomponente die Anzahl der
Kanten gleich der Anzahl der Knoten minus 1 ist, ergibt die Konstruktion
für jede Zusammenhangskomponente einen Baum.
Insgesamt erhalten wir einen aufspannenden Wald. Dieser ist nicht ein-
deutig. Er hängt von der Wahl der Startknoten und der Reihenfolge ab, in
der wir adjazente Knoten behandeln.
Beispiel. Figur 5.13 zeigt einen Graphen mit seinem BFS-Baum. Die Hoch-
zahlen bei den Knoten geben die Besuchsreihenfolge an.

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

Fig. 5.13: Breitensuche mit BFS-Baum.

Verwaltung der Knoten aus Vad in einer Queue. In einer Queue


können wir Elemente zwischenspeichern. Auf die in der Queue gespeicher-
ten Elemente können wir nicht beliebig zugreifen. Es gilt das Prinzip first

in – first out“ (FIFO-Prinzip). Dies bedeutet, dass wir auf die Elemente in
der Reihenfolge des Einspeicherns zugreifen können. Wir können immer nur
das Element entfernen, das am längsten in der Queue war.
Wir definieren eine Queue durch die Zugriffsfunktionen:
1. ToQueue(vertex k) speichert k in die Queue ein.
2. FromQueue entfernt das Element aus der Queue, das sich schon am längs-
ten in der Queue befindet.
3. QueueEmpty überprüft die Queue auf Elemente.
230 5. Graphen

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.

Mit unserem Pseudocode folgen wir der Darstellung in [Sedgewick88].


Algorithmus 5.11.
vertex parent[1..n]; node adl[1..n]; int where[1..n], nr
BFS()
1 vertex k
2 for k ← 1 to n do
3 where[k] ← 0, parent[k] ← 0
4 nr ← 1
5 for k ← 1 to n do
6 if where[k] = 0
7 then Visit(k), nr ← nr + 1

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

Laufzeitanalyse des BFS-Algorithmus. Sei G = (V, E) ein Graph, n =


|V |, m = |E| und T (n, m) die Laufzeit von BFS. Da wir jeden Knoten genau
einmal aus der Queue holen, wird die repeat-until Schleife (Zeilen 3 – 11)
n–mal wiederholt.
Bei Darstellung durch eine Adjazenzliste wird die while-Schleife für den
Knoten k (Zeilen 6 – 10) deg(k) mal durchlaufen. Insbesondere
∑ ist die An-
zahl aller Iterationen von while für alle Knoten gleich k deg(k) = 2m. Ins-
gesamt ergibt sich für die Laufzeit bei Darstellung durch eine Adjazenzliste
T (n, m) = O(n + m)19 .
Bei Darstellung durch eine Adjazenzmatrix ist die Anzahl der Iteratio-
nen der ersten Anweisung der for-Schleife gleich n2 . Für die Laufzeit folgt
T (n, m) = O(n2 ).
Weitere Leistungen von BFS.
1. Test auf Zyklen bei Graphen. Finden wir bei der Untersuchung der Umge-
bung eines Knotens k einen Knoten j mit where[j] ̸= 0 und parent[j] ̸= k
(Zeile 7 von Visit), so führt die Kante {k, j} zu einem Zyklus, denn j
befindet sich bereits im Baum. Wir können den zugehörigen Zyklus er-
mitteln, indem wir den letzten gemeinsamen Vorfahren von k und j im
BFS-Teilbaum, den das Array parent darstellt, suchen. Eine Lösung hier-
zu studieren wir im Abschnitt 6.1.3.
2. Ermittlung von Abständen. Durch den Algorithmus BFS lassen sich die
Abstände für alle Knoten zur Wurzel des jeweiligen Teilbaums berechnen.
Dazu ist die folgende Modifikation notwendig:
a. Wir definieren ein globales Array int dist[1..n].
b. In BFS wird dist initialisiert. Wir fügen in Zeile 3 dist[k] ← 0 ein.
c. In Visit, Zeile 9, fügen wir dist[no.v] ← dist[k] + 1 ein.
Nach der Terminierung sind in dist die Abstände zur jeweiligen Wurzel
gespeichert.

5.4.2 Die Tiefensuche

Wir beschreiben den Algorithmus Tiefensuche (depth-first search, kurz DFS)


zunächst informell.
19
Das bedeutet nach Definition 1.12, dass es n0 , m0 , c ∈ N gibt, sodass T (n, m) ≤
c(n + m) gilt für n ≥ n0 oder m ≥ m0 .
232 5. Graphen

(1) Wähle einen Startknoten.


(2) Besuche ausgehend von diesem Knoten den ersten Nachbarn n1 und dann
(3) den ersten Nachbarn n2 von n1 und dann
(4) den ersten Nachbarn n3 von n2 ,
(5) und so weiter . . . , bis alle ersten Nachbarn besucht sind.
(6) Besuche den zweiten Nachbarn m vom zuletzt besuchten Knoten,
(7) und so weiter . . . .
(8) Erreichen wir nicht alle Knoten, so setzen wir die Tiefensuche mit einem
weiteren Startknoten fort, ein Knoten, der bisher noch nicht erreicht wur-
de. Jetzt besuchen wir aus der Menge der nicht besuchten Knoten alle
Knoten, die von diesem Knoten aus erreichbar sind. Dies wiederholen wir
solange, bis alle Knoten besucht sind.
Beispiel. Die Besuchsreihenfolge ergibt sich bei Figur 5.14 längs des gestri-
chelten Pfades, der im Zentrum beginnt, dieses einmal umrundet und im
Nordosten im äußeren Knoten endet.

Fig. 5.14: Tiefensuche.

Wir präzisieren für einen Graphen mit Knotenmenge V = {1, . . . , n} unser


Vorgehen mit
Algorithmus 5.12.
vertex parent[1..n], node adl[1..n], boolean visited[1..n]
int btime[1..n], etime[1..n], time
DFS()
1 vertex k
2 for k ← 1 to n do
3 visited[k] ← false , parent[k] ← 0
4 time ← 0
5 for k ← 1 to n do
6 if not visited[k]
7 then Visit(k)
5.4 Elementare Graphalgorithmen 233

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

Fig. 5.15: Tiefensuche mit DFS-Baum.


234 5. Graphen

Wir setzen für einen Knoten k von G bezüglich einer DFS-Traversierung


tb (k) := btime[k] (begin time – Aufrufzeitpunkt) und te (k) := etime[k] (end
time – Terminierungszeitpunkt). Das Aktivierungsintervall
I(k) := [tb (k), te (k)]
gibt den Zeitraum an, in dem der (rekursive) Aufruf von Visit(k) aktiv ist.
Satz 5.13. Sei G = (V.E) ein (gerichteter oder nicht gerichteter) Graph und
seien j, k ∈ V . Dann gilt:
1. Falls I(j) ∩ I(k) ̸= ∅ ist, folgt I(j) ⊂ I(k) oder I(k) ⊂ I(j).
2. I(j) ⊂ I(k) gilt genau dann, wenn k Vorfahre von j im jeweiligen DFS-
Baum ist.
Beweis.
1. Sei I(j) ∩ I(k) ̸= ∅. Wir nehmen ohne Einschränkung der Allgemeinheit
tb (j) < tb (k) und tb (k) < te (j) an. Dann besuchen wir k beim rekursiven
Abstieg von j aus. Folglich muss Visit zuerst im Knoten k und dann im
Knoten j terminieren. Also folgt I(k) ⊂ I(j).
2. I(j) ⊂ I(k) ist äquivalent zu tb (k) < tb (j) und te (j) < te (k). Dies wie-
derum ist äquivalent zur Bedingung k ist Vorfahre von j im jeweiligen
DFS-Baum.
Die Behauptungen sind bewiesen. 2
Definition 5.14. Sei G = (V, E) ein gerichteter Graph und T ein DFS-Wald
von G. Eine Kante e = (v, w) ∈ E heißt Baumkante, wenn e auch eine Kante
in T ist. Sie heißt Rückwärtskante, wenn w Vorfahre von v in T ist, sie heißt
Vorwärtskante, wenn w ein Nachfahre von v in T ist. Alle übrigen Kanten
aus E heißen Querkanten.
Beispiel. Figur 5.16 zeigt einen gerichteten Graphen und einen DFS-Baum
mit eingezeichneter Quer-, Rückwärts- und Vorwärtskante.

A.

A.
C B E
B E

D F C D F

Fig. 5.16: Gerichteter Graph mit Quer-, Rückwärts- und Vorwärtskante.

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

Laufzeitanalyse des Algorithmus DFS. Sei G = (V, E) ein Graph, n =


|V |, m = |E| und T (n, m) die Laufzeit von DFS. Dann gilt T (n, m) = O(n +
m).
Der Aufwand für die Zeilen 3 und 6 in DFS ist in der Ordnung O(n).
Da Visit genau einmal für jeden Knoten aufgerufen wird, ist die Anzahl der
Aufrufe von Visit gleich n. Die Anzahl der Iterationen von while in Visit ist
durch den Grad des jeweiligen Knotens gegeben. Die Summe über die Grade
aller Knoten ergibt 2m. Insgesamt folgt T (n, m) = O(n + m).
Bemerkung. Bei der Tiefensuche ergibt sich die Besuchsreihenfolge durch die
rekursiven Aufrufe. Implizit ist damit der Aufrufstack verbunden. Ersetzt
man die Rekursion durch einen (expliziten) Stack, so ist die Besuchsreihen-
folge durch das LIFO-Prinzip (last in first out) im Gegensatz zum FIFO-
Prinzip (first in first out), das wir bei der Breitensuche anwenden, festgelegt.
Hier sieht man, dass der Bereich der besuchten Knoten bei der Tiefensuche in
die Tiefe des Graphen geht. Bei der Breitensuche dehnt sich der Bereich der
besuchten Knoten gleichmäßig längs der Grenze zwischen bereits besuchten
und noch nicht besuchten Knoten aus.

5.5 Gerichtete azyklische Graphen


Einen azyklischen gerichteten Graphen (directed acyclic graph, kurz DAG)
verwenden wir zur Modellierung der Abhängigkeiten zwischen den Kapiteln
eines Buches, den einzelnen Schritten von Produktionsabläufen oder von Da-
teien bei Include-Mechanismen. Ein weiteres Beispiel ist die Darstellung der
Struktur von arithmetischen Ausdrücken.
Beispiel. Figur 5.17 zeigt die Struktur eines arithmetischen Ausdrucks mit
wiederkehrenden Teilausdrücken:
(a + b) ∗ c ∗ ((a + b) ∗ c + (a + b + e) ∗ (e + f )).

∗.

∗ +

+ c ∗

a b + +

e f

Fig. 5.17: Syntaxgraph eines arithmetischen Ausdrucks.


236 5. Graphen

Der DFS-Algorithmus kann einen gerichteten Graphen auf Zyklen testen.


Satz 5.15. Ein gerichteter Graph ist genau dann azyklisch, wenn bei DFS
keine Rückwärtskanten entstehen.
Beweis. Jede Rückwärtskante schließt einen Zyklus. Für die umgekehrte
Richtung sei v1 , . . . , vn ein Zyklus und vi der erste Knoten, für den DFS
aufgerufen wird. Dann ist I(vi−1 ) ⊂ I(vi ), d.h. vi−1 ist ein Nachfahre von
vi im entsprechenden DFS-Baum (Satz 5.13). Die Kante (vi−1 , vi ) ist somit
eine Rückwärtskante im DFS-Wald. 2

Topologisches Sortieren. Bei azyklischen gerichteten Graphen sind die


Knoten partiell geordnet. Eine partiell geordnete Menge können wir linear
anordnen, in Übereinstimmung mit der partiellen Ordnung. Dies präzisieren
wir jetzt.
Definition 5.16.
1. (M, ≤) heißt eine partiell geordnete Menge, falls für m1 , m2 , m3 ∈ M gilt:
a. m1 ≤ m1 (Reflexivität).
b. Wenn m1 ≤ m2 und m2 ≤ m1 , dann folgt m1 = m2 (Antisymmetrie).
c. Wenn m1 ≤ m2 und m2 ≤ m3 , dann folgt m1 ≤ m3 (Transitivität).
2. Unter einer topologischen Sortierung einer partiell geordneten Menge M ,
verstehen wir eine lineare Anordnung, die die partielle Ordnung respek-
tiert, das heißt, wenn w ≤ v gilt, dann kommt w vor v in der Anordnung.
Bemerkungen:
1. Sei G = (V, E) ein azyklischer gerichteter Graph und v1 , v2 ∈ V . Wir de-
finieren v1 ≤ v2 genau dann, wenn v2 von v1 aus erreichbar ist. Hierdurch
definieren wir eine partielle Ordnung auf V .
2. Zeichnet man bei einer linearen Anordnung der Knoten des Graphen alle
Kanten des Graphen ein, so handelt es sich bei der Anordnung genau
dann um eine topologische Sortierung, wenn alle Kanten von links nach
rechts gerichtet sind.
Beispiel. Topologische Sortierungen der Knoten des Graphen der Figur 5.18
sind J,K,L,M,A,C,G,H,I,B,F,E,D und A,B,C,F,E,D,J,K,L,M,G,H,I.

A. B H I

C G

D J K

F E L M
Fig. 5.18: Topologische Sortierungen.
5.5 Gerichtete azyklische Graphen 237

Figur 5.19 zeigt, dass es sich bei J,K,L,M,A,C,G,H,I,B,F,E,D tatsächlich


um eine topologische Sortierung handelt.

J. K L M A C G H I B F E D

Fig. 5.19: Topologisches Sortieren – lineare Anordnung.

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

5.6 Die starken Zusammenhangskomponenten

Eine starke Zusammenhangskomponente eines gerichteten Graphen G ist ein


bezüglich der Relation gegenseitig erreichbar“ maximaler Teilgraph von G

(Definition 5.5). Wir behandeln zwei Algorithmen zur Ermittlung der starken
Zusammenhangskomponenten. Beide Algorithmen basieren auf dem Algorith-
mus DFS, angewendet auf einen gerichteten Graphen. Der erste Algorithmus
ist nach Tarjan benannt. Der zweite Algorithmus wurde von Kosaraju20 entwi-
ckelt, jedoch nicht publiziert. Unabhängig von Kosarajus Entdeckung wurde
der Algorithmus von Sharir21 in [Sharir81] publiziert.
Mit T bezeichnen wir in diesem Abschnitt den DFS-Baum von G (Ab-
schnitt 5.4.2). Genauer handelt es sich bei T um einen azyklischen Graphen,
der nicht zusammenhängend sein muss. Der Graph T hängt von der Reihen-
folge der Knoten k ab, in der wir Visit(k) im Algorithmus 5.12 aufgerufen.
Sei v ein Knoten von G. Mit Tv bezeichnen wir den Teilbaum von T , der aus
den Knoten besteht, die wir beim rekursiven Abstieg von v aus entdecken,
d. h. es handelt sich um die Knoten w mit tb (w) ∈ I(v).
Der Algorithmus von Tarjan. Der Algorithmus von Tarjan entsteht
durch eine Modifikation der Visit Funktion im Algorithmus 5.12. Er be-
rechnet nacheinander die starken Zusammenhangskomponenten eines gerich-
teten Graphen. Die starken Zusammenhangskomponenten ergeben sich als
Teilbäume von T . Sei C eine starke Zusammenhangskomponente von G und
sei u der erste Knoten von C, in dem wir Visit aufrufen, dann liegen alle
Knoten von C im Teilbaum Tu , dem Teilbaum mit Wurzel u. Wir nennen
u Wurzel der starken Zusammenhangskomponente C. Tarjans Algorithmus
ermittelt die Wurzeln der starken Zusammenhangskomponenten und damit
auch diese. Sei G = (V, E), V = {1, . . . , n}.

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.

2. Die while-Schleife (Visit, Zeile 5) inspiziert die Umgebung von k. Für


Knoten, die noch nicht besucht sind, erfolgt der rekursive Aufruf von
Visit. Nachdem der Aufruf terminiert, erfolgt ein Update von low[k] (Zeile
8). Befindet sich ein zu k adjazenter Knoten, der bereits besucht wurde,
auf dem Stack, erfolgt ebenfalls ein Update von low[k] (Zeile 10).
3. Nachdem die Inspektion der Umgebung von k abgeschlossen ist (die while-
Schleife terminiert), prüfen wir, ob k Wurzel einer starken Zusammen-
hangskomponente ist. Falls dies der Fall ist (Zeile 12: low[k] = df s[k]), be-
finden sich die Knoten dieser starken Zusammenhangskomponente in der
umgekehrten Reihenfolge des rekursiven Abstiegs auf dem Stack. In der
repeat-until Schleife tragen wir für jeden der Knoten dieser starken Zu-
sammenhangskomponente die Wurzel der starken Zusammenhangskom-
ponente in das Array component ein.
Für einen Knoten v sind der Teilbaum Tv ⊂ T mit Wurzel v und die
Menge der Rückwärtskanten

Rv = {(u, w) | u ∈ Tv und w ist ein Vorfahre von v in T }


240 5. Graphen

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.

Visit entdeckt in Zeile 9 Endknoten von aktiven Querkanten und Endknoten


von Rückwärtskanten, die von v ausgehen. In Zeile 10 wird für diese Knoten
v der Wert low[v] aktualisiert.
Ergibt sich low[v] aufgrund einer Rückwärtskante oder einer aktiven Quer-
kante, die von einem Nachfahren von v ausgeht, so berechnen wir low[v]
rekursiv aus den low-Werten der Nachfolger (Zeile 8).
Beispiel. Figur 5.20 zeigt einen gerichteten Graphen und einen zugeordne-
ten DFS-Baum mit eingezeichneten Rückwärts- und Querkanten. Querkanten
sind (D, A) und (K, F ). Die letzte Kante ist zum Zeitpunkt der Entdeckung
eine aktive Querkante. Neben jedem Knoten k ist hochgestellt tb (k), low(k)
notiert.

E H I .
A1,1 D4,4

E 11,11
A. D G C 2,1
G 5,4

B 3,1 F 6,4 H 9,9


7,6
B C F J J

I 10,9
8,6
K K

Fig. 5.20: Starke Zusammenhangskomponenten mit Tarjans Algorithmus.

Wir entfernen im DFS-Wald jede in eine Wurzel einer starken Zusam-


menhangskomponente eingehende Kante. Die dabei entstehenden Teilbäume
bilden die starken Zusammenhangskomponenten {A, B, C}, {D, F, G, J, K},
{H, I} und {E}.
5.6 Die starken Zusammenhangskomponenten 241

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

Der Algorithmus von Kosaraju-Sharir. Sei G ein gerichteter Graph und


Gr der G zugeordnete reverse Graph (der Graph mit denselben Knoten, aber
umgedrehten Kanten). Der Algorithmus benutzt die Tatsache, dass die star-
ken Zusammenhangskomponenten für G und Gr übereinstimmen. Sei te (v)
die Nummerierung der Knoten von G bezüglich der Terminierung von Visit.
Dann ist der Knoten v mit der höchsten te –Nummer Wurzel der letzten Kom-
ponente des DFS-Waldes, die bei DFS betreten wurde. Im reversen Graphen
242 5. Graphen

ermitteln wir die starke Zusammenhangskomponente, die v enthält, durch


DFS mit v als Startknoten. Wir erhalten folgenden Algorithmus
Algorithmus 5.23.
1. Führe DFS in G durch und nummeriere jeden Knoten v in der
Reihenfolge der Terminierung von Visit.
2. Konstruiere den zu G reversen Graphen Gr = (V, Er ) aus G.
3. Führe die Tiefensuche in Gr durch; starte Visit mit dem Knoten v
mit der höchsten Nummer te (v). Alle Knoten, die wir erreichen, bil-
den die erste starke Zusammenhangskomponente von G. Falls wir
nicht alle Knoten erreichen, betrachten wir alle verbleibenden Kno-
ten v und starten Visit mit dem Knoten mit der höchsten Nummer
te (v). Die jetzt erreichten Knoten bilden die zweite starke Zusam-
menhangskomponente von G. Wir wiederholen dies solange, bis alle
Knoten in Gr besucht sind.
Die Laufzeit für Schritt 1 und 3 ist O(n + m), die für Schritt 2 O(m).
Insgesamt folgt, dass die Laufzeit in der Ordnung O(n + m) ist.

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

Fig. 5.21: Starke Zusammenhangskomponenten mit Kosaraju-Sharir.

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

Sei v die Wurzel des aufspannenden Baumes einer Komponente von Gr


und w ein weiterer Knoten in dieser Komponente. Dann gibt es einen Pfad
von v nach w in Gr , also auch einen Pfad von w nach v in G. Wir zeigen,
dass v und w in G gegenseitig erreichbar sind. Ohne Einschränkung sei v ̸= w.
Angenommen, es wäre tb (w) < tb (v). Da es in G einen Pfad von w nach v
gibt, folgt te (v) < te (w). Ein Widerspruch, da die Wurzel v die höchste
Nummer te (v) hat. Also gilt: tb (v) < tb (w). Da te (w) < te (v) gilt, folgt
I(w) ⊂ I(v). Somit ist v Vorfahre von w im entsprechenden DFS-Baum von
G (Satz 5.13). Daher gibt es einen Pfad von v nach w in G. v und w sind
gegenseitig erreichbar. Hieraus ergibt sich, dass zwei beliebige Knoten aus
einer Komponente von Gr in G gegenseitig erreichbar sind. 2

5.7 Ein probabilistischer Min-Cut-Algorithmus

Der probabilistische Min-Cut-Algorithmus (Algorithmus 5.33) ist ein Monte-


Carlo-Algorithmus (Definition 1.49). Er basiert auf einer einfachen Idee. Der
unmittelbar aus dieser Idee folgende Algorithmus SimpleMinCut hat eine ge-
ringe Erfolgswahrscheinlichkeit. Durch eine bemerkenswerte Idee können wir
die Erfolgswahrscheinlichkeit beträchtlich erhöhen. Durch unabhängige Wie-
derholungen, ein Standardverfahren bei probabilistischen Algorithmen, wird
die Erfolgswahrscheinlichkeit nochmals erhöht. Der Algorithmus wurde erst-
mals in [Karger93] publiziert. Wir folgen mit unserer Darstellung [KarSte96].
Eine weitere Methode zur Berechnung eines minimalen Schnitts studieren wir
im Abschnitt 6.8 (Satz 6.67).
Definition 5.25. Sei G = (V, E) ein zusammenhängender Multigraph.22 Ei-
ne Teilmenge C ⊂ E heißt ein Schnitt für G, wenn der Graph G̃ = (V, E \ C)
in mindestens zwei Komponenten zerfällt. Ein Schnitt C heißt minimaler
Schnitt (min-cut), wenn |C| minimal für alle Schnitte von G ist.
Bemerkung. Sei C ein minimaler Schnitt von G. Dann gilt

|C| ≤ min deg(v).


v∈V

|C| kann auch < minv∈V deg(v) sein.


Definition 5.26. Sei G = (V, E) ein zusammenhängender Multigraph. Sei
e = {v, w} ∈ E. Der Multigraph G/e entsteht aus G durch Kontraktion von
e, wenn wir in G alle Kanten zwischen v und w entfernen und die Knoten v
und w identifizieren.
Der folgende Algorithmus RandContract wählt in einem Graphen G =
(V, E) mit n = |V | und m = |E| eine Kante e zufällig und berechnet G/e. G sei
durch eine ganzzahlige Adjazenzmatrix adm[1..n, 1..n] gegeben. adm[i, j] = k
22
Zwischen zwei Knoten sind mehrere Kanten erlaubt.
244 5. Graphen

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

Fig. 5.22: SimpleMinCut.

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)

Beweis. Sei C ein minimaler Schnitt von G, k = |C| und

G = I0 = (V0 , E0 ), I1 = (V1 , E1 ), . . . , In−2 = (Vn−2 , En−2 )

die Folge der Multigraphen, die bei Ausführung von SimpleMinCut durch
Kontraktion entsteht. Nach der i–ten Iteration entsteht Ii .

pr := p(SimpleMinCut berechnet einen minimalen Schnitt)


≥ p(SimpleMinCut gibt C zurück).

Die letzte Wahrscheinlichkeit ist gleich der Wahrscheinlichkeit, dass Rand-


Contract in Zeile 3 keine Kante aus C wählt.

n−3
|Ei | − k ∏
n−3
k
p(SimpleMinCut gibt C zurück) = = 1− .
i=0
|Ei | i=0
|Ei |
246 5. Graphen

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)

Dies zeigt die Behauptung des Satzes. 2


Bemerkung. Jeder minimale Schnitt in K3 , dem vollständige Graphen mit drei
Knoten (und drei Kanten), besteht aus einem Paar von Kanten. Die untere
Schranke für die Wahrscheinlichkeit, dass SimpleMinCut einen bestimmten
minimalen Schnitt berechnet, wird für K3 angenommen.
Die Wahrscheinlichkeit dafür, dass die gewählte Kante nicht in C liegt, ist
für die erste Kante am höchsten und nimmt für jede weitere Kante ab. Um
die Erfolgswahrscheinlichkeit zu erhöhen, wenden wir folgende Idee an. Führe
nur so viele Kontraktionen durch, dass etwa √n2 Knoten übrig beiben (statt
n − 2 vielen Kontraktionen), wobei n gleich der Anzahl der Knoten von G
ist, und setze das Verfahren rekursiv fort. Führe insgesamt zwei unabhängige
Wiederholungen durch. Dadurch wird die Erfolgswahrscheinlichkeit – wie wir
gleich sehen werden – beträchtlich erhöht.
Algorithmus 5.30.
edgeset L(graph G)
1 graph I ⌈← I˜ ← G
(√ )⌉
2 int t ← √n2 + 2−1
3 if t ≤ 10
4 then enumerate all cuts and return a minimal cut
5 while |VI | ≥ t + 1 do
6 I ← RandContract(I)
7 while |VI˜| ≥ t + 1 do
8 I˜ ← RandContract(I) ˜
9 EI ← L(I), EI˜ ← L(I) ˜
10 if |EI | ≤ |EI˜|
11 then return EI
12 else return EI˜

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 ).

Mit k = 2 log2 (n − α) folgt nach Lemma B.24


( ) ( )
T̃ (n) = O 2 log2 (n − α)22 log2 (n−α) = O n2 log2 (n) .
( )
Wegen T (n) ≤ T̃ (n) ist auch T (n) von der Ordnung O n2 log2 (n) . 2

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

(siehe Beweis von Satz 5.29).


Die Erfolgswahrscheinlichkeit von L ist durch zwei unabhängige Phasen
bestimmt. Die Wahrscheinlichkeit, dass ein Schnitt C mit |C| = k die erste
Phase, die Ausführungen von RandContract, überlebt, ist ≥ 1/2. Die zweite
Phase besteht aus den rekursiven Ausführungen von L.
Sei prG := p(L berechnet einen minimalen Schnitt von G). Da die Wahr-
scheinlichkeit, dass L mit I oder I˜ einen minimalen Schnitt von G berechnet,
größer gleich 12 prI oder 12 prI˜ ist, und die beiden Ereignisse unabhängig sind,
gilt ( )( )
1 1
prG ≥ 1 − 1 − prI 1 − prI˜ .
2 2
Tatsächlich hängt diese Formel nur von n = |VG | und von t = |VI | = |VI˜| ab.
Wir ersetzen prG durch prn und prI = prI˜ durch prt und erhalten
1
prn ≥ prt − prt2 .
4

Wir setzen wie oben α = √2−1 2
, n = 2k + α und xk = pr(√2k +α) . Da der
Algorithmus 5.30 für kleine n einen minimalen Schnitt berechnet, gilt x1 = 1.
1
xk = xk−1 − x2k−1 .
4
Wir setzen yk := 4
xk − 1. Es gilt xk = 4
yk +1 und

1
yk = yk−1 + + 1.
yk−1
23
Dies ist eine einfache nicht lineare Differenzengleichung erster Ordnung.
Wir zeigen durch Induktion nach k, dass

k < yk < k + Hk−1 + 3

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.

Dabei ist auch die unbeschränkte Fläche zu zählen.


3. Sei G = (V, E) ein Graph. Mit ∆ = maxv∈V deg(v) bezeichnen wir den
Maximalgrad von G. Zeigen Sie: G kann mit ∆ + 1 vielen Farben so
gefärbt werden, dass benachbarte Knoten verschiedene Farben besitzen.
4. Zeigen Sie, dass in einem Graphen die Anzahl der Knoten ungeraden
Grads gerade ist.
5. Seien [x], [y] ∈ Z21 . [x] ∼ [y] genau dann, wenn x ≡ y mod 7 gilt. Zeigen
Sie, dass dadurch eine Äquivalenzrelation definiert wird. Stellen Sie diese
mit einem Graphen dar und charakterisieren Sie die Äquivalenzklassen.
6. Sei G = (V, E) ein Graph, n = |V |, m = |E|. Zeigen Sie:
a. Ist G zusammenhängend,
( ) dann gilt m ≥ n − 1.
b. Für m ≥ n−12 + 1 folgt, dass G zusammenhängend ist.
7. Sei G = (V, E) ein Graph mit |V | ≥ 2. Zeigen Sie, dass folgende Aussagen
äquivalent sind:
a. G ist ein Baum.
b. G ist azyklisch und besitzt genau |V | − 1 viele Kanten.
c. G ist azyklisch und beim Hinzufügen einer Kante entsteht stets ein
Zyklus.
d. G ist zusammenhängend und G \ {e} := (V, E \ {e}) zerfällt in genau
zwei Komponenten für alle e ∈ E.
e. Zu je zwei Knoten gibt es genau einen Pfad, der diese Knoten ver-
bindet.
8. Sei G = (V, E) ein Graph und |V | = 6. Zeigen Sie: Es gibt 3 Knoten,
sodass der von diesen Knoten erzeugte Teilgraph vollständig ist, oder es
gibt 3 Knoten, sodass der davon erzeugte Teilgraph aus isolierten Knoten
besteht.
9. Sei G = (V, E) ein Graph und n = |V | ≥ 3. Für je zwei Knoten k und
l, die nicht adjazent sind, gelte deg(k) + deg(l) ≥ n. Dann besitzt G
einen Hamiltonkreis 24 , d. h. einen Zyklus, der jeden Knoten (genau ein-
24
William Rowan Hamilton (1805 – 1865) war ein irischer Mathematiker und Phy-
siker, der vor allem für seine Beiträge zur Mechanik und für seine Untersuchung
der Quaternionen bekannt ist.
Übungen 251

mal) enthält. Dieses Ergebnis wird Ore zugeschrieben ([Gould88, Theo-


rem 5.2.1]). Insbesondere besitzt ein Graph mit deg(v) ≥ n2 für v ∈ V
einen Hamiltonkreis.
10. Sei V = {I1 , . . . , In } eine Menge von Intervallen. Wir ordnen V den
Intervallgraphen G = (V, E) zu: {Ii , Ij } ∈ E, 1 ≤ i, j ≤ n, i ̸= j, genau
dann, wenn Ii ∩ Ij ̸= ∅ gilt. Geben Sie einen effizienten Algorithmus an,
der bei gegebenem V den Intervallgraphen G erzeugt. Analysieren Sie
Ihren Algorithmus.
11. Alkuin war ein Gelehrter am Hof Karls des Großen und einer seiner wich-
tigsten Berater. Eine mathematische Aufgabensammlung, die Alkuin zu-
geschrieben wird, enthält die folgende Aufgabe [Herrmann16, Seite 259]:
Ein Mann will mit einem Wolf, einer Ziege und einem Krautkopf einen
Fluss überqueren. Dazu steht ihm ein Boot zur Verfügung, worin außer
ihm selbst als Ruderer nur ein Tier oder der Krautkopf Platz findet. Sind
Ziege und Wolf allein, so frisst der Wolf die Ziege. Sind Ziege und Kraut-
kopf allein, so frisst die Ziege den Krautkopf. Wie kann der Mann Wolf,
Ziege und Krautkopf sicher an das andere Ufer bringen?
12. Entwickeln Sie eine nicht rekursive Version von DFS.
13. Wir klassifizieren die Kanten eines Graphen analog zu Definition 5.14.
Welche Typen von Kanten (Baum-, Vorwärts-, Rückwärts- oder Quer-
kanten) entstehen in Abhängigkeit von der Art des Traversierens (BFS
oder DFS) bei Graphen und bei gerichteten Graphen?
14. Welche der linearen Anordnungen (1) A,G,H,I,B,F,D,E,J,C,K,L,M
(2) A,B,J,K,L,M,C,G,H,I,F,E,D (3) A,B,J,K,L,M,G,C,H,I,F,E,D sind to-
pologische Sortierungen des Graphen aus dem Beispiel auf Seite 236. Ge-
ben Sie die Aufrufe von Visit für die topologischen Sortierungen an. Kann
jede topologische Sortierung von V durch TopSort erzeugt werden?
15. Zeigen Sie, dass ein gerichteter azyklischer Graph G mindestens einen
Knoten besitzt, in den keine Kante hineinführt, und zeigen Sie weiter,
dass es genau einen Knoten gibt, in den keine Kante hineinführt, wenn G
ein gerichteter Baum ist, d. h. es gibt einen Knoten von dem alle übrigen
Knoten erreichbar sind.
16. Sei G = (V, E) ein gerichteter Graph, V1 , . . . , Vr seine starken Zusam-
menhangskomponenten. Gred = (Vred , Ered ), wobei Vred = {V1 , . . . , Vr },
Ered = {(Vi , Vj ) | i ̸= j und es gibt v ∈ Vi , w ∈ Vj : (v, w) ∈ E}, heißt
der G zugeordnete reduzierte Graph. Zeigen Sie: Gred ist azyklisch.
17. Sei A die Adjazenzmatrix eines gerichteten Graphen G und Ar die r–te
Potenz von A, r ≥ 1.
a. Zeigen Sie: G besitzt Ar [i,j] viele verschiedene gerichtete Wege von
i nach j der Länge r. Insbesondere ist G genau dann ein gerichteter
azyklischer Graph, wenn es ein r ∈ N mit Ar = 0 gibt (d. h. A ist
nilpotent).
252 5. Graphen

b. Sei A ∈ M (n × n, {0, 1}), A[i, i] = 0, i = 1, . . . , n. Geben Sie einen


effizienten Algorithmus an, der entscheidet, ob A nilpotent ist.
18. Sei (M, ≤) eine endliche geordnete Menge (d. h. für a, b, c ∈ M gilt:
a ≤ a, aus a ≤ b und b ≤ a folgt a = b, aus a ≤ b und b ≤ c folgt a ≤ c).
Wegen der Transitivität der ≤ Relation können aus Relationen der Form
a < b, a ̸= b und es gibt kein c mit: a < c < b, alle Relationen gewonnen
werden. Wir ordnen (M, ≤) einen gerichteten Graphen G = (V, E) zu:
V = M, E = {(a, b)|a < b, a ̸= b und es gibt kein c mit: a < c < b}.
a. Sei M = {a, b, c, d, e, f }. Es gelte: a < b, a < c, a < d, a < e, a < f ,
b < c, b < f , c < e, c < f , e < d, e < f .
Zeichnen Sie den (M, ≤) zugeordneten gerichteten Graphen.
Für die folgenden Teilaufgaben sei (M, ≤) eine beliebige endliche geord-
nete Menge.
b. Besitzt der (M, ≤) zugeordnete gerichtete Graph Zyklen? Was sind
seine starken Zusammenhangskomponenten?
c. Wie bestimmen Sie mithilfe des (M, ≤) zugeordneten gerichteten
Graphen für a ∈ M alle b ∈ M mit b > a und wie alle b ∈ M
mit b < a?
19. Sei G = (V, E) ein zusammenhängender Graph, |V | ≥ 3 und T ein
DFS-Baum von G mit Wurzel r. R bezeichnet die Menge der Rückwärts-
kanten. Ein Knoten v ∈ V heißt Artikulationspunkt , wenn G \ {v} :=
(V \ {v}, E \ {e ∈ E | v Endpunkt von e}) nicht zusammenhängend ist.
Ein Graph ohne Artikulationspunkte heißt zweifach zusammenhängend
oder nicht separierbar . Ein Block eines Graphen ist ein maximaler nicht
separierbarer Teilgraph von G.
a. Machen Sie sich die Begriffe mithilfe einer Skizze klar.
b. Zeigen Sie: v ist genau dann Artikulationspunkt von G, wenn es
u, w ∈ V \ {v} gibt, sodass jeder einfache Pfad von u nach w durch
v geht.
c. Zeigen Sie: r ist genau dann ein Artikulationspunkt, wenn r mindes-
tens zwei Söhne besitzt.
d. Sei v ∈ V, v ̸= r. Zeigen Sie: v ist genau dann ein Artikulationspunkt,
wenn es einen Sohn v ′ von v gibt und für alle (u, w) ∈ R ist w kein
Vorfahre von v, falls u ein Nachfahre von v ′ oder v ′ ist.
e. low(v) := min({tb (v)} ∪ {tb (w)|(u, w) ∈ R und u ist Nachfahre von
v oder u = v}).
Zeigen Sie, dass gilt: v ∈ V, v ̸= r ist genau dann Artikulationspunkt,
wenn es einen Sohn v ′ von v gibt mit low(v ′ ) ≥ tb [v].
f. Geben Sie einen Algorithmus zur Berechnung von low(v) für alle
v ∈ V und zum Auffinden der Artikulationspunkte an.
20. Sei G = (V, E) ein zusammenhängender Graph, |V | ≥ 3. Zeigen Sie, G
ist genau dann zweifach zusammenhängend, wenn es für je zwei Knoten
einen Zyklus gibt, der beide enthält.
6. Gewichtete Graphen

In diesem Kapitel behandeln wir gewichtete Graphen, also Graphen, deren


Kanten positiv gewichtet sind. Im Einzelnen befassen wir uns mit der Be-
rechnung minimaler aufspannender Bäume, dem Abstandsproblem und der
Berechnung des maximalen Flusses in einem Netzwerk.
Im ersten Abschnitt studieren wir Algorithmen, die wir später anwenden.
Es geht um Priority-Queues, den Union-Find-Datentyp, das LCA-Problem
und ein effizienteres Verfahren für das RMQ-Problem aus dem ersten Kapitel.
Priority-Queues implementieren wir mit binären Heaps. Bei der Analyse des
Union-Find-Datentyps wenden wir die Ackermann-Funktion an. Beim LCA-
Problem geht es um die Bestimmung des letzten gemeinsamen Vorfahren von
zwei Knoten in einem Wurzelbaum. Die Lösung dieses Problems in linearer
Laufzeit ist eine Voraussetzung für einen Algorithmus zur Verifikation eines
minimalen aufspannenden Baums in linearer Laufzeit.
Die Algorithmen von Borůvka, Kruskal und Prim konstruieren minimale
aufspannende Bäume und der Algorithmus von Dijkstra löst das Abstandspro-
blem. Die Algorithmen von Prim und Dijkstra verallgemeinern die Breitensu-
che aus dem Kapitel 5. Bei der Implementierung ersetzt die Priority-Queue
die Queue, die wir bei der Breitensuche einsetzen. Der Union-Find-Datentyp
kommt in Kruskals Algorithmus zur Anwendung.
Der probabilistische Algorithmus von Karger, Klein und Tarjan berechnet
in linearer Laufzeit einen minimalen aufspannenden Baum. Wesentlich dafür
ist ein Algorithmus, der in linearer Laufzeit die Verifikation eines minimalen
aufspannenden Baums durchführt.
Der Algorithmus von Warshall ermittelt den transitiven Abschluss eines
Graphen und der Algorithmus von Floyd die Abstandsmatrix. Die Algorith-
men von Ford-Fulkerson und Edmonds-Karp lösen das Flussproblem in Netz-
werken. Zunächst präzisieren wir den Begriff des gewichteten Graphen.
Definition 6.1. Ein Graph G = (V, E) mit einer Abbildung g : E −→ R>0
heißt gewichteter Graph. Die Abbildung g heißt Gewichtsfunktion. Für e ∈ E
heißt g(e) das Gewicht∑
von e. Das Gewicht von G ist die Summe der Gewichte
aller Kanten, g(G) = e∈E g(e).
Zur Darstellung von gewichteten Graphen verwenden wir – wie zur Dar-
stellung von Graphen – die Datenstrukturen Adjazenzliste und Adjazenzma-
trix. Wir müssen die Definitionen nur geringfügig erweitern.
© 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_6
254 6. Gewichtete Graphen

1. Die Adjazenzmatrix adm ist eine n × n–Matrix mit Koeffizienten aus


R≥0 , {
g({i, j}), falls {i, j} ∈ E,
adm[i, j] :=
0 sonst.
2. In den Listenelementen der Adjazenzliste speichern wir das Gewicht einer
Kante. Wir erweitern deshalb das Listenelement für Graphen auf Seite
227 um die Komponente weight.

6.1 Grundlegende Algorithmen


Wir studieren in diesem Abschnitt grundlegende Algorithmen, die wir im
restlichen Kapitel anwenden. Es handelt sich um Priority-Queues und um
den Union-Find-Datentyp. Diese beiden Datentypen zählen zu den hervorge-
hobenen Datenstrukturen und sind in viele Situationen anwendbar.
Im Abschnitt 6.5 geht die Berechnung des letzten gemeinsamen Vorfah-
ren (lowest common ancestor, kurz LCA) von zwei Knoten in einem Baum
wesentlich ein. Wir behandeln das LCA-Problem, das linear äquivalent zum
RMQ-Problem ist (Abschnitt 1.5.4), als eigenständiges Problem. Es geht um
ein grundlegendes algorithmisches Problem, das intensiv studiert wurde.

6.1.1 Die Priority-Queue

Die Priority-Queue verallgemeinert den abstrakten Datentyp Queue. Bei ei-


ner Queue verlassen die Elemente in der gleichen Reihenfolge die Queue,
in der sie gespeichert wurden (first in, first out – FIFO-Prinzip). Bei einer
Priority-Queue weisen wir jedem Element beim Einspeichern eine Priorität
zu. Unsere spätere Anwendung erfordert es, die Priorität eines gespeicherten
Elementes zu erniedrigen. Das Element mit der geringsten Priorität verlässt
als nächstes Element die Queue. Eine Priority-Queue ist durch folgende Funk-
tionen definiert:
1. PQInit(int size) initialisiert eine Priority-Queue für size viele Elemente.
2. PQUpdate(element k, priority n) fügt k in die Priority-Queue mit der
Priorität n ein. Befindet sich k bereits in der Priority-Queue und besitzt
k höhere Priorität als n, dann erniedrigt PQUpdate die Priorität von k
auf n. PQUpdate gibt true zurück, falls eine Einfügeoperation oder ein
Update stattgefunden hat, ansonsten false.
3. PQRemove liefert das Element mit der geringsten Priorität und entfernt
es aus der Priority-Queue.
4. PQEmpty überprüft die Priority-Queue auf Elemente.
Bei der Implementierung der Priority-Queue verwenden wir folgende Da-
tentypen: type element = 1..n, type index = 0..n und
6.1 Grundlegende Algorithmen 255

type queueEntry = struct


element elem
int prio
Wir implementieren eine Priority-Queue mithilfe der beiden Arrays
prioQu[1..n] und pos[1..n] und organisieren das Array prioQu – geordnet
nach den Prioritäten – als Min-Heap (Abschnitt 2.2.1), d. h. mit a[i] :=
prioQu[i].prio gilt
⌊n⌋ ⌊ ⌋
n−1
a[i] ≤ a[2i] für 1 ≤ i ≤ und a[i] ≤ a[2i + 1] für 1 ≤ i ≤ .
2 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

Die Organisation von prioQu[1..n] als Min-Heap unterlegt prioQu[1..n]


die Struktur eines binären Baums minimaler Höhe mit n Knoten (Abschnitt
2.2.1). Die Anzahl der Iterationen der while-Schleife in UpHeap und in
DownHeap ist durch ⌊log2 (n)⌋, beschränkt (Lemma 2.16).
UpHeap ist analog zu DownHeap implementiert (Algorithmus 2.12).
UpHeap und DownHeap operieren auf den Arrays prioQu und pos. Wer-
den Elemente in prioQu umgestellt, so müssen die neuen Positionen in pos
eingetragen werden.
Neue Elemente fügen wir zunächst hinter dem letzten Element der Queue
ein. UpHeap bringt das Element dann an die richtige Stelle im Heap. Entfer-
nen wir das Element minimaler Priorität, dann setzen wir anschließend das
letzte Element an die erste Stelle. Mit DownHeap stellen wir die Heapbedin-
gung wieder her. Wir geben jetzt die Algorithmen PQUpdate und PQRemove
genauer an.
256 6. Gewichtete Graphen

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)).

6.1.2 Der Union-Find-Datentyp

Der Union-Find-Datentyp unterstützt das dynamische Partitionieren einer


Menge V = {v1 , . . . , vn }, in Teilmengen Vi ⊂ V , i = 1, . . . , l, d. h.


l
V = Vi , Vi ∩ Vj = ∅ für i ̸= j.
i=1

Für jede Teilmenge Vi sei ein Repräsentant ri ∈ Vi gewählt, i = 1, . . . , l.


Weiter seien Funktionen definiert:
1. Find(element x) gibt für x ∈ Vi den Repräsentanten ri von Vi zurück.
2. Union(element x, y) gibt false zurück, falls x und y in derselben Teilmenge
Vi liegen, sonst true. Befinden sich x und y in verschiedenen Teilmengen
Vi und Vj , so ersetzt Union Vi und Vj durch Vi ∪ Vj und wählt einen
Repräsentanten für Vi ∪ Vj .
3. FindInit(element v1 , . . . , vn ) initialisiert Teilmengen Vi = {vi },
i = 1, . . . , n. Der Repräsentant von Vi ist vi .
6.1 Grundlegende Algorithmen 257

Der Union-Find-Datentyp und die Analyse der Implementierungen sind


intensiv studiert worden. Ein wesentlicher Anteil ist Tarjan zuzuschreiben.
Wir folgen mit unserer Darstellung [Tarjan99] und [HopUll73].
Wir nehmen bei der Implementierung ohne Einschränkung an, dass V =
{1, . . . , n} gilt. Wir implementieren V1 , . . . , Vl mithilfe von Wurzelbäumen.
Jedem Vi entspricht ein Wurzelbaum Ti . Die Elemente von Vi sind in den
Knoten von Ti gespeichert. Der Repräsentant von Vi ist das Element, das in
der Wurzel gespeichert ist. Sei T := ∪li=1 Ti der Wald, den die Ti bilden. Wir
stellen T durch das Array parent[1..n] dar. Dann gilt:
1. Die Elemente i und j gehören zur selben Menge Vi , falls i und j zum sel-
ben Baum gehören, d. h. dieselbe Wurzel besitzen. Dabei ist jede Wurzel
w durch parent[w] = 0 codiert.
2. Zwei Komponenten mit Wurzeln i und j werden durch parent[j] ← i
(oder parent[i] ← j) zu einer Komponente zusammengefasst.
3. FindInit(n) belegt Speicher für das Array parent und initialisiert jedes
Feld von parent auf 0.
Wir geben jetzt Pseudocode für Find und Union an.
Algorithmus 6.4.
int Find(int i)
1 while parent[i] > 0 do
2 i ← parent[i]
3 return i

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

Bemerkung. Nachdem ein Knoten i Nachfolger eines Knotens j wurde, bleibt


rank[i] unverändert. Dies gilt auch, wenn wir Pfadkomprimierung anwenden.
Dann speichert aber rank[i] nicht mehr die Höhe von i.
Pfadkomprimierung. Die Idee bei Pfadkomprimierung ist, den Vaterkno-
ten eines Knotens durch die Wurzel des Baums zu ersetzen. Wenn wir diese
Idee konsequent anwenden, müssen wir bei der Vereinigung einen der beiden
Bäume komplett reorganisieren. Dies ist zu aufwendig. Deshalb ändern wir
nur in Find die Verzeigerung längs des Pfades von i bis zur Wurzel. Wir setzen
Pfadkomprimierung zusammen mit Balancierung ein. Durch die Pfadkompri-
mierung kann sich die Höhe eines Baums ändern. In rank[i] steht dann nicht
mehr die Höhe des Knotens i. Die Zahl rank[i] heißt der Rang des Knotens
i. Der Rang des Knotens i approximiert den Logarithmus aus der Anzahl
der Knoten im Teilbaum mit Wurzel i (Lemma 6.7, Punkt 2). Wir benut-
zen den Rang als Kriterium für die Balancierung und erweitern Find um
Pfadkomprimierung. Wir zeigen dies zunächst an einem
Beispiel. Bei Pfadkomprimierung ersetzen wir den Vaterknoten eines Kno-
tens durch die Wurzel des Baums, wie Figur 6.1 zeigt.

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

Fig. 6.1: Pfadkomprimierung.


6.1 Grundlegende Algorithmen 259

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.

Wir betrachten jetzt den Union-Find-Algorithmus nach n−1 vielen Union


und m vielen Find Aufrufen. Ein Schritt besteht aus dem Aufruf einer der
beiden obigen Funktionen. Insgesamt gibt es n−1+m viele Schritte. Für einen
Knoten u bezeichnen wir mit Tu den Teilbaum mit Wurzel u, mit rank(u) den
Rang des Knotens u und mit Ur := {u | rank(u) = r}, nachdem l Schritte
des Algorithmus ausgeführt wurden, d. h. nachdem der l–te Aufruf beendet
ist. rank(u) ist monoton wachsend in der Anzahl der Schritte.
Lemma 6.7. Es gilt für 0 ≤ l ≤ n − 1 + m:
1. Die Funktion rank(u) ist längs des Pfades von einem Knoten zur Wurzel
des Teilbaums streng monoton wachsend.
2. Es gilt rank(u) ≤ ⌊log2 (|Tu |)⌋ ≤ ⌊log2 (n)⌋. Insbesondere ist die Höhe des
Baums höchstens log2 (n).
3. |Ur | ≤ n/2r .

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

= log2 (2 · 2rank(u) ) = log2 (2rank(u) + 2rank(v) )


≤ log2 (|Tu | + |Tv |) = log2 (|T̃u |),
wobei v die Wurzel des zweiten Baums und T̃u die Vereinigung von Tu
und Tv bezeichnet. Aus |Tu | ≤ n folgt die zweite Abschätzung in Punkt
2. Mit Punkt 1 folgt die Aussage über die Höhe.
3. Für u, v mit rank(u) = rank(v) gilt wegen Punkt 1 Tu ∩ Tv = ∅, also folgt
∪ ∑ ∑
n≥| Tu | = |Tu | ≥ 2r = |Ur |2r .
u∈Ur u∈Ur u∈Ur

Das Lemma ist daher bewiesen. 2

Laufzeitanalyse. Die Laufzeitanalyse des Union-Find-Algorithmus (Satz


6.10) verwendet die Ackermann-Funktion (Algorithmus 1.5). Wir leiten des-
halb Eigenschaften der Ackermann-Funktion her. Wir zeigen zunächst, dass
die Ackermann-Funktion


 n + 1, falls m = 0,
A(m, n) = A(m − 1, 1), falls n = 0,


A(m − 1, A(m, n − 1)) sonst,
monoton wächst und geben dazu eine äquivalente Definition an. Wir definie-
ren eine Familie von Funktionen Am (n) durch
A0 (n) = n + 1,
m−1 (1) = Am−1 ◦ . . . ◦ Am−1 (1).
Am (n) = An+1
| {z }
n+1

Lemma 6.8. Es gilt


1. Am (n) = A(m, n) für alle m, n ≥ 0.
2. Am (n) ≥ n + 1 für alle m, n ≥ 0.
3. Am (n + 1) > Am (n) für alle m, n ≥ 0.
4. Anm (1) ≥ n + 1 für alle n ≥ 0.
Beweis.
1. Setze A(m, n) = Ãm (n). Dann gilt
Ãm (n) = Ãm−1 (Ãm (n − 1)) = Ãm−1 (Ãm−1 (Ãm (n − 2))) = . . . =
Ãnm−1 (Ãm (0)) = Ãnm−1 (Ãm−1 (1)) = Ãn+1
m−1 (1).

Da Am und Ãm die gleiche Rekursionsgleichung erfüllen und da A0 (n) =


Ã0 (n) gilt, stimmen Am (n) und Ãm (n) = A(m, n) für alle m, n ≥ 0
überein.
2. Wir zeigen die Aussage durch Induktion nach m. Für m = 0 gilt
A0 (n) = n + 1. Der Induktionsanfang ist richtig.
Die Induktionshypothese ist Am−1 (n) ≥ n + 1 für m ≥ 1 und alle n ≥ 0.
Es ist zu zeigen, dass Am (n) ≥ n + 1 für alle n ≥ 0 gilt. Wir zeigen die
6.1 Grundlegende Algorithmen 261

Aussage durch Induktion nach n.


Für n = 0 gilt Am (0) = Am−1 (1) ≥ 2 (nach der Induktionshypothese für
m). Der Induktionsanfang ist demnach richtig.
Am (n) = Am−1 (Am (n − 1)) ≥ Am (n − 1) + 1 ≥ n + 1. Die erste
Abschätzung folgt nach der Induktionshypothese für m und die zweite
nach der Induktionshypothese für n.
3. Für m = 0 gilt A0 (n + 1) = n + 2 > A0 (n) = n + 1. Für m ≥ 1 gilt
Am (n + 1) = Am−1 (Am (n)) ≥ Am (n) + 1 > Am (n) (mit Punkt 2).
m (1)) ≥ Am (1) + 1 ≥ . . . ≥ Am (1) + n = n + 1.
4. Anm (1) = Am (An−1 n−1 0

Das Lemma ist gezeigt. 2


Lemma 6.9. Die Funktion A(m, n) wächst streng monoton in beiden Argu-
menten und stärker im ersten als im zweiten. Genauer gilt

Am+1 (n) ≥ Am (n + 1) ≥ Am (n) + 1

für alle m, n ≥ 0.
Beweis. Sei m ≥ 0.

Am+1 (n) = Am (Anm (1)) ≥ Am (n + 1) = Am−1 (Am (n)) ≥ Am (n) + 1.

Dies zeigt die Aussagen des Lemmas. 2

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

(1) δ(u) = max{k | rank(parent(u)) ≥ Ak (rank(u))}.

Wegen rank(parent(u)) ≥ rank(u) + 1 = A0 (rank(u)) (Lemma 6.7) ist δ(u)


wohldefiniert und es folgt δ(u) ≥ 0.

(2) r(u) = max{r | rank(parent(u)) ≥ Arδ(u) (rank(u))}.

Wegen Lemma 6.7 gilt

(3) ⌊log2 (n)⌋ ≥ rank(parent(u)) ≥ Aδ(u) (rank(u)).

Für δ(u) ≥ 2 folgt mit Lemma 6.9

(4) ⌊log2 (n)⌋ ≥ Aδ(u) (rank(u)) ≥ Aδ(u)−2 (rank(u) + 2) ≥ Aδ(u)−2 (2).

Sei α(n) definiert durch

α(n) = min{k | Ak (2) ≥ n}.

Aus Aδ(u)−2 (2) ≤ ⌊log2 (n)⌋ < n (siehe (4)) und der Definition von α folgt
unmittelbar
262 6. Gewichtete Graphen

(5) δ(u) ≤ α(⌊log2 (n)⌋) + 2.

Die Funktion α(n) ist im Wesentlichen die Umkehrfunktion von Am (2).


Sie ist eine extrem langsam wachsende Funktion. Es gilt

A0 (2) = 3, A1 (2) = 4, A2 (2) = 7, A3 (2) = 25 − 3 = 29, A4 (2) = 265536 − 3.

Daher gilt

α(0) = . . . = α(3) = 0, α(4) = 1, α(5) = α(6) = α(7) = 2,

α(8) = . . . = α(29) = 3, α(30) = . . . = α(265536 − 3) = 4.


Die Zahl 265536 ist eine 65.537 stellige Binärzahl. Dies sind ungefähr 20.000
Dezimalstellen. Die Funktion α nimmt für alle praktisch vorkommenden In-
puts einen Wert ≤ 4 an.

Satz 6.10. Der Union-Find-Algorithmus, mit Balancierung nach dem Rang


und Pfadkomprimierung, besitzt für n − 1 Union und m Find Aufrufe eine
Laufzeit im schlechtesten Fall von der Ordnung O((m + n) · α(n)).
Beweis. Die Laufzeit für einen Union Aufruf ist, wenn man von den beiden
Find Aufrufen absieht, konstant. Es erfolgen n − 1 viele Union Aufrufe. Die
Laufzeit tU für alle Ausführungen der Funktion Union (ohne Find Aufrufe)
ist demnach in der Ordnung O(n).
Wir schätzen jetzt die Laufzeit für alle Find Ausführungen ab. Wir be-
trachten zunächst den Aufruf Find(u) für ein u. Sei P : u = u1 , . . . , ur = v
der Pfad von u zur Wurzel v vor der l–ten Ausführung von Find. Die Rechen-
zeit von Find verteilt sich zu gleichen Anteilen auf die einzelnen Knoten des
Pfades. Für jeden einzelnen Knoten ist der Aufwand konstant. Wir nehmen
dafür eine Zeiteinheit an. Wir richten jetzt Zeitzähler ein: für jeden Knoten
u den Zähler tu und für die Funktion Find den Zähler tF . Mit diesen Zählern
erfassen wir den Aufwand für alle Ausführungen von Find. Wir betrachten
den l–ten Schritt und die Knoten ui auf P , i = 1, . . . , r.
1. tui erhöhen wir um eins, wenn ui ̸= u, ui ̸= v (d. h. ui ist weder Blatt
(rank(ui ) ≥ 1) noch Wurzel) und wenn ein j > i existiert mit δ(uj ) =
δ(ui ).
2. tF erhöhen wir um eins, falls (a) ui Blatt oder Wurzel ist, oder wenn (b)
für alle j > i gilt δ(uj ) ̸= δ(ui ).
Die Laufzeit für alle Find Ausführungen beträgt

tF + tu .
u

Wir betrachten den ersten Fall. Sei i < j mit δ(ui ) = δ(uj ) = k. Dann
gilt für ui , uj
6.1 Grundlegende Algorithmen 263

rank(v) ≥ rank(parent(uj )) ≥ Ak (rank(uj ))


r(ui )
≥ Ak (rank(parent(ui ))) ≥ Ak (Ak (rank(ui )))
r(u )+1
= Ak i (rank(ui )).

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

tui ≤ rank(ui )(α(⌊log2 (n)⌋) + 2).

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

tF ≤ m(α(⌊log2 (n)⌋) + 4).

Wir erhalten

t U + tF + tu ≤ c(m + n)α(⌊log2 (n)⌋) = O((m + n) · α(n)).
u

Dies zeigt den Satz. 2

6.1.3 Das LCA- und das RMQ-Problem

Wir betrachten zunächst das Problem, den letzten gemeinsamen Vorfahren


(lowest common ancestor (LCA)) von zwei Knoten in einem Wurzelbaum zu
berechnen. Seien u und v Knoten in einem Wurzelbaum. Der letzte gemein-
same Vorfahre von u und v ist der gemeinsame Vorfahre von u und v, der
den größten Abstand von der Wurzel besitzt.
Figur 6.2 zeigt den letzten gemeinsamen Vorfahren (gefüllt) der beiden
gefüllten Blattknoten.

Fig. 6.2: Letzter gemeinsamer Vorfahre.

Tarjans Algorithmus zur Berechnung des letzten gemeinsamen Vor-


fahren. Der folgende Algorithmus LCA, publiziert in [Tarjan79], berechnet
für jedes Knotenpaar {u, v} aus einer Liste Q den letzten gemeinsamen Vor-
fahren in einen Wurzelbaum T . Wir stellen T mit der Knotenmenge {1, . . . , n}
und der Adjazenzliste adl[1..n] dar. Bei der Berechnung des letzten gemein-
samen Vorfahren wenden wir den Union-Find-Datentyp an.
Der erste Aufruf ist LCA(root). Der Aufrufer stellt einen mit FindInit(n)
initialisierten Union-Find-Datentyp bereit. LCA verwendet das boolsche Ar-
ray marked[1..n], das mit false“ vorbelegt ist.

6.1 Grundlegende Algorithmen 265

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

Fig. 6.3: LCA-Berechnung.

Sei L = {v ∈ T | der Aufruf LCA(v) ist zum Zeitpunkt t terminiert}.


Dies sind die Knoten links vom Pfad v0 , . . . , vl−1 und unterhalb von vl =
k. Sei
Vi = {v ∈ L | v ist Nachfahre von vi }, i = 0, . . . , l.
Für die Knoten v ∈ Vi ist vi der letzte gemeinsame Vorfahre von v und
k.
2. Zum Zeitpunkt t bilden die Knoten aus {vi } ∪ Vi eine Union-Find Parti-
tion. Find(v) gibt die Wurzel der Partition zurück, in der v liegt. Da vi
Vorfahre der Knoten dieser Partition ist, wurde früher ancestor[Find(v)]
= vi gesetzt (Zeile 5).
266 6. Gewichtete Graphen

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

a[k] = min{a[l] | i ≤ l ≤ j}.

Ein Reduktions-Algorithmus für das LCA-Problem. Der folgende Al-


gorithmus reduziert das LCA-Problem für einen Wurzelbaum T mit der Kno-
tenmenge {1, . . . , n} durch eine Tiefensuche in T auf das RMQ-Problem (Ab-
schnitt 1.5.4).

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

Fig. 6.4: Reduktion des LCA- auf das RMQ-Problem.

Satz 6.14. Seien k und l Knoten von T mit ino[k] < ino[l]. Dann gilt

lca(k, l) = no[rmqde (ino[k], ino[l])].

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.

Ein linearer Algorithmus für das inkrementelle RMQ-Problem. ⌈ ⌉ Sei


a[0..n − 1] ein inkrementelles Array von Zahlen und k := log 2 (n)/2 . Wir
zerlegen a in Teilarrays der Länge k. Wir erhalten m = ⌈n/k⌉ viele Teilarrays,
wobei das letzte Teilarray eine Länge ≤ k haben kann. Um die nachfolgenden
Ausführungen zu vereinfachen, nehmen wir an, dass die Zerlegung ohne Rest
aufgeht, d. h. alle Teilarrays haben die Länge k. Die dabei entstehenden
Teilarrays bezeichnen wir von links ⌊beginnend
⌋ mit a0 , . . . , am−1 .
Seien i < ⌊j Indizes für a, γ = i/k , der Index des Teilarrays in dem i

liegt und δ = j/k , der Index des Teilarrays in dem j liegt. Dann ist i mod k
die Position von i in aγ und j mod k die Position von j in aδ , wie Figur 6.5
zeigt.
γ i δ j
.
i mod k j mod k

Fig. 6.5: Zerlegung in Teilarrays.

Das Minimum mi,j in a[i..j] ergibt sich dann als

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

Bei den Teilarrays a0 , . . . , am−1 handelt es sich um inkrementelle Arrays.


Deshalb geben wir einen Algorithmus für das RMQ-Problem für ein inkre-
mentelles Array b[0..k − 1] an. Ohne Einschränkung nehmen wir an, dass
b[0] = 0 gilt (sonst ersetze b[i] durch b[i] − b[0], i = 0, . . . , k − 1). Da b[0] und
die Folge der Differenzen −1, +1 zweier aufeinander folgender Einträge das
Array b[0..k − 1] festlegen, gibt es 2k−1 viele Belegungen für ein inkremen-
telles Array b der Länge k mit b[0] = 0. Wir nummerieren die Belegungen
durch. Für die l–te Belegung speichern wir in einer Tabelle Tl für alle Ind-
expaare i < j die Position des Minimums von b[i..j]. Es gibt (k − 1)k/2 viele
Indexpaare i < j. Aus diesem Grund ist Tl ein Array der Länge (k − 1)k/2.
Wir erhalten die Folge Ta = (T1 , . . . , T2k−1 ). Mithilfe von Ta können wir jetzt
Anfragen nachschlagen. Die Belegung von b bestimmt l und somit Tl und die
Indizes i, j bestimmen den Eintrag in Tl . Die Tabelle Ta hängt nur von k ab
und wir können sie deshalb für alle Teilarrays a0 , . . . , am−1 von a verwenden.
Insbesondere können wir jetzt die Position von mα und mω nachschlagen.
Zur Berechnung von mµ dient ein Array c[0..m−1] und ein Array p[0..m−
1]. Wir schlagen die Position des Minimums von al in Ta nach und speichern
das Minimum in c[l] und seine Position in p[l] ab. Es gilt
mµ = min c[γ + 1..δ − 1].
Mithilfe der Tabelle Tc können wir rmqc (γ + 1, δ − 1) durch zweimaliges
Nachschlagen in Tc ermitteln (Satz 1.37).
Figur 6.6 visualisiert die Tabellen zur Berechnung von
rmqa (i, j) = min{rmqa (i mod k, k − 1), rmqa (0, j mod k), rmqc (γ + 1, δ − 1)}.

Tc

c
.

Ta

a
i j

Fig. 6.6: Das inkrementelle RMQ-Problem.


270 6. Gewichtete Graphen

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

und deshalb folgt


( ( ))
n n
m log2 (m) = O log2 = O(n).
log2 (n) log2 (n)

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.

Reduktion des RMQ-Problems auf das LCA-Problem. Wir reduzie-


ren das RMQ-Problem für ein beliebiges ganzzahliges Array a[1..n] auf das
LCA-Problem. Dazu ordnen wir dem Array a einen kartesischen Baum B zu.
RMQ-Anfragen für a führen wir dann auf LCA-Anfragen für B zurück. Die
Konstruktion von B aus a erfolgt in der Ordnung O(n). Wir definieren den
Begriff kartesischer Baum und beschreiben die einzelnen Schritte der Reduk-
tion.
Definition 6.17. Sei a[1..n] ein Array von ganzen Zahlen. Ein binärer Baum
B, der die Elemente a[1], . . . , a[n] speichert, heißt der a zugeordnete kartesi-
sche Baum, wenn gilt:
1. B erfüllt die Heapbedingung (Definition 2.11).
2. Die Inorder-Ausgabe von B ergibt a[1], . . . , a[n] (Definition 4.4).
Bemerkung. Der a zugeordnete kartesische Baum B ist eindeutig bestimmt,
denn die Heapbedingung bestimmt die Wurzel und die Inorder-Ausgabe legt
den linken und rechten Teilbaum der Wurzel eindeutig fest. Per Rekursion
folgt die Eindeutigkeit von B.
6.1 Grundlegende Algorithmen 271

Wir verwenden verkettete Listen von Knotenelementen vom Typ node,


um B zu implementieren. (Definition Seite 137). Der folgende Algorithmus
erzeugt den einem Array zugeordneten kartesischen Baum.
Algorithmus 6.18.
tree BuildCartesianTree(int a[1..n])
1 node rnode, nd, b
2 b ← new(node)
3 b.element ← a[1], b.lef t ← null
4 b.right ← null, rnode ← b
5 for i ← 2 to n do
6 nd ← new(node)
7 nd.element ← a[i]
8 nd.right ← null
9 rnode ← UpHeap(rnode, a[i])
10 if rnode ̸= null
11 then nd.lef t ← rnode.right
12 rnode.right ← nd
13 else nd.lef t ← b, b ← nd
14 rnode ← nd
15 return b

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)

Fig. 6.7: Der zugeordnete kartesische Baum.

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

6.2 Die Algorithmen von Dijkstra und Prim


Der Algorithmus von Dijkstra, publiziert in [Dijkstra59], löst das Abstands-
problem für einen Knoten in einem gewichteten Graphen, d. h. der Algo-
rithmus berechnet die Abstände von einem festen Knoten zu allen anderen
Knoten. Der Algorithmus von Prim1 konstruiert einen minimalen aufspannen-
den Baum (siehe [Prim57]). Bevor wir die Algorithmen erklären, präzisieren
wir die erste Problemstellung.
Definition 6.21. Sei G = (V, E) ein zusammenhängender gewichteter Graph
und seien v, w ∈ V . Sei v = v1 , . . . , vn+1 = w ein Pfad P von v nach w. Die
Länge von P ist
∑ n
l(P ) := g({vi , vi+1 }).
i=1
Der Abstand d(v, w) von v und w ist definiert durch
d(v, w) := min{l(P ) | P Pfad von v nach w}.
Bemerkung. Ein kürzester Pfad von v nach w ist ein einfacher Pfad. Da es
nur endlich viele einfache Pfade von v nach w gibt, ist der Abstand d(v, w)
definiert. Man rechnet leicht nach, dass für einen zusammenhängenden Gra-
phen die Abbildung d die Axiome einer Metrik erfüllt (Definition B.25). Wir
sind an einem Algorithmus interessiert, der zu gegebenem Knoten v nicht
nur die Abstände d(v, w) zu allen übrigen Knoten w berechnet, sondern mit
d(v, w) auch einen kürzesten Pfad von v nach w speichert.
Der Algorithmus von Dijkstra. Sei G = (V, E) ein zusammenhängen-
der gewichteter Graph und v ∈ V . Dijkstras Algorithmus findet einen Pfad
minimaler Länge von v nach w für alle w ∈ V . Er konstruiert einen G auf-
spannenden Baum T = (VT , ET ) mit Wurzel v. Der Pfad von v nach w
in T ist für alle w ∈ V ein Pfad minimaler Länge von v nach w in G, d.h.
dT (v, w) = d(v, w), wobei dT den Abstand in T bezeichnet. Ein solcher Baum
heißt kürzester Wege-Baum (shortest-path tree, kurz SPT).
1
Der Algorithmus wird in der Literatur als Algorithmus von Prim bezeichnet,
obwohl er bereits 1930 in einer Arbeit von Vojtěch Jarnı́k (1897 – 1970), einem
tschechischen Mathematiker, der auf dem Gebiet der Zahlentheorie und Analysis
arbeitete, publiziert wurde.
274 6. Gewichtete Graphen

Algorithmus. Sei G = (V, E) ein zusammenhängender gewichteter Graph,


v ∈ V . Der Algorithmus von Dijkstra berechnet die Abstände d(v, w) für alle
w ∈V:
1. Start: Setze T := ({v}, ∅).
2. Konstruktionsschritt: Es sei T = (VT , ET ) konstruiert.
Wähle w ∈ V \ VT mit d(v, w) minimal für alle w ∈ V \ VT und einen
Pfad P minimaler Länge v = v1 , . . . , vk−1 , vk = w von v nach w in G.
Setze T := (VT ∪ {w}, ET ∪ {vk−1 , w}).
3. Wiederhole Schritt 2 solange, bis VT = V gilt.
Wir führen jetzt den Algorithmus am Beispielgraphen der Figur 6.8 durch.

B
2
D A.
1 2 1 2
1 A. 1 B D
1 1
3 4
C E C E
6

Fig. 6.8: SPT-Berechnung mit Dijkstras Algorithmus.

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

Fig. 6.9: Der MST ist nicht eindeutig.

Algorithmus. Sei G = (V, E) ein zusammenhängender gewichteter Graph.


Der Algorithmus von Prim berechnet einen minimalen aufspannenden Baum
T = (VT , ET ) von G in den folgenden Schritten:
1. Start: Wähle v ∈ V beliebig und setze T := ({v}, ∅).
2. Konstruktionsschritt: Es sei T = (VT , ET ) konstruiert.
Wähle eine Kante e = {v, w} mit g(e) minimal für v ∈ VT und w ∈
/ VT .
Setze T := (VT ∪ {w}, ET ∪ {e}).
3. Wiederhole Schritt 2 solange, bis VT = V gilt.
276 6. Gewichtete Graphen

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

Fig. 6.10: MST mit Prims Algorithmus.

Lemma 6.24 (Schnitteigenschaft). Sei G = (V, E) ein zusammenhängender


gewichteter Graph und U ⊂ V , U ̸= V .
EU = {{v, w} ∈ E | v ∈ U, w ∈
/ U }.
Zu jeder Kante e ∈ EU minimalen Gewichts gibt es einen minimalen auf-
spannenden Baum T = (V, ET ) von G mit e ∈ ET .
Beweis. Sei e ∈ EU eine Kante minimalen Gewichts, T ein minimaler auf-
spannender Baum von G und e = {u, v} ∈ / ET . Der Graph T ∪ {e} besitzt
einen Zyklus Z. Neben e gibt es eine weitere Kante e′ = {r, s} ∈ Z mit r ∈ U
und s ∈/ U . T ′ = (T ∪ {e}) \ {e′ } ist ein Baum und spannt G auf. Nach der
Wahl von e ist g(T ′ ) = g(T ) + g(e) − g(e′ ) ≤ g(T ). Da T ein minimaler auf-
spannender Baum für G ist, folgt g(T ′ ) = g(T ). Somit ist T ′ ein minimaler
aufspannender Baum von G mit Kante e. 2
Bemerkung. Der Graph G zerfällt in zwei Komponenten, wenn wir alle Kan-
ten aus EU entfernt. Deshalb sagen wir, EU definiert einen Schnitt in G. Die
Aussage des Lemmas 6.24 lautet mit dieser Notation: Für jede Kante e mi-
nimalen Gewichts in einem Schnitt gibt es einen minimalen aufspannenden
Baum, der e enthält.
Satz 6.25. Sei G = (V, E) ein zusammenhängender gewichteter Graph.
Prims Algorithmus berechnet einen minimalen aufspannenden Baum von G.
Beweis. Sei T der mit Prims Algorithmus konstruierte aufspannende Baum.
T = ({v1 , . . . , vn }, {e1 . . . , en−1 }), Ui = {v1 , . . . , vi }, i = 1, . . . , n. Die Ui bil-
den eine aufsteigende Kette U1 ⊂ U2 ⊂ . . . ⊂ Un = V . Es ist Ui ̸= Uj für
i ̸= j, denn ei hat einen Endpunkt in Ui und einen in Ui+1 \ Ui .
Sei Tmin ein minimaler aufspannender Baum für G. Wie im Beweis des Lem-
mas 6.24 konstruiere für i = 1, . . . , n − 1 zu jedem Ui und ei ein fi ∈ E.
Die Kante ei ist für den Schnitt EUi eine Kante minimalen Gewichts. Setze
T0 = Tmin und Ti = (Ti−1 ∪ {ei }) \ {fi }, i = 1, . . . , n − 1. Da ein Endpunkt
von fi in Ui+1 \ Ui liegt, aber beide Endpunkte von ej für j = 1, . . . , i − 1 in
Ui liegen, ist fi ∈ / {e1 , . . . , ei−1 }. Es gilt g(Ti ) = g(Tmin ), i = 1, . . . , n − 1. Da
T = Tn−1 gilt, ist auch T ein minimaler aufspannender Baum für G. 2
6.2 Die Algorithmen von Dijkstra und Prim 277

Die Implementierung der Algorithmen von Dijkstra und Prim. Die


folgende Implementierung der beiden Algorithmen setzt nicht voraus, dass
der Graph G zusammenhängend ist. Für jede Zusammenhangskomponente
von G löst sie das Abstandsproblem oder berechnet einen minimalen aufspan-
nenden Baum. Beide Algorithmen starten mit T = ({v}, ∅). In jedem Schritt
erweitern wir T . Wir wählen einen Knoten und verbinden ihn mit einer Kante
mit einem Knoten aus T , falls dies möglich ist.
Wir teilen die Knoten V = {1, . . . , n} von G in drei disjunkte Gruppen
ein:
VT : Knoten von T.
Vad : = {w ̸∈ VT | es gibt ein u ∈ VT : {u, w} ∈ E}.
VR : V \ (VT ∪ Vad ).

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

3. Zu welcher der Mengen VT , Vad oder VR ein Knoten k gehört vermerken


wir im Array priority:
priority[k] ≥ 0 für k ∈ VT .
−infinite < priority[k] = −prio(k) < 0, falls k ∈ Vad .
priority[k] = −infinite, falls k ∈ VR .
Die Konstante infinite ist größer als jeder auftretende Wert für prio(k).
4. Die Implementierung des Waldes T erfolgt durch das Array parent[1..n].
Wir geben zunächst eine Implementierung an, falls der Graph durch eine
Adjazenzmatrix gegeben ist.
Matrix priority-first search. MatrixPriorityFirst implementiert beide Al-
gorithmen.
Algorithmus 6.26.
int adm[1..n, 1..n], priority[1..n]; vertex parent[1..n]
const infinite = maxint −1

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.

wobei w die Wurzel der Komponente von k bezeichnet.


7. Die Laufzeit von MatrixPriorityFirst ist von der Ordnung O(n2 ).
Liste priority-first search. ListPriorityFirst implementiert beide Algorith-
men, falls wir G durch eine Adjazenzliste darstellen.
Wir verwalten die Knoten aus Vad in einer Priority-Queue.
Algorithmus 6.27.
int priority[1..n]; vertex parent[1..n]; node adl[1..n]
const infinite = maxint − 1
280 6. Gewichtete Graphen

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

wobei w die Wurzel der Komponente von k bezeichnet.


6. PQUpdate und PQRemove haben die Laufzeit O(log2 (n)) (Abschnitt
6.1.1). Deshalb ist die Laufzeit von ListPriorityFirst von der Ordnung
O((n + m) log2 (n)).
7. Werden zur Implementierung einer Priority-Queue an Stelle von binären
Heaps Fibonacci-Heaps verwendet, so kann man die Operationen PQ-
Init und PQUpdate mit der Laufzeit O(1) implementiert, PQRemove
mit der Laufzeit O(log2 (n)) (siehe [CorLeiRivSte07, Kapitel 20]). Die
Laufzeit von Algorithmus 6.27 verbessert sich durch die Verwendung von
Fibonacci-Heaps auf O(m + n log2 (n)).

6.3 Der Algorithmus von Kruskal

Der Algorithmus von Kruskal2 , publiziert in [Kruskal56], berechnet für einen


zusammenhängenden gewichteten Graphen G = (V, E) einen minimalen auf-
spannenden Baum T = (V, ET ). Wir beschreiben den Algorithmus zunächst
informell.
1. Start: Setze T := (V, ∅).
2. Konstruktionsschritt: Es sei T = (V, ET ) konstruiert.
Wähle eine Kante e ∈ E \ ET so, dass T ∪ {e} azyklisch und g(e) minimal
ist. Setze T := (V, ET ∪ {e}).
3. Wiederhole Schritt 2 solange, bis |ET | = |V | − 1 gilt.
Wir führen den Algorithmus am Beispielgraphen der Figur 6.11 durch.

2
B D B D
1 2
1 2
1 A. 6 1 A.

3 4 4
C E C E
6

Fig. 6.11: MST mit Kruskals Algorithmus.

Satz 6.28. Sei G = (V, E) ein zusammenhängender gewichteter Graph. Der


Algorithmus von Kruskal berechnet einen minimalen aufspannenden Baum
für G.
Beweis. Unmittelbar aus der Konstruktion ergibt sich, dass der Algorithmus
einen G aufspannenden Baum erzeugt. Sei T = (V, ET ) der mit Kruskals
Algorithmus erzeugte Baum. ET = {e1 , . . . , en−1 }, g(ei ) ≤ g(ei+1 ) für i =
1, . . . , n − 2. Sei Tmin = (V, Emin ) ein minimaler aufspannender Baum für
2
Joseph B. Kruskal (1928 – 2010) war ein amerikanischer Mathematiker.
282 6. Gewichtete Graphen

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

type edge = struct


vertex v1 , v2
weight w

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.

6.4 Der Algorithmus von Borůvka

Der Algorithmus von Borůvka3 ist ein weiterer deterministischer Algorithmus


zur Berechnung eines minimalen aufspannenden Baums für einen gewichteten
Graphen. Borůvka formulierte das MST-Problem im Jahre 1926 im Zusam-
menhang mit dem Entwurf eines Netzwerks zur Elektrifizierung von Mähren,
einem Teil der heutigen Tschechischen Republik ([Borůvka26]). Sein Algo-
rithmus gilt als erster Algorithmus für die Lösung des MST-Problems.
Sei G = (V, E) ein gewichteter Graph. Falls die Gewichte g(e), e ∈ E,
paarweise verschieden sind, gibt es nur eine Anordnung der Kanten aufstei-
gend nach Gewichten. In diesem Fall ist der minimale aufspannende Baum
eindeutig bestimmt. Bei gleichen Gewichten führen wir auf der Menge der
Kanten die Min-Max-Ordnung ein, die analog zur längen-lexikographischen
Anordnung erklärt ist. Wir definieren

{u, v} < {ũ, ṽ} genau dann, wenn


g({u, v}) < g({ũ, ṽ}) oder
g({u, v}) = g({ũ, ṽ}) und min{u, v} < min{ũ, ṽ} oder
g({u, v}) = g({ũ, ṽ}) und min{u, v} = min{ũ, ṽ} und
max{u, v} < max{ũ, ṽ}

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

zwei Knoten und e = {v, w} ∈ E. Die Kante e zu kontrahieren bedeutet die


Endpunkte v und w zu identifizieren, d. h. zu einem Knoten zusammenzufas-
sen. Bei dieser Identifikation können Schleifen und mehrfache Kanten entste-
hen. Wir entfernen alle Schleifen und bei mehrfachen Kanten entfernen wir
alle bis auf die kleinste Kante. Sei G̃ = (Ṽ , Ẽ) der Graph, der aus G = (V, E)
entsteht, wenn wir in G alle Kanten aus EMI kontrahieren. Wir schreiben
für das Ergebnis G̃ = G/EMI . Bei der Kontraktion aller minimal inzidenten
Kanten identifizieren wir die Knoten, die in einer Zusammenhangskomponen-
te C von (V, EMI ) liegen. Wir wählen für jede Zusammenhangskomponente
C einen Repräsentanten R(C) ∈ V und definieren Ṽ als Menge dieser gewähl-
ten Repräsentanten. Somit ist Ṽ ⊂ V . Den Übergang von G zu G̃ bezeichnen
wir als Kontraktion von G.

Lemma 6.30. Die Kontraktion der minimal inzdenten eines zusammenhän-


geneden Graphen G = (V, E) mit mindestens zwei Knoten reduziert die An-
zahl der Knoten um mindestens die Hälfte.
Beweis. Die Kontraktion identifiziert die Knoten in jeder Zusammenhangs-
komponente von (V, EMI ). Da jede Komponente mindestens zwei Knoten
enthält, verbleiben nach der Kontraktion höchstens n/2 viele Knoten. Die
Anzahl der Knoten wird deshalb mindestens halbiert. 2

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

Fig. 6.12: Kontraktion der minimal inzidenten Kanten.

Wir geben jetzt einen Algorithmus für die Kontraktion an.


Algorithmus 6.31.
(graph,edges) Contract(graph G)
1 EMI ← set of minimal incident edges of G
2 Ṽ ← {R(C1 ), . . . , R(Ck )} ← {connected components of (V, EMI )}
3 Ẽ ← edges of Ṽ
4 return ((Ṽ , Ẽ), EMI )
6.4 Der Algorithmus von Borůvka 285

Bemerkungen: Wir betrachten die Implementierung von Contract genauer,


falls der Graph G durch eine Adjazenzliste definiert ist.
1. Die Bestimmung der minimal inzidenten Kanten EMI erfordert eine In-
spektion der Adjazenzliste. Der Aufwand ist von der Ordnung O(m).
2. Wir ermitteln die Zusammenhangskomponenten von (V, EMI ) durch Brei-
tensuche in (V, EMI ). Der Aufwand ist von der Ordnung O(n). Die Wur-
zeln der dabei entstehenden aufspannenden Bäume dienen als Repräsen-
tanten der jeweiligen Zusammenhangskomponenten. Wir vermerken für
jeden Knoten eine Referenz auf die Wurzel des Baums seiner Zusammen-
hangskomponente.
3. Die Berechnung der Kanten von Ṽ erfordert eine nochmalige Inspektion
der Adjazenzliste von V . Liegen die Endpunkte einer Kante in verschiede-
nen Zusammenhangskomponenten, so nehmen wir eine Kante zwischen
den Wurzeln der beiden aufspannenden Bäume in die Adjazenzliste von
Ṽ auf. Falls bereits eine Kante existiert, erfolgt ein Update des Gewichtes,
falls die neue Kante geringeres Gewicht aufweist.
Die Addition der Laufzeiten unter Punkt 1 - Punkt 3 zeigt, dass die Laufzeit
von Contract in der Ordnung O(n + m) ist.
Lemma 6.32. Sei G = (V, E) ein gewichteter Graph und EMI die Menge
der minimal inzidenten Kanten. Dann gilt:
1. Es gibt keinen Zyklus in G, der nur aus minimal inzidenten Kanten be-
steht.
2. Jede Kante aus EMI ist Kante eines jeden minimalen aufspannenden
Waldes von G.
Beweis.
1. Angenommen, in EMI gäbe es einen Zyklus Z = v0 , v1 , . . . , vl = v0 .
Sei (vi , vi+1 ) die größte Kante. Dann gilt (vi−1 , vi ) < (vi , vi+1 ) und
(vi+1 , vi+2 ) < (vi , vi+1 ) (die Indizes sind dabei modulo l zu rechnen).
Die Kante (vi , vi+1 ) ist somit keine minimal inzidente Kante. Ein Wider-
spruch.
2. Sei e = {u, v} ∈ EMI . Die Knoten u und v liegen in derselben Zusam-
menhangskomponente K von G. Angenommen, es gibt einen minimalen
aufspannender Baum T von K, der die Kante e nicht enthält. Die Kante
e ist minimal inzident für u oder v, ohne Einschränkung für u. Sei P ein
Weg von u nach v in T . Sei e′ die erste Kante in diesem Pfad. T ′ entstehe
aus T , indem wir e hinzunehmen und e′ entfernen. T ′ ist dann auch ein
aufspannender Baum von K und da alle Kanten verschiedene Gewichte
besitzen, gilt g(T ′ ) < g(T ). Deshalb ist T kein minimaler aufspannender
Baum. Ein Widerspruch.
Damit sind die Behauptungen gezeigt. 2
286 6. Gewichtete Graphen

Bemerkung. Borůvkas Algorithmus folgt der Greedy-Strategie, die wir aus


Punkt 2 von Lemma 6.32 ableiten: Starte mit n Knoten und wähle als Kan-
tenmenge die minimal inzidenten Kanten. Wende dies auf den Graphen, der
durch Kontraktion aller minimal inzidenten Kanten entsteht, rekursiv an. Ge-
nauer gilt
Satz 6.33. Sei G = (V, E) ein gewichteter Graph, EMI die Menge der mi-
nimal inzidenten Kanten und T̃ ein minimaler aufspannender Wald von
G̃ = G/EMI . Dann ist
T = (V, ET̃ ∪ EMI )
ein minimaler aufspannender Wald von G. Falls G zusammenhängend ist,
folgt, dass T ein minimaler aufspannender Baum ist.
Beweis. Mit Lemma 6.32 folgt, dass T azyklisch ist. Falls G zusammenhäng-
end ist, folgt unmittelbar aus der Konstruktion von T , dass T zusammenhäng-
end ist. 2

Borůvkas Algorithmus startet mit T = (V, ∅) und G = (V, E). In einem


Konstruktionsschritt betrachten wir für ein konstruiertes T = (V, ET ) und
ein konstruiertes G = (V, E) die Komponenten V1 , . . . , Vl von (V, EMI ). Falls
V nur noch eine Komponente hat, sind wir fertig. Sonst wählen wir für jede
Komponente Vi die kleinste Kante ti , die einen Endpunkt in Vi und einen End-
punkt im Komplement von Vi hat. Wir setzen mit T = (V, ET ∪ {t1 , . . . , tl })
und G = G/EMI das Verfahren rekursiv fort.
Diese Idee implementieren wir jetzt für einen zusammenhängenden ge-
wichteten Graphen mithilfe der diskutierten Kontraktions-Technik.
Algorithmus 6.34.
edges Boruvka(graph G)
1 if |V | ≥ 2
2 then (G, EMI ) = Contract(G)
3 return EMI ∪ Boruvka(G)
4 return E

Satz 6.35. Borůvkas Algorithmus berechnet einen minimalen aufspannenden


Baum. Die Anzahl der (rekursiven) Aufrufe von Boruvka ist ≤ ⌊log2 (n)⌋ + 1
und die Laufzeit ist in der Ordnung O((n + m) log2 (n)).
Beweis. Durch vollständige Induktion folgt aus Satz 6.33, dass Boruvka einen
minimalen aufspannenden Baum berechnet. Contract reduziert die Anzahl
der Knoten um mehr als die Hälfte (Lemma 6.30). Sei T (n) die Anzahl der
Aufrufe der Funktion Boruvka in Abhängigkeit von n. Dann gilt
(⌊ n ⌋)
T (1) = 1, T (n) ≤ T + 1, n ≥ 2.
2
Aus Satz 1.28 folgt, dass T (n) ≤ ⌊log2 (n)⌋ + 1 = O(log2 (n)) gilt. Da die
Laufzeit für Contract von der Ordnung O(n + m) ist, ergibt sich für die
Laufzeit die Ordnung O((n + m) log2 (n)). 2
6.4 Der Algorithmus von Borůvka 287

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

Fig. 6.13: MST mit dem Algorithmus von Borůvka.

Konstruktion des Borůvka-Baums. Sei Boruvkai die i–te Ausführung


von Boruvka (Algorithmus 6.34), i = 1, . . . l. Wir setzen G0 = G und be-
zeichnen mit Gi = (Vi , Ei ) den Wert der Variablen G und mit EMI i
⊂ Ei−1
den Wert der Variablen EMI nach Ausführung der Zeile 2 in Boruvkai . Die
i
Knoten Vi von Gi sind die Komponenten von (Vi−1 , EMI ). Eine Komponente
C von Gi−1 können wir auch als Teilmenge von V auffassen:
1. Die Komponenten von G0 sind die Knoten V von G, also einelementige
Teilmengen.
2. Wir nehmen an, dass die Komponenten von Gi−1 mit Knoten aus V
identifiziert sind. Eine Komponente Ci = {c1 , . . . , ck } von Gi besteht aus
Knoten ci von Vi , i = 1, . . . , k. Die Knoten ci wiederum sind Komponen-
ten von Gi−1 und wir können sie daher per Rekursion mit Knoten aus V
identifizieren. Wir identifizieren Ci mit ∪kj=1 cj ⊂ V .
Bei dieser Sichtweise fasst Borůvkas Algorithmus in einem Schritt zwei
oder mehreren Komponenten zu einer neuen Komponente zusammen.
Sei v ∈ V und sei Ci (v), die Komponente von Vi , die v enthält, i = 1 . . . l.
Dann gilt C0 (v) = {v} ⊂ C1 (v) ⊂ . . . ⊂ Cl (v) = V . Im Folgenden fassen wir
eine Komponente Ci (v) von Gi als Teilmenge von Vi oder auch als Teilmenge
von V auf, ohne in der Bezeichnung zu unterscheiden.
Mithilfe von Algorithmus 6.34 ordnen wir G den folgenden gewichteten
Baum B zu:
1. Die Knoten der i–ten Ebene von B sind die Knoten Vl−i von Gl−i , i =
0 . . . l. Insbesondere sind die Blätter von B die Knoten V von G.
2. Ein Knoten u in der Ebene i − 1 ist Vorgänger eines Knotens v in der
Ebene i, wenn u = R(C(v)) ein Repräsentant der Komponente C(v) ist,
i = 1 . . . l. Jeder Knoten in der Ebene i − 1 besitzt mindestens zwei Nach-
folger in der Ebene i. Sei ev die minimal inzidente Kante mit Endpunkt v
288 6. Gewichtete Graphen

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}.

Fig. 6.14: Graph mit zugeordnetem Borůvka-Baum.

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

Beweis. Da nl = n und ni ≤ ni+1/2 gilt, folgt ni ≤ n/2l−i , i = 0, . . . , l. Aus


n0 ≤ 2nl folgt l ≤ ⌊log2 (n)⌋. Die Anzahl der Knoten ist


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

6.5 Die Verifikation minimaler aufspannender Bäume

Sei G = (V, E) ein zusammenhängender gewichteter Graph und T = (V, ET )


ein aufspannender Baum von G. Zur Beantwortung der Frage, ob T ein mini-
maler aufspannenden Baum von G ist, verwenden wir folgendes Kriterium: T
ist genau dann ein minimaler aufspannender Baum, wenn jede Kante e ∈ /T
auf dem Zyklus, der in T ∪ {e} entsteht, die Kante maximalen Gewichts ist.
Dies können wir durch einen Vergleich beantworten, wenn für jeden Pfad in
T , aus dem durch Hinzunahme einer Kante aus E \ ET ein Zyklus entsteht,
die Kante maximalen Gewichts berechnet ist. Folglich genügt es einen Al-
gorithmus für dieses Problem, das sogenannte Pfad-Maximum-Problem, zu
entwickeln.
Sei T ein gewichteter Baum, seien u und v Knoten von T und sei P (u, v)
der (eindeutige) Pfad, der u und v in T verbindet. Das Pfad-Maximum-
Problem betrachtet die Berechnung einer Kante maximalen Gewichts auf dem
Pfad P (u, v). Ein Algorithmus, der dieses Problem mit linearer Laufzeit löst,
kann benutzt werden, um einen Algorithmus mit linearer Laufzeit zur Verifi-
kation, ob ein gegebener aufspannender Baum ein minimaler aufspannender
Baum ist, zu implementieren. Wir reduzieren zunächst die Lösung des Pfad-
Maximum-Problems auf die Klasse der voll verzweigten Bäume.
Reduktion des Pfad-Maximum-Problems auf voll verzweigte Bäu-
me. Der folgende Satz, publiziert in [King97], reduziert das Pfad-Maximum-
Problem für einen Baum T auf das Problem, Kanten maximalen Gewichts
auf einem Pfad im T zugeordneten Borůvka-Baum zu berechnen. Zunächst
betrachten wir die notwendige Laufzeit für die Berechnung des zugeordneten
Borůvka-Baums im folgenden Lemma.
Lemma 6.39. Sei T ein gewichteter Baum mit n Knoten. Die Laufzeit des
Borůvka Algorithmus angewendet auf T ist von der Ordnung O(n). Insbeson-
dere ist die Laufzeit zur Berechnung des T zugeordneten Borůvka-Baums von
der Ordnung O(n).
Beweis. Für einen Baum T gilt n+m = 2n−1 = O(n). Wenden wir Contract
auf einen Baum an, so ist das Ergebnis wieder ein Baum. Sei T0 = T und seien
T1 , . . . , Tl , die Bäume, die während der Ausführung von Boruvka entstehen.
Sei ni die Anzahl der Knoten und mi = ni − 1 die Anzahl der Kanten von Ti ,
290 6. Gewichtete Graphen

i = 0, . . . , l. Die Laufzeit für die Tiefensuche zur Ermittlung der Komponen-


ten von Ti und damit auch die Laufzeit von Contract und auch die Laufzeit
einer Iteration von Boruvka ist cni , wobei c konstant ist. Wir summieren über
alle Iterationen und erhalten wegen ni ≤ n/2i

l ∑l
n
cni ≤ c i
≤ 2cn = O(n)
i=0 i=0
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

Fig. 6.15: Ein Baum mit zugeordnetem Borůvka-Baum.

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)

Beweis. Sei PT (u, v) : u = u0 , . . . , ur = v der Pfad, der u und v in T ver-


bindet und f = {ui , ui+1 } die Kante mit maximalem Gewicht auf PT (u, v).
Wir nehmen an, dass die Kontraktion von f in der j–ten Ausführung von
Boruvka erfolgt. Sei C ⊂ Tj−1 die Komponente, die ui enthält und C ′ ⊂ Tj ,
die Komponente, die nach der Kontraktion von f die Knoten ui und ui+1
enthält (siehe Seite 288). Ohne Einschränkung nehmen wir an, dass f ei-
ne minimal inzidente Kante der Komponente C ist (sonst betrachte die
Komponente, die ui+1 , . . . , ur = v enthält). Da die Kanten des Teilpfads
PT (u, ui ), der u mit ui verbindet, geringeres Gewicht als f besitzen, muss
PT (u, ui ) ⊂ C gelten. Also gibt es einen Pfad von u nach C in B. Die Kan-
te (C ′ , C) in B entspricht f und liegt auf dem Pfad PB (u, v). Deshalb folgt
maxe∈PT (u,v) g(e) ≤ maxẽ∈PB (u,v) gB (ẽ).
Sei f˜ = (x̃, ỹ) eine Kante von PB (u, v), sei x̃ der Endpunkt von f˜ mit der
größeren Tiefe und sei f das Bild von f˜ unter der Abbildung ε : EB −→ ET
(siehe Seite 288). Wir zeigen, dass f eine Kante von PT (u, v) ist. Da der letz-
te gemeinsame Vorfahre von u und v in B ungleich x̃ ist, folgt, dass u ∈ x̃
6.5 Die Verifikation minimaler aufspannender Bäume 291

und v ∈ / x̃ gilt (x̃ bezeichnet jetzt die x̃ entsprechende Komponente in T ).


T \ {f } zerfällt in zwei Komponenten T1 = T ∩ x̃ und T2 = T ∩ x̃. Da u ∈ T1
und v ∈ T2 liegt, zerfällt auch PT (u, v) in zwei Komponenten. Somit gilt
f ∈ PT (u, v). Hieraus folgt maxe∈PT (u,v) g(e) ≥ maxẽ∈PB (u,v) gB (ẽ). 2

Um das Pfad-Maximum-Problem für einen Baum T zu lösen, genügt es


damit die Kante maximalen Gewichts in einem ungerichteten Pfad zu berech-
nen, der zwei Blätter v und w in einem voll verzweigten Baum B verbindet.
Wir berechnen den letzten gemeinsamen Vorfahren u der beiden Knoten
v und w in B mit den Algorithmen, die Satz 6.15 zugrunde liegen. Dies er-
folgt mit einer Vorverarbeitungszeit in der Ordnung O(n) in der Zeit O(1)
(Abschnitt 6.1.3). Jetzt führen wir die Berechnung der Kante maximalen
Gewichts des ungerichteten Pfades von v nach w auf die Berechnung der
Kanten maximalen Gewichts der beiden gerichteten Pfade von u nach v und
von u nach w zurück. Die Reduktion des Pfad-Maximum-Problems in T auf
gerichtete Pfade im zugeordneten Borůvka-Baum erfolgt somit in der Vorver-
arbeitungszeit O(n) und konstanter Anfragezeit für jeden Pfad.
Das Pfad-Maximum-Problem für einen voll verzweigten Baum. Sei
T = (V, E) ein voll verzweigter Baum mit Wurzel r und seien (u1 , v1 ), . . . ,
(um , vm ) Paare von Knoten, wobei vi ein Blatt und ui ein Vorfahre von vi ist,
i = 1, . . . , m. Sei pi der Pfad der ui mit vi verbindet und Q = {p1 , . . . , pm }
die Menge dieser Pfade.
Wir entwickeln einen Algorithmus, der eine Kante maximalen Gewichts
auf jedem Pfad aus Q ermittelt. Die hier skizzierte Lösung nimmt Bezug auf
Arbeiten von King ([King97]) und Komlós ([Komlós85]).
In einem ersten Schritt speichern wir für jeden Knoten v in einer Liste
M [v] die Anfangsknoten der Pfade aus Q, die durch v gehen, d. h. jene Pfade,
die in einem Vorfahren von v beginnen und v enthalten. Jeder Knoten u ∈
M [v] definiert einen Pfad, nämlich den Pfad von u nach v. Wir identifizieren
die Anfangsknoten mit diesen Pfaden und sprechen deshalb auch von den
Pfaden in M [v]. Wir berechnen M [v] ausgehend von den Blättern, von unten
nach oben.
Die Liste M [vi ], i = 1, . . . , m, enthält die Anfangsknoten aller Pfade aus
Q, die in vi enden. Für alle anderen Knoten v setzen wir M [v] gleich der
leeren Liste.
Ausgehend von M [v1 ], . . . , M [vm ], berechnen wir M [v] für alle v, die auf
einem der Pfade aus Q liegen. Seien w1 , . . . , wk die Nachfolger von v und
sei M ′ [wi ] die Liste, die wir erhalten, wenn wir in M [wi ] alle Elemente v
entfernen. Wir definieren M [v] als Konkatenation der Listen M ′ [wi ],
M [v] := M ′ [w1 ]|| . . . ||M ′ [wk ].
Die Liste M [v] berechnen wir durch modifizierte Tiefensuche in T (Algorith-
mus 5.4.2).
In einem zweiten Schritt berechnen wir ausgehend von der Wurzel r wei-
tere Listen. Dazu betrachten wir für einen Knoten v und einen Pfad p ∈ Q
292 6. Gewichtete Graphen

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

Fig. 6.16: Pfad-Maximum-Problem für einen voll verzweigten Baum.

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.

M [5] : B M [8] : A M [13] : C


M [14] : A M [17] : A, C, H
M [F ] : B, A M [G] : A M [H] : C, A, C
M [B] : A M [C] : A, A
M [A] :

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

Hieraus folgt die letzte Abschätzung. 2


6.5 Die Verifikation minimaler aufspannender Bäume 295

Bemerkung. Durch die geschickte Codierung der Vektoren M [v]v∈V und


L[v]v∈V als Bitvektoren ist die komplette Berechnung von L[v]v∈V in der
Zeit O(n + m) mithilfe von Bit-Operationen möglich. Für die Details verwei-
sen wir hier auf eine Arbeit von King ([King97]). Als Resultat erhalten wir
für einen voll verzweigten Baum mit n Blättern einen Algorithmus, der für
m Pfade eine Kante maximalen Gewichts mit einer Laufzeit der Ordnung
O(n + m) berechnet.
Sei G = (V, E) ein zusammenhängender gewichteter Graph mit n Knoten
und m Kanten. Die Verifikation eines minimalen aufspannenden Baums T =
(V, ET ) von G erfolgt durch
Algorithmus 6.42.
boolean MSTVerify(graph (V, E), tree (V, ET ))
1 B ← BoruvkaTree(V, ET )
2 Q 1 ← E \ ET
3 Q2 ← LCA(B, Q1 )
4 MaxEdgeInit(B, Q2 )
5 for each edge e = {u, v} ∈ Q1 do
6 if e.weight < MaxEdge(u, v).weight
7 then return false
8 return true

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

Satz 6.43. Sei G = (V, E) ein zusammenhängender gewichteter Graph mit


n Knoten und m Kanten. Die Verifikation eines minimalen aufspannenden
Baums T von G erfolgt durch den Algorithmus 6.42 mit der Laufzeit O(n+m).

Beweis. Die Erzeugung des Borůvka-Baums B (Lemma 6.39), die Reduktion


des LCA-Problems auf das RMQ-Problem durch Algorithmus 6.13 und die
Erstellung der Tabellen für die Lösung des RMQ-Problems (Satz 6.16) erfolgt
mit der Laufzeit in der Ordnung O(n). Die Durchführung aller LCA-Abfragen
erfolgt in der Zeit O(m) (Satz 6.15). Der Algorithmus MaxEdgeInit initiali-
siert die Tabelle L in der Zeit O(n + m) (Lemma 6.41 und anschließende
Bemerkung). Eine Abfrage MaxEdge führen wir in der Zeit O(1) durch (loc.
cit.). Die Anzahl der Kanten |E \ ET | ist < m. Die Laufzeit von Algorithmus
6.42 ist daher von der Ordnung O(n + m). 2

6.6 Ein probabilistischer MST-Algorithmus

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

Da F –schwere Kanten in keinem minimalen aufspannenden Baum eines


Graphen G vorkommen, besitzen G und G \ {e} für eine F –schwere Kante
{e} denselben minimalen aufspannenden Baum. Wir können daher bei der
Berechnung eines MST zuerst die F –schweren Kanten des Graphen entfernen.
Wir beschreiben jetzt, wie wir die F –schweren Kanten eines Graphen G
mit n Knoten und m Kanten identifizieren.
Sei F ⊂ G ein aufspannender azyklischer Teilgraph und T1 , . . . , Tl seien
die Zusammenhangskomponenten von F . Mit ni bezeichnen wir die Anzahl
der Knoten von Ti und mit mi die Anzahl der Kanten e = (u, v) von G \ F ,
deren Endpunkte im selben Baum Ti liegen. Für alle diese Kanten können wir
eine Kante maximalen Gewichts auf dem Pfad, der u mit v in Ti verbindet,
mit der Laufzeit der Ordnung O(ni + mi ) berechnen (siehe Bemerkungen zu
Algorithmus 6.42). Wir führen den modifizierten Algorithmus MSTVerify für
jede Komponente
∑l Ti aus
∑l und erhalten so die F –schweren Kanten von G in
der Zeit O( i=1 ni + i=1 mi ) = O(n + m).

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 )

Satz 6.51. Bei Input eines gewichteten zusammenhängenden Graphen G


mit n Knoten und m Kanten gibt KKT-MST die Kanten eines minimalen
aufspannenden Baums von G zurück. Der Erwartungswert der Laufzeit von
KKT-MST ist von der Ordnung O(n + m).
300 6. Gewichtete Graphen

Beweis. Sei Gi = (Vi , Ei ), i = 1, 2, 3. Wir zeigen zunächst, dass KKT-MST


einen minimalen aufspannenden Baum berechnet. Aus Satz 6.33 folgt, dass
die Kanten aus F1 zum minimalen aufspannenden Baum von G gehören. Be-
sitzt G1 nur einen Knoten, dann bilden die Kanten aus F1 den minimalen
aufspannenden Baum. F2 ist azyklisch und folglich auch ein azyklischer Teil-
graph von G. Nach Lemma 6.46 gehören die F2 –schweren Kanten nicht zu
einem minimalen aufspannenden Baum von G1 . Deshalb stimmen die minima-
len aufspannenden Bäume von G1 und G3 überein. Da |V2 |, |V3 | < n und |E2 |,
|E3 | < m sind, terminieren die rekursiven Aufrufe von KKT-MST. Nach der
Induktionsannahme berechnet der Aufruf KKT-MST(G3 ) einen minimalen
aufspannenden Baum von G3 . Aus Satz 6.33 folgt, dass F1 ∪ KKT-MST(G3 )
ein minimaler aufspannender Baum von G ist.
Es bleibt noch zu zeigen, dass der Erwartungswert der Laufzeit von
KKT-MST von der Ordnung O(n + m) ist. Es gilt |V1 | ≤ n/8 (Lemma
6.30) und |E1 | < m, |V1 | = |V2 | = |V3 |, E(|E2 |) = m/2 (Satz 6.48) und
E(|E3 |) ≤ 2 · n/8 = n/4 (loc. cit.). Die Laufzeit für Contract, SampleSubgraph
und DeleteHeavyEdges ist in der Ordnung O(n + m). Es folgt für den Erwar-
tungswert T (n, m) der Laufzeit von KKT-MST
(n m) (n n)
T (n, m) ≤ T , +T , + c(n + m)
8 2 8 4
für eine Konstante c. Wir betrachten die Rekursionsgleichung, die wir erhal-
ten, wenn wir ≤“ durch =“ ersetzen. Wie leicht nachzurechnen ist, besitzt
” ”
diese die Lösung 2c(n + m). Folglich gilt T (n, m) ≤ 2c(n + m). 2
Bemerkung. Der Algorithmus 6.50 ist eine geringfügige Modifikation des ur-
sprünglichen Algorithmus, die auf [MotRag95] zurückgeht. Sie führt zur obi-
gen Rekursionsgleichung, für die eine Lösung einfach anzugeben ist.

6.7 Transitiver Abschluss und Abstandsmatrix


Die Menge der Kanten E eines gerichteten Graphen G = (V, E) definiert
eine Relation ∼ auf der Menge der Knoten V : u ∼ v genau dann, wenn
(u, v) ∈ E. Diese Relation ist im Allgemeinen nicht transitiv abgeschlossen.4
Der Algorithmus von Warshall5 berechnet den transitiven Abschluss dieser
Relation. Der transitive Abschluss beschreibt für jeden Knoten v ∈ V , welche
Knoten w von v aus erreichbar sind.
Der Algorithmus von Floyd6 gibt die Abstände für alle Paare (v, w) von
Knoten in einem gerichteten Graphen an.
Beide Algorithmen, die im Jahr 1962 in [Floyd62] und [Warshall62] pu-
bliziert wurden, weisen dieselbe Struktur auf. Sie werden mit Floyd-Warshall
4
Transitiv abgeschlossen bedeutet, gilt u ∼ v und v ∼ w, dann gilt auch u ∼ w.
5
Stephen Warshall (1935 – 2006) war ein amerikanischer Informatiker.
6
Robert W. Floyd (1936 – 2001) war ein amerikanischer Informatiker und Turing-
Preisträger.
6.7 Transitiver Abschluss und Abstandsmatrix 301

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

GT = (V, ET ) mit ET := {(v, w) | w erreichbar von v und w ̸= v}

heißt transitiver Abschluss oder transitive Hülle von G.


Bemerkungen:
1. G ist ein Teilgraph von GT und GT ist der kleinste transitiv abgeschlos-
sene Graph, der G umfasst.
2. Bei Graphen ist der transitive Abschluss durch die Komponenten be-
stimmt: w ist erreichbar von v, d. h. (v, w) ∈ ET , genau dann, wenn v und
w in derselben Zusammenhangskomponente von G liegen. Wir berechnen
mit Breitensuche (Algorithmus 5.11) alle Komponenten und damit den
transitiven Abschluss mit der Laufzeit in der Ordnung O(n + m) oder
O(n2 ).
3. Bei gerichteten Graphen gilt: Der Knoten w ist erreichbar von v genau
dann, wenn w in Zv , der Zusammenhangskomponente von v, liegt. Die
Ermittlung von ET entspricht der Ermittlung von Zv für alle v ∈ V . Der
Aufwand ist von der Ordnung O(n3 ) oder O(n(n+m)), falls wir Tiefensu-
che (Algorithmus 5.12) oder Breitensuche (Algorithmus 5.11) verwenden.
Der Algorithmus von Warshall. Sei G = (V, E) ein gerichteter Graph
und a die Adjazenzmatrix von G. Der Algorithmus von Warshall berechnet
eine Folge von Matrizen a0 , a1 , . . . , an , n = |V |, mit Koeffizienten aus {0, 1}.
Diese Folge ist definiert durch die rekursive Gleichung

a0 := a,
ak [i, j] := ak−1 [i, j] or (ak−1 [i, k] and ak−1 [k, j]) für k = 1, . . . , n.

Die Matrizen ak besitzen mittels folgender Definition und folgendem Satz


eine anschauliche Interpretation.
Definition 6.53. Ein einfacher Pfad von i nach j, welcher bis auf i und j
nur Knoten ≤ k enthält, heißt k–Pfad.
Satz 6.54. Für die Matrix ak , 1 ≤ k ≤ n, sind äquivalent:
1. ak [i, j] = 1 für i ̸= j.
2. Es existiert ein k–Pfad von i nach j für i ̸= j.
Insbesondere beschreibt an den transitiven Abschluss von G.
7
Stephen Cole Kleene (1909 – 1994) war ein amerikanischer Mathematiker und Lo-
giker. Nach ihm ist der Kleene-Abschluss einer formalen Sprache und der Kleene
Operator ∗ benannt.
302 6. Gewichtete Graphen

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

Fig. 6.17: Der transitive Abschluss mit Warshalls Algorithmus.

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

Bemerkung. Die äußerste for-Schleife berechnet die Matrizen a1 , a2 , . . . , an .


Für festes k berechnen wir dann durch die beiden inneren for-Schleifen die
Matrix ak . Die Laufzeit von Warshall ist von der Ordnung O(n3 ).
Der Algorithmus von Floyd. Wir können die Abstände für alle Paare von
Knoten in positiv gewichteten Graphen mit dem Algorithmus von Dijkstra
berechnen (Abschnitt 6.2). Der folgende Algorithmus von Floyd leistet dies
auch für Graphen mit negativen Gewichten, falls keine Zyklen mit negativen
Gewichten auftreten.
Sei G = (V, E) ein gewichteter gerichteter Graph. Wir stellen G mit der
Adjazenzmatrix a dar:


 g(i, j), falls (i, j) ∈ E,
a[i, j] := ∞, falls (i, j) ∈
/ E, i ̸= j,


0, falls i = j.
Die Abstandsmatrix ad von G ist definiert durch:
{
Länge eines kürzesten Pfades von i nach j,
ad [i, j] := d(i, j) :=
∞, falls kein Pfad existiert.
Zur Berechnung der Abstandsmatrix ad aus a definieren wir eine Folge
von Matrizen a0 , . . . , an durch die rekursive Gleichung
a0 = a,
ak [i, j] = min{ak−1 [i, j], ak−1 [i, k] + ak−1 [k, j]} für k = 1, . . . , n.
Satz 6.56. Für die Matrix ak gilt: ak [i, j] ist die Länge eines kürzesten k–
Pfades von i nach j oder ak [i, j] = ∞, falls kein k–Pfad von i nach j existiert.
Insbesondere definiert an die Abstandsmatrix von G.
Beweis. Wir zeigen die Aussage durch Induktion nach k:
Für k = 0 ist a[i, j] ist die Länge eines 0–Pfades (Kante) von i nach j. Der
Induktionsanfang ist richtig.
Aus k − 1 folgt k: Sei Mk [i, j] die Menge der k–Pfade von i nach j und
sei P ∈ Mk [i, j] ein Pfad minimaler Länge. Wir betrachten zwei Fälle.
1. Falls k ∈ / P , dann ist P ∈ Mk−1 [i, j] und P ist von minimaler Länge,
also folgt nach der Induktionsvoraussetzung l(P ) = ak−1 [i, j]. Da P ein
k–Pfad minimaler Länge ist, gilt l(P ) ≤ ak−1 [i, k]+ak−1 [k, j] und folglich
ak [i, j] = min{ak−1 [i, j], ak−1 [i, k] + ak−1 [k, j]} = ak−1 [i, j] = l(P ).
2. Falls k ∈ P ist, zerlege P in P1 ∈ Mk−1 [i, k] und P2 ∈ Mk−1 [k, j]. P1 und
P2 sind (k − 1)–Pfade minimaler Länge. Nach Induktionsvoraussetzung
gilt l(P1 ) = ak−1 [i, k] und l(P2 ) = ak−1 [k, j]. Da P ein k–Pfad minimaler
Länge ist, gilt l(P ) = ak−1 [i, k] + ak−1 [k, j]. Es gibt keinen (k − 1)–Pfad
kürzerer Länge, also gilt ak−1 [i, k] + ak−1 [k, j] ≤ ak−1 [i, j] und folglich
ak [i, j] = min{ak−1 [i, j], ak−1 [i, k] + ak−1 [k, j]} = ak−1 [i, k] + ak−1 [k, j] =
l(P ).
Damit ist alles gezeigt. 2
304 6. Gewichtete Graphen

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

Fig. 6.18: Die Abstandsmatrix mit Floyds Algorithmus.

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]

Bemerkung. Die äußerste for-Schleife berechnet nacheinander die Matrizen


a1 , a2 , . . . , an . Für festes k berechnen wir dann durch die beiden inneren for-
Schleifen die Matrix ak . Die Laufzeit T (n) von Floyd ist von der Ordnung
O(n3 ).

Berechnung der kürzesten Pfade. Wir modifizieren den Algorithmus von


Floyd so, dass wir die Rekonstruktion aller kürzesten Pfade ermöglichen. Da-
zu verwenden wir eine n × n–Matrix P . In P [i, j] speichern wir den größten
6.8 Flussnetzwerke 305

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

a[i, j] ← a[i, k] + a[k, j]; P [i, j] ← k.

Wir können aus P alle kürzesten Pfade gewinnen.


Algorithmus 6.58.
Path(vertex i, j)
1 vertex k
2 k ← P [i, j]
3 if k > 0
4 then Path(i, k), print(k), Path(k, j)

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

400 170 280

s. t

300 100 320

s1 s2
540

Fig. 6.19: Ein Flussnetzwerk.

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?

Dabei sind folgende Nebenbedingungen für den Transport zu beachten:


1. Über eine Leitung kann nicht mehr gepumpt werden, als die Kapazität
zulässt.
2. Für einen Knoten n, n ̸= s, t gilt:
Was in n hinein fließt, muss auch wieder abfließen.
Wir präzisieren die Situation des vorangehenden Beispiels in der folgenden
Definition 6.59.
1. Ein Flussnetzwerk oder kurz Netzwerk N = (V, E, s, t) besteht aus einem
gewichteten gerichteten Graphen (V, E). Die Gewichtsfunktion
c : E −→ R>0
heißt Kapazität. s ∈ V heißt Quelle, t ∈ V heißt Senke.
2. Sei S ⊂ V .
In(S) := {(v, w) ∈ E | v ∈
/ S, w ∈ S}.
Out(S) := {(v, w) ∈ E | v ∈ S, w ∈
/ S}.
3. Ein Fluss für N ist eine Abbildung f : E −→ R≥0 mit
a. f∑(e) ≤ c(e) für e∑
∈ E (Kapazitätsschranke).
b. f (e) = f (e) für v ∈ V \ {s, t} (Flusserhaltung13 ).
∑e∈In(v) ∑e∈Out(v)
4. F = e∈In(t) f (e) − e∈Out(t) f (e) heißt totaler Fluss bezüglich f .
5. Sei S ⊂ V ein Schnitt, d. h. s ∈ S, t ∈ / S.

C(S) := c(e)
e∈Out(S)

heißt Kapazität des durch S definierten Schnittes.


6. S definiert einen Schnitt minimaler Kapazität, falls C(S) für alle Schnitte
S minimal ist.
13
Flusserhaltung wird auch als das erste Kirchhoffsche Gesetz bezeichnet.
6.8 Flussnetzwerke 307

Beispiel. Im Netzwerk der Figur 6.20 definiert S = {s, n1 } einen Schnitt


minimaler Kapazität mit C(S) = 600.

300
n1 n2

400 170 280

s. t

300 100 320

s1 s2
540

Fig. 6.20: Ein Schnitt minimaler Kapazität.

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)

Beweis. Mit S = {s} folgt das Corollar aus Satz 6.60. 2

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)

Dies zeigt das Corollar. 2


Algorithmus von Ford-Fulkerson. Der Algorithmus von Ford-Fulkerson
konstruiert einen Fluss f für N , sodass der totale Fluss F bezüglich f maxi-
mal ist. Der Algorithmus startet mit dem 0 Fluss und vergrößert den Fluss
schrittweise.
Die Methode zur Vergrößerung des totalen Flusses besteht darin, einen
Pfad von s nach t zu finden, die eine Erhöhung des Flusses für jede seiner
Kanten erlaubt. Dabei können auch Kanten mit Fluss > 0 entgegen der
Kantenrichtung im Pfad enthalten sein. Eine Erhöhung des Flusses längs einer
Kante ist möglich, falls der aktuelle Fluss kleiner der Kapazitätsschranke
ist, oder falls die Kante entgegen der Richtung enthalten ist (und der Fluss
erniedrigt wird). Wir zeigen das Vorgehen zunächst an einem Beispiel.
Beispiel. Die erste Zahl der Kantenbeschriftungen ist die Kapazität, die zwei-
te der Fluss. Pfade mit Zunahme sind P = s, s1 , s2 , t (Zunahme = 50) und
P = s, n1 , s2 , t, (Zunahme = 100), wie Figur 6.21 zeigt.
6.8 Flussnetzwerke 309

300;300
n1 n2

400;200 170;50 280;250

s. t

300;250 200;100 320;200

s1 s2
540;300

Fig. 6.21: Pfade mit Zunahme.

Zur Modellierung der Pfade mit Zunahme dient der Restgraph.


Definition 6.63. Sei N = (V, E, s, t) ein Netzwerk und f : E −→ R≥0 ein
Fluss für N . Der gerichtete Graph Gf := (V, Ef ), wobei
Ef := {(v, w) ∈ V 2 | ((v, w) ∈ E und f (v, w) < c(v, w))
oder ((w, v) ∈ E und f (w, v) > 0)},
heißt Restgraph von N bezüglich f .
cf : Ef −→ R+ ,


 c(v, w) − f (v, w) für (v, w) ∈ E, (w, v) ∈
/ E,
cf (v, w) := f (w, v) für (v, w) ∈
/ E, (w, v) ∈ E,


c(v, w) − f (v, w) + f (w, v) für (v, w) ∈ E, (w, v) ∈ E.
heißt Restkapazität von N bezüglich f .
Bemerkung. Eine Kante (v, w) ∈ E, kann zu den Kanten (v, w) und (w, v) im
Restgraphen führen. Sind weder (v, w) noch (w, v) Kanten in G, so gibt es
auch im Restgraphen keine Kante zwischen v und w. Deshalb gilt |Ef | ≤ 2|E|.
Beispiel. Figur 6.22 zeigt die Konstruktion des Restgraphen, wobei Kapa-

zität, Fluss“ als Kantenbeschriftung dienen:

12,12 12
n1 n2 n1 n2
5
16,11 9,4 20,15 11 5 15 5

s. 4,1 10,2 7,7 t s. 5 9 4 7 t


5
13,8 4,4 4
8
3
s1 s2 s1 s2
14,11 11

Fig. 6.22: Netzwerk mit Fluss und Restgraph.


310 6. Gewichtete Graphen

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

g ist ein Fluss für N und Fg = Ff + ∆.


Beweis. Die Behauptung folgt unmittelbar aus der Konstruktion. 2
Bemerkung. Sei v0 , . . . , vk ein Pfad P mit Zunahme ∆, den wir zur Berech-
nung von g verwenden. Die Kanten (vi , vi+1 ) von P mit cf (vi , vi+1 ) = ∆
treten nicht mehr als Kanten im Restgraphen Gg auf.
Satz 6.66. Sei N = (V, E, s, t) ein Netzwerk, f ein Fluss für N mit dem
totalen Fluss F . Dann sind äquivalent:
1. F ist maximal.
2. Für jeden Pfad P von s nach t gilt für die Zunahme ∆ = 0.
Beweis. Falls für einen Pfad P ∆ > 0 gilt, kann F erhöht werden und ist
infolgedessen nicht maximal. Dies zeigt, dass aus Aussage 1 Aussage 2 folgt.
Wir zeigen jetzt, dass aus Aussage 2 auch Aussage 1 folgt. Sei

S = {w ∈ V | es gibt einen Pfad P von s nach w mit ∆ > 0} ∪ {s}.

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)

Sei F ∗ ein maximaler Fluss und S ∗ ⊂ V ein Schnitt minimaler Kapazität.


Dann gilt:
F ≤ F ∗ ≤ C(S ∗ ) ≤ C(S).
Wegen F = C(S) folgt: F = F ∗ und C(S ∗ ) = C(S). 2
6.8 Flussnetzwerke 311

Der Beweis ergibt zusätzlich:


Satz 6.67 (Theorem vom maximalen Fluss – Schnitt minimaler Kapazität).
Der Wert des maximalen totalen Flusses ist gleich dem Wert eines Schnit-
tes minimaler Kapazität. Die Zusammenhangskomponente der Quelle s im
Restgraphen des Netzwerkes mit maximalem Fluss ist ein Schnitt minimaler
Kapazität.
Der Algorithmus von Ford-Fulkerson vergrößert den totalen Fluss mit-
hilfe von Pfaden mit Zunahme. Der Algorithmus besteht aus den folgenden
Schritten:
1. Suche einen Pfad P mit Zunahme von s nach t.
2. Vergrößere den Fluss längs P mit den Formeln von Satz 6.65.
3. Wiederhole Schritt 1 und Schritt 2, solange ein Pfad mit Zunahme exis-
tiert.
Falls kein Pfad mit Zunahme existiert, ist der totale Fluss maximal.
Beispiel. Die erste Zahl bei der Kantenbeschriftung gibt die Kapazität an,
die nachfolgende Zahl den Fluss beim Start des Algorithmus. Die weiteren
Zahlen geben die Flüsse nach Flussvergrößerung mittels der Pfade

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

Fig. 6.23: Maximaler Fluss – Schnitt minimaler Kapazität.

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

Wenn wir für den zunehmenden Weg abwechselnd s, n, m, t und s, m,


n, t wählen, so terminiert Ford-Fulkerson nach 2c vielen Iterationen (un-
abhängig von |V | und |E|).
6.8 Flussnetzwerke 313

Algorithmus von Edmonds-Karp. Im Algorithmus 6.68 ist nicht festge-


legt, nach welcher Methode ein Pfad mit Zunahme zu ermitteln ist. Dieser
Schritt beeinflusst die Laufzeit wesentlich. Der Vorschlag von Edmonds und
Karp für die Wahl des Pfades mit Zunahme ist: Wähle in jedem Schritt in
Ford-Fulkerson einen Pfad mit Zunahme von s nach t mit einer minimalen
Anzahl von Kanten.
Die Pfade mit Zunahme von s nach t in N sind die gerichteten Wege von
s nach t im Restgraphen Gf . Einen kürzesten gerichteten Weg (minimale
Kantenanzahl) ermitteln wir durch Breitensuche in Gf (Algorithmus 5.11).
Die Laufzeit ist in der Ordnung O(n + m), falls der Graph durch eine Adja-
zenzliste gegeben ist und von der Ordnung O(n2 ), falls der Graph durch eine
Adjazenzmatrix definiert ist (n = |V |, m = |E|).

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 .

δf (v, w) := Abstand von v und w in Gf ,


wobei alle Kanten in Gf mit dem Gewicht eins zu versehen sind.
Lemma 6.69. Seien f1 , f2 ,. . . die Flüsse in einem Netzwerk, die wir mithilfe
von Pfaden mit Zunahmen nach Edmonds-Karp konstruieren, dann gilt für
v ∈ V \ {s}: δfi (s, v) ist monoton wachsend in i.
Beweis. Angenommen, es gibt w ∈ V \ {s} und i ∈ N mit

δfi+1 (s, w) < δfi (s, w).

Setze f := fi , f ′ := fi+1 und

U = {u ∈ V \ {s} | δf ′ (s, u) < δf (s, u)}.

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

δf (s, y) ≤ δf (s, x) + 1 ≤ δf ′ (s, x) + 1 = δf ′ (s, y),

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

1. (x, y) ∈ E. Dann gilt f (x, y) = c(x, y). Es folgt (y, x) ∈ Ef .


2. (x, y) ∈
/ E. Dann gilt (y, x) ∈ E und f (y, x) = 0, also folgt f (y, x) <
c(y, x) und (y, x) ∈ Ef .
Da (x, y) ∈
/ Ef und (x, y) ∈ Ef ′ , enthält der Pfad P mit Zunahme, der
zur Konstruktion von f ′ aus f verwendet wurde (y, x) (in der Richtung von
y nach x).
Da P ein kürzester Weg ist und x ∈ / U , folgt

δf (s, y) = δf (s, x) − 1 ≤ δf ′ (s, x) − 1 = δf ′ (s, y) − 2 < δf ′ (s, y),

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

δf˜(s, u) = δf˜(s, v) + 1 ≥ δf (s, v) + 1 = δf (s, u) + 2

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

2. Gegeben sei ein mit ganzen Zahlen gewichteter Graph G:

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

a. Zu G wird eine Kante hinzugefügt.


b. Zu G werden ein Knoten und mehrere inzidente Kanten hinzugefügt.
8. Sei G ein gewichteter Graph und T ein minimaler aufspannender Baum
für G. Wir verändern das Gewicht einer einzigen Kante von G. Diskutie-
ren Sie die Auswirkungen auf T .
9. Wir modellieren ein Kommunikationsnetz mit bidirektionalen Verbin-
dungen mit einem gewichteten Graphen G = (V, E). Die Knoten sind
Knoten im Kommunikationsnetzwerk und die Kanten sind Kommuni-
kationsverbindungen. Das Gewicht einer Kante (u, v) ist die Ausfall-
Wahrscheinlichkeit p(u, v) ∈ [0, 1] für die Verbindung (u, v). Wir nehmen
an, dass diese Wahrscheinlichkeiten voneinander unabhängig sind. Die
Wahrscheinlichkeit, ∏ndass eine Verbindung v = v0 , . . . , vn = w von v nach
w ausfällt, ist 1 − i=1 (1 − p(vi−1 , vi )). Geben Sie einen Algorithmus an,
um Wege mit geringster Ausfall-Wahrscheinlichkeit zu berechnen.
10. Vier Personen P1 , P2 , P3 und P4 möchten in der Dunkelheit einen Steg
überqueren. Der Steg kann nur überquert werden, wenn eine Fackel mit-
geführt wird. Die vier Personen besitzen nur eine Fackel und der Steg
trägt maximal zwei Personen. Die Personen sind unterschiedlich schnelle
Läufer. P1 benötigt für eine Überquerung fünf Minuten, P2 zehn Minu-
ten, P3 zwanzig Minuten und P4 fünfundzwanzig Minuten. Überqueren
zwei Personen zusammen den Steg, dann richtet sich die benötigte Zeit
nach dem langsameren Läufer. In welcher Reihenfolge müssen die vier
Personen den Steg überqueren, damit alle an das andere Ufer gelangen
und die dazu verbrauchte Zeit minimal ist.
a. Modellieren Sie das Problem mit einem Graphen.
b. Mit welchem Algorithmus kann das Problem gelöst werden?
c. Geben sie eine Lösung des Problems an.
11. Sei G = (V, E) ein zusammenhängender Graph.
d(G) := max{d(i, j) | i, j ∈ V } heißt Durchmesser von G.
e(i) := max{d(i, j) | j ∈ V } heißt Exzentrizität von i.
r(G) := min{e(i) | i ∈ V } heißt Radius von G.
i ∈ V heißt Mittelpunkt von G, wenn e(i) = r(G) gilt.
a. Zeigen Sie: r(G) ≤ d(G) ≤ 2r(G)
b. Entwickeln Sie Algorithmen zur Berechnung von d(G), r(G) und zur
Ermittlung aller Mittelpunkte.
c. Wie gehen Sie vor, falls Sie nur an den Mittelpunkten interessiert
sind?
12. Alle Städte eines Landes sollen durch Magnetschwebebahnen verbunden
werden. Beim Aufbau des Schienennetzes soll darauf geachtet werden, die
Länge der zu erstellenden Schienen zu minimieren.
a. Wie bestimmen Sie jene Städte, die durch einen Schienenstrang zu
verbinden sind?
b. Was ergibt sich für eine Struktur?
Übungen 317

c. Nachdem ein Schienennetz erstellt wurde, beschließt das Parlament


die Hauptstadt zu verlegen. Die Summe der Fahrtzeiten von der neu-
en Hauptstadt zu allen übrigen Städten mit der Magnetschwebebahn
soll minimal sein. Entwickeln Sie einen Algorithmus zur Ermittlung
aller Städte des Landes, die die obige Bedingung erfüllen. Nehmen
Sie dabei an, dass jede Strecke in beiden Richtungen befahrbar ist
und die Fahrzeiten in beiden Richtungen gleich sind.
d. Erläutern Sie Ihr Vorgehen mithilfe eines Beispiels mit fünf Städten.
13. Verifikation eines kürzesten Wege-Baumes in linearer Zeit. Sei G = (V, E)
ein zusammenhängender gewichteter Graph, r ∈ V und T ein Teilbaum
mit Wurzel r, der G aufspannt. Entwickeln Sie einen Algorithmus mit
linearer Laufzeit, der prüft, ob die Pfade in T kürzeste Pfade in G sind.
14. Sei X = {x1 , . . . , xn } eine Menge von boolschen Variablen. Wir betrach-
ten boolsche Ausdrücke der Form b = b1 ∧ . . . ∧ bn mit bj = bj 1 ∨ bj 2 ,
j = 1, . . . , n, und bj 1 , bj 2 ∈ L = {x1 , . . . , xn , x1 , . . . , xn }.
Der Term v ∨ w ist äquivalent zu v =⇒ w und zu w =⇒ v. Wir schreiben
statt v ∨ w die Implikationen v =⇒ w und w =⇒ v.
Wir ordnen einem Ausdruck b einen gerichteten Graphen G = (V, E) zu.
Wir setzen V = L. Jeder Term v ∨ w in b definiert die Kanten (v, w) und
(w, v).
Geben Sie mithilfe von G einen Algorithmus an, der entscheidet, ob ein
Ausdruck b erfüllbar ist, d. h. ob es eine Belegung der Variablen x1 , . . . , xn
mit 0 und 1 gibt, sodass b(x1 , . . . , xn ) = 1 gilt. Dies wird als 2-SAT-
Problem bezeichnet.14
15. Ein Netzwerk sei definiert durch die Adjazenzliste

A: (B,25,15),(F,5,5) B: (F,15,15), (T,5,0)


D: (H,10,0),(F,30,30) F: (T,35,35), (J,20,15)
.
H: (J,20,15) J: (T,30,30)
S: (A,20,20),(D,50,30),(H,20,15) T:

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

17. Wir betrachten ein Netzwerk mit n Quellen S1 , . . . , Sn und m Senken


T1 , . . . , Tm . Lösen Sie das mmaximale Flussproblem für ein solches Netz-
werk. Führen Sie Ihre Lösung am Beispiel von Figur 6.24 durch.

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

Fig. 6.24: Variante.

18. Sei G = (V ∪ W, E) ein bipartiter Graph. Sei Z ⊂ E. Z heißt eine


Zuordnung in G, wenn jeder Knoten höchstens Endpunkt einer Kante
ist. Eine maximale Zuordnung ist eine Zuordnung mit einer maximalen
Anzahl von Kanten. In naheliegender Weise kann G ein Flussnetzwerk
N zugeordnet werden. Arbeiten Sie die Details aus. Zeigen Sie, dass das
Problem der Berechnung einer maximalen Zuordnung auf die Berechnung
eines maximalen Flusses in N reduziert werden kann.
Ermitteln eine maximale Zuordnung im folgenden bipartiten Graphen.

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].

A.1 Endliche Wahrscheinlichkeitsräume und


Zufallsvariable
Für die Analyse von Algorithmen können wir uns meistens auf endliche Wahr-
scheinlichkeitsräume einschränken.
Definition A.1.
1. Ein n-Tupel von Zahlen

n
p = (p1 , . . . , pn ), pi ∈ R, 0 ≤ pi ≤ 1, mit pi = 1
i=1

heißt Wahrscheinlichkeitsverteilung (kurz: Verteilung).


Gilt pi = n1 , i = 1, . . . , n, dann heißt (p1 , . . . , pn ) die Gleichverteilung.
2. Ein Wahrscheinlichkeitsraum (X , pX ) besteht aus einer endlichen Menge
X = {x1 , . . . , xn } mit einer Wahrscheinlichkeitsverteilung
p = (p1 , . . . , pn ). pi ist die Wahrscheinlichkeit von xi , i = 1, . . . , n. Wir
schreiben pX (xi ) := pi und betrachten pX als Abbildung X → [0, 1], die
x ∈ X seine Wahrscheinlichkeit zuordnet. pX bezeichnen wir auch als
Wahrscheinlichkeitsmaß auf X .
3. Ein Ereignis E ist eine Teilmenge E von X . Wir erweitern das Wahrschein-
lichkeitsmaß auf Ereignisse, pX (E) oder kurz p(E) ist definiert durch

pX (E) = pX (y).
y∈E

Bemerkung. Sei (X , p) ein Wahrscheinlichkeitsraum und seien A und B Er-


eignisse. Unmittelbar aus der Definition A.1 folgt:
1. p(X ) = 1 und p(∅) = 0.
2. p(A ∪ B) = p(A) + p(B), falls A ∩ B = ∅.
3. p(X \ A) = 1 − p(A).
© 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
320 A. Wahrscheinlichkeitsrechnung

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.

Bemerkung. Die bedingten Wahrscheinlichkeiten p( |B) definieren eine Wahr-


scheinlichkeitsverteilung auf X . Sie beschreibt die Wahrscheinlichkeit von x
unter der Voraussetzung, dass das Ereignis B vorliegt. Dadurch wird das
Wahrscheinlichkeitsmaß p auf B konzentriert. Wir erhalten p(B |B) = 1 und
p(X \ B | B) = 0.
Beispiel. Wir betrachten nochmals das Beispiel Werfen ( mit
) einem Würfel von
oben mit der Wahrscheinlichkeitsverteilung p = 16 , . . . , 16 . Sei B das Ereignis

gerade Augenzahl“. Dann gilt( p(B) = 12 . Wir ) erhalten auf X die (bedingte)
Wahrscheinlichkeitsverteilung 0, 13 , 0, 13 , 0, 13 unter der Voraussetzung B.
Definition A.3. Sei X ein Wahrscheinlichkeitsraum und seien A, B ⊆ X
Ereignisse. A und B heißen unabhängig, wenn p(A ∩ B) = p(A) · p(B) gilt.
Für p(B) > 0 ist diese Bedingung äquivalent zu p(A|B) = p(A).
1
Andrei Nikolajewitsch Kolmogorow (1903 – 1987) war ein russischer Mathemati-
ker. Zu seinen großen Leistungen zählt die Axiomatisierung der Wahrscheinlich-
keitstheorie.
A.1 Endliche Wahrscheinlichkeitsräume und Zufallsvariable 321

Satz A.4. Sie X ein endlicher Wahrscheinlichkeitsraum, und sei X die


disjunkte Vereinigung der Ereignisse E1 , . . . , Er ⊆ X , mit p(Ei ) > 0 für
i = 1 . . . r. Dann gilt

r
p(A) = p(Ei ) · p(A | Ei )
i=1

für jedes Ereignis A ⊆ X .


Beweis. Es gilt A = ∪ri=1 (A ∩ Ei ), (A ∩ Ei ) ∩ (A ∩ Ej ) = ∅ für i ̸= j. Deshalb
folgt
∑ r ∑ r
p(A) = p(A ∩ Ei ) = p(Ei ) · p(A|Ei ).
i=1 i=1
2
Definition A.5. Sei (X , pX ) ein Wahrscheinlichkeitsraum und Y eine endli-
che Menge. Eine Abbildung X : X −→ Y heißt Y –wertige Zufallsvariable auf
X . Wir sagen X ist eine reelle Zufallsvariable, wenn Y ⊂ R, und eine binäre
Zufallsvariable, wenn Y = {0, 1}.

Beispiel. Wir betrachten das Würfelexperiment von oben. Die Abbildung


X : {1, . . . , 6} −→ {0, 1}, die einem geraden Ergebnis 0 und einem ungeraden
Ergebnis 1 zuordnet, ist eine binäre Zufallsvariable.
Definition A.6. Sei X : X −→ Y eine reelle Zufallsvariable.
1. Der gewichtete Mittelwert

E(X) := p(X = y) · y,
y∈Y

wobei p(X = y) = p({x ∈ X | X(x) = y}), heißt Erwartungswert von X.


2. Der Erwartungswert der Zufallsvariablen (X − E(X))2 heißt Varianz von
X.
Var(X) := E((X − E(X))2 ).
Die Varianz ist ein Maß für die erwartete Abweichung einer Zufallsvaria-
blen von ihrem Erwartungswert zum Quadrat.2
3. Die Standardabweichung von X ist

σ(X) = Var(X).

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

Beispiel. Wir betrachten die Zufallsvariable X aus dem vorangehenden Bei-


spiel, X : {0, . . . , 6} −→ {0, 1}, die einem geraden Ergebnis des Würfelexpe-
riments 0 und einem ungeraden Ergebnis 1 zuordnet. Es gilt E(X) = 1/2.
Die Zufallsvariable (X − 1/2)2 nimmt für alle Augenzahlen den Wert 1/4 an.
Deshalb gilt Var(X) = 1/4 und σ(X) = 1/2.
Satz A.7. Seien X und Y reelle Zufallsvariable, a, b ∈ R. Dann gilt
1. E(aX + bY ) = aE(X) + bE(Y ).
2. Var(X) = E(X 2 ) − E(X)2
Beweis. Seien x1 , . . . , xn die Werte von X und y1 , . . . , ym die Werte von Y .
Dann gilt

E(aX + bY ) = p(X = xi ∩ Y = yj ) · (axi + byj )
i,j

=a p(X = xi ∩ Y = yj ) · xi +
i,j

b p(X = xi ∩ Y = yj ) · yj
i,j
∑ ∑
=a p(X = xi ) · xi + p(Y = yj ) · yj
i j
= aE(X) + bE(Y ),

(beachte, p(X = xi ) = j p(X = xi ∩ Y = yj ) (Satz A.4)) und

E((X − E(X))2 ) = E(X 2 − 2XE(X) + E(X)2 ) = E(X 2 ) − E(X)2 .

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

Beweis. Mit Satz A.4 folgt



m
p(X = x) = p(X = x | Y = yi )p(Y = yi ).
i=1

Die Wertemenge von X sei {x1 , . . . , xn }. Dann gilt


A.2 Spezielle diskrete Verteilungen 323


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

Dies zeigt die Behauptung. 2


Satz A.10 (Markovsche Ungleichung). Sei X eine Zufallsvariable, die nur
nicht negative ganze Zahlen als Werte annimmt. Dann gilt für jede reelle Zahl
r>0
1
p(X ≥ rE(X)) ≤ .
r
Beweis. Aus
∑ ∑
E(X) = i · p(X = i) ≥ i · p(X = i)
i≥1 i≥rE(X)

≥ r · E(X) p(X = i)
i≥rE(X)

= r · E(X) · p(X ≥ r · E(X))

folgt die Behauptung. 2

A.2 Spezielle diskrete Verteilungen

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

gilt. Der zu X assoziierte Wahrscheinlichkeitsraum mit der Verteilung p =


(p(X = i))i≥0 ist abzählbar und nicht mehr endlich. Die Definitionen und
324 A. Wahrscheinlichkeitsrechnung

Sätze aus dem Abschnitt A.1 sind auf die allgemeinere Situation übertragbar.
Wenn die Reihe


E(X) = i · p(X = i)
i=0

konvergiert, heißt E(X) der Erwartungswert der diskreten Zufallsvariablen X.

∑∞ 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.).

Erwartungswert und Varianz einer Zufallsvariablen sind einfach zu berech-


nen, wenn eine explizite Formel für die erzeugende Funktion der Zufallsvaria-
blen bekannt ist.

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

heißt erzeugende Funktion der Zufallsvariablen X.3

Für |z| ≤ 1 konvergiert die Potenzreihe GX (z) und stellt im Inneren


des Konvergenzbereichs eine unendlich oft differenzierbare Funktion dar.
Die Ableitung ergibt sich durch gliedweises Differenzieren ([AmannEscher02,
Kap. V.3]).



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

E(X) = G′X (1), E(X 2 ) = G′′X (1) + G′X (1) und


Var(X) = G′′X (1) + G′X (1) − G′X (1)2 .

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.

Var(X) = E(X 2 ) − E(X)2 = p − p2 = p(1 − p).

2
4
Jakob Bernoulli (1655 – 1705) war ein schweizer Mathematiker und Physiker.
326 A. Wahrscheinlichkeitsrechnung

Definition A.15. Eine Zufallsvariable X heißt binomialverteilt mit Parame-


ter (n, p), n > 0, 0 < p < 1, wenn X die Werte 0, 1, 2, . . . , n annimmt und
(n)
p(X = i) = pi (1 − p)n−i
i
gilt.5

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.

Fig. A.1: Verteilung der Einsen in einer Zufallsfolge.

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:

E(X) = np, E(X 2 ) = n(n − 1)p2 + np und Var(X) = np(1 − p).

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 .

Die Aussagen 1 und 2 folgen unmittelbar.

Var(X) = E(X 2 ) − E(X)2 = n(n − 1)p2 + np − n2 p2 = np(1 − p).

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.

Fig. A.2: Verteilung bei einer Zufalls-Hashfunktion.

Wir betrachten für die Binomialverteilung den Limes für n → ∞ wobei


n · p = λ konstant gehalten werden soll, d. h. wir setzen p = λ/n:
(n) ( n ) ( λ )i ( λ
)n−i
lim pi (1 − p)n−i = lim 1−
n→∞ i n→∞ i n n
( )n−i
λi n(n − 1) . . . (n − i + 1) λ
= lim 1 −
i! n→∞ ni n
( )n ( ) ( )( )−i
λ i
λ 1 i−1 λ
= lim 1 − lim 1 − ... 1 − 1−
i! n→∞ n n→∞ n n n
i
λ
= e−λ
i!
(für das letzte = siehe Satz B.19).
328 A. Wahrscheinlichkeitsrechnung

Die Poisson-Verteilung gibt für eine kleine Auftrittswahrscheinlichkeit


p = nλ eines Ereignisses und für eine große Anzahl n von Wiederholungen
eines Bernoulli-Experiments näherungsweise an, mit welcher Wahrscheinlich-
keit das Ereignis i–mal eintritt. Die Poisson-Verteilung wird deshalb manch-
mal als die Verteilung der seltenen Ereignisse bezeichnet.
Definition A.17. Eine Zufallsvariable X heißt Poisson-verteilt 6 mit Para-
meter λ, λ > 0, wenn X die Werte 0, 1, 2, . . . annimmt und

λ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 .

Die Aussagen 1 und 2 folgen unmittelbar und

Var(X) = E(X 2 ) − E(X)2 = λ2 + λ − λ2 = λ.

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

p(X = i) = p(1 − p)i−1

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

Beispiel. Die Terminierung eines Las-Vegas-Algorithmus hängt oft vom erst-


maligen Eintreten eines Ereignisses ab. Dies wird durch die geometrische
Verteilung bestimmt. Figur A.3 zeigt für Algorithmus 1.50 und für p = 1/2
die Verteilung der Anzahl der Iterationen bis die Abbruchbedingung eintritt.

Fig. A.3: Terminierung eines Las-Vegas-Algorithmus.

Satz A.20. Sei X eine geometrisch verteilte Zufallsvariable mit Parameter


p. Dann gilt:
1 2−p 1−p
E(X) = , E(X 2 ) = 2
und Var(X) = .
p p p2
Beweis. Mit der geometrische Reihe (Anhang B (F.8)) folgt

∑ ∞
p ∑ i
GX (z) = p(1 − p)i−1 z i = ((1 − p)z)
i=1
1 − p i=1
( )
p 1
= · −1 ,
1−p 1 − (1 − p)z
p 2p(1 − p)
G′X (z) = und G′′X (z) = .
(1 − (1 − p)z) 2 (1 − (1 − p)z)3
Die Aussagen 1 und 2 folgen unmittelbar und
2(1 − p) 1 1 1−p
Var(X) = E(X 2 ) − E(X)2 = 2
+ − 2 = .
p p p p2
2
Definition A.21. Eine Zufallsvariable X heißt negativ binomialverteilt mit
Parameter (r, p), r > 0, 0 < p < 1, wenn X die Werte r, r + 1, . . . annimmt
und ( )
k−1
p(X = k) = pr (1 − p)k−r
r−1
∑ ( )r
p
gilt (beachte k≥r p(X = k) = GX (1) = 1−(1−p) = 1 (siehe unten)).
330 A. Wahrscheinlichkeitsrechnung

Bemerkung. Sei k = r + i. Die Identität


( ) ( ) ( ) ( )
k−1 r+i−1 r+i−1 i −r
= = = (−1)
r−1 r−1 i i

erklärt die Bezeichnung negative Binomialverteilung“ (Lemma B.18).



Wir betrachten ein Bernoulli-Experiment mit Ereignis E und Erfolgswahr-
scheinlichkeit p. Sei X die Zufallsvariable, welche zählt, wie oft wir das Ex-
periment wiederholt
( )müssen, bis das Ereignis
( E r mal eintritt. Wir erhalten
)
p(X = k) = p k−1 r−1 p r−1
(1 − p) k−r
= r−1 p (1 − p)
k−1 r k−r
. Folglich ist X
negativ binomialverteilt mit Parameter (r, p).
Die negative Binomialverteilung mit Parameter (1, p) ergibt die geometri-
sche Verteilung. Umgekehrt erhalten wir die negative Binomialverteilung mit
Parameter (r, p) als Summe von r geometrisch verteilten Zufallsvariablen mit
Parameter p.
Beispiel. Die Anzahl der F –leichten Kanten in einem Zufallsgraphen mit r
Knoten wird durch eine negativ binomialverteilte Zufallsvariable mit den
Parametern (r, 1/2) majorisiert (Satz 6.48). Figur A.4 zeigt die negative Bino-
mialverteilung für r = 50 und p = 1/2.

Fig. A.4: Verteilung einer oberen Schranke der F –leichten Kanten.

Satz A.22. Sei X eine negativ binomialverteilte Zufallsvariable mit Parame-


ter (r, p). Dann gilt:

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

Multiplikation mit (pz)r ergibt


∞ (
∑ ) ( )r
k−1 r k−r k pz
GX (z) = p q z = .
r−1 1 − qz
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

Definition A.23. Eine Zufallsvariable X heißt hypergeometrisch verteilt mit


den Parametern (n, M, N ), n, M ≤ N , wenn X die Werte 0, 1, 2, . . . , M an-
nimmt und ( )( )
M N −M
k n−k
p(X = k) = ( )
N
n

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).

Fig. A.5: Verteilung bei der Quicksort-Zerlegung.

Satz A.24. Sei X eine hypergeometrisch verteilte Zufallsvariable mit den


Parametern (n, M, N ). Dann gilt:
( )
M M M N −n
E(X) = n und Var(X) = n 1− .
N N N N −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

k − 1 vorangehenden Ziehungen r − 1 viele Kugeln das Merkmal M aufweisen.


Wir bezeichnen das letzte Ereignis mit E. Dann gilt p(X = k) ist die beding-
te Wahrscheinlichkeit dafür, dass wir im k–ten Zug eine Kugel mit Merkmal
M unter der Bedingung E ziehen. Wir erhalten für k = r, . . . , r + N − M
( )( )
M N −M
r−1 k−r M − (r − 1)
p(X = k) = ( ) .
N N − (k − 1)
k−1

Eine einfache Rechnung ergibt die Übereinstimmung mit der definierenden


Formel in Definition A.25.
Da p(X = k) eine (bedingte)
∑ Wahrscheinlichkeitsverteilung definiert, gilt
die Normierungsbedingung k p(X = k) = 1, d. h.

r+N −M ( )( ) ( )
k−1 N −k N
= .
r−1 M −r M
k=r

Satz A.26. Sei X eine negativ hypergeometrisch verteilte Zufallsvariable mit


den Parametern (r, M, N ). Dann gilt:
N +1 (N + 1)(N − M )(M + 1 − r)
E(X) = r und Var(X) = r .
M +1 (M + 1)2 (M + 2)
Beweis. Die Erzeugendenfunktion für X ist

r+N −M ( )( )
1 k−1 N −k
GX (z) = ( ) zk .
N r−1 M −r
M k=r

Die Ableitung der Erzeugendenfunktion GX (z) ist



r+N −M ( )( )
′ 1 k−1 N −k
GX (z) = ( ) k z k−1
N r−1 M −r
M k=r


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

Für die Ableitung G′X im Punkt 1 folgt


r+1+(N +1)−(M +1) ( )( )
r ∑ k−1 N +1−k
G′X (1) = ( )
N r M + 1 − (r + 1)
M k=r+1
( )
N +1
r N +1
= ( ) = r .
N M +1 M +1
M
A.2 Spezielle diskrete Verteilungen 335

Die Formel für den Erwartungswert von X ist somit gezeigt.

Die zweite Ableitung der Erzeugendenfunktion GX (z) ist


−M +1
r+N∑ ( )( )
r k−1 N − (k − 1)
G′′X (z) = ( ) (k − 2) z k−3 .
N r M −r
M k=r+1

Für G′′X (1) folgt


( r+N∑
−M +1 ( )( )
r k−1 N +1−k
G′′X (1) = ( ) k
N r M −m
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

Für die Varianz von X folgt


( )
N +1 N +2 N +1
Var(X) = r (r + 1) −1−r
M +1 M +2 M +1
(N + 1)(N − M )(M + 1 − r)
=r .
(M + 1)2 (M + 2)

Die Behauptung des Satzes ist daher gezeigt. 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

Fig. A.6: Verteilung beim uniformen Sondieren.

Bemerkung. Für N → ∞ und p = M/N konstant konvergiert die negative


hypergeometrische Verteilung gegen die negative Binomialverteilung:
( )( )
k−1 N −k ( )
r−1 M −r k−1
lim ( ) = pr (1 − p)k−r .
N →∞
N/M konst.
N r − 1
M

Die Wahrscheinlichkeitsrechnung, insbesondere die Binomialverteilung,


die Poisson-Verteilung und die negative hypergeometrische Verteilung wen-
den wir bei der Analyse von Hashverfahren im Abschnitt 3.4, bei Zufalls-
funktionen (Satz 3.19), dem Modell des uniformen Sondieren (Satz 3.23) und
bei universellen Familien von Hashfunktionen (Corollar 3.21) an. Die Bino-
mialverteilung benutzen wir auch bei der Bestimmung der Endpunkte von
Irrfahrten (Abschnitt 1.6). Mithilfe der geometrischen Verteilung ermitteln
wir den Erwartungswert der Terminierung von Las-Vegas-Algorithmen (Al-
gorithmus 1.50). Die hypergeometrische Verteilung kommt bei der Analyse
von Quicksort (Abschnitt 2.1.1) und die negative Binomialverteilung bei der
Analyse von Zufallsgraphen (Satz 6.48) zum Einsatz.
B. Mathematische Begriffe und nützliche
Formeln

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.

Natürliche Zahlen. Wir bezeichnen mit N = {1, 2, . . .} die Menge der


natürlichen Zahlen und mit N0 = {0, 1, 2, . . .} die Menge der natürlichen
Zahlen mit der 0. Für natürliche Zahlen gilt der folgende
Satz B.1 (Division mit Rest). Für alle z, a ∈ N0 , a ̸= 0, gibt es eindeutig
bestimmte Zahlen q, r ∈ N0 mit: z = q · a + r, 0 ≤ r < a.
Beweis. Die Existenz folgt durch Induktion nach z: Für 0 ≤ z < a ist
z = 0 · a + z die gewünschte Darstellung. Sei z ≥ a. Dann folgt aus der
Induktionshypothese die Darstellung für z − a = q · a + r, 0 ≤ r < a. Wir
erhalten z = (q + 1) · a + r.
Um die Eindeutigkeit zu zeigen, nehmen wir an, dass z zwei Darstellungen

z = q 1 · a + r1 = q 2 · a + r 2

besitzt. Dann gilt 0 = (q1 − q2 ) · a + (r1 − r2 ). Somit teilt a die Zahl r1 − r2 .


Da |r1 − r2 | < a gilt, folgt r1 = r2 und q1 = q2 . 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

Die Darstellung heißt b–adische Entwicklung von z mit den Koeffizienten zi


zur Basis b.
© 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
338 B. Mathematische Begriffe und nützliche Formeln

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

eine Darstellung der 0. Dann gilt z mod b = z0 = 0 und z/b mod b = z1 = 0


und so weiter. Insgesamt folgt zi = 0, i = 0, . . . , n − 1, d. h. die Darstellung
der 0 und damit auch die Darstellung von z ist eindeutig. 2
∑n−1
Hat z die Darstellung z = i=0 zi bi , so schreiben wir
z = (zn−1 . . . z0 )b oder einfach z = zn−1 . . . z0 ,
falls die Basis b aus dem Kontext klar ist.
Lemma B.3. Der maximale Wert einer b–adischen Zahl mit n Stellen ist
bn −1. Die Anzahl der Stellen der b–adischen Entwicklung von z ist ⌊logb (z)⌋+
1.
Beweis. Der maximale Wert zmax einer b–adischen Zahl mit n Stellen

n−1 ∑
n−1 ∑
n ∑
n−1
zmax = (b − 1)bi = (b − 1) bi = bi − bi = bn − 1.
i=0 i=0 i=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

folgt die Behauptung. 2


Bemerkung. Genauer gilt:
1 1 1 1
(F.1) Hn = ln(n) + γ + − + − ε, 0 < ε < .
2n 12n2 120n4 252n6
Dabei ist γ = 0, 5772156649 . . . die Eulersche Konstante 1 (siehe zum Beispiel
[Knuth97, Seite 75]).
∑n
Bemerkung. Wegen der Formel (F.1) betrachten wir Hn := i=1 1i , obwohl
eine Summe auftritt, als geschlossene Formel.
Lemma B.6. Eine Formel für die Summe der ersten n harmonischen Zah-
len:
∑ n
Hi = (n + 1)Hn − n.
i=1

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.

Definition B.9. Sei x ∈ Zn . x heißt Einheit in Zn , wenn es ein y ∈ Zn gibt


mit xy = [1].
Die Einheiten in Zn bilden mit der Multiplikation eine Gruppe.
Satz B.10. Sei [x] ∈ Zn . [x] ist genau dann eine Einheit in Zn , wenn x und
n teilerfremd sind, d. h. der größte gemeinsame Teiler von x und n ist 1.
Beweis. Siehe zum Beispiel [DelfsKnebl15, Proposition A.17]. 2
Definition B.11. Die Gruppe der Einheiten in Zn heißt prime Restklassen-
gruppe modulo n und wird mit Z∗n bezeichnet.
Corollar B.12. Für eine Primzahl p ist Zp ein Körper.
Beweis. Ein kommutativer Ring mit 1 ist genau dann ein Körper, wenn jedes
Element ̸= 0 eine Einheit ist. Da für eine Primzahl n = p alle Zahlen 1, . . . , p−
1 zu p teilerfremd sind, folgt die Behauptung aus Satz B.10. 2
Bemerkung. Den Körper Zp bezeichnen wir auch mit Fp (field ist die englische
Bezeichnung für einen Körper). Eine andere Bezeichnung für einen endlichen
Körper ist Galois2 -Feld.
Quadratische Reste in Zn . Quadratische Reste wenden wir bei Hashver-
fahren an (Kapitel 3, Abschnitt 3.3.2).
Definition B.13. Eine Zahl j ∈ {0, . . . , n − 1} heißt Quadratzahl modulo n,
wenn es ein i ∈ N gibt mit j ≡ i2 mod n. Ist j eine Quadratzahl modulo n,
so heißt [j] ein Quadrat in Zn .
2
Évariste Galois (1811 – 1832) war ein französischer Mathematiker. Er ist für seine
Arbeiten zur Lösung algebraischer Gleichungen, der sogenannten Galoistheorie
berühmt, die aus heutiger Sicht Körpererweiterungen untersucht.
B. Mathematische Begriffe und nützliche Formeln 341

Beispiel. Wir identifizieren die Quadrate in Z∗11 und Z∗13 .


1. [1], [3], [4], [9], [10], [12] sind alle Quadrate in Z∗13 . −1 ≡ 12 mod 13, [−1]
ist somit ein Quadrat und auch [−3], [−4], [−9], [−10], [−12] sind Quadra-
te in Z13 .
2. [1], [3], [4], [5], [9] sind alle Quadrate in Z∗11 . 2 (≡ −9), 6 (≡ −5), 7 (≡
−4), 8 (≡ −3), 10 (≡ −1) modulo 11 sind keine Quadratzahlen modulo
11.
Während im ersten Fall die negativen Quadratzahlen wieder Repräsentanten
von Quadraten sind, tritt dies im zweiten Fall nicht ein.

Die Frage, ob bei quadratischem Sondieren (Definition 3.11) alle freien


Plätze in einer Sondierfolge auftreten, hängt eng damit zusammen, alle Qua-
drate und die negativen Quadrate und deren Anzahlen zu identifizieren. Dies
leistet der folgende
Satz B.14. Sei p > 2 eine Primzahl. Dann gilt:
1. Die Zahlen i2 mod p, i = 1, . . . , p−1
2 , sind modulo p paarweise verschie-
den. Jedes Quadrat in Zp besitzt einen Repräsentanten von dieser Form.
2. Sei p = 4k + 3, k ∈ N0 . Dann ist [-1] kein Quadrat in Zp .
Beweis. 1. Sei 0 ≤ i < j ≤ p−1 2 2
2 . Falls i mod p = j mod p ist, teilt p die
Zahl j − i = (j − i)(j + i). Dann teilt p auch j − i oder j + i. Dies ist
2 2

ein Widerspruch, da j − i < p − 1 und j + i < p − 1. Folglich sind die Zah-


2 , modulo p paarweise verschieden. Sei y ∈ N,
len i2 mod p, i = 1, . . . , p−1
y = kp + x, 0 ≤ x ≤ p − 1. Dann ist y 2 = (kp)2 + 2kpx + x2 ≡ x2 mod p und
(p − x)2 = p2 − 2px + x2 ≡ x2 mod p. Deshalb sind alle Quadrate von der
gewünschten Form.
2. Für p ≡ 3 mod 4 ist p−1 2 ungerade. Angenommen, [−1] wäre ein Qua-
drat, d. h. −1 ≡ n2 mod p, dann folgt mit dem kleinen Satz von Fermat3
−1 ≡ (−1)(p−1)/2 ≡ np−1 ≡ 1 mod p. Somit gilt 2 ≡ 0 mod p. Ein Wider-
spruch. 2

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}.

Beweis. Die Elemente aus {[i2 ] ∈ Zp | i = 1, . . . , (p − 1)/2} sind paarweise


verschieden und Quadrate. Da das inverse Element eines Quadrats und das
Produkt aus zwei Quadraten wieder ein Quadrat ist, sind, falls [−1] kein Qua-
drat ist, die negativen von Quadraten keine Quadrate, mit anderen Worten
3
Pierre de Fermat (1607 – 1665) war ein französischer Mathematiker. Kleiner
Satz von Fermat: Für eine Primzahl p und eine zu p teilerfremde Zahl n gilt
np−1 ≡ 1 mod p.
342 B. Mathematische Begriffe und nützliche Formeln

die Elemente {[−i2 ] | i = 1, . . . , p−1


2 } sind paarweise verschieden und keine
Quadrate. Daher ist die Aussage des Corollars gezeigt. 2
Bemerkung. Die Bedingung p = 4k + 3, k ∈ N0 ist sogar äquivalent dazu,
dass [-1] kein Quadrat in Zp ist.4 Bei Verwendung von Primzahlen p, die die
Bedingung nicht erfüllen, werden somit bei quadratischem Sondieren nicht
alle Tabellenplätze sondiert.
Endliche Summen und Partialbruchzerlegung. Wir stellen zunächst
Formeln für spezielle endliche Folgen zusammen.


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.

Die Summenbildung von rationalen Funktionen erfolgt analog zur Integra-


tion von rationalen Funktionen mithilfe der Partialbruchzerlegung. Division
mit Rest liefert für eine rationale Funktion f (n) = p(n)
q(n) die Darstellung

r(n)
f (n) = s(n) + mit Polynomen r(n) und s(n) und deg(r) < deg(q).
q(n)

Für den Teil r(n)


q(n) führen wir die Partialbruchzerlegung durch. Dazu unter-
scheiden wir, ob die Nullstellen des Nennerpolynoms q(n) alle verschieden
sind oder ob es mehrfache Nullstellen gibt.
∏l
1. Einfache Nullstellen: q(n) = k=1 (n − nk ), ni ̸= nj , i ̸= j.
Bei verschieden Nullstellen gibt es eine Zerlegung in der Form

r(n) ∑ ak
l
(F.2) = .
q(n) n − nk
k=1

Die Koeffizienten ∑ak bestimmen wir durch einen Koeffizientenvergleich.


n 1 1
Soll zum Beispiel i=2 i(i−1) berechnet werden, so können wir i(i−1) als

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.

Ein Koeffizientenvergleich liefert −a = 1 und a+b = 0. Also a = −1, b = 1


und damit haben wir die Partialbruchzerlegung
1 −1 1
= + .
i(i − 1) i i−1

Als Ergebnis ergibt sich dann


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

2. Mehrfache Nullstellen: Ist nj eine l–fache Nullstelle, so müssen wir für


nj bei der Partialbruchzerlegung l Brüche folgendermaßen ansetzen:
a1 a2 al
+ + ... + .
n − nj (n − nj ) 2 (n − nj )l
∑n 1
Soll zum Beispiel i=1 i2 (i+1) berechnet werden, so machen wir folgen-
den Ansatz:
1 a b c
2
= + 2+ .
i (i + 1) i i i+1
Die ersten beiden Brüche stammen dabei von der doppelten Nullstelle.
Ein Koeffizientenvergleich liefert a = −1, b = 1, c = 1 und damit
1 −1 1 1
= + 2+ .
i2 (i + 1) i i i+1

Endliche Summe von Logarithmen. Wir geben eine geschlossene Formel


für eine endliche Summe von Logarithmen an.
Lemma B.16.

n ( )
⌊log2 (r)⌋ = (n + 1)⌊log2 (n)⌋ − 2 2⌊log2 (n)⌋ − 1 .
r=1

Beweis. Induktion nach n: Für n = 1 ergeben beide Seiten der Gleichung 0.


Der Induktionsanfang ist aus diesem Grund richtig. Wir schließen jetzt von
n auf n + 1. Für ⌊log2 (n + 1)⌋ = ⌊log2 (n)⌋ folgt


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

Im Fall ⌊log2 (n + 1)⌋ = ⌊log2 (n)⌋ + 1 ist (n + 1) eine Potenz von 2, d. h.


n + 1 = 2⌊log2 (n+1)⌋ und

n+1 ( )
⌊log2 (r)⌋ = (n + 1)⌊log2 (n)⌋ − 2 2⌊log2 (n)⌋ − 1 + ⌊log2 (n + 1)⌋
r=1
= (n + 1)(⌊log2 (n + 1)⌋ − 1)
( )
−2 2⌊log2 (n+1)⌋−1 − 1 + ⌊log2 (n + 1)⌋
( )
= (n + 2)⌊log2 (n + 1)⌋ − (n + 1) − 2 2⌊log2 (n+1)⌋ 2−1 − 1
= (n + 2)⌊log2 (n + 1)⌋ − 2⌊log2 (n+1)⌋ − 2⌊log2 (n+1)⌋ + 2
( )
= (n + 2)⌊log2 (n + 1)⌋ − 2 2⌊log2 (n+1)⌋ − 1 .

Das Lemma ist somit bewiesen. 2

Binomial-Koeffizienten. Wählt man aus n Elementen ohne ( )Zurücklegen k


Elemente ohne Berücksichtigung der Reihenfolge, so gibt es nk viele Möglich-
keiten dies zu tun.
Definition B.17. Sei n ∈ R, k ∈ Z. Wir setzen
{
(n) n·(n−1)·...·(n−k+1)
,k≥0
k·(k−1)·...·1
=
k 0, k < 0.
(n)
k heißt Binomial-Koeffizient und wird als ”n über k“ gesprochen.

Die Binomial-Koeffizienten treten bei der Darstellung von (x + y)n als


Polynom in x und y auf. Nach dem binomischen Lehrsatz gilt
∑n ( )
n k n−k
(F.3) (x + y)n = x y .
k
k=0
∑∞ ( )
Sei α ∈ R. Die binomische Reihe k=0 αk xk konvergiert für |x| < 1 und
es gilt
∑∞ ( )
α α k
(F.4) (1 + x) = x .
k
k=0

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

Lemma B.18. Wir stellen Formeln für die Binomial-Koeffizienten auf.


1. Seien n, k ∈ N, n < k. Dann gilt
(n)
= 0.
k
2. Sei n ∈ R, k ∈ Z. Dann gilt
(n) ( ) ( )
n n+1
+ = .
k k+1 k+1

3. Seien r, k ∈ N, r > k > 1. Dann gilt


(r) r (r − 1)
= .
k k k−1

4. Seien r, s ∈ N, n ∈ Z. Dann gilt


∑ ( r ) ( s ) (r + s)
= .
k n−k n
k

5. Seien r, s ∈ N, n ∈ Z. Dann gilt


∑(r)( s ) (r + s)
= .
k n+k r+n
k

6. Seien r, s ∈ N, m = min(r, s). Dann gilt


m (r)(s) ( )
r+s−1
k =s .
k k r−1
k=1

7. Seien k, n ∈ Z. Dann gilt


( ) ( )
n+k−1 k −n
= (−1) .
k k

8. Seien n, m ∈ N, m < n. Dann gilt


n (
∑ ) ( )
k n+1
= .
m m+1
k=0

Beweis.
1. Die Behauptung folgt unmittelbar aus der Definition der Binomial-
Koeffizienten.
346 B. Mathematische Begriffe und nützliche Formeln

2. Für k < 0 folgt die Behauptung unmittelbar. Sei k ≥ 0.


( n ) ( n ) n · (n − 1) · . . . · (n − k + 1) n · (n − 1) · . . . · (n − k)
+ = +
k k+1 k · (k − 1) · . . . · 1 (k + 1) · k · . . . · 1
n · (n − 1) · . . . · (n − k + 1)(k + 1 + n − k)
=
(k + 1) · k · . . . · 1
(n + 1) · n · (n − 1) · . . . · (n − k + 1)
=
(k + 1) · k · . . . · 1
( )
n+1
= .
k+1
3. (r) ( )
r! r (r − 1)! r r−1
= = = .
k k!(r − k)! k (k − 1)!(r − k)! k k−1
4. Die Binomialentwicklung von (x + 1)r (x + 1)s = (x + 1)r+s ergibt
∑(r) ∑(s) ∑∑(r)( s ) ∑ (r + s)
xn xn = xn = xn .
n
n n
n n
k n − k n
n
k

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

6. Mit 3 und 4 folgt


∑ (r)(s) ∑(r)(s − 1) (
r+s−1
)
k =s =s .
k k k k−1 r−1
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

(siehe Punkt 2).


B. Mathematische Begriffe und nützliche Formeln 347

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

Wir differenzieren die n–te Partialsumme und erhalten



n
nxn+1 − (n + 1)xn + 1
(F.6) ixi−1 = .
i=0
(x − 1)2

Multiplizieren wir die Gleichung mit x, so erhalten wir



n
nxn+2 − (n + 1)xn+1 + x
(F.7) ixi = .
i=0
(x − 1)2

Wir differenzieren die letzte Gleichung und erhalten



n
n2 xn+2 − (2n2 + 2n − 1)xn+1 − (n + 1)2 xn − x − 1
i2 xi−1 = .
i=0
(x − 1)3

Multiplizieren wir die Gleichung mit x, so erhalten wir



n
n2 xn+3 − (2n2 + 2n − 1)xn+2 − (n + 1)2 xn+1 − x2 − x
i2 xi = .
i=0
(x − 1)3

Aus den obigen Formeln folgt, dass für |x| < 1



∑ ∞
∑ ∑∞
i 1 i−1 1 x
(F.8) x = , ix = und ixi =
i=0
1 − x i=0 (1 − x) 2
i=0
(1 − x)2

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

streng monoton wachsend gegen ex .


348 B. Mathematische Begriffe und nützliche Formeln

(( )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

Corollar B.20. Für alle x ∈ R gilt 1 − x ≤ e−x .


Beweis. Für x ≥ 1 gilt 1 − x ≤ 0 < e−x und für x < 1 gilt 1 − x ≤ 1 − x/2 ≤
(1 − x/2)2 ≤ e−x . Die letzte Abschätzung folgt mit Satz B.19. 2

Die Jensensche Ungleichung. Die Jensensche7 Ungleichung ist eine ele-


mentare Ungleichung für konvexe und konkave Funktionen.
Definition B.21. Sei f : I −→ R, I ein Intervall. f heißt konkav , wenn es
eine Abbildung λ : I −→ R gibt, sodass gilt:
f (x) ≤ f (x0 ) + λ(x0 )(x − x0 ), für alle x, x0 ∈ I.
Lemma B.22. Sei f : I −→ R eine zweimal stetig differenzierbare Funktion
und f ′′ < 0. Dann ist f konkav.
Beweis. Wir entwickeln f im Punkt x0 nach der Taylorformel:
f (x) = f (x0 ) + f ′ (x0 )(x − x0 ) + R1 (x)
mit dem Lagrangeschen8 Restglied
(x − x0 )2 ′′
R1 (x) = f (ξ),
2!
wobei ξ zwischen x und x0 liegt. Da f ′′ < 0 gilt, folgt die Behauptung. 2
7
Johan Ludwig Jensen (1859 - 1925) war ein dänischer Mathematiker.
8
Joseph-Louis de Lagrange (1736 - 1813) war ein italienischer Mathematiker und
Astronom. Er ist unter anderem für den Lagrange-Formalismus aus der klassi-
schen Mechanik berühmt.
B. Mathematische Begriffe und nützliche Formeln 349

Lemma B.23 (Jensensche Ungleichung). Sei f ∑ : I −→ R eine konkave Funk-


n
tion, a1 , . . . , an ∈ R, ai > 0, i = 1, . . . n, und i=1 ai = 1. Dann gilt für
x1 , . . . , x n ∈ I ( n )
∑n ∑
ai f (xi ) ≤ f ai xi .
i=1 i=1
∑n
Beweis. Setze x0 = i=1 ai xi . x0 ∈ I. Da f konkav ist, gilt
ai f (xi ) ≤ ai f (x0 ) + ai λ(x0 )(xi − x0 ), i = 1, . . . , n. Hieraus folgt


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

Dies zeigt die Behauptung. 2


Transformation zur Lösung von Rekursionsgleichungen. Es gibt Re-
kursionsgleichungen, die sich durch eine Variablentransformation in eine Dif-
ferenzengleichung transformieren lassen (Bemerkung nach Corollar 1.27, Be-
weis von Satz 2.31 und von Satz 5.31). Aus einer geschlossenen Lösung der
Differenzengleichung können wir dann, falls wir die Lösung auf R≥0 fortsetzen
können, eine geschlossene Lösung der Rekursionsgleichung durch Anwendung
der inversen Transformation berechnen. In der Anwendung ist die Lösung
meist durch Funktionen definiert, deren Definitionsbereich aus den positiven
reellen Zahlen besteht und auf N eingeschränkt ist. Daher ist in diesen Fällen
die Fortsetzung auf kanonische Weise gegeben.

Sei f : N × R≥0 −→ R≥0 eine Funktion und

yk = y(k) = f (k, y(k − 1)) für k > 1, y(1) = b,


eine Differenzengleichung erster Ordnung.9 . Sei Ly eine geschlossene Lösung
für y(k), d. h. y(k) = Ly (k) für k ∈ N. Die Funktion Ly besitze eine Fortset-
zung auf R≥0 . Wir bezeichnen die Fortsetzung wieder mit Ly .

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)

für n ∈ N mit n ≥ t(n0 ). 2


Bemerkung. Die Formel für Lx hängt von der Wahl der Fortsetzung Ly ab.
Unabhängig von der Wahl von Ly gilt x(t(n)) = Ly (n) für n ∈ N. Die Aussage
über die Ordnung von Lx hängt nicht von der Wahl der Fortsetzung Ly ab.
Metrische Räume. In einem euklidischen Vektorraum, wie zum Beispiel
dem Rn , kann der Abstand von zwei Punkten mithilfe des Skalarprodukts
und des Satzes von Pythagoras10 berechnet werden. Die Abstandsfunktion,
die dadurch definiert ist, hat Werte in der Menge der positiven reellen Zahlen,
sie ist symmetrisch und es gilt die Dreiecksungleichung. Diese besagt für drei
Punkte in der Ebene, die ein Dreieck bilden, dass die Summe der Längen
von zwei Seiten des Dreiecks stets größer der Länge der dritten Seite des
Dreiecks ist. Nimmt man diese Eigenschaften als Axiome einer Abbildung
auf X × X, für eine Menge X, so erhalten wir die Definition einer Metrik und
eines metrischen Raumes.
Definition B.25 (Metrischer Raum). Sei X eine beliebige Menge. Eine Ab-
bildung
d : X × X −→ R
heißt Metrik oder Abstandsfunktion auf X, wenn für beliebige Elemente x, y
und z ∈ X die folgenden Axiome erfüllt sind:
1. d(x, y) ≥ 0 und d(x, y) = 0 genau dann, wenn x = y gilt
(positiv definit).
2. d(x, y) = d(y, x) (Symmetrie).
3. d(x, y) ≤ d(x, z) + d(z, y) (Dreiecksungleichung).
X heißt metrischer Raum, wenn auf X eine Metrik definiert ist.
Beispiele für (endliche) metrische Räume sind zusammenhängende Gra-
phen (Kapitel 5 und 6). Umgekehrt besitzt jeder endliche metrische Raum
eine Darstellung durch einen positiv gewichteten Graphen.
10
Pythagoras von Samos (um 570 v. Chr. – nach 510 v. Chr.) war ein antiker
griechischer Philosoph. Es fehlen verlässliche Quellen zu seiner Person.
Literatur

Lehrbücher

[AhoHopUll83] A. V. Aho, J. E. Hopcroft, J. D. Ullman: Data Structures and


Algorithms. Reading, MA: Addison-Wesley Publishing Company, 1983.
[AhoHopUll74] A. V. Aho, J. E. Hopcroft, J. D. Ullman: The Design and Analysis
of Computer Algorithms. Reading, MA: Addison-Wesley Publishing Company,
1974.
[AmannEscher02] H. Amann, J. Escher: Analysis 1, 3. Aufl. Basel, Boston, Berlin:
Birkhäuser Verlag, 2002.
[Backhouse86] R. C. Backhouse: Program Construction and Verification. Engle-
wood Cliffs, New Jersey: Prentice Hall, 1986.
[Bellman57] R. Bellman: Dynamic Programming. Princeton, NJ: Princeton Univer-
sity Press, 1957.
[Bosch06] K. Bosch: Elementare Einführung in die Wahrscheinlichkeitsrechnung. 9.
Aufl. Wiesbaden: Friedrich Vieweg & Sohn Verlag, 2006.
[CorLeiRiv89] T. H. Cormen, C. E. Leiserson, R. L. Rivest: Introduction to Algo-
rithms. Cambridge, London: The MIT Press, 1989.
[CorLeiRivSte07] T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein: Algorith-
men - Eine Einführung, 2. Aufl. München, Wien: Oldenbourg Verlag, 2007.
[DelfsKnebl15] H. Delfs, H. Knebl: Introduction to Cryptography, 3rd ed. Berlin,
Heidelberg, New York: Springer-Verlag, 2015.
[DieMehSan15] M. Dietzfelbinger, K. Mehlhorn, P. Sanders: Algorithmen und Da-
tenstrukturen. Berlin, Heidelberg, New York: Springer-Verlag, 2014.
[Elaydi03] S. Elaydi: An Introduction to Difference Equations. Berlin, Heidelberg,
New York: Springer-Verlag, 2003.
[Feller68] W. Feller: An Introduction to Probability Theory and its Applications.
3rd ed. New York: John Wiley & Sons, 1968.
[Fischer14] G. Fischer: Lineare Algebra. 18. Aufl. Wiesbaden: Springer Spektrum,
2014.
[GarJoh79] M. R. Garey, D. S. Johnson: Computers and Intractability: A Guide to
the Theory of NP-Completeness. San Francisco: W. H. Freeman, 1979.
[Gould88] R. Gould: Graph Theory. Menlo Park, California: The Benja-
min/Cummings Publishing Company, 1988.
[GraKnuPat94] R. L. Graham, D. E. Knuth, O. Patashnik: Concrete Mathematics,
2nd ed. Reading, MA: Addison-Wesley Publishing Company, 1994.
[Gries81] D. Gries: The Science of Programming. Berlin, Heidelberg, New York:
Springer-Verlag, 1981.
[HanHarJoh98] D. Hankerson, G. Harris, P. Johnson, Jr.: Introduction to Infor-
mation Theory and Data Compression. Boca Raton, Boston, New York: CRC
Press, 1998.
[Havil07] J. Havil: Gamma. Berlin, Heidelberg, New York: Springer-Verlag, 2007.
[Herrmann16] D. Herrmann: Mathematik im Mittelalter. Berlin, Heidelberg:
Springer-Verlag, 2016.

© 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

[HopMotUll07] J. Hopcroft, R. Motwani, J. Ullman: Introduction to Automata


Theory, Languages, and Computation, 3rd ed. Reading, MA: Addison-Wesley
Publishing Company, 2007.
[Hromkovič04] J. Hromkovič: Randomisierte Algorithmen. Stuttgart: B. G. Teub-
ner, 2004.
[Hromkovič14] J. Hromkovič: Theoretische Informatik, 5. Aufl. Wiesbaden: Sprin-
ger Vieweg, 2014.
[Jungnickel13] D. Jungnickel: Graphs, Networks and Algorithms, 4th ed. Berlin,
Heidelberg, New York: Springer-Verlag, 2013.
[Kao16] M. Kao (ed.): Encyclopedia of Algorithms. New York: Springer Science
and Business Media, 2016.
[KelPisPfe04] H. Kellerer, D. Pisinger, U. Pferschy: Knapsack problems. Berlin,
Heidelberg, New York: Springer-Verlag, 2004.
[KerRit78] B. W. Kernighan, D. M. Ritchie: The C Programming Language. Engle-
wood Cliffs, New Jersey: Prentice Hall, 1978.
[KelPet91] W. G. Kelley, A. C. Peterson: Difference Equations. San Diego: Acade-
mic Press, 1991.
[Knebl20] H. Knebl: Algorithms and Data Structures. Cham: Springer Nature Swit-
zerland AG, 2020.
[Knuth97] D. E. Knuth: The Art of Computer Programming, Volume 1/ Funda-
mental Algorithms. Reading, MA: Addison-Wesley Publishing Company, 1998.
[Knuth98] D. E. Knuth: The Art of Computer Programming, Volume 2/ Seminu-
merical Algorithms. Reading, MA: Addison-Wesley Publishing Company, 1998.
[Knuth98a] D. E. Knuth: The Art of Computer Programming, Volume 3/ Sorting
and Searching. Reading, MA: Addison-Wesley Publishing Company, 1998.
[Knuth11] D. E. Knuth: The Art of Computer Programming, Volume 4A/ Combi-
natorial Algorithms Part 1. Boston: Pearson Education, Inc., 2011.
[MartToth90] S. Martello, P. Toth P: Knapsack Problems: Algorithms and Com-
puter Implementations. Chichester: Wiley, 1990.
[MotRag95] R. Motwani, P. Raghavan: Randomized Algorithms. Cambridge: Cam-
bridge University Press, 1995.
[RemUll08] R. Remmert, P. Ullrich: Elementare Zahlentheorie, 3. Aufl. Basel:
Birkhäuser Verlag, 2008.
[Sedgewick88] R. Sedgewick: Algorithms, 2nd ed. Reading, MA: Addison-Wesley
Publishing Company, 1988.
[SedWay11] R. Sedgewick, K. Wayne: Algorithms, 4th ed. Reading, MA: Addison-
Wesley Publishing Company, 2011.
[Schrijver03] A. Schrijver: Combinatorial Optimization. Berlin, Heidelberg, New
York: Springer-Verlag, 2003..
[Wirth83] N. Wirth: Algorithmen und Datenstrukturen. Stuttgart: B. G. Teubner,
1983.
[Wirth83a] N. Wirth: Systematisches Programmieren. Stuttgart: B. G. Teubner,
1983.

Zeitschriften-Artikel

[Ackermann28] W. Ackermann: Zum Hilbertschen Aufbau der reellen Zahlen.


Math. Ann. 99: 118-133, 1928.
[AdeLan62] G. M. Adel’son-Vel’skiĭ, E. M. Landis: An algorithm for the organiza-
tion of information. Doklady Akademia Nauk USSR 146: 263 – 266, 1962 (engl.
Übersetzung in Soviet Math. 3: 1259 – 1263, 1962).
[AragSeid89] C. R. Aragon, R. G. Seidel: Randomized search trees. Proceedings
of the 30th Annual IEEE Symposium on Foundations of Computer Science:
540–545, 1989.
Literatur 353

[BayMcC72] R. Bayer, E. McCreight: Organization and maintenance of large orde-


red indices. Acta Informatica, 1: 173–189, 1972.
[BeFa00] M. Bender, M. Farach-Colton: The LCA problem revisited. Theoretical
Informatics. LATIN 2000. Lecture Notes in Computer Science, 1776: 88 – 94,
Springer-Verlag, 2000.
[Borůvka26] O. Borůvka: O jistém problému minimálnı́m. Práca Moravské
Přı́rodove̊decké Společnosti, 3: 37–58, 1926.
[Carlson87] S. Carlson: A variant of Heapsort with almost optimal number of com-
parisons. Information Processing Letters, 24: 247-250, 1987.
[CarWeg79] J. L. Carter, M. N. Wegman: Universal classes of hash functions. Jour-
nal of Computer and System Sciences, 18: 143–154, 1979.
[CopWin90] D. Coppersmith, S. Winograd: Matrix multiplication via arithmetic
progressions. Journal of Symbolic Computation, 9(3): 251–280, 1990.
[Dijkstra59] E. W. Dijkstra: A note on two problems in connexion with graphs.
Numerische Mathematik, 1(1): 269–271, 1959.
[Ďurian86] B. Ďurian: Quicksort without a stack. Proc. Math. Foundations of Com-
puter Science, Lecture Notes in Computer Science, 233: 283 – 289, Springer-
Verlag, 1986.
[EdmoKarp72] J. Edmonds, R. M. Karp: Theoretical improvements in algorithmic
efficiency for network flow problems. Journal of the ACM, 19(2): 248 – 264,
1972.
[Faller73] N. Faller: An adaptive system for data compression. Record of the 7th
Asilomar Conference on Circuits, Systems and Computers (IEEE): 593-597,
1973.
[FordFulk56] L. R. Ford Jr., D. R. Fulkerson: Maximal flow through a network.
Canadian Journal of Mathematics 8: 399-404, 1956.
[FordFulk62] L. R. Ford Jr., D. R. Fulkerson: Flows in Networks. RAND Corpora-
tion Report R-375-PR, 1962.
[Floyd62] R.W. Floyd : Algorithm 97: Shortest path. Communications of the ACM,
5(6): 345, 1962.
[Floyd64] R. W. Floyd: Algorithm 245: Treesort. Communications of the ACM,
7(12): 701, 1964.
[Hoare62] C. A. R. Hoare: Quicksort. Computer Journal, 5: 10–15, 1962.
[HopUll73] J. E. Hopcroft, J. D. Ullman: Set merging algorithms. SIAM Journal
on Computing 2(4): 294–303, 1973.
[Huffman52] D. A. Huffman: A method for the construction of minimum-
redundancy codes. Proceedings of the IRE: 1098–1101, 1952.
[IliPen10] V. Iliopoulos, P. Penman: Variance of the number of comparisons of
randomised Quicksort. http://arXiv.org/abs/1006.4063v1, 2010.
[KarOfm62] A. Karatsuba, Yu. Ofman: Multiplication of multidigit numbers on au-
tomata. Doklady Akademia Nauk USSR 145: 293 – 294, 1962 (engl. Übersetzung
in Soviet Physics Doklady 7: 595 – 596, 1963).
[Karger93] D. R. Karger: Global min-cuts in RNC, and other ramifications of a
simple min-cut algorithm. Proc. 4’th ACM-SIAM SODA: 21-30, 1993.
[KarSte96] D. R. Karger, C. Stein: A new approach to the minimum cut problem.
Journal of the ACM, 43(4): 601-640, 1996.
[KarKleTar95] D. R. Karger, P. N. Klein, R. E. Tarjan: A randomized linear-time
algorithm to find minimum spanning trees. Journal of the ACM, 42(2): 321-328,
1995.
[King97] V. King: A simpler minimum spanning tree verification algorithm. Algo-
rithmica 18: 263–270, 1997.
[Komlós85] J. Komlós: Linear verification for spanning trees, Combinatorica, 5:
57–65, 1985.
354 Literatur

[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

[Queens@TUD-Team16] TU Dresden: News, 2016. https://tu-dresden.de/tu-


dresden/newsportal/news/neuer-weltrekord-fuer-queens-tud-team, 2016.
357

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

Algorithmus – Tarjan, letzter gemeinsamer


– Berechnung der n–ten Fibonacci-Zahl Vorgänger 264
21 – Warshall/Floyd 301
– effizienter Algorithmus 14
– Entwurfsmethoden 32 Backtracking 49
– – Branch-and-Bound mit Back- b-adische Entwicklung
tracking 49 – Koeffizienten 337
– – Divide-and-Conquer 35 – maximaler Wert 338
– – dynamisches Programmieren 41 Baum 135, 223
– – Greedy 37 – ausgeglichener Baum 142
– – Rekursion 33, 76, 246, 299 – – Balancefaktor 146
– Korrektheit 2 – – Einfügen 145
– – Nachbedingung 2 – – Löschen 152
– – Vorbedingung 2 – – Rotationen 146
– Laufzeit, siehe Laufzeit – B-Baum 165
– LZ77 203 – – direkt benachbarte Seiten 172
– LZ78 205 – – Löschen eines Elements 171
– LZW 208 – – Pfadlängen 167
– probabilistische Algorithmen 57 – – Seiten 166
– – binäre Suchbäume 156 – – Suchen und Einfügen 168
– – Las-Vegas-Algorithmus 61 – – Underflow 171
– – Min-Cut-Algorithmus 243 – BFS-Baum 229
– – Monte-Carlo-Algorithmus 61 – binärer Baum 137
– – MST-Algorithmus 296 – – Inorder-Ausgabe 137
– – Quicksort 87 – – Postorder-Ausgabe 138
– – Quickselect 105 – – Preorder-Ausgabe 137
– schnelles Potenzieren 21 – binärer Codebaum 187
Algorithmus von – binärer Suchbaum 138
– Borůvka 283 – – Löschen 140
– Dijkstra 273 – – Suchen und Einfügen 139
– Edmonds-Karp 313 – – symmetrischer Vorgänger 141
– Faller, Gallager und Knuth 186 – Blattknoten 136
– Floyd, maximaler Fluss 303 – Borůvka-Baum 288
– Ford-Fulkerson 308 – Codebaum 176
– Huffman 183 – DFS-Baum 233
– Karger, Klein, Tarjan 296 – digitaler Suchbaum 206
– Kosaraju-Sharir 241 – – Einfügen 206
– Kruskal 281 – – Suchen 206
– Prim 275 – gewichteter Codebaum 187
– Strassen 31 – Höhe eines Knotens 136
– Tarjan, starke Zusammenhangskom- – Höhe und Tiefe des Baumes 137
ponenten 238 – kartesischer Baum 270
© 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
360 Index

– Knoten 136 – – Berechnung des Repräsentanten 195


– leerer Baum 136 – – Codierung der Länge der Nachricht
– letzter gemeinsamer Vorgänger 264 202
– linker (rechter) Nachfolger 137 – – Decodierung 201
– linker (rechter) Teilbaum 137 – – Decodierung mit Reskalierung 201
– Nachfahre, Vorfahre 136 – – Intervallzuordnung 193
– Nachfolger, Vorgänger 136 – – Reskalierung 197
– Pfadlänge 136 – – Underflow-Behandlung 199
– Rot-Schwarz-Baum 212 – Codebaum, siehe Baum
– Sohnknoten 136 – Codierung von X über Y 176
– Teilbäume 136 – eindeutig decodierbar 177
– Tiefe eines Knotens 137 – Huffman-Codes 182
– Tiefensuche 138 – – adaptives Verfahren 186
– Vaterknoten 136 – – Codierung und Decodierung 186
– Verifikation minimaler aufspannender – – Gewichtsliste 192
Bäume 289 – – Huffman-Algorithmus 183
– voll verzweigt 288 – – Huffman-Baum 187
– Vorgängerarray 226 – Informationsgehalt 182
– Wurzelbaum 136 – kompakt oder minimal 183
Bellmansche Optimalitätsgleichung 41 – Kriterium für die eindeutige
binäre Suche, siehe Suchen in Arrays Decodierbarkeit 177
Binomialkoeffizient, siehe Formeln – Lauflängencodierung 67
binomische Reihe, siehe Formeln – Lempel-Ziv-Codes 203
Berechnungsprobleme – mittlere Codewortlänge 182
– 2-SAT-Problem 317 – Nachricht 176
– 8-Damenproblem 50 – Präfixcodes 180
– Abstands-Problem 273, 303 – Präfixcodes für natürliche Zahlen 181
– Chinese-Postman- und Traveling- – – Elias-Delta-Code 182
Salesman-Problem 219 – – Elias-Gamma-Code 181
– Halteproblem 3 – Quelle (ohne Gedächtnis) 182
– House-Utility-Problem 216 – – Entropie 182
– Königsberger-Brückenproblem 215 – Symbol 176
– kürzeste Pfade 273 – unmittelbare Codes, siehe Präfixcodes
– LCA-Problem 264 – verlustlose Codes 177
– minimaler aufspannender Baum – Wort 176
(MST-Problem) 275 – – leere Wort 176
– Pfad-Maximum-Problem 289, 291 – Wörterbuchmethoden 203
– RMQ-Problem 42, 264
– – inkrementelles 268 Datenstrukturen
– Rucksackproblem – Priority-Queue 254
– – 0,1-Rucksackproblem 52 – Queue 229
– – fraktales Rucksackproblem 53 – Treap 158
– – Gewichtsvektor 52 – Union-Find 256
– – Kapazität 52 – – Laufzeitanalyse 260
– – Profitdichte 53 – – nach dem Rang 258
– – Profitvektor 52 – – Pfadkomprimierung 258
– Task-scheduling Problem 37 Differenzengleichungen, siehe lineare
Differenzengleichungen
Catalanzahlen 213
Codes 176 Editierdistanz 46
– Alphabet 176 endliche Summen und Partialbruchzer-
– arithmetische Codes 193 legung 342
– – adaptive arithmetische Codierung Endrekursion 86
202 Entropie, siehe Codes, -Quelle
Index 361

entscheidbar 3 – – inzidente Kante 221


Eulersche Konstante 339 – – Kontraktion 243, 284
Exponentialfunktion 347 – – minimal inzidente Kante 283
– – Querkante 234
Fibonacci-Zahlen 19 – – Rückwärtskante 234
Formeln 337 – – Vorwärtskante 234
– Binomialkoeffizient 343 – Knoten 220
– binomische Reihe 344 – kritischer Pfad 220
– geometrische Reihe 344 – Kreis 222
– – Ableitung Partialsumme 344 – Kreiseigenschaft 296
– – Partialsumme 344 – kürzester Wege-Baum 273
– harmonische Zahl 338 – minimaler aufspannender Baum 275
– Mittelpunkt 316
goldener Schnitt 22 – nicht separierbar 252
Graphen 215, 253 – Ordnung 221
– Abstand von Knoten 222, 273 – Pfad 221
– adjazent 220 – – einfach 222
– Adjazenzliste 226 – – geschlossen 222
– Adjazenzmatrix 226 – – Länge 221, 222, 273
– Artikulationspunkt 252 – priority-first search
– azyklisch 222 – – Liste priority-first search 279
– bipartit 216, 224 – – Matrix priority-first search 278
– Darstellung 225 – Radius 316
– dicht besetzt 226 – reduzierter Graph 251
– dünn besetzt 226 – reverser Graph 241
– Durchmesser 316 – Schnitt 243, 306
– ebene Graphen 217 – – Kapazität eines Schnitts 306
– elementare Graphalgorithmen 227 – – minimaler 243
– – Breitensuche 227 – Schnitteigenschaft 276
– – Tiefensuche 231 – starke Zusammenhangskomponente
– Eulerkreis 216 238
– erreichbar 221 – – Wurzel 238
– Exzentrizität 316 – stark zusammenhängend 222
– F –schwere Kanten 297 – Teilgraph 221
– Flussnetzwerk 306 – – aufspannender Teilgraph 221
– – Kapazität 306 – – erzeugter Teilgraph 221
– – Pfad mit Zunahme 310 – Test der Azyklizität 236
– – Quelle 306 – topologisches Sortieren 236
– – Restgraph 309 – totaler Fluss, siehe Graph, -
– – Restkapazität 309 Flussnetzwerk
– – Schnitt minimaler Kapazität 306 – transitiver Abschluss 301
– – Senke 306 – Umgebung eines Knotens 221
– – totaler Fluss 306 – unterliegender Graph 221
– gegenseitig erreichbar 222 – vollständiger Graph 226
– (gerichteter) azyklischer Graph 222, – Vorwärtskante 234
235 – Wald 223
– gerichteter Graph 220 – Weg, siehe Pfad
– gewichteter Graph 253 – zugeordneter gerichteter Graph 221
– Gewichtsfunktion 253 – Zuordnung
– Grad eines Knotens 221 – – maximale Zuordnung 318
– Hamiltonkreis 250 – – perfekte Zuordnung 224
– Intervallgraph 251 – zusammenhängend 221
– Kante 220 – – zweifach zusammenhängend 252
– – Baumkante 234 – Zusammenhangskomponente 223
362 Index

– Zyklus 222 Irrfahrt auf der Geraden 58


ISAM (Indexed Sequential Access
harmonische Zahl 338 Method) 175
Hashverfahren 111
– Analyse Jensensche Ungleichung 347
– – offene Adressierung 129
– – Verkettungen 124 Keim 66
– Belegungsfaktor 126 konkav 348
– Einfügen, Suchen und Löschen 121 Kontraktion, siehe Graph, -Kante
– Fingerabdruck 62
– Hashfunktion 62, 111, 112 Laufzeit 6, 9
– – Division und Multiplikation 113 – durchschnittliche (aveage case) 10
– exponentielle 14
– – universelle Familie 114, 116
– Instanzen der Größe n 9
– – Verfahren bei Verwendung
– logarithmische, lineare, quasi-lineare,
universeller Familien 119
quadratische 14
– Hashtabelle 111
– polynomiale Laufzeit 14
– Kollision 62, 112
– schlechtester Fall (worst case) 9
– Kollisionsauflösung 119
leeres Wort 176
– – Sondierfolge 121
Levenshtein-Abstand 49
– – Doppelhashing 123
lineare Differenzengleichungen 14
– – lineares Sondieren 122
– allgemeine Lösung 23
– – quadratisches Sondieren 122
– Anfangsbedingung 14
– Kollisionswahrscheinlichkeit 114
– charakteristisches Polynom 25
– Primärbereich 119
– erster Ordnung 14
– Rehashing 124
– Expandieren der rechten Seite 15
– Schlüssel 111
– geschlossene Lösung 14
– Überlaufbereich 119
– Kontraktion 26
– uniformes Sondieren (uniform
– Lösung zur Anfangsbedingung 23
hashing) 129
– Methode der Variation der Konstan-
– Verfahren zur Behandlung der
ten 15
Kollisionen 112
– spezielle Lösung 24
– – offene Adressierung 121
– Transformation zur Lösung von
– – separate Verkettungen 120
Rekursionsgleichungen 348
– – Verkettungen mit Überlaufbereich
– zugeordnete homogene Gleichung 15
119
– zweiter Ordnung 19
Heapsort 89
linearer Kongruenzengenerator 67
– binäre Heaps 90
– Startwert 67
– Heapaufbau 91
logistische Differenzengleichung 248
– Laufzeitanalyse 94
– Optimierungen 96 Mastertheorem 28
– – binäre Suche 98 Matroid 38
– – sequenzielle Suche 97 – Austausch-Eigenschaft 38
– Sortierphase 93 – Vererbungs-Eigenschaft 38
Heapspeicher 120 metrischer Raum 350
Höhenbalancierung, siehe Datenstruk- – Abstandsfunktion oder Metrik 350
turen, -Union-Find Modellierung von Problemen durch
Huffman-Verfahren, siehe Codes, Graphen 215
-Huffman-Verfahren Multivariate Polynome 64
Multiplikation von zwei quadratischen
inkrementelles Array 268 Matrizen 30
Inorder-Ausgabe, siehe Baum, -binärer
Baum NP-vollständige Probleme 52
Invariante der Schleife 2
Inversion 107 O–Notation 10
Index 363

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

Das könnte Ihnen auch gefallen