Algorithmen und Datenstrukturen - IT

Werbung
d 4 2
i 9 9
b 2 1
f 6 3
s
a 1 8
c 3 6
e 5 5
g 7 4
h 8 7
Bachelorstudiengang Informatik/IT-Sicherheit
Algorithmen und Datenstrukturen
[AlgoDat]
Autoren:
Dr. rer. nat. Werner Massonne
Prof. Dr.-Ing. Felix C. Freiling
Friedrich-Alexander-Universität Erlangen-Nürnberg
Algorithmen und Datenstrukturen
[AlgoDat]
Studienbrief 1: Die Programmiersprache C
Studienbrief 2: Basiskonzepte und Datenstrukturen
Studienbrief 3: Sortierung und Mengen
Studienbrief 4: Balancierte Bäume
Studienbrief 5: Algorithmen auf Graphen
Autoren:
Dr. rer. nat. Werner Massonne
Prof. Dr.-Ing. Felix C. Freiling
1. Auflage
Friedrich-Alexander-Universität Erlangen-Nürnberg
© 2015 Felix Freiling
Friedrich-Alexander-Universität Erlangen-Nürnberg
Department Informatik
Martensstr. 3
91058 Erlangen
1. Auflage (3. Dezember 2015)
Didaktische und redaktionelle Bearbeitung:
Bärbel Wolf-Gellatly
Das Werk einschließlich seiner Teile ist urheberrechtlich geschützt. Jede Verwendung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne
Zustimmung der Verfasser unzulässig und strafbar. Das gilt insbesondere
für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen.
Um die Lesbarkeit zu vereinfachen, wird auf die zusätzliche Formulierung
der weiblichen Form bei Personenbezeichnungen verzichtet. Wir weisen deshalb darauf hin, dass die Verwendung der männlichen Form explizit als
geschlechtsunabhängig verstanden werden soll.
Das diesem Bericht zugrundeliegende Vorhaben wurde mit Mitteln des Bundesministeriums für Bildung, und Forschung unter dem Förderkennzeichen
160H11068 gefördert. Die Verantwortung für den Inhalt dieser Veröffentlichung liegt beim Autor.
Inhaltsverzeichnis
Seite 3
Inhaltsverzeichnis
Einleitung zu den Studienbriefen
I.
Abkürzungen der Randsymbole und Farbkodierungen . . . . . . . . .
II.
Zu den Autoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
III.
Modullehrziele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
Studienbrief 1 Die Programmiersprache C
1.1 Lernergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Advance Organizer . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3 Motivation, Geschichte, wesentliche Merkmale . . . . . . . . . . . . .
1.4 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.5 Elementare Datentypen, Operatoren und Ausdrücke . . . . . . . . . .
1.5.1 Elementare Datentypen . . . . . . . . . . . . . . . . . . . . .
1.5.2 Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.5.3 Arithmetische Operatoren, Vergleiche und logische Operatoren
1.5.4 Typumwandlung . . . . . . . . . . . . . . . . . . . . . . . . .
1.5.5 Inkrement- und Dekrement-Operatoren . . . . . . . . . . . .
1.5.6 Bit-Manipulation . . . . . . . . . . . . . . . . . . . . . . . . .
1.5.7 Zuweisungsoperatoren und bedingte Ausdrücke . . . . . . .
1.5.8 Vorrang und Reihenfolge, Seiteneffekte . . . . . . . . . . . .
1.6 Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.6.1 Bedingte Anweisungen . . . . . . . . . . . . . . . . . . . . .
1.6.2 Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.7 Programmstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.7.1 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.7.2 Gültigkeitsbereiche und Speicherklassen . . . . . . . . . . .
Initialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.7.3 Präprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8 Zeiger und Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.1 Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.2 Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.3 Zeiger als Funktionsparameter . . . . . . . . . . . . . . . . .
1.8.4 Adressarithmetik . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.5 Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.6 Zeiger auf Zeiger . . . . . . . . . . . . . . . . . . . . . . . .
Mehrdimensionale Felder . . . . . . . . . . . . . . . . . . . .
Kommandozeilenparameter . . . . . . . . . . . . . . . . . . .
1.8.7 Zeiger auf Funktionen . . . . . . . . . . . . . . . . . . . . . .
1.9 Strukturen und Verbunde . . . . . . . . . . . . . . . . . . . . . . . .
1.9.1 Strukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
typedef und sizeof . . . . . . . . . . . . . . . . . . . . . . . .
Dynamische Datenstrukturen . . . . . . . . . . . . . . . . . .
1.9.2 Verbunde . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.10 C-Standardbibliothek . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.10.1 Eingabe und Ausgabe . . . . . . . . . . . . . . . . . . . . . .
Formatierte Ausgabe . . . . . . . . . . . . . . . . . . . . . .
1.10.2 Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . .
1.10.3 Weitere Bibliotheken . . . . . . . . . . . . . . . . . . . . . . .
<ctype.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . .
<string.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . .
<math.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . .
<stdlib.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . .
<time.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
<limits.h> und <float.h> . . . . . . . . . . . . . . . . . . . .
9
6
7
8
9
9
9
11
17
17
18
20
21
22
23
25
26
27
27
30
33
33
35
37
39
40
40
42
43
44
45
46
47
49
50
51
51
55
55
58
58
59
59
63
64
64
64
64
65
65
66
Seite 4
Inhaltsverzeichnis
1.11 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
1.12 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Studienbrief 2 Basiskonzepte und Datenstrukturen
2.1 Lernergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Advance Organizer . . . . . . . . . . . . . . . . . . . . . . . .
2.3 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4 Asymptotische Aussagen . . . . . . . . . . . . . . . . . . . . .
2.5 Maschinenmodell, Zeitmodell und Problemgrößen . . . . . . .
2.6 Worst Case, Best Case, Average Case und amortisierte Kosten
2.7 Pseudocode . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.8 Probabilistische Algorithmen . . . . . . . . . . . . . . . . . . .
2.9 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.10 Effizienz, Komplexitätsklassen P und NP . . . . . . . . . . . .
2.11 Strukturierte Datentypen . . . . . . . . . . . . . . . . . . . .
2.11.1 Bäume . . . . . . . . . . . . . . . . . . . . . . . . . .
Suchbäume . . . . . . . . . . . . . . . . . . . . . . .
Implementierung von Bäumen . . . . . . . . . . . . .
Durchmusterung und Grundoperationen . . . . . . . .
Balancierte Bäume . . . . . . . . . . . . . . . . . . .
2.11.2 Graphen . . . . . . . . . . . . . . . . . . . . . . . . .
Repräsentation von Graphen . . . . . . . . . . . . . .
2.12 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . .
2.13 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Studienbrief 3 Sortierung und Mengen
3.1 Lernergebnisse . . . . . . . . . . . . . . . .
3.2 Advance Organizer . . . . . . . . . . . . . .
3.3 Sortieren . . . . . . . . . . . . . . . . . . .
3.3.1 Minsort . . . . . . . . . . . . . . . .
3.3.2 Eine untere Schranke . . . . . . . .
3.3.3 Bubblesort . . . . . . . . . . . . . .
3.3.4 Heapsort . . . . . . . . . . . . . . .
3.3.5 Mergesort . . . . . . . . . . . . . .
3.3.6 Quicksort . . . . . . . . . . . . . . .
Schlechtester Fall . . . . . . . . . .
Mittlerer Fall . . . . . . . . . . . . .
3.3.7 Bucketsort . . . . . . . . . . . . . .
3.4 Mengen . . . . . . . . . . . . . . . . . . . .
3.4.1 Suchen in geordneten Mengen . . .
Lineare Suche . . . . . . . . . . . .
Binärsuche . . . . . . . . . . . . . .
Interpolationssuche . . . . . . . . .
3.4.2 Digitale Suchbäume . . . . . . . . .
3.4.3 Hashing . . . . . . . . . . . . . . . .
Hashing mit Verkettung . . . . . . .
Hashing mit offener Adressierung .
Perfektes und universelles Hashing .
Hashing vs. balancierte Suchbäume
3.4.4 Das Union-Find-Problem . . . . . . .
Erste Lösung . . . . . . . . . . . . .
Zweite Lösung . . . . . . . . . . . .
3.4.5 Priority Queues . . . . . . . . . . . .
3.5 Zusammenfassung . . . . . . . . . . . . . .
3.6 Übungen . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
101
. 101
. 101
. 101
. 102
. 102
. 104
. 104
. 107
. 109
. 110
. 110
. 112
. 114
. 115
. 116
. 116
. 117
. 118
. 120
. 121
. 124
. 126
. 128
. 129
. 129
. 131
. 134
. 136
. 137
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
73
73
73
73
74
75
78
79
80
81
84
85
85
87
89
89
93
94
96
98
99
Studienbrief 4 Balancierte Bäume
139
4.1 Lernergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Inhaltsverzeichnis
4.2
4.3
4.4
4.5
4.6
4.7
Seite 5
Advance Organizer . . . . . . . . . . . . . . . . . .
Einführung . . . . . . . . . . . . . . . . . . . . . .
AVL-Baum . . . . . . . . . . . . . . . . . . . . . . .
B-Bäume . . . . . . . . . . . . . . . . . . . . . . .
4.5.1 Knotenstruktur und Suche in einem B-Baum
4.5.2 Einfügen . . . . . . . . . . . . . . . . . . .
4.5.3 Löschen . . . . . . . . . . . . . . . . . . .
4.5.4 Varianten von B-Bäumen . . . . . . . . . .
Zusammenfassung . . . . . . . . . . . . . . . . . .
Übungen . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
139
139
140
143
144
145
147
150
151
152
Studienbrief 5 Algorithmen auf Graphen
155
5.1 Lernergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
5.2 Advance Organizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
5.3 Topologisches Sortieren . . . . . . . . . . . . . . . . . . . . . . . . . 155
5.4 Systematische Durchmusterung von Graphen . . . . . . . . . . . . . 159
5.5 Eine nähere Betrachtung von DFS . . . . . . . . . . . . . . . . . . . . 163
5.5.1 Zusammenhangskomponenten . . . . . . . . . . . . . . . . . 166
Bestimmung von Zusammenhangskomponenten . . . . . . . 167
Bestimmung von starken Zusammenhangskomponenten . . 168
Mehrfache Zusammenhangskomponenten . . . . . . . . . . . 170
5.6 Minimale aufspannende Bäume . . . . . . . . . . . . . . . . . . . . . 171
5.7 Greedy-Algorithmen und Matroide . . . . . . . . . . . . . . . . . . . . 173
5.8 Kürzeste Wege, der Algorithmus von Dijkstra . . . . . . . . . . . . . . 175
5.8.1 Bellman-Ford-Algorithmus . . . . . . . . . . . . . . . . . . . . 179
5.8.2 Dynamische Programmierung, der Algorithmus von FloydWarshall . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Transitive Hüllen . . . . . . . . . . . . . . . . . . . . . . . . . 183
5.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
5.10 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Liste der Lösungen zu den Kontrollaufgaben
Verzeichnisse
I.
Abbildungen
II.
Definitionen .
III.
Exkurse . . .
IV.
Tabellen . . .
V.
Literatur . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
187
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
189
. 189
. 189
. 190
. 190
. 190
Stichwörter
193
Liste der Lösungen zu den Übungen
197
Anhang
203
Seite 6
Einleitung zu den Studienbriefen
Einleitung zu den Studienbriefen
I. Abkürzungen der Randsymbole und Farbkodierungen
Definition
D
Exkurs
E
Quelltext
Q
Übung
Ü
Zu den Autoren
Seite 7
II. Zu den Autoren
Felix Freiling ist seit Dezember 2010 Inhaber des Lehrstuhls für ITSicherheitsinfrastrukturen an der Friedrich-Alexander-Universität ErlangenNürnberg. Zuvor war er bereits als Professor für Informatik an der RWTH Aachen
(2003-2005) und der Universität Mannheim (2005-2010) tätig. Schwerpunkte
seiner Arbeitsgruppe in Forschung und Lehre sind offensive Methoden der
IT-Sicherheit, technische Aspekte der Cyberkriminalität sowie digitale Forensik.
In den Verfahren zur Online-Durchsuchung und zur Vorratsdatenspeicherung
vor dem Bundesverfassungsgericht diente Felix Freiling als sachverständige
Auskunftsperson.
Werner Massonne erwarb sein Diplom in Informatik an der Universität des Saarlandes in Saarbrücken. Er promovierte anschließend im Bereich Rechnerarchitektur
mit dem Thema „Leistung und Güte von Datenflussrechnern“. Nach einem längeren Abstecher in die freie Wirtschaft arbeitet er inzwischen als Postdoktorand bei
Professor Freiling an der Friedrich-Alexander-Universität.
Seite 8
Einleitung zu den Studienbriefen
III. Modullehrziele
Dieses Modul vertieft das große Themengebiet „Algorithmen und Datenstrukturen“, beschäftigt sich also
mit der strukturierten Umsetzung konkreter Problemstellungen in Form von Programmen. In den Programmiermodulen der ersten beiden Semester, insbesondere im Modul „Programmierkonzepte“, haben
Sie bereits viele Grundkenntnisse über Abstraktion, Datenstrukturen, Algorithmen, Algorithmenentwurf
und Algorithmenanalyse erworben. Die Grundbegriffe werden in diesem Modul vorausgesetzt, ebenso die
allgemeinen Techniken der Programmierung. In diesen Modulen stand die Programmiersprache Java im
Vordergrund. Java beinhaltet bereits „höhere“ Datenstrukturen und Operationen auf diesen Datenstrukturen.
Für den Programmierer bringt dies den Vorteil, sich nicht mehr mit den Details ihrer Implementierung
beschäftigen zu müssen. Andererseits spielen gerade diese Implementierungen eine wichtige Rolle bei der
Einschätzung des Laufzeitverhaltens von Programmen unter dem Gesichtspunkt ihrer Effizienz1 .
Effizienzanalysen von Programmen oder Algorithmen werden in der Regel nicht auf der Ebene abstrahierender Programmiersprachen wie Java ausgeführt, sondern mit Hilfe von „maschinennaher“ Programmiersprachen, die es erlauben, direkte Rückschlüsse auf die Laufzeit eines Programms auf einer realen
Rechnerarchitektur zu ziehen. Üblicherweise wird nicht mal eine reale Programmiersprache verwendet,
sondern sogenannter Pseudocode. Auch in diesem Modul werden wir teilweise Pseudocode verwenden,
wenn dadurch die Darstellung von Algorithmen „eleganter“, d. h. kompakter und verständlicher wird. Reale
Programmiersprachen erzeugen einen gewissen programmiertechnischen „Overhead“, der bei Pseudocode
weggelassen werden kann, um sich auf den Kern eines Algorithmus konzentrieren zu können. Als eigentliche
Basissprache verwenden wir in diesem Modul jedoch die reale Programmiersprache C. Die Übersetzung von
Pseudocode in ein reales C-Programm wird Ihnen keine großen Schwierigkeiten bereiten, sobald Sie den
C-Programmierkurs im ersten Studienbrief dieses Moduls absolviert haben. Dieser Studienbrief setzt Grundkenntnisse im Bereich elementarer Datenstrukturen voraus. Sollten Sie bereits über Programmiererfahrung
in C verfügen, so können Sie diesen Studienbrief komplett überspringen.
Im zweiten Studienbrief werden die Begrifflichkeiten und Methoden herausgearbeitet, die es erlauben,
Algorithmen auf einer klar definierten Basis zu bewerten und zu vergleichen. Die genaue Definition dieses
Umfeldes ist wichtig, um nicht „Äpfel mit Birnen“ vergleichen zu müssen. Im Vordergrund stehen hierbei
ein einheitliches Maschinenmodell und ein einheitliches Zeitmodell. Es werden darauf aufbauend Methoden
zur asymptotischen Analyse von Programmlaufzeiten vorgestellt. Der zweite Teil des Studienbriefs befasst
sich mit fundamentalen Datenstrukturen, die in der Informatik eine ganz bedeutende Rolle spielen und
einen Zentralpunkt der Algorithmenentwicklung darstellen.
In den Studienbriefen 3 bis 5 werden einige der bedeutendsten Algorithmen vorgestellt und analysiert.
Studienbrief 3 beschäftigt sich mit den Themen Sortieren und Mengen. Auf das Sortieren von Daten entfällt
ein erheblicher Anteil der Rechenkapazität von Computern. Eine effiziente Verwaltung und ein effizienter
Zugriff auf Mengen, die letztendlich Datenbestände darstellen, sind von ebenso fundamentaler Bedeutung
bei der Programmentwicklung wie die Sortieralgorithmen.
Studienbrief 4 beschäftigt sich mit Suchbäumen. Suchbäume sind Datenstrukturen, die insbesondere für die
Ablage und Verwaltung sehr großer Datenmengen (z. B. in Datenbanken) geeignet sind. Wir werden einige
gebräuchliche Suchbäume kennenlernen sowie die Algorithmen für deren dynamische Konstruktion.
In Studienbrief 5 beschäftigen wir uns mit Graphen. Die Darstellung von Daten und Datenabhängigkeiten in
Form von Graphen ist in der Informatik allgegenwärtig, die Implementierung effizienter Graphenalgorithmen
daher fundamental. Die Einsatzmöglichkeiten von Graphenalgorithmen sind sehr groß. Beispielsweise beruht
die Berechnung von Routen durch ein Navigationssystem auf Graphenalgorithmen.
Das gesamte Modul ist mit vielen praktischen Übungen angereichert, anhand derer Sie die Implementierung
der vorgestellten Algorithmen und Datenstrukturen in der Programmiersprache C einüben können. Die
konkrete Implementierung eines Algorithmus vertieft das Verständnis und vermittelt ein Gefühl für den
praktischen Umgang mit Algorithmen.
1
Was unter dem Begriff „Effizienz“ genau zu verstehen ist, wird im Verlauf des Moduls definiert. Im Moment reicht die intuitive
Vorstellung, dass Effizienz so viel wie „Schnelligkeit“ bedeutet.
Studienbrief 2 Basiskonzepte und Datenstrukturen
Seite 73
Studienbrief 2 Basiskonzepte und Datenstrukturen
2.1 Lernergebnisse
Sie sind mit den Grundbegriffen zur Algorithmenanalyse vertraut. Die Konzepte
zur Analyse und Bewertung von Algorithmen sind Ihnen bekannt, Maschinenund Zeitmodell können Sie benennen und begründen. Darüber hinaus können
Sie einige fortgeschrittene Analysemethoden für rekursive Algorithmen benennen und anwenden. Sie können Algorithmen in einem Pseudocode formulieren.
Sie können deterministische und probabilistische Algorithmen definieren und
ihre grundsätzlichen Unterschiede aufzeigen. Des Weiteren können Sie den Begriff „Effizienz“ definieren und einige Effizienzklassen benennen und begründen.
Zwei grundlegend wichtige Datenstrukturen, Bäume und Graphen, können Sie
definieren, charakterisieren und implementieren.
2.2 Advance Organizer
In Studienbrief 1 wurde mit dem Erlernen der Programmiersprache C das Handwerkszeug für die konkrete Implementierung von Algorithmen geschaffen. In
diesem Studienbrief erlernen Sie die Grundtechniken zur Entwicklung und Analyse bzw. Bewertung von Algorithmen. Algorithmen lösen konkrete Probleme.
Zur Lösung eines Problems können viele unterschiedliche Algorithmen formuliert
werden. Daher ist es wichtig, diese objektiv in ihrer Leistungsfähigkeit miteinander
vergleichen und einstufen zu können. Die Grundlagen dazu werden im ersten Teil
dieses Studienbriefs vermittelt.
Der zweite Teil des Studienbriefs beschäftigt sich mit Datenstrukturen. Die strukturierte Anordnung von Daten ist fundamental wichtig, um darauf einen leistungsfähigen Algorithmus zu implementieren. Hier werden die Datenstrukturen
vorgestellt und definiert, auf denen die Algorithmen in den folgenden Studienbriefen aufbauen.
2.3 Einführung
Allgmein wird behauptet, dass der Begriff „Algorithmus“ auf Herrn Abu Dscha’far
Muhammad ibn Musa al-Chwarizmi (s. Bild 2.1) zurückgeht, der um das Jahr 800
n. Chr. im Haus der Weisheit in Bagdad lehrte. In Wahrheit jedoch leitet sich der
Begriff vom altgriechischen αλ γOριθ µει(ν) ab, was so viel wie „wahre Laufzeiten
geschickt in einer O-Notation verstecken“ bedeutet.
Abb. 2.1: Muhammad ibn
Musa al-Chwarizmi
Seite 74
Studienbrief 2 Basiskonzepte und Datenstrukturen
2.4 Asymptotische Aussagen
asymptotische Analyse
Die Methodik der asymptotischen Aussagen bezüglich der Laufzeit von Algorithmen wurden bereits im Modul „Programmierkonzepte“ vorgestellt. Diese sogenannte O-Notation bildet die Grundlage zur Effizienzanalyse von Algorithmen.
Die grundlegenden Definitionen werden an dieser Stelle noch einmal wiederholt,
und es werden einige Regeln für den Umgang mit der O-Notation vorgestellt.
Definition 2.1: O-Notation
D
Seien f , g : N0 → R+ , dann gilt:
O( f ) = {g : ∃c > 0 : ∃n0 > 0 : ∀n > n0 : g(n) ≤ c · f (n)}
Ω( f ) = {g : ∃c > 0 : ∃n0 > 0 : ∀n > n0 : g(n) ≥ c · f (n)}
Θ( f ) = O( f ) ∩ Ω( f )
o( f ) = {g : ∀c > 0 : ∃n0 > 0 : ∀n > n0 : g(n) ≤ c · f (n)}
ω( f ) = {g : ∀c > 0 : ∃n0 > 0 : ∀n > n0 : g(n) ≥ c · f (n)}
obere und untere Schranken
O( f ) ist die Menge aller Funktionen, die asymptotisch nach oben durch f beschränkt sind. Man spricht daher von einer oberen Schranke. Ω( f ) ist die Menge
von Funktionen, die asymptotisch mindestens so stark wachsen wie f . Dies bezeichnet man als untere Schranke. Θ( f ) ist die Menge aller Funktionen, die asymptotisch
„genauso stark“ wachsen wie f . Hier spricht man von einer scharfen Schranke bzw.
einer Entsprechung. Die o- und ω-Symbolik werden weniger häufig benutzt. Der
Effekt des Allquantors für c ist, dass sich f und g (für genügend große n) beliebig
weit unterscheiden müssen. So ist bspw. 3n2 ∈ O(n2 ), aber 3n2 ∈
/ o(n2 ). Hingegen
2
gilt n · log2 n ∈ o(n ).
Man kann folgende, etwas hinkende Analogie zu den reellen Zahlen benutzen, um
die Bedeutung der O-Notation optisch besser darzustellen. Im mathematischen
Sinn ist das natürlich falsch:
g ∈ O( f )
g ∈ Ω( f )
g ∈ Θ( f )
g ∈ o( f )
g ∈ ω( f )
≈
≈
≈
≈
≈
g≤
g≥
g=
g<
g>
f
f
f
f
f
Obwohl die O-Notation eigentlich auf Mengen basiert, benutzt man Schreibweisen
wie n2 + 5n = n2 + O(n) = O(n2 ). Das Gleichheitssymbol bezeichnet hier mathematisch korrekt ein „enthalten in“. Solche Gleichungen sind immer von links nach
rechts zu lesen.
Es folgen einige Rechenregeln für die O-Notation. Die Beweise sind eher Fleißarbeit
und deswegen Teil der Übungen.
2.5 Maschinenmodell, Zeitmodell und Problemgrößen
Seite 75
Satz 2.1
S
Seien f , h : N0 → R+ , dann gilt:
a · f (n)
f (n) + h(n)
f (n) + h(n)
O( f (n)) · O(h(n))
∈
∈
∈
⊆
Θ( f (n)) ∀a > 0
Ω( f (n))
O( f (n)) , wenn h(n) ∈ O( f (n))
O( f (n) · h(n))
Kontrollaufgabe 2.1
Gilt
2n+1
K
∈ O(2n )?
In diesem Modul werden meist Kombinationen von Polynomen und logarithmischen Funktionen in den Laufzeitanalysen vorkommen, weswegen der folgende Satz nun bewiesen wird, um ein wenig den praktischen Umgang mit der ONotation zu zeigen.
Satz 2.2
S
Sei p(n) = ∑ki=0 ai ni ein Polynom und sei ak > 0, dann gilt: p(n) ∈ Θ(nk )
Beweis:
Es ist zu zeigen, dass p(n) ∈ O(nk ) und p(n) ∈ Ω(nk ) gilt.
1) Für alle n > 0 gilt:
k
k
k
p(n) ≤ ∑ |ai |ni ≤ nk ∑ |ai | = nk ( ∑ |ai |)
i=0
i=0
⇒ p(n) ∈ O(nk ) da
i=0
k
∑ |ai | konstant.
i=0
2) Sei A = ∑k−1
i=0 |ai |, dann gilt für positive n:
p(n) ≥ ak nk − Ank−1 =
a
ak k
k
n + nk−1
n−A ,
2
2
und damit p(n) ≥ a2k nk für n > 2A
ak . Wir wählen c =
k
Definition von Ω(n ) und erhalten so p(n) ∈ Ω(nk ).
ak
2
und n0 =
2A
ak
in der
2.5 Maschinenmodell, Zeitmodell und Problemgrößen
Um die Komplexität von Algorithmen untersuchen bzw. vergleichen zu können,
benötigen wir feste Bezugsgrößen. Es macht für uns wenig Sinn, reale Laufzeiten
(z. B. in Sekunden) auf einem realen Computer zu messen, denn dann wären die
Analysen technologieabhängig und somit nicht allgemeingültig und nicht übertragbar. Zudem wären die Analysen sehr kompliziert und von vielen kleinen Details
abhängig, die bei einer grundlegenden Effizienzanalyse gar nicht interessieren.
Als Beispiel sei nur das mehrstufige Cache-System einer modernen CPU genannt,
das einen erheblichen Einfluss auf die reale Laufzeit eines Programms hat, am
grundsätzlichen Verhalten des Programms allerdings nichts ändert. Wir benötigen
Seite 76
Studienbrief 2 Basiskonzepte und Datenstrukturen
also ein Modell, das einerseits einfach, aber andererseits konkret genug ist, um die
damit erhaltenen Ergebnisse auf reale Hardware übertragen zu können.
O-Notation
Das Ziel der Effizienzanalyse von Algorithmen ist die Gewinnung asymptotischer
Aussagen, wobei wir uns der O-Notation bedienen. Die Definition der O-Notation
impliziert die Betrachtung großer Problemgrößen und die Vernachlässigung von
Proportionalitätskonstanten und nichtdominanten Termen. Gewinnen wir bspw.
die Aussage, dass ein Algorithmus die asymptotische Laufzeit Θ(n2 ) hat, so wissen
wir, dass die Laufzeit des Algorithmus mit der Problemgröße n quadratisch wächst.
Die Abbildung dieser asymptotischen Laufzeit auf eine reale Maschine ändert
nichts am quadratischen Laufzeitverhalten, könnte also bspw. 2, 56n2 + 753n +
423, 38 Millisekunden aufgrund der Eigenschaften der realen CPU betragen.
Maschinenmodell
Wir wählen als Maschinenmodell ein einfache Variante des von-Neumann-Modells,
das Ihnen aus dem Modul „Rechnerstrukturen“ bekannt ist und nennen diese RAM
(Random Access Machine). Die Ein-/Ausgabeeinheit und Teile der Speichereinheit
wie die Festplatten interessiert uns in der Regel nicht, unsere RAM besteht also
nur aus der CPU und dem Hauptspeicher. Programm und Eingaben liegen vor
dem Programmstart im Hauptspeicher und nach dem Programmlauf ebenfalls.
Unsere RAM ist eine sequentielle Maschine, verfügt also nur über eine einzige
Recheneinheit. Der Hauptspeicher ist in (unendlich vielen) Zellen s[0], s[1], . . . fester
Größe organisiert, auf die „wahlfrei“ zugegriffen werden kann, d. h. der Zugriff
ist beliebig, und alle einzelnen Schreib-/Leseoperationen auf eine Speicherzelle
benötigen dieselbe Zeit. Die CPU unserer RAM beherrscht alle üblichen binären
und unären arithmetischen und logischen Befehle auf Daten, deren Größe denen
einer Speicherzelle entspricht, sowie Kontrollflussbefehle (bedingte und unbedingte Sprünge). Die RAM hat einen Registersatz beliebiger, aber fester Größe.
Die arithmetischen und logischen Befehle arbeiten ausschließlich auf Registern,
demzufolge hat ein Register auch die Größe einer Speicherzelle. Register können
mit Konstanten geladen werden, und der Hauptspeicherzugriff erfolgt ausschließlich indirekt über Register, d. h. die Befehle R j = s[Ri ] bzw. s[Ri ] = R j für beliebige
Register Ri und R j sind die einzigen unterstützten Hauptspeicherbefehle.1
Zeitmodell
Für unsere RAM definieren wir ein ganz einfaches Zeitmodell. Wir postulieren,
dass alle Befehle der RAM dieselbe Ausführungszeit haben. Die Ausführungszeit
eines CPU-internen Befehls entspricht damit auch der Zeit eines lesenden oder
schreibenden Speicherzugriffs. Die Laufzeit eines RAM-Programms entspricht
demnach genau der Anzahl der ausgeführten RAM-Befehle.
Kontrollaufgabe 2.2
K
Beherrscht die RAM den Datentransferbefehl Ri = R j ?
Warum interessiert es uns nicht, dass auf einer realen CPU bspw. Additionund Divisionsbefehle keineswegs dieselbe Laufzeit haben?
Wie viele Zeiteinheiten benötigt das Umkopieren eines Blocks von 100 Speicherzellen mindestens?
1
Da die Größe eines Registers begrenzt ist, können durch die indirekte Adressierung auch nur endlich
viele Speicherzellen adressiert werden. Teile des unendlich großen Hauptspeichers können somit
nicht adressiert werden. Das soll uns aber nicht weiter interessieren. Vereinfachend gehen wir immer
davon aus, dass der Hauptspeicher genügend groß ist, und dass genügend viele Zellen indirekt über
Register adressiert werden können.
2.5 Maschinenmodell, Zeitmodell und Problemgrößen
Seite 77
Exkurs 2.1: Parallele Algorithmen
E
Die RAM ist ein sequentielles Maschinenmodell, und wir betrachten in
diesem Modul auch ausschließlich sequentielle Algorithmen. Die Welt der
parallelen Algorithmen steht außerhalb unserer Betrachtungen, und es ist
auch kein einfaches Maschinenmodell denkbar, das den gesamten Bereich
der parallelen Algorithmen abdeckt.
In der modernen Computerwelt existiert die Parallelität auf vielerlei Ebenen. Innerhalb eines Prozessorkerns kann es möglich sein, durch einen
Befehl (gleiche) Operationen auf mehreren Daten auszuführen (SIMD für
Single Instruction - Multiple Data). Innerhalb eines physikalischen Prozessors
können mehrere unabhängige Prozessorkerne existieren, die gleichzeitig unterschiedliche Programme oder unterschiedliche Threads eines Programms
ausführen können, und innerhalb eines Rechners können mehrere dieser
physikalischen Prozessoren existieren. Alle diese Prozessoren und Prozessorkerne teilen sich einen gemeinsamen Hauptspeicher, kommunizieren
und synchronisieren sich also über diesen. Ein übliches Maschinenmodell
dieser sogenannten Shared-Memory-Architekturen ist die PRAM (Parallel
Random Access Machine). Die PRAM wird zur Entwicklung und Analyse
paralleler Algorithmen benutzt, und es existieren davon einige Unterarten,
die verschiedene, gleichzeitig mögliche Operationen auf dem gemeinsamen
Hauptspeicher erlauben.
Bei einem Rechnerverbund, bei dem jeder Rechner über einen eigenen
Hauptspeicher verfügt, spricht man von einem verteilten System. Hochleistungsrechner kommunizieren dabei über schnelle, dedizierte Netzwerke,
lose gekoppelte Systeme über verschiedene Arten von Netzwerken, bspw.
über das Internet. Die Maschinenmodelle sind jeweils unterschiedlich.
Wir haben bisher schon mehrfach den intuitiven Begriff der Problemgröße benutzt,
ohne zu sagen, was genau damit gemeint ist. Allgemein ist die Problemgröße die
Größe der Eingabe eines Programms. Sollen bspw. n Zahlen sortiert werden, so hat
die Problemgröße offensichtlich etwas mit n zu tun. Aber es gibt zwei mögliche
Arten der Interpretation. Ist die Problemgröße die Anzahl der Bits, die erforderlich
ist, um die n Zahlen zu codieren, oder ist die Problemgröße die Zahl n (eventuell
multipliziert mit einem konstanten Faktor) selbst?
Problemgröße
Wir gehen davon aus, dass eine Speicherzelle – wie bei einem realen Rechner –
eine „kleine“ Integer-Zahl aufnehmen kann. Sollen also n kleine Integer-Zahlen
sortiert werden, so stehen diese n Zahlen in n Speicherzellen. Die Problemgröße ist
also n Speicherzellen bzw. n · m Bit, wenn m die Größe einer Speicherzelle in Bit ist.
Vereinfachend sagen wir, dass die Problemgröße n ist. Die Problemgröße ist also
immer die Anzahl der Eingabedaten eines Programms. Wir betrachten in diesem
Modul ausschließlich Algorithmen, bei denen jeder Wert einer Eingabe und auch
die Zwischenergebnisse immer in eine Speicherzelle hineinpassen. Zusammen
mit der Forderung, dass alle RAM-Befehle in einer Zeiteinheit ausgeführt werden
können, bezeichnen wir das als uniformes Komplexitätsmaß.
uniformes Komplexitätsmaß
Sollen n „große“ Zahlen sortiert werden, so müssen diese auf mehr als n Speicherzellen aufgeteilt werden. Die Problemgröße ist dann größer als n. Ein leicht
verständliches Beispiel, bei dem das uniforme Komplexitätsmaß nicht angebracht
ist, ist ein Programm zur Zerlegung einer Zahl in ihre Primfaktoren. Angenommen, eine Zahl n = p · q mit zwei großen Primzahlen p und q soll in diese zerlegt
werden. Das Verfahren dazu ist enorm aufwendig, obwohl dem Programm nur
Bit-Komplexitätsmaß
Seite 78
Studienbrief 2 Basiskonzepte und Datenstrukturen
eine einzige Zahl als Eingabe übergeben wird. Das uniforme Komplexitätsmaß
ist hier offensichtlich unsinnig. Hier macht es erheblich mehr Sinn, die Länge der
binären Codierung von n als Problemgröße anzusehen, und man spricht daher
vom Bit-Komplexitätsmaß. Die Bit-Komplexität entspricht dem Komplexitätsbegriff
bei Turing-Maschinen in der theoretischen Informatik.
E
Exkurs 2.2: „Schnelle“ Multiplikation großer Zahlen
An dieser Stelle wird es Zeit, das Augenzwinkern von Herrn al-Chwarizmi
auf Seite 73 aufzuklären. Die Laufzeit eines Algorithmus zur Multiplikation
ganzer Zahlen nach der bekannten Schulmethode ist recht offensichtlich
Θ(n2 ), wenn n die Bit-Komplexität der Problemgröße – in etwa entspricht
das der Anzahl der Ziffern der zu multiplizierenden Zahlen – angibt. Schönhage und Strassen beschreiben in [und Volker Strassen, 1971] ein Verfahren,
das eine Laufzeit von O(n · log2 n · log2 log2 n) hat, also schneller ist als die
Schulmethode, und das bis zum Jahr 2007 das effizienteste, bekannte Multiplikationsverfahren war. Wegen der großen, in der O-Notation verborgenen,
Konstanten ist der Schönhage-Strassen-Algorithmus allerdings erst für Zahlen mit mehreren 10000 Ziffern absolut schneller als die Schulmethode.
Asymptotische Aussagen sollte man also stets etwas kritisch betrachten,
manchmal eher praktisch als akademisch.
2.6 Worst Case, Best Case, Average Case und amortisierte
Kosten
Worst Case, Best Case und Average Case
D
Die Laufzeit eines Algorithmus hängt von den Eingaben ab. Wenn wir die Problemgröße mit n fixieren, dann muss es keineswegs so ein, dass alle Eingaben der Größe
n dieselbe Laufzeit bewirken. Wir werden in diesem Modul mehrfach Algorithmen
kennenlernen, deren Laufzeit nicht von der Problemgröße, sondern von der Art
der Eingaben bzw. von ihrer Gruppierung abhängt. Ein Sortieralgorithmus, der
bspw. (in Zeit O(n)) erkennt, wenn ihm bereits ein sortiertes Feld übergeben wird,
erzielt einen wunderbaren besten Fall (best case). Derselbe Algorithmus kann aber
im schlechtesten Fall (worst case) eine quadratische Laufzeit haben und im mittleren
Fall (average case) eine Laufzeit von O(log2 n · n). Wie wir noch sehen werden, sind
die Analysemethoden für die drei Fälle oftmals unterschiedlich. Wir wollen an
dieser Stelle die drei Begriffe formal definieren.
Definition 2.2: Laufzeit
Sei P ein RAM-Programm. Seien weiterhin n ∈ N die Größe einer Eingabe
für P und In die Menge aller möglichen Eingaben der Größe n für P. Als T (i)
bezeichnen wir die Laufzeit von P für Eingabe i ∈ In . Dann gilt:
worst case:
T (n) = max{T (i) : i ∈ In }
best case:
T (n) = min{T (i) : i ∈ In }
average case: T (n) = |I1n | · ∑i∈In T (i)
amortisierte Kosten
Darüber hinaus spricht man von amortisierten Kosten, wenn die Laufzeit einer
ganzen Folge von Eingaben der Größe n betrachtet wird. Wenn diese Folge eine
Länge von m hat, dann sind die amortisierten Kosten für eine Eingabe der Folge
definiert als Tmm , wobei Tm die Gesamtlaufzeit der Folge ist. In der Regel werden dabei
für Tm die worst-case-Kosten der gesamten Folge angenommen. Die amortisierten
2.7 Pseudocode
Seite 79
Kosten können unter Umständen geringer ausfallen als die worst-case-Kosten einer
einzelnen Eingabe.
2.7 Pseudocode
Das im vorangegangenen Abschnitt eingeführte Maschinen- und Zeitmodell der
RAM erlaubt es uns, Programme, die im Maschinencode der RAM geschrieben
sind, zu analysieren. Wir wollen unsere Algorithmen allerdings nicht in Maschinencode formulieren, sondern in einer Hochsprache. Was uns also fehlt, ist ein
Modell, dass die Ausführung eines Hochsprachenprogramms auf eine RAM abbildet. Wir könnten für eine beliebige Hochsprache einen Compiler für RAM-Code
entwickeln und die erzeugten RAM-Programme dann analysieren, aber das ist
angesichts der Größe der RAM-Programme zu kompliziert und zu unübersichtlich.
Stattdessen verwenden wir eine einfache Hochsprache und vereinfachen das Zeitmodell der RAM noch weiter. Es ist in der Welt der Algorithmenanalyse unüblich,
Programme in einer realen höheren Programmiersprache zu formulieren. Die
Formulierung erfolgt vielmehr in sogenanntem Pseudocode, der von vielen Implementierungsdetails wie z. B. Variablendeklarationen, Typkonvertierungen usw.
abstrahiert und dadurch kurz und prägnant ist. Pseudocode ist im Kern ähnlich zu
einer (imperativen) Programmiersprache wie C oder PASCAL. Die Übersetzung
eines Pseudocode-Programms in eine dieser Sprachen oder umgekehrt ist für einen
geübten Programmierer einfach zu bewerkstelligen. Ein Pseudocode-Programm
besteht aus Zuweisungen an Variablen, einfachen Berechnungen, Schleifen und
Funktionen. Darüber hinaus ist der freie Umgang mit mathematischen Notationen
erlaubt, deren direkte Umsetzung in einer realen Hochsprache leicht möglich ist.
Ein Beispiel (die Bedeutung des Programms ist an dieser Stelle unwichtig):
Quelltext 2.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Pseudocode und Hochsprache
Q
union (int A,B,C) {
if (GROESSE[A] <= GROESSE[B]) {
z = MAPIN[A]; y = MAPIN[B];
}
else {
z = MAPIN[B]; y = MAPIN[A];
}
for (alle x in Liste[z])
IST_IN[x] = y;
Liste[y] = Liste[y] vereinigt Liste[z];
MAPOUT[y] = C;
MAPIN[C] = y;
GROESSE[C] = GROESSE[A] + GROESSE[B];
}
Jeder Autor verwendet im Prinzip seinen eigenen Pseudocode. Der in diesem
Modul verwendete Pseudocode ist C-ähnlich, auf eine formale Einführung wollen
wir allerdings verzichten. Syntax und Semantik sollten leicht verständlich sein,
und Sie werden in den Übungen genügend Gelegenheit haben, Pseudocode in C
zu übersetzen.
Das Zeitmodell für Pseudocode ist denkbar einfach: Einfache Anweisungen benötigen eine konstante Zeit, Funktionsaufrufe eine konstante Zeit zuzüglich der
Zeit zur Ausführung des Funktionsrumpfes. Die Konstanten können sich bei unterschiedlichen Anweisungen durchaus unterscheiden. Eine Unterscheidung wird
Zeitmodell für Pseudocode
Seite 80
Studienbrief 2 Basiskonzepte und Datenstrukturen
allerdings nur dann gemacht, wenn es sinnvoll ist. Oftmals ist eine Unterscheidung
nicht erforderlich, um Laufzeitabschätzung zu machen. Beispielsweise dominiert
bei Sortieralgorithmen oftmals die Anzahl der Vergleiche die Laufzeit, sodass es
nicht erforderlich ist, Konstanten für sonstige Anweisungen überhaupt zu betrachten. Die Gesamtlaufzeit eines Pseudocode-Programms ist dann die Summe der
Laufzeiten der ausgeführten Anweisungen.
Es ist natürlich nicht erlaubt, wahre Laufzeiten im Pseudocode in „einfachen
Anweisungen“ zu verstecken, deren Laufzeit als konstant angenommen wird.
Grundlage dessen, was als konstante Laufzeit einer Anweisung angenommen
wird, ist der (fiktiv) aus einer Anweisung erzeugte RAM-Code. Intuitiv sollte
meist klar sein, welche Anweisungen ein Äquivalent in RAM-Code mit konstanter
Laufzeit haben, die Details einer solchen Übersetzung werden allerdings erst
im Modul „Systemnahe Programmierung“ herausgearbeitet. Eine PseudocodeAnweisung der Art sortiere Feld A ist übrigens unproblematisch, solange sie als
Funktionsaufruf interpretiert wird. Die Laufzeit besteht dann aus einer Konstanten
zum Aufruf der Funktion plus der Zeit für das Sortieren des Feldes, die keineswegs
konstant ist.
Drei besondere Funktionen werden gelegentlich in Pseudocode benötigt und mit
konstanter Laufzeit angesetzt: alloc und free zur Allokierung und Freigabe von
Speicher so wie random zur Generierung einer Zufallszahl. Zumindest die Anweisungen zur Speicherverwaltung haben in der Realität nicht automatisch eine
konstante Laufzeit. Die korrekte Analyse von Programmen, die solche Anweisungen enthalten, ist daher sehr wesentlich.
2.8 Probabilistische Algorithmen
Man spricht von einem probabilistischen oder randomisierten Algorithmus, wenn
darin eine random-Anweisung zur Anwendung kommt, ansonsten spricht man von
einem deterministischen Algorithmus. In C haben wir bereits die Bibliotheksfunktion rand kennengelernt, die eine ganzzahlige Zufallszahl im Intervall von 0 bis
RAND_MAX generiert.2
Las Vegas- und Monte Carlo-Algorithmen
Die Laufzeit eines probabilistischen Algorithmus hängt von einer zufälligen Wahl
ab, die innerhalb des Algorithmus getroffen wird. Somit ist die Laufzeit für eine
feste Eingabe keine Zahl, sondern eine Zufallsvariable, die von einer zufälligen
Wahl abhängt. Wie geht man damit um? Wenn wir wissen, dass der Algorithmus
auf jeden Fall terminiert und das korrekte Ergebnis liefert, dann können wir die Terminierung abwarten. Solche Algorithmen werden auch als Las Vegas-Algorithmen
bezeichnet. Wir können allerdings auch einen Timer in die RAM einbauen, der den
Algorithmus nach einer fest voreingestellten Zeit stoppt. Allerdings kann dann
nicht mehr sichergestellt werden, dass ein bis dahin berechnetes Zwischenergebnis
auch korrekt ist. Ist das Zwischenergebnis allerdings mit hoher Wahrscheinlichkeit
korrekt, so hat auch diese Vorgehensweise eine realistische Daseinsberechtigung.
Solche Algorithmen werden als Monte Carlo-Algorithmen bezeichnet. Ein Las
Vegas-Algorithmus wird in Abs. 3.4.3 vorgestellt, ein bekanntes Beispiel für einen
Monte Carlo-Algorithmus ist der Miller-Rabin-Primzahltest3 .
2
3
Auf die Problematik, dass es sich hierbei um Pseudo-Zufallszahlen handelt, soll hier nicht weiter
eingegangen werden. Wir gehen im Folgenden davon aus, dass wir perfekte Zufallszahlen generieren
können.
siehe z. B. bei http://de.wikipedia.org/wiki/Miller-Rabin-Test.
2.9 Rekursion
Seite 81
Als Erweiterung von Def. 2.2 definieren wir die randomisierte Laufzeit eines
probabilistischen RAM-Programms P wie folgt:
Definition 2.3: Randomisierte Laufzeit
D
worst case:
Tr (n) = max{time0r (i) : i ∈ In }
average case: Tr (n) = |I1n | · ∑i∈In time0r (i)
Dabei ist time0r (i) der Erwartungswert der Laufzeit von P für Eingabe i ∈ In .
Diese Definition macht allerdings nur bei Las Vegas-Algorithmen Sinn. Für Monte
Carlo-Algorithmen, bei denen die Laufzeit beschnitten wird, benutzt man eine
„umgekehrte Laufzeitdefinition“. Ohne dies formal auszuführen, wird bei Monte
Carlo-Algorithmen eine Laufzeitschranke so definiert, dass nach Ablauf dieser Zeit
der Algorithmus mit hoher Wahrscheinlichkeit ein korrektes Ergebnis liefert.
Kontrollaufgabe 2.3
K
Man kann die Zahl π bestimmen, indem man ein Quadrat mit Seitenlänge 1
um den Einheitskreis legt und immer wieder zufällig darauf „feuert“. Aus
dem Verhältnis der „Treffer“ außerhalb und innerhalb des Kreises lässt sich
π „errechnen“. Handel es sich hier um einen Las Vegas- oder einen Monte
Carlo-Algorithmus?
2.9 Rekursion
Die Laufzeit rekursiver Algorithmen ist oftmals intuitiv leicht ersichtlich, formal aber weniger leicht beschreibbar. In diesem Abschnitt wollen wir uns ein
wenig näher mit der Laufzeitanalyse rekursiver Algorithmen beschäftigen. Eine
grundsätzliche Methode, die insbesondere bei der Klasse der divide-and-conquerAlgorithmen zur Anwendung kommt, ist die Substitutionsmethode. Zunächst
werden wir diese vorstellen, dann das sogenannte Master-Theorem (aber nicht
bewiesen), das ein starkes mathematisches Werkzeug zur Analyse rekursiver Algorithmen darstellt.
Zunächst müssen wir festlegen, wie überhaupt die Laufzeit eines rekursiven Algorithmus definiert werden soll. Allgemein gehen wir davon aus, dass der Algorithmus in Form einer Funktion vorliegt, die sich selbst aufruft. Wir zählen, wie oft
die Funktion (rekursiv) aufgerufen wird und addieren die Kosten dieser Funktionsaufrufe. Es wird festgelegt, dass jedem einzelnen Funktionsaufruf die Kosten
zur Ausführung des Funktionsrumpfes einschließlich der Kosten zur „Versorgung“
der rekursiven Aufrufe zugeordnet werden, aber abzüglich der Kosten für die
rekursiven Aufrufe. Die Gesamtkosten ergeben sich dann aus der Summe der so
ermittelten Kosten aller Funktionsaufrufe.
Beispiel 2.1
In Studienbrief 1 wurde ein rekursives Verfahren zur Bestimmung der
Fibonacci-Zahlen vorgestellt. Der Rumpf der Funktion ist sehr einfach und
besteht nur aus einer Addition und zwei Funktionsaufrufen. Die Kosten
Laufzeit rekursiver
Algorithmen
B
Seite 82
Studienbrief 2 Basiskonzepte und Datenstrukturen
eines Funktionsaufrufs sind offensichtlich O(1). Allerdings ist die Gesamtzahl der Funktionsaufrufe sehr hoch, denn jeder Funktionsaufruf initiiert
seinerseits zwei neue Funktionsaufrufe. Die Gesamtzahl der Funktionsaufrufe wächst daher geometrisch in n, d. h. die Laufzeit des Algorithmus ist
Θ(2n ).4
Rekursionsgleichungen
Die Laufzeiten rekursiver Algorithmen können oft mit Hilfe von Rekursionsgleichungen der allgemeinen Form f (n) = F( f (1), f (2), . . . , f (n − 1)) beschrieben werden. Die Kosten (Laufzeit) f bei Problemgröße n errechnet sich also durch Anwendung einer Funktion F aus den Kosten kleinerer Problemgrößen. Zudem werden
Anfangswertbedingungen für f festgelegt. Solch eine Rekursionsgleichung steht
oft in ganz engem Bezug zur rekursiven Funktion selbst, bspw. wäre im Fall unseres Fibonacci-Algorithmus eine Funktion f (n) = x · ( f (n − 1) + f (n − 2)) (x ≥ 1) mit
f (1) = f (2) = c eine naheliegende Kostenbeschreibung. Allerdings hilft eine Rekursionsgleichung noch nicht wirklich weiter, denn ihr ist meist nicht auf den ersten
Blick die „wahre“ Laufzeit anzusehen. Wir benötigen eine geschlossene Form der
Kostenfunktion, zu deren Lösung nicht der Algorithmus selbst ausgeführt werden
soll. Es gilt also Rekursionsgleichungen zu lösen!
Substitution
Die wichtigste Methode zur Lösung einer Rekursionsgleichung ist die Substitutionsmethode. Diese besteht aus zwei Schritten:
1. Erraten eines Lösungsansatzes.
2. Beweis des Ansatzes durch Induktion; Gewinnung der Konstanten.
Beispiel 2.2
B
Wir nehmen an (erraten), dass der vorgestellte Fibonacci-Algorithmus exponentielle Laufzeit hat, also eine Laufzeit von f (n) = Θ(cn ) für eine Konstante
c > 1.
Die Kostenfunktion ist f (n) = x · ( f (n − 1) + f (n − 2)), wobei wir der Einfachheit halber x = 1 setzten, um x nicht durch die ganze Rechnung mitschleppen
zu müssen.
Wir zeigen zunächst, dass f (n) ≥ acn für geeignete Konstanten a, c > 0 gilt.
Der Induktionsschritt5 ist dann wie folgt:
f (n) = f (n − 1) + f (n − 2) ≥ acn−1 + acn−2 = acn ·
Damit die Annahme f (n) ≥ acn gilt, muss also
c+1
c2
c+1
c2
≥ 1 sein. Die Lösung der
quadratischen Gleichung
= 0 führt dazu, dass c ≤
sein muss, damit der Induktionsschritt gilt.
c2 − c − 1
√
5+1
2
∼ 1.618
Analog
zeigt man durch Umkehrung von „≥“ zu „≤“, dass f (n)
≤ a0 d n für
√
√
d ≥ 5+1
gilt. Zusammengefasst gilt also f (n) = Θ(cn ) für c = 5+1
2
2 .
Mastertheorem
Der Fibonacci-Algorithmus ist kein klassischer divide-and-conquer-Algorithmus,
weil die Problemgröße in der Rekursion nicht um einen konstanten Faktor verrin4
5
Selbstverständlich existieren sehr viel effizientere Verfahren zur Bestimmung von Fibonacci-Zahlen.
Dies ist an dieser Stelle allerdings nicht von Bedeutung.
Für den Induktionsanfang wählt man ein geeignetes a und n = 1.
2.9 Rekursion
Seite 83
gert wird. Typische Rekursionsgleichungen von divide-and-conquer-Algorithmen
haben die Form
T (n) = a · T (n/b) + Θ(nk ),
wobei a ≥ 1 und b > 1 gelten. (n/b kann hierbei auch dn/be oder bn/bc bedeuten.)
Diese Rekursionsgleichung beschreibt die Laufzeit eines Algorithmus, der die
Eingabe der Größe n in a Teilprobleme der Größe n/b zerlegt, also die Teilprobleme
durch a rekursive Aufrufe löst und aus den erhaltenen Teillösungen die Gesamtlösung zusammensetzt. Θ(nk ) beschreibt – wie oben festgelegt – die Kosten zur
Ausführung des Funktionsrumpfes, also die Zeit zum Zerlegen in Teilprobleme
und zum Zusammenfügen der Teillösungen ohne die Kosten der rekursiven Aufrufe. Für Rekursionsgleichungen dieser Art gilt das sogenannte Mastertheorem, das
ein sehr starkes Mittel für ihre Lösung darstellt:
Satz 2.3
S
Für eine Rekursionsgleichung der Form
T (n) = a · T (n/b) + Θ(nk ) mit a ≥ 1, b > 1 und k ≥ 0 gilt:

