Sortieren

Werbung
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.
Herunterladen