Ein anderer Weg, die Laufzeit der quadratischen Binärsuche zu

Werbung
Ein anderer Weg, die Laufzeit der quadratischen Binärsuche zu zeigen, ist das
Auflösen der Rekursionsgleichung. Zur Vereinfachung nehmen wir ohne Bek
schränkung der Allgemeinheit an, dass n = 22 ist, da man sonst nicht auf Gaußklammern verzichten könnte, die die Rechnung komplizierter machen würden.
√
Nach jedem Schritt verkürzt sich das zu durchsuchende Intervall von n auf n
Einträge. Wir müssen also folgende Gleichung lösen:
√
T (n) = T ( n) + c
für eine Konstante c
q √
n +c+c
=T
1
= T (n 4 ) + 2c
1
= T (n 8 ) + 3c
1
= T (n 2k ) + kc
Als Rekursionsanker haben wir T (2) = b (wobei b konstant). Wir suchen also
1
das k, für das n 2k = 2 gilt.
1
n 2k = 2
1
log n = log2 = 1
2k
log n = 2k
log log n = k
Setzen wir das in die Rekursionsgleichung ein erhalten wir:
T (n) = T (2) + c ∗ log log n
= Θ(log log n)
Abschließend lassen sich die drei Suchalgorithmen in folgendem Satz zusammenfassen:
Satz 2.5.1. Eine Menge S von n Elementen aus einem linear geordneten Universum (U, ≤) sei sortiert in einem Feld gespeichert. Dann gilt:
1. Binärsuche sucht nach einem gegebenen a ∈ U . Benötigt O(log n) Zeit.
2. Falls U = [0, 1] und die Elemente von S zufällig und gleichverteilt aus U
gezogen sind, dann braucht die Interpolationssuche eine erwartete Zeit
von O(log log n).
3. Unter der Voraussetzung von 2. braucht die quadratische Binärsuche
erwartet O(log log n) Zeit.
Alle drei Algorithmen arbeiten auf sortierten Feldern, diese haben jedoch einen
Nachteil: das Einfügen oder Entfernen einzelner Elemente ist nicht effizient
möglich und benötigt O(n) Zeit. Hat man eine, sich dynamisch verändernde,
Menge von Elementen sollte man auf eine effizientere Datenstruktur, wie sie im
nächsten Kapitel vorgestellt werden, zurückgreifen.
35
Kapitel 3
Datenstrukturen
Eine Datenstruktur bezeichnet eine Art Daten abzuspeichern, so dass gewisse
Operationen effizient durchführbar sind. Eine Datenstruktur ist die algorithmische Realisierung eines abstrakten Datentyps.
Zum Beispiel ist im sortieren Feld das Suchen sehr effizient. Einfügen und Entfernen jedoch nicht, da man beim Einfügen ein neues Array erstellen muss und
alle Elemente mitsamt dem Eingefügten hineinkopieren muss. Gleiches gilt beim
Entfernen und kostet somit Θ(n). Alternativ hierzu könnte man gelöschte Felder markieren ohne ein neues Array zu erstellen. Dies ist aber auch nicht zu
empfehlen, da auf Dauer größere unbesetzte Bereiche entstehen.
3.1
Wörterbücher (Dictionaries)
Abstrakter Datentyp: eine Menge S ∈ U , U : Universum (meistens linear geordnet)
Operationen:
• SUCH(a,S) mit a ∈ U
liefert 0 falls a ∈
/ S und 1 falls a ∈ S.
Bemerkung. Die Rückgabe ist zwar für den ADT sinnvoll, in der Praxis
(zum Beispiel in einem Telefonbuch) werden üblicherweise anstelle der 1
ein Verweis auf a sowie zusätzliche Informationen dazu ausgegeben. In der
Regel ist a ein Schlüssel in einem größeren Datensatz.
• EINF(a,S) S := S ∪ {a}
• STREICHE(a,S) S := S \ {a}
Bemerkung. Bei EINF und STREICHE bleibt S nach Definition von ∪ und \
unverändert, wenn a schon in S enthalten ist, beziehungsweise gar nicht enthalten war.
Nun benötigen wir eine effiziente Datenstruktur für den abstrakten Datentyp.
Als erste Idee könnte man das sortierte Feld haben.
36
3.1.1
sortiertes Feld
Die Suchen-Funktion benötigt O(log n) Zeit, Einfügen und Streichen aber Θ(n).
Die beiden letzteren Operationen sind nicht effizient (siehe vorheriges Kapitel),
daher kommt das sortierte Feld nicht in Frage.
3.1.2
Hashing
Wir haben eine Hashfunktion h : U → N. Für jedes a ∈ U berechnet h(x) die
Stelle, an die a gespeichert wird. Da U im Allgemeinen nicht begrenzt ist, der
Speicher jedoch schon, ist h in der Regel nicht injektiv, bildet also verschiedene
a auf die selbe Stelle h(a) ab. Daher sollte man h besser folgendermaßen beschreiben: h : U → [1, m], wobei m die Größe des verfügbaren Feldes angibt.
Bei einem nicht injektiven h kann es aber zu Konflikten kommen, wenn a 6= b
aber h(a) = h(b). Daher speichert man für jeden Hashwert eine Liste von Elementen, die den gleichen Hashwert haben, siehe Abbildung 3.1.
1
i
m
a
b
Abbildung 3.1: Hashing Beispiel für h(a) = h(b) = i
Wenn die Hashfunktion die Eingaben gut verteilt, also jedes Ergebnis zwischen
1 und m die gleiche Wahrscheinlichkeit hat, und das m groß genug gewählt wurde, üblicherweise Θ(|S|), dann sind die Operationen des Wörterbuchproblems
erwartet in O(1) möglich, also in Konstanter Zeit“.
”
Bemerkung. Hier ist nicht mal eine lineare Ordnung erforderlich.
In der Praxis wird Hashing oft erfolgreich verwendet, motiviert durch die binäre
Suche gibt es aber noch weitere interessante Datenstrukturen, diese werden im
Folgenden vorgestellt.
3.1.3
Binärer Baum
Ein binärer Baum speichert die Elemente von S in seinen inneren Knoten. Für
jeden inneren Knoten v gilt:
1. Elemente im linken Teilbaum sind kleiner, als das Element von v
2. Elemente im rechten Teilbaum sind größer, als das Element von v
Zur Veranschaulichung siehe Abbildung 3.2.
Die Blätter stehen für erfolgloses Suchen. Man landet also in einem Blatt, wenn
ein Element nicht in dem Baum gespeichert ist.
37
a
v
<a
>a
Abbildung 3.2: Ein binärer Baum mit zwei angedeuteten Teilbäumen
Beispiel. Wir wollen die Zahlen 5, 4, 6 ,3 in einen leeren Baum speichern, dieser
Entwickelt sich dann wie in der Abbildung 3.3 zu sehen
5
4
6
3
5
5
4
5
4
5
6
4
6
3
Abbildung 3.3: Einfügen der Zahlenfolge 5, 4, 6, 3 in einen leeren Baum
Das Streichen ist etwas komplizierter. Dabei sucht man den zu streichenden
Knoten und ersetzt ihn durch das Maximum aus dem linken Teilbaum. Da das
Maximum aus dem linken Teilbaum ebenfalls Kinder haben kann, die allerdings
nur kleiner sein können, zieht man dessen Teilbaum an die Stelle des Maximums.
In einem binären Baum geht das Suchen, Einfügen und Streichen in O(h) Zeit,
wobei h die Höhe des Baumes bezeichnet, da man den Baum jeweils maximal
bis zu einem Blatt durchlaufen muss.
Im günstigsten Fall ist der Baum b balanciert“, dann gilt: h = Θ(log n) (Ab”
bildung 3.5).
Im schlechtesten Fall hat jeder Knoten nur ein Kind, dann gilt: h = Θ(n) (Abbildung 3.4).
Im schlechtesten Fall wird der Baum somit zu einer verketteten Liste und garantiert keine logarithmische Laufzeit mehr. Wie kann man nun einen Baum so
definieren, dass der schlechteste Fall immer noch eine effiziente Laufzeit für die
Operationen auf dem Baum garantiert?
Zuerst wollen wir dazu untersuchen, wie groß die mittlere Höhe eines zufälligen
Binärbaums ist. Hierfür gibt es 2 Ansätze.
Behauptung 3.1.1. In einen ursprünglich leeren Baum werden n Elemente
aus U eingefügt, wobei jeder Permutation der aufsteigenden Ordnung gleich
38
Abbildung 3.4: Ein Binärbaum im ungünstigsten Fall
Abbildung 3.5: Ein Binärbaum im optimalen Fall
wahrscheinlich ist. Dann gilt: Die erwartete Höhe des entstehenden Baums ist
O(log n).
Beweis. Beweis zu komplex, siehe Cormen - Introduction to Algorithms p.254
Stattdessen untersuchen wir, wie viel n Einfügeoperationen im Mittel kosten.
Wir gehen davon aus, das jedes Element mit der Wahrscheinlichkeit n1 auftritt.
Damit ist die Wahrscheinlichkeit, dass das i-t kleinste Element ai an erster Stelle
eingefügt wird n1 . Damit werden in den linken Teilbaum i−1 Elemente eingefügt
und in den rechten Teilbaum n − i Elemente, s. Abbilding 3.6. Diese Teilbäume
werden wiederum zufällig aufgebaut.
Um die Rekursionsgleichung aufzustellen müssen wir nun den Erwartungswert
T (n) der Einfügezeit für n Elemente über alle Auswahlmöglichen des ersten
Elements ai ausrechnen. Für jedes i setzen sich die Gesamtkosten wie folgt
zusammen: T (i − 1) + T (n − i) + O(n), wobei T (i − 1) und T (n − i) für die
erwarteten Kosten für den Aufbau beider Teilbäume stehen, und der lineare
Term dadurch zustande kommt, dass man die restlichen n − 1 Elemente mit
ai vergleichen muss um den Binärbaum gemäß der Invariante aufzubauen. Nun
39
ai
zufällige
Bäume
n-i
i-1
Elemente
Elemente
Abbildung 3.6: Die Wahrscheinlichkeit des ai Elements beträgt n1 , dass es an
erster Stelle eingefügt wird. Die beiden Teilbäume sind beides zufällige Bäume
mitteln wir über alle i und erhalten folgende Rekursionsgleichung:
n
1X
[T (i − 1) + T (n − i) + O(n − 1)]
n i=1
( n
)
1 X
=
[T (i − 1) + T (n − i)] + nO(n − 1)
n i=1
T (n) =
n
=
1X
[T (i − 1) + T (n − i)] + O(n − 1)
n i=1
Der Rekursionsanker liegt bei T (1) = c wobei c eine Konstante ist. Die Rekursionsgleichung ist identisch mit der Rekursionsgleichung der mittleren Laufzeit
des Quicksort-Algorithmus.
Es gilt also folgender Satz:
Satz 3.1.2. Fügt man in einen ursprünglich leeren binären Suchbaum n Elemente ein, wobei die Reihenfolge jeder Permutation der aufsteigenden Ordnung
gleich wahrscheinlich ist, so erfordert dies im Mittel Θ(n log n) Zeit.
Folgerung: Die mittlere Tiefe eines Elements des Suchbaums ( Abstand zu der
Wurzel ) ist Θ(log n). Entsprechend benötigt man im Mittel für das Suchen bzw.
Streichen eines Elements Θ(log n) Zeit.
Dies geht in der Praxis meistens gut, aber wie kann man nun den schlechtesten
Fall ebenfalls auf Θ(log n) drücken?
3.1.4
AVL-Baum (Adelson-Velski/Landis - 1962)
AVL-Bäume wurden von Georgi Adelson-Velski und Jewgeni Landis 1962 entwickelt. Ziel war es einen binären Suchbaum zu erstellen, der möglichst ausgeglichen ist um eine Laufzeit von Θ(log n) für die Operationen SUCH, EINF und
STREICHE zu garantieren.
40
Definition 3.1.3 (AVL-Baum). AVL-Bäume sind binäre Suchbäume, wobei
für jeden inneren Knoten v gilt: Die Höhe der beiden Unterbäume von v unterscheidet sich um höchstens 1.
Man schreibt zur Vereinfachung in jeden Knoten die Differenz der Höhen des
linken Teilbaums zum rechten Teilbaum auf. Ist sie Betragsmäßig kleiner oder
gleich 1, so ist der Baum ein AVL-Baum. Siehe dazu Abbildungen 3.7 und 3.8.
+1
0
0
0
0
Abbildung 3.7: Beides sind AVL-Bäume, da jeder Knoten die Invariante eines
AVL-Baums erfüllt.
+2
0
0
0
Abbildung 3.8: Das ist kein AVL-Baum, da bei der Wurzel die Höhe des linken
Teilbaums um 2 geringer ist, als die Höhe des rechten Teilbaums.
Satz 3.1.4. Die Höhe eines AVL-Baums mit n inneren Knoten ist Θ(log n)
Beweis. Sei nh die minimale Anzahl der inneren Knoten eines AVL-Baums der
Höhe h. Damit ein AVL-Baum möglichst unausgeglichen ist, muss der Baum in
jedem inneren Knoten soweit unausgeglichen sein, wie es die Invariante zulässt.
Das heißt der linke Teilbaum eines inneren Knotens ist immer um 1 größer als
der rechte Teilbaum eines Knotens. Siehe dazu Abbildung 3.9. Daraus ergibt
sich folgende Rekursionsgleichung: nh = nh−1 + nh−2 + 1 Durch Induktion lässt
sich nh von unten durch die Fibinacci-Zahlen abschätzen.
Induktionsbehauptung :
nh ≥ f ibo(h − 1)
41
+1
h-2
h-1
Abbildung 3.9: Der linke Teilbaum hat die Höhe h-1, der rechte Teilbaum die
Höhe h-2. Wird dies in jedem Knoten rekursiv fortgesetzt hat man einen AVLBaum der größtmöglichsten Höhe für n Knoten.
Induktionsanfang :
√
n1 = 1 = f ibo(0) = 1
√
n2 = 2 > f ibo(1) = 1
Induktionsschritt : 3 ≤ h → h + 1
nh = nh−1 + nh−2 + 1
≥ f ibo(h − 2) + f ibo(h − 3) + 1
≥ f ibo(h − 2) + f ibo(h − 3)
√
= f ibo(h − 1)
√
Da sich die Fibonacci-Zahlen von unten durch den Goldenen Schnitt (Φ =
abschätzen lassen gilt weiterhin:
nh ≥ Φh−2
log nh ≥ (h − 2) log Φ
⇒ h = O(log nh )
= O(log n)
Also folgt, ein AVL-Baum mit n Knoten hat die Höhe O(log n).
42
5+1
2 )
Herunterladen