Eidgenössische Technische Hochschule Zürich Ecole polytechnique fédérale de Zurich Politecnico federale di Zurigo Federal Institute of Technology at Zurich Institut für Theoretische Informatik Peter Widmayer Tobias Pröger Datenstrukturen & Algorithmen Lösung 5.1 27. März 2013 Lösungen zu Blatt 5 FS 13 Vereinigung von AVL-Bäumen. Seien T1 und T2 zwei AVL-Bäume der Höhen h1 bzw. h2 . Für einen Baum T bezeichne h(T ) seine Höhe. Besteht ein Baum T aus einer Wurzel v mit dem linken Teilbaum Tl (v) und dem rechten Teilbaum Tr (v), dann ist der Balancierungsfaktor von v definiert als bal(v) := h(Tr (v))−h(Tl (v)). Für einen AVL-Baum ist bal(v) ∈ {−1, 0, 1} für jeden Knoten v des Baums. Insbesondere ist bal(v) = −1, wenn der linke Teilbaum höher ist als der rechte. Nun wird die Vereinigung wie folgt realisiert: 1) Wir berechnen zunächst die Werte von h1 bzw. h2 , was genau der Länge eines längsten Pfades von der Wurzel zu einem Blatt in T1 bzw. T2 entpricht. Dazu starten wir bei der Wurzel und wählen als Nachfolger eines Knotens v genau den Knoten, an dem der höhere Teilbaum gespeichert ist: Ist bal(v) = −1, fahren wir mit dem linken Nachfolger fort, ansonsten mit dem rechten (für bal(v) = 0 können beide Nachfolger gewählt werden). Die Werte von h1 und h2 können insgesamt in Zeit O(h1 + h2 ) bestimmt werden. 2) Entferne das kleinste Element x aus T2 . Nach der Entfernung ergibt sich ein Baum T20 mit der Höhe h ∈ {h2 − 1, h2 }. Die Operation kann in Zeit O(h2 ) durchgeführt werden. 3) Ist |h1 − h| ≤ 1 (die Höhendifferenz von T1 und T20 also maximal 1), dann wird der Algorithmus beendet und ein neuer Baum mit Wurzel x, T1 als linkem Teilbaum und T20 als rechtem Teilbaum zurückgegeben. Für diese Operation fällt nur Zeit O(1) an. 4) Sei nun o.B.d.A. h1 > h + 1 (der Fall h1 < h − 1 verläuft symmetrisch). Wir starten in T1 an der Wurzel und folgen solange dem rechten Nachfolgerknoten, bis wir einen Knoten v finden, der die Wurzel eines Baums T10 mit Höhe h oder h+1 repräsentiert. Wir bestimmen ausserdem den Vorgänger u dieses Knotens v (damit ist T10 der rechte Teilbaum von u, und die Wurzel von T10 ist v). Die Berechnung von u und v können wir wie folgt in Zeit O(h1 ) durchführen. 1 v ← Wurzel(T1 ) 2 h0 ← h1 3 while h0 > h + 1 do 4 u←v 5 if bal(v) = −1 then h0 ← h0 − 2 else h0 ← h0 − 1 6 v ← NachfolgerRechts(v) 5) Ersetze den rechten Teilbaum von u durch den Baum, der x als Wurzel, T10 als linken und T20 als rechten Teilbaum hat. Dieser Baum mit x an der Wurzel ist ein Suchbaum, da alle Schlüssel in T1 (und damit insbesondere in T10 ) kleiner als x sind, und alle Schlüssel aus T20 grösser oder gleich x sind. Er ist sogar ein AVL-Baum, denn der Balancierungsfaktor von x ist h − h0 ∈ {−1, 0}. Analog überlegt man sich, dass der gesamte Baum noch immer ein Suchbaum ist. Lediglich die AVL-Eigenschaft kann verletzt sein, da der neue rechte Teilbaum von u eine Tiefe von h0 + 1 (statt vorher h0 ) besitzt. Wir erhalten den folgenden schematischen Ablauf: Die gesamte Operation kann in Zeit O(1) durchgeführt werden. 6) Wie beim Einfügen eines neuen Knotens müssen die Balancierungsfaktoren der Knoten auf dem Pfad von u bis zur Wurzel des Baums geprüft und ggf. durch Rotationen “repariert” werden. Dies kostet maximal Zeit O(h1 ). Insgesamt können die Schritte 1 – 6 in Zeit O(h1 +h2 ) durchgeführt werden. Da h1 , h2 ∈ O(log n), beträgt die Gesamtlaufzeit also O(log n). Lösung 5.2 Amortisierte Analyse. Eine gute Wahl ist k = 2n. Dies bedeutet, dass ein Array doppelter Länge erzeugt wird, sobald das aktuelle Array voll ist. Um zu zeigen, dass mit dieser Wahl jede Einfügeoperation konstante amortisierte Kosten hat, führen wir eine amortisierte Analyse durch. Dazu definieren wir eine Potentialfunktion, welche jedem Array-Zustand einen Wert zuordnet (diesen Wert kann man intuitiv als “Kontostand” interpretieren). Zur Erinnerung: In der amortisierten Analyse mittels Potentialfunktion definiert man Φi als das Potential nach der i-ten Operation. Die i-te Operation habe tatsächliche Kosten ti . Dann sind die amortisierten Kosten der i-ten Operation definiert als ai := ti + Φi − Φi−1 . Mit dieser Definition folgt für eine Folge von m Operationen ! m m m X X X ai = (ti + Φi − Φi−1 ) = ti + Φm − Φ0 , (1) i=1 i=1 i=1 und somit erhalten wir m X i=1 ti = m X ai + Φ0 − Φm . (2) i=1 Wenn es also gelingt, die amortisierten Kosten jeder Operation sowie den Term Φ0 − Φm abzuschätzen, dann erhält man so auch eine Abschätzung für die tatsächlichen Gesamtkosten. Wenn manP die Potenzialfunktion beispielsweise so wählt, dass Φm ≥ Φ0 für jedes m, dann folgt P m m i=1 ti ≤ i=1 ai , d.h. die tatsächlichen Gesamtkosten können durch die Summe der amortisierten Kosten nach oben abgeschätzt werden. 2 a) Wir definieren das Potential (bzw. den Kontostand) eines Arrays der Grösse n als 6 · Anz. d. Elem. in der oberen Hälfte des Arrays (also in Positionen n + 1, ..., n). 2 (3) Zu beachten ist, dass sich auch n ändert, wenn das Array vergrössert wird. Aus der Definition folgt Φ0 = 0 (anfangs ist das Array leer), und weil Φi mit dieser Definition nie negativ sein kann, ist auch klar, dass Φi ≥ 0 für i > 0 gilt, also insbesondere Φm ≥ Φ0 . Wir müssen also nur noch untersuchen, wie gross die amortisierten Kosten einer Einfügeoperation sind. Dazu unterscheiden wir zwei Fälle: Wenn bei der i-ten Einfügeoperation das Array nicht verdoppelt wird (d.h. es ist noch nicht voll), dann ist ti = 1 und Φi − Φi−1 ≤ 6 (= 0 falls das Array noch nicht halb voll ist, und = 6 sonst), und somit ai ≤ 1 + 6 = 7. Wenn bei der i-ten Einfügeoperation das Array von Grösse n auf Grösse 2n verdoppelt wird, sind die tatsächlichen Kosten ti = 2n |{z} + Array anlegen n |{z} + Elemente kopieren 1 |{z} = 3n + 1 (4) neues Element einfügen und die Potentialdifferenz beträgt Φi − Φi−1 = 6 · (1 − n ) = 6 − 3n. 2 (5) Die amortisierten Kosten sind in diesem Fall ai = 3n + 7 − 3n = 7, also ebenfalls konstant. b) Wir zeigen nun, dass auch für das Entfernen eine amortisiert konstante Laufzeit möglich ist. Dabei wird das Array erst dann von der Grösse n auf die Grösse n/2 verkleinert, wenn es nur noch n/4 Elemente im Array hat, und nicht bereits, wenn es noch n/2 Elemente hat. Dies verhindert, dass die Arraygrösse stets verdoppelt und wieder halbiert wird, wenn man in ein Array zuerst n/2 Elemente einfügt und dann immer abwechselnd eines einfügt und dieses gleich wieder löscht. Für die amortisierte Analyse definieren wir das Potential (bzw. den Kontostand) eines Arrays der Grösse n als n 3 · Anz. leerer Positionen in der unteren Hälfte des Arrays (also in Pos. 1, ..., ). 2 (6) Wenn bei einer Löschoperation i das Array nicht halbiert wird, gilt ai = 1 + 0, falls das gelöschte Element in der oberen Hälfte des Arrays liegt, oder ai = 1 + 3, falls das gelöschte Element in der unteren Hälfte liegt. Wird dagegen bei der Löschoperation i das Array halbiert, dann gilt ti = n/2 |{z} + Array anlegen n/4 |{z} 3 = n, 4 (7) Elemente kopieren und die Potentialdifferenz beträgt Φi − Φi−1 = 3 · (1 − n/4). (8) Somit sind die amortisierten Kosten in diesem Fall ai = 43 n + 3 · (1 − n/4) = 3. Für jede Löschoperation sind also die amortisierten Kosten konstant (genauer: ai ≤ 4). Es ist einfach zu sehen, dass Φ0 − Φm ≤ m. Für die tatsächlichen Kosten erhalten wir m X i=1 ti = m X ai + Φ0 − Φm ≤ 4m + m ∈ O(m). i=1 3 (9) Damit ist die amortisierte Analyse für das Löschen abgeschlossen. Es ist nun leicht zu sehen, dass man mit der Potentialfunktion 6· (Anzahl Elemente in der oberen Hälfte des Arrays + (10) Anzahl leerer Positionen in der unteren Hälfte des Arrays) auch für beliebige Folgen von Einfüge- und Löschoperationen zeigen kann, dass die amortisierten Kosten jeder Operation konstant sind. Bemerkung: Man könnte natürlich auch weitere Kosten in die Analyse mit einbeziehen, z.B. wenn man davon ausgeht, dass das Löschen eines Arrays der Länge n die Kosten Θ(n) (und nicht 0) hat. Lösung 5.3 Median nach Blum. a) Wir teilen die Folge zunächst in bN/5c Gruppen mit genau 5 Elementen und eine Gruppe mit 2 Elementen: 8, 13, 17, 5, 11 29, 3, 4, 11, 10 15, 7, 30, 57, 1 2, 6, 9, 17, 7 14, 13. Der erste rekursive Aufruf erfolgt auf den Medianen dieser Gruppen. Für die letzte Gruppe ist der Median per Definition 14. Somit ist die erste Folge 11, 10, 15, 7, 14 Als Median-der-Mediane ergibt sich das Element 11. Dieses Element wird nun als Pivotelement benutzt und der Aufteilungsschritt wie in Quicksort durchgeführt (wir vertauschen zunächst 11 mit 13, führen die Aufteilung durch und vertauschen am Ende 15 mit 11): 8, 7, 9, 5, 6, 2, 3, 4, 1, 10, 7 11 30, 57, 11, 29, 13, 17, 17, 13, 14, 15 Die erste Folge hat mehr Elemente als die zweite. Da wir nach dem dN/2e-ten Element suchen, ruft sich Auswahl nun mit der längeren Folge rekursiv auf, also 8, 7, 9, 5, 6, 2, 3, 4, 1, 10, 7 Hinweis: Je nach Implementierung der Aufteilung können die Elemente der Folge natürlich auch in anderen Reihenfolgen stehen. b) Im ersten rekursiven Aufruf ruft sich Auswahl immer mit genau dN/5e Elementen auf. Im besten Fall ist der Median-der-Mediane der richtige Median nach dem wir suchen, dann entfällt der zweite Aufruf komplett. Wenn der Median-der-Mediane genau ein Element “daneben” ist, dann erfolgt der zweite rekursive Aufruf auf dN/2e Elementen. Im 1 N schlimmsten Fall sind immerhin 3( 2 5 − 2) + 2 + 1 Elemente kleiner (oder grösser) als der Median-der-Mediane (es gibt ( 12 N5 − 2) fünfelementige Gruppen, die je 3 Elemente beisteuern, die Gruppe des Pivotelements selbst steuert 2 bei, die Gruppe mit weniger als 5 Elementen könnte aus nur einem Element bestehen und 1 beisteuern). Die Anzahl der Elemente im zweiten Rekursionsaufruf beträgt dann 1 N 7 − 1 ≈ N + 3. (11) N −3 2 5 10 4