« MIME Type in Delphi ermitteln | Home | Einsteigertutorium zu Ruby »

Sortieralgorythmen

Von Bastian in Allgemeine Artikel | 1.August 2007

Hiermit leite ich ein allgemeineres Tutorial ein, welches seinen Schwerpunkt auf Sortieralgorithmen richtet.
Die Aufgabe irgendwelche Daten nach einer logischen Reihenfolge zu sortieren ist ein Problem, dass sicherlich bereits vielen Programmierern vertraut sind.

In die meisten Programmiersprachen sind Funktionen wie sort() bereits in den Standard Bibliotheken enthalten. Diese erfüllen ihre Aufgabe im Regelfall völlig ausreichend, aber was ist, wenn man etwas anders machen möchte, man beispielsweise keinen Speicherplatz verschwenden darf, oder einem die Bibliotheken nicht zur Verfügung stehen (oder man einfach neugierig ist zu erfahren was unter der Haube passiert)?

Ich werde mich in diesem Tutorial auf Iterative und Rekursive Versionen der Algorithmen beschränken (ich bin mit funktionaler und deduktiver Programmierung nicht sonderlich vertraut). Bevor ich nun die üblichen Algorithmen zum Sortieren vorstellen, insbesondere ihre Idee und eventuell etwas Code (in verschiedenen Sprachen).
Im folgenden sei angenommen, das wir eine Liste (also eine Reihe von Werten, auch Feld oder Array genannt) von Elementen, bei denen eine eindeutige Reihenfolge definiert ist, sortieren wollen.

Selection Sort

Die Idee hinter Selection Sort ist, dass wir immer das Minimum (respektive Maximum) der Liste, die wir sortieren wollen ermitteln und dieses dann am vordersten Platz einfügen. Dieser vorderste Platz wandert durch die Liste durch, somit wird die Liste in aufsteigender (respektive absteigender) Reihenfolge sortiert. Hier nun eine kleine Implementation in C++:

  1. void selSort(vector< int >& arr){
  2. for(int i = 0; i < arr.size(); i++){
  3. int min = i;
  4. for(int j = i + 1; j < arr.size(); j++){
  5. if(arr[min] > arr[j])
  6. min = j;
  7. }
  8. int t = arr[i];
  9. arr[i] = arr[min];
  10. arr[min] = t;
  11. }
  12. }

Insertion Sort

Wenn man sich eine Liste, die man sortieren möchte aufgeteilt vorstellt. In einen sortierten und einen unsortierten Teil. Wir nehmen nun das erste Element des unsortierten Teils und schieben es solange in den sortierten Teil hinein, bis es seine richtigig einsortierte Position erreicht hat. Dieser Vorgang wird wiederholt, bis die gesamte Liste sortiert ist (diesmal in Java):

  1. void insSort(int[] arr){
  2. for(int i = 0; i < arr.length - 1; i++){
  3. for(int j = i + 1; j > 0 && arr[j] < arr[j-1]; j--){
  4. int t = arr[j - 1];
  5. arr[j - 1] = arr[j];
  6. arr[j] = t;
  7. }
  8. }
  9. }

Quicksort

Quicksort ist ein wenig schwieriger zu verstehen als die, die vorher implementiert wurden. Wir suchen in unserer Liste nach einer Position, wo links von dieser Position alle Elemente kleiner sind als ein bestimmtes Pivot-Element (Pivot ist das Element an dem wir uns orientieren) und wo rechts davon alle Elemente größer sind.

Dann wird Quicksort rekursiv auf diese neuen Listen angewand, solange bis eine Liste nurnoch ein Element hat, denn dann ist die Liste trivialerweise sortiert. Wie man durch kurzes Überlegen sicherlich schnell bemerkt ist die Wahl des Pivot-Elementes kritisch für die Qualität dieses Algorithmus. Das einfachste wäre das Element in der Mitte der Liste zu nehmen, aber es gibt viele verschiedene Möglichkeiten.

Hier jetzt eine Beispielimplementation in C++ unter Verwendung von templates (damit der Algorithmus nicht nur auf einer Art von Daten läuft)(In meinem Code ist func eine übergebene Vergleichsfunktion, die 0 bei Gleichheit, -1 wenn der erste Wert kleiner, 1 wenn der erste Wert größer als der zweite ist):

  1. template <class T> class QuickSort{
  2. public:
  3. void operator()(T* start, T* end, int (*func)(T, T)){
  4. if(start < end){
  5. T median = *(start + ((end - start) / 2 / sizeof(T)));
  6. T *r =end, *l = start;
  7. while(l <= r){
  8. while(func(*l, median) == -1) l++;
  9. while(func(*r, median) == 1) r--;
  10. if(func(*l, *r) == 1) swap(r, l);
  11. l++;
  12. r--;
  13. }
  14. (*this)(start, r, func);
  15. (*this)(l, end, func);
  16. }
  17. }
  18.  
  19. void swap(T* a, T* b){
  20. T tmp = *a;
  21. *a = *b;
  22. *b = tmp;
  23. }
  24. };

Heapsort

Die Idee hinter Heapsort ist äußerst elegant, wenn auch etwas schwierig zu verstehen, denn vorher muss man erst noch einen ADT (Abstrakten DatenTyp) definieren. In diesem Fall nehmen wir den ADT Max-Heap. Ein Max-Heap ist ein binärer Baum (was ein Baum ist spare ich mir für ein späteres Tutorial), für den gilt, dass die Kinder eines Knotens K kleiner sind als K, die Kinder stehen untereinander in keinerlei Beziehung. Der Trick ist nun eine Liste, die nunmal linear ist intern als einen Max-Heap zu betrachten, bzw in einen zu verwandeln. Durch überlegen erhält man folgende Regeln:

  1. Das erste Element der Liste ist die Wurzel
  2. Das linke Kind hat den Index 2k, wobei k der index des Elternknotens ist
  3. Das rechte Kind hat den Index 2k+1
  4. Der Elternknoten eines knoten k hat den indes k/2 (abgerundet)

