Untitled

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