Martin Lenders Algorithmen und Programmierung 3: Algorithmen (und Datenstrukturen) 1. Analyse von Algorithmen 16.10.2008 „Definition“: Ein Algorithmus ist eine Schritt­für­Schritt­Anleitung zur Lösung eines Problems. Man kann z. B. nach Größe des eigentlichen Quelltexts bewerten (sinnvoll beispielsweise bei wenig Speicherplatz). (1) Frage nach der Effizienz von Algorithmen. (2) Frage nach der Korrektheit des Algorithmus. 1.1 EFFIZIENZ VON ALGORITHMEN (insbesondere Laufzeit) Eingabe Algorithmus Ausgabe ● ● Algorithmus transformiert Eingabe in Ausgabe Die Laufzeit eines Algorithmus (Funktion die einer Eingabe die Laufzeit des Algorithmus darauf zuordnet) hängt von der Größe der Eingabe ab 1.1.1 EXPERIMENTELLE ANALYSE ● Implementierung ● Messung der Laufzeit mit vielen, „typischen“ Eingaben Problematisch! 1.1.2 THEORETISCHE ANALYSE ● abstrakte Beschreibung des Algorithmus (Pseudocode) ● Charakterisierung (oder Abschätzung) der Laufzeit (als Funktion der Eingabegröße) ● dabei betrachten wir den schlechtesten Fall für alle Eingaben fester Länge („worst­case“) Warum? ■ konservative Gründe: Wenn ich das längste Zeitmaß weiß, „kann ich ja eventuell schon mal beruhigt sein“ 1.1.3 PSEUDO­CODE ● abstrakte Beschreibung eines Algorithmus: „Programm“ ● „strukturierter“ (& detaillierter) als prosaische (=in gesprochener Sprache formulierter) Beschreibung ● im Allgemeinen weniger detailliert als Java­Programm Beispiel: ArrayMax(A, n) Input: Feld A[1,...,n] von ganzen Zahlen Output: max A[i] 1≤i ≤n currentMax ← A[1] for i = 2 to n if currentMax < A[i] then currentMax = A[i] return currentMax Martin Lenders im Detail: ● Kontrollflussanweisungen (for, while, repeat, if ... then ... else, goto) ● Deklarationen ● Methodenaufrufe ● Wertrückgabe ● Ausdrücke (Zuweisungen, Tests, ...) Verboten bzw. sinnlos: ArrayMax A , n return max A[i] 1≤i≤n da nicht analysierbar (nicht leicht in gängiger Programmiersprache zu implementieren) 1.1.4 REGISTERMASCHINE (RAM – RANDOM ACCESS MACHINE) ● Rechenterm 0 1 2 ○ arithmetische Operationen (+, ­, *, /) ○ Kontrollfluss (IMP, bedingte Sprünge) ○ all diese Operationen können in 1 Takt bearbeitet werden ● Linear organisierter Speicher mit wahlfreiem Zugriff ○ jede Zelle kann eine gane Zahl speichern ○ auf jede Zelle kann in 1 Takt (durch Angabe ihrer Adresse) zuge­ griffen werden ○ der Speicher ist unbegrenzt 3 4 ... Rechenterm Akk _Hilfsregister Pseudo­Code ist O.K. falls jede primitive Operation durch höchstens 10 RAM­Anweisungen ausgeführt werden kann. 1.1.5 LAUFZEIT EINES ALGORITHMUS Definition: A: Algorithmus (RAM­Programm) I: Eingabe für A T A I := Anzahl der RAM­Operationen die A auf Eingabe I durchführt (Rechenzeit/Laufzeit von A auf I) T A n= max T A I Laufzeit von A (Funktion ℕ ℕ) „worst­case“ (wegen max) I , Größe von I =n Beispiel: (I) mit Addition Algorithmus: double(x) Input: x ∈ ℕ Output: 2 ∙ x Pseudocode: y ← x y ← y + x return y RAM: 0 1 x y ... LOAD 0 STORE 1 LOAD 1 ADD 0 STORE 1 LOAD 1 RETURN (CPU (CPU (CPU (CPU (CPU (CPU <-> <<-> <- X) // entspricht Zeile 1 Y) Y) // entspricht Zeile 2 CPU + X) Y) Y) // Enspricht Zeile 3 T A x=7 da 7 Zeilen 21.10.2008 Martin Lenders (II) ohne Addition Algorithmus: double(x) Input: x Output: 2 ∙ x Pseudocode: z ← x // 1­mal y ← x // 1­mal for i = 1 tu z do // x­mal y ← y + 1 // x­mal return y // 1­mal T A x"=" 32 x Typischerweise analysieren wir Algorithmen im Pseudo­Code (Laufzeit ≈ Anzahl der Pseudocodezeilen). Das ist zulässig solange der verwendete Pseudo­Code die Eigenschaft hat, dass jede Code­Zeile durch konstant viele RAM­Operationen realisiert werden kann. 1.1.6 ASYMPTOTISCHE LAUFZEIT ● „moderate“ Änderung des Maschinenmodells ändern die Laufzeit nur um einen konstanten Faktor ● d. h. Addition, Multiplikation, Subtraktion, Division sind z. B. erlaubt ● die asymptotische Laufzeit ignoriert Konstante Faktoren und Terme niederer Ordnung Beispiel: 3 T A n =1,75 n 0,4⋅log2 n 3 = n Erinnerung: verschwindend gering O­Notation Definition: ℕ f , g ∈ℕ f =O g ⇔ ∃c 0 ∃n 0 ∈ℕ: ∀ n≥ n 0 : f n ≤c⋅g n f =O g auch: manchmal schreibt man für f n =O g n i. f ∈O g ii. Beispiel: (i) 12 n −4≤12 ⋅n d. h. 12 n −4=O n f n c g n 2 (ii) n =O n gilt nicht (iii) n 3127256 n² 10 10 ⋅log 2 n=On3 10 10 10 10 10 ≤10 3 n 10 3 10 10 10 10 2 n 10 10 10 log2 n ∀ n≥1 3 ≤10 n 10 n ... =2⋅10 10 n 31010 log2 n 10 1010 ≤3⋅10 c Definition: f , g ∈ℕ 10 3 3 n n ≥log 2 n g n ∀ n ≥1 n ≥127 256 ℕ f⋅ g⇔ ∃c0 ∃ n 0∈ set n : ∀ n≥ n0 : f n ≥c⋅g n klar: f = g gdw. g=O f Definition: f = g gdw. f =O g und g=O f ⇔ f = g 3 ? 1010 n ≥10 log2 n Martin Lenders 1.1.7 O­NOTATION 1.) d f n=∑ a i⋅n i mit a d ≥0 i =0 f n=O n d 2.) Wir sagen „2 n = O(n)“ statt „2n = O(n²)“ (s. auch 1.) 3.) Wir sagen „2 n = O(n)“ statt „2n = O(3n­6)“ 4.) Die Analogie 0 ≙ , Ω ≙ ≥, Θ ≙ = klappt oft (aber Vorsicht: nicht immer) 5.) 6.) f =O g , g=O h⇒ f =Oh ( f ≤g , g≤h , f ≤h ) n =O n ∀ ≤ log n =O n ∀ , 0 log n 10 10 10 10 0,000000001 =O n Beispiel: PrefixAverage(X,n) Input: X [0] ,... , X [n−1] , n Output: 23.10.2008 A [0] ,... , A[n−1] mit A[i]= 1 ∑ X [ j] i1 j ≤i 1: A <­ leeres Feld mit n Elementen// 1­mal durchlaufen 2: for i = 0 to n­2 // n­mal durchlaufen 3: sum <­ 0 // n­mal durchlaufen 4: for j = 0 to i do // (i+1)­mal durchlaufen 5: sum <­ sum + X[j] // im i­ten Durchlauf der äußeren // Schleife 6: A[i] <­ sum/(i+1) // n­mal durchlaufen 7: return A // 1­mal durchlaufen Gesamtkosten: ∑ n −1 O 1 O n O 1 & 7 2, 3 & 6 i1 i=0 4 & 5 =O1O nO n2 =O n2 Algorithmus: PrefixAverage(X,n) Input: X [0] ,... , X [n−1] , n Output: 1: 2: 3: 4: 5: 7: Laufzeit: A [0] ,... , A[n−1] mit A[i]= 1 ∑ X [ j] i1 j≤i A <­ leeres Feld mit n Elementen// 1­mal durchlaugen sum <­ 0 // 1­mal durchlaufen for i = 0 to n­1 do // n­mal durchlaufen sum <­ sum + X[i] // n­mal durchlaufen A[i] <­ sum/(i+1) // n­mal durchlaufen return A // 1­mal durchlaufen O n Problemspezifisch!! Martin Lenders 1.1.8 REKURSION Beispiel: Potenzieren: x, n ∈ ℕ n p x , n ≔ x =x⋅...⋅x n­mal Lösung: (1): Iterativ: O(n) (2): { p x , n ≔ x n = x⋅p x , n−1 , n0 1, n=0 Algorithmus: Pow(x,n) Input: x , n∈ℕ Output: x n 1: if n = 0 then retunr 1 2: return x * Pow (x, n­1) Laufzeit: { T n = O 1 , n=0 T n−1O1 , n0 Lösung: (n > 0) mit C > 0 T(n) T(n­1) + C T (n­2) + 2 ∙ C T(n­3) + 3 C per Induktion: T(n) T(n­k) + l ∙ C ∀ k≥1 für k – n : T n≤T 0=n ∙ C Damit: T(n) = O(n) x ∙ Pow x , n−1 Bemerkung: =x ∙ x ∙ Pow x , n−2 =x ∙ x ∙ x ∙ Pow x , n−3 =x⋅...⋅x x , 0 ⋅P n­mal { 1, n=0 n 2, p x , n=2k (3): p x , n= 2 n−1 2, p x , n=2k1 2 1 x 2 k = x k 2 x 2 k1=x⋅x 2k =x⋅ x k 2 Algorithmus: PowFast(x,n) Input: x , n∈ℕ Output: x n 1: if n = 0 then retunr 1 2: if n = 2k then 3: z <­ PowFast(x, n/2) 4: return z * z // nicht so: return (PowFast(x, n/2))² 5: if n = 2k + 1 then // wegen Seiteneffekte 6: z <­ PowFast(x, (n­1)/2) 7: return z * z * x Martin Lenders Laufzeit: (i) Beschränken uns auf n = 2r für r ≥ 0 { O 1 , n=1 T n = n T O1 , n1 2 { S r = O 1 ,r =0 S r −1O1 , n0 S r ≔ T 2 r wie in (2) S r =Or =T 2r also T n =Olog n 1.1.9 REKURSIONSBÄUME : Beispiel: PowFast (n­2r) powFast (x, n) powFast (x, n/2) powFast (x, n/4) x2 powFast (x, 2) x powFast (x, 1) 1 powFast (x, 0) Beispiel: Fibonacci­Zahlen F(0) = F(1) = 1 F(n) = F(n­1) + F(n­2), n ≥ 2 F(5) F(4) F(3) F(2) F(1) F(3) F(2) F(1) F(0) F(2) F(1) F(1) F(0) F(1) F(0) #Knoten ≥ 1,5n 28.10.2008 Definition: Rekursionsbaum einer rekursiven Prozedur A ist ein Baum TA, wobei ● jeder Aufruf der Prozedur entspricht einem Knoten ● Kinder von Knoten v sind die (rekursiven) Aufrufe von A die v tätigt (mit den jeweiligen Argumenten) ● Wurzel: A(x) Martin Lenders Die Rechenzeit von A auf x lässt sich bestimmen, indem man die in den Knoten des Berechnungsbaums von A auf x anfallende Arbeit aufsummiert. Beispiel: Mergesort zum Sortieren von A[0], ..., A[n­1] [ ] A[0] A [ n−1 2 A n−1 1 2 ] A[n­1] n 2 n 2 ⇓ ⇓ sortieren (rek) sortiert sortieren (rek) sortiert Merge Analyse: (a) T n ≤2⋅T n 2 T n =O n⋅log2 n per Induktion: (b) Rekursionsbaum für MergeSort auf A[0], ..., A[n] A, A1, Arbeit: c⋅ n Arbeit: c⋅ 2 n 2 A2, ∣A2∣= n 2 n Arbeit: c⋅ 2 n 4 A11, A111 ... A[0] ∣A1∣= Arbeit: c⋅n ∣A∣=n A[1] ∣A11∣= n 4 A12, A112 A121 ∣A12∣= n 4 A122 A21, ∣A21∣= ∣A1ij∣= n 8 n 4 A22, ∣A22∣= n 4 Martin Lenders Gesamtkosten: n n n n n n c⋅n c⋅ 2 c⋅ 2 c⋅ 4 c⋅ 4 c⋅ 4 c⋅ 4 ... Wurzel Kinder der Wurzel Enkel der Wurzel =c⋅ n n n n n n n ... 2 2 4 4 4 4 =c⋅n nn... =c⋅n⋅ 1...1 #Stufen im Rekursionsbaum =c⋅n⋅Höhe des Baumes=c⋅n⋅log2 n zum Beispiel „Fibonacci­Zahlen“ Laufzeit der direkten Rekursion: T n = also: { { 1 , n≤1 T n−1T n−21 , n1 1 , n≤1 T n−1T n−2 , n1 damit T n ≥F n (per Induktion) T n ≥ F 0= F 1=1 F n =F n−1F n−2 , n1 =F n−2 F n−3F n−2 ≥2⋅F n−2 ≥2⋅2⋅F n− 4 ≥2⋅2⋅2⋅F n−2 i ≥2 ⋅F n−2 i per Induktion i=n/2 F n ≥2 n/ 2= En n also T n ≥ 2 Damit Alternativen: ● Tabellieren der bereits berechneten F(i) für 0 i < n (dynamische Programmieren) Laufzeit: O(n); Platzbedarf Ω(n) ● iterative Lösung Laufzeit: O(n); Platzbedarf O(1) ● F n F n−1F n−2 1 1 F n−1 = = ⋅ F n−2 F n−1 1 0 F n−2 ≕M F n f n≔ mit F n−1 1 1 fn ≔ f 1 0 n−1 Iteration: fn =M⋅f n−1=M⋅M⋅f n −2=M⋅M⋅M⋅f n−3 Ind = M⋅...⋅M⋅f n−i i≥1 i ­mal i=n−1 =M i⋅f n−i = M n−1⋅ f1 1 f 1≔ 1 Martin Lenders 1.1.11 REKURSION AUF DER RAM Algorithm main() x ← 5 subroutine1(x) x ← 3 Algorithm subroutine1(i) k ← i + 1 subroutine2(k) Algorithm subroutine2(k) y ← 6 ● ● merken welche Unterroutine gerade ausgeführt wird es gibt für jede Unterroutine eine Struktur in der ihr lokaler Kontext (Variablenwerte, Rücksprungadresse) gespeichert wird Aufruf von subroutine2 Aufruf von subroutine1 Programm­ Start ● Kontext x ← 5 PC ← main Kontext k ... PC → main::3 subroutine1 Kontext y ... PC → subroutine1::3 subroutine2 Kontexte werden in eine Stapel (Stack) verwaltet 1.2 DATENSTRUKTUREN Schema zur Organisation von Daten, so dass gewisse Operationen auf / Manipulaition von Daten effizient durchführbar sind. 1.2.1 BEISPIEL STAPEL (STACK) Manipulation von Objekten erfolgt nach dem LIFO (last­in­first­out) Prinzip (hier: „Objekte“ = int) d. h. wir benötiogen die Operationen ● einfügen eines neuen Elements (push) ● entfernen des zuletzt eingefügten Elements (pop) ● lesen des zuletzt eingefügten Elements (top) (ohne es zu entfernen) ● Test ob der Stapel leer ist ● Initialisieren der Datenstrukturen Ausblick: algebraische SpezifikationF (x',S') = pop(push(x,S), S) ⇒ x'= x uns S'= S pop: int x Stapel (int) ­> int x Stapel (int) push: int x Stapel (int) ­> Stapel (int) Implementierung: Martin Lenders F 0 1 2 3 ... M­1 top int­Array F Zeiger top auf oberstes Element push (x) top ← top + 1 F[top] ← x Melde Fehler falls Array F voll (top == M­1) pop() return F[top­­] Fehler signalisiert falls Stack leer ist (top == ­1) ● ● ● ● Java­Implementierung class IntArrayStack { int F[]; int top; int M; IntArrayStack(int m) { M = m; top = ­1; F = new int [M] } int pop() throws EmptyIntArrayStackException { if (top == ­1) throw new EmptyIntArrayStackException; else return F[top­­]; } 1.2.2 ANALYSE VON DATENSTRUKTUREN ● Wie effizient sind die Operationen auf der Datenstruktur? (in Abhängigkeit von der Anzahl der in der Struktur gespeicherten Objekte) ● Wieviel Speicherplatz benötigt die Struktur? hier: ● Platzbedarf: ϴ(M) ● ϴ(1) Zeit für push & pop Probleme: ● maximale Größe fest ● Implementierungsspezifische Ausnahmebehandlung Martin Lenders Lösung: Verkettete Listen ● einfach verkettete Liste NIL 12 7 5 Knoten ● ● Feld von Zeiger auf Knoten zu speichern Feld von Objekt zu speichern Einfügen &Löschen am Anfang der einfach verketteten Liste ist einfach und effizient (ϴ(1) Zeit) Analyse: ϴ(1) Zeit pro Operationen ϴ(n) Platz 1.2.3 „INDIREKTE“ ANWENDUNG VON STAPELN ● als Hilfsmittel für andere Datenstrukturen ● beim Entwurf von Algorithmen Beispiel: Spann eines Feldes gegeben: Feld X[0], ..., X[n­1] von n Zahlen Berechne für alle 0≤i≤n−1 S[i] = max. Anzahl von aufeinander folgenden Elementen unmittelbar von X[i] die kleiner als X[i] Algoritmus Span (X, n) S <­ Neues Feld mit n Elementen for i = 0 to n­1 do S[i] <­ 0 for j = 0 to i do if X[i­j] <= X[i] do S[i]++ else break; Laufzeit: n−1 i // n­mal // max. i­mal ∑ ∑ 1 =n 2 i=0 j=0 Besser mittels Hilfsstruktur ● arbeiten X vom kleinsten bis zum größten surch ● von rechts „sichtbare“ Element im Stapel (von rechts nach links) ● am Index i entferne alle Elemente von der Spitze des Stapels die X [i] & zählen span (X[i] je um eins höher legen X[i] auf Stapel Algoritmus Span (X, n) S <­ Neues Feld mit n Elementen A <­ neuer leerer Stapel for i = 0 to n­1 do while (!isEmpty(A) && X[top(A)] <= X[i]) do pop(A); push(i,A) if isEmpty(A) S[i] <­ i+1 else S[i] <­ i­top(A) Martin Lenders Analyse n push­Operationen n pop­Operationen 2n isEmpty­Operationen Stack mit einfach verketteten Listen ⇒ Θ(n) Gesamtlaufzeit 1.3 DYNAMISIERUNG ARRAY­BASIERENDER DATENSTRUKTUREN konkretes Beispiel: Stapel Ziel: ● Implementierung von Stacks durch Arrays ● keine feste Größe Idee: (nur push­Operationen) ● Θ(1) Zeit für push­Operationen, solange noch Platz ↓ A ██████████████ M ⇓ ↓ A' ██████████████░░░░░░░░░░░ M M' ● Θ(M) Zeit für push­Operationen die Umkopieren erfordert (typischerweise: M' = 2M) Idee: Wir haben zu jedem Zeitpunkt ein Array, in dem alle Elemente des Stapels abgespeichert sind Wen Platz in diesem Array nicht mehr ausreicht legen wir ein neues Feld doppelter Größe an und kopieren das alte Feld an den Anfang des neuen Feldes Problem dabei: ● Es gibt push­Operationen die Ω(# Elemente im Stapel) Zeit brauchen ● ● ABER: ● Jede Folge von n push/pop Operationen braucht O(n) Zeit Begründung: 1. Eine pop­Operation benötigt nur Θ(1) Zeit. D. h. alle pop­Operationen in einer Folge von n push/pop­ Operationenbenötigen O(n) Zeit. 2. Betrachte Folge von n push­Operationen die k­te push­Operation benötigt Zeit T k= { k O k k=2 O 1 sonst Gesamtheit für n push­Operationen: n ⌈ log2 n ⌉ i=0 i=0 ⌈ log2 n ⌉ ∑ T k=O n ∑ T 2 =O n ∑ O 2i =O n O n =O n „amortisierte Kosten von O(1) i i=0 Martin Lenders push­Op# Array a Kosten (US­$) 1 █ 1 2 ██ 1 + 1 3 ███░ 2 + 1 4 ████ 1 5 █████░░░ 4 + 1 6 ██████░░ 1 7 ███████░ 1 8 ████████ 1 9 █████████░░░░░░░ 8 + 1 ... ... ... 15 ███████████████░ 1 16 ████████████████ 1 17 ... 16 + 1 Definition: Gegeben sei eine Datenstruktur für die eine Folge von n Operationen insgesamt T(n) Zeit T n benötigt, Dann nennen wir die amortisierten Kosten pro Operation. n alternatives Argument via Bankiersmethode 06.11.2008 Idee: ● Jede elementare Operation verursacht gewisse Kosten (z. B: Kosten von pop O(1): 1$ Kosten von push (kein Umkopieren) O(1): 1$ Kosten von push (mit Umkopieren) O(Göße des Arrays): #Elemete im Stack$) ● Eine Folge F von Operationen verursacht damit Gesamtkosten cF (Ziel: Zeige, dass cF = O(n) fallst |F| = n) ● Wir werden zeigen: Falls wir jeder Operation „3$“ „mitgeben“ können wir am Schluss die Kosten cF begleichen Buchhaltervorschrift: ● pop­Operation (bringt 3$ mit) ○ bezahle die Kosten (1$) und lege 2 $ auf das Sparbuch (der DS) ● push­Operation (bringt 3$ mit) ○ kein Kopieren: bezahle Koster (1$) und lege 2$ auf des DS Sparbuch ○ mit Kopieren: bezahle Kosten (#Elemente im Stack$) von meinen 3$ + Geld auf dem Sparbuch Wir werden zeigen: Das Sparbuch ist nie im Minus 1. die Kosten cF können beglichen werden 2. es gibt cF = 3$ ∙ |F| ↓~~~~~~~~↓ █████████▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░ M M ● ● eine push­Operation die veranlasst, dass ein Feld mit M Einträge verdoppelt wird, hat zur Folge, dass danach mindestens M Operationen ausgeführt werden können die nur 1$ kosten. Dabei werden 2M$ auf das Konto eingezahlt. die nächsten anfallenden kosten 2M$ Martin Lenders Problem: Der Platzbedarf der DS hängt nicht von der Anzahl der Operationen der Elemente ab, die in der DS gespeichert werden, sondern von der Anzahl der durchgeführten Operationen Lösung: Modifiziere pop 2M ██████████▒▒▒▒▒▒▒▒▒▒ ↓ ↓ M ██████████ (falls Array nur noch ¼ belegt ist, halbiere % kopiere dann) 2M █████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ↓ ↓ M █████▒▒▒▒▒ Martin Lenders 2. Implementierung in Java 2.1 DATENABSTRAKTION Geheimnisprinzip: Kenntnis der Spezifikation eines Moduls, Datentyps, Systems ist notwendige und hinreichende Voraussetzung für deren korrekte Benutzung. Bsp.: Prozedurale Abstraktion Datenabstraktion ist die Anwendung des Geheimnisprinzips auf die Darstellung komplexer Datenstrukturen: ● nicht direkt manipulierbar ● nur über (prozedurale) Schnittstelle manipulierbar (die geeignete Operationen zur Verfügung stellen) Bsp: Stacks (als Array) public class Stack { priv. int max = 100; priv. Object[] S = new Object[max]; priv. int t = 0; publ. void push (Object o) throws Exception { if (t==max) throw new Exception (); S[t++]=0; } publ. Object pop () ... } Problem: direkte Manipulation der Daten möglich Stack s = new Stack(); s.t = 124; // mit private nicht mehr zulässig s.push(3); Einschränkung der Sichtbarkeit bei der klassenbasierten Datenabstraktion: ● Das Klassenkonzept unterstützt die Zusammenfassung von Daten und den sie manipulierenden Operationen (in einer sysntaktischen Form) ● In Java (und den meisten imperativen Sprachen mit klassenbasierender Datenabstraktion) kann das Verbergen der internen Repräsentation durch Sichtbarkeitsmodifikatoren ○ public: überall sichtbar ○ (default): innerhalb des Pakets sichtbar ○ protected: innerhalb des eigenen Paketes & in den Unterklassen ○ private: nur innerhalb der umschließenden Klasse erreicht werden. Diese steuern die Sichtbarkeit von Attributen und Methoden. 2.2 ABSTRAKTE DATENTYPEN Definition: Ein ADT ist eine Menge von Objekten mit einer Menge von Operationen auf diesen Objekten alternativ: Ein Typ der nur über die Operationen seiner Schnittstelle manipuliert werden können. Beispiel: Stack Operationen: push (x, S) S pop (S) S Objekte: Folgen (von Elementen) ↑ nach dem LIFO­Prinzip Martin Lenders Vorteil der Datenabstraktion ● Implementierung ist unabhängig vom Modell ● Sicherheit: Objekte können nicht in ungültige Zustände versetzt werden ● Flexibilität: Code kann unabhängig vom benutzenden Code entwickelt werden ● Komfort: Benutzer abstrahieren von der Repräsentation Reale Welt „reale“ Objekte mit „realen“ Operationen Modellierung Modell abstrakte Objekte Operationen ADT Implementierung Datenstruktur Methoden 2.2.1 EXPLIZIETE SCHNITTSTELLENBESCHREIBUN IN JAVA „Probleme“: ● Operationen müssen aus dem Quelltext der Klasse heraus gesucht werden ● de Implementierung der Methoden wird nicht vorm Benutzer verborgen Deklaration des Schnittstellentyps interface Stackinterface { void push (Object o); Object pop(); } Methoden haben leeren Rumpf keine (non­const) Attribute Standardsichtbarkeit public ● ● ● class Stack implements StackInterface { private int max = 100; private Object[] S = new Object[max]; private int t = 0; public void push (Object o) throws Exception { if (t==max) throw new Exception (); S[t++]=0; } public Object pop () ... } class ListStack implements StackInterface { private class Node { Object item; Node next; } private Node first; ⋮ public void push (Object o) ... Martin Lenders public Object pop () ... } Im Programm: 1. Festlegung auf eine konkrete Implementierung Stack s = new Stack (); 2. evtl. andere Implementierung verwendbar StackInterface s = new Stack(); 3. geht nicht: StackInterface s = new StackInterface(); Implementierung von Schnittstellen Pflichten: 1. alle Schnittstellenmethoden müssen implementiert werden 2. alle Schnittstellenmethoden müssen public sein Rechte: 1. Parameter von Schnittstellenmethoden dürfen umbenannt werden 2. beliebige weitere Attribute/Methoden 3. Schnittstelle kann als Typ verwendet werden (aber keine Instanzen möglich) 9, 1, 2, 4, 3, 7, 5 push_left push_right pop_left pop_right Bemerkung: ● eine Klasse kann mehrere Schnittstellen implementieren ● „guter Stil“ als Typbezeichner möglichst Schnittstellentypen verwenden, Klassentyp nur bei Instanzierung Martin Lenders 3. ADT – Anwendungen, Implementierungen 3.1 DER ADT PRIORITÄTSWARTESCHLANGE Motivation: 1. Scheduling von Prozessen im Betriebssystem benötigt Operationen: ○ finde Prozess mit höchster Priorität ○ erzeuge neuen Prozess (mit vergebener Priorität) ○ verringere Priorität eines Prozesses 2. Sortierung nach wiederholten Bestimmen des kleinsten Elements benötigte Operationen: ○ finde Element, das am kleinsten ist ○ entferne Element, das am kleinsten ist ADT PWS (priority queue) U: Universum; total geordnet via Objekte des ADT: endliche Teilmenge S ⊂ U Operationen des ADT: findmin: S ↦ min S deletemin: S ↦ S' mit S' = S \ min S insert (x,S) ↦ S' mit S' = S ∪ {x} x ∈ U Damit: Algorithm PQSort(A) Input: Array A[1], ..., [N] VON Elementen aus U Output: Sortierung von A (bezüglich der Ordnung auf U) PQ H; for i = 1 to n do H.insert(A[i]); for i = 1 to n do print H.findmin(); H.deletemin(); Laufzeit: T x Laufzeit von X ∈{ findmin , deletemin , insert} Gesamtlaufzeit: O n⋅T insert n⋅T findmin n⋅T deletemin n 3.1.1. IMPELEMTENTIERUNG DES ADT PWS (FÜR U = ℕ) (a). Implentieren der PWS als einfach verkette Liste ⇒ 7 12 2 9 ­­­> 13 T findmin =O ∣S∣ , T insert =O1 ,T deletemin=O ∣S∣ Damit: Gesamtlaufzeit: On 2 (b). als Folge l unsortierter Listen mit je m Elementen, bei denen die Minima in einer Liste (unsortiert) verkettet sind l −1 haben m 1 hat ≤ m Elemente 11 7 12 { 3 17 9 5 13 4 0 20 1 l = 4, m = 3 Martin Lenders l ∙ m = n T findmin=O l T insert =Ol T deletemin =O l m l =m= n Damit Gesamtlaufzeit O n⋅l n⋅mn = On⋅ n (c). Heaps: Ein heapgeordneter Baum für einen Menge S ⊂ U ist ein Baum auf den Elementen von S wobei gilt: u Kind von v ⇒ u ≥ v Wurzel 7 ist ein Heapsortierter Baum 8 Blätter Beispiele: 2 5 2 6 3 insbesondere: 3 5 7 9 8 7 11 Eigenschaften heap­geordneter Bäume: ● Minimum ist in definierter Wurzel ● Jeder Teilbaum ist heap­geordnet in Java: interface PrioritätsWarteSchlange { void insert (Object o); Object findmin(); void deletemin(); } Implementierung als Heap ● min­Heap­geordnete Bäume / Wälder Implementireung der PWS­Operationen Auf heapgeordnete Bäume ● findmin: trivial ● deletemin: setzt die Wurzel auf „∞“ und lässt sie „absinken“ bis zu einem Blatt und entfernt dieses ● insert: ausgehend von einer „freien Position“ (abhängig von Details der Baumstruktur) fürge neues Objekt ein und lass es „aufsteigen“ konkreter: binäre Bäume wir beobachten: ● binär ● heap­geordnet ● Tiefe von Blättern unterscheidet sich maximal um 1 ● alle bis auf einen inneren Knoten haben genau zwei Kinder Fakt: Die maximale Tiefe eines binären Heaps mit n Elementen is O(log n) Martin Lenders ● deletemin auf binären Heaps ○ schreibe rechtestes Blatt in die Wurzel & lass es absinken (entferne das Blatt, mache es zu ersten freien Blatt und aktualisisere das rechteste Blatt ... .... ● ● ● wir stellen binäre Heaps so dar, dass die letzte Ebene des Baumes von links nach rechts ausgefüllt ist wir „merken“ uns das rechteste“ Blatt der letzten Ebene & das erste freie Blatt insert auf binäre Heaps ○ schreibe das neue Element in das nächste freie Blatt, mache es zum rechtesten Blatt, lass es aufsteigen und bestimme das neue nächste freie Blatt Mögliche Implementierungen von binären Heaps 1. verzweigte Struktur (später!) 2. flach in einem Array 1 2 4 8 3 5 9 10 6 11 12 7 13 14 15 1 2 3 4 5 6 7 . . . . ● ● ● ● 15 Speichere binären Heap mit n Elementen in Feld der Länge n Knoten mit Index i hat linkes (rechtes) Kind bei 2i (2i+1) „letztes“ Element ist bei Index n gespeichert die erste freie Position bei Index n+1 Dynamisierung durch iteratives Verdoppeln/Halbieren (armortisiert O(1) pro Operation) Laufzeit: deletemin & insert: O(log n) findmin O(1) Damit Laufzeit von PWSSort (Heapsort) O(n ⋅ log n) Ziel: (Effiziente) Implementierung der Operationen (P, Q PWS) P.meld(Q) modifiziert P so, dass es alle Elemente von Q enthält Martin Lenders 3.1.2 ERWEITERUNG DES ADT PWS: Schnittstelle in Java: interface VerschmelzbarePWS { void insert (Object o) Object findmin(); void deletemin(); void meld(VerschmelzbarePWS P); } Implementierung: Ergänze Implementierung mittels bin. Heaps durch „triviales“ meld (d.h. |Q|­mal: P.insert(Q.findmin()); Q.deletemin()) class NaiveVPWS implements VerschmelzbarePWS { private BinaererHeap P; // irgendwo existiert class BinaererHeap // implements PWS void deletemin() { P.deletemin; } ... void meld (NaiveVPWS Q) { while (!Q.isEmpty) { P.insert(Q.findmin()); Q.deletemin(); } } } Vererbung (Einschluss­Polymorphie) in Java (Polymorhie = Vielgestaltigkeit) Klasse (bzw. Schittstelle) Y wird als Erweiterung der Klasse X vereinbart und erbt damit die Eigenschaften von X interface VPWS extends PWS { void meld(); } bzw. class VbinaererHeap extends BinaererHeap implements VPWS { void meld() ...... } Implementierung von meld(): Laufzeit: m ‧ (log n + log m), m = |Q|, n = |P| (für Implementierung mit binären Heaps) d. h. ● ● nicht sehr effektiv auch ungünstig das lediglich Nutzung bereits implementierter Methoden ⇒ neue Implementierung als Binomialheaps Martin Lenders Binomialbäume: Bi bezeichnet einen Binomialbaum vom Grad i. Induktiv definiert: B0: Bi+1: Bi Bi zum Beispiel: B0 #Knoten nach Ebene: 1 B1 #Knoten nach Ebenen: 1 1 B2 #Knoten nach Ebenen: 1 2 1 B3 #Knoten nach Ebenen: 1 3 3 1 (s. Pascal'sches Dreieck) Es gilt: 1. Bi hat 2i Knoten (Induktion) 2. Die Wurzel von Bi hat i Kinder 3. Die Tiefe von Bi ist i Binomialheap mit n Elementen S ist ein Wald von Binomialbäumen in denen die Elemente von S heapgeordnet sind. Jeder Bi kommt damit höchstens einmal vor, da sich jede Zahl aus der Summe von Zweierpotenzen darstellen läßt (Bi hat immer 2i Knoten). Das geht immer ! Beispiel: S = {7, 5, 1, 4, 13, 6} 5 n = 6 = 22 + 21 (Summer von 2er­Potenzen) 1 7 4 6 13 B1 Es gilt: B2 Wir benötigen ϴ(log n) Binomialbäume in einem Binomialheap für n Elemente (Hinweis: Binärdarstellung) Martin Lenders Die (Wurzeln) der Binomialbäume sind in einer Liste verkettet, sortiert nach ihrem Grad. Beispiel: S = {7, 5, 1, 4, 13, 6, 15, 9, 3, 8, 27, 21, 34, 99} n = 14 = 23 + 22 + 21 3 1 5 7 B1 ● 4 8 6 13 15 21 9 99 34 27 B2 B3 deletemin() z. B. durch Ermitteln des Minimums der Wurzeln Implementierung der VPWS­Operationen 1. meld() P Q B1 B0 B2 B3 B2 Vorgehen: Wenn zwei Bäume die gleiche Struktur: Zusammenfassung → B2 + B2 = B3 Dadurch wird die Invarianz erhalten B0 B1 ⇒ P + Q = B4 analog zu binäree Addition von |P| = 23 + 22 + 21 = 14 = [001110]2 und |Q| = 22 + 20 = 5 = [000101]2: 001110 + 000101 010011 d. h. ● durchlaufe Wurzellisten von P, Q (angefangen beim kleinsten Grad) ● zu jedem Zeitpunkt stellen wir sicher, dass es im Resultat nur min. einen Baum pro Grad gibt ● so gibt jeweils maximal einen Baum C der aus dem vorherigen Schritt als Übertrag kommt ● sei A der aktuelle Binomialbaum von P sei B der aktuelle Binomialbaum von Q Fall 1: es gibt ein C a) deg A < deg B: schreibe A in die Wurzelliste von P ∪ Q, ersetze A durch sinen Nachfolger b) deg A > deg B: umgekehrt zu a) c) deg A = deg B i. min B < min A: mache A zum Kind der Wurzel von B und setze das Ergebnis als Übertrag C. Ersetze A (und B) durch ihre Nachfolger Martin Lenders ii. min A < min B: umgekehrt zu ii) Fall 2: es gibt ein C: s. Übungsblatt Analyse des Verschmelzend Das Verschmelzen von P und Q benötigt Zeit O(max (log |P|, log |Q|)) ⇒ Effektiver als mit Binärheaps Implementierung der restlichen Heap­Operationen Probleme: deletemin()? min B0 B1 B2 B3 B4 min B3 B2 Entfernt man die Wurzel von Bi erhält man {B0, B1, B2, B3, ..., Bn­1,} min B2 B1 B0 findmin(P): Durchlaufen der Wurzelliste O(log |P|) Zeit insert(P,x): durch meld(P, {x}) O(log |P|) Zeit deletmin(P): Finde Bi in der Wurzelliste der Minimumspeichert (in seiner Wurzel), entferne Bi aus der Wurzelliste. Entferne Wurzel von Bi (resultiert in {B0, B1, ..., Bn­1}) und erzeuge damit neuen Binomialheap. Verschmelze den neuen & den alten Heap. O(log |P|) Zeit 3.1.3 DAS LOKALISIEREN VON EINTRÄGEN IN DATENSTRUKTUREN Typischerweise „wissen“ Einträge in einer DS wo sie in der Struktur gespeichert sind. Sonst ist das Manipulieren von Einträgen in der Datenstruktur „schwierig“ (d. h. Unmöglich / ineffizient) Bsp: ● Löschen von Einträgen aus einer PWS ● Ändern der Priorität von Einträgen einer PWS Martin Lenders Beispiel: PWS für Prozessobjekte mit ganzzahligen Prioritäten 1 2 3 4 6 Prozesse: 5 P1 hat Priorität 2 P2 hat Priorität 1 P3 hat Priorität 5 mögliche Lösung: Schnittstellenmethode zum Einfügen von Objekten liefern einen „Zeiger“ auf den Eintrag des Objects in der D. S. z. B. class PWSEintrag { int Priorität; Object Eintrag; int Position; } PWSEintrag 5 P3 1 2 PWSEintrag 7 P11 4 3 6 5 7 3,2 BÄUME Mathematischer Kontext ● Kreisfreier zusammenhängender, ungerichteter Graph Informatik­Kontext ● ● ● ● ● Wurzel w ∊ V gerichteter Graph T=(V, E) (von der Wurzel weg) (u, v) ∊ E v heißt Kind (Nachfolger) von u u heißt Vater/Mutter (Vorgänger) von v Beispiel: Blätter (externe Knoten) sind Knoten ohne Kinder typischerweise gibt es eine (totale) Ordnung auf die Kinder der Knoten w Tiefe = 2 v ● Jeder v ∊ V ist von w auf (gerichteten) Pfad erreichbar. Die Länge dieses Pfades heißt die Tiefe von v. Martin Lenders 3.2.1 BÄUME ALS ADT Schnittstelle zum ADT Knoten ● speichert Objekte ● Manipulation/Zugriff: void setInfo (Object o); Object getInfo(); Schnittstelle zum ADT Baum (Auswahl) ● Zugriff auf die Wurzelliste Knoten getRoot(); Knoten getParent(Knoten k); boolean isLeaf(Knoten k); Knoten getChild(Knoten k, int i); int getDegree(Knoten k); // Anzahl der Kinder Knoten Index / Info t t u Knoten Index u Info v1 v2 v3 Liste von Knoten / / Index v1 Info / Index v2 Index v3 Info / Implementierung von Bäumen 1. Verkettete Strukturen auf den Knoten Platzbedarf für Baum mit n Knoten O(n) 2. Arraydarstellung k­närer Bäume k k1 Info / −1 Elementen abgespeichert. k−1 Das i­te Kind eines Knotens der beim j­ten Eintrag gespeichert ist, wird im (k ‧ j+i)­ten Eintrag abgelegt. Zugriff auf Elternknoten durch ganzzahlige Division (evtl.) Problem: das Array kann exponentiell groß (im Vergleich zum Baum) sein. k­näre Bäume der Höhe k wird im Array mit Martin Lenders Spezialfall: k­näre Bäume (k ≥ 2) ● jeder Knoten hat ≤ k Kinder ● falls jeder innere Knoten genau k Kinder hat, heißt der Baum wohl ● bei geordneten Binärbäumen heißt das 1. Kind linkes Kind, das 2. Kind rechtes Kind Beispiel: k = 3 1 5 3 6 7 8 4 9 10 11 12 h 2 13 n0 h = 0 0 3 =1 h = 1 3 3⋅30 =n 03⋅n 0=4 h = 2 h = 3 43⋅3=14 139⋅3=40 n 3 n1−1 i ∑3 = 2 i=0 n1 0 Algorithmen auf Bäume ● Traversieren von Bäumen (Binärbäumen): ○ inorder: besuche erst den linken Teilbaum unter der Wurzel, dann die Wurzel, dann den rechten Teilbaum ○ preorder: besuche zuerst die Wurzel, dann links, dann rechts ○ postorder: besuche erst links, dann rechts, dann die Wurzel Anwendung von inorder­Traversieren: Beispiel 1: Zeichnen von Bäumen T = (V, E) Baum in: V → ℕ inorder­Traversierung h: V → ℕ die Abbildung Höhenfunktion { 2 Vℝ v ↦ in u,hu ist eine kreuzungsfreie Zeichnung von T in der Ebene 6 h=0 2 9 1 4 3 1 Diese Traversierungen benötigt O(n) Zeit. 7 5 5 h=1 10 8 h=2 11 10 h=3 Martin Lenders Beispiel 2: Arithmetische Ausdrücke rekursiv definiert: (i) x ∊ ℤ ist ein Arithmetischer Ausdruck (ii) e, f arithmetische Ausdrücke ⇒ (e + f) sind arithmetische Ausdrücke (e – f) z. B. 17439, ­25, (17439 + 17439) O.K. 7 ( ­ 3 nicht O. K. Darstellung als Ausdrucksbaum: z. B. ((2+5)­(7+12)) → + + 2 5 7 12 Auswertung durch postorder­Traversierung UP­N (Umgekehrt Polnische Notation): 2, 5, +, 7, 12, +, ­ Beispiel 3: Spielbäume: Am Beispiel TicTacToe Größe der letzten Ebene 199⋅89⋅8⋅7...9 ! ... Breitensuche in Bäumen v w1 w4 w2 w5 w6 v, w1, w2, w3 w3 w4, w5, | w6, w7, | w8, w9 w7 Traversierung mit Queue. Nachteil: Viel Speicher. Martin Lenders Tiefensuche in Bäumen v w1 w4 w2 w5 w6 w3 w7 Traversierung mit Stack. w8 w9 v ↓ w1, w2, w3 ↓ w4, w5, w2, w3 Martin Lenders 3.3 ADT WÖRTERBUCH (DICTIONARY) Verwalten einer endlichen Teilmenge S ⊂ U eines Universums U. Operationen: find(k, S): bestimme, ob k ∊ S insert(k, S): füge k zu S hinzu delete(k, S): lösche k aus S Bemerkung: ● Ggf. Fehlerbehandlung (z. B. Bei delete) ● im Allgemeinen ist S eine Multimenge Vielfache Anwendung! Implementierung: (1) Verkettete Liste Platzbedarf Θ(|S|) Laufzeit insert: Θ(1) ggf. Θ(|S|) find: Θ(|S|) delete: Θ(1) falls Zeiger in der Liste, sonst Θ(|S|) (2) Hashing (Streuspeicherung) 1. Idee: Finde „gute“ Abbildung h1: U → ℕ 2. Finde gute Abbildung: h2: ℕ → [1, ... , N] 3. Speichere S in eine Array T [1, ..., N] und zwar speichern wir x ∊ U in T[h2(h1(x))] Beispiel: U = Zeichenebene y 6 3 A 1 9 B 5 8 D C2 4 E 7 0 x h1(A) = h1(B) = 3 h1(C) = 2 h1(D) = 5 h1(E) = 4 F = (1010,1010) h1(F) = 1010 C E D A,B F N = 4 h2 = (n mod 4) + 1 ℕ → [1, ..., 4] D,F C A,B E h1(x) heißt der Hashcode von x h2(x) heißt Kompressionsfunktion Hashcodes: (1) Zeichenketten s = s1 ... sk ∊ Σ* (a) Länge (b) h 1 s= ∑ h 1 si ⋅∣∣ wobei h 1 ℕ ↦i ={ 1, ... , l } Martin Lenders z. B.: ASCII s = AFFE ∣∣=256 h1 A =65 h1 F=70 h1 s=65⋅25670⋅25670⋅25669=... h1 E=69 (2) Floatingpoint­Zahlen (a) h1 x=⌊ x ⌋ Ganzzahlanteil von x (b) h1 (x) = Matisse von x als Integer Kompressionsfunktion: Idee: h2 soll S „möglichst gleichmäßig“ auf [1 ... ℕ] aufteilen Typischerweise ist N eine Primzahl Bsp. 1. h2(x) = 1 + (x mod N) 2. h2(x) = 1 + a ∙ x + b mod N) a ≠ 0 (mod N) a, b ∊ ℕ Hashing mit Verkettung U Universum S ⊂ U, |S| = n h: U → [0, ..., N­1] Hashfunktion in m Tabelle T der Größe N. Idee: Speichere x ∊ S an Position h(x) in T x1, x2 ∊ S kollidieren (bezgl h) falls h1(x) = h2(x) Behandlung von Kollison durch Verkettung T[i] zeigt auf eine verkettete Liste, in der alle x ∊ DS mit h(x) = i gespeichert sind. Bsp.: T D F / C / A B / E / Einfügen/Löschen/Suchen eines Eintrags e mit Schlüssel k ● berechne i = h(k) ● Einfügen/Löschen/Suchen von e in Liste T[i] wie oben Analyse: Platzbedarf: Laufzeit: O N Array T O n =O nN Knoten der Liste Bem.: Ggf muss noch Platz zum Speichern von h berücksichtigt werden. hängt von: 1. der Zeit, die zur Berechnung von h() benötigt wird 2. der Länge der Liste die den Eintrag speichert CS(k) = {y ∊ S | h(y) = h(k)} Falls h in konstanter (O(1)) Zeit ausgewertet werden kann, benötigen alle Operationen Θ(1 + |CS(k)|) Zeit. Martin Lenders Analyse der mittleren Laufzeit von Hashing mit Verkettung n, h, U sei fest (U sei endlich, |U| = u) S ⊂ U, |S| = n wird zufällig (glichverteilt) gewählt) U hat un viele n­elementige Teilmengen Für V ⊂ U mit |N| = n fest, gilt 1 1 Pr S=V = pv = = ∣w∣ u n W ={S⊂U |∣S∣=n} für k ∊ U fest betrachten wir CS(k) (Zufallsvariable) 1 E[ C S k ]= ∑ p v⋅C v k = ⋅∑ C v k ∣w∣ v∈W v∈W Bemerkung: ∣C S k ∣= ∑ y∈S kk =h y { is y mit is y= 1 y∈ S 0 sonst Damit pv 1 1 E[∣C S k∣]= ∑ ∣C k∣= ∣w∣ ∑ ∣w∣ V ∈W v V ∈W = ∑ y∈U h k =h y 1⋅pr is y=1= ∑ y∈U h k =h y E [ is y](für y fest) 1 i v y= ∑ ∑ iv y = ∑ ∑ ∣w∣ ∑ p v iv y y∈U hk =h y V ∈W y ∈U h k=h y y∈U V ∈W h k =h y n u HASHING DURCH VERKETTEN Analyse der erwarteten Zugriffszeit Universum U |U| = u Hashtabelle mit N Einträgen Hashfunktion (fest): h: U → [0, ..., N­1] n­elementige Teilmengen von U sollen verwaltet werden Zufallsexperiment S∈ U n wird zufällig (unter Gleichverteilung gewählt) Analyse Zufallsvariable (für k ∊ U) ⇒ ⇒ E [C S k ]= ∑ y∈U h k =h y n n = u u |CS(k)| = |{y ∊ S | h(k) = h(y)}| ∑ y∈U h k=h y n 1= ⋅∣{ y∈U ∣h k =h y }∣ u Def.:Eine Hashfunktion h: U → [0, ..., N­1] heißt fair, falls |{y ∊ U | h(y) = i}| ≤ falls h fair: ⌈ ⌉ u N für alle 0 ≤ i < N Martin Lenders ⌈ ⌉ n u n E[C S x ]≤ ⋅ ≈ ← Belegungsfaktor der Tabelle u N N Zusammengefasst: Die erwartete Zugriffszeit für Hashing mit Verkettung (bei Verwendung einer fairen Hashfuktion) bei der Verwaltung von n­elementigen Teilmengen von U in einer Tabelle mit N Einträgen ist O 1 n N Beispiel: h := ..., N−1] {[0,... , u−1][0, x ↦ x mod N ist fair. Bemerkung: (a) mit N = Θ(n) erhalten wir Θ(1) (erwartete) Zugriffszeit und Θ(n) Speicher (unter den bekannten Annahmen) (b) falls n nicht bekannt ist, kann durch Verdoppeln / Halbieren der Tabellengröße (inkl. Umkopieren) Θ(1) armortisiert erwartete Laufzeit bei Θ(n) Platz erreicht werden. ALTERNATIVE U : NIVERSELLES HASHING Idee: Wählen h beim Aufbau der Struktuur zufällig (unter Gelichverteilung) aus einer Menge von „guten“ Hashfunktionen Analyse: Sei S ⊂ U mit |S| = n mod x ∊ U fest Für h ∊ H sei C x h={ y ∈S | h x =h y } { ∣C x h∣= ∑ xy h mit xy = 1 , h x=h y 0 , sonst y∈S E[∣C x h∣]=E ∑ xy h =∑ Pr h x =h y [ ] y∈ S ∣{h∈ H | h x=h y}∣ 1 ≤ ∣H∣ N 1 n = N N mit Pr h x=h y = gilt E[∣C x h∣]≤∑ y∈ S y∈ S Def: Eine Menge H ⊂ {0, ..., N­1}U von Hashfunktionen heißt universell, falls ∀x,y ∊ U mit x ≠ y |{h ∊ H | h(xI) = h(y)}| ≤ ∣H∣ N Damit: Die erwartete Zugriffszeit für Hashing mit Verkettung bei zufälligen Wahl von h aus einer universllen Familie von H­Funktionen bei der Verwaltung einer (festen) n­elementigen Teilmenge S ⊂ U in einer Tabelle jeder! Mit N Einträgen ist Θ1 n bei ΘnN∣h∣ Speicher, mit |h| = Platzbedarf, um h zu codieren N Universelle Hashfunktion 1. {0, ..., N­1}{0, ..., n­1} ist universell, aber nit platzsparend repräsentierbar bzw. effizient auswertbar 2. Angenommen, x ∊ U kann in eindeutiger Weise als (r+1)­Tupel x = (x0, ..., xr) mit 0 ≤ xi < N für alle i und eine Primzahl N. (z. B. N = 257 und x wird byteweise gelesen) Für a= (a0, ..., ar) ∊ {a, ..., N­1}r+1 definieren wir die Hashfunktion h0 x =h a Dann ist 0, ..., ar x0, ... , xr = ∑ 0≤x ir r1 H={ha ∨a∈{0,... , N −1} ai x i mod N } universell Martin Lenders Bemerkung: (a) zum Abspeichern von h a (b) Zum Berechnen von h a 0, 0,... , hr wird Θ(r) Platz benötigt. ..., a x 0, ... , xr wird Θ(r) Zeit benötigt r UNIVERSELLE HASHFUNKTION U = {0, ..., u­1} u = Nr+1 N Primzahl (Größe der Hashtabelle) x ∊ U ⇔ (x1, ..., xr) mit 0 ≤ xi < N Zu (a0, ..., ar) ∊ {0, ..., N­1}r+1 definieren wir (mit r+1 generierten Zufallszahlen) ha : { U {0,... , N−1} r x= x 0, ... , x r ↦ ∑ a i⋅xi mod N i=0 Dann ist H= {ha | a∈{0,... N−1}r1 } universelle Bemerkung: 1. Um h ∊ H zufällig zu wählen, müssten r+1 Zufallszahlen im Intervall [0, N­1] erzeugt werden. 2. Um (bei festem h ∊ H) zu x = (x0, ..., xr) h(x) auszurechnen werde r+1 Multplikation (und Additionen) modulo N benötigt Angenommen: x , y ∈U mit x≠ y und h a x=ha y Da x ≠ y muss x0, ... , x r ≠ y 0, ... , y r wir nehmen o.B.f.A. An, dass x0 = y0 r r i=0 i=0 r ha x =h a y ⇔ ∑ ai x i=∑ a i y i mod N a0 x 0−a 0 y 0 =a0 x 0− y 0 =∑ ai y i−x i mod N (darum N Primzahl, sonst keine i=0 inverse Multiplikation) Bei gegebenen x, y gibt jede Wahl von a1, ..., ar auf der rechten Seite eine feste Zahl C. Da N prim ist und x 0≠x y , gibt es genau eine Möglichkeit a0 zu wählen, um die Gleichung zu erfüllen. a0 =C⋅ x 0 – y 0 −1 mod N a0, ... , ar zu wählen r ∣H∣ Damit ∣{ h a | h a x=h y }∣=N = N Es gibt genau Nr Möglichkeiten Martin Lenders ADT GEORDNETES WÖRTERBUCH Ziel: Verwaltung einder Teilmenge S von Elementen aus einem total geordneten Universum U (wir werden die Ordnung mit ≤ bezeichnen) und den Operationen für S ⊆ U • find (x), bestimme, ob x ∊ S • insert (x), setze S ← S ∪ {x} • delete(x), setze S ← S \ {x} • min(), bestimme min S • max(), bestimme max S • succ(x): bestimme min {y ∊ S | y ≥ x} • pred(x): bestimme max {y ∊ S | y ≤ x} Bemerkung: Wir nehmen an, dass die Ordnungsrelation in O(1) Zeit entschieden werden kann. IMPLEMENTIERUNG DES ADT GEORDNETES WÖRTERBUCH (1) Suchbäume (→ später) (2) (nach ≤) geordnete (doppelt) verkettete Liste (zeitlich gesehen ineffizient) (3) Skip­Liste SKIP­LISTE • Hierarchische DS mit r Stufen L1, ..., Lr • In Stufen Li ist die Menge Si ⊂ S in einer verketteten Liste gespeichert, wobei S=S 1⊇S 2⊇S3⊇...⊇S r =∅ S = {­∞, 5, 7, 13, 17, 21,26, 39, 42, +∞} neue Listen per „Münzwurf“, ­∞, +∞ immer enthalten . Δ₄ . L4 ↓ ↓ . . Δ₃ . . L3 ↓ ↓ ↓ ↓ . . . Δ₂ . . L2 ↓ ↓ ↓ ↓ ↓ -∞ → 5 → 7 →13 →17 →26 →39 →42 →+∞ L1 25 (gesucht) S4 = ∅ S3 = {5,2,6} S2 = {5,13,26} S1 = S Für z ∊ Si+1 ⊂ Si gibt es einen Zeiger vom Eintrag für z in Li+1 zum Eintrag für z in Li Suche in einer Skip­Liste (find, succ, pred) Suche nax q ∊ U • bestimme in Li das Intervall Δi welches q enthält durch lineare Suche • die lieare Suche in Li beginnt sind Anfangspunkt des Intervals Δi Bestimmung der Si Si = S i = 1 while Si = ∅ Si+1 = {z ∊ Si | Münzwurf für z zeigt Kopf} i = i + 1 r = i Löschen von z ∊ S • Lösche z aus allen Li mit z ∊ Si (inklusive der vertikalen Zeiger) Martin Lenders Einfügen von z ∊ S • werfe eine Münze, bis zum ersten Mal Zahl auftaucht, j = #Münzwürfe – 1 • füge z in die Listen L1, ..., Lj+1 ein. Suche in Li (i = 1, ..., j+1) nach z und bestimme Intervall Δi mit z ∊ Δi ◦ unterteile Δi bei z ◦ verkette die Vorkommen von z in aufeinander folgenden Stufen vertikal • falls j+1 > r erzeuge j+1 – r neue Stufen die nur z enthalten Die Größe der DS (sowie die Zugriffszeit) hängen von der zufälligen Entscheidung ab, die der Algorithmus trifft. Für c > 2 ist Pr rc ‧ logn≤ 1 n (Pr = Wahrscheinlichkeit, c beliebig) c−1 Sei x ∊ S und k ∊ ℕ: 1 2k Pr es gibt mehr als k Stufen =Pr Pr x ist in mehr als k Stufen= ∪ x ist in mehr als k Stufen x ∈S n ≤∑ Pr x ist in mehr als k Stufen= x ∈S Pr es gibt mehr als k Stufen ≤ mit k =c⋅log 2 n≤ n c log 2 n 2 = n 2k n 1 = n−1 c n n ∣S∣ 2k ■ E[Größe der DS]=n Zu x ∊ S betrachten wir Zufallsvarianle hx = #Stufen die x enthalten pi=Pr h x =i i 1 pi 1 2 2 3 1 1 1 ⋅ = 2 2 4 1 8 1 E[h x ]=∑ i⋅Pr h x =i =∑ i⋅ i =2 i≥i i≥1 2 ∑ 1i =1 i≥1 2 d 1 i i−1 i x =∑ i⋅x − ∑ i x ∑ dx x i≥1 i≥1 i≥1 x 1− x 1 ∑ q i= 1−q i≥0 1 ∑ q i= 1−q −1= i≥1 E[Größe der DS]=E [ ∑ h x ]=∑ E[h x ]=2⋅n x∈ S x∈S 2 1−1−q q = 1−q 1−q d x 1 = dx 1−x 1−x 2 Martin Lenders Die erwartete Zeit zum Löschen eines über einem Zeiger identifizierten Elements ist Θ(1) (falls alle Listen doppelt verkettet sind) Man kann zeigen, dass die erwartete Sachzeit Θ(log n) ist. BALANCIERTE MEHRWEG­SUCHBÄUME 3 ( -∞ ] 0 ] ) 1 +∞ 0;1 ≤0 0<x≤1 5 >1 In einem Mehrweg­Suchbaum, der eine Menge S über einem Universum U (mit totaler Ordnung ≤) speichert ist jeder innere Knoten v mit d ≥ 2 Knoten mit d ­ 1 Schlüsseln k1 (v) < ,,, < kd­1(v) aus U vorkommt. Das i­te speichert nur Elemente > ki­1(v) und ≤ ki(v) sind. Wobei k0(v) = ­∞ kd(v) = +∞. Die Elemente aus S werden in den Blättern gespeichert. 10 2;6 1 3 12;16 9 12 13 17 Ein Mehrweg­Suchbaum heißt (2,4)­Baum, falls (1) Jeder innere Knoten (außer der Wurzel) hat ≥ 2 und ≤ 4 Kinder (2) Alle Blätter sind auf der gleichen Tiefe ⇒ Abbildung oben ist ein (2,4)­Baum, genauer: (2,3)­Baum Für die Höhe h eines (2,4)­Baumes mit n Blättern gilt 1 log 2 n=log 4 n≤h≤ log 2 n 2 Mit anderen Worten (2,4)­Bäume sind balanciert Access (x, T) Input: x ∊ U, Suchbaum TS für S ⊂ U Ausgabe: Blatt y ∊ S mit y = min {z ∊ S | z ≥ x} begin v ← Wurzel von T while v ist kein Blatt do Bestimme 1 ≤ i ≤ deg(v) mit ki­1 < x ≤ ki (v) v ← i­tes Kind von v return v Die Prozedur Access terminiert in einem (2, 4)­Baum der Höhe h in O(h) Schritten Allgemeiner: In einem Mehrweg­Suchbaum T der Höhe h terminiert die Prozedur in Schritten. Mittels Access kann in einem (2, 4)­Baum der n Elemente speichert find / succ in O(log n) realisiert werden. Martin Lenders Einfügen von x in einen (2,4)­Baum (i) Einfügen in Knoten vom Grad < 4 ist einfach (ii) Einfügen in Knoten v mit 4 Kindern: ◦ Füge ein wie bei (i) (generiert Knoten mit 5 Kindern). Zerteile diesen in v1;v2 mit 2 bzw. 3 Kindern. Setze das Einfügen (jetzt von v2) kekursiv zur Wurzel fort. v w w x x v1 w v2 x An der Wurzel einfach (erzeuge neue Wurzel über v1,v2) Mit diese Prozedur kann in einem (2, 4)­Baum mit n Elementen in O(log n) Zeit ein neues Element eingefügt werden ◦ Löschen eines Knoten v aus (2,4)­Baum Sei p Mutterknoten von v (i) deg ≥ 3 ⇒ Löschen einfach (ii) deg = 2 (a) Falls ein Nachbar von p mind 3 Kinder hat, stehle eines davon (b) Falls alle Nachbarn von p genau zwei Kinder haben, verschmelzen wir p mit diesem Nachbarn q zu einem Knoten mit 3 Kindern und setzen das Löschen (von q) rekursiv zur Wurzel fort. An der Wurzel einfach: entferne alte Wurzel und mache einziges Kind zu neuen Wurzelliste Mit dieser Prozedur kann in einem (2,4)­Baum der n Elemente speichert in O(log n) Zeit eines der Elemente speichert in O(log n) Zeit eines der Elemente gelöscht werden. ( a, b )­B ÄUME Ein Mehrweg­Suchbaum heißt ­Baum, falls (1) Jeder innere Knoten (außer der Wurzel) hat ≥ und ≤ Kinder (2) Alle Blätter sind auf der gleichen Tiefe Es gilt Tiefe eines (a,b)­Baums (s. Übung) Anwendung: Datenstruktur im Externspeicher 1972 Bayer & McGeight ( ) Blockgröße des Externspeichers • „klassisch“: (2, 3)­Bäume, aber bei (2, 4)­Bäumen sind die amortisierten Kosten für das Löschen eines Baumes (tatsächliche armotisierte Rebalicierungskosten sind • (2,4)­Bäume „=“ Rot­Schwarz­Bäume Armortisierte Rebalancierungskosten in (2,5)­Bäumen Rebalancierungskosten: '­ Löschen: Anzahl der Knotenverschmelzungen '­ Einfügen: Anzahl der Knotenspaltungen Es gilt: Wenn in einem anfangs leeren (2,5)­Baum eine Folge von n Einfüge­/Löschoperationen durchführe, sind die gesamten Rebalancierungskosten (d. h. die amortisierten Rebalancierungskosten sind ) Martin Lenders Wir zeigen das mittels der Buchhaltermethode: • Wir halten folgende Invariante aufrecht: Knoten vom Grad 1 speichern ≥ • 2 3 4 5 6 3,00 € 1,00 € 0,00 € 0,00 € 1,00 € 3,00 € Jede Operation bringt 2 € mit. Wenn jede Operation 2 € mitbringt, können wir • die Rebalancierungskosten bezahlen • die Invariante aufrecht erhalten Einfügen: 1. Falls keine Rebalancierung, speichere (ggf.) 1 € bei dem betroffenen Knoten, fertig 2. v hat 5 Kinder, x wird an v angefügt (bringt 2 € mit) w 1 € v 2€ V zahlt 1 € für seine Spaltung x w v1 0€ v2 0€ x Löschen 1. wie oben 2. deg v 3 4 5 0 € 0 € 0 € u 1€ v 0/1 € 1€ y deg v 3 4 1 € 0 € 0 € w x +2 € v y 2 u 1€ u w v 1€ 1€ 0 € u,v 2€ 0/1 € +2 € die zum Einfügen an w mitgebracht werden Martin Lenders IMPLEMENTIERUNF VON GEORDNETEN WÖRTERBÜCHERN FÜR ZEICHENKETTEN Trie (retrieval) Bsp: {bear, bell, bid, bug, sell, sock} kein Wort Präfix eines anderen b e a l r s i u d e y l o l c l k Ein (Standard­)Trie für eine Menge S von Zeichenketten über einem endlichen Alphabet ist ein gewurzelter, geordneter Baum bei dem • jede Kante mit einem Symbol ∊ beschriftet ist • die Kinder eines Knotens (entsprechend der Kantenbeschriftung) geordnet sind • die Kantenbeschriftungen von der Wurzel zu den Blätter ergeben die Wörter aus S Ein Trie benötigt Platz. Suchen, Löschen & Einfügen von benötigt ((2,4)­Baum der Kinder eines Knotens ordnet) b e ar bzw. Zeit s id uy ell ock ll Kompremierter Trie entsteht aus Standard­Trie durch Kompression von Ketten von Knoten mit Ausgrad = 1 zu einer Kante {bear, bell, bid, bug, sell, sock} S[5] b e ar id uy s (5, 1, 3) ell ock ll "toch" = S[5][1..3] Kompakte Darstellung: Kantenbeschreibung nicht explizit, sondern nur als Zeiger in die entsprechenden Strings von Martin Lenders Anwendung: String­Patternmatching Gegeben: Finde: alle Position d. h. und an denen in vorkommt Bsp: Naiv: mississippi is M kommt in T an Positionen 2 & 5 vor. Teste, ob Eigenschaft (*) für i = 1, 2, ..., n­m+1 gilt schematisch: 1 2 ... m m+1 ... n T 1 ... m M Laufzeit: Beobachtung: M kommt an Pos. i in T vor gdw. M Präfix von T[i, ... n] i­te Suffix von T d. h. um alle Vorkommen von M in T zu finden, muss ich alle Suffize von T finden die mit M anfangen Speichere alle Sufizes von T in einem komprimierten Trie. Platzbedarf: , kann in Zeit konstruirt werden (ohne Beweis) „Suffixbaum“ für T Bsp: T = missippi$ S(T) = {mississipi$, ississipi$, ssissipi$, sissipi$, ..., ppi$, pi$, i$, $} |S(T)| = 12 im allgemeinen: T = ssippi$ s (1,1) p sippi$ (3,7) ippi$ (2,7) pi$ i$ i $ (7,7) ppi$ $ Baum mit n Blättern alle inneren Knoten Ausgrad > 1, d. h. insgesamt Knoten / Kanten Suche nach M in T ≙ Suche nach den Einträgen im Suffixbaum die M als Präfix haben. Laufzeit: Martin Lenders SUFFIX­ARRAY $ < ab$ < abcab$ < b$ < bcab$ < cab$ $ < a < b < c (lexikografische Ordnung auf {$,a,b,c}*) T = abcab$ = T1 bcab$ = T2 cab$ = T3 ab$ = T4 b$ = T5 $ = T6 T 6T 4T 1T 5T 2T 3 M = ab M $ ab ab$abcab$ ... SEQUENCE­ALLIGNMENT PROBLEM s. Google: „Meinten Sie ...“ Eingabe: ocurance ↕ occurence Zu zwei Strings: X=x1 ... xm oc­urance ||||||||| occurence (Überkreuzung der Entsprechungen nicht erlaubt) x1 x2 x3 ... xm \ \ | y1 y2 y3 ... yn M = {(1,2),(2,3), ..., (m,n)} Y=y1 ... yn Eine Folge M von Paaren von Indizes aus {1, ..., m} {1, ..., n} bei der kein Index zweimal vorkommt heißt ein Matching (Zuordnung) zwischen X und Y Ein Matching M heißt Alignment falls (i,j);(i',j') M und i < i' ⇒ j < j' („keine Kreuzung“) Bsp. X = stop Y = tops stop­ :|||: ­tops stop |||| tops Güte (Kosten) eines Alignments Parameter: (1) > 0 (Lückenkosten) Für jede Position von X oder Y die nicht zugeordnet ist zahlen wir . (2) für p, q gibt es Fehlerkosten . Falls (i,j) \in M zahlen wir M =i1, j 1 ,... ,i k , j k Alignment von X =x1, ... , x m und Y = y 1, ... , y n k │ m­k ─────┴───── m ||||| ─────┬───── n k │ n­k Martin Lenders Alignment­Problem: Gegeben X, Y berechne Alignment M mit minimalen Kosten 1. Für ein Alignment M von X, Y gilt: oder 2. In einem Alignment M von X und Y mit ist entweder die letzte Position von X oder von Y nicht zugeordnet (da Alignment kreuzungsfrei). OPT(i,j): Kosten des besten Alignments von (wir wollen OPT(m,n)) X =x 1, x 2, ... , x m ∋ Y x m X i =x1, ... , x i und Y j = y 1, ..., y j , yn = y 1, y 2, ..., y n +δ (*) als Rekursion: m=6 n=6 5, 5 4,4 5, 6 6,5 4,5 5,4 4,5 5,5 4,6 5,4 5,5 → Mehrfachauswertung, d. h. schlecht 6,4 wir suchen OPT(m,n) Berechnen von OPT(m,n): 1. rekursiv gemäß (*): schlechte Idee, exponentiell (in max(m,n)) großen Rekursionsbaum; viele Teilproblem tauchen dann mehrfach auf. 2. Zwischenspeichern („Caching“) von Teilergebnissen in einer Tabelle mit m ∙ n Einträgen 0 1 ... i­1 i j­1 x x j x (*) 0 j+1 n i+1 ... m­1 m Martin Lenders Beispiel: =2 1, 1, = 3, 0, { Vokal-Vokal Konsonant-Konsonant Vokal-Konsonant x-x 0 0 1 2 3 4 n a m e 0 2 4 6 8 0 2 1 n 2 2 a 4 3 e 6 4 m 8 n n Zeilenweise Ausfüllen der Tabelle Algorithmus Align (X,Y) Eingabe: X = x1, ..., xm, Y = y1, ..., yn Ausgabe: Kosten des günstigsten Alignments von X und Y for i = 0 to m do OPT[i, 0] = i ∙ δ for j = 0 to m do OPT[0, j] = j ∙ δ for j = 0 to m do for i = 0 to m do OPT[i,j] = min { OPT[i­1,j­1]+ }, OPT[i­1,j]+δ, OPT[i,j­1]+δ } return OPT[m,n] Analyse: Laufzeit: m⋅n Speicherplatzbedarf: m⋅n Bemerkung: Platzbedarf kann auf min m, n verbessert werden: Ausfüllen der j­ten Zeile benötigt aus OPT nur den Inhalt der (j­1)­ten Zeile 3. X = x1x2x3 Y = y1y2y3y4 X3 3,0 3,4 x 3, X2 X1 2,0 2,1 δ δ 1,0 δ 1,1 x δ 2, 1,2 1, 0,0 0,1 0,2 y3 1,3 x 0 y4 2,3 1,4 y3 0,3 0,4 Y1 Y2 Y3 Y4 0 definiere gerichteten, gewichteten Graphen mit Kanten auf Knotenmenge Martin Lenders seien die Kosten eines kürzesten Weges in Dann gilt für alle i, j: f(i,j) = OPT(i,j) von (0,0) nach (i,j) Bemerkung: „Meinten Sie...“ bei Google funktioniert ungefähr wie eine Mischung aus Alignment und Suffix­ Bäume Algorithmen auf Graphen GRUNDBEGRIFFE AUS DER GRAPHENTHEORIE Ein gerichteter Graph G = (V, E) besteht aus einer Menge V von Knoten und einer Menge Kanten zwischen den Knoten. von Bem.: E entspricht einer 2­stelligen Relation auf V z. B. G = (V, E) V = (x, y) E ⇔ x | y 1 2 (2,4) ∊ E 3 4 5 6 7 Illustration von gerichteten Graphen: u v (u,v) ∊ E u, v heißen die Endpunkte der Kante e = (u,v) G heißt ungerichtet, falls ∀x, y ∊ V: (x, y) ∊ E ⇒ (x,y) ∊ E üblicherweise fassen wir bei ungerichteten Graphen die Knotenmenge als Teilmenge von {x} bzw (x,x) „Schleifen“ (engl. „self­loops“) Bei gewichteten Graphen gibt es eine Funktion w: E → ℝ (Gewichtsfunktion). Für e ∊ E heißt w(e) das Gewicht (manchmal Länge oder Kosten) von e. Eine Folge (v1, ..., vm) von Knoten aus V mit (vi,vi+1) ∊ E für (1 ≤ i m) heißt Weg π von v1 nach vm Die Kosten von π sind Bemerkung: ist ebenfalls ein Weg π heißt einfach, falls vi ≠ vj, ∀ i ≠ j Martin Lenders Falls v1 = vm heißt π Kreis Falls es zu allen u, v ∊ V einen Weg von u nach v gibt, heißt G stark zusammenhängend (bzw. zusammenhängend falls G ungerichtet ist) Beobachtung: G =(V,E) zusammenhängend ⇒ |E| ≥ |V| ­ 1 REPRÄSENTATION VON GRAPHEN 1. Adjazenzlistendarstellung mit repräsentier durch Listen Platzbedarrf: 2. Adjezenzmatrixdarstellung ­Matrix , Platzbedarf DER ADT GRAPH Benötigte Operationen (1) knoten(): (2) kanten(): (3) adjazenzliste(u): (4) loescheKante(u,v): (5) erzeugeKante(u,v): (6) gewicht(u,v) (7) setzeGewicht(u,v,w'): (8) istKante(u,v): (9) (10) (11) (12) (13) (14) liefert Liste der Knoten des Graphentheorie dto. Für die Kanten liefert die Liste aller zu u adjazenten Knoten entferne die Kante (u, v) aus E füge die Kante (u,v) zu E hinzu liefere w(u,v) setze w(u,v) := w' liefert 1, falls (u, v) ∊ E 0, sonst knotenInfo(u): liefert die zu u assoziierten Informationen kantenInfo(u,v): dto. Für Kante (u, v) setzeKnotenInfo(v,o) setze die zu v assoziierte Info auf o setzeKantenInfo(u,v,o) dto. für (u,v) loescheKnoten(u): entferne u aus V und entferen (u,v),(v,u) ∊ E (für alle v) erzeugeKnoten(u): füge u zu V hinzu Laufzeitanalyse: Operationen (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) A­Liste A­Matrix Martin Lenders (11) (12) (13) (14) GRAPHENALGORITHMEN KÜRZESTE WEGE IN GRAPHE G = (V, E) gerichteter Graphentheorie mit V = {1, ..., n} Ges.: 4 Gewichtsfunktion w: E → ℝ+ Abstandsfunktion δ: V ⨉ V → ℝ+ mit δ(i,j) = Länge des kürzesten Weges von i nach j Bemerkung: 2 0,8 3 1,2 1 127,3 0,5 6 Falls es keinen Weg von i nach j in G gibt so ist δ(i, j) = ∞ 5 Weg von 1 (über 3 und 2) zum Knoten 5 Länge = 1,2 + 0,8 +127,3 = 129,3 Eigenschaften kürzester Wege 1. Da es keine negativen Kreise in G gibt, ist jeder kürzester Weg einfach i l j 2. Teilmengen von kürzesten Wegen sind ebenfalls kürzeste Wege 3. wenn l der grösste Knoten auf dem kürzesten Weg von i nach j ist, dann zerfällt dieser in 2 Teilwege (selbst wieder kürzeste Wege) auf denen l nicht liegt Für sei Länge des kürzesten Weges von i nach j deren Zwischenknoten alle aus der Menge sind Für Für j π i Knotenwerte ≤ k k liegt nicht auf π k liegt auf π k i π j i ≤ k-1 ≤ k-1 Länge also: π Länge , für j Martin Lenders Bottom­Up Berechnung der for i = 1 to n for j = 1 to n <­ (i,j) ∊ E ? w(i,j) : +∞ for k = 1 to n for i = 1 to n for j = 1 to n = min( + , ) return Laufzeit O(n³) Floyd­Warshall­Algorithmus Platzproblem O(n³) (O(n²) möglich) All­pairs­shortest­path­problem Bem.: Kürzeste Wege können ebenfalls berechnet werden Sehr unintuitiv, andere Variante: j i = Länge des kürzesten Weges von i nach j der höchstens m Kanten enthält k , j) w(k en Kant ≤ m-1 j i nten ≤ m Ka Es gilt ibs. ­Matrix ­Matrix wobei wir annehmen, dass falls wie oben Algorithmus, um D^(m) aus D^(m­1) und W zu berechnen: Algorithmus Naechster (D(m­1), W) Initialisiere n ⨉ n­Matrix D(m) = for i = 1 to n do for j = 1 to n do = ∞ for k = 1 to n do = min ( (m) return D Laufzeit von Naechster Berechnung von δ mittels Naechster: Initialisiere D(0) for m = 1 to n­1 + w(k,j)) Martin Lenders D(m) = Naechster (D(m­1), W) return D(n­1) Laufzeit: Zusammenhang mit Matrixmultiplikation ­Matrizen mit d. h. mit Wenn ◈ die Matrixmultiplikation gemäß der Vorschrift (*) bezeichnet, so gilt D(m) = D(m­1) ◈ W Der obige Algorithmus berechnet D(0) D(1) = D(0) ◈ W = W ◈ W = W2 D(2) = D(0) ◈ W ◈ W = W ◈ W ◈ W = W3 D(n­1) = Wn­1 Statt iterativ können wir D^{(n­1)} aus W auch durch schnelle Exponentation berechnen D(2) = W2 = Naechster(W,W) D(4) = W4 = Naechster(D(2),D(2)) D(8) = W8 = Naechster(D(4),D(4)) Fazit: Aufrufe von Naechster: Gesamtlaufzeit: DAS SINGLE­SOURCE­SHORTEST­PATH­PROBLEM Geg.: Gewichteter, gerichteter Graph G = (V, E) (Gewichtsfunktion w: E → ℝ≥0), Startknoten s ∊ V Ges.: δ(i) := Länge des kürzesten Weges von s nach i (für alle i ∊ V) Die folgenden Algorithmen speichern für jeden Knoten einen Wert di der als Abschätzung für δ(i) dient. Dabei ist stets di ≥ δ(i) und es gibt in G stets einen Weg von s nach i der Länge di. Am Ende wird di = δ(i) für alle sein (das ist natürlich zu zeigen). Martin Lenders Relaxation einer Kante (u,v) ∊ E du u w(u,v) dv v s Prozedur RELAX(u,v) Eigenschaften der Relaxation angenommen wir starten mit (0) Die d­Werte werden nie größer (unter Relaxierung) (1) Unmittelbar nach RELAX(u,v) gilt dv ≤ du + w(u,v) (2) Für jede Folge von RELAX­Operationen ist δ(v) ≤ dv, ∀v ∊ V Beweis: (i) (*) gilt am Anfang der Folge (ii) Angenommen, ist nicht wahr. Sei v der erste Knoten, bei dem die Aufruf RELAX(u,v) dazu führt, dass dv < δ(v) gilt. Unmittelbar nach RELAX(u,v) gilt: dv = du + w(u,v), dv δ(v) ≤ δ(v) + w(u,v) u v also du + w(u,v) < δ(u) + w(u,v) ⇔d u < δ(u) s Also muss auch du < δ(u) vor RELAX(u,v) gelten (da RELAX(u,v) du nicht bearbeitet). Dies ist ein Widerspruch zur Annahme, dass v der erste Knoten ist bei dem (*) passiert. ∎ (3) Gilt zu einem Zeitpunkt δ(v) = dv, so bleibt diese Eigenschaft bei (weiteren) RELAX­Operationen erhalten (dies folgt aus (0) und (2)) (4) Für einen kürzesten Weg (mit ) s u v der die Kante (u,v) enthält gilt: Wenn du = δ(u), dann gilt nach RELAX(u,v) auch dv = δ(v) du = δ(u) u v RELAX(u,v) π s s π u v Kürzester Pfad von s über u nach v mit RELAX(u,v) (5) Für einen kürzesten Weg s → v1 → v2 → ... → vk gilt: jede Folge von Relaxierungen die die Teilfolge (RELAX(s,v1), RELAX(v2,v3), RELAX(vk­1,vk)) enthält, führt dazu, dass für (Folgt ais (4) mit Induktion) Martin Lenders 7 5 s 4 2 1 3 1. RELAX (s,1) → d1 = δ(1) 2. RELAX (3,7) 4. RELAX (1,2) → d2 = δ(2) 3. RELAX (2,4) 5. RELAX (2,3) → d3 = δ(3) 6. RELAX (3,4) → d4 = δ(4) 1 2 3 4 5 6 s→1 1→2 2→3 3→4 R(5,7) R(1,2) R(2,4) R(2,3) |E| Relaxierungen R(3,4) (|V|­1)­mal R(7,2) R(5,5) R(5,1) ALGORITHMUS VON BELLMAN­FORD Input: G = ({1,...,n}, E), w, s Output: Array d mit d[i] = δ(i) for i = 1 to n d[i] := +∞ d[s] := 0 for i = 1 to n­1 for all (u,v) ∊ E RELAX(u,v) return d Korrektheit: mit (5) Laufzeit: rot = kürzester Weg Martin Lenders DER ALGORITHMUS VON DIJKSTRA (zur Berechnung kürzester Wege) Geg.: G = (V, E) gerichtet Kantengewichte w: E → ℝ≥0 (≥ 0 wichtig!!) s ∊ V (Startknoten) Ges.: δ(i) := Abstand von i ∊ V zu s (für alle i ∊ V) ist ein sogenannter greedy (gieriger) Algorithmus. Funktioniert im Grunde wie Bellman­Ford, jedoch in einer anderer Reihenfolge 11 δ=∞ s s 17 δ=0 12 11 5 12 δ=∞ δ=0 δ = 11 5 17 δ = 17 1 1 7 δ=∞ 7 δ = 12 δ=∞ 11 δ = 11 s 12 δ=∞ 11 5 s 17 δ=0 12 δ = 16 δ=0 δ = 17 11 s 12 δ=0 δ = 11 5 17 δ = 16 1 δ = 12 7 δ = 17 17 1 δ = 12 7 5 δ = 16 1 δ = 12 δ = 11 7 δ=∞ Martin Lenders for i = 1 to n d[i] := +∞ d[s] := 0 initialisiere PWS Q mit V, geordnet nach d­Werten B := ∅ while Q ≠ ∅ u := extractMin(Q) B := B ∪ {u} // Sortierung bleibt aufrecht!! // im eigentlichen Code nicht benötigt // rein argumentativ for all (u,v) ∊ E RELAX(u,v) return d[]; Korrektheit: Wir zeigen wenn die Anweisung „B := B ∪ {u}“ ausgeführt wird, dann ist d[u] = δ(u). Beweis durch Widerspruch: Leicht zu sehen: d[s] = δ(s) wenn s (als Erster) in B aufgenommen wird. Sei u der erste Knoten, den der Algorithmus in B aufnimmt, wobei d[u] > δ(u) (s ≠ u) u B s Kürzester Weg s ↝ u x y B ist nicht leer bevor u eingefügt wird. p: kürzester Weg s ↝ u (p geht von s ∊ B zu u ∉ B) d. h. es existierty ∊ V: 1. Knoten auf p nicht in B x ∊ V: Vorgänger von y auf p (∊ B) Wenn u in B eingefügt wird, gilt 1. δ(x) = d[x] (weil u der erste Knoten ist, dessen d­Wert ≠ δ­Wert, wenn er blau wird) 2. δ(y) = d[y] (wegen (1) & der Relaxierung von x → y bei der Aufnahme von x in B) Damit: d[y] = δ(y) ≤ δ(u) (y liegt vor u auf p & alle Kantengewichte ≥ 0) ≤ d[u] (gilt immer) Da u := extractMin(Q) und y ∊ Q d[y] ≥ d[u] Also d[y] = δ(y) ≤ δ(u) ≤ d[u] ≤ d[y] damit d[y] = δ(y) = δ(u) = d[u] ↯ zur Annahme (*) „Darauf müssen Sie nicht selber kommen ...“ ;­) |E| Kantenrelaxierungen (d­Update & decreaseKey) Insg. O((|E| + |V|) ∙ log2|V|) O(|E| ∙ log2|V|) |V| x ExtractMin O(|V| ∙ log2|V|) Martin Lenders GREEDY ALGORITHMEN Schlechtes Beispiel: suche kürzesten Weg von s nach t. Strategie: nimm immer die Kante mit dem geringsten Gewicht 100000 1 s 100 1 t BERECHNUNG VON MINIMAL AUFSPANNENDE BÄUME (minimum spanning tree – MST) geg.: Ungerichteter, gewichteter, zusammenhängender Graph G = (V,E); w: E → ℝ ges.: M ⊂ E mit der Eigenschaft, dass (i) (V,M) ist zusammenhängend („M spannt G auf“) (ii) ist minimal Bsp.: 3 2 M: w(M) = 1+ 2+ 4 + 3 + 5 = 15 1 6 4 3,5 5 7 8 5 9 Beobachtung: M kann keine Kreise haben (mit anderen Worten (V,M) ist ein Baum) Die Algorithmen halten eine Menge A ⊆ E aufrecht, die die Eigenschaft hat, dass es einen MST (V, M) gibt mit A ⊆ M Falls e = {u,v} ∊ E die eigenschaft hat, dass A ∪ {u,v} auch in einem MST von G enhalten ist, nennen wir e sicher für A GENERISCHER MST­ALGORITHMUS: setze A := ∅ solange es Kante e gibt, die sicher für A ist, füge e zu A hinzu return A Korrektheit klar Martin Lenders A {x,a} kreuzt S, {y,u} nicht x {z, b} leichteste Kante über S. y u S ⊂ V = {x,y,z,u,v} z a v MST V\S = {a,b} b Begriffe: (i) Ein Schnitt in G = (V, E) ist eine Zerlegung V = S ⊎ (V\S) (ii) e = {u,v} kreuzt Schnitt S falls u ∊ S; v ∊ V\S (iii) A ⊂ E respektiert Schnitt S gdw. kein e ∊ A kreuzt S Satz: A ⊆ E mit A ⊆ M wobei (V, M) MST A respektiert Schnitt S ⊆ V Bew.: s t {s,t} kreuzt S z b V\S {s,t} ∉ A (A respektiert S) M' = M\{t} ∪ {z,b} ist ein aufspandnender Baum w(M') = + w(z,b) = w(M) mit anderen Worten: M' ist MST Algorithmus (von Kruskal): A = ∅ Bearbeite Kanten e ∊ E sortiert nach w(e) falls aktuelles e keinen Kreis in A erzeugt, setze A := A ∪ {e} sonst verwirf e return A Martin Lenders Vorlesung mit F. Stehn Der folgende Stoff basiert auf 2 Büchern: 1. Data Structures & Algorithms in Java (ISBN: 978­0471738848) 2. The Java Language Spec. java.sun.com/docs/books/jls 0. TYPEN IN JAVA Java ist eine streng typisierte Sprache Definition: Ein Typ oder ein Datentyp ist ein Tupel aus einer Objektmenge und aus auf diese Menge definierte Operationen Typ Primitiver Typ boolean Referenz-Typ Zahlen-Typen Ganzzahlen byte short int long Klassen-Typ Interface-Typ Gleitkommazahlen char float double 1. TYPUMWANDLUNG Definition: Typumwandlung ist das Überführen eines Typs A in einen Typ B 1.1. IDENTISCHE ÜBERFÜHRUNG Jeder Typ A kann in den Typen A überführt werden 1.2. WEITENDE UMWANDLUNG VON PRIMITIVEN byte → short → int → long → float → double char ↗ int i = 1234567890; float f = i; System.err.println(i­(int)f); ≠ 0 1.3 BEGRENZENDE TYPUMWANDLUNG short → byte char → byte ↘ char ↘ short double → float → long → int ↗ byte → char ↘ short float f_min = Float.MIN_VALUE; float f_max = Float.MAX_VALUE; System.out.println((int)f_min + " " + (int)f_max); 0x80000000 0x77FFFFFFFF ­2.145.483.648 System.out.println((short)f_min + " " + (short)f_max); 0 1 Array-Typ Martin Lenders byte → char byte → int → char ⇒ gleichzeitig weitend und begrenzend 1.4 UMWANDLUNGEN VON REFERNZ­DATENTYPEN Von einer weitenden Typumwandlung einer Referenz A in eine Referenz B wird gesprochen, wenn 1. A, B sind Klassen und B Oberklasse von A 2. A, B sind Interfaces und B Oberinterface von A 3. A ist Klasse, B ist ein Interface, das von A implementiert wird Integer i = new Integer(42); Number n = i; Von einer begrenzenden Typumwandlung einer Referenz A in eine Referenz B wird gesprochen, wenn 1. A, B sind Klassen und B Unterklasse von A 2. A, B sind Interfaces und B Unterinterface von A 3. B ist Klasse, A ist ein Interface, das von B implementiert wird Number n = new Integer(42); Integer i = (Integer)n; bei falschem Casting: ClassCastException Autoboxing Integer i = 42; i++; int a = 42 int b = 42 Integer aa = a; Integer bb = b; a==a; // == true b==b; // == true aa == bb; // == true !! (alledings bei Zahlen n ­127 < n < 127, false)