Algorithmen und Datenstrukturen

Algorithmen und Datenstrukturen — Skript zur Vorlesung — Ulrik Brandes Wintersemester 2011/2012 (Entwurf vom 12. Oktober 2011) Vorwort Dieses Skrip...
0 downloads 0 Views 1MB Size
Algorithmen und Datenstrukturen — Skript zur Vorlesung —

Ulrik Brandes Wintersemester 2011/2012 (Entwurf vom 12. Oktober 2011)

Vorwort Dieses Skript entstand im Wintersemesters 2007/08 parallel zur neu konzipierten Vorlesung gleichen Titels und wird seither von Fehlern bereinigt. Es sollte zunächst als Anhaltspunkt für den Inhalt der Vorlesung verstanden werden und unter anderem dazu dienen, die relevanten Stellen in der angekennzeichneten Aufzeichnungen der gegebenen Literatur oder den mit Vorlesung aus dem Wintersemester 2008/09 zu identifizieren. Mein herzlicher Dank gilt Martin Mader für das erste Setzen der handschriftlichen Vorlesungsnotizen und Mennatallah El Assady für das Einfügen der Aufzeichnungsverweise.

i

Inhaltsverzeichnis 1 Einführung

1

1.1

Beispiel: Auswahlproblem . . . . . . . . . . . . . . . . . . . .

1

1.2

Maschinenmodell . . . . . . . . . . . . . . . . . . . . . . . . .

4

1.3

Komplexität . . . . . . . . . . . . . . . . . . . . . . . . . . . .

6

2 Sortieren

11

2.1

SelectionSort . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

2.2

Divide & Conquer (QuickSort und MergeSort) . . . . . . . . . 13

2.3

HeapSort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

2.4

Untere Laufzeitschranke . . . . . . . . . . . . . . . . . . . . . 26

2.5

Sortierverfahren für spezielle Universen . . . . . . . . . . . . . 27

2.6

Gegenüberstellung . . . . . . . . . . . . . . . . . . . . . . . . 32

3 Suchen

34

3.1

Folgen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

3.2

Geordnete Wörterbücher . . . . . . . . . . . . . . . . . . . . . 42

4 Streuen

60

4.1

Kollisionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

4.2

Kollisionsbehandlung . . . . . . . . . . . . . . . . . . . . . . . 63

4.3

Kollisionsvermeidung . . . . . . . . . . . . . . . . . . . . . . . 67 ii

Algorithmen und Datenstrukturen (WS 2010/2011) 4.4

iii

Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70

5 Ausrichten

71

6 Graphen

78

6.1

Bäume und Wälder . . . . . . . . . . . . . . . . . . . . . . . . 83

6.2

Durchläufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85

6.3

Kürzeste Wege . . . . . . . . . . . . . . . . . . . . . . . . . . 94

Kapitel 1 Einführung Anhand eines einfachen Problems soll deutlich gemacht werden, welche Schwierigkeiten beim Vergleich verschiedener algorithmischer Lösungsansätze auftreten können, um dann einige sinnvolle Kriterien festzulegen.

1.1

Beispiel: Auswahlproblem

1.1 Problem (Auswahlproblem) „Bestimme das k-t kleinste von n Elementen“ gegeben: Elemente a1 , . . . , an mit einer Ordnung ≤ sowie ein k ∈ {1, . . . , n} gesucht: aπ(k) für eine Permutation π : {1, . . . , n} → {1, . . . , n} mit aπ(1) ≤ aπ(2) ≤ · · · ≤ aπ(n) 1.2 Bemerkung Natürliche Spezialfälle des Auswahlproblems mit festem k sind:   Minimumssuche 1 k= n Maximumssuche   n b 2 c Median Die Bestimmung des Mittelwerts von n Zahlen ist kein Spezialfall des Auswahlproblems. 1

k-SELECT

Algorithmen und Datenstrukturen (WS 2010/2011)

2

Wir diskutieren vier verschiedene Ansätze zur Lösung des Auswahlproblems und nehmen dabei an, dass die Elemente in einem Array M [1, . . . , n] bereit stehen. Ansatz A: Die konzeptionell einfachste Methode besteht darin, die Elemente des Arrays zunächst bezüglich ≤ nicht-absteigend zu sortieren und dann das k-te auszugeben.

Algorithmus 1: Auswahl nach Sortieren sort(M ) print M [k]

Um die Güte dieses Vorgehens beurteilen zu können, muss mindestens mal der verwendete Sortieralgorithmus bekannt sein. Möglicherweise hängt dessen Güte jedoch auch noch von der Eingabe und der Art der Elemente und Ordnung ab (vgl. Kapitel 2). Ansatz B: Das Auswahlproblem Algorithmus 2: lässt sich auch elementar, d.h. ohne Wiederholte Minimumssuche Verwendung eines anderen Algorithfor i = 1, . . . , k − 1 do mus, lösen. Statt alle Elemente zu sorM ← M \ {min M } tieren, genügt auch die k-malige Beprint min M stimmung und Wegnahme eines Minimums. Das letzte davon ist das gesuchte Element. Im vorstehenden Pseudo-Code ist ein Spezialfall des Auswahlproblems, die Minimumssuche, als elementare Operation aufgeführt. Um die tatsächliche Komplexität besser beurteilen zu können, geben wir eine ausführlichere Implementation an. Darin wird das jeweils kleinste Element der Restfolge an die erste Stelle geholt, sodass schliesslich in M [1, . . . , k] die k kleinsten Elemente stehen. Algorithmus 3: Wiederholte Minimumssuche (detailliert) for i = 1, . . . , k do m←i for j = i + 1, . . . , n do if M [j] < M [m] then m ← j vertausche M [i] und M [m] print M [k]

Algorithmen und Datenstrukturen (WS 2010/2011)

3

1.3 Bemerkung Wird die wiederholte Minimumssuche n mal ausgeführt, entspricht die Ausgabereihenfolge der Elemente einer vollständigen Sortierung der Elemente. Darauf kommen wir in Abschnitt 2.1 zurück. MinSort Ansatz C: Statt wie in Ansatz B immer das kleinste Element der Restfolge zu suchen, können wir auch alle Elemente durchgehen und für jedes testen, ob es unter den bisher betrachteten zu den k kleinsten gehört. Dies ist gerade dann der Fall, wenn das Element kleiner ist als das größte der k bisher kleinsten. Sind alle Element durchgetestet, ist das größte der k kleinsten das gesuchte Element. Statt wiederholter Minimumssuchen in der (anfangs sehr langen) Restfolge führen wir also Maximumssuchen in einem (für kleine k sehr kurzen) Anfangsstück aus. Algorithmus 4: Aktualisierung einer vorläufigen Lösung begin m ← maxpos(M,1,k ) for i = k + 1, . . . , n do if M [i] < M [m] then vertausche M [i] und M [m] m ← maxpos(M,1,k ) print M [m] end int maxpos(array M , int l, r) begin m←l for i = l + 1, . . . , r do if M [i] > M [m] then m ← i return m end

Ansatz D: Gibt es in der verwendeten Programmiersprache (d.h. in der zum Sprachumfang gehörigen Standardbibliothek) oder einem zur Verfügung stehen Paket bereits eine entsprechende Methode, kann einfach diese aufgerufen werden. Für die Beurteilung dieses Vorgehens ist dann allerdings detaillierte Kenntnis über das Verhalten der Methode erforderlich, da die (meist unbekannte) Implementation (und sei es nur im aktuellen Kontext, in dem

Algorithmen und Datenstrukturen (WS 2010/2011)

4

das Auswahlproblem gelöst werden soll) sehr ineffizient sein könnte. Beim Vergleich dieser vier Lösungsansätze stellt man schnell fest, dass es keinen eindeutig besten gibt. Die Beurteilung erfordert mehr Information: bei den Ansätzen A und D über die Implementation selbst, und in allen Fällen über die zu erwartenden Eingaben. Insbesondere das Verhältnis der Größen von n und k ist von Bedeutung (da die Laufzeit von Ansatz A nur von n abhängt, die von B und C aber auch von k), aber z.B. auch, ob Vergleiche und Umspeicherungen ähnlich schnell ausgeführt werden können.

1.2

Maschinenmodell