Das bedeutet, wir machen aus unserer Liste einen Max-Heap (d.h. das größte Element der Liste steht am Anfang) und tauschen dann das erste Element mit dem letzten unseres Max-Heaps und bauen wieder einen Max-Heap, diesmal aber um ein Element verringert. Das wird solange wiederholt, bis die Liste sortiert ist. Beispielimplementation ist in Ruby:

  1. class Heapsort
  2. def left(k)
  3. return 2*k
  4. end
  5.  
  6. def right(k)
  7. return 2*k+1
  8. end
  9.  
  10. def maxHeapify(arr, k)
  11. max = k
  12. max = left(k) if(left(k) <= @heapsize && arr[left(k)] > arr[max])
  13. max = right(k) if(right(k) <= @heapsize && arr[right(k)] > arr[max])
  14. if k != max
  15. arr[k], arr[max] = arr[max], arr[k]
  16. maxHeapify(arr, max)
  17. end
  18. end
  19.  
  20. def buildMaxHeap(arr)
  21. @heapsize = arr.length - 1
  22. (arr.length / 2).downto 0 do |i|
  23. maxHeapify(arr, i)
  24. end
  25. end
  26.  
  27. def heapsort(arr)
  28. buildMaxHeap(arr)
  29. (arr.length - 1).downto(1) do |i|
  30. arr[0], arr[i] = arr[i], arr[0]
  31. @heapsize -= 1
  32. maxHeapify(arr, 0)
  33. end
  34. end
  35. end

Mergesort

Mergesort ist nun unser erster Algorithmus, der nicht in-place sortiert, er braucht also zusätzlichen Speicher zum sortieren. Wir teilen die Liste, die wir sortieren wollen, in zwei Teile auf und zwar solange bis nurnoch Listen mit zwei Element vorhanden sind, dann werden die Listenteile sortiert zusammengefügt. Beispielimplementation ebenfalls in Ruby:

  1. def mergesort(arr, l, r)
  2. if l < r
  3. m = (r + l)/2
  4. mergesort(arr, l, m)
  5. mergesort(arr, m+1, r)
  6. merge(arr, l, m, r)
  7. end
  8. end
  9.  
  10. def merge(arr, l, m, r)
  11. left, right = Array.new, Array.new
  12. 0.upto(m-l){ |i| left[i] = arr[l+i] }
  13. 0.upto(r - (m + 1)) { |i| right[i] = arr[m+i+1] }
  14. j = i = 0
  15. l.upto(r) do |k|
  16. if right[j] == nil || (left[i] != nil && left[i] < right[j])
  17. arr[k] = left[i]
  18. i += 1
  19. else
  20. arr[k] = right[j]
  21. j += 1
  22. end
  23. end
  24. end

Countingsort

Countingsort ist ein Verfahren eine Liste in linearer Zeit zu sortieren, indem man einige Annahmen über die zu sortierenden Daten macht. Die zu sortierenden Zahlen müssen alle natürliche Zahlen zwischen 0, .., k mit k Element der Natürlichen Zahlen . Wir bestimmen für jede Zahl die Anzahl der Vorkommen, danach generieren wir aus der Liste mit der Anzahl der einzelnen Zahlen eine neue sortierte Liste. Z.B. würde in dem folgenden Code bei count[50] die Anzahl der Vorkommen der Nummer 50 gespeichert sein und so oft fügen wir die Nummer 50 in unsere Liste ein (natürlich erst, wenn sie dran ist, also nicht vor der 49 *g*). Beispielcode in Ruby:

  1. def countingsort(arr, max)
  2. count = Array.new(max, 0)
  3. 0.upto(arr.length - 1) { |i| count[arr[i]] += 1 }
  4. arr.clear
  5. 0.upto(count.length - 1) do |i|
  6. count[i].times{ |j| arr.push(i) }
  7. end
  8. end

Dies waren bei weitem nicht alle, aber die Grundlegensten. Nun noch zum Abschluss die Komplexität und Besonderheiten der Algorithmen in einer kurzen Übersicht (in-place bedeutet, dass der Algorithmus keinen extra Speicher zum sortieren benötigt, Je höher die Laufzeit, desto schneller wächst die Ausführungsdauer für mehr Elemente):

  1. Selectionsort: Quadratische Laufzeit (n²), in-place
  2. Insertionsort: Quadratische Laufzeit (n²), in-place
  3. Quicksort: Laufzeit (n log n) bis (n²), in-place
  4. Heapsort: Laufzeit (n log n), in-place
  5. Mergesort: Laufzeit (n log n), nicht in-place
  6. Countingsort: Lineare Laufzeit (n), nicht in-place

Literaturempfehlungen

  1. Cormen et al., Algorithmen - Eine Einführung, ISBN 3-486-27515-1, 1188 Seiten, EUR 69,80
  2. Bernd Owsnicki-Klewe, Algorithmen und Datenstrukturen, ISBN3-89639-172-0, 224 Seiten, EUR 15,80

Besondere Anmerkung des Authors: Jeglicher Code hier ist unter der GNU General Public License Version 2 oder später, freigegeben.

[Author Firewing | Diskussionsthread im Forum]

Kommentare