Prof. Dr. Peter Sanders G.V. Batz, C. Schulz, J. Speck Karlsruher Institut für Technologie Institut für Theoretische Informatik 7. Übungsblatt zu Algorithmen I im SS 2010 http://algo2.iti.kit.edu/AlgorithmenI.php {sanders,batz,christian.schulz,speck}@kit.edu Musterlösungen (Implizite binäre Suchbäume, 2 + 1 + 2 + 4 + 1 Punkte) Aufgabe 1 Ein binärer Suchbaum, bei dem auch die inneren Knoten Datenelemente enthalten (siehe 6. Übung), heiße perfekt balanciert, wenn seine Höhe blog nc beträgt bei n Datenelementen (siehe 6. Übung). a) Zeichnen Sie einen perfekt balancierten binären Suchbaum, der die Elemente 2, 3, 4, 7, 10, 14, 15, 19, 23 enthält und bei dem auch die inneren Knoten des Baumes Elemente tragen. b) Wir betrachten nun die implizite Darstellung von perfekt balancierten binären Suchbäumen, bei denen auch die innere Knoten Elemente tragen. Diese Darstellung erfolgt mit Hilfe von Arrays (analog zur impliziten Darstellung binärer Heaps). Dabei dürfen nur weniger als die Hälfte aller Arrayeinträge leer sein. Geben Sie die Formeln für den Index des linken und rechten Kindes des Elements mit Index i an (nehmen Sie an, dass beide Kinder existieren). c) Der Suchbaum aus a) soll nun implizit mit Hilfe eines Arrays dargestellt werden. Geben Sie den Inhalt des ensprechenden Arrays an. Leere Arrayeinträge werden mit dem Symbol ⊥ bezeichnet. d) Gegeben sei eine sortierte Folge von n Elementen. Geben Sie einen Algorithmus an, der in O(n) Zeit die implizite Darstellung eines perfekt balancierten binären Suchbaumes erzeugt. Der dargestellte Baum enthalte alle Elemente der Folge. Wieder dürfen nur weniger als die Hälfte aller Arrayeinträge leer sein. e) Argumentieren Sie warum Ihr Algorithmus aus d) das gewünschte Laufzeitverhalten aufweist. Musterlösung: a) 10 15 3 4 2 23 14 19 7 b) leftChild (i ) = 2i und rightChild (i ) = 2i + 1 c) Zustand des Arrays: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 10 3 15 2 4 14 23 ⊥ ⊥ ⊥ 7 ⊥ ⊥ 19 1 d) Sei s die sortierte Sequenz. Weiter verwenden wir zwei Arrays S[1..n] und B[1..m]. In B werde die implizite Darstellung des Suchbaumes gespeichert, S verwenden wir um eine Kopie der Folge s aufzunehemen (Arrays ermöglichen wahlfreien Zugriff in O(1) Zeit). Es werden s, n und m, sowie die Arrays S und B sozusagen als globale Variablen behandelt. 1: build 2: S := h⊥, . . . , ⊥i : Array //S allokieren | {z } n-mal 3: 4: 5: for i = 1..n do S[i] := si m := 2blog nc+1 − 1 B := h⊥, . . . , ⊥i : Array | {z } //S initialisieren //wähle m so groß, dass weniger als die Hälfte leer steht //B alokieren und initialisieren m-mal 6: build rec(1, n + 1, 1) //Baumaufbau für Sequenz S[1..n] Die eigentliche rekursive Funktion: 1: build rec(`, r, i) 2: if ` = r then return 3: assert i ≤ m 4: k := b(r + `)/2c 5: B[i] := S[k] 6: build rec(`, k, 2i) 7: build rec(k + 1, r, 2i + 1) //bei leerer Sequenz fertig //wähle Mitte“ von S[`..r − 1] ” //Element S[k] positionieren //Baumaufbau für Sequenz S[`..k − 1] //Baumaufbau für Sequenz S[k + 1..r − 1] e) Für jedes Element von S findet genau ein Aufruf der rekursivern Funktion statt. Oder aber es ist ` = r, was zum Abbruch der Rekusion führt. Die Ausführung der rekusiven Funktion selbst (ohne Tochteraufrufe) erfordert konstant viel Zeit. Insgesamt braucht der Aufruf build rec(1, n + 1, 1) inklusive Tochteraufrufe also O(n) Zeit. Das berechnen von blog nc lässt sich in O(log n) Zeit erledigen, das Berechnen von 2blog nc+1 sogar in O(log log n) Zeit. Also dauert das Berechnen von m nur O(log n) Zeit. Weiter ist m = 2blog nc+1 − 1 ≤ 2log n+1 − 1 < 2n = O(n). Also dauert das Initialisieren von B nicht mehr als O(n) Zeit. Das Initialisieren von S dauert sowieso nur O(n) Zeit. Insgesamt haben wir einen Zeitbedarf von O(n) Zeit. Aufgabe 2 (Suchen in sortierten Arrays bei mehrfachen Vorkommen, 1 + 1 + 4 + 2 Punkte) Betrachten Sie den Fall eines sortierten Arrays, bei dem Elemente auch mehrfach auftreten dürfen. a) Warmup: Gegeben sei ein sortiertes Array A der Länge n von ganzen Zahlen. Geben Sie einen Algorithmus an, der in schlimmstenfalls Θ(k + log n) Zeit die Positionen des ersten und des letzten Auftretens eines Elementes in A ermittelt. Dabei sei k die Anzahl der Vorkommen des gesuchten Elementes. b) Argumentieren Sie warum Ihr Algorithmus aus a) das gewünschte Laufzeitverhalten aufweist. c) Geben Sie einen weiteren Algorithmus an, der in O(log n) Zeit die Positionen des ersten und des letzten Auftretens eines Elementes in A ermittelt. d) Argumentieren Sie warum Ihr Algorithmus aus c) das gewünschte Laufzeitverhalten aufweist. Musterlösung: a) Man Suche mittels binärer Suche nach dem gesuchten Element x ∈ Z. Wird dieses nicht gefunden, so wissen wir, dass x nicht in A vorkommt. Wenn doch, dann liefert uns die binäre Suche die Posiotion eines Vorkommen in A, sei dieses i0 . Nun sucht man in A, ausgehend von Position i0 , jeweils nach links und rechts bis jeweils zum ersten mal ein Element 6= x auftritt. Da A sortiert 2 ist und somit alle Vorkommen von x in A nebeneinander liegen, findet man so die Position des ersten und des letztem Vorkommens von x in A. b) Die binäre Suche benötigt schlimmstenfalls Θ(log n) Zeit, die Suchen nach links und rechts ausgehend von Posiotion i0 zusammen Θ(k) Zeit. c) Sei x das gesuchte Elemente. Um die Position des ersten Auftretens von x zu ermitteln führt man eine modifizierte binäre Suche durch. Die Modifikation besteht darin, dass nicht nach einem Element x gesucht wird, sondern nach zwei nebeneinander liegenden Elementen, wobei das rechte Element = x sein soll und das linke Element < x. Dabei kann sich auch herausstellen, dass x gar nicht in A enthalten ist. Die Position letzten Vorkommens von x bestimmt man analog mit einer modifizierten binären Suche nach zwei Elementen von denen das linke Element = x und das rechte Element > x sein soll. d) Die modifizierten binären Suchen benötigen jeweils O(log n) Zeit. 3 (Adressierbare Heaps, 2 + 2 + 1 + 1 Punkte) Aufgabe 3 Gegeben sei ein leerer, adressierbarer binärer Heap, der implizit als Array realisert ist. D.h. in diesem Fall speichert das Array Handles auf Paare der Form (Schlüssel, Datenelement). Außerdem seien 6 Datenelemente a1 , . . . , a6 gegeben. a) Stellen Sie den Zustand des adressierbaren Heaps graphisch dar, wie er nach Ausführen der Operationsfolge insert(a1 , 18), insert(a2 , 42), insert(a3 , 21), insert(a4 , 7), insert(a5 , 19), insert(a6 , 13) aussieht. Z.B. kann man den Zustand nach Ausführen der beiden ersten Operationen wie folgt darstellen: 1 18 a1 2 42 a2 Die Doppelpfeile symbolisieren Handles für jeweils beide Richtungen. b) Nun werde decreaseKey(a5 , 5) ausgeführt. Wie sieht der Zustand des Arrays danach aus? c) Nun werde deleteMin ausgeführt. Wie sieht der Zustand des Arrays nun aus? d) Zuletzt werde decreaseKey(a1 , 11) ausgeführt. Wie sieht der Zustand des Arrays nun aus? Musterlösung: a) Zustand: 7 a4 13 a 6 19 a 5 1 2 3 4 5 18 a 1 6 21 a 3 42 a 2 b) Zustand: 7 a4 13 a 6 5 a5 1 2 3 4 18 a 1 5 6 21 a 3 42 a 2 c) Zustand: 4 7 a4 13 a 6 5 a5 1 2 3 4 5 18 a 1 21 a 3 42 a 2 d) Zustand: 7 a4 13 a 6 5 a5 1 2 3 4 11 a 1 5 21 a 3 42 a 2 (Bulk Insertion bei binären Heaps, 5 Punkte) Zusatzaufgabe Gegeben sei ein binärer Heap der n Elemente enthält. Es sollen nun k Elemente auf einmal eingefügt werden. Geben Sie ein Verfahren an (kein Pseudocode), mit dem man die Einfügung in O(min{k log k + log n, k + log n log k}) Schritten erledigen kann. Sie können davon ausgehen, dass der Heap genau 2m − 1 Elemente enthält (m ∈ N). Musterlösung: Falls k ∈ Ω(n) so kann man auf allen k + n Elementen die Funktion buildHeap aufrufen, die dann eine Laufzeit von O(k) hat. Sei also im Folgenden k immer wesentlich kleiner als n. Die Menge der einzufügenden Elemente sei K. Man kann nun einfach (unter Verletzung der Heapeigenschaft) die Elemente aus K hinten an den Heap anhängen. 1 y x n 1 k Es sei x der letzte Knoten der gemeinsamen Vorgänger von allen Elementen aus K. Es sei N (blau im Bild) die Menge aller gemeinsamen Vorgänger von K außer x. Aufgrund der Heapeigenschaft ist N in der Reihenfolge von 1 bis y sortiert. Es sei k̄ die kleinste Zweierpotenz mit k ≤ k̄. An den Teilbaum T (hier grün) mit Wurzel x könnte man maximal k̄ Elemente anhängen. Da T ein vollständiger Binärbaum ist, der auf jeder Ebene doppelt so viele Knoten wie auf der darüberliegenden Ebene hat, hat T genau k̄ − 1 < 2k ∈ O(k) Knoten. Aus der Höhe der Bäume fogt, dass es dlog ne − dlog ke + 1 ∈ O(log n) gemeinsame Vorgänger von K gibt. Fall 1: k ≤ log n Wir dürfen also maximal O(k log k + log n) Schritte benötigen. Also wählt man folgendes Vorgehen: • Finde x in O(log n) Schritten. 5 • Schneide T aus dem Heap aus und sortiere die Menge K ∪ T in O(k log k) Schritten. • Schneide N aus dem Heap aus und erhalte dabei die Sortierung in N . Führe dann einen Mergeschritt wie beim Mergesort aus um die sortierten Mengen N und K ∪ T zu der sortierten Menge N ∪ K ∪ T zu vereinigen. Dies benötigt O(k + log n) Schritte. • Füge nun nacheinander die kleinsten Elemente der Liste N ∪ K ∪ T in den Heap ein. Beginne bei der ursprünglichen Stelle von 1, und fahre fort bis zur ursprünglichen Stelle von y. Danach füge die sortierten Elemente ebenenweise in den Teilheap ab der ursprünglichen Stelle von x ein. Dies benötigt O(k + log n) Schritte. Damit ist die Gesamtlaufzeit in O(k log k + log n) und das Ergebnis erfüllt die Heapeigenschaft an jeder Stelle (!). Fall 2: k > log n Wir dürfen also maximal O(k + log n log k) Schritte benötigen. Also wählt man folgendes Vorgehen: • Finde x in O(log n) Schritten. • Schneide T aus dem Heap aus und führe auf der Menge K ∪ T in O(k) Schritten buildHeap aus. Der Heap hat dann eine Größe in O(k). • Entnehme nun mit der Standardfunktion die #N kleinsten Elemente aus dem Heap der Menge K ∪ T . Erhalte dadurch eine sortierte Menge M . Dies benötigt O(log n log k) Schritte. • Schneide N aus dem Heap aus und erhalte dabei die Sortierung in N . Führe dann einen Mergeschritt wie beim Mergesort aus um die sortierten Mengen N und M zu der sortierten Menge N ∪ M zu vereinigen. Dies benötigt O(log n) Schritte. • Füge nun nacheinander die kleinsten Elemente der Liste N ∪ M in den großen Heap ein. Beginne bei der ursprünglichen Stelle von 1, und fahre fort bis zur ursprünglichen Stelle von y. Dies benötigt O(log n) Schritte. • Füge die restlichen #N Elemente aus N ∪ M mit der Standardfunktion in den aus der Menge K ∪ T entstandenen Heap ein. Dies benötigt O(log n log k) Schritte. • Verbinde den aus der Menge K ∪ T entstandenen Heap mit dem großen Heap. Dies geht in O(1). Damit ist die Gesamtlaufzeit in O(k + log n log k) und das Ergebnis erfüllt die Heapeigenschaft an jeder Stelle (!). 6