Schon für einen Vergleich auf Basis der Ausführungszeiten stellen sich viele Detailfragen. Wird die Laufzeit etwa in Sekunden gemessen, lässt sie sich nicht für alle Eingaben im Vorhinein angeben und hängt zudem von zahlreichen Faktoren ab. Drei einfache Beispiele: • In welcher Programmiersprache wurde implementiert? • Auf welchem Rechner wird das Programm ausgeführt (Aufbau, Taktfrequenz, Speicherzugriffszeiten, etc.)? • Wie sind die Daten gespeichert (Organisation, Medium, etc.)? Die Beurteilung von Algorithmen und Datenstrukturen werden wir von diesen Faktoren weitgehend unabhängig machen, indem wir z.B. statt der Ausführungszeiten die Anzahl der elementaren Schritte zählen, die ein Algorithmus ausführt. Um vereinbaren zu können, was ein elementarer Schritt sein soll, müssen wir allerdings ein paar Festlegungen treffen, die zwar nach Möglichkeit realistisch, aber trotzdem unabhängig von konkreten Rechnern sein sollten. Um die Komplexität eines Verfahrens sinnvoll beurteilen zu können, führen wir daher zunächst ein Maschinenmodell ein, in dem Laufzeit und Speicherplatzbedarf hinreichend genau und auf standardisierte Weise gemessen werden können.

Algorithmen und Datenstrukturen (WS 2010/2011)

5

Abbildung 1.1: Aufbau der Random Access Machine 1.4 Def inition (Random Access Machine) Die Random Access Machine (RAM) ist ein abstraktes Maschinenmodell mit (siehe Abb. 1.1) • einer endlichen Zahl von Speicherzellen für das Programm, • einer abzählbar unendlichen Zahl von Speicherzellen für Daten, (Speicheradressen aus N0 ), • einer endlichen Zahl von Registern, • einem Befehlszähler (spezielles Register) und • einer arithmetisch-logischen Einheit (ALU). In Speicherzellen und Registern stehen wiederum natürliche Zahlen, und diese können von der ALU verarbeitet werden. Der Befehlszähler wird nach jeder Befehlsausführung um eins erhöht, kann aber auch mit einem Registerinhalt überschrieben werden.

Algorithmen und Datenstrukturen (WS 2010/2011)

6

Als Anweisungen stehen zur Verfügung • Transportbefehle (Laden, Verschieben, Speichern), • Sprungbefehle (bedingt und unbedingt), • arithmetische und logische Verknüpfungen. Die Adressierung erfolgt direkt (Angabe der Speicherzelle) oder indirekt (Adressierung über Registerinhalt). 1.5 Bemerkung 1. Mit den Sprungbefehlen sind alle Schleifentypen (for, while, repeatuntil) und auch Rekursionen realisierbar. 2. Der Unterschied zur Registermaschine besteht in der Möglichkeit zur indirekten Adressierung, und anders als bei der Random Access Stored kein vonNeumann Program (RASP) Machine sind Programm und Daten getrennt. Modell

1.3

Komplexität

Wir werden die Komplexität von Algorithmen vor allem durch zwei Größen beschreiben: Laufzeit: Anzahl Schritte Speicherbedarf: Anzahl benutzter Speicherzellen Tatsächlich wäre selbst die genaue Anzahl der Schritte zu mühsam zu bestimmen. Wir müssten z.B. präzise angeben, auf welche Weise genau ein Wert aus dem Speicher über Register in die ALU kommt und weiterverarbeitet wird. Es soll uns aber reichen, dass Vorgänge dieser Art durch eine unbekannte, aber konstante Anzahl von Schritten realisiert werden können. Entsprechend werden wir konstante Faktoren in Laufzeiten und Speicherplatz weitgehend ignorieren und uns auf das asymptotische Wachstum der Komplexität im Verhältnis zur Größe der Eingabe konzentrieren.

Algorithmen und Datenstrukturen (WS 2010/2011)

7

1.6 Def inition (Asymptotisches Wachstum) Zu einer Funktion f : N0 → R wird definiert: (i) Die Menge   es gibt Konstanten c, n0 > 0 mit O(f (n)) = g : N0 → R : |g(n)| ≤ c · |f (n)| für alle n > n0 der Funktionen, die höchstens so schnell wachsen wie f . (ii) Die Menge  Ω(f (n)) =

es gibt Konstanten c, n0 > 0 mit g : N0 → R : c · |g(n)| ≥ |f (n)| für alle n > n0



der Funktionen, die mindestens so schnell wachsen wie f . (iii) Die Menge ( Θ(f (n)) =

) es gibt Konstanten c1 , c2 , n0 > 0 mit g : N0 → R : |g(n)| c1 ≤ |f ≤ c2 für alle n > n0 (n)|

der Funktionen, die genauso schnell wachsen wie f . (iv) Die Menge   zu jedem c > 0 ex. ein n0 > 0 mit o(f (n)) = g : N0 → R : c · |g(n)| ≤ |f (n)| für alle n > n0 der Funktionen, die gegenüber f verschwinden. (v) Die Menge  ω(f (n)) =

 zu jedem c > 0 ex. ein n0 > 0 mit g : N0 → R : |g(n)| ≥ c · |f (n)| für alle n > n0

der Funktionen, denen gegenüber f verschwindet.

Algorithmen und Datenstrukturen (WS 2010/2011)

8

Das folgende Beispiel zeigt, dass mit den eingeführten Notationen nicht nur Konstanten ignoriert, sondern oft auch komplizierte Laufzeitfunktionen vereinfacht werden können. 1.7 Beispiel Ein (reelles) Polynom vom Grad d ∈ N0 besteht aus d + 1 Koeffizienten ad , . . . , a0 ∈ R, wobei ad 6= 0 verlangt wird.PReelle Polynome p beschreiben d i Funktionen p : R → R vermöge p(x) = i=0 ai · x für alle x ∈ R. Polynome mit anderen Zahlenbereiche für die Koeffizienten, Definitions- und Wertebereiche sind analog definiert. Ist p : N0 → R ein Polynom vom Grad d, dann gilt für alle n ≥ 1 p(n) =

d X i=0

  1 1 ai · n = ad + ad−1 · 1 + . . . + a0 · d · nd n n

und damit |p(n)| ≤

i

d X

! |ai |

· nd

für alle n ≥ 1 ,

i=0 d

also p(n) ∈ O(n ). Das Polynom läßt sich weiter umschreiben zu   a0 1 ad−1 1 · + ... + · · nd p(n) = ad · 1 + ad n1 ad n d    1 ad−1 ad−2 1 a0 1 = ad · 1 + · + · + ... + · · nd n ad ad n ad nd−1 sodass |p(n)| ≥ |ad | · n

d

d−1 X ai für alle n > ad i=0

also p(n) ∈ Ω(nd ) und insgesamt p(n) ∈ Θ(nd ) . Das Wachstum einer durch ein Polynom beschriebenen Zahlenfolge hängt also nur vom Grad des Polynoms ab.

Algorithmen und Datenstrukturen (WS 2010/2011)

9

Das Wachstum einiger wichtiger Folgen im Vergleich: n 1 10 100 1 000 10 000 log10 n 0 1 2 3 4 0 ≈3 ≈7 ≈ 10 ≈ 13 log √2 n n 1 ≈3 10 ≈ 32 100 2 n 1 100 10 000 1 000 000 100 000 000 1 1 000 1 000 000 1 000 000 000 1 Billionen n3 2n 2 1 024 ≈ 1030 ≈ 10301 > 103 000 ≈3 ≈ 13 781 ≈ 2 · 1041 > 10414 1, 1n ≈ 1 157 n! 1 3 628 800 ≈ 9 · 10 ... ... n 200 3 000 40 000 n 1 10 000 000 000 10 10 10

Merksatz: „Ein Programm mit 1050 Operationen wird auf keinem noch so schnellen Rechner jemals fertig werden.“

Motiviert durch das relative Wachstum der obigen Folgen halten wir einige für die Komplexitätsbeurteilung nützliche Merkregeln fest. 1.8 Satz (i) g ∈ O(f ) genau dann, wenn f ∈ Ω(g). g ∈ Θ(f ) genau dann, wenn f ∈ Θ(g). (ii) logb n ∈ Θ(log2 n) für alle b > 1. „Die Basis eines Logarithmus’ spielt für das Wachstum keine Rolle“

bx = a ⇐⇒ logb a = x

