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