AuD – Zusammenfassung 2. Listen (= Folge von Daten → Lineare Anordnung von Einträgen) – Operationen: – Durchlaufen, Aufzählen oder Suchen von Einträgen – Einfügen und Löschen – – Anwendungen: List (mit head und tail), Stack, Queue Implementierung als: Felder (arrays), verkettete Listen (linked lists) 2.1 Listen als Felder Attribute: – t[] data – int capacity – int size O(1) Operationen + mittlerer Aufwand: – T at(int i) – reserve(int n) – resize(int n) – push_back(T obj) – insert(int i, T obj) – pop_back() – erase(i) speichert Daten als Feld Länge des Feldes data (= momentan verfügbarer Speicher) Anzahl Einträge in Vector (= momentan benötigter Speicher) Zugriff auf Eintrag i reserviert Einträge/ erhöht capacity erweitert oder schrumpft Vector fügt eintrag am Ende ein fügt Eintrag an Postion i ein löscht den letzten Eintrag löscht Eintrag i O(1) O(1) O(n) O(1) O(n) 2.2 Einfach verkettete Listen – – Referenzen auf den folgenden Eintrag (= Verketten) Jeder Eintrag wird repräsentiert als Knoten (node) mit den Attributen – T data Refernz auf Datum (eigentlicher Eintrag) – Node<T> next Referenz auf folgenden Knoten (null kennzeichnet Ende) 2.3 Doppelt verkettete Listen (doubly linked lists) – zusätzliches Attribut pro Knoten: – Node<T> prev Referenz auf vorhergehenden Knoten (previous) (null kennzeichnet Anfang) – Liste Dlist<T> hält Referenzen auf – ersten (head) und – letzten (tail) Eintrag – Mittlerer Aufwand: – size – front/back – at – push_front/push_back – insert – pop_front/pop_back – erase – – O(n) O(1) O(n) O(1) O(n) O(1) O(n) Einfügen/Löschen = Umsetzen der prev und next Referenzen Sonderfälle für erstes und letztes Element: – Muss head und tail ändern 2.4 Iteratoren = abstraktes Konzept zum Traversieren von Daten 3. Bäume 3.1 Begriffe zu Bäumen – – – – – – – – – – – Knoten (node) → Unterscheidung in intene und externe Knoten (Blätter) Wurzel (root) = oberster Knoten, hat kein Elternteil Kante (edge) → verbinden Knoten Baum = zusammenhängender, zyklenfreier Graph (wachsen in der Informatik von oben nach unten) Kindknoten (child) Elternknoten (parent) → bis auf die Wurzel hat jeder Knoten genau einen Elternknoten Teilbaum (subtree) → jeder Knoten definiert einen Teilbaum Blatt (leaf) → haben keine Kinder Pfad (path) = Folge von durch Kanten verbundene Knoten (dabei keine doppelten Knoten), zu jedem Knoten existiert genau ein Pfad von der Wurzel Niveau/ Ebene (level) Höhe (height) = maximales Niveau + 1 bzw. Anzahl Ebenen 3.2 Binärbaum Definition: Ein Baum, bei dem jeder Knoten höchstens zwei Kindknoten hat, heißt Binärbaum Definition vollständiger Binärbaum: Ein Binärbaum heißt voll, wenn jeder Knoten entweder 0 (Blätter) oder 2 (innerer Knoten) Kinder hat. Ein voller Binärbaum heißt vollständig, wenn alle Blätter auf derselben Ebene liegen. → Ein vollständiger Binärbaum der Höhe n hat 2^n – 1 Knoten 3.3 Baumdarstellung von Termen – Operatoren verknüpfen Teilausdrücke – Operatoren in internen Knoten – Konstanten in Blättern – höchstens 2-stellige (binäre) Operatoren, z.B. X + Y – Keine Klammerung nötig 3.3.1 – – Traversieren von Binärbäumen Berechnung des Wertes erfordert ein Durchlaufen des Baums Ebenso Anzeige des dargestellten Terms in Textform: – (gewohnte) Infix-Notation – Umgekehrte Polnische Notation (Postfix- Notation) – Polnische Notation (Prefix-Notation) rekursiver Durchlauf: – Zum Berechenen des Werts ist nötig: – Berechne linken Teilbaum → Rekursion – Berechne rechten Teilbaum → Rekursion – Verknüpfe Ergebnisse gemäß Operator – – preorder traversal – Gib aktuellen Knoten aus (A) – Traversiere rekursiv linken Teilbaum (B)* – Traversiere rekursiv rechten Teilbaum (C)* *falls vorhanden Ausgabe: A, B, C x+y xy+ +xy – – – – inorder traversal – traversiere rekursiv linken Teilbaum (B)* – Gib aktuellen Knoten aus (A) – traversiere rekursiv rechten Teilbaum (C)* Ausgabe: B, A, C postorder traversal – Traversie rekursiv linken Teilbaum (B)* – Traversiere rekursiv rechten Teilbaum (C)* – Gib aktuelle Knoten aus (A) Ausgabe: B, C, A Beispiel: preorder: +, *, 2, 5, *, +, 2, 1, 3 inorder: 2, *, 5, +, 2, +, 1, *, 3 postorder: 2, 5, *, 2, 1, +, 3, *, + – – – (Prefix-Notation, Polnische Notation), Immer Wurzel zuerst (Infix-Notation), Klammerung fehlt → falsche Interpretation (Postfix-Notation, Umgekehrte Polnische Notation), Immer Wurzel zuletzt Preorder und postorder erlauben Rekonstruktion des Baums – Preorder: Wurzel, linkes K, rechtes K top-down – Postorder: Linkes K, rechtes K, Wurzel bottom-up Inorder-Darstellung nicht eindeutig: Benötigt explizite Klammerung level-order: – Durchlauf der einzelnen Ebenen von links nach rechts – z.B. für vollständigen Binärbaum der Höhe n – 2^k Knoten auf Ebene k → insgesamt 2^n – 1 3.4 Syntaxanalyse und Syntaxbaum – Syntaxanalyse: – Analyse von (hierarchisch) strukturierten Daten – Struktur ist definiert durch ein Regelwerk, die Grammatik – Ergebnis ist ein Syntaxbaum – Beispiel: arithmetische Ausdrücke – Eingabe: Arithmetischer Ausdruck, z.B. 2 * (3 + 1) – Ausgabe: Baumdarstellung – Vorgehensweise: 1. Vorverarbeitungsschritt: lexikalische Analyse zerlegt Eingabe in Sequenz von Einzelteilen (tokens), hier: Zahlen, Operatoren und Begrenzer (Klammern) 2. Syntaxanalyse durch Parser (to parse = zerlegen): Wir betrachten einen rekursiven top-down Ansatz – Grammatik: – definiert „Sprache“ – z.B. Backus-Naur-Form 3.5 Level-order Traversierung Ausgabe: +, *, *, 2, 5, +, 3, 2, 1 → nicht rekursiv möglich 4. Suchbäume – – – Wichtigste Anwendung von Bäumen: effiziente Suche Knoten speichern Schlüsselwerte Unterstützte Operationen: – Suchen/Finden, Einfügen, Löschen 4.1 Binäre Suchbäume – – – Knoten X hält Schlüsselwert key(X) Baumstruktur definiert Ordnung: – Alle Schlüsselwerte im – linken Teilbaum left(X) sind kleiner als key(X) – rechten Teilbaum right(X) sind größer als key(X) Binäre Suche → Abstieg im Baum – Beginnend von der Wurzel – Rekursiv oder iterativ Beispiel: – erfolgreiche Suche nach 6 6 < 7 → links → 4 < 6 → rechts → 6 = 6 – erfolglose Suche nach 10 7 < 10 → rechts → 9 < 10 → rechts → null – – – – Minimum: Knoten links unten Maximum: Knoten rechts unten Oft ist man nur an einem Bereich(range) interessiert Bereich = sortierte Teilfolge im Interval [X Beginn , X Ende] Einfügen eines neuen Eintrages mit Schlüssel k: 1. Erfolglose Suche → letzter besuchter Knoten X (X hat kein linkes und/oder rechtes Kind!) 2. Erzeuge neuen Knoten Y mit Schlüssel k 3. Einhängen von Y unter X als linkes (k < key(X)) bzw. rechtes (key(X) < k) Kind Hilfsknoten (dummy nodes) – Künstlicher Wurzelknoten head mit – Ersetze null (kein Knoten) durch Pseudoknoten nil → Alles Knoten außer nil sind interne Knoten Löschen Knoten X (Löschen = Hochziehen eines Teilbaums). 3 Fälle: a) X ist Blatt ↔ X hat kein Kind – Trivial: entferne Knoten X b) X hat ein Kind – Ersetze Knoten X durch dessen Kind Y (=Teilbaum) – Hochziehen von Y – Hier analog zum Löschen aus ein verketteten Liste c) X hat zwei Kinder – Seien Teilbäume L und R linkes und rechtes Kind von X 1. Suche Knoten M in R, der am weitesten links steht (Minimum) Seien P Vater und MR rechtes Kind von M 2. Ersetze X durch M 3. Setze L als linkes Kind und R als rechtes Kind von M 4. Setze MR als linkes Kind von P ein Löschen der Wurzel – Probelm: Wurzel hat keinen Vater – Wurzel wird verwendet, um Baum zu referenzieren – Kein Problem, falls Hilfsknoten head verwendet wird – head ist eigentliche Wurzel, kann aber nicht gelöscht werden – Referenz auf head ist immer möglich – head kann immer als Elternknoten genutzt werden – Ohne Hilfsknoten muss Speziallfall betrachtet werden 4.2 Balancierte Bäume – Balance/Ausgleich → minimiere Höhe 4.2.1 AVL-Bäume (Binärbaum) Definition: Ein binärer Baum heißt AVL-Baum wenn die Höhen von linken und rechten Teilbäumen sich höchstens um 1 unterscheiden. (AVL-Kriterium) bal(T) = h(Tr) – h(Tl) (Höhe des rechten Teilbaumes minus Höhe des linken Teilbaumes) Einfügen in AVL-Baum: → kann AVL-Eigenschaft verletzen – Wiederherstellen der AVL-Eigenschaft mit: – Rotation und Doppelrotation oder – tri-node restructuring Fallunterscheidungen beim (lokalen) Balancieren – – T ist linkslastig: bal = -2 – linker Teilbaum von T ist rechtslastig (LR) – sonst (LL) T ist rechtslastig: bal = +2 – Rechter Teilbaum von T ist linkslastig (RL) – sonst (RR) 4.2.2 – – → 2x Rotation L/R → Rotation rechts → 2x Rotation R/L → Rotation links 2-3-4-Bäume(kein Binärbaum) Jeder interne Knoten hat 2, 3 oder 4 Kinder Jeder innere Knoten hält 1,2 oder 3 Schlüssel – – – sortierte Liste von Schlüsseln k1 < … < kn definiert Ordnung von Teilbäumen Suche analog zu binären Suchbäumen Einfügen benötig split: 1. Eigntlicher split: Reihenfolge der Teilbäume t0, t1, t2, t3 bleibt erhalten 2. Neuer Wurzelknoten b wird in Elternknoten hochgezogen – Verschmelzen (merge) mit Elternknoten – Fallunterscheidung über Grad des Elternknotens Einfügen in 2-3-4-Baum: – bottum-up – Erfolglose Suche bis zu einem Blatt – 1. Fall: Einfügen in 2- oder 3- Knoten → Es entsteht ein 2- oder 3-Knoten – 2. Fall: Einfügen in 4-Knoten – Teile 4-Knoten (split) → mittlerer Eintrag wird hochgezogen – Dadurch kann ein split des Elternknotens nötig werden → Solange nötig rekursiv fortsetzen – ggf. bis zur Wurzel – Einfügen im entsprechenden Blattknoten (2- oder 3-Knoten) – Splits erfolgen bottum-up von „unten nach oben“ – top-down – Erfolglose Suche bis zu einem Blatt – Teile dabei im Abstieg alles besuchten 4-Knoten (split) → Einfügen erfolgt garantiert in 2- oder 3-Knoten Elternknoten zu split kann kein 4-Knoten sein – Füge Eintrag in Blattknoten ein → Es entsteht ein 3- oder 4-Knoten – Splits erfolgen top-down von „oben nach unten“ 4.2.3 Rot-Schwarz-Baum (Binärbaum) Definition: Ein Rot-Schwarz-Baum (red-black-tree) ist ein binärer Suchbaum, in dem jdem Knoten ein Farbattribut zugeordnet ist, so dass gilt 1. Ein Knoten ist entweder rot oder schwarz 2. Die Wurzel ist schwarz* 3. Alle Blätter sind schwarz** 4. Beide Kinder eines roten Knotens sind schwarz 5. Jeder Pfad von einem gegebenen Knoten zu einem erreichbaren Blatt enthält dieselbe Anzahl schwarzer Knoten *Man kann die Wurzel immer umfärben **Annahme: Blätter sind Null-Knoten/dummies, d.h. nur interne Knoten halten Schlüssel Einfügen in Rot-Schwarz-Baum: – Analog zu 2-3-4-Bäumen Fallunterscheidung in der Praxis: – Farben bestimmen Fälle, keine Abbildung auf 2-3-4-Baum – Füge neuen roten Knoten n ein – Bezeichnungen: parent p, grandparent g, uncle u Alternative: (bottum-up) tri-node restructuring – Füge neuen roten Knoten ein – Fals rot-rot: Restrukturierung oder Umfärben – Umfärben kann höher rot-rot erzeugen → Rekursion Eigenschaften von AVL- und Rot-Schwarz-Bäumen: – Baum mit n Einträgen benötigt Speicherplatz in O(n) – Suche in O(log n) – Einfügen in O(log n) – Löschen in O(log n) AVL-Baum vs. Rot-Schwarz-Baum – Suche in AVL-Bäumen ist effizienter, da geringere Höhe – AVL: h(n) < 1,44 log(n + 2) – 1 – Rot-Schwarz h(n) < 2 log(n + 2) – Einfügen in Rot-Schwarz-Bäume ist effizienter möglich 4.2.4 B-Baum Definition: Ein Baum heißt B-Baum, wenn er für m > 0 folgende Eigenschaften erfüllt: 1. Jeder Knoten hat höchstens 2m + 1 Kinder 2. Jeder Knoten hat mindestens m + 1 Kinder – mit Ausnahme der Wurzel: sie hat mindestens 2 Kinder oder ist ein Blatt 3. Jeder innere Knoten mit k Schlüsseln hat k + 1 Kinder 4. Alle Blätter liegen auf der gleichen Ebene – – – – Suche – Beginne bei Wurzel – Suche Schlüssel k im Knoten – Ggf. rekursiver Abstieg Einfügen – Erfolglose Suche bis zu einem Blattknoten – 1. Fall: Knoten hat < 2m Einträge → in Liste einsortieren – 2. Fall: Knoten hat = 2m Einträge → erzeuge neuen Knoten – Linke Hälfte der Einträge (m Stück) bleibt – Rechte Hälfte wird in neuen Knoten verschoben – Mittlerer Eintrag wird in Elternknoten eingefügt – Einfügen setzt sich ggf. rekursiv bis zur Wurzel fort B-Bäume wachsen an der Wurzel. (Löschen: B-Bäume schrumpfen an der Wurzel) → Rekursion 5. Heaps Definition: Ein Binärbaum ist ein Heap, wenn für jeden Knoten X (außer der Wurzel gilt) X >= parent(X) – – – – – Genauer: binärer Min-Heap (analog Max-H. X <= parent(X)) (Es gibt Varianten, die kein Binärbäume sind) Heap-Eigenschaft definiert Teilordnung – Elternknoten <= Kinder, aber – Kinder in beliebiger Reihenfolge Effizienter Zugriff auf kleinsten Eintrag (=Wurzel) in O(1) Zusätzliche Struktureigeenschaft (Balance) – Alle Ebenen bis auf die letzte sind vollständig gefüllt – Die letzte Ebene ist linksbündig gefüllt Einfügen von X in Heap – Einfügen als neuen Blattknoten – Erhalte dabei Struktureigeenschaft „linksbündig“ → an+1 ← x – Heap-Eigenschaft wieder herstellen; – 1. Beginne mit neuem Knoten X – 2. Solange Wert des Vaterknotens px > x Vertausche Werte von px und x Weiter mit Überprüfung von Vaterknoten: x ← px – Eingefügter Knoten wandert solange nach oben, bis die Heap-Eigenschaft wieder gilt – Maximaler Aufwand ist O(log n), da Binärbaum balanciert ist Entfernen des kleinsten Eintrags: – Lösche Wurzel – Ersetze Wurzel durch Eintrag rechts unten – Heap schrumpft um letzten Eintrag – Heap-Eigenschaft wieder herstellen: – 1. Beginne mit neuer Wurzel w – 2. Solange Wert von w größer als kleinerer Wert der Kinder Vertausche Werte Wurzel ↔ Kind Weiter mit entsprechendem Kind als neuer Wurzel w – Knoten wandert nach unten, bis Heap-Eigenschaft wieder gilt – Maximaler Aufwand ist O(log n), da Binärbaum balanciert ist – Balance bleibt wieder erhalten Heap-Konstruktion: Top-down vs. Bottom-up – – Top-down: Erzeuge Binärbaum durch insert Bottom-up – Annahme: alle Teilbäume unter Ebene i sind bereits Heaps – Kann Heaps auf Ebene i durch downheap konstruieren – Starte mit vorletzter Ebene i = h – 1 → Annahme erfüllt 1. Konstruiere alle Heaps bis Ebene i mit downheap 2. Wiederholen für i:= i – 1 bis Wurzel erreicht wird → Verschmelzen von Heaps mittels downheap Aufwand: Mittel Schlechtester Fall Bottom-up O(n) O(n) Top-down O(n) O(n log n) Heapsort O(n log n) O(n log n) Heapsort: – arbeitet in-place – nicht stabil – Sortierreihenfolge beachten – Min-Heap → absteigend – Max-Heap → aufsteigend 6. Hashverfahren 6.1 Hashfunktionen – – – – Einfache Datenstruktur: Feld, Tabelle – Hashtabelle (auch Streuwerttabelle), hash table – to hash = zerhacken – Oft Bezeichnung bucket für Positionen i in a Bestimmung der Position eines Objekts durch Hashfunktion – Objekt als Paar von Schlüssel und Wert – Adressierung: Anwendung der Hashfunktion auf Schlüssel Eine Hashfunktion h(x) muss – konsistent sein – deterministisch sein Ein Hashfunktion soll möglichst – effizient berechenbar sein (in jedem Fall O(1)) – zu einer Gleichverteilung von Schlüsseln führen → möglichst wenige Kollisionen → ganze Tabelle ausnutzen (h surjektiv) Die Hashfunktion soll Daten möglichst gut streuen, d.h. Möglichst zufälliges (aber deterministisches) Verhalten 6.2 Kollisionsbehandlung Hashverfahren mit Verkettung (seperate chaining): – Jeder Tabelleneintrag (bucket) speichert Liste von Einträgen – i.d.R. Verkettete Liste – ebenso möglich: sortierte Liste oder Suchbaum – Einfügen: – Neuer Eintrag wird in die Liste eingefügt – Bei Kollision hatte die Liste schon mehr als einen Eintrag – Suchen: – Abbruch bei leerer Liste (oder z.B. null) → erfolglose Suche – Ansonsten wird Liste nach Eintrag durchsucht, dazu wird jeder Listeneintrag auf Gleichheit getestet – Diese sequentielle Suche kann immernoch erfolglos sein – Löschen: – Erfolgreiche Suche liefert Liste (und Listeneintrag) – Lösche Eintrag aus Liste – Eigenschaften von seperate chanining: – einfach zu implementieren – Einfügen in konstanter Zeit O(1) möglich – Reduziert Anzahl Vergleiche bei Suche im Mittel um Faktor m – Keine Sonderbehandlung von gelöschten Einträgen – Sei α = n/m Füllgrad (load factor) – Durchschnittliche Länge der bucket Listen ist α – Durchschnittlicher Aufwand für Suche 1 + α (erfolglos) und 1 + α/2 (erfolgreich) Offene Addressierung (open addressing) – Keine sekundäre Datenstruktur (Liste) – stattdessen – Suche nach einer alternativen „Adresse“ im Fall einer Kollision – Auswerten einer alternativen Hashfunktion – Iterativ bis (freier) Eintrag gefunden wird – Beispiele: Lineares Sondieren, Quadratisches Sondieren, Doppel-Hashing – Eignet sich besonders für statische Tabellen Lineares Sondieren (linear probing) – Falls Eintrag h(x) bereits besetzt ist, teste nacheinander die Einträge – – – – – h(x)+1, h(x)+2, . . . , h(x)+i, . . . solange bis ein freier Eintrag gefunden wurde → gehe zum nächsten Eintrag Allgemeiner: addiere im i. Versuch eine lineare Funktion c * i – h(x) +/- c * i Eigenschaften: – Sondieren beim Einfügen und beim Suchen – Einfügen nur für n < m möglich, bei n = m muss Tabelle vergrößert werden (rehashing) → wähle tabelle möglichst groß genug – Vorsicht beim Löschen – Löschen = Ersetzen durch einen speziellen Wert, der eine vormals belegte Position markiert – → Löschen von Einträgen reduziert die Kosten folgender Suchen nicht! (Im Gegensatz zur Verkettung) Problem: Lineares Sondieren neigt zu clustering („Klumpenbildung“) → schlechte Streuung, schlechte Ausnutzung der Tabelle – – – – Anzahl Sondierungen im Mittel – Erfolglos (1/2)(1+(1-a)-²) – Erfolgreich (1/2) (1+(1-a)-1 ) Einfügen und Suchen mit linearen Sondieren benötigt im Mittel weniger als 5 Sondierungen Je höher Füllstand, desto schlechter Such- und Einfügeverhalten Einfügen und Suchen im Mittel in O(1) Aber Aufwand für rehashing in O(n) !! Quadratisches Sondieren (quadratic probing) – Wie lineares Sondieren, aber quadratische Funktion – Ziel: clustering reduzieren – Fall Eintrag h(x) bereits besetzt ist, teste nacheinander die Einträge – – – solange bis ein freier Eintrag gefunden wird Allgemeiner: addiere quaratische Funktion c1i + c2i2 Anfällig für schlecht gewählte Tabellengröße m „Sekundärcluster“ (Ausbildung fester Muster) h(x)+1, h(x)+4, h(x)+9, . . . , h(x)+i2, . . . Doppel-Hashing (double hashing) – Ziele: Reduziere clustering effektiv – Verwende zweite Hashfunktion h2 != h – Falls Eintrag h(x) bereits besetzt ist, teste nacheinander die Einträge h(x) + i h2(x) , i = 1, 2, . . . – Anzahl Sondierungen im Mittel: – 1/ (1 – a) (erfolglos) – (1/a) * ln(1/(1 – a)) (erfolgreich) – Doppel-Hashing benötigt im Mittel weniger Sondierungen als lineares Sondieren Übersicht: – Typischerweise Suchen, Einfügen, Löschen in O(1) – Abhängig vom Füllgrad a – Einfügen im schlechtesten Fall in O(n) 7. Graphen – beliebige Netzwerke von Knoten (nodes) und Kanten (edges) – Listen und Bäume sind Spezialfälle Verschiedene Klassen von Graphen: – Ungerichtete Graphen (undirected graph) – Definition: Ein ungerichteter Graph ist ein geordnetes Paar mit – einer Menge von Knoten V und – einer Menge von Kanten E – Kanten sind ungerichtet (beide Richtungen) – Gerichtete Graphen (directed graph) – Definition: Ein gerichteter Graph ist ein geordnetes Paar G = (V,E) mit – einer Menge von Knoten V und – einer Menge von Kanten E – Kanten sind jetzt geordnete Paare, d.h. gerichtet – Schleifen sind erlaubt – Gewichtete Graphen (weighted graph) – Definition: Ein gewichteter Graph ist ein (gerichteter oder ungerichter) Graph G = (V,E) zusammen mit einer Funktion die jeder Kante ein Gewicht zuordnet Begriffe zu Graphen: – Teilgraph (subgraph) definiert durch Teilmengen von V und E – Grad (degree) eines Knoten – Anzahl der eingehenden Kanten in ungerichten Graphen – unterscheide indegree und outdegree in gerichteten Graphen – Ein gerichteter Graph ist symmetrisch, wenn zu jeder Kante auch die entgegengesetzte Kante existiert – – – – Eine Folge von Knoten die durch Kanten verbunden sind heißt – Pfad, wenn alle Knoten vi verschieden sind, oder – Zyklus Ein gerichteter Graph heißt zyklisch, wenn er einen Zyklus enthält – Ansonsten: azyklisch Multigraphen – Verbindung von zwei Knoten durch mehr als eine Kante Hypergraphen – Kante verbindet mehr als einen Knoten gleichzeitig 7.1 Datenstrukturen für Graphen Grundlegende Operationen auf Graphen: – Knoten einfügen/löschen – Kanten einfügen/löschen – Aufzählen aller Knoten/Kanten – Reihenfolge beliebig – Bestimmte Aufzählungen (traversals) – Entscheiden ob zwei Knoten durch eine Kante verbunden sind – Aufzählen aller Kanten, die... – von einem bestimmten Knoten ausgehen – zu einem bestimmten Knoten führen Kantenliste: – Speichere Kanten E als Liste – Beispiel: int[] edgelist = { 6,11,1,2,1,3,3,1,3,4,3,6,4,1,5,3,5,5,6,2,6,4,6,5 }; – – – – Erste zwei Einträge: Anzahl Knoten und Kanten Dann: Je zwei aufeinanderfolgende Einträge bezeichnen eine Kante Knoten werden durch Indices bezeichnet Knotenliste: – Speichere Nachbarschaft von Knoten, d.h. für gerichteten Graphen ausgehende oder eingehende Kanten – Beispiel: int[] nodelist = { 6,11,2,2,3, 0, 3,1,4,6, 1,1,2,3,5, 3,2,4,5 }; – Erste zwei Einträge: Anzahl Knoten und Kanten – Nächste Zahl speichert Anzahl ausgehende Kanten – Es folgt Liste der Nachbarknoten – ODER: Aufteilung in: – Nachbarschaften für alle Knoten – Liste der Startpositionen in Nachbarschaftsliste – int[] neighborhoods = { 2,3, 1,4,6, 1, 3,5, 2,4,5 }; – int[] nodeNhd = { 0, 2, 2, 5, 6, 8, 11 }; Eigenschaften von Kanten- und Knotenlisten: – Eignen sich v.a. Für nicht veränderbare Graphen, z.B. – bestehendes Straßennetz – Abspeichern von Graphen (etwa als Dateiformat) Adjazenzmatrizen: – Zeile i = Index des Startknotens → liefert von i ausgehende Kanten – Spalte j = Index des Endknotens → liefert in j eingehende Kanten – ist symmetrisch für – ungerichtete Graphen – symmetrische (gerichtete) Graphen Adjazenzliste: – Motivation – speichere Nachbarschaften ähnlich wie in Knotenlisten, aber ermögliche dynamische Änderungen – Speichere Zeilen der Adjazenzmatrix ohne Nulleinträge – Beispiel: – 1→2→3 – 2 – 3→1→4→6 – 4→1 – 5→3→5 – 6→2→4→5 Vergleich der Darstellungen: 8. Algorithmen auf Graphen 8.1 Durchlaufen/Durchsuchen Durchlaufen (graph traversal): – Verallgemeinerung von Bäumen auf Graphen – Kein Wurzelknoten → Start von vorgegebenen Knoten – Statt Kinder: ausgehende Kanten (ohne Ordnung) – Anwendung oft Suchen eines Knotens – Statt Ausgabe eines Knotens auch Färben – Tiefensuche: – Gehe immer so weit – so tief – wie möglich – Umkehren zur letzten Kreuzung, wenn entweder Sackgasse oder wenn Kreuzung vorher schon einmal erreicht – Tiefendurchlauf analog preorder traversal für Bäume – Breitensuche – analog level traversal für Bäume – FIFO-Eigenschaft → Auslesen der Knoten aus open in der Reihenfolge, in der sie erreicht (mark) werden – Konsequenz → Aufzählen der Knoten (visit) aufsteigend geordnet nach „Entfernung“ (Anzahl Kanten) bis zum Startknoten Was kann noch berechnet werden: – Zeitpunkte: Wann wird ein Knoten besucht – Distanzen: Entfernung zum Startknoten in Kanten – Aufspannender Baum (spanning tree): Durchlauf erzeugt Baumstruktur (nicht eindeutig) – – – – – – – – – – 8.2 Topologisches Sortieren gegeben ist ein azyklischer gerichteter Graph Gerichtete Kanten definieren eine partielle Ordnung (i, j) ↔ i kommt vor j Interpretiere Knoten z.B. als Aufgaben, Tätigkeiten Kanten modellieren Abhängigkeiten Beispiele: to-do-Liste, Arbeitsablauf Gesucht ist eine Reihenfolge der Knoten, so dass jeder Knoten nach all seinen Vorgängern erscheint Reihenfolge i.a. Nicht eindeutig Topologisches Sortieren mit DFS: – Es existiert mindestens eine Quelle + Senke – Idee: – Starte von allen Senken – DFS in umgekehrter Richtung (entlang eingehender Kanten) – Gitb Knoten aus, wenn alle eingehenden Kanten abgearbeitet sind – Zu ausgegebenen Knoten gibt es keine Abhängigkeiten mehr 8.3 Kürzeste Wege Länge von Wegen = Summe von Kantengewichten Definition Spannbaum: Als Spannbaum oder spanning tree eines ungerichteten (und zusammenhängenden) Graphen bezeichnet man einen Teilgraphen, der (1.) ein Baum ist und (2.) alle Knoten des Graphen enthält. Definition MST (Minimum Spanning Tree): Ein Spannbaum eines gewichteten Graphen heißt dann MST, wenn die Summe der Kantengewichte minimal ist. D.h. es gibt keinen weiteren Spannbaum mit geringerem Gesamtgewicht. (Der MST ist i.a. nicht eindeutig) Algorithmus von Dijkstra zur Berechnung kürzester Wege: – Wähle Knoten mit kürzester Distanz – Priorität = Wegstrecke – Minimiere Länge von Pfaden im Spannbaum – Keine negativen Kantengewichte erlaubt Mit Tabelle: – Vorarbeit: – setze jeden Knoten als unbesucht – setze jede Distanz auf unendlich – setze jeden Vorgäner auf null – Distanz des Startknotens ist 0 – Vorgänger des Startknotens ist der Startknoten selbst – Wiederhole bis alle Knoten besucht sind: – setze unbesuchten Knoten mit der geringsten Distanz als aktuell besucht (als erstes den Startknoten (Distanz null, beim Rest unendlich) – für alle unbesuchten Nachbarn: addiere eigene Distanz und das Kantengewicht – wenn Summe geringer ist als deren aktuelle Distanz, – dann setze sie – und setze aktuellen Knoten als seinen Vorgänger Algorithmus von Prim zur Berechnung des MST: – Wähle Knoten, der über kürzeste Kante erreichbar ist – Priorität = Kantenlänge – Minimiere Summe der Kantengewichte im Spannbaum Vorgehensweise: – Wähle einen Startknoten – Kürzeste Verbindung zu einem Knoten finden → Wird zum Baum hinzugefügt – Kürzeste Verbindung eines Knotens zum Baum finden → Wird zum Baum hinzugefügt – Solange wiederholen bis der Baum alle Knoten enthält Bellman-Ford Algorithmus – Berechnet kürzeste Wege – Negative Kantengewichte erlaubt – Aber keine Zyklen mit negativen Gewichten A*-Algorithmus (Modifikation von Dijkstra): Ändern der Priorität → – Ausbreiten der Suche zuerst in vielversprechende Richtungen – Keine Garante für schnelleres erreichen (aber wir hoffen es) 8.4 Maximaler Fluss??? 9. Dynamische Programmierung Rucksackproblem: – Brute-Force-Suche: – Systematisches Aufzählen und Bewerten aller Belegungen – → nicht praktikabel (O(2^n)) – Greedy: – bei jeder Entscheidung siegt die Gier (maximaler Profit, minimales Gewicht...) – Keine Garantie, dass optimale Konfiguration erreicht wird – Reduktion auf wenige Entscheidungen – Beschneidung des Lösungsraums – Lösung abhängig vom Entscheidungskriterium – Es ist oft möglich, gute Konfiguration schnell zu finden – In der Regel nur lokales Optimum (kann beliebig schlecht sein) → dynamische Programmierung – Optimale Lösung besteht aus optimalen Teillösungen – Gilt rekursiv für jede optimale Teillösung – Es müssen nur optimale Teillösungen betrachtet werden – pareto-optimale Teilmengen: – → Es gibt keinen Punkt links oberhalb – Rekursion: – → führt zur dynamischen Programmierung – Berechne optimale Lösung für – jedes Maximalgewicht und – – – jede Teilmenge von Gegenständen Maximaler Profit p(k, g) lässt sich rekursiv ausdrücken als: Rekursion über – verbleibende Anzahl k → k – 1 und – verbleibende Kapazität g → g – gk (k einpacken) oder g → g Gesamtlösung ergibt sich als p(n, C) – – Dynamische Programmierung Übersicht: – Ziel: Lösen von Optimierungsproblemen – Zerlege Problem in einfache Teilprobleme – Kleine Teilprobleme können effizient gelöst werden – Am besten Definition mit Indizes – Annahme: Optimal Lösung besteht aus optimalen Teillösungen – Optimale Teillösungen können sich überlappen, d.h. voneinander unabhängige Teile können wiederum gemeinsame Teilprobleme enthalten – im Gegensatz zu divide and conquer Ansatz – Jede Teillösung wird einmal berrechnet und gespeichert Voraussetzungen für D.P.: – Problem lässt sich geeignet zerlegen – Optimalitätsprinzip: Jede optimale Lösung setzt sich zwingend aus optimalen Teillösungen zusammen – Das gilt nur für bestimmte nicht für alle Probleme Anwendungen: – Berechnen der Fibonacci-Zahlen – Berechnen von Binomialkoeffizienten – Optimales Auswerten von Matrixprodukten – Optimales Herausgeben von Wechselgeld