Datenstrukturen

Werbung
Datenstrukturen
Mariano Zelke
Sommersemester 2012
Einfach verkettete Listen
Eine Zeiger-Implementierung von einfach verketteten Listen, also Listen
mit Vorwärtszeigern.
//Deklarationsdatei liste.h für einfach verkettete Listen.
enum boolean {False, True};
class liste{
private:
typedef struct Element {
int data;
Element *next;
};
//Kann auch anderer Datentyp sein
Element *head, *current;
public: liste( ) // Konstruktor
{ head = new Element; current = head; head->next = 0; }
Mariano Zelke
Datenstrukturen
2/32
Public: Die weiteren Operationen
I
I
I
I
I
I
I
I
I
void insert(int data); // Ein neues Element mit Wert data
// wird nach dem gegenwärtigen Element eingefügt. Der Wert von
// current ist unverändert.
void remove( ); // Das dem gegenwärtigen Element folgende
// Element wird entfernt. Der Wert von current ist unverändert.
void next( ); // Das nächste Element wird aufgesucht. Dazu
// wird current um eine Position nach rechts bewegt.
void movetofront( ); // current erhält den Wert head.
boolean end( ); // Zeigt current auf das letzte Element?
boolean empty( ); // Ist die Liste leer?
int read( ); // Gib das Feld data des nächsten Elements aus.
void write(int wert); // Überschreibe das Feld data des
// nächsten Elements mit der Zahl wert.
void search(int wert); // Suche rechts von der gegenwärtigen
// Zelle nach der ersten Zelle z mit Datenfeld wert. Der Zeiger
// current wird auf den Vorgänger von z zeigen.
}
Mariano Zelke
Datenstrukturen
3/32
Auswirkung von public/private
I
I
Ein Programm, dass eine Liste a aus der Klasse liste benutzt, darf
nur über die public-Operationen auf a zugreifen.
Insbesondere ist kein direkter Zugriff erlaubt auf die Zeiger *head
und *current.
I
I
I
I
I
Falsch: a.current = a.head; stattdessen: a.movetofront();
Falsch: a.current = a.current->next; stattdessen: a.next();
Falsch: if(a.head->next == null) stattdessen: if(a.empty())
...
Auch auf die Felder data und *next jedes Elementes kann nicht
direkt zugegriffen werden
I
I
I
I
Falsch: a.current->next->data = 10;stattdessen: a.write(10);
Falsch: a.current->next = a.current->next->next;
stattdessen: a.remove();
Falsch: if(a.current->next == null) stattdessen: a.end();
...
Die Klasse liste ist ein abstrakter Datentyp, der für die Benutzung
ausschließlich die public-Operationen bereit stellt und sonst nichts.
Mariano Zelke
Datenstrukturen
4/32
Was ist ein Baum?
Ein Baum T wird durch eine Knotenmenge V und eine Kantenmenge
E ⊆ V × V dargestellt.
Eine gerichtete Kante (i, j) führt von i nach j.
Wann ist T ein Baum?
I
T muss genau einen Knoten r besitzen, in den keine Kante
hineinführt. r heißt die Wurzel von T .
I
In jeden Knoten darf höchstens eine Kante hineinführen,
I
und jeder Knoten muss von der Wurzel aus erreichbar sein.
Eine disjunkte Vereinigung von Bäumen heißt Wald.
Mariano Zelke
Datenstrukturen
5/32
Zentrale Begriffe
I
Wenn (v , w ) eine Kante ist, dann nennen wir v den Vater von w
und sagen, dass w ein Kind von v ist.
I
I
Ausgangsgrad (v ) ist die Anzahl der Kinder von v .
I
I
I
Einen Knoten b mit Ausgangsgrad (b) = 0 bezeichnen wir als Blatt.
T heißt k-när, falls der Ausgangsgrad aller Knoten höchstens k ist.
Für k = 2 sprechen wir von binären Bäumen.
Ein Weg von v nach w ist eine Folge (v0 , . . . , vm ) von Knoten mit
v0 = v , vm = w und (vi , vi+1 ) ∈ E für alle i (0 ≤ i < m).
I
I
I
I
I
Die anderen Kinder von v heißen Geschwister von w .
v ist ein Vorfahre von w und w ein Nachfahre von v .
Die Länge des Weges ist m, die Anzahl der Kanten des Weges.
Tiefe(v ) ist die Länge des (eindeutigen) Weges von r nach v .
Höhe(v ) ist die Länge des längsten Weges von v zu einem Blatt.
T heißt geordnet, falls für jeden Knoten eine Reihenfolge der Kinder
vorliegt.
Mariano Zelke
Datenstrukturen
6/32
Operationen auf Bäumen (als Datenstrukturen)
(1) Wurzel: Bestimme die Wurzel von T .
(2) Vater(v ): Bestimme den Vater des Knoten v in T .
Wenn v = r , dann ist der Null-Zeiger auszugeben.
(3) Kinder(v ): Bestimme die Kinder von v . Wenn v ein Blatt ist, dann
ist der Null-Zeiger als Antwort zu geben.
(4) Für binäre geordnete Bäume:
(4a) LKind(v ): Bestimme das linke Kind von v .
(4b) RKind(v ): Bestimme das rechte Kind von v .
(4c) Sollte das entsprechende Kind nicht existieren, ist der Null-Zeiger als
Antwort zu geben.
(5) Tiefe(v ): Bestimme die Tiefe von v .
(6) Höhe(v ): Bestimme die Höhe von v .
(7) Baum(v , T1 , . . . , Tm ): Erzeuge einen geordneten Baum mit Wurzel v
und Teilbäumen T1 , . . . , Tm .
(8) Suche(x): Bestimme alle Knoten mit Wert x. (Nur sinnvoll, wenn
Knoten einen Wert speichern.)
Mariano Zelke
Datenstrukturen
7/32
Implementierung mittels Vater-Array
Annahme: Jeder Knoten besitzt eine Zahl aus {1, . . . , n} als Namen und
zu jedem i ∈ {1, . . . , n} gibt es genau einen Knoten mit Namen i.
I
Ein Integer-Array Parent speichert für jeden Knoten v den Namen
des Vaters von v :
Parent[v ] = Name des Vaters von v .
Wenn v = r , dann setze Parent[v ] = r .
The good and the bad
schnelle Vater-Bestimmung (Zeit = O(1)).
schnelle Bestimmung der Tiefe von v (Zeit = O(Tiefe(v ))).
minimaler Speicherplatzverbrauch: Bäume mit n Knoten benötigen
Speicherplatz n
für die Bestimmung der Kinder muss der gesamte Baum durchsucht
werden
Mariano Zelke
Datenstrukturen
8/32
Beispiel für Vater-Array
5
4
2
6
7
1
3
8
9
10
Knotennr.
Vater
Mariano Zelke
1 2 3 4 5 6 7 8 9 10
2 4 2 5 5 5 6 6 8 1
Datenstrukturen
9/32
Die Adjazenzlisten-Implementierung
Für Bäume mit n Knoten benutze ein Kopf-Array“ Head mit den Zellen
”
1, . . . , n: Head[i] ist ein Zeiger auf die Liste der Kinder von i.
1
2
3
4
5
6
7
8
9
10
10
1
2
4
7
9
5
3
4
6
8
2
1
6
7
3
8
9
10
Head
Mariano Zelke
Datenstrukturen
10/32
Stärken und Schwächen
Die Kinderbestimmung gelingt schnellstmöglich, in Zeit
O(Anzahl der Kinder).
Höhe (v ) wird auch angemessen unterstützt mit der Laufzeit
O(Anzahl der Knoten im Teilbaum mit Wurzel v ). Warum?
Für die Bestimmung des Vaters muss möglicherweise der gesamte
Baum durchsucht werden! Auch die Bestimmung der Tiefe ist
schwierig, da der Vater nicht bekannt ist.
Speicherplatzverbrauch: Für Bäume mit n Knoten benötigen wir
I
I
2n − 1 Zeiger: einen Zeiger für jede der n Zellen von Head und einen
Zeiger für jede der n − 1 Kanten
und 2n − 1 Zellen: n Zellen für das Array Head und n − 1 Zellen für
die n − 1 Kanten.
Zu großer Speicherplatz. Aber: die Adjazenzlisten-Darstellung kann
auch für Graphen benutzt werden.
Mariano Zelke
Datenstrukturen
11/32
Die Binärbaum-Implementierung
Ein Knoten wird durch die Struktur
typedef struct Knoten {
int wert;
Knoten *links, *rechts;
};
dargestellt.
I
Wenn der Zeiger z auf die Struktur des Knoten v zeigt,
I
I
I
I
dann ist z → wert der Wert von v und
z → links (bzw. z → rechts) zeigt auf die Struktur des linken
(bzw. rechten) Kindes von v .
Der Zeiger wurzel zeige auf die Struktur der Wurzel des Baums.
Im Vergleich zur Adjazenzlisten-Darstellung:
I
I
Mariano Zelke
Ähnliches Laufzeitverhalten bei den Operationen
aber bessere Speichereffizienz: 2n Zeiger (zwei Zeiger pro Knoten)
und n Zellen (eine Zelle pro Knoten).
Datenstrukturen
12/32
Beispiel Binärbaum-Implementierung
wurzel
wert: 5
*links *rechts
wert: 4
wert: 6
*links *rechts
*links *rechts
wert: 2
wert: 7
wert: 8
*links *rechts
*links *rechts
*links *rechts
wert: 1
wert: 3
wert: 9
*links *rechts
*links *rechts
*links *rechts
wert: 10
*links *rechts
Mariano Zelke
Datenstrukturen
13/32
Die Kind-Geschwister-Implementierung
Ein Knoten wird durch die Struktur
typedef struct Knoten {
int wert;
Knoten *LKind, *RGeschwister;
};
dargestellt.
I
Wenn der Zeiger z auf die Struktur des Knoten v zeigt,
I
I
I
I
dann ist z → wert der Wert von v und
z → LKind (bzw. z → RGeschwister) zeigt auf die Struktur des
am weitesten links stehenden Kindes (bzw. des rechten
Geschwisterknotens) von v .
Der Zeiger wurzel zeige wieder auf die Struktur der Wurzel des
Baums.
Im Vergleich zur Binärbaum-Darstellung:
I
I
Mariano Zelke
Ähnliches Laufzeitverhalten und ähnliche Speichereffizienz,
aber die Darstellung ist für alle Bäume und nicht nur Binärbäume
anwendbar!
Datenstrukturen
14/32
Beispiel Kind-Geschwister-Implementierung
wert: 5
*RGeschwister
*LKind
wurzel
wert: 4
*RGeschwister
*LKind
wert: 6
*RGeschwister
*LKind
wert: 2
*RGeschwister
*LKind
wert: 7
*RGeschwister
*LKind
wert: 8
*RGeschwister
*LKind
wert: 1
*RGeschwister
*LKind
wert: 3
*RGeschwister
*LKind
wert: 9
*RGeschwister
*LKind
wert: 10
*RGeschwister
*LKind
Mariano Zelke
Datenstrukturen
15/32
Suche in Bäumen: Postorder, Preorder und Inorder
Sei T ein geordneter Baum mit Wurzel r und Teilbäumen T1 , . . . , Tm .
I
Postorder: Durchlaufe rekursiv die Teilbäume
T1 , . . . , Tm nacheinander. Danach wird die Wurzel r besucht.
I
Preorder: besuche zuerst r und danach durchlaufe rekursiv die
Teilbäume T1 , . . . , Tm .
I
Inorder: Durchlaufe zuerst T1 rekursiv, besuche dann die Wurzel r
und durchlaufe letztlich die Teilbäume T2 , . . . , Tm rekursiv.
void preorder (Knoten *p){
Knoten *q;
if (p != null) {
cout << p->wert;
for(q = p->LKind; q != null; q = q->RGeschwister)
preorder(q);
}
}
Mariano Zelke
Datenstrukturen
16/32
Welcher Knoten wird direkt nach v besucht?
Postorder:
I
das linkeste Blatt im rechten Nachbarbaum.
I
Wenn v keinen rechten Geschwisterknoten besitzt, dann wird der
Vater von v als nächster besucht.
Preorder:
I
das linkeste Kind von v .
I
Wenn v ein Blatt ist, dann das erste nicht-besuchte Kind des
tiefsten, nicht vollständig durchsuchten Vorfahren von v
Inorder:. . . ?
Wenn wir zusätzliche Zeiger gemäß einer dieser Reihenfolgen einsetzen,
dann können wir Rekursion vermeiden: besuche die Knoten gemäß der
neuen Zeiger (zusätzlicher Verwaltungsaufwand).
Die Datenstrukturen heißen dann postorder-, preorder-, inorder-gefädelte
Bäume
Mariano Zelke
Datenstrukturen
17/32
Eine nicht-rekursive Preorder-Implementierung
Der Teilbaum mit Wurzel v ist in Preorder-Reihenfolge zu durchlaufen.
(1) Wir fügen einen Zeiger auf die Struktur von v in einen anfänglich
leeren Stack ein.
(2) Solange der Stack nicht leer ist, wiederhole:
(a) Entferne das erste Stack-Element w mit Hilfe der Pop-Operation.
Besuche w .
(b) Füge die Kinder von w in umgekehrter Reihenfolge in den Stack ein.
/∗ Durch die Umkehrung der Reihenfolge werden die Bäume später
in ihrer natürlichen Reihenfolge abgearbeitet. ∗ /
Die Laufzeit ist linear in der Knotenzahl n.
I
Jeder Knoten wird genau einmal in den Stack eingefügt.
I
Insgesamt werden also höchstens O(n) Stackoperationen
durchgeführt. Stackoperationen dominieren aber die Laufzeit.
Mariano Zelke
Datenstrukturen
18/32
Laufzeit
Für einen Baum mit n Knoten, der in Kind-Geschwister-Darstellung,
Adjazenzlisten- oder Binärbaum-Darstellung vorliegt, können die
Reihenfolgen Postorder, Preorder bzw. Inorder in Zeit O(n) berechnet
werden.
Mariano Zelke
Datenstrukturen
19/32
Graphen
(Link)
(a) Ein ungerichteter Graph G = (V , E ) besteht aus einer endlichen
Menge
und einer Teilmenge
V von Knoten (engl.: vertices)
E ⊆ {u, v } u, v ∈ V , u 6= v von Kanten (engl.: edges).
I
I
Die Endpunkte u, v einer ungerichteten Kante {u, v } sind
gleichberechtigt.
u und v heißen dann Nachbarn.
Wir sagen auch: u und v sind adjazent.
(b) Für die Kantenmenge E eines gerichteten Graphen G = (V , E ) gilt
E ⊆ {(u, v ) | u, v ∈ V , u 6= v }.
I
I
Mariano Zelke
Der Knoten u ist Anfangspunkt und der Knoten v Endpunkt der
Kante (u, v ).
v heißt auch ein direkter Nachfolger von u und u ein direkter
Vorgänger von v .
Datenstrukturen
20/32
Warum Graphen?
Graphen modellieren
I
das World Wide Web: Die Knoten entsprechen Webseiten, die
(gerichteten) Kanten entsprechen Hyperlinks.
I
Rechnernetzwerke: Die Knoten entsprechen Rechnern, die
(gerichteten und/oder ungerichteten) Kanten entsprechen
Direktverbindungen zwischen Rechnern.
Das Schienennetz der Deutschen Bahn: Die Knoten entsprechen
Bahnhöfen, die (ungerichteten) Kanten entsprechen
Direktverbindungen zwischen Bahnhöfen.
I
I
Bei der Erstellung von Reiseplänen müssen kürzeste (gewichtete)
Wege zwischen einem Start- und einem Zielbahnhof bestimmt
werden.
I
Schaltungen: die Knoten entsprechen Gattern, die (gerichteten)
Kanten entsprechen Leiterbahnen zwischen Gattern.
I
...
Mariano Zelke
Datenstrukturen
21/32
Das Königsberger Brückenproblem
1
2
3
4
Mariano Zelke
Datenstrukturen
22/32
Das Königsberger Brückenproblem
Gibt es einen Rundweg durch Königsberg, der alle Brücken über
den Pregel genau einmal überquert?
Enthält dieser Graph einen Weg,
der alle Kanten genau einmal
enthält und zum Startpunkt zurückkehrt?
a
1
b
2
c
f
3
d
e
g
4
Mariano Zelke
Datenstrukturen
23/32
Euler-Kreis
Ein Euler-Kreis beginnt in einem Knoten v , durchläuft alle Kanten genau
einmal und kehrt dann zu v zurück.
Enthält dieser Graph einen Euler-Kreis?
Nein, denn dazu müsste jeder
Knoten eine gerade Anzahl
von Nachbarn haben.
1
a
b
2
c
f
3
d
e
g
4
Mariano Zelke
Datenstrukturen
24/32
Euler-Kreis
Ein Euler-Kreis beginnt in einem Knoten v , durchläuft alle Kanten genau
einmal und kehrt dann zu v zurück.
Enthält dieser Graph einen Euler-Kreis?
Nein, denn dazu müsste jeder
Knoten eine gerade Anzahl
von Nachbarn haben.
1
a
b
2
c
f
3
d
Die Existenz von
Euler-Kreisen ist
auch für gerichtete
Graphen interessant.
Mariano Zelke
e
g
4
Datenstrukturen
25/32
Wichtige Begriffe
Sei G = (V , E ) ein gerichteter oder ungerichteter Graph.
I
Eine Folge (v0 , v1 , ..., vm ) heißt ein Weg in G ,
falls für jedes i (0 ≤ i < m) gilt
I
I
(vi , vi+1 ) ∈ E (für gerichtete Graphen) bzw.
{vi , vi+1 } ∈ E (für ungerichtete Graphen).
Die Weglänge ist m, die Anzahl der Kanten. Ein Weg heißt einfach,
wenn kein Knoten zweimal auftritt.
I
Ein Weg heißt ein Kreis, wenn v0 = vm und (v0 , ..., vm−1 ) ein
einfacher Weg ist. G heißt azyklisch, wenn G keine Kreise hat.
I
Ein ungerichteter Graph heißt zusammenhängend, wenn je zwei
Knoten durch einen Weg miteinander verbunden sind.
Mariano Zelke
Datenstrukturen
26/32
Topologisches Sortieren
Es sind n Aufgaben a0 , . . . , an−1 auszuführen. Allerdings gibt es eine
Menge P von p Prioritäten zwischen den einzelnen Aufgaben.
Die Priorität (i, j) impliziert, dass Aufgabe ai vor Aufgabe aj
ausgeführt werden muss.
Das Ziel ist die Erstellung einer Reihenfolge, in der alle Aufgaben
ausgeführt werden können, bzw. festzustellen, dass eine solche
Reihenfolge nicht existiert.
I
Eine graphentheoretische Formulierung:
I
I
I
Wähle V = {0, . . . , n − 1} als Knotenmenge.
Wir verabreden, dass Knoten i der Aufgabe ai entspricht.
Wir setzen genau dann eine Kante von i nach j ein,
wenn (i, j) eine Priorität ist.
Wie ist das Ziel zu formulieren?
Bestimme eine Reihenfolge v1 , . . . , vi , . . . , vn der Knoten, so dass es
keine Kante (vi , vj ) mit j < i gibt.
Mariano Zelke
Datenstrukturen
27/32
Die Idee
Eine Aufgabe aj kann als erste Aufgabe ausgeführt werden, wenn es
keine Priorität der Form (i, j) in P gibt.
I
Ein Knoten v von G heißt eine Quelle, wenn Eingangsgrad(v ) = 0
ist, wenn v also kein Endpunkt einer Kante ist.
I
Also bestimme eine Quelle v , führe v aus und entferne v zusammen
mit allen von v ausgehenden Kanten.
Wiederhole dieses Verfahren, solange G noch Knoten besitzt:
I
bestimme eine Quelle v , führe v aus und entferne v zusammen mit
allen von v ausgehenden Kanten.
Welche Datenstrukturen sollten wir verwenden?
Mariano Zelke
Datenstrukturen
28/32
Ein erster Versuch
Wir verketten alle p Kanten in einer Liste Priorität“ und benutzen ein
”
Integer-Array Reihenfolge“ sowie zwei boolesche Arrays Erster“ und
”
”
Fertig“ mit jeweils n Zellen.
”
Setze Zähler = 0. Für alle i setze Fertig[i] = falsch.
Wiederhole n Mal:
(0) Durchlaufe das Array Erster. Setze Erster[i] = wahr genau dann,
wenn Fertig[i] = falsch.
(1) Durchlaufe die Liste Priorität. Wenn Kante (i, j) angetroffen
wird, setze Erster[j] = falsch.
(2) Bestimme das kleinste j mit Erster[j] = wahr. Danach setze
(a) Fertig[j] = wahr,
(b) Reihenfolge[Zähler++] = j (Aufgabe j wird ausgeführt)
(c) und durchlaufe die Prioritätsliste: entferne jede Kante (j, k), da aj
eine Ausführung von Aufgabe ak nicht mehr behindert.
Mariano Zelke
Datenstrukturen
29/32
Eine Laufzeitanalyse
I
Was kostet besonders viel Laufzeit?
I
I
I
I
In jeder Iteration muss die Liste Priorität vollständig durchlaufen
werden: Zeit = O(p).
Weiterhin muss das Array Erster jeweils initialisiert werden:
Zeit = O(n).
Die Laufzeit pro Iteration ist dann durch O(p + n) beschränkt. Die
Gesamtlaufzeit ist O(n · (p + n)), da wir n Iterationen haben.
Was können wir verbessern?
I
I
Wir müssen nur die in j beginnenden Kanten entfernen.
Warum kompliziert nach der ersten ausführbaren Aufgabe suchen?
Eine vor aj nicht in Betracht kommende Aufgabe ak wird nur
interessant, wenn (j, k) eine Priorität ist.
Mariano Zelke
Datenstrukturen
30/32
Der zweite Versuch
Stelle die Prioritäten durch eine Adjazenzliste mit dem Kopf-Array
Priorität dar. Benutze ein Array Eingangsgrad mit Eingangsgrad[v ] = k,
falls v Endpunkt von k Kanten ist. Benutze eine Queue Schlange.
(1) Initialisiere die Adjazenzliste Priorität durch Einlesen aller
Prioritäten. (Zeit = O(n + p)).
(2) Initialisiere das Array Eingangsgrad. (Zeit = O(n + p)).
(3) Alle Knoten v mit Eingangsgrad[v ] = 0 werden in Schlange
gestopft. (Zeit = O(n)).
(4) Setze Zähler = 0; Wiederhole solange, bis Schlange leer ist:
(a) Entferne einen Knoten i aus Schlange.
(b) Setze Reihenfolge[Zähler++] = i.
(c) Durchlaufe die Liste Priorität[i] und reduziere den Eingangsgrad
für jeden Nachfolger j von i um 1. Wenn jetzt Eingangsgrad[j] = 0,
dann stopfe j in Schlange: Aufgabe aj ist jetzt ausführbar.
Mariano Zelke
Datenstrukturen
31/32
Die Analyse
I
Die Initialisierungen laufen in O(n + p) Schritten ab.
I
Ein Knoten wird nur einmal in die Schlange eingefügt. Also
beschäftigen sich höchstens O(n) Schritte mit der Schlange.
Eine Kante (i, k) wird, mit Ausnahme der Initialisierungen, nur dann
inspiziert, wenn i aus der Schlange entfernt wird.
I
I
I
Jede Kante wird nur einmal angefasst“
”
und höchstens O(p) Schritte behandeln Kanten.
Das Problem des topologischen Sortierens wird für einen Graphen mit n
Knoten und p Kanten in Zeit O(n + p) gelöst.
Schneller geht’s nicht.
Mariano Zelke
Datenstrukturen
32/32
Herunterladen