2. Suchen und Sortieren
2.4 Heapsort
Heapsort
Der zweite effiziente Algorithmus, den wir kennenlernen, ist Heapsort. Der für diese Vorlesung vielleicht wichtigste Grund, warum wir ihn drannehmen ist, dass er eine schlaue Datenstruktur verwendet, die wir später im Kontekt von Graphenalgorithmen wieder brauchen werden: den Heap.
Ein Binärbaum ist ein Baum mit Wurzel, in dem jeder Knoten bis zu zwei Kinder hat.
Die Tiefe eines Knotens
Ein Binärbaum ist fast vollständig, wenn
(1) alle Knoten der Tiefe
Übungsaufgabe 2.4.1
Zeichnen Sie einen fast vollständigen Binärbaum mit
Ein Heap ist ein fast vollständiger
Binärbaum, in dem jeder Knoten
Ein Heap
Heaps sind gut, weil sie zwei wichtige Operationen effizient unterstützen:
Ein neues Element in einen Heap einfügen
Wir hängen das neue Element als Blatt an die erste verfügbare Position (soweit oben wie möglich, soweit links wie möglich). Dann lassen wir es nach oben "blubbern", bis es die richtige Position gefunden hat, also größer ist als das Element in seinem Eltern-Knoten.
Hier ist der Pseudo-code dafür:
# Pseudo-code für das Einfügen eines neuen Elementes in einen Heapinsert(newLabel, heap):
create new leaf v at lowest and left-most position
label(v) := newLabel
while v != root and label(u) < label(parent(v)):
swap label(v) and label(parent(v))
v := parent(v)
Die Wurzel extrahieren
Dazu nehmen wir das letzte Element des Heaps (ganz unten, ganz rechts), löschen den Knoten und schieben das Element in den Wurzelknoten. Um die Heap-Eigenschaft wieder herzustellen, lassen wir jetzt das Element von der Wurzel nach unten fallen, bis es seinen richtigen Platz gefunden hat. Das heißt, solange ein Kind ein kleineres Element hat, vertauschen wir es mit dem kleineren der beiden Kinder.
# Pseudo-code für das Extrahieren der Wurzel in einen HeapremoveRoot(heap):
result := label(root(heap))
swap label(root) and label(last element of heap)
delete last element of heap)
v := root
while v has a child w with label(v) > label(w):
swap label(v) with label(w) (or with label(w') in case v has another child w' with label(w') < label(w))
v := w
return result
Hier nun eine vollständige Darstellung von Heapsort an einem Beispiel:
Hier finden Sie eine Animation von Heapsort.
Zeigen Sie, dass so ein Heap weitere Operationen unterstützt,
nämlich
decreaseKey (node, newLabel)
.
In dieser Operation haben Sie einen Knoten gegeben (also einen
Pointer zu
einem Knoten im
Heap, oder
auch einfach einen Index, falls Sie Ihren Heap als Array
implementiert
haben), und wollen
das alte Label durch ein neues ersetzen (newLabel
).
Als
Anwendung stellen Sie
sich vor,
dass Ihre Objekte Produkte in einem Supermarkt sind, die nach
Preis
sortiert werden
sollen;
der Preis kann sich aber ändern.
Beschreiben Sie mit Pseudo-Code, wie Sie
decreaseKey (node, newLabel)
implementieren.
Die Hauptarbeit liegt natürlich darin, nach der Änderung die
Heap-Eigenschaft
wiederherzustellen.
Laufzeitanalyse
Sei
Bei jeder Einfügeoperation lassen wir das Element nach oben treiben;
auf jeder Ebene
kann es maximal eine Vergleichsoperation geben, und auf Ebene 0 (sollte
das Element bis zur
Wurzel hochgeschwommen sein) gibt es keine Vergleichsoperation mehr.
Daher gibt es maximal
Vergleichsoperationen. Wie schaut es mit dem Abbau des Heaps aus? Wenn
wir
ein Element
Wir folgern:
Theorem 2.4.3 Sei
Implementierung von Heapsort
Vielleicht fürchten Sie, Heapsort sei schwierig zu implementieren, weil wir ja erst einmal eine Datenstruktur für Binärbäume brauchen, also wie in Elm
type BinaryTree a
= Empty
| Branch a BinaryTree BinaryTree
Das ist glücklicherweise nicht der Fall. Da unsere Heaps immer fast
vollständige
Binärbäume
sind, gibt es eine besonders einfache Repräsentierung. Tun wir so, als
hätten wir von Anfang
an einen fast vollständigen Binärbaum mit
Als nächstes numerieren wir die Zellen des Arrays und die Knoten des
Binärbaumes durch, von 0
bis
Jetzt können wir einfach den Heap (die blauen Zahlen) im Array in den jeweiligen Zellen abspeichern:
Um jetzt aber den Algorithmus laufen lassen zu können, brauchen wir eine
einfache Formel,
um von einem Knoten
Und gerade beim Abbau des Heaps müssen wir für einen Knoten
Ein Knoten mit Index
Übungsaufgabe 2.4.4
Schreiben Sie in Python eine Funktion
insert(value, heap, index)
.
Hier ist die Idee, dass heap
ein Array
ist und die ersten index
Elemente bereits ein
Heap sind; nach Einfügeoperation sollen also die
ersten index+1
Elemente aus array
den
neuen Heap darstellen.
Übungsaufgabe 2.4.5
Schreiben Sie, aufbauend auf insert
, eine Funktion
buildheap
, die aus dem Array einen Heap macht. Sie
soll
keinen zusätzlichen Speicher belegen, also kein neues Array
erschaffen!
Übungsaufgabe 2.4.6
Jetzt wird es schwieriger: schreiben Sie eine Funktion
extractRoot(heap, size)
; die Annahme ist hier
wieder,
dass heap
ein Array ist und
die ersten size
Elemente von heap
einen Heap
darstellen. Nach der Wurzelentfernung sollen die ersten
size-1
wiederum den korrekten Heap (nach Löschen) darstellen. Die
Funktion
soll den ursprünglichen Wert an der Wurzel zurückgeben.
Übungsaufgabe 2.4.7
Schreiben Sie jetzt eine Funktion heapSort
,
aufbauend auf dem,
was Sie bis jetzt implementiert haben. An keiner Stelle sollen
Sie ein
neues Array bauen!
Heapsort braucht also im schlimmsten Fall
- Es gibt keine Rekursion braucht; Sie brauchen also keine Umgebung mit Stack-Management.
- Es ist in place, braucht keinen zusätzlichen Speicherplatz (bzw. nur die wenigen lokalen Variablen, die im Code definiert sind).