Datenstrukturen Mariano Zelke Sommersemester 2012 Prioritätswarteschlangen Der abstrakte Datentyp Prioritätswarteschlange“: Füge Elemente (mit ” Prioritäten) ein und entferne jeweils das Element höchster Priorität. I I Eine Schlange ist eine sehr spezielle Prioritätswarteschlange: Die Priorität eines Elements ist der negative Zeitpunkt der Einfügung. Der abstrakte Datentyp Prioritätswarteschlange“ umfasst dann die ” Operationen I I I I void insert(x,Priorität), int delete max(), void change priority(wo,Priorität? ), wähle Priorität? als neue Priorität und void remove(wo), entferne das durch wo beschriebene Element. Wir müssen eine geeignete Datenstruktur entwerfen. Mariano Zelke Datenstrukturen 2/28 Der Heap Ein Heap ist ein Binärbaum mit Heap-Struktur, der Prioritäten gemäß einer Heap-Ordnung abspeichert. Ein geordneter Binärbaum T der Tiefe t hat Heap-Struktur, wenn: (a) jeder Knoten der Tiefe höchstens t − 2 genau 2 Kinder hat, (b) wenn ein Knoten v der Tiefe t − 1 weniger als 2 Kinder hat, dann I I haben alle Knoten der Tiefe t − 1, die rechts von v liegen, kein Kind wenn v genau ein Kind hat, dann ist es ein linkes Kind. (Daraus folgt, dass nur höchstens ein Knoten genau ein Kind haben kann.) Ein Binärbaum mit Heap-Struktur ist ein fast vollständiger binärer Baum: Alle Knoten links von v haben zwei Kinder, alle Knoten rechts von v haben keine Kinder. Mariano Zelke Datenstrukturen 3/28 Beispiele Heap-Struktur Dieser Baum hat Heap-Struktur: genauso wie dieser: und dieser: Dieser Baum hat keine HeapStruktur: dieser auch nicht: Mariano Zelke Datenstrukturen 4/28 Heap-Ordnung Ein geordneter binärer Baum T mit Heap-Struktur speichere für jeden Knoten v die Priorität p(v ) von v . T hat Heap-Ordnung, falls für jeden Knoten v und für jedes Kind w von v gilt p(v ) ≥ p(w ) I Die höchste Priorität wird stets an der Wurzel gespeichert. Für die Operation delete max() muss nur die Priorität der Wurzel überschrieben werden. I Wie sollte man einen Baum mit Heap-Struktur implementieren? Wir arbeiten mit einem Array. Mariano Zelke Datenstrukturen 5/28 Die Datenstruktur Heap (Link) Das Array H ist ein Heap für T , wenn T Heap-Struktur und Heap-Ordnung hat. Zusätzlich muss gelten I H[1] = p(r ) für die Wurzel r von T und I wenn H[i] die Priorität des Knotens v speichert, dann gilt H[2 · i] = p(vL ) für das linke Kind vL von v und H[2 · i + 1] = p(vR ) für das rechte Kind vR . Beispiel 9 4 7 besitzt den Heap: (9,4,7,3,1) Der folgende Baum verletzt die Heap-Struktur: 9 3 1 4 dargestellt als Array 0 9 4 7 3 1 Mariano Zelke 3 Datenstrukturen 7 1 Sein Heap“ ” (9,4,7,3, ,1) enthält ein Loch. 6/28 Die Funktion Insert I Wie navigiert man in einem Heap H? Wenn Knoten v in Position i gespeichert ist, dann ist das linke Kind vL in Position 2 · i, das rechte Kind in Position 2 · i + 1 und der Vater von v in Position bi/2c gespeichert. I Wenn wir die Priorität p einfügen wollen, liegt es nahe, p auf der ersten freien Position abzulegen. Wir erhöhen also den Zähler n für die Anzahl der Elemente im Heap um eins: n = n + 1, und speichern danach die neue Priorität ab: H[n] = p I I Der neue Baum hat Heap-Struktur, aber die Heap-Ordnung ist möglicherweise verletzt. Wie kann die Heap-Ordnung kostengünstig repariert werden? Mariano Zelke Datenstrukturen 7/28 Wir fügen die Priorität 11 ein 9 7 2 Nach dem Anhängen von 11 ist die Heap-Ordnung verletzt. 3 5 1 11 Darum rutscht die 11 nach oben: 9 7 2 11 5 1 3 Und ein weiterer Vertauschungsschritt repariert die Heap-Ordnung: 11 7 2 Mariano Zelke Datenstrukturen 9 5 1 3 8/28 Die Repair up Prozedur Die Klasse heap enthalte die Funktion repair up. void heap::repair up (int wo){ int p = H[wo]; while ((wo > 1) && (H[wo/2] < p)){ H[wo] = H[wo/2]; wo = wo/2; } H[wo] = p; } I Wir verschieben die Priorität solange nach oben, bis I I I entweder die Priorität des Vaters mindestens so groß ist oder bis wir die Wurzel erreicht haben. Wie groß ist der Aufwand? Höchstens proportional zur Tiefe des Baums! Mariano Zelke Datenstrukturen 9/28 Die Funktion Delete max() H repräsentiere einen Heap mit n Prioritäten. Für delete max: I gib die Priorität H[1] zurück I Überschreibe die Wurzel mit H[n] I und verringere n um 1. I Durch das Überschreiben mit H(n) ist das entstandene Loch an der Wurzel verschwunden: Die Heap-Struktur ist wiederhergestellt. Allerdings ist die Heap-Ordnung möglicherweise verletzt und muss repariert werden. I Die Prozedur repair up versagt: sie ist nur anwendbar, wenn die falsch stehende Priorität größer als die Vater-Priorität ist. Mariano Zelke Datenstrukturen 10/28 Ein Beispiel 5 3 2 1 Wir setzen das letzte Heap-Element an die Wurzel: 3 5 1 3 Nach Entfernen des Maximums fehlt die Wurzel. 4 4 2 1 5 4 2 Damit ist die Heap-Ordnung verletzt. Darum vertauschen wir die Wurzel mit dem größten Kind. Heap-Ordnung immer noch verletzt. Vertausche weiter mit 3 dem größten Kind, bis wieder Heap-Ordnung 1 erreicht. 5 4 2 Repariere die Heap-Ordnung nach unten. Mariano Zelke Datenstrukturen 11/28 Die Prozedur Repair down Die Klasse heap enthalte die Funktion repair down. void heap::repair down (int wo){ int kind; int p = H[wo]; while (wo <= n/2){ kind = 2 * wo; if ((kind < n) && (H[kind] < H[kind + 1])) kind ++; if (p >= H [kind]) break; H[wo] = H[kind]; wo = kind; } H[wo] = p; } Animation I Die Priorität p wird mit der Priorität des größten Kinds“ verglichen ” und möglicherweise vertauscht. Die Prozedur endet, wenn wo die richtige Position ist, bzw. wenn wo ein Blatt bezeichnet. I Wie groß ist der Aufwand? Höchstens proportional zur Tiefe. Mariano Zelke Datenstrukturen 12/28 Change priority und Remove I void change priority (int wo, int p): I I Wir aktualisieren die Priorität, setzen also H [wo] = p. Aber wir verletzen damit möglicherweise die Heap-Ordnung! I I I Wenn die Priorität angewachsen ist, dann rufe repair up auf. Ansonsten hat sich die Priorität verringert und repair down ist aufzurufen. void remove(int wo): I I Stelle die Heap-Struktur durch wieder her durch H[wo] = H[n]; n = n − 1; und repariere Heap-Ordnung wie bei change priority. Alle vier Operationen insert, delete max, change priority und remove benötigen Zeit höchstens proportional zur Tiefe des Heaps. Mariano Zelke Datenstrukturen 13/28 Die Tiefe eines Heaps mit n Knoten Der Binärbaum T besitze Heap-Struktur. I Wenn T die Tiefe t besitzt, dann hat T mindestens 1 + 21 + 22 + . . . + 2t−1 + 1 = 2t Knoten I aber nicht mehr als 1 + 21 + 22 + . . . + 2t−1 + 2t = 2t+1 − 1 Knoten. I Also folgt 2Tiefe(T ) ≤ n < 2Tiefe(T )+1 . Tiefe (T ) = blog2 nc und alle vier Operationen werden somit in logarithmischer Zeit unterstützt! Mariano Zelke Datenstrukturen 14/28 Heapsort Ein Array (A[1], . . . , A[n]) ist zu sortieren. Dazu benutzen wir den Heap h, der anfangs leer ist. for (i = 1; i <= n ; i++) h.insert(A[i]); for (i = n; i >= 1 ; i--) A[i] = h.delete max(); //Das Array A ist jetzt aufsteigend sortiert. I Zuerst wird n Mal eingefügt und dann n Mal das Maximum entfernt. I Sowohl die anfängliche Einfügephase wie auch die letztliche Entfernungsphase benötigen Zeit höchstens O(n · log2 n). Heapsort ist eines der schnellsten Sortierverfahren. Die anfängliche Einfügephase kann sogar noch weiter beschleunigt werden! Mariano Zelke Datenstrukturen 15/28 Wie kann der Heap schneller geladen werden? Führe statt vielen kleinen Reparaturen eine große Reparatur durch. I Lade den Heap ohne Reparaturen I Beginne die Reparatur mit den Blättern. Jedes Blatt ist schon ein Heap und eine Reparatur ist nicht notwendig. Wenn t die Tiefe des Heaps ist, dann kümmern wir uns als nächstes um die Knoten v der Tiefe t − 1: I I I I I Sei Tv der Teilbaum mit Wurzel v . Tv ist nur dann kein Heap, wenn die Heap-Ordnung im Knoten v verletzt ist: Repariere mit repair down, gestartet in v . Höchstens ein Vertauschungsschritt wird benötigt. Wenn v ein Knoten der Tiefe t − j ist, dann muss höchstens die Heap-Ordnung im Knoten v repariert werden. Höchstens j Vertauschungsschritte genügen. Es gibt nur wenige teure Reparaturschritte! Mariano Zelke Datenstrukturen 16/28 Analyse I Es gibt 2t−j Knoten der Tiefe t − j (für j ≥ 1). I Für jeden dieser Knoten sind höchstens j Vertauschungsschritte t P durchzuführen, für alle Knoten ist dies also durch j · 2t−j j=1 beschränkt. I Behauptung: t P j · 2t−j = 2t+1 − t − 2. j=1 Wir geben einen induktiven Beweis: t+1 t X X j · 2t+1−j = 2 j · 2t−j + t + 1 j=1 j=1 = 2 · (2t+1 − t − 2) + t + 1 = 2t+2 − (t + 1) − 2. I Da 2t ≤ n gilt, ist 2t+1 − t − 2 ≤ 2n − log n − 2 Der Heap kann in linearer Zeit geladen werden. Mariano Zelke Datenstrukturen 17/28 Die Klasse heap class heap{ private: int *H; // H ist der Heap. int n; // n bezeichnet die Größe des Heaps. void repair up (int wo); void repair down (int wo); public: heap (int max) { H = new int[max]; n=0; } //Konstruktor void insert (int priority); int delete max( ); void change priority (int wo, int p); void remove(int wo); void heapsort(); void build heap(); void write (int i) { n++; H[n] = i; } }; Mariano Zelke Datenstrukturen 18/28 Prioritätswarteschlangen: Zusammenfassung (a) Ein Heap mit n Prioritäten unterstützt jede der Operationen insert, delete max, change priority und remove in Zeit O(log2 n). I Für die Operationen change priority und remove muss die Position der zu ändernden Priorität bekannt sein. (b) build heap baut einen Heap mit n Prioritäten in Zeit O(n). (c) heapsort sortiert n Zahlen in Zeit O(n log2 n). Mariano Zelke Datenstrukturen 19/28 Das Single-Source-Shortest-Path-Problem Ein gerichteter Graph G = (V , E ) und eine Längen-Zuweisung länge: E → R≥0 an die Kanten des Graphen ist gegeben. Bestimme kürzeste Wege von einem ausgezeichneten Startknoten s ∈ V zu allen Knoten von G . I Die Länge eines Weges ist die Summe seiner Kantengewichte. I Mit Hilfe der Breitensuche können wir kürzeste-Wege Probleme lösen, falls länge(e) = 1 für jede Kante e ∈ E gilt. Für allgemeine nicht-negative Längen brauchen wir eine ausgeklügeltere Idee. I I Mariano Zelke Kantengewichte sind nicht-negativ: Die kürzeste, mit s verbundene Kante (s, v ) ist ein kürzester Weg von s nach v . Dijkstras Algorithmus setzt diese Beobachtung wiederholt ein. Datenstrukturen 20/28 Dijkstras Algorithmus (1) Setze S = {s} und für alle Knoten v ∈ V \ {s} setze länge (s, v ) wenn (s, v ) ∈ E distanz[v] = ∞ sonst. /* distanz[v] ist die Länge des bisher festgestellten kürzesten Weges von s nach v . */ (2) Solange S 6= V wiederhole (a) wähle einen Knoten w ∈ V \ S mit kleinstem Distanz-Wert. /* distanz[w] ist die tatsächliche Länge eines kürzesten Weges von s nach w . */ (b) Füge w in S ein. (c) Aktualisiere die Distanz-Werte der Nachfolger von w : Setze für jeden Nachfolger u ∈ V \ S von w distanz[u] = min(distanz[u], distanz[w]+länge(w,u)); Mariano Zelke Datenstrukturen 21/28 Beispiel Dijkstras Algorithmus 4 3 10 s 20 5 4 Mariano Zelke 5 2 4 3 0 Datenstrukturen 23 28 29 1 22/28 Beispiel Dijkstras Algorithmus S V \S distanz[4]=3 4 3 10 s 2 4 20 5 distanz[5]=0 3 distanz[3]=20 4 Mariano Zelke distanz[2]=∞ 5 0 distanz[0]=4 Datenstrukturen 23 28 29 1 distanz[1]=∞ 22/28 Beispiel Dijkstras Algorithmus S V \S distanz[4]=3 4 3 10 s 2 4 20 5 distanz[5]=0 3 distanz[3]=13 4 Mariano Zelke distanz[2]=8 5 0 distanz[0]=4 Datenstrukturen 23 28 29 1 distanz[1]=32 22/28 Beispiel Dijkstras Algorithmus distanz[4]=3 4 3 10 s 2 4 20 5 distanz[5]=0 3 distanz[3]=12 4 Mariano Zelke distanz[2]=8 5 0 distanz[0]=4 Datenstrukturen 23 28 29 1 distanz[1]=32 22/28 Datenstrukturen für Dijkstras Algorithmus In der Vorlesung Algorithmentheorie“ wird gezeigt, dass Dijkstras ” Algorithmus korrekt ist und das Kürzeste-Wege-Problem effizient löst. I I Darstellung des Graphen G : Wir implementieren G als Adjazenzliste, da wir dann sofortigen Zugriff auf die Nachfolger u von w im Aktualisierungschritt (2c) haben. Implementierung der Menge V \ S: I I Knoten sind gemäß ihrem anfänglichen Distanzwert einzufügen. Ein Knoten w mit kleinstem Distanzwert ist zu bestimmen und zu entfernen. Wähle einen Min-Heap, um die entsprechende Prioritätswarteschlange zu implementieren: I I Mariano Zelke Ersetze die Funktion delete max() durch die Funktion delete min(). (Und passe repair up und repair down entsprechend an.) Implementiere den Aktualisierungschritt (2c) durch change priority(wo,neue Distanz). Woher kennen wir die Position wo? Datenstrukturen 23/28 Minimale Spannbäume (Link) Sei G = (V , E ) ein ungerichteter, zusammenhängender Graph. Jede Kante e ∈ E erhält eine positiv, reellwertige Länge länge(e)“. ” I Ein Baum T = (V 0 , E 0 ) heißt ein Spannbaum für G , falls V 0 = V und E 0 ⊆ E . I Die Länge eines Spannbaums ist die Summe der Längen seiner Kanten. I Ein minimaler Spannbaum ist ein Spannbaum minimaler Länge. I Je zwei Knoten von G bleiben auch in einem Spannbaum T miteinander verbunden, denn ein Baum ist zusammenhängend. Wenn wir aber irgendeine Kante aus T entfernen, dann zerstören wir den Zusammenhang. I Wir suchen nach einem zusammenhängenden Teilgraph in G , der minimale Länge hat. Mariano Zelke Datenstrukturen 24/28 Der Algorithmus von Prim: Die Idee I Angenommen wir wissen, dass ein Baum B in einem minimalen Spannbaum enthalten ist. I Wir möchten eine kreuzende Kante zu B hinzufügen: e soll also einen Knoten in B mit einem Knoten außerhalb von B verbinden. I Der Algorithmus von Prim wählt eine kürzeste kreuzende Kante. I In der Vorlesung Algorithmentheorie“ wird gezeigt, dass auch ” B ∪ {e} in einem minimalen Spannbaum enthalten ist: Der Algorithmus berechnet also einen minimalen Spannbaum. Worauf müssen wir bei der Implementierung achten? I I I Mariano Zelke Eine kürzeste kreuzende Kante muss schnell gefunden werden. Wenn der Baum B um einen neuen Knoten u anwächst, dann erhalten wir neue kreuzende Kanten, nämlich in u endende Kanten. Datenstrukturen 25/28 Der Algorithmus von Prim (1) Setze S = {0}. /* B ist stets ein Baum mit Knotenmenge S. Zu Anfang besteht B nur aus dem Knoten 0. */ (2) Solange S 6= V , wiederhole: (a) Bestimme eine kürzeste kreuzende Kante e = {u, v }. (b) Füge e zu B hinzu. (c) Wenn u ∈ S, dann füge v zu S hinzu. Ansonsten füge u zu S hinzu. /* Beachte, dass wir neue kreuzende Kanten erhalten, nämlich alle Kanten die den neu hinzugefügten Knoten als einen Endpunkt und einen Knoten aus V \ S als den anderen Endpunkt besitzen. */ Mariano Zelke Datenstrukturen 26/28 Beispiel Prims Algorithmus Hamburg Rostock Bremen Oldenburg 2 9 39 16 12 11 21 18 Berlin Hannover 6 7 Dortmund 16 19 Leipzig 8 17 10 Frankfurt/M. 14 12 Karlsruhe Dresden Nürnberg 15 3 München Mariano Zelke Datenstrukturen 27/28 Beispiel Prims Algorithmus Hamburg Rostock Bremen Oldenburg 2 9 39 16 12 11 21 18 Berlin Hannover 6 7 Dortmund 16 19 Leipzig 8 17 10 Frankfurt/M. V \S 14 12 S Karlsruhe Dresden Nürnberg 15 3 München Mariano Zelke Datenstrukturen 27/28 Beispiel Prims Algorithmus Hamburg Rostock Bremen Oldenburg 2 9 39 16 12 11 21 18 Berlin Hannover 6 7 Dortmund 16 19 Leipzig 8 17 10 Frankfurt/M. V \S 14 12 S Karlsruhe Dresden Nürnberg 15 3 München Mariano Zelke Datenstrukturen 27/28 Beispiel Prims Algorithmus Hamburg Rostock Bremen Oldenburg 2 9 39 16 12 11 21 18 Berlin Hannover 6 7 Dortmund 16 19 Leipzig 8 17 10 Frankfurt/M. V \S 14 12 S Karlsruhe Dresden Nürnberg 15 3 München Mariano Zelke Datenstrukturen 27/28 Beispiel Prims Algorithmus Hamburg Rostock Bremen Oldenburg 2 9 39 16 11 Berlin Hannover 6 7 Leipzig 8 Dortmund 10 Frankfurt/M. Dresden 14 12 Nürnberg Karlsruhe 3 München Mariano Zelke Datenstrukturen 27/28 Die Datenstrukturen für Prims Algorithmus I I Für jeden Knoten u ∈ V \ S bestimmen wir die Länge l(u) einer kürzesten Kante, die u mit einem Knoten in S verbindet. Wir verwalten die Knoten in V \ S mit einer Prioritätswarteschlange und definieren l(u) als die Priorität des Knotens u. I I I I Initialisiere einen Min-Heap, indem jeder Nachbar u von Startknoten 0 mit Priorität länge({0, u}) einfügt wird, bzw. mit Priorität ∞, wenn u kein Nachbar ist. Wir bestimmen also eine kürzeste kreuzende Kante, wenn wir einen Knoten in u ∈ V \ S mit niedrigster Priorität bestimmen. Beachte, dass sich nur die Prioritäten der Nachbarn von u ändern. Implementiere G durch eine Adjazenzliste, da wir stets nur auf die Nachbarn eines Knoten zugreifen müssen. Mariano Zelke Datenstrukturen 28/28