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