Eine Priority Queue gibt immer den Eintrag mit der kleinsten Priorität zurück: den nächsten Knoten für Dijkstra, die zwei seltensten Zeichen für Huffman, den nächsten Termin. Ein binärer Heap macht das schnell, mit einem einfachen Array. Hier steht, wie er aufgebaut ist, wie push und pop ihn in Ordnung halten, zum Durchklicken und als Pseudocode, und wie man mit ihm sortiert.
Eine Priority Queue kann zwei Dinge:
Wie sie das macht, ist erst einmal egal. Die einfachste Art, eine Liste, in der pop jedes Mal den kleinsten sucht, ist richtig, aber pop muss dafür alle Einträge ansehen. Eine sortierte Liste macht pop schnell, dafür muss push die richtige Stelle finden und alles dahinter verschieben. Ein Heap macht beides schnell:
| push | pop | bei einer Million Einträgen | |
|---|---|---|---|
| unsortierte Liste | schnell | alle ansehen | eine Million Vergleiche je pop
|
| sortierte Liste | verschieben | schnell | bis zu einer Million Verschiebungen je push
|
| binärer Heap | \(\log_2 n\) Schritte | \(\log_2 n\) Schritte | etwa 20 Schritte |
Priority Queues stecken in Dijkstra und A* (Graphen und Wege), in Huffman-Codes (die zwei seltensten Bäume zuerst), in jedem Terminkalender und in jedem Betriebssystem, das entscheidet, welches Programm als Nächstes rechnen darf.
Ein binärer Heap ist ein Array, das man sich als Baum denkt. Ganz oben steht der Eintrag an Index 0. Die Kinder des Eintrags an Index \(i\) stehen an den Indizes \(2i + 1\) und \(2i + 2\), sein Elternteil an \(\lfloor (i - 1) / 2 \rfloor\):
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| Kinder | 1, 2 | 3, 4 | 5, 6 | 7, 8 | 9, 10 | 11, 12 | 13, 14 |
| Elternteil | – | 0 | 0 | 1 | 1 | 2 | 2 |
Der Baum ist immer von oben nach unten und in jeder Zeile von links nach rechts gefüllt, ohne Lücken. Deshalb braucht er keine Zeiger: Das Array ist der Baum.
Für jeden Eintrag gilt: Kein Kind ist kleiner als sein Elternteil. Daraus folgt, dass ganz oben, an Index 0, der kleinste Eintrag steht. Geschwister sind dabei nicht geordnet, und eine Zeile weiter unten kann kleinere Zahlen haben als eine weiter oben, nur eben nicht unter demselben Elternteil. Ein Heap ist also nicht sortiert, er ist gerade so geordnet, dass man den kleinsten Eintrag sofort findet.
Ein neuer Eintrag kommt hinten an das Array, an die erste freie Stelle des Baums. Dort verletzt er vielleicht die Heap-Eigenschaft: Er ist kleiner als sein Elternteil. Dann tauscht er mit ihm den Platz, und das so lange, bis sein Elternteil nicht mehr größer ist oder er oben angekommen ist. Er sickert nach oben.
push(heap, eintrag):
eintrag hinten an heap anfügen
i ← letzter Index von heap
solange i > 0:
eltern ← (i - 1) / 2, abgerundet
wenn heap[eltern] ≤ heap[i]: fertig
heap[eltern] und heap[i] tauschen
i ← eltern
Probier es aus mit 5, 3, 8, 1, 4 und 2. Oben steht der Heap als Baum, darunter dasselbe als Array, mit den Indizes klein daneben:
Ein Eintrag steigt höchstens so viele Stufen auf, wie der Baum hoch ist: bei \(n\) Einträgen etwa \(\log_2 n\), bei einer Million also 20.
Der kleinste Eintrag steht an Index 0. Ihn herauszunehmen, reißt ein Loch an der Spitze. Also kommt der letzte Eintrag des Arrays dorthin, und das Array wird um eins kürzer. Der neue an der Spitze ist meist zu groß. Er tauscht mit dem kleineren seiner beiden Kinder, solange dieses kleiner ist als er. Er sickert nach unten.
pop(heap):
wenn heap leer: es gibt nichts
oben ← heap[0]
letzter ← den letzten Eintrag aus heap nehmen
wenn heap nicht leer:
heap[0] ← letzter
i ← 0
wiederholen:
kleinster ← i
für kind in (2i + 1, 2i + 2):
wenn kind < Länge von heap und heap[kind] < heap[kleinster]:
kleinster ← kind
wenn kleinster = i: fertig
heap[kleinster] und heap[i] tauschen
i ← kleinster
gib oben zurück
Erst kommen 5, 3, 8, 1, 4 und 2 hinein, dann holt pop dreimal den kleinsten heraus:
Typische Fehler
2i + 1 kann hinter dem Ende liegen.Legt man alle Zahlen mit push in einen Heap und holt sie mit pop wieder heraus, kommen sie sortiert heraus, die kleinste zuerst. Das ist Heapsort. Es braucht für \(n\) Zahlen \(n\)-mal push und \(n\)-mal pop, jedes höchstens \(\log_2 n\) Schritte: zusammen \(O(n \log n)\), so schnell wie die besten Sortierverfahren, auch im schlechtesten Fall.
Ohne zweites Array geht es auch: Man baut den Heap im Array selbst auf und legt den jeweils größten Eintrag (mit einem Heap, in dem kein Kind größer ist als sein Elternteil) ans Ende, in den Teil, der schon sortiert ist. In der Sorting Sandbox kannst du Heapsort selbst programmieren und mit den anderen Verfahren vergleichen.