(iii) (log2 n)d ∈ o(nε ) für alle d ∈ N0 jedes ε > 0. „Logarithmen wachsen langsamer als alle Polynomialfunktionen“ (iv) nd ∈ o((1 + ε)n ) für alle d ∈ N0 und jedes ε > 0. „Exponentielles Wachstum ist immer schneller als polynomiales“ (v) bn ∈ o((b + ε)n ) für alle b ≥ 1 und jedes ε > 0. „Jede Verringerung der Basis verlangsamt exponentielles Wachstum“ Beweis.

(skizzenhaft)

(i) Folgt unmittelbar aus den Definitionen. (ii) Folgt aus logb n = (log2 b) · log2 n. nd n (1+ε) n→∞

(iv) lim

= 0; Plausibilitätsargument:

log bx = x log b bx = 2x log2 b (n+1)d nd

=

nd +O(nd−1 ) −→ nd n→∞

1, das

prozentuale Wachstum von nd wird also immer kleiner, wohingegen das von (1 + ε)n konstant ε > 0 beträgt.

Algorithmen und Datenstrukturen (WS 2010/2011) (log2 n)d nε n→∞

(iii) lim

bn n n→∞ (b+ε)

(v) lim

(log2 n)d ε )log2 n (2 n→∞

= lim

 b n b+ε

= lim

n→∞

10

und dann wie in (iv) mit log2 n statt n.

= 0, da

b b+ε

< 1. 

Die nächste Aussage ist vor allem für Algorithmen interessant, in denen Teilmengen fester Größe betrachtet werden. 1.9 Satz Für festes k ∈ N0 gilt   n ∈ Θ(nk ) . k Für alle n > k =: n0 gilt   n n−1 n − (k − 1) n nk = · · ... · . = k k! k k−1 k − (k − 1)

Beweis.

Wegen

n k



n−i k−i

≤ n, i = 0, . . . , k − 1, folgt daraus  n k k

und damit

n k



   k n 1 k ·n ≤ ≤ nk = k k

∈ Ω(nk ) ∩ O(nk ) = Θ(nk ).



In den folgenden oft verwendeten Näherungsformeln wird statt der Funktion selbst der Fehler der Abschätzung asymptotisch angegeben, und zwar einmal additiv und einmal multiplikativ. Die Schreibweise bedeutet, dass es in der jeweiligen Wachstumsklasse eine Folge gibt, für die Gleichheit herrscht. 1.10 Satz Für alle n ∈ N0 gilt (i) Hn :=

n X 1 k=1

k

= ln n + O(1)

(harmonische Zahlen)

   n n  √ 1 (ii) n! = 2πn · · 1+Θ e n

(Stirlingformel)

Kapitel 2 Sortieren Das Sortieren ist eines der grundlegenden Probleme in der Informatik. Es wird geschätzt, dass mehr als ein Viertel aller kommerzieller Rechenzeit auf aus [3, p. 71] Sortiervorgänge entfällt. Einige Anwendungsbeispiele: • Adressenverwaltung (lexikographisch) • Trefferlisten bei Suchanfragen (Relevanz) • Verdeckung (z-Koordinate) • ... Wir bezeichnen die Menge der Elemente, die als Eingabe für das Sortierproblem erlaubt sind, mit U (für Universum). Formal kann das Problem dann folgendermaßen beschrieben werden: 2.1 Problem (Sortieren) gegeben: Folge (a1 , . . . , an ) ∈ U n mit Ordnung ≤ ⊆ U × U gesucht: Permutation (d.h. bijektive Abbildung) π : {1, . . . , n} → {1, . . . , n} mit aπ(1) ≤ aπ(2) ≤ · · · ≤ aπ(n) Wir nehmen an, dass die Eingabe wieder in einem Array M [1, . . . , n] steht Eingabe: und dass sie darin sortiert zurück gegeben werden soll. Insbesondere wird M [1, . . . , n] daher von Interesse sein, welchen zusätzlichen Platzbedarf (Hilfsvariablen, Zwischenspeicher) ein Algorithmus hat. 11

Algorithmen und Datenstrukturen (WS 2010/2011)

12

Neben der Zeit- und Speicherkomplexität werden beim Sortieren weitere Gütekriterien betrachtet. Zum Beispiel kann es wichtig sein, dass die Reihenfolge von zwei Elemente mit gleichem Schlüssel nicht umgekehrt wird. Verfahren, in denen dies garantiert ist, heißen stabil. Unter Umständen werden auch • die Anzahl Vergleiche C(n) und

„comparisons“

• die Anzahl Umspeicherungen M (n)

„moves“

getrennt betrachtet (in Abhängigkeit von der Anzahl n der zu sortierenden Elemente), da Schlüsselvergleiche in der Regel billiger sind als Umspeicherungen ganzer Datenblöcke.

2.1

SelectionSort

Algorithmus 3 zur Lösung des Auswahlproblems hat das jeweils kleinste Element der Restfolge mit dem ersten vertauscht. Wird der Algorithmus fortgesetzt, bis die Restfolge leer ist, so ist am Ende die gesamte Folge nichtabsteigend sortiert. Algorithmus 5: SelectionSort for i = 1, . . . , n − 1 do m←i for j = i + 1, . . . , n do if M [j] < M [m] then m ← j vertausche M [i] und M [m]

j ≤ i

m

Die Anzahlen der Vergleiche und Vertauschungen sind für SelectionSort also Pn−1 C(n) = n − 1 + n − 2 + · · · + 1 = i=1 i = (n−1)·n 2 M (n) = 3 · (n − 1) und die Laufzeit des Algorithmus damit in Θ(n2 ) im besten wie im schlechtesten Fall. Dadurch, dass ein weiter vorne stehendes Element hinter gleiche andere vertauscht werden kann, ist der Algorithmus nicht stabil. Aus der Vorlesung „Methoden der Praktischen Informatik“ ist bereits bekannt, dass es Algorithmen gibt, die eine Folge der Länge n in Zeit O(n log n)

„Sortieren durch Auswählen“; hier speziell: MinSort

Algorithmen und Datenstrukturen (WS 2010/2011)

13

sortieren. Es stellt sich also die Frage, wie man Vergleiche einsparen kann. Man sieht leicht, dass die hinteren Elemente sehr oft zum Vergleich herangezogen werden. Kann man z.B. aus den früheren Vergleichen etwas lernen, um auf spätere zu verzichten?

2.2

Divide & Conquer (QuickSort und MergeSort)

Die nächsten beiden Algorithmen beruhen auf der gleichen Idee: Sortiere zwei kleinere Teilfolgen getrennt und füge die Ergebnisse zusammen. Dies dient vor allem der Reduktion von Vergleichen zwischen Elementen in verschiedenen Teilfolgen. Benötigt werden dazu Vorschriften • zur Aufteilung in zwei Teilfolgen und • zur Kombination der beiden sortierten Teilfolgen. Die allgemeine Vorgehensweise, zur Lösung eines komplexen Problems dieses auf kleinere Teilprobleme der gleichen Art aufzuteilen, diese rekursiv zu lösen und ihre Lösungen jeweils zu Lösungen des größeren Problems zusammen zu setzen, ist das divide & conquer-Prinzip.

2.2.1

QuickSort

Wähle ein Element p („Pivot“, z.B. das erste) und teile die anderen Elemente der Eingabe M auf in M1 : die höchstens kleineren Elemente M2 : die größeren Elemente Sind M1 und M2 sortiert, so erhält man eine Sortierung von M durch Hintereinanderschreibung von M1 , p, M2 . Algorithmus 6 ist eine mögliche Implementation von QuickSort.

hard split, easy join

Algorithmen und Datenstrukturen (WS 2010/2011)

14

Algorithmus 6: QuickSort Aufruf: quicksort(M, 1, n)

r

l p

quicksort(M, l, r) begin if l < r then i ← l + 1; j←r p ← M [l] while i ≤ j do while i ≤ j and M [i] ≤ p do i←i+1 while i ≤ j and M [j] > p do j ←j−1 if i < j then vertausche M [i] und M [j]

j

i

r ≤ > j

l p ≤ > i

l p

if l < j then vertausche M [l] und M [j] quicksort(M, l, j − 1) if j < r then quicksort(M, j + 1, r)

r ≤

≤ > j i r

l ≤ quicksort

end 2.2 Beispiel (QuickSort) 5

8

7

27

9

1

17

23

1

5

7

27

9

8

17

23

7

27

9

8

17

23

23

9

8

17

27

17

9

8

23

8

9

17

8

9

p j

> quicksort

Algorithmen und Datenstrukturen (WS 2010/2011)

15

2.3 Satz Die Laufzeit von QuickSort ist (i) im besten Fall in Θ(n log n)

best case

(ii) im schlechtesten Fall in Θ(n2 ) Beweis. aus

worst case

Die Laufzeit für einen Aufruf von quicksort setzt sich zusammen

• linearem Aufwand für die Aufteilung und • dem Aufwand für die Sortierung der Teilfolgen. Der Gesamtaufwand innerhalb einer festen Rekursionsebene ist damit linear in der Anzahl der Elemente, die bis dahin noch nicht Pivot waren oder sind. Da bei jedem Aufruf ein Pivot hinzu kommt, ist die Anzahl der neuen Pivotelemente in einer Rekursionsebene • mindestens 1 und • höchstens doppelt so groß wie in der vorigen Ebene. Die folgenden Beispiele zeigen, dass es Eingaben gibt, bei denen diese beiden Extremfälle in jeder Rekursionsebene auftreten. Sie sind damit auch Beispiele für den besten und schlechtesten Fall. vorsortiert Pivot immer der Median 0 1 2 3

0 1

log n n−2 n−1 n Θ(n log n)

Θ(n2 ) 

Algorithmen und Datenstrukturen (WS 2010/2011)

16

Welcher Fall ist typisch? In der folgenden Aussage wird angenommen, dass alle möglichen Sortierungen der Eingabefolge gleich wahrscheinlich sind. 2.4 Satz Die mittlere Laufzeit von QuickSort ist in Θ(n log n).

average case

Beweis. Jedes Element von M wird genau einmal zum Pivot-Element. Ist die Eingabereihenfolge in M zufällig, dann auch die Reihenfolge, in der die Elemente zu Pivot-Elementen werden. Da die Anzahl der Schritte von der Anzahl der Vergleiche bei der Aufteilung in Teilfolgen dominiert wird, bestimmen wir den Erwartungswert der Anzahl von Paaren, die im Verlauf des Algorithmus verglichen werden. Die Elemente von M seien entsprechend ihrer korrekten Sortierung mit a1 < . . . < an bezeichnet. Werden Elemente ai , aj , i < j, verglichen, dann ist eines von beiden zu diesem Zeitpunkt Pivot-Element, und keins der Elemente ai+1 < . . . < aj−1 war bis dahin Pivot (sonst wären ai und aj in verschiedenen Teilarrays). Wegen der zufälligen Reihenfolge der Pivot-Wahlen ist die Wahrscheinlichkeit, dass von den Elementen ai < . . . < aj gerade ai oder aj 1 1 + j−i+1 . Dies gilt für jedes Paar, sodass zuerst gewählt werden, gerade j−i+1 sich als erwartete Anzahl von Vergleichen und damit mittlere Laufzeit ergibt n−1 n−i+1 n X n X X 2 X 1 2 = ≤2 = 2n · Hn j − i + 1 k k i=1 k=2 i=1 k=1 j=i+1

n−1 X n X i=1



Θ(n log n) .

Satz 1.10

 Nach Satz 2.3 ist die Laufzeit also immer irgendwo in Ω(n log n) ∩ O(n2 ), wegen Satz 2.4 jedoch meistens nahe der unteren Schranke. 2.5 Bemerkung (randomisiertes QuickSort) Die average-case Analyse zeigt auch, dass eine Variante von Quicksort, in der das Pivot-Element zufällig (statt immer von der ersten Position) gewählt wird, im Mittel auch auf Eingaben schnell ist, die lange vorsortierte Teilfolgen enthalten. Die gleiche Wirkung erhält man durch zufälliges Permutieren der Eingabe vor dem Aufruf von QuickSort.

2.2.2

MergeSort

Bei QuickSort ist die Aufteilung zwar aufwändig und kann ungünstig erfolgen, garantiert dafür aber eine triviale Kombination der Teilergebnisse. Im

Algorithmen und Datenstrukturen (WS 2010/2011)

17

Gegensatz dazu gilt bei MergeSort:

easy split, hard join

• triviale Aufteilung in günstige Teilfolgengrößen • linearer Aufwand für Kombination (und zusätzlicher Speicherbedarf)

Algorithmus 7: MergeSort Aufruf: mergesort(M, 1, n) mergesort(M, l, r) begin if l < r then m ← b l+r−1 c 2 mergesort(M, l, m) mergesort(M, m + 1, r) i ← l; j ← m + 1; k ← l while i ≤ m and j ≤ r do if M [i] ≤ M [j] then M 0 [k] ← M [i]; i←i+1 else M 0 [k] ← M [j]; j ←j+1 k ←k+1 for h = i, . . . , m do M [k + (h − i)] ← M [h] for h = l, . . . , k − 1 do M [h] ← M 0 [h] end

r

l M: m

r

l M: →i

→j

m m+1

M0 :

M: j

Algorithmen und Datenstrukturen (WS 2010/2011)

18

2.6 Beispiel (MergeSort) 5

8

7

27

9

1

17

23

5

8

7

27

9

1

17

23

5

8

7

27

9

1

17

23

5

8

7

27

9

1

17

23

5

8

7

27

1

9

17

23

5

7

8

27

1

9

17

23

1

5

7

8

9

17

23

27

2.7 Satz Die Laufzeit von MergeSort ist in Θ(n log n). Beweis. Wie bei QuickSort setzt sich die Laufzeit aus dem Aufwand für Aufteilung und Kombination zusammen. Für MergeSort gilt: • konstanter Aufwand für Aufteilung und • linearer Aufwand für Kombination (Mischen). In jeder Rekursionsebene ist der Aufwand damit linear in der Gesamtzahl der Elemente 0 1 2 log n und wegen der rekursiven Halbierung der Teilfolgenlänge ist die Rekursionstiefe immer log n. Die Gesamtlaufzeit ist daher immer in Θ(n log n). 

best, average und worst case

Algorithmen und Datenstrukturen (WS 2010/2011)

2.3

19

HeapSort

SelectionSort (Abschnitt 2.1) verbringt die meiste Zeit mit der Auswahl des Extremums und kann durch eine besondere Datenstruktur zur Verwaltung der Elemente beschleunigt werden. Wir werden hier immer das Maximum ans Ende der Restfolge setzen und wollen daher einen Datentyp, der schnell das Maximum einer veränderlichen Menge von Werten zurück gibt. Die Prioritätswarteschlange ist ein abstrakter Datentyp für genau diesen Zweck. MaxPriorityQueque insert(item a) item extractMax() Falls beide Operationen mit o(n) Laufzeit realisiert sind, ergibt sich eine Verbesserung gegenüber SelectionSort. Eine mögliche solche Implementation ist ein binärer Heap: darin werden die Elemente in einem vollständigen binären Baum gespeichert, der die folgende Bedingung erfüllen muss. Heap-Bedingung: Für jeden Knoten gilt, dass der darin gespeicherte Wert nicht kleiner ist als die beiden Werte in seinen Kindern. ≥ ≥



≥ ≥



Eine unmittelbare Folgerung aus der Heap-Bedingung ist, dass im ganzen Teilbaum eines Knotens kein größerer Wert vorkommt. Ein binärer Heap kann in einem Array realisiert werden, d.h. ohne Zeiger etc. zur Implementation der Baumstruktur:

Algorithmen und Datenstrukturen (WS 2010/2011)

20

1 b 2i c

2 3 4

i

5 6 7 8

2i 2i + 1

9

Die beiden Operationen der Prioritätswarteschlange werden dann wie folgt umgesetzt. Bei insert a → M wird das neue Element hinter allen Elementen im Array eingefügt (also als rechtestes Blatt im Binärbaum) und solange mit seinem Elternknoten vertauscht, bis die Heap-Bedingung wieder hergestellt ist. Algorithmus 8: insert a → M (M enthält aktuell n Elemente) begin i←n+1 while (i > 1) and (M [b 2i c] < a) do // nicht-striktes and i M [i] ← M [b 2 c] i ← b 2i c M [i] ← a end Da der Binärbaum vom Blatt bis höchstens zur Wurzel durchlaufen wird und jeweils konstanter Aufwand anfällt, ist die Laufzeit in O(log n).

Algorithmen und Datenstrukturen (WS 2010/2011)

21

Analog kann für extractMax a ← M das erste Arrayelement – die Wurzel des Baumes, und damit das größte Element im Heap – entfernt und das so entstandene „Loch“ jeweils mit einem größten Kind gefüllt werden. Da das schließlich zu löschende Blatt in der Regel nicht an der gewünschten Stelle steht (nämlich am Ende des Arrays), bietet sich jedoch eine andere Vorgehensweise an: Wir schreiben das letzte Element des Arrays in die Wurzel, und vertauschen von dort absteigend solange mit einem größeren Kind, bis die Heap-Bedingung wieder hergestellt ist. Dieses Methode wird auch heapify, der zu Grunde liegende Prozess versickern genannt. Die Implementation erfolgt in der Regel etwas allgemeiner, um die Wiederherstellung der Heap-Bedingung auch an anderer Stelle i 6= 1 als der Wurzel veranlassen zu können und die rechte Grenze (den rechtesten tiefsten Knoten) vorgeben zu können, an dem die Vertauschungen stoppen sollen. Um Zuweisungen einzusparen, werden die Vertauschungen außerdem nicht explizit durchgeführt, sondern das Element a in der Wurzel (des Teilbaums) zwischengespeichert, und das entstandene Loch absteigend von unten gefüllt, bis das Element selbst einzutragen ist. Algorithmus 9: Wiederherstellung der Heap-Bedingung heapify(i, r) begin a ← M [i]; j ← 2i while j ≤ r do if (j < r) and (M [j + 1] > M [j]) then j ← j + 1 if a < M [j] then f06-25:43 M [i] ← M [j] i ← j; j ← 2i else j ←r+1 M [i] ← a end Unter Verwendung von heapify kann die Extraktion des Maximums jetzt wie folgt durchgeführt werden.

Algorithmen und Datenstrukturen (WS 2010/2011)

22

Algorithmus 10: extractMax a ← M begin if n > 0 then a ← M [1] M [1] ← M [n] n←n−1 heapify(1, n) return a else error „Heap leer“ end Wieder wird der Binärbaum maximal von der Wurzel zu einem Blatt durchlaufen, sodass die Operationen Einfügen und Maximumssuche in O(log n) ⊂ o(n) Zeit augeführt werden. Durch Einfügen aller Elemente in einen Heap und wiederholter Extraktion des Maximums kann SelectionSort also schneller implementiert werden. Wir vermeiden nun noch die Erzeugung einer Instanz des Datentyps und führen die notwendigen Operationen direkt im zu sortierenden Array aus. Der sich daraus ergebende Sortieralgorithmu heißt HeapSort und besteht aus zwei Phasen: 1. Aufbau des Heaps: Herstellen der Heap-Bedingung von unten nach oben, 2. Abbau des Heaps: Maximumssuche und Vertauschen nach hinten Unter Verwendung von heapify ist die Implemention denkbar einfach. Algorithmus 11: HeapSort begin for i = b n2 c, . . . , 1 do heapify(i, n) for i = n, . . . , 2 do vertausche M [1], M [i] heapify(1, i − 1) end

// Aufbau // Abbau

Algorithmen und Datenstrukturen (WS 2010/2011)

23

2.8 Beispiel Aufbau des Heaps: 5

5

8

8 7

7

27

27

heapify(4,8) 9

9 1

1 17

17

23

23 heapify(3,8) 5

5

27

8 17

17

23

27

heapify(2,8) 9

9 1

1 7

8 heapify(1,8) 27 23 17 8 9 1 7 5

7 23

Algorithmen und Datenstrukturen (WS 2010/2011)

24

2.9 Beispiel Abbau des Heaps: 27

23

23

9 17

17

vertausche(1,8), heapify(1,7)

8

8

9

5 1

1 7

7

5

27 vertausche(1,7), heapify(1,6) 9

17

8

9 7

7

vertausche(1,6), heapify(1,5)

1

8

5

5 17

1 23

23

27

27

vertausche(1,5), heapify(1,4) 8

7

5

5 7

1

vertausche(1,4), heapify(1,3)

1

8

9

9 17

17 23

23

27

27 vertausche(1,3), heapify(1,2) 1

5

5

1 7

7

vertausche(1,2), heapify(1,1)

8

8

9

9 17

17 23

27

23 27

Algorithmen und Datenstrukturen (WS 2010/2011)

25

2.10 Satz Die Laufzeit von HeapSort ist in O(n log n). Beweis. Wir nehmen zuächst an, dass n = 2k − 1. Beim Aufbau des Heaps werden bn/2c Elemente in ihren Teilbäumen versickert. Für die ersten n/4 Elemente haben diese Höhe 1, für die nächsten n/8 Elemente die Höhe 2 und allgemein gibt es n/2i Elemente der Höhe i, i = 2, . . . , log n. Damit ist der Aufwand für den Aufbau dlog ne

log n X i X n · i = n 2i 2i i=2 i=2 log n

log n X i X i = n 2 − 2i 2i i=2 i=2

! (konstruktive Null)

log n−1

! X i + 1 log Xn i = n − 2i 2i i=1 i=2 !  log n−1  X i+1 i log n 2 + − i − = n i 2 2 2 n i=2   log n−1  X 1 log n    = n 1 + −  i 2 n   i=2 | {z } M [m] then l ←m+1 else if k < M [m] then r ←m−1 else return „k ist in M “ return „k ist nicht in M “ end 3.5 Beispiel [TODO: z.B. 0,2,5,5,8,10,12,13,18,23,36,42,57,60,64,666, Anfragen nach 18,9.] 3.6 Bemerkung Wir könnten sogar angeben, an welcher Stelle k im Array M auftritt, gehen aber hier davon aus, dass der aufrufende Programmteil nicht wissen kann, dass die Schlüssel in einem Array verwaltet werden und daher auch mit der Positionsinformation nichts anfangen kann. Zum einen verwenden die Verfahren in den nächsten Abschnitten andere Datenstrukturen, und zum anderen ändern sich die Positionen im Allgemeinen, wenn Elemente eingefügt und gelöscht werden (da das Array immer sortiert sein muss). 3.7 Satz Binäre Suche auf einem sortierten Array der Länge n benötigt Θ(log n) Schritte, um einen Schlüssel k zu finden bzw. festzustellen, dass kein Element mit Schlüssel k in M enthalten ist. Beweis. Die Anzahl der Schleifendurchläufe ist immer Θ(log n), da das Intervall M [l, . . . , r] nach jedem Durchlauf eine Länge hat, die um höchstens eins von der Hälfte der vorherigen abweicht.  3.8 Bemerkung Moderne Prozessoren verwenden Pipelining, sodass Sprünge in bedingten Verzweigungen in der Regel zu Zeitverlust führen. Bei im Wesentlichen gleich-

Algorithmen und Datenstrukturen (WS 2010/2011)

41

verteilten Eingaben kann es daher günstiger sein, statt m ← b l+r c eine un2 1 gleiche Aufteilung wie z.B. m ← b 3 (l + r)c zu wählen, weil die Bedinung in der Schleife dann voraussichtlich häufiger zutrifft als nicht und der Inhalt der Pipeline dann nicht verloren geht. Eine ähnliche Idee wie in der voraus gegangenen Bemerkung liegt auch der Interpolationssuche zu Grunde. Handelt es sich bei den Schlüsseln des Arrays um Zahlen und liegt k deutlich näher an einem der Inhalte der Randzellen, macht es unter Umständen Sinn, nicht in der Mitte, sondern entsprechend näher an diesem Rand einen Vergleich vorzunehmen und damit gleich mehr als die Hälfte der verbleibenden auszuschließen. Algorithmus 18: Interpolationssuche interpolationsearch(M [0, . . . , n − 1], x) begin l ← 0; r ← n − 1 while x ≥ Mj[l] and x ≤ M [r] k do m←l+

x−M [l] (r M [r]−M [l]

− l)

if x > M [m] then l ←m+1 else if x < M [m] then r ←m−1 else return „x ist in M “ return „x ist nicht in M “ end 3.9 Beispiel [TODO: Gleiche zwei Anfragen wie oben – hat’s was gebracht?] Binäre und Interpolationssuche setzen voraus, dass man die Länge des Arrays kennt. Wenn die Länge unbekannt (oder zumindest sehr groß) und das gesuchte Element auf jeden Fall (insbesondere an eher kleiner Indexposition) enthalten ist, bietet sich an, den Suchbereich zunächst vorsichtig von vorne

ausreichend: intervallskalierte Daten

Algorithmen und Datenstrukturen (WS 2010/2011)

42

einzugrenzen. Algorithmus 19: Exponentielle Suche expsearch(M [0, . . .], x) begin r←1 while x > M [r] do r ← 2r binsearch(M [b 2r c, . . . , r], x) end Die Korrektheit des Verfahrens ergibt sich daraus, dass nach Abbruch der while-Schleife sicher k ∈ [M [b 2r c], M [r]] gilt. Für die Laufzeit beachte, dass die Teilfolge, auf der binär gesucht wird, in genau so vielen Schritten bestimmt wird, wie die Suche dann anschließend auch braucht. Natürlich kann statt binärer Suche auch jedes andere Suchverfahren für sortierte Folgen benutzt werden.

3.2

Geordnete Wörterbücher

Ein Wörterbuch heißt geordnet, wenn es so organisiert ist, dass zu jedem Zeitpunkt mit linearem Aufwand alle Paare aus Schlüsseln und Elementen in Sortierreihenfolge ausgeben werden können. Voraussetzung ist daher, dass wir über der Grundmenge, aus der die Schlüssel stammen, eine Ordnung ≤ gegeben haben. Auch wenn die Schlüssel in den Beispielen der Einfachheit halber wieder Zahlen sein werden, wird also in der Regel nicht ausgenutzt, um wieviel sich zwei Werte unterscheiden. Verfahren für ungeordnete Wörterbucher, die andere Voraussetzungen an die Schlüssel machen, behandeln wir im nächsten Kapitel.

3.2.1

Binäre Suchbäume

Statt Listen oder Arrays werden wir nun Binärbäume als Datenstruktur für die Organisation der Schlüssel verwenden. Wir nehmen dabei an, dass in jedem Knoten ein Element mit seinem zugehörigen Schlüssel, die beiden Kinder und der Vorgänger im Baum wie folgt gespeichert werden.

Algorithmen und Datenstrukturen (WS 2010/2011)

43

Node node parent node left, right item item Ein beliebiger Binärbaum heißt binärer Suchbaum, wenn die Schlüssel so auf die Knoten verteilt sind, dass die Suchbaumeigenschaft erfüllt ist. v.parent enthält v.item

v

und damit v.key = v.item.key

Suchbaumeigenschaft:

v.elem = v.item.elem

v.lef t

L(v)

v.right

w.key < v.key w.key > v.key

∀w ∈ L(v) ∀w ∈ R(v)

R(v)

Wegen der Suchbaumeigenschaft können die Elemente eines Suchbaums T mittels inorder-Durchlauf in Linearzeit sortiert ausgegeben werden (Aufruf: inordertraversal(T.root)). Binäre Suchbäume könne daher zur Implementation geordneter Wörterbücher verwendet werden. Algorithmus 20: inorder-Durchlauf inordertraversal(v) begin if v 6= nil then inordertraversal(v.lef t) print v.key inordertraversal(v.right) end

vgl. HeapBedingung

Algorithmen und Datenstrukturen (WS 2010/2011)

44

Im Folgenden werden die Methoden des ADT Dictionary mit binären Suchbäumen implementiert: Algorithmus 21: find(k) v ← search(T, k) if v 6= nil then return v.item else return nil

// kein Element in T hat Schlüssel k

search(T, k) begin v ← T.root while (v 6= nil) and (v.key 6= k) do if k < v.key then v ← v.lef t else v ← v.right return v end 3.10 Beispiel find(4)

7

2

1

12

5

3 4∈ /T

9

6

Algorithmen und Datenstrukturen (WS 2010/2011)

45

Die Laufzeit ist offensichtlich linear in der Höhe des Baumes. Aber wie hoch kann ein binärer Baum sein, wenn er die Suchbaumeigenschaft erfüllt? Ähnlich wie bei der Rekursionstiefe von QuickSort kann eine ungünstige Aufteilung zu einer Höhe (z.B. für n = 8) von

und mindestens blog nc führen. Algorithmus 22: insert(a, k) v ← T.root if v = nil then T.root ← newnode((a, k)) else while v 6= nil and k 6= v.key do u←v if k < v.key then v ← v.lef t else v ← v.right if k = v.key then print “Schlüssel k bereits vergeben“ else v ← newnode(a, k) if k < u.key then u.lef t ← v else u.right ← v v.parent ← u

höchstens n − 1

Algorithmen und Datenstrukturen (WS 2010/2011)

46

Algorithmus 23: remove(k) v ← search(T, k) if v 6= nil then a ← v.elem if v.lef t 6= nil then u←v v ← v.lef t while v.right 6= nil do v ← v.right u.item ← v.item w ← v.lef t if v = u.lef t then u.lef t ← w else u ← v.parent u.right ← w else w ← v.right if v = T.root then T.root ← w; u ← w else u ← v.parent if k < u.key then u.lef t ← w else u.right ← w if w 6= nil then w.parent ← u deletenode(v) else a ← nil return a 3.11 Bemerkung Falls der Baum in einem Array realisiert ist, kann parent weggelassen und während des Abstiegs identifiziert werden.

Algorithmen und Datenstrukturen (WS 2010/2011)

3.2.2

47

AVL-Bäume

Um kurze Laufzeiten für die Wörterbuchoperationen garantieren zu können, muss die Höhe der Suchbäume niedrig gehalten werden. Um dem Idealfall eines vollständigen Binärbaums (dessen Höhe logarithmisch ist) möglichst nahe zu kommen, wird eine weitere Eigenschaft gefordert. Balanciertheitseigenschaft: Für jeden (inneren) Knoten eines Binärbaums gilt, dass die Höhe der Teilbäume der beiden Kinder sich um höchstens 1 unterscheidet. Ein binärer Suchbaum, der zusätzlich die Balanciertheitseigenschaft aufweist, heisst AVL-Baum. Der folgende Satz zeigt, dass die Höhe balancierter Binär- AdelsonVelskij, bäume asymptotisch minimal ist. Landis 3.12 Satz Ein AVL-Baum mit n Knoten hat Höhe Θ(log n). Beweis. Jeder Binärbaum mit n Knoten hat Höhe Ω(log n). Für die Abschätzung nach oben betrachte das umgekehrte Problem: Wieviele innere Knoten hat ein AVL-Baum der Höhe h mindestens? Nenne diese Zahl n(h). n(2) = 2

n(1) = 1

h ≥ 3: Die beiden Unterbäume der Wurzel müssen ebenfalls AVL-Bäume sein und haben mindestens Höhe h − 1 und h − 2. n(h)

≥ n(h − 1) + n(h − 2) + 1 > 2 · n(h − 2) da n(h) offensichtlich streng monoton wachsend

h−2

h−1

h

≥ 2d 2 e−1 · n(h − 2 · (dh/2e − 1)) | {z }

h

∈{1,2}

h

⇐⇒ ⇐⇒

≥ 2d 2 e−1 log n(h) ≥ h2 − 1 h ≤ 2 log n(h) + 2

Also hat ein AVL-Baum mit n Knoten Höhe O(log n).



Algorithmen und Datenstrukturen (WS 2010/2011)

48

find kann unverändert implementiert werden, bei insert und remove muss jedoch die Balanciertheit erhalten werden. Dazu speichert man in jedem Knoten v zusätzlich den Höhenunterschied seiner beiden Teilbäume. diesem Knoten: v L(v)

R(v)

!

balance(v) = h(R(v)) − h(L(v)) ∈ {−1, 0, 1}

Fügt man wie gehabt ein neues Blatt ein, wächst die Höhe der Teilbäume von Vorfahren um höchstens 1. Möglicherweise wird an diesen dadurch die Balanciertheitseigenschaft verletzt. Sei u der erste Knoten auf dem Weg vom eingefügten Blatt zur Wurzel, der nicht balanciert ist (d.h. |balance(u)| > 1). Seien ferner v und w die beiden Nachfahren auf dem Weg von u zum eingefügten Knoten (diese existieren sicher, da die Balanciertheitseigenschaft nur bei Knoten der Höhe mindestens 2 verletzt sein kann). W urzel

u v w

← neu eingefügt Wir bezeichenen mit a, b, c die Knoten u, v, w in inorder-Reihenfolge und mit T0 , T1 , T2 , T3 deren Unterbäume, ebenfalls in inorder-Reihenfolge. Wir können annehmen, dass balance(u) = +2, weil die Fälle für balance(u) = −2 symmetrisch sind und daher analog behandelt werden können. Der erste Knoten v auf dem Weg von u zum eingefügten Blatt ist dann auf jeden Fall das rechte Kind von u, und wir unterscheiden danach, ob w rechtes oder linkes Kind von v ist (d.h., ob b = v, c = w oder b = w, c = v gilt).

Algorithmen und Datenstrukturen (WS 2010/2011)

49

Fall 1: (b = v, c = w) u 2 a

v

einfache Rotation

w -1 c

1 b

T0 T1

v 0 b

u 0 a

w -1 c T3

T3

T0

c T3

T0

T1

T2

T1 b

T2

T2 T0

a

T1 b

T2

a

c T3

Dabei spielt es keine Rolle, ob T2 oder T3 der zu hohe Teilbaum mit dem eingefügten Blatt ist. Fall 2: (b = w, c = v)

2. u 2 a T0

1. -1 v c

w -1 b

doppelte Rotation

w 0 b

u 0 a

v 1 c T2

T2

T3

T0

T1

T3

T0 a

T1

T3

T1 T0 a

T1

b T2

c

b T2

c

Auch hier spielt es keine Rolle, ob in T1 oder T2 eingefügt wurde. In allen acht Fällen ist die Höhe des Teilbaums von b anschließend gleich der Höhe des Teilbaums von u vor der Einfügeoperation, sodass alle Vorfahren auch weiterhin balanciert sein müssen.

T3

Algorithmen und Datenstrukturen (WS 2010/2011)

50

Das Entfernen eines Elements beginnt ebenfalls wie beim gewöhnlichen binären Suchbaum. Dabei wird ein Knoten gelöscht, und auch hier kann dadurch die Balanciertheitseigenschaft an einem Knoten auf dem Weg vom gelöschten Knoten zur Wurzel verletzt sein. Beachte, dass dies auf maximal einen Knoten u zutrifft, weil die Verletzung durch einseitig verringerte Teilbaumhöhe sich nicht nach oben fortsetzt.   v Kind von u mit größerer Höhe, w Kind von v mit größerer Höhe Wir bezeichnen mit  (bei Gleichheit beliebig)

u v x

Situation wie vorher =⇒ einfache oder doppelte Rotation

w

Achtung: Der Unterbaum mit neuer Wurzel b = v bzw. b = w ist um 1 weniger hoch als vorher mit Wurzel u. Ein neue Verletzung der Balanciertheitsbedingung kann daher an einem Knoten u0 weiter oben auf dem Weg zur Wurzel auftreten und wir müssen weitere Rotationen vornehmen, bis die Wurzel erreicht ist.

3.2.3

Rot-Schwarz-Bäume

Durch die Balanciertheitsbedingung wird geährleistet, dass AVL-Bäume nicht allzu weit vom Ideal eines vollständigen Binärbaums abweichen und zumindest asymptotisch gleiche Maximalhöhe haben. Eine anderer Ansatz, Abweichungen in Grenzen zuzulassen, besteht in der Markierung einiger Knoten als Ausnahmen, die im Test von Eigenschaften nicht berücksichtigt werden. Speziell werden die Knoten eines binären Suchbaums in diesem Kapitel so gefärbt, dass der Teilbaum der normalen Knoten wie ein perfekter Binärbaum nur Blätter gleicher Tiefe hat. Statt der Balanciertheit speichern wir an jedem Baumknoten eine Farbmarkierung mit folgender Bedeutung: schwarz: rot:

normaler Knoten Ausgleichsknoten (für Höhenunterschiede)

Algorithmen und Datenstrukturen (WS 2010/2011)

51

Um nun dem Idealfall von Blätter gleicher Tiefe nahe zu kommen, wird für die Färbung folgende schwächere Invariante gefordert:

Wurzelbedingung: Die Wurzel ist schwarz. Farbbedingung: Die Kinder von roten Knoten sind schwarz (oder nil). Tiefenbedingung: Alle Blätter haben die gleiche schwarze Tiefe (definiert als die Anzahl schwarzer Vorfahren −1). 3.13 Beispiel

12 5

15

3

10 4

7 6

schwarz

13 11

17 14

8 schwarze Tiefe: 2

rot 3.14 Satz Die Höhe eines rot-schwarzen Baumes mit n Knoten ist Θ(log n). Beweis. handelt.

Höhe Ω(log n) ist wieder klar, weil es sich um einen Binärbaum

Da ein roter Knoten keinen roten Elternknoten haben kann, ist die Höhe nicht größer als zweimal die maximale schwarze Tiefe eines Blattes. Da alle

Algorithmen und Datenstrukturen (WS 2010/2011)

52

Blätter die gleiche schwarze Tiefe haben, kann diese aber nicht größer als blog nc sein.  insert Beginnen wie bei binärem Suchbaum. y Element wird in ein neues Blatt w eingefügt.  schwarz, falls Wurzel Färbe w rot sonst Wurzel- und Tiefenbedingung bleben so erfüllt, aber die Farbbedingung ist verletzt, falls der Elternknoten v von w auch rot ist. Wegen der Wurzelbedingung ist v dann nicht die Wurzel und hat einen schwarzen (sonst schon vorher Konflikt) Elternknoten u. Diese Situation heißt doppelrot bei w. Fall 1: (der Geschwisterknoten z von v ist schwarz)

u

u

a

a

oder

z

2

z

v

v c

b w

1

w c

b

wie bei AVL-Bäumen + Umfärben

Problem beseitigt a

b

c

z

Die beiden symmetrischen Fälle (wie bei AVL) werden analog behandelt. Fall 2: (der Geschwisterknoten z von v ist rot)

Algorithmen und Datenstrukturen (WS 2010/2011)

u

53

u oder

z

v

z

v

w

w

Umfärben

u z

u v

z

v

w

w

Die beiden symmetrischen Fälle (wie bei AVL) werden analog behandelt. Falls u die Wurzel ist, dann wird auch u schwarz gefärbt. Falls doppelrot jetzt bei u auftritt, werden Fall 1 und 2 iteriert, bis die Wurzel erreicht oder das Problem wegrotiert ist. remove Beginnen wie bei binärem Suchbaum. y Es wird ein Knoten v mit höchstens einem Kind gelöscht. u

u

u [fertig]

oder a)

