Bubblesort
Bubblesort
Bubblesort
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
Erstellt von Mario Mayr<br />
<strong>Bubblesort</strong><br />
<strong>Bubblesort</strong> bezeichnet einen einfachen, stabilen Sortieralgorithmus, der eine Reihe linear<br />
angeordneter Elemente (etwa Zahlen) der Größe nach anordnet.<br />
<strong>Bubblesort</strong> wird von Donald E. Knuth als vergleichsbasierter Sortieralgorithmus bezeichnet<br />
[1] . Das bedeutet, dass der Sortieralgorithmus sämtliche Entscheidungen alleine auf Basis des<br />
Größenvergleichs je zweier Elemente fällt, nicht etwa aufgrund der Inspektion der<br />
Binärdarstellung eines Elements.<br />
<strong>Bubblesort</strong> ist der einfachste solcher Algorithmen. Einzige Anforderung an den<br />
Vergleichsoperator ist, dass er eine Totalordnung der Liste ermöglicht.<br />
<strong>Bubblesort</strong> gehört zur Klasse der In-place-Verfahren, was bedeutet, dass der Algorithmus zum<br />
Sortieren keinen zusätzlichen Arbeitsspeicher außer den lokalen Laufvariablen der Prozedur<br />
benötigt.<br />
Wegen der einerseits mangelhaften Effizienz und anderseits breiten Verfügbarkeit deutlich<br />
besserer Alternativen wird <strong>Bubblesort</strong> meistens nur als Beispiel für einen schlechten oder<br />
naiven Sortieralgorithmus verwendet.<br />
Prinzip<br />
Der Algorithmus vergleicht der Reihe nach zwei benachbarte Elemente und vertauscht sie,<br />
falls sie in der falschen Reihenfolge vorliegen. Dieser Vorgang wird solange wiederholt, bis<br />
keine Vertauschungen mehr nötig sind. Hierzu sind in der Regel mehrere Durchläufe<br />
erforderlich.<br />
Je nachdem, ob auf- oder absteigend sortiert wird, steigen die größeren oder kleineren<br />
Elemente wie Blasen im Wasser (daher der Name) immer weiter nach oben, das heißt, an das<br />
Ende der Reihe. Auch werden immer zwei Zahlen miteinander in „Bubbles“ vertauscht.<br />
Schlimmster Fall:<br />
Umgekehrt sortierte Liste: <strong>Bubblesort</strong> Im Falle der umgekehrt sortierten Liste (n,n-1,...,2,1)<br />
werden maximal viele Vertauschungen ausgeführt: um das erste (und größte) Element n ganz<br />
nach rechts zu bewegen, werden n − 1 Vertauschungen vorgenommen.<br />
Laufzeit Θ(n 2 ) für Listen der Länge n<br />
Bester Fall<br />
Falls die Liste bereits sortiert ist wird <strong>Bubblesort</strong> die Liste nur einmal durchgehen um<br />
festzustellen, dass die Liste bereits sortiert ist, weil keine benachbarten Elemente vertauscht<br />
werden mussten. Daher benötigt <strong>Bubblesort</strong> Θ(n) Schritte um eine bereits sortierte Liste zu<br />
bearbeiten.<br />
Falls die Elemente der Liste bereits nah den Stellen sind, die sie nach der Sortierung<br />
bekommen sollen, ist die Laufzeit erheblich besser als Θ(n 2 ).
Erstellt von Mario Mayr<br />
Hasen und Schildkröten<br />
Die Positionen der Elemente vor dem Sortieren entscheiden maßgeblich den Sortieraufwand.<br />
Große Elemente am Anfang wirken sich nicht gravierend aus, da sie schnell nach unten<br />
getauscht werden, kleine Elemente am Ende bewegen sich jedoch eher langsam nach oben.<br />
Darum spricht man bei diesen Elementen von Hasen und Schildkröten.<br />
Beispiel<br />
Eine Reihe von fünf Zahlen soll aufsteigend sortiert werden.<br />
Die fett gedruckten Zahlen werden jeweils verglichen. Ist die linke größer als die rechte, so<br />
werden beide vertauscht; das Zahlenpaar ist dann blau markiert. Im ersten Durchlauf wandert<br />
somit die größte Zahl ganz nach rechts. Der zweite Durchlauf braucht somit die letzte und<br />
vorletzte Position nicht mehr zu vergleichen. --> Dritter Durchlauf: kein Vergleich<br />
letzte/vorletzte/vorvorletzte....<br />
55 07 78 12 42 1. Durchlauf<br />
07 55 78 12 42<br />
07 55 78 12 42<br />
07 55 12 78 42<br />
07 55 12 42 78 2. Durchlauf<br />
07 55 12 42 78<br />
07 12 55 42 78<br />
07 12 42 55 78 3. Durchlauf<br />
07 12 42 55 78<br />
07 12 42 55 78 4. Durchlauf<br />
07 12 42 55 78 Fertig sortiert.<br />
Formaler Algorithmus<br />
Der Algorithmus sieht im Pseudocode so aus:<br />
prozedur bubbleSort( A : Liste sortierbarer Elemente ) definiert als:<br />
n := Länge( A ) - 1<br />
wiederhole<br />
vertauscht := falsch<br />
für jedes i in 0 bis n wiederhole:<br />
falls A[ i ] > A[ i + 1 ] dann<br />
vertausche( A[ i ], A[ i + 1 ] )<br />
vertauscht := wahr<br />
falls ende<br />
für ende<br />
n := n - 1<br />
während vertauscht<br />
prozedur ende<br />
Quelle: Wikipedia
Erstellt von Mario Mayr<br />
Quicksort<br />
Quicksort ist ein schneller, rekursiver, nicht-stabiler Sortieralgorithmus, der nach dem<br />
Prinzip Teile und herrsche arbeitet. Der Algorithmus hat den Vorteil, dass er über eine sehr<br />
kurze innere Schleife verfügt (was die Ausführungsgeschwindigkeit stark erhöht) und ohne<br />
zusätzlichen Speicherplatz auskommt (abgesehen von dem für die Rekursion zusätzlichen<br />
benötigten Platz auf dem Aufruf-Stack).<br />
Eine zufällige Permutation des Umfangs n, in der jedes Element von 1 bis n lediglich einmal<br />
vorkommt, wird mit Quicksort sortiert.<br />
Prinzip<br />
Quicksort wählt ein Element aus der zu sortierenden Liste aus (Pivotelement) und zerlegt die<br />
Liste in zwei Teillisten, eine untere, die alle Elemente kleiner und eine obere, die alle<br />
Elemente gleich oder größer dem Pivotelement enthält. Diese Teillisten werden rekursiv<br />
sortiert. Anschließend wird das Ergebnis zusammengesetzt. Es besteht (in der Reihenfolge)<br />
aus der unteren Liste, dem Pivotelement und der oberen Liste.<br />
Eine direkte Implementation dieses Prinzips hat allerdings den Nachteil, dass sie zum einen<br />
zusätzlichen Speicher benötigt und zum anderen unnötig viele Operationen durchführt.<br />
Deshalb wird ein Verfahren verwendet, das als teilen oder auch partitionieren bezeichnet<br />
wird. Dieses arbeitet in-place.<br />
Hierbei wird nach der Wahl des Pivotelementes zunächst ein Element vom Anfang der Liste<br />
beginnend gesucht, das größer als (oder gleichgroß wie) das Pivotelement und damit für die<br />
untere Liste zu groß ist. Entsprechend wird vom Ende der Liste beginnend ein kleineres<br />
Element als das Pivotelement gesucht. Die beiden Elemente werden dann vertauscht und<br />
landen damit in der jeweils richtigen Liste. Der Vorgang wird fortgesetzt, bis sich die untere<br />
und obere Suche treffen. Auf diese Art und Weise entstehen die beiden Teillisten aus der<br />
Gesamtliste. Sie haben auch schon die richtige Position. Es müssen nur noch die Teillisten<br />
sortiert werden.
Erstellt von Mario Mayr<br />
Laufzeit<br />
Die Laufzeit des Algorithmus hängt im wesentlichen von der Wahl des Pivotelementes ab.<br />
Im Worst Case (schlechtesten Fall) wird das Pivotelement stets so gewählt, dass es das größte<br />
oder das kleinste Element der Liste ist. Dies ist etwa der Fall, wenn als Pivotelement stets das<br />
Element am Ende der Liste gewählt wird und die zu sortierende Liste bereits sortiert vorliegt.<br />
Die zu untersuchende Liste wird dann in jedem Rekursionsschritt nur um eins kleiner und die<br />
benötigte Rechenzeit ist asymptotisch genau n 2 .<br />
Im Best Case (bester Fall) wird das Pivotelement stets so gewählt, dass die entstehenden<br />
Teillisten beide gleich groß sind. In diesem Fall ist die Laufzeit des Algorithmus durch<br />
O(n·log(n)) nach oben beschränkt. Auch für den durchschnittlichen Fall gilt diese obere<br />
Schranke.<br />
Wahl des Pivot-Elemts:<br />
erstes, letztes oder mittleres Element wählen -> sehr ineffizient<br />
Median -> beste Variante aber aufwendiger (Effizientz = ?)<br />
Random -> einfach und effizient<br />
Der Worst Case tritt nur sehr selten auf und im mittleren Fall ist Quicksort schneller, da die<br />
innerste Schleife von Quicksort nur einige wenige, sehr einfache Operationen enthält.<br />
( Heute ist Quicksort für ein breites Spektrum von praktischen Anwendungen die bevorzugte<br />
Sortiermethode, weil er schnell ist und, sofern Rekursion zur Verfügung steht, einfach zu<br />
implementieren ist )<br />
Quicksort setzt jedoch voraus, dass effizient (d.h. mit Aufwand O(1)) über einen Index auf die<br />
Elemente zugegriffen werden kann. Dies ist jedoch meist nur bei Arrays der Fall
Erstellt von Mario Mayr<br />
Quicksort an einem Beispiel<br />
Um ein Array zu sortieren, wählt QuickSort zunächst eine spezielle Zahl im Array, die wir<br />
Pivot nennen wollen. Wie das Pivot gewählt wird, sehen wir uns später an. Dann werden alle<br />
Zahlen, welche kleiner als das Pivot sind, auf die linke Seite des Arrays gebracht, alle Zahlen,<br />
welche größer als das Pivot sind, werden auf die rechte Seite des Arrays gebracht. Als<br />
Beispiel betrachten wir das Array mit den Zahlen<br />
7 10 8 6 11 4 9 3 1 5 2 12<br />
Der Übersichtlichkeit halber haben wir die Zahlen kleiner als 7, welches wir als Pivotelement<br />
wählen, blau markiert. Die Zahlen größer 7 haben wir rot markiert. Dass die 7 gerade am<br />
Anfang des Arrays steht, betrachten wir zunächst als Zufall. Als ersten Schritt ändert<br />
QuickSort das Array nun folgendermaßen:<br />
3 2 5 6 1 4 7 9 11 8 10 12<br />
Nun sind alle Zahlen, welche kleiner sind als 7, links von ihr. Alle Zahlen welche größer sind<br />
als die 7, sind rechts von ihr. Dabei interessiert es uns im Moment gar nicht, in welcher<br />
Reihenfolge die Zahlen links und rechts von der 7 stehen. Alles was wir fordern, ist, dass alle<br />
blauen Zahlen links von der 7 sind, die roten Zahlen rechts von der 7.<br />
Als nächstes sortieren wir den Teil links von der 7 wieder mit QuickSort. Als Pivot wählen<br />
wir die 3. Wieder markieren wir die Zahlen kleiner als drei mit blau, die grösseren Zahlen mit<br />
rot. Die Zahl 7 und die grösseren Zahlen betrachten wir im Moment nicht, deshalb markieren<br />
wir sie gelb:<br />
3 2 5 6 1 4 7 9 11 8 10 12<br />
QuickSort bringt die blauen und die roten Zahlen wieder in die richtige Ordnung. Man erhält<br />
dann das Folgende:<br />
1 2 3 6 5 4 7 9 11 8 10 12<br />
Das wird fortgesetzt bis der linke Teil des Arrays sortiert ist. Das sieht dann wie folgt aus:<br />
1 2 3 4 5 6 7 9 11 8 10 12<br />
Nun wird rechts von der 7 sortiert. Wieder wird ein Pivot gewählt, hier die 9, und die<br />
kleineren und grösseren Zahlen auf die jeweils richtige Seite gebracht.
Erstellt von Mario Mayr<br />
Wir können nun bereits den grundsätzlichen Algorithmus mit PseudoCode beschreiben:<br />
procedure QuickSort( int[] Array, int L, int R )<br />
if ( L < R)<br />
{<br />
mitte := partitioniere(Array, L, R)<br />
QuickSort(Array, L, mitte-1)<br />
QuickSort(Array, mitte+1, R)<br />
}<br />
endprocedure<br />
Dabei wählt die Prozedur partitioniere ein Pivot, und bringt die Zahlen, die kleiner sind, auf<br />
die linke Seite von dem Pivot und die grösseren Zahlen auf die rechte Seite des Pivots. Sie<br />
partioniert das Array. Als Rückgabewert gibt partitioniere die neue Position des Pivots<br />
zurück.<br />
Wir müssen uns noch zwei Dinge überlegen:<br />
1. Wie partitionieren wir ein Array?<br />
2. Wie wählen wir das Pivot?<br />
Wir wenden uns zunächst der Wahl des Pivot-Elements zu.<br />
Wahl des Pivot-Elements (schon beschrieben in LAUFZEIT)<br />
Die Wahl des Pivotelements ist offensichtlich wichtig. Bei einer QuickSortAnalyse? bemerkt<br />
man, dass man am Besten das Element als Pivot wählt, welches am Schluss genau in der<br />
Mitte zu liegen kommt. In diesem Fall ist die Laufzeit von Quicksort O(n log(n)). Leider ist<br />
dieses Element nicht ganz einfach zu finden. Die schlechteste Wahl des Pivots ist das<br />
Element, welches ganz am linken oder ganz am rechten Rand des zu sortierenden Intervals<br />
endet. Falls man immer dieses Element wählt so ist die Laufzeit von Quicksort O(n^2), also<br />
wesentlich schlechter als O(n log(n)).<br />
Man sollte deshalb nicht immer das linke Element als Pivot wählen, so wie wir das in<br />
unseren Beispielen getan haben. Falls nämlich das Array bereits sortiert ist, oder nahezu<br />
sortiert ist, führt dies zu dem einem äußerst langsamen "QuickSort".<br />
Als Kompromiss wird oft ein zufälliges Element als Pivot-Element genommen. Eine<br />
DetaillierteQuickSortAnalyse? zeigt, dass wir dann erwarten können, dass QuickSort in O(n<br />
log(n)) läuft. Noch besser kann es sein, von drei zufällig gewählten Elementen jeweils das<br />
mittlere zu nehmen.<br />
In unserem Text fahren wir fort und nehmen das linke Element als Pivot, obwohl das in jeder<br />
praktischen Implementation schlecht ist.
Erstellt von Mario Mayr<br />
Partitionierung<br />
Wir wenden uns jetzt der Frage zu, wie man bei gewählten Pivot das Array partitioniert. Also<br />
der Frage: wie kommt man effizient von<br />
7 10 8 6 11 4 9 3 1 5 2 12<br />
auf<br />
2 5 6 1 4 3 7 9 11 8 10 12<br />
Die effiziente Implementation dieses Schrittes ist übrigens der Grund, weshalb QuickSort<br />
instabil sortiert. StabilesSortieren ist fast unmöglich mit QuickSort.<br />
Der Einfachheit halber nehmen wir an, dass das Pivot am linken Rand des Arrays steht. Falls<br />
dies nicht so ist, so kann man zuerst das linke Element des Arrays mit dem Pivotelement<br />
vertauschen.<br />
Die Partitionierung macht man nun üblicherweise folgendermaßen. Man verwendet zwei<br />
Zeiger, einen links und einen rechts auf das Array:<br />
7 10 8 6 11 4 9 3 1 5 2 12<br />
^ ^<br />
L R<br />
Nun geht man mit dem linken Zeiger soweit nach rechts, bis er auf eine rote Zahl zeigt. Mit<br />
dem rechten Zeiger geht man soweit nach links, bis er auf eine blaue Zahl zeigt. Danach sieht<br />
die Situation wie folgt aus:<br />
7 10 8 6 11 4 9 3 1 5 2 12<br />
^ ^<br />
L R<br />
Jetzt vertauscht man die Elemente auf welche die Zeiger zeigen:<br />
7 2 8 6 11 4 9 3 1 5 10 12<br />
^ ^<br />
L R<br />
immer: weiter und vertauschen<br />
...<br />
Und ein letzes Mal vertauschen:<br />
7 2 5 6 1 4 3 9 11 8 10 12<br />
^ ^<br />
L R<br />
Jetzt ziehen wir nochmals weiter (aufpassen, Reihenfolge!)<br />
7 2 5 6 1 4 3 9 11 8 10 12<br />
^<br />
LR<br />
Und stoppen hier, weil beide Zeiger auf dasselbe Element zeigen. Nun vertauschen wir noch<br />
das Pivotelement mit dem Element, auf welches gezeigt wird, und sind fertig:<br />
3 2 5 6 1 4 7 9 11 8 10 12<br />
^<br />
LR
Erstellt von Mario Mayr<br />
In PseudoCode könnte das Ganze so aussehen:<br />
procedure partitioniere( int[] Array, int L, int R )<br />
L0 := L // Wählt als Pivot-Element Array[L0]. Dies<br />
// ist für praktische Anwendungen<br />
// SCHLECHT !!! (siehe Wahl des<br />
// Pivot-Elements)<br />
While ( L < R )<br />
{<br />
if ( Array[R] > Array[L0] )<br />
{<br />
R := R – 1<br />
}<br />
Else<br />
{<br />
if ( Array[L]