1 Quicksort

Werbung
Wolfgang Hönig / Andreas Ecke
WS 09/10
1 Quicksort
1.1 Aufgabentyp
Gegeben sei die Folge: [...]
Wenden Sie auf diese Folge den Quicksort-Algorithmus an und dokumentieren Sie den
Rechenablauf wie folgt:
-
Pivotelement jeweils kennzeichnen
Stellung der Indizes i, j unmittelbar vor dem Tausch von Elementen
Stellung der Indizes i, j unmittelbar vor den rekursiven Aufrufen
Teilfolgen nach den rekursiven Aufrufen
1.2 Überblick
1. Pivotelement x = a
hj
länge(a)−1
2
ki
; i = 0; j = länge(a) − 1
2. Solange i ≤ j
a) Solange a[i] < x : i + +
b) Solange a[j] > x : j − −
c) Falls i ≤ j: Vertausche a[i] und a[j]; i + +; j − −
3. quicksort(a[imin ] . . . a[j]), falls mindestens zweielementig
quicksort(a[i] . . . a[jmax ]), falls mindestens zweielementig
1.3 Grundidee
Große Elemente sollen schnell an das Ende und kleine Elemente schnell an den Anfang der
Folge gebracht werden. Die (relative) Größe abzuschätzen ist allerdings recht schwierig.
Dafür gibt es das Pivotelement (x): Elemente die kleiner als dieses sind, können als „klein“
betrachtet werden, der Rest als „groß“. Quicksort teilt die Folge in zwei Hälften (das Pivotelement ist die Grenze) und sorgt mittels Tauschoperationen dafür, dass sich in der linken
Hälfte die „kleinen“ Elementen befinden und in der rechten Hälfte die „großen“ Elemente.
Danach wird auf beiden Hälften rekursiv wieder der Quicksortalgorithmus aufgerufen. Die
Effizienz hängt von der Wahl des Pivotelements ab, welches natürlich ein mittlerer Wert
der Folge sein sollte. Der Algorithmus bei uns nimmt hierbei einfach an, dass dieser mittlere Wert auch bei dem mittleren Element zu finden ist (bzw. links von der Mitte, wenn
die Anzahl der Elemente geradzahlig ist.)
1.4 Erklärung am Beispiel
Gegeben sei die Folge: 6,8,4,3,7
Quicksort arbeitet auf Feldern (also Arrays in C, im folgenden habe dieses den Namen a).
Zu prokollieren ist der in der Aufgabe geforderte Rechenablauf. Dies wiederum bedeutet,
dass der Algorithmus im Kopf nachvollzogen werden muss.
Im ersten Schritt wird das Pivotelement x bestimmt. Das Eingabefeld a hat in diesem Beispiel 5 Elemente. Das mittlere Element ist offenbar das dritte Element oder in C-Notation
1
Wolfgang Hönig / Andreas Ecke
WS 09/10
a[2]. Allgemein lässt sich die Indexposition auch wie im Überblick angegeben berechnen,
allerdings ist es einfacher, wenn man sich merkt, dass es immer das mittlere Element (oder
links davon) ist. Das Pivotelement wird durch eine Einrahmung gekennzeichnet. Zusätzlich
werden zwei Indexzeiger i und j benötigt. Damit ergibt sich folgendes:
6
↑
i
8
4
3
7
↑
j
Große Elemente sollen schnell an das Ende wandern und kleine an den Anfang. Der Algorithmus sucht nun also links vom Pivotelement ein großes Element und rechts vom Pivotelement ein kleines Element und vertauscht diese miteinander. Praktisch geschieht dies,
durch die Fälle 2a bis 2c: der i-Zeiger wird solange erhöht (also nach rechts verschoben),
bis ein passendes Element, welches größer oder gleich dem Pivotelement x ist gefunden
wurde. Analoges geschieht für j:
6
↑
i
8
4
3
↑
j
7
Offenbar ist 6 im Vergleich zu 4 schon groß (d.h. i wird nicht verändert). Aber 7 ist im
Vergleich zu 4 nicht klein, also wird j nach links verschoben (dekrementiert). Da die 3 im
Vergleich zur 4 klein ist, kann der Tausch erfolgen (bei dem i und j auch entsprechend
angepasst werden, um Doppelvertauschungen zu vermeiden):
3
8
↑
i
4
↑
j
6
7
8 ist relativ zur 4 ein großes Element. Die 4 ist relativ zur 4 ein kleines Element (man
beachte, dass im Algorithmus a[i] < x bzw. a[j] > x steht, dass heißt 2a bzw. 2b enden
spätestes beim Pivotelement). Damit erfolgt also wieder ein Tausch und es folgt:
3
4
↑
j
8
↑
i
6
7
Nun ist j < i, sodass 3. ins Spiel kommt. Da beide Teilfolgen mindestens zweielementig
sind erfolgen zwei rekursive Aufrufe (d.h. auf beide Teilfolgen wird nochmal Quicksort
angewendet.) Beide Teilfolgen werden bei uns gleichzeitig aufgeschrieben, auch wenn der
Algorithmus (meistens) sequentiell abgearbeitet wird. Die Trennung der Teilfolgen erfolgt
durch ein ||. Da bei der ersten Teilfolge (3,4) keine echte Mitte haben, wird als Pivotelement
das links von der Mitte gewählt, also 3:
3
↑
i
4
↑
j
8
↑
i
6
7
↑
j
Links: 4 ist relativ zu 3 groß, also wird j auf die 3 verschoben. Rechts: 8 ist schon relativ
groß, also bleibt i bestehen. Die 7 ist allerdings auch relativ groß, also wird j auf die 6
verschoben:
3
↑
i,j
4
8
↑
i
6
↑
j
7
Der Tausch ergibt (zu beachten ist, dass das Pivotelement quasi mitgetauscht wird, da es
vorher in einer Variable x gespeichert wurde. Es wurde sich also der Wert 6 und nicht die
Indexposition des Feldes gemerkt):
2
Wolfgang Hönig / Andreas Ecke
3
↑
j
4
↑
i
6
↑
j
8
↑
i
WS 09/10
7
Die Folge ist nun größtenteils sortiert, da nur noch eine Teilfolge (8,7) mindestens zweielementig ist. Damit ergibt sich im dritten Durchlauf:
3,
4,
6,
3,
4,
6,
8
↑
i
7
↑
j
7
↑
j
8
↑
i
und als Ergebnis erhalten wir (wie zu erwarten war):
3,
4,
6,
7,
8
1.5 Anmerkungen
1.5.1 Eigenschaften
best case
O(n log n)
average case
O(n log n)
worst case
O(n2 )
stabil
nein
3
rekursiv
ja
2 Shellsort
2.1 Aufgabentyp
Wenden Sie auf die Folge: [...] den Shellsort-Algorithmus an. Wählen Sie hierbei für die
Folge der Sortierabstände: [...]. Dokumentieren Sie jeweils:
- Abstand h und Anzahl anz der Teilfolgenglieder,
- die Auswahl der zu sortierenden Teilfolge,
- die Wirkung der Funktion „Insertsort“ auf diese Teilfolge.
2.2 Überblick
1. Folge hn , hn−1 , . . . , 1 wählen (durch Aufgabe gegeben)
2. Für jedes hi :
j
k
a) anz = länge(a)
hi
b) für jedes 0 ≤ k < hi
i. Teilfeld markieren:
a[k], a[k + hi ], a[k + 2hi ], . . . , a[k + (anz − 1)hi ]
ii. InsertSort auf Teilfeld anwenden
2.3 Grundidee
Der vergleichsweise ineffiziente Algorithmus InsertSort (teilweise auch als Insertionsort bezeichnet), soll optimiert werden. Die Grundidee hierbei ist, Teilfelder vorzusortieren sodass
die komplette Sortierung des Gesamtfeldes dann letztendlich schneller abläuft.
2.4 Erklärung am Beispiel
Gegeben sei die Folge: 32, 6, 21, 5, 23, 11, 17, 8, 3, 12, 10 sowie die Sortierabstände 3,1
Der erste Sortierabstand ist laut Aufgabe h = 3. Die Folge
Damit ergibt
j hat 11k Elemente.
11 länge(a)
sich für die Anzahl der Elemente in der Teilfolge: anz =
= 3 =3
hi
h = 3, anz = 3
Nun wird für jedes 0 ≤ k < 3 das Teilfeld markiert. Für k = 0 ergibt sich damit: a[0], a[0 +
3], a[0 + 6], oder grafisch:
32
↑
6
21
5
↑
23
11
17
↑
8
3
12
10
Dieses Teilfeld wird wiederum sortiert (mit Insertsort). Die genauen Schritte, welche Insertsort vollzieht sind hier erstmal nicht so wichtig, sodass als nächstes das Ergebnis dieses
Durchlaufes aufgeschrieben wird:
5
6
21
17
23
11
32
8
3
12
10
4
Wolfgang Hönig / Andreas Ecke
WS 09/10
Mit k = 1 folgt nun das nächste Teilfeld a[1], a[1 + 3], a[1 + 6]:
5
6
↑
21
17
23
↑
11
32
8
↑
3
12
10
Dieses Teilfeld wird wiederum mit Insertsort sortiert:
5
6
21
17
8
11
32
23
3
12
10
Mit k = 2 folgt nun das letzte Teilfeld a[2], a[2 + 3], a[2 + 6]:
5
6
21
↑
17
8
11
↑
32
23
3
↑
12
10
21
12
10
Was mit Insertsort sortiert ergibt:
5
6
3
17
8
11
32
23
Damit ist
j
k h = 3 abgeschlossen. Nun folgt der nächste Sortierabstand h = 1. Mit anz =
länge(a)
= 11
hi
1 = 11 und k = 0 folgt:
h = 1, anz = 11
5
↑
6
↑
3
↑
17
↑
8
↑
11
↑
32
↑
23
↑
21
↑
12
↑
10
↑
Ergebnis:
3, 5, 6,
8,
10,
11,
12,
17,
21,
23,
32
2.4.1 Klausurrelevante Schreibweise
h = 3, anz = 3
32
↑
6
21
5
↑
23
11
17
↑
8
3
12
10
5
6
↑
21
17
23
↑
11
32
8
↑
3
12
10
5
6
21
↑
17
8
11
↑
32
23
3
↑
12
10
h = 1, anz = 11
5
↑
6
↑
3
↑
17
↑
8
↑
11
↑
32
↑
23
↑
21
↑
12
↑
10
↑
Ergebnis:
3, 5, 6,
8,
10,
11,
12,
17,
21,
23,
32
5
Wolfgang Hönig / Andreas Ecke
WS 09/10
2.5 Anmerkungen
2.5.1 Eigenschaften
Algorithmus
InsertSort
ShellSort
best case
O(n)
O(n log n)
average case
O(n2 )
O(n1,2 )
worst case
O(n2 )
O(n1,2 )
stabil
ja
nein
rekursiv
nein
nein
Die Komplexität von Shellsort hängt sehr stark von der gewählten Folge der Sortierabstände ab. Die angegeben Komplexitäten gelten nur für eine (gut gewählte) Beispielfolge mit
h(t + 1) = 2h(t) + 1; andere Folgen können andere Komplexitäten erzielen, welche jedoch
im average case immer schlechter als n log n sein werden.
6
3 Heapsort
3.1 Aufgabentyp
Wenden Sie auf die Folge: [...] den Heapsort-Algorithmus an. Dokumentieren Sie in der
Phase 1:
- das Einordnen in den binären Baum
- das schrittweise (knotenweise) Herstellen der Heap-Eigenschaft; hier insbesondere die
Veränderungen durch die Funktion „sinkenlassen“.
und in der Phase 2:
- das Abspalten des jeweils letzten Elementes (Blattes) im Wechsel mit der
- Wirkung der Funktion „sinkenlassen“.
3.2 Überblick
1. Herstellen heap-Eigenschaft (jeder Knoten muss größeren Wert haben als seine Nachfolger) (Phase 1)
a) Sinkenlassen eines jeden Knoten in einer Ebene / Schritt (von unten nach oben)
2. Sortieren (Phase 2)
a) Tausch von Wurzel- und letztem Element
b) Abspaltung des letzten Elements
c) Sinkenlassen des Wurzelelements
3.3 Grundidee
Ein Array mit den zu sortierenden Daten kann leicht als Baum dargestellt werden, in dem
alle Ebenen von links nach rechts und oben nach unten aufgefüllt werden. Der linke und
rechte Nachfolger eines Knotens kann bei einem solchen Baum leicht über Indexoperationen berechnet werden. Es genügt also einen solchen Baum zu betrachten, da durch die
entsprechenden Indexoperationen die Sortierung dann auch auf Feldern funktioniert.
Bäume können außerdem die so genannte heap-Eigenschaft erfüllen. Das bedeutet, dass
jeder Knoten einen größeren Wert als seine Nachfolgerknoten haben muss. Wenn der gesamte Baum diese heap-Eigenschaft erfüllt, steht zwangsläufig an der Wurzel des Baumes
das größte Element der Folge. Dieses kann mit dem letzten Element der Folge getauscht
werden, damit nunmehr das größte Element am Ende der Folge steht. Für die restliche Folge muss nun wiederum die heap-Eigenschaft hergestellt werden, damit ein erneuter Tausch
beginnen kann.
3.4 sinkenlassen
Zur Herstellung der heap-Eigenschaft wird die Funktion sinkenlassen benutzt. Diese funktioniert auf einem kleinen Baum mittels eines einfachen Vergleichs:
1
2
3
7
Wolfgang Hönig / Andreas Ecke
WS 09/10
Offenbar verletzt das Wurzelelement dieses Baumes die Heap-Eigenschaft. Außerdem ist 3
der größere der beiden Nachfolger, also werden 1 und 3 getauscht und es ergibt sich:
3
2
1
Größere Bäume können durch ebenenweises (von oben nach unten) Anwenden dieser Tauschregel mit der heap-Eigenschaft versehen werden. Dafür muss gelten, dass bereits beide Teilbäume die heap-Eigenschaft erfüllen (daher wird das sinkenlassen später auch von unten
nach oben auf alle Knoten einmal angewendet). Nun kann die heap-Eigenschaft für den
gesamten Baum hergestellt werden, indem solange mit dem jeweils größten Nachfolger getauscht wird, bis es keinen Nachfolger mehr gibt, der größer als das aktuelle Element ist
(trivial erfüllt, wenn das Element gar keinen Nachfolger hat).
1
7
5
4
7
2
3
7
5
−→
6
4
1
2
3
5
−→
6
4
6
2
3
1
3.5 Erklärung am Beispiel
Gegeben sei die Folge: 13, 6, 25, 4, 23, 11, 18, 9, 3, 19, 10, 7, 2
Zuerst wird aus der gegebenen Folge der Baum modelliert, indem die Ebenen des Baumes
von links nach rechts und oben nach unten gefüllt werden. Somit müssten immer die obersten Ebenen vollständig gefüllt sein, lediglich die letzte Ebene kann ein teilweise Füllung
aufweisen. Bei der Beispielfolge ergibt sich:
13
6
25
4
9
11
23
3
19
7
10
18
2
In Phase 1 muss die heap-Eigenschaft durch ebenenweises sinkenlassen hergestellt werden.
Im ersten Schritt wird hierzu die vorletzte Ebene betrachtet (Die letzte Ebene enthält
kein Nachfolger mehr, sodass die heap-Eigenschaft trivial erfüllt ist). Dies sind also die
Knoten mit der Beschriftung 18, 11, 23 und 4. Da der Knoten 18 ebenfalls keine Nachfolger
mehr besitzt, muss sinkenlassen auf die anderen drei Knoten angewendet werden. Dies
ist auch entsprechend mit s(Knotennummer) zu kennzeichnen (Auch wenn s(23) sowie
s(11) keine direkte Wirkung zeigen, sollte doch gekennzeichnet werden, dass sinkenlassen
aufgerufen wurde, da vorher noch nicht klar ist, dass der Teilbaum tatsächlich schon die
heap-Eigenschaft erfüllt):
13
s(11)
s(23)
s(4)
−→
6
25
9
4
11
23
3
19
10
7
18
2
In der zweiten Ebene werden entsprechend die Knoten mit der Beschriftung 25 und 6 sinken
8
Wolfgang Hönig / Andreas Ecke
WS 09/10
gelassen und im letzten Schritt der Knoten mit der Beschriftung 13:
13
s(25)
s(6)
−→
25
25
23
19
9
4
3
6
18
11
10
7
18
23
s(13)
−→
19
9
2
4
3
6
13
11
10
7
2
Offenbar erfüllt der Baum jetzt die heap-Eigenschaft.
In der Phase 2 wird der eigentliche Sortiervorgang ausgeführt, indem das Wurzelelement
mit dem letzten Element getauscht und nach Abspaltung des (neuen) letzten Elementes
wieder die heap-Eigenschaft des Restbaumes hergestellt wird.
In unserem Beispiel wird also zuerst die 2 mit der 25 getauscht. Danach wird die 25
abgespaltet, das heißt sie ist nicht mehr an der Sortierung beteiligt. Dies wird durch einen
Rahmen gekennzeichnet. Für den Restbaum (also ohne die 25) wird wiederum die heapEigenschaft durch sinkenlassen der 2 hergestellt:
2
23
23
Tausch
−→
18
9
4
19
3
6
11
10
7
19
s(2)
−→
18
10
9
13
4
25
3
6
11
2
7
13
25
Dies geht im nächsten Schritt analog weiter: 7 und 23 werden getauscht, die 23 wird
abgespaltet und die heap-Eigenschaft wird durch s(7) wieder hergestellt:
19
7
Tausch
−→
10
9
4
3
s(7)
−→
18
19
6
11
2
23
10
18
9
13
4
25
7
3
6
11
2
23
13
25
Diese beiden Schritte genügend meistens in der Klausur (siehe Aufgabentext!). Eine vollständige Lösung kann in der Aufgabensammlung zur Lösung 6.13 (S. 195 ff.) nachgelesen
werden.
3.6 Anmerkungen
3.6.1 Eigenschaften
best case
O(n log n)
average case
O(n log n)
worst case
O(n log n)
stabil
nein
9
rekursiv
nein
Herunterladen