Hilfsunterlagen zu Datenstrukturen und Algorithmen 708.031 Stefan Klamp, WS 2010/11 1 Inhaltsverzeichnis 1 Einführung 5 1.1 Ein erstes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Analyse der Laufzeit . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.2.1 Bester Fall (best case) . . . . . . . . . . . . . . . . . . . . 7 1.2.2 Schlechtester Fall (worst case) . . . . . . . . . . . . . . . . 7 1.2.3 Durchschnittlicher Fall (average case) 7 . . . . . . . . . . . 2 Asymptotische Schranken 5 8 2.1 O-Notation (asymptotisch obere Schranke) . . . . . . . . . . . . 8 2.2 9 2.3 Ω-Notation (asymptotisch untere Schranke) Θ-Notation (asymptotisch exakte Schranke) . . . . . . . . . . . 9 2.4 Grenzwertkriterium . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.5 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.6 Rechen-Regeln für die O-Notation . . . . . . . . . . . . . . . . . 11 2.7 Häug vorkommende Funktionen . . . . . . . . . . . . . . . . . . 12 2.8 Anmerkung zum Logarithmus . . . . . . . . . . . . . . . . . . . . 12 . . . . . . . . . . . 3 Elementare Datenstrukturen 13 3.1 Lineares Feld . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.2 Lineare Liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.3 Stapel (Stack) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.4 Schlange (Queue) . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 4 Rekursionen 4.1 4.2 4.3 Erstes Beispiel 15 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Iterative Version 4.1.2 Rekursive Version 15 . . . . . . . . . . . . . . . . . . . . . . . 15 . . . . . . . . . . . . . . . . . . . . . . 15 Methoden zur Lösung von Rekursionsgleichungen . . . . . . . . . 16 4.2.1 Lösen durch Einsetzen . . . . . . . . . . . . . . . . . . . . 16 4.2.2 Lösen durch Induktion . . . . . . . . . . . . . . . . . . . . 16 Zweites Beispiel - Fibonacci-Zahlen . . . . . . . . . . . . . . . . . 16 4.3.1 Iterative Version . . . . . . . . . . . . . . . . . . . . . . . 17 4.3.2 Rekursive Version . . . . . . . . . . . . . . . . . . . . . . 17 4.4 Die Ackermann-Funktion . . . . . . . . . . . . . . . . . . . . . . 18 4.5 Türme von Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 5 Sortieren durch Verschmelzen - Mergesort 20 6 Halde (Heap) 23 6.1 Verhalden (Heapify) . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Aufbau einer Halde . . . . . . . . . . . . . . . . . . . . . . . . . . 24 6.3 Sortieren mit Halden (Heap Sort) . . . . . . . . . . . . . . . . . . 25 6.4 Wartschlange (Priority Queue) 25 2 . . . . . . . . . . . . . . . . . . . 24 7 Quicksort 27 7.1 Partition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 7.2 Quicksort 27 7.3 Randomisierter Quicksort . . . . . . . . . . . . . . . . . . . . . . 28 7.4 Quicksort (iterativ) . . . . . . . . . . . . . . . . . . . . . . . . . . 31 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Untere Schranke für vergleichende Sortierverfahren Ω (n log n) 8.1 Herleiten der unteren Schranke 8.2 worst-case optimal 8.3 Sortieren durch Fachverteilung (RadixSort) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Finden der i-kleinsten Zahl 32 32 33 33 34 9.1 Minimum und Maximum . . . . . . . . . . . . . . . . . . . . . . . 9.2 Allgemeiner Fall . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 9.3 Allgemeiner Fall in linearer Zeit im worst-case . . . . . . . . . . . 37 10 Gestreute Speicherung (Hashing) 34 38 10.1 Grundidee . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 10.2 Gute Hashfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . 38 10.2.1 Divisionsmethode . . . . . . . . . . . . . . . . . . . . . . . 38 10.2.2 Multiplikationsmethode 38 . . . . . . . . . . . . . . . . . . . 10.3 Behandlung von Kollisionen . . . . . . . . . . . . . . . . . . . . . 38 10.3.1 Kollisionsbehandlung mit Überlauferliste (Chaining) . . . 39 10.3.2 Oene Adressierung 40 . . . . . . . . . . . . . . . . . . . . . 11 Suchen in linearen Feldern 43 11.1 Ohne Vorsortierung . . . . . . . . . . . . . . . . . . . . . . . . . . 11.1.1 Sequentielle Suche 43 . . . . . . . . . . . . . . . . . . . . . . 43 11.1.2 Seq.Suche mit Wahrscheinlichkeitsverteilung . . . . . . . . 43 11.2 Mit Vorsortierung . . . . . . . . . . . . . . . . . . . . . . . . . . 11.2.1 Binärsuche (Binary Bisection Search) 44 . . . . . . . . . . . 44 11.2.2 Interpolationssuche . . . . . . . . . . . . . . . . . . . . . . 45 11.2.3 Quadratische Binärsuche . . . . . . . . . . . . . . . . . . 46 11.2.4 Fastsearch . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 11.2.5 Zusammenfassung 47 . . . . . . . . . . . . . . . . . . . . . . 12 Binärbäume 48 12.1 Grundlagen - Denitionen . . . . . . . . . . . . . . . . . . . . . . 12.2 Nötige Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . 12.3 Reihenfolge der Knoten 48 48 . . . . . . . . . . . . . . . . . . . . . . . 49 12.3.1 Symmetrische Reihenfolge (SR) . . . . . . . . . . . . . . . 49 12.3.2 Haupreihenfolge (HR) . . . . . . . . . . . . . . . . . . . . 49 12.3.3 Nebenreihenfolge (NR) . . . . . . . . . . . . . . . . . . . . 49 12.4 Sortierte Binärbäume . . . . . . . . . . . . . . . . . . . . . . . . 12.4.1 Suchen in Binärbäumen . . . . . . . . . . . . . . . . . . . 12.4.2 Finden des Maximus in einen Binärbaum 3 . . . . . . . . . 49 49 50 12.4.3 Finden des Minimums in einen Binärbaum . . . . . . . . 50 12.4.4 Finden des Vorgängers eines Knotens . . . . . . . . . . . 50 12.4.5 Finden des Nachfolger eines Knotens . . . . . . . . . . . 51 12.4.6 Einfügen in den Binärbaum . . . . . . . . . . . . . . . . . 51 12.4.7 Löschen eines Werts 51 . . . . . . . . . . . . . . . . . . . . . 13 (2-4)-Bäume 53 13.1 Implementierbare Funktionen . . . . . . . . . . . . . . . . . . . . 53 13.1.1 Suchen in (2-4)-Bäumen . . . . . . . . . . . . . . . . . . . 53 13.1.2 Einfügen in (2-4)-Bäumen . . . . . . . . . . . . . . . . . . 54 . . . . . . . . . . . . . . . . . 54 13.2 Beweis der logarithmischen Höhe . . . . . . . . . . . . . . . . . . 13.1.3 Entfernen in (2-4)-Bäumen 55 13.3 Beweis des linearen Speichers 55 . . . . . . . . . . . . . . . . . . . . 13.4 Anwendung: Mischbare Warteschlangen . . . . . . . . . . . . . . 56 13.5 Sortieren mit (2-4)-Bäumen . . . . . . . . . . . . . . . . . . . . . 56 13.5.1 Analyse des Sortierens mit (2,4)-Bäumen . . . . . . . . . 14 Amortisierte Kosten bei (2,4)-Bäumen 58 14.1 Denitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.1.1 minimaler, maximaler Kontostand 57 b(T ) . . . . . . . . . . 58 59 14.1.2 Anhängen, Wegnehmen eines Blattes . . . . . . . . . . . . 59 14.1.3 Spalten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 14.1.4 Stehlen und Verschmelzen . . . . . . . . . . . . . . . . . . 60 14.2 Abrechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 15 Optimales Kodieren 61 15.1 Nötige Denitionen . . . . . . . . . . . . . . . . . . . . . . . . . . 61 15.2 Darstellung des Kodierers . . . . . . . . . . . . . . . . . . . . . . 62 15.3 Eigenschaften von optimalen Codebäumen . . . . . . . . . . . . . 62 15.4 Human . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 15.4.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . 64 15.4.2 Beweis der Optimalität von Human . . . . . . . . . . . . 64 4 1 Einführung 1.1 Ein erstes Beispiel Eine ungeordnete Folge von Zahlen soll aufsteigend sortiert werden. Ein erster intuitiver Ansatz wäre, von links nach rechts gehend, jede einzelne Zahl so weit nach links zu verschieben, bis sie am richtigen Platz angelangt ist. Die verbale Formulierung kann auch in Form eines Pseudocodes ausgedrückt werden. Ein Pseudocode ist eine an höhere Programmiersprachen angelehnte Formulierung. Der vorgeschlagene Algorithmus wird in der Fachliteratur Insertion-Sort (Sortieren durch Einfügen ) genannt. INSERTION_SORT (A) 1: FOR i=2 TO n DO 2: h←A[i] 3: j←i-1 4: WHILE A[j]>h AND j>0 DO 5: A[j+1]←A[j] 6: j←j-1 7: A[j+1]←h 1.2 Analyse der Laufzeit Wir suchen nun die Abhängigkeit der Laufzeit T von der Inputgröÿe der zu sortierenden Zahlen). Im obigen Zahlenbeispiel ist n (Anzahl n = 6. Die Grundidee ist einfach die einzelnen Schritte zu zählen, sie mit konstanten Zeitfaktoren ci (als Platzhalter für die Zeit die sie benötigen) zu versehen und sie in Abhängigkeit der Inputgröÿe n darzustellen. Die Zeitfaktoren ci sind je nach Implementierung (Sprache, Hardware, etc.) unterschiedlich. Sie können aber dann immer als konstant angesehen werden. 5 Pseudocode Insertion_Sort (A) Zeitkonstante Anzahl der Wiederholungen c0 c1 c2 c3 c4 c5 c6 c7 1 1: FOR i=2 TO n DO 2: h=A[i] 3: j=i-1 4: WHILE A[j]>h AND j>0 DO 5: A[j+1]=A[j] 6: j=j-1 7: A[j+1]=h n (n − 1) (n − 1) P n Pn i=2 ti (ti − 1) Pi=2 n i=2 (ti − 1) n−1 Bemerkungen: Die Zeit um den Algorithmus aufzurufen wird als unabhängig von der Inputgröÿe angenommen. In den zukünftigen Betrachtungen wird sie deshalb nie berücksichtigt. Als optisches Kennzeichen trägt der Algorithmuskopf daher auch keine eigene Zeilennummer. Weitere Bemerkungen zu den einzelnen Zeilen: 1. In der Schleife werden die einzelnen Elemente abgearbeitet. Pro Schleife i schon n erreicht hat oder nicht. Falls nicht i um eins erhöht. Benötigte Zeit: c1 . Der Body der for -Schleife wird (n − 1) mal ausgeführt (i läuft von 2 bis n). Der Schleifenkopf wird einwird geschaut, ob der Index wird mal öfter ausgeführt als der Body, da die Abbruchbedingung noch einmal überprüft werden muss. Das gilt auch für die 2. Die Zuweisung benötigt eine konstante Zeit while -Schleife in Zeile 4. c2 . 3. Ebenfalls eine Zuweisung mit einer Subtraktion: 4. Die Konstante c4 c3 beinhaltet die beiden Vergleiche als auch deren logische Verknüpfung. Wenn man sich ein Zahlenbeispiel genauer anschaut, sieht man, dass es von der Verteilung der Zahlen abhängt, wie oft man die while -Schleife durchlaufen und damit die Abfrage durchführen muss. Man deniert eine allgemeine Variable ti : ti ist die Anzahl der while -Abfragen, 1 die der Algorithmus für das i-te Element des Inputs machen muss . 5. , 6. und 7. sind ebenfalls Zuweisungen: c5 , c6 und c7 Wenn man nun einfach alle Zeiten zusammenzählt, erhält man die Laufzeit in Abhängigkeit der Inputgröÿe T (n) = c1 n+c2 (n−1)+c3 (n−1)+c4 · n X ti +c5 · i=2 Oensichtlich hängt die Laufzeit Zeitkonstanten Folge A ci T n: T n X i=2 (ti −1)+c6 · n X (ti −1)+c7 (n−1) i=2 nicht nur von der Inputgröÿe n und den ab, sondern auch davon wie die Zahlen in der ungeordneten vorliegen (= Einuss von ti ). Dadurch kann man drei wichtige Fälle beste Fall (best case ), der schlechteste Fall (worst case ) und der durchschnittliche Fall (average case ). unterscheiden: der 1 Bemerkung: Man könnte auch eine Variable w denieren, welche zeigt wie oft die Schleife i für das i-te Element durchlaufen werden muss. Der Zusammenhang wäre dann wi = ti − 1. 6 1.2.1 Bester Fall (best case) Der beste (schnellste) Fall liegt vor, wenn der Algorithmus nie in die WHILE- A[j] > h and j > 0 in durchgeführt: ti = 1, ∀i = 2, 3..., n. Schleife hineingehen muss. Das heiÿt, die logische Abfrage Zeile 5 wird jeweils nur einmal pro Element Mit Pn i=2 ti = Pn i=2 1 = (n − 1) ergibt sich für die Laufzeit: T (n) = n · (c1 + c2 + c3 + c4 + c7 ) − (c2 + c3 + c4 + c7 ) = clin n + crest Das heiÿt, die Laufzeit nimmt linear mit der Inputgröÿe zu. 1.2.2 Schlechtester Fall (worst case) i-te ti = i Der schlechteste (langsamste) Fall liegt vor, wenn der Algorithmus das 2 Element um i−1 ∀i = 2, 3, ..n. Die Laufzeit beträgt: Plätze nach vorne verschieben muss . Das heiÿt: T (n) = c1 n+c2 (n−1)+c3 (n−1)+c4 · n X i+c5 · i=2 Mit Pn i=2 i= n·(n+1) 2 −1 und Pn i=2 (i − 1) n X (i−1)+c6 · i=2 = n X (i−1)+c7 (n−1) i=2 n·(n−1) erhält man folgende Form: 2 T (n) = cquad n2 + clin n + crest Das heiÿt, die Laufzeit nimmt quadratisch mit der Inputgröÿe zu. 1.2.3 Durchschnittlicher Fall (average case) n Zahlen sind zufällig verteilt (gleichverteilt). Im Durchschnitt ist die Hälfte A[1..i − 1] gröÿer als das i-te Element. Das heiÿt ti ≈ 2i . Mit Pn Pn i n·(n+1) 1 1 erhält man wieder eine quadratische i=2 ( 2 ) = 2 i=2 (i) = − 2 + 4 Alle der Elemente in Abhängigkeit der Laufzeit von der Inputgröÿe. 2 Die Eingabefolge ist umgekehrt (absteigend) sortiert! 7 2 Asymptotische Schranken Sowohl die Laufzeit T (n) asymptotische Schranken ci , Die Konstanten als auch der Speicherbedarf S(n) werden meist durch angegeben. welche in der Einführung deniert wurden, sind direkt von der Implementation (Taktfrequenz, Hardware allgemein,....) abhängig. Man will aber ein davon unabhängiges Kriterium nden, um Algorithmen objektiv un- 3 tereinander zu vergleichen . Die Grundidee ist, dass bei einem immer gröÿer werdenden Input n die Kon- stanten vernachlässigbar werden. Weiters können Terme mit niederer Ordnung als verschwindend angesehen werden. T (n) = c1 n3 + c2 n2 + c3 n + c4 kann man, Konstanten c1 , c2 und c3 als auch die Terme Zum Beispiel mit einer Laufzeit von bei sehr groÿem c2 n2 + c3 n + c4 n , sowohl die vernachlässigen. Wenn man das ganze in mathematische Denitionen verfasst, erhält man die sogenannte 2.1 O-, Θ- und Ω-Notation. O-Notation (asymptotisch obere Schranke) f (n), g(n) : N → R+ . Eine Funktion g(n) ist eine asyptotisch obere Schranke für f (n), wenn eine positive Konstante c und eine natürliche Zahl n0 existieren, sodass ab diesem n0 die Funktion f (n) für alle n > n0 kleiner als c · g(n) bleibt. Wir betrachten grundsätzlich Funktionen f (n), g(n) : N → R+ f (n) = O (g(n)) ⇔ ∃c ∈ R, c > 0, n0 ∈ N : f (n) ≤ c · g(n) 3 Deshalb ∀n ≥ n0 benutzt man auch Pseudocode, um den Algorithmus zu analysieren. 8 (Ohh) 2.2 Ω-Notation (asymptotisch untere Schranke) g(n) ist eine asyptotisch untere Schranke für f (n), c und eine natürliche Zahl n0 existieren, sodass Funktion f (n) für alle n > n0 gröÿer als c · g(n) bleibt. Eine Funktion wenn eine positive Konstante ab diesem n0 die f (n), g(n) : N → R+ f (n) = Ω (g(n)) ⇔ ∃c ∈ R, c > 0, n0 ∈ N : f (n) ≥ c · g(n) 2.3 Θ-Notation Eine Funktion g(n) positive Konstanten diesem n0 ∀n ≥ n0 (Omega) (asymptotisch exakte Schranke) f (n), wenn zwei c2 und eine natürliche Zahl n0 existieren, sodass ab f (n) für alle n > n0 zwischen c1 · g(n) und c2 · g(n) ist eine asyptotisch exakte Schranke für c1 und die Funktion bleibt. f (n), g(n) : N → R+ f (n) = Θ (g(n)) ⇔ ∃c1 , c2 ∈ R, c1 , c2 > 0, n0 ∈ N : c1 · g(n) ≤ f (n) ≤ c2 · g(n) (Theta) 9 ∀n ≥ n0 2.4 Grenzwertkriterium Das Grenzwertkriterium ist ein alternatives und in vielen Fällen hilfreiches f (n) Kriterium, um zu bestimmen, ob eine Funktion zu einer Funktionsklasse f (n) g(n) existiert, kann anhand dieses Wertes die Funktionsklasse festgestellt werden: O(g(n)), Ω(g(n)) oder Θ(g(n)) gehört. Falls der Grenzwert lim n→∞ limn→∞ f (n) =K g(n) • 0 ≤ K < ∞: f (n) = O(g(n)) • 0 < K ≤ ∞: f (n) = Ω(g(n)) • 0 < K < ∞: f (n) = Θ(g(n)) f (n) = | sin n|, (n) existiert limn→∞ fg(n) Beachte, dass die Umkehrung nicht unbedingt gilt, z.B., g(n) = 1. Es gilt f (n) = O(g(n)) aber der Grenzwert nicht. 2.5 1. Beispiele 5n + 3 = O(n) zu zeigen: ∃c > 0, n0 ∈ N : wähle c = 6, n0 = 3: 5n + 3 ≤ c · n ∀n ≥ n0 5n + 3 ≤ 6n für 3≤n 2. für n≥3 6n3 6= O(n2 ) 3 2 Annahme: 6n = O(n ) ⇔ ∃c > 0, n0 ∈ N : 6n3 ≤ c · n2 6n ≤ c c n≤ 6 für 6n3 ≤ c · n2 ∀n ≥ n0 n ≥ n0 n ≥ n0 für für nicht möglich für beliebige Wahl von 3. n≥3 n ≥ n0 c, n0 ; daher 6n3 6= O(n2 ). n3 = O(2n ) n3 ∞ 3n2 6n 6 = = lim = lim n = lim n = 0. n 2 n→∞ 2n n→∞ n→∞ n→∞ ∞ 2 ln 2 2 (ln 2) 2 (ln 2)3 lim (Regel von L'Hospital) Exponentielle Funktionen (Basis > 1) Funktionen. 10 wachsen schneller als polynomielle 4. √ ln n = O( n) ln n ∞ lim √ = = lim n→∞ ∞ n→∞ n 1 n 1 √ 2 n 2 = lim √ = 0. n→∞ n Polynomielle Funktionen wachsen schneller als logarithmische. 5. n 2 = Θ(n2 ) (1) ∃c1 > 0, c2 > 0, n0 ∈ N : c1 = 41 , c2 = 12 , n0 = 2: zu zeigen: wähle (1) : c1 n2 ≤ n(n−1) 2 (2) ≤ c2 n2 ∀n ≥ n0 n(n − 1) 1 2 n ≤ 4 2 1 2 2n2 2n n ≤ − 4 4 4 n ≤ 2n − 2 2 ≤ n für n ≥ 2 n ⇒ = Ω(n2 ) 2 n(n − 1) 1 ≤ n2 2 2 n2 n n2 − ≤ 2 2 2 −n ≤ 0 n ≥ 0 für n ≥ 2 n = O(n2 ) ⇒ 2 n n 2 2 2 = O(n ) folgt 2 = Θ(n ). (2) : n 2 2 = Ω(n ) und alternative Methode: aus n lim 22 n→∞ n Da 2.6 0< 1 2 <∞ = lim n→∞ n gilt 2 1− n(n − 1) n2 − n = lim = lim n→∞ 2n2 n→∞ 2n2 2 1 n = Θ(n2 ). Rechen-Regeln für die O-Notation O (f (n)) + O (g(n)) = O (max {f (n), g(n)}) O (f (n)) · O (g(n)) = O (f (n) · g(n)) f (n) = O (g(n)) ∧ g(n) = O (h(n)) ⇒ f (n) = O (h(n)) O (f (n)) − O (f (n)) = O (f (n)) 11 = 1 . 2 Beweis der 1. Regel: O (f (n)) + O (g(n)) = O (max {f (n), g(n)}) f ∗ (n) = O(f (n)) und g ∗ (n) = Wir müssen zeigen, dass für alle Funktionen O(g(n)) gilt, dass f ∗ (n) + g ∗ (n) = O(max{f (n), g(n)}). Aus der Denition der O-Notation folgt f ∗ (n) = O(f (n)) ⇔ ∃c1 > 0, n01 ∈ N : f ∗ (n) ≤ c1 f (n) ∀n ≥ n01 g ∗ (n) = O(g(n)) ⇔ ∃c2 > 0, n02 ∈ N : g ∗ (n) ≤ c2 g(n) ∀n ≥ n02 und daher f ∗ (n) + g ∗ (n) ≤ c1 f (n) + c2 g(n) ∀n ≥ max{n01 , n02 } =: n0 ≤ c1 max{f (n), g(n)} + c2 max{f (n), g(n)} = (c1 + c2 ) max{f (n), g(n)}. | {z } =:c3 Das heiÿt, es existiert ein c3 > 0 und ein n0 ∈ N, sodass f ∗ (n) + g ∗ (n) ≤ c3 max{f (n), g(n)}) ∀n ≥ n0 ⇒ f ∗ (n) + g ∗ (n) = O(max{f (n), g(n)}). 2.7 Häug vorkommende Funktionen Name O-Notation konstant O(1) O(log n) O(nc ),0 < c < 1 O(n) O(n · log n) O(nc ), c > 1 O(cn ), c > 1 O(n!) logarithmisch Wurzelfunktion linear linearlogarithmisch polynominal exponential Fakultät 2.8 Bsp.-Funktion Bsp.-Algorithmus 3 Addition ld (n), ln (n) √ Suchen 1 n, n 3 3n + 4 2n · ld(n) 4n2 + 7n + 10 2n , 10n n! Primzahltest Maximum nden Sortieren Matrizenoperationen NP-vollständige Alg. Anmerkung zum Logarithmus Es kommt sehr häug vor, dass bei der Laufzeitanalyse der Logarithmus zur Basis 2 verwendet wird. Es lässt sich aber jeder Logarithmus mit einer beliebigen Basis b in einen anderen Logarithmus mit der Basis b0 folgendermaÿen umrechnen: logb0 (x) = 1 · logb (x) logb (b0 ) Die beiden Versionen unterscheiden sich nur durch einen konstanten Faktor, der natürlich asymptotisch gesehen vernachlässigbar wird. Das heiÿt: O(logb0 n). 12 O(logb n) = 3 Elementare Datenstrukturen 3.1 Lineares Feld Benachbarte Elemente stehen im Speicher direkt nebeneinander. Der Zugri auf das i-te Element erfolgt in konstanter Zeit: A[i] in angegeben, nehmen wir hier in der Vorlesung die Gröÿe O(1). Falls nicht anders n des Feldes als gegeben an. 3.2 Lineare Liste Benachbarte Elemente sind miteinander verkettet. Die Datenstruktur eignet sich besonders, wenn die Länge in vorhinein nicht bekannt ist. Die Verkettung erfolgt durch Pointer auf die Nachfolger. Es existieren die Funktionen NACH(p), VOR(p) und WERT(p) um auf das nachfolgende, das vorher- gehende Elemente oder den Wert direkt zuzugreifen. Weiters gibt es die Möglichkeit die Liste doppelt zu verketten. 3.3 Stapel (Stack) Der Stapel ist ein spezielles lineares Feld, bei dem das Einfügen und Entfernen nur an der Spitze erlaubt ist. Dadurch wird die LIFO-Strategie (last-in, rst-out) implementiert. Die Funktionen STAPEL_LEER, PUSH und POP werden verwendet um zu überprüfen, ob der Stapel leer ist, um ein Element auf den Stapel zu werfen bzw. vom Stapel S zu entfernen. 13 STAPEL_LEER(S) 1: IF t=0 2: RETURN TRUE 3: ELSE FALSE PUSH (S,x) 1: IF t=n THEN 'overflow' 2: ELSE 3: t←t+1 4: S[t]←x POP (S) 1: IF STACK_LEER THEN 'underflow' 2: ELSE 3: x←S[t] 4: t←t-1 Alle Operationen benötigen nur O(1) Zeit! Anwendung: Abarbeiten von Rekursionen, umkehren einer Reihenfolge, Umwandlung von Inx-Ausdrücken in Postx-Ausdrücken,... 3.4 Schlange (Queue) Implementiert die FIFO-Strategie (rst-in, rst-out). Die Schlange die Funktionen PUT und Q verwendet GET, um Werte in die Schlange zu schreiben und sie wieder auszulesen. PUT (Q,x) 1: IF anzahl=n THEN 'overflow' 2: ELSE 3: anzahl←anzahl+1 4: IF ende<n THEN ende←ende+1 5: ELSE ende←1 6: Q[ende]←x GET (Q) 1: IF anzahl=0 THEN 'underflow' 2: ELSE 3: anzahl←anzahl-1 4: x←Q[anfang] 5: IF anfang<n THEN anfang←anfang+1 6: ELSE anfang←1 7: RETURN x Alle Operationen benötigen O(1) Zeit! 14 4 Rekursionen Viele Algorithmen besitzen sowohl eine iterative als auch eine rekursive Lösung. Sie unterscheiden sich darin, dass die iterative Version meist einen etwas längeren Kode besitzt, während die rekursive Version mehr Speicher benötigt, um alle Rekursionsschritte zwischenzuspeichern. 4.1 Erstes Beispiel Der Algorithmus soll die Fakultät n! berechnen. 4.1.1 Iterative Version FAKULTAET_ITERATIV(n) 1: f←1 2: FOR i←1 to n 3: f←f*i 4: RETURN f Die Laufzeit beträgt T (n) = O(n), da nur eine Schleife durchlaufen werden O(1) unabhängig von der Inputgröÿe. muss. Der Speicherbedarf ist mit 4.1.2 Rekursive Version FAKULTAET_REKURSIV(n) 1: IF n=1 THEN 2: RETURN 1 3: ELSE 4: RETURN (n*FAKULTAET_REKURSIV(n-1)) In Zeile 1 bendet sich die Abbruchbedingung für die Rekursion. Zur Analyse benötigt man die Laufzeit für den Fall, wenn abgebrochen wird. Hier benötigt der Algorithmus im Abbruchfall n=1 eine Laufzeit von O(1). Im Normalfall (während der Rekursionen) kann man folgende Rekursionsgleichung aufstellen: T (n) = O(1) + T (n − 1) | {z } | {z } 1 2 1. Beinhaltet die konstante Zeit, die die Zeilen 1-3 benötigen 2. Ist die Laufzeit um ein Element verringert, da FAKULTAET_REKURSIV mit (n-1) aufgerufen wird. Die Inputgröÿe ist hier die Zahl n selbst, von der die Fakultät n! berechnet werS(n) = O(1) + S(n − 1). Der den soll. Für den Speicherverbrauch gilt ebenfalls Speicherverbrauch von rekursiven Algorithmen hängt im Allgemeinen von der maximalen Rekursionstiefe ab (= Anzahl der gleichzeitig oenen Funktionsaufrufe). 15 4.2 Methoden zur Lösung von Rekursionsgleichungen 4.2.1 Lösen durch Einsetzen Es wird ein Rekursionsschritt tiefer T (n − 1) = O(1) + T (n − 2) in die Orginalgleichung T (n) = O(1) + T (n − 1) eingesetzt: T (n) = O(1) + O(1) + T (n − 2) Analog erhält man durch weiteres Einsetzen eine allgemeine Formel in Abhängigkeit der Rekursionstiefe T (n) k = O(1) + O(1) + T (n − 2) = O(1) + O(1) + O(1) + T (n − 3) = ... = k · O(1) + T (n − k) Die Rekursion verläuft bis zur Abbruchbedingung n = 1, dass heiÿt, bis T (1) k . Mit T (1) = T (n − k) auftritt. Man will nun eine Lösung unabhängig von bekommt man n−k =1⇒k =n−1 T (n) und setzt in die Gleichung ein: = (n − 1) · O(1) + T (n − n + 1) = (n − 1) · O(1) + T (1) = (n − 1) · O(1) + O(1) T (n) = O(n) Für den Speicherverbrauch S(n) = O(1) + S(n − 1) folgt analog S(n) = O(n). 4.2.2 Lösen durch Induktion Zur Lösen der Rekursionsgleichung durch Induktion muss zuerst eine Lösung geschätzt und diese Annahme durch Induktion bewiesen werden. 4.3 Zweites Beispiel - Fibonacci-Zahlen Die Denition der Fibonacci-Zahlen: fn = fn−1 + fn−2 ; n ≥ 3 ; f1 = 1, f2 = 1 Die ersten Zahlen lauten 1, 1, 2, 3, 5, 8, 13, 21, ... Die Folge ist monton wachsend und besitzt folgende Schranken: fn = fn−1 + fn−2 = fn−2 + fn−3 +fn−2 | {z } ≤fn−2 16 ≥ 2 · fn−2 ⇒ ≤ 3 · fn−2 ⇒ n fn = Ω(2 2 ) n fn = O(3 2 ) Mit den Anfangswerten 2 n−1 2 ≤ fn ≤ 3 f1 = f2 = 1 n−1 2 folgt: √ √ ⇔ c1 · ( 2)n ≤ fn ≤ c2 · ( 3)n ⇒ fn = Θ (cn ) 4.3.1 Iterative Version Es wird die n-te Fibonacci-Zahl gesucht: FIBONACCI(n) 1: fib ← 1 2: fib_1 ← 1 3: FOR i ← 3 to n 4: fib_2 ← fib_1 5: fib_1 ← fib 6: fib ← fib_1 + fib_2 7: RETURN fib 4.3.2 Rekursive Version Es wird die n-te Fibonacci-Zahl gesucht: FIBONACCI_R(n) 1: IF n≤2 THEN 2: RETURN 1 3: ELSE 4: RETURN (FIBONACCI_R(n-1)+FIBONACCI_R(n-2)) Die Rekursiongleichung lautet T (1) = O(1). T (n) = O(1) + T (n − 1) + T (n − 2) mit T (2) = Analog zu den Fibonacci-Zahlen kann folgende Abschätzung ge- macht werden: T (n) = O(1) + T (n − 1) + T (n − 2) = 2O(1) + 2T (n − 2) + T (n − 3) | {z } ≤T (n−2) und daher T (n) = Die Lösung ist ≥ 2T (n − 2) ⇒ ≤ 3T (n − 2) ⇒ n T (n) = Ω(2 2 ) n T (n) = O(3 2 ) T (n) = Θ(cn ). Der Speicheraufwand beträgt S(n) = O(1) + max{S(n − 1), S(n − 2)} = O(1) + S(n − 1) = O(n). Die rekursive Version ist einfacher zu implementieren, besitzt jedoch exponentielle Laufzeit und linearen Speicherbedarf. 17 4.4 Die Ackermann-Funktion Es gibt zahlreiche Arten dieser rekursiven Funktion. Zum Beispiel: ACKER(n,x,y) 1: IF n=0 THEN 2: RETURN x+1 3: ELSE IF y=0 THEN 4: IF n=1 THEN RETURN x 5: IF n=2 THEN RETURN 0 6: IF n=3 THEN RETURN 1 7: IF n≥4 THEN RETURN 2 8: RETURN ACKER(n-1, ACKER(n,x,y-1),x) 4.5 Türme von Hanoi Regeln: • 3 Stäbe A, B und C • Am Stab A liegen zu Beginn • Diese sollen am Ende auf Stab B liegen • Es darf jeweils nur eine Scheibe bewegt werden • Es darf nie eine gröÿere auf einer kleineren Scheibe liegen n unterschiedliche groÿe Scheiben Zum Beispiel mit 7 Scheiben: Rekursiver Algorithmus zur Lösung: HANOI(n,x,y,z) 1: IF n=1 THEN 2: Lege Scheibe von 'x' nach 'y' 3: ELSE 4: HANOI(n-1,x,z,y) 5: HANOI(1,x,y,z) 6: HANOI(n-1,z,y,x) 18 Der Aufruf erfolgt zum Beispiel mit HANOI (7,'A','B','C') und fordert den Al- gorithmus auf die 7 obersten Scheiben von A auf B unter Verwendung von C zu legen. Für die Anzahl der Züge Z(n) (n ist hier die Anzahl der Scheiben) gilt mit Z(1) = 1 Z(n) = Z(n − 1) + 1 + Z(n − 1) = 1 + 2 · Z(n − 1) = 1 + 2 [1 + 2 · Z(n − 2)] = 1 + 2 + 4 · Z(n − 2) = 1 + 2 + 4 + 8 · Z(n − 2) = ··· = k−1 X 2i + 2k · Z(n − k) i=0 Die Abbruchbedingung ist erreicht, wenn das Argument k = n − 1. n−k = 1 ist, also Damit ist Z(n) = n−2 X 2i + 2n−1 · 1 = n−1 X i=0 Für die Laufzeit gilt mit 2i = 2n − 1 = O(2n ). i=0 T (1) = O(1) analog T (n) = 1 + 2 · T (n − 1) = . . . = O(2n ). In diesem Beispiel ist die exponentielle Laufzeit des rekursiven Algorithmus optimal. Für den Speicherverbrauch gilt mit S(1) = O(1) S(n) = O(1) + max{S(n − 1), S(1), S(n − 1)} = O(1) + S(n − 1) = O(n). 19 5 Sortieren durch Verschmelzen - Mergesort Das Sortierverfahren arbeitet auf der Basis der gehend von einem linearen Feld 1. 2. 3. A der Gröÿe divide&conquer -Strategie. Aus- n: Teile das Feld in 2 gleich groÿe Hälften Sortiere die Teilfelder rekursiv mit demselben Verfahren Verschmelze die sortierten Teilfelder zu einem sortierten Gesamtfeld Der Pseudocode dazu (wobei die Indizes i und j jeweils auf die Grenzen des aktuellen Teilfeldes verweisen): MERGESORT(A,i,j) 1: IF i<j THEN 2: k ←b(i+j)/2c 3: MERGESORT (A,i,k) 4: MERGESORT (A,k+1,j) 5: VERSCHMELZE (A,i,k,j) Der Aufruf erfolgt mit MERGESORT(A,1,n). Der Verschmelzungsprozess verwendet Hilfsfelder tierten Teilfolgen A[i..k] 4 und A[k + 1..j] B und C, um die bereits sor- zu einer gesamten sortierten Teilfolge zusammenzufügen : VERSCHMELZE(A,i,k,j) 1: FOR l←i TO k DO B[l]←A[l] 2: FOR r←k+1 TO j DO C[r]←A[r] 3: B[k+1]← ∞ 4: C[j+1]← ∞ 5: l←i, r←k+1 6: FOR m←i TO j 7: IF B[l]<C[r] THEN 8: A[m]←B[l] , l←l+1 9: ELSE A[m]←C[r] , r←r+1 4∞ wird in der Praxis mit der gröÿten darstellbaren Zahl implementiert. 20 T (n) = O(j−i+1) = O(n). MERGESORT : T (n) = T (n) = O(n · log n) ist: Der Verschmelzungsprozess besitzt eine Laufzeit von Daraus ergibt sich folgende Rekursionsgleichung für 2T ( n2 ) + O(n), T (1) = O(1), deren Lösung n T (n) = 2 · T ( ) + O(n) h 2 n n i = 2 · 2 · T ( ) + O( ) + O(n) 4 2 n n = 4 · T ( ) + 2 · O( ) + O(n) 4 2 n = 4 · T ( ) + 2 · O(n) h 4 n n i = 4 · 2 · T ( ) + O( ) + 2 · O(n) 8 4 n = 8 · T ( ) + 3 · O(n) 8 n k = 2 · T ( k ) + k · O(n) 2 Die Abbruchbedingung ist erreicht, wenn das Argument n 2k = 1, also k = ldn. T (n) = 2ldn · T (1) + ldn · O(n) = n · O(1) + O(n log n) = O(n log n). Für den Speicherverbrauch ergibt sich n S(1) = O(1) S(n) = S( ) + O(n) 2 n n = S( ) + O( ) + O(n) 4 2 n n n = S( ) + O( ) + O( ) + O(n) 8 4 2 k−1 X 1 n = S( k ) + O(n) · i 2 2 i=0 n = 1 ⇒ k = ldn 2k ldX n−1 1 = O(1) + O(n) · i 2 i=0 | {z } P < ∞ i=0 1 2i =2 = O(n). In diesem Fall dominiert also der Speicher der Hilfsfelder B und C (O(n)) den Speicherverbrauch, der sich aus der Rekursionstiefe ergeben würde (O(log n)). 21 Ein Beispiel mit der Zahlenfolge < 5, 2, 4, 6, 1, 3 > baum. 22 ergibt folgenden Rekursions- 6 Halde (Heap) Die Halde ist eine lineares Feld, welches die sogenannte Haldenbedingung: Haldenbedinung A[i]≥ max {A[2i], A[2i + 1]}, ∀i = 1, 2, . . . Direkt aus der Denition ergibt sich, dass erfüllt: n 2 . A[1] das Maximum des linearen Feldes ist. Beispiel einer Halde: Die Halde lässt sich auch als binärer Baum darstellen. Mit den Funktionen LINKS(i) und RECHTS(i) kann auf den linken bzw. rech- ten Nachfolger des i-ten Elements in der Baumstruktur zugegrien werden, analog mit VATER(i) auf den Vorgänger. LINKS (i) 1: RETURN 2i RECHTS(i) 1: RETURN 2i + 1 VATER (i) 1: RETURN 2i Wichtige Beobachtungen: • Das Maximum sitzt in der Wurzel des Baums • In jedem Knoten ist die Haldenbedingung erfüllt • Die Maximale Tiefe des Baums ist • In einem Teilbaum sind alle darunter liegenden Knoten kleiner oder maximal gleich groÿ. 23 blog nc Beweis für die Höhe des Haldenbaums: des Niveau bis auf das letzte voll (es gibt 2i In jedem Haldenbaum ist je- Elemente der Höhe i), das letzte Niveau enthält mindestens 1 Element. Für die Anzahl der Elemente in einem Haldenbaum der Höhe h gilt also n ≥ 1 + 2 + 4 + . . . + 2h−1 + 1 = h−1 X 2i + 1 = (2h − 1) + 1 = 2h , i=1 Für die Höhe 6.1 h gilt daher h ≤ bldnc (h ganzzahlig). Verhalden (Heapify) Verhalden ist der zentrale Prozess für alle Anwendungen der Haldenstruktur. Mit n als Gröÿe der Halde (Die Länge des linaren Feldes). VERHALDE (A,i) 1: l←LINKS(i), r←RECHTS(i) 2: index ← i 3: IF l≤n AND A[l]>A[i] THEN index←l 4: IF r≤n AND A[r]>A[index] THEN index←r 5: IF i6=index THEN 6: VERTAUSCHE (A[i], A[index]) 7: VERHALDE (A,index) Analyse: Der rekursive Aufruf erfolgt auf einem Teilbaum, der einen der beiden i als Wurzel hat. Dieser Teilbaum hat maximal halb so viele Elemente i. Die Rekursiongleichung lautet daher T (n) ≤ O(1) + T ( n2 ). Die Lösung ist T (n) = O(log n): Söhne von wie der Teilbaum mit Wurzel n T (n) ≤ O(1) + T ( ) h 2 n i ≤ O(1) + O(1) + T ( ) 4 n = 2 · O(1) + T ( ) 4 n ≤ 3 · O(1) + T ( ) 8 n ≤ k · O(1) + T ( k ) 2 = ldn · O(1) + O(1) = O(log n) Diese Schranke folgt auch aus der Beobachtung, dass maximal die Höhe des Haldenbaums, 6.2 h ≤ bldnc, durchlaufen wird. Aufbau einer Halde Eine Halde kann aus einem beliebigen linearen Feld 24 A aufgebaut werden: BAUE_HALDE (A) 1: FOR i←n/2 DOWNTO 1 2: VERHALDE (A,i) Analyse: n 2 -mal durchgeführt. D.h. T (n) = O(n·log n). Bei genauerer Betrachtung sieht man aber, dass eben nicht alle Elemente durch Das Verhalden wird die ganze Baumhöhe durch verhaldet werden müssen. Die Schranke O(n · log n) ist nicht scharf. Ein genauerer Ansatz ergibt sich aus der Beobachtung, dass es in einer Halde n ≤ d h+1 e Elemente gibt, die mit Höhe h verhaldet werden Pbldnc2 n Pbldnc h müssen: T (n) ≤ ) = O(n) . h=0 d 2h+1 eO(h) = O(n h=0 2h P ∞ h (Die Summe kann durch eine unendliche Summe h=0 2h = 2 abgeschätzt n mit Elementen werden.) Damit kann eine Halde in linearer Zeit aus einem linearen Feld gebaut werden. 6.3 Sortieren mit Halden (Heap Sort) Mit Hilfe des VERHALDE -Algorithmus kann man auch einen Sortieralgoritmus konstruieren. HEAP_SORT(A) 1: BAUE_HALDE(A) 2: FOR i ← n DOWNTO 2 DO 3: VERTAUSCHE (A[1],A[i]) 4: n ← n-1 5: VERHALDE (A,1) O(n), die von VERHALDE O(log n). Die T (n) = O(n) + (n − 1) · O(log n) = O(n log n). Der Die Laufzeit von BAUE_HALDE ist Laufzeit beträgt daher Algorithmus arbeitet 6.4 in-place. Wartschlange (Priority Queue) Eine weitere Anwendung dieser Datenstruktur ist die Warteschlange mit Prioritäten: Eine Warteschlange liegt vor, wenn folgende drei Operationen durchgeführt werden können: • EINFUEGEN (A,x) • MAX (A) • ENTFERNE_MAX (A) Die Funktionen mit Hilfe einer Halde umgesetzt in Pseudocode: MAXIMUM (A) 1: RETURN A[1] 25 ENTFERNE_MAX (A) 1: A[1] ← A[n] 2: n ← n-1 3: VERHALDE (A,1) EINFUEGEN (A,x) 1: n←n+1 , A[n]←x , i←n 2: WHILE i>1 AND A[i]>A[VATER(i)] DO 3: VERTAUSCHE (A[i],A[VATER(i)]) 4: i←Vater(i) Die Laufzeiten betragen für MAXIMUM, ENTFERNE_MAX und EINFUEGEN respektiv O(1), O(log n) und O(log n). Mittels Einfügen kann eine Halde natürlich auch aufgebaut werden. Diese Variante ist jedoch inezient. BAUE_HALDE_LANGSAM(A) 1: N ← 1 2: FOR i=2 TO n DO 3: EINFUEGEN (A,A[i]) Die Laufzeit beträgt n X T (n) = O ! = O(n log n). ldi i=2 | {z } <(n−1)·ldn Diese asymptotische Schranke ist schon scharf, da n X i=2 n X ldi > i= n 2 Die Laufzeit ist also mit das eine Halde in ldi > n n n n ld = ldn − = Ω(n log n). 2 2 2 2 T (n) = Θ(n log n) deutlich langsamer als BAUE_HALDE, linearer Zeit bilden kann. 26 7 Quicksort Quicksort ist einer der meist verwendeten Sortieralgorithmen. Die Idee beruht auf dem divide&conquer -Prinzip5 . Der Kern des Algorithmus ist die Zerlegung des Feldes (Partition). Zerlegen (Partition): Speichere das lineare Feld A so um, dass alle Elemente in A[1..k] 7.1 kleiner als alle Elemente in A[k + 1..n] sind. Partition Das Teilfeld ausgehend vom linken (unteren) Index Index r wird partitioniert. A l bis zum rechten (oberen) bezeichnet das bearbeitete lineare Feld. Als Be- zugswert (Pivotelement) nimmt man etwa das erste Feldelement. PARTITION (A,l,r) 1: p ← A[l] 2: j←l-1 , k←r+1 3: LOOP 4: REPEAT j←j+1 UNTIL A[j]≥p 5: REPEAT k←k-1 UNTIL A[k]≤p 6: IF j<k THEN 7: VERTAUSCHE (A[j],A[k]) 8: ELSE 9: RETURN k Die Laufzeit beträgt O(n), wobei n = r−l die Anzahl der betrachteten Elemente ist. 7.2 Quicksort Auf der Basis von PARTITION kann der Sortieralgorithmus QUICKSORT struiert werden. QUICKSORT (A,l,r) 1: IF l<r THEN 2: k ← PARTITION(A,l,r) 3: QUICKSORT (A,l,k) 4: QUICKSORT (A,k+1,r) 5 Dabei wird das Problem in kleinere Teilprobleme zerlegt und rekursiv aufgelöst. 27 kon- Die Rekusionsgleichung dazu lautet T (n) = T (k) + T (n − k) + O(n) Im besten Fall (balancierte Aufteilung des Rekursionsbaums; k = n/2 in jedem Rekursionsschritt) beträgt die Laufzeit n n T (n) = T ( ) + T ( ) + O(n) 2 2 n = 2 · T ( ) + O(n) h 2 n n i = 2 2 · T ( ) + O( ) + O(n) 4 2 n = 4 · T ( ) + 2 · O(n) 4 n = 8 · T ( ) + 3 · O(n) 8 n k = 2 · T ( k ) + k · O(n) 2 k=ldn = n · O(1) + ldn · O(n) = O(n log n). In diesem Fall ergibt sich dieselbe Rekursionsgleichung wie bei Mergesort. Im schlechtesten Fall (das Feld ist genau umgekehrt sortiert der Rekursionsbaum ist entartet; k=1 in jedem Rekursionsschritt) beträgt die Laufzeit T (n) = T (1) + T (n − 1) + O(n) = T (n − 1) + O(n) + O(1) = T (n − 2) + O(n) + O(n − 1) + 2 · O(1) = T (n − 3) + O(n) + O(n − 1) + O(n − 2) + 3 · O(1) = T (n − k) + k−1 X O(n − i) + k · O(1) i=0 k=n−1 = O(1) + n−2 X O(n − i) + (n − 1) · O(1) i=0 = n · O(1) + O = n · O(1) + O n−2 X i=0 n X ! (n − i) ! i−1 i=1 = O(n) + O 7.3 n(n + 1) −1 2 = O(n) + O(n2 ) = O(n2 ). Randomisierter Quicksort Durch eine randomisierte Version von Quicksort kann man eine gute durchschnittliche Performance erreichen. Das Pivotelement wird dabei zufällig mit 28 6 einer Gleichverteilung aus allen möglichen Werten ausgewählt . Es werden dazu ein paar Zeilen (jetzt 1 und 2) eingefügt. Die Funktion RANDOM(l,r) einen zufälligen (Gleichverteilung) Integerwert im Bereich l bis liefert r. RANDOM_PARTITION (A,l,r) 1: ind ← RANDOM(l,r) 2: VERTAUSCHE(A[ind],A[l]) 3: p ← A[l] 4: j ← .... 5: . 6: . Die erwartete Laufzeit beträgt T (n) = O(n · log n). O(n2 ) kann natürlich Bemerkung: Der schlechteste Fall Herleitung der average case Laufzeit: T (n) = n−1 X immer noch auftreten! Für die erwartete Laufzeit gilt P (k) [T (k) + T (n − k) + O(n)] , k=1 P (k) wobei die Wahrscheinlichkeit ist, dass PARTITION(A,1,n) den Index liefert. Wir nehmen eine Gleichverteilung T (n) = n−1 X k=1 = P (k) = 1 n−1 an (k k = 1, . . . , n − 1). 1 [T (k) + T (n − k) + O(n)] n−1 n−1 1 X n−1 O(n) [T (k) + T (n − k)] + n−1 n−1 k=1 = 2 n−1 n−1 X T (k) + O(n). k=1 Wir zeigen nun mit vollständiger Induktion, dass T (n) = O(n log n) ⇒ ∃c > 0 : T (n) ≤ c · n log n. • Induktionsanfang n = 2: T (2) ≤ 2 · T (1) + c1 · 2 ≤ 2 · c2 + c1 · 2 ≤ c · 2 log 2 Für jeden Wert von c1 und c2 gibt es eine Konstante c, sodass die Unglei- chung erfüllt ist. 6 Der mit 1 n! worst-case, dass bei jedem Rekursionschritt der schlimmste Fall auftritt, wird dann extrem unwahrscheinlich ! 29 • zu zeigen: wenn ∃c > 0 : T (k) ≤ c · k log k ∃c > 0 : T (n) ≤ c · n log n: für k = 1, . . . , n − 1, dann n−1 2 X T (n) = T (k) + O(n) n−1 k=1 ≤ = 2 n−1 n−1 X c · k log k + O(n) k=1 n−1 2c X k log k + O(n) n−1 k=1 Pn−1 Wir schätzen nun die Summe n−1 X k log k k=1 dn 2 e−1 k log k = k=1 X k log k + | {z } k=1 ≤log n 2 ab: n−1 X dn 2 e−1 ≤ X = log n ≤log n n−1 X k(log n − 1) + k log n k=d n 2e k=1 = log n k log k | {z } k=d n 2e dn 2 e−1 dn 2 e−1 X X k− k + log n k=1 n−1 X dn 2 e−1 X k=1 n(n−1) 2 ≤ (log n) k k=1 | {z } = k k=d n 2e k=1 k − n−1 X | {z } ≥ n ( n −1) 2 2 2 n(n − 1) 2 − n n 2(2 − 1) 2 Diese Abschätzung können wir in die ursprüngliche Formel einsetzen: n n 2c n(n − 1) 2 ( 2 − 1) T (n) ≤ (log n) − + O(n) n−1 2 2 n 1 − + O(n) = c · n log n − c | {z } 4 2 ≤d·n ≤ c · n log n − c n 1 − 4 2 +d·n ≤ c · n log n. Im letzten Schritt können wir für jedes beliebige sodass c n 4 − 1 2 d eine Konstante c wählen, ≥ d · n. Im mittleren Fall ist die Laufzeit also T (n) = O(n · log n). 30 7.4 Quicksort (iterativ) Der Algorithmus steigt immer in die die gröÿere Hälfte auf einem Stack kleinere S7. der beiden Hälften ein und stapelt QUICKSORT_ITERATIV (A) 1: l=1 , r=n 2: PUSH(1,n) 3: REPEAT 4: IF l<r THEN 5: k = PARTITION(A,l,r) 6: IF (k-l) < (r-k) THEN 7: PUSH(k+1,r) 8: r = k 9: ELSE PUSH(l,k) 10: l = k+1 11: ELSE POP(l,r) 12: UNTIL STAPEL_LEER(S) Der Speicherbedarf mehr S(n) beträgt im Gegensatz zum rekursiven Algorithmus nur O(log n). 7 PUSH(a,b) entspricht dabei der Funktion PUSH(S,x={a,b}), welche bei den elementaren Datenstrukturen deniert worden ist. Analoges gilt für POP(l,k), wobei auch gleichzeitig die vom Stapel geholten Werte die aktuellen l und k Werte überschreiben. 31 8 Untere Schranke für vergleichende Sortierverfahren Viele verwendete schnelle Sortierverfahren (z.B.: O(n log n). besitzen eine Laufzeit von MERGESORT, HEAPSORT ) Die Frage, die sich dabei stellt, lautet: Geht es besser? Man kann beweisen, dass jedes Sortierverfahren, das mittels Vergleichen arbei- 8 tet , mindestens c · n log n Vergleiche im worst case braucht (wobei c > 0 und konstant ist). 8.1 Herleiten der unteren Schranke Ω (n log n) Der Kontrolluss eines vergleichenden Sortierverfahrens kann mittels eines sogenannten Entscheidungsbaummodells dargestellt werden. Darin scheinen alle möglichen und nötigen Entscheidungen auf, um ein sortiertes lineares Feld zu erhalten. Beispiel: Es liegt eine Sequenz von 3 Zahlen vor < 5, 8, 2 >). < a1 , a2 , a3 > (z.B. Dann schaut der zugehörige Entscheidungsbaum folgendermaÿen aus: Die Blätter stellen alle möglichen Permutationen des Inputs dar. Die Anzahl der Blätter ist daher gleich n!. Das worst-case Verhalten entspricht dem längsten Ast im Entscheidungsbaum (Anzahl der inneren Knoten = Anzahl der Vergleiche). Der ideale Sortieralgorithmus enspricht einem vollständigen, ausgeglichenen Baum. Dadurch wird der längste Ast ( = worst case) minimiert. Die Höhe beträgt der Stirling-Approximation h ≥ log n! > ( ne )n n n e h ≥ log(n!). Mit Hilfe erhält man: = n · log n − n · log e = Ω(n · log n) 8 D.h., bei dem die Information über die korrekte Anordnung der Elemente dadurch gewonnen wird, dass jeweils einzelne Elemente direkt miteinander verglichen werden. 32 Ω(n · log n) ist die untere Schranke für die Anzahl der im worst case zum Sortieren notwendigen Vergleiche (und somit für die worst case Laufzeit vergleichender Sortierverfahren). 8.2 worst-case optimal Im Zusammenhang der unteren Schranke kann einen Sortieralgorithmus als worst-case optimal bezeichnen, wenn er für jede Eingabefolge in O(n · log n) sortiert. Zum Beispiel sind 8.3 MERGESORT und HEAPSORT worst-case optimal. Sortieren durch Fachverteilung (RadixSort) RadixSort ist ein Beispiel für ein nicht vergleichsbasiertes Sortierverfahren. Nicht vergleichsbasierte Sortierverfahren arbeiten ohne Vergleiche und müssen daher zusätzliche Annahmen über den Input machen. Der Input zu RadixSort besteht aus n Dezimalzahlen der Länge d. RADIXSORT (A,d) 1: FOR i = 1 TO d DO 2: Ordne A nach i-ter Ziffer von hinten in Fächer ein 3: Fasse die Fächer in aufsteigender Reihenfolge wieder in A zusammen Zeile 2 wird Streuphase genannt, und Zeile 3 bezeichnet man auch als Sammelphase. RadixSort ist korrekt, weil nach den ersten k Durchläufen die Zahlen, eingeschränkt auf die letzten k Ziern, sortiert sind. Eine wichtige Bedingung für die Korrektheit der Sammelphase ist, dass die vorige Reihenfolge innerhalb der Fächer aufrechterhalten werden muss. Diese Eigenschaft eines Sortieralgorithmus, dass Elemente mit identischen Sortierschlüsseln in Input und Output in gleicher Reihenfolge erscheinen, nennt man Die Laufzeit von RadixSort betragt Stabilität. T (n) = O(d·n), ist also linear, wenn d als konstant betrachtet wird. In diesem Fall ist RadixSort asymptotisch schneller als vergleichsbasierte Sortierverfahren, wie z.B. QuickSort, hat aber einen erhöhten Speicherbedarf. 33 9 Finden der i-kleinsten Zahl Das Finden der i-kleinsten Zahl in einer Inputfolge A[1..n] von n Zahlen kann man bewerkstelligen, indem man zunächst die Zahlen sortiert und dann das Ele- T (n) = O(n log n)+ Ω(n log n) Zeit benötigt. Es ment an der Stelle i, A[i], nimmt. Dieses Verfahren benötigt O(1) = O(n log n) Zeit, da Sortieren mindestens stellt sich die Frage, ob es ein schnelleres Verfahren gibt. 9.1 Minimum und Maximum Im Fall von Minimum (i = 1) und Maximum (i = n) kommen wir mit n−1 Vergleichen aus, da wir in einer Schleife die Elemente der Inputfolge durchgehen können und dabei nur das Maximum/Minimum der bisher betrachteten Zahlen mitspeichern müssen. In diesen Fällen ist daher 9.2 T (n) = O(n) Allgemeiner Fall Im allgemeinen Fall 1<i<n können wir das Finden der i-kleinsten Zahl in einem linearen Feld mittels Partition (Zerlegen von Feldern) lösen, das bereits bei QuickSort verwendet wurde. Partition teilt das Feld in Teilfelder der Länge k und n − k, wobei alle Elemente im linken Teilfeld kleiner als die Elemente im i ≤ k , suche daher weiter im linken Teilfeld (i − k)-kleinste Zahl im rechten Teilfeld A[k+1..n]. rechten Teilfeld sind. Wenn sonst suche die A[1..k], SELECT(A, l, r, i) 1: IF l = r THEN RETURN A[l] 2: k = PARTITION (A, l, r) 3: x = k - l + 1 4: IF i<=x THEN SELECT(A, l, k, i) 5: ELSE SELECT(A, k+1, r, i-x) Die Laufzeit hängt wieder vom Index k ab, der von Partition geliefert wird, und beträgt T (n) ≤ T (max{k, n − k}) + O(n) Wieder kann man den besten und schlechtesten Fall unterscheiden. Im besten 34 Fall ist in jedem Rekursionsschritt k= n 2: n T (n) ≤ T ( ) + O(n) 2 n n ≤ T ( ) + O( ) + O(n) 4 2 n n n ≤ T ( ) + O( ) + O( ) + O(n) 8 4 2 ! k−1 X 1 n ≤ T( k ) + O n 2 2i i=0 ! ldn−1 X 1 k=ldn = O(1) + O n = O(n). 2i i=0 Im schlechtesten Fall ist in jedem Rekursionsschritt k=1 (bzw k = n − 1): T (n) ≤ T (n − 1) + O(n) ≤ T (n − 2) + O(n − 1) + O(n) ≤ T (n − 3) + O(n − 2) + O(n − 1) + O(n) ≤ T (n − k) + k−1 X O(n − i) i=0 k=n−1 = O(1) + O n−2 X ! (n − i) = O(1) + O(n2 ) = O(n2 ). i=0 Im mittleren Fall ergibt sich nach einer ähnlichen Analyse wie bei Quicksort eine Laufzeit von T (n) = O(n). Herleitung der average case Laufzeit: T (n) = n−1 X Für die erwartete Laufzeit gilt P (k) [T (max{k, n − k}) + O(n)] , k=1 wobei P (k) die Wahrscheinlichkeit ist, dass PARTITION(A,1,n) den Index k liefert. Bei randomisierter Pivotwahl ist es gerechtfertigt, eine Gleichverteilung P (k) = 1 n−1 anzunehmen (k T (n) = = 1, . . . , n − 1). n−1 1 X [T (max{k, n − k})] + O(n) n−1 k=1 = ≤ bn 2c n−1 X k=1 k=b n 2 c+1 1 X 1 T (n − k) + n−1 n−1 n−1 X 2 T (k) + O(n). n−1 n k=b 2 c 35 T (k) + O(n) Wir zeigen nun mit vollständiger Induktion, dass T (n) = O(n) ⇒ ∃c > 0 : T (n) ≤ c · n. • Induktionsanfang n = 2: T (2) ≤ 2 · T (1) + c1 · 2 ≤ 2 · c2 + c1 · 2 ≤ c · 2 Für jeden Wert von c1 chung erfüllt ist (z.B.: • zu zeigen: wenn und c2 gibt es c ≥ c1 + c2 ). eine Konstante ∃c > 0 : T (k) ≤ c · k für c, sodass die Unglei- k = 1, . . . , n − 1, dann ∃c > 0 : T (n) ≤ c · n: T (n) ≤ n−1 X 2 T (k) + d · n n−1 n k=b 2 c n−1 X 2 c·k+d·n n−1 c k=b n 2 bn n−1 2 c−1 X 2c X = k− k + d · n n−1 ≤ k=1 k=1 Für die beiden Summen gilt: n−1 X k= n(n − 1) 2 k= b n2 c(b n2 c − 1) 2 k=1 bn 2 c−1 X k=1 ≥ n−1 n 2 (2 − 2) 2 Diese Abschätzung können wir in die ursprüngliche Formel einsetzen: n−1 n 2c n(n − 1) 2 ( 2 − 2) T (n) ≤ − +d·n n−1 2 2 n =c·n−c −1 +d·n 4 ≤ c · n. d eine Konstante c wählen, − 1 ≥ d · n. Im letzten Schritt können wir für jedes beliebige sodass für genügend groÿes n gilt Im mittleren Fall ist die Laufzeit also c n 4 T (n) = O(n). 36 9.3 Allgemeiner Fall in linearer Zeit im worst-case Durch deterministische Wahl (Berechnung) des Pivotelementes für Partition kann sogar die Laufzeit im schlechtesten Fall von O(n2 ) auf O(n) reduziert werden. Dabei geht man folgendermaÿen vor: 1. Teile A[1..n] in Gruppen zu je 5 Elementen: A[1..5], A[6..10], ... 2. Bestimme für jeden dieser n/5 Gruppen ihren Median 3. Rufe SELECT rekursiv auf, um den Median dieser n/5 Zahlen zu bestimmen (i = b n2 c) 4. Dieser Median ist das Pivotelement Schritte 1 und 2 brauchen O(n) h fur Pärtition Zeit, da pro Gruppe der Median in O(1) Zeit bestimmt werden kann (konstante Gruppengröÿe). Die Zeit für Schritt 3 beträgt T ( n5 ). 3n 10 Elemente sind gröÿer (bzw. kleiner) als h, denn 1 n n die Hälfte der lokalen Gruppenmediane ist gröÿer (kleiner) als h ( · 2 5 = 10 n 2n 3n Elemente), sowie mindestens zwei weitere Elemente pro Gruppe ( 10 + 10 = 10 Elemente). Mit dieser Pivotwahl teilt Partition das Feld also in zwei Teilfelder, Beobachtung: Mindestens dessen Gröÿenverhältnis zwischen 3:7 und 7:3 liegt, es muss also im schlimmsten 7n 10 weitergesucht werden. Die Rekursiongleichung lautet also Fall auf einem Telfeld der Gröÿe T (n) ≤ T ( Wir zeigen 7n n ) + T ( ) + O(n) 10 5 T (n) = O(n) ⇔ ∃c > 0, ∃n0 ∈ N : T (n) ≤ c · n ∀n ≥ n0 : n 7n +c· +d·n 10 5 9 =c·n· +d·n 10 9c =n· +d 10 c ≤ c · n für d ≤ 10 T (n) ≤ c · d gibt es also eine Konstante c (mit c ≥ 10d), sodass T (n) ≥ T (n) = O(n) in jedem Fall. Für jede Konstante c · n, also 37 10 Gestreute Speicherung (Hashing) Die Datenstrukur Hashtabelle wird zur Lösung des Wörterbuchproblems verwendet. Das Wörterbuchproblem ist durch die drei Funktionen chen und 10.1 Löschen Einfügen, Su- deniert. Grundidee Die Idee ist direkt aus dem Schlüssel mithilfe einer Hashfunktion die Adresse zur Speicherung der Daten zu berechnen. 10.2 Gute Hashfunktionen Die Hashfunktion sum U) auf m h bildet dabei Werte w aus der Menge der Schlüssel (Univer- verschiedene Indizes der Hashtabelle T ab. h : U → {0, 1, . . . m − 1} Die ideale Hashfunktion sollte für alle Werte w∈U eine Gleichverteilung über 1 , d.h. {h(w) = j} = m die Wahrscheinlichkeit, dass ein Wert w genau den Index j zugewiesen bekommt 1 ist m für eine Hashtabelle der Gröÿe m). alle Indizes j = 0, 1, . . . m − 1 der Hashtabelle haben (P 10.2.1 Divisionsmethode m Dividiere den Wert durch und nimm den Rest: h(w) = w mod m Vorteil: Schnell berechenbar Nachteil: nicht für alle m geeignet. 10.2.2 Multiplikationsmethode Multipliziere den Wert w mit einer xen Konstante multipliziere den gebrochenen Teil f rac(.) A, 0 < A < 1, m.9 wobei des Resultats mit und h(w) = bm · f rac(w · A)c Vorteil: m 10.3 Behandlung von Kollisionen ist unkritisch h(w) für zwei verschiedene Werte h(w) = h(w0 ). Eine Kollision liegt vor, wenn die Hashfunktion w und w0 denselben Index j liefert: 9 Anmerkung: f rac(.) ∈[0, 1) 38 10.3.1 Kollisionsbehandlung mit Überlauferliste (Chaining) Die Idee liegt darin, Werte, die zum gleichen Index führen, als verkette Liste zu organisieren. Das heiÿt, dass in der Hash-Tabelle jeweils nur eine Pointer (Adresse) steht, welcher auf den ersten Datensatz in der Liste zeigt. Die drei nötigen Funktionen zur Lösung des Wörterbuchproblems können folgendermaÿen implementiert werden: • Einfügen: w Der Index j wird mithilfe der Hashfunktion aus dem Wert des Schlüssels berechnet. Falls es zu einer Kollision kommt, wird der Datensatz an die erste Stelle der Liste eingefügt (Laufzeit • T (n) = O(1)). Suchen: Der Index j wird mithilfe der Hashfunktion aus dem Wert w des Schlüssels berechnet. Die lokale Liste wird nach dem Wert durchsucht. Die T (n) O(lj ). Laufzeit liste • ist dabei abhängig von der Länge lj der lokalen Überläufer- Löschen: Das Löschen basiert auf dem Suchen. Sobald das Element gefunden wurde, werden einfach der davor liegende und der danach liegende Datensatz dementsprechend verknüpft. Die Laufzeit beträgt ebenfalls O(lj ). Wir nehmen an, dass man eine ideale Hashfunktion vorliegen hat, die alle eingefügten Werte gleichverteilt über die Hashtabelle streut und in O(1) Zeit be- rechnet werden kann. Über alle Möglichkeiten gemittelt ergibt sich eine durchschnittliche Überläuferlistenlänge. Diese Länge ist abhängig von der Anzahl der eingefügten Werte und der Gröÿe Belegungsfaktor α m n der Hashtabelle. Sie wird durch den ausgedrückt: α= n m Die Laufzeit zum Suchen eines Elements beträgt im schlimmsten Fall (worst case) O(n), falls alle n eingefügten Werte sich in einer Überläuferliste benden. Die Wahrscheinlichkeit, dass dieser worst case mit einer idealen Hashfunktion 1 n−1 , da m gezogen werden muss. eintritt, ist n−1 Mal zufällig derselbe Index wie beim ersten Mal Bei der Analyse der mitteren Laufzeit für das Suchen eines Elementes T unterscheidet man zwischen den Fällen, (w ∈ T ) oder nicht gefunden wird (w ∈ / T ). der Hashtabelle gefunden wird 1. w∈ / T: w in das das Element In diesem Fall muss bis zum Listenende gesucht werden. Die er- n m . Zusammen mit der Laufzeit O(1) für die Berechnung der Hashfunktion ergibt sich in diesem Fall eine mittlere 2. wartete Listenlänge beträgt α= Laufzeit fur das Suchen von Θ(1 + α). w ∈ T : Sei w als i-tes Element in T eingefügt worden. Damit ergibt sich für w die erwartete Listenposition n−i m + 1, da n − i Elemente nach i eingefügt 39 wurden, davon im Mittel ein m-tel in derselben Liste (Elemente werden am Beginn der Liste eingefügt). Der erwartete Suchaufwand beträgt n X P (i) i=1 n−i +1 m n X 1 n−i = +1 n m i=1 n 1 X n−i = +1 n i=1 m n =1+ 1 X (n − i) mn i=1 1 n(n − 1) mn 2 1 α 1 n − = 1 + + O( ) = Θ(1 + α) =1+ 2m 2m 2 m =1+ Der Fall Die w∈T ist also ordnungsmäÿig nicht schneller als w∈ / T. erwartete Suchzeit ist damit Θ(1 + α), was einer Laufzeit von O(1) entspricht, wenn man das Verhältnis zwischen n und m als konstant ansehen kann. 10.3.2 Oene Adressierung Bei der oenen Adressierung werden keine Pointer verwendet. Der dadurch frei gewordene Speicher kann dazu verwendet werden die Hashtabelle gröÿer zu gestalten. Die Werte werden dabei direkt in die Hashtabelle gespeichert. Bei einer Kollision wird der nächste freie Platz gesucht. Dabei wird zuerst immer mithilfe einer herkömmlichen Hashfunktion ein Index berechnet, um dann, falls diese Stelle schon besetzt ist (Kollision!), mit weiteren Versuchen (Probing, i > 0) einen freien Platz zu nden. Man verwendet dazu eine erweiterte Hashfunktion h(w, i), i die sogenannte Versuchs- oder Sondiei = 0, 1, 2, . . . m − 1 angibt. Für die ideale Hashfunktion h(w, i) gilt, dass die Sequenz h(w, 0), h(w, 1), . . . , h(w, m − 1) für jeden Wert w mit 1 Wahrscheinlichkeit m! eine der m! Permutationen von 0, 1, . . . , m − 1 ist. 0 In der Praxis werden folgende Näherungen verwendet (h (w), h1 (w) und h2 (w) sind hierbei herkömmliche Hashfunktionen U → {0, 1, . . . m − 1}): wobei der zweite Parameter rungszahl mit • Linear Probing: Falls es zu einer Kollision kommt, wird einfach der danach liegende Wert genommen: h(w, i) = [h0 (w) + i] mod m Nachteil: Es kommt zu einem primären Ballungseekt (primary clustering; benachbarte Positionen sind mit höherer Wahrscheinlichkeit belegt). 40 • Quadratic Probing: Indem man die Sprünge beim Versuch eine freie Stelle zu nden polynomiell ansteigen lässt, kann man den primären Ballungseekt verhindern. h(w, i) = [h0 (w) + f (i)] mod m Wobei f (i) zum Beispiel eine quadratische Funktion (f (i) = c1 i2 + c2 i) sein kann. Nachteil: Sobald es zu einer Kollision zwischen Werten w und w0 kommt, sind auch die nachfolgenden Indizes alle gleich. Das wird der sekundäre Ballungseekt (secondary clustering) genannt. • Double Hashing: Um auch den sekundären Ballungseekt zu verhin- dern, werden die Sprünge mithilfe einer zweiten Hashfunktion vom Wert w abhängig gemacht. h(w, i) = [h1 (w) + i · h2 (w)] mod m Der Pseudocode ist für diese drei Methoden analog aufgebaut. EINFUEGEN(T,w) 1: i←0 2: REPEAT 3: ind ← h(w,i) 4: IF T[ind] = frei THEN 5: T[ind] ← w 6: RETURN 'Okay' 7: i ← i+1 8: UNTIL i=m 9: RETURN 'Tabelle voll' SUCHE(T,w) 1: i←0 2: REPEAT 3: ind ← h(w,i) 4: IF T[ind] = w THEN RETURN ind 5: i ← i+1 6: UNTIL T[ind]=frei OR i=m 7: RETURN 'nicht gefunden' Man kann den erwarteten Suchaufwand in einer Hash-Tabelle T mit oener n angeben. Bei dieser m 1. Wieder unterscheidet man bei der Adressierung in Abhängigkeit vom Belegungsfaktor Form der Kollisionsbehandlung gilt α≤ α= Analyse der mitteren Laufzeit für das Suchen eines Elementes tabelle T nicht gefunden wird (w O(1) w in der Hash- zwischen den Fällen, dass das Element gefunden wird (w ∈ / T ). ∈ T) oder Wir nehmen eine ideale Hashfunktion an, die in Zeit pro Versuch berechnet werden kann. 41 1. w ∈ / T: pi Jeder Versuch bis auf den letzten ist besetzt. Sei scheinlichkeit, dass die Wahr- genau i Versuche besetzt sind. Dann ist die erwartete Anzahl der Versuche E(#Versuche) = |{z} 1 + frei ∞ X i · pi pi = 0, i > n i=1 | {z } besetzt Um die Summe abzuschätzen, denieren wir mit dass mindestens i qi die Wahrscheinlichkeit, Versuche besetzt sind: n =α m n n−1 q2 = P (1. & 2. Versuch besetzt) = · < α2 m m−1 n i n n−1 n−i qi = · · ... · < = αi m m−1 m−i m q1 = P (1. Weiters gilt ∞ X pi = qi − qi+1 . i · pi = i=1 Versuch besetzt) ∞ X i=1 i · qi − = Damit ergibt sich für die Summe ∞ X i · qi+1 = i=1 ∞ X i · qi − i=1 ∞ X (i − 1) · qi = i=2 ∞ X qi , i=1 und damit für die erwartete Anzahl der Versuche E(#Versuche) = 1 + ∞ X qi < 1 + i=1 da 2. ∞ X αi = ∞ X i=1 i=0 αi = 1 , 1−α α < 1. w ∈ T: In diesem Fall ist die erwartete Anzahl der Versuche E(#Versuche) = 1 1 ln α 1−α w ∈ T also ordnungsmäÿig schneller als w∈ / T . Ist die Hashtabelle z.B. zu 90% voll (α = 0.9) kommt man Schnitt mit < 3 Versuchen aus. (ohne Beweis). Hier ist der Fall der Fall im erwartete Suchaufwand in einer Hash-Tabelle T mit oener Adressierung 1 Θ( 1−α ), falls der gesuchte Wert w nicht in der Tabelle vorhanden ist (w ∈ / T ). Im Fall, dass der gesuchte Wert w in der Hash-Tabelle T vorhanden ist (w ∈ T ), 1 1 beträgt der erwartete Suchaufwand Θ α ln 1−α . Der ist 42 11 Suchen in linearen Feldern Will man in einer Datenmenge (sie liegt als lineares Feld A[1..n] vor) nur suchen, dann gibt es mehrere Möglichkeiten. Generell kann unterschieden werden, ob sortierter das Feld in oder in unsortierter Form vorliegt. Beim Suchen kann der schlechteste Fall, dass sich das gesuchte Element gar nicht in den Daten bendet, relativ oft vorkommen. 11.1 Ohne Vorsortierung 11.1.1 Sequentielle Suche Erste Idee: Einfach von Anfang bis Ende alles durchsuchen. SUCHE (A,x) 1: i←0 2: WHILE i<n 3: i ← i+1 4: IF A[i]=x THEN RETURN i 5: ELSE RETURN -1 Die Laufzeit case O(n). T (n) beträgt im worst case O(n), im best case O(1) und im average 11.1.2 Sequentielle Suche bei bestimmter Wahrscheinlichkeitsverteilung Wenn die Zugriwahrscheinlichkeitsverteilung 10 bekannt ist, dann kann die Su- che im erwarteten Fall schneller gehen. Voraussetzung ist, dass die Werte nach diesen Wahrscheinlichkeiten geordnet sind. Das heiÿt, der am häugsten gesuchte Wert steht am Anfang des Feldes, während der am wenigsten oft gesuchte Wert am Ende steht. Sei 1 ≤ i ≤ n, und sei pn+1 pi die Zugriswahrscheinlichkeit auf A[i] bis zum Ende des Feldes). Dann ergibt sich für eine Gleichverteilung (pi für 1 ≤ i ≤ n + 1) n+1 X i=1 für die Wahrscheinlichkeit für eine erfolglose Suche (also = 1 n+1 eine erwartete Suchzeit von n+1 1 X (n + 1)(n + 2) n i = i= = + 1 = O(n). n+1 n + 1 i=1 2(n + 1) 2 Bei einer exponentiellen Verteilung (pi = 1 2i , pn+1 = 1 − Pn 1 i=1 2i = 1 2n ) beträgt die erwartete Suchzeit hingegen n X i=1 i· 1 1 1 2 + (n + 1) · < 2 + 1 = 3 = O(1), 2i 2n 1 − 21 ist also konstant und unabhängig von n. 10 Die Zugriswahrscheinlichkeit ist die Wahrscheinlichkeit, dass auf ein bestimmtes Element zugegrien werden will, bzw. nach einem bestimmten Element gesucht wird. 43 11.2 Mit Vorsortierung Für die folgenden Suchalgorithmen gilt jeweils, dass sie mit Funktionsname(A,1,n) aufgerufen werden, und als Rückgabewert den Index des zu ndenden Elements zurückgeben. Falls das Element nicht in der Datenmenge vorhanden ist, wird −1 retourniert. Weiters geht man davon aus, dass die Werte in aufsteigender Reihenfolge sortiert sind. 11.2.1 Binärsuche (Binary Bisection Search) Idee: Teile das Feld in zwei gleich groÿe Hälften. Vergleiche den gesuchten Wert mit dem mittleren Element. Falls sie gleich sind, dann ist man fertig. Wenn der gesuchte Wert kleiner ist, dann suche in der unteren Hälfte, sonst in der oberen Hälfte. Die rekursive Version: BINSUCH (von,bis,x) 1: IF von≤bis THEN 2: m ← b(von + bis)/2c 3: IF x=A[m] THEN 4: RETURN m 5: ELSE 6: IF x<A[m] THEN m←BINSUCH(von,m-1,x) 7: ELSE THEN m←BINSUCH(m+1,bis,x) 8: ELSE m ← -1 Die Laufzeit beträgt T (n) ≤ O(1) + T ( n2 ) = O(log n). Zum Beispiel bei einer Gröÿe von 1 Million Werte, benötigt der Algorithmus im schlechtesten Fall (der Wert ist nicht enthalten) nur 20 Schritte! Die erwartete Laufzeit beträgt ebenfalls T (n) = O(log n). Nach 1 Schritt kann 1 m = b(1 + n)/2c). Element bereits gefunden werden (das Element in der Mitte bei Nach 2 Schritten können 2 Elemente gefunden werden (die Elemente in der Mitte der beiden Hälften), nach 3 Schritten 4 Elemente usw. Allgemein können nach i Schritten 2i−1 Elemente gefunden werden. Damit ergibt sich für die erwartete Laufzeit E(# Schritte) = ld(n+1) 1 X i · 2i−1 n i=1 es gilt: k X i=1 1 > [ld(n + 1) − 1] (2ld(n+1) − 1) n = ld(n + 1) − 1 = Ω(log n) und damit T (n) = Θ(log n). 44 i · 2i−1 > (k − 1) · (2k − 1) Die iterative Version: BINSUCH_ITERATIV (von,bis,x) 1: pos ← -1 2: REPEAT 3: m ← b(von + bis)/2c 4: IF x=A[m] THEN pos ← m 5: ELSE 6: IF x<A[m] THEN bis←m-1 7: ELSE von←m+1} 8: UNTIL (pos 6=-1) OR (von>bis)} 9: RETURN pos 11.2.2 Interpolationssuche Idee: Man sucht nicht jeweils in der Mitte, sondern dort, wo das Element sein sollte. Die Annahme ist, dass die Werte linear ansteigen. Mit Hilfe der Werte des ersten Elements A[1] und des letzten Elements A[n] wird linear interpoliert, welchen Index der gesucht Wert haben sollte. Die rekursive Version: INTSUCH(von,bis,x) 1: IF A[von] < A[bis] THEN j k x−A[von] 2: t ← von + (bis − von) · A[bis]−A[von] 3: IF x=A[t] THEN RETURN t 4: ELSE IF x<A[t] THEN 5: RETURN INTSUCH(von,t-1,x) 6: ELSE 7: RETURN INTSUCH (t+1,bis,x) 8: ELSE IF x=A[von] THEN RETURN von 9: ELSE RETURN -1 Die erwartete Suchzeit ist extrem gut: Zum Beispiel bei 232 ≈ 4.3 · 109 O(log(log n)) Werten benötigt dieser Algorithmus maximal 5 worst case wäre eine extrem stark ansteigende, nichtlineare Verteilung: z.B.: A[k] = k!. (!!) Schritte, wenn man eine vernünftige Verteilung annehmen kann. Der Die Laufzeit beträgt dann Θ(n). 45 Um zu zeigen, dass der Fall A[n − 1] um 1 verkleinert, also gilt A[k] = k! den worst case liefert, wenn nach x = gesucht wird, zeigen wir, dass sich der betrachtete Bereich immer nur j = n, (j − i) · x−A[i] A[j]−A[i] < 1. Wenn x = A[n − 1] gesucht wird, also (n − 1)! − i! <1 n! − i! (n − i) [(n − 1)! − i!] < n! − i! (n − i) · Dies gilt, da (n − i) [(n − 1)! − i!] < (n − i)!(n − 1)! ≤ (n − 1)(n − 1)! = n(n − 1)! − (n − 1)! ≤ n! − i!. 11.2.3 Quadratische Binärsuche Idee:Verhindere den worst-case der Interpolationssuche durch Anwendung der √ √ Interpolationssuche auf n Teilfelder der Länge n. Die rekursive Version: QUADSUCH(von,bis,x) 1: n=bis-von+1 2: IF A[von] < A[bis] THEN j k x−A[von] 3: t = von + (bis − von) · A[bis]−A[von] 4: IF x=A[t] THEN RETURN t 5: ELSE IF x<A[t] THEN √ 6: REPEAT t=t-b nc UNTIL√x≥A[t] 7: RETURN QUADSUCH(t,t+b nc-1,x) 8: ELSE √ 9: REPEAT t=t+b nc UNTIL √ x≤A[t] 10: RETURN QUADSUCH(t-b nc+1,t,x) 11: ELSE IF x=A[von] THEN RETURN von 12: ELSE RETURN -1 Die Interpolationssuche liefert zunächst den Index t. Von dort aus sucht man in Sprüngen von √ n weiter, bis man das Teilfeld der Gröÿe √ n identiziert hat, das das gesuchte Element enthält. Bei guten Verteilungen kann das Teilfeld in erwarteter konstanter Zeit identiziert werden. Damit ergibt sich eine er- √ T (n) = O(1) + T ( n) = O(log(log n)). Im schlechtesten √ n Teilfelder untersucht werden (in REPATFall müssen aber alle √ √ √den inneren UNTIL-Schleifen), was eine Gesamtlaufzeit von T (n) = n + T ( n) = O( n) wartete Laufzeit von ergibt. Anmerkung: In den inneren REPAT-UNTIL-Schleifen müssten die Fälle 1 und t>n noch extra behandelt werden. 46 t< 11.2.4 Fastsearch Idee: Man könnte die beiden Vorteile aus Binärsuche und Interpolationssuche kombinieren. Der neue Algorithmus hat dann O(log n) O(log log n) im mittleren Fall und im schlechtesten Fall. Die rekursive Version: FASTSEARCH (von,bis,x) 1: IF von≤bis THEN 2: mB ← b(von j+ bis)/2c k x−A[von] 3: mI ← von + (bis − von) · A[bis]−A[von] 4: IF mB >mI THEN VERTAUSCHE (mB ,mI ) 5: IF x = A[mB ] THEN RETURN mB 6: ELSE IF x = A[mI ] THEN RETURN mI 7: ELSE IF x<A[mB ] THEN RETURN FASTSEARCH(von,mB -1,x) 8: ELSE IF x<A[mI ] THEN RETURN FASTSEARCH(mB +1,mI -1,x) 9: ELSE RETURN FASTSEARCH(mI +1,bis,x) 10: RETURN -1 11.2.5 Zusammenfassung Laufzeitverhalten der Suchverfahren in sortierten Feldern: Binärsuche Interpolationssuche Quadratische Binärsuche Fastsearch Beispiel mit 109 Mittlerer Fall Schlechtester Fall O(log n) O(log log n) O(log log n) O(log log n) O(log n) O(n) √ O( n) O(log n) 11 Werten - Anzahl der Vergleiche: Mittlerer Fall Binärsuche Interpolationssuche Quadratische Binärsuche Fastsearch 11 Es Schlechtester Fall 30 30 5 1.000.000.000 5 32.000 10 60 wird der Logarithmus zur Basis 2 zur Berechnung verwendet. Die Werte sind gerundet. 47 12 Binärbäume 12.1 Grundlagen - Denitionen Denition: Ein Baum ist eine Menge, die durch eine sog. Nachfolgerrelation strukturiert ist. In einem Baum gilt: 1 (I) ∃ Knoten w ohne VATER(w ), das ist die Wurzel 1 (II) ∀ Knoten k 6= w ∃ Knotenfolge k0 , k1 , . . . , kt mit k0 = k , kt = w ki =VATER(ki−1 ) für i = 1, 2, ..., t (Ast zwischen k und w, Länge t) und Ein Binärbaum ist ein spezieller Baum, in dem jeder Knoten maximal zwei Söhne besitzt. 12.2 Nötige Funktionen Ausgehend von einem Pointer k auf einen beliebigen Knoten sind folgende FunkB implementiert werden kann: tionen nötig, damit ein Binärbaum • LINKS(k): zeigt auf den linken Sohn des • RECHTS(k): zeigt auf den rechten Sohn des • VATER(k): zeigt auf den Vaterknoten des • WERT(k): liefert den Wert des Knotens • WURZEL(B): zeigt auf die Wurzel des Baumes k -ten k Knotens k -ten k -ten Knotens Knotens zurück Anmerkung: Falls keine Sohnknoten oder Vaterknoten existieren wird rückgeliefert. 48 nil zu- 12.3 Reihenfolge der Knoten Die Knoten können in verschiedener Art und Weise eingefügt bzw. ausgelesen werden. 12.3.1 Symmetrische Reihenfolge (SR) Die Knoten werden in folgender Reihenfolge ausgelesen: Linker Teilbaum rekursiv in SR, Wurzel, rechter Teilbaum rekursiv in SR. Arithmetische Ausdrücke werden als SR(k) 1: IF 2: 3: 4: Inx -Notation ausgelesen. k6=nil THEN SR(LINKS(k)) SCHREIBE k SR (RECHTS(k)) 12.3.2 Haupreihenfolge (HR) Analog die Auslesefolge: Wurzel, linker Teilbaum rekursiv in HR und rechter Teilbaum rekursiv in HR. 12.3.3 Nebenreihenfolge (NR) Analog die Auslesefolge: linker Teilbaum rekursiv in NR, rechter Teilbaum rekursiv in NR und die Wurzel. Arithmetische Ausdrücke werden als Postx - Notation ausgelesen. 12.4 Sortierte Binärbäume Im weiteren Abschnitt wird die SR verwendet. Der Binärbaum ist eine sehr mächtige Datenstruktur, mit der viele Funktionen eektiv implementiert werden SUCHEN, EINFÜGEN, LÖSCHEN, MINIMUM, MAXIMUM, VORGÄNGER, NACHFOLGER. können: Alle Funktionen können in O(Höhe des Baumes) implementiert werden. Vorteil: Es kann das Wörterbuchproblem dynamisch gelöst werden Nachteil: Laufzeiten bis Θ(n) 12.4.1 Suchen in Binärbäumen Suche den Wert CHE w im Binärbaum B. Der rekursive Algorithmus wird mit (b,Wurzel) gestartet. 49 SU- SUCHE(w,k) 1: IF k=nil OR w=WERT(k)THEN 2: RETURN k 3: ELSE 4: IF w<WERT(k) THEN 5: SUCHE(w,LINKS(k)) 6: ELSE 7: SUCHE(w,RECHTS(k)) 12.4.2 Finden des Maximus in einen Binärbaum Liefert den gröÿten Wert des Binärbaumes zurück. Der Algorithmus wird mit Baum_Maximum(Wurzel(B)) aufgerufen. BAUM_MAXIMUM(k) 1: WHILE RECHTS(k)6=nil 2: k=RECHTS(k) 3: RETURN WERT(k) 12.4.3 Finden des Minimums in einen Binärbaum Liefert den kleinsten Wert des Binärbaumes zurück. Der Algorithmus wird mit Baum_Minimum(Wurzel(B)) aufgerufen. BAUM_MINIMUM(k) 1: WHILE LINKS(k)6=nil 2: k←LINKS(k) 3: RETURN WERT(k) 12.4.4 Finden des Vorgängers eines Knotens Es wird der nächstkleinere Wert zum WERT(k) VORGÄNGER(k) 1: IF LINKS(k)6=nil 2: RETURN BAUM_MAXIMUM(LINKS(k)) 3: y←VATER(k) 4: WHILE y6=nil AND k=LINKS(y) 5: k←y 6: y←VATER(y) 7: RETURN(WERT(y)) 50 gefunden. 12.4.5 Finden des Nachfolger eines Knotens Es wird der nächsthöhere Wert zum WERT(k) gefunden. NACHFOLGER(k) 1: IF RECHTS(k)6=nil 2: RETURN BAUM_MINIMUM(RECHTS(k)) 3: y=VATER(k) 4: WHILE y6=nil AND k=RECHTS(y) 5: k=y 6: y=VATER(y) 7: RETURN(WERT(y)) 12.4.6 Einfügen in den Binärbaum In den Binärbaum B wird der Wert w eingefügt. EINFÜGEN(B,w) 1: y←nil 2: x←WURZEL(B) 3: WHILE x6=nil 4: DO y ← x 5: IF w<WERT(x) THEN 6: x←LINKS(x) 7: ELSE x←RECHTS(x) 8: VATER[w]←y 9: IF y=nil 10: THEN WURZEL(B)←w 11: ELSE IF w<WERT(y) 12: THEN LINKS(y)←w 13: ELSE RECHTS(y)←w 12.4.7 Löschen eines Werts Der Knoten k soll gelöscht werden. Dabei müssen drei Fälle unterschieden wer- den, da die Sortierung in SR erhalten bleiben muss: 1. k ist ein Blatt: einfach entfernen 2. k hat nur einen Sohn: Den Sohn von k 3. k hat zwei Söhne: Finde den Knoten k0 51 an den Vater von k anhängen mit dem nächst gröÿten Wert LÖSCHEN(k) 1: IF LINKS(k)=nil OR RECHTS(k)=nil 2: THEN y←k 3: ELSE y ← NACHFOLGER(k) 4: IF LINKS(y)6=nil 5: THEN x←LINKS(y) 6: ELSE x←RECHTS(y) 7: IF x6=nil 8: THEN VATER(x)←VATER(y) 9: IF VATER(y)=nil 10: THEN WURZEL(B)←x 11: ELSE IF y = LINKS(VATER(y)) 12: THEN LINKS(VATER(y))←x 13: ELSE RIGHT(VATER(y))←x 14: IF y6=k 15: THEN WERT(k)←WERT(y) 16: IF ('andere Felder vorhanden, mitkopieren') 17: RETURN y 52 13 (2-4)-Bäume (2-4)-Bäume sind durch folgende Eigenschaften deniert: 1. Alle Äste sind gleich lang 2. Die Ordnung (maximale Anzahl der Söhne eines Knotens) ist gleich 4 3. Innere Knoten haben ≥ 2 Söhne 4. Die Blätter enhalten von links nach rechts die Werte aufsteigend sortiert 5. Jeder innere Knoten mit tionen x1 , ..., xt−1 wobei t Söhnen (2 ≤ t ≤ 4) speichert t − 1 Hilfsinformaxi = gröÿter Wert im Teilbaum des i-ten Sohnes von links Dadurch kann eine logarithmische Höhe garantiert werden (13.2). Beispiel eines (2,4)-Baums: Weiters wird die Ordnung α(k) eines Knotens k als die Anzahl der Söhne de- niert. 13.1 Implementierbare Funktionen Die Funktionen SUCHEN, EINFÜGEN und ENTFERNEN entsprechen dem Wörterbuchproblem. Man kann mithilfe von (2,4)-Bäumen das Wörterbuchproblem bestehende aus n Elementen in O(log n) pro Funktion und S(n) = Θ(n) Speicher lösen. 13.1.1 Suchen in (2-4)-Bäumen Mit Hilfe der Information in den inneren Knoten kann von der Wurzel ausgehend nach unten der entsprechende Wert gesucht werden. Pro Knoten benötigt der O(1). Die gesamte T (n) = Θ(h) = Θ(log n) Suchprozess eine konstante Zeit der Höhe des Baumes ab: 53 Laufzeit hängt direkt von 13.1.2 Einfügen in (2-4)-Bäumen Zuerst wird der richtige Knoten gesucht und dort das neue Element eingefügt. Dabei müssen zwei Fälle unterschieden werden: 1. Fall: α(k) ≤ 4 nach dem Einfügen: Ergebnis ist wieder ein (2-4)-Baum 2. Fall: α(k) = 5 nach dem Einfügen: Es liegt eine sog. kein (2,4)-Baum vor. Es muss SPALT -Operation durchgeführt werden, um wieder einen (2,4)- Baum zu erhalten. SPALTEN: Der Knoten k. von k erhält einen Bruderknoten k 0 unmittelbar rechts Dabei werden die zwei rechtesten (gröÿten) Söhne von k auf k0 umgehängt. Bemerkung: Dabei muss die führt werden (siehe v, v 0 ), SPALT -Operation auch potentiell öfters durchge- maximal aber nur logarithmisch oft: T (n) = O(h) = O(log n). 13.1.3 Entfernen in (2-4)-Bäumen Auch hier wird zuerst der entsprechende Knoten gesucht. Nach dem Entfernen muss man wieder zwei Fälle unterscheiden: 1. Fall: α(k) ≥ 2 2. Fall: α(k) = 1 nach dem Entfernen: Ergebnis ist wieder ein (2-4)-Baum. nach dem Entfernen: Es liegt nun vor. Es muss entweder eine STEHL- oder eine kein (2,4)-Baum mehr VERSCHMELZUNGS - Operation durchgeführt werden, um wieder einen (2,4)-Baum zu erhalten. Sei k0 ein direkter Bruder von k (a) α(k 0 ) ≥ 3, dann muss man einen Sohn von (b) α(k 0 ) = 2, dann muss k mit k 0 STEHLEN k 0 VERSCHMOLZEN 54 werden Bemerkung: Auch hier muss die VERSCHMELZUNGS -Operation auch poten- tiell öfters durchgeführt werden, maximal aber nur logarithmisch oft: T (n) = O(h) = O(log n). 13.2 Beweis der logarithmischen Höhe Alle erwähnten Funktionen können im schlimmsten Fall mit O(h) durchgeführt werden. Die Datenstruktur (2-4)-Bäume garantiert eine logarithmische Höhe (d.h. die Höhe hängt logarithmisch von der Datenmenge n h ab). Beweis: 2h ≤ ≤ 4h = 22h n h ≤ log2 n Also kann man die Höhe h ≤ 2h abschätzen mit log2 n ≤ h ≤ log2 n 2 was der Denition der 13.3 Θ-Notation entspricht: h = Θ (log n). Beweis des linearen Speichers Im Unterschied zum Binärbaum, bei dem jeder Knoten (auch innere Knoten) einen Wert speichert, sind in (2-4)-Bäumen die Werte blattorientiert, d.h. aus- schlieÿlich in den Blättern gespeichert, während die inneren Knoten nur Hilfsinformationen speichern. Trotz dieses zusätzlichen Speicheraufwands ist der Speicherverbrauch linear in der Anzahl der Werte (Blätter). Die maximale Anzahl an inneren Knoten in einem (2-4)-Baum tritt auf, wenn jeder Knoten nur 2 Söhne hat. Dann gilt für den Speicherverbrauch eines (2-4)-Baumes mit n Werten (Blättern) # Knoten Es gilt also ≤n+ X X n n 1 1 + + ... + 1 = n i≥0 i <n i = 0∞ i = 2n. 2 4 2 2 S(n) = Θ(n). 55 13.4 Anwendung: Mischbare Warteschlangen Eine Anwendung der (2-4)-Bäume sind sogenannte mischbare Warteschlangen, Halde ), zu den drei Funktionen EINFÜGEN(S,x), MAXIMUM(S) und ENTFERNE_MAX(S) auch noch die Funktion MISCHE(S,S') implementiert. die im Gegensatz zu den Warteschlangen mit Prioritäten (siehe Kapitel Die Struktur des (2-4)-Baums für mischbare Warteschlangen ist die folgende: Die Blätter speichern S in beliebiger Reihenfolge und jeder innere Knoten speichert das Maximum seines Teilbaums, plus einen Zeiger, der auf das entsprechende Maximum-Blatt zeigt. Alle 4 Funktionen EINFÜGEN, MAXIMUM, ENTFERNE_MAX und MISCHEN können damit in 13.5 O(log n) 12 . implementiert werden Sortieren mit (2-4)-Bäumen Sortieren mit (2-4)-Bäumen besitzt den Vorteil sowohl auch adaptiv worst-case optimal als zu sein. Die inneren Knoten des (2-4)-Baums besitzen dabei nur die Information des Maximums des jeweiligen Teilbaums. Die Idee beruht darauf in einen anfangs leeren (2-4)-Baum die Werte einzufügen. Folgende Schritte werden durchgeführt (bottom-up): • Man startet mit dem Blatt ganz links (das aktuelle Minimum) • Man läuft bis zur Wurzel der • w0 des Teilbaums T 0 (w0 x und man macht Man läuft von w0 zu Blatt ai wählt immer den ersten Teilbaum von links mit 12 Eine O(n) ist der erste Knoten, > ai ) Implementierung mit der Datenstruktur möglich machen. 56 Halde zum linken Bruder (man w(T B) > ai ) würde das Mischen nur teuer mit SPALT -Operationen Bemerkung: Es sind potentiell logarithmisch viele durch- zuführen. Wie aber gezeigt werden kann, amortisieren sich diese im Laufe des Sortiervorgangs. 13.5.1 Analyse des Sortierens mit (2,4)-Bäumen Dazu benötigt man ein Maÿ für die Unsortierheit einer Folge ( 'Die Anzahl der Fehlstände), welches folgendermaÿen deniert ist: Sei ai a1 , a2 , ..., an eine (unsortierte) Zahlenfolge. , d.h. Anzahl der Zahlen die rechts von ai fi = Anzahl der Fehlstände für stehen und aber kleiner als ai sind. fi = |{aj | j > i , aj < ai }| Die Summe aller Fehlstände F ist dann ein Maÿ der Unsortiertheit der Zahlen- folge: F = n X fi i=1 Die Laufzeit T (n) beim Einfügen des beträgt mit i-ten si = Anzahl ai : der nötigen SPALT -Operationen Elements T (n) = O n X ! (log fi + si ) i=1 Mit der obigen Denition, der Abschätzung aus der Amoritisierungsanalyse Pn n 3 i=1 si ≤ 2 n und der Abschätzung · log Fn erhält man für die Laufzeit: Pn i=1 log fi = log Πni=1 fi ≤ log F T (n) = O n · log + n n 57 F n n = 14 Amortisierte Kosten bei (2,4)-Bäumen Mithilfe der Amortisierungsanalyse ermittelt man die nötige durchschnittliche Zeit über alle Operationen im schlimmsten Fall. Damit kann gezeigt werden, dass man auch mit z.B. relativ teuren Umstrukturierungsoperationen (hier Spalten, Stehlen und Verschmelzen) die Gesamtlaufzeit verbessern kann, wenn die übrigen Funktionen (Einfügen, Entfernen, Suchen) dank dieser Umstrukturierungen weniger Zeit benötigen. 14.1 Denitionen Orientiert am Begri Amortisierung deniert man eine und entsprechend ein Konto Die Balance b b(T) Balance b(k) für Knoten für den gesamten Baum 13 . k ist folgendermaÿen deniert: −1 1 0 2 3 1 b(k) = α(k) = 0 4 −1 5 für einen Knoten Der Kontostand eines Baums T ist deniert als die Summe der Balancen aller inneren Knoten: b(T ) = X b(k) 14 : Folgende Variablen werden im Lauf der Analyse verwendet i Anzahl der Einfügeoperationen j Anzahl der Löschoperationen m Anzahl aller Operationen: m=i+j S Anzahl der Spaltungen V Anzahl der Verschmelzungen D Anzahl der Stehlvorgänge Satz (1): D≤j Beweis: Stehloperationen können nur bei Löschvorgängen auftreten und dann höchstens einmal. Satz (2): i−j 2 13 Zur Dierenzierung zu den Binärbäumen wird hier T als Bezeichnung für den (2,4)-Baum verwendet. 14 In der Analyse wird SUCHEN nicht berücksichtigt, da es keine Strukturänderung des Baums zur Folge hat (keine Kontostandsänderung). S+V ≤m+ 58 Der 2.Satz wird mithilfe des Kontostandes und den Beobachtungen 1-4 bewiesen (siehe weiter unten). Falls Satz(1) und Satz(2) richtig sind, dann kann man zeigen, das alle Spalt-, Verschmelz- und Stehloperationen zusammen nur linear mit den Einfüge- und Löschoperationen (m S+V +D ≤m+ = j + i) anwachsen. i−j 2i + 2j + i − j + 2j 3i + 3j 3 +j = = = m = O(m) 2 2 2 2 14.1.1 minimaler, maximaler Kontostand b(T ) Beobachtung 1: Ein (2,4)-Baum T mit n Blätter ist stets begrenzt in seinem Kontostand durch: 0 ≤ b(T ) ≤ n 2 Beweis: b(T ) = # Knoten mit α(k) = 3 X n n n n + ... + 1 = b(T ) ≤ + + 3 9 27 3i i≥1 "∞ # X 1 3 n <n −1 =n −1 = . i 3 2 2 i=0 14.1.2 Anhängen, Wegnehmen eines Blattes Beobachtung 2: Sei T 0 aus einem (2,4)-Baum T durch EINFÜGEN oder ENT- FERNEN eines Blattes enstanden, dann gilt: b(T 0 ) ≥ b(T ) − 1 Es ändert sich nur der Grad eines einzelnen Knotens Balance α(k) und damit den Kontostand b(T ) k um 1, das verringert die um maximal 1. 14.1.3 Spalten T 0 aus einem Baum T durch SPALTEN enstanden, d.h. ein 0 00 0 Knoten k mit α(k) = 5 wird in Knoten k und k gespalten mit α(k ) = 3 und 00 α(k ) = 2. Dann gilt (der Grad des Vaterknotens v ändert sich zusätzlich um Beobachtung 3: Sei 1, siehe Beobachtung 2): b(T ) ≥ b(T ) − b(k) + b(k 0 ) + b(k 00 ) −1 |{z} | {z } | {z } |{z} −1 0 = b(T ) + 1. 59 1 b(v) 14.1.4 Stehlen und Verschmelzen T 0 aus einem Baum T durch STEHLEN enstanden, d.h. ein 0 0 Knoten k mit α(k) = 1 stiehlt einen Sohn von einem Knoten k mit α(k ) ∈ 0 {3, 4}. Die Balance b(k) erhöht sich um 1, während sich die Balance b(k ) maBeobachtung 4a: Sei ximal um 1 verringert: b(T ) ≥ b(T ) +1 −1 |{z} |{z} b(k) b(k0 ) = b(T ). T 0 aus einem Baum T durch VERSCHMELZEN enstanden, 0 0 d.h. ein Knoten k mit α(k) = 1 wird mit einem Knoten k mit α(k ) = 2 zu einem 00 00 Knoten k mit α(k ) = 3 verschmolzen. Dann gilt (der Grad des Vaterknotens v verringert sich zusätzlich um 1, siehe Beobachtung 2): Beobachtung 4b: Sei b(T ) ≥ b(T ) − b(k) − b(k 0 ) + b(k 00 ) −1 |{z} | {z } | {z } |{z} −1 0 1 b(v) = b(T ) + 1. 14.2 Abrechnung Man nimmt an, dass ingesamt m=i+j Operationen auf einen anfangs leeren 15 . Der Anfangskontostand ist b(T (2,4)-Baum ausgeführt werden der Endkontostand maximal Abbuchungen b(Tm ) = n 2 = 0 ) = 0, während i−j (Beobachtung 1) sein kann. 2 (Kontostand erniedrigt sich) geschehen nur durch ENTFER- NEN oder EINFÜGEN von Blättern - (Beobachtung 2 →≤ m). Einzahlungen (Kontostand erhöht sich) geschehen nur durch SPALTEN (Beobachtung 3→≥ 1) , VERSCHMELZEN (Beobachtung 4b LEN (Beobachtung 4a →≥ 1) oder STEH- →≥ 0). Die Kontoabrechnung schaut dann folgendermaÿen aus: Anfangszustand - Abbuchungen + Einzahlungen = Endzustand und damit erhält man den Beweis zu Satz (2) 0 − m + (S + V ) ≤ b(Tm ) ≤ S+V ≤m+ 15 Es i−j 2 i−j . 2 wird vorausgesetzt, dass mehr Elemente eingefügt als entfernt werden i > j . 60 (1) (2) 15 Optimales Kodieren Es soll ein optimaler Kodierer (z.B. Text T) C(T ) entworfen werden, welcher eine Information mit möglichst geringer Bitanzahl eindeutig überträgt. Die Anforderungen an den optimalen Kodierer • sollte schnell kodieren • sollte eindeutig dekodieren (präx-frei) • minimale Kodelänge 15.1 C(T ) sind: Nötige Denitionen Als Beispiel soll der Text 'SUSANNE' kodiert werden. Folgende Denitionen werden benötigt: • T = 'SUSANNE' ist der zu kodierende Text (im allg. eine Information, welche kodiert werden sollte) • Z = {S, U, A, N, E} • |Z| = 5 das Alphabet ist die Gröÿe des Alphabets • fi ist die Auftrittsfrequenz. z.B.: • pi ist die Auftrittswahrscheinlichkeit - z.B.: • fS = 2, fU = 1 H(T ) (Pseudoeinheit T pro Zeichen enthält: Die Entropie die der Text H(T ) = − pS = 2 7 , oder pU = 1 7 'bit') ist die eigentliche Information, |Z| X pj · log2 pj j=1 Wenn man den Text 'SUSANNE' naiv kodiert, dann benötigt man 3 Bits pro Buchstabe. Für den ganzen Text benötigt man 3 · 7 = 21 Bits. Die Entropie (der 2.24 Bits pro Buchstabe, was eigentliche Informationsgehalt) beträgt aber nur auch die theoretische untere Grenze darstellt. 61 Idee: Zeichen die häuger vorkommen, werden mit weniger Bits belegt. Ein Problem, dass dabei auftritt, ist das Präx-Problem. Damit der Kode eindeutig interpretierbar bleibt, darf kein Buchstabe kodiert ein Präx eines ande- C(x1 ) = 10 ren Buchstaben sein. Zum Beispiel wenn für 'S' und C(x2 ) = 100 für 'A' gegeben wäre, dann wäre das Kodestück ...100... nicht eindeutig deko- 16 dierbar! 15.2 Darstellung des Kodierers Mit der Darstellung des Kodieres (und gleichzeitig des Dekodierers) als Binärbaum kann man Präxfreiheit garantieren. Die Datenstruktur hat dabei folgende Eigenschaften: • Binärbaum (sog. Kodebaum) • Werte sind blattorientiert (daher präxfrei) • Die Wortlänge entspricht der Astlänge li Wir suchen jenen Codebaum, der die Codelänge 15.3 L(C) = Pn i=1 fi li minimiert. Eigenschaften von optimalen Codebäumen Bevor wir einen Algorithmus zur Erzeugung eines optimalen Codebaums angeben, betrachten wir zunächst einige Eigenschaften eines optimalen Codebaums. 1. Der optimale Codebaum ist ein voller Binärbaum (d.h., jeder Knoten ist ein Blatt oder hat genau 2 Söhne). Beweis: Sei C ein Codebaum, der einen inneren Knoten besitzt. Dann kann man daraus durch Löschen von k k mit Grad 1 einen Baum C0 mit einer kleineren Codelänge erzeugen: X L(C 0 ) = L(C) − fi · 1 ⇒ L(C 0 ) < L(C) i∈T (k) | {z >0 } T (k) ist dabei die Menge aller zu codierenden Zeichen (Blätter) unterhalb von k . Durch Löschen des Knotens k (Anhängen des einzigen Unterbaumes an seinen Vater) wird die Länge aller Wörter in T (k) um 1 verkürzt. 16 In der Literatur wird oft der Begri 'prex codes' verwendet. Damit sind aber genau präx-freie Kodes gemeint sind. 62 2. Seltene Zeichen sind tiefer im Baum als häuge (fj Beweis: Sei C optimal, aber ∃j, k : fj > fk , lk < lj . Sei j vertauscht sind. Wenn C dem die Wörter fur L(C 0 ) ≥ L(C) > fk ⇒ lj ≤ lk ). und k C0 ein Code bei optimal ist, dann und daher 0 ≤ L(C 0 ) − L(C) X X = fi li0 − fi li i i = fj lj0 + fk lk0 − fj lj − fk lk = fj lk + fk lj − fj lj − fk lk = (lk − lj ) (fj − fk ) < 0, | {z } | {z } <0 >0 was einen Widerspruch darstellt. C kann daher nicht optimal sein. 3. Es existiert ein optimaler Codebaum, bei dem die chen Geschwister sind. Beweis: Sei f1 ≥ f2 ≥ . . . ≥ fn , d.h., seien zwei seltensten Zei- xn , xn−1 die zwei seltensten Zeichen (Anm.: es kann mehrere solche Zeichen geben). ≥ li , ∀i), denn gäbe Vertauschen von xi und xn einen xn besitzt das xi mit li > ln längste Codewort (ln es ein Wort würde ein kürzeren Code liefern. Dann gilt: (a) xn muss einen Bruder besitzen, da der Binärbaum voll ist, und (b) dieser Bruder muss ein Blatt sein, denn wäre es ein innerer Knoten, li > ln (Widerspruch). Sei dieses Blatt xj . xj 6= xn−1 . Falls xj 6= xn−1 , kann man xj und vertauschen, ohne L(C) zu verschlechtern: X X L(C 0 ) − L(C) = fi li0 − fi li gäbe es ein Zeichen Entweder xn−1 xj = xn−1 xi mit oder i i = fn−1 lj + fj ln−1 − fn−1 ln−1 − fj lj = (fn−1 − fj ) (lj − ln−1 ) ≤ 0. {z } | {z } | ≤0 15.4 ≥0 Human Die Methode nach Human liefert garantiert einen optimalen Kodebaum. Mit folgender Vorgangsweise wird der Kodebaum konstruiert: • Erstelle einen 'Wald' mit jeweils einen Baum pro Zeichen. • Suche die beiden Bäume mit den kleinsten Wahrscheinlichkeiten und verbinde sie zu einen neuen Baum, welche nun die Summe der Wahrscheinlichkeiten der Unterbäume besitzt. • Wiederhole den Vorgang, bis nur noch ein Baum übrig ist. 63 15.4.1 Implementierung Der Human-Algorithmus kann sehr eektiv mit einer Warteschlange Q mit Prioritäten implementiert werden, und zwar mit der Datenstruktur Halde. Dabei wird invers geordnet - d.h. das Minimum liegt an der Wurzel der Halde. Dem Algorithmus wird das Alphabet fi s Z mit den zugehörigen Auftrittsfrequenzen übergeben. HUFFMAN (Z,f) 1: n←|Z| 2: INIT_Q (Z) 3: FOR i←1 TO (n-1) 4: z←NEUER_KNOTEN 5: LINKS(z) ← MINIMUM(Q), ENTFERNE_MIN 6: RECHTS(z)← MINIMUM(Q), ENTFERNE_MIN 7: f(z) ← f(x)+f(y) 8: EINFUEGEN(Q,z) 9: RETURN MINIMUM(Q) Analyse: Alle Operationen der Warteschlange mit Prioritäten (mit einer Halde implementiert) können in innerhalb der Schleife n−1 O(log n) durchgeführt werden. Dabei werden sie mal aufgerufen, d.h. T (n) = O(n · log n). 15.4.2 Beweis der Optimalität von Human Z = {x1 , x2 , . . . , xn } das zu kodierende Alphabet mit den Auftrittswahrf1 ≥ f2 ≥ . . . ≥ fn . Der Human-Algorithmus bestimmt den 0 Codebaum für Z aus dem Codebaum für Z = Z \ {xn−1 , xn } ∪ {w}, mit f (w) = fn−1 + fn . Der Beweis erfolgt mittels vollständiger Induktion: Sei scheinlichkeiten • Der Human-Algorithmus liefert oensichtlich einen optimalen Codebaum für • |Z| = 2. B 0 ein optimaler Codebaum für Z 0 , ist B für Z . Es gilt zu zeigen: Ist Codebaum li = li0 i = 1, . . . , n − 2 ln−1 = ln = l(w) + 1 64 auch ein optimaler Für die Codelänge von L(B) = n X B ergibt sich damit fi li = fn ln + fn−1 ln−1 + n−2 X i=1 fi li i=1 = (fn + fn−1 )(l(w) + 1) + n−2 X fi li0 i=1 = f (w)l(w) + f (w) + n−2 X fi li0 i=1 0 = f (w) + L(B ). Sei B nicht optimal, d.h., ∃B̃ : L(B̃) < L(B), und B̃ hat xn , xn−1 als B̃ 0 erzeugen, durch Geschwister. Daraus kann man wiederum einen Baum Ersetzen von xn , xn−1 durch w. Dann gilt: L(B̃) < L(B) f (w) + L(B̃ 0 ) < f (w) + L(B 0 ) L(B̃ 0 ) < L(B 0 ), was aber ein Widerspruch zur Annahme der Optimalität von 65 B0 ist.