v

v w

w w

Algorithmen und Datenstrukturen (WS 2010/2011)

u

54

u

doppelschwarz

b)

bei w

v

w

w (zeigt an, dass Tiefenbedingung verletzt) Falls w vorhanden, betrachte Geschwisterknoten y von w. Fall 1: (y schwarz mit mindestens einem roten Kind z) Farbe von u

c u

a

c u

oder

b y

a y

w

z

Rotation

b

+Umfärben

w

b

a

c

[fertig] w

z

Fall 2: (y schwarz ohne rotes Kind, oder y gibt es nicht) 2.1:

u y

2.2:

u w

y

u y

Fall 3: (y rot)

w

[fertig]

w

Fortsetzen für doppelschwarz bei u

u w

y

Algorithmen und Datenstrukturen (WS 2010/2011) y

u y

w

einfache Rotation

u

+ Umfärben

55

y Fall 1 oder 2.1, d.h. nach einem weiteren Schritt fertig. w

3.15 Satz find, insert und remove sind in rot-schwarzen Bäumen in Zeit O(log n) realisierbar. insert und remove benötigen maximal eine bzw. zwei (einfache oder doppelte) Rotationen. [Erinnerung: remove aus AVL-Baum benötigt evtl. O(log n) Rotationen]

3.2.4

