2.2 Effiziente Sortieralgorithmen: Merge-, Quick-, Heapsort
Ich hoffe, die Diskussion am Ende des letzten Abschnitts hat gezeigt: um große Datenmengen sortieren zu können, brauchen wir nicht unbedingt schnellere Rechner sondern bessere Algorithmen. SelectionSort und InsertionSort sind vom Prinzip her einfach zu langsam.
MergeSort
Der einfachste "effiziente" Sortieralgorithmus ist wahrscheinlich MergeSort.
Er funktioniert nach folgendem Prinzip: teile das Array in zwei
Teile, die jeweils ungefähr
mergesort(array): left = array[0..n/2] right = array[n/2..n] leftSorted = mergesort(left) rightSorted = mergesort(right) return merge(leftSorted, rightSorted)
Wie aber fügen wir zwei sortierte Listen (bzw. Arrays)zu einer sortierten zusammen? Wir vergleichen die jeweils ersten Elemente von array1 und array2; das kleinere davon muss nun an den Anfang der Gesamtliste verschoben werden. Dann wiederholen wir diesen Schritt bis eine der beiden Listen leer ist. Hier sehen Sie eine Animation:























Hier sehen Sie meinen Pseudocode für merge:
merge(array1, array2):
if array1 empty:
return array2
else if array2 empty:
return array1
else if array1[0] < array2[0]:
return array1[0] + merge(array1[1..], array2)
else:
return array2[0] + merge(array1, array2[1..])
Die Schreibweise array1[1..]
bezeichnet hier das
Subarray von array1
, von dem wir das erste Element entfernt haben.
merge
in Elm: merge : List comparable -> List comparable -> List comparable
Elm ist dafür gebaut, mit Listen zu arbeiten, und somit ist es relativ
einfach, merge
in Elm zu implementieren. In Java
würde ich hier empfehlen, auf Rekursion zu verzichten und mit
drei Pointern zu arbeiten: array1
bzw. array2
zeigen und
time java MergeSort < german-50000.txt > /dev/null
Auf meinem Rechner benötgit dies ca 0,7 Sekunden, ist also rund vierzigmal schneller als InsertionSort.
Komplexitätsanalyse
Die Laufzeitanalyse von MergeSort ist deutlich herausfordernder als die von SelectionSort und InsertionSort. Beginnen wir mit der Subroutine merge.
array1
und array2
zwei sortierte Arrays der Länge
merge(array1, array2)
im Besten Fall und im schlechtesten Fall
ausführt.
mergesort
;
schreiben Sie zu jedem Knoten in diesem Baum, wieviele
Vergleiche der dortige Aufruf von merge
durchführt.
mergesort
auf einem
Array der Größe
Seien Sie so präzise, wie Sie können! Geben Sie möglichst
genau an, wieviele Vergleiche mergesort
im Worst-Case durchführt.
merge(array1, array2)
tätigt höchstens array1
plus Länge von array2
).
Wie können wir von hier aus die maximale Anzahl von Vergleichsoperationen in einem
Aufruf von mergeSort(array)
bestimmen?
Da MergeSort ein rekursiver Algorithmus ist, bietet es sich immer an, seinen Rekursionsbaum
zu skizzieren. Für ein Array der Größe 14 sähe der zum Beispiel so aus:
Jedes graue Rechteck stellt einen Aufruf von mergesort
dar, und die
Zahl darin ist die Größe des Arrays bei diesem Aufruf. Die Anzahl der Vergleichsoperationen,
die merge
tätigt, um zwei sortierte Arrays der Größen
Die insgesamte Anzahl der Vergleichsoperationen, die ein Aufruf von
mergesort(array)
nach sich zieht, ist also die Summe der roten Zahlen.
A
ein Array aus mergesort(A)
maximal
Für
mergesort(array)
liegt in
Ebene 0).
Die unterste Ebene, also Ebene 4, ist unvollständig, weil es bereits auf Ebene 3 Arrays mit nur
einem
Element gibt. Fragen wir uns also: Wieviele vollständige Ebenen gibt es?
Eine erste Beobachtung ist
Die letzte vollständige Ebene ist also die, wo zuerst Arrays der Größe 1 auftreten; denn die
haben dann keine Kinderknoten im Baum mehr. Eine 1 wird zum ersten Mal in Ebene
In der Vorlesung Algorithmen und Komplexität im dritten Semester werden Sie weitere Sortieralgorithmen kennenlernen, z.B. QuickSort und HeapSort. Beide führen ähnlich viele Vergleichsoperationen aus wie MergeSort.