5 Aufwandsbetrachtungen am Beispiel des Sortierens 5.1 Praktische Komplexität 5.2 Entwicklung eines Sortieralgorithmus 5.3 Untere Schranke fürs Sortieren 5.4 Digitales Sortieren 5.5 O-Arithmetik 5.6 Gewinnung von Sortierverfahren 5.6.1 5.6.2 5.6.3 5.6.4 5.6.5 Mischsortieren Einfüge-Sortieren Shells Sortieren Heapsort Quicksort Zwei wichtige Fragen in der Informatik: 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 Theorien von Berechnungsmodellen. 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 oder Platz benötigt, als zur Verfügung steht. Bemerkung: In der praktischen Komplexitätstheorie versucht man den Aufwand zur Lösung eines Problems abzuschätzen. Einige große Zahlen: Zahl der Atome der Erde: 2 170 Zahl der Atome der Sonne: 2 190 Zahl der Atome der Milchstraße: 2 Zahl der Atome des Universums: 2 Alter der Erde: 2 Alter des Universums: 2 Lebenserwartung des Universums, falls das Universum geschlossen ist: 2 Zeit bis sämtliche Materie in schwarzen Löchern kollabiert, falls das Universum offen ist: 223 265 30 Jahre 34 Jahre 37 Jahre 10 76 10 Quelle: B. Schneier: Applied Cryptography, ISBN: 0-471-12845-7 Jahre Das Sortierproblem: 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 23 26 31 41 53 58 59 62 64 84 93 97 Ein erster Lösungsvorschlag: Erzeuge alle Permutationen der angegebenen Zahlen, dann wähle die korrekte. 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! 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 . Ein zweiter Lösungsvorschlag: Sei X die Kollektion der zu sortierenden Zahlen. Bis X leer Wiederhole Bestimme ein maximales Element in X, setze X := X - {m}, setze m an korrekte Position in Endfolge. Ende Beispiel: X = {15, 36, 23, 59, 64, 12} m = 64 sortierte Teilfolge: 64 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 X = {15, 12} m = 15 sortierte Teilfolge: 15, 23, 36, 59, 64 X = {12} m = 12 sortierte Teilfolge: 12, 15, 23, 36, 59, 64 X = {} Ende des Verfahrens. sortierte Folge: 12, 15, 23, 36, 59, 64 Abschätzung des Aufwands: Annahme: Dominante Operation ist der Vergleich zweier Zahlen. Sei n die Anzahl der zu sortierenden Elemente, dann gilt für die Zahl V der Vergleiche: V = (n - 1) + (n - 2) + (n - 3) + . . . + 1 = n * (n - 1) / 2 9 17 Ist n = 10 , dann V ≈ 5 * 10 9 Führt man 10 Vergleiche in einer Sekunde durch, 8 dann Sortierdauer ≈ 5 * 10 Sekunden ≈ 5787 Tage ≈ 15,85 Jahre Bemerkung: Dieser Algorithmus ist nur für kleine Elementzahlen verwendbar. Ein dritter Lösungsvorschlag: Man verfeinere den Lösungsvorschlag zwei, indem man die bei der Bestimmung eines Maximums gewonnene Information speichert. Beispiel: Bestimmung des Maximums: 64 59 64 36 15 59 36 23 64 12 59 Maximum = 64 Als neues Maximum kommen nur die Elemente in Frage, die das alte Maximum verdrängt hat; hier 12 oder 59. Neuer Auswahlbaum: 59 59 12 36 15 59 36 23 59 Maximum = 59 Neuer Auswahlbaum: 36 36 36 15 12 23 36 Die Fortsetzung des Verfahrens liefert schließlich die sortierte Folge: 12, 15, 23, 36, 59, 64. Abschätzung des Aufwands: Dominante Operation: Vergleich zweier Elemente Anzahl der Vergleiche: Bestimmung des ersten Maximums erfordert n – 1 Vergleiche Bestimmung jedes weiteren Maximums benötigt höchstens log2 (n – 1) Vergleiche Damit obere Schranke für die Zahl der Vergleiche: (n – 1 ) + (n – 1) * log2 (n – 1) 9 Zahlenbeispiel: n = 10 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 Untere Sortierschranke: Bemerkungen: 1. Um n Zahlen zu sortieren benötigt man mindestens n Operationen, denn jede Zahl muß mindestens einmal betrachtet werden. 2. Durch n Vergleiche kann man zwischen n 2 Objekten unterscheiden. Gesucht ist ein minimales x mit x 2 ≥ n! Benutzt man die Abschätzung n 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 Digitales Sortieren, auch Fachsortieren oder Radix-Sortieren Beispiel: unsortierte Folge: 572, 576, 017, 025, 064, 012, 017, 006, 045, 103, 204 Verteilen nach der letzten Ziffer in zehn Fächer: 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 572 012 103 064 204 025 045 576 006 017 017 Verteilen nach der mittleren Ziffer in die Fächer: 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 103 204 006 012 017 017 025 045 064 572 576 Verteilen nach der ersten Ziffer in die Fächer: 0: 1: 2: 3: 4: 5: 006 012 017 017 025 045 064 103 204 572 576 6: 7: 8: 9: Bemerkung: Für das Verteilen ist ein stabiler Verteilalgorithmus zu wählen. Aufwandsabschätzung für das digitale Sortieren: Der Aufwand wird bestimmt durch das Produkt maximale Ziffernzahl * Anzahl Elemente Dies ist ein linearer Aufwand Konstante * Anzahl, und fast immer besser als Konstante * Anzahl * log (Anzahl) Bemerkung: Das digitale Sortieren ist vom Kernaufwand her ein lineares Verfahren. Es bleibt die Frage: Lag ein logischer Trugschluß bei der Bestimmung einer unteren Sortierschranke in 5.3 vor? O-Arithmetik: Seien f und g reellwertige Funktionen: f, g: R R; 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) } Θ (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: g ist von der Ordnung f, g = O (f), f dominiert g. Bemerkung: Man beachte, daß bei Verwendung des Gleichheitszeichens dieses im Gegensatz zum sonst üblichen Gebrauch von links nach rechts zu lesen ist. Beispiele: h (x) ≡ x + 3 i (x) ≡ 4 * x + 6 3 j (x) ≡ 251 * x + 10 12 *x ∈ O (x) ∈ O (x) 3 ∈ 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) Bemerkungen zur O-Notation: (i) Die O-Notation eliminiert additive Terme. 2 2 O (x + 6) = O (x ) O (9 + 1) = O (1) (ii) Die O-Notation eliminiert multiplikative Konstanten. O (17 * x) = O (x) 100 2 2 O (2 * x ) = O (x ) (iii) 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) Einige Rechenregeln der O-Arithmetik: Seien f und g Funktionen und c > 0 eine Konstante. (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. Beispiele für Dominanzen: x x dominiert x! 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 2 Wachstum von x : x 1 10 100 1000 10000 100000 1000000 10000000 100000000 1000000000 2 x 1 100 10000 1000000 100000000 10000000000 1000000000000 100000000000000 10000000000000000 1000000000000000000 Wachstum wichtiger Funktionen: 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 Die Sortieraufgabe: Gegeben: Gesucht: Unsortierte Folge U. Sortierte Folge S mit S ist Permutation von U. Zerlegungsprinzip: 1. Die unsortierte Folge U wird in zwei Teilfolgen Folge1 und Folge2 zerlegt. 2. Folge1 und Folge2 werden getrennt sortiert. 3. Die sortierten Teilfolgen Folge1 und Folge2 werden zur sortierten Gesamtfolge S zusammengefaßt. Mögliche Randbedingungen: (i) Folge2 enthält genau ein Element. Dies führt auf das Einfüge-Sortieren, eine Verfeinerung des EinfügeSortierens ist das Sortieren nach D. Shell. (ii) Die sortierten Teilfolgen Folge1 und Folge2 enthalten etwa gleich viele Elemente, die Zusammenfüge-Operation ist das Mischen. (iii) Alle Elemente der Folge1 sind kleiner oder gleich den Elementen der Folge2, das Zusammenfügen der beiden Folgen ist in diesem Fall trivial. Man erhält den Quicksort-Algorithmus. 2 5 5 9 5 9 9 5 9 6 6 2 Mischen 9 2 6 Mischen 5 2 6 Teilen 2 6 Teilen Mischsortieren: Die Idee des Mischsortierens zeigt das folgende Bild. Illustration des Mischsortierens: Beispielfolge: 48, 41, 19, 82, 25, 46, 39, 97, 68, 73. Zerlegungsphase: 48 41 19 82 25 46 39 97 68 73 48 41 19 82 25 46 39 97 68 73 48 41 19 82 25 46 39 97 68 73 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. Mischphase: 41 48 19 82 19 41 48 25 25 82 19 25 41 48 82 39 46 97 68 39 46 97 73 68 73 39 46 68 73 97 19 25 39 41 46 48 68 73 82 97 Aufwandsabschätzung für das Mischsortieren: Annahme: Dominante Operation ist der Vergleich zweier Elemente. 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)). // Algorithmusgerüst für // das Einfüge-Sortieren 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: Unsortierte Folge: 82 84 6 9 41 88 60 26 67 44 96 69 69 52 4 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 Aufwand beim Einfüge-Sortieren: Fall 1: 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 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 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 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: 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. 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. i+1 // Algorithmusgerüst zu Heapsort 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 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: Unsortierte Folge: 29 49 66 53 11 35 66 41 35 50 90 47 88 59 19 82 29 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 Bemerkungen zu Heapsort: 1. Der vollständige binäre Baum der Höhe h enthält 2 h+ 1 − 1 Knoten. 2. Die Summe der Höhen aller Knoten eines vollständigen binären Baumes ist 2 h + 1 − 1 − ( h + 1) . 3. Die Bildung des Anfangshaufens ist von linearem Aufwand, dies folgt aus Bem. 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 = ( 2 h + 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 swap (int& i, int& j) { int h = i; i = j; j = h; }//swap 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 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); } }//qsort void quicksort (int a [], int laenge) { qsort (a, 0, laenge-1); einfsort (a, laenge); }//quicksort Beispiel zu Quicksort bei Cutoff von 3: Unsortierte Folge: 1 23 1 63 64 82 38 88 2 64 27 60 9 37 38 7 55 84 96 50 8 61 rekursive Partionierungsgrenzen: links = 0 rechts = 21 links = 0 rechts = 6 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 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 Sortierte Folge: 1 1 2 7 8 9 23 27 37 38 38 50 55 60 61 63 64 64 82 84 88 96 Aufwand bei Quicksort: Annahme: n = Länge der Folge k + 1 = Position des Trennelementes Für den erwarteten Aufwand A (i), i ≥ 1 gelten die Rekursionsgleichungen A (0) = 0 A (1) = 0 A (n) = (n – 1) + A (k) + A (n – k – 1) (n ≥ 2, 0 ≤ k < n) Der erwartete Aufwand läßt sich auch berechnen mittels: n −1 A (n ) = ∑ k =0 1 ∗ (n − 1 + A (k ) + A (n − k − 1)) n 1 n −1 = (n − 1) + ∗ ∑ ( A (k ) + A (n − k − 1)) n k =0 2 n −1 = (n − 1) + ∗ ∑ A (k ) n k =0 Daher auch: n−2 2 ∗ ∑ A (k ) A (n − 1) = (n − 2) + n −1 k =0 Damit: n −1 n ∗ A (n ) = n ∗ (n − 1) + 2 ∗ ∑ A (k ) k =0 n−2 (n − 1) ∗ A (n − 1) = (n − 1) ∗ (n − 2) + 2 ∗ ∑ A (k ) k =0 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 Fortwährendes Einsetzen liefert: n B (n ) < B (1) + ∑ k=2 n = 2∗ ∑ k =2 2 k 1 k Da n 1 1 ≈ ∫ dx = ln (n ) − ln ( 2) ∑ k k=2 2 x n 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.