B-Bäume

Suchbäume können als Indexstruktur für extern gespeicherte Daten verwendet werden. Ist der Index allerdings selbst zu groß für den Hauptspeicher, dann werden viele Zugriffe auf möglicherweise weit verstreut liegende Werte nötig (abhängig davon, wie der Baumdurchlauf und die Speicherung der Knoteninformationen zusammenpassen). Idee Teile Index in Seiten auf (etwa in Größe von Externspeicherblöcken) und sorge dafür, dass Suche nur wenige Seiten benötigt. Ein Vielwegbaum speichert pro Knoten mehrere Schlüssel in aufsteigender Reihenfolge und zwischen je zwei Schlüsseln (und vor dem Ersten und nach dem Letzten) einen Zeiger auf ein Kind. Die Zahl der Kinder ist damit immer 1 größer als die der Schlüssel. a1 < a2 < · · · < ak

w0

w1

w2

wk−1

wk

Algorithmen und Datenstrukturen (WS 2010/2011)

56

Suchbaum, falls für alle Knoten v mit Schlüssel a1 , . . . , ak und Kindern w0 , . . . , wk gilt: Alle Schlüssel in T (wi−1 ) < ai < alle Schlüssel in T (wi ) i = 1, . . . , k B-Baum der Ordnung d, d ≥ 2: Vielwegsuchbaum mit Wurzelbedingung: Knotenbedingung: Schlüsselbedingung: Tiefenbedingung:

