Zusammenfassung 6

Werbung
Algo&Komp. - Wichtige Begriffe
Mattia Bergomi 2008
Woche 10−11
Gegeben sei eine Menge von Datensätzen x1 , . . . , xn . Wir müssen sie in einer geeigneten Datenstruktur speichern so, dass wir sie effizient finden, modifizieren, einfügen,
löschen usw. können. Je nachdem welche dieser Operationen am wichtigsten sind, benutzen wir verschiedene Strategien. Im Folgenden sind vier dieser speziellen Datenstrukturen beschrieben.
1 (a, b)-Bäume
Operationen: Find, Insert, Delete, FindMin, FindMax.
Voraussetzungen: Schlüssel key[x] sind alle voneinander verschieden.
• Die Datensätze x (mit dazugehörigem key[x]) sind nur an den Blättern gespeichert. Die restlichen (inneren) Knoten sind “Wegweiser”, um den richtigen Weg
zu den Schlüsseln anzuzeigen.
Der Baum muss höhenbalanciert sein, d.h. alle Blätter haben dieselbe Distanz
von der Wurzel.
• a ≤ #Kindern(v) ≤ b, ∀ innere Knoten v. (Ausnahme für die Wurzel: 2 ≤ # ≤ b)
Für a, b muss gelten: a ≥ 2, b ≥ 2a − 1.
• Jeder innere Knoten v besitzt einen Wert ρ(v) := #Kinder(v), sowie ρ(v) − 1
verschiedene Werte K1 , . . . , Kρ(v)−1 (die “Wegweiser”) mit der Bedeutung:
Ki−1 < {alle Schlüssel im i-ten Unterbaum von v} ≤ Ki
(mit der Konvention K0 := −∞, Kρ(v) = +∞).
Wurzel
22, 39
x ≤ 22
K1
22 < x ≤ 39
6, 15
8
51, 62
31
x ≤ 31
2, 5
x > 39
K2
16
26
x > 31
34
42, 48
55, 57
61
2 5 6 8 15 16 22 26 31 34 39 42 48 51 55 57 62 64 71
Schlüssel
Abbildung 1: Ein Beispiel eines (2, 3)-Baumes
-1-
Algo&Komp. - Wichtige Begriffe
Mattia Bergomi 2008
Theorem Für ein (a, b)-Baum mit n Blättern und Höhe h (d.h. Anzahl Kanten von
einem Blatt zur Wurzel) gilt:
2ah−1 ≤ n ≤ bh
logb (n) ≤ h ≤ 1 + loga
1.1
n
2
, d.h. h = Θ (log(n))
Operationen
F IND (x) (wobei key[x] =: k) Laufzeit: O (log(n))
• Fange an der Wurzel an.
• Dank den “Wegweisern” Ki wissen wir immer, in welchem unteren Teilbaum
sich k befinden muss (analog wie bei der binären Suche. . . ).
• Am Ende kommen wir zu einem Blatt: entweder haben wir x mit key[x] = k
hier gefunden, oder x existiert in diesem Baum nicht.
D ELETE (x) (Annahme: x existiert im Baum) Laufzeit: O (log(n))
• F IND (x) und lösche (Blatt) x.
• Da a ≤ #Kindern(v) ≤ b immer gelten muss, kontrollieren wir, ob der Vater v
von x weniger als a Söhne hat (d.h. ρ(v) < a). Falls nicht, sind wir fertig.
• Sonst betrachten wir einen Nachbarn (Bruder) w von v:
– Falls w genau a Söhne hat (ρ(w) = a), so wird v selbst ein Sohn von w.
– Falls w mehr als a Söhne hat (ρ(w) > a), dann adoptiert v einen Sohn des
Nachbars.
In beiden Fällen ist jetzt wieder a ≤ #Kindern(v) ≤ b.
I NSERT (x) (Annahme: x existiert nicht im Baum) Laufzeit: O (log(n))
• F IND (x) und wir kommen zu einem Blatt w, dessen Schlüssel der kleinste im
Baum ist, der grösser als key[x] ist.
• Füge x links von w ein und verbinde es mit dem Vater v von w.
• Da a ≤ #Kindern(v) ≤ b immer gelten muss, kontrollieren wir, ob jetzt ρ(v) >
b ist. Falls nicht, sind wir fertig.
• Sonst muss man eine sogenannte Rebalancierung durchführen:
R EBALANCIERUNG ()
v := Vater von x und w.
while ρ(v) > b do
if (v 6= Wurzel) u := Vater(v); else u := neue Wurzel mit Sohn
v;
Söhne
Zerteile v in zwei Knoten v1 , v2 mit: v1 := Vater der ersten b+1
2
v2 := Vater der restlichen Söhne
v := u;
end while
-2-
Algo&Komp. - Wichtige Begriffe
2
Mattia Bergomi 2008
Union-Find Struktur
Operationen: MakeNewSet, Union, Find (mit Path Compression!).
• Eine Union-Find Struktur ist eine Sammlung von gerichteten Bäumen.
• Jeder Knoten enthält ein Element, und alle Kanten zeigen “nach oben”, zur Wurzel des Baumes.
• Für ein Element x ist es nicht wichtig, wo es im Baum liegt, sondern nur wer der
Repräsentant seines Baumes ist!
Repräsentant = Wurzel (= “Leader” des Baums!).
2.1
Operationen
M AKE N EW S ET (x) (Ziel: Füge x ein) Laufzeit: O (1)
Füge x als neuen Baum mit Wurzel = x ein.
U NION (x, y) (Ziel: Vereinige Bäume, die x und y enthalten) Laufzeit: O (1)
Hänge die Wurzel des niedrigeren der zwei Bäume an die Wurzel des höheren an.
F IND (x) (Ziel: Finde Repräsentant des Baumes mit x) Laufzeit: O (log(n))
• Laufe von x zur Wurzel und gebe sie als Resultat.
• PATH -C OMPRESSION ()
Laufe den Pfad von x zur Wurzel nochmal durch, und verbinde alle Knoten auf
diesen Pfad direkt mit der Wurzel (die im ersten Schritt schon gefunden wurde).
Somit werden alle weiteren F IND Operationen auf diesen Knoten weniger lange
brauchen! (Siehe Abbildung 2, blaue Pfeile)
Repräsentant(T2 )
Repräsentant(T1 )
Path Compression
T2
x
T1
Abbildung 2: Ein Beispiel einer Union-Find Struktur
-3-
Algo&Komp. - Wichtige Begriffe
Mattia Bergomi 2008
Theorem Für jeden enthaltenden Baum T mit n Knoten gilt immer:
n ≥ 2Height(T ) , d.h. Height(T ) ≤ log(n).
Laufzeit einer Folge von Operationen: (mit Path-Compression)
• Ausgehend von einer leeren Struktur benötigt eine Folge von k Operationen (wovon l M AKE N EW S ET) O (k log∗ (l)) ≈ O (k).
• Ausgehend von einer leeren Struktur benötigt eine Folge von k Operationen (wobei alle F IND am Ende durchgeführt werden) O (k).
3
Hash Tables
Operationen: Insert, Find, Delete.
Voraussetzungen: Speicherplatz M mit m Zellen, Objekte k ∈ S zu speichern. Die
Objekte müssen hier nicht geordnet sein (z.B. MP3 Files. . . ).
• Eine Hash-Funktion h : S → M, k 7→ h(k) ordnet jedem Objekt k eine Speicherzelle h(k) zu.
• Idealerweise möchten wir, dass h injektiv ist, aber leider ist oft |Objekte| m,
d.h. h kann nicht immer injektiv sein (gewünscht: so injektiv wie möglich).
• Wenn zwei verschiedene Objekte auf dieselbe Zelle abgebildet werden (d.h.
h(k1 ) = h(k2 ) aber k1 6= k2 ), dann spricht man von Kollisionen.
1
2
3
S
h
Hash-Funktion
h(k)
schon besetzt
k
schon besetzt
schon besetzt
k1
Kollision
k2
m−1
m
Zu speichernde
Objekte
schon besetzt
Speicherplätze (Zellen)
Abbildung 3: Schema einer Hashing-Funktion
Wenn Kollisionen passieren, dann kann man entweder die kollidierenden Elemente
in derselben Zelle durch eine Liste speichern, oder eine andere freie Zelle suchen. Im
zweiten Fall gibt es mehrere Strategien (z.B. versuche mit Zellen h(k)+1, h(k)+2, . . .
oder wie in Serie 8, Aufgabe 3).
-4-
Algo&Komp. - Wichtige Begriffe
Mattia Bergomi 2008
Wenn eine Funktion h nicht injektiv sein kann, dann ist die beste Wahl eine Funktion,
die diese Kollisionen gleichmässig auf M verteilt, d.h. |h−1 (i)| = |S|
m , ∀i ∈ M . Dann
1
ist die Wahrscheinlichkeit, dass zwei zufälligen Elemente k1 , k2 kollidieren, ≤ m
.
Definition (Universelle Familie) Eine Familie H von Hash-Funktionen heisst universell falls
1
|{h ∈ H | h(k1 ) = h(k2 )|}
≤
|H|
m
In diesem Fall gilt dann: Für beliebiges k ∈ S und eine zufällige h ∈ H ist die
erwartete Anzahl Kollisionen von k mit einem anderen Element < 1.
Bemerkung:
Auch wenn hier Hash-Funktionen ein sehr abstraktes Konzept scheinen, sind in der
Praxis sehr nützlich:
Eine Hash-Funktion ist eine Funktion, die beliebige Daten beliebiger Länge (also auch
Files, da sie nur eine lange Sequenz von 0 und 1 sind) in eine String fester Länge zu
transformieren: diese entspricht eine “Etikette” des Files und wird “Hash Value” oder
“Checksum” genannt.
Um zwei (riesige) Files zu vergleichen, braucht dann man nicht lange Berechnungen
durchführen, sondern muss nur diese zwei kleine Etiketten vergleichen (Kollisionen
sind hier zwei verschiedene Files, die dieselbe Etikette kriegen, also als “gleiche Files”
interpretiert werden; das muss passieren, da fast unendlich viele File existieren, aber
nur endlich viele Etiketten der festen Länge).
Eine praktische Anwendung von Hash-Funktionen haben wir alle schon implizit gesehen, z.B. wenn wir in E-Mule ein neues (grosses) File in Sharing (freigegebene Dateien) setzen: E-Mule beginnt zu rechnen, mit dem Hinweis “File Hashing. . . ”; am Ende
besitzt dieses File eine Etikette (wie z.B. 710C11B563A18CD100AD890F05D5864B)
und alle können diese Etikette lesen um zu wissen, ob wir ein File haben, das sie interessiert (auch wenn es bei uns ein anderen Namen hat).
-5-
Algo&Komp. - Wichtige Begriffe
4
Mattia Bergomi 2008
Fibonacci Heaps
Operationen: Insert, Extract-Min, Decrease-Key.
• Die Struktur besteht aus eine Sammlung von Bäumen mit Wurzel, wo jeder Knoten einem Elemente x entspricht.
• Alle Wurzeln liegen in einer Wurzelliste, und es gibt einen Zeiger M in[H], der
auf die kleinste Wurzel zeigt.
• Der Schlüssel key[x] eines Knotens x ist immer ≤ als die Schlüssel aller seiner
Kinder.
• Um zu vermeiden, dass die Struktur zu dünn wird, darf jeder Knoten höchstens
ein Kind verlieren, bevor die Struktur weitere Umformungen braucht. Diese Information speichern wir im Feld marked[x].
• Genauer, jeder Knoten x besitzt:
– key[x]: Schlüssel (Wert) des Objekts x
– parent[x]: Zeiger auf den Vater von x
– child[x]: Zeiger auf ein beliebiges Kind von x
– rank[x]: Anzahl Kindern von x
– marked[x]: Ja/Nein-Wert der besagt, ob x schon ein Kind verloren hat,
seitdem es Kind eines anderen Knotens geworden ist
M in[H]
Wurzelliste
31
43
4
40
36
78
80
56
90
85
marked
86
90
Abbildung 4: Ein Beispiel eines Fibonacci Heap
-6-
31
Algo&Komp. - Wichtige Begriffe
4.1
Mattia Bergomi 2008
Operationen
I NSERT (x) Amortisierte Laufzeit: O (1)
• Füge x als neue Wurzel in die Wurzelliste ein.
• Aktualisiere M in[H], falls nötig.
E XTRACT M IN () Amortisierte Laufzeit: O (log(n))
• Sei z := M in[H]: lösche z und gebe z aus.
• Alle Kinder von z (mit dazugehörigen Unterbäumen) werden jetzt neue Wurzeln,
d.h. wir haben rank[z] neue Bäume.
• Wir müssen jetzt M in[H] aktualisieren. Um zu vermeiden, dass es zu viele Wurzeln gibt, rufen wir die folgende Prozedur auf:
C ONSOLIDATE ()
– Vereinige sukzessiv Bäume, deren Wurzeln gleichen rank (= Anzahl Kindern) haben, indem ein Baum an die Wurzel des anderen angehängt wird,
bis alle Wurzeln verschiedenen rank haben.
D ECREASE K EY (x, k) (Annahme: k < key[x]) Amortisierte Laufzeit: O (1)
• Finde x und setze key[x] = k.
• Falls x Wurzel ist, dann aktualisiere M in[H] und wir sind fertig.
• Sonst besitzt x einen Vater v: falls key[v] ≤ k, dann sind wir fertig, sonst rufen
wir die folgende Prozedur auf:
C UT (x)
– v :=Vater(x).
– Füge x (mit seinem Unterbaum) in die Wurzelliste ein.
– Falls marked[v] = “False”, setze marked[v] = “True” und wir sind fertig.
– Sonst hätte v keinen Sohn mehr verlieren dürfen (was aber gerade passiert
ist!), deswegen wiederholen wir die Prozedur mit v, rufen also C UT (v) auf.
C HANGE K EY (x, k) Amortisierte Laufzeit: O (log(n))
Siehe Serie 10, Aufgabe 2.
Laufzeit einer Folge von Operationen: Ausgehend von einer leeren Struktur benötigt
eine Folge von k Operationen (wovon l E XTRACT M IN), wobei sich in jedem Moment
höchstens n Elementen in die Struktur befinden, eine Laufzeit von O (k + l log(n))
(amortisierte Laufzeitanalyse).
Theorem
• Sei x mit Kindern y1 , . . . , yrank[x] in der Reihenfolge, in der sie mit x verbunden
wurden (d.h. y1 = ältestes Kind). Dann gilt: rank[yi ] ≥ i − 2, ∀i ≥ 2.
rank[x]
.
• Für jeder Knoten x hat der Teilbaum mit Wurzel x immer Grösse ≥ 23
• ⇒ Für Heaps mit n Elementen gilt rank[x] ≤ log3/2 (n), für alle Knoten x.
-7-
Algo&Komp. - Wichtige Begriffe
5
Mattia Bergomi 2008
Amortisierte Analyse
Die amortisierte Laufzeitanalyse liefert Schranken für die durchschnittliche Laufzeit einer Operation und, was wichtiger ist, die Kosten für eine Folge von k Operationen. Die
amortisierte Analysis ist, im Unterschied zur allgemeinen Laufzeitanalyse, präziser und
liefert oft bessere Schranken für die tatsächliche Laufzeiten einer Folge von Operationen. Es werden nicht nur die maximalen (“worst case”) Kosten der einzelnen Schritte
betrachtet, sondern es wird der “worst case” aller k Operationen zusammen (nacheinander durchgeführt) analysiert. Häufig gibt es Operationen, die sehr teuer sind, aber diese
“teuren” Fälle können nur wenige Male pro Ablauf des Algorithmus vorkommen, und
alle anderen Fälle sind relativ günstig.
Um die Analyse durchzuführen, definiert man eine sogenannte Potentialfunktion Φ(i),
i = 0, . . . , k, die immer eine Eigenschaft der Situation nach der i−ten Operation beschreibt. Diese kann im Prinzip beliebig definiert werden, aber nur sinnvolle Definitionen werden zu guten Resultaten führen. Insbesondere wählen wir immer Φ(i) ≥ 0.
Ziel dabei ist es, jeder Operation i einen mittleren (amortisierten) Kostenwert a(i) zuzuweisen. Wir definieren also:


 Φ(i) := Potential nach der i−ten Operation ≥ 0
t(i) := Akkumulierte tatsächliche (klassische) Kosten bis zur i−ten Operation ≥ 0


a(i) := Amortisierte (durchschnittliche) Kosten für die i−te Operation
(Beachte, dass mit t(i) nicht die klassischen Kosten für die i−te Operation gemeint ist,
sondern die akkumulierten (totalen) Kosten bis zur i−ten Operation!).
Wir setzen dann:
∆Φ
tatsächl. Kosten i-te Op.
}|
{
z
}|
{
z
a(i) := t(i) − t(i − 1) + Φ(i) − Φ(i − 1)
(hier muss a(i) explizit berechnet werden, oder mindestens eine Abschätzung der Typ
a(i) ≤ . . . gefunden werden). Dann ist die Summe der amortisierten Kosten:
k
X
=0
≥0
z}|{ z }| {
a(i) = t(k) − t(0) + Φ(k) −Φ(0) ≥ t(k) − Φ(0).
i=1
Somit kriegen wir eine neue Schranke
t(k) ≤ Φ(0) +
k
X
a(i)
i=1
die oft besser (kleiner) als die klassische Schranke ist.
-8-
Herunterladen