Organisatorisches • Vorlesung: Gerhard Woeginger (Zimmer 4024 im E1) • Email: [email protected] • Webseite: http://algo.rwth-aachen.de/Lehre/SS17/DSA.php VL-07: Sortieren II: HeapSort • Nächste Vorlesung: Donnerstag, Mai 11, 10:15–11:45 Uhr, Aula 1 (Datenstrukturen und Algorithmen, SS 2017) Gerhard Woeginger SS 2017, RWTH DSAL/SS 2017 VL-07: Sortieren II: HeapSort 1/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 2/28 Sortieren II / HeapSort Heaps • Einführung • Heapaufbau • HeapSort • Prioritätswarteschlangen DSAL/SS 2017 VL-07: Sortieren II: HeapSort 3/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 4/28 Heaps (1) Heaps (2) 0 16 1 Heap (deutsch: Halde, Haufen) 2 14 Ein Heap ist ein Binärbaum, der Elemente mit Schlüsseln enthält. Die Heap-Bedingung für Max-Heaps fordert: 10 3 4 8 7 Der Schlüssel eines Knotens ist stets mindestens so gross wie die Schlüssel seiner Kinder. 6 9 3 8 9 2 Weiters gilt: 5 7 4 1 16 14 10 8 7 9 3 2 4 1 Alle Ebenen (mit Ausnahme der untersten) sind komplett gefüllt. 0 1 2 3 4 5 6 7 8 9 Die Blätter befinden sich damit auf einer (oder zwei) Ebene(n). Die Blätter der untersten Ebene sind linksbündig angeordnet. Wurzel = roter Knoten Blätter = blaue Knoten plus die beiden rechten grünen Knoten Innere Knoten = alle Nicht-Blätter DSAL/SS 2017 VL-07: Sortieren II: HeapSort 5/28 Arrayeinbettung eines Heaps DSAL/SS 2017 VL-07: Sortieren II: HeapSort 6/28 Heaps: Eigenschaften (1) Heaps werden oft in Arrays eingebettet: Lemma Vergrössert man den Schlüssel der Wurzel, so bleibt Baum ein Heap. Arrayeinbettung Das Array a wird wie folgt als Binärbaum aufgefasst: Die Wurzel liegt in a[0]. Das linke Kind von a[i] liegt in a[2*i+1]. Lemma Das rechte Kind von a[i] liegt in a[2*i+2]. Ab Position b n2 c erfüllt jedes Array die Heapbedingung. Durch die “möglichst vollständige” Füllung der Ebenen werden „Löcher“ im Array vermieden. Lemma Ein Heap hat b n2 c innere Knoten. Vergrössert man den Baum um ein Element, so wird das Array um genau ein Element länger. DSAL/SS 2017 VL-07: Sortieren II: HeapSort 7/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 8/28 Heaps: Eigenschaften (2) Definition Die Höhe eines Heaps ist die Anzahl von Kanten auf dem Weg von Wurzel zum äusserst linken Blatt. Heapaufbau Lemma Die Höhe H eines Heaps mit n Elementen erfüllt (a) 2H ≤ n und (b) n ≤ 2H+1 − 1. Ergo: H = blog2 nc und H ∈ O(log n). Lemma Es sei ` die Anzahl aller Knoten im linken Teilbaum, und es sei r die Anzahl aller Knoten im rechten Teilbaum, Dann gilt r ≤ ` ≤ 2r + 1. Beweis: ` ≤ 2H − 1 und r ≥ 2H−1 − 1 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 9/28 Naiver Heapaufbau (1) DSAL/SS 2017 VL-07: Sortieren II: HeapSort 10/28 Naiver Heapaufbau (2) 2 7 3 9 14 4 1 2 3 4 5 6 7 8 9 10 10 8 16 15 2 7 10 3 14 8 16 9 4 15 void bubble ( int E [] , int pos ) { while ( pos > 0) { int parent = floor ( ( pos -1) /2 ) ; if ( E [ parent ] > E [ pos ]) { break ; } swap ( E [ parent ] , E [ pos ]) ; pos = parent ; } } Heap mit n Elementen hat Höhe H ≤ log n. Heapaufbau, naiv Damit kostet jedes Einfügen H ≈ log n Vergleiche. Der Heap wird Reihe für Reihe von oben nach unten aufgebaut, indem Ergo: Zum naiven Aufbau eines Heaps mit n Elementen benötigt man O(n log n) Vergleiche, und im Worst-Case Ω(n log n) Vergleiche. ein neues Element möglichst weit links angefügt wird, und dann rekursiv nach oben getauscht wird, solange es grösser als sein Elternknoten ist. DSAL/SS 2017 VL-07: Sortieren II: HeapSort 11/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 12/28 Heapify (1): Bessere Strategie von [Floyd, 1964] Heapify (2) Betrachte Element E[i] unter der Annahme, dass der rechte und linke Teilbaum darunter bereits Heaps sind. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Problem: E[i] kann kleiner als seine Kinder sein. Ziel: Wir wollen die beiden Teilbäume – Heaps – zusammen mit E[i] zu einem GesamtHeap verschmelzen. Methode: Wir lassen E[i] langsam in den Heap hineinsinken, wodurch der gesamte Teilbaum mit Wurzel E[i] zum Heap wird. Heapify Finde das Maximum der Werte E[i] und seiner beiden Kinder. Ist E[i] das grösste Element, dann ist der gesamte Teilbaum auch ein Heap. Fertig. void heapify ( int E [] , int n , int pos ) { int next = 2 * pos + 1; while ( next < n ) { if ( next + 1 < n && E [ next + 1] > E [ next ]) { next = next + 1; } if ( E [ pos ] >= E [ next ]) { break ; } swap ( E [ pos ] , E [ next ]) ; pos = next ; next = 2 * pos + 1; } } Andernfalls vertausche E[i] mit dem grössten Element und führe Heapify in diesem Unterbaum weiter aus. DSAL/SS 2017 VL-07: Sortieren II: HeapSort 13/28 Heapify (3): Beispiel DSAL/SS 2017 VL-07: Sortieren II: HeapSort 14/28 Heapify (4): Laufzeit 0 16 1 2 12 3 4 14 7 7 8 2 8 Heapify vergleicht den Knoten mit den beiden Kindern. ⇒ zwei Vergleiche 10 5 6 9 Die Worst-Case Komplexität von Heapify in einem Heap mit n Knoten ist daher maximal 2 blog2 nc. 3 9 Falls Heapify auf einen Teilbaum der Höhe h angewandt wird, ist die Worst-Case Komplexität ≈ 2h. 1 16 12 10 14 7 9 3 2 8 1 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 15/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 16/28 BuildHeap (1) BuildHeap (2): Korrektheit Strategie: Wandle Array von unten nach oben (bottom-up) in Heap um. Lemma Der Algorithmus BuildHeap ist korrekt und terminiert. 2 7 10 3 9 1 2 3 4 5 14 4 8 Initialisierung: Jeder Knoten i ≥ bn/2c ist ein Blatt und damit Wurzel eines trivialen Heaps. Schleifen-Invariante: Zu Beginn der for-Schleife ist jeder Knoten i+1, . . . , E .length die Wurzel eines Heaps. 16 15 In jedem Schritt sind alle Kinder des betrachteten Knotens i bereits Wurzeln von Heaps (Schleifeninvariante). void BuildHeap ( int E []) { for ( int i = E . length / 2 - 1; i >= 0; i - -) { heapify (E , E . length , i ) ; } } ⇒ Die Bedingung für den Aufruf von heapify ist erfüllt. Dekrementierung von i stellt Schleifeninvariante wieder her. Terminierung: Bei i = 0 ist gemäss Schleifeninvariante jeder Knoten 1, 2, . . . , n die Wurzel eines Heaps. Nützliche Invariante: Nach jedem Aufruf von heapify(E,E.length,i) sind die Knoten i, . . . , E.length - 1 schon Wurzeln von Heaps. DSAL/SS 2017 VL-07: Sortieren II: HeapSort 17/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 18/28 BuildHeap (3): Laufzeit Fakten h+1 • Ein Heap hat maximal P∞dn/2 h e Knoten mit2 der Höhe h. • Formel für q < 1: h=0 h · q = q/(1 − q) . HeapSort Lemma Die Worst-Case Komplexität von BuildHeap ist Θ(n) Beweis: Die Laufzeit von Heapify für einen Knoten der Höhe h ist 2h. Die Zeitkomplexität von BuildHeap ist daher ! blog2 (n)c l ∞ X X n m h · 2h ≤ 2n · = 4n ∈ O(n). 2h+1 2h h=0 DSAL/SS 2017 h=0 VL-07: Sortieren II: HeapSort 19/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 20/28 HeapSort (1): Algorithmus und Beispiel HeapSort (2): Analyse 41 26 17 25 3 19 6 69 Die Worst-Case Komplexität von BuildHeap ist Θ(n). 17 12 4 8 2 13 Die Worst-Case Komplexität von Heapify in einem Heap mit n Knoten ist maximal 2 log2 n. 34 Für HeapSort erhalten wir somit: ! n−1 X W (n) ≤ 2 log2 n + n 0 41 26 17 25 19 17 8 3 6 69 12 4 2 13 34 0 1 2 3 4 5 6 7 i=1 void heapSort ( int E []) { BuildHeap ( E ) ; for ( int i = E . length - 1; i > 0; i - -) { swap ( E [0] , E [ i ]) ; heapify (E , i , 0) ; } } DSAL/SS 2017 VL-07: Sortieren II: HeapSort = 2 · (n − 1) · log2 (n) + n ∈ O(n log n) Zusätzlicher Speicherplatzbedarf ist konstant (lokale Variablen). 21/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 22/28 HeapSort (3): Zusammenfassung Prioritätswarteschlangen HeapSort sortiert in O(n log n) Zeit. HeapSort verwendet O(1) zusätzlichen Speicherplatz. HeapSort ist ein in-place Algorithmus. HeapSort ist nicht stabil. DSAL/SS 2017 VL-07: Sortieren II: HeapSort 23/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 24/28 Die Prioritätswarteschlange (1) Die Prioritätswarteschlange (2) Prioritätswarteschlange (priority queue) void insert(PriorityQueue pq, Element e, int k) mit dem Schlüssel k in pq ein. Betrachte Elemente (Datensätze), die mit einem Schlüssel (key) versehen sind. fügt das Element e gibt das Element mit dem kleinsten Schlüssel zurück; benötigt nicht-leere pq. Element getMin(PriorityQueue pq) Jeder Schlüssel sei höchstens an ein Element vergeben. entfernt das Element mit dem kleinsten Schlüssel; benötigt nicht-leere pq. void delMin(PriorityQueue pq) Schlüssel werden als Priorität betrachtet. setzt den Schlüssel von Element e auf k; e muss in pq enthalten sein. k muss ausserdem kleiner als der bisherige Schlüssel von e sein. void decrKey(PriorityQueue pq, Element e, int k) Mit Heaps ist eine effiziente Implementierung möglich. DSAL/SS 2017 VL-07: Sortieren II: HeapSort 25/28 Drei Prioritätswarteschlangenimplementierungen Operation Array sortiertes Array isEmpty(pq) insert(pq,e,k) getMin(pq) delMin(pq) decrKey(pq,e,k) Θ(1) Θ(1) Θ(n) Θ(n)* Θ(1) Θ(1) Θ(n)* Θ(1) Θ(1) Θ(n)* DSAL/SS 2017 VL-07: Sortieren II: HeapSort 26/28 Organisatorisches Heap Θ(1) Θ(log n) Θ(1) Θ(log n) Θ(log n) • Nächste Vorlesung: Donnerstag, Mai 11, 10:15–11:45 Uhr, Aula 1 • Webseite: http://algo.rwth-aachen.de/Lehre/SS17/DSA.php * Beinhaltet das Verschieben aller Elemente rechts von k. DSAL/SS 2017 VL-07: Sortieren II: HeapSort 27/28 DSAL/SS 2017 VL-07: Sortieren II: HeapSort 28/28