Informatik 2 - Script 2. Teil Inhaltsverzeichnis 3 Analyse von Algorithmen 50 3.1 O-Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 3.2 Beispiel: Sortieren durch Einfügen (Insertion Sort) . . . . . . . . . . . . . . . . . . . . . . 50 3.3 Polynomauswertung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.4 Verschiedene Arten der Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.5 Untere Schranke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.6 Exakte Schranke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.7 Sortieren durch Auswählen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 3.8 Bubblesort 55 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Algorithmische Methoden 4.1 55 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 4.1.1 Berechnung der Fakultät . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 4.1.2 Berechnung des ggT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 4.1.3 Türme von Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 4.1.4 Fibonacci-Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 4.2 Dynamische Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 4.3 Divide and Conquer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 4.3.1 Mergesort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 4.3.2 Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 4.3.3 Randomisierte Version von Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . 70 Backtracking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 4.4.1 73 4.4 N-Damen Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Weitere Ergebnisse zum Sortieren 5.1 5.2 76 Heapsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 5.1.1 Heapsort-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 5.1.2 Prioritäts-Warteschlange . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 Sortieren von natürlichen Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 48 5.2.1 5.3 Bucketsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 Radixsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 49 3 Analyse von Algorithmen 3.1 O-Notation f: N → R+ . Dann ist O(f) = {g: N → R+ | ∃ c > 0, ∃n0 ≥ 1 so, dass g(n) ≤ c * f(n) ∀ n ≥ n0 } Für g ∈ O(f) sagen wir g ist O(f ) oder g in O(f) (kurz: g = O(f)). Beispiele: 1. 7u - 3 = O(n) (g(n) = 7n - 3, f(n) = n) 2. 20n3 + 10n logn = O(n3 ) (g(n) = 20n3 + 10n logn ≤ 20n3 + 10n2 ≤ 30n3 , f(n) = n3 ) 3.2 Beispiel: Sortieren durch Einfügen (Insertion Sort) Gegeben: Array A bestehend aus n ganzen Zahlen Ziel: sortiere A so um, dass A[i] ≤ A[i+1] ∀ i ∈ {0,...,n-2} Idee: halte einen sortierten und einen nicht-sortierten Bereich in A vor. In jedem Durchlauf würde das nächste Element aus den nicht-sortierten Bereich und füge es an der richtigen Stelle im sortierten Bereich ein. ≤ 0 ≤ i-1 x nicht sortiert i i+1 n-1 Durchführung: 7 5 1 1 1 | 7 5 4 2 5 | 7 5 4 1 1 | 7 5 4 4 4 | 7 2 2 2 2 | 6 6 6 6 6 3 3 3 3 3 8 8 8 8 8 A[0],...,A[i-1] sortiert. 50 int i,j; for(int i = 0;i < n;i++){ x = A[i]; j = i - 1; while ((j >= 0) && (A[j] > x)){ A[j+1] = A[j]; j- } A[j+1] = x; } Es sei t1 die Anzahl der Elemente in A[0],...,A[i-1] in Durchlauf i, die größer als x sind. Ergibt: Laufzeit T(n) = 5n-3 + 4 Pn−1 i=1 ti + 2(n-1) = 7n-5 + 4 Pn−1 i=1 ti Best-Case: Array bereits sortiert ⇒ ti = 0 ⇒ T(n) = 7n-5 = O(n) Worst-Case: Array Elemente in umgekehrter Reihenfolge ⇒ ti = i P 4(n−1)∗n T(n) = 7n-5 + 4 n−1 = 7n-5 + 2(n-1) * n i=1 ti = 7n-5 + 2 = 2n2 + 5n - 5 = O(n2 ) Best-Case: linear in n O(n) Worst-Case: quadratisch in n O(n2 ) P Lemma: Ist g(n) = ni=1 ci ∗ ndi ein Polynom m * n mit ci , di ∈ R+ , dann ist g(n) = O(nk ) für k ≥ max di , 1 ≤ i ≤ m di k Beweis: P Pm di ≤ kk Pm n ≤di n falls ⇒ i=1 ci ∗ n ≤ i=1 ci ∗ n = nk m i=1 ci Setze c = Pm i=1 ci Dann gilt P g(n) = ci ndi ≤ c ∗ nk d.h. g(n) = O(nk ) ∀n ≥ 1 51 Weitere Beispiele: 1. 3 log n + log log n = O(log n) 2. 21 00 = O(1) 3. n log n = 0( n2 log n2 ) 4. 5 n = O( n1 ) 5. log(2n2 ) = O(log n) Wichtige Funktionen: log log n n log2 n log n n log n n 23 n2 √ n3 (0 < < 12 ) n3 2n Gegeben: Array A mit n Zahlen Ziel: berechne Präfix-Durchschnitte Pi 1 S[i] = i+1 j=1 A[j], i = 0,...,n-1 Algorithmus 1: double S[] = new double[n]; for(int i = 0;i <= n-1;i++){ double a = 0.0; for(int j = 0;j <= i; j++){ a = a + A[j]; } S[i] = a/(i+1); } Laufzeit: T1 (n) = O(n2 ) Algorithmus 2: double S[] = new double[n]; double a = 0.0; for(int i = 0;i <= n-1;i++){ a = a + A[i]; S[i] = a/(i+1); } Laufzeit: T2 (n) = O(n) 52 3n n nn ... 3.3 Polynomauswertung p(x) = Pn i=0 ci x i Horner-Schema: p(x) = c0 + x(c1 + ... + x(cn−2 + x(cn−1 + cn x))...) Gegeben: Feld C, dass die Koeffizienten c0 , ..., cn enthält. double p = c[n]; for(int i = n-1;i >= 0;i- -) p = p * x + c[i]; Laufzeit: O(n) 3.4 Verschiedene Arten der Analyse • Best-Case: zu optimistisch • Worst-Case: Analyse gilt für jede Eingabe. In vielen algorithmischen Problemen wird der worst-case angenommen. • Average-Case: Eingabe von Wahrscheinlichkeitsverteilung erzeugt. Theoretisch interessant, aber in der Praxis oft schwierig zu analysieren bzw. realistische Verteilung zu simulieren / modellieren. 3.5 Untere Schranke f: N → R+ Ω(f ) = {g : N → R+ |∃c > 0, ∃n0 ≥ 1 : g(n) ≥ c ∗ f (n), ∀n ≥ n0 } Beispiele: • 3 log n + log log n ist Ω(log n) • n2 + n3 ist Ω(n3 ) 3.6 Exakte Schranke Θ(f ) = {g : N → R+ | ∃c1 , c2 > 0, ∃n0 ≥ 1 : c1 f (n) ≤ g(n) ≤ c2 f (n) Bemerkung: g ∈ Θ(f ), wenn g ∈ O(f ) und g ∈ Ω(f ) 53 3.7 Sortieren durch Auswählen Idee: halte sortierten und nicht-sortierten Bereich in A. In jeder Iteration wählt man das minimale Element aus dem nicht-sortierten Bereich und fügt es am Ende des sortierten Bereichs ein. ≤ 0 ≤ i-2 i-1 Beispiel: | 1 1 1 1 7 | 2 2 2 5 7 | 3 3 1 5 7 | 4 4 4 5 7 | .. . 1 2 3 4 5 2 2 4 5 7 6 6 6 4 5 3 3 3 6 6 8 8 8 8 8 6 7 8 | for(int i = 0;i < n-1;i++){ int index = i; int min = A[i]; for(int j = i+1;j < n;j++){ if (A[i] < min) { min = A[j]; index = j; } } int temp = A[i]; A[i] = A[index]; A[index] = temp; } Laufzeit: Pn−1 i=0 (c1 + c2 (n − i)) c1 , c2 sind Konstanten! P = n−1 i=0 (c1 + c2 i) P = c1 ∗ n + c2 ni=1 i = c1 ∗ n + c2 ∗ n (n + 1)/2 = O(n2 ) 54 nicht sortiert i n-1 3.8 Bubblesort Idee: laufe wiederholt von links nach rechts durch das zu sortierende Array. In jedem Durchlauf vergleiche nacheinander benachbarte Paare. Sind sie in der falschen Reihenfolge, so vertausche diese. Beispiel: 7 5 1 1 5 1 4 2 1 4 2 4 4 2 5 3 2 6 3 5 6 3 6 0 3 7 0 | 8 0 | 6 0 | 7 7 | 8 (nach 1. Durchlauf) 8 (nach 2. Durchlauf) 8 (nach 3. Durchlauf) Bemerkung: Nach dem i-ten Durchlauf sind die größten i Elemente an ihrer endgültigen Position. for(int i = 0; i < n-1; i++){ for(int j = 0;j <= n-2-i; j++){ if (A[j] > A[j+1]){ int temp = A[j]; A[j] = A[j+1]; A[j+1] = temp; } } } Laufzeit: Pn−1 P T(n) ≤ n−2 i=1 i = c * i=0 c * (n - i - 1) = c * 4 4.1 (n−1)n 2 = Θ(n2 ) Algorithmische Methoden Rekursion Ein rekursiver Algorithmus löst ein Problem, indem er eine oder mehrere kleinere Instanzen des gleichen Problems löst. 4.1.1 Berechnung der Fakultät 0! = 1, n! = n(n-1)! für n ≥ 1. 55 public static int factorial(int n){ if (n == 0) return 1; else return (n * factorial(n-1); } Das kann ersetzt werden durch ein nicht-rekursives Programm. public static int factorial(int n){ int f = 1; for(int i = 1;i < = n; i++) f = f*i; return f; } Jedes rekursive Programm kann durch ein nicht-rekursives Programm ersetzt werden, das die gleichen Berechnungen durchführt. 4.1.2 Berechnung des ggT public static int ggT(int x, int y){ if (x == y) return x; else if (x > y) return ggT(x - y, y; else return ggT(x, y - x; } 4.1.3 Türme von Hanoi 3 Positionen. Position 1 hat anfangs eine Anzahl von Scheiben, die der größe nach geschichtet sind. Die größte Scheibe liegt unten. Ziel: die Scheiben sollten von Position 1 nach Position 2 bewegt werden. Dabei darf zu jedem Zeitpunkt nur eine Scheibe bewegt werden und eine größere Scheibe darf nie auf einer kleineren liegen. 56 Idee: bewege n Scheiben von Position 1 nach Position 2. 1. bewege n-1 Scheiben von Pos 1 nach Pos 3 (über Pos 2) 2. bewege die größte Scheibe von Pos 1 nach Pos 2 3. bewege n-1 Scheiben von Pos 3 nach Pos 2 (über Pos 1) public static void BewegeScheibe(int n, int from, int to, int via){ if (n > 0){ BewegeScheibe(n-1,from,via,to); System.out.println(”Bewege Scheibe von ” + from + ” nach ” + to); BewegeScheibe(n-1,via,to,from); } } n = 3: BS(3,1,2,3) BS(2,1,3,2) Bewege Scheibe von 1 BS(2,3,2,1) BS(2,3,2,1) BS(1,3,1,2) //Bewege Bewege Scheibe von 3 BS(1,1,2,3) //Bewege BS(2,1,3,2) BS(1,1,2,3) //Bewege Bewege Scheibe von 1 BS(1,2,3,1) //Bewege nach 2 Scheibe von 3 nach 1 nach 2 Scheibe von 1 nach 2 Scheibe von 1 nach 2 nach 3 Scheibe von 2 nach 3 57 Sei B(n) die Anzahl der Bewegungen für ein Problem mit n Scheiben. B(n) = 2B(n-1) + 1 B(1) = 1 Lemma: Der Algorithmus benötigt B(n) = 2n -1 Züge. Idee: B(n) = 2B(n - 1) + 1 = 2(2B(n - 2) + 1) + 1 = 2( 2( 2B(n - 3) + 1) + 1) + 1 = 23 B(n-3) + 4 + 2 + 1 = 23 2B(n-4) + 23 + 22 + 21 + 20 n−1 = 2P B(1) + (2n−2 + 2n−3 + . . . + 20 ) n−1 i = i=0 2 = 2n -1. 58 Beweis: Induktion über n n = 1: B(1) = 1 = 21 -1 → stimmt! n y n + 1: Es sei B(n) = 2n -1 Zeige: B(n+1) = 2n+1 - 1 B(n+1) = 2 B(n) + 1 = 2 (2n - 1) + 1 = 2n+1 - 2 + 1 = 2n+1 - 1 → stimmt! 4.1.4 Fibonacci-Zahlen F0 = 0, F1 = 1, Fn = Fn−1 + Fn−2 für n ≥ 2 public static int fib(int n){ if (n <= 1) return n; else return (fib(n-1) + fib(n-2)); } Laufzeit: T(0) = T(1) = 1 T(n) = T(n-1) + T(n-2) + c≥1 ⇒ T(n) ≥ Fn , n ≥ 0 (per Induktion) √ n−2 Fn ≥ 1+2 5 für n ≥ 2 (⇒ exponentielle Laufzeit) n = 2: F2 = 1 = √ 0 1+ 5 2 n y n + 1: Fn+1 = Fn + Fn−1 ≥IV = √ n−2 1+ 5 2 √ √ n−3 1+ 5 1+ 5 1+ 2 2 √ 2 √ 3+ 5 ≥! (1+4 5) 2 = √ √ =6+2 5≥1+2 5+5 √ n−3 √ 2 1+ 5 ⇒ Fn+1 ≥ 1+2 5 2 √ n−1 → stimmt! = 1+2 5 59 + √ n−3 1+ 5 2 Problem: Zahlen werden mehrfach berechnet. F6 → F5 + F4 → [(F4 + F3 ) + (F3 + F2 )] → . . . Es baut sich ein sehr großer Binärbaum auf, bei dem beinahe jeder Wert zwei Nachfolger hat. Vermeidbar: durch iterative Lösung [→ Präsenzaufgabe] Algorithmus angeben mit T(n) = O(n). 4.2 Dynamische Programmierung Idee: definiere Lösung eines Problems rekursiv und fülle schrittweise eine Tabelle auf. Beginne dabei mit den Elementen am Rekursionsende und verwende die schon berechneten Werte als Grundlage zur Berechnung neuer Werte. Beispiele: 1. Fibonacci-Zahlen 2. Binomialkoeffizienten n n! = k!(n−k)! k n = n−1 + n−1 k k−1 k n = 1, nn = 1 0 2 1 1 1 2 2 3 3 3 1 2 3 4 4 4 4 1 2 3 4 Idee: 2 2 n beginne mit 11 , dann berechne nächste Zeile usw. bis wir beim Wert 1 2 k ankommen. Der Wert von ab ergibt sich aus der Summe der beiden Werte oberhalb im Pascal’schen Dreieck. 3. Rucksack-Problem geg.: • n Gegenstände mit Größen w0 , ..., wn−1 ∈ N und Gewinne P0 , ..., Pn−1 ∈ N • Rucksack der Kapazität B ∈ N ges.: optimale Packung des Rucksacks, d. h. eine Teilmenge I ⊂ { 0,...,n-1} mit P maximalen Gewinn Pi∈I wi ≤ B undP P P = max{ i i∈I i∈I 0 Pi | I’ ⊂ { 0,...,n-1}, i∈I 0 wi ≤ B} 60 Beispiel: Ein Dieb, der einen Safe ausräumt, findet in ihm N Gegenstände unterschiedlicher Größe und unterschiedlichen Werts vor, hat aber nur einen kleinen Rucksack mit Fassungsvermögen B um die Beute zu transportieren. Das Rucksackproblem besteht darin eine Kombination der Gegenstände zu suchen, die der Dieb für den Rucksack auswählen sollte, um den Gesamtwert der Beute zu maximieren. Gegenstände Größe wi Gewinne Pi 0,...,5 3 4 6,...,9 4 5 10,11 7 10 12,13 8 11 14 9 13 Mögliche Lösung: P P I={ 0,...,4} P wi = 15 ≤ 17, P pi = 5 ∗ 4 = 20 I={ 12,14} wi = 17 ≤ 17, pi = 24 Idee: verwende ein Feld A, wobei A[i][t] = normales Gewicht einer Packung von Gegenständen aus {0,...,i} mit Gewinn = t. Wir setzen A[i][t] = ∞ (bzw. B+1), falls es keine Packung mit Gewinn = t und Gegenständen aus { 0,...,i} gibt. Lemma: A[i][t] = min{A[i-1][t], wi + A[i-1][t-Pi ]} Beweis: es gibt zwei Möglichkeiten. Der i-te Gegenstand gehört zu einer Teilmenge mit minimalem Gewicht und Gewinn = t oder nicht! Im ersten Fall ist das Gewicht A[i-1][t-Pi ] + wi und im zweiten Fall A[i-1][t] ↓i | t→ 0 1 2 3 4 5 6 7 0 0 0 0 0 0 0 0 0 1 / / / / / / / / 2 / / / / / / / / 3 / / / / / / / / 4 3 3 3 3 3 3 3 3 5 / / / / / / 4 4 6 / / / / / / / / 7 / / / / / / / / 8 / 6 6 6 6 6 6 6 61 9 / / / / / / 7 7 10 / / / / / / / 8 11 / / / / / / / / 12 / / 9 9 9 9 9 9 13 / / / / / / 10 10 14 / / / / / / / 11 15 / / / / / / / / 16 / / / 12 12 12 12 12 17 / / / / / / 13 13 18 / / / / / / / 14 19 / / / / / / / / Algorithmus: int P[] = new int[n]; int w[] = new int[n]; //Eingabe... .. . //Alg: int s = 0; for(int i = 0;i <= n-1; i++) s = s + P[i]; //Initialisieren, A[0][.] int A[][] = new int[n][s+1]; for(int t = 0;t <= s; t++){ if (P[0] != t) A[0][t] = B + 1; else A[0][t] = w[0]; } A[0][0] = 0; //Berechnung der Werte A[1][.], A[2][.],...,A[n-1][.] for(int t = 0;t <= s; t++){ if (t < P[i]) A[i][t] = A[i-1][t]; else if (A[i-1][t] <= A[i-1][t-P[i]] + w[i]) A[i][t] = A[i-1][t]; else A[i][t] = A[i-1][t-P[i]] + w[i]; } } //Bestimme max j mit A[ne-1][j] ≤ B int j = 0; for(int t = 1;t <= s; t++){ if (A[n-1][t] <= B){ j = t; } } Satz: Der Algorithmus hat Laufzeit O(n∗s) = O(n2 Pmax ) wobei s = Pmax = max { Pi | 0 ≤ i ≤ n-1 } Pn−1 i=0 Pi und Bemerkung: Der Algorithmus lässt sich modifizieren, sodass auch die zugehörige Teilmenge der Gegenstände mitberechnet wird (siehe ebenfalls Übung!) 62 4.3 Divide and Conquer Idee: ”Teile und Herrsche” Teile großes Problem in mindestens 2 kleinere Probleme. Dann verknüpfe die Teillösungen zu einer Gesamtlösung. 4.3.1 Mergesort Sequenz S von Zahlen ist zu sortieren. (a) Divide enthält S mindestens 2 Elemente, so teile S in zwei Sequenzen S1, S2. S1 bestehend aus b n2 c Zahlen. S2 bestehend aus den verbleibenden d n2 e zahlen Sortiere S1 und S2 rekursiv (b) Conquer mische die sortierten Sequenzen S1, S2 zu einer Gesamtfolge S Beispiel: 85 24 63 45 17 31 96 50 85 24 63 45 17 31 96 50 .. 85 24 63 45 . 85 24 63 45 Die entstandenen Listen haben nur ein Element. Sie können nun aufwärts mit den darüber liegenden Listen vermischt werden (siehe auch Kapitel 1.6!). Implementierung: Voraussetzung: Sequenz S sein ein Array. public static void mergeSort(int[] s){ int n = s.length; inf (n <= 2) //sortiert return s; int m = n/2; int[] s1 = new int[m]; int[] s2 = new int[n-m]; 63 //Belegung for(int i = 0;i < m; i++){ s1[i] = s[i]; } for(int i = 0;i < n-m;i++){ s2[i] = s[m+i]; } //Rekursiver Aufruf mergeSort(s1); mergeSort(s2); merge(s1,s2,s); //siehe Kapitel 1.6 /*Mischt s1, s2 zu einer Sequenz s*/ } Laufzeit: T(n): Worst-Case Laufzeit für ein Array der Länge n T(n) = T(b n2 c) + T(d n2 e + c1 n + c2 T(1) = c0 , wobei c0 , c1 und c2 Konstanten. Falls n = 2k : T(2k ) = 2 T(2k−1 ) + c1 2k + c2 = 2 (2T(2k−2 ) + c1 2k−1 + c2 ) = 4 (2T(2k−3 ) + c1 2k−1 + c2 ) + 2c1 2k + (1 + 2)c2 = 8 T(2k−3 )+ 3 c1 2k + (1 + 2 + 4)c2 = 2i T(2k−i ) + i c1 2k + (1 + 2 + 2i−1 ) c2 | {z } 2i −1 = 2k c0 + kc1 2k + (2k − 1) c2 = 2k (kc1 + c2 + c0 ) - c2 =n=2k n(c1 log n + (c0 + c2 )n - c2 ⇒k=logn für n = 2k ist T(n) = O(n log n) Allgemeines n Beh.: T(n) ist monoton wachsend Bew.: per Induktion nach n I.A.: T(2) = 2c0 + 2c1 + c2 ≥ c0 = T(1) I.V.: Beh. gilt für alle i = 2,...,n, d. h. T(i) ≥ T(i-1) für i = 2,...,n I.S.: T(n+1) = T(b n+1 c) + T(d n+1 e) + c1 (n+1) + c2 2 2 ≥I.V. T(b n2 c) + T(d n2 e) + c1 (n) + c2 = T(n) 64 Für allgemeine n schätzen wir T(n) durch T(n0 ) ab, wobei n’ die kleinste Zweierpotenz größer gleich n ist. n = 2log n T(n) ≤ T(2dlog ne ) = T(n0 ) = c1 2dlog ne dlog ne + (c0 + c2 ) 2dlog ne ≤ c1 21+log n 1 + log n + (c0 + c2 ) 21+log n log n = 2c1 2|{z} (1 + log n) + (c0 + c2 ) 2-2log n n = 2c1 n log n + (2c1 + 2c0 + 2c2 ) n = c1 n log n + c1 n = O(n log n) Iterative Version von Mergesort Problem: hoher Speicheraufwand beim Array kopieren Idee: Mische bottom-up jeweils 2 sortierte Sequenzen der Länge l in einer neuen Sequenz der Länge 2l zusammen. (L = 1) (L = 2) (L = 4) (L = 8) Kosten pro Iteration O(n) für mischen. Anzahl der Iterationen log2 n. Nach i Iterationen ist L = 2i . 2i = n ⇒ i = log2 n ⇒ Gesamtlaufzeit O(n log n) Implementierung: public static void mergesort2(int[] a){ int l = 1; while (l < a.length){ /* Mische aufeinanderfolgende Teilfolgen der Länge l zu sortierten Teilfolgen der Länge 2l*/ } l = 2l; } 65 ... k k + l-1 ... k+l k + 2l - 1 int k = 0; while(k + 2l - 1 < n){ merge(a, k, k+l-1, k+2l-1); k = k + 2l; } if (k + k-1 < n-1){ //sonst fertig merge(a, k, k+l-1, n-1);} public static void merge(int[] a, int k, int i, int j){ //mischt Sequenz a[k], a[k+1],...,a[i] //mit Sequenz a[i+1], a[i+2],...,a[j] } 4.3.2 Quicksort (C. A. R. Houre, 1960) Sortierungsverfahren, dass in der Praxis häufig verwendet wird. Worst-Case-Laufzeit: = O(n2 ) Average-Case-Laufzeit: = O(n log n) (Wenn alle Eingabepermutationen gleich wahrscheinlich sind). Im Gegensatz zu Mergesort (einfache Version) benötigt Quicksort keinen zuätzlichen Speicherplatz. geg.: Array von Zahlen, dass zu sortieren ist. Idee: • wähle ein Pivotelement x aus A • ordne Array so um, dass zwei Teilfelder entstehen – wobei ein Teil ≤ x und – der andere Teil ≥ x A: ⇓ x Elemente ≤ x Elemente ≥ x 66 Dann sortiere die beiden Teilfelder rekursiv. Hoffnung: beide Teilfelder etwa gleich groß. Quicksort: für A[l],A[l+1],...,A[r], (l < r) • wähle x aus A[l],...,A[r] • teile und ordne so um, dass ∃ i ∈ { l,...,r} mit – A[l],...,A[i-1] ≤ x – A[i+1],...,A[r] ≥ x – A[i] = x • sortiere A[l],...,A[i-1] rekursiv • sortiere A[i+1],...,A[r] rekrusiv Wir wählen als Pivotelement das Element aus dem zu sortierenden Teilarray (d.h. x = A[0]). Im folgenden sind die Elemente X (eckiger Kasten) Pivotelemente und die Elemente X (runder Kasten) Elemente, die Ihre endgültige Position im sortierten Array erreicht haben. 1. Schritt 2. Schritt 3. Schritt 4. Schritt 5. Schritt 6. Schritt 7. Schritt 8. Schritt Endarray 85 24 24 17 17 24 63 63 24 17 24 63 45 45 63 63 45 45 31 31 45 17 17 45 45 31 31 45 45 17 31 31 31 31 50 50 50 50 31 50 50 50 50 63 63 96 50 85 96 96 85 96 Implementierung: public static void quicksort(int[] A, int l, int r){ int x; if (l < r){ x = P[l]; /*Finde i ∈ { 1,...,r} und ordne um, so dass ≤ x quicksort(A,l,r-1); quicksort(A,l+1,r); } } 67 x ≥ x */ Umordnung des Feldes: a b c d e f g h ⇓ b c d e a f g h Dies Lösung ist nicht gut, da extra Speicherplatz benötigt wird. Idee: • halte zwei Indizes i und j anfangs i = l + 1 und j = r • solange l < i ≤ j ≤ r – lasse i von links nach rechts laufen bis A[i] > x – lasse j von rechts nach links laufen bis A[j] < x – vertausche A[i] und A[j] • füge Pivotelement an Position i - 1 = j ein Beispiel 1: 85 = x (l) 24 i 63 45 17 31 96 50 85 ⇒ j = x (l) 24 63 45 17 31 50 j 96 i • i>j – vertausche l und i - 1: ⇒ 50 24 63 45 17 31 85 96 = x(l) i 24 63 45 17 Beispiel 2: 85 l 24 i 63 45 17 31 96 99 ⇒ j 85 l 31 j 96 i 99 Anm. d. Autors: Fragt mich nicht wo die 99 da oben herkommt! Bin mir generell nicht so sicher, was uns der Künstler damit sagen will... sollte jemand Aufzeichnungen besitzen, die mehr Sinn ergeben: Immer her damit! Ich werde es dann sofort ändern! • i>j – vertausche l und i - 1 = j: 31 24 63 45 17 85 96 99 68 public static void quicksort(int[] A, int l, int r){ int x, i , j, temp; if (l < r){ x = A[l]; i = l + 1; j = r; while (i <= j){ while((i <= j) && (A[i] <= x)) i++; while((i <= j) && (A[j] >= x)) j--; if (i < j){ temp = A[i]; A[i] = A[j]; A[j] = temp; } } i--; A[l] = A[i]; A[i] = x; quicksort(A,l,i-1); quicksort(A,i+1,r); } } Laufzeitanalyse von Quicksort: Umordnen von A[l],...,A[r] benötigt O(r-l) Zeit (Anweisungen i++, j- - werden insgesamt (r-l) mal ausgeführt). Lauzeit ≤ c1 (r-l) + c2 Best-Case Analyse T(n) = Best-Case Laufzeit zum sortieren eines Arrays mit n Elementen mittels Quicksort. T(0) = i0 T(n) = c1 · n + c2 + T b n−1 c + T d n−1 e 2 2 ⇒ T(n) = O(n log n) Worst-Case Laufzeit T(n) = Worst-Case Laufzeit mittels Quicksort T(0) = c0 T(n) = c1 · n + c2 + max (T(nl ) + T(nr )), nl + nr = n - 1 + c2 n + c0 Beh.: T(n) = n(n+1) 2 Bew.: Induktion nach n I.A.: T(0) = c0 I.V.: Beh. gilt für alle i = 0,...,n I.S.: n y n + 1 69 z.Z.: Behauptung gilt für n + 1 Hilfs Behauptung: T(i) - T(n-1) ≤ T(0) + T(n) Bew. d. linken Seite: + c2 i + c0 (i + 1) + c1 (n−i+1)(n−1) + c2 (n - i) + c0 (n - i + 1) T(i) + T(n-1) =IV c1 i(i+1) 2 2 c1 = 2 (i (i + 1) + (n - i)(n - i + 1) + c2 n + c0 (n + 2) Bew. d. rechten Seite: + c2 n + c0 (n + 1) T(0) + T(n) =IV c0 + c1 n(n+1) 2 c1 = 2 n(n + 1) + c2 n + c0 (n + 2) Zu zeigen also: i(i + 1) + (n - i )(n - i + 1) ≤ n(n + 1) ⇔ i2 + i + n2 - 2ni + i2 +n - i ≤ n2 + n ⇔ 2i2 - 2ni ≤ 0 √ ⇔i-n≤0⇔i≤n Aus der Hilfsbehauptung folgt: T(n+1) ≤ c1 (n + 1) + c2 + T(0) + T(n) + c2 n + c0 (n + 1) =IV c1 (n + 1) + c2 + c0 + c1 n(n+1) 2 n(n + 1) = c1 + (n + 1) + c0 (n + 2) + c2 (n + 1) 2 | {z } n(n+1)+2n+2 (n+1)(n+2) = 2 2 = c1 4.3.3 (n+1)(n+2) 2 + c2 (n + 1) + c0 (n + 2) √ Randomisierte Version von Quicksort Ein Algorithmus ist randomisiert, wenn sein Verhalten nicht nur durch die Eingabe sondern auch durch Werte , die von einem Zufallsgenerator produziert werden, bestimmt ist. Praxis: Pseudozufallszahlen-Generatoren erzeugen Zahlen deterministisch, die lediglich zufällig aussehen. Randomized Quicksort (RQ) Pivotelement wird zufällig aus dem Teilfeld A[l],...,A[r] ausgewählt. k = zufällig gewähltes Elementaus { l,...,r } ⇒ A[k] wird Pivotelement. Vertausche A[l] und A[k]. Hoffnung: erwarte Größe der beiden Teilprobleme ist etwa gleich! ≤x x ' 70 ≥x Zeige: erwartete Laufzeit von RQ ist O(n log n). zur Verinfachung: Alle Eingabewerte seien verschieden. O(n log n) Schranke gilt auch für beliebige Eingaben. Sei A ein Feld mit n Zahlen x1 ,...,xn . Für ein x aus A sei rang(x) = |{ y aus A | y ≤ x } |. Da die Eingabewerte verschieden sind, gilt folgendes: ∀ i = 1,...,n ∃ genau ein x aus A mit rang(x) = i Wählt RQ ein Element mit Rang i, so haben wir Teilprobleme der Größe i - 1 und ein Problem der Größe n - i. T(n) = erwartete Laufzeit von RQ auf ein Feld der Größe n. T(0) = c0 T(n) = c1 n + c2 + n1 (T(0) + T(n-1)) + n1 (T(1) + T(n-2)) + ... + n1 (T(n-1) + T(0)) P = c1 n + c2 + n1 n−1 i=0 (T(i) + T(n-1-i)) P = c1 n + c2 + n2 n−1 i=0 T(i) ( a· n log n + b n ≥ 1 Beh.: T(n) = b n=0 [wobei a, b Konstanten sind und im Beweis bestimmt werden] Bew.: n = 0: T(0) = c0 ≤ b (erfüllt für alle b ≥ c0 ) n = 1: T(1) = c1 + c2 + 2c0 ≤ b (erfüllt für b ≥ c1 + c2 + 2c0 ) I.S.: n-1 y n P T(n) = n2 n−1 i=0 T(n) + c1 n + c2 P 2 ≤I.V. n2 n−1 i=1 (a · i log i + b) + n b + c1 n + c2 Pn−1 2b = 2a i=1 i log i + 6n · 6 n + c1 n + c2 n Es gilt nun: Pn−1 i=1 i log i ≤ ⇒ T(n) ≤ 2a 1 ( n 2 1 2 n2 log n - 18 n2 n2 log n - 18 n2 ) + 2b + c1 n + c2 ≤ a n log n - a4 n + 2b + c1 n + c2 a = an log n + b + (b + c1 n + c2 − n) ≤ an log n + b 4 | {z } gilt für b+c1 n+c2 − a4 ≤0 71 Einschub: ⇔ a4 n ≥ b + c1 n + c2 ⇔ a4 n ≥ c1 + b+c2 n b + c2 bzw. a ≥ 4c1 + 4 | {zn } ≤4(b+c2 ) 2 d.h. falls a ≥ 4c1 + 4(b + c2 ) folgt a ≥ 4c1 + 4 b+c n und damit durch Äquivalenzumformung: b + c1 n + c2 - a4 n ≤ 0 bzw. T(n) ≤ a · n log n + b P 1 2 1 2 Es fehlt noch: n−1 i=0 i log i ≤ 2 n log n - 8 n P 2 Es gilt sofort n−1 i=0 i log i ≤ n log n. Dies reicht aber nicht in der obigen Abschätzung. Daher: Pd n2 e−1 Pn−1 Pn−1 i log i = i log i + i=1 i=0 d n e−1 i log i 2 [ log (d n2 e − 1) ≤ log ( n2 = logn - log2 = log n - 1, ”≤” weil d n2 e − 1 ≤ wachsend] Pd n2 e−1 P ≤ (log n - 1) i=1 i + n−1 d n e−1 i n 2 2 = log n Pn−1 [Beachte: ≤ 1 2 i=1 i- Pd n2 e−1 i=1 Pd n2 e−1 i=1 i i = 21 (d n2 e − 1) (d n2 e) ≥ 12 ( n2 -1) ( n2 )] log n (n(n-1)) - 1 2 ( n2 -1) n2 = 12 n2 log n - 21 n log n - 18 n2 + 41 n ≤ 21 n2 log n - 18 n2 ↑ falls 14 n - 12 n log n ≤ 0 ⇔ 14 n ≤ n1 n log n ⇔ 1 2 ≤ log n; gilt für n ≥ 2 Damit ist die erwartete Laufzeit von RQ O(n log n). 72 und log() monoton 4.4 Backtracking Methode, um den gesamten Lösungsraum nach einer zulässigen oder optimalen Lösung abzusuchen. Idee: Stelle den Lösungsraum als Baum dar und durchsuche den Baum systematisch. Kann man die Lösung in einem Zweig nicht finden, so geht man zurück und durchsucht einen anderen Zweig. 4.4.1 N-Damen Problem platziere N Damen auf einem N x N großen Schachbrett, so dass sie sich gegenseitig nicht schlagen können. formal: finde N Positionen auf einem (N x N) Schachbrett { (i , j)|0 ≤ i ≤ N-1, 0 ≤ j ≤ N-1 }, so dass auf jeder Zeile, Spalte, Diagonalen und Co-Diagonalen nur eine Position ausgewählt ist. (i , j) = Position an der i-ten Zeile und j-ten Spalte. 0 0 1 2 3 1 2 X 3 X X X Wir haben N Zeilen und N Spalten. 2N-1 Diagonalen d ∈ { 0,...,2N-2 } jeweils Paare (i , j) mit i + j = d 0 1 2 3 0 d=0 d=1 d=2 d=3 1 d=1 d=2 d=3 d=4 2 d=2 d=3 d=4 d=5 3 d=3 d=4 d=5 d=6 2N-1 Co-Diagonalen c ∈ { 0,...,2N-2 } jeweils Paare (i , j) mit j - i + N-1 = c bzw. j = c + i - N+1 0 1 2 3 0 c=3 c=2 c=1 c=0 1 c=4 c=3 c=2 c=1 2 c=5 c=4 c=3 c=2 3 c=6 c=5 c=4 c=3 73 Idee zum Algorithmus: Beginnend mit Zeile 0 platzieren wir Damen Zeile für Zeile in der Reihenfolge 0,1,...,N-1, so dass sich keine zwei Damen schlagen können. Ist dies in Zeile i nicht möglich, so gehen wir zurück zu Zeile i-1 und setzen die Dame dort um. Innerhalb einer Zeile probieren wir die Positionen von links nach rechts durch. Beispiel: Allgemeine Situation: Bei jedem Knoten des Suchbaums sind Damen bereits konfliktfrei in den Zeilen 0,...,i-1 plaziert und diese Plazierung soll (falls möglich) vervollständigt werden. 74 Speicherung einer aktuellen Plazierung: static int[] rowplace = new int[N]; //rowplace[i] = j, j ∈ { 0,...,N-1 } //d.h. in Zeile i steht eine Dame auf Position (i,j) //rowplace[i] = N falls keine Dame un Zeile i static boolean[] col = new boolean[N]; //col[j] = true, falls Spalte j eine Dame hat //col[j] = false, falls Spalte j keine Dame hat static boolean[] diag = new boolean[2N-1]; //diag[d] = true, falls Diagonale d eine Dame hat //diag[d] = false, falls Diagonale d keine Dame hat static boolean[] codiag = new boolean[2N-1]; //codiag[c] = true, falls Co-Diagonale c eine Dame hat //codiag[c] = false, falls Co-Diagonale c keine Dame hat Einige Methoden zum Platzieren, Löschen von Damen: stativ void placeQueen(int i, int j){ rowplace[i] = j; col[j] = true; diag[i+j] = true; codiag[j-i+N-1] = true; } public static void moveQueen(int i, int j){ rowplace[i] = N; col[j] = false; diag[i+j] = false; codiag[j-i+N-1] = false; } Methode zum testen, ob eine Dame plazierbar ist: static boolean safe(int i, int j){ return ((rowplace[i] == N) && (!col[j]) && (!diag[i+j]) && (!codiag[j-i+N-1])); } 75 Backtracking-Algorithmus: static boolean complete(int i){ if (i == N) return true; else { boolean success = false; int j = 0; while ((j < N) && (!success)){ if (safe(i,j)){ placeQueen(i,j); success = complete(i+1); if (!success) removeQueen(i,j); } j++; } return success; } } 5 Weitere Ergebnisse zum Sortieren 5.1 Heapsort Laufzeit: O(n log n) Heap: Datenstruktur, die als vollständiger Binärbaum aufgefasst werden kann: 1. jeder Knoten im Baum enthält ein Objekt 2. abgesehen von der untersten Ebene sind alle Ebenen vollständig gefüllt 3. auf der untersten Ebene sind die Knoten von links nach rechst bis zu einem Punkt vollständig vorhanden 76 Binärbaum: Struktur T = (V,E), wobei V Knotenmenge und E Kantenmenge, die gemäß einer ”Vater-Kind”-Relation verbunden sind. 1. T hat einen ausgezeichneten Knoten r ∈ V (root, Wurzel) 2. jeder Knoten v ∈ V hat maximal zwei Kinder (rechtes bzw. linkes Kind) 3. für jeden Knoten v ∈ V gibt es einen Pfad von der Wurzel r zu v Begriffe: (a) für Knoten v ∈ V ist Vater(v) der unmittelbar nächste Knoten auf Pfad zur Wurzel (Vater(8) = 3) (b) Knoten w ist Kind von Knoten v, wenn v Vater von w ist (c) Blatt ist ein Knoten ohne Kinder (z.B. Knoten 7) (d) Höhe eines Knoten ist die maximale Anzahl von Kanten auf einem Pfad (im induzierten Teilgraphen) von dem Knoten zu einem Blatt • Höhe(7) = 0 • Höhe(3) = 1 • Höhe(1) = 2 (e) Höhe eines Baums ist die Höhe der Wurzel 77 Ein Heap hat die Heapeigenschaft, wenn ∀ Knoten v 6= r gilt: Element in Vater(v) ≥ Element in v (⇒ größtes Element steht an der Wurzel!) Heap mit Heapeigenschaft: Speicherung eines Heaps in einem Array A: Also: für i ∈ { 0,...,n-1 } gilt: left(i) = 2i + 1 right(i) = 2i + 2 vater(i) = b i−1 c (i 6= 0) 2 Speichere nun Knoten (bzw. deren Elemente) von oben nach unten Ebene für Ebene ab: Wert 16 Index 0 14 1 10 2 8 3 7 4 9 5 3 6 2 7 4 8 1 9 78 Bemerkung: Ein Heap mit n Elementen hat Höhe blog(n)c. Beweis: sei H ein Heap der Höhe h. ⇒ H enthält mindestens |1 + 2 + 4 +{z ... + 2h−1 + 1} = 2h Elemente. Schichten 0 bis h - 1 Außerdem enthält H höchstens 1 + 2 + 4 + ... + 2h−1 + 2h = 2h+1 - 1 Elemente. Gesucht ist zu gegebener Anzahl von n Elementen ein h mit 2h ≤ n ≤ 2h+1 - 1 ||log... ⇒ h ≤ logn und h+1 ≥ log(n+1) ⇒ log(n)-1 < log(n+1)-1 ≤ h ≤ log(n) ⇒ h = blog(n)c Erstellen der Heap-Eigenschaft heapify(A,i,size), wobei A = Array, i = int und size = Größe des Heaps gegeben. • Teilbäume mit Wurzel, left(i) und right(i) erfüllen Heapeigenschaft • Knoten i verletzt Heapeigenschaft, also A[i] < A[left[i]] und/oder A[i] < A[right[i]] Gesucht: ungeordneter Teilbaum mit Wurzel i, der die Heapeigenschaft erfüllt. Idee: Vertausche rekursiv Elemente mit dem größeren der beiden Kinder. 79 Beispiel: Funktionsweise von heapify static void heapify(int[] A, int i, int size){ int l = 2i + 1; int r = 2i + 2; int largest = i; if ((l < size) && (A[i] < A[l])) largest = l; if ((r < size) && (A[largest] < A[r])) largest = r; if (largest != i) { //vertausche A[i] und A[largest] heapify(A,largest,size); } } 80 Laufzeitanalyse: heapify(A,i,size) benötigt O(hi ), wobei hi die Höhe des Knotens i ist. O(log ni ) wobei ni die Anzahl der Knoten im Teilbaum mit Wurzel i Schema von heapify Idee: • baue den Heap bottom-up auf • starte bei Knoten i = b n2 c-1 (Vater(n-1) = b (n−1)−1 c = b n2 − 1c = b n2 c − 1) 2 • durchlaufe den Baum von unten nach oben pro Ebene von rechts nach links (d.h. vermindere i jeweils um 1) und rufe heapify auf. static void buildHeap(int[] A){ int n = A.length; for(int i = n/2 - 1;i >= 0;i- -) heapify(A,i,n); } 81 Beispiel: Schema eines Heaps (mit Indexmarkierungen) Anm. d. Autors: Wenn man lediglich heapify anwendet, dann bekommt man zwar eine gewisse Ordnung in den Heap, allerdings treten auch wieder Elemente auf, die die Heapeigenschaften nicht erfüllen (in diesem Beispiel im vorletzten Schritt z. B. die 4, deren Unterbaum die Heapeigenschaft nicht erfüllt. In diesem Fall muss man den Baum nochmals untersuchen. Allerdings hat sie (die heutige Vorlesung wurde nicht von Prof. Jansen gehalten) sich diesbezüglich nicht so ganz deutlich geäußert. 82 Laufzeitanalyse: (von buildHeap) Der Heap hat insgesamt die Höhe blog nc. Wir haben höchstens 1 Knoten mit der Höhe blog nc 2 Knoten mit der Höhe blog nc-1 4 Knoten mit der Höhe blog nc-2 .. . d n2 e Knoten mit der Höhe 1 Für jeden Knoten mit Höhe h ist der Aufwand von heapify O(h) bzw. c̄ · h (wobei c̄ Konstante ist) P nc a ⇒ T(n) ≤ blog 2 · c̄· blog nc − a a=0 | {z } Höhe d. Teilbaums Pblog nc 2a · (blog nc − a) P nc i· =Indexmodifikation c̄ blog i=0 = c̄ a=0 2| blog{znc−i} log n 2blog nc ≤ 2 i = ni 2i 2 2 = c̄ · ≤ c̄ n 2 = c̄ · 1 n 2 Pblog nc i 2i−1 i=1 P∞ i i=1 2i−1 n 2 Pblog nc i=1 i 12 ⇒x= 2 T(n) ≤ c̄ 5.1.1 i−1 n 1 2 (1− 12 )2 || f(x) = P∞ i=0 xi = 1 1−x für |x| < 1 , f 0 (x) = P∞ i=1 ixi−1 = 1 (1−x)2 = 2c̄n ∈ O(n) Heapsort-Algorithmus Idee: • baue sortierte Folge von hinten nach vorne auf. • Vertausche wiederholt die Wurzel des Heaps mit dem letzten Element des Heaps • rufe heapify auf um jeweils die Heapeigenschaft widerherzustellen 83 Heapsort-Algorithmus: public void heapsort(int[] A) { int size = A.length; buildHeap(A); ← O(n) for(int i = size-1; i > 0; i–){ vertausche A[0] und A[i] ; size–; heapify(A,0,size);← O(log n) } } Laufzeit: O(n log n) 84 5.1.2 Prioritäts-Warteschlange Datenstruktur, die eine Menge S verwaltet und die folgenden Operationen unterstützt: • Insert(S,x): fügt Element x in S ein • Maximum(S): gibt größtes Element aus • ExtractMax(S): entfernt größtes Element und gibt es aus Ein Heap kann eine Prioritäts-Warteschlange (priority queue) effizient realisieren. Insert(S,x): Füge neues Element x auf unterster Ebene ein (am Ende des Arrays). Vertausche dann neues Element mit dem Vater bis ein Vaterknoten ≥ dem neuen Element ist Untere Schranken Satz: Ein Algorithmus , der n Elemente mit Hilfe von Vergleichen sortiert, hat eine Laufzeit von Ω(n log n). Beweis: Wir betrachten Eingaben, in denen alle Zahlen verschieden sind und o.B.d.A. konzentrieren wir uns auf die Operation < . Der Algorithmus bekommt Eingabezahlen x1 ,...,xn und entscheidet welche Permutation der Zahlen die richtige Sortierung liefert. Insgesammt gibt es n! Permutationen. Betrachte einen Entscheidungsbaum - mit n! Permutationen an der Wurzel - durch einen Vergleich ”xi < xj ” werden zwei Kinder erzeugt: * linkes Kind enthält alle Permutationen, die konsistent mit xi < xj sind * rechtes Kind enthält alle Permutationen, die konsistent mit xi > xj sind 85 Schema eines Entscheidungsbaums Diesen Schritt wiederholt man in jedem Knoten, bis wir zu Knoten mit jeweils nur einer Permutation kommen. Bemerkung: (a) Ein Pfad im Baum von der Wurzel aus entspricht einer Folge von Vergleichen, die der Algorithmus ausführt (b) enthält ein Knoten ≥ zwei Permutationen, so ist die richtige Permutation bzw. Sortierung noch nicht eindeutig festgelegt Beispiel für einen Entscheidungsbaum Behauptung: Jeder Entscheidungsbaum enthält mindestens einen Pfad mit ≥ n Kanten (für alle n ≥ 4). 2 86 n 2 log n - Beweis: 1. ein Binärbaum, in dem alle Pfade ≤ k Kanten besitzen, hat höchstens 2k Blätter. Beispiel: k = 3: 23 = 8 Blätter Es gilt also 2k ≥ n!, wobei k die maximale Pfadlänge ist (alle Permutationen müssen auf die Blätter verteilt werden) n ⇒ 2k ≥ n! ≥ n(n-1)(n-2)...(n- n2 ) ≥ ( n2 2 ) n ⇒ k ≥ log( n2 ) 2 = n 2 log( n2 ) = n 2 log(n) - n 2 log(2) | {z } =1 = n 2 log(n) - n 2 2. Es gilt nun: n log(n) - n2 ≥ n4 log(n) ∀n ≥ 4, 2 weil n4 log(n) ≥ n2 ⇔ log(n) ≥ 2 ⇔ n ≥ 4 5.2 Sortieren von natürlichen Zahlen gegeben: n Zahlen von x0 ,...,xn−1 ∈ { 0,...,n-1 } (die Zahlen seien anfangs im Array A[0],...,A[n-1] gespeichert. 87 5.2.1 Bucketsort Verwende ein Array von m Buckets B[0],...,B[n-1]. Jeder Bucket enthält eine Liste von Elementen, die anfangs leer ist. Jeder Bucket enthält einen Verweis auf das erste und letzte Element der Liste: Bucket-Schema Algorithmus (Pseudo-Code) for(int i = 0;i <= n-1; i++){ füge A[i] am Ende der Liste in Bucket B[A[i]] ein; for(int j = 1, j <= n-1; j++) hänge Liste in B[j] am Ende von B[0] an; Am Ende steht die sortierte Liste in B[0]. Laufzeit: O(n + m) Bemerkung: Ist m ==(n), so ist die Gesamtlaufzeit O(n). 5.3 Radixsort Sortiere n natürliche Zahlen, die aus jeweils d Ziffern bestehen und jede Ziffer einen Wert aus { 0,1,...,k-1 } hat. Die Ziffernpositionen seien von rechts nach links durchnummeriert. Position d d-1 ... 2 Ziffern ad ad−1 ... a2 Werte k d−1 k d−2 ... k 1 P ⇒ di=1 ai · k i−1 = a1 k 0 + a2 k 1 + ... + ad k d−1 1 a1 k0 88 Pseudo-Code: for (int l = 1; l <= d; l++){ sortiere die n Zahlen gemäß Ziffer l mit Bucketsort ; Dezimalzahlen k = 10 025 329 004 839 720 355 155 720 004 025 355 155 329 839 —↑ 004 720 025 329 839 255 155 -↑- 004 025 155 329 355 720 839 ↑— Laufzeit: O(d · (n + k)) Bemerkung: Ist d = O(1) und k = O(n), so ist die Gesamtlaufzeit O(n). Korrektheit des Algorithmus: Beh.: nach l Iterationen, 1 ≤ l ≤ d, sind die n Zahlen gemäß den letzten l Ziffern sortiert Bew.: √ per induktion l=1 l-1 y l: Seien x,y zwei beliebige Zahlen aus der zu sortirenden Sequenz. O.B.d.A. sei x auf den letzten l Ziffern kleiner als y. Zeige: x steht vor y nach l Iterationen Es sei xk , yk die k-te Ziffer von x bzw. y. (1) xl ≤ yl → x landet im Bucket mit kleinerem Index als y (2) xl = yl → x ist auf den letzten l-1 Ziffern kleiner als y →IV x steht vor y nach l-1 Iterationen. x,y gelangen in demselben Bucket in Iteration l. x steht aber vor y, weil y später eingefügt wird 89 √