Algorithmen (und Datenstrukturen)

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