Zusammenfassung Theorie

Werbung
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
Herunterladen