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