, falls a < bk
 Θ(nk )
k
T (n) =
Θ(n · log2 n) , falls a = bk

Θ(nlogb a )
, falls a > bk
Beispiel 2.3
B
Nehmen wir einen rekursiven Algorithmus an, bei dem die Problemgröße
gedrittelt wird mit der Rekursionsgleichung
T (n) = a · T (n/3) + n
(also b = 3 und k = 1)
Der interessante Wert bk ist 3. Die Laufzeit des Algorithmus wird entscheidend durch die Konstante a geprägt. Schauen wir uns zunächst das Ergebnis
für a = 2, a = 3 und a = 4 an:

, falls a = 2
 Θ(n)
Θ(n · log2 n)
, falls a = 3
T (n) =

Θ(nlog3 4 ) ∼ Θ(n1.26 ) , falls a = 4
Wegen Θ(nk ) = n in der Rekursionsgleichung ist die Laufzeit bereits durch
Ω(n) nach unten beschränkt.
Für a < 3 wird die Laufzeit nicht mehr „schlimmer“, sie wird also nicht durch
die Rekursion geprägt, sondern durch die Ausführung des Funktionsrumpfes. Für a > 3 wächst die Laufzeit polynomiell und für a = 3 „ausgeglichen“,
d. h. die logarithmische Rekursionstiefe prägt die Laufzeit.
Eine praktische Anwendung des Mastertheorems wird Ihnen in den Übungen zu
Studienbrief 3 begegnen.
Seite 84
Studienbrief 2 Basiskonzepte und Datenstrukturen
2.10 Effizienz, Komplexitätsklassen P und NP
Wir haben schon mehrfach den Begriff effizienter Algorithmus verwendet. Wann
wird ein Algorithmus überhaupt als effizient bezeichnet? Allgmein lautet die Definition so, dass ein Algorithmus A effizient ist, wenn es ein Polynom p gibt und
für eine Problemgröße n die Laufzeit des Algorithmus O(p(n)) ist. Um Kompatibilität mit der theoretischen Informatik, insbesondere der Komplexitätstheorie,
herzustellen, ist hierbei n die Bit-Komplexität der Eingabe, denn eine RAM kann
eine Turing-Maschine in polynomieller Zeit simulieren und umgekehrt. Die Implementierungsdetails eines effizienten Algorithmus, d. h. das zugrunde liegende
Maschinenmodell ist demnach irrelevant.
Effiziente Algorithmen und polynomielle Laufzeit
Ein Problem kann nunmehr effizient gelöst werden, wenn dafür ein Algorithmus
gefunden wird, der eine polynomielle Laufzeit hat. Die Klasse der Probleme, für
die es eine effiziente Lösung gibt, wird als P bezeichnet. Die Definition scheint
großzügig gewählt zu sein, weil auch Polynome ganz beachtliche Wachstumsraten
haben können, aber es gibt viele Probleme, für die keine effiziente Lösung bekannt
ist. Das vielleicht prominenteste Problem dieser Art ist das Travelling-SalesmanProblem: Ein Handlungsreisender muss n Städte genau einmal besuchen und dabei
den kürzesten Gesamtweg finden. Für dieses Problem ist bisher kein effizienter
RAM-Algorithmus gefunden worden. Es bleibt wohl nichts anderes übrig, als alle
n! möglichen Routen durchzuprobieren, und dies führt zu einer exponentiellen
Laufzeit.
P 6= NP?
Das Problem des Handlungsreisenden lässt sich gut parallelisieren; die zu untersuchenden Routen lassen sich bspw. auf die Prozessoren einer PRAM verteilen.
Jeder Prozessor schreibt die von ihm gefundene kürzeste Route in eine gemeinsame Speicherzelle, wobei der kleinste Wert gewinnt. Da eine PRAM aber nur
über endlich viele Prozessoren verfügt, führt dies auch nicht zu einem effizienten
Algorithmus. Man benötigte so etwas wie eine beliebig große PRAM mit einem
separaten Prozessor für jede Route. Praktisch ist das zwar nicht realisierbar, aber
theoretische ist dieses Vorgehen sehr interessant, weil damit eine neue Klasse von
Problemen definiert werden kann, nämlich NP. NP steht für „nichtdeterministisch
polynomiell“. Informal kann man sich eine Lösung für ein Problem aus NP – wie
beschrieben – so vorstellen, dass alle Lösungswege parallel durchprobiert werden,
wobei zumindest der gewinnende polynomielle Laufzeit hat. Dass P ⊆ NP gilt, ist
offensichtlich, aber die Frage, ob NP ⊆ P gilt, ist wohl das berühmteste offene Problem der Informatik. Es wird zwar angenommen, dass P 6= NP ist, aber bewiesen
ist dies keineswegs.
NP-hart und
NP-vollständig
Man bezeichnet ein Problem ph als NP-hart, wenn jedes Problem aus der Klasse NP
auf dieses Problem polynomiell reduzierbar ist. Das heißt, dass für jedes Problem
p ∈ NP ein Verfahren mit polynomieller Laufzeit existiert, das p auf ph abbildet.
Durch die Lösung von ph kann somit auch p effizient gelöst werden. Ist ph selbst in
NP, so bezeichnet man dieses Problem als NP-vollständig. Das Travelling-SalesmanProblem ist ein NP-vollständiges Problem, und es existieren ganze Sammlungen
NP-vollständiger Probleme6 . Die Klasse der NP-vollständigen Probleme ist eine
echte Teilmenge von NP. Wenn gezeigt werden könnte, dass nur ein einziges dieser
NP-vollständigen Probleme effizient gelöst werden könnte, dann wäre P = NP
bewiesen, weil damit alle Probleme aus NP auf dieses polynomiell reduzierbar und
damit effizient lösbar wären. Da bis heute kein effizient lösbares NP-vollständiges
Problem gefunden wurde, nimmt man an, dass P 6= NP gilt.
weitere Komplexitätsklassen
„Oberhalb“ von NP existieren weiter Komplexitätsklassen von Problemen, bspw.
EXPT IME und NEXPT IME. Erstere ist die Klasse der Probleme, die in deterministischer, exponentieller Zeit gelöst werden können, letztere ist die Klasse der
6
siehe z. B. bei http://de.wikipedia.org/wiki/Karps_21_NP-vollst%C3%A4ndige_Probleme.
2.11 Strukturierte Datentypen
Probleme, die in nichtdeterministischer, exponentieller Zeit gelöst werden können.
Es gilt zwar NP ⊆ EXPT IME, aber nicht EXPT IME ⊆ NP.
2.11 Strukturierte Datentypen
Strukturierte Datentypen dienen zur strukturierten Gruppierung von Speicherzellen. Einige strukturierte Datentypen haben wir bereits im Studienbrief 1 kennengelernt, andere sind Ihnen aus dem Modul „Programmierkonzepte“ bekannt.
Die Operationen darauf und die konkrete Realisierung in C sind bekannt oder
leicht nachvollziehbar. Diese strukturierten Datentypen werden hier nur kurz der
Vollständigkeit halber aufgelistet, der ganz überwiegende Teil dieses Abschnittes
beschäftigt sich dann mit Bäumen und Graphen.
Array oder Feld: Ein Array besteht aus Elementen des gleichen Datentyps.
Struktur: Eine Struktur besteht aus einer festen Anzahl von Elementen verschie-
dener Datentypen.
File: Ein File besteht aus einer unbegrenzten Folge von Elementen.
Stapel (auch Stack bzw. Keller): Ein Stapel repräsentiert eine dynamische Fol-
ge von Elementen mit den Operationen „leeren“, „einfügen hinten“ (push)
und „streichen hinten“ (pop) nach dem LIFO (Last In First Out)-Prinzip.
Schlange (Queue): Eine Schlange repräsentiert eine dynamische Folge von Ele-
menten mit den Operationen „leeren“, „einfügen hinten“ und „streichen
vorne“ nach dem FIFO (First In First Out)-Prinzip.
Stapel und Schlangen7 können mit Feldern implementiert werden. Bei einem
Stapel existiert ein Zeiger auf das letzte Element, bei einer Schlange ein
Zeiger auf das erste und ein Zeiger auf das letzte Element. Schlangen können
auch zyklisch sein.
Liste:
Eine Liste repräsentiert eine Folge, die beliebig veränderbar ist. Jedes
Listenelement enthält neben der Information einen oder mehrere Verweise
auf benachbarte Elemente. Die wichtigsten Listen sind:
• einfach verkettete Liste
• doppelt verkettete Liste
• zyklisch verkettete Liste
Auf Listen werden die Operationen „einfügen“ und „streichen“ ausgeführt.
Je nach Art der Implementierung (meist durch Felder oder dynamische Datenstrukturen) sind die Kosten der Elementaroperationen unterschiedlich.
2.11.1 Bäume
In Studienbrief 1 wurde bereits der binäre Suchbaum als Beispiel für eine rekursive
Datenstruktur informal eingeführt. Bäume sind mit die wichtigsten Datenstrukturen der Informatik und werden oftmals zur Speicherung von Daten benutzt,
7
Eine spezielle Variante der Schlangen, nämlich Warteschlangen mit Prioritäten (Priority Queues),
werden in Abs. 3.4.5 behandelt.
Seite 85
Seite 86
Studienbrief 2 Basiskonzepte und Datenstrukturen
insbesondere auch bei Datenbanken. Ein Baum kann formal wie folgt definiert
werden.
Definition 2.4: Baum
D
Bäume bestehen aus Knoten und Blättern. Sei V = {v1 , v2 , . . . } eine unendliche Menge von Knoten und B = {b1 , b2 , . . . } eine unendliche Menge von
Blättern, dann ist die Menge der Bäume über V und B wie folgt definiert:
1. Jedes Element bi ∈ B ist ein Baum. bi ist die Wurzel dieses Baums.
2. Wenn T1 , . . . , Tm (m ≥ 1) Bäume mit paarweise verschiedenen Mengen
von Knoten und Blättern sind und v ∈ V ein neuer Knoten ist, dann
ist das (m + 1)-Tupel T = hv, T1 , . . . , Tm i ein Baum. Der Knoten v ist die
Wurzel des Baums, m ist der Grad von v, Ti ist der i-te Unterbaum
von T , und die Wurzel von Ti ist das i-te Kind von v. v heißt Elter8
dieses Kindes. Die Kinder eines Elterknotens heißen Geschwister.
Der Grad bzw. die Ordnung eines Baums ist das Maximum aller Grade
seiner Knoten.
Binärbaum
Abb. 2.2 zeigt einen Baum in der üblichen graphischen Darstellung. Nach Konvention wird die Wurzel eines Baums als oberster Knoten gezeichnet, die Blätter sind
demnach unten zu finden. Die Pfeile verweisen auf die Kinder eines Knotens. Der
Baum hat Grad 4. Bäume mit Grad 2 werden als Binärbäume bezeichnet.
Abb. 2.2: Ein Baum
In einem Baum existieren Pfade, die stets von „oben“ nach „unten“ verlaufen, also
von Richtung Wurzel in Richtung Blätter. Ein Pfad führt von einem Elterknoten
zu einem seiner Kinder, dann ggf. zu einem dessen Kinder usw. Die Tiefe eines
8
In der Literatur ist auch der Begriff Vater üblich, dem neuen Trend folgend wählen wir jedoch die
Unisex-Bezeichnung Elter.
2.11 Strukturierte Datentypen
Seite 87
Baums wird durch den längsten möglichen Pfad von der Wurzel zu einem Blatt
bestimmt. Der Beispielbaum von oben hat bspw. die Tiefe 3.
Definition 2.5: Tiefe und Pfade
D
Sein B ein Baum und p = (v0 , . . . , vt )(t ≥ 1) eine Folge von Knoten und
Blättern9 , für die gilt: vn+1 (0 ≤ n ≤ t − 1) ist Kind von vn , dann ist p ein
Pfad in B der Länge t.
Sei v die Wurzel von B und sei k ein Knoten oder ein Blatt von B, dann ist
die Länge des Pfades von v nach k die Tiefe von k.
Die Tiefe T des Baums B ist das Maximum aller Tiefen der Blätter des
Baums.
Suchbäume
Ein Baum wird zum Suchbaum, wenn seinen Knoten und Blättern Schlüssel zugeordnet werden. Schlüssel entstammen einer Wertemenge W (z. B. Integer-Zahlen),
die eine Ordnungsrelation besitzt. Es kann also jeweils für zwei Schlüssel angegeben werden, ob ein Schlüssel kleiner, größer oder gleich dem anderen ist. Die im
Baum abgelegten Schlüssel unterliegen einer Ordnung. Bei einem Binärbaum ist
diese leicht definierbar: Der Schlüssel eines Knotens ist größer als der (oder gleich
dem) Schlüssel der Wurzel seines linken Unterbaums und kleiner als der (oder
gleich dem) Schlüssel der Wurzel seines rechten Unterbaums. Abb. 2.3 zeigt einen
solchen binären Suchbaum. Diese Methodik kann verallgemeinert werden:
Definition 2.6: Suchbaum
Sei B ein Baum der Ordnung n und sei S eine Schlüsselmenge. B ist ein
n-närer Suchbaum, wenn gilt:
1. Jedem Knoten und jedem Blatt ist eine Schlüsselmenge S0 ⊆ S zugeordnet. S0 ist eine geordnete Menge < s1 , . . . , sk > mit 1 ≤ k ≤ n − 1, für
die gilt: s1 ≤ s2 ≤ · · · ≤ sk .
2. Jeder Knoten v mit Grad m (2 ≤ m ≤ n) verfügt über eine geordnete
Schlüsselmenge < s1 , . . . , sm−1 > von m − 1 Schlüsseln. Für alle Schlüssel si (1 ≤ i ≤ m − 1) gilt: si ist ≥ alle Schlüssel der Wurzel des i-ten
Unterbaums von v. Zudem ist Schlüssel sm−1 ≤ alle Schlüssel des
m-ten Unterbaums von v.
3. Jeder Knoten v mit Grad 1 verfügt über genau einen Schlüssel s, wobei
s ≥ alle Schlüssel der Wurzel des einzigen Unterbaums von v gilt.
Abb. 2.4 zeigt einen Suchbaum der Ordnung 4, der dieser Definition entspricht. Alle
Knoten und Blätter haben maximal 3 Schlüssel, und die Schlüssel eines Knotens
„teilen“ die Schlüssel ihrer Unterbäume.
9
Nach Def. 2.4 kann nur vt ein Blatt sein
D
Seite 88
Studienbrief 2 Basiskonzepte und Datenstrukturen
Abb. 2.3: Binärer Suchbaum
10
20
4
1
7
17
19
Abb. 2.4: Suchbaum
mit Ordnung m=4
20 60
10
5 7
30 35 50
11 12 15
21 24
33
38 43 45
70 90
52 55
65 67 69
80 85
96
Kontrollaufgabe 2.4
K
Warum entspricht der Binärbaum in Abb. 2.3 nicht zu 100% dieser Definition? Wie könnte der Baum bspw. umgebaut werden, damit er der Definition
entspricht?
Oder anders herum: Welcher relativ willkürlicher Passus der Definition,
der eher implementierungstechnische Gründe bei n-nären Suchbäumen hat,
müsste geändert werden, damit der Baum der Definition entspricht?
Speichern von Informationen in Suchbäumen
Die Schlüssel können als Informationen aufgefasst werden, die in einem Suchbaum gespeichert sind. Wir unterscheiden knotenorientierte und blattorientierte
Suchbäume. Bei einem knotenorientierter Baum sind die Informationen über alle
Knoten und Blätter des Baums verteilt. Bezüglich Informationsgehalt wird also
nicht zwischen Knoten und Blättern unterschieden. Im Gegensatz dazu sind bei einem blattorientierten Suchbaum die Informationen ausschließlich in den Blättern
vorhanden, d. h. die Schlüssel in den Knoten bilden lediglich Wegweiser zu den
Blättern.
knotenorientierter Suchbaum
Betrachten wir zunächst den Fall eines knotenorientierten Suchbaums. Wenn die
Schlüssel Informationen darstellen, dann bedeuten doppelte Schlüssel auch doppelte Informationen. Das mehrfache Vorhandensein von Schlüsseln macht bei
manchen Anwendungen Sinn, bei anderen nicht. Im Folgenden werden wir immer
von ungleichen Schlüsseln bei knotenorientierten Bäumen ausgehen. Formal ist
dazu in der Definition die Forderung zu ergänzen, dass alle Schlüsselmengen aller
Knoten und Blätter disjunkt sind.
blattorientierter Suchbaum
Bei blattorientierten Bäumen ist es etwas komplizierter, wenn einerseits keine
doppelten Informationen gespeichert werden sollen, aber andererseits einzelne
Blättern und Knoten identische Schlüssel haben dürfen. Dazu werden zwei Schlüsselmengen SK ⊆ S für Knoten und SB ⊆ S für Blätter definiert, die nicht disjunkt,
2.11 Strukturierte Datentypen
Seite 89
sondern insbesondere auch identisch sein können. Die Schlüsselmengen der einzelnen Knoten müssen allerdings paarweise disjunkt sein, ebenso die der einzelnen
Blätter.
Implementierung von Bäumen
Die Implementierung eines Baums kann mittels Felder oder dynamisch mit Strukturen realisiert werden, wie wir es bereits in Studienbrief 1 kennengelernt haben.
Ist die maximale Größe eines Baums zur Kompilierungszeit bekannt, so bietet sich
die Speicherung in Feldern an, weil dadurch der Zusatzaufwand zur dynamischen
Speicherallokation eingespart werden kann. Die Implementierung kann bei einem
(vollständigen) binären Baum wie folgt durchgeführt werden:
Wenn M = {v1 , . . . , vn , b1 , . . . , bn+1 } die Menge der Knoten und Blätter ist, so wird
eine beliebige injektive Funktion pos : M → N festgelegt, für die gilt:
1. pos(x), x ∈ M gibt die Position (Index) im Feld an.
2. inh(pos(x)) ist der „Inhalt“ des Knotens oder Blatts, also z. B. der Schlüssel.
3. LK(pos(x)) ist die Position des linken Kindes von x.
4. RK(pos(x)) ist die Position des rechten Kindes von x.
Das Feld zur Speicherung des binären Baums aus Abb. 2.3 könnte bspw. wie folgt
aufgebaut sein:
pos(x)
1
2
3
4
5
6
7
inh(pos(x))
10
4
20
1
7
17
19
LK(pos(x))
2
4
6
-
RK(pos(x))
3
5
7
-
Tabelle 2.1: Baum, gespeichert in einem Feld
Da die Funktion pos nach N abbildet, können fehlende Kinder durch leere 0Verweise in LK bzw. RK angezeigt werden. Insbesondere haben Blätter zwei 0Verweise und können darüber identifiziert werden. Diese Implementierung ist
leicht auf n-näre Bäume erweiterbar. Im Folgenden gehen wir allerdings stets von
einer Implementierung von Bäumen durch dynamische Strukturen aus, wobei
Zeiger auf Folgeknoten verweisen.
Durchmusterung und Grundoperationen
Folgende Grundoperationen werden auf Suchbäumen ausgeführt:
• Suche nach einem Schlüssel
• Einfügen eines Schlüssels
• Entfernen eines Schlüssels
Des Weiteren ist die Durchmusterung bzw. strukturierte Durchsuchung eines
Baums eine wichtige Operation, die allerdings nicht auf Suchbäume beschränkt
ist. Wegen des rekursiven Baumaufbaus ist eine Durchmusterung leicht durch
Aufruf der folgenden rekursiven Funktion möglich, der die Wurzel des Baums
Durchmusterung von
Suchbäumen
Seite 90
Studienbrief 2 Basiskonzepte und Datenstrukturen
als Parameter übergeben wird. Hier werden bspw. die Schlüssel systematisch
ausgedruckt.
Quelltext 2.2
Q
1
durchmustern(knoten s) {
drucke Schluessel von s aus;
3
for {alle Kinder k von links nach rechts}
4
durchmustern(k);
5 }
2
Präordnung und
Postordnung
Die „Aktion“, also in dem Fall das Ausdrucken der Schlüssel, findet zuerst an der
Wurzel statt, dann an der Wurzel des am weitesten links stehenden Unterbaums
usw. Man spricht daher von einer Präordnung, weil die Aktion vor dem rekursiven
Aufruf der Unterbäume stattfindet. Die Präordnung kann auch mit der Aktionsreihenfolge wK1 K2 . . . Km gekennzeichnet werden, wobei w die Wurzel und Ki die
Kinder symbolisieren. Vertauscht man im Listing Zeile (2) mit den Zeilen (3) und
(4), so wird der Baum in Postordnung durchmustert, man schreibt K1 K2 . . . Km w.
In Abb. 2.5 sind die Knoten eines Baums in der Reihenfolge nummeriert, in denen
die Aktionen bei Postordnung und Präordnung ausgeführt werden.
Abb. 2.5: Durchmusterung mit Präordnung und Postordnung
1 12
Präordnung
Postordnung
28
31
lexikographische Ordnung
K
75
44
52
10 11
63
87
11 9
12 10
96
Speziell bei binären Bäumen existiert noch der Begriff der symmetrischen oder
lexikographischen Ordnung, die der Aktionsreihenfolge LwR entspricht, wobei L
bzw. R für den linken bzw. rechten Unterbaum stehen.
Kontrollaufgabe 2.5
Wie muss das Listing verändert werden, um eine Durchmusterung eines
Binärbaums in lexikographischer Ordnung zu erzeugen?
Die Laufzeit der Durchmusterung ist offensichtlich Θ(n) (n gleich Anzahl Knoten und Blätter), da jeder Knoten und jedes Blatt Θ(1) „Besuche“ hat. Auf einen
formalen Beweis soll an dieser Stelle verzichtet werden.
Nun zu den drei Grundoperationen. Wir betrachten hier knotenorientierte Suchbäume, denn beim Einfügen und Streichen ist für blattorientierte Suchbäume nicht
2.11 Strukturierte Datentypen
Seite 91
allgemein formulierbar, was zu tun ist. Oder anders ausgedrückt: Es hängt weitgehend von äußeren Parametern ab, wann und warum ein Schlüssel in einem Knoten
ergänzt bzw. gelöscht wird. Wir nehmen weiterhin an, dass die Schlüssel w im
Baum vom Typ Integer sind.
Die Suche nach einem Schlüssel (s. nachfolgendes Listing) ist dem Durchmusterungsverfahren sehr ähnlich, nur dass hier nicht alle Kinder eines Knotens besucht
werden, sondern nur der Unterbaum, in dem der Schlüssel enthalten sein kann.
Dieser Unterbaum ist wegen der Struktur des Suchbaums eindeutig. Da wir nicht
gefordert haben, dass jeder innere Knoten die maximal mögliche Anzahl von Kindern haben muss, besteht keine Gewähr, dass es diesen Unterbaum tatsächlich
gibt; existiert er nicht, so wird die Suche erfolglos abgebrochen (Zeile 12). Das
Ergebnis ist vom Typ knoten, womit ein Zeiger auf einen Knoten gemeint ist. Dieser
Zeiger zeigt bei einer erfolgreichen Suche auf den Knoten bzw. das Blatt, das den
Schlüssel enthält. Die Laufzeit einer Suche entspricht offensichtlich der Länge des
Suchpfades und damit maximal der Tiefe des Baums.
Quelltext 2.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Q
knoten suche(knoten s; int w) {
knoten v;
if (w in Schluesselmenge von s)
return s;
else if (s ist Blatt)
return NULL
else {
v = Wurzel des Unterbaums von s, in dem der
Schluessel gemaess der Schluesselordnung
enthalten sein kann;
if (v == NULL)
return NULL
else return suche(v,w);
}
}
Die Grundoperationen „einfügen“ und „streichen“ betrachten wir an dieser Stelle
nur für binäre knotenorientierte Suchbäume mit unterschiedlichen Schlüsseln.10
Die Verfahren werden für spezielle Suchbäume in Studienbrief 4 gesondert betrachtet, weil sie stark von der genauen Definition dieser Bäume abhängen. In einem
10
Suche nach einem
Schlüssel
Die Regel, dass Kinder immer „linksbündig“ an einen Knoten gehängt werden sollen, vernachlässigen
wir hierbei. Zur Umsetzung wären ggf. geringfügige Transformationen auf Blattebene beim Einfügen
oder Streichen eines Schlüssels notwendig, die für die Betrachtung der generellen Verfahren allerdings
unwichtig sind.
Einfügen eines Schlüssels
Seite 92
Studienbrief 2 Basiskonzepte und Datenstrukturen
binären Suchbaum enthält jeder Knoten bzw. jedes Blatt genau einen Schlüssel.
Das Einfügen eines Schlüssels geschieht folgendermaßen:
Quelltext 2.4
Q
1
2
3
4
5
6
7
einfuege(knoten s; int w) {
knoten v;
if(suche(s,w) == NULL){
v = Knoten, in dem die Suche endete;
haenge neues Blatt mit Schluesel w an v
}
}
Wenn der Schlüssel bereits vorhanden ist, ist nichts zu tun (Zeile 3). Ansonsten
endet die Suche in einem Knoten oder in einem Blatt. Wir gehen davon aus, dass
durch eine kleine Erweiterung des Suchverfahrens ein Zeiger v auf diesen Knoten
bzw. dieses Blatt gewonnen werden kann. An v wird ein neues Blatt mit dem neuen
Schlüssel angehängt. Es ist zu beachten, dass auch in dem Fall, dass die Suche in
einem Knoten endet, kein Unterbaum an der Einfügestelle existieren kann, denn
sonst hätte die Suche nicht in diesem Knoten geendet. Bei dieser Vorgehensweise
wird also immer ein neues Blatt an den Baum angehängt. Es wird niemals ein
innerer Knoten eingefügt.
Die Laufzeit entspricht offensichtlich auch der Länge des Suchpfades und damit
maximal der Tiefe des Baums, da Erzeugung und Einhängen eines Unterbaums
konstante Zeit benötigen.
Streichen eines Schlüssels
Nun zum Streichen eines Schlüssels (s. nachfolgendes Listing). Wenn der Schlüssel
nicht vorhanden ist, muss er auch nicht gelöscht werden (Zeile 3). Ansonsten liefert
die Suche einen Zeiger auf ein Blatt oder auf einen Knoten. Ist der Schlüssel in
einem Blatt, so wird dieses gestrichen (Zeile 5). Ist der Schlüssel in einem Knoten,
so wird ein kleiner Trick angewandt. Es wird der am weitestem rechts stehende
Knoten x im linken Unterbaum von v gesucht (Zeilen 7 und 8), sofern dieser
Unterbaum vorhanden ist. Ansonsten wird der am weitesten links stehenden
Knoten im rechten Unterbaum gesucht (Zeile 9). Einer der beiden Unterbäume
muss vorhanden sein, denn sonst wäre der Knoten ein Blatt. Ist der linke Unterbaum
vorhanden, und ist x der darin am weitesten rechts stehende Knoten, so ist dessen
Schlüssel kleiner als der Schlüssel in v, aber größer als alle anderen Schlüssel im
linken Unterbaum von v. Für den rechten Unterbaum gilt dies analog. Nun sind
zwei Fälle zu unterscheiden:
1. Ist x ein Blatt, so wird dessen Schlüssel nach v übertragen, womit die Ordnung im Baum erhalten bleibt. Blatt x wird dann gelöscht (Zeilen 11 und
12). Abb. 2.6 zeigt das Verfahren an einem Beispiel; hier wird der Schlüssel
30 aus dem Baum gelöscht.
2. Ist x ein Knoten, so wird der Schlüssel von x nach v transferiert, und anschließend wird der Schlüssel von x im Unterbaum gelöscht, dessen Wurzel
x ist. Dies erfolgt rekursiv, bis die Blattebene erreicht ist (Zeilen 15 und 16).
Bei diesem Verfahren müssen niemals innere Knoten aus einem Baum gelöscht
werden. Die Laufzeit des Verfahrens entspricht offensichtlich maximal der Tiefe
des Baums.
2.11 Strukturierte Datentypen
Seite 93
Quelltext 2.5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Q
streiche(knoten s; int w) {
knoten v,x;
if((v = suche(s,w)) != NULL){
if(v ist ein Blatt)
loesche v;
else {
if (linker Unterbaum von v ist vorhanden)
x = rechtester Knoten im linken Unterb. von v;
else x = linkester Knoten im rechten Unterb. von v;
if (x == BLATT) {
ersetze Schluessel in v durch den in x;
loesche x;
}
else {
ersetze Schluessel in v durch den in x;
streiche(x, Schlussel von x);
}
}
}
}
10
9
10
30
33
25
12
Abb. 2.6: Streichen in
einem binären Suchbaum
27
⇒
9
27
25
33
12
Kontrollaufgabe 2.6
Beschreiben Sie die den Löschvorgang des Schlüssels 30 im Baum aus Abb.
2.6, wenn der Knoten mit Schlüssel 27 nicht existiert.
Balancierte Bäume
Wir nennen einen Suchbaum geeignet11 , wenn alle drei Grundoperationen in Laufzeit O(log2 n) ausgeführt werden können, wobei n die Summe der Knoten und
Blätter des Baums ist. Die bisherige Definition bzw. die im vorangegangenen Abschnitt vorgestellten Grundoperationen führen nicht automatisch zu geeigneten
Suchbäumen, was leicht am Beispiel eines Binärbaums gezeigt werden kann. Ein
durch fortlaufendes Einfügen erzeugter Binärbaum könnte in der Form „entartet“ sein, dass jeder Knoten nur ein Kind hat. Der Baum ist dann eigentlich kein
11
Es existiert in der Literatur (z. B. [Schöning, 1997]) der Begriff eines optimalen Suchbaums, der über
Zugriffsverteilungen und gewichtete Pfadlängen definiert ist. Der einfachere und schwächere Begriff
„geeigneter Suchbaum“ soll uns an dieser Stelle aber reichen.
K
Seite 94
Studienbrief 2 Basiskonzepte und Datenstrukturen
Baum mehr, sondern eine Liste, bei der im schlimmsten Fall alle Knoten nacheinander durchsucht werden müssen, was zu einer Laufzeit von O(n) führt. Man
könnte darauf vertrauen, dass solche Entartungen sehr selten auftreten, oder man
könnte von Zeit zu Zeit den Baum komplett neu aufbauen. Der sicher elegantere
Weg ist es allerdings, balancierte Bäume zu benutzen. Geeignete Suchbäume sind
balanciert.
Definition 2.7: Balancierter Baum
D
Sei B ein Baum und sei n die Anzahl der Knoten und Blätter des Baums. B
heißt balanciert, wenn für die Tiefe T des Baums gilt:
T = O(log2 n).
höhenbalancierter Baum
Das heißt informal, dass Knoten in der Regel mehr als ein Kind und alle Blätter
in etwa dieselbe Tiefe haben. Man spricht deswegen auch von einem höhenbalancierten Baum. Beispiele für höhenbalancierte Bäume sind AVL-Bäume und
B-Bäume. Darauf werden wir in Studienbrief 4 zurückkommen.
gewichtsbalancierter Baum
Neben höhenbalancierten Bäumen existiert noch gewichtsbalancierte Bäume. Dabei haben alle Unterbäume eines Knotens in etwa dieselbe Anzahl an Knoten und
Blättern. Im Fall eines binären Suchbaums B wird eine beschränkte Balance α
vorgegeben und die Wurzelbalance ρ(B) wie folgt definiert:
|Br |
l|
ρ(B) = |B
|B| = 1 − |B| , wobei |B| die Anzahl der Blätter des Baums und |Bl | (|Br |) die
Anzahl der Blätter des linken (rechten) Unterbaums bezeichnen.
Ein Baum B ist von beschränkter Balance α, wenn für jeden Unterbaum B0 von B,
dessen Wurzel ein Knoten ist, α ≤ ρ(B0 ) ≤ 1−α gilt. Diese Bäume werden als√BB[α]Bäume bezeichnet, und es kann gezeigt werden, dass diese für 14 < α ≤ 1 − 22 eine
logarithmische Tiefe haben (s. z. B. [Mehlhorn, 1986]).
Wenn ein balancierter Suchbaum vorliegt, so ist die Suche darin mit dem genannten
Verfahren wegen der logarithmischen Tiefe in Zeit O(log2 n) möglich. Schwieriger
ist das Einfügen und Entfernen von Schlüsseln. Einerseits haben die Knoten und
Blätter begrenzte Kapazität, d. h. die maximale Anzahl an Schlüsseln ist dort fest
vorgegeben. Andererseits sind keine leeren Knoten und Blätter erlaubt. Demzufolge
müssen balancierte Suchbäume beim Einfügen und Entfernen von Schlüsseln ggf.
umgebaut werden, d. h. es müssen Knoten und Blätter eingefügt bzw. entfernt
werden, ohne dass die Balance-Eigenschaft verloren geht. Die in Studienbrief 4
vorgestellten Suchbäume sind geeignete Suchbäume, und es werden jeweils die
Algorithmen vorgestellt, die das Einfügen und Entfernen von Schlüsseln in Zeit
O(log2 n) ermöglichen.
2.11.2 Graphen
Ein Graph ist eine dem Baum verwandte Datenstruktur, dem aber insbesondere
die strenge Hierarchie eines Baums fehlt. Es gibt weder eine Wurzel am Anfang,
noch gibt es Blätter am Ende. Ein Graph besteht nur aus Knoten, und Kanten
verweisen von Knoten zu nachfolgenden Knoten. Abb. 2.7 zeigt einen Graphen
mit 5 Knoten und 6 Kanten. Die Kanten werden als Pfeile zwischen Quell- und
Zielknoten gezeichnet. Viele Strukturen der realen Welt lassen sich als Graphen
repräsentieren. Als Beispiel sei eine Straßennetzkarte genannt. Die Straßen selbst
entsprechen den Kanten, und Städte oder Straßenkreuzungen (je nach Detailgehalt
2.11 Strukturierte Datentypen
Seite 95
der Karte) entsprechen den Knoten. Auf einer solchen, als Graph repräsentierten,
Straßennetzkarte, können bspw. kürzeste Fahrtrouten berechnet werden, wenn die
Kanten mit Kilometerangaben versehen werden. Andere Anwendungsgebiete von
Graphen sind die Netzanalyse in der Elektrotechnik, die Erkennung molekularer
Strukturen in der organischen Chemie, die Segmentierung von Computerprogrammen, der Entwurf integrierter Schaltkreise (VLSI-Entwurf) usw. Formal sind
Graphen wie folgt definiert:
Definition 2.8: Graphen
1. Ein endlicher Graph G = (V, E) besteht aus einer endlichen Menge
von Knoten V = {v1 , . . . , vn } (n = |V |) und einer endlichen Menge von
Kanten E, E ⊆ V ×V . (v, w) ∈ E heißt Kante von v nach w. Der Knoten
v heißt Quelle, der Knoten w Ziel der Kante (v, w).
D
2. Ein Pfad p =< v0 , . . . , vk > in G von Knoten x nach Knoten y ist eine
Folge v0 . . . vk von Knoten mit v0 = x und vk = y, und (vi , vi+1 ) ∈ E für
0 ≤ i < k. k heißt Länge (oder Kardinalität) des Pfades.
Ein Pfad heißt Zyklus, wenn x = y gilt für k ≥ 1.
3. Ein Pfad p =< v0 , . . . , vk > heißt einfach, wenn vi 6= v j gilt für i 6=
j (außer vielleicht v0 = vk ). Ein einfacher, zyklischer Pfad, der alle
Knoten des Graphen beinhaltet, heißt Hamilton-Zyklus.
4. Ein Graph G heißt azyklisch, wenn es keinen Zyklus in G gibt.
5. Ein Graph G heißt ungerichtet, wenn mit (v, w) ∈ E stets (w, v) ∈ E gilt.
Ansonsten heißt er gerichtet. Ein Paar von Kanten (v, w) und (w, v) bei
einem ungerichteten Graphen schreiben wir abkürzend als {v, w}.
6. Der Ingrad eines Knotens v ist die Anzahl der Knoten w, von denen
eine Kante zu v existiert, der Outgrad eines Knotens v ist die Anzahl
der Knoten w, zu denen eine Kante von v existiert:
indeg(v) = |{w, (w, v) ∈ E}|
outdeg(v) = |{w, (v, w) ∈ E}|
Bei einem ungerichteten Graph sind Ingrad und Outgrad jedes Knotens identisch und bezeichnen den Grad des Knotens.
7. Ein Graph G heißt gewichtet, wenn jeder Kante (i, j) eine Zahl ci, j ∈ R
zugeordnet ist. ci j heißt das Gewicht von Kante (i, j).12
8. Sei G = (V, E) ein Graph. Der Graph G0 = (V 0 , E 0 ) heißt Untergraph
von G, wenn V 0 ⊆ V und E 0 ⊆ E gilt.
Beispiel 2.4
B
Bild 2.7 zeigt einen (ungewichteten) gerichteten, azyklischen Graphen mit der Knotenmenge V = {1, 2, 3, 4, 5} und den Kanten
E = {(1, 2), (1, 3), (2, 4), (2, 5), (3, 5), (4, 5)}.
Wir legen künftig n = |V | und e = |E| als Bezeichnung der Anzahl von Knoten bzw.
Kanten eines Graphen fest. Des Weiteren bezeichnen wir künftig einen azyklischen,
ungerichteten Graphen als DAG (directed acyclic graph).
12
Wir legen fest, dass bei einem ungerichteten Graph stets ci j = c ji für alle Kanten (i, j) gilt.
DAG
Seite 96
Studienbrief 2 Basiskonzepte und Datenstrukturen
Abb. 2.7: Ein Graph
1
4
2
5
3
Die enge Verwandtschaft von Bäumen und Graphen zeigt die folgende Definition.
Ein Baum ist somit eine Sonderform eines gerichteten Graphen.
D
Definition 2.9: Wälder und Bäume
1. Ein gerichteter Graph A = (V, E) heißt gerichteter Wald, wenn gilt:
a) A ist azyklisch.
b) indeg(v) ≤ 1 für alle v ∈ V .
Ein Knoten v ∈ V heißt Wurzel von A, wenn indeg(v) = 0 gilt.
2. Ein gerichteter Wald heißt Baum, wenn er genau eine Wurzel hat.
3. Sei G = (V, E) ein gerichteter Graph. Ein gerichteter Wald A = (V, E 0 )
mit E 0 ⊆ E heißt aufspannender Wald von G. Ist A ein Baum, so heißt
er aufspannender Baum (spanning tree).
B
Beispiel 2.5
Der Graph in Abb. 2.7 ist offensichtlich kein Wald, da Knoten „5“ Ingrad
3 hat. Beispielsweise durch Streichen der Kanten (3,5) und (4,5) kann ein
aufspannender Baum des Graphen erzeugt werden.
Repräsentation von Graphen
Für Graphenalgorithmen spielt es eine große Rolle, wie die Repräsentation eines
Graphen im Speicher eines Rechners realisiert wird, wie also Knoten und Kanten
real gespeichert werden. Üblich ist die Repräsentation als Adjazenzmatrix oder als
Adjazenzliste.
D
Definition 2.10: Adjazenzmatrix
Sei G = (V, E) ein Graph und sei n = |V |. Eine Adjazenzmatrix ist eine (n × n)boolesche Matrix A = (ai j )1≤i, j≤n , für die gilt:
ai, j =
1
0
, falls (i, j) ∈ E
, falls (i, j) ∈
/E
2.11 Strukturierte Datentypen
Seite 97
Definition 2.11: Adjazenzliste
D
Sei G = (V, E) ein Graph und sei n = |V |. Eine Adjazenzliste (Nachbarschaftsliste) von G besteht aus n linearen Listen, wobei gilt:
Liste i(1 ≤ i ≤ n) enthält alle j mit (i, j) ∈ E.
Die n Listenköpfe sind in einem Feld13 der Größe n gespeichert.
Beispiel 2.6
B
In Abb. 2.8 sind Adjazenzmatrix und Adjazenzliste eines Graphen dargestellt. Die Knoten sind der Deutlichkeit halber durchnummeriert. Bei einem
ungerichteten Graph wäre die Adjazenzmatrix symmetrisch.
1 2 3 4 5
1
4
2
5
3
1
2
3
4
5
0
1
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
1
1
0
0
0
1
2
3
4
5
Abb. 2.8: Adjazenzmatrix
und Adjazenzliste
5
1
5
3
Eine dritte Art der Repräsentation haben wir bereits im Zusammenhang mit Bäumen kennengelernt: Adjazenzfelder. In jedem Knoten sind die ausgehenden Kanten
jeweils in einem Feld gespeichert. Diese Art der Repräsentation ist allerdings i. Allg.
nur bei einem kleinen, maximalen Outgrad sinnvoll.
Es ist offensichtlich, dass eine Adjazenzmatrix O(n2 ) Speicherplatz benötigt. Eine
Adjazenzliste benötigt O(n + e) Speicherplatz. Da e ≤ n2 gilt, ergibt das ebenfalls
einen Speicherplatzbedarf von O(n2 ). Bei „dünnen“ Graphen (e n2 ) wird allerdings sehr viel weniger Speicher als bei einer Adjazenzmatrix benötigt.
Adjazenzmatrix
Welche Art der Repräsentation gewählt wird, hängt in erster Linie davon ab, welche
Algorithmen für einen Graphen implementiert werden sollen, welche Zugriffe
und Operationen also auf einem Graphen ausgeführt werden sollen. Es ist die
Repräsentation zu wählen, welche die benötigten Implementierungen möglichst
effizient unterstützt. Oftmals ist die Adjazenzliste die geeignetere Repräsentation,
allerdings ist hier die Bestimmung der Vorgänger eines Knotens schwieriger als
bei einer Adjazenzmatrix.
Die Frage, ob eine bestimmte Kante in einem Graphen vorhanden ist, ist bei einer
Adjazenzmatrix in Zeit O(1) zu beantworten, bei der Adjazenzliste muss eine der
Teillisten durchsucht werden, was allgemein eine Laufzeit von O(e) ergibt. Einige
graphentheoretische Probleme lassen sich mit Methoden der linearen Algebra
lösen, was die Repräsentation als Adjazenzmatrix favorisiert.
Adjazenzlisten sind in der Regel dann vorteilhaft, wenn Graphen gemäß ihrem
strukturellen Aufbau durchsucht werden sollen, also gemäß ihrer Nachbarschaftsbeziehungen. Bei einer Adjazenzmatrix muss immer eine komplette Zeile durchsucht werden, um die Folgeknoten eines Knotens zu bestimmen, was eine Laufzeit
13
Dieses Feld ist gemäß einer „Durchnummerierung“ der Knoten geordnet.
Adjazenzliste
Seite 98
Studienbrief 2 Basiskonzepte und Datenstrukturen
von O(n) ergibt. Sollen Graphen dynamisch verändert werden, insbesondere Knoten aus einem Graphen gestrichen oder zu diesem hinzugefügt werden, so liegen
die Vorteile stark auf Seiten der Adjazenzliste.
Da beide Repräsentationen ihre Vor- und Nachteile haben, kann es sinnvoll sein,
beide Repräsentationen zur Verfügung zu haben bzw. eine der Repräsentationen
mit anderen Datenstrukturen (z. B. Suchbäume oder Hashtabellen (s. Abs. 3.4.3))
anzureichern.
Ist ein Graph gewichtet, so müssen die Gewichte zusätzlich in die Repräsentation
aufgenommen werden. Bei einer Adjazenzmatrix ist dies oft durch Verwendung
einer Matrix mit reellen Zahlen statt einer booleschen Matrix möglich, ansonsten
können Zeiger in der Adjazenzmatrix auf Gewichte verweisen. Bei einer Adjazenzliste werden die Gewichte zusätzlich zu den Verweisen in den Listenelementen
gespeichert.
2.12 Zusammenfassung
Die Definition eines eindeutigen, realitätsnahen und einfachen Modells ist für eine
objektive Effizienzanalyse von Algorithmen fundamental wichtig. Von diesem
Modell ausgehend können verschiedene Methoden angewandt werden, um in
einem konkreten Fall, also für einen konkreten Algorithmus, Effizienzaussagen
zu treffen und diesen mit anderen Algorithmen zu vergleichen. Die hier eingeführte Modellbildung bezieht sich auf sequentielle Algorithmen, kann aber auf
verschiedenste Weise für parallele Algorithmen erweitert werden.
Reale Rechnerhardware arbeitet mit einigen wenigen elementaren Datentypen
wie Integer- oder Fließkommazahlen. Strukturierte Datentypen bilden komplexere
Datenmodelle auf diese elementaren Datentypen ab. Strukturierte Datentypen,
oft auch als abstrakte Datentypen bezeichnet, definieren neben der Struktur der
Daten im Rechner selbst auch die Methoden, mit denen auf diesen Daten gearbeitet werden kann. Die Effizienz dieser Methoden ist äußerst wichtig, um darauf
aufbauend effiziente Algorithmen formulieren und implementieren zu können.
2.13 Übungen
Seite 99
2.13 Übungen
Übung 2.1
Ü
Beweisen Sie formal die Aussagen von Satz 2.1.
Übung 2.2
Ü
Zeigen Sie, dass das Travelling-Salesman-Problem ein Problem der Klasse
NP ist.
Wählen Sie zur Formulierung des Algorithmus (in Pseudocode) als Modell einer nichtdeterministischen Registermaschine eine synchrone priority
CRCW (Concurrent Read Concurrent Write) PRAM: Die PRAM verfügt über
beliebig viele Prozessoren, die auf einen gemeinsamen Speicher zugreifen.
Alle Prozessoren arbeiten synchron dasselbe Programm ab. Dabei ist sowohl
der gleichzeitige lesende als auch der gleichzeitige schreibende Zugriff auf
dieselbe Speicherzelle erlaubt. Werden gleichzeitig mehrere schreibende
Zugriffe auf eine Speicherzelle getätigt, so wird vereinbart, dass der kleinste
zu schreibende Wert Priorität hat.
Die Prozessoren der PRAM haben fest vorgegebene Prozessornummern
procnum, die mit 0 beginnen. Die Parallelität im Algorithmus wird durch
for-pardo-Konstrukte ausgedrückt.
Zeigen Sie anschließend, wie der Algorithmus auch für das schwächere
Modell einer CREW (Concurrent Read Exclusive Write) PRAM formuliert
werden kann. Bei einer CREW PRAM ist das gemeinsame Schreiben auf
eine Speicherzelle verboten, das gemeinsame Lesen ist jedoch erlaubt.
Übung 2.3
Erweitern Sie den Binärbaum aus Übung 1.13 zu einem n-nären Suchbaum,
wobei n ≥ 2 frei wählbar ist. Es reicht, einen Baum mit Integer-Schlüsseln
zu implementieren. Anforderungen an Balancen sind keine gestellt.
Der Einfachheit halber halten wir uns nicht ganz genau an Definition 2.6: Die
Anzahl der Schlüssel in einem Knoten darf größer sein als die Anzahl seiner
Kinder. Die Kinder hängen weiterhin linksbündig an einem Knoten. Durch
die Auflockerung der Schlüsselordnung gelingt es, Suchbäume mit gut
gefüllten Knoten zu erzeugen, ohne detaillierte Verfahren festzulegen, wie
Bäume beim Einfügen neuer Schlüssel ggf. umstrukturiert werden müssen.
Mit einem Suchbaum (B-Baum), der Definition 2.6 genau einhält, werden
wir uns in Studienbrief 4 beschäftigen.
Ü
Seite 100
Ü
Studienbrief 2 Basiskonzepte und Datenstrukturen
Übung 2.4
Implementieren Sie die Grundoperationen aus Abs. 2.11.1 für einen binären,
knotenorientierten Suchbaum in C. Würfeln Sie in beliebiger Reihenfolge
Integer-Schlüssel zum Einfügen und Streichen. Geben Sie das Ergebnis
strukturiert aus.
Tipp: Sie können auf Teil 1 von Übung 1.14 aufsetzen.
Ü
Übung 2.5
Formulieren Sie die Suche gemäß Abs. 2.11.1 für blattorientierte Suchbäume
in Pseudocode.
Welche Probleme können Sie erkennen, wenn Sie Schüssel in einem blattorientierten binären Suchbaum einfügen oder streichen wollen?
Ü
Übung 2.6
Schreiben Sie ein Programm, das einen Graphen mit 20 Knoten generiert,
wobei beliebige Kanten per Zufallsgenerator erzeugt werden. Speichern Sie
den Graphen als Adjazenzliste und als Adjazenzmatrix. Geben Sie Adjazenzliste und Adjazenzmatrix in einer übersichtlichen Form auf dem Bildschirm
aus.
Liste der Lösungen zu den Kontrollaufgaben
Seite 187
Liste der Lösungen zu den Kontrollaufgaben
Lösung zu Kontrollaufgabe 2.1 auf Seite 75
2n+1 = 2 · 2n . Damit gilt nach Satz 2.1: O(2 · 2n ) = O(2) · O(2n ) = O(2n ). Damit gilt
2n+1 ∈ O(2n ).
Lösung zu Kontrollaufgabe 2.2 auf Seite 76
Der Datentransferbefehl Ri = R j kann bspw. durch den logischen Befehl Ri =
R j and R j „simuliert“ werden.
Die Laufzeit eines Divisionsbefehls ist zwar länger als die eines Additionsbefehls, aber dennoch konstant bzw. – in Abhängigkeit von der Implementierung in
Hardware – nach oben durch einen konstanten Wert beschränkt. Bei einer asymptotischen Laufzeitanalyse spielt die Konstante keine Rolle, bei einer absoluten
Zeitmessung natürlich schon. Es gibt bei realen CPUs Befehle, die komplette Schleifen nach einem Schleifenzähler abarbeiten. Solche Befehle beherrscht die RAM
natürlich nicht, da diese Befehle keine konstante Laufzeit haben.
Hierzu sind mindestens 200 Befehle erforderlich, weil nicht direkt von einer Speicherzelle in eine andere Speicherzelle geschrieben werden kann. Es ist also der
Umweg über ein Register erforderlich.
Lösung zu Kontrollaufgabe 2.3 auf Seite 81
Generell weder noch, da das Verfahren wegen der unendlich vielen Nachkommastellen von π nie mit einem korrekten Ergebnis terminieren kann. Will man jedoch
π auf eine feste Anzahl von Nachkommastellen genau bestimmen, dann ist das
Verfahren ein Monte Carlo-Algorithmus.
Lösung zu Kontrollaufgabe 2.4 auf Seite 88
Die Definition besagt für einen Binärbaum, dass bei einem Knoten, der nur ein
Kind hat, dieses immer das linke Kind ist. Man könnte also bspw. die 19 an die
Stelle der 17 schreiben, und die 17 als linkes Kind an die 19 anhängen.
Wenn ein Knoten weniger Kinder hat als es die Ordnung des Baums erlaubt, dann
könnten die Kinder eigentlich an beliebigen Positionen angehängt werden, solange
das der Schlüsselordnung genügt. In unserer Definition sind die Kinder „linksbündig“ angehängt. Bei einer Implementierung eines n-nären Suchbaums werden die
Verweise auf die Kinder in der Regel in Feldern gespeichert. Die Linksbündigkeit
macht dann die Zugriffe auf die Kinder effizienter, weil nicht das gesamte Feld
durchforstet und auf NULL-Zeiger überprüft werden muss. Ansonsten müsste eine
andere Datenstruktur als ein Feld für die Speicherung der Verweise gewählt oder
ein Feld mit den Indizes der nicht NULL-Zeiger angelegt werden.
Lösung zu Kontrollaufgabe 2.5 auf Seite 90
Quelltext 5.19
1
2
durchmustern(knoten s) {
durchmustern(linkes Kind);
Q
Seite 188
Liste der Lösungen zu den Kontrollaufgaben
3
drucke Schluessel von s aus;
durchmustern(rechtes Kind);
4
5
}
Lösung zu Kontrollaufgabe 2.6 auf Seite 93
Der am weitesten rechts stehende Knoten im linken Unterbaum des Knotens v mit
Schlüssel 30 ist der Knoten x mit Schlüssel 25. Der Schlüssel 30 von v wird mit
Schlüssel 25 überschrieben, und anschließend wird Schlüssel 25 im Unterbaum
mit Wurzel x gelöscht. Dort ist der Knoten mit Schlüssel 12 der am weitesten rechts
stehende Knoten. Dieser ist ein Blatt; sein Schlüssel wird also nach x transferiert,
und das Blatt wird gelöscht.
Verzeichnisse
Seite 189
Verzeichnisse
I. Abbildungen
Abb. 1.1:
Abb. 1.2:
Abb. 1.3:
Abb. 2.1:
Abb. 2.2:
Abb. 2.3:
Abb. 2.4:
Abb. 2.5:
Abb. 2.6:
Abb. 2.7:
Abb. 2.8:
Abb. 3.1:
Abb. 3.2:
Abb. 3.3:
Abb. 3.4:
Abb. 3.5:
Abb. 3.6:
Abb. 3.7:
Abb. 3.8:
Abb. 3.9:
Abb. 3.10:
Abb. 3.11:
Abb. 4.1:
Abb. 4.2:
Abb. 4.3:
Abb. 4.4:
Abb. 4.5:
Abb. 4.6:
Abb. 4.7:
Abb. 4.8:
Abb. 4.9:
Abb. 4.10:
Abb. 4.11:
Abb. 4.12:
Abb. 4.13:
Abb. 5.1:
Abb. 5.2:
Abb. 5.3:
Abb. 5.4:
Abb. 5.5:
Abb. 5.6:
Abb. 5.7:
Abb. 5.8:
Abb. 5.9:
Abb. 5.10:
Abb. 5.11:
Feld a im Hauptspeicher . . . . . . . . . . . . . . . . .
Verkettete Liste . . . . . . . . . . . . . . . . . . . . .
Binärbaum . . . . . . . . . . . . . . . . . . . . . . . .
Muhammad ibn Musa al-Chwarizmi . . . . . . . . . . .
Ein Baum . . . . . . . . . . . . . . . . . . . . . . . . .
Binärer Suchbaum . . . . . . . . . . . . . . . . . . . .
Suchbaum mit Ordnung m=4 . . . . . . . . . . . . . .
Durchmusterung mit Präordnung und Postordnung . .
Streichen in einem binären Suchbaum . . . . . . . . .
Ein Graph . . . . . . . . . . . . . . . . . . . . . . . . .
Adjazenzmatrix und Adjazenzliste . . . . . . . . . . . .
Heap als Baum und als Feld . . . . . . . . . . . . . . .
Wiederherstellung der Heap-Eigenschaft . . . . . . . .
Schema von Bucketsort . . . . . . . . . . . . . . . . .
Binärsuche . . . . . . . . . . . . . . . . . . . . . . . .
Digitaler Suchbaum . . . . . . . . . . . . . . . . . . .
Komprimierter digitaler Suchbaum . . . . . . . . . . .
Hashtafel . . . . . . . . . . . . . . . . . . . . . . . . .
Hashing mit Verkettung . . . . . . . . . . . . . . . . .
Hashing mit offener Adressierung . . . . . . . . . . . .
Partitionierung und union . . . . . . . . . . . . . . . .
Pfadkomprimierung . . . . . . . . . . . . . . . . . . .
AVL-Baum . . . . . . . . . . . . . . . . . . . . . . . . .
Fall 1: Rechtsrotation . . . . . . . . . . . . . . . . . . .
Fall 2: Links-Rechtsrotation . . . . . . . . . . . . . . .
Fall 3: Linksrotation . . . . . . . . . . . . . . . . . . .
Fall 4: Rechts-Linksrotation . . . . . . . . . . . . . . .
Schlüsselordnung . . . . . . . . . . . . . . . . . . . .
B-Baum mit Ordnung m=4 . . . . . . . . . . . . . . .
Einfügen in einen B-Baum . . . . . . . . . . . . . . . .
Split eines Knotens . . . . . . . . . . . . . . . . . . . .
Rückführung des Löschens auf einen Schlüssel im Blatt
Übernahme von Schlüsseln . . . . . . . . . . . . . . .
Verschmelzung von Schlüsseln . . . . . . . . . . . . .
Übernahme und Verschmelzung . . . . . . . . . . . .
Topologische Sortierung . . . . . . . . . . . . . . . . .
Kantenklassen, dfsnum und compnum . . . . . . . . .
Starke Zusammenhangskomponenten . . . . . . . . .
Durchmusterung mit DFS . . . . . . . . . . . . . . . .
Graph mit MST . . . . . . . . . . . . . . . . . . . . . .
Entfernungen in einem Graphen . . . . . . . . . . . .
Kürzeste Wege von Knoten s aus . . . . . . . . . . . .
Relaxierung einer Kante . . . . . . . . . . . . . . . . .
Kürzeste Wege nach Bellman-Ford . . . . . . . . . . .
Folge von δk (1, 4) . . . . . . . . . . . . . . . . . . . . .
Bestimmung von δk (i, j) . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
42
56
71
73
86
88
88
90
93
96
97
104
105
113
117
118
120
121
121
125
129
133
140
141
141
142
142
144
144
146
146
148
149
150
151
156
164
166
169
171
175
177
180
181
182
182
II. Definitionen
Definition 2.1:
Definition 2.2:
Definition 2.3:
O-Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Laufzeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Randomisierte Laufzeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
74
78
81
Seite 190
Verzeichnisse
Definition 2.4:
Definition 2.5:
Definition 2.6:
Definition 2.7:
Definition 2.8:
Definition 2.9:
Definition 2.10:
Definition 2.11:
Definition 3.1:
Definition 3.2:
Definition 3.3:
Definition 3.4:
Definition 3.5:
Definition 4.1:
Definition 4.2:
Definition 5.1:
Definition 5.2:
Definition 5.3:
Definition 5.4:
Definition 5.5:
Definition 5.6:
Definition 5.7:
Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Tiefe und Pfade . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Suchbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Balancierter Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Wälder und Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Adjazenzmatrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Adjazenzliste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Grundoperationen auf Mengen . . . . . . . . . . . . . . . . . . . .
Digitaler Suchbaum . . . . . . . . . . . . . . . . . . . . . . . . . . .
Perfekte Hashfunktion . . . . . . . . . . . . . . . . . . . . . . . . .
Universelle Hashfunktionen . . . . . . . . . . . . . . . . . . . . . .
Ackermann-Funktion . . . . . . . . . . . . . . . . . . . . . . . . . .
AVL-Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B-Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Topologisches Sortieren . . . . . . . . . . . . . . . . . . . . . . . .
Induzierter Untergraph . . . . . . . . . . . . . . . . . . . . . . . . .
Zusammenhangskomponenten . . . . . . . . . . . . . . . . . . . .
Mehrfache Zusammenhangskomponenten . . . . . . . . . . . . .
Teilmengensysteme, Matroide und zugehörige Kostenfunktion
Kürzeste (billigste) Wege in Graphen . . . . . . . . . . . . . . . .
transitive Hülle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
86
87
87
94
95
96
96
97
115
118
126
127
133
140
143
155
162
167
170
173
175
183
III. Exkurse
Exkurs 1.1:
Exkurs 1.2:
Exkurs 2.1:
Exkurs 2.2:
C-Compiler . . . . . . . . . . . . . . . . . .
Strukturen als Parameter . . . . . . . . .
Parallele Algorithmen . . . . . . . . . . .
„Schnelle“ Multiplikation großer Zahlen
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
11
53
77
78
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
21
34
59
65
65
89
IV. Tabellen
Tabelle
Tabelle
Tabelle
Tabelle
Tabelle
Tabelle
1.1:
1.2:
1.3:
1.4:
1.5:
2.1:
Vorrang und Assoziativität der Operatoren
Deklaration und Definition . . . . . . . . .
Umwand- lungszeichen . . . . . . . . . . .
String-Funktionen . . . . . . . . . . . . . .
Mathematische Funktionen . . . . . . . . .
Baum, gespeichert in einem Feld . . . . .
.
.
.
.
.
.
V. Literatur
G. M. Adelson-Velskii and E. M. Landis. An algorithm for the organization of information. Soviet Math.
Doklady. 3, S. 1259-1263, 1962.
Vorlesung Algorithmen. Kap. 7 Graphenalgorithmen. Universität Tübingen, 2005.
Rudolf Bayer and Edward M. McCreight. Organization and maintenance of large ordered indices. Acta
Informatica, Volume 1, 1972.
R. E. Bellman. On a routing problem. Quarterly of Applied Mathematics 16(1), Brown University, 1958.
O. Bittel. Algorithmen und Datenstrukturen. Vorlesungsskript, FH-Konstanz, 2007.
Martin Dietzfelbinger, Kurt Mehlhorn, and Peter Sanders. Algorithmen und Datenstrukturen: Die Grundwerkzeuge. Springer, 2014.
Literatur
Seite 191
Edsger W. Dijkstra. A note on two problems in connexion with graphs. Numerische Mathematik 1, S. 269-271,
1959.
Robert W. Floyd. Algorithm 97 (shortest path). Communications of the ACM 5, 1962, 6, S. 345, 1962.
Brian W. Kernighan and Dennis M. Ritchie. The C Programming Language. Prentice Hall, 1978.
Brian W. Kernighan and Dennis M. Ritchie. Programmieren in C. Hanser Fachbuch, 1990.
Donald Ervin Knuth. The Art of Computer Programming, Volume III: Sorting and Searching. Addison-Wesley,
1973.
Joseph B. Kruskal. On the shortest spanning subtree of a graph and the traveling salesman problem.
Proceedings of the American Mathematical Society, 1956.
Kurt Mehlhorn. Data Structures and Algorithms 2: Graph Algorithms and NP-completeness. Springer Verlag, 1984.
Kurt Mehlhorn. Datenstrukturen und Algorithmen 1: Sortieren und Suchen. Teubner Verlag, 1986.
Kurt Mehlhorn and Peter Sanders. Algorithms and Data Structures. Springer Verlag, 2010.
Uwe Schöning. Algortithmen - kurz gefasst. Spektrum Hochschultaschenbuch, 1997.
Axel Stutz and Peter Klingebiel. Übersicht über die C Standard-Bibliothek.
http://www2.hs-fulda.de/~klingebiel/c-stdlib/index.htm, 1999.
Robert E. Tarjan. Efficiency of a good but not linear set union algorithm. Journal of the ACM (JACM) Volume
22 Issue 2, 1975.
Ronald Rivest Thomas H. Cormen, Charles E. Leiserson and Clifford Stein. Algorithmen - Eine Einführung.
Oldenbourg Wissenschaftsverlag, 2010.
Arnold Schönhage und Volker Strassen. Schnelle Multiplikation großer Zahlen. Computing 7, Springer Verlag,
S. 281–292, 1971.
Stichwörter
Seite 193
Stichwörter
BB[α]-Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Adjazenzfeld . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Adjazenzliste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Adjazenzmatrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
amortisierte Kosten . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
asymptotische Analyse . . . . . . . . . . . . . . . . . . . . . . . . 74
aufspannender Baum . . . . . . . . . . . . . . . . . . . . . . . . . 96
average case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
best case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Binärbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Bit-Komplexitätsmaß . . . . . . . . . . . . . . . . . . . . . . . . . 77
blattorientierter Suchbaum . . . . . . . . . . . . . . . . . . . . 88
Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
Rekursionsgleichungen . . . . . . . . . . . . . . . . . . . . . . . 82
Schlüssel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Schlange . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Shared Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
spanning tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
Stapel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Struktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
strukturierte Datentypen . . . . . . . . . . . . . . . . . . . . . 85
Substitution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Suchbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85, 87
Turing-Maschine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
uniformes Komplexitätsmaß . . . . . . . . . . . . . . . . . . 77
verteilte Systeme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
DAG . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
deterministische Algorithmen . . . . . . . . . . . . . . . . 80
worst case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
effizienter Algorithmus . . . . . . . . . . . . . . . . . . . . . . . 84
Zeitmodell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
geeigneter Suchbaum . . . . . . . . . . . . . . . . . . . . . . . . . 93
gewichtsbalancierter Baum . . . . . . . . . . . . . . . . . . . 94
Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
höhenbalancierter Baum . . . . . . . . . . . . . . . . . . . . . . 94
knotenorientierter Suchbaum . . . . . . . . . . . . . . . . . 88
Las Vegas-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . 80
lexikographische Ordnung . . . . . . . . . . . . . . . . . . . . 90
Liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Maschinenmodell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Mastertheorem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Monte Carlo-Algorithmus . . . . . . . . . . . . . . . . . . . . 80
NP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
NP-hart . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
NP-vollständig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
O-Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74, 76
P . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
parallele Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . 77
Postordnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Präordnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
probabilistische Algorithmen . . . . . . . . . . . . . . . . . 80
Problemgröße . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Pseudocode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
RAM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
randomisierte Algorithmen . . . . . . . . . . . . . . . . . . . 80
Anhang
Seite 195
Anhang
Prioritäten und Assoziativitäten der Operatoren in C
Operatoren
() [] -> .
! ~ ++ -- + - * & (type) sizeof
* / %
+ << >>
< <= > >=
== !=
&
∧
|
&&
||
?:
= += -= *= /= %= &= ∧ = |= <<= >>=
,
Assoziativität
von links
von rechts
von links
von links
von links
von links
von links
von links
von links
von links
von links
von links
von rechts
von rechts
von links
Herunterladen