Datenstrukturen

Werbung
Datenstrukturen
Mariano Zelke
Sommersemester 2012
Mathematische Grundlagen: Das Handwerkszeug
I
Formeln:
-
n
P
i=
-
i=1
k
P
ai =
i=0
I
n·(n+1)
2
und
ak+1 −1
a−1 ,
falls a 6= 1
Rechnen mit Logarithmen: Es gelte a, b > 1 und x, y > 0 seien
reelle Zahlen. Dann ist
1. loga (x · y ) = loga x + loga y
2. loga (x y ) = y · loga (x)
3. aloga x = x
4. loga x = (loga b) · (logb x)
5. b loga x = x loga b
Mariano Zelke
Datenstrukturen
2/26
Die asymptotische Notation
f , g : N → R≥0 seien Funktionen, die einer Eingabelänge n ∈ N eine
nicht-negative Laufzeit f (n), bzw. g (n) zuweisen.
I
Die Groß-Oh Notation: f = O(g ) ⇔ Es gibt eine positive Konstante
c > 0 und eine natürliche Zahl n0 ∈ N, so dass
f (n) ≤ c · g (n)
für alle n ≥ n0 gilt: f wächst höchstens so schnell wie g .
I
f = Ω(g ) ⇔ g = O(f ) : f wächst mindestens so schnell wie g .
I
f = Θ(g ) ⇔ f = O(g ) und g = O(f ) : f und g wachsen gleich
schnell.
I
I
= 0: f wächst
Die Klein-Oh Notation: f = o(g ) ⇔ lim gf (n)
n→∞ (n)
langsamer als g .
g (n)
n→∞ f (n)
f = ω(g ) ⇔ lim
Mariano Zelke
= 0: f wächst schneller als g .
Datenstrukturen
3/26
Asymptotik
Der Grenzwert der Folge
f (n)
g (n)
f (n)
n→∞ g (n)
möge existieren und es sei lim
= c.
1. Wenn c = 0, dann ist f = o(g )
2. Wenn 0 < c < ∞, dann ist f = Θ(g )
3. Wenn c = ∞, dann ist f = ω(g )
4. Wenn 0 ≤ c < ∞, dann ist f = O(g )
5. Wenn 0 < c ≤ ∞, dann ist f = Ω(g )
Mariano Zelke
Datenstrukturen
4/26
Wachstums-Hierarchie
Es seien a > 1, b > 1 und k > 1 Konstanten. Dann bilden die
folgenden Funktionen von N>0 nach R≥0 eine Wachstumshierarchie:
bn
nk
n · log2 n
n
n1/k
loga n
(k)
loga n
wächst asymptotisch schneller
n!
1
Mariano Zelke
Datenstrukturen
5/26
Das Lösen von Rekursionsgleichungen
Mastertheorem: Die Rekursion T (1) = c, T (n) = aT bn + t(n) ist zu
lösen, wobei b > 1, a ≥ 1, c > 0.
- Wenn t(n) = O n(logb a)−ε für eine positive Konstante ε > 0, dann
ist T (n) = Θ(nlogb a )
- Wenn t(n) = Θ(nlogb a ), dann ist T (n) = Θ(nlogb a · logb n)
(logb a)+ε für eine positive Konstante ε > 0 und
- Wenn t(n)
=
Ω
n
a · t bn ≤ αt(n) für eine Konstante α < 1, dann ist
T (n) = Θ(t(n))
I
Wann ist es anwendbar?
I
Wann nicht? (Beispiel: Türme von Hanoi)
I
Was tun, wenn nicht?
Mariano Zelke
Datenstrukturen
6/26
Laufzeitbestimmung von C++ Programmen
I
Zuweisungen: Eine Zuweisung zu einer einfachen“ Variablen ist
”
einfach zu zählen, eine Zuweisung zu einer Array-Variablen ist mit
der Länge des Arrays zu gewichten.
I
Auswahl-Anweisungen: Häufig genügt: Bedingung +
Gesamtaufwand für den längsten der alternativen Anweisungsblöcke.
I
Schleifen: Häufig genügt: Maximale Anzahl der auszuführenden
Anweisungen innerhalb einer Schleife × Anzahl der
Schleifendurchläufe
Mariano Zelke
Datenstrukturen
7/26
Elementare Datenstrukturen: Listen, Stacks und Queues
I
Listen passen sich der Größe der zu speichernden Datenmenge
dynamisch an. Wenn die Position eines einzufügenden oder zu
entfernenden Elements bekannt ist, dann gelingt die Operation
schnell. Muss nach der Position gesucht werden, dann ist die
Laufzeit fürchterlich.
Die Adjazenzlistendarstellung von Graphen ist eine wichtige
Anwendung.
I
Stacks: Einfügen und Entfernen des jüngsten Elements, beide
Operationen gelingen schnell.
Stacks finden zum Beispiel in der Implementierung der Rekursion
eine wichtige Anwendung.
I
Queues: Einfügen und Entfernen des ältesten Elements, beide
Operationen gelingen schnell.
Queues modellieren Warteschlangen. Eine wichtige Anwendung ist
die Implementierung der Breitensuche.
I
Deques: Verallgemeinern Stacks und Queues
Mariano Zelke
Datenstrukturen
8/26
Bäume
I
Als gerichteter Graph: alle Knoten müssen von der Wurzel aus
erreichbar sein; zusätzlich hat jeder Knoten höchstens eine
eingehende Kante
Als ungerichteter Graph: ein Baum ist ein zusammenhängender,
kreisfreier Graph
I
Wichtige Datenstrukturen: Vater-Array (wenn nur das
Hochklettern im Baum unterstützt werden muss),
Binärbaum-Darstellung, Kind-Geschwister-Implementierung
(wenn ein schneller Zugriff auf die Kinder erforderlich ist)
I
Bäume können zum Beispiel mit dem Präorder-, Postorder- und
Inorder-Verfahren durchlaufen werden. Die Verfahren sind schnell:
Zeit O(n) für Bäume mit n Knoten
I
Anwendungen dieser Verfahren sind zum Beispiel die Berechnung
der Tiefe des Baumes, bzw. die Berechnung der Anzahl der Blätter
Mariano Zelke
Datenstrukturen
9/26
Graphen – Tiefensuche
Adjazenzlistendarstellung: für viele Anwendungen (z.B. das Navigieren
in Graphen) ausreichend. Vorteile: schnelle Bestimmung aller Nachbarn
bzw. aller direkten Nachfolger
Tiefensuche
void tsuche(int v){
Knoten *p; besucht[v] = 1;
for (p = A[v]; p !=0; p = p->next)
if (!besucht [p->name]) tsuche(p->name);}
I
I
I
I
Ein Aufruf von Tiefensuche für Knoten w , bei ausschließlich
unmarkierten Knoten, besucht alle von w aus erreichbaren Knoten
Tiefensuche für Graphen G = (V , E ) ist schnell: Laufzeit
O(|V | + |E |).
Die Baumkanten definieren den Wald der Tiefensuche.
Ungerichtete Graphen besitzen neben den Baumkanten nur
Rückwärtskanten. Gerichtete Graphen besitzen Baum- und
Vorwärtskanten sowie Rückwärts- und rechts-nach-links Querkanten.
Mariano Zelke
Datenstrukturen
10/26
Anwendungen der Tiefensuche
I
Ungerichtete Graphen: Schnelle Antwort auf die Fragen, ob ein
Graph ein Baum, ein Wald oder zusammenhängend ist
I
Die Bäume des Waldes der Tiefensuche entsprechen den
Zusammenhangskomponenten des Eingabegraphen.
I
Gerichtete Graphen: Schnelle Antwort auf die Fragen, ob ein Graph
azyklisch oder stark zusammenhängend ist, bzw. die Berechnung
einer topologischen Sortierung
Mariano Zelke
Datenstrukturen
11/26
Graphen – Breitensuche
void Breitensuche(int v){
Knoten *p; int w; queue q;
for (int k =0; k < n ; k++) besucht[k] = 0;
q.enqueue(v); besucht[v] = 1;
while (!q.empty( )){
w = q.dequeue( );
for (p = A[w]; p != 0; p = p->next)
if (!besucht[p->name]){
q.enqueue(p->name);
besucht[p->name] = 1;}}}
I
I
I
Breitensuche für Graphen G = (V , E ) ist schnell: Zeit O(|V | + |E |)
Ein Aufruf von Breitensuche für Knoten w erzeugt über die
Baumkanten einen Baum kürzester Wege vom Startknoten w zu
allen von w aus erreichbaren Knoten
Anhand dieses Baumes der Breitensuche lassen sich Rückwärts- und
Querkanten in G klassifizieren.
Mariano Zelke
Datenstrukturen
12/26
Prioritätswarteschlangen
insert, delete max, change priority, remove.
I
I
I
I
I
Heaps speichern die Prioritäten mit Heap-Struktur und
Heap-Ordnung ab.
Insbesondere: die an Knoten v abgespeicherte Priorität ist ≥ den
von den beiden Kindern gespeicherten Prioritäten: Größte Priorität
also an der Wurzel!
Heap-Struktur garantiert, dass der Heap n Prioritäten durch n
aufeinanderfolgende Zellen abspeichern kann.
Insbesondere: wenn die Priorität des Knotens v in Position i
abgespeichert ist, dann wird die Priorität des linken Kindes in
Position 2 · i und die Priorität des rechten Kindes in Position 2 · i + 1
gespeichert. Die Priorität des Vaters befindet sich in Position bi/2c.
Die einzelnen Operationen werden mit Hilfe der Operationen
repair up und repair down implementiert. Sämtliche Operationen
auf einem Heap mit n Prioritäten benötigen nur Laufzeit O(log2 n).
Mariano Zelke
Datenstrukturen
13/26
Anwendung von Prioritätswarteschlangen
I
Implementierung zahlreicher Algorithmen, typischerweise: falls
wiederholt kleinste oder größte Schlüssel zu bestimmen und
Aktualisierungsschritte auszuführen sind
I
Fundamentales Beispiel: die Implementierung des Algorithmus von
Dijkstra
Weitere Beispiele: Implementierungen der Algorithmen für minimale
Spannbäume
I
I
I
I
Prim
Kruskal (hier auch: Union-Find-Datenstruktur)
Heapsort
Mariano Zelke
Datenstrukturen
14/26
Der abstrakter Datentyp Wörterbuch“
”
insert(x), remove(x) und lookup(x).
Binäre Suchbäume:
I
speichern die Schlüssel in den Knoten eines Binärbaums.
I
Wesentlich: die binäre Suchbaumordnung: alle im linken Teilbaum
eines Knoten v gespeicherten Schlüssel sind kleiner als der von v
gespeicherte Schlüssel ist. Alle im rechten Teilbaum von v
gespeicherten Schlüssel sind größer.
I
Mit der binären Suchbaumordnung kann ein Schlüssel schnell
gefunden werden, indem die namensgebende Binärsuche
durchgeführt wird. Die Dauer einer
lookup-Operation/remove-Operation im worst-case proportional zur
Tiefe.
I
die Tiefe eines binären Suchbaums mit n Schlüsseln ist blog2 nc im
best-case, n − 1 im worst-case und O(log2 n) im Erwartungswert.
Mariano Zelke
Datenstrukturen
15/26
AVL-Bäume
Ein AVL-Baum ist ein binärer Suchbaum mit Höhenbalancierung
I
In der Höhenbalancierung wird verlangt, dass sich, für jeden Knoten
v , die Höhe des linken und rechten Teilbaums von v um höchstens
eins unterscheiden.
I
Die lookup-Operation wird wie für binäre Suchbäume mit binärer
Suche ausgeführt.
I
Die remove-Operation wird im Wesentlichen umgangen und durch
eine lazy remove“-Operation ersetzt.
”
Links- und Rechtsrotationen sind die wesentlichen Hilfsmittel in der
Ausführung der insert-Operation. Während im Zick-Zick und
Zack-Zack Fall nur eine Rotation pro Iteration auszuführen ist,
müssen im Zick-Zack und Zack-Zick Fall zwei Rotationen
ausgeführt werden.
I
I
Die Ausführungszeiten von insert, remove und lookup sind für einen
AVL-Baum mit n Schlüsseln durch O(log2 n) beschränkt.
Mariano Zelke
Datenstrukturen
16/26
Hashing
n
spielt eine wichtige Größe: n ist die
Der Auslastungsfaktor λ = m
Anzahl der in der Hashtabelle eingefügten Schlüssel und m ist die Größe
der Hashtabelle.
Hashing mit Verkettung
I
alle Kollisionen, also alle Schlüssel x mit Hashwert h(x) = i, werden
in eine sortierte Liste eingefügt.
I
Die erwartete Länge einer Liste ist durch 1 + λ beschränkt und
damit ist die erwartete Laufzeit einer insert, remove oder
lookup-Operation durch O(1) + λ beschränkt.
I
Eine erfolgreiche Hashfunktion ist h(x) = x mod m. Hier sollte m
eine Primzahl sein, die genügend weit von einer Zweierpotenz
entfernt ist.
Mariano Zelke
Datenstrukturen
17/26
Hashing mit offener Addressierung
I
I
I
I
I
I
Es wird direkt in die Tabelle gehasht.
Benutze dazu eine Folge h0 , h1 , . . . , hm−1 von Hashfunktionen: Ist
der ite Versuch des Einfügens von Schlüssel x an Position hi (x)
nicht erfolgreich gewesen, dann wird die Position hi+1 (x) im
nächsten Versuch getestet.
Insbesondere verwendet man die folgende Daumenregel: Ist der
Auslastungsfaktor auf 21 angestiegen, dann lade die Tabelle in eine
neue Tabelle doppelter Größe.
Im linearen Austesten verwendet man die Folge
hi (x) = (x + i) mod m
im doppelten Hashing benutzt man Hashfunktionen
f (x) = x mod m und g (x) = m∗ − (x mod m∗ ) und setzt
hi (x) = (f (x) + i · g (x)) mod m.
Im linearen Austesten besteht die Gefahr der Klumpenbildung,
allerdings sind die einzelnen Hashfunktionen wesentlicher schneller
auswertbar als für das doppelte Hashing.
Mariano Zelke
Datenstrukturen
18/26
Klausur am 27. Juli, 9 Uhr, Hörsaal HV und HVI
I
Denken Sie an eine rechtzeitige Anmeldung.
I
Seien Sie pünktlich zu 9:00 Uhr.
I
Sie dürfen ein handschriftlich beidseitig beschriebenes DIN A4-Blatt
als Hilfsmittel mitbringen.
I
Sie müssen mitbringen: dokumentenechten schwarzen oder blauen
Stift, einen gültigen Lichtbildausweis (z.B. Ihre Goethe-Card oder
Ihren Personalausweis)
I
Sie dürfen nicht benutzen: eigenes Schreibpapier, Skript,
Taschenrechner, eingeschaltetes Handy
I
Sitzordnung wird von uns kurz vorher bekannt gegeben.
Mariano Zelke
Datenstrukturen
19/26
Klausur im WS 2012/13 am 1.10., 9 Uhr in Hörsaal HV
I
Diese Klausur kann unabhängig von der Teilnahme an der ersten
Klausur mitgeschrieben werden.
I
Informationen dazu (Wiederholungsveranstaltungen etc.) finden Sie
einige Wochen vorher auf der Homepage.
Mariano Zelke
Datenstrukturen
20/26
Klausurvorbereitung
I
Der Stoff der Vorlesung bis einschließlich 3.7. ist klausurrelevant.
I
Wiederholen Sie die Übungsaufgaben.
I
Schauen Sie sich die alten Klausuren hinten im Skript an.
I
Nutzen Sie auch das Logbuch auf der Webseite für einen Überblick
über die Themen.
I
Am 17., 18., 23. und 24. Juli finden Helpdesk-Termine der Tutoren
statt. Die genauen Termine und Orte finden Sie auf der Homepage.
I
Dort finden Sie auch bald die Liste der Bonuspunkte.
Prüfen Sie dann Ihren Eintrag!
Mariano Zelke
Datenstrukturen
21/26
C++-Code vs. Pseudocode I
Tiefensuche
void tsuche(int v){
Knoten *p;
besucht[v] = 1;
for (p = A[v]; p !=0; p = p->next)
if (!besucht [p->name]);
tsuche(p->name);
}
Dieser C++-Code könnte als Pseudocode so geschrieben werden:
tsuche(Zahl v )
1
2
3
4
besucht[v] = true;
for jeden Knoten p in A[v ] der Reihenfolge nach do
if besucht[Name von p] = false then
tsuche(Name von p);
Mariano Zelke
Datenstrukturen
22/26
C++-Code vs. Pseudocode II
Der schon bekannte Pseudocode für Algorithmus A4 :
(1) Max1 = Max∗1 = a1 .
(2) Für k = 1, . . . , n − 1 setze
Max∗k+1 = max{Max∗k + ak+1 , ak+1 } und
Maxk+1 = max{Maxk , Max∗k+1 }.
(3) Maxn wird ausgegeben.
Das reicht als Beschreibung des Algorithmus aus, wenn klar ist, dass die
Eingabe aus n Zahlen a1 , a2 , . . . , an besteht.
Mariano Zelke
Datenstrukturen
23/26
C++-Code vs. Pseudocode III
Der schon bekannte Pseudocode für Matrixaddition mit einfach
verketteten Listen:
(1) Beginne jeweils am Anfang der Listen LA und LB .
(2) Solange beide Listen nicht leer sind, wiederhole
(a) das gegenwärtige Listenelement von LA (bzw. LB ) habe die Koordinaten
(iA , jA ) (bzw. (iB , jB )).
(b) Wenn iA < iB (bzw. iA > iB ), dann füge das gegenwärtige Listenelement
von LA (bzw. LB ) in die Liste LC ein und gehe zum nächsten Listenelement
von LA (bzw. LB ).
(c) Wenn iA = iB und jA < jB (bzw. jA > jB ), dann füge das gegenwärtige
Listenelement von LA (bzw. LB ) in die Liste LC ein und gehe zum nächsten
Listenelement von LA (bzw. LB ).
(d) Wenn iA = iB und jA = jB , addiere die beiden Einträge und füge die Summe
in die Liste LC ein. Die Zeiger in beiden Listen werden nach rechts bewegt.
(3) Wenn die Liste LA (bzw. LB ) leer ist, kann der Rest der Liste LB (bzw.
LA ) an die Liste LC angehängt werden.
Das reicht als Beschreibung aus, wenn das Format der Eingabe klar ist.
Mariano Zelke
Datenstrukturen
24/26
C++-Code vs. Pseudocode IV
Der Algorithmus von Prim läuft auf einem Graphen G = (V , E ) mit
V = {0, 1, . . . , n − 1}. Dafür ist der folgende Pseudocode schon
bekannt:
(1) Setze S = {0}.
(2) Solange S 6= V , wiederhole:
(a) Bestimme eine kürzeste kreuzende Kante e = {u, v }.
(b) Füge e zu B hinzu.
(c) Wenn u ∈ S, dann füge v zu S hinzu. Ansonsten füge u zu S hinzu.
Das reicht als Beschreibung des Algorithmus aus, vorausgesetzt, es wird
klar gemacht, wie die kreuzenden Kanten verwaltet werden, so dass die
kürzeste davon schnell gefunden werden kann.
Mariano Zelke
Datenstrukturen
25/26
C++-Code vs. Pseudocode V
Der folgende schon bekannte Pseudocode beschreibt insert(x) bei
Hashing mit offener Adressierung:
Wir arbeiten mit einer Folge
h0 , ..., hm−1 : U → {0, ..., m − 1}
von Hashfunktionen. Setze i = 0.
(1) Wenn die Zelle hi (x) frei ist, dann füge x in Zelle hi (x) ein.
(2) Ansonsten setze i = i + 1 und gehe zu Schritt (1).
Das reicht als Beschreibung aus, wenn klar ist, dass auf einem Array mit
Zellen 0, 1, . . . , m − 1 gearbeitet wird und wie die Hashfunktionen
aussehen.
Mariano Zelke
Datenstrukturen
26/26
Herunterladen