Algorithmen und Programmierung Daniel Waeber Semester 3 Inhaltsverzeichnis 1 Analyse von Algorithmen 1.1 Laufzeit von Algorithmen . . . . . . . . . . . 1.1.1 Experimentelle Analyse . . . . . . . . 1.1.2 Theoretische Analyse von Algorithmen 1.1.3 Pseudo-Code . . . . . . . . . . . . . . 1.1.4 Random Accesss Machsine . . . . . . 1.1.5 Laufzeit eines Algorithmus . . . . . . 1.1.6 Wachstum der Laufzeit . . . . . . . . 1.1.7 O-Notation . . . . . . . . . . . . . . . 1.1.8 O-Notation HOWTO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 5 5 6 6 7 7 7 1.1.9 -Beispiel . . . . . . . . . . 1.1.10 Rekursionsbäume . . . . . . . . . . . . . . 1.1.11 Rekursion in der Registermaschine . . . . Datenstrukturen . . . . . . . . . . . . . . . . . . 1.2.1 Datenstruktur Stack . . . . . . . . . . . . 1.2.2 “Indirekte” Anwendung von Stapeln . . . Dynamisierung Array-basierter Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 9 11 12 12 14 15 2 Implementierung 2.1 Datenabstraktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 Einschränkung der Sichtbarkeit bei der klassenbasierten Datenabstraktion 2.2 Abstrakter Datentype (ADT) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Explizite Schnittstellenbeschreibung . . . . . . . . . . . . . . . . . . . . . 2.3 Anonyme Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4 Polymorphe Typsystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Vererbung (inheritance) Erweiterung (extension) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.2 Schnittstellenvererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.3 Mehrfachvererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.4 Typ-Verträglichkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.5 Verdecken, Ersetzen und Identifizieren von Methoden und Attributen bei Namenskollisionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.6 Abstakte Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.7 Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5 Generizität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5.1 Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 18 18 19 21 21 3 ADTs - Anwendungen - Implementierungen 3.1 ADT Prioritätswarteschlange . . . . . . . . . 3.1.1 we proudly present: Knauers List-Sort . . . 3.1.2 Implementierung: Heap . . . . . . . . 3.1.3 Folgerung/Anwendung . . . . . . . . . 3 31 31 31 31 33 MEGA 1.2 1.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 23 23 24 25 26 27 28 28 3.2 3.3 3.4 3.1.4 Erweiterung des ADT Prioritätswarteschlange . . . . . . . 3.1.5 Binomial Heap . . . . . . . . . . . . . . . . . . . . . . . . Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 ADT Baum . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.2 Implementierung von Bäumen . . . . . . . . . . . . . . . . 3.2.3 Spezialfall - Binäre Bäume . . . . . . . . . . . . . . . . . 3.2.4 Implementierung von Binären Bäumen durch Arrays . . . 3.2.5 Lokalisieren von Einträgen im Datenstrukturen . . . . . . 3.2.6 Algorithmen auf Bäumen . . . . . . . . . . . . . . . . . . 3.2.7 Baumtraversierung . . . . . . . . . . . . . . . . . . . . . . Wörterbücher(Dictonary) . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Abstrakter Datentyp . . . . . . . . . . . . . . . . . . . . . 3.3.2 Implementierung . . . . . . . . . . . . . . . . . . . . . . . 3.3.3 Hashing (Streuspeicherung) . . . . . . . . . . . . . . . . . 3.3.4 Hasing mit Verkettung . . . . . . . . . . . . . . . . . . . . ADT Geordnete Wörterbücher . . . . . . . . . . . . . . . . . . . 3.4.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . 3.4.2 Binäre Suchbäume . . . . . . . . . . . . . . . . . . . . . . 3.4.3 Balancierte Suchbäume (2-4-Bäume, Rot-Schwarz-Bäume) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 33 35 35 36 36 36 36 37 38 38 38 39 39 40 41 42 42 45 Kapitel 1 Analyse von Algorithmen Definition: Ein Algorithmus ist eine Schritt-für-Schritt Anweisung, mit dem ein Problem in endlich vielen Schritten lösbar ist. ⇒ Frage nach Korrektheit und Effizienz des Algorithmus. 1.1 Laufzeit von Algorithmen • im Allgemeinen transformieren Algorithmen eine Eingabe in eine Ausgabe • die Laufzeit eines Algorithmus hängt typischerweise von der Eingabe-Grös̈e ab • die Bestimmung der mittleren Laufzeit eines Algorithmus ist mathematisch oft sehr anspruchsvoll (selbst experimentell schwierig) • Analyse der Laufzeit im schlechtesten Fall (“worst case”) 1.1.1 Experimentelle Analyse • Implementation des Algorithmus. • Messung der Laufzeit des Programms auf Eingaben verschiedener Grös̈e und Struktur. 1.1.2 Theoretische Analyse von Algorithmen Statt einer konkreten Implementierung verwenden wir eine abstraktere Beschreibung. (Pseudo Code) • Charakterisierung der Laufzeit als Funktion der Länge der Eingabe • Dabei werden alle Eingaben der Länge n berücksichtigt 1.1.3 Pseudo-Code • abstrakte Beschreibung eines Algorithmus • “strukturierter” als prosaische Beschreibung • weniger detailierter als Java Programm Algorithm arrayMax ( a , n ) Input : Feld A von n ganzen Zahlen Output : Ein grösstes Element in A currentMax ←A [ 0 ] 5 for i =0 to n−1 do i f A[ i ] > currentMax then currentmax←A[ i ] return currentMax Psedo-Code im Detail: • Kontrollflussanweisungen – for . . . do – while . . . do – repeat . . . until – if . . . then . . . else . . . • Deklarationen • Methodenaufrufe – f(arg1, arg2, . . . ) • Rückgabe – return • Ausdrücke – Zuweisung ← – Gleichheitstest = – ... 1.1.4 Random Accesss Machsine • Rechenkern – arithmetische Ausdrücke +,−,∗,/ – Kontrollflussoperatoren bedingte/unbedingte Sprünge – konstante Zeit pro Operation • linear organisierter Speicher mit wahlfreiem Zugriff – jede Zelle speichert beliebige Zahl – auf Zellen kann über ihre Adresse in konstanter Zeit zugegriffen werden – unendlich gros̈er Speicher • primitive Operationen des Pseudocodes können in konstant vielen RAM-Anweisungen realisiert werden. 1.1.5 Laufzeit eines Algorithmus Definition: A Algorithmus (implementiert auf einer RAM) I Eingabe fuer A TA (I) = Anzahl der elementaren RAM Op. die A auf I durchfuehrt, Laufzeit von A auf I TA (n) = max(TA (I)) (I Eingabe der Groesse n) “worst case” Laufzeit von A Algorithm double ( x ) Input : x ∈ N Output : 2x y←2·x return y LOAD 0,0 MULT 0,2 STORE 0,0 RET Algorithm double ( x ) y←x y←y+x return y Algorithm double ( x ) z←x for i ←1 to x do z←z+1 return z Da jede Zeile in ≤ 5 RAM-Operationen übersetzt werden kann, kann die Laufzeit auch in Pseudocode berechnet werden 1.1.6 Wachstum der Laufzeit Eine vernünftige Änderung des Maschinenmodells verändert die Laufzeit von Algorithmen nur um einen konstanten Faktor. Asymptotische Laufzeit berücksichtigt keine konstanten Faktoren und keine Terme niederer Ordnung. Wir interessieren uns daher für das asymptotische Wachstum von TA (n). 1.1.7 O-Notation Definition: def f (n) ∈ O(g(n)) ⇔ ∃n0 ∃c ≥ 0 : f (n) ≤ c · g(n)∀n ≥ n0 Beispiel: 12n − 4 = O(n) 12n − 4 ≤ 12 · n n2 = O(n) nein Definition: 1.1.8 f = Ω(g) gdw. g = Ω(f ) O-Notation HOWTO 1. Falls f (n) = Pd i=0 ai n i (a d 6= 0) f (n) = O(nd ) 2. Wir sagen 2n = O(n) statt 2n = O(n2 ) 3. Wir sagen O(n) statt O(3n − 6) Beispiel: Gegeben eine Folge von n Zahlen X[0], · · · , X[n − 1]. 1 Berechne die Folge A[0], · · · , A[n − 1], wobei A[i] = i+1 (X[0] + · · · + X[i]) Algorithm P r e f i x A v e r a g e (X, n ) Input : X[0], . . . , X[n − 1] Output : [0], . . . , A[n − 1], mitA[i] = (X[0], . . . , X[i) A← l e e r e s Feld mit n Z e l l e n for i =0 to n−1 sum←0 for j =0 to i sum←sum + X[ j ] A[ i ] ← sum i+1 return A Tpa (n) = O(n + n + n + n(n+1) 2 + n(n+1) 2 // // // // // // // n n n n ∗ (n + 1)/2 n ∗ (n + 1)/2 n 1 // // // // // // 1 1 n−1 n−1 n−1 1 + n + 1) = O(n2 ) Algorithm P r 3 f 1 x 4 v 3 r 4 g 3 (X, n ) Input : X[0], . . . , X[n − 1] Output : [0], . . . , A[n − 1], mitA[i] = (X[0], . . . , X[i) A←Feld mit n Z e l l e n sum←A [ 0 ] for 1 to n−1 sum←sum + X[ i ] A[ i ] ←sum / ( i +1) return A Tp4 (n) = O(1 + 1 + (n − 1) + (n − 1) + (n − 1) + 1) = O(n) 1.1.9 MEGA-Beispiel Wie löse ich ein Problem, wenn ich weis̈, wie ich ein einfacheres Problem löse? Beispiel: n! = n Y i=0 i; n! = n · (n − 1)! n > 0 1 n=0 Algorithm f a c t ( n ) Input : n Output : n! i f ( n=0) then return 1 else return n· f a c t ( n−1) // 1 // 1 // 1 + T (n − 1) Sei Tf act die Laufzeit von f act. Tf act (0) ≤ c Tf act (n) ≤ c + Tf ac (n − 1) Tf act (n) ≤ c + Tf act (n − 1) (1.1) ≤ c + (c + Tf act (n − 2) (1.2) ≤ 3c + Tf act (n − 3) (1.3) ≤ kc + Tf act (n − k) (1.4) (Beweis mit vollständiger Induktion über k ) Tf act (n) ≤ cn + Tf act (0) ≤ cn + c = O(n) 1.1.10 Rekursionsbäume Algorithm Summe(A, n ) Input : Feld A von n Elementen Output : Summe über A i f n=1 return A [ 0 ] else return A[ n]+Summe(A, n−1) C C Aufruf C CW durch C.K. ;) Summe(A,5) C CW Summe(A,4) C CW Summe(A,3) C CW Summe(A,2) - Summe(A,1) Beispiel: Potenzieren p(x, n) = xn wobei x, n ∈ N 1 n=0 p(x, n) = x · p(x, n − 1) n ≥ 1 2n n 2 2n+1 = x(xn )2 Es geht aber besser, da x = (x ) bzw. x 1 n=0 k )2 = p(x, n ) (x n = 2k p(x, n) = 2 n−1 k 2 x(x ) = x · p(x, 2 ) n = 2k + 1 Algorithm Power ( x , n ) Input : x, n ∈ N Output : xn i f n=0 then return 1 e l s e i f n g e r a d e then y←Power ( x , n2 ) return y·y else y←Power ( x , n−1 2 ) return y·y·x • lineare Rekursion • O(log n) C C C CW Power(x,n) C CW Power(x,n/2) C CW Power(x,n/4) C CW ... - Power(x,1) T (x, n) ≤ O(1) + T (x, n2 ) falls n gerade T (x, n) ≤ O(1) + T (x, ⌊ n2 ⌋) falls n ungrade Algorithm Power ( x , n ) Input : x, n ∈ N Output : xn i f n=0 then return 1 e l s e i f n g e r a d e then return Power ( x , n2 ) · Power ( x , n2 ) else y←Power ( x , n−1 2 ) n−1 return Power ( x , n−1 2 ) ·Power ( x , 2 ) ·x • # Op auf Schicht log n: ≥ c · n • # Op auf Schicht log n − 1: ≥ c · n/2 • # Op auf Schicht log n − k: ≥ c · n/2k Insgesamt: ≥ Beispiel: Plog n k=0 c· n 2k =c· Plog n 1 k=0 2k ≥c·n Verfahren zum Summieren der Einträge eines Feldes. Algorithm Sum(A, i , n ) Eingabe : F eldAvonZahlenmitIndexiundAnzahln Ausgabe : A[i] + A[i + 1] + . . . + A[i + n] i f n=1 return A[ i ] return Sum( i , n/2)+Sum( i+n / 2 , ( n +1)/2) P n i Allgemein: # Op ≤ c log i=0 2 = O(n) Beispiel: Fibonacci Zahlen: F0 = 0; F1 = 1; Fi = Fi−1 + Fi−2 Algorithm Fib ( i ) Eingabe i Ausgabe F( i ) i f ( i =0) then return 0 i f ( i =1) then return 1 return f i b ( i −1)+ f i b ( i −2) Schlechte Idee! Sei T (k) die Anzahl der rekursiven Aufrufe von F ib(k) T (0) = 1 T (1) = 1 T (2) = 1 + T (O) + T (1) T (k) = 1 + T (k − 1) + T (k − 2) ≥ Fk−1 Da Fk ≥ 2Fk−2 hat man ein exponentielles Wachstum. F[ 0 ] = 0 F[ 1 ] = 1 F [ i ]= u n d ef für 1 < i < n Algorithm Fib ( i ) Eingabe i Ausgabe F( i ) i f F [ i ]= u n d ef then F [ i ] = Fib ( i −1) + Fib ( i −2) return F [ i ] • Beispiel für dynamisches Programmieren • geht auch direkt über lineare Rekursion → Übung Noch besser: Fi−1 Fi − 2 Fi 1 1 Fi−1 = = · Fi−1 Fi−1 1 0 Fi−2 k d.h. ~vi = M · ~v vi−k i−1 = M · ~ 1 mit ~v1 = ; ~vi = M i−1 · ~v1 0 M i−1 kann mittels sukz. Quadrieren in O(log(i)) berechnet werden! 1.1.11 Rekursion in der Registermaschine Algorithm main x←5 subroutine1 (x) Algorithm s u b r o u t i n e 1 ( i ) k← i +1 subroutine2 (k) Algorithm s u b r o u t i n e 2 ( j ) . . . • merken, welche “Unterroutine” gerade ausgeführt wird subroutine2subroutine16 ? main 6 ? • 6 -? • Die Liste der laufenden Unterroutinen wird auf einem Methodenstapel (nach dem LIFO Prinzip) verwaltet • Beim Aufruf einer neuen Methode legen wir auf den Stapel eine Datensatz der angibt, wer der Aufrufer ist, welche Rücksprungadresse ist zu verwenden und was sind die Werte der lokalen Variablen. ⇒ ermöglicht Rekursion! Speicherabbild: System code Main code Subrotine code 1.2 Datenstrukturen 1.2.1 Datenstruktur Stack • Verwaltung eine Menge von Objekten nach LIFO Prinzip • (im Moment soll der Stack nur ganze Zahlen beinhalten) • erforderliche Operationen einfügen (push) füge das Argument der push-Operation zu Menge verwalteten der Objekte hinzu extrahieren (pop) entfernt und liefert das letzte Element (isEmpty?) liefert true, wenn Stack leer ist (top) liefert das oberste Element ohne es zu entfernen Vorschau auf die algebraische Spezifikation P op(P ush(S, x)) = (S; x) Implementierung von Stacks mittels Feldern (Arrays) x x x top • Allozieren eines Felds F der Größe M • eine Zeigervariable z zeigt auf den ersten freien Eintrag in F • ein push(x) speichert x in F [z] und setzt z ← z + 1 Falls z = M melde Fehler “Stapel voll” • ein pop liefert F [z − 1] und setzt z ← z − 1 Falls z < 0 melde Fehler “Stapel leer” • geht gut, solange nicht das Maximum M erreicht wird class IntArrayStack { int F [ ] ; int z ; int M; I n t A r r a y S t a c k ( int m){ M = m; z = 0; F = new int [M] ; } int pop ( ) throws EmptyStackException { i f ( z == 0) throw . . . return F[−−z ] ; } ... } Analyse: • Linearer (in der Anzahl der in der Struktur verwalteten Objekte (M )) Platzbedarf • O(1) für push + pop • evtl. Ω(M ) Zeit bei Initialisierung Problem/Einschränkung: • Maximalgröße des Stapels muss bei Initialisierung bekannt sein • Implementierungspezifische Ausnahmen! Implementierung von Stacks mittels verketteten Listen • einfach verkettete Liste • einfügen und entfernen am Anfang der Liste effektiv möglich head next next next next data data data data • Einfügen/Löschen am Anfang der Liste in O(1) am besten am 2. Element einfügen und vertauschen, damit Referenzen nicht zerstört werden. • Einfügen/Löschen am Ende der Liste in Θ(n) Implementierung des Stapels Element im Stapel werden in der entsprechenden Reihenfolge in der e.v. Liste gespeichert. Dabei wird das oberste Element im Stapel am Anfang der Liste gespeichert. • Platzbedarf Θ(n) • Laufzeit push/pop/top O(1) 1.2.2 “Indirekte” Anwendung von Stapeln • als Hilfsstruktur in anderen Datenstrukturen • beim Entwurf von Algorithmen Beispiel: ^ | | |<--------------| | <--| |<--| |<--| |<--| | | +---+---+---+---+---+--> Gegeben ist Feld X[0], · · · , X[n − 1] Berechnen für 0 ≤ i ≤ n − 1 S[i] S[i] = maximale Anzahl von aufeinanderfolgenden unmittelbaren Element vor X[i], sodass X[j] ≤ X[i] Algorithm Span1(X,n) Eingabe: Feld X mit n Zahlen Ausgabe: Feld S S <- Neuse Feld von n Zahlen mit 0 for i=0 to n-1 do for j=0 to i do if X[i-j] <= X[i] then S[i]+=1 else break return S Analyse Auf benötigt der Algorithmus Θ(n2 ) Es geht auch besser • Abarbeiten von X von links nach rechts • Wir speichern in einem Stack die Indizes der sichtbaren Elemente • Am Index i – hole Elemente aus dem Stapel, bis X[j] > X[i] (pop) – Setze S[i] = i − j – Lege i auf den Stack (push) Algorithm Span2(X,n) S <- Neues Feld von n Zahlen A <- neuer leerer Stapel for i=0 to n-1 do while(!isEmpty(A) && X[top(A)] <= X[i]) pop(A) if(IsEmpty(A)) then S[i] = i+1 else S[i] = i-top(A) push(A,i) Da nur n-mal gepushed wird, können wir nur ≤ n-mal popen. ⇒ lineare Laufzeit 1.3 Dynamisierung Array-basierter Datenstrukturen Ziel: Stapel mit Arrays implementieren, aber die Größe der Struktur soll beliebig sein. Idee: Wir haben zu jeden Zeitpunkt ein Feld, in dem die Elemente des Stapels abspeichert sind. Wenn der Platz in dem Feld nicht mehr ausreicht, um alle Elemente des Stapels aufzunehmen, dann legen wir ein neues Feld (doppelter Größe) an und kopieren das alte Feld an dessen Anfang. x x x x push x x x x x x push x x x x x x x x x x x x Diese Struktur hat im worst-case ein schlechtes Laufzeitverhalten, da es push-Operationen gibt, mit Ω(n) Laufzeit. (n = # Elemente im Stapel) ABER: Jede Folge von n push/pop Operationen benötigt O(n) Zeit • Eine pop-Operation benötigt im worst-case O(1) Zeit. D.h. alle pop-Operationen Zusammen in einer Folge von n push/pop Operationen benötigen O(n) viel Zeit. • Wir betrachten Folge von n push-Operationen. Kosten 1000 1000 2000 2000 2000 4000 4000 ... 1500 # push 1000 1 1000 1 2000 1 4000 ... 8000 m 1000 2000 ... 4000 ... 8000 ... ... Kosten/Op < 2/op < 2/op < 2/op < 2/op Die 2i -te push-Operation benötigt O(2i ) Alle anderen push-Operationen benötigen O(1) O(2i ) f allsk = 2i mit Tk O(1) sonst Gesamtzeit für n push-Operationen: n X k=0 ⌈log n⌉+1 ⌈log n⌉+1 Tk = ( X k=0 ⌈log n⌉ T2i ) + O(n) = O( X 2i ) + O(n) = k=0 O(2 − 1) + O(n) = O(n) Jede Folge von n push-Operationen erfordert O(n) Laufzeit ⇒ Amortisierte Kosten von O(1) Definition: Gegeben sei eine Datenstruktur, und eine Folge von n Operation, bei der insgesamt m Elemente manipuliert werden. Der Zeitbedarf für diese n Operationen sei T (n, m). Dann die amortisierten Kosten pro Operation. nennen wir T (n,m) n Beispiel: Stack mit Operation mpop(k) k ∈ N entfernt k Elemente vom Stapel. Kosten für mpop(k) sind O(k) Op Kosten für Op. Sparbuch push 1 2 1 push 1 2 2 1 2 3 push push 1 2 4 0 2 mpop(2) 2 push 1 2 3 1 2 4 push 0 1 mpop(3) 3 Amortisierte Kosten der beiden Operationen sind O(1) Beweis: mit Buchhaltermethode Wir nehmen an, eine push-Op kostet 1 e, eine mpop(k)-Operation kostet k e Wir behaupten, dass folgendes “Zahlungsschema” jede Folge von n-Operationen bezahlen kann: • push-Op. erhält 2 e • mpop-Op. erhält 0 e (daraus folgt, das die Gesamtkosten der Folge ≤ n2 e sind. Der Grund ist der, dass mpop(k) nur dann aufgerufen werden kann, wenn ≥ k Elemente vorher mit push auf den Stapel gelegt wurden. Von jeder dieser k push Operationen ist noch 1 e übrig. (mit dem anderen wurde die push-Operation bezahlt) Kapitel 2 Implementierung 2.1 Datenabstraktion Definition: Geheimnisprinzip: Kenntnis der Spezifikation eines Moduls/Systems ist notwendige und hinreichende Voraussetzung für dessen korrekte Benutzung. Beispiel: Prozedurale Abstraktion • reguläre Benutzung einer Prozedur ist ihr Aufruf • zum Aufruf einer Prozedur ist nur die Kenntnis ihrer Signatur nötig • es ist dem Aufrufer nicht möglich auf lokale Variablen zuzugreifen • er muss die Spezifikation kennen Definition: Datenabstraktion ist die Anwendung des Geheimnisprinzips auf die Darstellung komplexere Datenstrukturen: • ein zusammengesetztes Datenobjekt ist nicht direkt manipulierbar • über prozedurale Schnittstelle manipulierbar, die geeignete Operationen zur Verfügung stellt. Beispiel: Implementierung von Stacks mit Feldern. c l a s s S tack { int max = 1 0 0 ; O b j ect [ ] S = new O b j ect [ max ] ; int t =0; void push ( O b j ect o ) throws E xcep tion { i f ( t==max) throw new E xcep tion ( ) ; S [ t++]=o ; } O b j ect pop ( ) { } } direkte Manipulation von Attributen möglich!! S tack s = new S tack ( ) ; s . t++; 17 public c l a s s S tack { private int max = 1 0 0 ; private O b j ect [ ] S = new O b j ect [ max ] ; private int t =0; public void push ( O b j ect o ) throws E xcep tion { i f ( t==max) throw new E xcep tion ( ) ; S [ t++]=o ; } public O b j ect pop ( ) { } } 2.1.1 Einschränkung der Sichtbarkeit bei der klassenbasierten Datenabstraktion • Das Klassenkonzept unterstützt die Zusammenfassung von Daten und den sie manipulierenden Operationen (in einer syntaktischen Form) • In Java kann das Verbergen der internen Repräsentation durch Sichtbarkeitmodifikationen public, protected, private erreicht werden. Sie steuern die Sichtbarkeitbereiche von Methoden und Attributen. public: überall sichtbar protected: innerhalb des eigenen Paketes + in den Unterklassen default: innerhalb des eigenen Paketes private: nur innerhalb der umschließenden Klasse 2.2 Abstrakter Datentype (ADT) Definition: • Menge von Objekten zusammen mit Menge von Operation auf diesen Objekten • Ein Typ, dessen Objekte nur über die Operationen seiner Schnittstelle manipuliert werden können. (und eben nicht durch direkten Zugriff auf die Daten/Attribute) • Üblicherweise gehört zum ADT auch Spezifikation der Fehlerreaktion der Operation Beispiel: ADT Stack • kann beliebeige Objekte speichern • Einfügen/Löschen nach dem LIFO-Prinzip • Operationen: push(object): Füge ein Element ein Object pop(): Entferne und liefere das zuletzt eingefügte Element zurück • Fehlerspezifikation – Stapel-Unterlauf: pop liefert Stapel-Unterlauf, falls es mit leerem Stapel aufgerufen wird. REALE WELT reale Objekte mit realen Operationen Modellierung MODELL abstraktes Objekt Operationen Implementierung DATENSTRUKTUR mit Methoden Vorteile • Implementierung unabhängig vom Modell • Sicherheit: Objekte können nicht in “ungültige” Zustände gebracht werden. • Flexibilität: Code kann unabhängig vom benutzten Code entwickelt werden • Komfort: Benutzer abstrahiert von Repräsentation 2.2.1 Explizite Schnittstellenbeschreibung Zur Schnittstelle von Stack: • die Operationen müssen dem Quelltext der Klasse heraus gesucht werden • die Implementierung der Methoden wird nicht vor dem Benutzer verborgen • Klasse ist nur für de Erzeugung von Objekten nötig, nicht für die Benutzung interface StackI { void push ( O b j ect o ) throws . . . ; O b j ect pop ( ) throws . . . ; O b j ect top ( ) throws . . . ; boolean i s e m p t y ( ) throws . . . ; } • Attribute können nur als Konstanten vereinbart werden • Methoden haben nur einen leeren Rumpf • keine Konstruktoren • Sichtbarkeit ist standardmäßig public public c l a s s ArrayStack implements S tack { private int t =0; private O b j ect [ ] S=new O b j ect [ . . . ] public void push ( O b j ect x ){ ... } } public c l a s s L i s t S t a c k implements S tack { private c l a s s Item { O b j ect v a l u e ; Item n ext ; } public void push ( O b j ect y ){ ... } } Im Programm: • MyStack s = new MyStack(); // Festlegung auf konkrete Implementierung • Stack s = new MyStack(); // zwar korrekt, aber andere Implementierung verwendbar • Stack s = new Stack(); // geht nicht; Pflichten bei Implementierung einer Schnittstelle • Alle Schnittstellen-Methoden müssen public sein. • Alle Schnittstellen-Methoden müssen implementiert werden. • Java unterstützt auch unvollständige(abstract) Klassen Rechte • Parameternamen können beliebig verändert werden. • Ergebnistyp “verkleinern” ist erlaubt. • Exeptions dürfen direkt behandelt werden. (aber keine Zusätzlichen dürfen geworfen werden) • beliebige weitere Attribute und Methoden erlaubt. • Schnittstelle kann als Datentyp verwendet werden (Variablen, Parameter, Ergebnisse), aber keine Objekte davon erzeugen. Satz: Eine Klasse kann auch mehrere Schnittstellen implementieren c l a s s D r e i e c k implements Konvex , Polygon Falls zwei Schnittstellen eine identische Methode deklarieren, wird dies durch eine Implementiert überschrieben. Code-Style: Als Typebezeichner in der Regel Schnittstellentypen verwenden, Klassentypen nur bei Erzeugung mit new verwenden. ⇒ Benutzer weiß nicht einmal wie die Implementierung heißt, kennt nur die Schnittstelle. Beispiel: Integer-Stack void add ( S tack S ) { s . push ( ( I n t e g e r ) s . pop ( ) + ( I n t e g e r ) s . pop ( ) ) ; } Problem Keine Konstruktoren in Schnittstellen Mögliche Lösung. (“abstract factory pattern”) class StackFactory { public s t a t i c S tack c r e a t e S t a c k ( ) { return new MyStack ( ) ; } } 2.3 Anonyme Klassen i n t e r f a c e Funktion { double von ( double x ) ; } c l a s s xq implements Funktion { public double von ( double x ){ return x∗x ; } } Funktion f= new xq ( ) ; f . von ( 3 ) ; new Funktion ( ) { public double von ( double x ){ return 2∗ x } . von ( 5 ) ; 2.4 Polymorphe Typsystem Vielgestaltigkeit Definition: ein und derselbe Prozedurname kann sich auf verschiedene Prozeduren beziehen. In Java: Universelle Poymorphie Universelle Polymorphie Parametrische Polymorphie (Generizitaet) 2.4.1 Einschluss-Polymorphie (Vererbung) Vererbung (inheritance) Erweiterung (extension) Klasse (oder Schnittstelle) Y wird als Erweiterung der Klasse X vereinbart und erbt damit die Eigenschaften von X. // // c l a s s X { Text von X } c l a s s Y extends X { Text von Y } entspricht fast c l a s s Y { Text von X ; Text von Y } Sprechweise: Y heißt Unterklasse/Spezialisierung von X X heißt Oberklasse/Verallgemeinerung von Y c l a s s Konto { int nummer ; int g e l d void e i n z a h l e n ( int menge ) { . . . } boolean abheben ( int menge ) { . . . } } c l a s s V e r z i n s t e s K o n t o extends Konto { int z i n s s a t z void b e r e c h e z i n s ( ) { . . . } } class class class class A B C D {} extends A{} extends B{} extends B{} A B C 2.4.2 // // D Schnittstellenvererbung i n t e r f a c e X { Text von X} i n t e r f a c e Y extends X { Text von Y} entspricht fast i n t e r f a c e Y { Text von X; Text von Y} <<interface>> Y <<interface>> X c l a s s A extends B implements I {} c l a s s C extends A {} // C i m p l e m e n t i e r t somi t auch I 2.4.3 Mehrfachvererbung bei Klassen in Java nicht möglich, bei Schnittstellen schon. i n t e r f a c e I extends X, Y, Z {} 2.4.4 Typ-Verträglichkeit Idee: Ist Y Unterklasse von X, kann ein Y-Objekt wie ein X-Objekt benutzt werden (da es alle Eigenschaften eines X-Objektes hat.) Definition: Wir nennen einen Klassentype Y mit einem Klassentyp X verträglich, wenn Y Unterklasse (direkt oder indirekt) von X ist. Konto a = new V e r z i n s t e s K o n t o ( ) ; a . ein zah len (1000000) a . b e r e c h n e z i n s ( ) ; // g i b t s t a t i s c h e n F e h l e r , da Konto−O b j e k t e d i e Methode n i c h t Definition: Statischer Typ einer Variable ist der vereinbarte Variablentyp Dynamischer Typ einer Variable ist der Typ des Objektes, auf das verwiesen wird. Der dynamisch Typ ist immer mit dem statischen Typ verträglich. Typanpassung (casting) Konto a = new V e r z i n s t e s K o n t o ( ) ; a . einzahlen (a ) ; Verzinsteskonto s = ( Verzinsteskonto ) a ; // s t a t . Type von s i s t V e r z i n s t e s Konto s . berechnezins ( ) ; // k u e r z e r ( ( VerzinstesKonto ) a ) . b erech n ezin s ( ) ; Typkorrektheit bei der Typanpassung: (dynamisch Überprüfung, ggf. ClassCastException) ist Korrekt, wenn der dynamische Typ von x B oder ein Untertyp von B ist. A x; (B) x ; Typkorrektheit bei Vergleichen(Verweisen): ist typkorrekt gdw. der stat. Typ von a ein Untertype von b ist oder wenn einer ein Schnittstellentyp ist. a == b Prüfen des dynamischen Typs x instanceof A • typkorrekt genau dann wenn der stat. Typ von x Untertype von A ist (oder umgekehrt) oder wenn einer ein Schnittstellentyp ist. • liefert true genau dann wenn der dyn. Typ von x ein Untertyp von A ist. Konto a = new V e r z i n s t e s K o n t o ( ) ; Konto b = new Konto ( ) ; a instanceof V e r z i n s t e s K o n t o // t r u e a instanceof Konto // t r u e b instanceof V e r z i n s t e s K o n t o // f a l s e 2.4.5 Verdecken, Ersetzen und Identifizieren von Methoden und Attributen bei Namenskollisionen vertikale Kollisionen : • bei Attributen, Klassen, Schnittstellen: verdecken (hiding); c l a s s A{ int m; } c l a s s B extends A { f l o a t m; } • bei Methoden – verschiedene Signaturen: überladen (overloading) class A { f ( int x ){} } c l a s s B extends A { f ( f l o a t x ) {} } B x = new B ( ) ; x. f (1); // f aus A x. f (1.2); // f aus B – gleiche Signatur: ∗ beide Statisch: verdecken ∗ beide nicht statisch: ersetzen ∗ sonst Fehler zur Übersetzungszeit • Sichtbarkeit verdeckender Namen darf nicht geringer sein, als die der verdeckten, ersetzten Eigenschaften class C { boolean x ; s t a t i c int n ; char y ; s t a t i c void op ( ) { n=1; } } c l a s s D extends C { float n ; // s t a t i c int x ; // char y ; // s t a t i c void op ( ) { n=1; // } } verdeckt s ta t i sc he s int n verdeckt boolean x v e r d e c k t c har y g e h t n i c h t , da n n i c h t s t a t i c Verhindern von Ersetzen/Verdecken • Unterklassenbildung verhindern final class C { . . . }; • verhindert das Ersetzen von op() in Unterklasse f i n a l void op ( ) { } ; c l a s s A{ void op1 ( ) { op2 ( ) ; } void op2 ( ) {} } c l a s s B extends A { void op2 ( ) {} } new B ( ) . op1 ( ) ; // ande re F u n k t i o n a l i t a e t a l s op ( ) von A // v e r h a e l t s i c h ande rs a l s e r w a r t e t Vererbung zwischen Schnittstellen: interface I { String label = ‘ ‘ I ’ ’ ; } i n t e r f a c e J extends I { String label = ‘ ‘ J ’ ’ ; } c l a s s C implements J { p r in t ( label ) // g i b t ‘ ‘ J ’ ’ aus } Verdecken statischer Methoden class A { s t a t i c void p ( ) } c l a s s B extends A { s t a t i c void p ( ) } A a = new B ( ) ; a.p(); new B ( ) . p ( ) ( (A)new B ( ) ) . p ( ) ( (B) a ) . p ( ) ; 2.4.6 { p r i n t ( ”A” ) ; } { p r i n t ( ”B” ) ; } // // // // // statisch A B A B | | | | | nicht stat . B B B B Abstakte Klassen Definition: Eine Klasse heißt abstrakt, wenn mind. eine ihre Methoden nicht implementiert ist. Diese muss, wie die Klasse selbst als abstract deklariert werden. → Instanziierung nicht möglich, aber als Typ bzw. Oberklasse verwendbar. abstract c l a s s Muster { float x , float y ; void bewege ( f l o a t dx , f l o a t dy ){ ... } abstract f l o a t f l a e c h e ( ) ; } c l a s s Quadrat extends Muster { float s ; float f laech e (){ return s ∗ s ; } } c l a s s K r e i s extends Muster { float r ; float f laech e (){ return Math . PI∗ r ∗ r ; } } abstract c l a s s A{ abstract void m1 ( ) ; void op ( ) { . . } ; void op2 ( ) { . . } ; } c l a s s B extends A{ void m1 ( ) { . . . } ; } abstract c l a s s C extends A{ void m1 ( ) { . . . } ; abstract m2 ( ) { . . . } ; } abstract c l a s s D extends A{ void m2 ( ) { . . . } ; } c l a s s E extends A{ void m2 ( ) { . . . } ; } 2.4.7 Object Die Klasse Object aus java.lang ist implizit Oberklasse jeder Klasse. c l a s s O b j ect { public S t r i n g t o S t r i n g ( ) { . . . } ; // T e x t u e l l e B e s c h r e i b u n g public boolean e q u a l s ( O b j ect x ) { . . . } ; // G l e i c h h e i t de r R e f e r e n z e n protected O b j ect c l o n e ( ) { . . . } // l i e f e r t f l a c h e Kopie throws C lon eNotS u p p or ted E xception { . . . } ; protected f i n a l i z e ( ) { . . . } ; // von S p e i c h e r b e r e i n i g u n g a u f g e r u f e n } Psudo-Generizität mittel Object S tack s = new S tack ( ) ; String t = ” teest ” ; s . push ( ( O b j ect ) t ) ; S t r i n g u = ( S t r i n g ) s . pop ( ) ; 2.5 Generizität S tack i n t e g e r S t a c k = new ArrayStack ( ) ; // b i t t e nur f u e r I n t e g e r verwenden i n t e g e r S t a c k . push ( ” abc ” ) ; // i s t e r l a u b t , da O b j e k t ⇒ Typvariablen i n t e r f a c e Stack<T> { void push (T x ) ; T pop ( ) ; } c l a s s MyStack<T> implements Stack<T>{ ... } Stack<I n t e g e r > i S t a c k = new MyStack<I n t e g e r > ( ) ; i S t a c k . push ( ” abc ” ) ; // T y p f e h l e r S t r i n g != I n t e g e r c l a s s C{ s t a t i c <T> T b lah ( S tack <T> s , T t ){ } } C.< I n t e r g e r >b lah (new Stack<I n t e g e r > new I n t e g e r ( 3 ) ) ; C . b lah (new Stack<I n t e g e r > new I n t e g e r ( 3 ) ) // A u tomati sc h mit I n t e g e r a u f g e r u f e n 2.5.1 Eigenschaften iStack hat Type Stack < Integer >. N umber ist Obertyp von Integer, aber Stack < N umber > ist nicht Obertyp von Stack < Integer > Number n = new I n t e g e r ( 4 ) ; Stack<Number> s = new MyStack<I n t e g e r >() // s o n s t wuerden Probleme a u f t a u c h e n : s . push (new Double ( 1 . 2 ) ) ; // F e h l e r Was ist also Obertyp von Stack < Integer >, Stack < String >? Nicht Stack < Object >, sondern Stack <? > ? ist ein Joker und bedeutet unbekannter Typ. null hat den Typ ?. Stack <?> s = new MyStack<S t r i n g > ( ) ; // s . push (new I n t e g e r ( 3 ) ) ; // s . push ( ” abc ” ) ; // s . pop ( ) ; // ok Fehler Fehler E r l a u b t ! Wird e i n O b j e k t Stack <? extends Number> s ; Number n = s . pop ( ) ; // S t a c k von U n t e r t y p Number // ok Stack <? super Number> p . push ( I n t e g e r ( 5 ) ; p . pop ( ) ; // S t a c k mit O b e rtyp Number // Geht // O b j e c t s; Zusaetzliche Schnittstellen sind Erlaubt: <? extends Number & I n t e r f a c e 1 & I n t e r f a c e 2 > Technisch: Java Compiler prüft Typsicherheit und entfernt danach alle Typinformationen. Intern wird Object verwendet. new Vector ( ) ; new Vector<Object > ( ) ; // raw t y p e // b e s s e r , a b e r das g l e i c h e x i n s t e n c e of T; T x = new T ( ) ; T [ ] x = new T [ ] ; T x = (T) y // // // // immer t r u e g e h t auch n i c h t . g e h t auch n i c h t . Warning s t a t i c <T extends O b j ect & Compareable <? super T>> T max( C o l l e c t i o n <? extends T> c ) ; Spaßveranstaltung: Schluss, schon um 15:09 Kapitel 3 ADTs - Anwendungen Implementierungen 3.1 ADT Prioritätswarteschlange Geg: n ganze Zahlen x1 , · · · , xn mit xi + xj ∀i 6= j Ges: Sortierung π : {1, · · · , n} → {1, · · · , n} mit xπ(1) < xπ(2) < · · · < xπ(n) Angenommen, wir haben eine ADT, der eine Menge S von n Zahlen unter folgenden Operationen verwaltet: f indmin() liefert min S deletemin() S → S \ min S init(X) S → X Dann kann X mit |X| = n wie folgt sortiert werden: S . i n i t (X) for i = 1 . . . n do p r i n t S . f in d n m in ( ) ; S . deletemin ( ) ; Die Laufzeit ist T (init(X)) + n(T (f indmin()) + T (deletemin())) 3.1.1 we proudly present: Knauers List-Sort Implimentierung als verkettete Liste 7 12 3 9 13 Sortieren mit dieser Implementierung: O(n) + n(O(n) + O(n)) = O(n2 ) Implimentierung als Folge unsortierter Listen + Folge der Minima dieser Listen 11 3.1.2 7 12 3 9 17 Implementierung: Heap Einfügen • suche den freien Platz • speichere das neue Element an diesem Platz 31 5 13 4 • stelle Ordnung wieder her: 2 2 5 9 6 7 9 2 5 6 7 1 9 1 5 1 7 6 9 5 2 7 6 – wenn wir einen neuen Schlüssel k einfügen, kann die Heap-Ordnung verletzt werden. – wir stellen diese dadurch wieder her, dass wir k auf dem Pfad zur Wurzel mit Schlüsseln vertauschen, die größer sind – da der Heap die Höhe log(n + 1) hat, werden dafür nur O(log n) Schritte benötigt. Löschen aus einem Heap • entferne das Element Wurzel • vertausche zunächst den Schlüssel der Wurzel mit dem Schlüssel des letzten Knotens • herstellen der Heap-Eigenschaft: 2 7 5 9 6 5 7 5 2 9 7 2 9 – vertausche (von der Wurzel ausgehend), das aktuelle Element mit dem m Minimum seiner Kinder, solange k < m – die Anzahl der Vertauschungen ist durch die Höhe beschränkt, also O(log n) Flache Implementierung über Arrays • Wir können einen Heap mit n Elementen in einem Feld der Länge n abspeichern. Für den Knoten mit Index i wird das linke Kind bei Index 2i das rechte Kind bei Index 2i + 1 gespeichrt • das letzte Element ist bei Index n gespeichert, die erste freie Position im Feld ist n + 1 • Dynamisierung durch iteriertes Verdoppeln/Halbieren, sodass man eine amortisierte Laufzeit für insert/deleteM in 1 2*1 2*1+1 2 2*2 2*2+1 k 5 2*k 2*k+1 2k 2k+1 3 3*2 6 3*2+1 7 3.1.3 Folgerung/Anwendung Wenn wir ADT Prioritätswarteschlange mit binären Heaps implementieren, können wir in O(n log n) Zeit sortieren → “Heapsort” • Aufbau des Heaps durch n-faches insert → O(n log n) • Sortieren, in dem man n-mal deleteMin aufruft → O(n log n) 3.1.4 Ziel: Erweiterung des ADT Prioritätswarteschlange effiziente Implementierung der Operation meld(P ,Q) liefert PWS in der alle Objekte der PWS P und Q gespeichert sind Binomialheaps Heaps implementiert mit Binomialbäumen Baum i + 1 entsteht durch das hinzufügen des Baums i an die Wurzel 0 0 0 0 1 2 1 Beispiel: 2 1 4 3 5 3 6 7 Elemente nur in verschiedenen Bäumen 1 5 2 11 4 13 17 15 27 16 29 32 37 • jede Zahl kann man aber nur in log n Baume aufteilen • Bsp: 13 = 23 + 22 + 20 3.1.5 Binomial Heap • Ein Binomialheap für n Elemente speichert diese in einer Folge von Binomialbäumen • Die binomialen Teilbäume sind Heap-geordnet • in dieser Folge gibt es keine zwei Bäume von gleichem Grad ⇒ diese Folge besteht aus O(log n) Bäumen, deren Grade eindeutig durch n bestimmt sind (Binärdarstellung) • die Wurzel der Bäume sind in einer verketteten Liste gespeichert, die nach Grad sortiert wird. Verschmelzen von Binomialheaps P, Q 13 2 7 9 11 19 17 27 1+2+4 =7 2 + 8 = 10 17 16 21 P: P Q P +1 5 8 19 11 23 42 78 Q: (1110)→ (0101)→ (10001)→ • durchlaufe die Wurzellisten von P und Q (angefangen bei kleinstem Grad) • zu jedem Zeitpunkt gibt es maximal einen Baum C, der als Übertrag aus dem vorherigen Schritt weitergereicht wird • sei A der aktuelle Binomialbaum von P ; B der aktuelle von Q 8 > > > > > > > > > >kein C > > > > > > > < > > > > > > > > > >C > > > > > > > : 8 > > > > > <deg(A) = deg(B) > > > deg(A) > > : deg(A) 8 > deg(A) > > > > > <deg(C) deg(C) > > > > > > : deg(C) < deg(B) > deg(B) 8 > <min(B) < min(A) mache A zum Kind der Wurzel von B erzeuge somit einen Bi+1 und füge ihn in die Liste des Ergebnisses > :sonst umgekert schreibe A in die Wurzelliste von P ∪ Q und ersetzte A durch das nächste Element symetrisch n = deg(B) = deg(C) = i schreibe A in die Wurzelliste von P ∪ Q min(C) > min(B) < min(deg(A), deg(B)) = deg(A) < deg(B) schreibe C in die Wurzellist von P ∪ Qes gibt keinen neuen Uebertrag mehr verschmelze C und A und erzeuge damit einen neuen Bi+1 , der als neuer Uebertrag fungiert. Ersetze ferne A durch das naechste Element in der Wurzellist von P analog = deg(B) < deg(A) mache C zum kind von Bsonst umgekerht Das Verschmelzen von zwei Binomialheaps die je n Elemente speichern, kann in O(log n) Zeit erfolgen. → “Binäre Addition” Einfügen in Binomialheap • zum Einfügen eines Elementes x in Binomealheap P erzeugen wir einen Binomialheap Q der Größe 1 mit Element x und verschmelzen diesen mit P . • dies geht also in O(log n) Löschen 3 2 5 1 4 17 13 15 27 16 32 29 47 • Suchen des Minimums in der Wurzelliste (→ O(log n)) • Sei A der Binomialbaum, der das min. speichert. – Entferne A aus dem Binomialheap Dies liefert einen neuen Bin.Heap P – Ist A ein Bin.Baum von Grad i, so sind die Kinder seiner Wurzel Binom. Bäume von Grad 0, 1, · · · , i − 1 – all diese koennen in O(log n) Zeit in einer Wurzellist neue verkettet werden, sodass ein neuer Bin.Heap Q entsteht. – rufe meld(P, Q) auf (→O(log n)) • Insgesamt wird also O(log n) Zeit für deleteM in benötigt. 3.2 Bäume w mathematisch zusammenhängender Graph ohne Kreise infomatisch • gerichteter Graph T = (V, E) • Wurzel w ∈ V • jeder Knoten v ∈ V {W } hat einen Vorgänger p(v) • (u, v) ∈ E ⇔ v heißt Nachfolger von u • Blätter/externe Knoten = Knoten ohne Kinder • Tiefe eines Knoten = Länge des Pfades von w nach v • Höhe (des Baumes) = max. Tiefe eines Knotens • Ordnung auf den Kindern eines Knotens 3.2.1 ADT Baum ADT Knoten • Speichert ein Objekt Item (vom Typ E) • Manipulation/Zugriff durch Methoden: – setItem(E x) – E getItem() • Methoden auf Baum – Zugriff auf Wurzel: Knoten getRoot() – “generische” Methoden int size() boolean isEmpty() List < E > getElements() List < Knoten > getN odes() Knoten getP arent(Knoten p) List < Knoten > getChildren(Knoten p) boolean isLeaf (Knoten p) boolean isRoot(Knoten p) 3.2.2 Implementierung von Bäumen • Knoten u speichert (Referenzen auf) – das mit u assoziierte Objekt (vom Typ E) – Vorgänger von u – die Liste der Kinder von u • Platzbedarf zum Speichern von n Knoten ist O(n) 3.2.3 Spezialfall - Binäre Bäume • Jeder Knoten hat max. 2 Kinder • In einem proper/sauber/wohlen/. . . Binärbaum hat jeder Knoten genau 2 Kindern • Bei geordneten Binärbäumen sprechen wir von linken und rechten Kindern • Bei Implementierung können die Referenzen auf Kinder direkt im Knoten gespeichert werden 3.2.4 1 2 Implementierung von Binären Bäumen durch Arrays 3 6 7 12 13 4 8 9 10 15 1 2 3 5 6 11 12 7 13 14 15 Baum der Höhe h wird in Feld mit 2h+1 − 1 Elementen gespeichert. Die Wurzel wird im 1-ten gespeichert. Das linke (rechte) Kind des Knotens das beim i-ten Eintrag gespeichert ist, wird beim 2i-ten (2i + 1-ten) Eintrag gespeichert Achtung: Schlimmstenfalls wird Ω(2n ) Platz benötigt. 3.2.5 Lokalisieren von Einträgen im Datenstrukturen Prinzipiell können wir die ganz Struktur nach einem Objekt das wir entfernen wollen durchsuchen. → ineffizient Beispiel: für Modifikationen • entfernen eines Elements • erniedrigen der Priorität eines Eintrags in einem PWS Lösung • Einträge in der Datenstruktur wissen wo sie gespeichert sind. PQEntry i n s e r t ( Key k , Item x ) P QEntry speichert: • Priorität des Objektes • Gespeichertes Objekt • Position des Eintrags in der Struktur PQEntry p o PQEntry P O 2 4 3.2.6 1 1 2 3 5 6 Algorithmen auf Bäumen • Arithmetische Ausdrücke mit ganzen Zahlen – binäre Operationen +, −, · – ganze Zahlen rekursiv definiert – x ∈ Z ist ein arithmetischer Ausdruck – e, f arithmetische Ausdrücke ⇒ (e + f ), (e − f ), (e · f ) sind Ausdrücke zu arithmetischen Ausdrücken können wir Ausdrucksbäume definieren ((2+5)-(7*12)) 2 - + * 5 7 12 3.2.7 Baumtraversierung inorder besuche erst alle linken Teilbäume d. Wurzel, dann die Wurzel, dann alles im rechten. preorder erst die Wurzel, dann linken, dann rechten postorder erst linken, dann rechten dann Wurzel Zeichnung von Bäumen use graphviz G E H F A B D A C J B C D E J H G F K I K Knoten A B C D E F G Tiefe 0 1 1 2 2 3 3 inorder Num 8 4 10 2 6 7 5 Dies gibt schon die Koordinaten des Knotens x(v) = inorder-Nummer von v y(v) = Tiefe von v I H I 3 3 3 1 an: J 2 9 K 2 11 Laufzeit Bei allen Traversierungen wird jeder Knoten nur O(1) mal besucht. Dieses Verfahren benötigt daher O(n) Zeit. 3.3 3.3.1 Wörterbücher(Dictonary) Abstrakter Datentyp modelliert eine Ansammlung von Einträgen. Jeder Eintrag besteht aus einem Schlüssel und einem Datum. In dieser Sammlung von Einträgen wollen wir (effizient) nach Einträgen mit gegebenem Schlüssel suchen. (verschiedene Einträge können den gleichen Schlüssel haben.) Beispiel: Telefonbuch, Symboltabelle, DNS-Listen Methoden • f ind(Keyk) Liefert einen Eintrag aus dem Wörterbuch, dessen Schlüssel k ist, falls dieser existiert. • f indAll(Key k) Liefert eine Liste aller Einträge deren Schlüssel k ist. • insert(Key k, Data d) Fügt ein Eintrag mit Schlüssel k und Datum d zu dem Wörterbuch hinzu. • delete(Entry e) Löscht den Eintrag e aus der Struktur. • entries() Liefert Liste mit allen Einträgen. 3.3.2 Implementierung 1. Verkettete Liste Laufzeiten auf WB. mit n Einträgen Platzbedarf Θ(n) find O(n) insert O(1) delete O(1) 3.3.3 Hashing (Streuspeicherung) Eine Hashfunktion h bildet Schlüssel aus einer Menge U (Schlüsseluniversum) in ein Intervall [0, · · · , N − 1] ⊂ N Beispiel: für eine H.fk. N → [0, · · · , N − 1]: h(x) = x mod N h(x) nennt man dem Hashwert des Schlüssels x Eine Hashtabelle für eine Schlüsseluniversum U besteht aus • eine Hfk. h: U → [0, · · · , N ] • einen Array (Hashtabelle der Größe N ) Idee: Eintrag mit Schlüssel k ∈ U wird bei T (h(k)) gespeichert. Beispiel: Hashfunktion für U = Zeichenketten N = 1000 Eine Hfn wird üblicherweise als Komposition von zwei Funktionen dargestellt 1. h1 : U → N 2. h2 : N → [0, · · · , N − 1] Idee: h2 soll die Schlüssel möglichst “gleichmäßig” in [0, · · · , N − 1] verteilen. Oft verwendete Hashcodes • Speicheradressen: interpretiere die Adresse von k als eine ganze Zahl. • Casten auf Integer. (für Schlüssel deren Länge beschränkt ist: float, byte, . . . ) • Für Schlüssel unbeschränkter Länge, Partitionieren in kleine Teile, casten zu int und Aufsummierung Oft verwendete Kompressionsfunktionen • h2 (x) = x mod N dabei wählt man üblicherweise N als Primzahl • h2 (x) = (ax + b) mod N wobei a, b ∈ N mit a 6= 0 mod N 3.3.4 Hasing mit Verkettung Idee Eintrag mit Schlüssel k soll in H.Tab T an Pos h(k) gespeichert werden. Kollision Einträge e1 , e2 mit Schlüssel k1 , k2 mit h(k1 ) = h(k2 ) Lösung Jeder Eintrag T [i] zeigt auf eine verkettete Liste, in der alle Einträge gespeichert sind, mit h(k) = i Universum U und Tabelleneinträge S n := |S| << |U | =: u Hashfunktion h : U → [0, · · · , N − 1] Tabelle T mit N eintragen w 1 2 a b x y u _ h(x) _ N z einfügen/löschen eines Eintrags e mit Schlüssel k • berechne i = h(k) • füge e in L[i] ein / lösche e aus L[i] find (k) • berechne i = h(k) • traversiere L[i] um alle e ∈ L[i] zu finden deren Schlüssel k ist. Analyse • Platzbedarf zum Speichern von n Elementen: O(n + N ) • Laufzeit (bei n gespeicherten Elementen) find: O(n) im worst case! Zu x ∈ U sei CS (x) := |{y ∈ S|h(x) = h(y)}| Beobachtung: Falls h in O(1) Zeit ausgerechnet werden kann, dann ist die Laufzeit von find, insert, delete O(1 + Cs (x)) Bem: Im schlechtesten Fall ist Cs (x) = n Nach dem Schubfach-Prinzip gibt es im Fall n ≥ k · N mindestens ein x ∈ S mit CS (x) ≥ k Mittlere Laufzeit von Hashing h sei fest, S wird zufällig (gleichverteilt) aus U gewählt. Wahrscheinlichkeit W = {S sup U |S| = n} −1 u 1 Die Wahrscheinlichkeit von S sup U ist pS = |W | = n Für festes x ∈ U betrachten wir CS (x)(Zufallsvariable) X 1 X Wir wollen den Erwartungswert E[CS (x)] = pS · CS (x) = CS (x) |W | S∈W S∈W ( 1 y∈S CS (x) = 1= iS (y) mit iS (y) = 0 y∈ /S y∈Sh(x)=h(y) y∈U h(x)=h(y) X X 1 X 1 Damit: E[CS (x)] = = is (y) |W | |W | S∈W y∈U h(x)=h(y) y∈U h(x)=h(y) 1 X iS (y) = E[iS (y)] = P r(iS (y) = 1) · 0 + P r(iS (y) = 0) · 0 = P r(y ∈ S) NR: |W | X X S∈W 0 P r(y ∈ S) = u−1 n−1 @ 0 @ u n Damit: E[CS (x)] = 1 A = 1 n u A X y∈U h(x)=h(y) n · |h−1 (h(x))| = |{y ∈ U |h(x) = h(y)}| u Definition: Eine Hashfunktion h : U → [0, · · · , N − 1] ist fair, wenn für alle i ∈ [0, · · · , N − 1] gilt: |{y ∈ U |h(y) = i}| ≤ ⌈ Nu ⌉ Beispiel: U = {0, · · · , u − 1} h : x → x mod N ist fair Falls h fair ist, gilt E[CS (x)] ≤ nu (1 + u N) = n N Belegungsf aktorβ + n u Zusammenfassung Für eine faire Hashfunktion ist die erwartete Laufzeit der Operationen f ind, insert und delete für ein festes x ∈ U bezüglich einer unter Gleichverteilung gewählten n ) Menge S sup U mit n = |S| O(1 + N Bemerkungen • mit N = Θ(n) erhalten wir O(1) erwartete Laufzeit • falls n a priori nicht bekannt ist, kann durch sukzessives Verdoppeln/Halbieren der Tabellengröße eine amortisierte erwartete Laufzeit erreicht werden. • alternativer Ansatz universelles Hashing 3.4 ADT Geordnete Wörterbücher U ist total geordnet: <U weitere Methoden • f irst() • last() • next(e) • prev(e) 3.4.1 Implementierung Beispiel: 1 3 4 U = N ⊃ S = {17, 3, 12, 1, 4, 7, 9, 13} 7 9 12 13 17 Suchen mittels Binärersuche in O(log n) Nachfolger : Suchen + nächstes Feld O(log n) Vorgaenger : Suchen + nächstes Feld O(log n) Problem: 3.4.2 Einfügen+Löschen erfordert Σ(n) Zeit Binäre Suchbäume Ein binärer Suchbaum für eine Menge S speichert in den inneren Knoten Elemente aus S. Die Blätter (externe Knoten) dienen als Platzhalter. Darüber hinaus gilt die Suchbaumeigenschaft. Bem: Zu einem inneren Knoten u bezeichnet key(u) den bei u gespeicherten Schlüssel. Sei v ein innerer Knoten, u ein innerer Knoten aus dem linken, w ein innerer Knoten aus dem rechten Teilbaum unter v. Dann gilt: key(u) ≤u key(v) ≤u key(w) 6 8 2 9 4 1 Beispiel: Suche Suche nach k ∈ U durchläuft einen Pfad im Suchbaum, ausgehend von der Wurzel. • Welcher Knoten als nächster besucht wird, hängt vom Vergleich zwischen k und dem Schlüssel des aktuellen Knotens u ab. 1. k = key(u) Suche erfolgreich beendet, gib u aus 2. k < Key(u) Such im linken Teilbaum weiter 3. k > Key(u) Such im rechten Teilbaum weiter Einfügen Suche nach dem Schlüssel unter dem wir einfügen wollen Falls die Suche bei einem inneren Knoten terminiert, gibt es bereits einen Eintrag unter diesem Schlüssel. Diesen überschreiben wir. Sonst terminiert die Suche bei einem externen Knoten. In dem Fall expandieren wir diesen in einen inneren Knoten, bei dem wir den neuen Schlüssel speichern (dieser innere Knoten erhält zwei externe Knoten als Kinder) Löschen eines Schlüssels k 1. Suchen Knoten der k speichert Fall 1 u hat mindestens ein externes Kind: Knoten kann entfernt werden und der Teilbaum unter diesem an seine Stelle eingefügt werden. Fall 2 u hat keine externen Kinder • Suche den internen Knoten w, der u in einer Inorder-Traversierung folgt • kopiere die Daten von w nach u • entferne w und sein linkes Kind Bem: – key(w) ist der Nachfolger von k – w kann gefunden werden, indem wir im rechten Teilbaum unter w nach links gehen – das linke Kind von w ist immer ein externer Knoten. Analyse • Platzbedarf zum Speichern von n Elementen: O(n) • Laufzeit für die Operationen proportional zur Höhe des Baumes Höhe von binären Suchbäumen • Im schlechtesten Fall Θ(n) • Analyse der Suchzeit im Mittel Wir betrachten folgendes Experiment: Wähle zufällig unter Gleichverteilung ein Element w, als Wurzel des Suchbaums. Wir bauen auf die gleiche Art für alle Element < w rekursiv einen Suchbaum. 5 1 2 7 4 6 8 3 0 + 1 + 1 + 2 + 2 + 2 + 2 + 3 = 13 Die innere Pfadlänge eines Baumes ist die Summe der Tiefen aller inneren Knoten Sie entspricht der Summe der Suchzeite nach den im dem Baum gespeicherten Schlüsseln. Beispiel: 1 2 0 3 i n 3 7 8 9 1 2 4 5 10 11 6 12 Pk−1 Cn = 0 + 1 + · · · + (n − 1) = Θ(n2 ) Cn = i=0 i · 2i = Θ(k · 2k ) = Θ(n log n) Die mittlere erwartete innere Pfadlänge Cn in einem Baum mit n Knoten: 13 14 − }i) + Ci−1 + Cn−i = (n − 1) Cn = (i| − | {z {z 1} + n links Pn i=1 (Ci−1 rechts + Cn−i ) = n X Ci−1 C0 +C1 +···+Cn −1 1 n |{z} i=1 n X + |i=1 {z } n X (Ci + Cn−i−1 ) Warscheinlichkeit Cn−i =2 |i=1 {z } Cn−1 +Cn−1 +···+C0 Pn i=1 Ci−1 n Cn = (n − 1) + 2X Ci−1 n i=1 n X nCn = (n − 1)n + 2 (n + 1)Cn+1 = n(n + 1) + (3.1) Ci−1 (3.2) i=1 n+1 X Ci−1 (3.3) i=1 (n + 1)Cn+1 − nCn = n(n + 1) − n(n − 1) + 2Cn = 2n + 2Cn (n + 1)Cn+1 = nCn + 2n + 2Cn = (n + 2)Cn + 2n Cn+1 2n Cn + = + 2} + 1} (n + 1)(n + 2) |n {z |n {z Bn+1 Also Bn+1 = Bn + Bn+1 = Bn + 2 n+2 2n (n+1)(n+2) = Bn−1 + ≤ Bn 2(n+1) (n+1)(n+2) 2 n+1 + 2 n+2 = 2 n+2 ≤ B0 + Cn = O(n log n) Pn 2 i=0 i+2 ≤2 Rn 1 0 i+2 di Einschub: Randomisiertes Quicksort Aufgabe: Gegeben S ⊂ N mit |S| = n, sortiere S Algorithmus • Wahle s0 ∈ S zufällig • Teile S auf in S< = {s ∈ S|s < s0 } und S> = {s ∈ S|s > s0 } • Sortiere S< , gib s0 aus, sortiere S> ≤ 2, 8 log n (3.4) (3.5) (3.6) 1 2 3 Beispiel: 4 5 4 6 7 1 Laufzeit n2 Analyse: 2 6 3 5 7 Laufzeit n log n Erwartungswert der Laufzeit (bzgl. aller möglicher Zufallsentscheidungen) Dn : erwartete Laufzeit zum Sortieren eine Menge der Größe n Pn−1 1 Dn ≤ c · n + i=1 n (Di + Dn−1−1 ) Wie vorher ⇒ Dn = O(n log n) 3.4.3 Balancierte Suchbäume (2-4-Bäume, Rot-Schwarz-Bäume) Mehrweg-Suchbäume tur Ein Mehrweg-Suchbaum für eine Schlüsselmenge S hat folgende Struk- • jeder innere Knoten hat d ≥ 2 Kinder und speichert d − 1 Elemente aus S • Für einen internen Knoten, der die Kinder v1 , · · · , vd hat und der die Schlüssel k1 , · · · , kd−1 speichert, gilt: – die Schlüssel im Teilbaum mit Wurzel v1 sind alle < k1 – die Schlüssel im Teilbaum mit Wurzel vd sind alle > kd−1 – die Schlüssel im Teilbaum mit Wurzel vi sind alle > ki−1 und < ki • Die Blätter sind Platzhalter 11 2 6 8 24 15 27 32 30 Bem: • Suchen analog zum binären Fall. Laufzeit ist O(Höhe · Grad) • Inorder Traversierung: analog zu binären Suchbäumen. Laufzeit ist O(n) (2, 4)-Bäume Ein (2, 4)-Baum ist ein Mehrweg Suchbaum, bei dem • jeder interne Knoten ≤ 4 Kinder hat und • alle Blätter die gleiche Tiefe haben Lemma: Die Höhe eines 2-4 Baumes der n Schlüssel speichert ist Θ(log n) Einfügen 10 2 Beispiel: Einfügen 9: 10 2 8 9 8 15 24 12 15 18 27 32 35 24 12 18 27 32 35 27 30 35 30 35 Einfügen 30: 9 2 5 7 10 14 Reparieren: 10 2 8 9 15 12 24 32 18 24 10 2 8 9 12 15 32 18 27 • v1 , · · · , v5 seien Kinder, k1 , · · · , k4 Schlüssel von v • ersetzen v durch v ′ : 3 Knoten mit Schlüssel k1 , k2 , Kinder v1 , v2 , v3 v ′′ : 2 Knoten mit Schlüssel k4 , Kinder v4 , v5 • k3 wird in die Mutter von v eingefügt. (evtl wird dabei eine neue Wurzel erzeugt) • Evtl. kann nun die Mutter von v überlaufen Analyse • Suchen kostet O(log n) • Zerteilen eines Knotens geht in O(1) • Überlaufen propagiert zur Wurzel hin, dh. es gibt nur O(log n) Überlaufe. Dh. Einfügen kann in O(log n) Zeit erfolgen Löschen Betrachte den Fall, dass der Knoten aus dem wir löschen nur Blätter als Kinder hat. (Ansonsten ersetzen wir den zu löschenden Schlüssel durch seinen Vorgänger und entfernen an dessen alter Position). 10 2 8 9 15 24 12 18 27 32 35 Löschen von 9, 27, 35 10 2 8 15 12 24 18 32 Löschen kann einen Unterlauf erzeugen (v wird zu eine 1-Knoten mit 1 Kind und 0 Schlüsseln) 9 2 5 7 10 14