Die Wurzel hat mindestens 2 und höchstens d Kinder. Jeder andere Knoten hat mindestens dd/2e und höchstens d Kinder. Jeder Knoten mit i Kindern hat i − 1 Schlüssel. Alle Blätter haben dieselbe Tiefe.

3.16 Beispiel (B-Baum der Ordnung 6)

22

11 12

37

38 40 41

24 29

42

65

46

58

43 45

59 63

48 50 51 53 56

72

80

66 70

93

83 85 86 74 75

find Suche in Knoten; falls Schlüssel nicht vorhanden, liegt er zwischen zwei enthaltenen Schlüsseln (bzw. vor dem Ersten oder hinter dem Letzten) y Suche an entsprechendem Kind fortsetzen. 3.17 Satz Ein B-Baum der Ordnung d mit n Knoten hat Höhe Θ(logd n). Beweis. Zeige zunächst: B-Baum mit n Schlüsseln hat n+1 externe Knoten („nil-Zeiger“). Induktion über Höhe h:

95 98

Algorithmen und Datenstrukturen (WS 2010/2011)

57

h = 0 Wurzel hat 1 ≤ i ≤ d − 1 Schlüssel und i + 1 Kinder, die alle externe Knoten sind. h → h + 1 Wurzel hat 1 ≤ i ≤ d − 1 Schlüssel und i + 1 Kinder, die alle Wurzeln von Teilbäumen der Höhe h sind. Diese haben n0 , . . . , ni viele Schlüssel und (n0 + 1) + · · · + (ni + 1) externe Knoten. Es gibt also insgesamt n0 + · · · + ni + i viele Schlüssel und (n0 + 1) + · · · + (ni + 1) = n0 + · · · + ni + i + 1 viele externe Knoten. Wegen der Tiefen- und Knotenbedingung für die Höhe h(n) eines B-Baumes mit n + 1 externen Knoten gilt dann aber  h d ≤ n ≤ dh+1 2· 2  insert Suche analog zu vorher das Blatt, in das eingefügt werden kann. Wieviele Schlüssel hat dieses Blatt? < d − 1 einfügen, fertig. = d − 1 Überlauf → teile die d Schlüssel auf in die kleinsten bd/2c und die größten dd/2e − 1 viele, für die zwei neue Knoten erzeugt werden, und das mittlere, (bd/2c + 1)-te, das im Elternknoten eingefügt wird. → evtl. Überlauf im Elternknoten, dann iterieren [evtl. neue Wurzel].

