Kapitel 6 Sortieren 6.1 Sortiermethoden Die Sortierung von Mengen von Datensätzen ist eine häufige algorithmische Operation auf Mengen bzw. Folgen von gleichartigen Datenobjekten (insbesondere in der betriebswirtschaftlichen Datenverarbeitung). Normalerweise gibt es einen Schlüsselbegriff nach dem sortiert wird, und zugehörige Attribute, die dem Schlüsselbegriff zugeordnet sind, d.h. man sortiert records bzw. Objekte. Im allgemeinen braucht man eine Prä-Ordnungsrelation (transitiv, reflexiv, total, nicht notwendig antisymmetrisch), die die Schlüsselbegriffe sortiert. Diese ist nicht automatisch gegeben. Z.B. ist die Sortierung von Texten, die im englischen / amerikanischen einfach auf der internen Darstellung durchgeführt wird, problematisch bei der Verwendung von Umlauten, da diese normalerweise aufgrund der internen Darstellung oft falsch einsortiert werden. Für die richtige Sortierung benötigt man die Definition einer passenden Ordnung auf den Zeichen, die z.B. ü zum u dazusortiert, d.h. t < u < ü < v, oder auch t < u ∼ ü < v, wie in einem Wörterbuch. Wir betrachten im folgenden das etwas vereinfachte Problem der Sortierung einer gegebenen Liste (Menge) von ganzen Zahlen. Wir betrachten hier vier typische Sortierverfahren. Sortieren durch Einfügen (Insertion Sort) fügt die restlichen Elemente nach und nach in die bereits sortierte Liste der abgearbeiteten Zahlen. Sortieren mit Bubble Sort Betrachtet jeweils zwei benachbarte Elemente und bringt diese in die richtige Reihenfolge. Der Vergleich läuft in zwei Schleifen: Die Innere startet mit den letzten beiden Elementen und schiebt das Vergleichsfenster nach vorne. Danach ist das kleinste Element am Anfang der Liste. Dieses Verfahren wird jeweils mit der kleineren Liste wiederholt. Man kann abbrechen, wenn sich in einem inneren Durchlauf nichts verändert hat. Quicksort Rekursives Verfahren, das das erste Element als Pivot nimmt, dann 1 Grundlagen der Programmierung 2, AS 2006, Kapitel 4, vom 17. Maerz 2006 2 die Liste zerlegt in eine Liste der kleineren und eine Liste der größeren Elemente, diese rekursiv sortiert, und dann einfach hintereinander hängt mit dem Pivot dazwischen. Mischsort (Merge Sort) Rekursives Verfahren, das die Liste in zwei Hälften zerlegt, diese rekursiv sortiert und dann wieder zusammenmischt. Es folgt die Implementierung der vier Sortierverfahren in Haskell. Hierbei wird die Kernidee der entsprechenden Verfahren demonstriert, nicht jedoch die Methoden der Speicherausnutzung wie in imperativen Programmiersprachen. Den Aspekt der Speicherverwaltung bei den Sortierverfahren kann man mittels Implementierung in Python anschaulich machen. In Haskell gibt es eingebaute Sortierfunktionen sort und sortBy im Modul List, so dass man bei Implementierungen auf diese zurückgreifen kann. ---Sortieren von Listen von Zahlen -- einfuegen sorteinfuegen xs = sorteinfuegenr xs [] sorteinfuegenr [] ys = ys sorteinfuegenr (x:xs) [] = sorteinfuegenr xs [x] sorteinfuegenr (x:xs) ys = sorteinfuegenr xs (sorteinfuegenr1 x ys) sorteinfuegenr1 x [] = [x] sorteinfuegenr1 x (y:ys) = if x <= y then x:y:ys else y : (sorteinfuegenr1 x ys) -- Insert-Sort: stabil und sorteinfuegeno xs = reverse sorteinfuegenor [] ys = ys sorteinfuegenor (x:xs) [] = sorteinfuegenor (x:xs) ys = O(n) f"ur vorsortierte: (sorteinfuegenor xs []) sorteinfuegenor xs [x] sorteinfuegenor xs (sorteinfuegenor1 x ys) sorteinfuegenor1 x [] = [x] sorteinfuegenor1 x (y:ys) = if x >= y then x:y:ys else y : (sorteinfuegenor1 x ys) -- Bubblesort bubblesort [] = [] bubblesort [x] = [x] bubblesort xs = let y:resty = bubblesort1 xs in y: (bubblesort resty) bubblesort1 [x] = [x] Grundlagen der Programmierung 2, AS 2006, Kapitel 4, vom 17. Maerz 2006 bubblesort1 (x:rest) = let (y:resty) = bubblesort1 rest in if x > y then y:x: resty else (x: y:resty) -- Bubblesort mit Optimierung bubblesorto [] = [] bubblesorto [x] = [x] bubblesorto xs = let (aenderung,y:resty) = bubblesorto1 xs in if aenderung then y: (bubblesorto resty) else xs bubblesorto1 [x] = (False,[x]) bubblesorto1 (x:rest) = let (aenderung, y:resty) = bubblesorto1 rest in if x > y then (True,y:x: resty) else (aenderung,x: y:resty) -- quicksort quicks [] = [] quicks [x] = [x] quicks [x,y] = if x <= y then [x,y] else [y,x] quicks (x:xs) = let (llt,lge) = splitlist x xs in (quicks llt) ++ (x: (quicks lge)) splitlist x y = splitlistr x y [] [] splitlistr x [] llt lge = (llt,lge) splitlistr x (y:ys) llt lge = if y < x then splitlistr x ys (y: llt) lge else splitlistr x ys llt (y:lge) -Versuch einer Optimierung durch variierte Wahl des Pivots. quicks2 [] = [] quicks2 [x] = [x] quicks2 [x,y] = if x <= y then [x,y] else [y,x] quicks2 (x:xs@(y:z:rest)) = if x >= y && x <= z || x >= z && x <= y then let (llt,lge) = splitlist x xs in (quicks2 llt) ++ (x: (quicks2 lge)) else quicks2 (z:x:y:rest) --- merge-sort mischsort xs = mergesort (length xs) xs mergesort _ [] = [] 3 Grundlagen der Programmierung 2, AS 2006, Kapitel 4, vom 17. Maerz 2006 4 mergesort _ [x] = [x] mergesort _ [x,y] = if x <= y then [x,y] else [y,x] mergesort len xs = let lenh = len ‘div‘ 2 in mische (mergesort lenh (take lenh xs)) (mergesort (len -lenh) (drop lenh xs)) mische [] ys = ys mische xs [] = xs mische (x:xs) (y:ys) = if x <= y then x: (mische xs (y:ys)) else y: (mische (x:xs) ys) Aussage 6.1.1 Sortieren von n Zahlen mittels Vergleich von Zahlen benötigt im schlechtesten Fall mindestens n ∗ log2 (n) Vergleiche. Genauer: Man nimmt an: das Programm hat die Eingaben a1 , . . . , an , das Programm ist ein Entscheidungsbaum, der an den Verzweigungen die Frage stellt: ist ai > aj für bestimmte Indizes i, j, und die Ausgabe am Ende ist die sortierte Permutation der ai . Dann gibt es immer eine Eingabemöglichkeit, so dass n ∗ log(n) Vergleiche nötig sind. Begründung. Der Entscheidungsbaum hat mindestens n! Blätter: pro Permutation der Eingabe ein Blatt. Die Tiefe d des Baumes ist mindestens log2 (n!) Offenbar gilt n! ≥ |1 ∗ .{z . . ∗ 1} ∗ n/2 ∗ . . . ∗ n/2 = (n/2)(n/2) . Anwenden des Loga| {z } n/2 n/2 rithmus ergibt log2 (n!) > log2 ((n/2)(n/2) ) = 0.5 ∗ n ∗ log2 (n/2) = 0.5 ∗ n ∗ (log2 (n) − 1). D.h. Ω(n ∗ log2 (n))1 . Eine andere Abschätzung dieser unteren Schranke der minimal notwendigen Anzahl der Vergleiche ist möglich mittels der Stirlingformel für n!. Der Faktor 0.5 steigt mit wachsendem n, aber bleibt < 1, da ja n! < nn . Allerdings konnte man noch nicht allgemein nachweisen, dass dies auch eine untere Schranke ist, wenn man alle Algorithmen zulässt. In bestimmten Spezialfällen kann es schnellere Algorithmen geben, z.B. wenn die Länge des Schlüssels nicht zu groß wird. Wenn beispielsweise bekannt ist, dass der Schlüssel 5-stellig ist, kann man vor dem Sortieren ein Index-Array der Länge 100000 anlegen, das Verweise auf die Daten enthält und damit in linearer Zeit sortieren. Bemerkungen zu den Eigenschaften der Sortierverfahren Sortieren durch Einfügen (Insert-Sort) Ist ein einfaches Verfahren, das für kleinere Listen schnell genug ist. Die Anzahl an Reduktionen (Zeitbedarf) ist im schlechtesten Fall quadratisch: man benötigt für eine Liste 1 Analog zu O(.) ist Ω(.) untere asymptotische Abschätzung Grundlagen der Programmierung 2, AS 2006, Kapitel 4, vom 17. Maerz 2006 5 der Länge n höchstens: 1 + 2 + . . . + (n − 1) Vergleiche (und Operationen). Dies sind (n − 1) ∗ n/2, d.h. O(n2 ). Das Verfahren ist auch im besten Fall linear, da der Vergleich und das Einfügen evtl. nur einmal pro Element gemacht werden, wenn die Liste in der richtigen Reihenfolge vorsortiert ist. Sortieren mit Bubblesort Ebenfalls quadratisch im schlechtesten Fall: (n − 1) + (n − 2) + . . . + 1 Operationen. Der beste Fall tritt ein, wenn die Liste schon sortiert ist, oder schon fast sortiert ist, da dann abgebrochen werden kann. Dann kann der Bubblesort in linearer Zeit ablaufen. Quicksort Dieses Verfahren hat Vorteile, wenn die Listen groß sind und die Werte zufällig verteilt sind. Quicksort ist im schlechtesten Fall quadratisch. Im Mittel ist der Zeitbedarf O(n ∗ log(n)). Beim Experimentieren in Haskell mit zufällig erzeugten Listen erscheint es als das beste Verfahren. Im worst-case hat der Quicksort die Laufzeit O(n2 ). Im Falle des Haskell-Implementierung tritt der schlechte Fall ein, wenn die Eingabeliste vorsortiert ist. Mischsort Ist im schlechtesten Fall O(n ∗ log(n)). Beim Experimentieren in Haskell ist es nur dann besser als Quicksort, wenn Teile der Listen bereits sortiert sind. In imperativen Sprachen hat es den Nachteil, dass man bei einer naiven Implementierung die Listen jeweils neu aufbauen muss. Es gibt eine Implementierung, die auch ohne extra Speicher auskommt, effizient ist und einen angepassten und ausgefeilten Merge-Algorithmus verwendet. Wir zeigen, dass der Mischsort einen Zeitbedarf von O(n ∗ log(n)) hat: Dazu müssen wir redms abschätzen. redms (n) = c + redmische (n) + 2 ∗ redms (n/2) = c + n + 2 ∗ redms (n/2) ≤ c + n + 2 ∗ (c + n/2 + 1) + 4 ∗ redms (n/4). Da dies log(n) Schritte erfordert, ergibt sich als Abschätzung: c ∗ n + n ∗ log(n) + 2 ∗ log(n). Der Term in der Mitte ist asymptotisch am größten, d.h. der Mischsort ist O(n ∗ log(n)). 6.1.1 Sortierprogramme in imperativen Programmiersprachen, Speicherverwaltung In imperativen Programmiersprachen kommt zu der eigentlichen Kernidee der Sortieralgorithmen noch der Aspekt der effizienten Speicherverwaltung dazu. Normalerweise ist die Eingabe ein Array der Schlüssel und Daten. Algorithmen, die die Sortierung nur innerhalb des Arrays vornehmen durch Tauschvorgänge, nennt man auch In-Place“Verfahren. ” Die betrachteten Sortierverfahren lassen sich bis auf Misch-Sort leicht als In-Place-Verfahren programmieren. Der Merge-Sort kann durch ein effizientes Grundlagen der Programmierung 2, AS 2006, Kapitel 4, vom 17. Maerz 2006 6 In-Place-Merge-Verfahren ebenfalls implementiert werden, allerdings ist der Algorithmus nicht offensichtlich zu finden: Man benötigt dazu ein schnelles inplace-Mischen. Ein neuer Algorithmus wurde von Arne Kutzner und Pok-Son Kim gefunden: def Quicksort(soArray): return Quicksortr(soArray,0,len(soArray)-1) def Quicksortr(soArray,iLo,iHi): if (iHi-iLo == 1): ##Optimierung, wenn Teilfeld zwei Elemente enthaelt if_groesser_then_tausch(soArray,iLo,iHi); return soArray; ## int Lo, Hi, Mid; ## long T,Mid2; Lo = iLo; Hi = iHi; Mid = (Lo + Hi) / 2; ## print "Mid: ", Mid; while (Lo <= Hi): while (isKleiner(soArray,Lo,Mid)): Lo = Lo+1 while (isGroesser(soArray,Hi,Mid)): Hi = Hi -1; if (Lo <= Hi): if (Mid == Hi): Mid = Lo; elif (Mid == Lo): Mid = Hi; if (Lo != Hi): vertausche(soArray,Lo, Hi); Lo = Lo +1; Hi = Hi-1; if (Hi > iLo): if (Lo < iHi): return soArray Quicksortr(soArray,iLo, Hi); Quicksortr(soArray,Lo, iHi); def isGroesser(soArray,x,y): def isKleiner(soArray,x,y): return (soArray[x] > soArray[y]) return (soArray[x] < soArray[y]) def vertausche(soArray,ind1, ind2): x = soArray[ind1]; soArray[ind1] = soArray[ind2]; soArray[ind2] = x; Grundlagen der Programmierung 2, AS 2006, Kapitel 4, vom 17. Maerz 2006 7 def if_groesser_then_tausch(soArray,ind1,ind2): if isGroesser(soArray,ind1,ind2): vertausche(soArray,ind1, ind2); Der Bubblesort in Python: def Bubblesort(soArray): laenge = len(soArray) for i in range(0,laenge-1): aenderung = 0 for j in range(0,laenge-1-i): if soArray[j] > soArray[j+1]: vertausche(soArray,j, j+1) aenderung = 1 if aenderung == 0: break Der Insertsort in Python: def Insertsort(soArray): laenge = len(soArray) for i in range(1,laenge): for j in range(i,0,-1): if soArray[j] < soArray[j-1]: vertausche(soArray,j, j-1) else: break Ein Sortierverfahren nennt man stabil, wenn es die Reihenfolge der Eingabe nicht unnötig ändert, d.h. wenn die Eingaben mit gleichen Schlüsselwerten nicht in der Reihenfolge vertauscht werden. Hier verhalten sich die Haskell-Algorithmen anders als die imperativen Algorithmen. Die Haskell-Sortier-Algorithmen sind alle stabil, aber benötigen zusätzliche Listen zum Sortieren. Die imperativen In-Place-Sortierverfahren sind nur teilweise stabil: Einfügeund Bubble-Sort lassen sich leicht stabil implementieren. Der Merge-Sort kann ebenfalls stabil implementiert werden. Der imperative Quicksort, so wie oben implementiert, ist durch die nicht-lokalen Vertauschungen nicht stabil. Der Grund ist, dass beim Tauschen der Elemente um den Pivot herum die Reihenfolge gleicher Elemente vertauscht wird.