Skript zu Algorithmen und Datenstrukturen

Werbung
Algorithmen und Datenstrukturen
— Skript zur Vorlesung —
Ulrik Brandes
Wintersemester 2011/2012
(Entwurf vom 12. Oktober 2011)
Vorwort
Dieses Skript entstand im Wintersemesters 2007/08 parallel zur neu konzipierten Vorlesung gleichen Titels und wird seither von Fehlern bereinigt. Es
sollte zunächst als Anhaltspunkt für den Inhalt der Vorlesung verstanden
werden und unter anderem dazu dienen, die relevanten Stellen in der angekennzeichneten Aufzeichnungen der
gegebenen Literatur oder den mit
Vorlesung aus dem Wintersemester 2008/09 zu identifizieren.
Mein herzlicher Dank gilt Martin Mader für das erste Setzen der handschriftlichen Vorlesungsnotizen und Mennatallah El Assady für das Einfügen der
Aufzeichnungsverweise.
i
Inhaltsverzeichnis
1 Einführung
1
1.1
Beispiel: Auswahlproblem . . . . . . . . . . . . . . . . . . . .
1
1.2
Maschinenmodell . . . . . . . . . . . . . . . . . . . . . . . . .
4
1.3
Komplexität . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
2 Sortieren
11
2.1
SelectionSort . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2
Divide & Conquer (QuickSort und MergeSort) . . . . . . . . . 13
2.3
HeapSort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4
Untere Laufzeitschranke . . . . . . . . . . . . . . . . . . . . . 26
2.5
Sortierverfahren für spezielle Universen . . . . . . . . . . . . . 27
2.6
Gegenüberstellung . . . . . . . . . . . . . . . . . . . . . . . . 32
3 Suchen
34
3.1
Folgen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.2
Geordnete Wörterbücher . . . . . . . . . . . . . . . . . . . . . 42
4 Streuen
60
4.1
Kollisionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.2
Kollisionsbehandlung . . . . . . . . . . . . . . . . . . . . . . . 63
4.3
Kollisionsvermeidung . . . . . . . . . . . . . . . . . . . . . . . 67
ii
Algorithmen und Datenstrukturen (WS 2010/2011)
4.4
iii
Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
5 Ausrichten
71
6 Graphen
78
6.1
Bäume und Wälder . . . . . . . . . . . . . . . . . . . . . . . . 83
6.2
Durchläufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6.3
Kürzeste Wege . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Kapitel 1
Einführung
Anhand eines einfachen Problems soll deutlich gemacht werden, welche Schwierigkeiten beim Vergleich verschiedener algorithmischer Lösungsansätze auftreten können, um dann einige sinnvolle Kriterien festzulegen.
1.1
Beispiel: Auswahlproblem
1.1 Problem (Auswahlproblem)
„Bestimme das k-t kleinste von n Elementen“
gegeben: Elemente a1 , . . . , an mit einer Ordnung ≤ sowie ein k ∈ {1, . . . , n}
gesucht: aπ(k) für eine Permutation π : {1, . . . , n} → {1, . . . , n} mit
aπ(1) ≤ aπ(2) ≤ · · · ≤ aπ(n)
1.2 Bemerkung
Natürliche Spezialfälle des Auswahlproblems mit festem k sind:


Minimumssuche
1
k= n
Maximumssuche

 n