... k

k1

k2

...

kd−1

k10

...

0 kbd/2c+1

0 kbd/2c

...

0 kbd/2c+2

...

kd0

Algorithmen und Datenstrukturen (WS 2010/2011)

58

remove Suche den Knoten, aus dem ein Schlüssel gelöscht werden soll. Wieviele Schlüssel enthält er? >

d

=

d

2 2

− 1 löschen, fertig. − 1 Unterlauf a) Knoten ist innerer Knoten (d.h. kein Blatt) → ersetze Schlüssel durch inorder-Vorgänger (oder Nachfolger), d.h. letzten Schlüssel im rechtesten Blatt des vorangehenden Teilbaums. ···

k

···

···

···

···

k0

··· ··

··

·

·

···

···

k0

k

→ fahre fort mit b). b) Knoten ist Blatt → falls ein direkter linker oder rechter Geschwisterknoten mehr als d d2 e − 1 Schlüssel enthält, verschiebe den Letzten bzw. Ersten von dort in den gemeinsamen Vorgänger und den trennenden Schlüssel von vorher in den Knoten mit Unterlauf. ··· ···

k

···

k1

···

k2

···

···

k

k1

···

k2

···

Unterlauf

→ sonst verschmilz den Knoten mit einem seiner direkter Geschwister und dem trennenden Schlüssel aus dem Vorgänger.

