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