b 2 c Median
Die Bestimmung des Mittelwerts von n Zahlen ist kein Spezialfall des Auswahlproblems.
1
k-SELECT
Algorithmen und Datenstrukturen (WS 2010/2011)
2
Wir diskutieren vier verschiedene Ansätze zur Lösung des Auswahlproblems
und nehmen dabei an, dass die Elemente in einem Array M [1, . . . , n] bereit
stehen.
Ansatz A: Die konzeptionell einfachste Methode besteht darin, die
Elemente des Arrays zunächst bezüglich ≤ nicht-absteigend zu sortieren
und dann das k-te auszugeben.
Algorithmus 1:
Auswahl nach Sortieren
sort(M )
print M [k]
Um die Güte dieses Vorgehens beurteilen zu können, muss mindestens mal
der verwendete Sortieralgorithmus bekannt sein. Möglicherweise hängt dessen
Güte jedoch auch noch von der Eingabe und der Art der Elemente und
Ordnung ab (vgl. Kapitel 2).
Ansatz B: Das Auswahlproblem
Algorithmus 2:
lässt sich auch elementar, d.h. ohne
Wiederholte Minimumssuche
Verwendung eines anderen Algorithfor i = 1, . . . , k − 1 do
mus, lösen. Statt alle Elemente zu sorM ← M \ {min M }
tieren, genügt auch die k-malige Beprint min M
stimmung und Wegnahme eines Minimums. Das letzte davon ist das gesuchte Element.
Im vorstehenden Pseudo-Code ist ein Spezialfall des Auswahlproblems, die
Minimumssuche, als elementare Operation aufgeführt. Um die tatsächliche
Komplexität besser beurteilen zu können, geben wir eine ausführlichere Implementation an. Darin wird das jeweils kleinste Element der Restfolge an die
erste Stelle geholt, sodass schliesslich in M [1, . . . , k] die k kleinsten Elemente
stehen.
Algorithmus 3: Wiederholte Minimumssuche (detailliert)
for i = 1, . . . , k do
m←i
for j = i + 1, . . . , n do
if M [j] < M [m] then m ← j
vertausche M [i] und M [m]
print M [k]
Algorithmen und Datenstrukturen (WS 2010/2011)
3
1.3 Bemerkung
Wird die wiederholte Minimumssuche n mal ausgeführt, entspricht die Ausgabereihenfolge der Elemente einer vollständigen Sortierung der Elemente.
Darauf kommen wir in Abschnitt 2.1 zurück.
MinSort
Ansatz C: Statt wie in Ansatz B immer das kleinste Element der Restfolge
zu suchen, können wir auch alle Elemente durchgehen und für jedes testen,
ob es unter den bisher betrachteten zu den k kleinsten gehört. Dies ist gerade dann der Fall, wenn das Element kleiner ist als das größte der k bisher
kleinsten. Sind alle Element durchgetestet, ist das größte der k kleinsten das
gesuchte Element. Statt wiederholter Minimumssuchen in der (anfangs sehr
langen) Restfolge führen wir also Maximumssuchen in einem (für kleine k
sehr kurzen) Anfangsstück aus.
Algorithmus 4: Aktualisierung einer vorläufigen Lösung
begin
m ← maxpos(M,1,k )
for i = k + 1, . . . , n do
if M [i] < M [m] then
vertausche M [i] und M [m]
m ← maxpos(M,1,k )
print M [m]
end
int maxpos(array M , int l, r) begin
m←l
for i = l + 1, . . . , r do
if M [i] > M [m] then m ← i
return m
end
Ansatz D: Gibt es in der verwendeten Programmiersprache (d.h. in der
zum Sprachumfang gehörigen Standardbibliothek) oder einem zur Verfügung
stehen Paket bereits eine entsprechende Methode, kann einfach diese aufgerufen werden. Für die Beurteilung dieses Vorgehens ist dann allerdings detaillierte Kenntnis über das Verhalten der Methode erforderlich, da die (meist
unbekannte) Implementation (und sei es nur im aktuellen Kontext, in dem
Algorithmen und Datenstrukturen (WS 2010/2011)
4
das Auswahlproblem gelöst werden soll) sehr ineffizient sein könnte.
Beim Vergleich dieser vier Lösungsansätze stellt man schnell fest, dass es
keinen eindeutig besten gibt. Die Beurteilung erfordert mehr Information:
bei den Ansätzen A und D über die Implementation selbst, und in allen
Fällen über die zu erwartenden Eingaben. Insbesondere das Verhältnis der
Größen von n und k ist von Bedeutung (da die Laufzeit von Ansatz A nur von
n abhängt, die von B und C aber auch von k), aber z.B. auch, ob Vergleiche
und Umspeicherungen ähnlich schnell ausgeführt werden können.
1.2
Maschinenmodell
Schon für einen Vergleich auf Basis der Ausführungszeiten stellen sich viele
Detailfragen. Wird die Laufzeit etwa in Sekunden gemessen, lässt sie sich
nicht für alle Eingaben im Vorhinein angeben und hängt zudem von zahlreichen Faktoren ab. Drei einfache Beispiele:
• In welcher Programmiersprache wurde implementiert?
• Auf welchem Rechner wird das Programm ausgeführt (Aufbau, Taktfrequenz, Speicherzugriffszeiten, etc.)?
• Wie sind die Daten gespeichert (Organisation, Medium, etc.)?
Die Beurteilung von Algorithmen und Datenstrukturen werden wir von diesen Faktoren weitgehend unabhängig machen, indem wir z.B. statt der Ausführungszeiten die Anzahl der elementaren Schritte zählen, die ein Algorithmus ausführt. Um vereinbaren zu können, was ein elementarer Schritt sein
soll, müssen wir allerdings ein paar Festlegungen treffen, die zwar nach Möglichkeit realistisch, aber trotzdem unabhängig von konkreten Rechnern sein
sollten.
Um die Komplexität eines Verfahrens sinnvoll beurteilen zu können, führen
wir daher zunächst ein Maschinenmodell ein, in dem Laufzeit und Speicherplatzbedarf hinreichend genau und auf standardisierte Weise gemessen werden können.
Algorithmen und Datenstrukturen (WS 2010/2011)
5
Abbildung 1.1: Aufbau der Random Access Machine
1.4 Def inition (Random Access Machine)
Die Random Access Machine (RAM) ist ein abstraktes Maschinenmodell mit
(siehe Abb. 1.1)
• einer endlichen Zahl von Speicherzellen für das Programm,
• einer abzählbar unendlichen Zahl von Speicherzellen für Daten,
(Speicheradressen aus N0 ),
• einer endlichen Zahl von Registern,
• einem Befehlszähler (spezielles Register) und
• einer arithmetisch-logischen Einheit (ALU).
In Speicherzellen und Registern stehen wiederum natürliche Zahlen, und diese
können von der ALU verarbeitet werden. Der Befehlszähler wird nach jeder
Befehlsausführung um eins erhöht, kann aber auch mit einem Registerinhalt
überschrieben werden.
Algorithmen und Datenstrukturen (WS 2010/2011)
6
Als Anweisungen stehen zur Verfügung
• Transportbefehle (Laden, Verschieben, Speichern),
• Sprungbefehle (bedingt und unbedingt),
• arithmetische und logische Verknüpfungen.
Die Adressierung erfolgt direkt (Angabe der Speicherzelle) oder indirekt
(Adressierung über Registerinhalt).
1.5 Bemerkung
1. Mit den Sprungbefehlen sind alle Schleifentypen (for, while, repeatuntil) und auch Rekursionen realisierbar.
2. Der Unterschied zur Registermaschine besteht in der Möglichkeit zur
indirekten Adressierung, und anders als bei der Random Access Stored kein vonNeumann
Program (RASP) Machine sind Programm und Daten getrennt.
Modell
1.3
Komplexität
Wir werden die Komplexität von Algorithmen vor allem durch zwei Größen
beschreiben:
Laufzeit: Anzahl Schritte
Speicherbedarf: Anzahl benutzter Speicherzellen
Tatsächlich wäre selbst die genaue Anzahl der Schritte zu mühsam zu bestimmen. Wir müssten z.B. präzise angeben, auf welche Weise genau ein Wert aus
dem Speicher über Register in die ALU kommt und weiterverarbeitet wird.
Es soll uns aber reichen, dass Vorgänge dieser Art durch eine unbekannte,
aber konstante Anzahl von Schritten realisiert werden können. Entsprechend
werden wir konstante Faktoren in Laufzeiten und Speicherplatz weitgehend
ignorieren und uns auf das asymptotische Wachstum der Komplexität im
Verhältnis zur Größe der Eingabe konzentrieren.
Algorithmen und Datenstrukturen (WS 2010/2011)
7
1.6 Def inition (Asymptotisches Wachstum)
Zu einer Funktion f : N0 → R wird definiert:
(i) Die Menge
es gibt Konstanten c, n0 > 0 mit
O(f (n)) = g : N0 → R :
|g(n)| ≤ c · |f (n)| für alle n > n0
der Funktionen, die höchstens so schnell wachsen wie f .
(ii) Die Menge
Ω(f (n)) =
es gibt Konstanten c, n0 > 0 mit
g : N0 → R :
c · |g(n)| ≥ |f (n)| für alle n > n0
der Funktionen, die mindestens so schnell wachsen wie f .
(iii) Die Menge
(
Θ(f (n)) =
)
es gibt Konstanten c1 , c2 , n0 > 0 mit
g : N0 → R :
|g(n)|
c1 ≤ |f
≤ c2 für alle n > n0
(n)|
der Funktionen, die genauso schnell wachsen wie f .
(iv) Die Menge
zu jedem c > 0 ex. ein n0 > 0 mit
o(f (n)) = g : N0 → R :
c · |g(n)| ≤ |f (n)| für alle n > n0
der Funktionen, die gegenüber f verschwinden.
(v) Die Menge
ω(f (n)) =
zu jedem c > 0 ex. ein n0 > 0 mit
g : N0 → R :
|g(n)| ≥ c · |f (n)| für alle n > n0
der Funktionen, denen gegenüber f verschwindet.
Algorithmen und Datenstrukturen (WS 2010/2011)
8
Das folgende Beispiel zeigt, dass mit den eingeführten Notationen nicht nur
Konstanten ignoriert, sondern oft auch komplizierte Laufzeitfunktionen vereinfacht werden können.
1.7 Beispiel
Ein (reelles) Polynom vom Grad d ∈ N0 besteht aus d + 1 Koeffizienten
ad , . . . , a0 ∈ R, wobei ad 6= 0 verlangt wird.PReelle Polynome p beschreiben
d
i
Funktionen p : R → R vermöge p(x) =
i=0 ai · x für alle x ∈ R. Polynome mit anderen Zahlenbereiche für die Koeffizienten, Definitions- und
Wertebereiche sind analog definiert.
Ist p : N0 → R ein Polynom vom Grad d, dann gilt für alle n ≥ 1
p(n) =
d
X
i=0
1
1
ai · n = ad + ad−1 · 1 + . . . + a0 · d · nd
n
n
und damit
|p(n)| ≤
i
d
X
!
|ai |
· nd
für alle n ≥ 1 ,
i=0
d
also p(n) ∈ O(n ). Das Polynom läßt sich weiter umschreiben zu
a0 1
ad−1 1
·
+ ... +
·
· nd
p(n) = ad · 1 +
ad n1
ad n d
1
ad−1 ad−2 1
a0
1
= ad · 1 + ·
+
· + ... +
·
· nd
n
ad
ad n
ad nd−1
sodass
|p(n)| ≥ |ad | · n
d
d−1 X
ai für alle n >
ad i=0
also p(n) ∈ Ω(nd ) und insgesamt
p(n) ∈ Θ(nd ) .
Das Wachstum einer durch ein Polynom beschriebenen Zahlenfolge hängt
also nur vom Grad des Polynoms ab.
Algorithmen und Datenstrukturen (WS 2010/2011)
9
Das Wachstum einiger wichtiger Folgen im Vergleich:
n
1
10
100
1 000
10 000
log10 n
0
1
2
3
4
0
≈3
≈7
≈ 10
≈ 13
log
√2 n
n
1
≈3
10
≈ 32
100
2
n
1
100
10 000
1 000 000 100 000 000
1
1 000 1 000 000 1 000 000 000 1 Billionen
n3
2n
2
1 024
≈ 1030
≈ 10301
> 103 000
≈3
≈ 13 781
≈ 2 · 1041
> 10414
1, 1n ≈ 1
157
n!
1
3 628 800 ≈ 9 · 10
...
...
n
200
3 000
40 000
n
1 10 000 000 000
10
10
10
Merksatz:
„Ein Programm
mit 1050
Operationen
wird auf
keinem
noch
so
schnellen
Rechner
jemals
fertig
werden.“
Motiviert durch das relative Wachstum der obigen Folgen halten wir einige
für die Komplexitätsbeurteilung nützliche Merkregeln fest.
1.8 Satz
(i) g ∈ O(f ) genau dann, wenn f ∈ Ω(g).
g ∈ Θ(f ) genau dann, wenn f ∈ Θ(g).
(ii) logb n ∈ Θ(log2 n) für alle b > 1.
„Die Basis eines Logarithmus’ spielt für das Wachstum keine Rolle“
bx = a ⇐⇒
logb a = x
(iii) (log2 n)d ∈ o(nε ) für alle d ∈ N0 jedes ε > 0.
„Logarithmen wachsen langsamer als alle Polynomialfunktionen“
(iv) nd ∈ o((1 + ε)n ) für alle d ∈ N0 und jedes ε > 0.
„Exponentielles Wachstum ist immer schneller als polynomiales“
(v) bn ∈ o((b + ε)n ) für alle b ≥ 1 und jedes ε > 0.
„Jede Verringerung der Basis verlangsamt exponentielles Wachstum“
Beweis.
(skizzenhaft)
(i) Folgt unmittelbar aus den Definitionen.
(ii) Folgt aus logb n = (log2 b) · log2 n.
nd
n
(1+ε)
n→∞
(iv) lim
= 0; Plausibilitätsargument:
log bx = x log b
bx = 2x log2 b
(n+1)d
nd
=
nd +O(nd−1 )
−→
nd
n→∞
1, das
prozentuale Wachstum von nd wird also immer kleiner, wohingegen das
von (1 + ε)n konstant ε > 0 beträgt.
Algorithmen und Datenstrukturen (WS 2010/2011)
(log2 n)d
nε
n→∞
(iii) lim
bn
n
n→∞ (b+ε)
(v) lim
(log2 n)d
ε )log2 n
(2
n→∞
= lim
b n
b+ε
= lim
n→∞
10
und dann wie in (iv) mit log2 n statt n.
= 0, da
b
b+ε
< 1.
Die nächste Aussage ist vor allem für Algorithmen interessant, in denen Teilmengen fester Größe betrachtet werden.
1.9 Satz
Für festes k ∈ N0 gilt
n
∈ Θ(nk ) .
k
Für alle n > k =: n0 gilt
n n−1
n − (k − 1)
n
nk
= ·
· ... ·
.
=
k
k!
k k−1
k − (k − 1)
Beweis.
Wegen
n
k
≤
n−i
k−i
≤ n, i = 0, . . . , k − 1, folgt daraus
n k
k
und damit
n
k
k
n
1
k
·n ≤
≤ nk
=
k
k
∈ Ω(nk ) ∩ O(nk ) = Θ(nk ).
In den folgenden oft verwendeten Näherungsformeln wird statt der Funktion
selbst der Fehler der Abschätzung asymptotisch angegeben, und zwar einmal
additiv und einmal multiplikativ. Die Schreibweise bedeutet, dass es in der
jeweiligen Wachstumsklasse eine Folge gibt, für die Gleichheit herrscht.
1.10 Satz
Für alle n ∈ N0 gilt
(i) Hn :=
n
X
1
k=1
k
= ln n + O(1)
(harmonische Zahlen)
n n √
1
(ii) n! = 2πn ·
· 1+Θ
e
n
(Stirlingformel)
Kapitel 2
Sortieren
Das Sortieren ist eines der grundlegenden Probleme in der Informatik. Es
wird geschätzt, dass mehr als ein Viertel aller kommerzieller Rechenzeit auf aus
[3, p. 71]
Sortiervorgänge entfällt. Einige Anwendungsbeispiele:
• Adressenverwaltung (lexikographisch)
• Trefferlisten bei Suchanfragen (Relevanz)
• Verdeckung (z-Koordinate)
• ...
Wir bezeichnen die Menge der Elemente, die als Eingabe für das Sortierproblem erlaubt sind, mit U (für Universum). Formal kann das Problem dann
folgendermaßen beschrieben werden:
2.1 Problem (Sortieren)
gegeben: Folge (a1 , . . . , an ) ∈ U n mit Ordnung ≤ ⊆ U × U
gesucht: Permutation (d.h. bijektive Abbildung) π : {1, . . . , n} → {1, . . . , n}
mit aπ(1) ≤ aπ(2) ≤ · · · ≤ aπ(n)
Wir nehmen an, dass die Eingabe wieder in einem Array M [1, . . . , n] steht Eingabe:
und dass sie darin sortiert zurück gegeben werden soll. Insbesondere wird M [1, . . . , n]
daher von Interesse sein, welchen zusätzlichen Platzbedarf (Hilfsvariablen,
Zwischenspeicher) ein Algorithmus hat.
11
Algorithmen und Datenstrukturen (WS 2010/2011)
12
Neben der Zeit- und Speicherkomplexität werden beim Sortieren weitere Gütekriterien betrachtet. Zum Beispiel kann es wichtig sein, dass die Reihenfolge
von zwei Elemente mit gleichem Schlüssel nicht umgekehrt wird. Verfahren,
in denen dies garantiert ist, heißen stabil. Unter Umständen werden auch
• die Anzahl Vergleiche C(n) und
„comparisons“
• die Anzahl Umspeicherungen M (n)
„moves“
getrennt betrachtet (in Abhängigkeit von der Anzahl n der zu sortierenden
Elemente), da Schlüsselvergleiche in der Regel billiger sind als Umspeicherungen ganzer Datenblöcke.
2.1
SelectionSort
Algorithmus 3 zur Lösung des Auswahlproblems hat das jeweils kleinste Element der Restfolge mit dem ersten vertauscht. Wird der Algorithmus fortgesetzt, bis die Restfolge leer ist, so ist am Ende die gesamte Folge nichtabsteigend sortiert.
Algorithmus 5: SelectionSort
for i = 1, . . . , n − 1 do
m←i
for j = i + 1, . . . , n do
if M [j] < M [m] then m ← j
vertausche M [i] und M [m]
j
≤
i
m
Die Anzahlen der Vergleiche und Vertauschungen sind für SelectionSort also
Pn−1
C(n) = n − 1 + n − 2 + · · · + 1 = i=1
i = (n−1)·n
2
M (n) = 3 · (n − 1)
und die Laufzeit des Algorithmus damit in Θ(n2 ) im besten wie im schlechtesten Fall. Dadurch, dass ein weiter vorne stehendes Element hinter gleiche
andere vertauscht werden kann, ist der Algorithmus nicht stabil.
Aus der Vorlesung „Methoden der Praktischen Informatik“ ist bereits bekannt, dass es Algorithmen gibt, die eine Folge der Länge n in Zeit O(n log n)
„Sortieren
durch
Auswählen“;
hier speziell:
MinSort
Algorithmen und Datenstrukturen (WS 2010/2011)
13
sortieren. Es stellt sich also die Frage, wie man Vergleiche einsparen kann.
Man sieht leicht, dass die hinteren Elemente sehr oft zum Vergleich herangezogen werden. Kann man z.B. aus den früheren Vergleichen etwas lernen,
um auf spätere zu verzichten?
2.2
Divide & Conquer (QuickSort und MergeSort)
Die nächsten beiden Algorithmen beruhen auf der gleichen Idee:
Sortiere zwei kleinere Teilfolgen getrennt
und füge die Ergebnisse zusammen.
Dies dient vor allem der Reduktion von Vergleichen zwischen Elementen in
verschiedenen Teilfolgen. Benötigt werden dazu Vorschriften
• zur Aufteilung in zwei Teilfolgen und
• zur Kombination der beiden sortierten Teilfolgen.
Die allgemeine Vorgehensweise, zur Lösung eines komplexen Problems dieses
auf kleinere Teilprobleme der gleichen Art aufzuteilen, diese rekursiv zu lösen
und ihre Lösungen jeweils zu Lösungen des größeren Problems zusammen zu
setzen, ist das divide & conquer-Prinzip.
2.2.1
QuickSort
Wähle ein Element p („Pivot“, z.B. das erste) und teile die anderen Elemente
der Eingabe M auf in
M1 : die höchstens kleineren Elemente
M2 : die größeren Elemente
Sind M1 und M2 sortiert, so erhält man eine Sortierung von M durch Hintereinanderschreibung von M1 , p, M2 . Algorithmus 6 ist eine mögliche Implementation von QuickSort.
hard split,
easy join
Algorithmen und Datenstrukturen (WS 2010/2011)
14
Algorithmus 6: QuickSort
Aufruf: quicksort(M, 1, n)
r
l
p
quicksort(M, l, r) begin
if l < r then
i ← l + 1;
j←r
p ← M [l]
while i ≤ j do
while i ≤ j and M [i] ≤ p do
i←i+1
while i ≤ j and M [j] > p do
j ←j−1
if i < j then
vertausche M [i] und M [j]
j
i
r
≤ >
j
l
p ≤ >
i
l
p
if l < j then
vertausche M [l] und M [j]
quicksort(M, l, j − 1)
if j < r then
quicksort(M, j + 1, r)
r
≤
≤
>
j i
r
l
≤
quicksort
end
2.2 Beispiel (QuickSort)
5
8
7
27
9
1
17
23
1
5
7
27
9
8
17
23
7
27
9
8
17
23
23
9
8
17
27
17
9
8
23
8
9
17
8
9
p
j
>
quicksort
Algorithmen und Datenstrukturen (WS 2010/2011)
15
2.3 Satz
Die Laufzeit von QuickSort ist
(i) im besten Fall in Θ(n log n)
best case
(ii) im schlechtesten Fall in Θ(n2 )
Beweis.
aus
worst case
Die Laufzeit für einen Aufruf von quicksort setzt sich zusammen
• linearem Aufwand für die Aufteilung und
• dem Aufwand für die Sortierung der Teilfolgen.
Der Gesamtaufwand innerhalb einer festen Rekursionsebene ist damit linear in der Anzahl der Elemente, die bis dahin noch nicht Pivot waren oder
sind. Da bei jedem Aufruf ein Pivot hinzu kommt, ist die Anzahl der neuen
Pivotelemente in einer Rekursionsebene
• mindestens 1 und
• höchstens doppelt so groß wie in der vorigen Ebene.
Die folgenden Beispiele zeigen, dass es Eingaben gibt, bei denen diese beiden
Extremfälle in jeder Rekursionsebene auftreten. Sie sind damit auch Beispiele
für den besten und schlechtesten Fall.
vorsortiert
Pivot immer der Median
0
1
2
3
0
1
log n
n−2
n−1
n
Θ(n log n)
Θ(n2 )
Algorithmen und Datenstrukturen (WS 2010/2011)
16
Welcher Fall ist typisch? In der folgenden Aussage wird angenommen, dass
alle möglichen Sortierungen der Eingabefolge gleich wahrscheinlich sind.
2.4 Satz
Die mittlere Laufzeit von QuickSort ist in Θ(n log n).
average case
Beweis. Jedes Element von M wird genau einmal zum Pivot-Element. Ist
die Eingabereihenfolge in M zufällig, dann auch die Reihenfolge, in der die
Elemente zu Pivot-Elementen werden.
Da die Anzahl der Schritte von der Anzahl der Vergleiche bei der Aufteilung in Teilfolgen dominiert wird, bestimmen wir den Erwartungswert
der Anzahl von Paaren, die im Verlauf des Algorithmus verglichen werden. Die Elemente von M seien entsprechend ihrer korrekten Sortierung mit
a1 < . . . < an bezeichnet. Werden Elemente ai , aj , i < j, verglichen, dann ist
eines von beiden zu diesem Zeitpunkt Pivot-Element, und keins der Elemente
ai+1 < . . . < aj−1 war bis dahin Pivot (sonst wären ai und aj in verschiedenen Teilarrays). Wegen der zufälligen Reihenfolge der Pivot-Wahlen ist die
Wahrscheinlichkeit, dass von den Elementen ai < . . . < aj gerade ai oder aj
1
1
+ j−i+1
. Dies gilt für jedes Paar, sodass
zuerst gewählt werden, gerade j−i+1
sich als erwartete Anzahl von Vergleichen und damit mittlere Laufzeit ergibt
n−1 n−i+1
n X
n
X
X 2
X
1
2
=
≤2
= 2n · Hn
j
−
i
+
1
k
k
i=1 k=2
i=1 k=1
j=i+1
n−1 X
n
X
i=1
∈
Θ(n log n) .
Satz 1.10
Nach Satz 2.3 ist die Laufzeit also immer irgendwo in Ω(n log n) ∩ O(n2 ),
wegen Satz 2.4 jedoch meistens nahe der unteren Schranke.
2.5 Bemerkung (randomisiertes QuickSort)
Die average-case Analyse zeigt auch, dass eine Variante von Quicksort, in
der das Pivot-Element zufällig (statt immer von der ersten Position) gewählt
wird, im Mittel auch auf Eingaben schnell ist, die lange vorsortierte Teilfolgen
enthalten. Die gleiche Wirkung erhält man durch zufälliges Permutieren der
Eingabe vor dem Aufruf von QuickSort.
2.2.2
MergeSort
Bei QuickSort ist die Aufteilung zwar aufwändig und kann ungünstig erfolgen, garantiert dafür aber eine triviale Kombination der Teilergebnisse. Im
Algorithmen und Datenstrukturen (WS 2010/2011)
17
Gegensatz dazu gilt bei MergeSort:
easy split,
hard join
• triviale Aufteilung in günstige Teilfolgengrößen
• linearer Aufwand für Kombination (und zusätzlicher Speicherbedarf)
Algorithmus 7: MergeSort
Aufruf: mergesort(M, 1, n)
mergesort(M, l, r) begin
if l < r then
m ← b l+r−1
c
2
mergesort(M, l, m)
mergesort(M, m + 1, r)
i ← l; j ← m + 1; k ← l
while i ≤ m and j ≤ r do
if M [i] ≤ M [j] then
M 0 [k] ← M [i];
i←i+1
else
M 0 [k] ← M [j];
j ←j+1
k ←k+1
for h = i, . . . , m do
M [k + (h − i)] ← M [h]
for h = l, . . . , k − 1 do
M [h] ← M 0 [h]
end
r
l
M:
m
r
l
M:
→i
→j
m m+1
M0 :
M:
j
Algorithmen und Datenstrukturen (WS 2010/2011)
18
2.6 Beispiel (MergeSort)
5
8
7
27
9
1
17
23
5
8
7
27
9
1
17
23
5
8
7
27
9
1
17
23
5
8
7
27
9
1
17
23
5
8
7
27
1
9
17
23
5
7
8
27
1
9
17
23
1
5
7
8
9
17
23
27
2.7 Satz
Die Laufzeit von MergeSort ist in Θ(n log n).
Beweis. Wie bei QuickSort setzt sich die Laufzeit aus dem Aufwand für
Aufteilung und Kombination zusammen. Für MergeSort gilt:
• konstanter Aufwand für Aufteilung und
• linearer Aufwand für Kombination (Mischen).
In jeder Rekursionsebene ist der Aufwand damit linear in der Gesamtzahl
der Elemente
0
1
2
log n
und wegen der rekursiven Halbierung der Teilfolgenlänge ist die Rekursionstiefe immer log n. Die Gesamtlaufzeit ist daher immer in Θ(n log n).
best, average und
worst case
Algorithmen und Datenstrukturen (WS 2010/2011)
2.3
19
HeapSort
SelectionSort (Abschnitt 2.1) verbringt die meiste Zeit mit der Auswahl des
Extremums und kann durch eine besondere Datenstruktur zur Verwaltung
der Elemente beschleunigt werden. Wir werden hier immer das Maximum
ans Ende der Restfolge setzen und wollen daher einen Datentyp, der schnell
das Maximum einer veränderlichen Menge von Werten zurück gibt. Die Prioritätswarteschlange ist ein abstrakter Datentyp für genau diesen Zweck.
MaxPriorityQueque
insert(item a)
item extractMax()
Falls beide Operationen mit o(n) Laufzeit realisiert sind, ergibt sich eine
Verbesserung gegenüber SelectionSort.
Eine mögliche solche Implementation ist ein binärer Heap: darin werden die
Elemente in einem vollständigen binären Baum gespeichert, der die folgende
Bedingung erfüllen muss.
Heap-Bedingung: Für jeden Knoten gilt, dass der darin gespeicherte Wert nicht kleiner ist als die beiden Werte in seinen
Kindern.
≥
≥
≥
≥
≥
≥
Eine unmittelbare Folgerung aus der Heap-Bedingung ist, dass im ganzen
Teilbaum eines Knotens kein größerer Wert vorkommt.
Ein binärer Heap kann in einem Array realisiert werden, d.h. ohne Zeiger etc.
zur Implementation der Baumstruktur:
Algorithmen und Datenstrukturen (WS 2010/2011)
20
1
b 2i c
2
3
4
i
5
6
7
8
2i
2i + 1
9
Die beiden Operationen der Prioritätswarteschlange werden dann wie folgt
umgesetzt. Bei insert a → M wird das neue Element hinter allen Elementen
im Array eingefügt (also als rechtestes Blatt im Binärbaum) und solange mit
seinem Elternknoten vertauscht, bis die Heap-Bedingung wieder hergestellt
ist.
Algorithmus 8: insert a → M (M enthält aktuell n Elemente)
begin
i←n+1
while (i > 1) and (M [b 2i c] < a) do
// nicht-striktes and
i
M [i] ← M [b 2 c]
i ← b 2i c
M [i] ← a
end
Da der Binärbaum vom Blatt bis höchstens zur Wurzel durchlaufen wird und
jeweils konstanter Aufwand anfällt, ist die Laufzeit in O(log n).
Algorithmen und Datenstrukturen (WS 2010/2011)
21
Analog kann für extractMax a ← M das erste Arrayelement – die Wurzel
des Baumes, und damit das größte Element im Heap – entfernt und das so
entstandene „Loch“ jeweils mit einem größten Kind gefüllt werden. Da das
schließlich zu löschende Blatt in der Regel nicht an der gewünschten Stelle
steht (nämlich am Ende des Arrays), bietet sich jedoch eine andere Vorgehensweise an: Wir schreiben das letzte Element des Arrays in die Wurzel,
und vertauschen von dort absteigend solange mit einem größeren Kind, bis
die Heap-Bedingung wieder hergestellt ist.
Dieses Methode wird auch heapify, der zu Grunde liegende Prozess versickern genannt. Die Implementation erfolgt in der Regel etwas allgemeiner,
um die Wiederherstellung der Heap-Bedingung auch an anderer Stelle i 6= 1
als der Wurzel veranlassen zu können und die rechte Grenze (den rechtesten
tiefsten Knoten) vorgeben zu können, an dem die Vertauschungen stoppen
sollen. Um Zuweisungen einzusparen, werden die Vertauschungen außerdem
nicht explizit durchgeführt, sondern das Element a in der Wurzel (des Teilbaums) zwischengespeichert, und das entstandene Loch absteigend von unten
gefüllt, bis das Element selbst einzutragen ist.
Algorithmus 9: Wiederherstellung der Heap-Bedingung
heapify(i, r) begin
a ← M [i]; j ← 2i
while j ≤ r do
if (j < r) and (M [j + 1] > M [j]) then j ← j + 1
if a < M [j] then
f06-25:43
M [i] ← M [j]
i ← j; j ← 2i
else
j ←r+1
M [i] ← a
end
Unter Verwendung von heapify kann die Extraktion des Maximums jetzt
wie folgt durchgeführt werden.
Algorithmen und Datenstrukturen (WS 2010/2011)
22
Algorithmus 10: extractMax a ← M
begin
if n > 0 then
a ← M [1]
M [1] ← M [n]
n←n−1
heapify(1, n)
return a
else
error „Heap leer“
end
Wieder wird der Binärbaum maximal von der Wurzel zu einem Blatt durchlaufen, sodass die Operationen Einfügen und Maximumssuche in O(log n) ⊂
o(n) Zeit augeführt werden.
Durch Einfügen aller Elemente in einen Heap und wiederholter Extraktion
des Maximums kann SelectionSort also schneller implementiert werden. Wir
vermeiden nun noch die Erzeugung einer Instanz des Datentyps und führen
die notwendigen Operationen direkt im zu sortierenden Array aus. Der sich
daraus ergebende Sortieralgorithmu heißt HeapSort und besteht aus zwei
Phasen:
1. Aufbau des Heaps:
Herstellen der Heap-Bedingung von unten nach oben,
2. Abbau des Heaps:
Maximumssuche und Vertauschen nach hinten
Unter Verwendung von heapify ist die Implemention denkbar einfach.
Algorithmus 11: HeapSort
begin
for i = b n2 c, . . . , 1 do
heapify(i, n)
for i = n, . . . , 2 do
vertausche M [1], M [i]
heapify(1, i − 1)
end
// Aufbau
// Abbau
Algorithmen und Datenstrukturen (WS 2010/2011)
23
2.8 Beispiel
Aufbau des Heaps:
5
5
8
8
7
7
27
27
heapify(4,8)
9
9
1
1
17
17
23
23
heapify(3,8)
5
5
27
8
17
17
23
27
heapify(2,8)
9
9
1
1
7
8
heapify(1,8)
27
23
17
8
9
1
7
5
7
23
Algorithmen und Datenstrukturen (WS 2010/2011)
24
2.9 Beispiel
Abbau des Heaps:
27
23
23
9
17
17
vertausche(1,8),
heapify(1,7)
8
8
9
5
1
1
7
7
5
27
vertausche(1,7),
heapify(1,6)
9
17
8
9
7
7
vertausche(1,6),
heapify(1,5)
1
8
5
5
17
1
23
23
27
27
vertausche(1,5),
heapify(1,4)
8
7
5
5
7
1
vertausche(1,4),
heapify(1,3)
1
8
9
9
17
17
23
23
27
27
vertausche(1,3),
heapify(1,2)
1
5
5
1
7
7
vertausche(1,2),
heapify(1,1)
8
8
9
9
17
17
23
27
23
27
Algorithmen und Datenstrukturen (WS 2010/2011)
25
2.10 Satz
Die Laufzeit von HeapSort ist in O(n log n).
Beweis. Wir nehmen zuächst an, dass n = 2k − 1. Beim Aufbau des Heaps
werden bn/2c Elemente in ihren Teilbäumen versickert. Für die ersten n/4
Elemente haben diese Höhe 1, für die nächsten n/8 Elemente die Höhe 2 und
allgemein gibt es n/2i Elemente der Höhe i, i = 2, . . . , log n. Damit ist der
Aufwand für den Aufbau
dlog ne
log n
X i
X n
·
i
=
n
2i
2i
i=2
i=2
log n
log n
X i
X i
= n 2
−
2i
2i
i=2
i=2
!
(konstruktive Null)
log n−1
!
X i + 1 log
Xn i
= n
−
2i
2i
i=1
i=2
!
log n−1 X i+1
i
log n
2
+
− i −
= n
i
2
2
2
n
i=2


log n−1

X 1 log n 


= n 1 +
−

i
2
n 

i=2
| {z }
<1
< 2n ∈ Θ(n) .
Ist nun allgemein 2k − 1 < n < 2k+1 − 1, dann bleiben die Überlegungen
richtig und der Aufwand verdoppelt sich höchstens.
Beim Abbau des Heaps ist die Höhe der Wurzel zu jedem Zeitpunkt höchstens
log n, sodass der Abbau O(n log n) Zeit benötigt.
Wie MergeSort kommt HeapSort also in jedem Fall mit Laufzeit O(n log n)
aus, braucht aber nur O(1) zusätzlichen Speicher.
Es bleibt die Frage, ob man nicht auch noch schneller sortieren kann?
Algorithmen und Datenstrukturen (WS 2010/2011)
2.4
26
Untere Laufzeitschranke
Eine triviale untere Schranke für die Laufzeit von Sortieralgorithmen ist Ω(n),
da natürlich alle Elemente der Eingabefolge berücksichtigt werden müssen.
Wir zeigen in diesem Abschnitt aber, dass die bisher erreichte Laufzeit von
O(n log n) für die in Problem 2.1 formulierte Aufgabenstellung bestmöglich
ist.
Wenn keine Einschränkungen der zu sortierenden Elemente vorliegen, muss
ein Sortieralgorithmus Paare von Elementen vergleichen, um ihre Reihenfolge
zu bestimmen. Ein solcher vergleichsbasierter Algorithmus heißt auch allgemeines Sortierverfahren (andere Beispiele betrachten wir im nächsten Abschnitt). Wieviele Vergleiche muss der beste Sortieralgorithmus (im schlechtesten Fall) mindestens machen?
Abhängig vom Ergebnis des ersten Vergleichs wird der Algorithmus irgendwelche weiteren Schritte ausführen, insbesondere weitere Vergleiche. Wir können den Ablauf des Algorithmus daher mit einem Abstieg im Entscheidungsbaum gleich setzen: An der Wurzel steht der erste Vergleich, an den beiden
Kindern stehen die Vergleiche, die im Ja- bzw. Nein-Fall als nächste ausgeführt werden, usw.
Bei n verschiedenen zu sortierenden Elementen (wir schätzen den schlechtesten Fall ab!) hat der Entscheidungsbaum n! Blätter, denn es gibt n! mögliche
Eingabereihenfolgen, und wenn zwei verschiedene Eingaben denselben Ablauf (insbesondere die gleichen Umsortierungen) zur Folge haben, wird eine
von beiden nicht richtig sortiert. Die Anzahl der Vergleiche entspricht aber
gerade der Höhe des Entscheidungsbaumes. In einem Binärbaum haben n!
Blätter mindestens n!2 Vorgänger, die wiederum mindestens 2n!2 Vorgänger ha-
Algorithmen und Datenstrukturen (WS 2010/2011)
27
ben, usw. Der längste Weg zur Wurzel hat damit mindestens
n n 2
n
n
log n! = log[n · (n − 1) · . . . · 2 · 1] ≥ log
= · log ∈ Ω(n log n)
2
2
2
Knoten. Es kann daher keinen vergleichsbasierten Sortieralgorithmus geben,
der bei jeder Eingabe eine Laufzeit in o(n log n) hat.
Wir haben damit den folgenden Satz bewiesen.
2.11 Satz
Jedes allgemeine Sortierverfahren benötigt im schlechtesten Fall Ω(n log n)
Vergleiche.
2.5
Sortierverfahren für spezielle Universen
Die untere Laufzeitschranke für allgemeine Sortierverfahren kann nur unterboten werden, wenn zusätzliche Annahmen über die zu sortierenden Elemente
gemacht werden dürfen. In diesem Abschnitt betrachten wir Spezialfälle, bei
denen die Universen aus Zahlen bestehen. Die Sortierverfahren können dann
die Größe dieser Zahlen zu Hilfe nehmen; tatsächlich kommen die Algorithmen ganz ohne paarweise Vergleiche aus.
2.5.1
BucketSort
Voraussetzung für BucketSort ist, dass die Elemente der Eingabe reelle Zah- bucket:
„Eimer“,
len sind. Wir nehmen weiter an, dass U = (0, 1]; dies ist keine Einschränkung, „Kübel“
da andere Eingabewerte immer entsprechend verschoben und skaliert werden
können.
Ausgehend von der Hoffnung, dass die Eingabezahlen M = {a1 , . . . , an } im
Intervall (0, 1] einigermaßen gleichmäßig verteilt sind, werden buckets
j
j+1
Bj = a ∈ M :
<a≤
für j = 0, . . . , n − 1
n
n
erzeugt und mit irgendeinem anderen Verfahren separat sortiert. Die sortierten Teilfolgen können dann einfach aneinander gehängt werden. Dahinter
steht die Überlegung, dass es besser ist, k Teilprobleme der Größe n/k zu
Algorithmen und Datenstrukturen (WS 2010/2011)
28
bearbeiten, als eines der Größe n, wenn die Laufzeitfunktion schneller als
linear wächst (was für allgemeine Sortierverfahren ja der Fall sein muss). Im
Idealfall landet jeder Eingabewert in einem eigenen bucket und das Sortieren
entfällt.
Algorithmus 12: BucketSort
begin
for j = 0, . . . , n − 1 do B[j] ← leere Liste
for i = 1, . . . , n do append B [dn · M [i]e − 1] ← M [i]
i←0
for j = 0, . . . , n − 1 do
sort(B[j])
// Sortierverfahren frei wählbar
while B[j] nicht leer do
M [i] ← extractFirst(B[j])
i←i+1
end
Wegen des Rückgriffs auf einen anderen Algorithmus für die Teilprobleme ist
BucketSort ein so genanntes Hüllensortierverfahren.
2.12 Satz
Die mittlere Laufzeit von BucketSort ist in O(n).
Beweis. Annahme: Die zu sortierenden Elemente sind gleichverteilt aus
(0, 1]. Die Laufzeit wird dominiert von der Sortierung der Buckets (alle übrigen Schritte benötigen Laufzeit O(n)). Wird ein Sortierverfahren verwendet, das Buckets der Größe nj = |B[j]| in O(n2j ) sortiert, ist die Laufzeit
P
2
O( n−1
j=0 nj ). Dies ist asymptotisch gleich mit
!
n−1
X
X
1 falls ai ∈ B[j]
O
bj,i1 · bj,i2
für bj,i =
0 sonst
j=0 0<i1 ≤i2 ≤n
Die Produkte bj,i1 · bj,i2 sind immer 1, wenn i1 = i2 , und die Summanden für
ein festes Paar i1 6= i2 sind im Mittel über alle Eingaben gerade 1/n, weil es
n2 Kombinationen von Buckets gibt, in denen ai1 und
Pai2 liegen können,
P P von
denen n gerade gleiche Buckets darstellen. Also O( n2j ) = O(
bj,i1 ·
j
j i1 ,i2
bj,i2 ) = O(n).
Algorithmen und Datenstrukturen (WS 2010/2011)
2.5.2
29
CountingSort
Sind nur ganzzahlige Werte zu sortieren, d.h. ist U ⊆ N0 , kann BucketSort
so vereinfacht werden, dass jedem möglichen Wert ein Eimer entspricht und
für jeden davon nur die Elemente mit diesem Wert gezählt werden. Die Sortierung ergibt sich dann einfach dadurch, dass für jeden möglichen Wert in
der Reihenfolge vom kleinsten zum größten die Anzahl seiner Vorkommen
wieder in das Array M geschrieben wird.
Wir nehmen der Einfachheit halber an, dass es sich um die Zahlen {0, . . . , k −
1} handelt (andere endliche Intervalle können wieder entsprechend verschoben werden).
Algorithmus 13:
begin
for j = 0, . . . , k − 1 do C[j] ← 0
for i = 1, . . . , n do C[M [i]] ← C[M [i]] + 1
i←1
for j = 0, . . . , k do
while C[j] > 0 do
M [i] ← j; i ← i + 1
C[j] ← C[j] − 1
end
Diese Version ist allerdings von geringer praktischer Bedeutung, da sie voraussetzt, dass tatsächlich nur Zahlen sortiert werden. Sind die Zahlen mit
anderen Daten assoziiert, dann geht die Zuordnung verloren, weil die Laufvariable j in der letzte Schleife nur die möglichen Werte annimmt, aber die
zu diesen Wert gehörigen Daten nicht in konstanter Zeit ermittelt werden
können.
Um die Zuordnung zu erhalten, wird ein Zwischenspeicher eingeführt, damit
die Elemente der Eingabe nicht überschrieben werden. Während bei BucketSort vom Wert eines Elements auf die Position geschlossen wurde, geschieht
dies beim folgenden CountingSort durch Bestimmung der Anzahl kleinerer „Sortieren
Elemente. Durch den eingeschränkten Wertebereich können diese Anzahlen durch
Abzählen“
effizient bestimmt werden, indem in einer dritten Vorverarbeitungsschleife
die den Positionen entsprechenden Präfixsummen der gezählten Vorkommen
gebildet werden. Um das Verfahren darüber hinaus stabil zu machen, werden
Algorithmen und Datenstrukturen (WS 2010/2011)
30
die Intervalle gleicher Werte von hinten nach vorne ausgelesen.
Algorithmus 14: CountingSort
begin
for j = 0, . . . , k − 1 do C[j] ← 0
for i = 1, . . . , n do C[M [i]] ← C[M [i]] + 1
for j = 1, . . . , k − 1 do C[j] ← C[j − 1] + C[j]
for i = n, . . . , 1 do
M 0 [C[M [i]]] ← M [i]
C[M [i]] ← C[M [i]] − 1
M ← M0
end
Die Laufzeit lässt sich unmittelbar aus den Schleifen ablesen.
2.13 Satz
Die Laufzeit von CountingSort ist in O(n + k).
2.5.3
RadixSort
Selbst bei ganzen Zahlen kann das Intervall möglicher Werte im Allgemeinen
nicht genügend stark eingeschränkt werden (man erhält also zu große k). Um
die Idee von CountingSort trotzdem verwenden zu können, nutzen wir nun
aus, dass bei Darstellung aller Zahlen bezüglich einer festen Basis zwar die
Länge variiert, nicht aber die Anzahl der möglichen Ziffern.
Hat man ein stabiles Sortierverfahren für das Universum U = {0, . . . , d − 1}
aus Ziffern in der d-ären Darstellung, dann können die Eingabewerte stellenweise von hinten nach vorne sortiert werden. Durch die Stabilität bleibt die
Reihenfolge bezüglich niederer Stellen erhalten, wenn bezüglich einer höherwertigen Stelle sortiert wird.
Algorithmus 15: RadixSort(M )
begin
s ← blogd (maxi=1,...,n M [i])c
for i = 0, . . . , s do
sortiere M bzgl. Stelle i (stabil) (∗)
end
Algorithmen und Datenstrukturen (WS 2010/2011)
31
Als Stellen-Sortierverfahren für (∗) bietet sich natürlich CountingSort an.
2.14 Satz
Die Laufzeit von RadixSort ist in Θ(s · (n + d)).
Auch wenn dadurch zusätzlicher Speicher benötigt wird, lohnt sich ein großes
d (z.B. d = 256 für byteweise Aufteilung), da s dann exponentiell kleiner wird.
Die Zähler und sogar der für CountingSort benötigte Zusatzspeicher lassen sich aber auch ganz vermeiden, denn für den Spezialfall von Binärzahlen (oder wenn man die Ziffern eines d-ären System in ihrer Binärdarstellung notiert), kann die folgende Variante verwendet werden. Darin wird
wie bei QuickSort partitioniert, allerdings nicht aufgrund eines Pivots, sondern jeweils aufgrund der b-ten Binärziffer. In der Binärdarstllung seien
die Bits dabei vom höchstwertigen zum niedrigstwertigen nummeriert und
bit(Bs−1 Bs−2 · · · B1 B0 , b) = Bb für b ∈ {0, . . . , s − 1}.
Algorithmus 16: RadixExchangeSort (für s-stellige Binärzahlen)
Aufruf: RadixExchangeSort(M, 1, n, s − 1)
RadixExchangeSort(M, l, r, b) begin
if l < r then
i ← l; j ← r
while i ≤ j do
while i ≤ j and bit(M [i], b) = 0 do i ← i + 1
while i ≤ j and bit(M [j], b) = 1 do j ← j − 1
if i < j then vertausche M [i], M [j]
if b > 0 then
RadixExchangeSort(M, l, i − 1, b − 1)
RadixExchangeSort(M, i, r, b − 1)
end
2.15 Satz
Die Laufzeit von RadixExchangeSort für s-stellige Binärzahlen ist in Θ(s · n).
Das Verfahren ist besonders geeignet, wenn s klein ist im Verhältnis zu n,
insbesondere falls s konstant oder zumindest s ∈ O(log n). Es ist insbesondere dann nicht geeignet, wenn wenige Zahlen mit großer Bitlänge zu sortieren
sind. Ein weiterer Nachteil ist, dass die für die Partitionierung benötigten
Algorithmen und Datenstrukturen (WS 2010/2011)
32
Austauschoperationen wie bei QuickSort verhindern, dass das Verfahren stabil ist.
2.6
Gegenüberstellung
Zum Abschluss des Kapitels sind einige der wesentlichen Vor- und Nachteile der verschiedenen Sortieralgorithmen in Tabelle 2.6 einander gegenüber
gestellt.
n log n
n log n
n log n
n
n+k
s · (n + d)
s·n
n log n
n log n
n log n
n log n
n+k
s · (n + d)
s·n
MergeSort
HeapSort
untere Schranke
BucketSort
CountingSort
RadixExchangeSort
s·n
s · (n + d)
n+k
n
n
n
n log n
n log n
n2
O(n)
Θ(n + d)
Θ(n + k)
Θ(n)
n.a.
7
X
X
X
n.a.
Binärzahlen, Bitlänge s
d-äre Zahlen, Wortlänge s
ganze Zahlen aus {0, k − 1}
reelle Zahlen aus (0, 1]
keine
keine
7
Θ(1)
keine
X
O(n)
keine
keine
Einschränkung
7
7
stabil
?
O(n)
Θ(1)
ZusatzSpeicher
Tabelle 2.1: Zusammenstellung der behandelten Sortierverfahren
n log n
n2
QuickSort
RadixSort
n2
n2
Laufzeitklasse
worst
average
best
SelectionSort
Algorithmus
Algorithmen und Datenstrukturen (WS 2010/2011)
33
Kapitel 3
Suchen
In diesem Kapitel behandeln wir Algorithmen für das Auffinden von Elementen in einer Menge bzw. den Test, ob das Element überhaupt in der Menge enthalten ist. Die Algorithmen basieren darauf, dass die Menge in einer
speziell für diese Art Anfrage gewählten Datenstruktur, einem Wörterbuch,
verwaltet wird.
Elemente können beliebige Daten – auch verschiedenen Typs – sein, sie müssen aber über Schlüssel (d.h. eine eindeutige Kennungen) desselben Typs
identifiziert werden. Grundsätzlich betrachten wir also Implementationen eines abstrakten Datentyps Dictionary, in dem Paare von Elementen und
Schlüsseln eingetragen werden.
Dictionary
elem find(key k)
item insert(elem a, key k)
elem remove(key k)
Wir sind vor allem an der schnellen Suche nach Einträgen interessiert.
3.1
Folgen
Einfache Datenstrukturen für die Implementation eines Wörterbuchs sind
Arrays und Listen. Diese haben unterschiedlich günstige Eigenschaften bezüglich Speicherbedarf, Zugriffszeiten und Größenveränderung (Einfügen und
34
Algorithmen und Datenstrukturen (WS 2010/2011)
35
Löschen), aber der wesentliche Freiheitsgrad ist in beiden Fällen die Reihenfolge, in der die Einträge angeordnet sind.
Wir gehen davon aus, dass die Suche in einem Array oder einer Folge M
als Durchlauf von vorne nach hinten implemeniert ist, und Zeit i benötigt,
wenn das Element an i-ter Stelle steht. Andere Suchstrategien können durch
entsprechende Umordnung darauf zurück geführt werden.
Bevor wir Anordnungsstrategien behandeln, betrachten wir zum Vergleich
zunächst unsortierte (beliebig angeordnete) Folgen, in denen durch lineare
Suche, d.h. Durchlaufen aller Positionen, in linearer Zeit festgestellt werden
kann, ob und wo ein Element mit einem bestimmten Schlüssel vorkommt.
3.1 Satz
Lineare Suche benötigt auch im mittleren Fall Zeit Θ(n).
Beweis. Kommt der gesuchte Schlüssel nicht vor, weiß man das erst nach
Durchlaufen der ganzen Folge. Kommt der Schlüssel vor, dann an jeder Stelle
i = 1, . . . , n mit gleicher Wahrscheinlichkeit 1/n. Die lineare Suche durchläuft
zunächst alle davor liegenden Stellen, prüft also insgesamt i Folgenelemente.
Die mittlere Laufzeit ist damit
n
X
n(n + 1)
n+1
1
·i=
=
∈ Θ(n) .
n
2n
2
i=1
Unter der Annahme, dass die Reihenfolge der zulässigen Werte im Array
zufällig ist und dass nach jedem Schlüssel mit gleicher Wahrscheinlichkeit
gesucht wird, kann keine bessere Strategie angegeben werden. Da der jeweils
gesuchte Schlüssel in diesem Szenario an beliebiger Stelle stehen kann, lernen wir nichts daraus, ihn an bestimmten anderen Stellen nicht gefunden zu
haben.
Wir behandeln zwei Methoden, durch spezifische Anordnung der Schlüssel
eine bessere Laufzeit zu bekommen.
• Umordnung von Listen aufgrund von Anfragen
• Speicherung in sortiertem Array
Algorithmen und Datenstrukturen (WS 2010/2011)
3.1.1
36
Selbstanordnende Folgen
Wenn die Reihenfolge der Schlüssel einer Liste geändert werden darf, sollte
das Ziel sein, häufiger angefragte Schlüssel weiter vorne stehen zu haben.
Sind die Anfragehäufigkeiten im Vorhinein bekannt, dann kann die Folge
danach sortieren werden. Wir nehmen an, dass dies nicht der Fall ist, und
behandeln drei nahe liegende und übliche Adaptionsstrategien:
• MF (move to front): Der Schlüssel, auf den gerade zugegriffen wurde,
wird an die erste Stelle gesetzt.
• T (transpose): Der Schlüssel, auf den gerade zugegriffen wurde, wird
mit seinem Vorgänger vertauscht.
• FC (frequency count): Sortiere die Liste immer entsprechend einem Zähler für die Zugriffshäufigkeiten.
3.2 Beispiel
Für eine Menge M = {1, . . . , 8} betrachten wir die Zugriffsfolgen
1. 10 × (1, 2, . . . , 8) und
2. 10 × 1, 10 × 2, . . . , 10 × 8 .
Bei beliebiger statischer Anordnung wird auf jeden Schlüssel und damit auf
jede Position 10× zugegriffen, die Laufzeit ist daher immer
10 ·
8
X
i=1
i = 10 ·
8·9
= 360
2
unabhängig von Anordnung und Anfragereihenfolge. Eine einzelne Suche benötigt im Mittel also 360
= 4.5.
80
Wird beginnend mit M = (1, . . . , 8) die Umordnungsstrategie MF verwendet, ergeben sich für die beiden Zugriffsfolgen folgende Suchzeiten:
P8
1. Die ersten 8 Zugriffe benötigen
i=1 i = 36 Schritte, nach denen
M = (8, 7, 6, . . . , 1) ist. Jeder weitere Zugriff benötigt 8 Schritte, da
der gesuchte Schlüssel
P8 immer gerade am Ende der Liste steht. Insgesamt im Mittel ( i=1 i + 9 · 8 · 8)/80 = 7.65.
Algorithmen und Datenstrukturen (WS 2010/2011)
37
2. Die ersten 10 Zugriffe erfolgen auf den ersten Schlüssel, dann 1 Zugriff
auf den zweiten, 9 auf den ersten, dann 1 Zugriff
auf den dritten, 9 auf
P
den ersten, usw. Insgesamt im Mittel also ( 8i=1 i + 8 · 9 · 1)/80 = 1.35.
MF kann also gut oder schlecht sein, je nach Zugriffsreihenfolge; ähnlich für
T und FC (hier evtl. noch zusätzlicher Speicher für Häufigkeitszähler). Experimentell zeigt sich, dass T schlechter als MF ist. FC und MF sind ähnlich,
wobei MF manchmal besser abschneidet.
Wir sind an allgemeinen Aussagen zur Güte interessiert und vergleichen MF
daher mit einem beliebigen Algorithmus A:
3.3 Def inition
Für einen Anordnungsalgorithmus A definieren wir für die Zugriffsfolge S =
(k1 , . . . , km )
CA (S)
FA (S)
Kosten für Zugriffe S.
kostenfreie Vertauschungen von benachbarten
Schlüsseln an den jeweils durchsuchten Positionen.
XA (S) kostenpflichtige Vertauschungen.
M:
k
F
X
Insbesondere gilt:
XMF (S) = 0
für alle S
FA (S) ≤ CA (S) − |S| für alle A, S
da man bei Kosten i für einen Zugriff mit
maximal i − 1 Schlüsseln kostenfrei tauschen
kann.
3.4 Satz
Für jeden Algorithmus A zur Selbstanordnung von Listen gilt für jede Folge S
von Zugriffen
CMF (S) ≤ 2 · CA (S) + XA (S) − FA (S) − |S| .
Algorithmen und Datenstrukturen (WS 2010/2011)
38
Beweis. Um das Verhältnis der Kosten von MF und A beurteilen zu können amortisierte
(ohne A zu kennen!) führen wir Buch über den Unterschied im Zustand der worst-case
Analyse
beiden Listen, indem wir die Anzahl der Inversionen (Paare von Schlüsseln,
die nicht in der gleichen Reihenfolge auftreten) abschätzen.
0
Beide Listen beginnen mit derselben Ordnung, MMF
= MA0 , und die Zahl
0
0
der Inversionen ist inv(MA , MMF ) = 0. Was ändert sich durch einen Zugriff
auf den i-te Schlüssel von MA ?
xk
MA :
k
i
MMF :
xk
k
j
xk : Anzahl der Schlüssel, die in MMF vor und
in MA hinter k liegen.
MF ordnet k an den Anfang der Liste
Vertauschungen von A bewirken
um
−FA (k) Inversionen
−xk
Inversionen
+XA (k) Inversionen
+(j − 1 − xk ) Inversionen
Also ist die Anzahl der Inversionen bei Zugriff t
t−1
t
inv(MAt , MMF
) = inv(MAt−1 , MMF
) − xk + (j − 1 − xk ) − FA (kt ) + XA (kt )
für S = (k1 , . . . , km ) und t ∈ {1, . . . , m}.
Statt der echten Kosten CMF (kt ) der t-ten Suche bei MF betrachten wir
deren amortisierten Kosten at : diese bestehen aus den echten Kosten und
der Veränderung des Unterschieds zur Liste von A (d.h. den Investitionen in
eine bessere Listenordnung bzw. dem Profit daraus):
t−1
t
at = CMF (kt ) + inv(MAt , MMF
) − inv(MAt−1 , MMF
)
= j − xk + (j − 1 − xk ) − FA (kt ) + XA (kt )
−1 − FA (kt ) + XA (kt )
= 2
(j − x )
| {z k}
≤ i
Anzahl Schlüssel, die in
beiden Listen vor k liegen:
j − 1 − xk ≤ i − 1
⇐⇒ j − xk ≤ i
Algorithmen und Datenstrukturen (WS 2010/2011)
39
Da für die amortisierten Kosten
|S|
X
at =
|S|
X
t−1
t
) − inv(MAt−1 , MMF
)
CMF (kt ) + inv(MAt , MMF
t=1
t=1
=
0
)+
−inv(MA0 , MMF
|
{z
}
=0
|S|
X
t=1
|S|
|S|
CMF (kt ) + inv(MA , MMF )
|
{z
}
≥0
|S|
≥
X
CMF (kt ) = CMF (S)
t=1
gilt, folgt
CMF (S) ≤
|S|
X
t=1
at ≤
|S|
X
2 · CA (kt ) − 1 − FA (kt ) + XA (kt )
t=1

= 2
|S|
X

CA (kt ) − |S| − FA (S) + XA (S)
t=1
= 2 · CA (S) − |S| − FA (S) + XA (S) .
MF ist also im Wesentlichen mindestens halb so gut wie ein beliebiger Anordnungsalgorithmus A, der sogar speziell für eine schon vorher bekannte
Zugriffsfolge S entworfen sein kann.
3.1.2
Sortierte Arrays
Können wir voraussetzen, dass M sortiert ist, dann bedeutet das Finden eines
anderen Elements an einer bestimmten Stelle, dass das gesuchte davor oder
dahinter stehen muss – je nachdem, ob sein Schlüssel kleiner oder größer als
der gefundene ist. Bei der binären Suche in Algorithmus 17 wird bei jedem
Vergleich mit einem Array-Element die Hälfte der verbleibenden Elemente
von der weiteren Suche ausgeschlossen.
Algorithmen und Datenstrukturen (WS 2010/2011)
40
Algorithmus 17: Binäre Suche
binsearch(M [0, . . . , n − 1], k) begin
l ← 0; r ← n − 1
while k ≥ M [l] andk ≤ M [r] do
m ← b l+r
c
2
if k > M [m] then
l ←m+1
else if k < M [m] then
r ←m−1
else
return „k ist in M “
return „k ist nicht in M “
end
3.5 Beispiel
[TODO: z.B. 0,2,5,5,8,10,12,13,18,23,36,42,57,60,64,666, Anfragen nach 18,9.]
3.6 Bemerkung
Wir könnten sogar angeben, an welcher Stelle k im Array M auftritt, gehen
aber hier davon aus, dass der aufrufende Programmteil nicht wissen kann,
dass die Schlüssel in einem Array verwaltet werden und daher auch mit der
Positionsinformation nichts anfangen kann. Zum einen verwenden die Verfahren in den nächsten Abschnitten andere Datenstrukturen, und zum anderen
ändern sich die Positionen im Allgemeinen, wenn Elemente eingefügt und
gelöscht werden (da das Array immer sortiert sein muss).
3.7 Satz
Binäre Suche auf einem sortierten Array der Länge n benötigt Θ(log n)
Schritte, um einen Schlüssel k zu finden bzw. festzustellen, dass kein Element mit Schlüssel k in M enthalten ist.
Beweis. Die Anzahl der Schleifendurchläufe ist immer Θ(log n), da das
Intervall M [l, . . . , r] nach jedem Durchlauf eine Länge hat, die um höchstens
eins von der Hälfte der vorherigen abweicht.
3.8 Bemerkung
Moderne Prozessoren verwenden Pipelining, sodass Sprünge in bedingten
Verzweigungen in der Regel zu Zeitverlust führen. Bei im Wesentlichen gleich-
Algorithmen und Datenstrukturen (WS 2010/2011)
41
verteilten Eingaben kann es daher günstiger sein, statt m ← b l+r
c eine un2
1
gleiche Aufteilung wie z.B. m ← b 3 (l + r)c zu wählen, weil die Bedinung in
der Schleife dann voraussichtlich häufiger zutrifft als nicht und der Inhalt der
Pipeline dann nicht verloren geht.
Eine ähnliche Idee wie in der voraus gegangenen Bemerkung liegt auch der
Interpolationssuche zu Grunde. Handelt es sich bei den Schlüsseln des Arrays
um Zahlen und liegt k deutlich näher an einem der Inhalte der Randzellen,
macht es unter Umständen Sinn, nicht in der Mitte, sondern entsprechend
näher an diesem Rand einen Vergleich vorzunehmen und damit gleich mehr
als die Hälfte der verbleibenden auszuschließen.
Algorithmus 18: Interpolationssuche
interpolationsearch(M [0, . . . , n − 1], x) begin
l ← 0; r ← n − 1
while x ≥ Mj[l] and x ≤ M [r]
k do
m←l+
x−M [l]
(r
M [r]−M [l]
− l)
if x > M [m] then
l ←m+1
else if x < M [m] then
r ←m−1
else
return „x ist in M “
return „x ist nicht in M “
end
3.9 Beispiel
[TODO: Gleiche zwei Anfragen wie oben – hat’s was gebracht?]
Binäre und Interpolationssuche setzen voraus, dass man die Länge des Arrays kennt. Wenn die Länge unbekannt (oder zumindest sehr groß) und das
gesuchte Element auf jeden Fall (insbesondere an eher kleiner Indexposition)
enthalten ist, bietet sich an, den Suchbereich zunächst vorsichtig von vorne
ausreichend:
intervallskalierte
Daten
Algorithmen und Datenstrukturen (WS 2010/2011)
42
einzugrenzen.
Algorithmus 19: Exponentielle Suche
expsearch(M [0, . . .], x) begin
r←1
while x > M [r] do r ← 2r
binsearch(M [b 2r c, . . . , r], x)
end
Die Korrektheit des Verfahrens ergibt sich daraus, dass nach Abbruch der
while-Schleife sicher k ∈ [M [b 2r c], M [r]] gilt. Für die Laufzeit beachte, dass
die Teilfolge, auf der binär gesucht wird, in genau so vielen Schritten bestimmt wird, wie die Suche dann anschließend auch braucht. Natürlich kann
statt binärer Suche auch jedes andere Suchverfahren für sortierte Folgen benutzt werden.
3.2
Geordnete Wörterbücher
Ein Wörterbuch heißt geordnet, wenn es so organisiert ist, dass zu jedem
Zeitpunkt mit linearem Aufwand alle Paare aus Schlüsseln und Elementen
in Sortierreihenfolge ausgeben werden können.
Voraussetzung ist daher, dass wir über der Grundmenge, aus der die Schlüssel
stammen, eine Ordnung ≤ gegeben haben. Auch wenn die Schlüssel in den
Beispielen der Einfachheit halber wieder Zahlen sein werden, wird also in der
Regel nicht ausgenutzt, um wieviel sich zwei Werte unterscheiden.
Verfahren für ungeordnete Wörterbucher, die andere Voraussetzungen an die
Schlüssel machen, behandeln wir im nächsten Kapitel.
3.2.1
Binäre Suchbäume
Statt Listen oder Arrays werden wir nun Binärbäume als Datenstruktur für
die Organisation der Schlüssel verwenden. Wir nehmen dabei an, dass in jedem Knoten ein Element mit seinem zugehörigen Schlüssel, die beiden Kinder
und der Vorgänger im Baum wie folgt gespeichert werden.
Algorithmen und Datenstrukturen (WS 2010/2011)
43
Node
node parent
node left, right
item item
Ein beliebiger Binärbaum heißt binärer Suchbaum, wenn die Schlüssel so auf
die Knoten verteilt sind, dass die Suchbaumeigenschaft erfüllt ist.
v.parent
enthält v.item
v
und damit
v.key = v.item.key
Suchbaumeigenschaft:
v.elem = v.item.elem
v.lef t
L(v)
v.right
w.key < v.key
w.key > v.key
∀w ∈ L(v)
∀w ∈ R(v)
R(v)
Wegen der Suchbaumeigenschaft können die Elemente eines Suchbaums T
mittels inorder-Durchlauf in Linearzeit sortiert ausgegeben werden (Aufruf:
inordertraversal(T.root)). Binäre Suchbäume könne daher zur Implementation geordneter Wörterbücher verwendet werden.
Algorithmus 20: inorder-Durchlauf
inordertraversal(v) begin
if v 6= nil then
inordertraversal(v.lef t)
print v.key
inordertraversal(v.right)
end
vgl. HeapBedingung
Algorithmen und Datenstrukturen (WS 2010/2011)
44
Im Folgenden werden die Methoden des ADT Dictionary mit binären Suchbäumen implementiert:
Algorithmus 21: find(k)
v ← search(T, k)
if v 6= nil then
return v.item
else
return nil
// kein Element in T hat Schlüssel k
search(T, k) begin
v ← T.root
while (v 6= nil) and (v.key 6= k) do
if k < v.key then
v ← v.lef t
else
v ← v.right
return v
end
3.10 Beispiel
find(4)
7
2
1
12
5
3
4∈
/T
9
6
Algorithmen und Datenstrukturen (WS 2010/2011)
45
Die Laufzeit ist offensichtlich linear in der Höhe des Baumes. Aber wie hoch
kann ein binärer Baum sein, wenn er die Suchbaumeigenschaft erfüllt?
Ähnlich wie bei der Rekursionstiefe von QuickSort kann eine ungünstige Aufteilung zu einer Höhe (z.B. für n = 8) von
und
mindestens blog nc
führen.
Algorithmus 22: insert(a, k)
v ← T.root
if v = nil then
T.root ← newnode((a, k))
else
while v 6= nil and k 6= v.key do
u←v
if k < v.key then
v ← v.lef t
else
v ← v.right
if k = v.key then
print “Schlüssel k bereits vergeben“
else
v ← newnode(a, k)
if k < u.key then
u.lef t ← v
else
u.right ← v
v.parent ← u
höchstens n − 1
Algorithmen und Datenstrukturen (WS 2010/2011)
46
Algorithmus 23: remove(k)
v ← search(T, k)
if v 6= nil then
a ← v.elem
if v.lef t 6= nil then
u←v
v ← v.lef t
while v.right 6= nil do
v ← v.right
u.item ← v.item
w ← v.lef t
if v = u.lef t then
u.lef t ← w
else
u ← v.parent
u.right ← w
else
w ← v.right
if v = T.root then
T.root ← w; u ← w
else
u ← v.parent
if k < u.key then
u.lef t ← w
else
u.right ← w
if w 6= nil then w.parent ← u
deletenode(v)
else
a ← nil
return a
3.11 Bemerkung
Falls der Baum in einem Array realisiert ist, kann parent weggelassen und
während des Abstiegs identifiziert werden.
Algorithmen und Datenstrukturen (WS 2010/2011)
3.2.2
47
AVL-Bäume
Um kurze Laufzeiten für die Wörterbuchoperationen garantieren zu können,
muss die Höhe der Suchbäume niedrig gehalten werden. Um dem Idealfall
eines vollständigen Binärbaums (dessen Höhe logarithmisch ist) möglichst
nahe zu kommen, wird eine weitere Eigenschaft gefordert.
Balanciertheitseigenschaft: Für jeden (inneren) Knoten eines Binärbaums gilt, dass die Höhe der Teilbäume der beiden Kinder sich um höchstens
1 unterscheidet.
Ein binärer Suchbaum, der zusätzlich die Balanciertheitseigenschaft aufweist,
heisst AVL-Baum. Der folgende Satz zeigt, dass die Höhe balancierter Binär- AdelsonVelskij,
bäume asymptotisch minimal ist.
Landis
3.12 Satz
Ein AVL-Baum mit n Knoten hat Höhe Θ(log n).
Beweis. Jeder Binärbaum mit n Knoten hat Höhe Ω(log n). Für die Abschätzung nach oben betrachte das umgekehrte Problem: Wieviele innere
Knoten hat ein AVL-Baum der Höhe h mindestens? Nenne diese Zahl n(h).
n(2) = 2
n(1) = 1
h ≥ 3: Die beiden Unterbäume der Wurzel müssen ebenfalls AVL-Bäume sein
und haben mindestens Höhe h − 1 und h − 2.
n(h)
≥ n(h − 1) + n(h − 2) + 1
> 2 · n(h − 2)
da n(h) offensichtlich streng
monoton wachsend
h−2
h−1
h
≥ 2d 2 e−1 · n(h − 2 · (dh/2e − 1))
|
{z
}
h
∈{1,2}
h
⇐⇒
⇐⇒
≥ 2d 2 e−1
log n(h) ≥ h2 − 1
h
≤ 2 log n(h) + 2
Also hat ein AVL-Baum mit n Knoten Höhe O(log n).
Algorithmen und Datenstrukturen (WS 2010/2011)
48
find kann unverändert implementiert werden, bei insert und remove muss
jedoch die Balanciertheit erhalten werden. Dazu speichert man in jedem Knoten v zusätzlich den Höhenunterschied seiner beiden Teilbäume. diesem Knoten:
v
L(v)
R(v)
!
balance(v) = h(R(v)) − h(L(v)) ∈ {−1, 0, 1}
Fügt man wie gehabt ein neues Blatt ein, wächst die Höhe der Teilbäume
von Vorfahren um höchstens 1. Möglicherweise wird an diesen dadurch die
Balanciertheitseigenschaft verletzt.
Sei u der erste Knoten auf dem Weg vom eingefügten Blatt zur Wurzel, der
nicht balanciert ist (d.h. |balance(u)| > 1). Seien ferner v und w die beiden
Nachfahren auf dem Weg von u zum eingefügten Knoten (diese existieren sicher, da die Balanciertheitseigenschaft nur bei Knoten der Höhe mindestens 2
verletzt sein kann).
W urzel
u
v
w
← neu eingefügt
Wir bezeichenen mit a, b, c die Knoten u, v, w in inorder-Reihenfolge und
mit T0 , T1 , T2 , T3 deren Unterbäume, ebenfalls in inorder-Reihenfolge. Wir
können annehmen, dass balance(u) = +2, weil die Fälle für balance(u) =
−2 symmetrisch sind und daher analog behandelt werden können. Der erste
Knoten v auf dem Weg von u zum eingefügten Blatt ist dann auf jeden Fall
das rechte Kind von u, und wir unterscheiden danach, ob w rechtes oder
linkes Kind von v ist (d.h., ob b = v, c = w oder b = w, c = v gilt).
Algorithmen und Datenstrukturen (WS 2010/2011)
49
Fall 1: (b = v, c = w)
u
2
a
v
einfache
Rotation
w
-1
c
1
b
T0
T1
v
0
b
u
0
a
w
-1
c
T3
T3
T0
c T3
T0
T1
T2
T1 b
T2
T2
T0
a
T1 b
T2
a
c T3
Dabei spielt es keine Rolle, ob T2 oder T3 der zu hohe Teilbaum mit
dem eingefügten Blatt ist.
Fall 2: (b = w, c = v)
2.
u
2
a
T0
1.
-1 v
c
w -1
b
doppelte
Rotation
w
0
b
u
0
a
v
1
c
T2
T2
T3
T0
T1
T3
T0 a
T1
T3
T1
T0 a
T1
b T2
c
b T2
c
Auch hier spielt es keine Rolle, ob in T1 oder T2 eingefügt wurde.
In allen acht Fällen ist die Höhe des Teilbaums von b anschließend gleich der
Höhe des Teilbaums von u vor der Einfügeoperation, sodass alle Vorfahren
auch weiterhin balanciert sein müssen.
T3
Algorithmen und Datenstrukturen (WS 2010/2011)
50
Das Entfernen eines Elements beginnt ebenfalls wie beim gewöhnlichen binären Suchbaum. Dabei wird ein Knoten gelöscht, und auch hier kann dadurch die Balanciertheitseigenschaft an einem Knoten auf dem Weg vom
gelöschten Knoten zur Wurzel verletzt sein. Beachte, dass dies auf maximal
einen Knoten u zutrifft, weil die Verletzung durch einseitig verringerte Teilbaumhöhe sich nicht nach oben fortsetzt.

 v Kind von u mit größerer Höhe,
w Kind von v mit größerer Höhe
Wir bezeichnen mit

(bei Gleichheit beliebig)
u
v
x
Situation wie vorher
=⇒ einfache oder doppelte Rotation
w
Achtung: Der Unterbaum mit neuer Wurzel b = v bzw. b = w ist um 1
weniger hoch als vorher mit Wurzel u. Ein neue Verletzung der Balanciertheitsbedingung kann daher an einem Knoten u0 weiter oben auf dem Weg
zur Wurzel auftreten und wir müssen weitere Rotationen vornehmen, bis die
Wurzel erreicht ist.
3.2.3
Rot-Schwarz-Bäume
Durch die Balanciertheitsbedingung wird geährleistet, dass AVL-Bäume nicht
allzu weit vom Ideal eines vollständigen Binärbaums abweichen und zumindest asymptotisch gleiche Maximalhöhe haben.
Eine anderer Ansatz, Abweichungen in Grenzen zuzulassen, besteht in der
Markierung einiger Knoten als Ausnahmen, die im Test von Eigenschaften
nicht berücksichtigt werden. Speziell werden die Knoten eines binären Suchbaums in diesem Kapitel so gefärbt, dass der Teilbaum der normalen Knoten
wie ein perfekter Binärbaum nur Blätter gleicher Tiefe hat.
Statt der Balanciertheit speichern wir an jedem Baumknoten eine Farbmarkierung mit folgender Bedeutung:
schwarz:
rot:
normaler Knoten
Ausgleichsknoten (für Höhenunterschiede)
Algorithmen und Datenstrukturen (WS 2010/2011)
51
Um nun dem Idealfall von Blätter gleicher Tiefe nahe zu kommen, wird für
die Färbung folgende schwächere Invariante gefordert:
Wurzelbedingung: Die Wurzel ist schwarz.
Farbbedingung:
Die Kinder von roten Knoten sind schwarz
(oder nil).
Tiefenbedingung: Alle Blätter haben die gleiche schwarze Tiefe
(definiert als die Anzahl schwarzer Vorfahren −1).
3.13 Beispiel
12
5
15
3
10
4
7
6
schwarz
13
11
17
14
8
schwarze Tiefe: 2
rot
3.14 Satz
Die Höhe eines rot-schwarzen Baumes mit n Knoten ist Θ(log n).
Beweis.
handelt.
Höhe Ω(log n) ist wieder klar, weil es sich um einen Binärbaum
Da ein roter Knoten keinen roten Elternknoten haben kann, ist die Höhe
nicht größer als zweimal die maximale schwarze Tiefe eines Blattes. Da alle
Algorithmen und Datenstrukturen (WS 2010/2011)
52
Blätter die gleiche schwarze Tiefe haben, kann diese aber nicht größer als
blog nc sein.
insert Beginnen wie bei binärem Suchbaum. y Element wird in ein neues
Blatt w eingefügt.
schwarz, falls Wurzel
Färbe w
rot
sonst
Wurzel- und Tiefenbedingung bleben so erfüllt, aber die Farbbedingung ist
verletzt, falls der Elternknoten v von w auch rot ist. Wegen der Wurzelbedingung ist v dann nicht die Wurzel und hat einen schwarzen (sonst schon
vorher Konflikt) Elternknoten u. Diese Situation heißt doppelrot bei w.
Fall 1: (der Geschwisterknoten z von v ist schwarz)
u
u
a
a
oder
z
2
z
v
v
c
b w
1
w
c
b
wie bei
AVL-Bäumen
+ Umfärben
Problem beseitigt
a
b
c
z
Die beiden symmetrischen Fälle (wie bei AVL) werden analog behandelt.
Fall 2: (der Geschwisterknoten z von v ist rot)
Algorithmen und Datenstrukturen (WS 2010/2011)
u
53
u
oder
z
v
z
v
w
w
Umfärben
u
z
u
v
z
v
w
w
Die beiden symmetrischen Fälle (wie bei AVL) werden analog behandelt.
Falls u die Wurzel ist, dann wird auch u schwarz gefärbt. Falls doppelrot
jetzt bei u auftritt, werden Fall 1 und 2 iteriert, bis die Wurzel erreicht oder
das Problem wegrotiert ist.
remove Beginnen wie bei binärem Suchbaum. y Es wird ein Knoten v mit
höchstens einem Kind gelöscht.
u
u
u
[fertig]
oder
a)
v
v
w
w
w
Algorithmen und Datenstrukturen (WS 2010/2011)
u
54
u
doppelschwarz
b)
bei w
v
w
w
(zeigt an, dass Tiefenbedingung verletzt) Falls w vorhanden, betrachte
Geschwisterknoten y von w.
Fall 1: (y schwarz mit mindestens einem roten Kind z)
Farbe von u
c u
a
c u
oder
b y
a y
w
z
Rotation
b
+Umfärben
w
b
a
c
[fertig]
w
z
Fall 2: (y schwarz ohne rotes Kind, oder y gibt es nicht)
2.1:
u
y
2.2:
u
w
y
u
y
Fall 3: (y rot)
w
[fertig]
w
Fortsetzen für
doppelschwarz
bei u
u
w
y
Algorithmen und Datenstrukturen (WS 2010/2011)
y
u
y
w
einfache Rotation
u
+ Umfärben
55
y Fall 1 oder 2.1,
d.h. nach einem weiteren Schritt fertig.
w
3.15 Satz
find, insert und remove sind in rot-schwarzen Bäumen in Zeit O(log n)
realisierbar. insert und remove benötigen maximal eine bzw. zwei (einfache
oder doppelte) Rotationen.
[Erinnerung: remove aus AVL-Baum benötigt evtl. O(log n) Rotationen]
3.2.4
B-Bäume
Suchbäume können als Indexstruktur für extern gespeicherte Daten verwendet werden. Ist der Index allerdings selbst zu groß für den Hauptspeicher,
dann werden viele Zugriffe auf möglicherweise weit verstreut liegende Werte nötig (abhängig davon, wie der Baumdurchlauf und die Speicherung der
Knoteninformationen zusammenpassen).
Idee Teile Index in Seiten auf (etwa in Größe von Externspeicherblöcken)
und sorge dafür, dass Suche nur wenige Seiten benötigt.
Ein Vielwegbaum speichert pro Knoten mehrere Schlüssel in aufsteigender
Reihenfolge und zwischen je zwei Schlüsseln (und vor dem Ersten und nach
dem Letzten) einen Zeiger auf ein Kind. Die Zahl der Kinder ist damit immer
1 größer als die der Schlüssel.
a1 < a2 < · · · < ak
w0
w1
w2
wk−1
wk
Algorithmen und Datenstrukturen (WS 2010/2011)
56
Suchbaum, falls für alle Knoten v mit Schlüssel a1 , . . . , ak und Kindern w0 , . . . , wk
gilt:
Alle Schlüssel in T (wi−1 ) < ai < alle Schlüssel in T (wi ) i = 1, . . . , k
B-Baum der Ordnung d, d ≥ 2: Vielwegsuchbaum mit
Wurzelbedingung:
Knotenbedingung:
Schlüsselbedingung:
Tiefenbedingung:
Die Wurzel hat mindestens 2 und höchstens d Kinder.
Jeder andere Knoten hat mindestens dd/2e und
höchstens d Kinder.
Jeder Knoten mit i Kindern hat i − 1 Schlüssel.
Alle Blätter haben dieselbe Tiefe.
3.16 Beispiel
(B-Baum der Ordnung 6)
22
11 12
37
38 40 41
24 29
42
65
46
58
43 45
59 63
48 50 51 53 56
72
80
66 70
93
83 85 86
74 75
find Suche in Knoten; falls Schlüssel nicht vorhanden, liegt er zwischen
zwei enthaltenen Schlüsseln (bzw. vor dem Ersten oder hinter dem Letzten)
y Suche an entsprechendem Kind fortsetzen.
3.17 Satz
Ein B-Baum der Ordnung d mit n Knoten hat Höhe Θ(logd n).
Beweis. Zeige zunächst: B-Baum mit n Schlüsseln hat n+1 externe Knoten
(„nil-Zeiger“).
Induktion über Höhe h:
95 98
Algorithmen und Datenstrukturen (WS 2010/2011)
57
h = 0 Wurzel hat 1 ≤ i ≤ d − 1 Schlüssel und i + 1 Kinder, die alle externe
Knoten sind.
h → h + 1 Wurzel hat 1 ≤ i ≤ d − 1 Schlüssel und i + 1 Kinder, die alle Wurzeln
von Teilbäumen der Höhe h sind. Diese haben n0 , . . . , ni viele Schlüssel
und (n0 + 1) + · · · + (ni + 1) externe Knoten. Es gibt also insgesamt
n0 + · · · + ni + i
viele Schlüssel und
(n0 + 1) + · · · + (ni + 1) = n0 + · · · + ni + i + 1 viele externe Knoten.
Wegen der Tiefen- und Knotenbedingung für die Höhe h(n) eines B-Baumes
mit n + 1 externen Knoten gilt dann aber
h
d
≤ n ≤ dh+1
2·
2
insert Suche analog zu vorher das Blatt, in das eingefügt werden kann.
Wieviele Schlüssel hat dieses Blatt?
< d − 1 einfügen, fertig.
= d − 1 Überlauf → teile die d Schlüssel auf in die kleinsten bd/2c und die
größten dd/2e − 1 viele, für die zwei neue Knoten erzeugt werden, und
das mittlere, (bd/2c + 1)-te, das im Elternknoten eingefügt wird. →
evtl. Überlauf im Elternknoten, dann iterieren [evtl. neue Wurzel].
...
k
k1
k2
...
kd−1
k10
...
0
kbd/2c+1
0
kbd/2c
...
0
kbd/2c+2
...
kd0
Algorithmen und Datenstrukturen (WS 2010/2011)
58
remove Suche den Knoten, aus dem ein Schlüssel gelöscht werden soll. Wieviele Schlüssel enthält er?
>
d
=
d
2
2
− 1 löschen, fertig.
− 1 Unterlauf
a) Knoten ist innerer Knoten (d.h. kein Blatt)
→ ersetze Schlüssel durch inorder-Vorgänger (oder Nachfolger),
d.h. letzten Schlüssel im rechtesten Blatt des vorangehenden Teilbaums.
···
k
···
···
···
···
k0
···
··
··
·
·
···
···
k0
k
→ fahre fort mit b).
b) Knoten ist Blatt
→ falls ein direkter linker oder rechter Geschwisterknoten mehr als
d d2 e − 1 Schlüssel enthält, verschiebe den Letzten bzw. Ersten von
dort in den gemeinsamen Vorgänger und den trennenden Schlüssel
von vorher in den Knoten mit Unterlauf.
···
···
k
···
k1
···
k2
···
···
k
k1
···
k2
···
Unterlauf
→ sonst verschmilz den Knoten mit einem seiner direkter Geschwister und dem trennenden Schlüssel aus dem Vorgänger.
Algorithmen und Datenstrukturen (WS 2010/2011)
···
···
k
···
59
···
···
···
···
k
···
Falls Unterlauf in Vorgänger: iteriere b) für innere Knoten.
[Bemerkung: anders als in a) gibt es jetzt kein überzähliges Kind.]
Kapitel 4
Streuen
Wir behandeln nun Implementationen ungeordneter Wörterbücher, in denen
die Schlüssel ohne Beachtung ihrer Sortierreihenfolge gespeichert werden dürfen, verlangen aber, dass es sich bei den Schlüsseln um Zahlen handelt. Dies
ist keine starke Voraussetzung, weil sich die Elemente anderer Schlüsseluniversen als Zahlen codieren lassen.
typisch
Bezeichnungen:
(Schlüssel-) Universum U ⊆ N0
Schlüsselmenge
K ⊆ U, |K| = n
Hashtabelle
H[0, . . . , m − 1]
(Array von Zeigern auf Datenelemente)
in JAVA:
Speicheradresse
Object.hashCode
Durch eine Hashfunktion h : U → {0, . . . , m − 1} wird jedem zulässigen hash (engl.):
Schlüssel k ∈ U seine Speicherstelle H[h(k)] in der Hashtabelle H zugewiesen. streuen
Im Idealfall gilt für die Menge K ⊆ U der vorkommenden Schlüssel
k1 6= k2 ∈ K =⇒ h(k1 ) 6= h(k2 )
(h|K injektiv)
weil Einfügen, Suchen und Löschen dann jeweils in Θ(1) realisiert werden
können.
4.1 Beispiel (Hashfunktionen)
1. h(k) = k mod m für Primzahl m
(Divisions- oder Kongruenzmethode)
60
Algorithmen und Datenstrukturen (WS 2010/2011)
2. h(k) = bm · (kα − bkαc)c z.B. für α = φ−1 =
(Multiplikationsmethode)
√
61
5−1
2
≈ 0, 61803
Die n+1 Intervalle nach Einfügen von 1, . . . , n haben nur 3 verschiedene
Größen; bei α = φ−1 am ähnlichsten.
4.1
Kollisionen
Im Allgemeinen ist allerdings h(k1 ) = h(k2 ) für einige h1 , h2 ∈ K. Dies
bezeichnen wir mit Kollisionen, k1 , k2 heißen Synonyme.
pm,n := P (keine Kollisionen) = 1· m−1
· m−2
· . . . · m−n+1
, falls Schlüssel durch
m
m
m
h gleichmäßig gestreut werden, d.h. alle Positionen gleichwahrscheinlich
sind.
4.2 Beispiel (Geburtstagsparadoxon)
p365,22 > 0.5 > p365,23
Anschauliche Deutung: bei 23 oder mehr Personen ist die Wahrscheinlichkeit,
dass zwei am gleichen Tag Geburtstag haben, größer als die Wahrscheinlichkeit, dass alle an verschiedenen Tagen Geburtstag haben.
Herleitung:
pm,n =
Qn−1
i=0
m−i
m
Qn−1
(1 − mi )
=
Qi=1
n
1 Pn−1
n−1 −i/m
= e− m i=1 i = e−( 2 )/m
≈
i=1 e
Für welches n ist pm,n ≈ 1/2?
n
e−( 2)/m = 12
⇔ − n2 /m = ln 12
n
⇔
= m ln 2
2
q
n(n−1)
2
⇒
n
≈
p
2(ln 2) · m (≈ 22.49 für m = 365)
goldener
Schnitt
Algorithmen und Datenstrukturen (WS 2010/2011)
62
Erwartete Anzahl Kollisionen Sei
1 falls h(ki ) = h(kj ) (Kollision von ki , kj )
Xij =
0 sonst
Bei gleichmäßiger Streuung ist E(Xij ) = 1/m für i 6= j.
!
X
X
X
n
E(X) = E
Xij =
E(Xij ) =
1/m =
· 1/m
2
i<j
i<j
i<j
Im Beispiel war m = 365, somit gilt hier
< 1 für n < 27
E(X) =
≥ 1 für n ≥ 27
Im Allgemeinen gilt
E(X) ≥ 1 ⇔ n(n − 1) ≥ 2m,
also etwa bei n ≥
√
2m.
4.3 Def inition (Belegungsfaktor)
Wir definieren den Belegungsfaktor (load factor) als β =
n
.
m
→ entscheidend für Nutzen
Erwartete Anzahl leerer Felder? Element k ∈ K steht nicht an Position
= 1 − m1 .
i ∈ {0, . . . , m − 1} mit Wahrscheinlichkeit m−1
m
Die Wahrscheinlichkeit, dass Position i nicht belegt ist, ist damit
"
#β
m
1
P (i ∈
/ {h(k1 ), . . . , h(kn )}) = (1 − m1 )n = (1 − )
≈ e−β
m
| {z }
≈e−1/m
= E(Xi )
für Xi =
0 H[i] belegt
1 H[i] frei
P
P
P
E(X) = E( Xi ) = E(Xi ) = e−β = m · e−β .
y Die erwartete Anzahl belegter Einträge ist m − m · e−β = m(1 − e−β ).
β = 1/2 :
β=1 :
1 − e−1/2 ≈ 0.39
1 − e−1 ≈ 0.63
Belegung 39%
Belegung 63%
Algorithmen und Datenstrukturen (WS 2010/2011)
4.2
4.2.1
63
Kollisionsbehandlung
Verkettung
Idee Die Hashtabelle wird als Array von Zeigern auf Listen implementiert,
wobei die Listen alle Schlüssel mit gleichem Hashfunktionswert enthalten.
4.4 Beispiel (h(k) = k mod p)
0
p−1
1
···
H:
4.5 Def inition (Sondieren)
Wenn die Hashtabelle über Verkettung realisiert wird, finden bei find eventuell mehrere Zugriffe auf Zeiger statt (Sondieren). Um die Anzahl der Sondierschritte zu analysieren, definieren wir
A(m, n)
A0 (m, n)
:= mittlere Anzahl bei erfolgreicher Suche.
:= mittlere Anzahl, falls Schlüssel nicht in der
Hashtabelle ist.
Wir gehen wieder davon aus, dass für das verwendete Modell
P (h(k) = i) =
gilt.
1
m
∀k ∈ U, i ∈ {0, . . . , m − 1}
Algorithmen und Datenstrukturen (WS 2010/2011)
64
Die mittlere Länge der Listen ist gerade der Belegungsfaktor β =
n
.
Also ist A0 (m, n) = 1 + β = 1 + m
n
m
∈ [0, ∞).
Zu A(m, n): Die Suche nach ki ergibt genau die Sondierungen wie beim Einfügen von ki , wenn vorher k1 , . . . , ki−1 eingefügt wurden. Im Mittel ist damit
P
Pn−1
Pn−1
1
i
1
0
A(m, n) = n1 n−1
i=0 A (m, i) = n
i=0 (1 + m ) = 1 + nm ·
i=0 i
=
1+
(n−1)·n
2nm
=1+
β
2
−
1
2m
≤1+
β
2
Statistische Eigenschaften Sei β = 1 (n = m). Dann sind 63% der
Hashtabelle T besetzt, 37% sind frei (s.o.). Wie groß muss n im Mittel sein,
Coupon
damit alle Plätze in T belegt sind?
Collector
Sei (ki )i=1,2,3,... , ki ∈ U die Folge in T einzufügender Schlüssel. Sei m(i) die An- Problem
zahl belegter Plätze nach Einfügen von k1 , . . . , ki und sei ik := min{i|m(i) =
k}, d.h. Einfügen von kik belegt erstmals den k-ten Platz in T .
Wir betrachten eine Zufallsvariable X mit
Xk = P
ik − ik−1
m
X=
k=1 Xk
Dann ist die gesuchte Zahl für n gerade E[X].
i:
m(i)
1
2
3
4
5
k1
k2
k3
k4
k5 · · ·
1
2
i1
i2
3
4
i3
X1 X2 X 3
···
i4
X4
i5
X5
···
k0
k 00
m−1
m
im−1
Xm−1
Sei ik−1 ≤ i < ik . Dann ist
pk := P (i + 1 = ik ) = m−k+1
m
E[Xk ] = p1k
P
P
P
m
E[X] = E[ m
Xk ] = m
E[Xk ] = m
1
1
1 m−k+1
Pm 1
= m · k=1 k = m · Hm ≈ m · ln m
im
Xm
Algorithmen und Datenstrukturen (WS 2010/2011)
65
Im Mittel sind also n = m ln m Einfügeoperationen notwendig, um alle m
ln m
= ln m > 1.
Positionen in der Hashtabelle zu füllen. Dann ist β = m m
4.2.2
Open Hashing
Idee In jedem Feld der Hashtabelle H wird nur ein Element gespeichert
(daher β ≤ 1).
Füge Schlüssel k1 ein:
Füge Schlüssel k2 ein (Kollision):
h(k1 ) = i, H[i] = k1 .
h(k2 ) = i, H[i] besetzt. y Finde
freien Platz in H.
Wähle eine Folge (di )i=1,2,... , di ∈ Z und teste
H[(h(k) + di )
mod m],
i = 1, 2, 3, . . .
Für die Folge (di )i gibt es verschiedene Wahlen:
lineares Sondieren: di = i
quadratisches Sondieren: di = i2
double Hashing: di = i · h0 (k), wobei h0 : U → {1, . . . , m − 1} eine
zweite Hashfunktion ist mit h0 (k) 6= 0 ∀k ∈ U.
Hier sollte m prim sein! Warum?
4.6 Problem
Einfügen und Suchen ist damit klar, Löschen ist aber ein Problem.
4.7 Beispiel
Sei h(k) = h(k 0 ) = i und füge k, k 0 ein:
H:
k
k0
i i+1
Lösche dann k. y Anschließend versagt die Suche nach k 0 !
Lösung:
• markiere gelöschte Schlüssel als “gelöscht”
Algorithmen und Datenstrukturen (WS 2010/2011)
66
• bei Suche werden sie behandelt wie belegte Felder
• beim Einfügen wie freie Felder
• ungünstig, wenn oft Elemente gelöscht werden, da die Suche verteuert.
4.8 Problem
Clusterbildung
4.9 Beispiel (lineares Sondieren)
H:
···
···
i i+1
Nächster einzufügender Schlüssel: k ∈ U
P (h(k) = i + 1) =
P (h(k) = i) =
1
m
5
m
⇒ Cluster wachsen tendenziell
Analyse
lineares Sondieren
double Hashing
1
A ≈ 12 (1 + 1−β
)
1
1
0
A ≈ 2 (1 + (1−β)2 )
1
A ≈ β1 ln( 1−β
)
1
0
A ≈ 1−β
Beweis.
lineares Sondieren: schwer [4, S. 139–141]
double Hashing: (idealisierende) Annahme: Sondierungen h1 (k), h2 (k), . . . zur
erfolglosen Suche von k mit
n
=1−β
P (H[hi (k)] ist frei) = 1 − m
E[Anzahl Sondierungen bis freies Feld gefunden] =
1
1−β
Algorithmen und Datenstrukturen (WS 2010/2011)
Also A0 (n, m) =
67
1
.
1−β
A(n, m) =
=
≈
=
Pn−1 0
Pn−1 1
Pn−1 1
1
1
m
i=0 A (m, i) = n
i=0 1− i = n
i=0 m−i
n
m
1
1
1
1
(
+ · · · + m ) = β (Hm − Hm−n )
β m−n+1
m
1
(ln m − ln(m − n)) = β1 ln m−n
β
1
1
ln 1−β
β
4.3
Kollisionsvermeidung
4.3.1
Streufunktionen
Ziel gleichmäßige Streuung durch Hashfunktion, wie in der Berechnung der
Kollisionswahrscheinlichkeiten unterstellt.
Problem Wir wissen nicht, welche Schlüssel K ⊆ U gespeichert werden.
Schlimmstenfalls ist h(k1 ) = H(k2 ) ∀k1 , k2 ∈ K
→ Sondieren bei Suche entspricht linearer Suche O(n).
Schlüsselmengen K ⊆ U führen zu sehr verschiedenen Laufzeiten. Versuche
daher, die Wahrscheinlichkeit für schlechtes Laufzeitverhalten besser auf diese
zu verteilen.
Idee Wähle aus einer Menge von Hashfunktionen
H ⊆ {h : U → {0, . . . , m − 1}}
zufällig eine aus.
4.3.2
Universelles Streuen
4.10 Def inition (universelles Streuen)
Eine Familie von Streufunktionen H ⊆ {h : U → {0, . . . , m − 1}} heißt universell, falls
|{h ∈ H : h(k1 ) = h(k2 )}| ≤
|H|
m
∀k1 , k2 ∈ U.
Algorithmen und Datenstrukturen (WS 2010/2011)
Bei zufälliger Wahl h ∈ H sind wir dann berechtigt, P (h(k1 ) = h(k2 )) =
anzunehmen.
68
1
m
4.11 Satz
Ist H universell, dann ist für k ∈ {k1 , . . . , kn } = K ⊆ U bei zufällig gewähln
tem h ∈ H die erwartete Anzahl Kollisionen gerade m
= β.
Beweis.
Sei Cij eine Zufallsvariable mit
1
h(ki ) = h(kj )
Cij =
0
sonst
Da H universell ist, ist P (Cij = 1) = m1 , und es folgt für k = ki