Algorithmen und Datenstrukturen (WS 2010/2011) ··· ···

k

···

59 ···

···

···

···

k

···

Falls Unterlauf in Vorgänger: iteriere b) für innere Knoten. [Bemerkung: anders als in a) gibt es jetzt kein überzähliges Kind.]

Kapitel 4 Streuen Wir behandeln nun Implementationen ungeordneter Wörterbücher, in denen die Schlüssel ohne Beachtung ihrer Sortierreihenfolge gespeichert werden dürfen, verlangen aber, dass es sich bei den Schlüsseln um Zahlen handelt. Dies ist keine starke Voraussetzung, weil sich die Elemente anderer Schlüsseluniversen als Zahlen codieren lassen. typisch Bezeichnungen: (Schlüssel-) Universum U ⊆ N0 Schlüsselmenge K ⊆ U, |K| = n Hashtabelle H[0, . . . , m − 1] (Array von Zeigern auf Datenelemente)

in JAVA: Speicheradresse Object.hashCode

Durch eine Hashfunktion h : U → {0, . . . , m − 1} wird jedem zulässigen hash (engl.): Schlüssel k ∈ U seine Speicherstelle H[h(k)] in der Hashtabelle H zugewiesen. streuen Im Idealfall gilt für die Menge K ⊆ U der vorkommenden Schlüssel k1 6= k2 ∈ K =⇒ h(k1 ) 6= h(k2 )

(h|K injektiv)

weil Einfügen, Suchen und Löschen dann jeweils in Θ(1) realisiert werden können. 4.1 Beispiel (Hashfunktionen) 1. h(k) = k mod m für Primzahl m (Divisions- oder Kongruenzmethode) 60

Algorithmen und Datenstrukturen (WS 2010/2011) 2. h(k) = bm · (kα − bkαc)c z.B. für α = φ−1 = (Multiplikationsmethode)



61 5−1 2

≈ 0, 61803

Die n+1 Intervalle nach Einfügen von 1, . . . , n haben nur 3 verschiedene Größen; bei α = φ−1 am ähnlichsten.

4.1

Kollisionen

Im Allgemeinen ist allerdings h(k1 ) = h(k2 ) für einige h1 , h2 ∈ K. Dies bezeichnen wir mit Kollisionen, k1 , k2 heißen Synonyme. pm,n := P (keine Kollisionen) = 1· m−1 · m−2 · . . . · m−n+1 , falls Schlüssel durch m m m h gleichmäßig gestreut werden, d.h. alle Positionen gleichwahrscheinlich sind. 4.2 Beispiel (Geburtstagsparadoxon) p365,22 > 0.5 > p365,23 Anschauliche Deutung: bei 23 oder mehr Personen ist die Wahrscheinlichkeit, dass zwei am gleichen Tag Geburtstag haben, größer als die Wahrscheinlichkeit, dass alle an verschiedenen Tagen Geburtstag haben. Herleitung: pm,n =

Qn−1 i=0

m−i m

Qn−1 (1 − mi ) = Qi=1 n 1 Pn−1 n−1 −i/m = e− m i=1 i = e−( 2 )/m ≈ i=1 e

Für welches n ist pm,n ≈ 1/2? n

e−( 2)/m = 12 ⇔ − n2 /m = ln 12 n ⇔ = m ln 2 2 q n(n−1) 2



n



p 2(ln 2) · m (≈ 22.49 für m = 365)

goldener Schnitt

Algorithmen und Datenstrukturen (WS 2010/2011)

62

Erwartete Anzahl Kollisionen Sei  1 falls h(ki ) = h(kj ) (Kollision von ki , kj ) Xij = 0 sonst Bei gleichmäßiger Streuung ist E(Xij ) = 1/m für i 6= j. !   X X X n E(X) = E Xij = E(Xij ) = 1/m = · 1/m 2 i