Kapitel 6 Elementare Sortieralgorithmen Ziel: Kennenlernen elementarer Sortierverfahren und deren Effizienz Zur Erinnerung: Das Sortier-Problem Gegeben: Folge A von n Elementen a 1, a 2,..., a n ; Eine lineare Ordnungsrelation auf A gegeben durch < Gesucht: Eine Permuation Π der n Elemente, so dass eine Folge a, a Π(1) Π(2),... a Π(n) entsteht mit a < a Π(1) Π(2),<...< a Π(n) (aufsteigende Folge) oder a > a Π(1) Π(2),>...> a Π(n) (absteigende Folge) AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 1 Sortieren durch Vergleich Vergleich von je zwei Elementen (Vergleichsoperation) Anzahl der Schlüsselvergleiche ist untere Schranke für Laufzeit von Sortieralgorithmen: Laufzeit Sortieralgorithmus = O(Anzahl der Schlüsselvergleiche) Bekannte Sortierverfahren: (im Folgenden erklärt) Sortieren durch Tauschen: Selection Sort, O(n 2 ) Sortieren durch Einfügen, in-place Sortieren, O(n 2 ) Sortieren durch Mischen, nicht in-place, O(n log n) Quicksort, in-place, O(n 2 ) Heapsort, in-place, O(n log n) AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 2 6.1 Selection Sort Sei eine Folge a 1,, a n von Schlüsseln gegeben, wobei ein Schlüssel a i an der i-ten Position in einem Feld A[1.. N] abgespeichert ist. Der Algorithmus Selection Sort ist wie folgt gegeben: For i=n downto 2 do m= Index des maximalen Schlüssels in A[1..i] vertausche A[i] und A[m] Grundlegende Idee: Satz Der Algorithmus Selection Sort benötigt zum Sortieren von n Elementen genau Vergleiche. Laufzeit ist: O(n 2 ) Beweis: AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 3 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 4
6.2 Insertion Sort Sei eine Folge a 1,, a n von Schlüsseln gegeben. Der Algorithmus InsertionSort (vgl. Kapitel 3.2.1) arbeitet wie folgt: for j = 2 to n do key = A[ j ] // Füge A[ j ] in die sortierte Liste A[1.. j 1] ein. i = j 1 while i > 0 und A[ i ] > key do A[i + 1] = A[ i ] i = i 1 A[i + 1] = key Laufzeit: (Wiederholung) 6.3 Merge Sort Sei eine Folge a 1,, a n von Schlüsseln gegeben. Der Algorithmus Merge Sort ist wie folgt gegeben: MergeSort(A, p, r) if p < r then q = (p + r)/2 MergeSort(A, p, q) MergeSort(A, q + 1, r) Merge(A, p, q, r) mischen zweier sortierter Teilfolgen. Merge Sort sortiert ein Feld der Länge n mit maximal n log 2 n Vergleichen AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 5 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 6 Merge Sort Divide-and-Conquer-Prinzip Teile: die zu sortierende Folge von Elementen in zwei Teilfolgen mit n/2 Elementen auf. Beherrsche: Sortiere die zwei Teilfolge rekursiv bis die Teilfolge nur noch 1 Element umfasst; die Teilfolge ist sortiert. Verbinde: mische die beiden sortierten Teilfolgen Beispiel MergeSort(A,p, r) if p < r then q = (p + r)/2 MergeSort(A, p, q) MergeSort(A, q + 1, r) Merge(A, p, q, r) Divide Aufwand: Zusätzlicher Speicher der Länge der zu sortierenden Folge notw. Aufteilen ist einfach/schnell, Conquer (Merge) ist aufwändig. 0 4 5 6 3 9 2 8 1 7 0 4 5 6 2 3 8 9 1 7 0 2 3 4 5 6 8 9 1 7 Conquer 0 1 2 3 4 5 6 7 8 9 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 7 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 8
Algorithmus Merge(A, p, q, r) Für die Indizes p, q, r des Feldes A gilt: p q < r. Die Teilfolgen A[p.. q] und A[q + 1.. r] sind bereits sortiert. Als Ergebnis ergibt sich eine sortierte Teilfolge A[p.. r]. Die Laufzeit von Merge ist O(n), wobei n = r p + 1. Beispiel Merge-Algorithmus Schritte 10-17 von Merge beim Aufruf Merge(A, 9, 12, 16). Vgl Cormen AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 9 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 10 Beispiel Merge-Algorithmus (Forts.) Berechnen der Laufzeit von Merge-Sort: Zur Vereinfachung sei angenommen, dass n eine Potenz von 2 ist, also n = 2 k. Das Teilen führt dann zu Teilproblemen der Größe n/2. Berechnung der Laufzeit T(n): Teile, d.h. Ermittlung der Feld-Mitte, erfordert konstante Zeit, (i) Zeigt die Felder und Indizes nach der Terminierung; Teilfeld A[9.. 16] ist sortiert Beherrsche: 2 Teilprobleme der Größe n/2 werden rekursiv gelöst: Verbinde: das Mischen auf einem Teilfeld der Länge n erfordert AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 11 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 12
Rekursive Aufteilung in Teilprobleme: D(n): Zeit zum Aufteilen, ist konstant (Finde Mitte): O(1) C(n): Zeit zum Mischen: O(n) Rekursionsgleichung: O(1) falls n=1 T(n) = 2T(n/2) + D(n) + C(n) = 2T(n/2) + O(n) falls n> 1 Rekursionsbaum der Baum besitzt log n +1 Ebenen Jede Ebene trägt die Zeit cn zu den Gesamtlaufzeit T(n) bei Wir formen die Gleichung um zu c falls n=1 T(n) = 2T(n/2) + cn falls n> 1 Zur Lösung der Gleichung: Rekusionsbaum (siehe nächste Folie): AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 13 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 14 Die Zerlegung in Teilaufgaben terminiert, wenn nur noch einzelne Elemente vorhanden sind; Bei n=2 k zu sortierenden Elementen, muss man (log n = k)-mal aufteilen, d.h. wir erzeugen einen Rekursionsbaum mit log n Ebenen Jede Ebene trägt cn zu Gesamtkosten bei, also: cn log n + cn, also Aufwand zum Teilen: Ө(n log n) Zusammenfassung: Laufzeitverhalten von Merge: Das Mischen zweier sortierter Teilfolgen erfordert O(n) Schritte, mit n = r p + 1 Jede der Zeilen 1-3 und 8-11 erfordert konstante Zeit. Die for-schleifen (Zeilen 4-7) erfordern O(n 1 + n 2 ) = O(n). Teile: d.h. Ermittlung der Feld-Mitte, erfordert O(1). Beherrsche: rekursives Lösen der 2 Teilprobleme: 2T(n/2). Verbinde: das Mischen (s.o.) erfordert O(n). Insgesamt: T MergeSort (n) = O(n log n) AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 15 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 16
Mergesort (rekursiv) mergesort(int a[],int b[],int l,int r) { /* rekursiver mergesort algorithmus */ if (l<r-1) { int m = (l+r)/2; mergesort(a,b,l,m); mergesort(a,b,m,r); merge(a,b,l,m,r); copy(b,a,l,r); merge(int a[],int b[],int l,int m,int r) { for (int i=l,int j=m,int k=l; k<r; k++){ if ((j r) ((i<m) && (a[i] a[j]))) b[k] = a[i++]; else b[k] = a[j++]; copy(int b[],int a[],int l,int r) { for (int i=l; i<r; i++) a[i] = b[i]; AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 17 Mergesort (iterativ) mergesort(int a[],int b[],int n) { /* iterativer mergesort algorithmus */ for (int width=1; width<n; width*=2) { for (int i=0; i<n; i+=2*width) { int l = i; int m = min(n,i+width); int r = min(n,i+2*width); merge(a,b,l,m,r); copy(b,a,0,n); merge(int a[],int b[],int l,int m,int r) { for (int i=l, int j=m, int k=l; k<r; k++){ if ((j r) ((i<m) && (a[i] a[j]))) b[k] = a[i++]; else b[k] = a[j++]; copy(int b[],int a[],int l,int r) { for (int i=l; i<r; i++) a[i] = b[i]; AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 18 Lessons Learned: Positives an Insertion Sort: In-Place sortierender Algorithmus, d.h. zu jedem Zeitpunkt wird nur eine konstante Anzahl von Elementen des Feldes außerhalb des Eingabefeldes gespeichert. Innere Schleifen sind kurz, für kleine Eingaben ist es ein schnelles Verfahren, aber im Worst-Case wird O(n 2 ) benötigt. Positives an Merge-Sort gutes Laufzeitverhalten für große Eingabegrößen n: O(n log n) geschickter Einsatz der Teile-und-Herrsche Technik. Gesucht: Ein Algorithmus, der in-place sortiert und O(n log n) benötigt! AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 19 6.4 Quicksort (C.A.R Hoare 1962) Idee: Gegeben Feld A[p.. r] wähle ein Element q aus A[p.. r] als Vergleichs-Element (Pivot) aus. Mögliche Wahl des Pivots? In-Place-Sortierung mit Divide-and-Conquer-Verfahren: Teile: Zerlege A[p.. r] in zwei Teilfelder A[p.. q 1] und A[q + 1.. r] so, dass gilt: Für alle i in A[p.. q 1]: A[i] A[q] und für alle j in A[q + 1.. r]: A[q] A[j] < q q > q A[p.. r] AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 20
Beherrsche: Sortieren der Teilfelder A[p.. q 1] und A[q + 1.. r] durch rekursiven Aufruf von Quicksort Verbinde: da in-place sortiert, kein Schritt notwendig, das Feld A[p.. r] ist sortiert. Damit ergibt sich der Algorithmus für Quicksort wie folgt: QUICKSORT (A, p, r) 1. if p < r 2. then q PARTITION (A, p, r) 3. QUICKSORT (A, p, q 1) 4. QUICKSORT (A, q + 1, r) Der Kern des Quicksort-Algorithmus ist der Algorithmus Partition, der das Feld A[p..r] in-place sortiert: PARTITION (A, p, r) 1. x A[r] 2. i p 1 3. for j p to r 1 4. do if A[j] x 5. then i i + 1 6 vertausche A[i] A[j] 7 vertausche A[i + 1] A[r] 8 return i + 1 4 Bereiche Java-Applet zur Veranschaulichung der Funktionsweise unter: http://www2.tcs.ifi.lmu.de/~gruberh/lehre/sorting/quick/quick.html AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 21 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 22 Beispiel: Algorithmus Partition auf einem 8-elementigen Feld (a) Pivot-Element x = A[r] = 4 PARTITION (A, p, r) 1. x A[r] 2. i p 1 3. for j p to r 1 4. do if A[j] x 5. then i i + 1 6 vertausche A[i] A[j] 7 vertausche A[i + 1] A[r] 8 return i + 1 Beispiel: Algorithmus Partition auf einem 8-elementigen Feld (a) Pivot-Element x = A[r] = 4 Die beiden Teilfolgen 4 und 4 werden schrittweise erstellt. (b) Erster Schritt: die erste Teilfolge enthält zunächst nur Element A[p] = 2, die anderen Elemente sind noch nicht einsortiert. (c) (d) Die größere Teilfolge wächst um die Elemente 8 und 7. (i) Pivot-Element wird abschließend zwischen die sortierten Teilfolgen eingefügt. AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 23 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 24
Eigenschaften des Algorithmus Partition Während der Ausführung des Algorithmus ist das Feld in vier (ggf. leere) Bereiche zerlegt. Für die for-schleife (Zeilen 3-6) definieren wir folgende Schleifeninvariante: Zu Beginn jeder Iteration der for-schleife gilt für jeden Feldindex k: 1. Für p k i gilt: A[k] x. 2. Für i + 1 k j 1 gilt: A[k] > x. 3. Für k = r gilt: A[k] = x. Zu zeigen: Die Invariante gilt vor der erste Iteration der for-schleife. Die Invariante bleibt auch nach jeder Iteration noch gültig. AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 25 Korrektheitsbeweis in 3 Schritten: (1) Initialisierung: vor der ersten Schleifeniteration gilt: i = p 1, j = p Es gibt keinen Wert zwischen p und i und keine Werte zwischen i + 1 und j 1. Die ersten beiden Bedingungen der Schleifeninvariante sind erfüllt. Mit der Zuweisung in Zeile 1 gilt auch die dritte Bedingung. PARTITION (A, p, r) 1. x A[r] 2. i p 1 3. for j p to r 1 4. do if A[j] x 5. then i i + 1 6 vertausche A[i] A[j] 7 vertausche A[i + 1] A[r] 8 return i + 1 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 26 Korrektheitsbeweis in 3 Schritten: (1) Initialisierung: vor der ersten Schleifeniteration gilt: i = p 1, j = p Es gibt keinen Wert zwischen p und i und keine Werte zwischen i + 1 und j 1. Die ersten beiden Bedingungen der Schleifeninvariante sind erfüllt. Mit der Zuweisung in Zeile 1 gilt auch die dritte Bedingung. (2) Fortsetzung: Nach dem Test in Zeile 4 sind zwei Fälle zu betrachten: Fall (a): A[j] > x, nur j wird inkrementiert, die Schleifeninvariante gilt weiterhin. Fall (b): A[j] x, Austausch der Elemente A[i] und A[j], die Schleifeninvariante ist erfüllt. Die zwei Fälle in einer Iteration des Algorithmus Partition: (3) Terminierung: bei Terminierung gilt: j = r, jedes Feldelement gehört zu einer der drei durch die Invariante beschriebenen Mengen. AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 27 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 28
6.5.2 Laufzeitverhalten von Quicksort Die Perfomanz von Quicksort hängt davon ab, wie gut die Zerlegung balanciert ist. im besten Fall ist Quicksort asymptotisch so schnell wie Sortieren durch Mischen, aber in-place und mit kleinen Konstanten-Werten im schlechtesten Fall kann Quicksort aber auch so langsam wie Sortieren mit Einfügen sein. (a) Best-Case Quicksort im Worst-Case: völlig unbalancierte Teilfelder Der schlechteste Fall ist, wenn die Zerlegung ein Teilfeld mit n 1 Elementen und eines mit 0 Elementen erzeugt (unbalanciert!). D.h. das Pivot-Element ist immer das größte oder immer das kleinste Element einer Teilfolge (k = n). (c) Average-Case (b) Worst-Case AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 29 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 30 Eine solche unbalancierte Zerlegung tritt bei jedem rekursiven Aufruf auf. Damit ergibt sich für die Laufzeit von Quicksort: Zerlegen erfordert O(n) und T(0) = O(1) (rekursiver Aufruf auf Feld der Größe 0 terminiert sofort) Rekursionsgleichung: T(n) = T(n 1) + T(0) + O(n) = T(n 1) + O(n) d.h. Aufsummieren der rekursiven Aufrufe: Quicksort benötigt zum Sortieren eines Feldes der Länge n maximal O(n 2 ) Vergleiche. AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 31 Quicksort besitzt im Worst-Case somit das gleiche Laufzeitverhalten wie Insertion-Sort. Zudem gilt: Quicksort benötigt eine Laufzeit von O(n 2 ) sogar dann, wenn das Feld bereits sortiert ist! Hier benötigt Insertion-Sort nur O(n). Quicksort im Best-Case: möglichst balancierte Zerlegung Der Algorithmus Partition erzeugt zwei Teilfelder der Größen n/2 bzw. n/2 1. man kann zeigen, dass in Best-Case gilt: T(n) = O(n log n). Quicksort im Average Case: es wird eine Mischung aus balancierten und unbalancierten Zerlegungen erzeugt. AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 32
diese Mischung aus guten und schlechten Zerlegungen beeinflusst jedoch nicht das asymptotische Gesamtverhalten. Quicksort benötigt auch im mittleren Fall O(n log n). Fazit: Entscheidend für die Laufzeit des Quicksort-Algorithmus ist eine gute Wahl des Pivot-Elementes Pragmatische Regeln für die Wahl des Pivot-Elementes: Nimm das letzte Element der (Teil)-Folge als Pivot Nachteil: sehr schlechte Performanz bei vorsortierten Feldern Wähle das mittlere Element (Median) des ersten, mittleren und letzten Elements der Folge Wähle zufälliges Element (liefert Average Case). AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 33 AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 34 Quicksort in C quicksort(int A[],int l,int r) { if (l<r) { int pivot = partition(a,l,r,r); if (pivot-l<r-pivot) { quicksort(a,l,pivot-1); quicksort(a,pivot+1,r); else { quicksort(a,pivot+1,r); quicksort(a,l,pivot-1); int partition(int A[],int l, int r, int pivot) { int i=l-1; j=r; /*left and right pointer*/ swap(a[pivot],a[r]); /*move pivot to the right end */ pivot=r; while(i<j) { do i++; while((i<j)&&(a[i] < A[pivot])); do j--; while((j>i)&&(a[j] > A[pivot])); if (i j) swap(a[i],a[pivot]); else swap(a[i],a[j]); return i; AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 35 Fazit: Bislang kennengelernt: Sortierverfahren, die auf Vergleichen von Werten basieren. Felder von n Elementen können in O(n log n) sortiert werden: dies ist für Merge-Sort und Heap-Sort im schlechtesten Fall eine obere Schranke. Quicksort erreicht sie im Mittel. Wir werden noch beweisen, dass Merge-Sort und Heap-Sort asymptotisch optimal sind. Es gibt kein anderes vergleichendes Sortierverfahren, das um mehr als einen Faktor c schneller ist. Aber: es gibt Nicht-vergleichende Sortierverfahren, deren Laufzeit linear ist: z.b. Radixsort, Bucketsort AuD,Kapitel 6 Sortieralgorithmen, WS10/11, C. Eckert 36