X
X
|K|
E
= β.
Cij  =
E(Cij ) ≤
m
kj ∈K\{ki }
kj ∈K\{ki }
Die erwartete Anzahl Kollisionen entspricht dann also wie erhofft dem Belegungsfaktor β.
4.12 Satz
Die Familie von Streufunktionen
Hp = {(ak + b mod p)
mod m :
0 < a < p, 0 ≤ b < p} , p prim, p > k
∀k ∈ U
ist universell.
Beweis.
Sei ha,b ∈ Hp mit ha,b (k) = (ak + b mod p) mod m.
Betrachte zunächst für k, k 0 ∈ {0, . . . , p − 1}
r = (ak + b) mod p
s = (ak 0 + b) mod p
Dann ist r − s ≡ |{z}
a (k − k 0 ) mod p, also r 6= s falls k 6= k 0 , weil p prim ist.
6=0
Es gibt also zunächst keine Kollisionen der Zahlen {0, . . . , p − 1}, sondern
diese werden nur permutiert.
Algorithmen und Datenstrukturen (WS 2010/2011)
69
Außerdem liefert jede der (p − 1) · p Wahlen für (a, b) ein anderes Paar r 6= s,
denn gegeben r 6= s können wir nach
a = (r − s) · (k − k 0 )−1
| {z }
mod p
Inverses bzgl. Zp
b = (r − ak)
mod p
auflösen. Weil es aber auch nur p(p − 1) Paare (r, s) mit r 6= s gibt, liegt
eine Bijektion vor. Werden a, b zufällig gewählt, dann erhalten wir also auch
jedes Paar r 6= s ∈ {0, . . . , p − 1} mit gleicher Wahrscheinlichkeit.
Die Kollisionswahrscheinlichkeit von k 6= k 0 ∈ U entspricht damit der Wahrscheinlichkeit, dass r ≡ s mod m für zufällig gewählte r 6= s ∈ {0, . . . , p − 1}.
Für festes r ist die Anzahl der s mit r 6= s und r ≡ s mod m höchstens
lpm
m
−1 ≤
p+m−1
p−1
−1 =
m
m
und wegen der zufälligen Wahl aus den p − 1 möglichen s 6= r ist die Kollisionswahrscheinlichkeit für k 6= k 0 ∈ U höchstens m1 . Also ist Hp universell. Wir wollen nun noch eine andere universelle Familie von Streufunktionen
betrachten.
Annahme U besteht aus Bitstrings fester Länge m, m ist prim.
Zerlege K ⊆ U in Blöcke der Länge blog mc
···
k:
kr
k1
k0
y 0 ≤ ki < m,
i = 0, . . . , r
Wir definieren die Menge von Hashfunktionen HB = {ha : U → {0, . . . , m − 1}}
durch
!
r
X
ai ki
mod m
ha (k) =
i=0
wobei a = (ar , . . . , a0 ) ∈ {0, . . . , m − 1}r+1 . Damit gilt |HB | = mr+1 .
Algorithmen und Datenstrukturen (WS 2010/2011)
70
4.13 Satz
HB ist universell.
Beweis. Seien k 6= k 0 ∈ U, dann ki 6= ki0 für ein i ∈ {0, . . . , r}. OBdA. sei
i = 0. Für ein ha ∈ HB gilt dann
0
ha (k) = ha (k ) ⇔
r
X
ai ki ≡
i=0
r
X
ai ki0
mod m
i=0
⇔ a0 (k0 − k00 ) ≡
r
X
ai (ki0 − ki )
mod m
i=1
⇔ a0 ≡
k00 )−1
(k0 −
| {z
}
mult. Inv. in (Zm ,+,·)
r
X
ai (ki0 − ki )
mod m
i=1
y Für alle Wahlen von (ar , . . . , a1 ) ∈ {0, . . . , m − 1}r existiert genau ein
a0 ∈ {0, . . . , m − 1} mit ha (k) = ha (k 0 ). Also gilt
P (Kollision k 6= k 0 ) =
mr
1
| {a : ha (k) = ha (k 0 )} |
= r+1 = .
|HB |
m
m
4.4
Ausblick
• viele andere Techniken zur Kollisionsbehandlung, z.B. Cuckoo-Hashing.
• viele andere Techniken zur Konstruktion von Hashfunktionen, z.B. perfektes Hashing (für statische Schlüsselmengen).
• viele andere Anwendungen, z.B. Bloom-Filter.
s. Übung
Kapitel 5
Ausrichten
Sucht man in einer Datenbank nach Zeichenketten (Wörtern, Namen, Gensequenzen), möchte man oft anders als in den vorangegangenen Kapiteln unterstellt nicht nur exakte Treffer, sondern auch ähnliche Vorkommen finden.
Wir behandeln hier das Basisproblem, die Ähnlichkeit zweier Zeichenketten
zu bestimmen, und nehmen dazu Bewertungen ganz bestimmter Formen der
Unähnlichkeit vor.
Um Problemstellung und Bewertungen eindeutig zu beschreiben, werden folgende Bezeichnungen verwendet, die in der Vorlesung „Theoretische Grundlagen der Informatik“ (4. Semester) vertieft werden.
• Σ ist ein endliches Alphabet, d. h. eine Menge von Symbolen
(auch: Zeichen),
S k
• s = a1 a2 · · · an ∈ Σ∗ =
Σ heißt Wort (auch: Sequenz )
k∈N0
der Länge |s| = n,
• das leere Wort ε ist das Wort der Länge Null,
• eine Teilsequenz s0 von s = a1 · · · an erfüllt s0 = ai · · · aj für 1 ≤ i, j ≤ n
(im Unterschied zu einem Teilwort, bei dem auch zwischendrin noch
Zeichen ausgelassen werden können)
71
Algorithmen und Datenstrukturen (WS 2010/2011)
72
5.1 Beispiel (Alphabete)
Wichtige Alphabete für die symbolische Darstellung von Information im
Rechner sind
Σbinär = {0, 1}
ΣASCII
enthält 128 Steuerzeichen, Ziffern und Buchstaben
American Standard Code for Information Interchange
ΣUnicode
enthält mehr als 100 000 Zeichen (vgl. www.unicode.org)
In Molekularbiologie und Bioinformatik werden z.B. Molekülketten als Folge
ihrer wesentlichen Elemente (Basenpaare, Aminosäuren) repräsentiert. Von
besonderer Bedeutung sind
ΣDNS = {A, C, G, T}
Desoxyribonukleinsäuren
ΣRNS = {A, C, G, U}
Ribonukleinsäuren
ΣP = {A, C, D, E, F, G, H, I, K, L, M, N, P, Q, R, S, T, V, W, Y}
Proteine (Eiweiße)
Um zwei Wörter über einem gegebenen Alphabet zu vergleichen, muß festgelegt werden, wie Unterschiede zu behandeln sind. Wir gehen hier davon aus,
dass Wörter aus anderen Wörtern durch drei Operationen entstehen können:
Substitution: ein Zeichen wird gegen ein anderes ausgestauscht
Einfügung: ein Zeichen wird hinzugefügt
Löschung: ein Zeichen wird weggelassen
Die Unähnlichkeit zweier Wörter kann dann z.B. als die minimale Anzahl
von Operationen dieses Typs, die nötig sind, um ein Wort in das andere zu
überführen, definiert werden. Da offensichtlich sinnlose Mehrfachersetzungen
ausgeschlossen werden können, ermöglicht die Verwendung eines Platzhalterzeichens _, das nicht aus dem verwendeten Alphabet stammt, die Darstellung Σ := Σ ] {_}
einer Folge von Operationen, die aus dem einen Wort das andere macht, durch
Untereinanderschreiben. Die beiden Beispiele
ACCTG
AAC_G
_ACCTG
AACG__
Algorithmen und Datenstrukturen (WS 2010/2011)
73
illustrieren, dass man die Sequenz AACG aus der Sequenz ACCTG zum Beispiel
durch eine Substitution und eine Löschung, oder durch eine Einfügung, eine
Substitution und zwei Löschungen erhält.
5.2 Def inition (Ausrichtung)
∗
∗
Ein Paar s̄, t̄ ∈ Σ × Σ heißt Ausrichtung von s, t ∈ Σ∗ , falls
engl.
alignment
• s̄|Σ = s, t̄|Σ = t (Weglassen aller Leerzeichen ergibt die Ausgangssequenzen)
• |s̄| = |t̄| (beide Sperrung gleich lang)
Da sie per Definition gleiche Länge haben, können Ausrichtungen stellenweise
bewertet werden.
5.3 Def inition (Bewertung)
Eine Funktion σ : Σ × Σ → R heißt Bewertung. Sie wird auf Ausrichtungen score
(s̄ = ā1 · · · ā` , t̄ = b̄1 · · · b̄` ) erweitert durch
σ(ā1 · · · ā` , b̄1 · · · b̄` ) =
`
X
σ(āi , b¯i )
i=1
. Bewertungen σ mit

0




 1
1
σ(a, b) =


1



∞
falls
falls
falls
falls
falls
a=b∈Σ
a 6= b ∈ Σ
a = _, b ∈ Σ
a ∈ Σ, b = _
a=_=b
(Abgleich, match)
(Substitution, mismatch)
(Einfügung)
(Löschung)
(unzulässig)
für alle a, b ∈ Σ heißen Editierdistanz.
edit distance
Abhängig vom Kontext kommen verschiedene Bewertungsfunktionen zum
Einsatz, in denen entweder eine Ähnlichkeit oder eine Unähnlichkeit, aber
immer stellenweise bewertet wird. Für unsere Algorithmen wird es egal sein,
wie die konkreten Bewertungen aussehen, sodass wir in diesem Abschnitt der
Einfacheit halber immer von der Editierdistanz ausgehen. Das Finden einer
Ausrichtung mit optimaler Bewertung σ ist daher ein Minimierungsproblem.
Die Zahl der möglichen Ausrichtungen ist beim weitem zu groß, um ein Mi- ÜBUNG
nimum durch Aufzählen zu suchen. Das folgende Lemma erlaubt es, sich auf
deutlich weniger zu beschränken.
Algorithmen und Datenstrukturen (WS 2010/2011)
74
5.4 Lemma
Zu s, t ∈ Σ∗ definiere opt(s, t) = min{σ(s̄, t̄) : (s̄, t̄) ist Ausrichtung von s, t}.
Dann gilt


 opt(a1 · · · an−1 , b1 · · · bm−1 ) + σ(an , bm ), 
opt(a1 · · · an , b1 · · · bm ) = min opt(a1 · · · an , b1 · · · bm−1 ) + σ(_, bm ),
| {z } | {z }


opt(a1 · · · an−1 , b1 · · · bm ) + σ(an , _)
=s
=t
Beweis. In einer Ausrichtung gibt es nur drei Kombinationsmöglichkeiten
für die beiden Zeichen an der jeweils letzten Stelle, da es sich entweder um
Abgleich bzw. Substitution, um eine Einfügung oder um eine Löschung handelt.
s̄ :
ā1 · · · · · · · · · ā`−1
ā`
b̄1 · · · · · · · · · b̄`−1
b̄`
t̄ :
|
{z
muss optimal sein
|
}
{z
damit optimal sein kann
}
Wäre in einer optimalen Ausrichtung das Anfangsstück ohne die Zeichen der
letzten Stelle nicht optimal, so könnte sie gegen eine andere ausgetauscht und
so die Bewertung zusammen mit derjenigen an der letzten Stelle verringert
werden.
Die unmittelbare Umsetzung dieser Beobachtung in einem rekursiven Algorithmus ist in Algorithmus 24 angegeben, führt aber zu inakzeptabler Laufzeit.
5.5 Satz
Die Laufzeit von Algorithmus 24 ist in Ω(3min{n,m} ).
Beweis.
...
Dass der naïve Ansatz zu exponentieller Laufzeit führt, liegt an der wiederholten Auswertung für die gleichen Argumente und lässt sich daher leicht
vermeiden.
5.6 Beispiel
Algorithmen und Datenstrukturen (WS 2010/2011)
75
Algorithmus 24: Optimale Ausrichtung (naïver Ansatz)
Eingabe
: Sequenzen s = a1 · · · an , t = b1 · · · bm ∈ Σ∗
Bewertung σ : Σ × Σ → R
Ausgabe
: Minimale Bewertung opt(s, t) einer Ausrichtung von
s, t
proc D(i, j) begin
P
if i = 0 then return jk=1 σ(_, bk )
Pi
_
if j = 0 then
 return k=1 σ(ak , )

 D(i − 1, j − 1) + σ(ai , bj ), 
return min D(i, j − 1) + σ(_, bj ),


D(i − 1, j) + σ(ai , _)
end
Aufruf: D(n, m)
s
A
C
C
T
G
t
0 ←
↑ 1
↑
2
↑
3
↑
4
↑
5
A
1
←
0 ←
↑ 1
↑ 2
↑ 3
↑ 4
A
2
1
1
↑
2
↑
3
↑
4
←
←
-
C
3
←
G
4
2
←
3
←
1 ←
↑ 2
↑ 3
2
1
-
2
2
2
s: ACCTG
t: AAC-G
5.7 Satz
Algorithmus 25 bestimmt eine optimale Ausrichtung in Θ(nm) Zeit mit
Θ(nm) Platz. Soll nur deren Bewertung bestimmt werden, kann der Platzbedarf auf Θ(min{n, m}) reduziert werden.
Beweis.
...
Zwei Beobachtungen: Aufteilung, Spiegelung
5.8 Satz
Eine optimale Ausrichtung kann in O(nm) Zeit mit O(min{n, m}) Platz
Algorithmen und Datenstrukturen (WS 2010/2011)
Algorithmus 25: Optimale Ausrichtung
Eingabe
: Sequenzen s = a1 · · · an , t = b1 · · · bm ∈ Σ∗
Bewertung σ : Σ × Σ → R
Ausgabe
: Optimale Ausrichtung von s, t
D[0, 0] ← 0
for j = 1, . . . , m do
D[0, j] ← D[0, j − 1] + σ(_, bj )
pred[0, j] ← left
for i = 1, . . . , n do
D[i, 0] ← D[i − 1, 0] + σ(ai , _)
pred[i, 0] ← above
for j = 1, . . . , m do
D[i, j] ← D[i − 1, j − 1] + σ(ai , bj )
pred[i, j] ← diagonal
if D[i, j − 1] + σ(_, bj ) < D[i, j] then
D[i, j] ← D[i, j − 1] + σ(_, bj )
pred[i, j] ← left
if D[i − 1, j] + σ(ai , _) < D[i, j] then
D[i, j] ← D[i − 1, j] + σ(ai , _)
pred[i, j] ← above
i ← n; s̄ ← ε
j ← m; t̄ ← ε
while i + j > 0 do
if pred[i, j] = diagonal then
s̄ ← ai s̄; i ← i − 1
t̄ ← bj t̄; j ← j − 1
else
if pred[i, j] = left then
s̄ ← _s̄; t̄ ← bj t̄; j ← j − 1
else
s̄ ← ai s̄; t̄ ← _t̄; i ← i − 1
print (s̄, t̄), D[n, m]
76
Algorithmen und Datenstrukturen (WS 2010/2011)
77
bestimmt werden.
Beweis. Bestimme (mit modifiziertem Alg. 25) ein k ∈ {0, . . . , m} so, dass Algorithmus
von Hirschberg
opt(a1 · · · ab n2 c , b1 · · · bk ) + opt(an · · · ab n2 c+1 , bm · · · bk+1 )
minimal
Berechne rekursiv optimale Ausrichtungen von a1 · · · ab n2 c mit b1 · · · bk̂ sowie
von an · · · ab n2 c+1 mit bm · · · bk̂+1 und gib als Ergebnis deren Aneinanderreihung zurück
5.9 Beispiel
...
Ausblick: semi-globales und lokales Ausrichten
Kapitel 6
Graphen
Beziehungen zwischen Objekten werden sehr oft durch binäre Relationen
modelliert. Wir beschäftigen uns in diesem Kapitel mit speziellen binären
Relationen, die nicht nur nur besonders anschaulich sind, sondern auch in
zahllosen Anwendungen auftreten.
Eine symmetrische und irreflexive binäre Relation E ⊆ V × V kann auch
als Menge von zweielementigen Teilmengen {u, v} ⊆ V aufgefasst werden,
da (u, v) ∈ E ⇐⇒ (v, u) ∈ E (Symmetrie) und E ∩ {(v, v) : v ∈ V } = ∅
(Irreflexivität).
6.1 Def inition (Graph)
Ein Paar G = (V, E) aus Mengen V und E ⊆ V2 heißt (endlicher, ungerichteter) Graph. Die Elemente von V heißen Knoten, die von E Kanten. Wenn keine Missverständnisse zu befürchten sind, schreiben wir kurz
n = n(G) = |V | und m = m(G) = |E| für deren Anzahl.
Graphen können sehr anschaulich dargestellt werden, indem man die Knoten
durch Punkte und die Kanten durch Kurven, welche die Punkte ihrer beiden
Knoten verbinden, repräsentiert. Die folgenden Beispiele sind Darstellungen
einiger wichtiger Familien von Graphen.
78
Algorithmen und Datenstrukturen (WS 2010/2011)
vollständige Graphen Kn
79
Hyperwürfel Qd
d
V = {1, . . . , n}
E = {1,...,n}
2
V = {0, 1}
E=
{(a1 , . . . , ad ), (b1 , . . . , bd )} :
d
P
|ai − bi | = 1
i=1
Zykel (auch: Kreise) Cn
Wege Pn
V = {0, . . . , n − 1}
E = {{i, i + 1 mod n} : i = 0, . . . , n − 1}
V = {0, . . . , n}
E = {{i, i + 1} : i = 0, . . . , n − 1}
6.2 Bemerkung
In der Graphentheorie werden auch allgemeinere binäre Relationen behandelt, in denen man auf Symmetrie (→ gerichtete Graphen), Irreflexivität (→
Graphen mit Schleifen) oder beides verzichtet. Gelegentlich werden auch Multiteilmengen (d.h. Mengen von so genannten Mehrfachkanten) von V × V (→
Multigraphen) oder Kanten mit beliebiger Knotenzahl (→ Hypergraphen)
betrachtet.
gerichteter
Graph
Multigraph
(mit Schleifen
und Mehrfachkanten)
Hypergraph
(mit drei Hyperkanten)
Wir beschränken uns jedoch auf den Fall ungerichteter Graphen ohne Schleifen oder Mehrfachkanten; diese werden zur besseren Unterscheidung oft als
Algorithmen und Datenstrukturen (WS 2010/2011)
80
schlichte Graphen bezeichnet.
6.3 Def inition (Adjazenz, Inzidenz, Grad)
Ist G = (V, E) ein Graph, dann heißen zwei Knoten u, v ∈ V adjazent
(auch: benachbart), falls es eine Kante {u, v} ∈ E gibt, und die Matrix
A(G) = (av,w )v,w∈V mit
(
1 falls {v, w} ∈ E
av,w =
0 sonst
Adjazenzmatrix des Graphen. Ein Knoten v ∈ V und eine Kante e ∈ E
heißen inzident, falls v ∈ e, und die Matrix I(G) = (iv,e ) v∈V mit
e∈E
iv,e
(
1 falls v ∈ e
=
0 sonst
Inzidenzmatrix des Graphen. Die Menge NG (v) = {w ∈ V : {v, w} ∈ E} der
zu v ∈ V adjazenten Knoten heißt Nachbarschaft von v und wir nennen deren
Kardinalität
X
X
dG (v) = |NG (v)| =
av,w =
iv,e ,
w∈V
e∈E
d.h. die Zeilensumme von v in A(G) oder I(G), den Grad von v. Der minimale und maximale Grad werden mit δ(G) = minv∈V dG (v) bzw. ∆(G) =
maxv∈V dG (v) bezeichnet.
Adjazenz- und Inzidenzmatrix sind einfach zu handhabende Datenstrukturen
zur Speicherung von Graphen. Wie wir in Kürze sehen werden, gehört zu den
wichtigsten Operationen auf einem Graphen z.B. das Aufzählen der Nachbarn
eines Knotens, und gerade dafür eignen sich die beiden Matrixdarstellungen
nicht besonders gut, insbesondere nicht, wenn sie viele Nullen enthalten. Außerdem würde in diesem Fall auch viel Speicherplatz verschwendet.
Für die Implementation von Graphenalgorithmen nehmen wir daher an, dass
als Graphdatenstruktur eine Adjazenzlistendarstellung verwendet wird. Diese
besteht aus einem Knotenarray, in dem zu jedem Knoten eine lineare Liste
mit seinen Nachbarn gespeichert wird, und entspricht somit einer komprimierten Darstellung der Adjazenzmatrix, in der alle Nullen übersprungen
Algorithmen und Datenstrukturen (WS 2010/2011)
81
werden. Die zu einem Knoten inzidenten Kanten und dessen Nachbarn erhalten wir dann durch einfaches Durchlaufen seiner Adjazenzliste.
Nachteil der Adjazenzlisten ist der zusätzliche Speicherplatzbedarf für die
Listenzeiger (was allerdings erst bei fast vollständigen Graphen ins Gewicht
fällt) und die höhere Laufzeit für den Test, ob zwei gegebene Knoten adjazent
sind.
Es sei noch angemerkt, das Graphen gelegentlich auch als Liste ihrer Knoten und Kanten gespeichert werden. Die Kantenliste ist eine komprimierte
Darstellung der Inzidenzmatrix; die Knotenliste ist dann u.a. deshalb nötig,
weil isolierte Knoten (Knoten ohne inzidente Kante) sonst nicht repräsentiert
wären.
6.4 Lemma (Handschlaglemma)
Für alle Graphen G = (V, E) gilt
X
dG (v) = 2m .
v∈V
Beweis. Prinzip des doppelten Abzählens: auf der linken Seite der Gleichung wird über alle Zeilen der Inzidenzmatrix summiert. Da jede Spalte
einer Kante entspricht und jede Kante genau zwei verschiedene Knoten enthält, ergibt die Summe über alle Spalten gerade zweimal die Anzahl der
Kanten.
6.5 Folgerung
Die Anzahl der Knoten ungeraden Grades ist gerade.
Beweis. Sei Vi = {v ∈ V : dG (v) ≡ i mod 2}, i = 0, 1, die Menge der
Knoten mit geradem bzw. ungeradem Grad. Mit dem Handschlaglemma 6.4
gilt
X
X
X
2m =
dG (v) =
dG (v) +
dG (v) ,
v∈V
v∈V0
|
v∈V1
{z
gerade
}
also ist auch die hinterste Summe gerade, sodass, weil jeder ihrer Summanden
ungerade ist, deren Anzahl gerade sein muss.
6.6 Satz
Es gibt immer zwei Knoten mit gleichem Grad.
Algorithmen und Datenstrukturen (WS 2010/2011)
82
Beweis. Für alle v ∈ V gilt 0 ≤ dG (v) ≤ n − 1. Gibt es einen Knoten mit
Grad 0, dann gibt es keinen mit Grad n − 1, und umgekehrt (ein Knoten vom
Grad 0 ist mit keinem anderen benachbart, einer vom Grad n − 1 mit allen
anderen Knoten; beides zusammen geht nicht). Also gilt in einem Graphen
sogar 0 ≤ dG (v) ≤ n − 2 oder 1 ≤ dG (v) ≤ n − 1 für alle v ∈ V . Mit
dem Taubenschlagprinzip folgt, dass von den n Knoten mindestens zwei den
gleichen der n − 1 möglichen Grade haben.
6.7 Def inition (Teilgraphen)
Sind G = (V, E) ein Graph sowie V 0 ⊆ V und E 0 ⊆ E Teilmengen seiner
Knoten und Kanten, dann ist G0 = (V 0 , E 0 ) ein Teilgraph von G, falls E 0 ⊆
V0
. Wir sagen dann G enthält G0 und schreiben G0 ⊆ G. Die Teilgraphen
2
!
[
0
G[V 0 ] = V 0 , E ∩ V2
und G[E 0 ] =
e, E 0
e∈E 0
heißen der von V 0 knoten- bzw. von E 0 kanteninduzierte Teilgraph. Ein Teilgraph heißt aufspannend, falls er alle Knoten enthält.
Wir werden außerdem Schreibweisen wie z.B. G − v für G[V \ {v}] oder
G + e für den Graphen G0 = (V, E ∪ {e}) verwenden. Um die Knoten- und
Kantenmenge eines Graphen G von anderen zu unterscheiden, schreiben wir
auch V (G) und E(G).
6.8 Def inition (Graphenisomorphismus)
Gibt es zu zwei Graphen G1 = (V1 , E1 ) und G2 = (V2 , E2 ) eine bijektive
Abbildung α : V1 → V2 mit
{u, v} ∈ E1 ⇐⇒ {α(u), α(v)} ∈ E2 ,
dann heißen die Graphen G1 und G2 isomorph, G1 ∼
= G2 , und α (Graphen)isomorphismus.
Wir interessieren uns vor allem für strukturelle Eigenschaften und werden
isomorphe Graphen daher nicht unterscheiden. Dadurch können wir z.B. von
dem vollständigen Graphen mit 7 Knoten sprechen, auch wenn seine Knotenmenge nicht {1, . . . , 7} ist.
Algorithmen und Datenstrukturen (WS 2010/2011)
83
6.9 Def inition (Wege und Kreise in einem Graphen)
Ein Graph G = (V, E) mit s, t ∈ V enthält einen (s, t)-Weg der Länge k,
falls er einen zu Pk isomorphen Teilgraphen mit Endknoten s und t enthält,
d.h. es gibt einen Teilgraphen P = (V (P ), E(P )) ⊆ G mit s, t ∈ V (P ) und
einen Isomorphismus α : {0, . . . , k} → V (P ) von Pk so, dass α(0) = s und
α(k) = t.
Die Länge eines kürzesten (s, t)-Weges heißt Abstand (auch: Distanz) von s
und t und wird mit dG (s, t) bezeichnet.
Der Graph enthält einen Kreis der Länge k, falls er einen zu Ck isomomorphen
Teilgraphen enthält.
6.10 Def inition (Zusammenhang)
Ein Graph G = (V, E) heißt zusammenhängend, wenn er zu je zwei Knoten
s, t ∈ V einen (s, t)-Weg enthält. Die inklusionsmaximalen zusammenhängenden Teilgraphen eines Graphen sind seine (Zusammenhangs)komponenten
und ihre Anzahl wird mit κ(G) bezeichnet.
6.1
Bäume und Wälder
Die folgende spezielle Klasse von Graphen ist uns schon begegnet und eine
zentrale Struktur der Informatik.
6.11 Def inition (Baum)
Ein zusammenhängender Graph ohne Kreise heißt Baum. Ein Graph, dessen
sämtliche Zusammenhangskomponenten Bäume sind, heißt Wald.
6.12 Satz
Für jeden Graphen G = (V, E) gilt m ≥ n − κ(G). Gleichheit gilt genau
dann, wenn G ein Wald ist.
Beweis. Induktion über die Anzahl m der Kanten bei fester Anzahl n von
Knoten. Enthält ein Graph keine Kante, sind keine zwei Knoten durch einen
Weg verbunden, jeder bildet also seine eigene Komponente (und damit einen
trivialen Baum) und es gilt m = 0 = n − κ(G). Fügt man zu einem Graphen
eine Kante zwischen bestehenden Knoten hinzu, verbindet sie zwei Knoten,
Algorithmen und Datenstrukturen (WS 2010/2011)
84
die entweder in derselben oder in zwei verschiedenen Komponenten (die dann
vereinigt werden) liegen.
Die Zahl der Komponenten verringert sich also um maximal eins, sodass die
Ungleichung weiterhin gilt. Wenn durch die neue Kante zwei Komponenten
vereinigt werden, entsteht kein neuer Kreis, denn sonst müsste es zwischen
den beiden neu verbundenen Knoten vorher schon einen Weg gegeben haben.
6.13 Satz
Für einen Graphen G = (V, E) sind folgende Aussagen äquivalent:
(i) G ist ein Baum.
(ii) Zwischen je zwei Knoten in G existiert genau ein Weg.
(iii) G ist zusammenhängend und hat n − 1 Kanten.
(iv) G ist minimal zusammenhängend, d.h. G ist zusammenhängend und
für alle e ∈ E ist G − e unzusammenhängend.
(v) G ist maximal kreisfrei (azyklisch), d.h. G ist kreisfrei und für alle
e ∈ V2 \ E enthält G + e einen Kreis.
Beweis. (i) ⇐⇒ (ii): Da Bäume zusammenhängend sind, gibt es zwischen
je zwei Knoten mindestens einen Weg. Gäbe es zwei verschiedene, so enthielte
deren Vereinigung einen Kreis. Gibt es umgekehrt immer genau einen Weg,
dann ist der Graph kreisfrei, weil es andernfalls zwei Wege zwischen je zwei
Knoten des Kreises gäbe.
(i) ⇐⇒ (iii): Folgt unmittelbar aus Satz 6.12.
(iii) =⇒ (iv): Wegen Satz 6.12 kann ein Graph mit n − 2 Kanten nicht
zusammenhängend sein.
Algorithmen und Datenstrukturen (WS 2010/2011)
85
(iv) =⇒ (v): Enthielte G einen Kreis, so könnte eine beliebige Kante des
Kreises entfernt werden und G wäre immer noch zusammenhängend. Könnte
man eine Kante hinzufügen, ohne einen Kreis zu erzeugen, kann es vorher
keinen Weg zwischen den beiden Knoten der neuen Kante gegeben haben,
der Graph wäre also nicht zusammenhängend gewesen.
(v) =⇒ (i): Da G kreisfrei ist, müssen wir nur zeigen, dass G auch zusammenhängend ist. Wäre G nicht zusammenhängend, so könnte zwischen zwei
Knoten in verschiedenen Komponenten eine Kante eingefügt werden, ohne
einen Kreis zu erzeugen.
6.14 Folgerung
Jeder zusammenhängende Graph enthält einen aufspannenden Baum.
Die Anzahl
µ(G) = m − n + κ(G)
der Kanten, die man aus einem Graphen entfernen muss/kann, um einen aufspannenden Wald mit gleicher Anzahl von Komponenten zu erhalten, heißt
auch zyklomatische Zahl des Graphen.
6.2
Durchläufe
In diesem Abschnitt werden wir Graphen durchlaufen, um Eigenschaften zu
testen oder Teilgraphen zu identifizieren. Durchlaufen bedeutet dabei, an einem Knoten zu starten und jeweils von einem bereits besuchten Knoten über
eine Kante den benachbarten Knoten aufzusuchen. Dazu werden zunächst die
Definitionen von Wegen und Kreisen verallgemeinert.
6.15 Def inition (Graphenhomomorphismus)
Gibt es zu zwei Graphen G1 = (V1 , E1 ) und G2 = (V2 , E2 ) eine Abbildung
α : V1 → V2 mit
{u, v} ∈ E1 =⇒ {α(u), α(v)} ∈ E2 ,
dann heißt α (Graphen)homomorphismus und wir nennen den Teilgraphen
α(G1 ) = (α(V1 ), {{α(u), α(v)} ∈ E2 : {u, v} ∈ E1 }) ⊆ G2
homomorphes Bild von G1 in G2 .
Algorithmen und Datenstrukturen (WS 2010/2011)
86
6.16 Def inition (Kantenzug und Tour)
Ist G = (V, E) ein Graph und α : V (Pk ) → V (G) ein Homomorphismus, dann
heißt α(Pk ) ⊆ G Kantenzug der Länge k. Ein Kantenzug heißt geschlossen
oder Tour, falls α(0) = α(k) (in diesem Fall ist α(Pk ) auch homomorphes
Bild des Kreises Ck ).
Eine Kante {v, w} ∈ E ist | {{i, i + 1} ∈ E(Pk ) : {α(i), α(i + 1)} = {v, w}} |
mal im Kantenzug α(Pk ) enthalten.
s
s
t
G und P5
6.2.1
Kantenzug
der Länge 5;
gleichzeitig (s, t)-Weg
t
Kantenzug
der Länge 5;
aber kein Weg
geschlossener
Kantenzug (eine Kante
zweimal enthalten)
Eulertouren
Manche Graphenprobleme unterscheiden sich nur geringfügig in der Definition, aber erheblich im Schwierigkeitsgrad. Wir beginnen mit einem „leichten“,
dass sogar als Beginn der Graphentheorie angesehen wird.
Euler,
1736
6.17 Def inition (Eulertour)
Eine Tour heißt Eulertour, falls sie jede Kante genau einmal enthält.
6.18 Satz
Ein zusammenhängender Graph enthält genau dann eine Eulertour, wenn
alle Knoten geraden Grad haben.
Beweis. Der Graph G = (V, E) enthalte eine Eulertour, d.h. es gebe eine
homomorphe Abbildung von Cm , dem Kreis mit m Kanten, auf G. Da jede
Kante nur einmal in der Tour vorkommt, sorgt jeder Knoten des Kreises
dafür, dass zwei zum selben Knoten von G inzidente Kanten in der Tour
Algorithmen und Datenstrukturen (WS 2010/2011)
87
enthalten sind. Da aber alle Kanten in der Tour vorkommen, muss die Anzahl
der zu einem Knoten von G inzidente Kanten gerade sein.
Betrachte nun umgekehrt einen zusammenhängenden Graphen, in dem jeder
Knoten geraden Grad hat. Beginne bei irgendeinem Knoten und wählen eine Algorithmus
HierKante. Am benachbarten Knoten wähle eine noch nicht gewählte Kante und von
holzer
fahre so fort. Dies geht solange, bis man wieder den ersten Knoten erreicht,
denn an jedem anderen gibt es wegen des geraden Grades immer eine noch
nicht gewählte Kante. Die gewählten Kanten sind homomorphes Bild eines
Kreises und an jedem Knoten ist die Anzahl der gewählten und ungewählten
Kanten gerade. Falls es noch einen Knoten mit ungewählten Kanten gibt,
dann wegen des Zusammenhangs auch einen, der zu schon gewählten Kanten
inzident ist. Wiederhole die Konstruktion von diesem Knoten aus und füge die
beiden homomorphen Bilder von Kreisen zu einem zusammen (Anfangsstück
des ersten bis zum gemeinsamen Knoten, dann der zweite Kantenzug und
schließlich das Endstück des ersten), bis alle Kanten einmal gewählt wurden.
6.19 Beispiel (Königsberger Brückenproblem)
Der unten stehende Graph stellt sieben Brücken (helle Knoten) über den
durch Königsberg fliessenden Fluss Pregel dar. Da nicht alle Knoten geraden
Grad haben, gibt es keine Eulertour (und damit keinen Rundgang, der jede
der Brücken genau einmal überquert).
C
A
D
B
Graphenmodell
Originalskizze
Algorithmen und Datenstrukturen (WS 2010/2011)
88
6.20 Beispiel (Haus vom Nikolaus)
Wenn man sich mit einem (nicht notwenig geschlossenen) Kantenzug, der alle
Kanten genau einmal enthält, begnügt, dann funktioniert die Konstruktion
aus dem Beweis von Satz 6.18 auch dann noch, wenn es zwei Knoten ungeraden Grades gibt. Man muss dann allerdings bei einem davon anfangen und
wird beim anderen aufhören.
6.2.2
Tiefensuche
Der Algorithmus zur Konstruktion einer Eulertour durchläuft solange Kanten
vom jeweils zuletzt besuchten Knoten, bis dieser nicht mehr zu unbesuchten
Kanten inzident ist. In diesem Fall wird an irgendeinem zu unbesuchten Kanten inzidenten Knoten weitergemacht.
In der Variante in Algorithmus 26 wird immer vom zuletzt gefundenen (statt
besuchten) Knoten aus weitergesucht. Auf diese Weise können z.B. ein aufspannender Baum, ein Kreis oder ein Weg zwischen zwei Knoten konstruiert
werden.
Algorithmen und Datenstrukturen (WS 2010/2011)
89
Algorithmus 26: Tiefensuche (depth-first search, DFS )
Eingabe
: Graph G = (V, E), Wurzel s ∈ V
Daten
: Knotenstack S, Zähler d
Ausgabe
: Tiefensuchnummern DFS (anfänglich ∞)
Vorgänger parent (anfänglich nil)
DFS[s] ← 1; d ← 2
markiere s; push s → S
while S nicht leer do
v ← top(S)
if es ex. unmarkierte Kante {v, w} ∈ E then
markiere {v, w}
if w nicht markiert then
DFS[w] ← d; d ← d + 1
markiere w; push w → S
parent[w] ← v
else pop v ← S
Durch die Speicherung von Graphen in Adjazenzlisten ist die Laufzeit der
Tiefensuche in O(n + m), denn an die jeweils nächste unmarkierte inzidente
Kante kommen wir so in O(1) und jeder Knoten und jede Kante werden nur
einmal markiert.
Offensichtlich erhalten alle Knoten eines Graphen, die im Verlauf des Algorithmus’ markiert werden, eine endliche Tiefensuchnummer DFS[v]. Falls
nach Ablauf des Algorithmus’ parent[w] = v gilt, heißt die Kante {v, w}
Baumkante, alle anderen markierten Kanten heißen Nichtbaumkanten (auch:
Rückwärtskanten).
6.21 Beispiel
Tiefensuche vom hellen Knoten aus: die Knoten sind mit ihren Tiefensuchnummern und die Kanten in Reihenfolge ihres Durchlaufs beschriftet.
Algorithmen und Datenstrukturen (WS 2010/2011)
8
9
1
4
9
1
3
90
11
10
8
10
12
7
13
2
3
5
4
7
6
2
5
11
6
Beobachtung: Die Tiefensuche liefert einen aufspannenden Baum (Baumkanten durchgezogen). Die Knoten einer Nichtbaumkante (gestrichelt) sind durch
einen monoton nummerierten Weg im Baum verbunden.
6.22 Lemma
Wenn v ∈ V auf S abgelegt wird, sei M ⊆ (V \ {v}) die Menge aller anderen
bereits markierten Knoten. Der Knoten v wird erst von S entfernt, nachdem
alle Knoten t ∈ V \ M , für die es einen (v, t)-Weg in G[V \ M ] gibt, ebenfalls
markiert wurden.
Beweis. Angenommen, es gibt einen Knoten, für den die Aussage falsch ist.
Wähle unter allen diesen dasjenige v ∈ V mit maximaler DFS[v] und einen
Knoten t ∈ V , der bei Entfernen von v aus S noch nicht markiert ist.
Gibt es einen (v, t)-Weg über unmarkierte Knoten, dann beginnt er mit einem
unmarkierten Nachbarn von v. In der while-Schleife werden alle Nachbarn
von v markiert, bevor v aus S entfernt wird, und wegen der Maximalität von
DFS[v] wird t markiert, bevor dieser Nachbar aus S entfernt wird. Das ist ein
Widerspruch.
6.23 Satz
Ist T ⊆ E die Menge der Baumkanten nach einer Tiefensuche auf G = (V, E)
mit Wurzel s ∈ V , dann ist (G[T ] + s) ⊆ G ein aufspannender Baum der
Zusammenhangskomponente, die s enthält.
Beweis. Wir stellen zunächst fest, dass alle zu Baumkanten inzidenten
Knoten markiert werden. Der von den Baumkanten induzierte Graph ist
zusammenhängend, da jede neue Baumkante immer inzident zu einem bereits
markierten Knoten ist. Er ist außerdem kreisfrei, denn jede neue Baumkante
Algorithmen und Datenstrukturen (WS 2010/2011)
91
enthält genau einen Knoten, der zuvor nicht markiert war (ein Kreis kann
also niemals geschlossen werden).
Weil Lemma 6.22 insbesondere für die Wurzel selbst gilt, werden alle Knoten
der Zusammenhangskomponente von s markiert.
Durch mehrfache Tiefensuche können daher alle Zusammenhangskomponenten bestimmt werden.
Der folgende Satz besagt, dass Nichtbaumkanten immer nur zwischen Knoten
verlaufen, die mit der Wurzel auf einem gemeinsamen Weg liegen. Daher kann
man die Tiefensuche z.B. auch benutzen, um in Komponenten, die keine
Bäume sind, als Zertifikat dafür einen Kreis zu konstruieren.
6.24 Satz
Ist T ⊆ E die Menge der Baumkanten nach einer Tiefensuche auf G =
(V, E), {v, w} ∈ E \ T eine Nichtbaumkante mit DFS[w] < DFS[v] und v =
v1 , v2 , . . . , vk = w der eindeutige (v, w)-Weg in G[T ], dann gilt parent[vi ] =
vi+1 für alle i = 1, . . . , k − 1.
Beweis. Die Existenz der Nichtbaumkante zeigt, dass v und w in der selben Zusammenhangskomponente liegen, und DFS[w] < DFS[v] bedeutet, dass
v später als w gefunden wird. Die Nichtbaumkante wird daher markiert während v (und nicht w) oben auf dem Stack liegt (andernfalls wäre sie eine
Baumkante). Das bedeutet aber, dass w zu diesem Zeitpunkt noch in S ist,
weil ein Knoten erst entfernt wird, nachdem alle inzidenten Kanten markiert
wurden. Die Behaupung folgt damit aus der Beobachtung, dass für einen
Knoten u, der auf dem Stack unmittelbar auf einen Knoten u0 gelegt wird,
parent[u] = u0 gilt.
6.2.3
Breitensuche
Wir ändern die Durchlaufreihenfolge nun so, dass immer vom zuerst gefundenen Knoten aus weitergesucht wird. Dadurch kann nicht nur wieder ein
aufspannender Wald konstruiert werden, sondern auch kürzeste Wege von
einem Anfangsknoten zu allen anderen in seiner Komponente.
Auch die Breitensuche benötigt nur Zeit O(n + m), weil die Adjazenzliste
jedes Knotens nur einmal durchlaufen werden muss. Offensichtlich erhalten
alle Knoten, die im Verlauf des Algorithmus’ markiert werden, eine endliche
Algorithmen und Datenstrukturen (WS 2010/2011)
92
Algorithmus 27: Breitensuche (breadth-first search, BFS )
Eingabe
: Graph G = (V, E), Wurzel s ∈ V
Daten
: Knotenwarteschlange Q
Ausgabe
: Breitensuchnummern BFS (anfänglich ∞)
Vorgänger parent (anfänglich nil)
BFS[s] ← 0
markiere s; enqueue Q ← s
while Q nicht leer do
dequeue v ← Q
for unmarkierte Kanten {v, w} ∈ E do
markiere {v, w}
if w nicht markiert then
BFS[w] ← BFS[v] + 1
markiere w; enqueue Q ← w
parent[w] ← v
Breitensuchnummer BFS[v]. Falls nach Ablauf des Algorithmus’ parent[w] =
v gilt, heißt die Kante {v, w} Baumkante, alle anderen markierten Kanten
heißen Nichtbaumkanten.
Man beachte, dass für Baumkanten BFS[w] = BFS[v]+1 gilt. Gilt dies für eine
Nichtbaumkante, nennen wir sie auch Vorwärtskante, andernfalls Querkante.
6.25 Beispiel
Breitensuche vom hellen Knoten aus: die Knoten sind mit ihren Breitensuchnummern und die Kanten in Reihenfolge ihres Durchlaufs beschriftet.
2
0
3
1
8
6
12
1
3
2
5
10
13
11
2
3
7
3
2
1
3
4
9
3
Algorithmen und Datenstrukturen (WS 2010/2011)
93
Beobachtung: Die Breitensuche liefert einen aufspannenden Baum (Baumkanten sind durchgezogen, Vorwärtskanten gestrichelt, Querkanten gepunktet). Die Breitensuchnummern sind gerade die Längen kürzester Wege von
der Wurzel aus.
6.26 Lemma
Alle Knoten und Kanten der Zusammenhangskomponente der Wurzel werden
markiert. Für jede markierte Kante {v, w} gilt |BFS[v] − BFS[w]| ≤ 1.
Beweis. Wird ein Knoten markiert, dann auch alle seine inzidenten Kanten
und alle seine Nachbarn. Eine nicht markierte Kante kann also nicht zu einem
markierten Knoten inzident sein.
Angenommen, es gibt einen nicht markierten Knoten v in der Zusammenhangskomponente der Wurzel s. Nach Definition des Zusammenhangs gibt es
einen (v, s)-Weg. Da s markiert wird, muss es auf diesem Weg zwei adjazente
Knoten geben, von denen genau einer markiert ist. Das ist ein Widerspruch.
Die Breitensuchnummern der Knoten in der Warteschlange unterscheiden
sich um höchstens 1 und die Knoten mit kleinerer Nummer werden zuerst
entnommen, denn wenn ein Knoten w markiert und eingefügt wird, wurde
zuvor ein Knoten v mit um 1 kleinerer Nummer entnommen. Wird eine weitere zu w inzidente Kante von einem Knoten v 0 aus markiert, dann ist w
selbst noch in der Warteschlange (sonst wären alle inzidenten Kanten bereits
markiert), sodass der Nachbar v 0 nach v und vor w eingefügt wurde und seine
Breitensuchnummer zwischen denen von v und w liegt.
6.27 Satz
Ist T ⊆ E die Menge der Baumkanten nach einer Breitensuche auf G = (V, E)
mit Wurzel s ∈ V , dann ist (G[T ] + s) ⊆ G ein aufspannender Baum der
Zusammenhangskomponente, die s enthält.
Beweis. Ist ns die Anzahl der Knoten in der Zusammenhangskomponente
von s, dann ist |T | = ns − 1, denn nach Lemma 6.26 werden alle ns Knoten
markiert, und eine Baumkante kommt genau dann hinzu, wenn ein anderer
Knoten als s markiert wird.
Wegen Satz 6.13 brauchen wir also nur noch zu zeigen, dass G[T ] für T 6= ∅
zusammenhängend ist. Ein anderer Knoten als s wird aber nur dann markiert,
wenn er einen bereits markierten Nachbarn hat.
Algorithmen und Datenstrukturen (WS 2010/2011)
94
6.28 Satz
Der eindeutige Weg von der Wurzel s zu einem Knoten v ∈ V in G[T ] + s ist
ein kürzester (s, v)-Weg in G und BFS[v] = dG (s, v).
Beweis. Wenn in der Breitensuche eine Baumkante {parent[w], w} durchlaufen wird, erhält w die Breitensuchnummer BFS[parent[w]] + 1. Der eindeutige (s, v)-Weg im Baum hat daher die Länge BFS[v].
Sind s = v0 , v1 , . . . , vk = v die Knoten auf einem (s, v)-Weg in G, dann
gilt |BFS[vi ] − BFS[vi−1 ]| ≤ 1 für alle i = 1, . . . , k wegen Lemma 6.26. Weil
BFS[s] = 0 hat jeder solche Weg mindestens BFS[v] Kanten.
6.3
Kürzeste Wege
Das am Ende des letzen Abschnitts gelöste Problem ist Spezialfall eines der
wichtigsten Graphenprobleme, für die es effiziente Algorithmen gibt. Wir
wollen es abschließend in größerer Allgemeinheit behandeln und führen daher
noch Richtungen und Bewertungen für die Kanten eines Graphen ein.
Wie in Bemerkung 6.2 bereits angedeutet, unterscheidet sich ein gerichteter Graph von einem ungerichteten dadurch,
dass die Kantenpaare geordnet
V
sind. Es gilt also nicht mehr E ⊆ 2 , sondern
E ⊆ (V × V ) \ {(v, v) : v ∈ V } .
Für eine Kante (v, w) ∈ E heißt v Anfangs- oder Startknoten, und w heißt
End- oder Zielknoten der Kante. Die Kante (v, w) heißt ausgehende Kante
von w und eingehende Kante von w.
Die meisten der oben definierten Konzepte übertragen sich in nahe liegender
Weise. Hier interessieren uns vor allem Wege: damit ein Weg ein gerichteter
Weg ist, muss für alle Paare aufeinander folgender Kanten der Zielknoten der
vorangehenden Kante Startknoten der nachfolgenden Kante sein.
Zur genaueren Modellierung ungleicher Beziehungen zwischen Objekten können die Kanten gerichteter wie ungerichteter Graphen bewertet (labeled ) sein.
Diese Bewertungen können z.B. Typen von Beziehungen unterscheiden (ERDiagramme) oder die Ähnlichkeit bzw. Distanz (Sequenz-Abgleich) zweier
Algorithmen und Datenstrukturen (WS 2010/2011)
95
Objekte ausdrücken. Wir beschränken uns hier auf reellwertige Kantenlabel,
die als Distanz interpretiert werden. In diesem Fall ist die Länge eines Weges
definiert als die Summe der Distanzen, mit denen seine Kanten beschriftet
sind (alternativ könnte etwa das Produkt von Funktionswahrscheinlichkeiten
für Übertragungsleitungen von Interesse sein).
6.29 Problem (Single-Source Shortest-Paths Problem, SSSP)
gegeben: gerichteter Graph G = (V, E; λ) mit Kantenlängen λ : E → R
gesucht: kürzeste Wege von s zu allen anderen Knoten
Mit Satz 6.28 haben wir oben gezeigt, dass in ungerichteten Graphen G =
(V, E; λ) mit uniformer Kantenbewertung λ(e) = 1 für alle e ∈ E eine Breitensuche von s aus zur Lösung des SSSP ausreicht, und es ist leicht zu sehen,
wie die Breitensuche für gerichtete Graphen mit uniformer Kantenbewertung
abzuwandeln ist (wir müssen uns bei der Schleife über alle unmarkierten inzidenten Kanten lediglich auf die ausgehenden beschränken).
6.3.1
Algorithmus von Dijkstra
Wir verallgemeinern die Breitensuche zunächst dahin gehend, dass das SSSPProblem in gerichteten Graphen mit nicht-negativen (statt uniformen) Kantenlängen λ : E → R≥0 gelöst wird.
Die wesentliche Änderung in Algorithmus 28 gegenüber der Breitensuche besteht im Austausch der Warteschlange gegen eine Prioritätswarteschlange,
die es erlaubt, kürzere, aber später gefundene Alternativen zu berücksichtigen, indem ein noch in Q befindlicher Knoten w weiter nach vorne geholt
wird. Dies geschieht immer dann, wenn die Distanz des nächsten Knotens v
zusammen mit der Länge der Kante (v, w) kleiner ist als die bisherige Schätzung für die Distanz, die w von s hat. Wir sagen dann, die Kante (v, w)
(deren Endknoten bisher als weiter entfernt angenommen wurden, als es die
Länge der Kante möglich macht) werde relaxiert.
6.30 Satz
Algorithmus 28 löst das SSSP-Problem auf gerichteten Graphen mit nichtnegativen Kantenlängen.
Beweis.
Wir beweisen die folgende Invariante.
Algorithmen und Datenstrukturen (WS 2010/2011)
96
Algorithmus 28: Algorithmus von Dijsktra
Eingabe
: gerichteter Graph G = (V, E; λ) mit λ : E → R≥0
Startknoten s ∈ V
Daten
: Prioritätswarteschlange Q
Ausgabe
: Distanzen d (anfänglich ∞)
Vorgänger parent (anfänglich nil)
d[s] ← 0
insert Q ← V
while Q 6= ∅ do
extractMin v ← Q
for (v, w) ∈ E do
if d[v] + λ(v, w) < d[w] then
d[w] ← d[v] + λ(v, w)
parent[w] ← v
Bei Ausführung von „extractMin v ← Q“ gilt d[v] = dG (s, v).
Da der Algorithmus erst abbricht, wenn alle erreichbaren Knoten einmal aus
Q entfernt wurden, folgt daraus die Korrektheit.
Angenommen, die Invariante gelte nicht immer, und v ∈ V wäre der erste
aus Q zu entnehmende Knoten, für den sie nicht gilt. Dann ist v 6= s, weil
d[s] = 0 = dG [s]. Sei P ein kürzester (s, v)-Weg (ein solcher exisitert, da
andernfalls d[v] = ∞ = dG [v]), z der erste Knoten auf P , der noch in Q ist,
und y dessen Vorgänger.
Die Anfangsstücke nach y bzw. z sind kürzeste (s, y)- und (s, z)-Wege, da
sie sonst durch kürzere ersetzt werden könnten und wir so einen kürzeren
(s, v)-Weg als P erhielten. Da y bereits aus Q entnommen ist, gilt
d[z] ≤ d[y] + λ(y, z)
k
k
dG (s, z)
dG (s, y)
k
dG (s, v) − dG (z, v)
| {z } | {z }
≤d[v]
≥0
Algorithmen und Datenstrukturen (WS 2010/2011)
97
Das heisst, entweder ist d[z] < d[v] (im Widerspruch zu removeMin v ← Q)
oder d[z] = d[v] = dG (s, v) (und die Invariante somit gar nicht verletzt). Die Effizienz hängt wesentlich von der für die Prioritätswarteschlange verwendeten Datenstruktur ab. Mit den in Kapitel 2 besprochenen Heaps ergibt
sich eine Laufzeit von O(n + m log n), da jede Kante zu einer Relaxation
führen kann.
Für m ∈ Θ(n2 ) ist ein einfaches Knotenarray für Q sogar besser, denn dann
können die maximal m Relaxationen in jeweils konstanter Zeit ausgeführt
werden. Zusammen mit n linearen Suchen nach dem jeweils kleinsten d[·]
ergibt also sich eine Laufzeit von O(n2 ).
Es gibt aber auch Heap-Implementationen, die im Allgemeinen zu besseren
Laufzeiten führen. In der Vorlesung Entwurf und Analyse von Algorithmen
(im Vertiefungs- bzw. Master-Studium) werden z.B. Fibonacci-Heaps eingeführt, mit denen Relaxationen in amortisiert konstanter sowie Extraktionen
in logarithmischer Zeit realisiert werden können. Die Gesamtlaufzeit ist dann
O(m + n log n).
Wie bei der Breitensuche kann ein kürzester Weg jeweils durch Rückverfolgen
der parent-Zeiger bis nach s ermittelt werden.
6.3.2
Algorithmus von Bellman/Ford
Da im allgemeinen Fall λ : E → R die Kanten und somit auch Wege und
sogar Zykel negative Länge haben können, kann das SSSP-Problems schlecht
gestellt sein, d.h. keine vernünftige Lösung haben. Enthält ein Graph nämlich
einen Kreis negativer Länge als Teil eines Weges von s nach t, dann könnte
der Kreis beliebig oft durchlaufen und die Länge des Weges damit immer
noch kürzer gemacht werden.
Der Bellman/Ford-Algorithmus führt die Relaxationen anders als der Algorithmus von Dijkstra nicht während eines Graphendurchlaufs, sondern schematisch in fester Reihenfolge aus. Aus dem Korrektheitsbeweis erkennt man,
dass nach n − 1 Relaxationsrunden keine Verkürzungen mehr möglich sind –
es sei denn, es gibt einen Zykel negativer Länge.
6.31 Satz
Algorithmus 29 löst das SSSP-Problem in Zeit O(nm), falls es keine Zykel
negtiver Länge gibt, und liefert andernfalls proper = false.
Algorithmen und Datenstrukturen (WS 2010/2011)
98
Algorithmus 29: Algorithmus von Bellman/Ford
Eingabe
: gerichteter Graph G = (V, E; λ) mit λ : E → R
Startknoten s ∈ V
Ausgabe
: Flag proper (anfänglich true)
Distanzen d (anfänglich ∞)
Vorgänger parent (anfänglich nil)
d[s] ← 0
for i = 1, . . . , n − 1 do
for (v, w) ∈ E do
if d[v] + λ(v, w) < d[w] then
d[w] ← d[v] + λ(v, w)
parent[w] ← v
for (v, w) ∈ E do
if d[v] + λ(v, w) < d[w] then proper ← false
(i)
Beweis. Sei dG (v, w) die Länge ein kürzesten (v, w)-Weges mit maximal i
Kanten. Wir beweisen die folgende Invariante durch Induktion über i.
(i)
Nach der i-ten Iteration gilt d[v] ≤ dG (s, v) für alle v ∈ V .
(
(0)
∞ = dG (s, v) für v 6= s
i = 0 : d[v] =
(0)
0 = dG (s, s)
für v = s
i→i+1:
Die Behauptung
folgt unmittelbar aus
(
(i)
dG (s, v)
oder
(i+1)
dG (s, v) =
(i)
dG (s, u) + λ(u, v) für ein u ∈ V
und
(i)
(i)
d[v] ← min { dG (s, u)
+ λ(u, v)} ∪ { dG (s, v)
| {z }
| {z }
(u,v)∈E
≥ d[u] nach Ind. Vor.
}
≥ d[v] nach Ind. Vor.
Wenn nach n − 1 Iterationen immer noch eine Kante (v, w) mit d[w] >
d[v] + λ(v, w) existiert, dann gilt
(n)
(n−1)
(n−1)
dG (s, w) ≤ dG (s, v) + λ(v, w) < d[w] = dG
| {z }
=d[v]
(s, w) .
Algorithmen und Datenstrukturen (WS 2010/2011)
99
Es gibt also einen (s, w)-Weg mit n Kanten, der kürzer ist als jeder (s, w)Weg mit n − 1 Kanten. Da es nur n Knoten gibt, muss der Weg einen Knoten
zweimal enthalten und der dazwischenliegende Kreis negative Länge haben.
Die Laufzeit wird offensichtlich von der Doppelschleife dominiert.
Literaturverzeichnis
[1] Reinhard Diestel, Graphentheorie. Springer-Verlag, 2000.
[2] Kurt Mehlhorn und Peter Sanders, Algorithms and Data Structures.
Springer-Verlag, 2008.
[3] Thomas Ottmann und Peter Widmayer, Algorithmen und Datenstrukturen. 4. Aufl., Spektrum Akademischer Verlag, 2002.
[4] Uwe Schöning, Algorithmik. Spektrum Akademischer Verlag, 2001.
100
Herunterladen