Algorithmen und Datenstrukturen Prof. Martin Lercher Institut für Informatik Heinrich-Heine-Universität Düsseldorf Teil 7 Vorrangwarteschlangen (Priority Queues) Version vom 2. Dezember 2016 1 / 53 Vorlesung 11 Fortsetzung 29. November 2016 2 / 53 Vorrangwarteschlangen (Priority-Queues) Eine Priority-Queue D soll Elemente mit Schlüsseln speichern, auf denen über ihre Schlüssel eine Ordnung definiert ist. Folgende Operationen sollen zur Verfügung stehen: • Initialisierung einer Datenstruktur D: D := init() bzw. Initialisierung einer Datenstruktur D mit einem Element k: D := init(k) • Einfügen eines Elementes k in Datenstruktur D: insert(D, k) • Entfernen eines Elementes k: delete(D, k) • Element mit kleinstem Schlüssel in D bestimmen: access-min(D) 3 / 53 Vorrangwarteschlangen (Priority-Queues) • Element mit kleinstem Schlüssel entfernen: delete-min(D) • Verändern des Schlüssels x von Element k zu Schlüssel y : relocate(D, k, y ) • Verkleinern des Schlüssels x von Element k auf Schlüssel y : decrease(D, k, y ) • Zusammenfügen von zwei Datenstrukturen D1 , D2 zu einer neuen Datenstruktur D: D = merge(D1 , D2 ) 4 / 53 Vorrangwarteschlangen (Priority-Queues) Anwendungen: • Diskrete Simulationen (Ereignisse, die zu einer bestimmten Zeit in der Zukunft passieren) • Verwaltung von Bandbreite (Zeitkritische Information, z.B. für VoIP, wird prioritisiert) • Hilfsfunktion für Algorithmen, die wiederholt Minima benötigen (z.B. in vielen Graphalgorithmen) 5 / 53 Vorrangwarteschlangen (Priority-Queues) Anmerkungen: Die Operation access-min(D) soll in O(1) Schritten, alle übrigen Operationen sollen in O(log(n)) Schritten ausführbar sein. Es ist nicht notwendig, die Elemente in der Datenstruktur zu suchen! Priority-Queues können auf verschiedene Arten implementiert werden, zum Beispiel durch Linksbäume, Binomial-Queues oder Fibonacci-Heaps. Ein Baum mit n + 1 Blättern und n inneren Knoten ist balanciert, wenn jedes Blatt eine Tiefe aus O(log(n)) hat. Zur Implementierung von Priority-Queues reicht eine wesentlich schwächere Forderung aus, um zu sichern, dass die obigen Operationen in der vorgegebenen Zeit ausführbar sind. 6 / 53 Linksbäume Linksbäume sind binäre heapgeordnete, links-rechts geordnete Bäume, die in ihren inneren Knoten Elemente mit Schlüsseln speichern. 1 Die Schlüsselwerte der Söhne sind stets größer als der Schlüsselwert des Vaters (Heapeigenschaft). 2 Jeder Knoten besitzt einen Distanzwert. Der Distanzwert der Blätter ist 0. Der Distanzwert an einem inneren Knoten ist der Distanzwert des rechten Sohnes plus 1. 3 Der Distanzwert des rechten Sohnes ist kleiner oder gleich dem Distanzwert des linken Sohnes. 1 2 7 / 53 Linksbäume Beispiel In den Zeichnungen lassen wir ab jetzt die Blätter weg. 1 2 Schlüssel 5 3 12 2 14 1 13 1 17 1 16 1 Distanzwert 3 1 7 2 15 1 23 1 20 1 27 1 19 1 8 / 53 Linksbäume Bemerkung: Die Anzahl der Blätter (und somit auch die Anzahl der inneren Knoten) in einem Linksbaum B mit einer Wurzel mit Distanzwert r ist mindestens 2r . =⇒ In einem Linksbaum hat das rechteste Blatt eine Tiefe von O(log(n)). Alle Operationen lassen sich auf das Verschmelzen von zwei Linksbäumen zurückführen. Die Operation insert(D, k) Verschmelze D mit einem Linksbaum, der einen einzigen inneren Knoten k mit Distanz 1 hat. Die Operation delete-min(D) Entferne die Wurzel und verschmelze die beiden Teilbäume der Wurzel. 9 / 53 Linksbäume Die Operation delete(D, k) Ersetze den Knoten k durch ein Blatt, das gegebenenfalls mit seinem Bruder vertauscht wird, um links den Teilbaum mit größerer Distanz anzuordnen. Adjustiere die Distanzwerte vom eingefügten Blatt bis zur Wurzel bzw. bis zum Ende in einem linken Sohn. Verschmelze den entstandenen Linksbaum und die beiden Teilbäume von k miteinander. Das Adjustieren der Distanzwerte benötigt höchstens log(n) viele Schritte. Die Distanzwerte beginnen bei 0 mit dem erzeugten Blatt und werden bei der Adjustierung auf dem Weg zur Wurzel immer schrittweise um genau 1 größer. Die Operation relocate(D, k, y ) delete(D, k), ändere den Schlüssel von k auf y und führe anschließend insert(D, k) aus. Die Operation decrease(D, k, y ) relocate(D, k, y ) 10 / 53 Linksbäume Die Operation merge(A, B) Wenn A (bzw. B) keine Elemente speichert, also aus genau einem Blatt besteht, dann ist das Ergebnis der Linksbaum B (bzw. A). Ohne Beschränkung der Allgemeinheit sei der Schlüssel an der Wurzel von A kleiner als der Schlüssel an der Wurzel von B (ansonsten werden die beiden Linksbäume vertauscht). Zuerst wird rekursiv mit der gleichen Methode der rechte Teilbaum von A mit dem Linksbaum B vereinigt. Das Ergebnis ist der neue rechte Teilbaum von A. Ist die Distanz vom rechten Teilbaum von A größer als die Distanz vom linken Teilbaum von A, so werden die beiden Teilbäume von A vertauscht. 11 / 53 Linksbäume Beispiel Verschmelzen zweier Linksbäume A und B mit merge(A, B): A 12 2 14 1 13 1 17 1 16 1 B 7 2 23 1 20 1 27 1 19 1 12 / 53 Linksbäume Beispiel Bäume vertauschen: A 7 2 23 1 27 1 B 12 2 20 1 14 1 13 1 17 1 16 1 19 1 13 / 53 Linksbäume Beispiel Verschmelzen zweier Linksbäume A und B mit merge(A.rechts, B): A 20 1 B 12 2 14 1 13 1 17 1 16 1 19 1 14 / 53 Linksbäume Beispiel Bäume vertauschen: A 12 2 B 20 1 14 1 13 1 17 1 16 1 19 1 15 / 53 Linksbäume Beispiel Verschmelzen zweier Linksbäume A und B mit merge(A.rechts, B): A 13 1 B 20 1 16 1 16 / 53 Linksbäume Beispiel Ergebnis der letzten Rekursion: 13 2 16 1 20 1 17 / 53 Linksbäume Beispiel Ergebnis der vorletzten Rekursion: 12 2 13 2 16 1 20 1 14 1 17 1 19 1 18 / 53 Linksbäume Beispiel Ergebnis: 7 2 12 2 23 1 13 2 16 1 20 1 14 1 27 1 17 1 19 1 19 / 53 Linksbäume Die Laufzeit der Vereinigungsoperation ist beschränkt durch die Summe der Längen der beiden Pfade von der Wurzel zum jeweils rechtesten Blatt, also logarithmisch in der Anzahl der gespeicherten Elemente beschränkt. Laufzeiten: init() insert(D, k) access-min(D) delete-min(D) delete(D, k) relocate(D, k, y ) decrease(D, k, y ) merge(D1 , D2 ) O(1) O(log(n)) O(1) O(log(n)) O(log(n)) O(log(n)) O(log(n)) O(log(n)) 20 / 53 Vorlesung 12 2. Dezember 2016 21 / 53 Binomial-Queues Binomialbäume sind heapgeordnete Bäume, die in allen Knoten Elemente mit Schlüsseln speichern. Ein Baum vom Typ B0 besteht aus genau einem Knoten. Ein Baum vom Typ Bi+1 für i ≥ 0 besteht aus zwei Kopien von Bäumen vom Typ Bi , indem man die Wurzel der einen Kopie zum Sohn der Wurzel der anderen macht. 22 / 53 Binomial-Queues B0 B1 B2 B3 B4 B i+1 Bi Bi 23 / 53 Binomial-Queues Eigenschaften: Ein Baum vom Typ Bi hat 2i Knoten, die Höhe i und hi Knoten auf Höhe h. (Binomialkoeffizient hi =Anzahl h-elementiger Teilmengen einer i-elementigen Menge) Die i Teilbäume der Wurzel sind vom Typ Bi−1 , Bi−2 , . . . , B0 . Zur Speicherung einer Menge von n Elementen verwenden wir einen Wald, der von jedem Typ Bi maximal einen Baum besitzt. Es werden genau so viele Binomialbäume benötigt wie Einsen in der Binärdarstellung von n = (dm−1 . . . d0 ) enthalten sind. Es wird genau dann ein Baum vom Typ Bj benötigt, wenn dj = 1. =⇒ Die Anzahl der Bäume in einer Binomial-Queue mit n > 1 Elementen ist höchstes log(n). Beispiel: 11 (dezimal) = 1 0 |{z} 1 |{z} 1 |{z} B3 B1 (binär) B0 Eine derartige Repräsentation einer Menge mit n Elementen ist eine Binomial- Queue vom Typ Dn . 24 / 53 Binomial-Queues Beispiel n = 11, {7, 10, 12, 13, 14, 16, 17, 19, 20, 23, 27} B0 B1 27 20 B3 7 23 17 14 12 16 13 10 19 25 / 53 Binomial-Queues Die Operation init(k) Erzeuge einen Baum mit genau einem Knoten, der k speichert. Zeitaufwand O(1) Die Operation access-min(Dn ) Durchsuchen der Wurzeln der Binomial-Bäume ⇒ Zeitaufwand O(log(n)) Die Operation merge(Dn , Dm ) (Das Vereinigen zweier Bäume vom gleichen Typ ist bereits definiert, ∈ O(1).) Vereinige die Bäume aus beiden Binomial-Queues vom Typ Dn und Dm . Gibt es in der Vereinigung zwei Bäume vom Typ B0 , so vereinige sie zu einem Baum vom Typ B1 . Gibt es anschließend zwei Bäume vom Typ B1 , so vereinige sie zu einem Baum vom Typ B2 , usw. (Addition zweier (Binär-)Zahlen nach der Schulmethode!) ⇒ Laufzeit: O(log(n + m)) 26 / 53 Binomial-Queues Die Operation insert(Dn , k) merge(Dn , init(k)) Die Operation delete-min(Dn ) Sei Bj der Baum mit access-min(Dn ) in der Wurzel. Sei Dn−2j der Wald Dn ohne Bj . Sei D2j −1 der Wald, der aus Bj entsteht, wenn in Bj die Wurzel entfernt wird. Verschmelze Dn−2j mit D2j −1 um delete-min(Dn ) zu erhalten. ⇒ Laufzeit: O(n − 2j + 2j − 1) = O(log(n)) 27 / 53 Binomial-Queues Die Operation delete(Dn , k) Sei Bj der Baum, der k enthält. Entferne Bj aus Dn . Sei Dn−2j das Ergebnis. Zerlege nun Bj in einen Wald F . r l der rechte Teilbaum, aus dem Bj der linke Teilbaum und Bj−1 Sei Bj−1 r zusammengesetzt wurde (Bj−1 beinhaltet die Wurzel von Bj ). r l l , dann wird Bj−1 in F aufgenommen und mit Bj−1 Ist k in Bj−1 fortgefahren. r l r Ist k in Bj−1 , dann wird Bj−1 in F aufgenommen und mit Bj−1 fortgefahren. Ist der Teilbaum mit Wurzel k erreicht, dann entferne k und füge die entstehenden Teilbäume zu F hinzu. Vereinige Dn−2j und F zu einer Binomial-Queue. ⇒ Laufzeit: O(log(n)) 28 / 53 Binomial-Queues Die Operation relocate(H, k, y ) delete(D, k), ändere den Schlüssel von k auf y und führe anschließend insert(D, k) aus. Bemerkung: Die Implementation von Binomial-Queues verlangt die programmtechnische Realisation von Bäumen mit unbeschränktem Grad (d.h. mit unbeschränkter Zahl von Söhnen). 29 / 53 Binomial-Queues Beispiel Nacheinander werden folgende Elemente eingefügt: 13, 17, 7, 20, 12, 19 13 13 13 7 17 20 17 13 13 17 17 13 7 17 20 7 7 13 20 17 7 13 17 20 12 ... 13 7 12 20 19 17 Aufgabe: Welche Boxen sind Binomial-Queues, welche Zwischenschritte? 30 / 53 Binomial-Queues Beispiel Herausnahme von 13 7 15 13 10 17 20 19 14 16 15 7 16 10 17 20 19 14 7 15 10 17 20 19 14 16 31 / 53 Binomial-Queues Laufzeiten: init(k) insert(D, k) access-min(D) delete-min(D) delete(D, k) relocate(D, k, y ) decrease(D, k, y ) merge(D1 , D2 ) O(1) O(log(n)) O(log(n)) O(log(n)) O(log(n)) O(log(n)) O(log(n)) O(log(n)) 32 / 53 Fibonacci-Heaps Ein Fibonacci-Heap ist eine Sammlung heapgeordneter Bäume. Die Struktur der Fibonacci-Heaps ist implizit durch die erklärten Operationen definiert, d.h., jede mit den bereitgestellten Operationen aufbaubare Struktur ist ein Fibonacci-Heap. Die Wurzeln der Bäume sind Elemente einer doppelt verketteten zyklisch geschlossenen Wurzelliste. Der Fibonacci-Heap besitzt einen Zeiger (Minimalzeiger) auf den Knoten mit kleinstem Schlüssel (den Minimalknoten) in der Wurzelliste. Die Söhne jedes Knotens sind ebenfalls doppelt zyklisch verkettet. Jeder Knoten hat einen Rang (:= Anzahl der Söhne) und ein Markierungsfeld (s.u.). 33 / 53 Fibonacci-Heaps Beispiel 8 17 12 1 22 18 4 10 20 34 / 53 Fibonacci-Heaps Die Operation init() Erzeuge einen leeren Fibonacci-Heap mit einem “Null”-Zeiger. Die Operation access-min(H) Der Minimalzeiger von H zeigt auf den Minimalknoten. Somit kann der kleinste Schlüssel direkt angegeben werden kann. Die Operation merge(H1 , H2 ) Hänge die Wurzellisten von H1 und H2 aneinander. Der neue Minimalzeiger wird auf den kleineren der Minimalknoten von H1 und H2 gesetzt. Die Operation insert(H, k) Erzeuge einen Fibonacci-Heap H 0 , der nur das Element k enthält. Der Rang von k ist 0. Das Element k ist unmarkiert. Anschließend werden H und H 0 mit der Operation merge(H, H 0 ) vereinigt. Bemerkung: Alle bisherigen Operationen sind in Zeit O(1) ausführbar! 35 / 53 Fibonacci-Heaps Die Operation delete-min(H) Entferne den Minimalknoten u aus der Wurzelliste und bilde eine neue Wurzelliste durch Einhängen der Liste der Söhne von u an Stelle von u. Durchführbar in O(1) Schritten (Pointer!). Verschmelze nun solange zwei heapgeordnete Bäume, deren Wurzeln denselben Rang haben, zu einem neuen heapgeordneten Baum, bis die Wurzelliste nur Bäume enthält, deren Wurzeln einen verschiedenen Rang haben. Beim Verschmelzen zweier Bäume B und B 0 vom Rang i entsteht ein Baum vom Rang i + 1. Ist das Element in der Wurzel v von B größer als das Element in der Wurzel v 0 von B 0 , dann wird v ein Sohn von v 0 . Das Markierungsfeld von v 0 wird auf unmarkiert gesetzt. Aktualisiere dabei auch den Minimalzeiger. (Dies entspricht der Schulmethode zur Addition von Binärzahlen.) ⇒ Worst-Case Laufzeit: O(n) 36 / 53 Fibonacci-Heaps Beispiel Aufbau eines neuen Fibonacci-Heaps mit den Schlüsseln 17, 20, 7, 10, 8 17 17 20 17 20 17 17 7 7 17 7 20 10 17 10 20 20 7 17 8 17 7 7 20 10 20 8 10 20 37 / 53 Fibonacci-Heaps Beispiel delete-min(H) 17 7 8 17 10 20 8 10 20 8 17 17 8 10 20 10 20 38 / 53 Fibonacci-Heaps Beispiel Einfügen der Schlüssel 12, 1, 22, 18 ,4 in den bestehenden Fibonacci-Heap 8 17 12 22 18 4 10 20 Aufgabe: Welcher Schlüssel fehlt in der Abbildung? 39 / 53 Fibonacci-Heaps Beispiel delete-min(H) 17 8 12 4 10 22 18 8 17 20 20 10 4 12 18 22 4 17 8 12 10 22 18 20 40 / 53 Fibonacci-Heaps Beobachtungen: Bis jetzt gilt: 1 Nach jeder insert(), delete-min() und merge()-Operation sind die Bäume in der Wurzelliste Binomialbäume. 2 Nach jeder delete-min()-Operation bilden die Bäume in der Wurzelliste eine Binomial-Queue. 41 / 53 Fibonacci-Heaps Die Operation decrease(H, k, y ) Trenne k von seinem Vater ab, verkleinere den Schlüssel auf y , und hänge den beim Abtrennen entstandenen neuen (heapgeordneten) Baum in die Wurzelliste. Aktualisiere den Minimalzeiger. Durchführbar in O(1) Schritten. Es soll verhindert werden, dass mehr als ein Sohn von einem Vater abgetrennt wird. Beim Abtrennen eines Knotens p von seinem Vater v wird v markiert. War v bereits markiert, wird auch v von seinem Vater v 0 abgetrennt und v 0 markiert, usw. Alle abgetrennten Teilbäume werden in die Wurzelliste aufgenommen. ⇒ Worst-Case Laufzeit: O(n) (leider, Übungsaufgaben!) 42 / 53 Fibonacci-Heaps Beispiel 1 4 8 17 * * 12 15 22 19 7 11 13 14 16 20 43 / 53 Fibonacci-Heaps Beispiel decrease(H, 17, 5) 1 15 7 11 13 14 * 16 4 12 * 8 * 5 20 22 19 44 / 53 Fibonacci-Heaps Die Operation delete(H, k) Setze k auf einen sehr kleinen Schlüssel (mit decrease(H, k, −∞)) und führe dann delete-min(H) aus. ⇒ Worst-Case Laufzeit: O(n) Die Operation relocate(H, k, y ) delete(H, k), ändere den Schlüssel von k auf y und führe anschließend insert(D, k) aus. ⇒ Worst-Case Laufzeit: O(n) 45 / 53 Fibonacci-Heaps Lemma (Hilfssatz für einen späteren Beweis) Sei p ein Knoten eines Fibonacci-Heaps H. Ordnet man die Söhne von p in der zeitlichen Reihenfolge, in der sie an p angehängt wurden, so gilt: Der i-te Sohn von p hat mindestens den Rang max{i − 2, 0}. 46 / 53 Fibonacci-Heaps Beweis. Als der i-te Sohn u von p an p angehängt wurde, hatten u und p den gleichen Rang. Dieser Rang war i − 1, falls Knoten p seither keinen Sohn verloren hat, der vor dem i-ten Sohn (zeitlich betrachtet) angehängt wurde, bzw. i, falls p seither einen Sohn verloren hatte, der zeitlich betrachtet vor dem i-ten Sohn angehängt wurde, bzw. i − 1 + j mit j ≥ 2, falls p seither j Söhne verloren hat, die zeitlich betrachtet vor dem i-ten Sohn angehängt wurden (letzteres kann nur vorkommen, wenn p in der Wurzelliste ist). Also hat u mindestens den Rang i − 1, falls u bisher noch keinen Sohn verloren hat. Da Knoten u höchstens einen Sohn verloren haben kann, da er sonst von p abgetrennt worden wäre, hat u mindestens den Rang i − 2. 47 / 53 Fibonacci-Heaps Lemma Jeder Knoten p vom Rang k eines Fibonacci-Heaps H ist Wurzel eines Teilbaums mit mindestens Fk+2 Knoten, wobei Fk+2 die (k + 2)-te Fibonacci-Zahl ist. Beweis. (induktiv) Sei Sk die minimale Anzahl der Knoten in einem Teilbaum mit Wurzel p vom Rang k in einem Fibonacci-Heap. Induktionsanfang: S0 ≥ 1 = F2 S1 ≥ 2 = F3 48 / 53 Fibonacci-Heaps Beweis Fortsetzung. Induktionsschritt: Sei p ein Knoten vom Rang k ≥ 2. Ordne die k Söhne in der Reihenfolge, in der sie an p angehängt wurden. Nach obigem Lemma hat der i-te Sohn mindestens den Rang max{i − 2, 0}. Daraus folgt (inklusiv p): Sk ≥ + + + + + .. . 1 F1 F2 F3 F4 F5 .. . + Fk Knoten p (= 1) Knoten im (= 1) Knoten im Knoten im Knoten im Knoten im .. .. . . und somit Sk ≥ 1 + ersten Teilbaum mit Rang ≥ 0 2-ten Teilbaum mit Rang ≥ 0 3-ten Teilbaum mit Rang ≥ 1 4-ten Teilbaum mit Rang ≥ 2 5-ten Teilbaum mit Rang ≥ 3 Knoten im k-ten Teilbaum mit Rang ≥ i − 2 Pk i=1 Fi . 49 / 53 Fibonacci-Heaps Beweis Fortsetzung. Für k ≥ 2 gilt: 1+ k X Fi = Fk+2 i=1 denn: Fk+2 = = = = = = Fk+1 + Fk Fk + Fk−1 + Fk Fk−1 + Fk−2 + Fk−1 + Fk Fk−2 + Fk−3 + Fk−2 + Fk−1 + Fk F2 + F1 + F2 + · · · Fk−1 + Fk Pk 1 + i=1 Fi Damit gilt also: Sk ≥ 1 + k X Fi = Fk+2 i=1 50 / 53 Fibonacci-Heaps Bemerkung: Da die Fibonacci-Zahlen exponentiell mit einer Basis von etwa 1.618 wachsen, hat jede Wurzel in einem Fibonacci-Heap mit n Knoten einen Rang k ∈ O(log(n)). Nach einer delete-min()-Operation ist der Fibonacci-Heap eine Binomial-Queue. Daraus folgt, dass die Anzahl der Bäume in der Wurzelliste nach einer delete-min()-Operation aus O(log(n)) ist. 51 / 53 Fibonacci-Heaps Worst-Case Laufzeiten: init(k) insert(H, k) access-min(H) delete-min(H) delete(H, k) relocate(H, k, y ) decrease(H, k, y ) merge(H1 , H2 ) O(1) O(1) O(1) O(n) O(n) O(n) O(n) O(1) Die Operationen delete-min(), delete(), relocate(), decrease() in Fibonacci-Heaps haben zwar eine schlechte Worst-Case-Laufzeit, aber dafür eine sehr gute amortisierte Laufzeit (siehe nächstes Kapitel). 52 / 53 Fibonacci-Heaps Theorem (Satz) Für das Ausführen von n Operationen beginnend mit einem leeren Fibonacci-Heap H ist die insgesamt benötigte tatsächliche Zeit beschränkt durch die gesamte amortisierte Zeit, wobei 1 die amortisierten Zeiten der delete-min(H)-, delete(H, k)- und relocate(H, k, y )-Operationen aus O(log(n)) sind und 2 die amortisierten Zeiten aller Operationen – init(k), insert(H, k), access-min(H, k) und decrease(H, k, y ) – aus O(1) sind. Das bitte merken! 53 / 53