Beispiel - Technische Universität Braunschweig

Algorithmen und Datenstrukturen
Werner Struckmann
Wintersemester 2005/06
1. Der Algorithmenbegriff
1.1 Der intuitive Algorithmenbegriff
1.2 Ein Beispiel: Sortieren durch Einfügen
1.3 Programmiersprachen: der praktische Algorithmenbegriff
1.4 Berechenbarkeit: der formale Algorithmenbegriff
2. Imperative Algorithmen
3. Sortieralgorithmen
4. Listen und abstrakte Datentypen
5. Objektorientierte Algorithmen
6. Bäume
7. Mengen, Verzeichnisse und Hash-Verfahren
8. Graphen
9. Entwurf von Algorithmen
10. Funktionale und deduktive Algorithmen
Der intuitive Algorithmenbegriff
Gegeben sei ein „Problem“. Eine Handlungsvorschrift, deren
mechanisches Befolgen
◮
ohne Verständnis des Problems
◮
mit sinnvollen Eingabedaten
◮
zur Lösung des Problems führt,
wird Algorithmus genannt.
Ein Problem, für dessen Lösung ein Algorithmus existiert, heißt
berechenbar.
1.1 Der intuitive Algorithmenbegriff
1-1
Beispiele für Algorithmen
◮
Zerlegung handwerklicher Arbeiten in einzelne Schritte,
◮
Kochrezepte,
◮
Verfahren zur schriftlichen Multiplikation,
◮
Algorithmus zur Bestimmung des größten gemeinsamen
Teilers zweier natürlicher Zahlen.
1.1 Der intuitive Algorithmenbegriff
1-2
Präzisierung des Begriffs
Ein Algorithmus ist eine wohldefinierte Rechenvorschrift, die eine
(evtl. leere) Menge von Größen als Eingabe verwendet und eine
Menge von Größen als Ausgabe erzeugt.
Ein Algorithmus ist also eine Abfolge von Rechenschritten, die die
Eingabe in die Ausgabe umwandelt.
◮
Der Algorithmus muss durch einen endlichen Text in einer
wohldefinierten Sprache beschrieben sein.
◮
Die Objekte der Berechnung müssen klar sein.
◮
Die Operationen müssen mechanisch ausführbar sein.
◮
Die Reihenfolge der Operationen muss feststehen.
1.1 Der intuitive Algorithmenbegriff
1-3
Eigenschaften von Algorithmen
◮
Terminierend: Für alle korrekten Eingaben hält der
Algorithmus nach endlich vielen Schritten an.
◮
Vollständigkeit: Alle Fälle, die bei korrekten Eingabedaten
auftreten können, werden berücksichtigt.
◮
Determiniert: Der Algorithmus liefert bei jedem Ablauf mit
den gleichen Eingaben das gleiche Ergebnis.
◮
Deterministisch: Der Algorithmus läuft bei jedem Ablauf mit
den gleichen Eingaben durch diesselbe Berechnung.
1.1 Der intuitive Algorithmenbegriff
1-4
Beispiele zu Eigenschaften
Nichtterminierender Algorithmus:
1. Wähle zufällig eine natürliche Zahl.
2. Ist die Zahl gerade, wiederhole ab 1.
3. Ist die Zahl ungerade wiederhole ab 1.
Nicht vollständiger Algorithmus:
1. Wähle zufällig eine Zahl x .
2. Wähle zufällig eine Zahl y .
3. Das Ergebnis ist x /y .
Was ist, wenn y = 0 sein sollte?
1.1 Der intuitive Algorithmenbegriff
1-5
Beispiele zu Eigenschaften
Nicht determinierter Algorithmus:
1. Wähle zufällig eine natürliche Zahl zwischen 260 und 264 .
2. Prüfe, ob die Zahl eine Primzahl ist.
3. Falls nicht, wiederhole ab 1.
Das Ergebnis ist immer eine Primzahl, aber nicht immer die
gleiche, daher ist der Algorithmus nicht determiniert.
Nichtdeterministischer Algorithmus, jedoch determiniert:
1. Mische das Eingabefeld zufällig.
2. Prüfe, ob jedes Element kleiner als das rechts von ihm
stehende ist.
3. Falls nicht, wiederhole ab 1.
1.1 Der intuitive Algorithmenbegriff
1-6
Algorithmen und Datenstrukturen
◮
Algorithmen
◮
◮
◮
◮
◮
◮
elementare Operationen
sequenzielle, bedingte, wiederholte Ausführung
Unterprogramme
die Schritte werden an anderer Stelle beschrieben und sind
mehrfach verwendbar.
Rekursionen
derselbe Algorithmus wird auf ein oder mehrere gleichartige
Teilprobleme angewendet.
„gleichzeitige“ Ausführung
Datenstrukturen
◮
◮
einfache Daten: Zahlen, Zeichen, Wahrheitswerte
komplexe Datenstrukturen: Felder, Listen, Bäume
1.1 Der intuitive Algorithmenbegriff
1-7
Codierung von Daten
◮
Ein Code ist eine (berechenbare) Funktion, die jeder
Zeichenkette einer Urbildmenge eindeutig eine Zeichenkette
aus einer Bildmenge zuordnet.
◮
In technischen Systemen dienen Codes überwiegend der
Darstellung von Nachrichten.
◮
Codierungen ermöglichen beispielsweise die Verwendung von
Nachrichten in Rechnern und die Übertragung von
Nachrichten.
◮
Beispiel „Zahldarstellung in Stellenwertsystemen“:
(22)10 = (112)4 = (10110)2
◮
Beispiel „ASCII-Code zur Darstellung von Zeichen“:
B 7−→ 66 7−→ 1000010
1.1 Der intuitive Algorithmenbegriff
1-8
Sortieren durch Einfügen
◮
◮
Spezifikation: Eine Liste < a1 , a2 , . . . , an > von n ganzen
Zahlen, n ≥ 1, soll aufsteigend sortiert werden.
Objekte: Zahlen, Listen von Zahlen.
◮
Operationen: Vergleiche, Einfügen in Liste, Löschen aus
Liste.
◮
Eingabe: Eine Liste < a1 , a2 , . . . , an > von n Zahlen.
◮
Ausgabe: Eine Permutation < a1′ , a2′ , . . . , an′ > der Eingabe
mit a1′ ≤ a2′ ≤ . . . ≤ an′ .
Die zu sortierenden Zahlen werden auch als Schlüssel bezeichnet.
Beim Sortieren durch Einfügen wird jede Zahl aus der
Ausgangsliste an die richtige Stelle in der Zielliste eingefügt.
1.2 Ein Beispiel: Sortieren durch Einfügen
1-9
Sortieren durch Einfügen
1. Fange beim zweiten Element an.
2. Bewege die Zahl an die richtige Stelle der Liste
3. Schiebe dazu solange Elemente in der Liste nach rechts bis
die richtige Stelle erreicht ist.
4. Wiederhole die Schritte mit dem nächsten Element bis das
Ende der Liste erreicht ist.
5. Die Liste hat jetzt die richtige Sortierung und kann
zurückgegeben werden
1.2 Ein Beispiel: Sortieren durch Einfügen
1-10
Beispiel
5 2 4 6 1 3
2 4 5 6 1 3
2 5 4 6 1 3
1 2 4 5 6 3
2 4 5 6 1 3
1 2 3 4 5 6
1.2 Ein Beispiel: Sortieren durch Einfügen
1-11
Pseudocode
insertionSort(A)
j ← 2;
while j ≤ length(A) do
key ← A[j];
// insert A[j] into the
// sorted sequence A[1 .. j-1]
i ← j - 1;
while i > 0 und A[i] > key do
A[i + 1] ← A[i];
i ← i - 1;
od;
A[i + 1] ← key;
j ← j + 1;
od;
1.2 Ein Beispiel: Sortieren durch Einfügen
1-12
Korrektheit und Komplexität
◮
Korrektheit: Leistet der Algorithmus das Gewünschte? Das
heißt: Sortiert der Algorithmus die Liste und terminiert dann?
◮
Komplexität: Wie viele Rechenschritte und wie viel Speicher
benötigt der Algorithmus?
1.2 Ein Beispiel: Sortieren durch Einfügen
1-13
Schleifeninvariante
In jeder Iteration gilt:
◮
A [1..j − 1] enthält immer die Elemente, die auch vorher dort
lagen.
◮
Die Elemente in diesem Bereich sind sortiert.
Eine Schleifeninvariante
◮
Initialisierung: gilt vor dem ersten Schleifendurchlauf.
◮
Fortsetzung: gilt vor dem n + 1-Schleifendurchlauf, falls sie
vor dem n-Schleifendurchlauf galt.
◮
Terminierung: liefert eine nützliche Bedingung, sobald die
Schleife abbricht.
1.2 Ein Beispiel: Sortieren durch Einfügen
1-14
Schleifeninvariante im Beispiel
Schleifeninvariante: Das Teilfeld A [1..j − 1] besteht aus den
ursprünglich in A [1..j − 1] enthaltenen Elementen in geordneter
Reihenfolge.
Initialisierung: j = 2. A [1] besteht aus dem Element, was vorher
auch schon dort war. Die Liste A [1] ist sortiert.
Fortsetzung: A [j − 1], A [j − 2], . . . werden jeweils nach rechts
verschoben, A [j ] an der richtigen Stelle eingefügt.
Terminierung: j = n + 1 eingesetzt in die Invariante ergibt: A [1..n]
enthält die Elemente, die vorher in A [1..n] enthalten waren, in
geordneter Reihenfolge.
1.2 Ein Beispiel: Sortieren durch Einfügen
1-15
Übersicht
Code
Kosten
insertionSort(A)
j ← 2;
c1
while j ≤ length(A) do
c2
key ← A[j];
c3
i ← j - 1;
c4
while i > 0 und A[i] > key c5
do;
A[i + 1] ← A[i];
c6
c7
i ← i - 1;
od
A[i + 1] ← key;
c8
j ← j + 1;
c9
od;
Anzahl
1
n
n−1
n−1
Pn
j =2 tj
Pn
(tj − 1)
j
=
2
Pn
j =2 (tj − 1)
n−1
n−1
tj : Anzahl, wie oft der Test der inneren Schleife ausgeführt wird
1.2 Ein Beispiel: Sortieren durch Einfügen
1-16
Laufzeit
Die Gesamtlaufzeit ergibt sich zu:
T (n) = c1 + c2 n + c3 (n − 1) + c4 (n − 1) + c5 ·
n
X
tj
j =2
n
n
X
X
+ c6 ·
(tj − 1) + c7 ·
(tj − 1)
j =2
j =2
+ c8 (n − 1) + c9 (n − 1)
1.2 Ein Beispiel: Sortieren durch Einfügen
1-17
Günstigster Fall
Wenn das Feld sortiert ist, gilt immer A[i] ≤ key , also tj = 1.
T (n) = c1 + c2 n + c3 (n − 1) + c4 (n − 1)
+ c5 (n − 1) + c8 (n − 1) + c9 (n − 1)
= an + b = Θ(n)
1.2 Ein Beispiel: Sortieren durch Einfügen
1-18
Ungünstigster Fall
Wenn das Feld umgekehrt sortiert ist, gilt nie A[i] ≤ key , also
tj = j .
T (n) = c1 + c2 n + c3 (n − 1) + c4 (n − 1)
!
1
+ c5 n(n + 1) − 1
2
1
1
+ c6 (n − 1)n + c7 (n − 1)n
2
2
+ c8 (n − 1) + c9 (n − 1)
= an2 + bn + c = Θ(n2 )
j
Mittlerer Fall: tj ≈ 2 .
Ergibt ebenfalls Θ(n2 ).
1.2 Ein Beispiel: Sortieren durch Einfügen
1-19
Programm und Programmiersprache
Ein Programm ist die Formulierung eines Algorithmus und seiner
Datenbereiche in einer Programmiersprache.
Eine Programmiersprache erlaubt es, Algorithmen präzise zu
beschreiben. Insbesondere legt eine Programmiersprache
◮
die elementaren Operationen,
◮
die Möglichkeiten zu ihrer Kombination und
◮
die zulässigen Datenbereiche
eindeutig fest.
Unter „programmieren“ versteht man den Vorgang des Erstellens
eines Programms.
1.3 Programmiersprachen: der praktische Algorithmenbegriff
1-20
Entwicklung der Programmiersprachen
JAVA
1995
93
91
SCHEME−Standard
89
87
C++
85
83
ADA
79
CSP
PROLOG
PASCAL
71
ALGOL68
67
59
Algol 68
◮
Modula-2
◮
Scheme
◮
Scheme/Java
◮
Java
LOGO
69
61
◮
SCHEME
C
73
63
Algol
SMALLTALK80
MODULA2
77
65
◮
OCCAM
81
1975
Programmiersprachen in der
Informatikausbildung
SIMULA
PL/I
BASIC
COBOL
ALGOL
LISP
57
1955
FORTRAN
1.3 Programmiersprachen: der praktische Algorithmenbegriff
1-21
Definition von Programmiersprachen
Unter Semiotik versteht man die Lehre von der Entstehung, dem
Aufbau und der Wirkungsweise von Zeichen und
Zeichenkomplexen. Sie umfasst die folgenden Bereiche.
Die lexikalische Struktur einer Programmiersprache bestimmt die
textuellen Grundbausteine der Programme. Solche Bausteine sind
etwa Schlüsselwörter und Bezeichner. Sie werden zum Beispiel
durch Aufzählung oder reguläre Ausdrücke bestimmt.
Die Syntax einer Programmiersprache beschreibt, wie aus den
Grundbausteinen vollständige Programme gebildet werden
können. In den meisten Fällen wird die Syntax einer
Programmiersprache durch eine kontextfreie Grammatik
festgelegt.
1.3 Programmiersprachen: der praktische Algorithmenbegriff
1-22
Definition von Programmiersprachen
Die Bedeutung der syntaktisch korrekten Programme ist durch die
Semantik der Sprache gegeben. Sie kann beispielsweise mithilfe
von Zustandsfolgen (operationelle Semantik) oder durch
Funktionen, die den syntaktischen Einheiten zugeordnet sind
(denotationale Semantik), definiert werden.
Die Pragmatik einer Programmiersprache untersucht ihre
Anwendbarkeit und Nützlichkeit. Sie gehört nicht zur Definition der
Sprache.
1.3 Programmiersprachen: der praktische Algorithmenbegriff
1-23
Definition von Programmiersprachen
Lexikalische Struktur:
Bezeichner: Buchstabe · (Buchstabe, Ziffer)∗
Schlüsselwörter: while, do, od
Syntax:
<Folge>
<Anweisung>
<Zuweisung>
<While>
::=
::=
::=
::=
<Anweisung> ; <Folge> | <Anweisung>
<Zuweisung> | <While> | ...
<Bezeichner> := <arith. Ausdruck>
while <log. Ausdruck> do <Folge> od
(Operationelle) Semantik:
Eine (partielle) Funktion f, die Zustände auf Zustände abbildet.
1.3 Programmiersprachen: der praktische Algorithmenbegriff
1-24
Klassifikation der Programmiersprachen
Die Programmiersprachen lassen sich grob in drei Klassen
einteilen:
◮
Maschinensprachen
Bits und Bytes, für den menschlichen Leser kaum verständlich
◮
Maschinenorientierte Sprachen (Assembler)
stellen die Befehle in einem Mnemo-Code dar
ADDIC 23, R0
STO R0, #12004
◮
Problemorientierte Sprachen
imperative, funktionale, objektorientierte, deduktive Sprachen
Ein Computer versteht nur Maschinensprachen!
1.3 Programmiersprachen: der praktische Algorithmenbegriff
1-25
Implementierung von Programmiersprachen
Compiler übersetzen Quellprogramme aus problemorientierten
Sprachen in äquivalente Zielprogramme in Maschinensprachen:
cc -o prog prog.c
prog input output
Interpreter lesen das Programm zusammen mit den Eingabedaten
ein und führen es aus:
scm prog.scm input output
Mischverfahren übersetzen das Programm zunächst mit einem
Compiler in eine Zwischensprache. Das übersetzte Programm wird
anschließend interpretiert:
javac prog.java
java prog input output
1.3 Programmiersprachen: der praktische Algorithmenbegriff
1-26
Verarbeitung von Java-Programmen
◮
Zuerst wird ein
Quellprogramm vom
Compiler in Bytecode
übersetzt.
◮
Im zweiten Schritt wird der
Bytecode vom Interpreter
ausgeführt. Der Bytecode
kann als Maschinencode
der sogenannten virtuellen
Java-Maschine angesehen
werden. Bytecode ist
portabel.
Java−Quellprogramm
javac
Java−Bytecode
java
VMfürWindows
java
VMfürLinux
1.3 Programmiersprachen: der praktische Algorithmenbegriff
1-27
Berechenbarkeit/Entscheidbarkeit
◮
Um zu zeigen, dass ein Problem berechenbar ist, kann man
einen Algorithmus angeben.
◮
Gibt es mathematisch beschreibbare Problemstellungen, die
nicht berechenbar sind? Solch ein Nachweis setzt eine
mathematisch exakte Formulierung des Algorithmenbegriffs
voraus.
◮
Es existieren verschiedene Ansätze zur Präzisierung des
Algorithmenbegriffs: Turing-Maschinen, Markov-Algorithmen,
partiell-rekursive Funktionen,. . .
1.4 Berechenbarkeit: der formale Algorithmenbegriff
1-28
Turing-Maschine
◮
Alan M. Turing (1912–1954), britischer Mathematiker:
„Berechenbar“ heißt auf einer Maschine ausführbar.
◮
Turing-Maschine: mathematisches Modell einer
Rechenmaschine.
1.4 Berechenbarkeit: der formale Algorithmenbegriff
1-29
Bestandteile einer Turing-Maschine
◮
beliebig langes Band bestehend aus einzelnen Feldern,
◮
Alphabet B von Zeichen, die in den Feldern gespeichert
werden können,
◮
Lese-/Schreibkopf für genau ein Feld,
Aktionen r , l , s < B ,
◮
◮
◮
◮
◮
Kopf ein Feld nach rechts bewegen: r ,
Kopf ein Feld nach links bewegen: l ,
stoppen: s ,
schreiben von x ∈ B : x ,
◮
Turing-Tafel,
◮
Steuereinheit steuert Bewegungen und Schreibaktionen,
jeweils in einem von endlich vielen Zuständen.
1.4 Berechenbarkeit: der formale Algorithmenbegriff
1-30
Turing-Tafel
Eine Turing-Tafel besteht aus einer Folge von Anweisungen der
Gestalt (z , x , a , z ′ ) mit
◮
z ∈ Z = {0, 1, . . . , m}: Zustand der Maschine vor Ausführung
der Anweisung,
◮
x ∈ B : Zeichen auf dem Band,
◮
◮
a ∈ A = {r , l , s } ∪ B : Aktion,
z ′ ∈ Z ∪ {⊥}: Folgezustand nach Ausführung der Anweisung.
⊥ ist der Endzustand. Für alle z ∈ Z , x ∈ B gibt es genau eine
Anweisung (z , x , a , z ′ ), mit a ∈ A , z ′ ∈ Z ∪ {⊥}. Eine
Turing-Maschine arbeitet also deterministisch.
1.4 Berechenbarkeit: der formale Algorithmenbegriff
1-31
Aktionen
z
a=l
... 1 0 3 ...
a=s
... 1 0 3 ...
a=2
a=r
z′ = ⊥
... 1 0 3 ...
1.4 Berechenbarkeit: der formale Algorithmenbegriff
z′
z′
... 1 0 3 ...
z′
... 1 2 3 ...
1-32
Alternative Darstellung
Zustandsüberführungsdiagramm:
x3 : a3
z0
x0 : a0
z1
x2 : a2
z11
x1 : a1
z2
1.4 Berechenbarkeit: der formale Algorithmenbegriff
...
1-33
Die churchsche These
◮
Eine (partielle) Funktion f : N0 → N0 heißt
turing-berechenbar, wenn es eine Turing-Maschine gibt, die
für jeden Eingabewert n aus dem Definitionsbereich nach
endlich vielen Schritten mit der Bandinschrift f (n) anhält.
◮
Die Turing-Berechenbarkeit ist ein mathematisches Modell zur
Beschreibung von Algorithmen.
◮
These von Church: Der intuitive Begriff „berechenbar“ wird
durch den mathematischen Begriff „turing-berechenbar“
erfasst. Es handelt sich hier um eine prinzipiell nicht
beweisbare These.
◮
Weitere mathematische Modelle (s. oben) erwiesen sich als
äquivalent. Dies ist ein starkes Indiz für die Gültigkeit der
churchschen These.
1.4 Berechenbarkeit: der formale Algorithmenbegriff
1-34
Das Halteproblem
◮
Das Halteproblem für Turing-Maschinen ist nicht entscheidbar
(berechenbar), d. h., es gibt keinen Algorithmus
(Turing-Maschine), der für alle Turing-Maschinen und für alle
möglichen Eingaben entscheidet, ob die Turing-Maschine mit
dieser Eingabe anhält oder nicht.
◮
Dieser Satz schließt nicht aus, dass man für spezielle
Turing-Maschinen entscheiden kann, ob sie halten. Er besagt
lediglich, dass es kein allgemeines Verfahren gibt.
1.4 Berechenbarkeit: der formale Algorithmenbegriff
1-35
1. Der Algorithmenbegriff
2. Imperative Algorithmen
2.1 Variable, Anweisungen und Zustände
2.2 Felder
2.3 Komplexität von Algorithmen
2.4 Korrektheit von Algorithmen
2.5 Konzepte imperativer Sprachen
2.6 Rekursionen
3. Sortieralgorithmen
4. Listen und abstrakte Datentypen
5. Objektorientierte Algorithmen
6. Bäume
7. Mengen, Verzeichnisse und Hash-Verfahren
8. Graphen
9. Entwurf von Algorithmen
10. Funktionale und deduktive Algorithmen
Paradigmen zur Algorithmenbeschreibung
In einem imperativen Algorithmus gibt es Variable, die
verschiedene Werte annehmen können. Die Menge aller Variablen
und ihrer Werte sowie der Programmzähler beschreiben den
Zustand zu einem bestimmten Zeitpunkt. Ein Algorithmus bewirkt
eine Zustandstransformation.
Ein funktionaler Algorithmus formuliert die Berechnung durch
Funktionen. Die Funktionen können rekursiv sein; auch gibt es
Funktionen höherer Ordnung.
2.1 Variable, Anweisungen und Zustände
2-1
Paradigmen zur Algorithmenbeschreibung
In einem objektorientierten Algorithmus werden Datenstrukturen
und Methoden zu einer Klasse zusammengefasst. Von jeder
Klasse können Objekte gemäß der Datenstruktur erstellt und über
die Methoden manipuliert werden.
Ein logischer (deduktiver) Algorithmus führt Berechnungen durch,
indem er aus Fakten und Regeln durch Ableitungen in einem
logischem Kalkül weitere Fakten beweist.
2.1 Variable, Anweisungen und Zustände
2-2
Beispiel: Algorithmus von Euklid
Der folgende, in einer imperativen Programmiersprache formulierte
Algorithmus von Euklid berechnet den größten gemeinsamen
Teiler der Zahlen x , y ∈ N mit x ≥ 0 und y > 0:
a := x;
b := y;
while b
do r :=
a :=
b :=
od
# 0
a mod b;
b;
r
Anschließend gilt a = ggT(x , y ).
2.1 Variable, Anweisungen und Zustände
2-3
Beispiel: Algorithmus von Euklid
Variable
r
a
b
z2
–
36
52
z5
36
52
36
z8
16
36
16
z11
4
16
4
z14
0
4
0
ggT(36, 52) = 4
Durchlaufene Zustände:
z0 , z1 , z2 , . . . , z14
Zustandstransformation:
z0 7−→ z14
2.1 Variable, Anweisungen und Zustände
2-4
Imperative Konzepte
◮
Variable: Abstraktion eines Speicherbereichs, ein Wert eines
gegebenen Datentyps kann gespeichert und beliebig oft
gelesen werden, solange nicht ein neuer Wert gespeichert
und der alte damit überschrieben wird.
◮
Zustand: Abstraktion des Speicherinhalts, Gesamtheit der
momentanen Werte aller Variablen, ändert sich durch die
Ausführung von Anweisungen.
◮
Anweisung: Vorschrift zur Ausführung einer Operation,
ändert im Allgemeinen den Zustand.
2.1 Variable, Anweisungen und Zustände
2-5
Datentypen
◮
Datentyp: Die Zusammenfassung von Wertebereichen und
Operationen zu einer Einheit.
◮
Abstrakter Datentyp: Schwerpunkt liegt auf den
Eigenschaften, die die Operationen und Wertebereiche
besitzen.
◮
Konkreter Datentyp: Beschreibt die Implementierung eines
Datentyps.
2.1 Variable, Anweisungen und Zustände
2-6
Grundlegende Datentypen
Oft werden mathematische Konzepte als Grundlage für einen
Datentyp verwendet. Ein Datentyp besteht aus einem
Wertebereich und einer Menge von Operationen.
◮
bool: die booleschen Werte „wahr“ und „falsch“
Operationen: logische Verknüpfungen, wie zum Beispiel „und“
und „oder“.
◮
int: ganze Zahlen, {minint, · · · , −1, 0, 1, · · · , maxint} ⊆ Z
minint und maxint besitzen etwa den gleichen Betrag.
Operationen: arithmetische und Vergleichsoperationen.
Die Rechengesetze gelten in der Regel nicht, wenn man über
die Darstellungsgrenzen gerät.
2.1 Variable, Anweisungen und Zustände
2-7
Grundlegende Datentypen
◮
real, float: Näherungswerte für reelle Zahlen, dargestellt
durch Gleitpunktzahlen:
r = m · be
m∈Z
Mantisse,
b ∈N
Basis,
e∈Z
Exponent
Die Darstellung erschließt den Zahlenbereich mit konstanter
relativer Genauigkeit.
Operationen: arithmetische und Vergleichsoperationen.
Die Rechengesetze gelten im Allgemeinen nur
näherungsweise.
Gleitpunktzahlen sollten nicht auf Gleichheit überprüft
werden, stattdessen sollte |x − y | < ε für einen kleinen Wert
von ε getestet werden.
2.1 Variable, Anweisungen und Zustände
2-8
Grundlegende Datentypen
◮
char: Zeichen aus einem Alphabet
Mit Vergleichsoperationen und den Funktionen
ord : char → int und chr : int → char, die eine Zuordnung
zwischen den Zeichen aus dem Alphabet und ganzen Zahlen
herstellen.
2.1 Variable, Anweisungen und Zustände
2-9
Variable
◮
◮
Variable x : t: Abstraktion eines Speicherplatzes und
Zuordnung eines Datentyps
X
Menge der Variablen
x:t
x∈X
t = τ(x )
Typ von x
v = σ(x ) Wert von x
v ∈ W (t ) Wertemenge von t
Deklaration: Ein Variablenname wird einem Speicherbereich
und einem Typ zugeordnet.
var i, j: int;
var r: real;
var c: char;
2.1 Variable, Anweisungen und Zustände
2-10
Zustand
Ein Zustand bezeichnet die Belegung aller Variablen zu einem
Zeitpunkt.
◮
Modellierung:
σ : x1 7→ v1 , . . . , xn 7→ vn
xi ∈ X , vi ∈ W (ti ) ∀i ∈ {1, . . . , n}
◮
◮
x1 · · · xn
Tabellarisch:
v1 · · · vn
Mathematisch: Abbildung σ : X → W = {v1 , . . . , vn }, wobei
σ(xi ) ∈ W (ti ) ∀i ∈ {1, . . . , n}
σ(x ) ist die Belegung der Variablen x im Zustand σ.
2.1 Variable, Anweisungen und Zustände
2-11
Zuweisung
◮
◮
Syntax: x ← v
v ∈ W (τ(x ))
Zuweisung, Grundbaustein: Nach Ausführung der
Zuweisung gilt σ(x ) = v .
Ist σ : X → W ein Zustand und wählt man eine Variable x ∈ X
sowie einen Wert v vom passenden Typ, so ist der transformierte
Zustand σ<x ←v > wie folgt definiert:
σ<x ←v > (y ) =
2.1 Variable, Anweisungen und Zustände
(
v
σ(y )
falls x = y
sonst
2-12
Semantik
Die Semantik beschreibt die Bedeutung eines Algorithmus.
Sie ist eine (partielle) Funktion, die jeder Anweisung eine
Zustandsänderung zuordnet:
[ ] : A → ((X → W ) → (X → W )) mit [a ](σ) = σ′
Semantik einer Zuweisung: [x ← v ](σ) = σ<x ←v >
2.1 Variable, Anweisungen und Zustände
2-13
Ausdrücke/Terme
Beispiele für Ausdrücke:
◮
Konstante: 3, 2.7182, ′ A ′
◮
Variable: x1 , x2 , y
◮
Operatoren und Funktionsaufrufe: x1 + 3, f (x1 )
◮
Zusammengesetzte Ausdrücke: f (x1 + 3) − 2.7182 + y
Falls ein Ausdruck t die Variablen x1 , . . . , xn enthält, schreiben wir
t (x1 , . . . , xn ).
Beispiel eines booleschen Ausdrucks: x ≤ 5 ∧ y < 4
2.1 Variable, Anweisungen und Zustände
2-14
Wert eines Ausdrucks
Die Auswertung von Ausdrücken mit Variablen ist
zustandsabhängig. An die Stelle der Variablen wird ihr aktueller
Wert gesetzt.
Beispiel: Für den Ausdruck 2 · x + 1 ist sein Wert im Zustand σ
durch 2σ(x ) + 1 gegeben.
Der so bestimmte Wert des Ausdrucks t (x1 , . . . , xn ) im Zustand σ
wird mit σ(t (x1 , . . . , xn )) bezeichnet.
Beispiel: σ(2 · x + 1) = 2σ(x ) + 1
2.1 Variable, Anweisungen und Zustände
2-15
Zuweisungen
Diese Festlegung erlaubt Wertzuweisungen mit Ausdrücken auf
der rechten Seite
y ← t (x1 , . . . , xn )
Der transformierte Zustand hierfür ist wie folgt definiert:
[y ← t (x1 , . . . , xn )](σ) = σ<y ←σ(t (x1 ,...,xn ))>
2.1 Variable, Anweisungen und Zustände
2-16
Zuweisungen
Beispiel: Semantik der Zuweisung
x ←2·x +1
Transformation: [x ← 2 · x + 1](σ) = σ<x ←2·σ(x )+1>
Hier handelt es sich nicht um eine rekursive Gleichung für x , da
auf der rechten Seite der „alte“ Wert von x benutzt wird.
Wertzuweisungen sind die einzigen elementaren Anweisungen
imperativer Algorithmen. Aus ihnen werden zusammengesetzte
Anweisungen gebildet, aus denen imperative Algorithmen
bestehen.
2.1 Variable, Anweisungen und Zustände
2-17
Zusammengesetzte Anweisungen
Elementare Anweisungen können auf unterschiedliche Arten zu
komplexen Anweisungen zusammengesetzt werden:
1. sequenzielle Ausführung,
2. bedingte Ausführung,
3. wiederholte Ausführung,
4. Ausführung als Unterprogramm,
5. rekursive Ausführung eines Unterprogramms.
Diese Möglichkeiten werden als Kontrollstrukturen bezeichnet. Wir
betrachten jetzt die ersten drei dieser zusammengesetzten
Anweisungen. Auf die anderen kommen wir am Schluss des
Kapitels zu sprechen.
2.1 Variable, Anweisungen und Zustände
2-18
Sequenzielle Ausführung von Anweisungen
◮
Definition: Sind a1 und a2 Anweisungen, so auch a1 ; a2
◮
Informelle Bedeutung: „Führe erst a1 , dann a2 aus.“
◮
Semantik: [a1 ; a2 ](σ) = [a2 ]([a1 ](σ))
◮
Darstellung im Flussdiagramm:
a1
a2
2.1 Variable, Anweisungen und Zustände
2-19
Bedingte Ausführung von Anweisungen
◮
Definition: Sind a1 und a2 Anweisungen und P ein
boolescher Ausdruck, so ist auch
if P then a1 else a2 fi
eine Anweisung.
◮
Voraussetzung: σ(P ) kann ausgewertet werden (ansonsten
ist die Anweisung undefiniert)
◮
Informelle Bedeutung: „Falls P gilt, führe a1 aus, sonst a2 .“
2.1 Variable, Anweisungen und Zustände
2-20
Bedingte Ausführung von Anweisungen
◮
Semantik:
◮



[a1 ](σ) falls σ(P ) = true
[ if P then a1 else a2 fi ](σ) = 

[a2 ](σ) falls σ(P ) = false
Darstellung im Flussdiagramm:
true
false
P
a1
2.1 Variable, Anweisungen und Zustände
a2
2-21
Wiederholte Ausführung von Anweisungen
◮
Definition: Ist a eine Anweisung und P ein boolescher
Ausdruck, so ist auch
while P do a od
eine Anweisung.
◮
Voraussetzung: σ(P ) kann ausgewertet werden (ansonsten
ist die Anweisung undefiniert)
◮
Informelle Bedeutung: „Solange P gilt, führe a aus.“
2.1 Variable, Anweisungen und Zustände
2-22
Wiederholte Ausführung von Anweisungen
◮
Semantik:
[ while P do a od ](σ) =



falls σ(P ) = false
σ


[ while P do a od ]([a ](σ)) sonst
◮
Diese Definition ist rekursiv.
While-Schleifen müssen nicht terminieren.
Darstellung im Flussdiagramm:
false
P
true
a
2.1 Variable, Anweisungen und Zustände
2-23
Flussdiagramme
Normierte Methode (DIN 66001) zur Darstellung von Programmen
◮
Beginn: Start
◮
Ende: Stop
◮
Elementare Anweisung:
a
P
◮
Entscheidung durch booleschen Ausdruck:
◮
Eingabe nach n:
◮
Ausgabe von p:
2.1 Variable, Anweisungen und Zustände
Eingabe n
Ausgabe p
2-24
Konkrete Umsetzung
◮
Sprachen mit nur diesen Anweisungen sind bereits
berechnungsuniversell.
◮
In existierenden Programmiersprachen gibt es fast immer
diese Anweisungen, oft jedoch mehr.
Beispiel: case- oder repeat-Anweisung.
◮
Schlüsselwörter und Syntax der Kontrollstrukturen variieren
von Sprache zu Sprache.
Beispiel: end if statt fi.
◮
Hierarchische Struktur der Anweisungen: Anweisungen
können Bestandteil anderer Anweisungen sein.
2.1 Variable, Anweisungen und Zustände
2-25
Imperative Algorithmen
◮
Es werden die Datentypen int, real und bool verwendet.
◮
Aufbau imperativer Algorithmen (Syntax):
<Programmname>
var x ,y ,. . . : int ; p ,q: bool ;
input : x1 , . . . , xn ;
a;
output : y1 , . . . , ym
◮
Variablendeklarationen
Eingabevariablen
Anweisung(en)
Ausgabevariablen
Die Semantik wird durch eine Zustandsüberführungsfunktion
beschrieben.
2.1 Variable, Anweisungen und Zustände
2-26
Imperative Algorithmen
Die Semantik eines imperativen Algorithmus ist eine partielle
Funktion
[PROG ] : W1 × · · · × Wn → V1 × · · · × Vm
[PROG ](w1 , . . . , wn ) = (σ(y1 ), . . . , σ(ym ))
σ = [a ](σ0 )
σ0 (xi ) = wi ∈ Wi für i = 1, . . . , n
wobei
PROG: <Programmname>
Wi : Wertebereich des Typs von xi , für i = 1, . . . , n
Vj : Wertebereich des Typs von yj , für j = 1, . . . , m
2.1 Variable, Anweisungen und Zustände
2-27
Imperative Algorithmen
◮
Der Algorithmus legt eine Zustandstransformation fest. Die
Eingabe befindet sich im Zustand σ0 .
◮
Die Ausführung imperativer Algorithmen besteht aus einer
Abfolge von Zuweisungen. Diese Folge wird mittels
„Sequenz“, „bedingter Ausführung“ und „wiederholter
Ausführung“ aus den Zuweisungen gebildet.
◮
Jede Zuweisung definiert eine Transformation des Zustands.
Die Semantik des Algorithmus ist durch die Kombination all
dieser Zustandstransformationen festgelegt.
◮
Falls die Auswertung von a nicht terminiert, ist die Funktion
[PROG ] nicht definiert.
2.1 Variable, Anweisungen und Zustände
2-28
Algorithmus von Euklid
Der Algorithmus von Euklid lautet in dieser Notation:
EUKLID
var a, b, r, x, y: int;
input x, y;
a ← x;
b ← y;
while b # 0 do
r ← a mod b;
a ← b;
b ← r;
od;
output a;
2.1 Variable, Anweisungen und Zustände
2-29
Fakultätsfunktion
FAK
k! = 1 · 2 · 3 · · · k
var x, y: int;
input x;
y ← 1;
while x > 1 do
y ← y * x;
x ← x - 1;
od;
output y;
Start
Eingabe x
y←1
x>1
true
y ←y·x
Es gilt:
[FAK ](w ) =
false
Ausgabe y
Stop
(
w ! für w > 0
1
2.1 Variable, Anweisungen und Zustände
x←x−1
sonst
2-30
Auswertung der Fakultätsfunktion
◮
◮
Signatur der Semantikfunktion: [FAK ] : int → int
Das Ergebnis der Berechnung ist die Belegung der Variablen
y im Endzustand: σ(y ).
◮
Der Endzustand σ ist laut Definition σ = [a ](σ0 ), wobei a die
Folge aller Anweisungen des Algorithmus ist.
◮
σ0 ist der Startzustand. Die Eingabe befindet sich in σ0 (x ).
◮
Da nur die zwei Variablen x und y auftreten, können wir einen
Zustand σ als Paar (σ(x ), σ(y )) schreiben.
◮
⊥ bedeutet, dass der Wert der Variablen keine Rolle spielt
bzw. undefiniert ist.
2.1 Variable, Anweisungen und Zustände
2-31
Auswertung der Fakultätsfunktion
Gesucht: [FAK ](3)
σ = [a ](σ0 ) = [a ](3, ⊥)
= [y ← 1; while x > 1 do y ← y ∗ x ; x ← x − 1 od ](3, ⊥)
= [ while x > 1 do y ← y ∗ x ; x ← x − 1 od ]([y ← 1](3, ⊥))
=: [ while B do β od ]([y ← 1](3, ⊥))
= [ while B do β od ](3, 1)
(
σ
falls σ(B ) = false
=
[ while B do β od ]([β](σ)) sonst
(
(3, 1)
falls σ(x > 1) = false
=
[ while B do β od ]([y ← y ∗ x ; x ← x − 1](σ)) sonst
2.1 Variable, Anweisungen und Zustände
2-32
Auswertung der Fakultätsfunktion
· · · = [ while B do β od ]([y ← y ∗ x ; x ← x − 1](3, 1))
= [ while B do β od ]([x ← x − 1]([y ← y ∗ x ](3, 1)))
= [ while B do β od ]([x ← x − 1](3, 3))
= [ while B do β od ](2, 3)
= [ while B do β od ]([y ← y ∗ x ; x ← x − 1](2, 3))
= [ while B do β od ](1, 6)
(
(1, 6)
falls σ(x > 1) = false
=
[ while B do β od ]([y ← y ∗ x ; x ← x − 1](1, 6)) sonst
= (1, 6)
[FAK](3) = 6
2.1 Variable, Anweisungen und Zustände
2-33
Fibonacci-Zahlen (iterativ)
f0 = 0,
f1 = 1,
fn = fn−1 + fn−2 , n > 1
FIB:
var x,a,b,c: int;
input x;
a ← 0;
b ← 1;
while x > 0 do
c ← a + b;
a ← b;
b ← c;
x ← x - 1;
od;
output a;
[FIB ](x ) =
2.1 Variable, Anweisungen und Zustände
(
die x -te Fibonacci-Zahl, falls x > 0,
0,
sonst.
2-34
Fibonacci-Zahlen (rekursiv)
Einzelheiten zu rekursiven Programmen werden später behandelt!
f0 = 0,
f1 = 1,
fn = fn−1 + fn−2 , n > 1
FIB:
var n, x: int;
input n;
if n = 0 then x ← 0 fi;
if n = 1 then x ← 1 fi;
if n > 1 then x ← FIB(n - 1) + FIB(n - 2) fi;
output x;
Ein rekursiv ausgedrückter Algorithmus ist häufig eleganter als
sein iteratives Äquivalent. Nachteil ist evtl. eine längere Laufzeit.
2.1 Variable, Anweisungen und Zustände
2-35
Felder
◮
Definition: Ein Feld ist eine indizierte Menge von Variablen
des gleichen Typs. Felder sind generische Datentypen.
◮
Deklaration:
var x[I]: T;
var coords[1..3]: real;
var str[0..4095]: char;
◮
Indexmenge: I endlich, häufig I ⊆ N, aber andere
Indexmengen sind möglich.
◮
Zugriff: x [i ] sowohl lesend als auch schreibend, wobei i ∈ I .
Längenänderungen von Feldern sind nicht in allen
Programmiersprachen möglich.
Felder werden auch als Arrays bezeichnet.
2.2 Felder
2-36
Verbreitete Spezialfälle von Feldern
◮
Vektor:
var v[1..3]: real;
◮
Matrix:
var v[1..4, 1..4]: real;
◮
Strings:
var str[0..102]: char;
2.2 Felder
2-37
For-Schleife
Die For-Schleife ist eine weitere Kontrollstruktur zur Wiederholung
von Anweisungen.
Es seien j , k ∈ N Konstante, var i : int eine Variable und a eine
Anweisung.
for i ← j to k do a; od entspricht
i ← j; while i ≤ k do a; i ← i + 1; od
i wird Laufvariable der Schleife genannt.
Viele Programmiersprachen enthalten Varianten dieser Schleife.
2.2 Felder
2-38
Maximale Summe eines Teilfelds
Die folgenden Programme berechnen die maximale Summe
aufeinanderfolgender Zahlen in einem Feld var x [1..n] : int .
 j





X

m = max 
x
[
k
]
|
1
≤
i
≤
j
≤
n





k =i
x sei das Feld:
2
3
4
5
6
7
8
9
10
1
31 −41 59 26 −53 58 97 −93 −23 84
Die maximale Summe aufeinanderfolgender Zahlen ist
x [3] + x [4] + x [5] + x [6] + x [7] = 187. Keine andere Summe
besitzt einen höheren Wert. Beispielsweise ist
x [1] + x [2] + x [3] + x [4] = 75.
2.2 Felder
2-39
Maximale Summe eines Teilfelds
Idee: Berechne alle möglichen Summen.
MaxSum1
var x[1..n]: int;
var i,j,k,s,m: int;
input x;
m ← 0; // Wert der leeren Summe
for i ← 1 to n do
for j ← i to n do
s ← 0;
for k ← i to j do
s ← s + x[k];
od;
m ← max(m, s);
od;
od;
output m;
Dieser Algorithmus enthält drei ineinandergeschachtelte Schleifen.
2.2 Felder
2-40
Maximale Summe eines Teilfelds
Idee: Einsparen der inneren Schleife durch Wiederverwenden
bereits bekannter Summen.
MaxSum2
var x[1..n]: int;
var i,j,s,m: int;
input x;
m ← 0; // Wert der leeren Summe
for i ← 1 to n do
s ← x[i];
m ← max(m, s);
for j = i+1 to n do
s ← s + x[j];
m ← max(m, s);
od;
od;
output m;
Dieser Algorithmus besitzt zwei ineinandergeschachtelte Schleifen.
2.2 Felder
2-41
Maximale Summe eines Teilfelds
Idee: Genau überlegen, was eine hohe Summe ausmacht. Diese
Lösung stammt von M. Shamos (1977).
MaxSum3
var x[1..n]: int;
var i,s,m: int;
input x;
m ← 0;
s ← 0;
for i ← 1 to n do
s ← max(0, s + x[i]);
m ← max(m, s);
od;
output m;
Dieser Algorithmus hat nur noch eine Schleife.
2.2 Felder
2-42
Einführung
◮
In der Regel lässt sich ein Problem durch verschiedene
Algorithmen lösen (zum Beispiel „maximale Summe
aufeinanderfolgender Elemente“).
◮
Welcher Algorithmus soll gewählt werden?
◮
Die Algorithmen müssen hinsichtlich ihres Verhaltens
verglichen werden.
◮
Man benötigt ein Maß für den Aufwand eines Algorithmus.
2.3 Komplexität von Algorithmen
2-43
Komplexität von Algorithmen und Problemen
◮
Unter der Komplexität eines Algorithmus versteht man den
Aufwand, den der Algorithmus zur Lösung des Problems
benötigt. Typischerweise ist hier die
◮
◮
Laufzeit des Algorithmus oder der
Speicherbedarf des Algorithmus
gemeint. Man unterscheidet die
◮
◮
◮
◮
Komplexität im günstigsten Fall (best-case), die
Komplexität im mittleren Fall (average-case) und die
Komplexität im ungünstigsten Fall (worst-case).
Unter der Komplexität eines Problems versteht man die
Komplexität des besten Algorithmus zur Lösung des Problems
im ungünstigsten Fall.
2.3 Komplexität von Algorithmen
2-44
Komplexität eines Algorithmus
◮
Umfang n eines Problems:
„Anzahl der Eingabewerte“ oder „Größe der Eingabewerte“
oder . . .
◮
Aufwand T (n) eines Algorithmus:
„Anzahl der Schritte“ oder „Anzahl bestimmter Operationen“
oder „Anzahl der benötigten Speicherplätze“ oder . . . , die der
Algorithmus braucht, um ein Problem vom Umfang n zu lösen.
Um sinnvolle Aussagen über die Komplexität eines Algorithmus zu
treffen, müssen n und T (n) mit Bedacht gewählt werden.
Beispiel: Im Algorithmus „Sortieren durch Einfügen“ war n die
Anzahl der zu sortierenden Zahlen und T (n) die Anzahl der
benötigten Vergleiche.
2.3 Komplexität von Algorithmen
2-45
Wachstum von Funktionen
◮
In der Regel stellt die Wachstumsrate der Laufzeit ein
einfaches und geeignetes Kriterium zur Messung der Effizienz
eines Algorithmus dar.
◮
Die Wachstumsrate erlaubt es uns, die relative
Leistungsfähigkeit alternativer Algorithmen zu vergleichen.
◮
Die Wachstumsrate von Algorithmen wird meistens mithilfe
der asymptotischen Notation angegeben.
◮
Die asymptotische Notation lässt konstante Faktoren
unberücksichtigt.
2.3 Komplexität von Algorithmen
2-46
Asymptotische Notation
Es sei eine Funktion g : N −→ R gegeben.
Θ(g ) = {f : N −→ R | ∃c1 > 0, c2 > 0, n0 > 0 ∀n ≥ n0 .
0 ≤ c1 g (n) ≤ f (n) ≤ c2 g (n)}
O (g ) = {f : N −→ R | ∃c > 0, n0 > 0 ∀n ≥ n0 . 0 ≤ f (n) ≤ cg (n)}
Ω(g ) = {f : N −→ R | ∃c > 0, n0 > 0 ∀n ≥ n0 . 0 ≤ cg (n) ≤ f (n)}
o (g ) = {f : N −→ R | ∀c > 0 ∃n0 > 0 ∀n ≥ n0 . 0 ≤ f (n) < cg (n)}
ω(g ) = {f : N −→ R | ∀c > 0 ∃n0 > 0 ∀n ≥ n0 . 0 ≤ cg (n) < f (n)}
2.3 Komplexität von Algorithmen
2-47
Gebräuchliche Wachstumsklassen
Θ(1)
Θ(log(n))
Θ(n)
Θ(n log(n))
Θ(n2 )
Θ(nk )
Θ(2n )
konstantes Wachstum
logarithmisches Wachstum
lineares Wachstum
„fast lineares“ Wachstum
quadratisches Wachstum
polynomiales Wachstum
exponentielles Wachstum
O (n2 )
Ω(n)
höchstens quadratisches Wachstum
mindestens lineares Wachstum
Beispiel: Die Laufzeit beim „Sortieren durch Einfügen“ beträgt im
günstigsten Fall Θ(n) und im ungünstigsten Falls Θ(n2 ). Die
Laufzeit kann also durch Ω(n) nach unten und durch O (n2 ) nach
oben abgeschätzt werden. Die Laufzeit wird auch durch O (n3 )
nach oben abgeschätzt! In der Praxis werden häufig möglichst
scharfe obere Schranken gesucht.
2.3 Komplexität von Algorithmen
2-48
Graphen einiger Funktionen
n log(n)
n
log(n)
1
2.3 Komplexität von Algorithmen
2-49
Graphen einiger Funktionen
2n
n3
n2
2.3 Komplexität von Algorithmen
2-50
Exemplarische Werte
n=
log2 (n) ≈
n log2 (n) ≈
n2 =
2n ≈
1
0
0
1
2
10
3
33
100
103
100
7
664
10000
1030
1000
10
9966
1000000
10301
10000
13
132877
100000000
103010
Maximales n bei gegebener Zeit, Ann.: 1 Schritt benötigt 1µs
n
n2
n3
2n
1 Min.
1 Std.
1 Tag
1 Woche
1 Jahr
6 · 107
7750
391
25
3.6 · 109
6 · 104
1530
31
8.6 · 1010
2.9 · 105
4420
36
6 · 1011
7.9 · 105
8450
39
3 · 1013
5.6 · 106
31600
44
2.3 Komplexität von Algorithmen
2-51
Asymptotische Notationen in Gleichungen
◮
2n2 + 3n + 1 = Θ(n2 ) heißt 2n2 + 3n + 1 ∈ Θ(n2 ).
◮
2n2 + 3n + 1 = 2n2 + Θ(n) heißt: Es gibt eine Funktion
f (n) ∈ Θ(n) mit 2n2 + 3n + 1 = 2n2 + f (n).
◮
2n2 + Θ(n) = Θ(n2 ) heißt: Für jede Funktion f (n) ∈ Θ(n) gibt
es eine Funktion g (n) ∈ Θ(n2 ) mit 2n2 + f (n) = g (n).
◮
In Gleichungsketten
2n2 + 3n + 1 = 2n2 + Θ(n) = Θ(n2 )
werden die Gleichungen einzeln gelesen: Die erste Gleichung
besagt, dass es eine Funktion f (n) ∈ Θ(n) mit
2n2 + 3n + 1 = 2n2 + f (n) gibt. Die zweite Gleichung sagt
aus, dass es für jede Funktion g (n) ∈ Θ(n) eine Funktion
h (n) ∈ Θ(n2 ) mit 2n2 + g (n) = h (n) gibt. Aus der
Hintereinanderausführung folgt 2n2 + 3n + 1 = Θ(n2 ).
2.3 Komplexität von Algorithmen
2-52
Eigenschaften der asymptotischen Notation
Eine Funktion f : N −→ R heißt asymptotisch positiv, wenn gilt:
∃n0 > 0 ∀n ≥ n0 . f (n) > 0.
Für alle asymptotisch positiven Funktionen f , g : N −→ R gelten
die folgenden Aussagen.
Transitivität:
f (n) = Θ(g (n)) ∧ g (n) = Θ(h (n)) =⇒ f (n) = Θ(h (n)),
f (n) = O (g (n)) ∧ g (n) = O (h (n)) =⇒ f (n) = O (h (n)),
f (n) = Ω(g (n)) ∧ g (n) = Ω(h (n)) =⇒ f (n) = Ω(h (n)),
f (n) = o (g (n)) ∧ g (n) = o (h (n)) =⇒ f (n) = o (h (n)),
f (n) = ω(g (n)) ∧ g (n) = ω(h (n)) =⇒ f (n) = ω(h (n)).
2.3 Komplexität von Algorithmen
2-53
Eigenschaften der asymptotischen Notation
Reflexivität:
f (n) = Θ(f (n)),
f (n) = O (f (n)),
f (n) = Ω(f (n)).
Symmetrie:
f (n) = Θ(g (n)) ⇐⇒ g (n) = Θ(f (n)).
Austausch-Symmetrie:
f (n) = Θ(g (n)) ⇐⇒ f (n) = O (g (n)) ∧ f (n) = Ω(g (n)),
f (n) = O (g (n)) ⇐⇒ g (n) = Ω(f (n)),
f (n) = o (g (n)) ⇐⇒ g (n) = ω(f (n)).
Θ, O , Ω, o und ω werden landausche Symbole genannt.
2.3 Komplexität von Algorithmen
2-54
Beispiele
8 = Θ(1)
3n2 − 5n + 8 = Θ(n2 )
3n2 − 5n + 8 = O (n2 )
3n2 − 5n + 8 = Ω(n2 )
logb (n)
loga (n) =
logb (a )
Θ(loga (n)) = Θ(logb (n))
12 log10 (n) = Θ(log(n))
12 log2 (n) = Θ(log(n))
2.3 Komplexität von Algorithmen
2-55
Laufzeit von imperativen Algorithmen
Folgende Annahmen werden zur Analyse der Laufzeit von
imperativen Algorithmen getroffen:
◮
Zuweisung: Die Laufzeit ist konstant.
◮
Sequenz: Die Laufzeit ist die Summe der Laufzeiten der
Einzelanweisungen.
◮
Alternative: Die Laufzeit ist im ungünstigsten Fall die Laufzeit
der Bedingungsauswertung plus dem Maximum der
Laufzeiten der Alternativen.
◮
Iteration: Die Laufzeit errechnet sich aus dem Produkt der
Laufzeit der inneren Anweisung und der Anzahl der
Iterationen. Hinzu kommt die Laufzeit für einen weiteren Test.
2.3 Komplexität von Algorithmen
2-56
Sortieren durch Einfügen
Code
Kosten
insertionSort(A)
c1
j ← 2;
c2
while j ≤ length(A) do
key ← A[j];
c3
i ← j - 1;
c4
while i > 0 und A[i] > key c5
do
A[i + 1] ← A[i];
c6
c7
i ← i - 1;
od;
A[i + 1] ← key;
c8
j ← j + 1;
c9
od;
2.3 Komplexität von Algorithmen
Anzahl
1
n
n−1
n−1
Pn
j =2 tj
Pn
(tj − 1)
j
=
2
Pn
j =2 (tj − 1)
n−1
n−1
2-57
Abschätzung durch die O-Notation
Code
Kosten
insertionSort(A)
j ← 2;
O (1)
O (n)
while j ≤ length(A) do
key ← A[j];
O (n)
i ← j - 1;
O (n)
while i > 0 und A[i] > key; O (n2 )
do
A[i + 1] ← A[i];
O (n2 )
i ← i - 1;
O (n2 )
od;
O (n)
A[i + 1] ← key;
j ← j + 1;
O (n)
od;
Die Laufzeit T (n) liegt in O (n2 ), d. h. T (n) = O (n2 ).
2.3 Komplexität von Algorithmen
2-58
Korrektheit von Softwaresystemen
In vielen Situationen ist eine korrekte Funktionsweise eines
Softwaresystems von großer Bedeutung. Dies gilt insbesondere,
wenn das System
◮
sicherheitskritisch (z. B. Atomreaktor),
◮
kommerziell kritisch (z. B. massenproduzierte Chips) oder
◮
politisch kritisch (z. B. Militär)
ist.
2.4 Korrektheit von Algorithmen
2-59
Korrektheit von Softwaresystemen
Es gibt mehrere Möglichkeiten, die Zuverlässigkeit von
Softwaresystemen zu erhöhen:
◮
Software Engineering:
Maßnahmen während des gesamten
Softwareentwicklungsprozesses.
◮
Programmierung:
Beispiel: Ausnahmebehandlung, Zusicherungen.
◮
Validation:
Systematische Tests unter Einsatzbedingungen; Tests zeigen
die Anwesenheit, aber nicht die Abwesenheit von Fehlern.
◮
Verifikation:
Mathematischer Nachweis der Korrektheit von Algorithmen.
2.4 Korrektheit von Algorithmen
2-60
Korrektheit von Softwaresystemen
Um ein System zu verifizieren zu können benötigt man Methoden,
Werkzeuge und Sprachen, zur
◮
Modellierung von Systemen auf hoher Abstraktionsebene,
◮
Spezifikation nachzuweisender Eigenschaften dieser
Systeme (Terminierungsverhalten, berechnete
Funktionswerte, . . . ) und zur
◮
Verifikation, d. h. zum formalen Beweis, dass ein
implementiertes System die spezifizierten Eigenschaften hat.
In diesem Abschnitt behandeln wir eine Möglichkeit zur
Spezifikation und Verifikation imperativer Algorithmen.
2.4 Korrektheit von Algorithmen
2-61
Hoaresche Logik
Es seien ein Algorithmus S sowie Bedingungen p und q gegeben.
◮
Wir schreiben in diesem Fall
{p} S {q}
und nennen
◮
p Vorbedingung,
◮
q Nachbedingung und
◮
(p,q) Spezifikation von S.
2.4 Korrektheit von Algorithmen
2-62
Hoaresche Logik
◮
S heißt partiell-korrekt bezüglich der Spezifikation (p,q), wenn
jede Ausführung von S, die in einem Zustand beginnt, der p
erfüllt und die terminiert, zu einem Zustand führt, der q erfüllt.
|= {p} S {q}
◮
S wird total-korrekt bezüglich der Spezifikation (p,q) genannt,
wenn jede Ausführung von S, die in einem Zustand beginnt,
der p erfüllt, terminiert und zu einem Zustand führt, der q
erfüllt.
2.4 Korrektheit von Algorithmen
2-63
Beispiele
◮
◮
◮
◮
◮
◮
◮
◮
◮
◮
|= {true }x ← 1{x = 1}
|= {x = 1}x ← x + 1{x = 2}
|= {y = a }x ← y {x = a ∧ y = a }
|= {x = a ∧ y = b }z ← x ; x ← y ; y ← z {x = b ∧ y = a }
|= {false }x ← 1{x = 42}
|= {true } while 0 = 0 do x ← 1 od {x = 23}
|= {x > 0} while x , 0 do x ← x − 1 od {x = 0}
|= {true } while x , 0 do x ← x − 1 od {x = 0}
|= {true } while p (x ) do α od {¬p (x )}
|= {x + y = a } while x , 0 do x ← x − 1; y ← y + 1 od {x =
0 ∧ x + y = a}
2.4 Korrektheit von Algorithmen
2-64
Hoarescher Kalkül
Zuweisung:
Sequenz:
If:
While:
Anpassungsregel:
2.4 Korrektheit von Algorithmen
{pxt } x ← t {p }
{p } S1 {r }, {r } S2 {q}
{p } S 1 ; S 2 {q }
{p ∧ e } S1 {q}, {p ∧ ¬e } S2 {q}
{p } if e then S1 else S2 fi {q}
{p ∧ e } S {p }
{p } while e do S od {p ∧ ¬e }
p ⊃ q, {q} S {r }, r ⊃ s
{p } S {s }
2-65
Hoarescher Kalkül
◮
Der hoaresche Kalkül besteht aus dem Axiomenschema für
die Zuweisung und Ableitungsregeln.
◮
Falls {p} S {q} mithilfe des hoareschen Kalküls hergeleitet
werden kann, schreibt man
⊢ {p} S {q}.
2.4 Korrektheit von Algorithmen
2-66
Schleifeninvariante
Die Bedingung p in der While-Regel heißt Schleifeninvariante.
{p ∧ e } S {p }
{p } while e do S od {p ∧ ¬e }
Eine Schleifeninvariante gilt vor jedem Schleifendurchlauf und
damit auch nach jedem Schleifendurchlauf, speziell also nach
Beendigung der Wiederholungsanweisung. Dann gilt zudem ¬e .
2.4 Korrektheit von Algorithmen
2-67
Schleifeninvariante
Beispiel: Für den folgenden Algorithmus ist q = k − 1 eine
Schleifeninvariante.
{q = 0 ∧ k = 1}
while k , n + 1 do
q ← q + 1;
k ← k + 1;
od;
{q = n }
Nach Ausführung der Schleife gilt: q = k − 1 ∧ ¬(k , n + 1)
Daraus folgt: q = n
2.4 Korrektheit von Algorithmen
2-68
Eigenschaften des Kalküls
Korrektheit:
⊢ {p } S {q} =⇒ |= {p } S {q}
relative Vollständigkeit:
⊢ {p } S {q} ⇐= |= {p } S {q}
2.4 Korrektheit von Algorithmen
2-69
Beispiel: Division mit Rest
◮
Es sollen zwei ganze Zahlen x , y ∈ Z mit x ≥ 0 und y > 0
durcheinander dividiert werden.
◮
Das Ergebnis der Division x /y ist der Quotient q und der Rest
r mit x = qy + r ∧ 0 ≤ r < y .
var x, y, q, r: int;
input x, y;
q ← 0;
r ← x;
while r >= y do
r = r - y;
q = q + 1;
od;
output q, r;
2.4 Korrektheit von Algorithmen
2-70
Beispiel: Division mit Rest
◮
Mithilfe des hoareschen Kalküls leiten wir jetzt den Ausdruck
{x ≥ 0} S {x = qy + r ∧ 0 ≤ r < y }
her, wobei S der obige imperative Algorithmus ist.
◮
Wegen der Korrektheit des Kalküls können wir dann
schließen, dass der Algorithmus bezüglich der angegebenen
Vor- und Nachbedingung partiell-korrekt ist.
◮
Die Bedingung y > 0 wird zum Nachweis der totalen
Korrektheit benötigt.
2.4 Korrektheit von Algorithmen
2-71
Beispiel: Division mit Rest
Wir zeigen zuerst, dass
x = qy + r ∧ 0 ≤ r
eine Schleifeninvariante ist. Dies ergibt sich aus:
x = qy + r ∧ 0 ≤ r ∧ r ≥ y
2.4 Korrektheit von Algorithmen
=⇒
x = (q + 1)y + (r − y ) ∧ 0 ≤ r − y
2-72
Beispiel: Division mit Rest
Die Behauptung folgt dann aus den beiden Aussagen
x ≥ 0 =⇒ 0 ≤ x
und
x = qy + r ∧ 0 ≤ r ∧ ¬(r ≥ y )
2.4 Korrektheit von Algorithmen
=⇒
x = qy + r ∧ 0 ≤ r < y .
2-73
Beispiel: Division mit Rest
◮
Da nach Voraussetzung y > 0 gilt, durchläuft die Variable r
eine monoton streng fallende Folge natürlicher Zahlen:
r0 , r1 , r2 , . . .
◮
Da y konstant ist, wird deshalb für ein i die Bedingung
ri < y
wahr. Das heißt, das Programm terminiert schließlich und ist
deshalb total-korrekt bezüglich der angegebenen
Bedingungen.
Bemerkung: Es gibt auch Kalküle zum Nachweis der totalen
Korrektheit.
2.4 Korrektheit von Algorithmen
2-74
Beispiel: Division mit Rest
Vor- und Nachbedingung, Schleifeninvariante als Annotation:
static private int remainder (int x, int y) {
assert x >= 0 && y > 0;
int q = 0,
r = x;
assert x == q * y + r && 0 <= r;
while (r >= y) {
r = r - y;
q = q + 1;
assert x == q * y + r && 0 <= r;
}
assert x == q * y + r && 0 <= r && r < y;
return r;
}
2.4 Korrektheit von Algorithmen
2-75
Wunschtraum
Es ist ein Algorithmus gesucht, der für beliebige p, S und q beweist
oder widerlegt, dass
⊢ {p} S {q}
gilt.
Solch ein Algorithmus kann nicht existieren!
Dann existiert natürlich erst recht kein analoger Algorithmus für die
totale Korrektheit.
2.4 Korrektheit von Algorithmen
2-76
Totale Korrektheit
◮
Zum Nachweis der totalen Korrektheit muss zusätzlich zur
partiellen Korrektheit die Terminierung gezeigt werden.
◮
Um die Terminierung von Schleifen nachzuweisen, gibt es
keine allgemeine Methode, oft funktioniert aber die
Vorgehensweise vom obigen Beispiel:
1. Man suche einen Ausdruck u, dessen Wert eine natürliche
Zahl ist.
2. Man beweise, dass u bei jedem Schleifendurchlauf echt
kleiner wird.
3. Da die natürlichen Zahlen wohlgeordnet sind, muss die
Schleife terminieren.
2.4 Korrektheit von Algorithmen
2-77
Beispiel: Insertionsort
P (j ): Das Feld A [1 · · · j ] ist eine Permutation des Ausgangsfeldes
A [1 · · · j ].
S (j ): Das Feld A [1 · · · j ] ist sortiert.
A ∗ [j ]: Der ursprünglich in A [j ] enthaltene Wert.
Pk∗ (j ): Das Feld A [1 · · · k − 1, k + 1 · · · j ] ist eine Permutation des
Ausgangsfeldes A [1 · · · j − 1].
Sk∗ (j ): Das Feld A [1 · · · k − 1, k + 1 · · · j ] ist sortiert.
2.4 Korrektheit von Algorithmen
2-78
Beispiel: Insertionsort
{n ≥ 1}
{P (1) ∧ S (1) ∧ n ≥ 1}
j ← 2;
{P (j − 1) ∧ S (j − 1) ∧ 2 ≤ j ≤ n + 1}
while j ≤ n do
key ← A[j];
i ← j - 1;
while i > 0 ∧ A[i] > key do
A[i+1] ← A[i];
i ← i - 1;
od;
A[i+1] ← key;
j ← j + 1;
od;
{P (j − 1) ∧ S (j − 1) ∧ 2 ≤ j ≤ n + 1 ∧ j > n}
{P (n) ∧ S (n)}
2.4 Korrektheit von Algorithmen
2-79
Beispiel: Insertionsort
{P (j − 1) ∧ S (j − 1) ∧ 2 ≤ j ≤ n + 1 ∧ j ≤ n}
{Pj∗ (j ) ∧ Sj∗(j ) ∧ 0 ≤ j − 1}
key ← A[j];
i ← j - 1;
{Pi∗+1 (j ) ∧ Si∗+1 (j ) ∧ key < A [i + 2], . . . , A [j ]
∧0 ≤ i ≤ j − 1 ∧ key = A ∗ [j ]}
while i > 0 ∧ A[i] > key do
A[i+1] ← A[i];
i ← i - 1;
od;
A[i+1] ← key;
j ← j + 1;
{P (j − 1) ∧ S (j − 1) ∧ 2 ≤ j ≤ n + 1}
2.4 Korrektheit von Algorithmen
2-80
Konzepte imperativer Sprachen
◮
Anweisungen
◮
◮
◮
Ausdrücke
◮
◮
◮
primitive Anweisungen: Zuweisung, Block, Prozeduraufruf
zusammengesetzte Anweisungen: Sequenz, Auswahl,
Iteration
primitive Ausdrücke: Konstante, Variable, Funktionsaufruf
zusammengesetzte Ausdrücke: Operanden/Operatoren
Datentypen
◮
◮
primitive Datentypen: Wahrheitswerte, Zeichen, Zahlen,
Aufzählung
zusammengesetzte Datentypen: Felder, Verbund, Vereinigung,
Zeiger
2.5 Konzepte imperativer Sprachen
2-81
Konzepte imperativer Sprachen
◮
Abstraktion
◮
◮
◮
◮
Anweisung: Prozedurdeklaration
Ausdruck: Funktionsdeklaration
Datentyp: Typdeklaration
Weitere Konzepte
◮
◮
◮
◮
◮
Ein- und Ausgabe
Ausnahmebehandlung
Bibliotheken
Parallele und verteilte Berechnungen
...
2.5 Konzepte imperativer Sprachen
2-82
Blöcke
◮
Motivation: Ein Block fasst mehrere Anweisungen und
Deklarationen zu einer Einheit zusammen.
◮
Syntax: Die Abgrenzung erfolgt durch syntaktische Elemente,
wie z. B. Schlüsselwörter (begin, end), Klammerung oder
Einrückung. Blöcke dürfen überall statt einer einzelnen
Anweisung stehen.
◮
Kontrollfluss: Die Ausführung eines Blocks beginnt mit der
ersten Anweisung und wird im Normalfall nach der letzten
beendet. Es gibt auch Anweisungen zum Verlassen des
Blocks: break, return.
◮
Lokale Variablen: Innerhalb eines Blocks können Variablen
deklariert werden, die nur in diesem Block verfügbar sind.
2.5 Konzepte imperativer Sprachen
2-83
Blöcke
◮
Globale Variable: Innerhalb eines Blocks sind alle
Bezeichner aus den umschließenden Blöcken sichtbar, soweit
sie nicht von einer inneren Deklaration überdeckt werden.
◮
Gültigkeitsbereich (scope): Ein Bezeichner ist innerhalb des
Blocks gültig, in dem er deklariert wurde, nicht aber
außerhalb. Die Gültigkeit ist eine statische Eigenschaft, die
sich aus dem Programmtext ableitet.
◮
Lebensdauer: Ist eine dynamische Eigenschaft und
bezeichnet den Zeitraum der Verfügbarkeit eines Wertes
während der Laufzeit. Werte von überdeckten Variablen sind
nach Beendigung des überdeckenden Blocks wieder
verfügbar.
2.5 Konzepte imperativer Sprachen
2-84
Blöcke
Code
var x,y: int;
· · · do
var x: int;
···
···
od;
···
Gültig
|
|
|
x
Sichtbar
|
|
|
|
|
|
|
y
|
|
|
|
|
|
|
x
|
|
|
x
|
|
|
|
|
|
|
y
|
|
|
|
x
Einzelheiten lernen Sie in der Vorlesung „Programmieren“.
2.5 Konzepte imperativer Sprachen
2-85
Prozeduren
◮
Abstraktion: Eine Prozedur fasst mehrere Anweisungen
zusammen und gibt ihnen einen Namen. Der Aufruf einer
Prozedur führt die Anweisungen aus und wirkt dabei wie eine
elementare Anweisung. Über Parameter können die
Anweisungen gesteuert werden.
◮
Wiederverwertung: Eine Prozedur wird nur einmal deklariert
und kann beliebig oft verwendet werden.
◮
Modularisierung: Die Implementation der Prozedur muss
dem aufrufenden Programm nicht bekannt sein.
Veränderungen innerhalb der Prozedur erfordern keine
Änderung des aufrufenden Programms.
2.5 Konzepte imperativer Sprachen
2-86
Deklaration von Prozeduren
◮
Deklaration:
proc P(p1 , . . . , pn ) begin a; end
◮
Name: P ist der Name der Prozedur und frei wählbar.
◮
Parameter: p1 , . . . , pn sind die Parameter der Prozedur. Sie
sind lokale Variablen mit eigenem Typ (formale Parameter),
denen beim Aufruf der Prozedur Werte (aktuelle Parameter)
zugewiesen werden. Die Gültigkeit der Parameter ist auf den
Rumpf der Prozedur beschränkt.
◮
Rumpf: a ist der Rumpf der Prozedur. Er enthält die
auszuführenden Anweisungen.
2.5 Konzepte imperativer Sprachen
2-87
Werte- und Referenzparameter
Werteparameter (call by value):
◮
Der aktuelle Parameter kann ein Ausdruck oder speziell auch
eine Variable sein.
◮
Es wird der Wert des Ausdrucks übergeben.
◮
Die Deklaration erfolgt (zum Beispiel) ohne das Präfix var.
2.5 Konzepte imperativer Sprachen
2-88
Werte- und Referenzparameter
Referenzparameter (call by reference):
◮
Der aktuelle Parameter muss eine Variable sein.
◮
Es wird die Variable (Adresse) übergeben.
◮
In der Deklaration werden Referenzparameter (zum Beispiel)
mit var bezeichnet.
Es gibt weitere Arten der Parameterübergabe.
2.5 Konzepte imperativer Sprachen
2-89
Beispiel
Aufgabe: Vertausche die Inhalte der Variablen x und y und addiere
zu beiden den Wert a .
proc vertausche(a: int; var x, y: int) begin
var z: int;
// lokale Variable
z ← x;
x ← y;
y ← z;
x ← x + a;
y ← y + a;
end
Die Wirkung ist die „simultane“ Ersetzung.
(x , y ) ← (y + a , x + a )
2.5 Konzepte imperativer Sprachen
2-90
Beispiel
Aufrufen lässt sich die Prozedur in unterschiedlichen Umgebungen
mit verschiedenen aktuellen Parametern:
vertausche(0, i, j);
vertausche(3, a[1], a[2]);
vertausche(a[3], a[1], a[2]);
vertausche(i, i, j);
2.5 Konzepte imperativer Sprachen
2-91
Funktionen
◮
Abstraktion: Funktionen sind Abstraktionen von Ausdrücken.
Der Aufruf einer Funktion berechnet einen Wert eines Typs τ.
◮
Deklaration:
func F(p1 , . . . , pn ): τ begin a; end
◮
Auswertung: Der Rückgabewert der Funktion wird (zum
Beispiel) durch eine spezielle return-Anweisung angegeben.
Diese Anweisung verlässt den Funktionsblock mit sofortiger
Wirkung. Häufig wird auch der letzte Term innerhalb einer
Funktion als Rückgabewert genommen, oder eine Zuweisung
zum Funktionsnamen legt den Rückgabewert fest.
◮
Seiteneffekt: Wenn der Aufruf einer Funktion den Wert einer
globalen Variablen verändert, spricht man von einem
Seiteneffekt.
2.5 Konzepte imperativer Sprachen
2-92
Beispiel
func EUKLID(x, y: int): int begin
var a,b,r: int
a ← x;
b ← y;
while b # 0 do
r ← a mod b;
a ← b;
b ← r;
od;
return a;
end
Für negative Werte der Parameter x und y hängt das Verhalten der
Funktion von der Implementierung des mod-Operators ab.
2.5 Konzepte imperativer Sprachen
2-93
Rekursive Definitionen
Die Funktion f : N −→ N wird durch


1







1 f (n) = 


f n2





f (3n + 1)
n = 0,
n = 1,
n ≥ 2, n gerade,
n ≥ 2, n ungerade.
rekursiv definiert.
Damit eine rekursive Funktion einen Wert liefern kann, muss
mindestens eine Alternative eine Abbruchbedingung enthalten.
2.6 Rekursionen
2-94
Auswertung von Funktionen
Funktionsdefinitionen können als Ersetzungssysteme gesehen
werden. Funktionswerte lassen sich aus dieser Sicht durch
wiederholtes Einsetzen berechnen. Die Auswertung von f (3) ergibt
f (3) → f (10) → f (5) → f (16) → f (8) → f (4) → f (2) →
f (1) → 1.
Terminiert der Einsetzungsprozess stets?
2.6 Rekursionen
2-95
Formen der Rekursion
◮
Lineare Rekursion
In jedem Zweig einer Fallunterscheidung tritt die Rekursion
höchstens einmal auf. Bei der Auswertung ergibt sich eine
lineare Folge von rekursiven Aufrufen.
◮
Endrekursion
Der Spezialfall einer linearen Rekursion bei dem in jedem
Zweig die Rekursion als letzte Operation auftritt.
Endrekursionen können effizient implementiert werden.
2.6 Rekursionen
2-96
Formen der Rekursion
◮
◮
Verzweigende Rekursion oder Baumrekursion


! 
k = 0, k = n,
1



n

!
!
=
n−1
n−1


k

+
sonst.

 k −1
k
Geschachtelte Rekursion



m+1
n = 0,




f (n, m) = 
f (n − 1, 1)
m = 0,




f (n − 1, f (n, m − 1)) sonst.
2.6 Rekursionen
2-97
Formen der Rekursion
◮
Verschränkte Rekursion oder wechselseitige Rekursion
Der rekursive Aufruf erfolgt indirekt.



n = 0,
true
even(n) = 

odd(n − 1) n > 0.



n = 0,
false
odd(n) = 

even(n − 1) n > 0.
2.6 Rekursionen
2-98
Fibonacci-Zahlen
Definition:



n=0
0




fib(n) = 
1
n=1




fib(n − 1) + fib(n − 2) n ≥ 2
Funktionswerte:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, . . .
2.6 Rekursionen
2-99
Fibonacci-Zahlen
Auswertung:
fib5
fib4
fib3
fib3
fib2
2.6 Rekursionen
fib1
fib0
1
0
fib2
fib2
fib1
fib1
fib1
fib0
fib1
fib0
1
1
0
1
0
1
2-100
Algorithmus von Euklid
Rekursive Version:



falls b = 0
a
ggT(a , b ) = 

ggT(b , a mod b ) falls b > 0
Auswertung:
ggT(36, 52) → ggT(52, 36) → ggT(36, 16) →
ggT(16, 4) → ggT(4, 0) → 4
ggT(36, 52) = 4
2.6 Rekursionen
2-101
Fakultät
Definition:
Auswertung:



x≤0
1
x! = 

x · (x − 1)! sonst
4! = 4 · 3! = 4 · 3 · 2! = 4 · 3 · 2 · 1! = 4 · 3 · 2 · 1 · 0! = 4 · 3 · 2 · 1 · 1 = 24
2.6 Rekursionen
2-102
Fakultät (rekursiv)
func FAK(x: int): int begin
if x <= 0
then return 1
else return x * FAK(x - 1)
fi
end
2.6 Rekursionen
2-103
Fakultät (rekursiv)
Berechnung von FAK(2):
1. Anlegen einer lokalen Variable x , mit Wert 2.
2. x , 0, daher Rückgabewert x · FAK (x − 1) = 2 · FAK (1)
2.1 Anlegen einer neuen lokalen Variable x mit dem Wert 1.
2.2 x , 0, daher Rückgabewert x · FAK (x − 1) = 1 · FAK (0)
2.2.1 Anlegen einer neuen lokalen Variable x mit dem Wert 0.
2.2.2 x = 0, daher ist der Rückgabewert 1.
2.3 Rückgabewert ist 1 · 1 = 1
3. Rückgabewert ist 2 · 1 = 2
2.6 Rekursionen
2-104
Fakultät (iterativ)
func FAK(x: int): int begin
var result: int;
result ← 1;
while x > 0 do
result ← result * x;
x ← x - 1;
od;
return result;
end
Diese Lösung hat einen von x unabhängigen Speicherbedarf.
2.6 Rekursionen
2-105
Rekursive Probleme
Viele Probleme besitzen eine rekursive Struktur. Für sie können
rekursive Lösungen häufig kurz und elegant formuliert werden:
◮
Berechnung rekursiv definierter mathematischer Funktionen,
◮
Überprüfen der Syntax von Ausdrücken,
◮
Operationen auf rekursiv definierten Datenstrukturen.
Wichtige Unterschiede:
◮
Rekursive Formulierungen sparen explizite Laufvariablen und
Kontrollstrukturen.
◮
Rekursive Aufrufe verbrauchen Speicher auf dem Stack, es
sei denn, es handelt sich um Endrekursion, die von modernen
Compilern optimiert wird.
2.6 Rekursionen
2-106
1. Der Algorithmenbegriff
2. Imperative Algorithmen
3. Sortieralgorithmen
3.1 Einfache Sortierverfahren
3.2 Sortieren durch Mischen
3.3 Quicksort
3.4 Eine untere Schranke für Sortierverfahren
3.5 Sortieren in linearer Zeit
4. Listen und abstrakte Datentypen
5. Objektorientierte Algorithmen
6. Bäume
7. Mengen, Verzeichnisse und Hash-Verfahren
8. Graphen
9. Entwurf von Algorithmen
10. Funktionale und deduktive Algorithmen
Problemstellung
◮
Gegeben: Eine Folge ganzer Zahlen < a1 , . . . , an >. Die Folge
< a1 , . . . , an > darf gleiche Elemente enthalten.
◮
Gesucht: Eine Sortierung der Eingabefolge, d. h. eine
Permutation < a1′ , . . . , an′ > der Eingabefolge derart, dass
a1′ ≤ a2′ ≤ · · · ≤ an′ gilt.
Die Problemstellung kann auf beliebige Datentypen übertragen
werden, auf denen eine Ordnungsrelation ≤ gegeben ist.
Eine Sortierung heißt stabil, wenn sie die Reihenfolge gleicher
Listenelemente nicht ändert. Wenn ein Sortieralgorithmus nur
stabile Sortierungen liefert, heißt er ebenfalls stabil.
Ein Sortieralgorithmus arbeitet in situ (in place), wenn er nur
konstanten/geringen zusätzlichen Speicher benötigt.
3.1 Einfache Sortierverfahren
3-1
Einfache Sortierverfahren
◮
Sortieren durch Einfügen (Insertionsort)
Dieses Verfahren wurde bereits ausführlich behandelt.
◮
Sortieren durch Auswählen (Selectionsort)
◮
Sortieren durch Vertauschen (Bubblesort)
3.1 Einfache Sortierverfahren
3-2
Sortieren durch Auswählen
min(unsortiert)
Beim Sortieren durch Auswählen wird stets das kleinste Element
der noch unsortierten Folge gesucht und am Ende der sortierten
Folge eingefügt.
sortiert
3.1 Einfache Sortierverfahren
unsortiert
3-3
Sortieren durch Auswählen
for i ← 1 to n-1 do
mp ← i;
for j ← i+1 to n do
if a[j] < a[mp] then
mp ← j;
fi;
od;
tmp ← a[i];
a[i] ← a[mp];
a[mp] ← tmp;
od;
3.1 Einfache Sortierverfahren
3-4
Sortieren durch Auswählen
Die Anzahl der Vergleiche beträgt:
n−1 X
n
X
i =1 j =i +1
n−1
X
1=
(n − (i + 1) + 1)
i =1
n−1
X
=
(n − i )
i =1
=n
n−1
X
i =1
1−
n−1
X
i
i =1
1
2
= n(n − 1) − (n − 1)n
1 2 1
n − n
2
2
= Θ(n2 )
=
3.1 Einfache Sortierverfahren
3-5
Sortieren durch Vertauschen
min(unsortiert)
Beim Sortieren durch Vertauschen werden zwei benachbarte
Elemente, die sich nicht in der richtigen Reihenfolge befinden,
vertauscht. Dadurch bewegt sich das kleinste Element der
unsortierten Folge nach vorne.
r
l
sortiert
3.1 Einfache Sortierverfahren
unsortiert
3-6
Sortieren durch Vertauschen
for l ← 1 to n-1 do
for r ← n downto l + 1 do
if a[r-1] > a[r] then
tmp ← a[r];
a[r] ← a[r - 1];
a[r - 1] ← tmp;
fi;
od;
od;
3.1 Einfache Sortierverfahren
3-7
Sortieren durch Vertauschen
Die Anzahl der Vergleiche beträgt:
n−1 X
n
X
l =1 r =l +1
n−1
X
1=
(n − (l + 1) + 1)
l =1
n−1
X
(n − l )
=
l =1
=n
n−1
X
l =1
1−
n−1
X
l
l =1
1
2
= n(n − 1) − (n − 1)n
1 2 1
n − n
2
2
= Θ(n2 )
=
3.1 Einfache Sortierverfahren
3-8
Sortieren durch Mischen
Sortieren durch Mischen (Mergesort) arbeitet rekursiv nach
folgendem Schema:
1. Teile die Folge in zwei Teilfolgen auf.
2. Sortiere die beiden Teilfolgen.
3. Mische die sortierten Teilfolgen.
3.2 Sortieren durch Mischen
4
2
9
4
2
9
2
4
1
5
2
1
6
5
8
2
1
6
5
9
1
2
6
8
2
2
6
8
9
4
8
5
3-9
Sortieren durch Mischen
Die Funktion Mergesort:
func Mergesort(a, p, r): begin
if p < r then
q ← ⌊(p+r)/2⌋;
Mergesort(a,p,q);
Mergesort(a,q+1,r);
Merge(a,p,q,r);
fi;
end
Aufruf: Mergesort(a,1,n)
3.2 Sortieren durch Mischen
3-10
Sortieren durch Mischen
func Merge(a, p, q, r): begin
n1 ← q-p+1; n2 ← r-q;
for i ← 1 to n1 do l[i] ← a[p+i-1]; od;
for j ← 1 to n2 do r[j] ← a[q+j]; od;
l[n1+1] ← ∞;
r[n2+1] ← ∞;
i ← 1;
j ← 1;
for k ← p to r do
if l[i] ≤ r[j]
then a[k] ← l[i];
i ← i+1;
else a[k] ← r[j];
j ← j+1;
od;
end
3.2 Sortieren durch Mischen
3-11
Sortieren durch Mischen
Schleifeninvariante:
Zu Beginn jeder Iteration der For-Schleife enthält das Teilfeld
a [p ..k − 1] die k − p kleinsten Elemente aus l [1..n1 + 1] und
r [1..n2 + 1] in sortierter Reihenfolge. Darüber hinaus sind l [i ] und
r [j ] die kleinsten Elemente ihrer Felder, die noch nicht nach a
zurückkopiert wurden.
3.2 Sortieren durch Mischen
3-12
Sortieren durch Mischen
◮
◮
Der Aufruf Merge(a, p, q, r) benötigt Θ(n) Schritte, wobei
n = r − p + 1 ist.
Die Anzahl der Schritte des Aufrufs Mergesort(a,1,n) ist
durch die folgende Rekursionsgleichung gegeben:



falls n = 1,
Θ(1),
T (n) = 

2T (n/2) + Θ(n), falls n > 1.
◮
Wir zeigen gleich: T (n) = Θ(n log(n)).
◮
Mergesort ist damit einer der schnellsten Algorithmen, die nur
mit Vergleichen zwischen Elementen arbeiten. Der
Algorithmus wird z. B. von Sun in den Java-Bibliotheken
verwendet.
◮
Mergesort ist ein Teile-und-Beherrsche-Algorithmus.
3.2 Sortieren durch Mischen
3-13
Teile-und-Beherrsche-Algorithmen
◮
Die Teile-und-Beherrsche-Methode (divide-and-conquer) ist
eine Vorgehensweise zum Entwurf von Algorithmen.
◮
Diese Methode kann bei einer rekursiven Problemstruktur
eingesetzt werden.
Teile-und-Beherrsche-Algorithmen bestehen aus drei Phasen:
◮
◮
◮
◮
Teile das Problem in eine Anzahl von gleichartigen
Teilproblemen auf.
Beherrsche diese Teilprobleme durch Rekursion. Wenn ein
Teilproblem hinreichend klein ist, wird es direkt gelöst.
Verbinde die Lösungen der Teilprobleme zur Gesamtlösung.
◮
Mergesort: Ein Problem der Größe n wird in 2 Teilprobleme
der Größe n/2 aufgeteilt.
◮
Allgemeiner: Ein Problem der Größe n wird in a Teilprobleme
der Größe n/b aufgeteilt.
3.2 Sortieren durch Mischen
3-14
Rekursionsgleichungen
Bei der Analyse von Teile-und-Beherrsche-Algorithmen treten
häufig Rekursionsgleichungen der folgenden Form auf:



falls n klein ist,
Θ(1),
T (n) = 

aT (n/b ) + f (n), falls n groß genug ist.
n/b ist hier als geeignete Rundung ⌊ bn ⌋ oder ⌈ bn ⌉ zu lesen.
Kurzschreibweise: T (n) = aT (n/b ) + f (n)
Lösungsmethoden:
◮
Substitutionsmethode
◮
Rekursionsbaummethode
◮
Mastertheorem
3.2 Sortieren durch Mischen
3-15
Rekursionsgleichungen
Mastertheorem (Beweis s. Cormen et al.):
◮
1. Fall: f (n) ist „polynomial kleiner“ als nlogb a .
Dann gilt T (n) = Θ(nlogb a ).
◮
2. Fall: f (n) ist „genauso groß“ wie nlogb a .
Dann gilt T (n) = Θ(nlogb a log(n)).
◮
3. Fall: f (n) ist „polynomial größer“ als nlogb a , und es gilt die
Regularitätsbedingung af (n/b ) ≤ cf (n) für eine Konstante
c < 1 und große n.
Dann gilt T (n) = Θ(f (n)).
3.2 Sortieren durch Mischen
3-16
Quicksort
1. Zerlege das zu sortierende Feld a [p ..r ] so in zwei
(möglicherweise leere) Teilfelder a [p ..q − 1] und a [q + 1..r ],
dass jedes Element von a [p ..q − 1] kleiner oder gleich a [q] ist
und dieses wiederum kleiner oder gleich jedem Element von
a [q + 1..r ] ist. Das Element a [q] heißt Pivot-Element.
2. Sortiere die Teilfelder a [p ..q − 1] und a [q + 1..r ] rekursiv.
3. Da die Teilfelder in-place sortiert werden, ist das Feld a [p ..r ]
danach sortiert.
4. Aufruf: quicksort(a,1,n).
3.3 Quicksort
3-17
Quicksort
proc quicksort(a: int []; p, r: int)
begin
var q: int;
if p < r then
q ← partition(a, p, r);
quicksort(a, p, q-1);
quicksort(a, q+1, r);
fi;
end
3.3 Quicksort
3-18
Quicksort
partition(a, p, r)
◮
Eingabeparameter: Das Feld a sowie die Grenzen p und r .
◮
Aufgabe: Aufteilen des Felds a in zwei Teilfelder a [p ..q − 1]
und a [q + 1..r ]. Die Teilfelder können leer sein.
◮
Rückgabewert: Index q.
Anschließend gilt:
∀k ∈ {p , . . . , q − 1}.a [k ] ≤ a [q],
∀k ∈ {q + 1, . . . , r }.a [q] ≤ a [k ].
3.3 Quicksort
3-19
Quicksort
1. Zwei Indizes i und j durchlaufen das Feld von links nach
rechts.
2. Initialisierung: i = p − 1, j = p .
3. Es gilt: p − 1 ≤ i ≤ j ≤ r − 1.
4. i und j teilen a [p ..r ] in vier Teilfelder auf:
p≤k ≤i:
i+1≤k ≤j−1:
j ≤k ≤r −1:
k =r:
3.3 Quicksort
a [k ] ≤ x .
a [k ] > x .
noch unbestimmt.
a [k ] = x .
3-20
Quicksort
func partition(a: int []; p, r: int): int
begin
var x, i, j: int;
x ← a[r];
i ← p-1;
for j ← p to r-1 do
if a[j] ≤ x
then i ← i+1;
swap(a[i], a[j]);
fi;
od;
swap(a[i+1], a[r]);
return i+1;
end
3.3 Quicksort
3-21
Laufzeit
3.3 Quicksort
◮
Die Laufzeit von Quicksort hängt davon ab, ob die Zerlegung
balanciert oder unbalanciert ist.
◮
Dies wiederum hängt von der Wahl der Pivot-Elemente ab.
◮
Wenn die Zerlegung balanciert ist, läuft der Algorithmus
asymptotisch so schnell wie „Sortieren durch Mischen“.
◮
Wenn die Zerlegung unbalanciert ist, kann der Algorithmus
asymptotisch so langsam wie „Sortieren durch Einfügen“
laufen.
3-22
Laufzeit im ungünstigsten Fall
◮
Für die Laufzeit im ungünstigsten Fall erhalten wir
T (n) = max (T (q) + T (n − q − 1)) + Θ(n).
0≤q ≤n −1
Es gilt dann: T (n) = O (n2 ).
3.3 Quicksort
◮
Außerdem gilt:
Im unbalancierten Fall ist die Laufzeit von Quicksort Θ(n2 ).
◮
Aus beiden Aussagen folgt:
Die Laufzeit von Quicksort ist im ungünstigsten Fall Θ(n2 ).
3-23
Laufzeit im günstigsten und im mittleren Fall
◮
Günstigster Fall: Dieser Fall tritt ein, wenn die Aufteilung so
gleichmäßig wie möglich erfolgt. Dann ist
T (n) ≤ 2T (n/2) + Θ(n).
Aus dem Mastertheorem folgt
T (n) = O (n log(n)).
◮
Mittlerer (erwarteter) Fall: Es seien alle Eingabefolgen gleich
wahrscheinlich. Dann gilt (ohne Beweis)
T (n) = O (n log(n)).
3.3 Quicksort
3-24
Modifikationen von Quicksort
Der Analyse für den mittleren Fall geht davon aus, dass alle
Eingaben gleich wahrscheinlich sind. Dies ist oft nicht der Fall. So
treten z. B. teilweise vorsortierte Folgen auf.
Man kann durch geeignete Verfahren Zufälligkeit erzwingen:
◮
Die Eingabe wird vor dem Sortieren nach einem Zufallsprinzip
gemischt. Dies ist in O (n) Schritten möglich.
◮
Wähle das Pivot-Element zufällig.
Der ungünstigste Fall kann dann zwar noch vorkommen, ist jedoch
unwahrscheinlicher geworden.
3.3 Quicksort
3-25
Vorheriges Mischen
proc randomize(a: int []; p, r : int)
begin
for i ← p to r do
swap(a[i], a[random(p, r)]);
od;
end
Dadurch erhält die Eingabe in O(n) Schritten eine zufällige
Reihenfolge. Der Aufruf lautet dann:
randomize(a,1,n);
quicksort(a,1,n);
3.3 Quicksort
3-26
Zufälliges Pivot-Element
func randomizedPartition(a: int []; p, r: int): int
begin
var i: int;
i ← random(p,r);
swap(a[i], a[r]);
return partition(a,p,r);
end
proc randomizedQuicksort(a: int []; p, r: int)
begin
var q: int;
if p < r then
q ← randomizedPartition(a, p, r);
randomizedQuicksort(a, p, q-1);
randomizedQuicksort(a, q+1, r);
fi;
end
3.3 Quicksort
3-27
Eine untere Schranke für Sortierverfahren
◮
Satz: Die Laufzeit von Sortieralgorithmen, die auf Vergleichen
von Elementen beruhen, liegt in Ω(n log n).
◮
Beweis: Ein Entscheidungsbaum ist ein Binärbaum, dessen
Knoten die Markierung < ai : aj > für einen durchgeführten
Vergleich von ai und aj haben. Die Wurzel entspricht dem
ersten Vergleich zweier Elemente. Das linke Kind von
< ai : aj > entspricht dem Vergleich, der im Falle ai < aj folgt,
das rechte Kind dem Vergleich, der im Falle ai ≥ aj folgt.
< ai , aj >
<
3.4 Eine untere Schranke für Sortierverfahren
≥
< ak , al >
< am , an >
< ··· ≥
< ··· ≥
3-28
Eine untere Schranke für Sortierverfahren
◮
Ein Entscheidungsbaum von Insertionsort zur Folge
(a1 , a2 , a3 ) ist:
< a1 , a2 >
≥
<
< a2 , a3 >
<
(a1 , a2 , a3 )
< a1 , a3 >
≥
<
< a1 , a3 >
<
(a1 , a3 , a2 )
(a2 , a1 , a3 )
≥
(a3 , a1 , a2 )
≥
< a2 , a3 >
<
(a2 , a3 , a1 )
≥
(a3 , a2 , a1 )
◮
Die Blätter repräsentieren die sortierten Felder. Alle 3! = 6
Möglichkeiten sind berücksichtigt.
◮
Jeder Entscheidungsbaum zu einer Folge (a1 , . . . , an ) hat
unabhängig vom Vergleichsverfahren n! Blätter.
3.4 Eine untere Schranke für Sortierverfahren
3-29
Eine untere Schranke für Sortierverfahren
◮
Satz: Jeder Entscheidungsbaum zu einer Folge (a1 , . . . , an )
besitzt die Höhe Ω(n log n).
◮
Beweis: Ein Entscheidungsbaum zu (a1 , . . . , an ) hat n!
Blätter. Binärbäume der Höhe h haben höchstens 2h Blätter.
◮
◮
◮
n! ≤ 2h ⇒ log2 (n!) ≤ log2 (2h ) = h
n
√
Für große n gilt (Stirlingsche Formel): n! ≈ 2πn en
n
Es folgt: h ≥ log2 en = n log2 (n) − n log2 (e ) = Ω(n log n)
◮
Jeder Lauf eines auf Vergleichen basierenden
Sortieralgorithmus entspricht genau einem Pfad von der
Wurzel zu einem Blatt des zugehörigen
Entscheidungsbaumes.
◮
Die Laufzeit ist daher nach unten durch die Höhe des
Entscheidungsbaumes beschränkt, liegt also in Ω(n log n).
◮
Entscheidungsbäume berücksichtigen ausschließlich die
vorgenommenen Vergleiche, keine weiteren Operationen.
3.4 Eine untere Schranke für Sortierverfahren
3-30
Sortieren in linearer Zeit
◮
Die bisher besprochenen Sortieralgorithmen haben im
günstigsten Fall eine Laufzeit von Ω(n log n).
◮
Das Sortieren von n Elementen erfordert mindestens das
Betrachten jedes Elements, die Laufzeit liegt also in Ω(n).
◮
Es gibt Verfahren, die in linearer Zeit arbeiten, diese
verwenden aber nicht den direkten Vergleich von Elementen
(s. obigen Satz).
3.5 Sortieren in linearer Zeit
3-31
Countingsort
Voraussetzungen:
◮
◮
Jeder der n zu sortierenden Werte ist eine natürliche Zahl aus
{0, . . . , k } mit k ∈ N.
Die zu sortierende Zahlenfolge ist in dem Feld a [1..n]
gespeichert.
Algorithmus:
1. Bestimme für jeden Wert x aus a die Anzahl count(x ) von
Werten aus a , die kleiner oder gleich x sind.
2. Speichere die Elemente x aus a von hinten beginnend im
Ergebnisfeld an der Position count(x ) und dekrementiere
count(x ).
3.5 Sortieren in linearer Zeit
3-32
Countingsort
var
var
for
for
count: int [0..k];
result: int [1..n];
i ← 0 to k do count[i] ← 0; od;
i ← 1 to n do
count[a[i]] ← count[a[i]] + 1; od;
// count[a [i ]] = |{x | x = i }|
for i ← 1 to k do
count[i] ← count[i] + count[i-1]; od;
// count[a [i ]] = |{x | x ≤ i }|
for i ← n downto 1 do
result[count[a[i]]] ← a[i];
count[a[i]] ← count[a[i]] - 1;
od;
3.5 Sortieren in linearer Zeit
3-33
Countingsort
var
var
for
for
count: int [0..k];
result: int [1..n];
i ← 0 to k do count[i] ← 0; od;
i ← 1 to n do
count[a[i]] ← count[a[i]] + 1; od;
for i ← 1 to k do
count[i] ← count[i] + count[i-1]; od;
for i ← n downto 1 do
result[count[a[i]]] ← a[i];
count[a[i]] ← count[a[i]] - 1;
od;
3.5 Sortieren in linearer Zeit
Θ(k )
Θ(n)
Θ(k )
Θ(n)
3-34
Countingsort
◮
Die Laufzeit liegt in Θ(n + k ).
◮
Das Verfahren ist schnell, wenn k nicht wesentlich größer als
n ist.
◮
Wenn k als konstant gesehen wird, liegt die Laufzeit in Θ(n).
◮
Countingsort ist ein stabiler Sortieralgorithmus.
3.5 Sortieren in linearer Zeit
3-35
Radixsort
Voraussetzungen:
◮
◮
Jeder der n zu sortierenden Werte ist eine natürliche Zahl aus
{0, . . . , 10m − 1} mit m ∈ N.
Die zu sortierende Zahlenfolge ist in dem Feld a [1..n]
gespeichert.
Algorithmus:
1. Sortiere für alle Stellen j = 0, . . . , m − 1 das Feld a mit einem
stabilen Sortieralgorithmus, der in Θ(n) arbeitet.
2. Beginne mit der niederwertigsten Stelle.
3. Countingsort ist stabil, arbeitet in Θ(n) und kann daher als
Sortieralgorithmus eingesetzt werden.
3.5 Sortieren in linearer Zeit
3-36
Radixsort
Beispiel: m = 3. Die Werte sind dreistellig und liegen daher im
Bereich 0, . . . , 999.
Index
1
2
3
4
5
3.5 Sortieren in linearer Zeit
a
293
127
126
325
225
j=0
293
325
225
126
127
j=1
325
225
126
127
293
j=2
126
127
225
293
325
3-37
Radixsort
◮
Countingsort wird m-mal aufgerufen. Die Laufzeit von
Countingsort beträgt Θ(n + k ) mit k = 10.
◮
Somit gilt für die Laufzeit T (n) von Radixsort
T (n) = Θ((n + k )m) = Θ((n + 10)m) = Θ(nm + 10m)
◮
Wenn m als konstant angesehen wird, folgt also T (n) = Θ(n).
3.5 Sortieren in linearer Zeit
3-38
Radixsort
◮
Die Stabilität des verwendeten Sortieralgorithmus ist von
entscheidender Bedeutung.
◮
Radixsort ist auch auf komplexe Vergleichsobjekte
anwendbar. So kann zum Beispiel bei Datumsangaben
Countingsort auf Tag, Monat und Jahr (in dieser Reihenfolge)
angewendet werden.
◮
Radixsort wird schneller, wenn größere Basen als 10 gewählt
werden.
3.5 Sortieren in linearer Zeit
3-39
Bucketsort
Voraussetzungen:
◮
Die n zu sortierenden Werte entstammen einer zufälligen
Auswahl. Hier: Sie sind gleichverteilt aus dem Intervall [0, 1).
◮
Die zu sortierende Zahlenfolge ist in dem Feld a [1..n]
gespeichert.
Algorithmus:
1. Unterteile das Intervall in n gleich große Bereiche.
2. Ordne jedem dieser Bereiche einen „Behälter“ (Bucket) zur
Aufnahme der in dieses Intervall gehörenden Elemente zu.
3. Speichere jeden der n Werte in dem ihm zugeordneten
Behälter.
4. Sortiere die Elemente in jedem Behälter.
5. Gib Behälter für Behälter die darin enthaltenen Elemente aus.
3.5 Sortieren in linearer Zeit
3-40
Bucketsort
var b: Liste [0..n-1] von int;
for i ← 1 to n do
Füge a [i ] zum Bucket b [⌊n · a [i ]⌋] hinzu.
od;
for i ← 0 to n-1 do
Sortiere b [i ], z. B. mit Insertionsort.
od;
for i ← 0 to n-1 do
Gib Behälter b [i ] aus.
od;
Für die erwartete Laufzeit von Bucketsort gilt T (n) = Θ(n). Einen
Beweis dieser Aussage findet man in Cormen et al.
Die Laufzeit für den ungünstigsten Fall liegt offensichtlich in O (n2 ).
3.5 Sortieren in linearer Zeit
3-41
Bucketsort
3.5 Sortieren in linearer Zeit
1
0.78
()
0
2
0.17
(0.12, 0.17)
1
3
0.39
(0.21, 0.23, 0.26)
2
4
0.23
(0.39)
3
5
0.72
()
4
6
0.94
()
5
7
0.26
(0.68)
6
8
0.12
(0.72, 0.78)
7
9
0.21
()
8
10
0.68
(0.94)
9
Feld a
Feld b
3-42
Zusammenfassender Vergleich der Sortieralgorithmen
Algorithmus
Worst-Case
Average-Case
In situ
Insertionsort
Selectionsort
Bubblesort
Mergesort
Quicksort
Heapsort (später)
Countingsort
Radixsort
Bucketsort
O (n2 )
O (n2 )
O (n2 )
O (n log n)
O (n2 )
O (n log n)
O (n)
O (n)
O (n2 )
O (n2 )
O (n2 )
O (n2 )
O (n log n)
O (n log n)
O (n log n)
O (n)
O (n)
O (n)
Ja
Ja
Ja
Nein
Ja
Ja
Nein*
Nein*
Nein
*) Nicht bei der hier gegebenen Implementierung.
3.5 Sortieren in linearer Zeit
3-43
1. Der Algorithmenbegriff
2. Imperative Algorithmen
3. Sortieralgorithmen
4. Listen und abstrakte Datentypen
4.1 Abstrakte Datentypen
4.2 Listen
4.3 Keller
4.4 Schlangen
5. Objektorientierte Algorithmen
6. Bäume
7. Mengen, Verzeichnisse und Hash-Verfahren
8. Graphen
9. Entwurf von Algorithmen
10. Funktionale und deduktive Algorithmen
Datentypen
Unter einem Datentyp versteht man die Zusammenfassung von
Wertebereichen und Operationen zu einer Einheit.
◮
Abstrakter Datentyp: Schwerpunkt liegt auf den
Eigenschaften, die die Wertebereiche und Operationen
besitzen.
◮
Konkreter Datentyp: Realisierung der Wertebereiche und
Operationen stehen im Vordergrund.
◮
Primitive Datentypen: bool, char, int, real, . . .
4.1 Abstrakte Datentypen
4-1
Datentypen
Komplexe Datentypen, sog. Datenstrukturen, werden durch
Kombination primitiver Datentypen gebildet. Sie besitzen selbst
spezifische Operationen. Datenstrukturen können vorgegeben
oder selbstdefiniert sein.
Dabei wird über das Anwendungsspektrum unterschieden in
◮
Generische Datentypen: Werden für eine Gruppe ähnlicher
Problemstellungen entworfen und sind oft im Sprachumfang
bzw. der Bibliothek einer Programmiersprache enthalten
(Feld, Liste, Keller, Schlange, Verzeichnis, . . . ).
◮
Spezifische Datentypen: Dienen der Lösung einer eng
umschriebenen Problemstellung und werden im
Zusamenhang mit einem konkreten Problem definiert.
4.1 Abstrakte Datentypen
4-2
Entwurfsprinzipien für Datentypen
Anforderungen an die Definition eines Datentyps:
◮
Die Spezifikation eines Datentyps sollte unabhängig von
seiner Implementierung erfolgen. Dadurch kann die
Spezifikation für unterschiedliche Implementierungen
verwendet werden.
◮
Reduzierung der von außen sichtbaren (zugänglichen)
Aspekte auf die Schnittstelle des Datentyps. Dadurch kann
die Implementierung später verändert werden, ohne dass
Programmteile, die den Datentyp benutzen, angepasst
werden müssen.
4.1 Abstrakte Datentypen
4-3
Entwurfsprinzipien für Datentypen
Aus diesen Anforderungen heraus ergeben sich zwei Prinzipien:
◮
Kapselung (encapsulation): Alle Zugriffe geschehen immer
nur über die Schnittstelle des Datentyps.
◮
Geheimnisprinzip (programming by contract): Die interne
Realisierung des Datentyps bleibt dem Benutzer verborgen.
4.1 Abstrakte Datentypen
4-4
Abstrakte Datentypen
Informatik-Duden: Ein Datentyp, von dem nur die Spezifikation und
Eigenschaften (in Form von zum Beispiel Regeln oder
Gesetzmäßigkeiten) bekannt sind, heißt abstrakt. Man abstrahiert
hierbei von der konkreten Implementierung.
Dies kann für
◮
eine klarere Darstellung,
◮
für den Nachweis der Korrektheit oder
◮
für Komplexitätsuntersuchungen
von Vorteil sein.
Ein abstrakter Datentyp wird kurz als ADT bezeichnet.
Ein ADT wird ohne Kenntnis der internen Realisierung verwendet
(Geheimnisprinzip). Dabei wird nur von der Schnittstelle
(Kapselung) Gebrauch gemacht.
4.1 Abstrakte Datentypen
4-5
Abstrakte Datentypen
Wir werden ADTen durch algebraische Spezifikationen
beschreiben:
◮
Eine Signatur bildet die Schnittstelle eines ADTs.
◮
Mengen und Funktionen, die zur Signatur „passen“, heißen
Algebren.
◮
Axiome schränken die möglichen Algebren ein.
Der Themenkomplex „algebraische Spezifikationen“ wird hier nur
einführend behandelt.
4.1 Abstrakte Datentypen
4-6
Signaturen
Eine Signatur Σ = (S , Ω) besteht aus
◮
einer Menge von Sorten S und
◮
einer Menge von Operatorsymbolen Ω.
Jedes Operatorsymbol f : s1 . . . sn → s besteht aus einem
Namen f , einer Folge s1 , . . . , sn ∈ S , n ≥ 0, von
Argumentsorten und einer Wertesorte s ∈ S .
Operatorsymbole ohne Parameter heißen Konstante.
4.1 Abstrakte Datentypen
4-7
Algebren
Es sei eine Signatur Σ = (S , Ω) gegeben. Eine Algebra
AΣ = (AS , AΩ ) zur Signatur Σ besteht aus
◮
◮
den Trägermengen As der Sorten s ∈ S ,
d. h. AS = {As | s ∈ S }, und
(partiellen) Funktionen auf den Trägermengen
Af : As1 × . . . × Asn → As ,
d. h. AΩ = {Af | f : s1 . . . sn → s ∈ Ω}.
4.1 Abstrakte Datentypen
4-8
Beispiel
Eine Signatur für den ADT „Bool“ sei (vorläufig) gegeben durch:
S = {Bool }
Ω = {true :→ Bool ,
false :→ Bool }
Mögliche Algebren für diese Spezifikation sind:
ABool = {T , F }
Atrue ≔ T
Afalse ≔ F
erwartungskonform
ABool = N
Atrue ≔ 1
Afalse ≔ 0
große Trägermenge
ABool = {1}
Atrue ≔ 1
Afalse ≔ 1
kleine Trägermenge
4.1 Abstrakte Datentypen
4-9
Axiome
◮
Die Zahl der möglichen Algebren kann durch Axiome
eingeschränkt werden.
◮
Axiome sind (hier) Gleichungen, die die Funktionen in ihrer
Wirkung einengen.
◮
Eine Menge von Axiomen bezeichnen wir mit Φ.
4.1 Abstrakte Datentypen
4-10
Signaturdiagramme
Signaturen lassen sich übersichtlich durch Signaturdiagramme mit
Sorten als Knoten und Operatorsymbolen als Kanten darstellen:
s0
f
..
.
s
sn
Ausblick: Signaturdiagramme sind Beispiele für Graphen, die wir in
Kürze betrachten werden.
4.1 Abstrakte Datentypen
4-11
Notationen für Operatorsymbole
Mit dem Platzhaltersymbol _ für Argumente von Funktionen führen
wir die folgenden Notationen ein:
Präfix:
f (_), + + _, . . .
f (x ), + + i
Infix:
_ ≤ _, _ + _, _ ∨ _, . . .
Postfix:
_!, _2 , . . .
a ≤ b , m + n, p ∨ q
Mixfix:
|_|, if _then_else _fi , . . .
|x |
n!, x 2
Bei der Präfixnotation schreiben wir auch kurz f .
4.1 Abstrakte Datentypen
4-12
ADT der Wahrheitswerte
S = {Bool }
Ω = {true :→ Bool ,
false :→ Bool ,
¬_ : Bool → Bool ,
_ ∨ _ : Bool × Bool → Bool ,
_ ∧ _ : Bool × Bool → Bool }
Φ = {x ∧ false = false ∧ x = false ,
x ∧ true = true ∧ x = x ,
true
Bool
¬
false
∨, ∧
x ∨ true = true ∨ x = true ,
false ∨ false = false ,
¬false = true , ¬true = false }
4.1 Abstrakte Datentypen
4-13
ADT der natürlichen Zahlen
S = {Nat }
Ω = {0 :→ Nat ,
succ
0
Nat
succ : Nat → Nat }
Φ = {}
◮
Damit wird z. B. die Zahl 3 als
succ (succ (succ (0))) = succ 3 (0) dargestellt.
◮
Der Term succ n (0) stellt die natürliche Zahl n dar. Da es keine
Axiome gibt, kann dieser Term nicht vereinfacht werden.
4.1 Abstrakte Datentypen
4-14
ADT der natürlichen Zahlen
S = {Nat }
Ω = {0 :→ Nat ,
succ : Nat → Nat ,
add : Nat × Nat → Nat }
succ
0
Nat
Φ = {add (x , 0) = x ,
add (x , succ (y )) =
add
succ (add (x , y ))}
Dies ist eine formale Spezifikation der natürlichen Zahlen mit der
Konstanten 0, der Nachfolgerfunktion und der Addition.
Implementierungen sind nicht verpflichtet, die Operationen gemäß
der Axiome zu realisieren. Sie müssen lediglich das durch die
Axiome beschriebene Verhalten gewährleisten.
4.1 Abstrakte Datentypen
4-15
Beispiel
Es soll 2 + 3 berechnet werden.
2 + 3 = add (succ (succ (0)), succ (succ (succ (0))))
= succ (add (succ (succ (0)), succ (succ (0))))
= succ (succ (add (succ (succ (0)), succ (0))))
= succ (succ (succ (add (succ (succ (0)), 0))))
= succ (succ (succ (succ (succ (0)))))
Der ADT Nat erfüllt eine besondere Eigenschaft: Jeder Term
besitzt eine eindeutige Normalform succ n (0). Diese entsteht, wenn
man die Gleichungen von links nach rechts anwendet, bis alle
add-Operationssymbole verschwunden sind.
4.1 Abstrakte Datentypen
4-16
Implementierung eines abstrakten Datentyps
Implementierung eines ADTs heißt:
◮
Realisierung der Sorten s ∈ S durch Datenstrukturen As
Beispiel: Nat { B m (m-stellige Vektoren über {0, 1})
◮
Realisierung der Operatoren f : s1 . . . sn → s durch
Funktionen Af : As1 × . . . × Asn → As
Beispiel: add : Nat × Nat → Nat { _ + _ : B m × B m → B m
◮
Sicherstellen, dass die Axiome (in den Grenzen der
darstellbaren Werte bzw. der Darstellungsgenauigkeit) gelten.
4.1 Abstrakte Datentypen
4-17
Implementierung eines abstrakten Datentyps
Beispiel: ANat = B m
Darstellung von x ∈ N mit m Binärziffern zm−1 , . . . , z0 ∈ B :
x=
m
−1
X
i =0
zi · 2i
Darstellbarer Zahlenbereich: 0 ≤ x ≤ 2m − 1
Die Gültigkeit der Rechengesetze muss gewährleistet sein.
4.1 Abstrakte Datentypen
4-18
Alternative Notation
Im Folgenden wird alternativ zur mathematischen Schreibweise
folgende an Programmiersprachen angelehnte Notation genutzt:
S = {Nat }
Ω = {0 :→ Nat ,
succ : Nat → Nat ,
add : Nat × Nat → Nat }
Φ = {add (x , 0) = x ,
add (x , succ (y )) =
succ (add (x , y ))}
4.1 Abstrakte Datentypen
type Nat
import ∅
operators
0 :→ Nat
succ : Nat → Nat
add : Nat × Nat → Nat
axioms ∀i , j ∈ Nat
add (i , 0) = i
add (i , succ (j )) =
succ (add (i , j ))
4-19
Algebraische Spezifikationen
Eine Import-Anweisung erlaubt die Angabe der Sorten, die
zusätzlich zur zu definierenden Sorte benötigt werden.
Eine algebraische Spezifikation eines ADTs besteht aus
◮
einer Signatur und
◮
aus Axiomen und ggf. zusätzlich
◮
aus Import-Anweisungen.
Eine Algebra, die die Axiome erfüllt, heißt Modell der Spezifikation.
Auf die Frage nach der Existenz und Eindeutigkeit von Modellen
können wir hier nicht eingehen.
4.1 Abstrakte Datentypen
4-20
Lineare Datentypen
4.2 Listen
◮
In diesem Kapitel besprechen wir die linearen Datentypen
Liste, Keller und Schlange.
◮
Nichtlineare Datentypen sind zum Beispiel Bäume und
Graphen, die später behandelt werden.
◮
In vielen Programmiersprachen sind lineare Datentypen und
ihre grundlegenden Operationen Bestandteil der Sprache
oder in Bibliotheken verfügbar.
◮
Listen spielen in der funktionalen Programmierung eine große
Rolle.
4-21
Listen
◮
Eine (lineare) Liste ist eine Folge von Elementen eines
gegebenen Datentyps.
◮
Es können jederzeit Elemente in eine Liste eingefügt oder
Elemente aus einer Liste gelöscht werden.
◮
Der Speicherbedarf einer Liste ist daher dynamisch, d. h., er
steht nicht zur Übersetzungszeit fest, sondern kann sich noch
während der Laufzeit ändern.
◮
Listen und ihre Varianten sind die wichtigsten dynamischen
Datenstrukturen überhaupt.
x1
4.2 Listen
x2
x3
...
xn
4-22
Typische Operationen für Listen
4.2 Listen
◮
Erzeugen einer leeren Liste
◮
Testen, ob eine Liste leer ist
◮
Einfügen eines Elements am Anfang/Ende einer Liste
◮
Löschen eines Elements am Anfang/Ende einer Liste
◮
Rückgabe des ersten/letzten Elements einer Liste
◮
Bestimmen von Teillisten, zum Beispiel: Liste ohne
Anfangselement
◮
Testen, ob ein gegebener Wert in einer Liste enthalten ist
◮
Berechnen der Länge einer Liste
◮
Bestimmen des Vorgängers/Nachfolgers eines Listenelements
4-23
Parametrisierte Datentypen
4.2 Listen
◮
Die im Folgenden eingeführten abstrakten Datentypen sind
parametrisiert.
◮
Die Spezifikation eines abstrakten Datentyps kann ein oder
mehrere Sortenparameter enthalten, die unterschiedlich
instanziiert werden können.
◮
Beispiel: In der Spezifikation der Listen tritt der Parameter T
auf. List (T ) kann dann beispielsweise durch List (Bool ),
List (Nat ) oder List (List (Nat )) instanziiert werden.
4-24
Listen
[]
length
head
T
Liste
:
4.2 Listen
Nat
tail
type List (T )
import Nat
operators
[] :→ List
_ : _ : T × List → List
head : List → T
tail : List → List
length : List → Nat
axioms ∀l ∈ List , ∀x ∈ T
head (x : l ) = x
tail (x : l ) = l
length ([]) = 0
length (x : l ) = succ (length (l ))
4-25
Implementierungen
4.2 Listen
◮
Listen können mithilfe verketteter Strukturen implementiert
werden. Hier gibt es viele Varianten: einfache und doppelte
Verkettung, Zeiger auf das erste und/oder letzte Element der
Liste, zirkuläre Listen, . . .
◮
Alternativ können Listen durch Felder implementiert werden.
Die Methoden sehen dann natürlich anders aus.
Beispielsweise müssen beim Einfügen eines Elements am
Anfang der Liste alle anderen Elemente um eine Position
nach hinten verschoben werden.
◮
Dynamische Datenstrukturen nutzen den zur Verfügung
stehenden Speicherplatz weniger effizient aus. Wenn der
benötigte Speicherplatz vor dem Programmlauf genau
abgeschätzt werden kann, können statische Strukturen
sinnvoll sein.
4-26
Implementierungen
Eine Liste kann als Verkettung einzelner Objekte implementiert
werden. Man spricht von einer einfach verketteten Liste.
Beispiel: Liste von Namen („Oliver“, „Peter“, „Walter“)
head
tail
Oliver
Peter
Walter
Die Kästen stellen sogenannte Knoten (engl. node) dar. Jeder
Knoten enthält einen Wert vom Typ T und eine Referenz auf den
nächsten Knoten. Der letzte Knoten enthält den leeren Verweis.
4.2 Listen
4-27
Bestimmen des ersten Listenelements
func head(l: Liste): T begin
var k: <Referenz auf Knoten>;
k ← <Kopf der Liste l>;
if k <ungültige Referenz> then
return <keinen Wert>;
fi;
return k.wert;
end
Aufwand: Θ(1)
4.2 Listen
4-28
Einfügen eines Listenelements am Anfang
1. Erzeugen eines neuen Knotens.
2. Referenz des neuen Knotens auf den ehemaligen Kopfknoten
setzen.
3. Kopfreferenz der Liste auf den neuen Knoten setzen.
head
X
Oliver
Peter
Walter
Aufwand: Θ(1)
4.2 Listen
4-29
Einfügen eines Listenelements am Anfang
func addFirst(v: T; l: Liste): Liste begin
var k: <Referenz auf Knoten>;
k ← <neuer Knoten>;
k.wert ← v;
k.referenz ← <Kopf der Liste l>;
<Kopf der Liste l> ← k;
return l;
end
4.2 Listen
4-30
Einfügen eines Listenelements am Ende
1. Navigieren zum letzen Knoten.
2. Erzeugen eines neuen Knotens.
3. Einhängen des neuen Knotens.
head
X
Oliver
Peter
Walter
Aufwand: Θ(n)
4.2 Listen
4-31
Einfügen eines Listenelements am Ende
func addLast(v: T; l: Liste): Liste begin
var k: <Referenz auf Knoten>;
var nk: <Referenz auf Knoten>;
k ← <Kopf der Liste l>;
nk ← <neuer Knoten>;
nk.wert ← v;
nk.referenz ← <ungültige Referenz>;
if k <ungültige Referenz> then
<Kopf der Liste l> ← nk; return l;
fi;
while k.referenz <gültige Referenz> do
k ← k.referenz;
od;
k.referenz ← nk; return l;
end
4.2 Listen
4-32
Vorgänger in einfach verketteten Listen
1. Navigieren bis zum Knoten k , dabei Referenz v auf
Vorgängerknoten des aktuell betrachteten Knotens mitführen.
2. Rückgabe des Knoten v .
Aufwand: Θ(n)
Beispiel: Bestimmung des Vorgängers von „Walter“
Schritt 1 (v =⊥)
Betrachteter
Knoten: α
Schritt 2 (v = α)
Betrachteter
Knoten: β
Schritt 3 (v = β)
Betrachteter
Knoten: γ
Objekt
gefunden,
R¨
uckgabe von
v = β
head
α
Oliver
4.2 Listen
γ
β
Peter
Walter
4-33
Vorgänger in einfach verketteten Listen
Finden des m-ten Vorgängers eines Knotens k in einer Liste:
1. Navigieren bis zum Knoten k , die Referenz v wird wie vorher
mitgeführt, beginnt aber erst am Kopf der Liste, sobald der
(m + 1)-te Knoten betrachtet wird.
2. Rückgabe von v .
Beispiel: Bestimmung des zweiten Vorgängers von „Walter“
Schritt 1 (v =⊥)
Betrachteter
Knoten: α
Schritt 2 (v =⊥)
Betrachteter
Knoten: β
Schritt 3 (v = α)
Betrachteter
Knoten: γ
Objekt
gefunden,
R¨
uckgabe von
v = α
head
α
Oliver
4.2 Listen
γ
β
Peter
Walter
4-34
Vorgänger in doppelt verketteten Listen
Alternativ kann man auch die Datenstruktur ändern: Jeder Knoten
wird um eine Referenz auf den Vorgängerknoten ergänzt. Die
Suche nach dem m-ten Vorgänger von k erfolgt dann von k aus
nach vorne.
Schritt 2
Betrachteter
Knoten: α
Schritt 1
Betrachteter
Knoten: β
head
α
Oliver
4.2 Listen
γ
β
Peter
Walter
4-35
Doppelt verkettete Listen
◮
Der Zugriff auf den Vorgängerknoten wird vereinfacht.
◮
Es wird zusätzlicher Speicherplatz pro Element verbraucht.
◮
Verwaltung des zweiten Zeigers bedeutet zusätzlichen
Aufwand.
Beispiel: Löschen eines Knotens
head
Oliver
4.2 Listen
Peter
Walter
4-36
Keller
◮
Ein Keller (stack) ist eine Liste, auf die nur an einem Ende
zugegriffen werden kann.
◮
Keller arbeiten nach dem Last-In-First-Out-Prinzip und
werden deshalb auch LIFO-Speicher genannt.
x5
x4
x4
x3
x2
x1
4.3 Keller
4-37
Operationen für Keller
◮
Erzeugen eines leeren Kellers (empty)
◮
Testen, ob ein Keller leer ist (empty?)
◮
Rückgabe des ersten Elements eines Kellers (top)
◮
Einfügen eines Elements am Anfang eines Kellers (push)
◮
Löschen eines Elements am Anfang eines Kellers (pop)
Keller dürfen nur mithilfe dieser Operationen bearbeitet werden.
4.3 Keller
4-38
Implementierungen
◮
Realisierung durch eine Liste:
top
...
xn
◮
x1
Realisierung durch ein Feld:
top
x1 x2 x3
4.3 Keller
...
xn
4-39
Keller
empty
top
T
empty?
Stack
push
Bool
pop
Anmerkung:
pop (empty ) =⊥
top (empty ) =⊥
Diese Fälle bleiben
undefiniert.
4.3 Keller
type Stack (T )
import Bool
operators
empty :→ Stack
push : Stack × T → Stack
pop : Stack → Stack
top : Stack → T
empty ? : Stack → Bool
axioms ∀s ∈ Stack , ∀x ∈ T
pop (push (s , x )) = s
top (push (s , x )) = x
empty ?(empty ) = true
empty ?(push (s , x )) = false
4-40
Implementierungen
4.3 Keller
◮
Es ist sinnvoll, bei der Implementierung von Datenstrukturen
auf bereits vorhandene Strukturen zurückzugreifen.
◮
Der abstrakte Datentyp „Keller“ wird durch Rückgriff auf den
Datentyp „Liste“ realisiert.
4-41
Implementierungen
◮
Die Sorte Stack(T) wird implementiert durch die Menge
AList (T ) der Listen über T .
◮
Die Operatoren werden durch die folgenden Funktionen
implementiert:
empty = []
push (l , x ) = x : l
pop (x : l ) = l
top (x : l ) = x
empty ?([]) = true
empty ?(x : l ) = false
Die Fehlerfälle pop (empty ) und top (empty ) bleiben unbehandelt.
In einer konkreten Realisierung müssen hierfür natürlich
Vorkehrungen getroffen werden.
4.3 Keller
4-42
Anwendungen
Keller gehören zu den wichtigsten Datenstrukturen überhaupt. Sie
werden zum Beispiel
◮
zur Bearbeitung von Klammerstrukturen,
◮
zur Auswertung von Ausdrücken und
◮
zur Verwaltung von Rekursionen
benötigt.
4.3 Keller
4-43
Anwendungsbeispiel: Überprüfung von
Klammerstrukturen
4.3 Keller
◮
Wir werden jetzt an einem konkreten Beispiel erläutern, wie
Keller in der Praxis benutzt werden.
◮
Ziel ist es, einen Algorithmus zu entwickeln, der eine Datei
daraufhin überprüft, ob die in dieser Datei enthaltenen
Klammern (, ), [, ], { und } korrekt verwendet wurden.
Beispielsweise ist die Folge "( [a] b {c} e )" zulässig, nicht aber
"( ] ]".
4-44
Anwendungsbeispiel: Überprüfung von
Klammerstrukturen
◮
◮
Es wird ein anfangs leerer Keller erzeugt.
Es werden alle Symbole bis zum Ende der Eingabe gelesen:
◮
◮
Eine öffnende Klammer wird mit push auf den Keller
geschrieben.
Bei einer schließenden Klammer passiert folgendes:
◮
◮
◮
4.3 Keller
Fehler, falls der Keller leer ist.
Sonst wird die Operation pop durchgeführt. Fehler, falls das
Symbol, das vom Keller entfernt wurde, nicht mit der
schließenden Klammer übereinstimmt.
Alle anderen Symbole werden überlesen.
◮
Fehler, falls der Keller am Ende der Eingabe nicht leer ist.
◮
Die Eingabe ist zulässig.
4-45
Schlangen
◮
Ein Schlange (queue) ist eine Liste, bei der an einem Ende
Elemente hinzugefügt und am anderen entfernt werden
können.
◮
Schlangen arbeiten nach dem First-In-First-Out-Prinzip und
werden deshalb auch FIFO-Speicher genannt.
x5
x1
x5 x4 x3 x2 x1
4.4 Schlangen
4-46
Operationen für Schlangen
◮
Erzeugen einer leeren Schlange (empty)
◮
Testen, ob eine Schlange leer ist (empty?)
◮
Einfügen eines Elements am Ende einer Schlange (enter)
◮
Löschen eines Elements am Anfang einer Schlange (leave)
◮
Rückgabe des ersten Elements einer Schlange (front)
Schlangen dürfen nur mithilfe dieser Operationen bearbeitet
werden.
4.4 Schlangen
4-47
Implementierungen
◮
Realisierung durch eine Liste:
Ende
Anfang
...
xn
◮
Realisierung durch ein zyklisch verwaltetes Feld:
Ende
-
4.4 Schlangen
x1
-
xn ...
Anfang
x3 x2 x1
-
...
-
4-48
Schlangen
empty
front
empty?
Queue
T
enter
Bool
leave
Anmerkung:
leave (empty ) =⊥
front (empty ) =⊥
Diese Fälle bleiben
undefiniert.
4.4 Schlangen
type Queue (T )
import Bool
operators
empty :→ Queue
enter : Queue × T → Queue
leave : Queue → Queue
front : Queue → T
empty ? : Queue → Bool
axioms ∀q ∈ Queue , ∀x ∈ T
leave (enter (empty , x )) = empty
leave (enter (enter (q, x ), y )) =
enter (leave (enter (q, x )), y )
front (enter (empty , x )) = x
front (enter (enter (q, x ), y )) =
front (enter (q, x ))
empty ?(empty ) = true
empty ?(enter (q, x )) = false
4-49
Implementierungen
Der abstrakte Datentyp „Schlange“ wird ebenfalls durch den
Rückgriff auf den abstrakten Datentyp „Liste“ implementiert.
◮
Die Sorte Queue(T) wird implementiert durch die Menge
AList (T ) der Listen über T .
◮
Die Operatoren werden durch die folgenden Funktionen
implementiert:
empty = []
enter (l , x ) = x : l
leave (x : []) = []
leave (x : l ) = x : leave (l )
4.4 Schlangen
front (x : []) = x
front (x : l ) = front (l )
empty ?([]) = true
empty ?(x : l ) = false
4-50
Anwendungen
Eine häufige Anwendung sind Algorithmen zur Vergabe von
Ressourcen an Verbraucher.
◮
Prozessverwaltung:
◮
◮
◮
◮
Druckerverwaltung:
◮
◮
◮
4.4 Schlangen
Ressource: Rechenzeit
Elemente der Warteschlange: rechenwillige Prozesse
Grundidee: Jeder Prozess darf eine feste Zeit lang rechnen,
wird dann unterbrochen und hinten in die Warteschlange
wieder eingereiht, falls weiterer Bedarf an Rechenzeit
vorhanden ist.
Ressource: Drucker
Elemente der Warteschlange: Druckaufträge
Grundidee: Druckaufträge werden nach der Reihenfolge ihres
Eintreffens abgearbeitet.
4-51
Deques
4.4 Schlangen
◮
Eine deque (double-ended queue) ist eine Liste, bei der an
beiden Enden Elemente hinzugefügt und entfernt werden
können.
◮
Nach den vorangegangenen Beispielen sollte klar sein,
welche Operationen eine Deque besitzt und wie diese
implementiert werden können.
4-52
1. Der Algorithmenbegriff
2. Imperative Algorithmen
3. Sortieralgorithmen
4. Listen und abstrakte Datentypen
5. Objektorientierte Algorithmen
5.1 Objekte und Klassen
5.2 Vererbung
5.3 Abstrakte Klassen
5.4 Objektorientierte Softwareentwicklung
6. Bäume
7. Mengen, Verzeichnisse und Hash-Verfahren
8. Graphen
9. Entwurf von Algorithmen
10. Funktionale und deduktive Algorithmen
Einführung
Das objektorientierte Paradigma der Algorithmenentwicklung hat
verschiedene Wurzeln:
◮
Realisierung abstrakter Datentypen
◮
rechnergeeignete Modellierung der realen Welt
(objektorientierte Analyse)
◮
problemnaher Entwurf von Sofwaresystemen
(objektorientiertes Design)
◮
problemnahe Implementierung
(objektorientierte Programmierung)
5.1 Objekte und Klassen
5-1
Grundlagen der Objektorientierung
Klassen
Begriffswelt des
Modellierenden
Person
Patient
Mitarbeiter
Arzt
Chefarzt
Krankenhaus
Mitarbeiter
Patienten
Schwestern Verwaltung
¨
Arzte
Prof. Dr. Sauerbruch
Dr. Quincy
Fr. Mu
¨ller
Objekte/Instanzen
5.1 Objekte und Klassen
Realit¨
at
5-2
Objekte
Ein Objekt ist die Repräsentation eines Gegenstands oder
Sachverhalts der realen Welt oder eines rein gedanklichen
Konzepts.
Es ist gekennzeichnet durch
◮
eine eindeutige Identität, durch die es sich von anderen
Objekten unterscheidet,
◮
statische Eigenschaften zur Darstellung des Zustands des
Objekts in Form von Attributen,
◮
dynamische Eigenschaften in Form von Methoden, die das
Verhalten des Objekts beschreiben.
5.1 Objekte und Klassen
5-3
Beispiele für Objekte
◮
Eine Person mit Namen „Müller“ und Geburtsdatum
„12.12.1953“ (Attribute mit Belegungen) und Methode
„alter(): int“.
◮
Eine rationale Zahl mit Zähler und Nenner (Attribute) und
Methoden „normalisiere()“ und „addiere(r: RationaleZahl)“
Es findet in der Regel eine Abstraktion statt. Gewisse Aspekte
(zum Beispiel das „Gewicht“ einer Person) werden nicht
berücksichtigt.
Der Zustand eines Objekts zu einem Zeitpunkt entspricht der
Belegung der Attribute des Objekts zu diesem Zeitpunkt.
Der Zustand eines Objekts kann mithilfe von Methoden erfragt und
geändert werden.
5.1 Objekte und Klassen
5-4
Methoden
◮
Methoden sind in der programmiersprachlichen Umsetzung
Prozeduren oder Funktionen, denen Parameter übergeben
werden können.
◮
Der Zustand eines eine Methode ausführenden Objekts (und
nur der dieses Objekts) ist der Methode im Sinne einer Menge
globaler Variablen direkt zugänglich. Er kann daher sowohl
gelesen als auch verändert werden.
5.1 Objekte und Klassen
5-5
Objektmodelle
◮
Wertbasierte Objektmodelle: In diesem Modell besitzen
Objekte keine eigene Identität im eigentlichen Sinn. Zwei
Objekte werden schon als identisch angesehen, wenn ihr
Zustand gleich ist.
Für zwei Objekte zur Datumsangabe d1 = „6.12.1986“ und
d2 = „6.12.1986“ gilt in diesem Modell d1 = d2 .
◮
Identitätsbasierte Objektmodelle: Jedem Objekt innerhalb
des Systems wird eine vom Wert unabhängige Identität
zugeordnet.
Zwei Objekte für Personen p1 = „Müller, 12.12.1953“ und
p2 = „Müller, 12.12.1953“ sind in diesem Modell nicht
identisch: p1 , p2 .
5.1 Objekte und Klassen
5-6
Kapselung und Geheimnisprinzip
Objekte verwenden das Geheimnisprinzip und das Prinzip der
Kapselung.
Sie verbergen ihre Interna
◮
Zustand (Belegung der Attribute),
◮
Implementierung ihres Zustands,
◮
Implementierung ihres Verhaltens.
Objekte sind nur über ihre Schnittstelle, also über die Menge der
vom Objekt der Außenwelt zur Verfügung gestellten Methoden,
zugänglich. Man spricht von den Diensten des Objekts.
5.1 Objekte und Klassen
5-7
Nachrichten
Objekte interagieren über Nachrichten:
◮
Ein Objekt x sendet eine Nachricht n an Objekt y .
◮
y empfängt die Nachricht n von x .
◮
Innerhalb einer Programmiersprache wird dieser Vorgang
meist durch einen Methodenaufruf implementiert.
◮
Nachrichten (Methodenaufrufe) können den Zustand eines
Objektes verändern.
◮
Ein Objekt kann sich selbst Nachrichten schicken.
5.1 Objekte und Klassen
5-8
Beziehungen zwischen Objekten
Objekte können in Beziehungen zueinander stehen.
◮
Die Beteiligten an einer Beziehung nehmen Rollen ein.
Rolle des Arztes: „behandelnder Arzt“,
Rolle des Patienten: „Patient“.
◮
Ein Objekt kann mit mehreren Objekten in der gleichen
Beziehung stehen.
Rolle von Arzt: „behandelnder Arzt“,
Rolle von Patient 1: „Patient“, Rolle von Patient 2: „Patient“.
◮
Nachrichten können nur ausgetauscht werden, wenn eine
Beziehung besteht.
◮
Beziehungen können sich während der Lebenszeit eines
Objekts verändern.
5.1 Objekte und Klassen
5-9
Klassen
◮
Objekte besitzen eine Identität, verfügen über Attribute und
Methoden, gehen Beziehungen zu anderen Objekten ein und
interagieren über Nachrichten.
◮
Es gibt in der Regel Objekte, die sich bezüglich Attribute,
Methoden und Beziehungen ähnlich sind. Daher bietet es sich
an, diese Objekte zu einer Klasse zusammenzufassen.
5.1 Objekte und Klassen
5-10
Klassen
Patient
name: String
geburtsdatum: Date
diagnose: String
p1: Patient
name: Mu
¨ller“
”
geburtsdatum: 03.01.1987
diagnose: Grippe“
”
5.1 Objekte und Klassen
p2: Patient
name: Meier“
”
geburtsdatum: 06.12.1986
diagnose: Husten“
”
p3: Patient
name: Schulz“
”
geburtsdatum: 22.09.1993
diagnose: Kleinwuchs“
”
5-11
Klassen
Eine Klasse ist die Beschreibung von Objekten, die über eine
gleichartige Menge von Attributen und Methoden verfügen. Sie
beinhaltet auch Angaben darüber, wie Objekte dieser Klasse
verwaltet (zum Beispiel erzeugt oder gelöscht) werden können.
◮
Klassendefinitionen sind eng verwandt mit abstrakten
Datentypen. Sie legen Attribute und Methoden der
zugehörigen Objekte fest.
◮
Objekte einer Klasse nennt man auch Instanzen dieser
Klasse.
◮
Beziehungen (Assoziationen) zwischen den Objekten werden
auf Klassenebene beschrieben.
Ein Konstruktor ist eine Methode zum Erzeugen von Objekten.
5.1 Objekte und Klassen
5-12
Klassenvariable und -methoden
◮
Es gibt Attribute von Klassen, die nicht an konkrete Instanzen
gebunden sind. Diese heißen Klassenvariable oder statische
Variable. In dieser Sprechweise werden instanzgebundene
Attribute auch als Instanzvariable bezeichnet.
◮
Klassenvariable existieren für die gesamte Lebensdauer einer
Klasse genau einmal – unabhängig davon, wie viele Objekte
erzeugt wurden.
◮
Neben Klassenvariablen gibt es auch Klassenmethoden, d. h.
Methoden, deren Existenz nicht an konkrete Objekte
gebunden ist. Klassenmethoden werden auch statische
Methoden genannt.
5.1 Objekte und Klassen
5-13
Vererbung
◮
Häufig gibt es Klassen, die sich in Attributen, Methoden und
Beziehungen ähnlich sind.
Beispiel: Zahl, natürliche Zahl, ganze Zahl, rationale Zahl.
5.2 Vererbung
◮
Man versucht, zu ähnlichen Klassen eine gemeinsame
Oberklasse zu finden, die die Ähnlichkeiten subsumiert und
ergänzt die Unterklassen nur um die individuellen
Eigenschaften.
◮
Eine Unterklasse erbt die Attribute und Methoden der
Oberklasse.
5-14
Vererbung
Person
name: String
geburtsdatum: Date
Patient
name: String
geburtsdatum: Date
patientenNr: Integer
diagnose: String
5.2 Vererbung
Arzt
Gemeinsamkeiten
name: String
geburtsdatum: Date
mitarbeiterNr: Integer
fachrichtung: String
5-15
Vererbung
◮
◮
◮
5.2 Vererbung
B erbt alle Attribute und
Methoden von A und fügt in
der Regel weitere hinzu.
AO
A ist die Basisklasse
(Oberklasse) und B die
abgeleitete Klasse
(Unterklasse).
B
B ist eine Spezialisierung
von A und A eine
Generalisierung von B.
5-16
Vererbung und Polymorphismus
◮
„Jede Instanz b ∈ B ist
auch ein a ∈ A.“
◮
Einfachvererbung: Jede
abgeleitete Klasse besitzt
genau eine Vaterklasse.
◮
In Java:
class B extends A {
... }
AO
B
Eine Variable vom Typ einer Basisklasse kann während ihrer
Lebensdauer sowohl Objekte ihres eigenen Typs als auch solche
von abgeleiteten Klassen aufnehmen. Dies wird als
Polymorphismus bezeichnet.
5.2 Vererbung
5-17
Vererbung
5.2 Vererbung
◮
Eine Unterklasse erbt von ihrer Oberklasse alle Attribute und
Methoden und kann diese um weitere Attribute und Methoden
ergänzen.
◮
„Erben“ heißt: Die Attribute und Methoden der Oberklasse
können in der Unterklasse verwendet werden, als wären sie in
der Klasse selbst definiert.
5-18
Vererbung
◮
Vererbungen können
mehrstufig sein.
◮
Jede abgeleitete Klasse
erbt die Attribute und
Methoden der jeweiligen
Oberklasse.
◮
Es entstehen
Vererbungshierarchien.
AO
BO
C
5.2 Vererbung
5-19
Vererbung
◮
Vererbungshierarchien
können sehr komplex sein.
◮
Sie lassen sich durch
azyklische gerichtete
Graphen darstellen.
A
F O X22
B
22
22
22
22
22
2
C
D
F O X22
E
5.2 Vererbung
22
22
22
22
22
2
F
G
5-20
Verdecken von Variablen
5.2 Vererbung
◮
Eine Unterklasse kann eine Variable deklarieren, die
denselben Namen trägt, wie eine der Oberklasse.
◮
Hierdurch wird die weiter oben liegende Variable verdeckt.
◮
Dies wird häufig dazu benutzt, um den Typ einer Variablen der
Oberklasse zu überschreiben.
◮
In manchen Programmiersprachen gibt es Konstrukte, die den
Zugriff auf verdeckte Variable ermöglichen (in Java:
Verwendung des Präfixes „super“).
5-21
Überlagern von Methoden
5.2 Vererbung
◮
Methoden, die aus der Basisklasse geerbt werden, dürfen in
der abgeleiteten Klasse überlagert, d. h. neu definiert,
werden.
◮
Da eine Variable einer Basisklasse Werte von verschiedenen
Typen annehmen kann, entscheidet sich bei überlagerten
Methoden im Allgemeinen erst zur Laufzeit, welche Methode
zu verwenden ist: Dynamische Methodensuche.
◮
Wird eine Methode in einer abgeleiteten Klasse überlagert,
wird die ursprüngliche Methode verdeckt. Aufrufe der
Methode beziehen sich auf die überlagernde Variante.
◮
In manchen Programmiersprachen gibt es Konstrukte, die den
Zugriff auf überlagerte Methoden ermöglichen (in Java:
Verwendung des Präfixes „super“).
5-22
Modifikatoren
Mithilfe von Modifikatoren können Sichtbarkeit und Eigenschaften
von Klassen, Variablen und Methoden beeinflusst werden.
◮
Die Sichtbarkeit bestimmt, ob eine Klasse, Variable oder
Methode in anderen Klassen genutzt werden kann.
◮
Eigenschaften, die über Modifikatoren gesteuert werden
können, sind z. B. die Lebensdauer und die Veränderbarkeit.
Beispiele für Modifikatoren in Java sind:
public, protected, private, static, final, . . .
5.2 Vererbung
5-23
Mehrfachvererbung
AX11
B
11
11
11
11
11
1
F
C
5.2 Vererbung
◮
Eine Klasse kann im Allgemeinen mehrere Oberklassen
besitzen. In diesem Fall spricht man von Mehrfachvererbung
(multiple inheritance) im Gegensatz zur Einfachvererbung
(single inheritance).
◮
Problematisch ist die Behandlung von Konflikten, wenn
gleichnamige Attribute oder Methoden in verschiedenen
Oberklassen definiert werden.
◮
Mehrfachvererbung ist in Java nur für Schnittstellen erlaubt.
5-24
Abstrakte Methoden
◮
Eine Methode heißt abstrakt, wenn ihre Deklaration nur die
Schnittstelle, nicht aber die Implementierung enthält. Im
Gegensatz dazu stehen konkrete Methoden, deren
Deklarationen auch Implementierungen besitzen.
◮
Java: Die Deklaration einer abstrakten Methode erfolgt durch
den Modifikator abstract. Anstelle des Rumpfes steht
lediglich ein Semikolon.
◮
Abstrakte Methoden können nicht aufgerufen werden, sie
definieren nur eine Schnittstelle. Erst durch Überlagerung in
einer abgeleiteten Klasse und durch Angabe der fehlenden
Implementierung wird eine abstrakte Methode konkret.
5.3 Abstrakte Klassen
5-25
Abstrakte Klassen
◮
Eine Klasse, die nicht instanziiert werden kann, heißt
abstrakte Klasse. Klassen, von denen Objekte erzeugt
werden können, sind konkrete Klassen.
◮
Jede Klasse, die mindestens eine abstrakte Methode besitzt,
ist abstrakt.
◮
Java: Eine Klasse ist abstrakt, wenn sie mindestens eine
abstrakte Methode enthält. Die Deklaration einer abstrakten
Klasse erfolgt ebenfalls durch den Modifikator abstract.
◮
Java: Es ist erforderlich, abstrakte Klassen abzuleiten und in
der abgeleiteten Klasse eine oder mehrere abstrakte
Methoden zu implementieren. Die Konkretisierung kann über
mehrere Stufen erfolgen.
5.3 Abstrakte Klassen
5-26
Schnittstellen
◮
Java: Eine Schnittstelle (Interface) ist eine Klasse, die
ausschließlich Konstanten und abstrakte Methoden enthält.
◮
Java: Zur Definition einer Schnittstelle wird das Schlüsselwort
class durch das Schlüsselwort interface ersetzt.
◮
Java: Alle Methoden einer Schnittstelle sind implizit abstrakt
und öffentlich, alle Konstanten final, statisch und öffentlich.
Redundante Modifikatoren dürfen angegeben werden.
◮
Java: Ein Interface darf keine Konstruktoren enthalten.
5.3 Abstrakte Klassen
5-27
Generizität
◮
Unter Generizität versteht man die Parametrisierung von
Klassen, Datentypen, Modulen, Prozeduren, Funktionen, . . .
◮
Als Parameter werden in der Regel Datentypen (manchmal
auch Algorithmen in Form von Prozeduren) verwendet.
◮
Deklariert man beispielsweise eine Liste, so sollte die Liste
generisch angelegt werden. Dann kann man später Listen von
Zahlen, Zeichen, o. ä. erzeugen.
◮
Auch ein Sortieralgorithmus kann generisch definiert werden.
Man setzt nur voraus, dass der Datentyp eine
Ordnungsrelation ≤ zur Verfügung stellt.
5.3 Abstrakte Klassen
5-28
Realisierung von abstrakten Datentypen
Die folgende Elemente müssen im Programm abgebildet werden:
◮
Name des ADT: wird üblicherweise der Klassenname
◮
Importierte ADTen: werden sowohl zu Definitionen mit dem
entsprechenden importierten Typ, als auch zu
Import-Anweisungen innerhalb des Programms
◮
Objekterzeugende Operatoren: sogenannte Konstruktoren
werden in (meist spezielle) Klassenmethoden abgebildet, die
ein neues Objekt des gewünschten Typs zurückliefern
◮
Lesende Operatoren: sogenannte Selektoren werden zu
Methoden, die auf die Attribute nur lesend zugreifen
◮
Schreibende Operatoren: sogenannte Manipulatoren
werden zu Methoden, die den Zustand des Objekts verändern
◮
Axiome: müssen sichergestellt werden
5.4 Objektorientierte Softwareentwicklung
5-29
Realisierung eines Kellers in Java (Idee)
type Stack (T )
import Bool
operators
empty :→ Stack
push : Stack × T → Stack
pop : Stack → Stack
top : Stack → T
empty ? : Stack → Bool
axioms ∀s ∈ Stack , ∀x ∈ T
pop (push (s , x )) = s
top (push (s , x )) = x
empty ?(empty ) = true
empty ?(push (s , x )) = false
5.4 Objektorientierte Softwareentwicklung
Die Implementation kann
beispielsweise durch Rückgriff
auf die in Java existierenden
Listen geschehen.
5-30
Realisierung eines Kellers in Java (Idee)
import java.util.*;
class Stack<T> {
protected List<T> data;
public Stack() { ... }
public void push(T elem) { ... }
public void pop() throws EmptyStackException
{ ... }
public T top() throws EmptyStackException
{ ... }
public boolean isEmpty() { ... }
}
5.4 Objektorientierte Softwareentwicklung
5-31
Problembereich
Probleml¨osung
Analyse
Softwareentwicklung
Analysemodelle
Design
Programmsystem
Entwurfsmodelle
5.4 Objektorientierte Softwareentwicklung
Implementierung
5-32
Was ist Software-Technik?
W. Hesse, H. Keutgen, A. L. Luft, H. D. Rombach: Ein
Begriffsystem für die Software-Technik, Informatik-Spektrum, 7,
1984, S. 200–213:
Software-Technik (Software-Engineering) ist das
Teilgebiet der Informatik, das sich mit der Bereitstellung
und systematischen Verwendung von Methoden und
Werkzeugen für die Herstellung und Anwendung von
Software beschäftigt.
5.4 Objektorientierte Softwareentwicklung
5-33
Software-Technik
◮
setzt solide Kenntnisse in (mindestens) einer
Programmiersprache voraus,
◮
ist nicht auf eine spezielle Programmiersprache (sondern evtl.
eher auf ein Paradigma) zugeschnitten,
◮
ist ein weites und wichtiges Gebiet der Informatik.
5.4 Objektorientierte Softwareentwicklung
5-34
Teilgebiete der Software-Technik
◮
Software-Entwicklung
◮
Software-Management
◮
Software-Qualitätssicherung
5.4 Objektorientierte Softwareentwicklung
5-35
Software-Entwicklung
◮
Planungsphase
◮
Definitionsphase
◮
Entwurfsphase
◮
Implementierungsphase
◮
Abnahme- und Einführungsphase
◮
Wartungs- und Pflegephase
5.4 Objektorientierte Softwareentwicklung
5-36
Software-Management
◮
Planung
◮
Organisation
◮
Personaleinsatz
◮
Leitung
◮
Kontrolle
5.4 Objektorientierte Softwareentwicklung
5-37
Software-Qualitätssicherung
◮
Qualitätssicherung
◮
Prüfmethoden
◮
Prozessqualität
◮
Produktqualität
5.4 Objektorientierte Softwareentwicklung
5-38
Weitere Aspekte
◮
Werkzeuge, Computer Aided Software Engineering (CASE)
◮
Wiederverwendbarkeit von Software
◮
Modellierung, z. B. Unternehmensmodellierung
◮
Modellierungssprachen, z. B. UML
5.4 Objektorientierte Softwareentwicklung
5-39
Unified Modelling Language
◮
UML (Unified Modelling Language) wurde in den 1990er
Jahren mit dem Ziel, eine einheitliche – auch grafische –
Notation für die objektorientierte Software-Entwicklung zur
Verfügung zustellen, definiert.
◮
Sie enthält ca. ein Dutzend verschiedener Modelltypen zur
Beschreibung der verschiedenen Systemaspekte.
◮
Entwickler der UML waren (und sind) Grady Booch, Ivar
Jacobson und James Rumbaugh.
◮
UML ist der De-facto-Standard für objektorientierte Analyse
und Design.
◮
Object Constraint Language (OCL) zur Formulierung von
Bedingungen.
◮
Für UML ist eine Vielzahl an Werkzeugen verfügbar.
◮
UML werden Sie in der Vorlesung „Software Engineering“ und
in Praktika gründlich kennen lernen.
5.4 Objektorientierte Softwareentwicklung
5-40
Unified Modelling Language
◮
◮
Anwendungsfalldiagramme (Benutzersicht)
Implementierungsdiagramme (statische Systemstruktur)
◮
◮
◮
◮
Objekt- und Klassendiagramme, Paketdiagramme
Komponentendiagramme
Verteilungsdiagramme
Verhaltensdiagramme (dynamisches Systemverhalten)
◮
◮
◮
◮
Aktivitätsdiagramme
Kollaborationsdiagramme
Sequenzdiagramme
Zustandsdiagramme
5.4 Objektorientierte Softwareentwicklung
5-41
Anwendungsfalldiagramme
◮
modellieren die Einbettung eines Systems in seine
Umgebung,
◮
beschreiben die Sicht auf Systemfunktionalität von außen und
◮
werden zur Spezifikation der globalen Systemanforderungen
eingesetzt.
Kreditkarten-Validerungsytem
Fuehre
Kartentransaktion
durch
Kunde
Haendler
Bearbeite
Rechnung
Einzelkunde
Firmenkunde
eV rwalte
Kundenkoto
Finazstitut
5.4 Objektorientierte Softwareentwicklung
5-42
Klassendiagramme
◮
stellen die statische Systemstruktur dar und
◮
beschreiben die Systemelemente und ihre Beziehungen
zueinander.
Bitmap
Bildschirm
zeichnen()
+Elemente
Pixel
x:Integer
y:Integer
Farbe:Colour
5.4 Objektorientierte Softwareentwicklung
Quadrat
Dreieck
Icon
5-43
Sequenzdiagramme
◮
stellen die Abfolge der Nachrichten dar.
◮
Sie basieren auf „Message Sequence Charts“.
Reader
Librarian
System
hand_bok
enter_bok_data
update
[reserved]notify
acknowledge_librarian
acknowledge_reader
5.4 Objektorientierte Softwareentwicklung
5-44
Object Constraint Language
◮
Object Constraint Language (OCL) ist eine Sprache, in der
z. B. Vor- und Nachbedingungen sowie Schleifeninvarianten
ausgedrückt werden können.
◮
OCL ist kein Bestandteil von UML, sondern ein Vorschlag für
eine Sprache zur Formulierung von Erläuterungen
(annotations). Prinzipiell kann hierfür jede Sprache, auch
Deutsch oder Englisch, verwendet werden.
◮
OCL basiert auf der Prädikatenlogik.
◮
Einige Typen von OCL: Void, Boolean, Integer, Real, String,
Tupel, Set, Ordered Set, Bag, Sequence.
5.4 Objektorientierte Softwareentwicklung
5-45
Objektorientierte Programmiersprachen
Algol 60
andere PLs
Ada 83
Modula-2
Simula
Einfache Vererbung
Koroutinen
Abstrakte Datentypen
Keine Vererbung
Eiffel
OOPLs
Klassenkonzept
Vererbung
Smalltalk
C++
Java
sowie verschiedene OO-Derivate anderer
Programmiersprachen (Visual-Basic, Delphi)
5.4 Objektorientierte Softwareentwicklung
5-46
Objektorientiertes Programmieren
◮
Identifizieren Sie die Klassen und die Beziehungen der
Klassen untereinander. Achten Sie auf Datenkapselung und
Wiederverwendbarkeit.
◮
Definieren Sie die Methoden. Denken Sie an die Möglichkeit
von abstrakten Methoden und Klassen.
◮
Attribute und Methoden, die unabhängig von Instanzen
existieren, sind als Klassenvariable bzw. -methoden zu
vereinbaren. In der Regel sollten Sie sich auf wenige
Klassenvariable und -methoden beschränken.
◮
Achten Sie auf Programmiersicherheit. Vergeben Sie nicht
mehr Zugriffsrechte als erforderlich (Geheimnisprinzip).
◮
Variable, die nicht verändert werden, sollten auch als
Konstante im Programm deklariert werden.
5.4 Objektorientierte Softwareentwicklung
5-47
1. Der Algorithmenbegriff
2. Imperative Algorithmen
3. Sortieralgorithmen
4. Listen und abstrakte Datentypen
5. Objektorientierte Algorithmen
6. Bäume
6.1 Bäume
6.2 Binäre Suchbäume
6.3 Ausgeglichene Bäume
6.4 Heapsort
7. Mengen, Verzeichnisse und Hash-Verfahren
8. Graphen
9. Entwurf von Algorithmen
10. Funktionale und deduktive Algorithmen
Listen und Bäume
Listen und Bäume:
◮
Listen: Jedes Listenelement besitzt genau einen Vorgänger
und einen Nachfolger. Ausnahmen: Das erste Element besitzt
keinen Vorgänger und das letzte keinen Nachfolger.
◮
Bäume (engl.: trees): Jedes Element besitzt genau einen
Vorgänger und mehrere Nachfolger. Ausnahmen: Das erste
Element besitzt keinen Vorgänger und die letzten Elemente
keine Nachfolger.
Anwendungen von Bäumen:
6.1 Bäume
◮
Ausdrücke,
◮
Speicherung von Mengen,
◮
hierarchisch strukturierte Daten, z. B. Dateibäume,
◮
Sortieralgorithmen.
6-1
Grundbegriffe
◮
Die Elemente eines Baums
heißen Knoten.
◮
Das erste Element eines Baums
ist die Wurzel.
◮
Die letzten Elemente werden
Blätter genannt.
◮
Innere Knoten sind Elemente,
die keine Blätter sind.
11
5
17
2
7
6
◮
6.1 Bäume
13
22
16
Jeder Knoten kann einen
Schlüssel und weitere
Informationen speichern.
6-2
Grundbegriffe
◮
◮
◮
6.1 Bäume
Nachfolgerknoten werden auch
Kindknoten und
Vorgängerknoten Vaterknoten
genannt.
Bäume sind wie Listen
dynamische Datenstrukturen.
Knoten können eingefügt und
gelöscht werden.
Bäume sind wie Listen rekursive
Datenstrukturen. Jeder Knoten
kann als die Wurzel eines
(Teil-)Baums angesehen
werden.
11
5
17
2
7
6
13
22
16
6-3
Pfade
11
◮
◮
In jedem Baum gibt es von der
Wurzel zu einem beliebigen
Knoten genau einen Pfad.
◮
Jeder Baum ist
zusammenhängend.
◮
6.1 Bäume
Ein Pfad in einem Baum ist eine
Folge aufeinanderfolgender
Knoten. Die Anzahl der Knoten
eines Pfades minus 1 heißt
dessen Länge.
Jeder Baum ist zyklenlos.
5
17
2
7
6
13
22
16
[17, 13, 16] ist der Pfad
vom Knoten mit
Schlüssel 17 zum
Knoten mit Schlüssel
16. Die Länge dieses
Pfades beträgt 2.
6-4
Niveau und Höhe
◮
◮
◮
◮
6.1 Bäume
Die Länge des Pfades von
der Wurzel zu einem Knoten
heißt Niveau des Knotens.
Knoten, die auf dem gleichen
Niveau liegen, heißen
Nachbarknoten.
Die Höhe eines Baums ist
das Maximum der Längen
aller Pfade im Baum.
Die Höhe eines Knotens ist
definiert als das Maximum
der Längen aller Pfade von
diesem Knoten zu einem
Blatt.
11
Niveau 0
5
17
2
7
6
13
Niveau 1
22
16
Niveau 2
Niveau 3
[11, 17, 13, 16] und
[11, 5, 7, 6] sind die
längsten Pfade. Da ihre
Länge 3 beträgt, ist die
Höhe des Baumes 3. Die
Höhe von Knoten „5“ ist 2.
6-5
Spezielle Bäume
Ist k ∈ N die Maximalzahl der Kinder eines Knotens, so spricht
man von einem k -nären Baum.
Beispiele:
◮
Binärbäume (binäre Bäume) sind 2-näre Bäume.
Ternärbäume (ternäre Bäume) sind 3-näre Bäume.
◮
B-Bäume werden später behandelt.
Sind die Kinder eines Knotens in einer definierten Weise geordnet,
so spricht man von einem geordneten Baum.
Beispiele:
6.1 Bäume
◮
Binäre Suchbäume sind geordnete Binärbäume.
◮
Heaps werden später behandelt.
6-6
Binärbäume
Binärbäume sind 2-näre Bäume. Die
maximale Anzahl von Kindern eines
Knotens ist 2. Die Kinder eines
Knotens bilden die Wurzeln des
linken und des rechten Teilbaums
des Knotens.
11
5
17
2
7
6
13
22
16
Höhe des Baumes: 3
Max. Anzahl Knoten: 15
Max. Anzahl Blätter: 8
Max. Anzahl innerer
Knoten: 7
6.1 Bäume
6-7
Binärbäume als abstrakter Datentyp
type BinTree (T )
import Bool
operators
empty :→ BinTree
bin : BinTree × T × BinTree → BinTree
left : BinTree → BinTree
right : BinTree → BinTree
value : BinTree → T
empty ? : BinTree → Bool
axioms ∀t ∈ T , ∀x , y ∈ BinTree
left (bin(x , t , y )) = x
right (bin(x , t , y )) = y
value (bin(x , t , y )) = t
empty ?(empty ) = true
empty ?(bin(x , t , y )) = false
6.1 Bäume
6-8
Eigenschaften binärer Bäume
Bezeichnungen:
◮
Höhe des Baums: h
◮
Anzahl der Knoten: n
In einem nichtleeren binären Baum gilt:
◮
◮
Maximalzahl der inneren Knoten: 2h − 1
Maximalzahl der Blätter: 2h
◮
h + 1 ≤ n ≤ 2h +1 − 1
◮
Folgerung:
log2 (n + 1) − 1 ≤ h ≤ n − 1
Das heißt: h liegt zwischen Θ(log(n)) und Θ(n).
6.1 Bäume
6-9
Traversierung eines Binärbaums
Unter der Traversierung eines Binärbaums versteht man ein
Verfahren, bei dem jeder Knoten eines Baums genau einmal
besucht wird.
◮
Beim Tiefendurchlauf (engl.: depth-first-search, DFS) wird
zuerst in die „Tiefe“ und erst dann in die „Breite“ gegangen.
Man besucht von einem Knoten erst die Kindknoten und setzt
dort das Verfahren rekursiv fort. Die Verfahren unterscheiden
sich darin, wann der Knoten selbst bearbeitet wird.
◮
◮
◮
◮
6.1 Bäume
Inorder-Durchlauf
Preorder-Durchlauf
Postorder-Durchlauf
Bei der Breitensuche (engl.: breadth-first-search, BFS) geht
man von einem Knoten zuerst zu allen Nachbarknoten, bevor
die Kindknoten besucht werden.
6-10
Tiefendurchlauf
◮
◮
◮
6.1 Bäume
Inorder-Durchlauf:
linker Teilbaum, Knoten, rechter
Teilbaum
2, 5, 6, 7, 11, 13, 16, 17, 22
Preorder-Durchlauf:
Knoten, linker Teilbaum, rechter
Teilbaum
11, 5, 2, 7, 6, 17, 13, 16, 22
Postorder-Durchlauf:
linker Teilbaum, rechter
Teilbaum, Knoten
2, 6, 7, 5, 16, 13, 22, 17, 11
11
1.
2.
5
17
2
7
6
13
22
16
6-11
Inorder-Durchlauf
proc inorder(x) begin
if x , nil then
inorder(left(x));
bearbeite(x);
inorder(right(x));
fi;
end
6.1 Bäume
6-12
Preorder- und Postorder-Durchlauf
proc preorder(x) begin
if x , nil then
bearbeite(x);
preorder(left(x));
preorder(right(x));
fi;
end
proc postorder(x) begin
if x , nil then
postorder(left(x));
postorder(right(x));
bearbeite(x);
fi;
end
6.1 Bäume
6-13
Breitendurchlauf
11
1.
◮
Levelorder-Durchlauf:
11, 5, 17, 2, 7, 13, 22, 6, 16
2.
3.
6.1 Bäume
5
17
2
7
6
13
22
16
6-14
Levelorder-Durchlauf
proc levelorder(x) begin
var q: queue;
enter(q,x);
while not isEmpty(q) do
y ← front(q);
leave(q);
bearbeite(y);
if left(y) , nil then enter(q,left(y));
if right(y) , nil then enter(q,right(y));
od;
end
6.1 Bäume
6-15
Binäre Suchbäume
◮
Der Wert eines Knotens ist ein
eindeutiger Schlüssel aus einer
Grundmenge.
◮
Die Grundmenge ist durch ≤
bzw. < total geordnet.
◮
Die Knoten enthalten
zusätzliche Nutzdaten.
◮
Die Anordnung der Knoten im
Baum basiert auf der
Ordnungsrelation.
6.2 Binäre Suchbäume
Person 5
root
11
5
Person 11
2
7
17
13
22
Person 2
Person 6
Person 17
Person 22
6
Person 7
16
Person 16
Person 13
6-16
Binäre Suchbäume
Ein binärer Suchbaum ist ein Binärbaum, bei dem für jeden Knoten
v , seinen linken Teilbaum b1 und seinen rechten Teilbaum b2 gilt:
Für jeden Knoten v1 aus b1 ist v1 < v und für jeden Knoten v2 aus
b2 ist v2 > v .
11
< 11
> 11
5
17
2
7
6
13
22
16
Die Nutzdaten werden im Folgenden nicht betrachtet.
6.2 Binäre Suchbäume
6-17
Basisalgorithmen für binäre Suchbäume
◮
Suchen eines Knotens,
◮
Bestimmen des Minimums oder Maximums,
◮
Bestimmen des Nachfolgers oder Vorgängers eines Knotens,
◮
Einfügen eines Knotens,
◮
Löschen eines Knotens.
Die Operationen müssen die Suchbaumeigenschaft
aufrechterhalten.
6.2 Binäre Suchbäume
6-18
Suchen eines Knotens (rekursiv)
11
7 < 11
5
17
7>5
2
7
6
13
22
16
func search(x,k) begin
if x = nil oder k = schlüssel(x)
then return x; fi;
if k < schlüssel(x)
then return search(links(x),k);
else return search(rechts(x),k); fi;
end
6.2 Binäre Suchbäume
6-19
Suchen eines Knotens (iterativ)
11
7 < 11
5
17
7>5
2
7
6
13
22
16
func search(x,k) begin
while x , nil und k , schlüssel(x)
do if k < schlüssel(x)
then x ← links(x);
else x ← rechts(x); fi;
od;
return x;
end
6.2 Binäre Suchbäume
6-20
Minimum und Maximum
11
5
17
2
7
6
13
22
16
func minimum(x) begin
while links(x) , nil do x ← links(x); od;
return x;
end
func maximum(x) begin
while rechts(x) , nil do x ← rechts(x); od;
return x;
end
6.2 Binäre Suchbäume
6-21
Nachfolger und Vorgänger
Bestimmung des Nachfolgers
(Knoten mit dem nächsthöheren
Schlüssel) eines Knotens k :
◮
◮
Falls k kein rechtes Kind hat, ist
der Nachfolger der nächste
Vorgänger von k , dessen linkes
Kind k oder ein Vorgänger von
k ist. Falls k das Maximum im
Baum ist, existiert kein
derartiger Vorgänger.
Falls k ein rechtes Kind hat, ist
der Nachfolger das Minimum im
vom rechten Kind
aufgespannten Teilbaum.
6.2 Binäre Suchbäume
Nachfolger von 7:
11
5
17
2
7
6
13
22
16
6-22
Nachfolger und Vorgänger
func successor(x) begin
if rechts(x) , nil
then return minimum(rechts(x));
fi;
y ← vater(x);
while y , nil und x = rechts(y)
do x ← y;
y ← vater(y);
od;
return y;
end
Die Bestimmung des Vorgängers erfolgt analog.
6.2 Binäre Suchbäume
6-23
Einfügen eines Knotens
Einfügen des Schlüssels 12:
11
12 > 11
5
17
12 < 17
2
7
13
22
12 < 13
6
6.2 Binäre Suchbäume
12
16
6-24
Einfügen eines Knotens
proc insert(T, z): begin
y ← nil;
x ← wurzel(T);
while x , nil
do y ← x;
if schlüssel(z) < schlüssel(x)
then x ← links(x);
else x ← rechts(x); fi; od;
vater(z) ← y;
if y = nil then wurzel(T) ← z;
else if schlüssel(z) < schlüssel(y)
then links(y) ← z;
else if schlüssel(z) > schlüssel(y)
then rechts(y) ← z;
else error(”Doppelter Schlüssel”);
fi; fi; fi;
end
6.2 Binäre Suchbäume
6-25
Löschen eines Knotens
Beim Löschen eines Knotens können 3 Fälle auftreten.
Der Knoten hat keine Kinder:
Er wird einfach gelöscht.
Der Knoten hat ein Kind:
Er wird ausgeschnitten.
11
11
5
17
2
7
6
6.2 Binäre Suchbäume
13
5
22
16
17
2
7
6
13
22
16
6-26
Löschen eines Knotens
Der Knoten hat zwei Kinder:
Aus dem rechten Teilbaum wird der Nachfolger bestimmt und
dieser dort gelöscht. Dieser Nachfolger hat höchstens ein Kind.
Der Nachfolger wird anstelle des zu löschenden Knotens
verwendet. Alternativ kann der Vorgänger im linken Teilbaum
genommen werden.
11
5
17
2
7
6
6.2 Binäre Suchbäume
13
22
16
6-27
Löschen eines Knotens
func delete(T, z): Knoten begin
if links(z) = nil oder rechts(z) = nil
then y ← z; else y ← successor(z); fi;
if links(y) , nil
then x ← links(y); else x ← rechts(y); fi;
if x , nil
then vater(x) ← vater(y); fi;
if vater(y) = nil
then wurzel(T) ← x;
else if y = links(vater(y))
then links(vater(y)) ← x;
else rechts(vater(y)) ← x; fi; fi;
if y , z
then schlüssel(z) ← schlüssel(y);
kopiere Nutzdaten; fi;
return y;
end
6.2 Binäre Suchbäume
6-28
Laufzeiten der Basisalgorithmen
Die Analyse der Algorithmen liefert den
Satz: Die Laufzeiten der Algorithmen
◮
Suchen eines Knotens,
◮
Minimum, Maximum,
◮
Nachfolger, Vorgänger,
◮
Einfügen eines Knotens und
◮
Löschen eines Knotens
liegen bei geeigneter Implementierung in der Zeit O (h ), wobei h
die Höhe des binären Suchbaums ist.
6.2 Binäre Suchbäume
6-29
Random-Tree-Analyse
Annahmen:
1. Die Schlüssel sind paarweise verschieden.
2. Die Bäume entstehen durch Einfüge-, aber nicht durch
Löschoperationen.
3. Jede der n! Permutationen der Eingabe ist
gleichwahrscheinlich.
Es gilt der
Satz: Für die mittlere Knotentiefe P (n) in einem zufällig erzeugten
binären Suchbaum gilt P (n) = O (log(n)).
Es gilt sogar schärfer der
Satz: Die erwartete Höhe eines zufällig erzeugten binären
Suchbaums mit n Schlüsseln ist O (log(n)).
Beweis: s. Cormen et al., S. 266–269.
6.2 Binäre Suchbäume
6-30
Gestaltsanalyse
Satz: Für die Anzahl bn der strukturell verschiedenen Binärbäume
gilt:
! 


falls n = 0,
1 2n
1,
=
bn =
Pn−1


n+1 n
falls n > 0.
k =0 bk bn−1−k ,
Annahmen:
1. Die Schlüssel sind paarweise verschieden.
2. Jeder der bn Binärbäume ist gleichwahrscheinlich.
Satz: Der mittlere Abstand eines
von der Wurzel eines
√Knotens
Binärbaums mit n Knoten ist O
n .
Beweis: s. Ottmann/Widmayer, S. 271–275.
6.2 Binäre Suchbäume
6-31
Ausgeglichene Bäume
◮
Höhe des Binärbaums: h
◮
Anzahl der Knoten: n
◮
Es gilt: log2 (n + 1) − 1 ≤ h ≤ n − 1.
1
2
Binäre Suchbäume können zu
Listen „entarten“:
3
5
8
13
21
6.3 Ausgeglichene Bäume
6-32
Ausgeglichene Bäume
Definition: M sei eine Klasse von Bäumen. Für T ∈ M sei n(T ) die
Knotenzahl und h (T ) die Höhe von T . M heißt ausgeglichen
(ausgewogen), wenn die beiden folgenden Bedingungen erfüllt
sind:
1. Ausgeglichenheitsbedingung:
∃c > 0 ∀T ∈ M : h (T ) ≤ c · log n(T ).
2. Rebalancierungsbedingung:
Falls eine Einfüge- oder Löschoperation, ausgeführt in einem
Baum T ∈ M , einen unausgeglichenen Baum T ′ < M erzeugt,
dann soll es möglich sein, T ′ mit Zeitaufwand O (log n) zu
einem Baum T ′′ ∈ M zu rebalancieren.
6.3 Ausgeglichene Bäume
6-33
AVL-Bäume
Definition: Ein binärer Suchbaum ist ein AVL-Baum, wenn für
jeden Knoten p des Baums gilt, dass sich die Höhe des linken
Teilbaums von der Höhe des rechten Teilbaums von p höchstens
um 1 unterscheidet.
G. M. Adelson-Velskii, E. M. Landis (1962)
6.3 Ausgeglichene Bäume
6-34
AVL-Bäume
Ein AVL-Baum:
Kein AVL-Baum:
11
11
5
17
2
7
6
6.3 Ausgeglichene Bäume
13
5
17
2
7
6
6-35
AVL-Bäume
Satz: Es sei ein beliebiger AVL-Baum der Höhe h mit n Knoten
gegeben. Dann gilt
h < 1.441 log2 (n + 2) − 0.328.
Die Klasse der AVL-Bäume erfüllt daher die
Ausgeglichenheitsbedingung. Wir werden gleich sehen, dass sie
auch die Rebalancierungsbedingung erfüllt. Damit gilt:
Satz: Die Klasse der AVL-Bäume ist ausgeglichen.
6.3 Ausgeglichene Bäume
6-36
Basisalgorithmen für AVL-Bäume
Da die Höhe h eines AVL-Baums durch
h < 1.441 log2 (n + 2) − 0.328
beschränkt ist, liegen die Laufzeiten der Algorithmen
◮
Suchen eines Knotens,
◮
Minimum, Maximum,
◮
Nachfolger und Vorgänger
in O (log n).
6.3 Ausgeglichene Bäume
6-37
Einfügen in AVL-Bäume
1. Wenn der einzufügende Schlüssel noch nicht im Baum
vorkommt, endet die Suche in einem Blatt. Der Schlüssel wird
dort wie bisher eingefügt.
2. Danach kann die AVL-Eigenschaft eines inneren Knotens k
verletzt sein.
3. Wir unterscheiden abhängig von der aufgetretenen Stelle die
folgenden Fälle:
1. Einfügen in linken Teilbaum des linken Kindes von k ,
2. Einfügen in rechten Teilbaum des linken Kindes von k ,
3. Einfügen in rechten Teilbaum des rechten Kindes von k ,
4. Einfügen in linken Teilbaum des rechten Kindes von k .
Die Fälle 1 und 3 sowie die Fälle 2 und 4 sind symmetrisch.
Die Rebalancierung erfolgt durch so genannte Rotationen bzw.
Doppelrotationen.
6.3 Ausgeglichene Bäume
6-38
Einfügen in AVL-Bäume
Fall 1: Einfügen in den linken Teilbaum des linken Kindes
y
x
-2
x
Rotation
-1
y
c
Einf¨
ugen
a
0
0
a
b
b
c
Rotiert wird hier das linke Kind nach rechts.
Fall 3 läuft spiegelbildlich ab.
6.3 Ausgeglichene Bäume
6-39
Einfügen in AVL-Bäume
Fall 2: Einfügen in den rechten Teilbaum des linken Kindes
z
x
-2
y
Doppelrotation
+1
x
0
0/-1
z
0/+1
y
a
d
Einf¨
ugen
b
a
b
c
d
c
Es hat eine Doppelrotation mit dem linken Kind nach links und
bzgl. des Vaters nach rechts stattgefunden.
Fall 4 läuft spiegelbildlich ab.
6.3 Ausgeglichene Bäume
6-40
Einfügen in AVL-Bäume
3
3
0
2
3
-1
1
2
3
2
-2
RR
-1
4
2
1
3
2
1
3
1
5
LR
2
1
3
2
1
1
4
5
6.3 Ausgeglichene Bäume
2
1
4
6
1
4
3
1
2
1
5
2
4
3
1
1
5
6
6-41
Einfügen in AVL-Bäume
LR
7
4
2
1
5
3
4
1
2
6
1
LR
1
5
3
4
2
6
2
1
1
6
3
5
7
7
9
4
2
1
8
1
6
3
5
4
1
2
1
7
1
6
3
5
2
7
9
9
-1
8
6.3 Ausgeglichene Bäume
6-42
Einfügen in AVL-Bäume
DR r/l
RR
LR
4
2
1
4
6
3
5
1
2
2
7
1
1
8
1
6
3
5
8
7
9
9
6.3 Ausgeglichene Bäume
6-43
Einfügen in AVL-Bäume
1. Fall Einfügen in linken Teilbaum des linken Kindes:
Rechtsrotation
2. Fall Einfügen in rechten Teilbaum des linken Kindes:
Doppelrotation, links/rechts
3. Fall Einfügen in rechten Teilbaum des rechten Kindes:
Linksrotation
4. Fall Einfügen in linken Teilbaum des rechten Kindes:
Doppelrotation, rechts/links
6.3 Ausgeglichene Bäume
6-44
Löschen in AVL-Bäumen
1. Der Schlüssel wird wie bisher gesucht und gelöscht.
2. Danach kann die AVL-Eigenschaft eines Knotens verletzt
sein.
3. Durch Rotationen kann die Ausgeglichenheit erreicht werden,
allerdings wird dadurch u. U. die AVL-Eigenschaft weiter oben
im Baum verletzt. Die Balance muss also ggf. rekursiv bis zur
Wurzel wiederhergestellt werden. Da für die Höhe h des
Baums h < 1.441 log2 (n + 2) gilt, bleibt die
Rebalancierungsbedingung erfüllt.
4
-1
4
L¨oschen von 5
2
1
6.3 Ausgeglichene Bäume
1
2
Rechtsrotation
5
3
-2
2
1
1
3
4
-1
3
6-45
Rot-Schwarz-Bäume
Ein Rot-Schwarz-Baum ist ein binärer Suchbaum, in dem jeder
Knoten über ein Zusatzbit zur Speicherung einer Farbe (rot oder
schwarz) verfügt.
Idee: Durch Einschränkungen bei der Färbung der Knoten auf den
Pfaden von der Wurzel zu einem Blatt wird sichergestellt, dass
jeder Pfad, der in der Wurzel beginnt, maximal doppelt so lang ist,
wie jeder andere solche Pfad.
Für nicht vorhandene Kind-Knoten, wird ein spezieller Null-Knoten
als Kind eingefügt.
8
4
12
2
6
1
Null
6.3 Ausgeglichene Bäume
3
Null
Null
10
5
Null
Null
7
Null
Null
14
9
Null
Null
11
Null
Null
13
Null
Null
15
Null
Null
Null
6-46
Rot-Schwarz-Bäume
Die Bedingungen an einen Rot-Schwarz-Baum lauten:
1. Jeder Knoten ist entweder rot oder schwarz.
2. Die Wurzel ist schwarz.
3. Jedes Blatt (Null-Knoten) ist schwarz.
4. Wenn ein Knoten rot ist, so sind beide Kinder schwarz.
5. Für jeden Knoten p gilt, dass alle Pfade vom Knoten zu einem
Blatt die selbe Anzahl schwarzer Knoten beinhalten.
8
4
12
2
6
1
Null
6.3 Ausgeglichene Bäume
3
Null
Null
10
5
Null
Null
7
Null
Null
14
9
Null
Null
11
Null
Null
13
Null
Null
15
Null
Null
Null
6-47
Rot-Schwarz-Bäume
Die gemäß Bedingung 5 eindeutig bestimmte Zahl wird
Schwarzhöhe bh (p ) eines Knotens p genannt. Hierbei wird p
selbst nicht mitgezählt.
bh(8) = 3
8
bh(12) = 2
4
12
2
6
10
14
bh(11) = 1
1
Null
6.3 Ausgeglichene Bäume
3
Null
Null
5
Null
Null
7
Null
Null
9
Null
Null
11
Null
Null
13
Null
Null
15
Null
Null
Null
6-48
Rot-Schwarz-Bäume
Satz: Es sei ein beliebiger Rot-Schwarz-Baum der Höhe h mit n
Knoten gegeben. Dann gilt
h < 2 log2 (n + 1).
Die Klasse der Rot-Schwarz-Bäume erfüllt daher die
Ausgeglichenheitsbedingung. Wir werden gleich sehen, dass sie
auch die Rebalancierungsbedingung erfüllt. Damit gilt:
Satz: Die Klasse der Rot-Schwarz-Bäume ist ausgeglichen.
6.3 Ausgeglichene Bäume
6-49
Basisalgorithmen für Rot-Schwarz-Bäume
Da die Höhe h eines Rot-Schwarz-Baums durch
h < 2 log2 (n + 1)
beschränkt ist, liegen die Laufzeiten der Algorithmen
◮
Suchen eines Knotens,
◮
Minimum, Maximum,
◮
Nachfolger und Vorgänger
in O (log n).
6.3 Ausgeglichene Bäume
6-50
Einfügen in Rot-Schwarz-Bäume
1. Einfügen eines Schlüssels mit bisherigem Algorithmus.
2. Der neue Knoten wird rot und erhält zwei Null-Knoten als
Kinder.
3. Danach kann die Rot-Schwarz-Eigenschaft verletzt sein:
3.1 Jeder Knoten ist entweder rot oder schwarz. Wird nicht
verletzt.
3.2 Die Wurzel ist schwarz. Wird verletzt, falls in den leeren Baum
eingefügt wird. Der neue Knoten wird dann schwarz gefärbt.
3.3 Jedes Blatt ist schwarz. Wird nicht verletzt.
3.4 Wenn ein Knoten rot ist, so sind beide Kinder schwarz. Nicht
durch k verletzt, da beide Kinder schwarze Null-Knoten sind.
Die Eigenschaft wird verletzt, falls k als Kind eines roten
Vaterknotens eingefügt wird.
3.5 Für jeden Knoten gilt, dass alle Pfade vom Knoten zu einem
Blatt die selbe Anzahl schwarzer Knoten beinhalten. Da nur
ein roter Knoten hinzukommt, wird diese Eigenschaft nicht
verletzt.
6.3 Ausgeglichene Bäume
6-51
Einfügen in Rot-Schwarz-Bäume
Maßnahmen, die die Rot-Schwarz-Eigenschaft 4 wiederherstellen:
◮
Links-, Rechts- und Doppel-Rotationen zwecks
Höhenausgleich. Dabei gibt die Einfärbung der Knoten
Aufschluss über die notwendigen Rotationen.
◮
Korrektur der Einfärbung der falsch eingefärbten Knoten.
Die Einfärbung wird in Richtung der Wurzel korrigiert, wodurch ein
Dienst zum Zugriff auf den Vaterknoten eines Knotens nötig wird.
Dieser sei im Folgenden mit parent (k ) beschrieben. Sechs Fälle
sind zu unterscheiden:
1. parent (k ) ist linkes Kind von parent (parent (k ))
1.1 Der Onkel von k ist rot.
1.2 Der Onkel von k ist schwarz und k ist rechtes Kind.
1.3 Der Onkel von k ist schwarz und k ist linkes Kind.
2. parent (k ) ist rechtes Kind von parent (parent (k )): analog.
6.3 Ausgeglichene Bäume
6-52
Einfügen in Rot-Schwarz-Bäume
Fall 1.1: Der Onkel von k ist rot:
y
y
Umf¨arben
w
z
x
k
z
x
a
d
b
w
c
e
k
a
d
b
e
c
Knoten y kann ebenfalls wieder Kind eines roten Knotens sein
(erneute Verletzung der Eigenschaft 4). In diesem Fall wird für y
rekursiv die Eigenschaft 4 wiederhergestellt. Da die Wurzel
schwarz ist, terminiert das Verfahren.
Der Fall, dass k linkes Kind von parent (k ) ist, wird analog
behandelt.
6.3 Ausgeglichene Bäume
6-53
Einfügen in Rot-Schwarz-Bäume
Fall 1.2: Der Onkel von k ist schwarz und k ist rechtes Kind:
y
y
Linksrotation
w
z
x
k
z
w
a
d
b
x
c
e
c
a
d
e
b
Durch Linksrotation entsteht Fall 1.3.
6.3 Ausgeglichene Bäume
6-54
Einfügen in Rot-Schwarz-Bäume
Fall 1.3: Der Onkel von k ist schwarz und k ist linkes Kind:
y
x
Rechsrotation
Umf¨arben
x
w
z
k
z
c
a
y
w
b
d
e
a
b
c
d
e
Es wird eine Rechtsrotation und eine Umfärbung durchgeführt.
6.3 Ausgeglichene Bäume
6-55
B-Bäume
◮
Der Zugriff auf den Primärspeicher (RAM, Hauptspeicher) ist
bezüglich der Zugriffszeit „billig“, wohingegen der Zugriff auf
den Sekundärspeicher (Festplatte) „teuer“ ist, vor allem dann,
wenn das Auslesen der Daten eine Änderung der Position des
Lesekopfes nötig macht oder der Beginn des einzulesenden
Bereiches abgewartet werden muss.
◮
Daher ist es sinnvoll, zusammenhängende Daten, auf die
komplett zugegriffen wird, möglichst beieinander liegend zu
speichern.
◮
Diese Idee kann man auf Suchbäume übertragen.
6.3 Ausgeglichene Bäume
6-56
B-Bäume
B-Bäume sind ausgeglichene geordnete k -näre Suchbäume,
deren Knoten
◮
◮
maximal k − 1 Schlüssel tragen können und
auf maximal k Kindknoten verweisen.
B-Bäume sind nicht binär, das B steht für „balanced“.
10
13
6.3 Ausgeglichene Bäume
14
17
20
30
23
24
27
6-57
B-Bäume
Ein 2t -närer Baum T heißt B-Baum der Ordnung t , t ≥ 2, wenn er
die folgenden Eigenschaften erfüllt:
1. Jeder Knoten x besitzt die folgenden Felder bzw. Funktionen:
◮ n (x ) ist die Anzahl der in Knoten x gespeicherten Schlüssel
◮ die n (x ) Schlüssel sind in aufsteigender Weise geordnet:
key1 (x ) ≤ key2 (x ) ≤ · · · ≤ keyn(x ) (x )
◮ leaf (x ) ist eine boolesche Funktion, die angibt, ob x ein Blatt
ist.
2. Jeder innere Knoten x trägt n(x ) + 1 Zeiger
c1 (x ), c2 (x ), . . . , cn(x )+1 (x ) auf seine Kindknoten.
3. Die Schlüssel keyi (x ) unterteilen die in den Teilbäumen
gespeicherten Werte. Sei treeKeys (y ) die Menge aller in
einem B-Baum mit Wurzel y gespeicherten Schlüssel, so gilt
für 1 ≤ i ≤ n(x ): ∀vi ∈ treeKeys (ci (x )),
vi +1 ∈ treeKeys (ci +1 (x )) : vi ≤ keyi (x ) ≤ vi +1 .
6.3 Ausgeglichene Bäume
6-58
B-Bäume
4. Jedes Blatt hat das gleiche Niveau. Es entspricht der Höhe h
des Baums.
5. Die Ordnung t ≥ 2 legt die obere und die untere Grenze der
Anzahl der Schlüssel und der Kindknoten eines Knotens fest:
◮ Jeder Knoten mit Ausnahme der Wurzel hat wenigstens t − 1
◮
◮
◮
Schlüssel.
Jeder innere Knoten mit Ausnahme der Wurzel hat wenigstens
t Kindknoten.
Ist der Baum nicht leer, so trägt die Wurzel mindestens einen
Schlüssel.
Jeder Knoten trägt maximal 2t − 1 Schlüssel. Damit hat jeder
innere Knoten höchstens 2t Kinder. Ein Knoten heißt voll,
wenn er genau 2t − 1 Schlüssel trägt.
Es gilt: t − 1 ≤ n(x ) ≤ 2t − 1 für alle Knoten mit Ausnahme der
Wurzel und t ≤ #Kinder von x ≤ 2t für alle inneren Knoten mit
Ausnahme der Wurzel.
6.3 Ausgeglichene Bäume
6-59
B-Bäume
Beispiel: B-Baum der Ordnung 3
Wurzel
50
10
20
30
40
k
45
55
59
70
...
3
◮
◮
◮
◮
7
8
9
Blatt
13
14
17
Blatt
75
...
65
66
Blatt
Für alle Knoten x mit Ausnahme der Wurzel gilt:
3 − 1 = 2 ≤ n(x ) ≤ 2 · 3 − 1 = 5.
Für alle inneren Knoten mit Ausnahme der Wurzel gilt:
3 ≤ # Kinder von x ≤ 2 · 3 = 6
Der Knoten k ist voll, sein rechter Bruder nicht.
Es gibt insgesamt 11 Blätter, die alle das Niveau 2 haben.
6.3 Ausgeglichene Bäume
6-60
B-Bäume
◮
Alle Pfade von der Wurzel bis zu den Blättern sind in einem
B-Baum gleich lang.
◮
Typischerweise werden B-Bäume hoher Ordnung benutzt. Die
Knoten enthalten dadurch sehr viele Werte, die Höhe des
Baumes ist aber gering.
◮
B-Bäume werden im Zusammenhang mit Datenbanken zum
Beispiel für Indizierungen verwendet.
◮
Die Anzahl der Knoten eines Niveaus nimmt bei einem
vollständigen B-Baum der Ordnung t exponentiell zur Basis 2t
zu: Jeder Knoten hat 2t Kinder, auf Niveau n befinden sich
(2t )n Knoten.
◮
Die Ordnung t eines B-Baumes wird auch als minimaler Grad
bezeichnet.
6.3 Ausgeglichene Bäume
6-61
B-Bäume
Wie für die binären Suchbäume ist die Laufzeit (und damit die
Anzahl der Festplattenzugriffe) für die meisten
B-Baum-Operationen abhängig von der Höhe des Baums.
Satz: Ist n ≥ 1 die Anzahl der Schlüssel eines B-Baumes der Höhe
h der Ordnung t , so gilt
h ≤ logt
6.3 Ausgeglichene Bäume
!
n+1
.
2
6-62
Suchen in B-Bäumen
Suche in B-Bäumen kombiniert die Suche in binären Suchbäumen
mit der Suche in Listen. Jeder Knoten muss dazu durch einen
Festplattenzugriff erst in den Speicher geladen werden, bevor er
bearbeitet werden kann.
Um einen Schlüssel k zu suchen:
1. Lies den Wurzelknoten x ein.
2. Vergleiche in x beginnend mit i = 1 jeden Schlüssel keyi (k )
mit k bis ein Wert keyi (x ) ≥ k oder i = n(x ) ist.
2.1 Ist k = keyi (x ), dann liefere Knoten x und Index i zurück und
beende die Suche.
2.2 Ist k , keyi (x ) und x ein Blatt, so ist der Schlüssel nicht
enthalten, und die Suche ist fehlgeschlagen.
2.3 Ist keyi (x ) > k bzw. keyi (x ) < k und i = n(x ), so lies Knoten
ci (x ) bzw. ci +1 (x ) ein und fahre mit Schritt 2 fort.
6.3 Ausgeglichene Bäume
6-63
Suchen in B-Bäumen
Suchen des Buchstabens Q in B-Bäumen der Ordnung 3:
Q>N
N
C
A
B
D
E
F
K
H
S
Q<S
L
M
O
Q
R
W
T
V
X
Y
Z
X
Y
Z
Q=Q
Q>O
Q>N
N
C
A
B
D
E
F
K
H
Suche erfolgreich
S
Q<S
L
M
O
P
R
W
T
V
Q<R
Q>O
Q>P
6.3 Ausgeglichene Bäume
Suche fehlgeschlagen
6-64
Suchen in B-Bäumen
1. Die Suche innerhalb eines Knotens erfolgt linear und ist
beendet, wenn ein Schlüssel größer oder gleich dem
gesuchten Schlüssel ist oder alle n(x ) Werte des Knotens
betrachtet worden sind. In einem B-Baum der Ordnung t ist
n(x ) ≤ 2t . Daher liegt die Laufzeit dieser lokalen Suche in
O (t ).
2. Wird der Schlüssel in einem inneren Knoten nicht gefunden,
so wird der nächste Knoten in Richtung der Blätter bearbeitet.
Die Anzahl der besuchten Knoten (die Anzahl der
Festplattenzugriffe) ist damit abhängig von der Höhe h des
Baumes. Sie liegt nach obigem Satz in Θ(h ) = Θ(logt n).
3. Die Laufzeit des gesamten Algorithmus liegt also in
O (t · h ) = O (t logt n).
6.3 Ausgeglichene Bäume
6-65
Einfügen in B-Bäume
Das Einfügen eines Wertes in einen B-Baum der Ordnung t ist
komplizierter als bei binären Suchbäumen:
◮
Suche analog zum binären Suchbaum zuerst ein Blatt, in dem
der Wert gespeichert werden kann. Sollte das Blatt vor dem
Einfügen bereits voll gewesen sein, so verstößt der Baum
danach gegen die B-Baum-Definition.
◮
Der übervolle Knoten wird in zwei Knoten am Median des
ursprünglichen Knotens aufgeteilt.
◮
Beispiel: B-Baum der Ordnung 3
S
Einf¨
ugen von W
C
6.3 Ausgeglichene Bäume
K
S
X
Y
C
K
W
X
Y
6-66
Einfügen in B-Bäume
◮
Der neue Vaterknoten muss in den ursprünglichen
Vaterknoten integriert werden, wodurch wiederum die
B-Baum-Eigenschaft verletzt sein kann.
◮
In Richtung Wurzel ist demnach solange jeder entstehende
übervolle Knoten aufzuteilen, bis spätestens ein neuer Vater
die neue Wurzel des Baumes bildet.
◮
Das beschriebene Verfahren durchläuft den Baum ggf.
zweimal: Erst wird der Baum in Richtung eines Blattes
durchsucht, der Knoten eingefügt und dann in Richtung der
Wurzel korrigiert.
◮
Ein effizienteres Verfahren, dass den Baum nur einmal
durchläuft, teilt auf dem Suchpfad in Richtung des Zielblattes
vorsorglich jeden vollen Knoten auf und fügt zum Schluss den
Wert in einen Knoten ein, der noch nicht voll ist
(One-Pass-Verfahren).
6.3 Ausgeglichene Bäume
6-67
Einfügen in B-Bäume
Beispiel: B-Baum der Ordnung 3
L
C
F
K
Q
H
S
F
K
L
W
C
F
Q
M
H
S
L
Q
V
K
C
K
T
S
R
C
F
H
V
W
Q
S
T
V
N
K
T
L
C
F
H
L
S
M
N
Q
T
V
W
R
T
V
W
P
K
C
6.3 Ausgeglichene Bäume
F
H
L
M
N
S
P
Q
R
6-68
Einfügen in B-Bäume
A
B
X
Y
K
A
B
C
F
H
L
M
N
S
Q
P
R
T
V
W
X
Y
D
C
A
B
D
F
H
L
K
N
M
S
P
Q
S
W
P
Q
R
T
V
W
X
Y
Z
C
A
B
6.3 Ausgeglichene Bäume
D
F
H
L
K
M
N
R
T
V
X
Y
Z
6-69
Einfügen in B-Bäume
E
C
A
B
D
E
F
H
L
K
N
M
S
W
P
Q
ohne One-Pass-Verfahren
R
T
V
X
Y
Z
E
N
A
B
D
6.3 Ausgeglichene Bäume
C
K
E
F
mit One-Pass-Verfahren
S
H
L
M
P
Q
R
W
T
V
X
Y
Z
6-70
Löschen in B-Bäumen
Um einen Schlüssel in einem B-Baum der Ordnung t zu löschen:
Suche den Knoten x , in dem der Schlüssel k = keyi (x ) gelöscht
werden soll. Wird der Schlüssel aus dem betreffenden Knoten x
entfernt, so können mehrere Fälle auftreten, von denen wir uns nur
drei beispielhaft ansehen:
1. x ist ein Blatt und trägt mehr als t − 1 Schlüssel. Dann kann
der Schlüssel einfach gelöscht werden.
6.3 Ausgeglichene Bäume
6-71
Löschen in B-Bäumen
2. x ist ein Blatt, trägt die minimale Anzahl von t − 1 Schlüsseln
und ein Bruder b von x trägt mindestens t Schlüssel. Dann
findet vor dem Löschen eine Rotation des kleinsten bzw.
größten Schlüssels von b mit dem Schlüssel des
Vaterknotens statt, dessen Teilbäume b und x sind.
K
N
S
K
P
S
L¨oschen von M
F
H
6.3 Ausgeglichene Bäume
L
M
P
Q
R
T
V
F
H
L
N
Q
R
T
V
6-72
Löschen in B-Bäumen
3. x ist ein Blatt und trägt die minimale Anzahl von t − 1
Schlüsseln. Gleiches gilt für beide Brüder. Dann findet eine
Verschmelzung von x und einem Bruder b statt, wobei der
Schlüssel des Vaterknotens, dessen Teilbäume b und x sind,
in der Mitte des neuen Knoten zwischen den Werten von x
und b gespeichert wird. Dabei kann der Vaterknoten die
kritische Größe t − 1 Schlüssel unterschreiten. Ggf. muss also
rekursiv nach oben verschmolzen werden
K
N
S
N
S
L¨oschen von H
F
H
6.3 Ausgeglichene Bäume
L
M
P
Q
R
T
V
F
K
L
M
P
Q
R
T
V
6-73
Löschen in B-Bäumen
◮
Die anderen Fälle werden ähnlich behandelt. Es erfolgt vor
dem Löschen evtl. ein Zusammenfügen oder ein Aufspalten
einzelner Knoten. Dabei muss ggf. rekursiv vorgegangen
werden.
◮
Man kann zeigen, dass die Laufzeit der beiden Algorithmen
zum Einfügen und Löschen ebenfalls in O (t logt n) liegt.
◮
Einzelheiten kann und sollte man in der Literatur (zum
Beispiel Cormen et al., S. 439–457) nachlesen.
6.3 Ausgeglichene Bäume
6-74
Überblick
6.4 Heapsort
◮
Heapsort ist ein Sortieralgorithmus, der ein gegebenes Feld
in-place sortiert.
◮
Heapsort verwendet eine auf Binärbäumen basierende
Datenstruktur, den binären Heap.
◮
Das zu sortierende Feld wird dabei als ausgeglichener binärer
Baum aufgefasst, der bis auf die Ebene der Blätter vollständig
gefüllt ist.
◮
Die Ebene der Blätter ist von links bis zu einem Endpunkt
gefüllt.
◮
In dem Binärbaum gilt nicht die Suchbaumeigenschaft,
sondern die Heap-Eigenschaft.
6-75
Heaps
Ein binärer Max-Heap ist ein Binärbaum, bei dem der Schlüssel
eines Vaterknotens größer oder gleich den Schlüsseln seiner
Kindknoten ist. Die Schlüssel der Kinder stehen untereinander in
keiner Beziehung.
Beim Heapsort wird das zu sortierende Feld a mit der Indexmenge
1 . . . n im Bereich 1 . . . h , h ≤ n, (Heap-Size) als binärer Heap
interpretiert:
◮
a [1] ist die Wurzel des Baumes.
◮
left (k ) = 2k ist der Index des linken Kindes des Knotens k .
◮
right (k ) = 2k + 1 ist der Index des rechten Kindes des
Knotens k .
◮
parent (k ) = ⌊ k2 ⌋ ist der Index des Vaters des Knotens k .
◮
◮
6.4 Heapsort
a .size = n ist die Gesamtlänge des Felds.
a .heapSize = h ist die Länge des Heaps.
6-76
Heaps
In einem Max-Heap gilt die Max-Heap-Eigenschaft:
∀k ∈ {2, . . . , h }.a [parent (k )] ≥ a [k ]
D. h., die Schlüssel der Kinder eines Knotens sind kleiner oder
gleich dem Schlüssel des Vaterknotens.
n=9
9
h=6
9 6 8 1 5 7 2 0 3
6.4 Heapsort
6
1
4
1
2
5
8
5
7
3
6
6-77
Heapsort
Der Algorithmus benötigt 3 Methoden:
6.4 Heapsort
◮
Max-Heapify: stellt die Max-Heap-Eigenschaft für einen
Teilbaum sicher.
◮
Build-Max-Heap: konstruiert ausgehend von einem
unsortierten Feld einen Max-Heap.
◮
Heapsort: sortiert ein ungeordnetes Feld in-place.
6-78
Max-Heapify
6.4 Heapsort
◮
Max-Heapify bekommt eine Referenz auf das Feld a
übergeben, in dem ggf. die Heap-Eigenschaft an nur einer
Stelle verletzt ist.
◮
Außerdem einen Index k , der denjenigen Knoten in a angibt,
der die Heap-Eigenschaft verletzt. Die Teilbäume unterhalb
von k verletzen die Heap-Eigenschaft nach Voraussetzung
nicht.
◮
Max-Heapify stellt die Heap-Eigenschaft her.
6-79
Max-Heapify
17 1
6
11 4
17 1
10 3
2
4 5
1 6
11 2
7 7
6
5 8 8 9 2 10 3 11
10 3
4 5
4
1 6
7 7
5 8 8 9 2 10 3 11
17 1
11 2
8 4
10 3
4 5
1 6
7 7
5 8 6 9 2 10 3 11
6.4 Heapsort
6-80
Max-Heapify
proc Max-Heapify(a: <Referenz auf T[]>; k: int) begin
var max: int;
max ← k;
if left(k) ≤ a.heapSize &&
a[left(k)] > a[max] then
max ← left(k);
fi;
if right(k) ≤ a.heapSize &&
a[right(k)] > a[max] then
max ← right(k);
fi;
if k , max then
swap(a[k], a[max]);
Max-Heapify(a, max);
fi;
end
6.4 Heapsort
6-81
Max-Heapify
6.4 Heapsort
◮
Alle Operationen in Max-Heapify bis auf den rekursiven Aufruf
sind in O (1) implementierbar.
◮
Falls ein rekursiver Aufruf erfolgt, dann nur auf Knoten, die
unterhalb von k liegen.
◮
Die Aufruffolge ist daher durch die Höhe des von k
aufgespannten Teilbaums nach oben begrenzt.
◮
Da auch der Teilbaum vollständig ist, liegt die Laufzeit von
Max-Heapify innerhalb von O (log n).
6-82
Build-Max-Heap
6.4 Heapsort
◮
Build-Max-Heap erhält als Eingabe eine Referenz auf ein
unsortiertes Feld und stellt sicher, dass im gesamten Feld die
Max-Heap-Eigenschaft gilt.
◮
Dies geschieht, indem Max-Heapify auf alle Knoten
angewendet wird.
◮
Da Max-Heapify erwartet, dass die Teilbäume unterhalb eines
Knotens die Heap-Eigenschaft erfüllen, muss vom Ende des
Felds begonnen werden.
◮
Alle inneren Knoten des Heaps liegen im Bereich ⌊ n2 ⌋ . . . 1. Auf
diese Knoten muss also Max-Heapify angewendet werden.
6-83
Build-Max-Heap
proc Build-Max-Heap(a: <Referenz auf T[]>) begin
var i: int;
a.heapSize ← a.length;
for i ← ⌊a.length / 2⌋ downto 1 do
Max-Heapify(a, i);
od;
end
6.4 Heapsort
6-84
Build-Max-Heap
12 1
12 1
1. Iteration
6 2
11 4
5 8
1 3
3 5
8 9
17 6
6 2
10 7
11 4
2 10 4 11
5 8
1 3
4 5
8 9
10 7
2 10 3 11
12 1
12 1
2. Iteration
3. Iteration
6 2
11 4
5 8
6.4 Heapsort
17 6
8 9
1 3
4 5
2 10 3 11
17 6
6 2
10 7
11 4
5 8
8 9
17 3
4 5
1 6
10 7
2 10 3 11
6-85
Build-Max-Heap
12 1
12 1
4. Iteration
11 2
6 4
5 8
17 3
4 5
8 9
1 6
11 2
10 7
2 10 3 11
8 4
5 8
6 9
17 3
4 5
1 6
10 7
2 10 3 11
17 1
5. Iteration
11 2
8 4
5 8
6.4 Heapsort
6 9
12 6 1 11 3 17 10 5 8 2 4
12 3
4 5
2 10 3 11
1 6
10 7
17 11 12 8 4 1 10 5 6 2 3
6-86
Build-Max-Heap
Der Aufruf von Max-Heapify liegt jeweils in O (log n), Max-Heapify
wird O (n)-mal aufgerufen, daher ergibt sich eine Laufzeit
innerhalb von O (n log n).
Allerdings ist eine genauere Abschätzung möglich: Es gilt, dass
ein Heap mit n Elementen die Höhe ⌊ld (n)⌋ hat und dass
höchstens ⌈ 2hn+1 ⌉ Knoten die Höhe h haben.
∞
X
h
h =0
⌊ld
(h )⌋ X
h =0
n
2h +1
2h
=2
 ⌊ld (h )⌋ 
 ∞

 X h 
 X h 


 = O (n)
 = O n
O (h ) = O n


2h 
2h 
h =0
h =0
Build-Max-Heap hat also eine lineare Laufzeit.
6.4 Heapsort
6-87
Heapsort
Eingabe für Heapsort ist ein Referenzparameter auf ein
unsortiertes Feld a , Ausgabe ist das sortierte Feld a . Der
Algorithmus arbeitet folgendermaßen:
1. Konstruiere einen Max-Heap.
2. Vertausche das erste Element (Wurzel, größtes Element) und
das letzte Element (Blatt ganz rechts) des Heaps.
3. Reduziere den Max-Heap um das Blatt ganz rechts.
4. Wende Max-Heapify auf die Wurzel an.
5. Solange Blätter vorhanden und nicht gleich der Wurzel sind,
fahre mit Schritt 2 fort.
6.4 Heapsort
6-88
Heapsort
proc Heapsort(a: <Referenz auf T[]>) begin
var i: int;
Build-Max-Heap(a);
for i ← a.length downto 2 do
swap(a[1], a[i]);
a.heapSize ← a.heapSize - 1;
Max-Heapify(a, 1);
od;
end
Die Laufzeit von Heapsort liegt in O (n log n).
6.4 Heapsort
6-89
Heapsort
12 1
Heap
6 2
12 6 1 11 3 17 10 5 8 2 4
11 4
5 8
1 3
3 5
8 9
10 7
2 10 4 11
17 1
3 1
swap
MaxHeapify
11 2
8 4
5 8
6.4 Heapsort
17 6
6 9
12 3
4 5
2 10 3 11
1 6
11 2
10 7
8 4
5 8
6 9
12 3
4 5
1 6
10 7
2 10 17 11
6-90
Heapsort
12 1
2 1
swap
MaxHeapify
11 2
8 4
5 8
10 3
4 5
6 9
1 6
11 2
3 7
2 10 17 11
8 4
5 8
10 3
4 5
3 7
6 9 12 10 17 11
11 1
10 1
MaxHeapify
swap, MaxHeapify
8 2
6 4
5 8
6.4 Heapsort
1 6
10 3
4 5
2 9 12 10 17 11
1 6
8 2
3 7
6 4
3 3
4 5
1 6
2 7
5 8 11 9 12 10 17 11
6-91
Heapsort
8 1
6 1
MaxHeapify
swap,MaxHeapify
6 2
5 4
3 3
4 5
1 6
5 2
2 7
2 4
10 8 11 9 12 10 17 11
3 3
4 5
1 6
8 7
10 8 11 9 12 10 17 11
1 1
Array
···
2 2
4 4
1 2 3 4 5 6 8 10 11 12 17
3 3
5 5
6 6
8 7
10 8 11 9 12 10 17 11
6.4 Heapsort
6-92
Prioritätswarteschlangen
Heaps können nicht nur zum Sortieren benutzt werden, sondern
auch für weitere Anwendungen. Eine wollen wir jetzt betrachten.
Eine Max-Prioritätswarteschlange ist eine Datenstruktur zur
Speicherung einer Menge S , deren Elementen ein Schlüssel
(Priorität) zugeordnet ist. Prioritätswarteschlangen besitzen die
folgenden Operationen:
◮
maximum(S) gibt das Element von S mit dem maximalen
Schlüssel zurück.
◮
extract-max(S) entfernt aus S das maximale Element.
◮
increase-key(S,x,k) erhöht den Schlüssel von x auf k .
◮
insert(S,x) fügt x in S ein.
Max-Prioritätswarteschlangen können effizient durch Max-Heaps
implementiert werden.
6.4 Heapsort
6-93
Prioritätswarteschlangen
proc maximum(S) begin
return S[1];
end
proc extract-max(S) begin
if heap-groesse(S) < 1 then error fi;
max ← S[1];
S[1] ← S[heap-groesse(S)];
heap-groesse(S) ← heap-groesse(S)-1;
Max-Heapify(S,1);
return max;
end
6.4 Heapsort
6-94
Prioritätswarteschlangen
proc increase-key(S,x,k) begin
if k < S[x] then error fi;
S[x] ← k;
while x > 1 ∧ S[vater(x)] < S[x] do
swap (S[x],S[vater(x)]);
x ← vater(x);
od;
end
proc insert(S,x) begin
heap-groesse(S) ← heap-groesse(S)+1;
S[heap-groesse(S)] ← −∞;
increase-key(S,heap-groesse(S),k);
end
6.4 Heapsort
6-95
Prioritätswarteschlangen
6.4 Heapsort
◮
Die Laufzeiten der vier Operationen für
Prioritätswarteschlangen der Größe n liegen in O (log n).
◮
Analog zu Max-Heaps und Max-Prioritätswarteschlangen
lassen sich Min-Heaps und Min-Prioritätswarteschlangen
definieren.
6-96
1. Der Algorithmenbegriff
2. Imperative Algorithmen
3. Sortieralgorithmen
4. Listen und abstrakte Datentypen
5. Objektorientierte Algorithmen
6. Bäume
7. Mengen, Verzeichnisse und Hash-Verfahren
7.1 Mengen
7.2 Verzeichnisse
7.3 Hashverfahren
8. Graphen
9. Entwurf von Algorithmen
10. Funktionale und deduktive Algorithmen
Mengen
Unter einer Menge verstehen wir jede Zusammenfassung M von
bestimmten, wohlunterschiedenen Objekten m unserer
Anschauung oder unseres Denkens (welche die Elemente von M
genannt werden) zu einem Ganzen.
Es kommt es nicht darauf an, wie oft und in welcher Reihenfolge
die Elemente einer Menge aufgeführt werden. Es gilt
{2, 4, 6, 8} = {6, 8, 2, 4, 4, 6, 8} = {8, 6, 2, 4}.
Für Mengen sind unter anderem die folgenden Operationen
definiert:
◮
◮
◮
◮
7.1 Mengen
Elementbeziehung m ∈ M
Vereinigungsbildung M ∪ N
Durchschnittsbildung M ∩ N
Differenzmengenbildung M \N
7-1
ADT Menge
type : Set (T )
import : Bool
operators :
∅ :→ Set
empty ? : Set → Bool
is _in : Set × T → Bool
insert : Set × T → Set
delete : Set × T → Set
union : Set × Set → Set
intersection : Set × Set → Set
difference : Set × Set → Set
···
axioms ∀s ∈ Set , ∀x , y ∈ T
···
7.1 Mengen
7-2
Geordnete Mengen
7.1 Mengen
◮
Die Elemente einer Menge können durch eine
Ordnungsrelation angeordnet sein.
◮
Eine Ordnungsrelation ist eine reflexive, transitive und
antisymmetrische Relation.
◮
Wenn je zwei Elemente vergleichbar sind, heißt die
Ordnungsrelation linear oder total.
7-3
ADT Geordnete Menge
type : OrderedSet (T )
import : Bool
operators :
∅ :→ OrderedSet
empty ? : OrderedSet → Bool
is _in : OrderedSet × T → Bool
insert : OrderedSet × T → OrderedSet
delete : OrderedSet × T → OrderedSet
union : OrderedSet × OrderedSet → OrderedSet
intersection : OrderedSet × OrderedSet → OrderedSet
difference : OrderedSet × OrderedSet → OrderedSet
min : OrderedSet → T
max : OrderedSet → T
···
axioms ∀s ∈ OrderedSet , ∀x , y ∈ T
···
7.1 Mengen
7-4
Mengenimplementierungen
Mengen können auf verschiedene Weisen realisiert werden. Einige
davon besprechen wir in diesem Kapitel. Wir befassen uns nur mit
endlichen Mengen.
Ungeordnete Mengen:
1. Bitfelder
2. Listen
3. Felder
Geordnete Mengen:
1. Listen
2. Suchbäume
7.1 Mengen
7-5
Kopieren von Objekten
7.1 Mengen
◮
Häufig besteht der Bedarf, ein Objekt zu kopieren, d. h. ein
zweites Objekt zur Verfügung zu haben, das dem ersten
vollständig gleicht.
◮
In objektorientierten Programmiersprachen ist eine
Anweisung der Form a ← b hierfür oft nicht geeignet.
◮
Es wird der Variablen a lediglich eine Referenz auf das
ursprüngliche Mengenobjekt zugewiesen. Manipulationen des
durch a referenzierten Objekts verändern so zugleich das
durch b bestimmte Objekt.
◮
Man unterscheidet zwischen so genannten flachen und tiefen
Kopien.
◮
Programmtechnische Details sind nicht Gegenstand dieser
Vorlesung.
7-6
Implementierung durch Bitfelder
Dieses Verfahren ist geeignet, Mengen A zu realisieren, die
Teilmenge einer kleinen endlichen Grundmenge G = {g1 , . . . , gn }
sind.
◮
◮
Es wird ein Bitfeld A mit |G | = n Bits vereinbart.
Es gilt gi ∈ A ⇔ A [i ] = 1. Wir vereinbaren: 0 , false, 1 , true.
g1
1
g1 ∈ A
g2
1
g2 ∈ A
g3
0
g3 6∈ A
···
gn
7.1 Mengen
0
gn 6∈ A
7-7
Implementierung durch Bitfelder
proc empty() begin
var A: BitSet;
var i: int;
for i ← 1 to n do
A[i] ← false;
od;
return A;
end
proc isIn(A: BitSet, i: int): bool begin
return A[i];
end
7.1 Mengen
7-8
Implementierung durch Bitfelder
func intersection(B, C: BitSet): Bitset begin
var A: BitSet;
var i: int;
for i ← 1 to n do
A[i] ← B[i] && C[i];
od;
return A;
end
func union(B, C: BitSet): Bitset begin
var A: BitSet;
var i: int;
for i ← 1 to n do
A[i] ← B[i] || C[i];
od;
return A;
end
7.1 Mengen
7-9
Implementierung durch Bitfelder
func difference(B, C: BitSet): BitSet begin
var A: BitSet;
var i: int;
for i ← 1 to n do
A[i] ← B[i] && ! C[i];
od;
return A;
end
7.1 Mengen
7-10
Implementierung durch Bitfelder
Die Laufzeit von isIn, insert und delete liegt in O (1). Die
Laufzeiten der Operationen union, intersection und
difference sind abhängig von der Kardinalität der Grundmenge
und liegen in O (n).
Vorteile:
◮
Für kleine Grundmengen ist die Implementierung sehr
effizient.
◮
Die Operationen besitzen teilweise eine konstante Laufzeit.
Nachteile:
7.1 Mengen
◮
Die Grundmenge muss vorher bekannt sein.
◮
Ggf. muss ein Wert in einen Index umgewandelt werden.
7-11
Implementierung durch Listen
7.1 Mengen
◮
Die Elemente der Menge werden in einer verketteten Liste
gespeichert.
◮
Die Reihenfolge der Elemente spielt keine Rolle.
◮
Elemente dürfen nicht mehrfach vorkommen.
◮
Die Anzahl der Elemente der Menge braucht nicht beschränkt
zu sein.
◮
Der Platzbedarf richtet sich nach der Größe der Menge, nicht
nach der Größe der Grundmenge.
◮
Die Sortierung innerhalb der Liste kann zusätzliche
Informationen enthalten (Beispiel: Reihenfolge des
Einfügens).
7-12
Implementierung durch Listen
7.1 Mengen
◮
Varianten: einfach und mehrfach verkettete Listen (siehe
Abschnitt über Listen).
◮
Pro Element wird mehr Speicherplatz verbraucht als bei der
Bitfeldimplementierung.
◮
Operationen sind aufwändiger als bei der
Bitfeldimplementierung.
◮
Im Folgenden: ausgewählte Algorithmen (diesmal aus
objektorientierter Sicht).
7-13
Implementierung durch Listen
func copy(): ListSet begin
var result: ListSet;
var tmp: T;
foreach tmp ← L.elements() do
result.insert(tmp);
od;
return result;
end
7.1 Mengen
7-14
Implementierung durch Listen
func equals(other: Set): bool begin
var tmp: T;
if size , other.size then
return false;
foreach tmp ← L.elements() do
if! other.isIn(tmp) then
return false;
od;
return true;
end
7.1 Mengen
7-15
Implementierung durch Listen
func union(other: Set): ListSet begin
var result: ListSet;
var tmp: T;
result ← other.copy();
foreach tmp ← L.elements() do
result.insert(tmp);
od;
return result;
end
7.1 Mengen
7-16
Implementierung durch Listen
func intersection(other: Set): ListSet begin
var result: ListSet;
var tmp: T;
foreach tmp ← L.elements() do
if other.isIn(tmp) then
result.insert(tmp);
od;
return result;
end
7.1 Mengen
7-17
Implementierung durch Listen
func difference(other: Set): ListSet begin
var result: ListSet;
var tmp: T;
foreach tmp ← L.elements() do
if! other.isIn(tmp) then
result.insert(tmp);
od;
return result;
end
7.1 Mengen
7-18
Vergleich
Operation
Liste
Bitfeld
isIn
insert
delete
copy
union
difference
intersection
equal
O (|s |)
O (|s |)
O (|s |)
O (|s |)
O (|t | + |t | · |s |)
O (|s | · |t |)
O (|s | · |t |)
O (|s | · |t |)
O (1)
O (1)
O (1)
O (|G |)
O (|G |)
O (|G |)
O (|G |)
O (|G |)
Die Operation insert muss „Dubletten“ verhindern.
7.1 Mengen
7-19
Implementierung durch Felder
◮
◮
◮
7.1 Mengen
Die Elemente der Menge s werden in einem Feld A fester
Länge gespeichert. Dabei belegen die Mengenelemente die
Felder 1 bis |s |.
Der Index des höchsten belegten Feldes wird in einer
Variablen size = |s | gespeichert.
Die Größe der Menge ist (in vielen Sprachen) durch die
Feldgröße beschränkt.
◮
Es ist keine dynamische Speicherverwaltung notwendig.
◮
Die Sortierung innerhalb des Felds kann zusätzliche
Informationen enthalten (zum Beispiel die Reihenfolge des
Einfügens).
7-20
Implementierung durch Felder
func isIn(t: T): bool begin
var i: int;
for i ← 1 to size do
if A[i] = t then
return true;
od;
return false;
end
7.1 Mengen
7-21
Implementierung durch Felder
proc insert(t: T) begin
if! isIn(t) then
if size = A.size then
error(“maximale Größe erreicht”);
A[size] ← t;
size ← size + 1;
fi;
end
func copy(): ArraySet begin
var result: ArraySet;
var i: int;
for i ← 1 to size do
result.A[i] ← A[i];
od;
result.size ← size;
return result;
end
7.1 Mengen
7-22
Implementierung durch Felder
7.1 Mengen
◮
Die Laufzeit fast aller Operationen ist denen der
Listenimplementierung ähnlich. Bei der Operation delete
müssen evtl. Bereiche des Felds verschoben werden.
◮
Pro Element ist der Speicherverbrauch geringer als bei
Implementation durch verkettete Listen.
◮
Allerdings verbrauchen auch nicht belegte Felder
Speicherplatz.
◮
Der Speicherplatzbedarf ist unabhängig von der tatsächlichen
Größe der Menge.
◮
Der Speicherplatzbedarf ist bei geringem Füllstand unnötig
groß.
◮
Einige Programmiersprachen bieten die Möglichkeit, die
Größe eines Felds zu verändern.
7-23
Implementierung geordneter Mengen durch Listen
7.1 Mengen
◮
Die Grundidee folgt der Implementierung von (ungeordneten)
Mengen durch Listen.
◮
Allerdings sind die Elemente in der Liste jetzt entsprechend
der Ordnungsrelation angeordnet.
◮
Einige Operationen lassen sich dadurch beschleunigen.
7-24
Implementierung geordneter Mengen durch Listen
Als Beispiel betrachten wir die Durchschnittsbildung. Jede Liste
wird mit einem Zeiger durchlaufen.
1. Setze zv auf den Anfang der ersten und zw auf den Anfang
der zweiten Liste.
2. Schreite mit zw solange fort, bis sein Wert w größer oder
gleich dem Wert v von zv ist.
3. Fallunterscheidung:
3.1 w = v : Nimm v in die Zielmenge auf und setze zv weiter.
3.2 w > v : Setze zv weiter.
4. Solange keine Zeiger am Ende angekommen ist, fahre mit 2
fort.
zv
1
8
head
zw
head
7.1 Mengen
6: Treffer 7: Ende
4
22
49
2
3
1
5
12
49
55
7-25
Implementierung geordneter Mengen durch Listen
func intersection(other: SortListSet): SortListSet begin
var result: SortListSet;
var i, j: <Referenz auf ListElement>;
i ← L.head;
j ← other.head;
while i <gültige Referenz> && j <gült. Ref.> do
if i.wert = j.wert then
result.insert(j.wert);
if i.wert <= j.wert then
i ← i.next;
else
j ← j.next;
fi;
od;
return result;
end
7.1 Mengen
7-26
Implementierung geordneter Mengen durch Listen
7.1 Mengen
◮
Die Operation insert muss die Ordnung der Elemente
beachten.
◮
Die Operationen isIn und delete müssen im Misserfolgsfall
nicht bis zum Ende der Liste suchen. Dies führt zwar nicht zu
einer Verbesserung der Komplexitätsklasse, dennoch
bedeutet es eine Effizienzsteigerung.
◮
Bei einigen Operationen (zum Beispiel union, intersection,
difference) kann durch die Ordnung der Liste eine
Laufzeitverbesserung erreicht werden, die zu einer anderen
Laufzeitklasse führt.
7-27
Implementierung geordneter Mengen durch Bäume
7.1 Mengen
◮
Anstatt die Elemente in einer Liste abzulegen, kann auch ein
binärer Suchbaum verwendet werden.
◮
Für die Operationen intersection, union, difference kann
analog zur Verwendung von Listen bei geordneten Mengen
effizienzsteigernd das gleichzeitige Durchwandern beider
Bäume durchgeführt werden.
◮
Insbesondere verbessern sich die Laufzeiten von isIn, insert
und delete. Dies hängt vom verwendeten Baumtyp ab.
7-28
Verzeichnisse
Es seien zwei (endliche) Mengen A und B gegeben.
◮
Ein Verzeichnis (Dictionary) ist eine Relation D ⊆ A × B mit
∀a ∈ A , b1 , b2 ∈ B : (a , b1 ) ∈ D ∧ (a , b2 ) ∈ D ⇒ b1 = b2 .
◮
Mathematisch gesehen ist ein Verzeichnis eine partielle
Abbildung von A nach B .
◮
Bezeichnungen:
A
a∈A
B
(a , b ) ∈ D
7.2 Verzeichnisse
Schlüsselmenge, Definitionsmenge
Schlüssel
Wertemenge
Assoziation
7-29
Verzeichnisse
◮
Verzeichnisse werden verwendet, um Schlüssel auf
Datenobjekte abzubilden.
◮
Beispiele:
◮
◮
◮
7.2 Verzeichnisse
Ergebnisliste einer Klausur: (Matrikelnummer, Note)
Dateisystem: (Dateiname, Datenblockmenge)
Da ein Verzeichnis als Menge von Paaren definiert ist, können
die besprochenen Mengenimplementierungen verwendet
werden.
7-30
Einige Operationen
keys : P(A × B ) → P(A )
keys (D ) = {a ∈ A | ∃b ∈ B .(a , b ) ∈ D }
delete : A × P(A × B ) → P(A × B )
delete (a , D ) = D \{(a , b ) | ∃b ∈ B .(a , b ) ∈ D }
associate : (A × B ) × P(A × B ) → P(A × B )
associate ((a , b ), D ) = delete (a , D ) ∪ {(a , b )}
7.2 Verzeichnisse
7-31
Einige Operationen
value : A × P(A × B ) → B
value (a , D ) = b
falls
∃b ∈ B .(a , b ) ∈ D ,
sonst undefiniert
values : P(A × B ) → P(B )
values (D ) = {b ∈ B | ∃a ∈ A .(a , b ) ∈ D }
size : P(A × B ) → N0
size (D ) = |D |
7.2 Verzeichnisse
7-32
Verzeichnisse
Verzeichnis von Namen und Telefonnummern
A Zeichenketten, B Zeichenketten, D = ∅
associate("Linus", "05313250", D)
associate("Lena", "05315148", D)
associate("Johannes", "01727512", D)
Führt zu D = {("Linus", "05313250"), ("Lena", "05315148"),
("Johannes", "01727512")}.
keys(D) = {"Linus", "Lena", "Johannes"}
values(D) = {"05313250", "05315148", "01727512"}
value(Linus, D) = "05313250"
value(Peter, D) = ⊥
size(D) = 3
delete(Johannes, D) =
{("Linus", "05313250"), ("Lena", "05315148")}
7.2 Verzeichnisse
7-33
Hashverfahren - Einführung
◮
Bei der Listenimplementierung einer Menge erfolgt der Zugriff
auf ein Element in der Zeit O (n).
◮
Bitfeldimplementierungen benötigen nur die Zeit O (1),
können jedoch sinnvoll nur kleine Mengen behandeln.
◮
Idee: Die beiden Möglichkeiten werden kombiniert.
◮
Hashverfahren basieren auf Verzeichnissen D ⊆ A × B
◮
◮
für deren Schlüsselmenge A = {0, . . . , n − 1} ⊆ N0 gilt
und deren Schlüssel berechnet werden mithilfe einer so
genannten Hashfunktion h : B → A .
◮
Das Verzeichnis heißt Hashtabelle.
◮
Für die zu speichernde Menge M ist M ⊆ B .
◮
7.3 Hashverfahren
Die Berechnung der Werte der Hashfunktion erfolgt in O (1)
Schritten.
7-34
Einführung
Beispiel:
A = {0, . . . , 9}, B = N0 ,
h : B → A , h (j ) = j mod 10
Die zu speichernde Menge sei
M = {35, 67} ⊆ N0 .
Das Einfügen der Zahlen 35
und 67 führt wegen h (35) = 5
und h (67) = 7 zu folgender
Hashtabelle:
7.3 Hashverfahren
Schlu
¨ssel
0
1
2
3
4
5
6
7
8
9
Werte
35
67
7-35
Einführung
◮
Gegeben sei eine Nummerierung der Buchstaben (A=1, B=2,
usw.)
◮
Hashfunktion für Vor- und Nachnamen:
h1 : Addiere Nummer des ersten Buchstabens des Vornamens
zur Nummer des ersten Buchstabens des Nachnamens.
h2 : Addiere Nummer des zweiten Buchstabens des
Vornamens zur Nummer des zweiten Buchstabens des
Nachnamens.
◮
Der Hashwert ergibt sich aus diesem Wert durch Restbildung
modulo Feldgröße, zum Beispiel mod 10.
◮
Beispiel:
h1 („Andreas Rot“) = (1 + 18) mod 10 = 9
h2 („Andreas Rot“) = (14 + 15) mod 10 = 9
7.3 Hashverfahren
7-36
Einführung
Ausgangsstruktur ist ein Feld, die Hashtabelle,
◮
dessen Indexmenge der Schlüsselmenge A entspricht und
◮
deren Felder, genannt Buckets, die Elemente der zu
speichernden Menge aufnehmen.
Ein Element der Wertemenge wird mithilfe der Hashfunktion auf
einen Index des Felds abgebildet und in diesem Feld gespeichert.
Ein Bucket kann im Allgemeinen mehrere Elemente aufnehmen.
Da die Wertemenge üblicherweise sehr groß ist, wird die
Hashfunktion nicht injektiv sein.
7.3 Hashverfahren
7-37
Einführung
Zusammenfassung des Prinzips:
◮
◮
◮
◮
Es ist eine Menge M ⊆ B zu speichern. B ist die Wertemenge.
Die Schlüsselmenge sei A = {0, . . . , n − 1} ⊆ N0 .
h : B → A ist eine Hashfunktion.
Das Feld zur Speicherung der Elemente von Menge M ist die
Hashtabelle R . A ist der Indexbereich von R .
∀i ∈ A .R [i ] = {b ∈ M | h (b ) = i } ⊆ B
∀i ∈ A .Bi = {b ∈ B | h (b ) = i } = h −1 (i ) ⊆ B
n
[
B=
Bi disjunkte Zerlegung von B
M=
i =0
n
[
i =0
7.3 Hashverfahren
b ∈ B | b ∈ M , h (b ) = i
7-38
Einführung
Bi
Bj
Bk
h
Menge M
h
h
Bucket 0:
Bucket 1:
Bucket 2:
···
Bucket n:
M ∩ B0
M ∩ B1
M ∩ B2
M ∩ Bn
Wertemenge B
isIn(M , b ) für b ∈ B :
1. Ermitteln des Buckets i = h (b ) in O (1) Schritten.
2. Durchsuchen des Buckets R [i ] in O (|R [i ]|) Schritten.
|R [h (b )]| sollte möglichst klein sein, ideal wäre 1.
7.3 Hashverfahren
7-39
Kollisionen
Je mehr Elemente die Menge enthält, desto wahrscheinlicher ist,
dass es zu einer Kollision kommt:
∃b , b ′ ∈ M .b , b ′ ∧ h (b ) = h (b ′ ).
Das heißt, h ist in diesem Fall nicht injektiv.
◮
◮
7.3 Hashverfahren
Open Hashing: Größe der Buckets erhöhen (d. h. mehrere
Elemente aufnehmen). |M | ist dann unbegrenzt.
Closed Hashing: Auf einen anderen Bucket ausweichen (d. h.
maximal ein Element pro Bucket). Dann ist |M | ≤ |R |.
7-40
Hashfunktionen
Hashfunktionen sollten surjektiv sein. Für B ⊆ N0 sind zwei
Beispiele für Hashfunktionen wie folgt definiert.
◮
Divisionsmethode: h : B → A mit
h (j ) = j
◮
◮
◮
◮
mod |A |
h (j ) ist schnell zu berechnen.
Vermieden werden sollte |A | = 2k , k ∈ N.
Besser: |A | ist Primzahl nicht zu nahe an 2k , k ∈ N.
Multiplikationsmethode: h : B → A mit
h (j ) = ⌊|A | · (j · c
◮
◮
7.3 Hashverfahren
x mod 1 = x − ⌊x ⌋
Günstig (nach Knuth): c =
mod 1)⌋, 0 < c < 1
√
5 −1
2
7-41
Open Hashing (with Chaining)
Feld R von Listen mit Indexmenge A = {0, 1, . . . , n − 1}. Bei
Kollisionen wird das neue Element b der Liste R [h (b )] am Kopf
hinzugefügt.
Beispiel:
M = {0, 1, 10, 11, 30, 41, 49, 51, 59}, n = 10, h (b ) = b mod n
0
30
10
0
1
51
41
11
···
9
7.3 Hashverfahren
1
h− (9) ∩ M = {59, 49}
59
49
7-42
Open Hashing (with Chaining)
◮
◮
Die Liste kann einfach oder doppelt verkettet sein.
Einfügen, Löschen und Suchen eines Elements b erfordern
◮ das Berechnen des Hash-Wertes h (b ) sowie
◮
das Ausführen der entsprechenden Listenfunktion (isIn(b),
insert(b), delete(b)) für die Liste R [h (b )].
Da die Hashfunktion in O (1) ausgeführt wird, dominiert somit
die jeweilige Listenfunktion die Laufzeit der Operation.
◮
Die Operationen union, intersection und difference können
sukzessive für jede Liste durchgeführt werden.
◮
Ist auf B eine Ordnung definiert, bieten sich neben
geordneten Listen auch andere Strukturen zur
Implementierung geordneter Mengen an.
7.3 Hashverfahren
7-43
Open Hashing (with Chaining)
Die Laufzeit der Operation isIn(b)
◮
beträgt im ungünstigsten Fall O (|M |). Dieser Fall tritt ein,
wenn alle Elemente von M in dem Bucket R [h (b )] liegen.
◮
beträgt im günstigsten Fall O (1). Dieser Fall tritt für
|R [h (b )]| = 0 ein.
Aussagekräftig ist daher nur der mittlere Fall. Es gilt:
Satz: Die Menge M sei durch „zufälliges Ziehen“ von Elementen
b1 , . . . , bm aus B entstanden. Für m =| M | ist die mittlere Laufzeit
der Operation isIn()
!
m
.
T (m) = O 1 +
|A |
7.3 Hashverfahren
7-44
Hashfunktionen
Bespiel 1: h(Name) = Anfangsbuchstabe des Namens
◮
Bei deutschen Nachnamen sind die Buckets ’S’ und ’E’ sehr
voll, die Buckets ’C’, ’Y’, ’X’, ’Q’ dagegen sehr leer.
◮
Besser ist es, bei Namen bzw. Zeichenketten y = y1 y2 y3 . . . yz
die folgende Hashfunktion zu verwenden.


z
X


h (y ) = 
OrdnungCodeX (yi )


j =1
7.3 Hashverfahren
mod (|A |)
7-45
Hashfunktionen
Beispiel 2: Ein Compiler trägt die Bezeichner eines Programms in
eine Hashtabelle ein.
M sei die Menge der Bezeichner, |M | = m.
Die Laufzeit für den Eintrag eines Bezeichers x ergibt sich durch:
Auswerten von h (x ):
Überprüfen von R [h (x )]:
evtl. Anfügen von x :
7.3 Hashverfahren
1
| R [h (x )] | +1
1
| R [h (x )] | +3
7-46
Hashfunktionen
Unter idealen Verhältnissen, also wenn nach dem Einfügen der
Elemente b1 , b2 , . . . , bi −1
i−1
|R [h (bi )]| =
|A |
ist, dann gilt für die Laufzeit zum Einfügen von m Elementen:
!
!
m
m
X
X
i−1
m+1
1
T (m) = (|R [h (bi )]| + 3) =
+3 =m
−
+3
|A |
2|A |
|A |
i =1
i =1
7.3 Hashverfahren
7-47
Hashfunktionen
Wenn der Programmierer nur Bezeichner der Art „A00“ bis „A99“
verwendet werden, also M = {A 00, A 01, . . . , A 99} ist, und
außerdem


z
X

h (y ) = 
OrdnungASCII (yi )


j =1
mod (|A |)
und A = {0, . . . , 99} gelten, dann ist zwar |A | = |M |, aber es
werden nur 19 der 100 Buckets benutzt.
7.3 Hashverfahren
7-48
Hashfunktionen
j0
A00
j0 + 1
A01
A10
j0 + 2
A02
A11
A20
···
···
···
···
···
j0 + 9
A09
A18
A27
···
A90
j0 + 10
A19
A28
···
A91
···
···
···
···
j0 + 18
A99
Es gibt also einen Bucket mit 10 Elementen und je zwei mit
9, 8, 7, . . . , 1 Elementen. Die Laufzeit für das Auffüllen eines
Buckets mit m Elementen beträgt
l (m) = 3 + 4 + 5 + · · · + (m + 3), die Gesamtlaufzeit also
l (10) + 2
9
X
l (i ) = 742 > 349, 5 = T (100).
i =1
Ursache: Der Trend aus M (benachbarte Bezeichner) schlägt auf
h (b ) (benachbarte Buckets) durch.
7.3 Hashverfahren
7-49
Open Hashing (with Chaining)
Zusammenfassung:
◮
Das Problem der Kollisionen wird durch Bucketlisten gelöst.
◮
Die Menge M bzw. die Buckets können (im Prinzip) beliebig
groß werden.
◮
Falls die Listen lang werden, steigt die Zugriffszeit.
7.3 Hashverfahren
7-50
Closed Hashing
Jedes Element von R fasst maximal ein Element aus B . Es wird
bei einer Kollision eine Folge von Hashfunktionen verwendet:
h0 : B → A , h1 : B → A , . . . , hn : B → A .
Einfügen von b ∈ B :
1. i ← 0
2. Fallunterscheidung bezüglich des Felds R [hi (b )]:
◮
◮
◮
Feld ist frei: Füge b ein und beende den Algorithmus.
Feld enthält bereits b : Beende den Algorithmus.
Feld ist bereits durch ein Element b ′ , b belegt (Kollision):
i ← i + 1.
3. Falls i < n wiederhole Schritt 2, andernfalls beende den
Algorithmus mit Fehler.
7.3 Hashverfahren
7-51
Closed Hashing
Einfügen von 1, 6, 5, 3, die Indexmenge sei A = {0, 1, 2, 3} und
h0 : B → A
hi : B → A
0
1
2
3
+1
h0 (1) = 1
0
1 1
2
3
h0 (j ) = j mod |A | = j mod 4
mit
mit
+6
h0 (6) = 2
hi (j ) = h0 (j ) + i mod |A |;
0
1 1
2 6
3
+5
h0 (5) = 1
0
1 1
2 6
3 5
h1 (5) = 2
h2 (5) = 3
+3
h0 (3) = 3
i>0
0
1
2
3
3
1
6
5
h1 (3) = 0
Das Feld R ist nun voll, 5 und 3 konnten nicht am „eigentlich“
vorgesehenen Platz eingefügt werden.
7.3 Hashverfahren
7-52
Closed Hashing
Suchen von b ∈ B
1. i ← 0
2. Fallunterscheidung bezüglich des Felds R [hi (b )]:
◮
◮
◮
Feld ist frei: b ist nicht enthalten, beende den Algorithmus.
Feld enthält b : b ist gefunden, beende den Algorithmus.
Feld ist durch ein Element b ′ , b belegt: i ← i + 1
3. Falls i < n wiederhole Schritt 2, andernfalls beende den
Algorithmus (Das Element ist im vollständig gefüllten Feld
nicht enthalten).
5?
h0 (5) = 1
7.3 Hashverfahren
0
1
2
3
3
1
6
5
1 6= 5; h1 (5) = 2
6 6= 5; h2 (5) = 3
7-53
Closed Hashing
Löschen eines Elements:
Problem: Das gelöschte Feld muss von einem leeren Feld
unterscheidbar sein, da es auf dem Suchpfad eines Elements
liegen kann.
Eine Lösung: Das Element bleibt gespeichert, das Feld wird dafür
mit einem booleschen Flag versehen, dass angibt, ob es sich um
ein aktives oder ein gelöschtes Element handelt. Notwendige
Änderungen:
◮
Der Algorithmus zum Suchen muss über als gelöscht
markierte Felder hinweggehen.
◮
Der Algorithmus zum Einfügen kann Elemente auch auf als
gelöscht markierten Feldern einfügen.
Folgerung: Die Laufzeit der Suche/des Löschens hängt nicht
länger vom tatsächlichen Füllgrads des Felds ab.
7.3 Hashverfahren
7-54
Lineare Kollisionsbehandlung
Bei der linearen Kollisionsbehandlung werden Hashfunktionen der
folgenden Form verwendet:
◮
◮
h0 : B → A Hashfunktion,
hi : B → A mit hi (j ) = (h0 (j ) + i ) mod |A | für i > 0.
Nach Bestimmung des ersten Hashwertes mit h0 berechnet h1 den
nächsten Index durch Addition von 1. Bei Erreichen des letzten
Indexes wird 0 als nächster Index bestimmt.
7.3 Hashverfahren
7-55
Lineare Kollisionsbehandlung
Die lineare Kollisionsbehandlung kann zu einer
Clusterbildung (primary clustering) führen. Je
größer ein Cluster, desto höher die
Wahrscheinlichkeit, dass der Hashwert h0 (z )
eines einzufügenden Elements z in dem Cluster
liegt, wodurch dieser verlängert wird. Dies hat
negative Auswirkungen auf die Laufzeit der
Suche nach freien Feldern.
7.3 Hashverfahren
Cluster
belegter
Felder
h0 (j)
h1 (j)
h2 (j)
h3 (j)
7-56
Quadratische Kollisionsbehandlung
Bei der quadratischen Kollisionsbehandlung werden
Hashfunktionen der folgenden Form verwendet:
◮
◮
◮
◮
◮
◮
7.3 Hashverfahren
h0 : B → A Hashfunktion,
hi : B → A mit hi (j ) = (h0 (j ) + c1 · i + c2 · i 2 ) mod |A | für
i > 0 und Konstanten c1 , c2 , 0.
Bezüglich des Clusterbildung ist die quadratische
Kollisionsbehandlung günstiger als die lineare.
Um die Hashtabelle vollständig auszunutzen, sind c1 und c2
geeignet zu wählen.
Da sich auch hier die Hashwerte von hi zweier Elemente
b , b ′ ∈ B mit h0 (b ) = h0 (b ′ ) im weiteren Verlauf gleichen,
kommt es ebenfalls zu Clustern, wenn auch in abgemilderter
Form (secondary clustering).
Bei der linearen und der quadratischen Kollisionsbehandlung
hängt das weitere Verhalten nicht vom Element ab.
7-57
Doppeltes Hashing
Beim doppelten Hashing werden Hashfunktionen der folgenden
Form verwendet:
◮
◮
ha : B → A , hb : B → A
hi : B → A mit hi (j ) = (ha (j ) + i · hb (j )) für i ≥ 0
◮
Eine der besten Methoden für Closed Hashing, da die Folgen
von Hashwerten nahezu zufällig erscheinen.
◮
Der erste Hashwert eines Elements hängt von nur einer
Hashfunktion ha ab.
◮
Der zweite dann auch von der zweiten Funktion hb .
◮
Dass beide Hashfunktionen für zwei unterschiedliche
Elemente aus B die gleichen Resultate erzeugen, ist
(abhängig von der Wahl von ha und hb ) unwahrscheinlich.
7.3 Hashverfahren
7-58
Doppeltes Hashing
hb (j ) und |A | sollten teilerfremd sein, damit die gesamte
Indexmenge durchsucht wird.
Möglichkeiten:
◮
◮
|A | = 2k und hb (j ) produziert nur ungerade Zahlen.
|A | ist eine Primzahl und ∀j ∈ B : 0 < hb (j ) < |A |
Beispiel (Cormen): ha : B → A mit ha (j ) = j mod |A | und
hb : B → A mit hb (j ) = 1 + (j mod k ) mit k etwas kleiner als |A |.
j = 123456, |A | = 701, k = 700 führt zu ha (j ) = 80, hb (j ) = 257
Der erste untersuchte Feld hat den Index 80, danach wird jedes
257te Feld (modulo 701) untersucht bis alle Felder betrachtet
wurden.
7.3 Hashverfahren
7-59
Closed Hashing
◮
Ist die Hashfunktion bzgl. B identisch gleichverteilt, so sind
1
bei einem Füllfaktor α = |An | < 1 höchstens 1−α
Versuche
notwendig, um zu entscheiden, dass ein Element nicht in M
enthalten ist.
◮
Ist die Hashfunktion bzgl. B identisch gleichverteilt, so sind
1
bei einem Füllfaktor α = |An | < 1 höchstens 1−α
Versuche
notwendig, um ein Element einzufügen.
◮
Ist die Hashfunktion bzgl. B identisch gleichverteilt, so sind
1
bei einem Füllfaktor α = |An | < 1 höchstens α1 ln 1−α
Versuche
notwendig, um ein Element zu finden. (Falls die
Wahrscheinlichkeit, mit der nach einem Element gesucht wird,
für alle Elemente gleich ist.)
Die Beweise dieser Aussagen findet man beispielsweise bei
Cormen et al., S. 242 ff.
7.3 Hashverfahren
7-60
Zusammenfassung
Operation isIn(x ), n = |M | Anzahl der Elemente der Menge
Implementierung
Bitfeld
Liste unsortiert
Liste sortiert
Rot-Schwarz-Baum
Open Hashing
Closed Hashing
7.3 Hashverfahren
Worst Case
O (1)
O (n)
O (n)
O (log(n))
O (n)
O (|A |)
Average Case
O (1)
O (n)
O (n)
O (log n)
O (1 + mn )
1
O ( 1−α
)
7-61
1. Der Algorithmenbegriff
2. Imperative Algorithmen
3. Sortieralgorithmen
4. Listen und abstrakte Datentypen
5. Objektorientierte Algorithmen
6. Bäume
7. Mengen, Verzeichnisse und Hash-Verfahren
8. Graphen
8.1 Mathematische Grundlagen
8.2 Darstellung von Graphen
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8.4 Ausgewählte Algorithmen für gewichtete Graphen
9. Entwurf von Algorithmen
10. Funktionale und deduktive Algorithmen
Einführung
Knoten
Ein Graph besteht aus Knoten (vertices
oder nodes), die durch Kanten (edges)
verbunden sind. Die Kanten können
◮
ungerichtet oder
◮
gerichtet
sein. Die entsprechenden Graphen heißen
ungerichtete bzw. gerichtete Graphen.
Kante
1
1
6
8
3
5
3
2
Gewicht
3
Zyklus
Ungerichteter
gewichteter
zyklischer Graph
Graphen, deren Kanten durch eine Zahl
gewichtet sind, nennt man gewichtete
Graphen.
Graphen können Zyklen enthalten.
Gerichteter
azyklischer Graph
8.1 Mathematische Grundlagen
8-1
Einführung
Knoten können durch eine Kante mit sich selbst verknüpft sein
(Schlinge).
ungerichteter zyklischer Graph mit Schlingen
Knoten können Attribute (zum Beispiel einen Wert oder eine
Farbe) besitzen.
gerichteter azyklischer gefärbter Graph
8.1 Mathematische Grundlagen
8-2
Anwendungsbeispiele
Ungerichtete Graphen:
◮
Kommunikationsnetz
◮
◮
Gewichte als Dauer einer Datenübertragung. Gesucht ist der
schnellste Übertragungsweg von a nach b .
Gewichte als Kosten einer Datenübertragung. Gesucht ist der
günstigste Übertragungsweg von a nach b .
Gerichtete Graphen:
◮
Straßennetz
◮
◮
Gewicht als Länge einer Wegstrecke. Gesucht ist die kürzeste
Strecke von a nach b .
Begriffsmodellierung, semantische Netze
◮
Unterbegriffe: ein „Auto“ ist ein „Fahrzeug“.
8.1 Mathematische Grundlagen
8-3
Anwendungsbeispiele
Gerichtete Graphen:
◮
Kontrollfluss in Programmen
◮
Welche Programmabschnitte werden bei gegebener Eingabe
nicht ausgeführt?
Gerichtete azyklische Graphen:
◮
Stammbäume
◮
◮
Gesucht sind die Vorfahren von x .
Vererbungshierachie in der objektorientierten
Programmierung
8.1 Mathematische Grundlagen
8-4
Gerichtete und ungerichtete Graphen
◮
Ein gerichteter Graph (Digraph) G ist ein Paar (V , E ) mit:
◮
◮
◮
◮
V ist eine endliche Menge (Knoten, Knotenmenge).
E ⊆ V × V ist eine Relation auf V (Kanten, Kantenmenge).
Eine Kante (u, u) ∈ E heißt Schlinge.
Ein ungerichteter Graph G ist ein Paar (V , E ) mit:
◮
◮
◮
◮
◮
V ist eine endliche Menge (Knoten, Knotenmenge).
E ⊆ {{u, v } | u, v ∈ V } ist eine Menge, deren Elemente einoder zweielementige Teilmengen von V sind. (Kanten,
Kantenmenge).
Eine einelementige Teilmenge {u} heißt Schlinge.
E kann als symmetrische Relation E ⊆ V × V angesehen
werden.
Man schreibt häufig (u, v ) ∈ E statt {u, v } ∈ E .
8.1 Mathematische Grundlagen
8-5
Adjazenz
Es sei ein gerichteter oder ungerichteter Graph G = (V , E )
gegeben. Falls (u, v ) ∈ E ist sagt bzw. schreibt man:
◮
(u, v ) tritt aus u aus,
◮
(u, v ) tritt in v ein,
◮
u und v sind adjazent (benachbart),
◮
u → v.
E ist die Adjazenzrelation.
8.1 Mathematische Grundlagen
8-6
Grad eines Knotens
Ungerichteter Graph:
◮
Der Grad eines Knotens ist die Anzahl der mit ihm in Relation
stehenden Knoten.
◮
Ein Knoten mit dem Grad 0 heißt isoliert.
Gerichteter Graph:
◮
Der Ausgangsgrad eines Knotens ist die Anzahl seiner
austretenden Kanten.
◮
Der Eingangsgrad eines Knotens ist die Anzahl seiner
eintretenden Kanten.
◮
Der Grad eines Knotens ist die Summe aus Ausgangs- und
Eingangsgrad.
8.1 Mathematische Grundlagen
8-7
Pfade
Es sei ein Graph G = (V , E ) gegeben.
◮
◮
Ein Pfad p der Länge k in G von u ∈ V zu u′ ∈ V ist eine
Folge p = (v0 , v1 , . . . , vk ) von Knoten mit u = v0 , u′ = vk und
(vi −1 , vi ) ∈ E , i = 1, . . . k .
Der Pfad p enthält die Knoten v0 , v1 , . . . , vk und die Kanten
(v0 , v1 ),. . . ,(vk −1 , vk ).
◮
Wenn es einen Pfad p von u ∈ V zu u′ ∈ V gibt, heißt u′ von u
über p erreichbar.
◮
Ein Pfad heißt einfach, wenn alle Knoten verschieden sind.
◮
Ein Teilpfad eines Pfads p = (v0 , v1 , . . . , vk ) ist eine Teilfolge
benachbarter Knoten.
8.1 Mathematische Grundlagen
8-8
Zyklen
◮
Ein Pfad p = (v0 , v1 , . . . , vk ) heißt Zyklus, wenn v0 = vk und
k > 0 ist.
◮
Ein Zyklus ist einfach, wenn seine Knoten paarweise
verschieden sind.
◮
Ein Graph ohne Schlingen wird einfach genannt.
◮
Ein Graph ohne Zyklen wird als azyklisch bezeichnet.
8.1 Mathematische Grundlagen
8-9
Zusammenhang
◮
Ein ungerichteter Graph heißt zusammenhängend, wenn
jedes Knotenpaar durch einen Pfad verbunden ist.
◮
Die Zusammenhangskomponenten eines ungerichteten
Graphen sind die Äquivalenzklassen bezüglich der
Äquivalenzrelation „ist erreichbar von“.
◮
Ein gerichteter Graph heißt stark zusammenhängend, wenn
jeder Knoten von jedem anderen Knoten aus erreichbar ist.
◮
Die starken Zusammenhangskomponenten eines gerichteten
Graphen sind die Äquivalenzklassen bezüglich der
Äquivalenzrelation „sind gegenseitig erreichbar“.
8.1 Mathematische Grundlagen
8-10
Teilgraphen und Isomorphie
◮
Ein Graph G ′ = (V ′ , E ′ ) ist ein Teilgraph von G = (V , E ), falls
V ′ ⊆ V und E ′ ⊆ E gilt.
◮
Ist eine Teilmenge V ′ ⊆ V gegeben, dann ist der durch V ′
induzierte Teilgraph Graph G ′ = (V ′ , E ′ ) von G = (V , E )
durch
E ′ = {(u, v ) ∈ E | u, v ∈ V ′ }.
bestimmt.
◮
Zwei Graphen G = (V , E ) und G ′ = (V ′ , E ′ ) sind isomorph,
wenn es eine bijektive Abbildung f : V → V ′ mit
(u, v ) ∈ E ⇔ (f (u), f (v )) ∈ E ′
gibt.
8.1 Mathematische Grundlagen
8-11
Spezielle Graphen
◮
Ein vollständiger Graph ist ein Graph, in dem jedes
Knotenpaar benachbart ist:
u, v ∈ V ⇒ (u, v ) ∈ E
◮
Ein bipartiter Graph ist ein Graph, in dem die Knotenmenge V
in zwei Teilmengen V1 und V2 zerlegt werden kann, dass alle
Kanten zwischen V1 und V2 verlaufen:
(u, v ) ∈ E ⇒ (u ∈ V1 , v ∈ V2 ) ∨ (u ∈ V2 , v ∈ V1 )
8.1 Mathematische Grundlagen
8-12
Bäume
◮
Ein Wald ist ein azyklischer, ungerichteter Graph.
◮
Ein (freier) Baum ist ein zusammenhängender Wald.
◮
Ein (gerichteter) Baum ist ein freier Baum, in dem einer der
Knoten vor den anderen ausgzeichnet ist. Dieser Knoten
heißt Wurzel.
◮
In einem geordneten Baum sind die Kinder jedes Knotens
geordnet.
Mit diesen Definitionen, können wir die Begriffe Kind, Vater,
Vorfahre, Höhe, Tiefe, binärer Baum, k -närer Baum, . . .
graphentheoretisch interpretieren.
8.1 Mathematische Grundlagen
8-13
Gewichtete Graphen
◮
Ein gewichteter Graph G = (V , E , w ) besteht aus einem
Graphen (V , E ) und einer Gewichtsfunktion
w : E → R,
die jeder Kante e ∈ E eine reelle Zahl w (e ) als Gewicht
zuordnet.
◮
Das Gewicht w (p ) eines Pfads p = (v0 , v1 , . . . , vk ) ist die
Summe der einzelnen Kantengewichte:
w (p ) :=
k −1
X
w ((vi , vi +1 ))
i =0
8.1 Mathematische Grundlagen
8-14
Kürzeste Pfade
◮
Ungewichtete Graphen: Ein Pfad minimaler Länge zwischen
zwei Knoten heißt kürzester Pfad zwischen diesen Knoten.
◮
Gewichtete Graphen: Ein Pfad minimalen Gewichts zwischen
zwei Knoten heißt kürzester Pfad zwischen diesen Knoten.
◮
Die Länge bzw. das Gewicht des kürzesten Pfades zwischen
zwei Knoten ist die Distanz der beiden Knoten.
◮
Kürzeste Pfade müssen nicht existieren (Beispiel: es existiert
ein Zyklus mit negativem Gewicht, der beliebig oft durchlaufen
werden kann).
◮
Kürzeste Pfade sind im Allgemeinen nicht eindeutig bestimmt.
8.1 Mathematische Grundlagen
8-15
Erweiterungen
◮
Ein Multigraph ist ein Graph, der Mehrfachkanten enthalten
kann.
◮
Eine Kante in einem Hypergraphen kann mehr als zwei
Knoten verbinden.
8.1 Mathematische Grundlagen
8-16
Nützliche Funktionen
Bei gerichteten Graphen:
◮
◮
◮
◮
Ausgangskanten: ak : V → P(E ),
ak (u) = {(u, v ) | (u, v ) ∈ E }
Eingangskanten: ek : V → P(E ),
ek (u) = {(v , u) | (v , u) ∈ E }
Ausgangsgrad: ag : V → N0 ,
ag (u) = |ak (u)|
Eingangsgrad: eg : V → N0 ,
eg (u) = |ek (u)|
8.2 Darstellung von Graphen
f
e
g
c
a
b
d
ak (a ) = {(a , g ), (a , b )}
ek (g ) = {(a , g ), (b , g ), (f , g )}
8-17
Nützliche Funktionen
◮
◮
Nachfolgerknoten:
nk : V → P(V ),
nk (u) = {v | (u, v ) ∈ E }
Vorgängerknoten: vk : V → P(V ),
vk (u) = {v | (v , u) ∈ E }
8.2 Darstellung von Graphen
f
e
g
c
a
b
d
nk (a ) = {g , b }
8-18
Möglichkeiten zur Speicherung von Graphen
◮
Kantenlisten
◮
Knotenlisten
◮
Adjazenzmatrizen
◮
Adjazenzlisten
8.2 Darstellung von Graphen
8-19
Kantenlisten
◮
◮
◮
◮
◮
Nummerierung der Knoten von 1 bis
|V | = n
Speicherung: |V |, |E |, Paare (a , b ) mit
(a , b ) ∈ E . Es werden 2 + |E | ∗ 2
Werte unsortiert gespeichert.
Einfügen von Kanten und Knoten:
O (1)
Löschen von Kanten erfordert ein
Durchsuchen der Liste: O (|E |)
6
5
7
3
1
2
4
Kantenliste: 7, 9, 1, 2, 1, 7, 2, 7,
3, 2, 3, 4, 3, 5, 4, 5, 5, 6, 6, 7
Löschen von Knoten erfordert ein
erneutes Nummerieren der Knoten
und ggf. Löschen von Kanten: O (|E |)
8.2 Darstellung von Graphen
8-20
Knotenliste
◮
◮
Nummerierung der Knoten von 1 bis
|V | = n
Speicherung: |V |, |E |, (ag (v ), nk (v ))
mit v ∈ V aufsteigend sortiert. Es
werden 2 + |V | + |E | Werte
gespeichert.
◮
Einfügen von Knoten: O (1)
◮
Einfügen und Löschen von Kanten
erfordert ein Durchsuchen der Liste:
O (|E | + |V |)
◮
6
5
7
3
1
2
4
Knotenliste: 7, 9, 2, 2, 7, 1, 7, 3,
2, 4, 5, 1, 5, 1, 6, 1, 7, 0
Löschen von Knoten erfordert ein
erneutes Nummerieren der Knoten
und ggf. Löschen von Kanten:
O (|E | + |V |)
8.2 Darstellung von Graphen
8-21
Adjazenzmatrizen
Ein Graph G = (V , E ) mit |V | = n wird als quadratische
n × n-Matrix a von booleschen Werten gespeichert. Es gilt
a [i , j ] = true ⇔ (i , j ) ∈ E .
Beispiel für einen gerichteten Graphen:
1
3
2
4


 1 1 0 1 
 0 0 0 1 


 0 0 1 1 


0 0 0 0
Bei ungerichteten Graphen braucht aus Symmetriegründen nur
eine Hälfte der Matrix gespeichert zu werden.
8.2 Darstellung von Graphen
8-22
Adjazenzliste
◮
◮
Nummerierung der Knoten von 1 bis |V |.
Implementierung durch |V | + 1 Listen:
◮
◮
Basisliste: Liste aller Knoten des Graphen.
Pro Knoten: Liste der Nachfolger des Knotens.
6
7
5
1
2
2
7
7
4
3
···
7
1
8.2 Darstellung von Graphen
2
8-23
Vergleich der Implementierungen
Es sei |V | = n, |E | = m.
Speicherbedarf
Kante Einfügen
Kante Löschen
Knoten Einfügen
Knoten Löschen
Kantenliste
Knotenliste
Adjazenzmatrix
Adjazenzliste
O(m)
O (1)
O (m)
O (1)
O (m)
O (n + m)
O (n + m)
O (n + m)
O (1)
O (n + m)
O (n2 )
O (1)
O (1)
O (n2 )
O (n2 )
O (n + m)
O (n)∗
O (n + m)∗
O (1)
O (n + m)
∗)
8.2 Darstellung von Graphen
für die hier gegebene Implementierung
8-24
Übersicht
In diesem Abschnitt wollen wir beispielhaft einige Algorithmen für
ungewichtete Graphen vorstellen.
◮
Systematisches Durchsuchen eines Graphen
◮
◮
Breitensuche (breadth-first search)
Tiefensuche (depth-first search)
◮
Zyklenfreiheit
◮
Topologisches Sortieren
◮
Erreichbarkeit
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-25
Breitensuche
Besuch aller Knoten eines Graphen G = (V , E ), die von einem
Startknoten s erreichbar sind.
◮
Es wird von s ausgegangen.
◮
Zuerst werden alle von s über eine Kante erreichbaren
Knoten besucht.
◮
Dann alle über zwei Kanten erreichbaren Knoten.
◮
usw.
2. Iteration
Startknoten
1. Iteration
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-26
Breitensuche
◮
Iterative Kontrollstruktur
◮
Schlange Q zur Speicherung der gerade bearbeiteten Knoten
in Reihenfolge der Iterationsschritte
Drei Verzeichnisse (Abbildungen, Dictionaries)
◮ d : V → N0 bildet jeden Knoten auf seine Entfernung vom
◮
◮
◮
Startknoten ab.
p : V → V bildet jeden Knoten auf den Vorgängerknoten ab,
von dem ausgehend er erreicht worden ist. p ergibt nach der
Abarbeitung einen Breitensuchbaum.
c : V → {weiß, schwarz, grau} ordnet jedem Knoten eine
Farbe abhängig von seinem Bearbeitungszustand zu:
◮
◮
◮
weiß: noch unentdeckt,
grau: Entfernung bereits bestimmt,
schwarz: abgearbeitet.
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-27
Breitensuche
proc BFS(G,s) begin
end
foreach u ∈ V \ {s} do
c(u) ← weiß; d(u) ← ∞; p(u) ← nil od;
c(s) ← grau; d(s) ← 0; p(s) ← nil;
Q ← empty;
enter(Q,s);
while! isempty(Q) do
u ← front(Q); leave(Q);
foreach v ∈ nk(u) do
if c(v) = weiß then
c(v) ← grau; d(v) ← d(u)+1;
p(v) ← u; enter(Q,v) fi; od;
c(u) ← schwarz; od;
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-28
Breitensuche
r
s
v
w
t
u
r
s
x
y
v
w
q: s
t
u
r
s
t
u
x
y
v
w
x
q: r, t, x
y
q: w, r
r
s
t
u
r
s
t
u
r
s
t
u
v
w
x
q: t, x, v
y
v
w
x
q: x, v, u
y
v
w
x
q: v, u, y
y
r
s
t
u
r
s
t
u
r
s
t
u
v
w
x
y
v
w
x
y
v
w
x
y
q: u, y
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
q: y
q: leer
8-29
Breitensuche
Satz: Es sei G = (V , E ) ein gerichteter oder ungerichteter Graph,
auf dem die Prozedur BFS für einen Startknoten s ∈ V ausgeführt
wird.
1. Die Prozedur entdeckt jeden Knoten v ∈ V , der von s aus
erreichbar ist. Bei der Terminierung ist d (v ) gleich der Distanz
von v von s für alle v ∈ V .
2. Die Laufzeit der Breitensuche liegt in O (|V | + |E |), das heißt,
die Laufzeit ist linear in der Größe der Adjazenzliste.
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-30
Breitensuche
Der implizit über das Verzeichnis p erzeugte Breitensuchbaum ist
r
s
t
u
v
w
x
y
Die Pfade von jedem Knoten in diesem Baum zum Startknoten
entsprechen kürzesten Pfaden in G .
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-31
Tiefensuche
Besuch aller Knoten eines Graphen G = (V , E ).
◮
Es wird von einem Startknoten s ausgegangen.
◮
Es wird rekursiv so weit wie möglich auf einem Pfad
vorangeschritten.
◮
Danach wird zu einer Verzweigung mit einem noch nicht
besuchten Knoten zurückgegangen (Backtracking).
1
a
14
j
13
11
10
i
6
2
b
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
d
12 3
16 15
f
e
9
7
h
c
4
8
g
5
8-32
Tiefensuche
◮
Rekursive Kontrollstruktur
◮
Prozeduren DFS(G) und DFS-visit(u)
◮
Zeitstempel
Vier Verzeichnisse (Abbildungen, Dictionaries)
◮ d : V → N0 Beginn der Bearbeitung eines Knotens
◮ f : V → N0 Ende der Bearbeitung eines Knotens
◮ p : V → V Vorgängerknoten
◮ c : V → {weiß, schwarz, grau} wie oben
◮
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-33
Tiefensuche
proc DFS(G) begin
foreach u ∈ V do
c(u) ← weiß; p(u) ← nil od;
zeit ← 0;
foreach u ∈ V do
if c(u) = weiß then
DFS-visit(u) fi; od;
end
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-34
Tiefensuche
proc DFS-visit(u): begin
c(u) ← grau;
zeit ← zeit+1;
d(u) ← zeit;
foreach v ∈ nk(u) do
if c(v) = weiß then
p(v) ← u;
DFS-visit(v) fi; od;
c(u) ← schwarz;
zeit ← zeit+1;
f(u) ← zeit;
end
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-35
Tiefensuche
1/
u
/
v
/
w
1/
u
2/
v
/
w
1/
u
2/
v
/
w
1/
u
2/
v
/
w
x
/
y
/
z
/
x
/
y
/
z
/
x
/
y
3/
z
/
x
4/
y
3/
z
/
1/
u
2/
v
/
w
1/
u
2/
v
/
w
1/
u
2/
v
/
w
1/
u
2/7
v
/
w
y
3/6
z
/
B
x
4/
B
y
3/
z
/
x
4/5
B
y
3/
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
z
/
x
4/5
B
y
3/6
z
/
x
4/5
8-36
Tiefensuche
1/
u
2/7
v
/
w
FB
1/8
u
2/7
v
1/8
u
/
w
FB
2/7
v
9/
w
FB
1/8
u
2/7
v
FB
9/
w
C
x
4/5
y
3/6
z
/
x
4/5
y
3/6
z
/
x
4/5
y
3/6
z
/
x
4/5
y
3/6
z
/
1/8
u
2/7
v
9/
w
1/8
u
2/7
v
9/
w
1/8
u
2/7
v
9/
w
1/8
u
2/7
v
9/12
w
FB
x
4/5
FB
C
y
3/6
z
10/
x
4/5
FB
C
y
3/6
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
z
10/
B
x
4/5
C
y
3/6
z B
10/11
FB
x
4/5
C
y
3/6
z B
10/11
8-37
Tiefensuche
Der implizit über das Verzeichnis p erzeugte Tiefensuchwald ist
1/8
u
2/7
v
FB
x
4/5
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
9/12
w
C
y
3/6
z B
10/11
8-38
Tiefensuche
Klassifikation der Kanten:
1. Baumkanten sind Kanten im Tiefensuchwald.
2. Rückwärtskanten B sind Kanten, die einen Knoten mit einem
Vorfahren im Tiefensuchwald verbinden.
3. Vorwärtskanten F sind diejenigen Nichtbaumkanten, die einen
Knoten mit einem Nachfahren im Tiefensuchwald verbinden.
4. Querkanten C sind alle übrigen Kanten. Sie können zwischen
verschiedenen Tiefensuchbäume verlaufen.
Bei einer Tiefensuche auf einem ungerichteten Graphen ist jede
Kante eine Baumkante oder eine Rückwärtskante.
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-39
Zyklenfreiheit
Mithilfe der Tiefensuche können Zyklen in gerichteten Graphen
ermittelt werden. Es gilt:
Ein gerichteter Graph G ist genau dann azyklisch, wenn der
Tiefensuchalgorithmus auf G keine Rückwärtskanten liefert.
Beweis: s. Cormen et al., Seite 554.
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-40
Topologisches Sortieren
◮
Gegeben sei ein gerichteter azyklischer Graph G = (V , E ).
Solche Graphen werden als DAG (directed acyclic graph)
bezeichnet.
◮
Eine topologische Sortierung von G ist eine lineare
Anordnung seiner Knoten mit der Eigenschaft, dass u vor v
liegt, wenn es einen Pfad von u nach v gibt.
◮
Gesucht ist ein Algorithmus, der zu einem DAG G = (V , E )
eine topologische Sortierung seiner Knotenmenge V
berechnet.
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-41
Topologisches Sortieren
Beispiel: Gesucht ist die Reihenfolge beim Ankleiden. Nach
Festlegung der Reihenfolge einzelner Kleidungsstücke entsteht
folgender Graph:
Socken
Unterhose
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
Schuhe
Hose
Hemd
Gu
¨rtel
Fliege
Uhr
Jacke
8-42
Topologisches Sortieren
Mit Tiefensuche kann für jeden Knoten die Endzeit seiner
Bearbeitung bestimmt werden. Sie ergibt eine topologische
Sortierung.
Ist die Bearbeitung eines
Knotens abgeschlossen, so
wird er am Kopf der zu Beginn
leeren Ergebnisliste eingefügt.
Die Ergebnisliste gibt die
Sortierung an.
Reihenfolge: Socken (18),
Unterhose (16), Hose (15),
Schuhe (14), Uhr (10), Hemd
(8), Gürtel (7), Fliege (5),
Jacke (4)
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
Socken
17/18
Unterhose
11/16
Schuhe
13/14
Hose
12/15
Hemd
1/8
G¨
urtel
6/7
Fliege
2/5
Uhr
9/10
Jacke
3/4
8-43
Floyd-Warshall-Algorithmus
Es soll die reflexive, transitive Hülle einer Relation bestimmt
werden. Dieses Problem entspricht dem Erreichbarkeitsproblem in
einem Graphen.


 0 1 0 0 
 0 0 0 1 


 0 0 0 1 


0 0 0 0
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
1
2
3
4


 1 1 0 1 
 0 1 0 1 


 0 0 1 1 


0 0 0 1
8-44
Floyd-Warshall-Algorithmus
Es sei V = {1, . . . , n}. Die Relation E ⊆ V × V liege als
Adjazenzmatrix r vor. Für 1 ≤ i , j ≤ n sei



true
r [i , j ] = 

false
falls
sonst
(i , j ) ∈ E
func FloyWars(r: bool [n,n]): bool [n,n] begin
var i,j,k: int;
for i ← 1 to n do r[i,i] ← true; od;
for k ← 1 to n do
for i ← 1 to n do
for j ← 1 to n do
r[i,j] ← r[i,j] ∨
(r[i,k] ∧ r[k,j]);
od; od; od;
return r;
end
8.3 Ausgewählte Algorithmen für ungewichtete Graphen
8-45
Übersicht
In diesem Abschnitt wollen wir beispielhaft einige Algorithmen für
gewichtete Graphen vorstellen.
◮
Minimale Spannbäume
◮
◮
◮
Algorithmus von Kruskal
Algorithmus von Prim
Kürzeste Pfade von einem Startknoten
◮
◮
Algorithmus von Dijkstra
Bellmann-Ford-Algorithmus
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-46
Minimale Spannbäume
◮
◮
Gegeben sei ein gewichteter Graph G = (V , E , w ).
Gesucht ist eine azyklische Teilmenge T ⊆ E , die alle Knoten
verbindet und deren Gesamtgewicht
w (T ) =
X
w (e )
e ∈T
◮
◮
minimal ist.
Eine Kantenmenge, die azyklisch ist und alle Knoten
verbindet, ist ein Baum, der Spannbaum genannt wird.
Es ist also ein minimaler Spannbaum gesucht.
4
3
3
3
2 6
8
6
4
◮
5
5
7
6
2
Dieser Baum ist im Allgemeinen nicht eindeutig bestimmt.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-47
Basisalgorithmus
◮
Der Algorithmus verwaltet eine Kantenmenge A , die den
minimalen Spannbaum Kante für Kante aufbaut.
◮
A ist stets Teilmenge eines minimalen Spannbaums.
◮
Eine Kante e ∈ E , die zu A hinzugefügt werden kann, ohne
die Eigenschaft zu verletzen, dass A Teilmenge eines
minimalen Spannbaums ist, heißt sichere Kante für A .
proc MST-Basis(G) begin
A ← ∅;
while A bildet keinen Spannbaum do
bestimme eine Kante e ∈ E ,
die sicher für A ist;
A ← A ∪ {e};
od;
return A;
end
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-48
Basisalgorithmus
Satz: Es sei G = (V , E ) ein zusammenhängender, gewichteter
Graph. A sei eine Teilmenge eines minimalen Spannbaums und
C = (VC , EC ) eine Zusammenhangskomponente aus dem Wald
GA = (V , A ). Dann gilt: Falls e ∈ E eine Kante mit minimalen
Gewicht ist, die C mit einer anderen Komponente von GA
verbindet, dann ist e sicher für A .
Beweis: s. Cormen, S. 569 f.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-49
Operationen für disjunkte Mengen
◮
MakeSet(x)
erzeugt die einelementige Menge {x }. x darf nicht bereits in
einer anderen Menge enthalten sein.
◮
Union(x,y)
bildet die Vereinigungsmenge x ∪ y . Es wird x ∩ y = ∅
vorausgesetzt.
◮
FindSet(x)
liefert einen Zeiger auf den Repäsentanten der eindeutig
bestimmten Menge, die x enthält.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-50
Algorithmus von Kruskal
◮
Selektiere fortwährend eine verbleibende Kante mit
geringstem Gewicht, die keinen Zyklus erzeugt, bis alle
Knoten verbunden sind (Kruskal, 1956).
◮
Eine eindeutige Lösung ist immer dann vorhanden, wenn alle
Gewichte verschieden sind.
4
3
8
3
2 6
6
4
3
5
5
7
6
2
Nach Wahl der Kanten 2, 2, 3 und 3 darf die verbleibende 3 nicht
gewählt werden, da sonst ein Zyklus entstünde.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-51
Algorithmus von Kruskal
proc MST-Kruskal(G) begin
A ← ∅;
foreach v ∈ V do MakeSet(v) od;
sortiere die Kanten aufsteigend nach
ihrem Gewicht;
foreach (u,v) ∈ E do
if FindSet(u) , FindSet(v) then
A ← A ∪ {(u,v)}
UnionSet(FindSet(u), FindSet(v));
fi;
od;
return A;
end
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-52
Algorithmus von Kruskal
◮
A ist zu jedem Zeitpunkt ein Wald, dessen Komponenten
nach und nach zu einem minimalen Spannbaum verbunden
werden.
◮
Die Laufzeit hängt von der Implementierung der disjunkten
Mengen ab.
◮
Bei einer geeigneten Realisierung der disjunkten Mengen
liegt die Laufzeit des Algorithmus von Kruskal in O (|E | log |V |).
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-53
Algorithmus von Prim
◮
Beim Algorithmus von Prim bildet die Kantenmenge A stets
einen Baum.
◮
Der Baum startet bei einem beliebigen Wurzelknoten und
wächst, bis er V aufspannt.
◮
In jedem Schritt wird eine Kante hinzugefügt, die A mit einem
isolierten Knoten von GA = (V , A ) verbindet und die
bezüglich dieser Eigenschaft minimal ist.
◮
Der Algorithmus verwendet zur Verwaltung der Knoten eine
Min-Prioritätswarteschlange Q , die auf einem Attribut
schlüssel basiert. Für jeden Knoten v ist schlüssel(v) das
kleinste Gewicht aller Kanten, die v mit einem Knoten des
Baums verbinden.
◮
p (v ) bezeichnet den Vater von v .
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-54
Algorithmus von Prim
proc MST-Prim(G,r) begin
foreach u ∈ V do
schlüssel(u) ← ∞; p(u) ← nil; od;
schlüssel(r) ← 0;
Q ← V;
while Q , ∅ do
u ← ExtractMin(Q);
foreach v ∈ nk(u) do
if v ∈ Q und w(u,v) < schlüssel(v)
then p(v) ← u;
schlüssel(v) ← w(u,v); fi;
od;
od;
end
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-55
Algorithmus von Prim
◮
Die Laufzeit des Algorithmus von Prim hängt von der
Implementierung der Min-Prioritätswarteschlange Q ab.
◮
Die Anweisung schlüssel(v) ← w(u,v) ist beispielsweise
eine Decrease-Operation.
◮
Wenn Q als binärer Min-Heap realisiert wird, liegt die Laufzeit
des Algorithmus von Prim in O (|E | log |V |). Dies entspricht der
Laufzeit des Algorithmus von Kruskal.
◮
Durch Verwendung von so genannten Fibonacci-Heaps kann
die Laufzeit des Algorithmus von Prim auf O (|E | + |V | log |V |)
verbessert werden.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-56
Problem der kürzesten Pfade bei einem Startknoten
◮
◮
Gegeben ist ein gewichteter Graph G = (V , E , w ) und ein
Startknoten s ∈ V .
Gesucht ist für jeden Knoten v ∈ V ein Pfad p = (v0 , . . . , vk )
von s = v0 nach v = vk , dessen Gewicht
w (p ) =
k
X
i =1
◮
w (vi −1 , vi )
minimal wird. Falls kein Pfad von s nach v existiert, sei das
Gewicht ∞.
Die Gewichte können im Allgemeinen negativ sein.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-57
Problem der kürzesten Pfade bei einem Startknoten
Beispiel: Bestimme den kürzesten Weg von Frankfurt nach Celle
Augsburg
3
2
9
9
4
Frankfurt
Braunschweig
9
1
8
Erfurt
2
3
2
8
Celle
9
3
6
Darmstadt
Der kürzester Weg ist (Frankfurt, Augsburg, Braunschweig, Celle).
Er hat das Gewicht 6.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-58
Bellmann-Ford-Algorithmus
◮
Der Algorithmus verwendet so genannte Relaxationen und
bestimmt so immer kleiner werdende Schätzungen d (v ) für
das Gewicht eines kürzesten Pfads vom Startknoten s aus zu
allen Knoten v ∈ V , bis er das tatsächliche Gewicht erreicht
hat.
◮
Der Algorithmus gibt genau dann wahr zurück, wenn der
Graph keine Zyklen mit negativem Gewicht enthält, die von s
aus erreichbar sind.
◮
p (v ) ist wie bisher der Vaterknoten von v .
◮
Der Algorithmus führt |V | − 1 Durchläufe über die Kanten des
Graphen aus.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-59
Bellmann-Ford-Algorithmus
Initialisierung:
proc Init(G,s) begin
foreach v ∈ V do
d(v) ← ∞;
p(v) ← nil; od;
d(s) ← 0;
end
Relaxation:
proc Relax(u,v,w) begin
if d(v) > d(u) + w(u,v)
then d(v) ← d(u) + w(u,v);
p(v) ← u; fi;
end
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-60
Bellmann-Ford-Algorithmus
proc Bellmann-Ford(G, s) boolean begin
Init(G,s);
for i ← 1 to |V| - 1 do
foreach (u,v) ∈ E do
Relax(u,v,w); od; od;
foreach (u,v) ∈ E do
if d(v) > d(u) + w(u,v)
then return false; fi; od;
return true;
end
Die Laufzeit des Bellmann-Ford-Algorithmus liegt in O (|E | · |V |).
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-61
Algorithmus von Dijkstra
◮
Der Algorithmus von Dijkstra löst das Problem der kürzesten
Pfade bei einem Startknoten, falls alle Gewichte nichtnegativ
sind.
◮
Wir setzen daher w (e ) ≥ 0 für alle e ∈ E voraus.
◮
Die Laufzeit des Dijkstra-Algorithmus ist bei guter
Implementierung besser als die des
Bellmann-Ford-Algorithmus.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-62
Algorithmus von Dijkstra
◮
Der Dijkstra-Algorithmus verwaltet eine Menge S von Knoten,
deren endgültige Gewichte der kürzesten Pfade vom
Startknoten aus bereits bestimmt wurden.
◮
Der Algorithmus wählt in jedem Schritt denjenigen Knoten
u ∈ V \ S mit der kleinsten Schätzung des kürzesten Pfads
aus, fügt u zu S hinzu und relaxiert alle aus u austretenden
Kanten
◮
In der Implementierung wird eine Min-Prioritätswarteschlange
Q für Knoten verwendet. Dabei dienen die d -Werte als
Schlüssel.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-63
Algorithmus von Dijkstra
proc Dijkstra(G, s) begin
Init(G,s);
S ← ∅;
Q ← V;
while Q , ∅ do
u ← ExtractMin(Q);
S ← S ∪ {u};
foreach v ∈ nk(u)
Relax(u,v,w); od; od;
end
Die Laufzeit des Dijkstra-Algorithmus hängt von der
Implementierung der Min-Prioritätswarteschlange Q ab. Bei guter
Implementierung von Q liegt die Laufzeit des Dijkstra-Algorithmus
in O (|E | + |V | · log |V |).
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-64
Algorithmus von Dijkstra
t
∞
x
∞
1
t
10
3
2
5
9
6
4
s
0
3
5
y
Q = [s,t,x,y,z]
t
8
2
6
4
s
0
5
5
y
5
9
6
4
2
7
z
Q = [t,x]
8.4 Ausgewählte Algorithmen für gewichtete Graphen
x
9
1
10
3
2
5
7
5
y
s
0
7
z
2
t
8
10
2
4
Q = [z,t,x]
x
9
1
6
7
∞
z
2
t
8
10
3
2 9
3
Q = [y,t,x,z]
x
13
1
9
7
∞
z
2
x
14
1
10
5
7
∞
y
s
0
t
8
10
10
s
0
x
∞
1
9
6
4
2
Q = [x]
3
2
5
7
5
y
s
0
7
z
9
6
4
7
5
y
2
7
z
Q = []
8-65
Ausblick
Die Graphentheorie ist ein umfangreiches Gebiet, in dem viele
weitere Fragestellungen untersucht werden. Wir stellen drei davon
kurz vor:
◮
Problem des Handlungsreisenden,
◮
planare Graphen,
◮
Färbungen von Graphen.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-66
Problem des Handlungsreisenden
◮
◮
Gegeben seien n durch Straßen verbundene Städte mit
Reisekosten c (i , j ) zwischen je zwei Städten i und j ,
1 ≤ i , j ≤ n.
Gesucht ist die billigste Rundreise, die jede Stadt genau
einmal besucht (Traveling Salesman Problem, TSP).
Augsburg
3
2
9
9
4
Frankfurt
Braunschweig
9
1
8
Erfurt
2
3
2
8
Celle
9
3
6
Darmstadt
Die billigste Rundreise kostet 13 Einheiten.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-67
Planare Graphen
◮
Gegeben sei ein beliebiger Graph G . Lässt sich G planar
zeichnen, das heißt, ohne sich schneidende Kanten?
◮
Im Beispiel unten ist dies möglich, im Allgemeinen jedoch
nicht.
◮
Anwendung: Chip- oder Leiterplattendesign. Leiterbahnen
sollen möglichst kreuzungsfrei gestaltet werden.
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-68
Färbungen von Graphen
◮
◮
◮
Gegeben sei ein Graph G . Die Knoten von G sollen derart
gefärbt werden, dass benachbarte Knoten verschiedene
Farben besitzen. Wie viele Farben werden benötigt?
Im Beispiel unten reichen bereits drei Farben. Für planare
Graphen werden im Allgemeinen vier Farben benötigt. Dieses
Ergebnis wurde 1976 von K. Appel und W. Haken gezeigt
(Vierfarbenproblem). Der Beweis war sehr umfangreich und
computergestützt.
Anwendungen: Einfärben von Landkarten (Knoten , Land,
Kante , Grenze), Vergabe überschneidungsfreier
Klausurtermine (Knoten , Fach, Kante , beide Fächer
werden vom gleichen Studenten gehört, Farbe , Termin)
8.4 Ausgewählte Algorithmen für gewichtete Graphen
8-69
1. Der Algorithmenbegriff
2. Imperative Algorithmen
3. Sortieralgorithmen
4. Listen und abstrakte Datentypen
5. Objektorientierte Algorithmen
6. Bäume
7. Mengen, Verzeichnisse und Hash-Verfahren
8. Graphen
9. Entwurf von Algorithmen
9.1 Einführung
9.2 Teile-und-Beherrsche-Algorithmen
9.3 Gierige Algorithmen
9.4 Backtracking-Algorithmen
9.5 Dynamische Programmierung
10. Funktionale und deduktive Algorithmen
Entwurf von Algorithmen
In diesem Kapitel stellen wir anhand von Beispielen einige
typische Prinzipien für den Entwurf von Algorithmen vor.
Die folgenden Techniken haben wir (implizit oder explizit) bereits
kennen gelernt.
9.1 Einführung
◮
Schrittweise Verfeinerung des Problems
◮
Reduzierung der Problemgröße durch Rekursion
◮
Einsatz von Algorithmenmustern
9-1
Schrittweise Verfeinerung des Problems
9.1 Einführung
◮
Die erste Formulierung des Problems erfolgt in einem sehr
abstrakten Pseudocode.
◮
Die schrittweise Verfeinerung basiert auf dem Ersetzen von
Pseudocode durch verfeinerten Pseudocode
◮
und letztlich durch konkrete Algorithmenschritte.
9-2
Problemreduzierung durch Rekursion
9.1 Einführung
◮
Diese Technik kann angewendet werden, wenn das Problem
auf ein gleichartiges, aber kleineres Problem zurückgeführt
werden kann.
◮
Die Rekursion muss schließlich auf ein oder mehrere kleine
Probleme führen, die sich direkt lösen lassen.
◮
Rekursion bietet sich an, wenn die Problemstruktur rekursiv
aufgebaut ist. Beispiele: Listen, Bäume.
◮
Zu rekursiven Lösungen gibt es iterative Entsprechungen
(zum Beispiel durch Einsatz eines Kellers, s. Aufgabe 23). Bei
der Auswahl zwischen iterativer und rekursiver Lösung ist die
Effizienz der Realisierung zu berücksichtigen.
9-3
Einsatz von Algorithmenmustern
Beispiele für Algorithmenmuster:
◮
Inkrementelle Vorgehensweise
◮
Teile-und-Beherrsche-Algorithmen
◮
Gierige Algorithmen (Greedy Algorithmen)
◮
Backtracking-Algorithmen
◮
Dynamische Programmierung
Die Zuordnung eines Musters zu einem Algorithmus ist nicht
immer eindeutig – und manchmal sogar unmöglich.
Beispielsweise kann der Algorithmus von Kruskal als
inkrementeller und als gieriger Algorithmus gesehen werden.
Es gibt weitere Algorithmenmuster.
9.1 Einführung
9-4
Inkrementelle Vorgehensweise
Beispiel: Sortieren durch Einfügen benutzt eine inkrementelle
Herangehensweise. Nachdem das Teilfeld a [1..j − 1] sortiert
wurde, wird das Element a [j ] an der richtigen Stelle eingefügt,
woraus sich das sortierte Teilfeld a [1..j ] ergibt.
Weitere Beispiele:
◮
Algorithmus von Kruskal
◮
Algorithmus von Prim
Beide Algorithmen bauen schrittweise einen minimalen
Spannbaum auf.
9.1 Einführung
9-5
Teile-und-Beherrsche-Algorithmen
◮
Teile das Problem in eine Anzahl von Teilproblemen auf.
◮
Beherrsche die Teilprobleme durch rekusives Lösen. Wenn
die Teilprobleme hinreichend klein sind, dann löse sie auf
direktem Wege.
◮
Verbinde die Lösungen der Teilprobleme zur Lösung des
Ausgangsproblems.
9.2 Teile-und-Beherrsche-Algorithmen
9-6
Beispiel: Sortieren durch Mischen
Sortieren durch Mischen (Mergesort, vgl. Abschnitt 3.2) arbeitet
rekursiv nach folgendem Schema:
1. Teile die Folge in zwei Teilfolgen auf.
2. Sortiere die beiden Teilfolgen.
3. Mische die sortierten Teilfolgen.
9.2 Teile-und-Beherrsche-Algorithmen
4
2
9
4
2
9
2
4
1
5
2
1
6
5
8
2
1
6
5
9
1
2
6
8
2
2
6
8
9
4
8
5
9-7
Beispiel: Sortieren durch Mischen
◮
Alternativ könnte man die Liste auch in mehr als zwei Listen
aufteilen, hätte dann aber in der Mischphase größeren
Aufwand.
◮
Allgemein: Die Rekursionstiefe kann durch stärkere Spaltung
verringert werden. Dies bedingt allerdings einen größeren
Aufwand in der Teile- und der Zusammenführungsphase.
9.2 Teile-und-Beherrsche-Algorithmen
9-8
Beispiel: Türme von Hanoi
◮
n Scheiben verschiedener Größe sind aufeinandergestapelt.
Es liegen stets nur kleinere Scheiben auf größeren.
◮
Der gesamte Stapel soll Scheibe für Scheibe umgestapelt
werden.
◮
Ein dritter Stapel darf zur Zwischenlagerung benutzt werden,
ansonsten dürfen die Scheiben nirgendwo anders abgelegt
werden.
◮
Auch in jedem Zwischenzustand dürfen nur kleinere Scheiben
auf größeren liegen.
9.2 Teile-und-Beherrsche-Algorithmen
9-9
Beispiel: Türme von Hanoi
Gesucht ist ein Algorithmus, der dieses Problem löst. Dazu muss
der Algorithmus angeben, in welcher Reihenfolge die Scheiben zu
bewegen sind.
1. Bringe die obersten n − 1 Scheiben von Turm 1 zu Turm 3.
2. Bewege die unterste Scheibe von Turm 1 zu Turm 2.
3. Bringe die n − 1 Scheiben von Turm 3 zu Turm 2.
1
2
9.2 Teile-und-Beherrsche-Algorithmen
3
9-10
Beispiel: Türme von Hanoi
1. Bringe die obersten n − 1 Scheiben von Turm 1 zu Turm 3.
2. Bewege die unterste Scheibe von Turm 1 zu Turm 2.
3. Bringe die n − 1 Scheiben von Turm 3 zu Turm 2.
proc hanoi(n: int; t1, t2, t3: Turm) begin
if n > 1 then
hanoi(n-1, t1, t3, t2); fi;
<bewege die Scheibe von t1 nach t2>;
if n > 1 then
hanoi(n-1, t3, t2, t1); fi;
end
9.2 Teile-und-Beherrsche-Algorithmen
9-11
Beispiel: Türme von Hanoi
◮
Diese Lösung besteht aus einer rekursiven Prozedur
hanoi(n, t1, t2, t3).
◮
Die Problemgröße ist n.
◮
Der Aufruf hanoi(n, t1, t2, t3) bewegt den Stapel von
t1 nach t2 und verwendet t3 als Hilfsstapel.
◮
Für die Anzahl T (n) der notwendigen Schritte gilt
◮



1
T (n) = 

2T (n − 1) + 1
für n = 1,
für n > 1.
Explizit ergibt sich T (n) = 2n − 1.
9.2 Teile-und-Beherrsche-Algorithmen
9-12
Komplexität von Teile-und-Beherrsche-Algorithmen
Die Problemgröße sei durch eine natürliche Zahl n gegeben. Die
Berechnung der Komplexität führt häufig auf
Rekurrenzgleichungen der Form
oder



falls n klein ist,
Θ(1),
T (n) = 

aT (n/b ) + f (n), falls n groß genug ist,
T (n) = f (T (n − 1), . . . , T (n − k ))
mit gegebenen Anfangswerten T (0),. . . ,T (k − 1).
Beispiel: Sortieren durch Mischen, Türme von Hanoi.
Wir wiederholen jetzt das Mastertheorem und das Verfahren zur
Lösung linearer Rekurrenzgleichungen mit konstanten
Koeffizienten.
9.2 Teile-und-Beherrsche-Algorithmen
9-13
Beispiel: Die Multiplikation nach Karatsuba
◮
Wie groß ist die Komplexität des klassischen Verfahrens zur
Multiplikation natürlicher Zahlen?
◮
Wenn der erste Faktor n-stellig und der zweite m-stellig ist,
dann müssen zuerst n · m Einzelmultiplikationen durchgeführt
werden. Anschließend sind m Zahlen der Maximallänge
n + m zu addieren. Das Ergebnis ist im Allgemeinen eine
(n + m)-stellige Zahl.
◮
Den größten Anteil trägt offenbar das Produkt n · m bei.
◮
Die Komplexität des Verfahrens liegt daher in Θ(n · m) bzw. in
Θ(n2 ), wenn beide Zahlen die Länge n besitzen.
9.2 Teile-und-Beherrsche-Algorithmen
9-14
Beispiel: Die Multiplikation nach Karatsuba
◮
Im Jahre 1962 stellte A. Karatsuba ein schnelleres Verfahren
zur Multiplikation vor.
◮
Die Idee besteht darin, die Zahlen x und y der Länge ≤ n in
Stücke der Länge ≤ n/2 aufzuteilen, sodass
x
y
= a · 10n/2 + b
= c · 10n/2 + d
gilt.
◮
Beispiel: n = 4,
9.2 Teile-und-Beherrsche-Algorithmen
x = 3141 = 31 · 102 + 41
9-15
Beispiel: Die Multiplikation nach Karatsuba
◮
Wir erhalten:
x ·y
= (a · 10n/2 + b )(c · 10n/2 + d )
= ac · 10n + (ad + bc ) · 10n/2 + bd
= ac · 10n + ((a + b )(c + d ) − ac − bd ) · 10n/2 + bd
◮
◮
Die Berechnung des Produkts zweier Zahlen x und y der
Länge ≤ n wird zurückgeführt auf die Berechnung der drei
Produkte ac , bd und (a + b )(c + d ) der Länge ≤ n/2.
Dann wird dasselbe Verfahren rekursiv auf diese drei
Produkte angewendet.
9.2 Teile-und-Beherrsche-Algorithmen
9-16
Beispiel: Die Multiplikation nach Karatsuba
Beispiel: x = 3141, y = 5927
x ·y
= 3141 · 5927
= 31 · 59 · 104 +
((31 + 41)(59 + 27) − 31 · 59 − 41 · 27) · 102 + 41 · 27
= 31 · 59 · 104 +
(72 · 86 − 31 · 59 − 41 · 27) · 102 + 41 · 27
= 1829 · 104 + (6192 − 1829 − 1107) · 102 + 1107
= 1829 · 104 + 3256 · 102 + 1107
= 18616707
9.2 Teile-und-Beherrsche-Algorithmen
9-17
Beispiel: Die Multiplikation nach Karatsuba
◮
◮
Für die Komplexität T (n) des Verfahrens gilt:



n=1
k , T (n) = 

3 · T n + kn, n > 1
2
Diese Rekurrenzgleichung besitzt die Lösung
T (n) = 3kn
◮
log2 (3)
log2 (3)
1,585
=Θ n
.
− 2kn = Θ n
Das ist deutlich günstiger als Θ n2 . Allerdings wirken sich
die Verbesserungen erst bei großen Zahlen aus.
9.2 Teile-und-Beherrsche-Algorithmen
9-18
Beispiel: Die Multiplikation nach Karatsuba
◮
Wir haben oben die Faktoren x und y in je zwei Teile zerlegt.
Durch Aufspalten in noch mehr Teile können wir die Laufzeit
weiter verbessern:
Für jedes ε > 0 gibt es ein Multiplikationsverfahren, das
höchstens c (ε)n1+ε Schritte benötigt. Die Konstante c (ε)
hängt nicht von n ab.
◮
In den 1970er Jahren wurde diese Schranke auf
O (n log(n) log(log(n)))
verbessert.
9.2 Teile-und-Beherrsche-Algorithmen
9-19
Gierige Algorithmen
Annahmen:
◮
Es gibt eine endliche Menge von Eingabewerten.
◮
Es gibt eine Menge von Teillösungen, die aus den
Eingabewerten berechnet werden können.
◮
Es gibt eine Bewertungsfunktion für Teillösungen.
◮
Die Lösungen lassen sich schrittweise aus Teillösungen,
beginnend bei der leeren Lösung, durch Hinzunahme von
Eingabewerten ermitteln.
◮
Gesucht wird eine/die optimale Lösung.
Vorgehensweise:
◮
Nimm (gierig) immer das am besten bewertete Stück.
9.3 Gierige Algorithmen
9-20
Beispiel: Algorithmus zum Geldwechseln
◮
Münzwerte: 1, 2, 5, 10, 20, 50 Cent und 1, 2 Euro.
◮
Wechselgeld soll mit möglichst wenig Münzen ausgezahlt
werden. 1,42 €: 1 € + 20 Cent + 20 Cent + 2 Cent
Allgemein: Wähle im nächsten Schritt die größtmögliche Münze.
In unserem Münzsystem gibt diese Vorgehensweise immer die
optimale Lösung. Im Allgemeinen gilt dies nicht. Angenommen, es
stünden 1, 5 und 11 Cent Münzen zur Verfügung. Um 15 Cent
herauszugeben, ergäbe sich:
◮
gierig: 11 + 1 + 1 + 1 + 1,
◮
optimal: 5 + 5 + 5.
9.3 Gierige Algorithmen
9-21
Gierige Algorithmen
func greedy(E: Eingabemenge): Ergebnis begin
var L: Ergebnismenge;
var x: Element;
E.sort();
while! E.empty() do
x ← E.first();
E.remove(x);
if valid(L ∪ {x }) then
L.add(x); fi;
od;
return L;
end
9.3 Gierige Algorithmen
9-22
Beispiel: Bedienreihenfolge im Supermarkt
◮
n Kunden warten vor einer Kasse.
◮
Der Bezahlvorgang von Kunde i dauere ci Zeiteinheiten.
◮
Welche Reihenfolge der Bedienung der Kunden führt zur
Minimierung der mittleren Verweilzeit (über alle Kunden)?
Die Gesamtbedienzeit Tges =
Die mittlere Verweilzeit ist
Pn
i =1 ci
ist konstant.
1
T = (c1 + (c1 + c2 ) + · · · + (c1 + · · · + cn ))
n
1
= (nc1 + (n − 1)c2 + (n − 2)c3 + · · · + 2cn−1 + cn )
n
n
1X
(n − k + 1)ck
=
n
k =1
9.3 Gierige Algorithmen
9-23
Beispiel: Bedienreihenfolge im Supermarkt
Die mittlere Verweilzeit pro Kunde
◮
steigt, wenn Kunden mit langer Bedienzeit vorgezogen
werden.
◮
sinkt, wenn Kunden mit kurzer Bedienzeit zuerst bedient
werden.
◮
wird minimal, wenn die Kunden nach ci aufsteigend sortiert
werden.
Konsequenzen:
◮
Greedy-Algorithmus ist geeignet.
◮
Die Funktion zur Bestimmung des nächsten Kandidaten wählt
den Kunden mit minimaler Bedienzeit.
Frage: Ist dies eine geeignete Strategie für die Prozessorvergabe?
9.3 Gierige Algorithmen
9-24
Beispiel: Algorithmus von Kruskal
◮
Selektiere fortwährend eine verbleibende Kante mit
geringstem Gewicht, die keinen Zyklus erzeugt, bis alle
Knoten verbunden sind (Kruskal, 1956).
◮
Eine eindeutige Lösung ist immer dann vorhanden, wenn alle
Gewichte verschieden sind.
4
3
8
3
2 6
6
4
3
5
5
7
6
2
Nach Wahl der Kanten 2, 2, 3 und 3 darf die verbleibende 3 nicht
gewählt werden, da sonst ein Zyklus entstünde.
9.3 Gierige Algorithmen
9-25
Matroide
◮
Greedy-Algorithmen liefern nicht immer eine optimale Lösung.
◮
Mithilfe der Theorie der gewichteten Matroide kann bestimmt
werden, wann ein Greedy-Algorithmus eine optimale Lösung
liefert.
◮
Die Theorie der bewerteten Matroide deckt nicht alle Fälle ab.
9.3 Gierige Algorithmen
9-26
Matroide
Ein Matroid ist ein Paar M = (S , I) mit folgenden Eigenschaften:
◮
S ist eine endliche Menge.
◮
I ist eine nichtleere Familie von Teilmengen von S .
◮
◮
Vererbungseigenschaft: Sind A ⊆ B und B ∈ I, so ist A ∈ I.
Austauscheigenschaft: Sind A , B ∈ I und |A | < |B |, so gibt es
ein x ∈ (B \ A ) mit A ∪ {x } ∈ I.
Die Mengen in I heißen unabhängig.
Eine unabhängige Menge A ∈ I heißt maximal, wenn es keine
Erweiterung x mit A ∪ {x } ∈ I gibt.
9.3 Gierige Algorithmen
9-27
Matroide
◮
◮
Ein Matroid M = (S , I) heißt gewichtet, wenn es eine
Gewichtsfunktion w : S → R+ gibt.
Die Gewichtsfunktion lässt sich auf Teilmengen A ⊆ S durch
w (a ) =
X
w (A )
x ∈A
erweitern.
◮
Eine Menge A ∈ I mit maximalem Gewicht heißt optimal.
9.3 Gierige Algorithmen
9-28
Matroide
Satz: Es sei M = (S , I) ein gewichtetes Matroid mit der
Gewichtsfunktion w : S → R+ . Der folgende gierige Algorithmus
gibt eine optimale Teilmenge zurück.
func greedy(M, w): I begin
A ← ∅;
sortiere S in monoton fallender Reihenfolge
nach dem Gewicht w;
foreach x ∈ S do
if A ∪ {x} ∈ I
then A ← A ∪ {x};
return A;
end
9.3 Gierige Algorithmen
9-29
Matroide
Satz: Die Komplexität des gierigen Algorithmus liegt in
O (n log n + n · f (n)),
wobei
◮
n log n der Aufwand für das Sortieren und
◮
f (n) der Aufwand für den Test A ∪ {x }
ist. n ist die Anzahl der Elemente von S , d. h. |S | = n.
9.3 Gierige Algorithmen
9-30
Matroide
Beispiel: Es sei G = (V , E ) ein ungerichteter Graph. Dann ist
MG = (SG , IG ) ein Matroid, dabei gilt:
◮
SG = E ,
◮
A ⊆ E : A ∈ IG ⇔ A azyklisch.
Eine Menge A von Kanten ist genau dann unabhängig, wenn der
Graph GA = (V , A ) einen Wald bildet.
Der Algorithmus von Kruskal ist ein Beispiel für das obige
allgemeine Verfahren.
9.3 Gierige Algorithmen
9-31
Backtracking-Algorithmen
◮
Das Backtracking realisiert eine systematische Suchtechnik,
die die Menge aller möglichen Lösungen eines Problems
vollständig durchsucht.
◮
Führt die Lösung auf einem Weg nicht zum Ziel, wird zur
letzten Entscheidung zurückgegangen und dort eine
Alternative untersucht.
◮
Da alle möglichen Lösungen untersucht werden, wird eine
Lösung – wenn sie existiert – stets gefunden.
9.4 Backtracking-Algorithmen
9-32
Beispiel: Wegsuche in einem Labyrinth
Gesucht ist ein Weg in einem Labyrinth von einem Start- zu einem
Zielpunkt.
◮
Gehe zur ersten Kreuzung und schlage dort einen Weg ein.
◮
Markiere an jeder Kreuzung den eingeschlagenen Weg.
◮
Falls eine Sackgasse gefunden wird, gehe zurück zur letzten
Kreuzung, die einen noch nicht untersuchten Weg aufweist
und gehe diesen Weg.
9.4 Backtracking-Algorithmen
9-33
Beispiel: Wegsuche in einem Labyrinth
◮
Die besuchten Kreuzungspunkte werden als Knoten eines
Baumes aufgefasst.
◮
Der Startpunkt bildet die Wurzel dieses Baumes. Blätter sind
die Sackgassen und der Zielpunkt.
◮
Der Baum wird beginnend mit der Wurzel systematisch
aufgebaut.
◮
Wegpunkte sind Koordinatentupel (x , y ). Im Beispiel ist (1, 1)
der Startpunkt und (1, 3) der Zielpunkt.
(1,1)
(2,1)
(2,2)
(1,2)
(2,3)
(3,3)
9.4 Backtracking-Algorithmen
(3,1)
(3,2)
(1,3)
9-34
Backtracking-Algorithmen
◮
◮
Es gibt eine endliche Menge K von Konfigurationen.
K ist hierarchisch strukturiert:
◮ Es gibt eine Ausgangskonfiguration k0 ∈ K .
◮ Zu jeder Konfiguration kx ∈ K gibt es eine Menge kx , . . . , kx
1
nx
von direkt erreichbaren Folgekonfigurationen.
◮
Für jede Konfiguration ist entscheidbar, ob sie eine Lösung ist.
◮
Gesucht werden Lösungen, die von k0 aus erreichbar sind.
9.4 Backtracking-Algorithmen
9-35
Backtracking-Algorithmen
proc backtrack(k: Konfiguration) Konfiguration
begin
if k ist Lösung then
print(k); fi;
foreach Folgekonfiguration k’ von k do
backtrack(k’); od;
end
◮
Dieses Schema terminiert nur, wenn der Lösungsraum
endlich und wiederholte Bearbeitung einer bereits getesteten
Konfiguration ausgeschlossen ist (keine Zyklen).
◮
Kritisch ist ggf. der Aufwand. Er ist häufig exponentiell.
9.4 Backtracking-Algorithmen
9-36
Backtracking-Algorithmen (Varianten)
◮
Lösungen werden bewertet. Zuletzt wird die beste
ausgewählt.
◮
Das angegebene Schema findet alle Lösungen. Oft genügt
es, den Algorithmus nach der ersten Lösung zu beenden.
◮
Aus Komplexitätsgründen wird eine maximale Rekursionstiefe
vorgegeben. Als Lösung dient dann beispielsweise die am
besten bewertete Lösung aller bisher gefundenen.
◮
Branch-and-Bound-Algorithmen.
9.4 Backtracking-Algorithmen
9-37
Branch-and-Bound-Algorithmen
◮
Das angegebene Schema untersucht jeden
Konfigurationsteilbaum.
◮
Oft kann man schon im Voraus entscheiden, dass es sich
nicht lohnt, einen bestimmten Teilbaum zu besuchen.
◮
Dies ist zum Beispiel bei einer Sackgasse der Fall oder wenn
man weiß, dass die zu erwartende Lösung auf jeden Fall
schlechter sein wird, als eine bisher gefundene.
◮
In diesem Fall kann auf die Bearbeitung des Teilbaums
verzichtet werden.
9.4 Backtracking-Algorithmen
9-38
Branch-and-Bound-Algorithmen
Beispiele:
◮
Spiele (insbesondere rundenbasierte Strategiespiele), zum
Beispiel Schach.
Konfigurationen entsprechen den Stellungen,
Nachfolgekonfigurationen sind durch die möglichen Spielzüge
bestimmt. Nachweisbar schlechte Züge müssen nicht
untersucht werden.
◮
Erfüllbarkeitstests von logischen Aussagen.
◮
Planungsprobleme.
◮
Optimierungsprobleme.
9.4 Backtracking-Algorithmen
9-39
Beispiel: Das N-Damen-Problem
Es sollen alle Stellungen von n Damen auf einem
n × n-Schachbrett ermittelt werden, bei denen keine Dame eine
andere bedroht. Es dürfen also nicht zwei Damen in der gleichen
Zeile, Spalte oder Diagonale stehen.
8
7
6
5
4
3
2
1
8
7
6
5
4
3
2
1
1 2 3 4 5 6 7 8
9.4 Backtracking-Algorithmen
1 2 3 4 5 6 7 8
9-40
Beispiel: Das N-Damen-Problem
◮
◮
◮
K sei die Menge aller Stellungen mit einer Dame in jeder der
ersten m Zeilen, 0 ≤ m ≤ n, sodass je zwei Damen sich nicht
bedrohen.
K enthält alle Lösungen. Nicht jede Stellung lässt sich
allerdings zu einer Lösung erweitern. So ist zum Beispiel
unten jedes Feld in der 7. Zeile bereits bedroht, sodass dort
keine Dame mehr gesetzt werden kann.
Durch Ausnutzung von Symmetrien lässt sich der Aufwand
verringern.
8
7
6
5
4
3
2
1
1 2 3 4 5 6 7 8
9.4 Backtracking-Algorithmen
9-41
Beispiel: Das N-Damen-Problem
proc platziere(zeile: int) begin
var i: int;
for i ← 1 to n do
if <feld (zeile, i) nicht bedroht> then
<setze Dame auf (zeile, i)>;
if zeile = n then
<gib Konfiguration aus>;
else platziere(zeile + 1); fi;
fi;
od;
end
9.4 Backtracking-Algorithmen
9-42
Beispiel: Das N-Damen-Problem
4
3
2
1
4
3
2
1
4
3
2
1
Sackgasse
1 2 3 4
1 2 3 4
4
3
2
1
4
3
2
1
1 2 3 4
Sackgasse
1 2 3 4
L¨osung
1 2 3 4
4
3
2
1
4
3
2
1
1 2 3 4
9.4 Backtracking-Algorithmen
4
3
2
1
1 2 3 4
4
3
2
1
1 2 3 4
1 2 3 4
9-43
Beispiel: Das N-Damen-Problem
◮
Das N-Damen-Problem ist für n ≥ 4 lösbar. Wenn die erste
Dame nicht richtig gesetzt ist, werden allerdings bis zu
(n − 1)! Schritte benötigt, um dies herauszufinden. Nach der
stirlingschen Formel ist
n
n! ≈ n · e
−n
√
2πn,
der Aufwand also exponentiell.
◮
Für jedes n ≥ 4 ist ein Verfahren bekannt (Ahrens, 1912), das
in linearer Zeit eine Lösung findet (nur eine, nicht alle). Es
basiert auf der Beobachtung, dass in Lösungsmustern häufig
Rösselsprung-Sequenzen auftreten.
◮
Im Jahre 1990 ist ein schneller probabilistischer Algorithmus
veröffentlicht worden, dessen Laufzeit in O (n3 ) liegt.
9.4 Backtracking-Algorithmen
9-44
Beispiel: Problem des Handlungsreisenden
◮
◮
◮
◮
Gegeben seien n durch Straßen verbundene Städte mit
Reisekosten c (i , j ) zwischen je zwei Städten i und j ,
1 ≤ i , j ≤ n.
Gesucht ist die billigste Rundreise, die jede Stadt genau
einmal besucht (Traveling Salesman Problem, TSP).
Eine solche Kantenfolge heißt hamiltonscher Zyklus.
Dieser Graph ist vollständig.
Augsburg
3
2
9
9
4
Frankfurt
Braunschweig
9
1
8
Erfurt
2
3
2
8
Celle
9
3
6
Darmstadt
Die billigste Rundreise kostet 13 Einheiten.
9.4 Backtracking-Algorithmen
9-45
Beispiel: Problem des Handlungsreisenden
◮
Ein naiver Algorithmus beginnt bei einem Startknoten und
sucht dann alle Wege ab.
◮
Die Komplexität dieses Verfahrens liegt in O (n!).
Das folgende Verfahren führt zu einer Näherungslösung:
◮
1. Die Kanten werden nach ihren Kosten sortiert.
2. Man wählt die billigste Kante unter den beiden folgenden
Nebenbedingungen:
◮
◮
9.4 Backtracking-Algorithmen
Es darf kein Zyklus entstehen (außer am Ende der Rundreise).
Kein Knoten darf zu mehr als 2 Kanten adjazent sein.
9-46
Beispiel: Problem des Handlungsreisenden
◮
Die Laufzeit dieses gierigen Algorithmus liegt in O (n2 log n2 ).
◮
Das Verfahren führt nicht immer zu einer optimalen Lösung.
Trotzdem wird es in der Praxis erfolgreich eingesetzt.
◮
Branch-and-Bound: Wenn man weiß, dass eine Lösung mit
Kosten k existiert (zum Beispiel durch obigen Algorithmus),
dann kann ein Backtrack-Algorithmus alle Teillösungen
abschneiden, die bereits teurer als k sind.
9.4 Backtracking-Algorithmen
9-47
Dynamische Programmierung
Rekursive Problemstruktur:
1. Aufteilung in abhängige Teilprobleme.
2. Berechnen und Zwischenspeichern wiederbenötigter
Teillösungen.
3. Bestimmung des Gesamtergebnisses unter Verwendung der
Teillösungen.
Die dynamische Programmierung ist mit der
Teile-und-Beherrsche-Methode verwandt. Die Teilprobleme sind
aber abhängig. Einmal berechnete Teillösungen werden
wiederverwendet.
Die dynamische Programmierung wird häufig bei
Optimierungsproblemen angewendet.
9.5 Dynamische Programmierung
9-48
Beispiel: Fibonacci-Zahlen



n=0
0




fib(n) = 
1
n=1




fib(n − 1) + fib(n − 2) n ≥ 2
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, . . .
func fib(n: int): int begin
if n < 2 then
return n;
fi;
return fib(n-1) + fib(n-2);
end
9.5 Dynamische Programmierung
9-49
Beispiel: Fibonacci-Zahlen
Berechnung von fib(5):
fib5
fib4
fib3
fib3
fib2
fib1
fib0
1
0
9.5 Dynamische Programmierung
fib2
fib2
fib1
fib1
fib1
fib0
fib1
fib0
1
1
0
1
0
1
9-50
Beispiel: Fibonacci-Zahlen
Die Berechnung von fib(5) führt zweimal auf die Berechnung von
fib(3). Die zugehörigen Teilbäume werden zweimal ausgewertet.
Aufwandsabschätzung:
T (n) = Anzahl der Funktionsaufrufe



n = 0, n = 1
1
=

1 + T (n − 1) + T (n − 2) n ≥ 2
T (n) wächst exponentiell. Wir haben in der Übung gezeigt:
T (n) = 1 +
√ n
√ n
!
!
√
 1 + 5 
 1 − 5 
1
 + 1 −
 − 1
5 
5 
1√
5
2
5
2
≈ 1,45 · 1,62n .
9.5 Dynamische Programmierung
9-51
Beispiel: Fibonacci-Zahlen
Iterative dynamische Lösung (vgl. Abschnitt 2.1):
func fibDyn(n: int): int begin
var i, result, minus1, minus2: int;
if n < 2 then return n; fi;
minus2 ← 0;
minus1 ← 1;
for i ← 2 to n do
result ← minus1 + minus2;
minus2 ← minus1;
minus1 ← result;
od;
return result;
end
9.5 Dynamische Programmierung
9-52
Beispiel: Das Rucksackproblem
Das Rucksackproblem (knapsack problem):
Ein Wanderer findet einen Schatz aus Edelsteinen.
◮
Jeder Edelstein hat ein bestimmtes Gewicht und einen
bestimmten Wert.
◮
Er hat nur einen Rucksack, dessen Kapazität durch ein
maximales Gewicht begrenzt ist.
Gesucht ist ein Algorithmus, der diejenige Befüllung des
Rucksacks ermittelt, die einen maximalen Wert hat, ohne die
Gewichtsbeschränkung zu verletzen.
9.5 Dynamische Programmierung
9-53
Beispiel: Das Rucksackproblem
Gegeben:
◮
◮
◮
◮
Kapazität c ∈ N,
Menge O mit n ∈ N Objekten o1 , . . . , on ,
Gewichtsfunktion g : O → N mit
Bewertungsfunktion w : O → N.
P
j ∈O
g (j ) > c ,
Gesucht ist eine Menge O ′ ⊆ O mit
X
j ∈O ′
g (j ) ≤ c und
X
w (j ) maximal.
j ∈O ′
Da Gegenstände nur vollständig oder gar nicht eingepackt werden,
spricht man auch vom 0-1-Rucksackproblem. Beim fraktionalen
Rucksackproblem können auch Teile eines Gegenstands
ausgewählt werden.
9.5 Dynamische Programmierung
9-54
Beispiel: Das Rucksackproblem
Der gierige Algorithmus führt nicht zur Lösung:
O = {o1 , o2 , o3 }, Kapazität c = 5.
Gewichte: g (o1 ) = 1, g (o2 ) = 2, g (o3 ) = 3.
Werte: w (o1 ) = 6, w (o2 ) = 10, w (o3 ) = 12.
Ein gieriger Algorithmus wählt das Objekt mit dem größten
relativen Wert.
w (o )
, r (o1 ) = 6, r (o2 ) = 5, r (o3 ) = 4.
r (o ) =
g (o )
O ′ = {o1 , o2 },
X
j ∈O ′
g (j ) = 3 ≤ 5 = c ,
X
w (j ) = 16.
j ∈O ′
Die optimale Lösung ist
O ′′ = {o2 , o3 },
9.5 Dynamische Programmierung
X
j ∈O ′′
g (j ) = 5 = c ,
X
w (j ) = 22.
j ∈O ′′
9-55
Beispiel: Das Rucksackproblem
Backtracking liefert die korrekte Lösung, ist aber ineffizient.
O = {o1 , o2 , o3 , o4 }, c = 10.
Gewichte: g (o1 ) = 2, g (o2 ) = 2, g (o3 ) = 6, g (o4 ) = 5.
Werte: w (o1 ) = 6, w (o2 ) = 3, w (o3 ) = 5, w (o4 ) = 4.
10
nein
zur Disposition
o : (g, w)
o1 : (2, 6)
ja
10
8
o2 : (2, 3)
10
8
8
6
o3 : (6, 5)
10
4
8
2 8
2
6
0
o4 : (5, 4)
10
5 4
8
3 2 8
3 2
6
1 0
(0/0), (5/4), (6/5), (2/3), (7/7), (8/8), (2/6), (7/10), (8/11), (4/9), (9/13), (10/14), jeweils (Gewicht/Wert).
Es ist O ′ = {o1 , o2 , o3 } mit g (O ′ ) = 10 und w (O ′ ) = 14.
9.5 Dynamische Programmierung
9-56
Beispiel: Das Rucksackproblem
Rückgabewert ist der Wert der optimalen Füllung.
Aufruf: btKnapsack(1,c).
func btKnapsack(i, rest: int): int begin
if i = n then
if g(i) > rest then return 0;
else return w(i); fi;
else if g(i) > rest then
return btKnapsack(i+1,rest);
else
return max(btKnapsack(i+1,rest),
btKnapsack(i+1,rest-g(i))+w(i));
fi; fi;
end
Das Optimierungspotential durch Vermeidung wiederkehrender
Berechnungen wird nicht genutzt.
9.5 Dynamische Programmierung
9-57
Beispiel: Das Rucksackproblem
Dynamische Programmierung:
O = {o1 , . . . , o5 }, c = 10.
Gewichte: g (o1 ) = 2, g (o2 ) = 2, g (o3 ) = 6, g (o4 ) = 5, g (o5 ) = 4.
Werte: w (o1 ) = 6, w (o2 ) = 3, w (o3 ) = 5, w (o4 ) = 4, w (o5 ) = 6.
Es wird ein zweidimensionalen Feld f [i , r ] berechnet:
r→
i↓
5
4
3
2
0
0
0
0
0
1
0
0
0
0
2
0
0
0
3
3
0
0
0
3
4
6
6
6
6
5
6
6
6
6
6
6
6
6
9
7
6
6
6
9
8
6
6
6
9
9
6
10
10
10
10
6
10
11
11
f [4, 9] = 10: Wenn o4 und o5 bei der Restkapazität 9 zur
Disposition stehen, beträgt der Wert der zusätzlichen Füllung 10.
9.5 Dynamische Programmierung
9-58
Beispiel: Das Rucksackproblem
Die zentrale Anweisung in btKnapsack:
return max(btKnapsack(i+1,rest),
btKnapsack(i+1,rest-g(i))+w(i));
r→
i↓
5
4
3
2
0
0
0
0
0
1
0
0
0
0
2
0
0
0
3
3
0
0
0
3
4
6
6
6
6
5
6
6
6
6
6
6
6
6
9
7
6
6
6
9
8
6
6
6
9
9
6
10
10
10
10
6
10
11
11
f [3, 8] = max (f [4, 8], f [4, 2] + 5) = max (6, 0 + 5) = 6
9.5 Dynamische Programmierung
9-59
Beispiel: Das Rucksackproblem
Der Algorithmus arbeitet folgendermaßen:
1. Berechne zunächst die Werte f [n, 0],. . . ,f [n, c ].
2. Berechne anschließend f [i , 0],. . . ,f [i , c ] für i = n − 1 bis i = 2
unter Rückgriff auf die bereits berechneten Werte der Zeile
i + 1.
3. Das Gesamtergebnis f [1, c ] ergibt sich dann aus
f [1, 10] = max (f [2, 10], f [2, 8] + 6) = max (11, 9 + 6) = 15.
9.5 Dynamische Programmierung
9-60
Beispiel: Das Rucksackproblem
func dynKnapsack(n, c: int): int begin
var f[2..n, 0..c]: int;
var i, r: int;
for r ← 0 to c do
if g(n) > r then f[n,r] ← 0;
else f[n,r] ← w(n); fi;
for i ← n - 1 downto 2 do
for r ← 0 to c do
if g(i) > r then
f[i,r] ← f[i+1,r];
else f[i, r] ←
max(f[i+1,r],f[i+1,r-g(i)]+w(i)); fi;
od; od;
if g(1) > c then return f[2, c];
else return max(f[2,c],f[2,c-g(1)]+w(1));
end
9.5 Dynamische Programmierung
9-61
Beispiel: Suche in einem Text
Im Folgenden werden Zeichenketten als Felder behandelt.
Gegeben:
◮
Feld t [1..n] von Zeichen, der Text,
◮
Feld p [1..m] von Zeichen, das Muster (pattern).
Es sei m ≤ n. In der Regel ist sogar m << n.
Gesucht: Vorkommen von p in t , d. h. Indices s , 0 ≤ s ≤ n − m, mit
t [s + 1..s + m] = p [1..m].
9.5 Dynamische Programmierung
9-62
Beispiel: Suche in einem Text
Naive Lösung: Vergleiche für alle s = 0..n − m und für alle i = 0..m
die Zeichen p [i ] = t [s + i ].
proc naiv(t, p): begin
var i,s: int;
for s ← 0 to n - m do
i ← 1;
while i ≤ m && p[i] = t[s+i] do
i ← i+1; od;
if i = m+1 then print(s); fi;
od;
end
9.5 Dynamische Programmierung
9-63
Beispiel: Suche in einem Text
◮
Als Maß für die Laufzeit nehmen wir die Anzahl der
ausgeführten Tests der inneren Schleife.
◮
Der schlimmste Fall tritt ein, wenn für jeden Wert s die
Zeichenkette p bis zum letzten Zeichen mit t verglichen
werden muss. Beispiel: t = "aaaaaaaaaaaaaaaaab",
p = "aaab".
◮
Die Laufzeit liegt in O ((n − m)m) = O (nm).
◮
Gesucht ist ein effizienterer Algorithmus.
9.5 Dynamische Programmierung
9-64
Beispiel: Suche in einem Text
s = 4, q = 5:
t: bacbababaabcbab
p:
ababaca
s ′ = s + 2:
t: bacbababaabcbab
p:
ababaca
Die nächste möglicherweise erfolgreiche Verschiebung ist
s ′ = s + (q − d [q]).
9.5 Dynamische Programmierung
9-65
Beispiel: Suche in einem Text
Es seien die Musterzeichen p [1..q] gegeben, die mit den
Textzeichen t [s + 1..s + q] übereinstimmen. Wie groß ist die
geringste Verschiebung s ′ > s für die
p [1..k ] = t [s ′ + 1..s ′ + k ]
mit s ′ + k = s + q gilt?
9.5 Dynamische Programmierung
9-66
Beispiel: Suche in einem Text
Die Präfixfunktion für das Muster ababababca:
i
p[i]
d[i]
9.5 Dynamische Programmierung
1
a
0
2
b
0
3
a
1
4
b
2
5
a
3
6
b
4
7
a
5
8
b
6
9
c
0
10
a
1
9-67
Beispiel: Suche in einem Text
proc Berechnung der Präfixfunktion d begin
d(1) ← 0;
k ← 0;
for q ← 2 to m do
while k > 0 ∧ p[k+1] , p[q]; do
k ← d[k]; od;
if p[k+1] = p[q]
then k ← k+1; fi;
d[q] ← k;
od;
return d;
end
9.5 Dynamische Programmierung
9-68
Beispiel: Suche in einem Text
Der Knuth-Morris-Pratt-Algorithmus:
proc kmp(t, p): begin
berechne d;
q ← 0;
for i ← 1 to n do
while q > 0 ∧ p[q+1] , t[i] do
q ← d[q]; od;
if p[q+1] = t[i]
then q ← q+1; fi;
if q = m
then print(i-m);
q ← d[q]; fi;
od;
end
Die Laufzeit dieses Algorithmus liegt in O (n + m).
9.5 Dynamische Programmierung
9-69
Weitere Algorithmenmuster
◮
Zufallsgesteuerte Algorithmen
◮
Verteilte und parallele Algorithmen
◮
Lokale Suche
◮
Amortisierte Analyse
◮
Approximative Algorithmen
◮
Genetische Algorithmen
◮
Schwarmbasierte Algorithmen und Koloniealgorithmen
◮
...
9.5 Dynamische Programmierung
9-70
Weitere Gebiete
◮
Mathematische Algorithmen
◮
Geometrische Algorithmen
◮
Algorithmen für Texte
◮
Lineare Programmierung
◮
...
Große Bedeutung für die Theorie der Algorithmen besitzt die
Komplexitätstheorie. Hierzu zählen zum Beispiel die Untersuchung
von Komplexitätsklassen wie P und NP und die so genannte
NP-Vollständigkeit. Die Komplexitätstheorie wird in der Vorlesung
„Theoretische Informatik II“ behandelt.
9.5 Dynamische Programmierung
9-71
1. Der Algorithmenbegriff
2. Imperative Algorithmen
3. Sortieralgorithmen
4. Listen und abstrakte Datentypen
5. Objektorientierte Algorithmen
6. Bäume
7. Mengen, Verzeichnisse und Hash-Verfahren
8. Graphen
9. Entwurf von Algorithmen
10. Funktionale und deduktive Algorithmen
10.1 Partielle und totale Funktionen
10.2 Funktionale Algorithmen
10.3 Prädikatenlogik
10.4 Deduktive Algorithmen
Einführung
Grundidee:
◮
Ein Algorithmus wird durch eine Funktion f realisiert.
◮
Die Eingabe des Algorithmus ist ein Element w aus dem
Definitionsbereich von f .
◮
Die Ausgabe des Algorithmus ist der Wert f (w ) aus dem
Wertebereich von f .
10.1 Partielle und totale Funktionen
10-1
Einführung
◮
Mathematische Funktionen sind häufig deklarativ definiert:
Sie beinhalten keine Angaben zur Durchführung ihrer
Berechnung.
◮
Beispiele: f (n, m) = n · m, f (n) = n!.
◮
Wie kann ein Algorithmus den Funktionswert f (w )
berechnen?
◮
Können alle berechenbaren Probleme derart gelöst werden?
10.1 Partielle und totale Funktionen
10-2
Partielle und totale Funktionen
Eine partielle Funktion
f : A −→ B
ordnet jedem Element x einer Teilmenge Df ⊆ A genau ein
Element f (x ) ∈ B zu. Die Menge Df heißt Definitionsbereich von f .
f ist eine totale Funktion, wenn Df = A gilt.
Beispiel:
f : R −→ R,
Df = R \ {0},
1
f (x ) =
x
Algorithmen können undefinierte Ausdrücke enthalten und müssen
nicht in jedem Fall terminieren, d. h.:
Algorithmen berechnen partielle Funktionen!
10.1 Partielle und totale Funktionen
10-3
Definition von Funktionen
◮
Wenn der Definitionsbereich einer Funktion endlich ist, lässt
sie sich durch Angabe aller Funktionswerte in einer Tabelle
definieren.
◮
Beispiel:
∧:B×B→B
false
false
true
true
10.1 Partielle und totale Funktionen
false
true
false
true
false
false
false
true
10-4
Definition von Funktionen
◮
In den meisten Fällen wird eine Funktion f : A → B durch
einen Ausdruck, der zu jedem Element aus Df genau einen
Wert von B liefert, beschrieben.
◮
Beispiel:
max : R × R → R



x
max(x , y ) = 

y
x≥y
x<y
= if x ≥ y then x else y fi
10.1 Partielle und totale Funktionen
10-5
Rekursive Definitionen (Wiederholung)
Die Funktion f : N −→ N wird durch


1







1 f (n) = 

n

f


2



f (3n + 1)
n = 0,
n = 1,
n ≥ 2, n gerade,
n ≥ 2, n ungerade.
rekursiv definiert.
10.1 Partielle und totale Funktionen
10-6
Auswertung von Funktionen (Wiederholung)
Funktionsdefinitionen können als Ersetzungssysteme gesehen
werden. Funktionswerte lassen sich aus dieser Sicht durch
wiederholtes Einsetzen berechnen. Die Auswertung von f (3) ergibt
f (3) → f (10) → f (5) → f (16) → f (8) → f (4) → f (2) →
f (1) → 1.
Terminiert der Einsetzungsprozess stets?
10.1 Partielle und totale Funktionen
10-7
Formen der Rekursion (Wiederholung)
◮
Lineare Rekursion,
◮
Endrekursion,
◮
Verzweigende Rekursion (Baumrekursion),
◮
Geschachtelte Rekursion,
◮
Verschränkte Rekursion (wechselseitige Rekursion).
10.1 Partielle und totale Funktionen
10-8
Funktionen höherer Ordnung
Funktionen können selbst Argumente oder Werte sein. In diesem
Fall spricht man von Funktionen höherer Ordnung oder
Funktionalen.
f : (A1 → A2 ) × A3 → B
g : A → (B1 → B2 )
h : (A1 → A2 ) → (B1 → B2 )
10.1 Partielle und totale Funktionen
10-9
Funktionen höherer Ordnung
Beispiele:
◮
Summe:
b
X
f (i )
i =a
◮
Komposition von Funktionen:
◮
Auswahl zwischen Funktionen:
◮
Bestimmtes Integral:
Z
f ◦g
if p then f else g fi
b
f (x ) dx
a
10.1 Partielle und totale Funktionen
10-10
Funktionale Algorithmen
◮
Ein Algorithmus heißt funktional, wenn die
Berechnungsvorschrift mittels einer Sammlung (partieller)
Funktionen definiert wird.
◮
Die Funktionsdefinitionen dürfen insbesondere Rekursionen
und Funktionen höherer Ordnung enthalten.
10.2 Funktionale Algorithmen
10-11
Funktionale Algorithmen
Beispiel:
f (0) = 0
f (1) = 1
f (n) = nf (n − 2)
Wenn wir als Datenbereich die Menge der ganzen Zahlen
zugrunde legen, berechnet dieser Algorithmus die Funktion
f : Z → Z mit Df = N und


0

n gerade








n −1
f (n) = 

2

Y




(2i + 1) n ungerade



i =0
10.2 Funktionale Algorithmen
10-12
Funktionale Programmiersprachen
Programmiersprachen, die in erster Linie für die Formulierung
funktionaler Algorithmen gedacht sind, heißen funktional.
Funktionale Programmiersprachen sind beispielsweise
◮
Lisp,
◮
Scheme,
◮
ML, SML und
◮
Haskell.
Man kann in vielen imperativen/objektorientierten
Programmiersprachen funktional programmieren – und umgekehrt!
10.2 Funktionale Algorithmen
10-13
Lisp und Scheme
◮
Lisp wurde Ende der 50er Jahre von John McCarthy
entwickelt.
◮
Im Laufe der Jahre wurden viele Lisp-Dialekte, u. a. Common
Lisp und Scheme definiert.
◮
Die erste Version von Scheme stammt aus dem Jahre 1975.
Autoren waren Guy Lewis Steele Jr. und Gerald Jay Sussman.
◮
Lisp und Scheme werden in der Regel interpretiert, nicht
compiliert.
10.2 Funktionale Algorithmen
10-14
Algorithmus von Euklid
Funktional geschrieben hat der
Algorithmus von Euklid
die Form:
ggT(a , 0) = a
ggT(a , b ) = ggT(b , a mod b )
Beispiel: ggT(36, 52) → ggT(52, 36) → ggT(36, 16) →
ggT(16, 4) → ggT(4, 0) → 4
10.2 Funktionale Algorithmen
10-15
Scheme: Algorithmus von Euklid
Der funktionale
Algorithmus von Euklid
lautet beispielsweise als Scheme-Programm:
(define (ggT a b)
(if (= b 0)
a
(ggT b (remainder a b))))
(ggT 36 52)
4
10.2 Funktionale Algorithmen
10-16
Terme
Terme sind aus
◮
Konstanten,
◮
Variablen,
◮
Funktions- und
◮
Relationssymbolen
zusammengesetzte Zeichenketten. Terme besitzen einen Typ.
Beispiele:
◮
◮
◮
◮
Die Konstanten . . . , −2, −1, 0, 1, 2, . . . sind int-Terme.
13 −
√
2 + 3 ist ein real-Term.
4 · (3 − 2) + 3 · i ist ein int-Term, falls i eine Variable vom Typ
int ist.
Ist b ein bool-Term und sind t , u int-Terme, so ist auch
if b then t else u fi ein int-Term.
10.2 Funktionale Algorithmen
10-17
Funktionsdefinitionen
Die Gleichung
f (x1 , . . . , xn ) = t (x1 , . . . , xn )
heißt Definition der Funktion f vom Typ τ, wenn gilt:
◮
f ist der Funktionsname.
◮
x1 , . . . , xn sind Variable, die formale Parameter genannt
werden. Die Typen von x1 , . . . , xn seien τ1 , . . . , τn .
◮
t ist ein Term, der die Variablen x1 , . . . , xn enthält. Der Typ von
t sei τ.
◮
f : τ1 × · · · × τn → τ heißt Signatur von f .
Der Fall n = 0 ist zugelassen. In diesem Fall liefert die Auswertung
stets das gleiche Ergebnis. Die Funktion entspricht somit einer
Konstanten.
10.2 Funktionale Algorithmen
10-18
Funktionsdefinitionen
Beispiele:
ZylVol(h , r )
π
max(x , y )
f (p , q, x , y )
g (x )
h (p , q)
10.2 Funktionale Algorithmen
= h · π · r2
(Signatur ZylVol: real×real→real)
= 3.1415926535 . . .
(Signatur: π :→real)
= if x > y then x else y fi
(Signatur max: int×int→int)
= if p ∨ q then 2 · x + 1 else 3 · y − 1 if
(Signatur f: bool×bool×int×int→int)
= if even(x ) then x2 else 3 · x + 1 fi
(Signatur g: int→int)
= if p then q else false fi
(Signatur h: bool×bool→bool)
10-19
Funktionsanwendungen
◮
Unter einer Anwendung (Applikation) einer Funktion
f (x1 , . . . , xn ) = t (x1 , . . . , xn ) versteht man einen Ausdruck
f (a1 , . . . , an ).
◮
Für die formalen Parameter werden Ausdrücke (aktuelle
Parameter) eingesetzt, die den richtigen Typ besitzen müssen.
◮
Die Auswertung liefert eine Folge
f (a1 , . . . , an ) → t (a1 , . . . , an ) → · · · .
◮
Es muss genau festgelegt werden, wie und in welcher
Reihenfolge auftretende (Teil-)Ausdrücke ausgewertet
werden.
◮
Diese Folge muss nicht terminieren.
10.2 Funktionale Algorithmen
10-20
Funktionsanwendungen
f (p , q, x , y ) = if p ∧ q then 2 · x + 1 else 3 · y − 1 fi
f (false , false , 3, 4) = if false ∧ false then 2 · x + 1
else 3 · y − 1 fi
= if false then 2 · 3 + 1 else 3 · 4 − 1 fi
= 3 · 4 − 1 = 11
x
g (x ) = if even(x ) then else 3 · x + 1 fi
2
1
g (1) = if even(1) then else 3 · 1 + 1 fi
2
=3·1+1=4
10.2 Funktionale Algorithmen
10-21
Funktionsanwendungen
h (p , q) = if p then 8 else 8/q fi
h (true , 2) = if true then 8 else 8/2 fi
=8
h (false , 2) = if false then 8 else 8/2 fi
=4
h (false , 0) = if false then 8 else 8/0 fi
undefiniert
h (true , 0) = if true then 8 else 8/0 fi
=8
Bei der Auswertung des Terms if b then t1 else t2 fi wird
zunächst der boolesche Term b ausgewertet, und dann, abhängig
vom Ergebnis, t1 oder t2 .
10.2 Funktionale Algorithmen
10-22
Funktionsdefinitionen
Eine Funktion kann auch in mehreren Gleichungen definiert
werden, jeweils für einen Teil der Argumente.
Beispiel:
f (0) = 0
f (1) = 2
f (−1) = 2
f (x ) = if x > 1 then x (x − 1) else − x (x − 1) fi
Die Auswertung erfolgt dabei von oben nach unten, wobei die erste
passende linke Seite für die Berechnung angewendet wird. Es
kommt daher auf die Reihenfolge der Gleichungen an.
10.2 Funktionale Algorithmen
10-23
Funktionsdefinitionen
Folgendes Gleichungssystem definiert eine andere Funktion.
f (−1) = 2
f (x ) = if x > 1 then x (x − 1) else − x (x − 1) fi
f (1) = 2
f (0) = 0
Hier sind die letzten beiden Gleichungen überflüssig.
Man kann mehrere Definitionsgleichungen immer in einer
Gleichung zusammenfassen, indem man geschachtelte
if-then-else-fi Konstrukte verwendet.
10.2 Funktionale Algorithmen
10-24
Funktionsdefinitionen
Das erste Beispiel oben lässt sich in einer Gleichung schreiben:
f (x ) = if x = 0 then 0
else if x = 1 then 2
else if x = −1 then 2
else if x > 1 then x (x − 1)
else − x (x − 1)
fi fi fi fi
Die Schreibweise mit mehreren Gleichungen ist in der Regel
übersichtlicher.
10.2 Funktionale Algorithmen
10-25
Funktionsdefinitionen
Ein Wächter (guard) ist eine Vorbedingung, die für die Anwendung
einer Definitionsgleichung erfüllt sein muss.
Beispiel:
f (0) = 0
f (1) = 2
f (−1) = 2
x > 1 :f (x ) = x (x − 1)
f (x ) = −x (x − 1)
10.2 Funktionale Algorithmen
10-26
Funktionsdefinitionen
Eine Funktionsdefinition kann unvollständig sein.
Beispiel:
f (0) = 0
f (1) = 2
f (−1) = 2
x > 1 :f (x ) = x (x − 1)
Gegenüber dem vorigen Beispiel fehlt hier die letzte Gleichung. Es
gibt daher keine Berechnungsvorschrift für Werte < −1. D. h., die
Funktion ist dort nicht definiert.
10.2 Funktionale Algorithmen
10-27
Funktionsdefinitionen
Funktionen können unter Verwendung von Hilfsfunktionen definiert
werden.
Beispiel:
Volumen(h , r , a , b , c ) = ZylVol(h , r ) + QuadVol(a , b , c )
ZylVol(h , r ) = h · KreisFl(r )
KreisFl(r ) = πr 2
QuadVol(a , b , c ) = a · b · c
Einsetzen führt zu einer einzeiligen Definition:
Volumen(h , r , a , b , c ) = h πr 2 + a · b · c
10.2 Funktionale Algorithmen
10-28
Auswertung von Funktionen
Volumen(3, 2, 5, 1, 5) = ZylVol(3, 2) + QuadVol(5, 1, 5)
= 3 · KreisFl(2) + QuadVol(5, 1, 5)
= 3π22 + QuadVol(5, 1, 5)
= 3π22 + 5 · 1 · 5
= 12π + 25
≈ 62.699111843
Alternativ kann einem Term ein Name gegeben werden, der dann
(mehrfach) verwendet werden kann:
f (a , b ) = x · x where x = a + b
ist gleichbedeutend mit
f (a , b ) = (a + b ) · (a + b ).
10.2 Funktionale Algorithmen
10-29
Applikative Algorithmen
Ein applikativer (funktionaler) Algorithmus ist eine Liste von
Funktionsdefinitionen
f1 (x1,1 , . . . , x1,n1 ) = t1 (x1,1 , . . . , x1,n1 ),
f2 (x2,1 , . . . , x2,n2 ) = t2 (x2,1 , . . . , x2,n2 ),
.. ..
.=.
fm (xm,1 , . . . , xm,nm ) = tm (xm,1 , . . . , xm,nm ).
Die erste Funktion ist die Bedeutung (Semantik) des Algorithmus.
Die Funktion wird für eine Eingabe (a1 , . . . , an1 ) wie beschrieben
ausgewertet. Die Ausgabe ist f1 (a1 , . . . , an1 ).
10.2 Funktionale Algorithmen
10-30
Gültigkeit und Sichtbarkeit
Beispiel:
f (a , b ) = g (b ) + a
g (a ) = a · b
b=3
f (1, 2) = g (2) + 1 = 2 · b + 1 = 2 · 3 + 1 = 7
Die globale Definition von b wird in der Definition von f durch die
lokale Definition verdeckt. Es treten also auch hier die Fragen nach
dem Gültigkeits- und dem Sichtbarkeitsbereich von Variablen auf,
wie wir sie in Kapitel 2 bei den imperativen Algorithmen
angetroffen haben.
10.2 Funktionale Algorithmen
10-31
Undefinierte Funktionswerte
Die Fakultätsfunktion ist definiert durch:
Fac(0) = 1
Fac(n) = n · Fac(n − 1)
Für negative Parameter terminiert die Berechnung nicht:
Fac(−1) = −1 · Fac(−2) = −1 · −2 · Fac(−3) = · · ·
Die Funktion Fac ist also partiell. Es gibt drei mögliche Ursachen
für undefinierte Funktionswerte:
◮
Die Parameter führen zu einer nicht terminierenden
Berechnung.
◮
Eine aufgerufene Funktion ist für einen Parameter undefiniert
(zum Beispiel Division durch 0).
◮
Die Funktion ist unvollständig definiert.
10.2 Funktionale Algorithmen
10-32
Komplexe Datentyen
Komplexe Datentypen (Datenstrukturen) entstehen durch
Kombination elementarer Datentypen und besitzen spezifische
Operationen. Sie können vorgegeben oder selbstdefiniert sein.
Die grundlegenden Datentypen werden auch Atome genannt.
Übliche Atome sind die Typen int, bool, float und char sowie
Variationen davon.
Es gibt in Bezug auf das Anwendungsspektrum eine
Unterscheidung in
◮
generische Datentypen: Sie werden für eine große Gruppe
ähnlicher Problemstellungen entworfen und sind oft im
Sprachumfang enthalten (Liste, Keller, Feld, Verzeichnis, . . . ).
◮
spezifische Datentypen: Sie dienen der Lösung einer eng
umschriebenen Problemstellung und werden im
Zusammenhang mit einem konkreten Problem definiert
(Adresse, Person, Krankenschein, . . . ).
10.2 Funktionale Algorithmen
10-33
Generische Datentypen der funktionalen
Programmierung
In der funktionalen Programmierung spielen die folgenden
generischen Datentypen eine hervorgehobene Rolle:
◮
Listen,
◮
Texte (Liste von Zeichen),
◮
Tupel,
◮
Funktionen.
10.2 Funktionale Algorithmen
10-34
Listen
Die Datenstruktur funktionaler Sprachen und Programmierung. Die
funktionale Programmierung wurde anfangs auch
Listenverarbeitung genannt. Lisp steht für „List Processor“.
Beispiele:
◮
Liste von Messwerten, geordnet nach
Aufzeichnungszeitpunkt, z. B. Zimmertemperatur (° C) im
Informatikzentrum nach Ankunft:
[17.0, 17.0, 17.1, 17.2, 17.4, 17.8].
◮
Alphabetisch geordnete Liste von Namen z. B. Teilnehmer der
kleinen Übung: [„Baltus“, „Bergmann“, „Cäsar“].
◮
Alphabetisch geordnete Liste von Namen mit Vornamen, d. h.
Liste von zweielementigen Listen mit Namen und Vornamen:
[[„Kundera“, „M.“], [„Hesse“, „S.“], [„Einstein“, „A.“]].
10.2 Funktionale Algorithmen
10-35
Listen
Syntax und Semantik:
◮
[D ] ist der Datentyp der Listen, d. h. der endlichen Folgen,
über D .
◮
Notation: [x1 , x2 , . . . , xn ] ∈ [D ] für x1 , x2 , . . . , xn ∈ D .
Beispiele:
◮
[real]: Menge aller Listen von Fließkommazahlen, z. B.
Messwerte, Vektoren über R.
◮
[char]: Menge aller Listen von Buchstaben, z. B. Namen,
Befunde, Adressen.
◮
[[char]]: Menge aller Listen von Listen von Buchstaben, z. B.
Namensliste.
10.2 Funktionale Algorithmen
10-36
Typische Listenoperationen
◮
[]: leere Liste.
◮
e: l: Verlängerung einer Liste l nach vorn um ein
Einzelelement e , z. B. 1 : [2, 3] = [1, 2, 3].
◮
length(l): Länge einer Liste l , z. B. length ([4, 5, 6]) = 3.
◮
head(l): erstes Element e einer nichtleeren Liste l = e : l ′ ,
z. B. head ([1, 2, 3]) = 1.
◮
tail(l): Restliste l ′ einer nichtleeren Liste l = e : l ′ nach
Entfernen des ersten Elementes, z. B. tail ([1, 2, 3]) = [2, 3].
◮
last(l): letztes Element einer nichtleeren Liste, z. B.
last ([1, 2, 3]) = 3.
10.2 Funktionale Algorithmen
10-37
Typische Listenoperationen
◮
init(l): Restliste einer nichtleeren Liste nach Entfernen des
letzten Elements, z. B. init ([1, 2, 3]) = [1, 2].
◮
l++l’: Verkettung zweier Listen l und l ′ , z. B.
[1, 2] + +[3, 4] = [1, 2, 3, 4].
◮
l!!n: Das n-te Element der Liste l , wobei 1 ≤ n ≤ length (l ),
z. B. [2, 3, 4, 5]!!3 = 4.
Vergleichsoperationen = und ,:
(e1 : t1 ) = (e2 : t2 ) ⇔ e1 = e2 ∧ t1 = t2 ,
l1 , l2 ⇔ ¬(l1 = l2 ).



[i , i + 1, i + 2, . . . , j − 1, j ]
[i , . . . , j ] = 

[]
10.2 Funktionale Algorithmen
falls i ≤ j ,
falls i > j .
10-38
Typische Listenoperationen
Die folgende Funktion berechnet rekursiv das Spiegelbild einer
Liste.
mirror :[int ] → [int ]
mirror ([]) = []
mirror (l ) = last (l ) : mirror (init (l ))
mirror ([1, 2, 3, 4]) = 4 : mirror (init ([1, 2, 3, 4]))
= 4 : mirror ([1, 2, 3]) = 4 : (3 : mirror ([1, 2]))
= 4 : (3 : (2 : mirror ([1])))
= 4 : (3 : (2 : (1 : mirror ([]))))
= 4 : (3 : (2 : (1 : [])))
= 4 : (3 : (2 : [1])) = 4 : (3 : [2, 1])
= 4 : [3, 2, 1] = [4, 3, 2, 1]
10.2 Funktionale Algorithmen
10-39
Typische Listenoperationen
Die folgende Funktion berechnet rekursiv das Produkt der
Elemente einer Liste.
prod :[int ] → int
prod ([]) = 1
prod (l ) = head (l ) · prod (tail (l ))
Die folgende Funktion konkateniert rekursiv eine Liste von Listen.
concat :[[t ]] → [t ]
concat ([]) = []
concat (l ) = head (l ) + +concat (tail (l ))
concat ([[1, 2], [], [3], [4, 5, 6]]) = [1, 2, 3, 4, 5, 6]
10.2 Funktionale Algorithmen
10-40
Sortierverfahren
Alle Algorithmen aus den vorhergehenden Kapiteln lassen sich
auch funktional beschreiben, häufig sehr viel eleganter. Als
Beispiel betrachten wir zwei Sortierverfahren.
Wiederholung: Es sei eine Ordungsrelation ≤ auf dem
Elementdatentyp D gegeben.
◮
◮
Eine Liste l = (x1 , . . . , xn ) ∈ [D ] heißt sortiert, wenn
x1 ≤ x2 ≤ · · · ≤ xn gilt.
Eine Liste l ′ = [D ] heißt Sortierung von l ∈ [D ], wenn l und l ′
die gleichen Elemente haben und l ′ sortiert ist.
◮
Eine Sortierung l ′ von l heißt stabil, wenn sie gleiche
Listenelemente nicht in ihrer Reihenfolge vertauscht.
l = [5, 9, 3, 8, 8], l ′ = [3, 5, 8, 8, 9] (nicht stabil wäre
l ′′ = [3, 5, 8, 8, 9])
◮
Ein Sortieralgorithmus heißt stabil, wenn er stabile
Sortierungen liefert
10.2 Funktionale Algorithmen
10-41
Sortieren durch Einfügen
Beim Sortieren durch Einfügen wird die Ordnung hergestellt, indem
jedes Element an der korrekten Position im Feld eingefügt wird.
insert (x , [])
= [x ]
x ≤ y :insert (x , y : l ) = x : y : l
insert (x , y : l ) = y : insert (x , l )
Für das Sortieren einer unsortierten Liste gibt es zwei Varianten:
sort 1([]) = []
sort 1(l ) = insert (head (l ), sort 1(tail (l )))
sort 2([]) = []
sort 2(l ) = insert (last (l ), sort 2(init (l )))
Welche dieser Algorithmen sind stabil?
10.2 Funktionale Algorithmen
10-42
Sortieren durch Auswählen
Beim Sortieren durch Auswählen wird das kleinste (größte)
Element an den Anfang (das Ende) der sortierten Liste angefügt.
Die folgende Funktion löscht ein Element aus einer Liste:
delete (x , [])
= []
x = y :delete (x , y : l ) = l
delete (x , y : l ) = y : delete (x , l )
Für das Sortieren einer unsortierten Liste gibt es wieder zwei
Varianten:
sort 3(l ) = x : sort 3(delete (x , l ))
where x = min(l )
sort 4(l ) = sort 4(delete (x , l )) + +[x ] where x = max (l )
Wie lauten min und max ? Was lässt sich über die Stabilität dieser
beiden Algorithmen aussagen?
10.2 Funktionale Algorithmen
10-43
Extensionale und intensionale Beschreibungen
Bisher wurden Listen durch Aufzählung oder Konstruktion
beschrieben. Man spricht von einer extensionalen Beschreibung.
Mengen werden implizit durch einen Ausdruck der Form
{t (x ) | p (x )} angegeben.
Beispiel: {x 2 | x ∈ N ∧ x mod 2 = 0} = {4, 16, . . .}
Analog hierzu bedeutet
[t (x ) | x ← l , p (x )]
die Liste aller Werte t (x ), die man erhält, wenn x die Liste l
durchläuft, wobei nur die Elemente aus l ausgewählt werden, die
der Bedingung p (x ) genügen.
[t (x ) | x ← l , p (x )] ist eine intensionale Definition. t (x ) ist ein Term.
x ← l heißt Generator und p (x ) ist eine Auswahlbedingung.
10.2 Funktionale Algorithmen
10-44
Intensionale Beschreibungen
[x | x ← [1 . . . 5]] = [1, 2, 3, 4, 5]
[x 2 | x ← [1 . . . 5]] = [1, 4, 9, 16, 25]
[x 2 | x ← [1 . . . 5], odd (x )] = [1, 9, 25]
Eine intensionale Beschreibung kann auch mehrere Generatoren
enthalten:
[x 2 − y | x ← [1 . . . 3], y ← [1 . . . 3]] = [0, −1, −2, 3, 2, 1, 8, 7, 6]
[x 2 − y | x ← [1 . . . 3], y ← [1 . . . x ]] = [0, 3, 2, 8, 7, 6]
[x 2 − y | x ← [1 . . . 4], odd (x ), y ← [1 . . . x ]] = [0, 8, 7, 6]
[x 2 − y | x ← [1 . . . 4], y ← [1 . . . x ], odd (x )] = [0, 8, 7, 6]
Man vergleiche die Effizienz der beiden letzten Beschreibungen.
10.2 Funktionale Algorithmen
10-45
Intensionale Beschreibungen
teiler (n) = [i | i ← [1 . . . n], n mod i = 0]
teiler (18) = [1, 2, 3, 6, 9, 18]
ggT (a , b ) = max ([d | d ← teiler (a ), b mod d = 0])
ggT (18, 8) = max ([d | d ← [1, 2, 3, 6, 9, 18], 8 mod d = 0])
= max ([1, 2]) = 2
primzahl (n) = (teiler (n) = [1, n])
primzahl (17) = (teiler (17) = [1, 17]) = true
concat (l ) = [x | l ′ ← l , x ← l ′ ]
concat ([[1, 2, 3],[4, 5, 6]]) = [1, 2, 3, 4, 5, 6]
10.2 Funktionale Algorithmen
10-46
Tupel
Tupel sind Listen fester Länge.
Beispiele:
◮
◮
◮
(1.0, −3.2) als Darstellung für die komplexe Zahl 1 − 3.2i .
(4, 27) als Abbildung eines Messzeitpunkts (4 ms ) auf einen
Messwert (27 V ).
(2, 3.4, 5) als Darstellung eines Vektors im R3 .
Der Typ t eines Tupels ist das kartesische Produkt der seiner
Elementtypen: t = t1 × t2 × . . . × tn
Schreibweise für Elemente des Typs t : (x1 , x2 , . . . , xn ) Man nennt
(x1 , x2 , . . . , xn ) ein n-Tupel.
Tupel sind grundlegende Typen aller funktionalen Sprachen.
10.2 Funktionale Algorithmen
10-47
Tupel
Auf der Basis von Tupeln lassen sich spezifische Datentypen
definieren:
◮
date: int × text × int . Datumsangaben mit Werten wie (2, „Mai“,
2001). Es dürfen nur gültige Werte aufgenommen werden.
◮
rat: int × int . Rationale Zahlen mit Werten wie (2,3) für 23 . Das
2-Tupel (Paar) (1, 0) stellt keinen gültigen Wert dar.
Beispiele für Funktionen auf rat:
ratAdd , ratMult :rat × rat → rat
kuerze :rat → rat
kuerze (z , n) = (z div g , n div g ) where g = ggT (z , n)
ratAdd ((z1 , n1 ), (z2 , n2 )) = kuerze (z1 n2 + z2 n1 , n1 n2 )
ratMult ((z1 , n1 ), (z2 , n2 )) = kuerze (z1 z2 , n1 n2 )
10.2 Funktionale Algorithmen
10-48
Funktionen höherer Ordnung
◮
Funktionen als Datentypen machen es möglich, Funktionen
auf Funktionen anzuwenden.
◮
Eine Funktion f : A → B ist vom Typ A → B .
◮
Die Operation → sei rechtsassoziativ, d. h.
A → B → C = A → (B → C )
10.2 Funktionale Algorithmen
10-49
Currying
◮
Das Currying vermeidet kartesische Produkte: Eine Abbildung
f :A ×B →C
kann als eine Abbildung
f : A → (B → C ) = A → B → C
gesehen werden.
◮
Beispiel: f : int × int mit f (x , y ) = x + y entspricht
fg : int → int → int mit f (x ) = gx : int → int und
gx (y ) = x + y . Hintereinanderausführung:
(fg (x ))(y ) = gx (y ) = x + y = f (x , y )
10.2 Funktionale Algorithmen
10-50
Funktionen höherer Ordnung
Funktionen können als Werte und Argumente auftreten.
Beispiel: Ein Filter, der aus einer Liste diejenigen Elemente
auswählt, die einer booleschen Bedingung genügen.
Spezifikation:
filter (p , l ) = [x | x ← l , p (x )]
Definition:
filter
filter (p , [])
: (t → bool ) × [t ] → [t ]
= []
p (x ) :filter (p , x : l ) = x : filter (p , l )
filter (p , x : l ) = filter (p , l )
10.2 Funktionale Algorithmen
10-51
Funktionen höherer Ordnung
Fortsetzung zum Filter, Anwendung:
p : int → bool
even(i ) :p (i )
p (i )
= true
= false
filter (p , [1 . . . 5]) = [2, 4]
10.2 Funktionale Algorithmen
10-52
Deduktive Algorithmen
deduktiver Algorithmus
logische
Aussagen
Anfrage
Auswertungsalgorithmus
fu
¨r Anfragen
Logisches Paradigma
Die wichtigste logische Programmiersprache ist Prolog.
10.3 Prädikatenlogik
10-53
Prädikatenlogik
◮
Grundlage des logischen Paradigmas ist die Prädikatenlogik.
◮
Beispiel einer Aussage: „Susanne ist Tochter von Petra“.
◮
Eine Aussageform ist eine Aussage mit Unbestimmten: x ist
Tochter von y .
◮
Durch eine Belegung der Unbestimmten kann eine
Aussageform in eine Aussage transformiert werden:
x ← Susanne , y ← Petra .
◮
Statt natürlichsprachiger Aussagen und Aussageformen,
werden in deduktiven Algorithmen atomare Formeln
verwendet: Tochter (x , y ).
10.3 Prädikatenlogik
10-54
Prädikatenlogik
Alphabet:
◮
Konstante: a , b , c , . . ..
◮
Unbestimmte/Variable: x , y , z , . . ..
◮
Prädikatssymbole: P , Q , R , . . . mit Stelligkeit.
◮
Logische Verknüpfungen: ∧, ⇒, . . ..
Atomare Formeln: P (t1 , . . . , tn ).
Fakten: Atomare Formeln ohne Unbestimmte.
Regeln haben die Form (αi ist atomare Formel):
α1 ∧ α2 ∧ · · · ∧ αn ⇒ α0
α1 ∧ α2 ∧ · · · ∧ αn wird als Prämisse, α0 als Konklusion der Regel
bezeichnet.
10.3 Prädikatenlogik
10-55
Beispiel
Zwei Fakten:
◮
Tochter (Susanne , Petra )
◮
Tochter (Petra , Rita )
Eine Regel mit Unbestimmten:
◮
Tochter (x , y ) ∧ Tochter (y , z ) ⇒ Enkelin(x , z )
Die Ableitung neuer Fakten erfolgt analog zur Implikation:
1. Finde eine Belegung der Unbestimmten einer Regel, so dass
auf der linken Seite (Prämisse) bekannte Fakten stehen.
2. Die rechte Seite ergibt den neuen Fakt.
10.3 Prädikatenlogik
10-56
Beispiel
Belegung der Unbestimmten der Regel:
x ← Susanne , y ← Petra , z ← Rita
Anwendung der Regel ergibt neuen Fakt: Enkelin(Susanne , Rita )
(Erste) Idee deduktiver Algorithmen:
1. Definition einer Menge von Fakten und Regeln sowie einer
Anfrage in Form einer zu prüfenden Aussage; z. B.
Enkelin(Susanne , Rita ).
2. Prüfen und Anwenden der Regeln, bis keine neuen Fakten
mehr erzeugt werden können.
3. Prüfen, ob Anfrage in Faktenmenge enthalten ist.
10.3 Prädikatenlogik
10-57
Deduktive Algorithmen
◮
Ein deduktiver Algorithmus D besteht aus einer Menge von
Fakten und Regeln.
◮
Aus einem deduktiven Algorithmus sind neue Fakten
ableitbar. Die Menge aller Fakten F (D ) enthält alle direkt oder
indirekt aus D ableitbaren Fakten.
◮
Ein deduktiver Algorithmus definiert keine Ausgabefunktion
wie applikative oder imperative Algorithmen. Erst die
Beantwortung von Anfragen liefert ein Ergebnis.
◮
Eine Anfrage γ ist eine Konjunktion von atomaren Formeln mit
Unbestimmten: γ = α1 ∧ α2 ∧ · · · ∧ αn
10.4 Deduktive Algorithmen
10-58
Beispiel: Addition zweier Zahlen
Fakten:
◮
suc (n, n + 1) für alle n ∈ Ž
Regeln:
1. true ⇒ add (x , 0, x )
2. add (x , y , z ) ∧ suc (y , v ) ∧ suc (z , w ) ⇒ add (x , v , w )
Anfrage: add (3, 2, 5) liefert true.
Auswertung:
◮
Regel 1 mit der Belegung x = 3: add (3, 0, 3)
◮
Regel 2 mit der Belegung x = 3, y = 0, z = 3, v = 1, w = 4:
add (3, 1, 4)
◮
Regel 2 mit der Belegung x = 3, y = 1, z = 4, v = 2, w = 5:
add (3, 2, 5)
10.4 Deduktive Algorithmen
10-59
Beispiel: Addition zweier Zahlen
◮
add (3, 2, x ) liefert x = 5.
◮
add (3, x , 5) liefert x = 2.
◮
add (x , y , 5) liefert
(x , y ) ∈ {(0, 5), (1, 4), (2, 3), (3, 2), (4, 1), (5, 0)}.
◮
add (x , y , z ) liefert eine unendliche Ergebnismenge.
◮
add (x , x , 4) liefert x = 2.
◮
add (x , x , x ) liefert x = 0.
◮
add (x , x , z ) ∧ add (x , z , 90) liefert (x , z ) = (30, 60).
Deduktive Algorithmen sind deklarativ (s. oben). Im Vergleich zu
applikativen und imperativen Algorithmen sind sie sehr flexibel –
und häufig ineffizient.
10.4 Deduktive Algorithmen
10-60
Auswertungsalgorithmus
Dieser informelle nichtdeterministische Algorithmus wertet
Anfragen aus:
1. Starte mit der Anfrage γ (anzusehen als Menge atomarer
Formeln).
2. Suche Belegungen, die entweder
◮ einen Teil von γ mit Fakten gleichsetzen (Belegung von
Unbestimmten von γ) oder
◮ einen Fakt aus γ mit einer rechten Seite einer Regel
gleichsetzen (Belegungen von Unbestimmten in einer Regel).
Setze diese Belegung ein.
3. Wende passende Regeln rückwärts an, ersetze also in γ die
Konklusion durch die Prämisse.
4. Entferne gefundene Fakten aus der Menge γ.
5. Wiederhole diese Schritte bis γ leer ist.
10.4 Deduktive Algorithmen
10-61
Beispiel: Addition zweier Zahlen
1. add (3, 2, 5).
2. add (3, y ′ , z ′ ), suc (y ′ , 2), suc (z ′ , 5).
3. y ′ = 1, dadurch Fakt suc (1, 2) streichen.
4. add (3, 1, z ′ ), suc (z ′ , 5).
5. z ′ = 4, dadurch Fakt suc (4, 5) streichen.
6. add (3, 1, 4).
7. add (3, y ′′ , z ′′ ), suc (y ′′ , 1), suc (z ′′ , 4).
8. y ′′ = 0, z ′′ = 3 beide Fakten streichen.
9. add (3, 0, 3) streichen, damit zu bearbeitende Menge leer, also
10. true .
10.4 Deduktive Algorithmen
10-62
Beispiel: Addition zweier Zahlen
1. add (3, 2, x ).
2. add (3, y ′ , z ′ ), suc (y ′ , 2), suc (z ′ , x ).
3. y ′ = 1, dadurch Fakt suc (1, 2), streichen.
4. add (3, 1, z ′ ), suc (z ′ , x ).
5. add (3, y ′′ , z ′′ ), suc (y ′′ , 1), suc (z ′′ , z ′ ), suc (z ′ , x ).
6. y ′′ = 0, dadurch Fakt suc (0, 1), streichen.
7. add (3, 0, z ′′ ), suc (z ′′ , z ′ ), suc (z ′ , x ).
8. z ′′ = 3, dadurch Regel 2 erfüllt, streichen.
9. suc (3, z ′ ), suc (z ′ , x ).
10. z ′ = 4, dadurch Fakt suc (3, 4) streichen.
11. suc (4, x ).
12. x = 5, die zu bearbeitende Menge ist leer und eine Lösung
für x bestimmt.
10.4 Deduktive Algorithmen
10-63
Deduktive Algorithmen
◮
Für eine Anfrage können unendlich viele Lösungen existieren:
add (x , y , z ).
◮
Die Bestimmung aller möglichen Berechnungspfade kann
durch Backtracking erfolgen.
Das angegebene Verfahren ist sehr vereinfacht:
◮
◮
◮
10.4 Deduktive Algorithmen
Wie wird verhindert, dass ein unendlich langer Weg
eingeschlagen wird?
Was ist mit Negationen?
10-64