Datenstrukturen & Algorithmen Lösungen zu Blatt 5 FS 13

Werbung
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
Herunterladen