Aus den obigen Ideen den vollständigen Beweis zu formulieren, ist eine gute Übungsaufgabe!
Laufzeitanalyse von Bubblesort
Ein Blick auf den Pseudocode verrät uns, dass die äußere for-Schleife auf jeder Instanz mit \(n\) Elementen mindestens einmal ausgeführt wird. Jeder Durchlauf der äußeren for-Schleife bewirkt immer einen kompletten Durchlauf der inneren for-Schleife, wobei die Anzahl der Durchläufe der inneren Schleife mit der Anzahl der Durchläufe der äußeren Schleife abnimmt. Genauer gilt, dass im \(i\)-ten Durchlauf der äußeren Schleife die innere Schleife genau \(n-i\)-mal durchlaufen wird.
Im besten Fall (Bestcase), falls die Instanz bereits aufsteigend sortiert ist, dann wird die äußere for-Schleife nur genau ein mal durchlaufen. Die innere for-Schleife läuft dabei genau \(n-1\)-mal und hat pro Durchlauf konstante Kosten. Dies ergibt insgesamt Kosten von \(\Theta(n)\).
Im schlimmsten Fall (Worstcase), falls die Instanz zu Beginn umgekehrt, d. h. absteigend, sortiert ist, dann erfolgen im \(i\)-ten Durchlauf der äußeren for-Schleife genau \(n-i\) viele Durchläufe der inneren for-Schleife mit jeweils konstanten Kosten. Der \(i\)-te Durchlauf der äußeren for-Schleife kostet somit \(c\cdot (n-i)\), wobei \(c\) hier eine Konstante ist. Insgesamt, über alle Durchläufe der äußeren for-Schleife aufsummiert, ergeben sich Kosten von \[\sum_{i=n-1}^{1}c\cdot (n-i) = c \cdot \sum_{i=n-1}^{1}(n-i) = c \cdot \sum_{i=1}^{n-1} i = c \cdot \frac{n(n-1)}{2} \in \Theta(n^2).\]
Selectionsort
Selectionsort basiert auf einer iterativen Suche nach dem Minimum. Dabei wird zu Beginn das kleinste Element bestimmt und an die erste Stelle getauscht. Danach wird im restlichen Array wieder das Minimum gesucht und an die zweite Stelle getauscht usw. Dieser Prozess bricht ab, sobald es nur noch ein Element im restlichen Array gibt.
Wieder betrachten wir, was auf unserer Beispielinstanz passiert. Zu Beginn wird das Minimum der Instanz ermittelt (blau markiert).
Das Minimum wird direkt an die erste Stelle getauscht und somit erhalten wir einen korrekt sortierten Bereich der Größe \(1\) am linken Ende der Folge. Nun wird das Minimum unter allen anderen Elementen bestimmt.
Nach dem Tausch an die zweite Position wird wiederum das Minimum unter den restlichen Karten (d. h. Karten 3 bis 8) bestimmt.
Nach dem Tausch an die dritte Position wird das Minimum unter Karten 4 bis 8 bestimmt.
Nach dem Tausch an die vierte Position wird das Minimum unter Karten 5 bis 8 bestimmt.
Es wird getauscht und das Minimum unter Karten 6 bis 8 bestimmt.
Wieder wird getauscht, danach das Minimum unter Karten 7 bis 8 bestimmt.
Nach einem weiteren Tausch sind wir fertig, da im restlichen Array nur noch ein Element (nämlich das Maximum der Instanz) übrig ist und dieses auch bereits an der richtigen Stelle steht.
Betrachten wir den Pseudocode von Selectionsort (Achtung, die Indizierung von Array \(A\) beginnt bei \(0\)):
function SelectionSort(A) {
for i := 0 to n-2 do {
currentMinIndex := i;
for j := i+1 to n-1 do {
if A[currMinIndex] > A[j] then currMinIndex := j;
}
temp := A[i];
A[i] := A[currMinIndex];
A[currMinIndex] := temp;
}
}
Korrektheit von Selectionsort
Die Korrektheit folgt aus der Invariante, dass nach Runde \(i\) bei Selectionsort die \(i\) kleinsten Elemente aufsteigend sortiert an den Positonen \(0\) bis \(i-1\) im (\(0\)-indizierten) Array stehen.
Die Invariante kann leicht per Induktion gezeigt werden:
Induktionsanfang: In der ersten Runde von Selectionsort wird das Minimum aus allen Elementen im Array ermittelt und an Indexposition \(0\) getauscht.
Für den Induktionsschritt nehmen wir an, dass nach der \(i\)-ten Runde die \(i\) kleinsten Elemente aufsteigend sortiert an den Positionen \(0\) bis \(i-1\) stehen. Selectionsort bestimmt dann aus den verbleibenden Elementen, d. h. den Elementen an Positonen \(i\) bis \(n-1\), das Minimum und tauscht es an die Position \(i\). Nach Annahme muss dieses Element größer (oder gleich) als jedes Element an Positionen \(0\) bis \(i-1\) sein und da es das Minimum der Elemente von Postionen \(i\) bis \(n-1\) ist, muss es folglich das \(i+1\)-kleinste Element der Gesamtfolge sein. Nach dem Tausch befinden sich somit die \(i+1\) kleinsten Elemente aufsteigend sortiert an den Postionen \(0\) bis \(i\) im Array. \(\Box\)
Laufzeitanalyse von Selectionsort
Die äußere for-Schleife wird auf jeder Instanz mit \(n\) Elementen genau \(n-1\)-mal durchlaufen und jedes Mal findet eine Vertauschung statt. Jeder Durchlauf der äußeren for-Schleife bewirkt immer einen kompletten Durchlauf der inneren for-Schleife. Für die Anzahl der Durchläufe der inneren Schleife gilt dabei, dass im \(i\)-ten Durchlauf der äußeren Schleife die innere Schleife genau \(n-i\)-mal durchlaufen wird.
Somit entsehen für jede Instanz mit \(n\) Elementen genau dieselben Kosten. Der \(i\)-te Durchlauf der äußeren for-Schleife kostet somit auf jeder Instanz \(c\cdot (n-i)\), wobei \(c\) hier eine Konstante ist. Insgesamt, über alle Durchläufe der äußeren for-Schleife aufsummiert, ergeben sich Kosten von \[\sum_{i=1}^{n-1}c\cdot (n-i) = c \cdot \sum_{i=1}^{n-1}(n-i) = c \cdot \sum_{i=1}^{n-1} i = c \cdot \frac{n(n-1)}{2} \in \Theta(n^2).\]
Insertionsort
Insertionsort arbeitet wie folgt: Vergleiche die von links her ersten beiden Karten und tauschen die kleinere nach links. Damit entsteht ein sortierter Bereich, welcher zwei Karten umfasst. Füge jede weitere Karte an die entsprechende Stelle im bereits sortierten Bereich ein. Hierzu vergleiche jeweils von rechts nach links mit allen Karten im aktuellen sortierten Bereich und vertausche, falls nötig. Falls keine Vertauschung erfolgt ist, dann ist die aktuelle Karte korrekt einsortiert.
Wir betrachten, was an unserem Beispiel passiert.
Zu Beginn haben wir einen aktuell sortieren Bereich der Größe \(1\) vorliegen, welcher nur aus der linkesten Karte besteht. Der jeweils aktuell sortierte Bereich ist grün markiert. Als ersten werden die ersten beiden Karten verglichen:
Es muss getauscht werden. Danach haben wir einen aktuell sortierten Bereich der Größe \(2\) und in diesen wird nun die dritte Karte von rechts her eingeordnet. D. h., als Nächstes wird Pik 10 mit Karo Ass verglichen:
Es wird getauscht und danach folgt der Vergleich mit der linkesten Karte.
Danach haben wir den aktuell sortierten Bereich auf Größe \(3\) erweitert:
Nun wird die vierte Karte nach dem selben Prinzip einsortiert. Dabei erfolgen nur die Vergleiche Pik 10 vs. Herz 7 und Herz 7 vs. Karo Ass. Danach erhalten wir:
Die nächsten drei Karten werden mit jeweils einem Vergleich einsortiert und danach haben wir folgende Situation:
Nun wird noch die letzte Karte einsortiert. Nach den Vergleichen Kreuz 7 vs. Kreuz Ass, Kreuz 7 vs. Kreuz König und Kreuz 7 vs. Kreuz 9 haben wir:
Fazit: Insertionsort vergrößert nach und nach den sortierten Bereich, in dem es die korrekte Position für das jeweils neu einzufügende Element von rechts her mittels linearer Suche ermittelt.
Betrachten wir den Pseudocode von Insertionsort (Achtung, die Indizierung von Array \(A\) beginnt bei \(0\)):
function InsertionSort(A){
for i := 1 to n-1 do{
j := i;
aktuellesElement := A[j];
while A[j-1] > aktuellesElement and j > 0 do{
A[j] := A[j-1];
j := j-1;
}
A[j] := aktuellesElement;
}
}
Korrektheit von Insertionsort
Übungsaufgabe. Analog zum Korrektheitsbeweis von Selectionsort.
Laufzeitanalyse von Insertionsort
Ein Blick auf den Pseudocode verrät uns, dass die for-Schleife auf jeder Instanz mit \(n\) Elementen genau \(n-1\)-mal durchläuft. Bei der while-Schleife hingegen hängt die Anzahl der Durchläufe davon ab, wie viele größere Elemente links vom aktuell betrachteten Element stehen.
Im besten Fall (Bestcase), falls die Instanz bereits aufsteigend sortiert ist, dann fällt pro Ausführung der while-Schleife nur genau ein Vergleich an. Somit hat in diesem Fall ein Durchlauf der for-Schleife nur konstante Kosten und wir haben insgesamt Kosten von \(\Theta(n)\).
Im schlimmsten Fall (Worstcase), falls die Instanz zu Beginn umgekehrt, d. h. absteigend, sortiert ist, dann erfolgen im \(i\)-ten Durchlauf der for-Schleife genau \(i\) viele Vergleiche und \(i\) viele Vertauschungen in der zugehörigen while-Schleife. Der \(i\)-te Durchlauf der for-Schleife kostet somit \(c\cdot i\), wobei \(c\) hier eine Konstante ist. Insgesamt, über alle Durchläufe der for-Schleife aufsummiert, ergeben sich Kosten von \[\sum_{i=1}^{n-1}c\cdot i = c \cdot \sum_{i=1}^{n-1}i = c \cdot \frac{n(n-1)}{2} \in \Theta(n^2).\]
Was haben wir gelernt?
- Wir haben drei grundlegende, jedoch stark verschiedene Sortierverfahren kennengelernt, deren Korrektheit bewiesen und deren Laufzeit analysiert.
- Alle betrachteten Verfahren verfolgen das Prinzip, dass eine anfangs kleine sortierte Teilfolge nach und nach zu einer größeren sortierten Teilfolge erweitert wird.
- Insertionsort erreicht dies, indem neue Elemente in die bereits bestehende sortierte Teilfolge einsortiert werden.
- Bubblesort vertauscht jeweils rundenweise alle benachbarten Elemente. Dabei steigen die großen Elemente ähnlich wie Blasen im Wasser nach oben. Somit bildet sich eine sortierte Teilfolge der größten Elemente und diese wird sukzessive erweitert, indem weitere kleinere Elemente zu dieser Folge aufsteigen.
- Selectionsort sucht jeweils iterativ das Minimum der verbleibenden Elemente und tauscht dies an die kleinste noch verfügbare Position im Array.
- Insertionsort und Bubblesort haben im besten Fall nur lineare Laufzeit und im schlimmsten Fall quadratische Laufzeit. Selectionsort hingegegen benötigt unabhängig von der Instanz immer quadratische Laufzeit.