+ 2 * A

Werbung
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.
Herunterladen