Zwei wichtige Fragen in der Informatik: 6 Aufwandsbetrachtungen am Beispiel des Sortierens 6.1 Praktische Komplexität 6.2 Entwicklung eines Sortieralgorithmus 6.3 Untere Schranke fürs Sortieren 6.4 O-Arithmetik 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 Gewinnung von Sortierverfahren Mischsortieren Einfüge-Sortieren Shells Sortieren Heapsort Quicksort Erste Frage: Wann ist ein Problem grundsätzlich unlösbar? Antwort: Ein Problem ist dann unlösbar, falls jede Beschreibung des Problems "innere" Widersprüche enthält. Präzisierungen des Begriffs "innere" Widersprüche liefern Modelle zur Berechenbarkeit. Zweite Frage: Wann ist ein Problem praktisch unlösbar? Antwort: Ein Problem ist dann praktisch unlösbar, falls seine Lösung mehr an Ressourcen bezüglich Zeit und/oder Platz benötigt, als zur Verfügung stehen. Bemerkung: In der praktischen Komplexitätstheorie versucht man den Aufwand zur Lösung eines Problems abzuschätzen. Stellvertretend für das allgemeine Sortierproblem betrachten wir das Problem der Sortierung einer Zahlenfolge. Gegeben: Eine Folge von Zahlen { x i } i = 1, ...,n , n ∈ N. Gesucht: Eine Anordnung (Permutation) der Zahlen x π (i) mit ∀ i ∈ 1.. n − 1: x π ( i ) ≤ x π ( i + 1) . { } i = 1, ..., n Beispiel: Ungeordnete Zahlenfolge: Geordnete Zahlenfolge: 31 41 59 26 53 58 97 93 23 84 62 64 Abschätzung des Aufwands: Sei n ∈ N die Anzahl der zu sortierenden Elemente, dann: Aufwand ≈ Zahl der Permutationen von N Elementen = n! = 1*2*3*4* ... *n n ⎛ n ⎞2 ≥ ⎜ ⎟ . ⎝ 2⎠ Bemerkung: Verfahren ist illusorisch, sei z. B. n = 200, 100 dann Aufwand ≥ 100 . 23 26 31 41 53 58 59 62 64 84 93 97 Ein zweiter Lösungsvorschlag: Ein erster Lösungsvorschlag: Sei X die Kollektion der zu sortierenden Zahlen. Erzeuge alle Permutationen der angegebenen Zahlen, dann wähle die korrekte. Bis X leer Wiederhole Bestimme ein maximales Element in X, setze X := X − {m}, setze m an korrekte Position in Endfolge. Ende Beispiel mit 3 Zahlen: 39 17 21 Alle Permutationen: 1) 39 17 21 2) 39 21 17 3) 17 39 21 4) 17 21 39 5) 21 17 39 6) 21 39 17 ⇐ sortierte Folge! Beispiel: Abschätzung des Aufwands: X = {15, 36, 23, 59, 64, 12} m = 64 sortierte Teilfolge: Annahme: Dominante Operation ist der Vergleich zweier Zahlen. 64 Sei n die Anzahl der zu sortierenden Elemente, dann gilt für die Zahl V der Vergleiche: X = {15, 36, 23, 59, 12} m = 59 sortierte Teilfolge: 59, 64 X = {15, 36, 23, 12} m = 36 sortierte Teilfolge: 36, 59, 64 X = {15, 23, 12} m = 23 sortierte Teilfolge: 23, 36, 59, 64 17 Führt man 10 Vergleiche in einer Sekunde durch, 8 dann Sortierdauer ≈ 5 * 10 Sekunden ≈ 5.787 Tage ≈ 15,85 Jahre 15, 23, 36, 59, 64 X = {12} m = 12 sortierte Teilfolge: 12, 15, 23, 36, 59, 64 X = {} Ende des Verfahrens. sortierte Folge: 9 Ist n = 10 , dann V ≈ 5 * 10 9 X = {15, 12} m = 15 sortierte Teilfolge: V = (n − 1) + (n − 2) + (n − 3) + . . . + 1 = n * (n − 1) / 2 12, 15, 23, 36, 59, 64 Bemerkung: Dieser Algorithmus ist nur für kleine Elementzahlen verwendbar. Ein dritter Lösungsvorschlag: Neuer Auswahlbaum: 59 Man verfeinere den Lösungsvorschlag zwei, indem man die bei der Bestimmung eines Maximums gewonnene Information speichert. 59 12 36 59 Beispiel: 15 Bestimmung des Maximums: 36 23 59 Maximum = 59 64 Neuer Auswahlbaum: 59 36 15 59 36 36 64 23 64 36 12 36 59 Maximum = 64 Als neues Maximum kommen nur die Elemente in Frage, die das alte Maximum verdrängt hat; hier 12 oder 59. 15 12 23 36 Die Fortsetzung des Verfahrens liefert schließlich die sortierte Folge: 12, 15, 23, 36, 59, 64. Die Präzisierung dieses Verfahrens ist unter der Bezeichnung Heapsort bekannt. Abschätzung des Aufwands: Untere Sortierschranke: Dominante Operation: Vergleich zweier Elemente Bemerkungen: Anzahl der Vergleiche: 1. Bestimmung des ersten Maximums erfordert n − 1 Vergleiche. Um n Zahlen zu sortieren benötigt man mindestens n Operationen, denn jede Zahl muß mindestens einmal betrachtet werden. 2. Bestimmung jedes weiteren Maximums benötigt höchstens log2 (n − 1) Vergleiche. Durch n Vergleiche kann man zwischen n 2 Objekten unterscheiden. 3. Entscheidungsbäume sind eine Möglichkeit, eine bestimmte Permutation auszuwählen. Damit obere Schranke für die Zahl der Vergleiche: (n − 1 ) + (n − 1) * log2 (n − 1) Gesucht ist ein minimales x mit x 2 ≥ n! 9 Benutzt man die Abschätzung Zahlenbeispiel: n = 10 n Zahl der Vergleiche ≤ ≤ ≤ = 9 9 10 * (1 + log2 (10 )) 9 10 3 10 * (1 + log2 ((2 ) )) 9 10 * (1 + 30) 9 31 * 10 9 Legt man wieder 10 Operationen in der Sekunde zugrunde, dann erhält man einen Richtwert von 31 Sekunden. Hat man sich um einen Faktor von 100 verschätzt, dann ist dieses Verfahren auch noch praktisch durchführbar, denn: 3100 s ≤ 3600 s ≤ 1 h ⎛ n ⎞2 n! ≥ ⎜ ⎟ , ⎝ 2⎠ dann erhält man für x: x ≥ n ⎛n⎞ ∗ log 2 ⎜ ⎟ . 2 ⎝ 2⎠ Eine bessere Abschätzung liefert Stirlings Formel: ⎛n⎞ n! ≈ 2 ∗ π ∗ n ∗ ⎜ ⎟ ⎝e⎠ n c<d a<b acdb b<d acbd dabc a<c dacb a<d adcb c<d nein c<d cabd cadb cdab dcab b<d a<d a<b b<d c<d b<d bcda bdca a<d bcad a<c ja nein cdba b<d cbda a<d cbad dbca b<c dcba c<d Ein Entscheidungsbaum für die Sortierung von vier Elementen a, b, c, d, rechte Hälfte, links ja, rechts nein. adbc a<d abdc a<d bacd badc bdac dbac abcd c<d b<d ja b<c Ein Entscheidungsbaum für die Sortierung von vier Elementen a, b, c, d, linke Hälfte, links ja, rechts nein. O-Arithmetik: Näherungsformeln: Beispiel: Harmonische Reihe: n Hn = ∑ k =1 1 k Seien f und g reellwertige Funktionen: f, g: R R. Bekannt ist: Hn = ln( n ) + γ + 1 1 1 − + + Re st ( n ) 2 4 2 * n 12 * n 120 * n mit γ = 0,5772156649 . . . Man sucht eine griffige Formulierung für den Restterm. Sinnvoll erscheinen Abschätzungen: ∀ n gilt : f (n) ≤ | Rest | ≤ g (n) wobei f und g einfache bekannte Funktionen sind. Zur Wachstumsschätzung nutzt man die folgenden fünf Funktionsklassen: O (f(x)) = { g(x) | ∃ c > 0, ∃ x0 ∈ R mit ∀x ≥ x0 : 0 ≤ g(x) ≤ c * f(x) } Ω (f(x)) = { g(x) | ∃ c > 0, ∃ x0 ∈ R mit ∀ x ≥ x0 : 0 ≤ c * f(x) ≤ g(x) } o (f(x)) = { g(x) | ∀ c > 0, ∃ x0 ∈ R mit ∀ x ≥ x0 : 0 ≤ g(x) ≤ c * f(x) } ω (f(x)) = { g(x) | ∀ c > 0, ∃ x0 ∈ R mit ∀ x ≥ x0 : 0 ≤ f(x) ≤ c * g(x) } Eventuell begnügt man sich auch mit geringeren Aussagen, wie: lim n → ∞ | Re st ( n ) | = 0, f (n) vorausgesetzt der Bruch läßt sich sinnvoll bilden. Θ (f(x)) = { g(x) | ∃ c1 > 0, ∃ c2 > 0, ∃ x0 ∈ R mit ∀x ≥ x0 : 0 ≤ c1* f(x) ≤ g(x) ≤ c2* f(x) } Bemerkung: Die Symbole O und o heißen auch Landausche Symbole, man spricht sie "groß O" und "klein o". Schon 1871 benutzte Paul du Bois-Reymond "klein o", und ab 1894 verwandte Paul Bachmann das Symbol O. Die wichtigste dieser Klassen ist die Klasse O (...). Man benutzt deshalb für g ∈ O (f) auch die folgenden Sprechund Schreibweisen: Bemerkungen zur O-Notation: (i) g ist von der Ordnung f, g = O (f), f dominiert g. 2 (ii) x+3 4*x + 6 3 12 251 * x + 10 * x 517 2 37 * x ∈ ∈ ∈ ∈ O (x) O (x) 3 O (x ) 2 O (x ) Bemerkungen: (i) (ii) (iii) Die Klassen O, Ω, o, ω, Θ sind transitiv. Die Klassen O, Ω, Θ sind reflexiv. Die Klasse Θ ist symmetrisch. ( iv ) g ∈ o (f ) ⇒ lim x→∞ g( x ) = 0. f ( x) Die O-Notation eliminiert multiplikative Konstanten. O (17 * x) = O (x) 100 2 2 O (2 * x ) = O (x ) (iii) Beispiele: ≡ ≡ ≡ ≡ 2 O (x + 6) = O (x ) O (9 + 1) = O (1) Bemerkung: Man beachte, daß bei Verwendung des Gleichheitszeichens dieses im Gegensatz zum sonst üblichen Gebrauch nur von links nach rechts zu lesen ist. h (x) i (x) j (x) m (x) Die O-Notation eliminiert additive Terme. Die O-Notation erlaubt die einfache Bildung oberer Schranken. Man lese von links nach rechts mit n konstant n x O (1) = O (x) = O (x ) = O (2 ). (iv) f = Ω (g) ⇔ g = O (f) (v) f = Θ (g) ⇔ f = O (g) und g = O (f) (vi) f = Θ (g) ⇔ f = O (g) und f = Ω (g) 2 Einige Rechenregeln der O-Arithmetik: Wachstum von x : Seien f und g Funktionen und c > 0 eine Konstante. x 1 10 100 1000 10000 100000 1000000 10000000 100000000 1000000000 (i) O (c * f) = O (f) (ii) O (f * g) = O (f) * O (g) (iii) O (f + g) = Max [O (f), O (g)] (iv) O (f) ≥ O (g) genau dann, falls f dominiert g. 2 x 1 100 10000 1000000 100000000 10000000000 1000000000000 100000000000000 10000000000000000 1000000000000000000 Beispiele für Dominanzen: x x dominiert x! Wachstum wichtiger Funktionen: x x! " a , a konstant x " x , a konstant, n konstant x n " x x " loga x für a > 1 a n m für n ≥ m x 1 10 100 1000 10000 log2x 0 3.3 6.6 10.0 13.3 x 1 10 100 1000 10000 2 3 x x 1 2 10 4 10 6 10 8 10 1 3 10 6 10 9 10 12 10 x 2 2 1024 30 > 10 300 > 10 3000 > 10 9 9 6 2 5 5 9 2 5 9 5 Alle Elemente der Folge 1 sind kleiner oder gleich den Elementen der Folge 2, das Zusammenfügen der beiden Folgen ist in diesem Fall trivial. Man erhält den Quicksort-Algorithmus. 6 (iii) 9 Die sortierten Teilfolgen Folge 1 und Folge 2 enthalten etwa gleich viele Elemente, die Zusammenfüge-Operation ist das Mischen. 5 (ii) 2 Folge 2 enthält genau ein Element. Dies führt auf das Einfüge-Sortieren, eine Verfeinerung des Einfüge-Sortierens ist das Sortieren nach Donald Shell. 6 (i) 6 Mögliche Randbedingungen: 6 3. Die sortierten Teilfolgen Folge 1 und Folge 2 werden zur sortierten Gesamtfolge S zusammengefaßt. Mischen 2. Folge 1 und Folge 2 werden getrennt sortiert. Teilen 1. Die unsortierte Folge U wird in zwei Teilfolgen Folge 1 und Folge 2 zerlegt. Mischsortieren: Die Idee des Mischsortierens zeigt das folgende Bild. Zerlegungsprinzip: Mischen 2 Unsortierte Folge U. Sortierte Folge S mit S ist Permutation von U. Teilen Gegeben: Gesucht: 2 Die Sortieraufgabe: Illustration des Mischsortierens: Beispielfolge: Mischphase: 48, 41, 19, 82, 25, 46, 39, 97, 68, 73. 41 48 19 82 25 39 46 97 68 73 Zerlegungsphase: 48 41 19 82 25 46 39 97 68 73 19 41 48 25 82 39 46 97 68 73 48 41 19 82 25 46 39 97 68 73 48 41 19 82 25 46 39 97 68 73 19 25 41 48 82 39 46 68 73 97 48 41 19 82 25 46 39 97 68 73 48 41 19 82 25 46 39 97 68 73 Bemerkung: Befinden sich die Daten in einem Array, dann ist die Bestimmung aller Teilfolgen von linearem Aufwand. 19 25 39 41 46 48 68 73 82 97 Aufwandsabschätzung für das Mischsortieren: Annahme: // Algorithmusgerüst für // das Einfüge-Sortieren Dominante Operation ist der Vergleich zweier Elemente. Bemerkung: Es wird nur der Aufwand für die Mischphase berechnet. Der Aufwand für die Teilungsphase ist O(n). Aufwand bei Folge der Länge 1: A (1) = 1 Sei A (n) der Aufwand bei Folgen der Länge n = 2 für k = 1, 2, 3, . . . k Es gilt die Rekursionsformel: A (n) = (n−1) + 2 * A (n/2) Vergröberung: A (n) = n + 2 * A (n/2) 2 = n + 2 * (n/2 + 2* A (n/ 2 )) 2 2 = 2 * n + 2 * A (n/2 ) ... k k = k * n + 2 * A (n/2 ) Aus der Annahme n = 2 damit k folgt k = log2 (n). k A (n) = k * n + 2 * A (1) = n * log2 (n) + n * 1 = n * (1 + log2 (n)) Die "stetige" Ergänzung für n nicht Potenz von 2 liefert A (n) ∈ O (n * log (n)). void einfuegesort (int a [], int laenge) { // a enthält die zu sortierenden Elemente, // laenge enthält ihre Anzahl. // Nach Ausführung ist a [0] .. a [laenge-1] // aufsteigend sortiert. if (laenge < 2) return; for (int i = 1; i < laenge; ++i) { // Einsortieren von a [i] int h = a [i]; int j = i-1; for ( ; j >= 0 && a [j] > h; --j) { a [j+1] = a [j]; } a [j+1] = h; } }//einfuegesort Beispiel zum Einfüge-Sortieren: Aufwand beim Einfüge-Sortieren: Unsortierte Folge: 82 84 6 9 41 88 60 26 67 44 96 69 69 52 4 Fall 1: Sortierter Unsortierter Rest: Anfang: 82 * 84 6 9 41 88 60 26 67 44 96 69 69 52 4 82 84 * 6 9 41 88 60 26 67 44 96 69 69 52 4 6 82 84 * 9 41 88 60 26 67 44 96 69 69 52 4 6 9 82 84 * 41 88 60 26 67 44 96 69 69 52 4 6 9 41 82 84 * 88 60 26 67 44 96 69 69 52 4 6 9 41 82 84 88 * 60 26 67 44 96 69 69 52 4 6 9 41 60 82 84 88 * 26 67 44 96 69 69 52 4 6 9 26 41 60 82 84 88 * 67 44 96 69 69 52 4 6 9 26 41 60 67 82 84 88 * 44 96 69 69 52 4 6 9 26 41 44 60 67 82 84 88 * 96 69 69 52 4 6 9 26 41 44 60 67 82 84 88 96 * 69 69 52 4 6 9 26 41 44 60 67 69 82 84 88 96 * 69 52 4 6 9 26 41 44 60 67 69 69 82 84 88 96 * 52 4 6 9 26 41 44 52 60 67 69 69 82 84 88 96 * 4 4 6 9 26 41 44 52 60 67 69 69 82 84 88 96 * Sortierte Folge: 4 6 9 26 41 44 52 60 67 69 69 82 84 88 96 Zahl der Vergleiche = 67 Jedes einzufügende Element wandert an den Anfang der sortierten Teilfolge. Aufwand bei Folgenlänge n: n ∗ (n − 1) 2 = O n2 1 + 2 + K + (n − 1) = ( ) Fall 2: Im Mittel wandert jedes einzufügende Element nur bis zur Mitte der sortierten Teilfolge. Aufwand bei Folgenlänge n: 1 ∗ (1 + 2 + K + (n − 1)) = O (n 2 ) 2 // Algorithmusgerüst für // das Sortieren nach Shell Beispiel zum Sortierverfahren nach Shell: Unsortierte Folge: 15 12 87 17 47 8 5 22 65 51 95 46 8 43 62 29 80 56 56 void teilsort (int a [], int laenge, int schritt) { // laenge enthält die Zahl der Elemente von a. // Nach Durchführung ist a [0], a [schritt], ... , // a [(laenge div schritt)*schritt] aufsteigend sortiert. if (laenge < schritt) return; for (int i = schritt; i < laenge; i += schritt) { // Einsortieren von a [i] int h = a [i]; int j = i - schritt; for ( ; j >= 0 && a [j] > h; j -= schritt) a [j+schritt] = a [j]; a [j+schritt] = h; } }//teilsort void shellsort (int a [], int laenge) { // Beispiel zur Berechnung von Inkrementen int inc = 1; while (inc < laenge) inc = 2*inc + 1; inc = (inc-1) / 2; for (int i = inc; i > 0; i = (i-1) / 2) { for (int j = 0; j < i; ++j) teilsort (a+j, laenge-j, i); } }//shellsort Ablauf des Sortierens: Inkrement = 15, Zustand nach Teilfolgensortierung: 15* 12 87 17 47 8 5 22 65 51 95 46 8 43 62 29* 80 56 56 15 12* 87 17 47 8 5 22 65 51 95 46 8 43 62 29 80* 56 56 15 12 56* 17 47 8 5 22 65 51 95 46 8 43 62 29 80 87* 56 15 12 56 17* 47 8 5 22 65 51 95 46 8 43 62 29 80 87 56* 15 12 56 17 47* 8 5 22 65 51 95 46 8 43 62 29 80 87 56 ab hier keine Änderungen bei Inkrement 15 Inkrement = 7, Zustand nach Teilfolgensortierung: 15* 12 56 17 47 8 5 22* 65 51 95 46 8 43 62* 29 80 87 56 15 12* 56 17 47 8 5 22 29* 51 95 46 8 43 62 65* 80 87 56 15 12 51* 17 47 8 5 22 29 56* 95 46 8 43 62 65 80* 87 56 15 12 51 17* 47 8 5 22 29 56 87* 46 8 43 62 65 80 95* 56 15 12 51 17 46* 8 5 22 29 56 87 47* 8 43 62 65 80 95 56* 15 12 51 17 46 8* 5 22 29 56 87 47 8* 43 62 65 80 95 56 15 12 51 17 46 8 5* 22 29 56 87 47 8 43* 62 65 80 95 56 Inkrement = 3, Zustand nach Teilfolgensortierung: 5* 12 51 8* 46 8 15* 22 29 17* 87 47 56* 43 62 56* 80 95 65* 5 12* 51 8 22* 8 15 43* 29 17 46* 47 56 80* 62 56 87* 95 65 5 12 8* 8 22 29* 15 43 47* 17 46 51* 56 80 62* 56 87 95* 65 Inkrement = 1, sortierte Folge: 5 8 8 12 15 17 22 29 43 46 47 51 56 56 62 65 80 87 95 Bemerkungen zu Shellsort: // Algorithmusgerüst zu Heapsort 1. Shellsort wurde 1959 von Donald L. Shell vorgeschlagen. 2. Das Sortieren nach Shell ist nicht stabil. 3. Die Analyse des Laufzeitverhaltens von Shellsort ist bisher nur bruchstückhaft gelungen. Das Laufzeitverhalten hängt entscheidend von der Inkrementfolge ab. Zu viele Inkremente führen zu überflüssigen Vergleichen, zu wenige zu quadratischem Verhalten. Bisher ist keine "beste" Inkrementfolge bekannt. 4. V. Pratt bewies 1969, daß die Laufzeit von Shellsort 2 von der Ordnung n*(log n) ist, falls man als Inp q kremente alle nutzbaren Zahlen 2 3 (p, q = 0, 1, 2, ...) wählt. Wegen der großen Anzahl an Inkrementen ist Pratts Folge nicht praxisrelevant. void perco_down (int a [], int i, int laenge) { int h = a [i]; while (i*2 + 1 < laenge) { int son = i*2 + 1; if ( (son+1 < laenge) && (a [son+1] > a [son])) ++son; if (h < a [son]) { a [i] = a [son]; i = son; } else break; } a [i] = h; }//perco_down i+1 5. Wählt man als Inkremente die Folge 2 – 1 für 0 ≤ i < log 2 n, dann erhält man eine Laufzeit von 3/2 O (n ). 6. Sedgewick empfiehlt folgende Inkrementfolge: 1391376, 463792, 198768, 86961, 33936, 13776, 4592, 1968, 861, 336, 112, 48, 21, 7, 3, 1. void heapsort (int a [], int laenge) { // Daten in a [0], ... , a [laenge-1] // Aufbau eines Haufens for (int i = laenge/2 - 1; i >= 0; --i) perco_down (a, i, laenge); for (int j = laenge-1; j > 0; --j) { // Verlängern des sortierten Restes int tmp = a [0]; a [0] = a [j]; a [j] = tmp; perco_down (a, 0, j); } }//heapsort Beispiel zu Heapsort: Bemerkungen zu Heapsort: Unsortierte Folge: 29 49 66 53 11 35 66 41 35 50 90 47 88 59 19 82 29 1. Der vollständige binäre Baum der Höhe h enthält h+1 2 − 1 Knoten. Bildung des Anfangshaufens: 90 82 88 53 50 66 66 41 35 49 11 47 35 59 19 29 29 Haufen * Sortierter Rest: 88 82 66 53 50 47 66 41 35 49 11 29 35 59 19 29 * 90 82 53 66 41 50 47 66 29 35 49 11 29 35 59 19 * 88 90 66 53 66 41 50 47 59 29 35 49 11 29 35 19 * 82 88 90 66 53 59 41 50 47 19 29 35 49 11 29 35 * 66 82 88 90 59 53 47 41 50 35 19 29 35 49 11 29 * 66 66 82 88 90 53 50 47 41 49 35 19 29 35 29 11 * 59 66 66 82 88 90 50 49 47 41 29 35 19 29 35 11 * 53 59 66 66 82 88 90 49 41 47 35 29 35 19 29 11 * 50 53 59 66 66 82 88 90 47 41 35 35 29 11 19 29 * 49 50 53 59 66 66 82 88 90 41 35 35 29 29 11 19 * 47 49 50 53 59 66 66 82 88 90 35 29 35 19 29 11 * 41 47 49 50 53 59 66 66 82 88 90 35 29 11 19 29 * 35 41 47 49 50 53 59 66 66 82 88 90 29 29 11 19 * 35 35 41 47 49 50 53 59 66 66 82 88 90 29 19 11 * 29 35 35 41 47 49 50 53 59 66 66 82 88 90 19 11 * 29 29 35 35 41 47 49 50 53 59 66 66 82 88 90 11 * 19 29 29 35 35 41 47 49 50 53 59 66 66 82 88 90 Sortierte Folge: 11 19 29 29 35 35 41 47 49 50 53 59 66 66 82 88 90 2. Die Summe der Höhen aller Knoten eines vollh+1 − 1 − (h + 1). ständigen binären Baumes ist 2 3. Die Bildung des Anfangshaufens ist von linearem Aufwand, dies folgt aus Bemerkung 2. 4. Der Aufwand für Heapsort ist O(n*log (n)), dies folgt aus den Bemerkungen 3 und 1. Beweis zu 2: Die Summe aller Höhen ist h S = ∑ 2 i * ( h − i) i=0 = h + 2 * ( h − 1) + 4 * ( h − 2 ) + 8 * ( h − 3) + ... + 2 h − 1 Multiplikation mit 2 ergibt: 2 * S = 2 * h + 4 * ( h − 1) + 8 * ( h − 2 ) + ... + 2 h Subtraktion der Gleichungen ergibt: S = − h + 2 + 4 + 8 + ... + 2 h − 1 + 2h = ( 2h + 1 − 1) − ( h + 1) // Algorithmusgerüst zu Quicksort const int cutoff = // implementationsabhängig; // Einfügesortieren bei fast sortierter Folge void einfsort (int a [], int laenge) { // laenge = Zahl der Array-Elemente if (laenge < 2) return; for (int i = 1; i < laenge; ++i) { int h = a [i]; int j = i-1; for ( ; j >= 0 && a [j] > h; --j) a [j+1] = a [j]; a [j+1] = h; } }//einfsort void qsort (int a [], int left, int right) { // globales cutoff if (left + cutoff < right) { int pivot = median3 (a, left, right); // pivot in a [right-1] int i = left; int j = right-1; // Trennschleife do { while (a [++i] < pivot); while (a [--j] > pivot); swap (a [i], a [j]); } while (j > i); // letzten swap rückgängig machen swap (a [i], a [j]); // Pivot zur Trennposition swap (a [i], a [right-1]); qsort (a, left, i-1); qsort (a, i+1, right); void swap (int& i, int& j) { int h = i; i = j; j = h; }//swap } }//qsort int median3 (int a [], int left, int right) { int mid = (left + right) / 2; if (a [left] > a [mid]) swap (a [left], a [mid]); if (a [left] > a [right]) swap (a [left], a [right]); if (a [mid] > a [right]) swap (a [mid], a [right]); swap (a [mid], a [right-1]); // a [left] und a [right-1] wirken als Grenzwert return a [right-1]; }//median3 void quicksort (int a [], int laenge) { qsort (a, 0, laenge-1); einfsort (a, laenge); }//quicksort Beispiel zu Quicksort bei Cutoff von 3: Aufwand bei Quicksort: Unsortierte Folge: 1 23 1 63 64 82 38 88 2 64 27 60 9 37 38 7 55 84 96 50 8 61 Annahme: n = Länge der Folge k + 1 = Position des Trennelementes rekursive Partionierungsgrenzen: links = 0 rechts = 21 links = 0 rechts = 6 Für den erwarteten Aufwand A (i), i ≥ 1 gelten die Rekursionsgleichungen 1 1 2 8 9 23 7 27 38 64 82 60 64 37 38 63 55 84 96 50 88 61 rekursive Partionierungsgrenzen: links = 8 rechts = 21 links = 11 rechts = 21 links = 14 rechts = 21 links = 14 rechts = 18 A (0) = 0 A (1) = 0 A (n) = (n – 1) + A (k) + A (n – k – 1) Der erwartete Aufwand läßt sich auch berechnen mittels: n −1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 8 8 8 8 8 7 9 23 7 27 38 37 38 55 50 60 61 63 64 64 82 84 96 88 9 23 7 27 38 37 38 55 50 60 61 63 64 64 82 84 96 88 9 23 7 27 38 37 38 55 50 60 61 63 64 64 82 84 96 88 9 23 7 27 38 37 38 55 50 60 61 63 64 64 82 84 96 88 9 23 7 27 38 37 38 55 50 60 61 63 64 64 82 84 96 88 8 9 23 27 37 38 38 50 55 60 61 63 64 64 82 84 88 96 A (n ) = ∑ k =0 1 ∗ (n − 1 + A (k ) + A (n − k − 1)) n = (n − 1) + 1 n −1 ∗ ∑ ( A (k ) + A (n − k − 1)) n k =0 = (n − 1) + 2 n −1 ∗ ∑ A (k ) n k =0 Daher auch: Sortierte Folge: 1 1 2 7 8 9 23 27 37 38 38 50 55 60 61 63 64 64 82 84 88 96 (n ≥ 2, 0 ≤ k < n) A (n − 1) = (n − 2) + n−2 2 ∗ ∑ A (k ) n −1 k =0 Damit: Fortwährendes Einsetzen liefert: n −1 n ∗ A (n ) = n ∗ (n − 1) + 2 ∗ ∑ A (k ) k =0 n B (n ) < B (1) + ∑ k=2 n−2 (n − 1) ∗ A (n − 1) = (n − 1) ∗ (n − 2) + 2 ∗ ∑ A (k ) n = 2∗ ∑ k =0 k =2 Differenzbildung ergibt: n ∗ A (n ) − (n − 1) ∗ A (n − 1) = (n − 1) ∗ (n − n + 2) + 2 ∗ A (n − 1) n ∗ A (n ) − (n + 1) ∗ A (n − 1) = 2 ∗ (n − 1) A (n ) A (n − 1) 2 ∗ (n − 1) − = n +1 n n ∗ (n + 1) Nach Einführung der Abkürzung B (i ) = A (i ) i+1 für i = 1, 2, K erhält man die Rekursionsgleichung: B (n ) = B (n − 1) + 2 ∗ (n − 1) n ∗ (n + 1) Man vereinfacht sie zur Ungleichung: B (n ) < B (n − 1) + 2 n 2 k 1 k Da n ∑ k=2 n 1 1 ≈ ∫ dx = ln (n ) − ln ( 2) k 2 x gilt: B (n) = O (log (n)) und damit: A (n) = (n + 1) ∗ B (n) = O (n ∗ log (n)) Bemerkung: Es sei daran erinnert, daß nur der erwartete Aufwand bei Quicksort von der Größenordnung n ∗ log (n) ist. Im Extremfall ist der Aufwand quadratisch. Allgemein gibt es bei der Aufwandsabschätzung drei Sichten: die pessimistische Sicht, die optimistische Sicht, die wahrscheinlichste Sicht. Für Quicksort liefern diese Sichten Aufwandsmaße von 2 O (n ), O (n∗log (n)), O (n∗log (n)). Bemerkung: Für die Berechnung der Aufwandsmaße für die pessimistische und optimistische Sicht bedient man sich ähnlicher Rekursionsgleichungen wie für die wahrscheinlichste Sicht. (Übungsaufgabe!) Die Frage bleibt: Soll man Quicksort, bei dem man den pessimistischen Fall nicht ausschließen kann, überhaupt einsetzen? In der Praxis wurde diese Frage fast immer mit ja beantwortet.