6 Aufwandsbetrachtungen am Beispiel des Sortierens 6.1

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