Skript zur Vorlesung + Präsenzübungen - public.fh

Werbung
Unterlagen zur Vorlesung
Algorithmik I
– SS 06 –
Semester
2
Studiengänge
MI/PI
R. Rüdiger
Fachbereich Informatik
FH Braunschweig/Wolfenbüttel
Wolfenbüttel, im Februar 2006
Inhaltsverzeichnis
Hinweise und Erläuterungen zur Literatur
1
1 Einführung und Grundlagen
1.1 Vorbemerkungen zur Notation und Implementierung
1.2 Algorithmen . . . . . . . . . . . . . . . . . . . . . . .
1.3 Komplexität von Algorithmen . . . . . . . . . . . . .
1.4 Beispiele zu diversen Komplexitätsklassen . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
6
10
25
33
2 Fundamentale Datenstrukturen
39
2.1 Allgemeines . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.2 Stacks und FIFO-Schlangen . . . . . . . . . . . . . . . . . . . 39
2.3 Suchalgorithmen . . . . . . . . . . . . . . . . . . . . . . . . . 42
3 Sortieren
3.1 Allgemeines . . . . . . . . . . . . . . . . . .
3.2 Sortieren von Arrays – elementare Methoden
3.3 Sortieren von Arrays – schnelle Methoden .
3.4 Sortieren von Sequenzen . . . . . . . . . . .
3.5 Sortieren in linearer Zeit . . . . . . . . . . .
4 Rekursion
4.1 Allgemeines . . . . . . . . . .
4.2 Backtracking . . . . . . . . .
4.3 Eine Anwendung: Mini-Parser
4.4 Heapsort rekursiv . . . . . . .
4.5 Das Acht-Damen-Problem . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
48
48
49
51
62
65
.
.
.
.
.
67
67
69
72
74
75
5 Dynamische Datenstrukturen
80
5.1 Allgemeines . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
5.2 Verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.3 Anwendungsbeispiel: Graphik-Editor . . . . . . . . . . . . . . 87
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Inhaltsverzeichnis
5.4
5.5
ii
Binäre Suchbäume (BST) . . . . . . . . . . . . . . . . . . . . 97
B-Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
6 Hashverfahren
6.1 Allgemeines . . . . . . . . . . . . . . . . . .
6.2 Statische Verfahren . . . . . . . . . . . . . .
6.3 Halbdynamische und dynamische Verfahren
6.4 Java-Klasse Hashtable . . . . . . . . . . . .
7 Präsenzübungen
7.1 Präsenzübungen
7.2 Präsenzübungen
7.3 Präsenzübungen
7.4 Präsenzübungen
7.5 Präsenzübungen
7.6 Präsenzübungen
7.7 Präsenzübungen
.
.
.
.
110
110
112
113
114
zur ersten Einführungsvorlesung . . . . . . .
zu Kapitel 1: Einführung und Grundlagen . .
zu Kapitel 2: Fundamentale Datenstrukturen
zu Kapitel 3: Sortieren . . . . . . . . . . . . .
zu Kapitel 4: Rekursion . . . . . . . . . . . .
zu Kapitel 5: Dynamische Datenstrukturen .
zu Kapitel 6: Hashverfahren . . . . . . . . . .
116
116
117
121
122
124
126
126
R. Rüdiger
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Algorithmik I
— 1. März 2006 —
Hinweise und Erläuterungen
zur Literatur
Im folgenden sind einige Hinweise zu Büchern zusammengestellt, die für die
Vorlesung von Bedeutung sind. Nicht immer ist die neueste Auflage zitiert.
Vor dem Kauf eines Buches sollten Sie in jedem Fall sorgfältig prüfen, ob es
Ihnen vom Stil, Inhalt, Niveau usw. her zusagt: nicht jedem gefällt alles . . . !
Alle Bücher, vielleicht mit Ausnahme einiger ganz neuer, sollten in einer der
Bibliotheken (FH, TU) vorhanden sein. Die zitierten Bücher gehen durchweg
vom Stoffumfang weit über die Vorlesungsinhalte hinaus.
Algorithmen allgemein
• Knuth [Knu97, Knu98a, Knu98b]: Diese Werke, die vor ein paar Jahren
in einer neuen Auflage erschienen sind, gelten als enzyklopädische Werke und wissenschaftliche Basis für die Thematik Algorithmen im weiten
Sinne und gehen in praktisch jeder Hinsicht über die Vorlesung hinaus.
Insbesondere mathematische Verfahren zur Komplexitätsanalyse werden
in diesen Bänden detailliert behandelt. Viele andere Bücher beruhen auf
dieser Arbeit von Knuth.
• Cormen, Leiserson, Rivest, Stein (CLRS) [CLRS01]: zwar von ganz
anderer Art als die Knuthschen Bücher, aber ebenfalls ein enzyklopädisches Werk über Algorithmen, das mehr oder weniger (fast) alles enthält,
was heute über Algorithmen bekannt ist. Erwähnenswert ist, dass einer
der Autoren zu den Erfindern des heute allgemein bekannten und verwendeten RSA-Verfahrens aus der Kryptologie gehört, nämlich Rivest. Der
zweite der Autoren, Leiserson, ist auch beteiligt an den bekannten MITlectures mit der Thematik Algorithmen. Neben ausformulierten Beispielen
in Java wird der Pseudo-Code aus diesem Buch ( CLRS-artiger Code“)
”
im vorliegenden Skript verwendet.
• Wirth [Wir86]: einer der absoluten Klassiker. Generationen von InformaR. Rüdiger
Algorithmik I
— 1. März 2006 —
Hinweise und Erläuterungen zur Literatur
2
tikern haben das Thema Algorithmen und Datenstrukturen nach diesem
Buch gelernt. Viele wesentlich neuere Bücher basieren auf diesem Band.
Die Gliederung und wesentliche Inhalte dieses Skripts basieren ebenfalls
auf diesem Buch.
• Sedgewick [Sed92]: Einer der Klassiker zum Thema Algorithmen und
Datenstrukturen. Ausführlich, verhältnismäßig elementar, aber sehr umfangreich.
• Ottmann, Widmayer [OW90]: gilt im deutschsprachigen Raum als eines
der Standardwerke; sehr umfangreich und zum Nachlesen insbesondere
mathematischer Details besonders geeignet.
• Schöning [Sch01, Sch02]: etwas mehr mathematisch ausgerichtete Darstellungen (Der Autor vertritt die Theoretische Informatik in Ulm.). Es
enthält aber auch sehr aktuelle, neue Ergebnisse zu Algortihmen.
Algorithmen mit Java als Implementierungssprache
• Doberkat, Dißmann [DD02]: Java-Einführung mit einem gewissen Schwerpunkt auf der Thematik Algorithmen. Besonderheit des Buches ist, dass
eine besondere Betonung auf die Darstellung von Konzepten gelegt wird.
Eine Reihe von Beispielen in diesem Skript stammen aus diesem Buch.
• Saake, Sattler [SS02]: eine sehr fundierte und ausserordentlich empfehlenswerte Darstellung von Algorithmen, die sich ausdrücklich auf Java
als konkrete Implementierungssprache abstützt. Inzwischen erschienen in
einer überarbeiteten und erweiterten 2. Auflage.
• Goodrich, Tamassia; Preiss [GT01, Pre99]: zwei sehr moderne Darstellungen der Thematik mit Java, die allerdings anspruchsvoll sind und
über die Vorlesung erheblich hinausgehen.
• Poetzsch-Heffter [PH00]: eine äußerst attraktive Einführung in Java,
deren bemerkenswerteste Eigenschaft ist, dass Konzepte zunächst stets
auf allgemeinem Hintergrund besprochen werden, bevor die Java-Details
behandelt werden. Für Leser mit wenig Erfahrung in der Programmierung
aber dadurch auch etwas schwierig.
• Jobst [Job99]: eigentlich eine Einführung in Java. Enthält aber auch
schöne Beispiele zu Algorithmen als Applets, z.B. Türme von Hanoi als
animiertes Applet, ebenso das Acht-Damen-Problem.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Hinweise und Erläuterungen zur Literatur
3
• Isernhagen [Ise01]: Im Prinzip zwar und in erster Linie eine umfangreiche und tiefgehende Einführung in C/C++ mit einer Betonung, wie
diese Sprachen sinnvoll eingesetzt werden sollten, insofern also keine JavaEinführung. Aber viele der zahlreichen Beispiele zu Algorithmen kann man
ohne große Mühe in Java umschreiben. Ist im 2. Semester besonders für
diejenigen geeignet, die jetzt Java lernen und bereits etwas Kenntnisse in
C/C++ haben. Das Übertragen von Programmen von einer in eine andere Sprache bringt häufig noch mehr Einsichten als das Neuprogrammieren
in nur einer Sprache. Inzwischen erschienen in einer überarbeiteten und
erweiterten 4. Auflage (gemeinsam mit H. Helmke).
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Literaturverzeichnis
[CLRS01] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and
Clifford Stein. Introduction to Algorithms. MIT Press, Cambridge,
Massachusetts, London, England, 2001.
[DD02]
Ernst-Erich Doberkat and Stefan Dißmann. Einführung in die objektorientierte Programmierung mit Java. Oldenbourg, 2 edition,
2002.
[Eng88]
H. Engesser, editor. Duden Informatik. Dudenverlag, 1988.
[GT01]
Michael T. Goodrich and Roberto Tamassia. Data Structures and
Algorithms in Java. John Wiley, 2001.
[Her95]
Dietmar Herrmann. Algorithmen Arbeitsbuch. Addison Wesley,
1995.
[Ise01]
Rolf Isernhagen. Softwaretechnik in C und C++. Hanser, 3 edition, 2001.
[Job99]
Fritz Jobst. Programmieren in Java. Hanser, 1999.
[Knu97]
Donald E. Knuth. The Art of Computer Programming. Fundamental Algorithms, volume 1. Addison Wesley, 3 edition, 1997.
[Knu98a] Donald E. Knuth. The Art of Computer Programming. Seminumerical Algorithms, volume 2. Addison Wesley, 3 edition, 1998.
[Knu98b] Donald E. Knuth. The Art of Computer Programming. Sorting
and Searching, volume 3. Addison Wesley, 2 edition, 1998.
[OW90]
Thomas Ottmann and Peter Widmayer. Algorithmen und Datenstrukturen. Wissenschaftsverlag, 1990.
[PH00]
Arnd Poetzsch-Heffter. Konzepte objektorientierter Programmierung. Mit einer Einführung in Java. Springer, 2000.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
LITERATURVERZEICHNIS
5
[Pre99]
Bruno R. Preiss. Data Structures and Algorithms with ObjectOriented Design Patterns in Java. John Wiley, 1999.
[Sch01]
Uwe Schöning.
2001.
[Sch02]
Uwe Schöning. Ideen der Informatik. Grundlegende Modelle und
Konzepte. Oldenbourg, 2002.
[Sed92]
Robert Sedgewick. Algorithmen. Addison-Wesley, 1992.
[SS02]
Gunther Saake and Kai-Uwe Sattler. Algorithmen & Datenstrukturen. dpunkt.verlag, 2002.
[Wir86]
N. Wirth. Algorithmen und Datenstrukturen mit Modula-2. Teubner, Stuttgart, 1986.
Algorithmik.
Spektrum Akademischer Verlag,
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Kapitel 1
Einführung und Grundlagen
1.1
Vorbemerkungen zur Notation und Implementierung
Um Algorithmen zu beschreiben bedarf es einer Notation. Dazu gibt es eine
ganze Reihe verschiedener Ansätze.
• Im einfachsten Fall kann man Algorithmen einfach in normaler Sprache beschreiben. In den meisten Fällen reicht das aber nicht aus, da es im
Zusammenhang mit Algorithmen fast immer um außerordentlich feine Details geht, die eine erhebliche Rolle z. B. für die Effizienz eines Algorithmus
spielen.
• Ein verbreiterter und sehr allgemeiner Ansatz ist der, ein Gemisch aus
normaler Sprache und einer mathematischen Notation zu verwenden. Der
Vorteil ist der, dass man eine Unabhängigkeit von den Details einer speziellen Programmiersprache erreicht, der Nachteil, dass ein Ausprobieren
”
des Algorithmus“ am Rechner nicht unmittelbar möglich ist.
Es ist allerdings durchaus umstritten, ob es sich dabei wirklich um einen
Nachteil handelt. Einer der bedeutendsten und sehr einflußreichen mathematisch-orientierten Informatiker, E. Dijkstra, hat stets vehement dafür
plädiert, in der Ausbildung jegliches Ausprobieren am Rechner“ strikt zu
”
unterbinden.
• Es gibt – als eine Art Kompromiß – auch Algorithmenbeschreibungssprachen, die sich an konkrete Programmiersprachen anlehnen, z.B. Adele (algorithm description language, Informatik-Handbuch), welches sich
sehr eng an die Pascal-Sprachfamilie (Pascal, Modula-2, Oberon) anlehnt.
Tatsächlich wird eine Notation dieser Art in sehr vielen Darstellungen
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.1 Vorbemerkungen zur Notation und Implementierung
7
verwendet und zwar selbst dann, wenn ausdrücklich etwa Java als Implementierungssprache gewählt wird (z. B. in [SS02]).
• Einer der bemerkenswertesten und unkonventionellen Ansätze ist der von
D. Knuth, der neben einer mathematisch orientierten Notation einen selbstdefinierten Assembler (MIX, in neuester Form MMIX) für einen fiktiven
Rechner verwendet. Seine Argumente dafür sind neben der Unabhängigkeit von konkreten und sich schnell wandelnden Sprachen die Ausprägung
eines Bewußtseins für Fragen der geschickten und effizienten Implementierung von Algorithmen auf der Hardware.
• In diesem Skript sind die Algorithmen in der Regel entweder in einem
CLRS-artigen Pseudo-Code oder in Java formuliert. Der Vorteil, eine Programmiersprache anstelle von Pseudo-Code zu verwenden, besteht natürlich darin, dass ein Ausprobieren am Rechner“ einfach möglich ist. Ei”
nige der Programme sind bewußt nicht vollständig abgedruckt; die Vervollständigung bildet dann eine Übungsaufgabe. Der Hauptnachteil einer
Darstellung in einer konkreten Programmiersprache besteht darin, dass –
anders als bei der Verwendung von Pseudo-Code – alle Einzelheiten ausgeführt werden müssen, egal, ob sie für das Verständnis wichtig sind oder
nicht. In gewisser Weise ist das anzusehen als Überspezifikation“ eines
”
Algorithmus.
Mathematische Notation
Folgende mathematische Notationen kommen im Zusammenhang mit Algorithmen regelmäßig vor:
• logische Konjunktion (UND, in Java: &&): ∧
• logische Disjunktion (ODER, in Java: ||): ∨
• logische Negation (NICHT, in Java: !): ¬
• kleinste ganze Zahl, die nicht kleiner als x ist: dxe
vgl. Abb. 1.1
• größte ganze Zahl, die nicht größer als x ist: bxc
• Summe von Termen:
s=
n
X
xi = xm + xm+1 + . . . + xn
i=m
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.1 Vorbemerkungen zur Notation und Implementierung
x x
−3
−2
x x
−1
0
1
y y
−3
−2
8
−1
2
3
y y
0
1
2
3
Abbildung 1.1: Operationen ceil und floor
Berechnung der Summe:
Algorithmus 1
1 i←m
2 s←0
3 while i ≤ n
4
do s ← s + x[i]
5
i←i+1
• Produkt von Termen:
p=
n
Y
xi = xm · xm+1 · . . . · xn
i=m
Berechnung des Produkts:
Algorithmus 2
1 i←m
2 p←1
3 while i ≤ n
4
do p ← p · x[i]
5
i←i+1
• Universeller Quantor über Indexbereich:
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.1 Vorbemerkungen zur Notation und Implementierung
9
(mit der Interpretation: die Pi sind Aussagen, und die Formel drückt aus,
dass die Pi für alle i mit m ≤ i ≤ n gelten.)
n
^
Pi = Pm ∧ Pm+1 ∧ . . . ∧ Pn
i=m
• Existenzieller Quantor über Indexbereich:
(mit der Interpretation: die Pi sind Aussagen, und die Formel drückt aus,
dass für (mindestens) ein i mit m ≤ i ≤ n die Aussage Pi gilt.)
n
_
Pi = Pm ∨ Pm+1 ∨ . . . ∨ Pn
i=m
Im Abschnitt 1.2 folgt nach einer informellen Definition des Begriffs Algorithmus eine Zusammenstellung von Beispielen für Algorithmen verschiedener
Art.
Hinweise zu Java als Implementierungssprache
Bei der Entwicklung von Java-Programmen sollte man folgende Punkte wissen und beachten:
• Eine Datei *.java kann mehrere Klassen enthalten. In Entwicklungsumgebungen kann das möglicherweise anders geregelt sein.
• Dateinamen und Klassennamen sind im Prinzip beliebig wählbar, ausgenommen in der Situation des folgenden Punktes.
• Höchstens eine Klasse kann public sein. Wenn das der Fall ist, so müssen
Dateiname und der Name dieser Klasse übereinstimmen.
• In einer Applikation muß genau eine Klasse die Methode main enthalten.
• Kompiliert werden stets Dateien (*.java).
• Gestartet wird diejenige Klasse (*.class), die die main-Methode enthält.
• public sollte in der Regel das sein, was auch andere Programmierer interessieren kann, was also in der Art von Bibliotheksklassen bzw. -methoden
verwendet werden kann. Eine generelle Faustregel: Man sollte stets so tun,
als ob man Programme für andere Programmierer entwickelt.
• Die gesamte Java-API enthält keine einzige main-Methode.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
1.2
10
Algorithmen
Grundlegendes
Algorithmen sind präzise beschriebene und automatisch durchführbare Verarbeitungsvorschriften (also Kochrezepte“).
”
Name Algorithmus: von persisch arabischem Mathematiker Ibn Mûsâ AlChwârismı̂ (9. Jahrhundert), Buch über die Regeln der Wiedereinsetzung und
Reduktion
Eigenschaften von Algorithmen (siehe dazu: Informatik-Duden [Eng88, S. 25]):
1. Algorithmen lösen stets eine ganze Klasse von Problemen; die Problemauswahl erfolgt durch Parameter.
2. Algorithmen sind in der Regel determiniert, d.h., sie liefern bei gleichen
Eingabewerten und unter gleichen Startbedingungen die gleichen Endwerte. (Es gibt aber auch nichtdeterminierte Algorithmen, z.B. stochastische
Algorithmen.)
3. Beschreibung eines Algorithmus: endlich (statische Finitheit); belegter
Speicherplatz zu jeder Zeit: ebenfalls endlich (dynamische Finitheit)
4. Resultat nach endlich vielen Schritten (Terminierung); Ausnahmen: z.B.:
Betriebssystem, Editor; in diesen Fällen erfolgt die Terminierung durch
äußeren Eingriff.
5. Algorithmus heißt deterministisch, wenn zu jedem Zeitpunkt seiner Ausführung höchstens eine Möglichkeit der Fortsetzung besteht (vgl. aber
nichtdeterministische endliche Automaten (Vorlesung Theoretische Informatik))
6. eine mögliche Formalisierung des Algorithmusbegriffs: durch Turing-Maschinen (abstraktes Maschinenmodell)
Beispiele für Algorithmen
Allgemein bekannt sind:
• z.B.: Addition, Subtraktion, Multiplikation ganzer Zahlen:
345
+ 678
1023
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
11
• oder: Fakultätsfunktion:
1
für n = 0
n! =
n(n − 1)! für n ≥ 1
Weitere Algorithmen:
Euklidischer Algorithmus
Der Euklidische Algorithmus gilt als das klassische“ Beispiel eines Algorith”
mus (s. [Eng88, S. 219]):
Er dient zur Bestimmung des größten gemeinsamen Teilers (ggT, gcd =
greatest common divisor) zweier gegebener natürlicher Zahlen a und b
z.B.: a = 693, b = 147:
a
693
147
105
42
=
=
=
=
b
4 · 147
1 · 105
2 · 42
2 · 21
+
+
+
+
r
105
42
21
0
allgemein:
r0 = a
r1 = b
r2
...
rn−2
rn−1
= q0 · b
= q1 · r 2
= q2 · r 3
... ·
= qn−2 · rn−1
= qn−1 · rn
+ r2
+ r3
+ r4
...
+ rn
+ 0
mit r2 < b = r1
mit r3 < r2
mit r4 < r3
mit rn < rn−1
Der letzte Rest 6= 0, also rn , ist der ggT von a und b.
Formulierung des Euklidischen Algorithmus in Pseudocode:
Algorithmus 3
Euclid(a, b)
1 repeat
2
r ← a mod b
3
a←b
4
b←r
5
until r = 0
6 return a
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
12
Eine Alternative, die die Substraktion verwendet:
Algorithmus 4
GCD(a, b)
1 while a 6= b
2
do if a > b
3
then a ← a − b
4
else b ← b − a
5 return a
Binäres Suchen
vgl. [Eng88, S. 85]
Binäres Suchen ist ein schnelles Verfahren zur Suche in einem z.B. aufsteigend sortierten Array (etwa Suche nach Telefonnummer im Telefonbuch).
Algorithmus 5
BinarySearch1 (a, x)
1 l←0
2 r ← length[a] − 1
3 found ← false
4 while l ≤ r and not found
5
do m ← b(l + r)/2c
6
if a[m] = x
7
then found ← true
8
else if a[m] < x
9
then l ← m + 1
10
else r ← m − 1
11 if found
12
then return m
13
else return −1
gesuchtes Element . . .
. . . rechts von der Mitte
. . . links von der Mitte
Zwei weitere Varianten werden in einem der folgenden Abschnitte vorgestellt
(in Abschnitt 2.3 auf Seite 45).
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
13
Acht-Damenproblem
vgl. [Eng88, S. 132]
Problem: gesucht eine Stellung für 8 Damen auf einem Schachbrett, so dass
diese sich entsprechend den Schachregeln nicht gegenseitig bedrohen, d. h., jede Zeile, jede Spalte und jede Diagonale darf höchstens eine Dame enthalten.
(Es gibt genau 92 Lösungen.)
Abbildung 1.2: Acht-Damen-Problem
Algorithmus ist Beispiel für sog. Backtracking-Verfahren
Hinweis: Macht die Beschäftigung mit Spielen“ einen Sinn?
”
Spieltheorie: Nobelpreis 1994 Wirtschaftswissenschaften, Reinhard Selten,
Uni Bonn (vgl. SPIEGEL 42/94, S. 134)
Problem von Ulam
Im konkreten Einzelfall statisch – d.h. aufgrund des Programmtextes – zu
entscheiden, ob ein Algorithmus (also z. B. eine While-Schleife) wirklich terminiert, kann selbst dann schwierig sein, wenn der Algorithmus extrem einfach aussieht.
Beispiel: Von dem folgenden sehr einfachen Algorithmus ist bis heute (erstaunlicherweise) unbekannt, ob er für alle n mit 1 < n ∈ N terminiert
(Problem von Ulam oder Syrakusproblem). Für endlich viele Werte, also die
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
14
10000
ersten 10, 100, 106 , 1010 (?), 1010
(?), . . . kann man es natürlich ausprobieren (ausreichend viel Geduld vorausgesetzt).
Der Algorithmus hat zwar keine besondere praktische Bedeutung. Nur: das
Terminieren von Algorithmen ist natürlich von fundamentaler Bedeutung
bei jeder Programmierung, und wenn man schon von einem derart einfachen
Algorithmus nicht statisch entscheiden kann, ob er terminiert oder nicht,
dann muß man sich bei den meist viel komplizierteren Problemen der Praxis
erst recht auf offene Fragen dieser Art einstellen.
Algorithmus 6
1 n ← Startwert
2 while n > 1
3
do if n ungerade
4
then n ← 3n + 1
5
else n ← n/2
Beispiel:
33 100
26 13
50
40
25 76 38
20 10 5
19
16
58
8
29
4
88 44 22 11
2 1
34
17
52
Türme von Hanoi
Die Türme von Hanoi sind das Standardbeispiel schlechthin, um eine nichttriviale Rekursion zu studieren und zu verstehen.
In Abb. 1.3 ist illustriert, was mit dem Spiel erreicht werden soll: der Scheibenstabel auf Stab 1 soll nach Stab 3 übertragen werden unter Beachtung
der folgenden Regeln:
1. Es darf stets nur jeweils eine Scheibe nach der anderen bewegt werden.
2. Es darf nie eine größere auf einer kleineren Scheibe liegen.
Der Algorithmus informell (Abb. 1.4):
Wie kann das Problem für N gelöst werden, wenn bereits bekannt ist, wie es
für N − 1 zu lösen ist? Außerdem ist festzulegen, wie das Problem für N = 1
zu lösen ist. Die Lösung hat eine direkte Analogie zu einem Induktionsbeweis
in der Mathematik.
Bemerkung: Der Algorithmus ist die Vorschrift, was zu tun ist, nicht die
Realisierung (das Durchspielen“) für konkrete Werte von N . Probleme für
”
spezielle Werte N probehalber zu lösen ist regelmäßig (viel) einfacher als
einen Algoritmus anzugeben, also ein Verfahren, welches auch ein Automat
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
15
Ausgangssituation:
}
N Scheiben
Zielsituation:
Stab 1
Stab 2
Stab 3
Abbildung 1.3: Türme von Hanoi
ausführen kann, der naturgemäß weder den Sinn der Sache sehen noch ein
Ziel vor Augen haben kann.
Der eigentliche rekursive Algorithmus:
Algorithmus 7
Türme-Von-Hanoi(N, X, Y, Z)
1 if N = 1
2
then Ziehe-Scheibe(X, Z)
3
else Türme-Von-Hanoi(N − 1, X, Z, Y )
4
Ziehe-Scheibe(X, Z)
5
Türme-Von-Hanoi(N − 1, Y, X, Z)
Die Aktion Ziehe Scheibe dient nur als Platzhalter; in einer realen Implementierung wird man im einfachsten Fall einfach einen Text ausgeben oder
– um einiges aufwändiger – eine grafische Ausgabe programmieren.
Im Internet sind jede Menge Animationen zu finden.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
16
1
N−1
Scheiben
{
}
N Scheiben
2
3
Stab 1
Stab 2
Stab 3
Abbildung 1.4: Türme von Hanoi: der Algorithmus
Algorithmus 8
Ziehe-Scheibe(Ausgang, Ziel )
Bewege die oberste Scheibe von Ausgang nach Ziel
Eine ausführlich kommentierte Implementierung:
Programmbeispiel 1 (Türme von Hanoi)
1
package Einleitung;
2
3
4
5
6
7
8
9
public class TowersOfHanoi {
void ZieheScheibe(char Ausgang, char Ziel) {
System.out.println(”Bewege Scheibe von ” + Ausgang + ” nach ” + Ziel);
}
void Hanoi (int N, char X, char Y, char Z) {
/* Die Prozedur Hanoi führt das Spiel Türme von Hanoi
durch, indem der Scheibenstapel von Stab X nach Stab Z
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
unter Verwendung von Stab Y als Zwischenablage
bewegt wird. */
if (N == 1) {
ZieheScheibe(X, Z);
} else { /* N > 1 */
Hanoi(N−1, X, Z, Y);
/* Stapel der Höhe N-1 von X nach Y mit Zwischenablage Z
ZieheScheibe(X, Z);
Hanoi(N−1, Y, X, Z);
/* Stapel der Höhe N-1 von Y nach Z mit Zwischenablage X
}
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
17
*/
*/
}
}
class HanoiApplication {
public static void main (String [ ] args) {
char A = ’A’, B = ’B’, C = ’C’;
int N = 3;
TowersOfHanoi th = new TowersOfHanoi();
th.Hanoi(N, A, B, C);
}
}
/* Result:
Bewege Scheibe von A nach C
Bewege Scheibe von A nach B
Bewege Scheibe von C nach B
Bewege Scheibe von A nach C
Bewege Scheibe von B nach A
Bewege Scheibe von B nach C
Bewege Scheibe von A nach C
/
*
Eine graphentheoretische Darstellung der Zustände mit 3 Scheiben findet
man in Abb. 1.5.
Alle waagerechten Verbindungen sind angedeutet; außerdem sind alle diagonal unmittelbar benachbarten Zustände durch Kanten zu verbinden (in der
Darstellung nicht angedeutet).
(detailliertere Erläuterungen in der Vorlesung)
Türme von Hanoi: Laufzeitverhalten im Detail bei 3 Scheiben auf 3 Stäben
in Abb. 1.6 auf S. 19.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
18
111
211
···
311
231
331
···
321
131
···
121
···
221
332
132
···
223
232
122
222
···
323
212
322
···
312
···
···
123
313
112
···
113
···
133
213
···
233
···
Abbildung 1.5: Darstellung aller Zustände mit 3 Scheiben
Türme von Hanoi: Komplexität des Algorithmus
• Allgemeines Problem: Gesucht ist die Abhängigkeit des Aufwands (der
Ressourcen) von der Problemgröße.
Das ist stets eine zentrale Frage bei jeder Algorithmenanalyse. Der Begriff,
unter dem diese Fragen und Analysen zusammengefaßt werden, lautet
Komplexität.
• Hier ist die Anzahl der Züge in Abhängigkeit von der Anzahl der Scheiben
gesucht.
• N = Anzahl der Scheiben, T (N ) = Anzahl der Züge
• 1 Scheibe bedeutet 1 Zug, also T (1) = 1
• bei N Scheiben gilt:
Es werden erst N − 1 Scheiben bewegt, dann 1, dann wieder N − 1, daher
gilt die Gleichung:
T (N ) = T (N − 1) + 1 + T (N − 1) = 2 · T (N − 1) + 1
• Das ist eine rekursive Gleichung für die (unbekannte) Funktion T (N ), die
mit der Anfangsbedingung T (1) = 1 zu lösen ist.
Es gibt allgemein eine Reihe verschiedener Methoden zur Lösung von Gleichungen dieses Typs.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
333
1.2 Algorithmen
19
Wert jeweils des lokalen N:
+--------------------------------------------------------+
| Hanoi(3,A,B,C)
3|
| +--------------------------------------------------+ |
| | Hanoi(2,A,C,B)
2| |
| | +--------------------------------------------+ | |
| | | Hanoi(1,A,B,C)
1| | |
| | |
| | |
| | |
X---------->
| | |
| | |
| | |
| | |
| | +--------------------------------------------+ | |
| |
| |
| |
X------->
| |
| |
| | +--------------------------------------------+ | |
| | | Hanoi(1,C,A,B)
1| | |
| | |
| | |
| | |
X---------->
| | |
| | |
| | |
| | |
| | +--------------------------------------------+ | |
| |
| |
| +--------------------------------------------------+ |
|
|
|
X---->
|
|
| +--------------------------------------------------+ |
| | Hanoi(2,B,A,C)
2| |
| | +--------------------------------------------+ | |
| | | Hanoi(1,B,C,A)
1| | |
| | |
| | |
| | |
X---------->
| | |
| | |
| | |
| | |
| | +--------------------------------------------+ | |
| |
| |
| |
X------->
| |
| |
| | +--------------------------------------------+ | |
| | | Hanoi(1,A,B,C)
1| | |
| | |
| | |
| | |
X---------->
| | |
| | |
| | |
| | |
| | +--------------------------------------------+ | |
| |
| |
| +--------------------------------------------------+ |
|
|
+--------------------------------------------------------+
Scheibe von A nach C
Scheibe von A nach B
Scheibe von C nach B
Scheibe von A nach C
Scheibe von B nach A
Scheibe von B nach C
Scheibe von A nach C
Abbildung 1.6: Ausgabe beim Ablauf der Türme von Hanoi
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
20
1. Methode: Lösung raten durch Probieren und dann durch vollständige Induktion beweisen
• einige Beispielwerte:
T (1)
T (2)
T (3)
T (4)
..
.
=
1
= 2 · T (1) + 1 = 2 · 1 + 1 = 3
= 2 · T (2) + 1 = 2 · 3 + 1 = 7
= 2 · T (3) + 1 = 2 · 7 + 1 = 15
2N − 1 (geratene Lösung)
T (N ) =
• Bemerkung: Dieses gilt für die optimale Lösung des Spiels (rechte Seite
des Dreiecks im Diagramm auf Seite 18).
• Induktionsbeweis:
– für N = 1 gilt T (1) = 1
?
– Schluß von N auf N +1: zu zeigen ist: T (N +1) = 2N +1 −1, falls T (N ) =
2N − 1 richtig ist und die Ausgangsgleichung T (N ) = 2 · T (N − 1) + 1
gilt:
T (N + 1) = 2 · T (N ) + 1 = 2 · (2N − 1) + 1 = 2N +1 − 1
2. Methode: durch wiederholtes Einsetzen (Iterationsmethode) Diese Methode ist relativ aufwändig und für dieses einfache Problem ein reichlich
massiver Ansatz. Man sollte diesen Punkt daher eher als Einüben der Methode verstehen; wenn es nur um die Lösung des Problems geht, so bekommt
man die hier wesentlich einfacher mit einer der anderen Methoden.
T (N ) = 2 · T (N − 1) + 1
Schritt Nr. 1
= 2(2 · T (N − 2) + 1) + 1
= 4 · T (N − 2) + 3
Schritt Nr. 2
= 4(2 · T (N − 3) + 1) + 3
= 8 · T (N − 3) + 7
Schritt Nr. 3
= 8(2 · T (N − 4) + 1) + 7
= 16 · T (N − 4) + 15
..
.
Schritt Nr. 4
= 2k · T (N − k) + 2k − 1
Schritt Nr. k
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
21
Damit diese Folge abbricht muß k bei gegebenem N so gewählt werden, dass
N − k = 1 ist, so dass in der letzten Gleichung gerade T (1) = 1 auftritt.
Setze also k = N − 1.
Dann folgt:
T (N ) = 2N −1 · T (1) + 2N −1 − 1
= 2N −1 + 2N −1 − 1
= 2 · 2N −1 − 1
N
= 2 −1
3. Methode: durch Variablensubstitution Ist in diesem Fall die einfachste Lösung.
Man setze
T 0 (N ) := T (N ) + 1.
Dann gilt
T 0 (1) = 2
und die Gleichung für die neue unbekannte Funktion T 0 (N ) ist jetzt
T 0 (N ) = 2 · T 0 (N − 1)
Jede Erhöhung von N um 1 führt also zu einer Verdopplung des Wertes von
T 0 , also
T 0 (N ) = 2N
Rücksubstitution ergibt dann wieder das Endergebnis
T (N ) = 2N − 1
Fibonacci-Zahlen
Fibonacci-Zahlen: spielen eine Rolle z.B. in

für
 0
1
für
f (n) =

f (n − 2) + f (n − 1) für
der Kombinatorik.
n=0
n=1
n≥2
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
22
einige Werte:
f (0)
f (1)
f (2)
f (3)
f (4)
f (5)
f (6)
f (7)
f (8)
...
= 0
= 1
= 1
= 2
= 3
= 5
= 8
= 13
= 21
Die Funktion f dient als Beispiel, dass eine – etwas naiv hingeschriebene –
rekursive Implementierung einer mathematisch rekursiv definierten Funktion
nicht in jedem Fall eine gute Lösung sein muß. In diesem Fall ist die rekursive
Implementierung sogar schlicht unsinnig (s. Abbildung 1.7)!
f
f
( 4
f
f
( 1
( 2
)
( 3
f
( 0
( 2
f
)
)
)
f
)
)
f
f
( 5
( 1
)
f
( 1
)
f
( 0
f
)
f
( 1
)
( 2
( 3
)
f
)
f
( 0
( 1
)
)
)
Abbildung 1.7: Rekursive Berechnung der Fibonacci-Zahlen
Busy Beaver
Der eifrige Biber“
”
vgl. [Eng88, S. 100] und Brauer, Inf. Spektr. 13, Heft 2, 61(1990)
Die Frage, die hier gestellt und beantwortet wird, lautet: Kann man jede Funktion durch einen Algorithmus berechnen? Es ist eine interessante
Übungsaufgabe, einen Interpreter für bb-Programme zu implementieren.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
23
Spiel mit dem Ziel, möglichst viele Striche auf ein ursprünglich leeres Band
zu schreiben (Jeder Strich ist ein gefällter Baum).
Regel: Jeder Spieler denkt sich eine Folge von n Anweisungen (n ≥ 1) der
folgenden Struktur aus:
z.B.:
0 | R 2
1
| | L 3
Bedeutung: Anweisung Nr. 1 besagt:
• Wenn auf dem Band eine 0 (d.h. ein Blank) vorgefunden wird, so ist
folgendes zu unternehmen:
1. einen | schreiben, d.h. 0 durch | ersetzen
2. eine Bewegung nach rechts (R) vornehmen und
3. fortfahren mit Anweisung Nr. 2
• wenn auf dem Band ein | vorgefunden wird, dann:
1. einen | schreiben, d.h. in diesem Fall: den | stehen lassen
2. eine Bewegung nach links (L) vornehmen und
3. fortfahren mit Anweisung Nr. 3
Gewonnen hat der Spieler, dessen Programm
1. innerhalb einer vorher vereinbarten Frist anhält und
2. die meisten Striche geschrieben hat.
Beispiel für ein vollständiges nicht-triviales busy-beaver- (bb-)Programm:
0 | R 2
1
| | L 3
0 | R 3
| | R Halt
0 | L 1
| 0 L 2
2
3
Ablauf für obiges Beispiel:
(s. Abbildung 1.8)
in diesem Fall also: 6 Striche mit 11 Zügen, dann Halt
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.2 Algorithmen
24
|
...
|
1
|
2
1
Halt
3
2
|
|
|
1
3
3
2
1
3
...
Abbildung 1.8: Busy Beaver
Es sei Σ(n) die Maximalzahl von Strichen, die ein haltendes, aus n Anweisungen bestehendes bb-Programm auf das leere Band schreiben kann
bekannt ist:
Anzahl
min. erforderliche
Anweisungen n Σ(n) Schrittzahl
1
1
1
2
4
6
3
6
11
4
13
96
5
?
?
6
?
?
7
?
?
Frage: Kann man Σ(n) durch ein Programm berechnen lassen? Annahme: es
steht beliebig (aber endlich viel) Zeit zur Verfügung.
Frage wird beantwortet durch den folgenden (streng beweisbaren)
1.2.1 Satz
Es gibt kein Verfahren (d.h. kein Programm, keinen Algorithmus), mit dem
man den Wert von Σ(n) für jedes n ∈ N berechnen kann.
Der Beweis wird indirekt geführt: Angenommen, es gäbe ein derartiges Verfahren. Dann läßt sich ein Widerspruch herleiten.
Einzelheiten: aus Theorie der Turing-Maschinen
Ergebnis ist insbesondere deswegen überraschend, weil man sich vorstellen
kann, dass man (mit entsprechendem Zeitaufwand) die Programme explizit
aufschreibt, z. B. maschinell:
Programme mit n = 1, 2, 3, . . . Anweisungen:
(1)
(1)
(1)
(1)
n = 1: P1
P2
P3
. . . P64
(2)
P2
(3)
P2
n = 2: P1
n = 3: P1
n = 4: . . .
(2)
P3
(2)
...
P20 736
(3)
P3
(2)
(3)
...
P16 777 216
(3)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.3 Komplexität von Algorithmen
25
Bemerkung: Gesamtzahl der Programme mit n Anweisungen:
(4(n + 1))2n
Begründung:
(2 × 2 × (n + 1))2 · . . . · (2 × 2 × (n + 1))2
|
{z
}
n mal
1.3
Komplexität von Algorithmen
Beispiel für Abzählen der Schrittzahl (Pseudo-Code):
Algorithmus 9
1 s←0
2 for i ← 0 to n − 1
3
do s ← s + a[i]
4 return s
Schrittzahl:
1
n+1
n
1
Summe ist 2n + 3
Abstrahiert wird: Die Laufzeit des Algorithmus ist proportional“ zu n.
”
Mathematische Präzisierung
Im Folgenden seien f und g Funktionen von N in R: f : N → R, g : N → R
1.3.1 Definition (Notation Groß-O)
Die Schreibweise g(n) = O(f (n)) bedeutet: es existieren Konstanten M und
n0 , so dass |g(n)| ≤ M |f (n)| gilt für alle n ≥ n0 .
also in Worten: fg(n)
ist für hinreichend große n dem Betrage nach durch M
(n) nach oben beschränkt.
1.3.2 Beispiel (zu O(n))
• 3n + 2 = O(n), denn 3n + 2 ≤ 4n für n ≥ 2.
• 100n + 6 = O(n), denn 100n + 6 ≤ 101n für n ≥ 6.
• 10n2 + 4n + 2 = O(n2 ), denn 10n2 + 4n + 2 ≤ 11n2 für n ≥ 5.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.3 Komplexität von Algorithmen
26
• 1000n2 + 100n − 6 = O(n2 ), denn 1000n2 + 100n − 6 ≤ 1001n2 für n ≥ 100.
• 3n + 3 = O(n2 ), denn 3n + 3 ≤ 3n2 für n ≥ 2.
• 3n + 2 6= O(1), denn 3n + 2 ≤ c für alle n ≥ n0 gilt nicht, egal wie c und
n0 gewählt werden.
• 10n2 + 4n + 2 6= O(n)
Das Gleichheitszeichen ist eine traditionelle Notation; gemeint ist: O(f (n))
bezeichnet eine Menge von Funktionen, und g ist ein Element dieser Menge.
Aus g(n) = O(f (n)) und h(n) = O(f (n)) kann also nicht auf g(n) = h(n)
geschlossen werden! Es macht daher auch keinen Sinn O(f (n)) auf die linke
Seite einer Gleichung zu schreiben. Manche Autoren bevorzugen daher eine
mathematisch korrekte aber etwas unkonventionelle Schreibweise wie z. B.
g ∈ O(f ). Dadurch wird deutlich, dass es sich bei diesen Notationen um
Aussagen über Funktionen und nicht über einzelne Funktionswerte handelt.
Eine wichtige Frage ist natürlich, ob man formale Beweise, als Abschätzungen
der obigen Art mit der Angabe von geeigneten Konstanten, in jedem Einzelfall explizit ausführen muß. Für eine große Klasse von Funktionen, nämlich
die Polynome, beantwortet der folgende Satz diese Frage.
1.3.3 Satz
i
Wenn f (n) = am nm + · · · + a1 n + a0 = Σm
i=0 ai n mit am > 0, dann gilt
f (n) = O(nm ).
1.3.4 Definition (Notation Groß-Ω)
Die Schreibweise g(n) = Ω(f (n)) bedeutet: es exisitieren Konstanten L und
n0 , so dass |g(n)| ≥ L|f (n)| gilt für alle n ≥ n0 .
Kurz formuliert: es existiert also eine untere Schranke.
1.3.5 Beispiel (zu Ω(n))
• 3n + 2 = Ω(n), denn 3n + 2 ≥ 3n für n ≥ 1.
• 100n + 6 = Ω(n), denn 100n + 6 ≥ 100n für n ≥ 1.
• 10n2 + 4n + 2 = Ω(n2 ), denn 10n2 + 4n + 2 ≥ n2 für n ≥ 1.
aber auch:
• 3n + 3 = Ω(1)
• 10n2 + 4n + 2 = Ω(n)
• 10n2 + 4n + 2 = Ω(1)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.3 Komplexität von Algorithmen
27
1.3.6 Satz
i
Wenn f (n) = am nm + · · · + a1 n + a0 = Σm
i=0 ai n und am > 0, dann gilt
f (n) = Ω(nm ).
1.3.7 Definition (Notation Groß-Θ)
Die Schreibweise g = Θ(f (n)) bedeutet: es gilt sowohl g(n) = O(f (n)) als
auch g(n) = Ω(f (n)).
Wichtig zum Verständnis ist hier, dass in dieser Definition die Konstanten in
O(. . .) und Ω(. . .) natürlich unabhängig voneinander zu wählen sind. Offenbar ist es unsinnig, von einer Funktion f (n) zu fordern, dass Cn2 ≤ f (n) ≤
Cn2 gilt.
1.3.8 Beispiel (zu Θ(n))
• 3n + 2 = Θ(n), denn 3n + 2 ≥ 3n für n ≥ 1 ∧ 3n + 2 ≤ 4n für n ≥ 2.
• 10n2 + 4n + 2 = Θ(n2 )
• 6 · 2n + n2 = Θ(2n )
• 3n + 2 6= Θ(1)
• 3n + 3 6= Θ(n2 )
1.3.9 Satz
i
Wenn f (n) = am nm + · · · + a1 n + a0 = Σm
i=0 ai n und am > 0, dann gilt
f (n) = Θ(nm ).
Ergänzung: In der Mathematik wird auch das Symbol o (Klein-o) eingeführt:
1.3.10 Definition (Notation Klein-o)
g(n) = o(f (n)) bedeutet: limn→∞ fg(n)
= 0.
(n)
Bemerkung: Der Grenzübergang könnte auch ein anderer sein. Die Notation
hat nur einen Sinn bzgl. eines vorher verabredeten Grenzüberganges.
Beispiele
Wichtig in der Praxis:
1. O(1)
2. O(log n)
3. O(n)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.3 Komplexität von Algorithmen
28
4. O(n log n)
5. O(n2 )
6. O(n3 )
7. O(2n )
Etwas pauschal formuliert kann man feststellen, dass für größere Werte von
n Algorithmen unter 1 - 4 als gut gelten, die Fälle 5 und 6 als schlecht und
7 als unbrauchbar.
zum Vergleich:
log2 n n n log2 n
n2
n3
2n
0 1
0
1
1
2
1 2
2
4
8
4
2 4
8
16
64
16
3 8
24
64
512
256
4 16
64
256 4 096
65 536
5 32
160 1 024 32 768 ca. 4 · 109 (4 294 967 296)
Erfahrungsgemäß bereiten diese formalen Definitionen ein gewisses Verständnisproblem, vor allem deswegen, weil sie viele Freiheiten lassen: es sind ja nur
irgendwelche Konstanten M und n0 anzugeben. (Das Problem tritt allerdings
in der Mathematik bei Abschätzungen jeglicher Art auch in gleicher Weise
auf.) Deshalb gibt der folgende Abschnitt nochmals eine nicht-formale Darstellung in einem größeren Zusammenhang, in der das Gewicht auf einer
Erklärung des Sinns der ganzen Sache liegt.
Der Sinn des Komplexitätsbegriffs
In der Informatik werden meistens zwei Anforderungen an Algorithmen gestellt. Sie sollen erstens die gewünschte Wirkung haben, also effektiv sein.
Damit ist gemeint, dass sie bei Einhaltung von Vorbedingungen eine Spezifikation korrekt erfüllen. So soll z. B. ein Sortieralgorithmus ein Array mit
gegebener Wertebelegung in einen neuen Zustand überführen, in dem das
Array sortiert ist. Oder ein Suchalgorithmus wie zum Beispiel das binäre
Suchen soll in einem Array ein Element entweder finden, falls es vorhanden
ist oder sonst anzeigen, dass es nicht vorhanden ist. Vorbedingung für das
korrekte Arbeiten ist in diesem Fall, dass das Array sortiert ist. Algorithmen
sollen zweitens aber auch möglichst kleine Anforderungen an Ressourcen stellen, also effizient sein. Das bedeutet insbesondere, dass sie schnell sind und
wenig Speicherplatzanforderungen stellen.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.3 Komplexität von Algorithmen
29
In diesem Abschnitt wird der Begriff der Effizienz vertieft behandelt: es wird
insbesondere diskutiert, wie sich die qualitative Formulierung geringer Ressourcenbedarf quantifizieren läßt. Die Erläuterungen halten sich an das Beispiel Laufzeitverhalten, also der sog. Laufzeitkomplexität.
Der nächstliegende Ansatz besteht offensichtlich darin, Laufzeiten einfach
in Abhängigkeit von der Problemgröße zu messen und dann in Form einer
Tabelle oder auch in Funktionsform die entsprechende Abhängigkeit anzugeben. Diese Methode ist durchaus sinnvoll, wenn man für einen bestimmten
Rechner unter gegebenen Betriebsbedingugen wissen will, welches denn die
Zeitanforderungen eines Algorithmus sind. Es gibt allerdings mindestens zwei
gravierende Nachteile. Erstens erhält man auf diese Weise nicht wirklich eine Aussage über den verwendeten Algorithmus selbst, sondern vielmehr nur
eine Information über die Kombination aus Algorithmus, genauer, dessen Implementierung, und der zugrundeliegenden Hardware, wobei möglicherweise
auch noch der spezielle Betriebszustand der Rechenanlage eingeht. Zweitens
weiß man bei diesem Ansatz nach wie vor nichts über das Zeitverhalten des
Algorithmus für Werte außerhalb des Meßbereiches. Eine brauchbare Extrapolation ist bei weitem nicht offensichtlich, denn die vielleicht naheliegende Faustregel aus der Technik, dass doppelte Anforderungen größenordnungsmäßig auch einen doppelten Ressourcenbedarf nach sich ziehen, gilt für
Algorithmen i. a. in keiner Weise.
Daher ist dieser Ansatz jedenfalls dann nicht sinnvoll, wenn man wirklich
Aussagen haben will über den Algorithmus selbst. Man abstrahiert in diesem Fall von der Hardware in der Weise, dass man Programmschritte zählt,
wobei Schritte verschiedener Arten wie etwa Vergleiche und Bewegungen, also Kopiervorgänge, durchaus verschieden gewichtet werden können. Häufig
werden diese Gewichtsfaktoren aber einheitlich auf 1 gesetzt. Implizit hat
man daher ein Modell eines abstrakten Rechners zugrunde gelegt, der ein
in einer Hochsprache formuliertes Programm direkt interpretiert, für den die
Hochsprache sozusagen als Assembler aufzufassen ist. Dieser Ansatz wird so
von Niklaus Wirth in seinem Buch über Algorithmen und Datenstrukturen
zugrundegelegt.
Einfache Algorithmen kann man auf diese Weise sehr gut und vollständig
analysieren. Z. B. ergibt sich für BubbleSort (Seite 37) die folgende Tabelle:
(aus Wirth)
Anzahl Vergleiche
Anzahl Bewegungen
Minimum
n(n − 1)
2
0
Mittel
Maximum
n(n − 1)
n(n − 1)
2
2
3n(n − 1) 3n(n − 1)
4
2
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.3 Komplexität von Algorithmen
30
Diese Aussagen sind leicht zu begründen: die Gleichheit der Formeln in der
ersten Zeile ergibt sich einfach daraus, dass die Anzahl der Vergleiche bei
BubbleSort unabhängig ist von der konkret vorliegenden Arraybelegung. Die
konkrete Form folgt dann daraus, dass beim ersten Durchlauf n−1 Vergleiche
gemacht werden, beim zweiten nur noch n − 2 usw. bis 1. Daher gilt also
(n − 1) + (n − 2) + · · · + 1 =
n(n − 1)
2
Dass die Anzahl der Bewegungen im Mittel gerade die Hälfte der Anzahl
im ungünstigsten Falls ist, bei dem mit jedem Vergleich ein Austausch und
damit 3 Bewegungen erforderlich sind, mag plausibel erschienen. Man sollte
aber nicht vergessen, dass hier bereits ein einfaches stochastisches Modell
eingeführt wird, dem die (implizite) Annahme zugrunde liegt, dass jede Permutation der n! möglichen Arraybelegungen gleichwahrscheinlich ist. Wenn
diese Annahme nicht zutrifft, wie das praktisch häufig der Fall sein wird,
dann stimmt auch diese mittlere Formel nicht.
Meistens interessieren diese Formeln nur für größere Werte von n: dann kann
2
man natürlich den exakten Ausdruck n(n−1)
ersetzen durch n2 . Wenn man
2
darüberhinaus auch nur an Verhältnissen der Laufzeiten zwischen zwei nWerten interessiert ist, dann kann die obige Aussage darauf reduziert werden,
dass die Laufzeit proportional zu n2 ist.
Es ist vielleicht nahe liegend, ähnliche Überlegungen für andere Algorithmen anzustellen: man sucht einfach Näherungsformeln für die Laufzeiten
in Abhängigkeit von der Problemgröße. Die konkreten Formeln sind dann
vielleicht komplizierter, aber im Prinzip sollte sich das Verfahren allgemein
anwenden lassen. Für viele Algorithmen ist das auch so möglich wie z. B. für
Quicksort.
Leider ist die Realität wesentlich komplizierter. Die (Zeit-) Komplexität von
Algorithmen ist ja eine Aussage über diskrete Systeme. Und denen fehlt in
der Regel eine Eigenschaft, die technische Systeme sonst meistens haben,
nämlich eine Art stetiges Verhalten zu zeigen. Eine Brücke z.B., die ein Gewicht von 1000 Tonnen tragen kann, wird auch ein weiteres Kilogramm aushalten und erst recht ein Kilogramm weniger. Systeme der Informatik sind
in dieser Hinsicht fundamental anders. So muß man z. B. bei der Zeitkomplexität von Algorithmen damit rechnen, das das Laufzeitverhalten für eine
Problemgröße, die eine 2er-Potenz ist, ganz anders ist, als etwa für einen Wert
unmittelbar darunter. Z. B. MergeSort zeigt ein solch irreguläres Verhalten.
Für 2er-Potenzen gibt es zwar eine einfache Formel für die Laufzeit; die sonstige Kurve hat aber eine Art fraktale Struktur. Nun läßt sich in diesem Fall
zwar trotzdem noch eine exakte Formel angeben, nicht aber ohne weiteres
eine vernünftige Näherung in irgend einem Sinne. Außerdem enthält eine solR. Rüdiger
Algorithmik I
— 1. März 2006 —
1.3 Komplexität von Algorithmen
31
che exakte Formel viel zu viel Information, an der man z. B. beim Vergleich
von Algorithmen garnicht interessiert ist.
Das hat zu einem Ansatz geführt, der nochmal erheblich von solchen Details abstrahiert und der – in voller Absicht – nur noch eine sehr abstrakte
Vergleichbarkeit erlaubt.
Man sucht für das Laufzeitverhalten nur noch nach oberen und unteren
Schranken und läßt dabei Konstanten frei. Wenn also f (n) das Laufzeitverhalten eines Algorithmus als Funktion der Problemgröße n darstellt und
wenn es möglich ist, f (n) für hinreichend große n durch die Funktion M g(n)
mit frei wählbarer Konstanten M zu beschränken, dann verwendet man die
Schreibweise f (n) = O(g(n)).
Die mathematisch präzise Formulierung lautet also: Für gegebene Funktionen
f (n) und g(n) gilt f (n) = O(g(n)) genau dann, wenn es zwei Konstanten
n0 und M gibt, so dass f (n) ≤ M g(n) gilt für n ≥ n0 (Positivität der
Funktionen unterstellt).
Mathematisch handelt es sich ausdrücklich nicht um eine Gleichheitsrelation.
Die gelegentlich auch verwendete und präzisere Notation ist f ∈ O(g), die
sich aber nicht allgemein durchgesetzt hat.
Damit hat man eine Schreibweise an der Hand, mit der man die Laufzeitkomplexität eines jeden Algorithmus abschätzen kann, wenn auch um den
Preis einer erheblichen Abstraktion. Insbesondere konkrete Zahlenwerte für
das Laufzeitverhalten lassen sich wegen der Freiheit in der Wahl der Konstanten M nicht mehr verläßlich angeben. Aus Sicht der Informatik hat diese
Notation zwei Aspekte, sozusagen einen syntaktischen und einen pragmatischen. Der syntaktische Aspekt besteht darin, dass man zunächst die formale
Definition richtig erfüllt. Aus dieser Sicht gehört jede Funktion, die z. B. der
Klasse O(n2 ) angehört natürlich auch der Klasse O(n3 ) an. Offenbar ist das
absolut analog zu der Situation, dass jede Zahl x, die etwa x ≥ 5 erfüllt,
natürlich auch x ≥ 0 erfüllt. Der Übergang von der ersten zur zweiten Aussage bedeutet einen Informationsverlust.
Solche Abschätzungen sollten natürlich eine möglichst genaue Information
liefern; daher ist die Frage nahe liegend, ob man obere Schranken nicht in
irgend einem Sinne optimieren kann. Wie also lautet z. B. die optimale obere
Schranke für die obige Funkton von BubbleSort? Nach kurzem Nachdenken
sieht man, dass die einzig richtige Antwort lautet: es ist die Funktion selbst
mit den Konstanten M = 1, n0 = 1. Offenbar ist die Frage so also noch
falsch gestellt. Hier kommt nun der pragmatische Aspekt zum Tragen: zumindestens in der Informatik beschränkt man sich auf eine Liste spezieller
Vergleichsfunktionen. Nur die sollen als obere Schranken Anwendung finden.
Eine mögliche Liste ist z. B. O(1), O(log n), O(n), O(n log n), O(n2 ), O(n3 ),
O(2n ).
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.3 Komplexität von Algorithmen
32
Mit dieser Selbstbeschränkung hat man zwei Probleme gleichzeitig gelöst:
das gerade formulierte Optimierungsproblem ist jetzt sinnvoll. Welches ist die
beste“ Funktion aus der Liste, die eine obere Schranke z.B. für BubbleSort
”
darstellt? O(n log n) ist es nicht, was sich leicht nachrechnen läßt; O(2n ) ist
zwar eine obere Schranke, ebenso wie O(n3 ), aber O(n2 ) ist auch in dem
obigen Sinne eine obere Schranke und natürlich eine schärfere, also bessere.
Für alle diejenigen, die eine Formalisierung des Problems sehen möchten,
lautet die Frage: welches ist der kleinste ganzzahlige Wert k, so dass n(n−1)
=
2
O(nk ) gilt. Die eindeutige Antwort lautet natürlich 2.
Der zweite Punkt ist, dass man nun auch außerordentlich verschiedene Algorithmen gut vergleichen kann wie z.B. BubbleSort und MergeSort, bei denen die exakten Lösungen nicht entfernt irgendwelche Ähnlichkeiten haben.
Tatsächlich ist die exakte Funktion bei MergeSort nur formulierbar mit den
ceil- und floor-Operationen.
Ebenso wie man obere Schranken einführt, kann man analog auch untere
Schranken einführen und dann auch nach der gleichzeitigen Existenz beider
Schranken fragen. Wenn also eine Funktion f (n) gleichzeitig von oben und
von unten bis auf Konstanten durch eine Funktion g(n) beschränkt werden
kann, dann wird die Notation f (n) = Θ(g(n)) verwendet. Hier wird nochmal
sehr deutlich, weshalb man die freien Konstanten einführt: ein quadratischer
Ausdruck wie bei BubbleSort kann natürlich nicht gleichzeitig von oben und
von unten etwa durch dieselbe Funktion n2 beschränkt werden. Wenn man
aber frei wählbare Konstanten zuläßt, dann gilt in diesem Fall n(n−1)
≤ 12 n2
2
für n ≥ 1 und gleichzeitig n(n−1)
≥ 41 n2 für n ≥ 2. Daher gehört BubbleSort
2
also zur Komplexitätsklasse Θ(n2 ).
So wichtig diese Notationen auch sind, so vorsichtig sollte man im Umgang damit sein und sich vor unzulässigen Schlußfolgerungen hüten. Z. B.
können sich zwei O(n)- Algorithmen“ durchaus extrem unterscheiden, weil
”
die auftretenden Konstanten natürlich nicht unwichtig sind, oder gar ver”
nachlässigbar“. Ein Beispiel: Reisezeiten sind grob betrachtet – unabhängig
vom Verkehrsmittel – stets Funktionen der Klasse O(n), wenn n die Entfernung bedeutet (doppelte Entfernung bedeutet doppelte Reisezeit). Die
Schlußfolgerung, dass daher die Wahl des Algorithmus“, in diesem Fall also
”
des Verkehrsmittels, eigentlich egal ist, ist reichlich gewagt: man denke etwa an eine Urlaubsreise nach Südafrika und die alternativen Verkehrmittel
Flugzeug/Fahrrad.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.4 Beispiele zu diversen Komplexitätsklassen
1.4
33
Beispiele zu diversen Komplexitätsklassen
Es folgt eine Reihe von Beispielen für Algorithmen verschiedener Komplexitätsklassen. Die Vervollständigung bzw. Implementierung einiger der abgedruckten Programme wird als Übungsaufgabe gestellt.
(Beispiele zum Teil aus [Her95, S. 45])
Horner-Schema
(Komplexität O(n))
Die Frage, die hier beantwortet werden soll:
Wie berechnet man den Wert eines Polynoms an einer gegebenen Stelle x,
z.B.:
p(x) = a0 + a1 x + a2 x2 + a3 x3 + a4 x4
Die direkte Möglichkeit, also u. a. den auftretenden Term x4 entsprechend
x4 = x · x · x · x auszurechnen, ist schlecht: denn mit x4 z.B. wurden bereits
x3 , x2 mitberechnet.
Abspeichern wäre eine andere Möglichkeit; das ist aber nicht erforderlich,
denn es geht viel einfacher:
Man schreibe
p(x) = a0 + (a1 + (a2 + (a3 + a4 x)x)x)x
Das läßt sich auswerten durch eine einfache Iteration:
y4 ← a4
y3 ← a3 + y4 · x
y2 ← a2 + y3 · x
y1 ← a1 + y2 · x
y0 ← a0 + y1 · x
Das ist der Grundgedanke des sog. Horner-Schemas“.
”
In algorithmischer Form:
Algorithmus 10
Horner(a, x)
1 y ← a[n − 1]
2 for i ← n − 2 downto 0
3
do y ← a[i] + y · x
4 return y
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.4 Beispiele zu diversen Komplexitätsklassen
34
Die naive Auswertung zum Vergleich:
Algorithmus 11
Polynomial(a, x)
1 y ← a[0]
2 for i ← 1 to n − 1
3
do p ← 1
4
for k ← 0 to i − 1
5
do p ← p · x also faktisch: p ← xi
6
y ← y + p · a[i]
7 return y
Eine Implementierung des Horner-Schemas, in dem die Anzahl der Schritte
gezählt und mit der des direkten (naiven) Verfahrens verglichen wird.
Programmbeispiel 2 (Komplexität von Algorithmen (1))
1
package Einleitung;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Complexity {
// Klausuraufgabe 5, SS03
private double a[ ];
private int n;
private int count1, count2;
public Complexity (double [ ] a) {
this.a = a;
n = a.length;
}
public double polynomial(double x) {
double y = a[0];
count1 = 0;
for (int i = 1; i < n; i++) {
double p = 1.0;
for (int k = 0; k < i; k++) {
p = p*x;
count1++;
}
y = y + a[i]*p;
}
return y;
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.4 Beispiele zu diversen Komplexitätsklassen
public double horner(double x) {
double y = a[n−1];
count2 = 0;
for (int i = n−2; i >= 0; i−−) {
y = y*x + a[i];
count2++;
}
return y;
}
public int getCount1() {
return count1;
}
public int getCount2() {
return count2;
}
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
35
}
...
Teilersuche
√
(Komplexität O( n) = O(cd ), d = Stellenzahl. Die Komplexität ist daher
exponentiell in der Stellenzahl.)
z.B.: n = x · y, n = 24
probiere x = 2, 3, 4, 5, 6, 7, . . .
6, 7, . . . sind überflüssig!
Algorithmus 12
Teiler-Suche(n)
√
1 w ← b nc
2 for t ← 2 to w
3
do if t | n
t teilt n
4
then print t
5
print n/t
Die Komplexität ist
√
√ d
√
O( n) = O( 10d ) = O( 10 ) = O(3.16d )
d = Anzahl der Dezimalstellen
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.4 Beispiele zu diversen Komplexitätsklassen
36
Bemerkung: Die Faktorisierung (großer) Zahlen spielt eine Rolle in der Kryptologie (Lehre von der Verschlüsselung), z.B. dem RSA-Verfahren (Rivest,
Shamir, Adleman).
Effizientes Potenzieren
(Komplexität O(log n))
Verfahren zum Potenzieren (von Zahlen, Matrizen, . . . (alles, was multipliziert
werden kann))
im einfachsten Fall: z.B.:
a16 = a
a · · · · a}
| · a · {z
16mal
Es geht offenbar einfacher, wenn der Exponent eine 2er-Potenz ist, z. B.:
2 2
4
2 2
16
a
a =
= a2
und wenn Exponent keine 2er-Potenz ist: dann Verwendung des Algorithmus
zum effizienten Potenzieren:
1. Beispiel: n = 18:
Die Grundidee: binäre Zerlegung des Exponenten:
4
3
2
1
a18 = a1·2 · a0·2 · a0·2 · a1·2 · a0·2
0
nach Durchlauf n x
p
0
18 1
a
1
9 1 a2
2
4 a2 a4
3
2 a2 a8
1 a2 a16
4
5
0 a18 a32
Ende
2. Beispiel: n = 7:
2
1
a7 = a1·2 · a1·2 · a1·2
nach Durchlauf
0
1
2
3
Ende
0
n x p
7 1 a
3 a a2
1 a3 a4
0 a7 a8
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.4 Beispiele zu diversen Komplexitätsklassen
37
Als Algorithmus formalisiert.
Algorithmus 13
Power(a, n)
berechnet an
1 x←1
2 p←a
3 while n > 0
4
do if n ungerade also n mod 2 6= 0
5
then x ← x · p
6
n ← bn/2c
7
p ← p2
8 return x
Die entsprechende rekursive Form macht alles noch etwas klarer (Präsenzaufgabe 17).
Sortieren: BubbleSort
(Komplexität O(n2 ))
Die Formulierung entsprechend [CLRS01]:
Algorithmus 14
Bubblesort(A)
1 for i ← 1 to length[A]
2
do for j ← length[A] downto i + 1
3
do if A[j] < A[j − 1]
4
then exchange A[j] ↔ A[j − 1]
R. Rüdiger
Algorithmik I
— 1. März 2006 —
1.4 Beispiele zu diversen Komplexitätsklassen
38
Multiplikation quadratischer Matrizen
(Komplexität O(n3 ))
Algorithmus 15
Multiply(a, b)
1
2
3
4
5
6
7
P
berechnet cij = n−1
k=0 aik bkj
for i ← 0 to n − 1
do for j ← 0 to n − 1
do s ← 0
for k ← 0 to n − 1
do s ← s + aik bkj
cij ← s
return c
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Kapitel 2
Fundamentale Datenstrukturen
2.1
Allgemeines
• Rolle von Programmiersprachen: Programmiersprache stellt abstrakten
Computer dar, der Ausdrücke dieser Sprache interpretieren kann.
• Elemente der Sprache verkörpern Abstraktion über die von der wirklichen
Maschine benutzten Objekte.
Bsp.: Zahlendarstellung
• Umgang mit mathematischen Begriffen wie z.B. Zahlen, Mengen, Folgen, Wiederholungen erhöht Zuverlässigkeit der Programme verglichen
mit Bits, Wörtern, Sprüngen
• Wie formuliert man Algorithmen?
Extremfälle: (1) entweder: maschinenorientierte und maschinenabhängige
Sprache anwenden
(2) oder (auf der anderen Seite): nur abstrakte (formale) Notation unter
völliger Vernachlässigung von Darstellungsproblemen (Wirth)
• Pascal (und Nachfolgesprachen) stellt Kompromiß zwischen diesen Extremen dar
2.2
Stacks und FIFO-Schlangen
Stacks und FIFO-Schlangen wird man eher unter den dynamischen Datenstrukturen vermuten als an diesem Abschnitt, in dem es eher um elementare
Arrays geht. Man kann diese Strukturen jedoch durchaus auf Arrays abbilden mit dem bekannten Vorteil geringer Kosten (schneller Zugriff, wenig
R. Rüdiger
Algorithmik I
— 1. März 2006 —
2.2 Stacks und FIFO-Schlangen
40
overhead) und dem Nachteil der mangelnden Flexibiltät, was die Größe der
Struktur betrifft. Implementierungen, die auf verketteten linearen Listen beruhen, sollen in Übungsaufgaben behandelt werden.
Die folgenden Formulierungen von Algorithmen sind dem Buch [CLRS01]
entnommen.
LIFO-Struktur (LIFO = Last-In-First-Out) = Stack
Basisoperationen:
• Stack empty
• Push
• Pop
• eventuell auch Stack full, falls dieser eine endliche Größe hat (was in einer
Implementierung natürlich immer der Fall ist)
Eine wichtige Anwendung von Stacks ist die Verwaltung von Rücksprungadressen bei Prozeduraufrufen, die in Abb. 2.1 veranschaulicht ist.
a2
a1
a3
ai = Adresse für den Rücksprung
Abbildung 2.1: Stack-Anwendung Rücksprungadressen
R. Rüdiger
Algorithmik I
— 1. März 2006 —
2.2 Stacks und FIFO-Schlangen
41
Algorithmus 16
Stack-Empty(S)
1 if top[S] = 0
2
then return true
3
else return false
Algorithmus 17
Push(S, x)
1 top[S] ← top[S] + 1
2 S[top[S]] ← x
Algorithmus 18
Pop(S)
1 if Stack-Empty(S)
2
then error “underflow”
3
else top[S] ← top[S] − 1
4
return S[top[S] + 1]
FIFO-Schlangen:
Basisoperationen:
• Enqueue: Einfügen am Ende (tail)
• Dequeue: Entfernen an der Spitze (head)
Algorithmus 19
Enqueue(Q, x)
1 Q[tail [Q]] ← x
2 if tail [Q] = length[Q]
3
then tail [Q] ← 1
4
else tail [Q] ← tail [Q] + 1
R. Rüdiger
Algorithmik I
— 1. März 2006 —
2.3 Suchalgorithmen
42
Algorithmus 20
Dequeue(Q)
1 x ← Q[head [Q]]
2 if head [Q] = length[Q]
3
then head [Q] ← 1
4
else head [Q] ← head [Q] + 1
5 return x
2.3
Suchalgorithmen
• Gegeben sei ein Array a, welches zur Laufzeit N Objekte aufnehmen kann.
• item ist ein Datensatz, auch Record“ genannt, mit Schlüsselfeld key, x
”
vorgegebener Schlüsselwert
• Suchen bedeutet: es ist ein Index i so zu bestimmen, dass gilt:
a[i].key = x
• Hier wird zur Vereinfachung a[i] selbst als Schlüssel genommen.
(Vergleiche sind damit also von der Form a[i] = x)
Lineares Suchen
• Ohne weitere Angaben bzw. Informationen über das Array a: a ist sequentiell zu durchlaufen
• Dabei wird Schritt für Schritt die Menge vergrößert, in der sich das gesuchte Element mit Sicherheit nicht befindet
• terminiert, wenn:
1. Das Element ist gefunden: a[i] = x oder:
2. Das ganze Array ist durchlaufen, aber kein Element hat den Schlüsselwert x
R. Rüdiger
Algorithmik I
— 1. März 2006 —
2.3 Suchalgorithmen
43
• daraus Algorithmus:
Algorithmus 21
Linear-Search1 (a, x)
1 i←0
2 while i < length[a] and a[i] 6= x
3
do i ← i + 1
4 if i = length[a]
5
then return false
6
else return true
Nach dem Terminieren der while-Schleife gilt (mit N = length[a])
(i = N ) ∨ (a[i] = x) ≡ ¬(i < N ∧ a[i] 6= x).
In einer Java-Implementierung wird man das etwas kürzer ausdrücken:
Programmbeispiel 3 (Lineare Suche (1))
1
...
2
int i = 0;
3
while (i < a.length && a[i] != x)
4
i++;
5
return i != a.length;
6
...
Die Reihenfolge der Booleschen Operanden ist wichtig!!
• Frage: Vereinfachung des Booleschen Ausdrucks im Algorithmus?
• Das Array wird am Ende um ein Feld erweitert.
• Hilfselement mit Schlüsselwert x an das Ende des vergrößerten Arrays
setzen ( sentinel“)
”
• Element wird dann auf jeden Fall gefunden, evtl. aber außerhalb des (eigentlichen) ursprünglichen Arrays
• Algorithmus (lineare Suche mit Sentinel):
R. Rüdiger
Algorithmik I
— 1. März 2006 —
2.3 Suchalgorithmen
44
Algorithmus 22
Linear-Search2 (a, x)
1 i←0
2 while a[i] 6= x
3
do i ← i + 1
4 if i = length[a] − 1 Array a ist um 1 vergrößert!
5
then return false
6
else return true
Zwei weitere Varianten für sequentielles Suchen (von vielen):
1. Self-organizing (transpose) sequential search
selbstorganisierende sequentielle Suche (Aufgabe 6, Klausur WS 98/99 in
Informatik I)
Algorithmus informell:
(a) Suche Element in Array.
(b) Falls Element gefunden und nicht an erster Position (mit Index 0) im
Array dann
(c) vertausche Element mit dem unmittelbar vorhergehenden und
(d) setze Index i auf das gefundene Element.
(e) Falls Element nicht gefunden, dann setze Index auf −1.
2. Die zweite Variante verwendet verkettete Listen: siehe Programmbeispiel 8
auf Seite 83.
Binäres Suchen
• weitere Beschleunigung des Suchverfahrens gegenüber linearem Suchen:
nur, wenn weitere Informationen vorliegen
• Elemente im Array a0 . . . aN −1 geordnet bedeutet:
N
−1
^
ak−1 ≤ ak ≡ (a0 ≤ a1 ) ∧ (a1 ≤ a2 ) ∧ . . . ∧ (aN −2 ≤ aN −1 )
k=1
R. Rüdiger
Algorithmik I
— 1. März 2006 —
2.3 Suchalgorithmen
45
• verschiedene Varianten des Verfahrens Binäres Suchen
1. erste (etwas naive) Variante, s. S. 12
maximale Anzahl an Vergleichen ist dlog N e
lineares Suchen: mittlere erwartete Anzahl von Vergleichen: N/2
2. weitere Variante: Gleichheit sollte erst in zweiter Linie geprüft werden:
denn diese tritt nur einmal auf beim Terminieren des Algorithmus.
Variante 2:
Algorithmus 23
BinarySearch2 (a, x)
1 l←0
2 r ← length[a]
3 while l < r
4
do m ← b(l + r)/2c
5
if a[m] < x
6
then l ← m + 1
7
else r ← m
8 if r < length[a] and a[r] = x
9
then return r
10
else return −1
3. eine Variante, die der intuitiven Vorstellung von binärem Suchen näher
kommt: Interpolationssuche. Man schätzt die Stelle ab, wo der gesuchte
Wert wohl liegen könnte (Formulierung aus Klausur WS 04/05):
R. Rüdiger
Algorithmik I
— 1. März 2006 —
2.3 Suchalgorithmen
46
Algorithmus 24
BinarySearch3 (a, x)
1 l←0
2 r ← length[a] − 1
3 while a[l] < x andx ≤ a[r]
x − a[l]
4
do m ← l +
· (r − l)
a[r] − a[l]
5
if x > a[m]
6
then l ← m + 1
7
else if x < a[m]
8
then r ← m − 1
9
else l ← m
10 . . . . . .
4. weitere Variante: rekursive Formulierung:
In Sprachen, die lokale Prozeduren in Prozeduren zulassen, kann man
eine Beibehaltung der Signatur durch lokale Einbettung der rekursiven
Prozedur in eine umgebende Prozedur erreichen. Man spart damit eine
Parameterübergabe.
(In Java ist das nicht möglich.)
Algorithmus 25
BinarySearch4 (a, x)
1 return BinsearchR (a, x, 0, length[a] − 1)
Algorithmus 26
BinsearchR (a, x, i, j)
1 if j < i
2
then return −1
3
else m ← b(i + j)/2c
4
if x < a[m]
5
then return BinsearchR (a, x, i, m − 1)
6
else if x > a[m]
7
then return BinsearchR (a, x, m + 1, j)
8
else return m
R. Rüdiger
Algorithmik I
— 1. März 2006 —
2.3 Suchalgorithmen
47
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Kapitel 3
Sortieren
3.1
Allgemeines
• Sortieren ist Prozeß des Anordnens einer gegebenen Menge von Objekten
in einer bestimmten Ordnung
• Leistungsanalyse derartiger Prozesse ist wichtig
• zwei Kategorien:
1. Sortieren von Arrays und
2. Sortieren von sequentiellen Files, auch bezeichnet als internes und externes Sortieren
• allgemeiner Rahmen: gegeben: Elemente a1 , a2 , . . . , an
• Sortieren bedeutet: Umordnen zu
ak1 , ak2 , . . . , akn
• Ordnungsfunktion f :
f (ak1 ) ≤ f (ak2 ) ≤ . . . ≤ f (akn )
• zum Vergleich herangezogen: Schlüsselwert
• Eine Sortiermethode heißt stabil, wenn die relative Ordnung der Elemente
mit gleichen Schlüsseln beim Sortieren unverändert bleibt.
• ist wichtig, wenn Elemente bereits nach einem anderen Schlüssel geordnet
sind
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.2 Sortieren von Arrays – elementare Methoden
3.2
49
Sortieren von Arrays – elementare Methoden
• Umstellen sollte am Ort“ ( in situ“) ausgeführt werden (also ohne Ver”
”
wendung eines zweiten Arrays, eines Hilfsarrays)
• Leistungsanalyse: Maß für Effizienz:
– Anzahl der Schlüsselvergleiche C (comparisons) und
– Anzahl der Bewegungen (Umstellungen, movements) M
• Algorithmen gelten als gut, wenn Laufzeitverhalten O(n log n)
direkte (elementare) Methoden: O(n2 )
In den folgenden Abschnitten sind die Algorithmen zum besseren Vergleich
einheitlich formuliert: Parameter sind stets a und n; a ist das Array, welches
im Indexbereich zwischen 1 und n sortiert wird. Eine Java-Implementierung,
die diese Index-Konventionen übernimmt, wird also auf die Arraykomponente
zum Index 0 verzichten und n in der Regel auf den größtmöglichen Index
setzen, also auf n = a.length − 1.
StraightInsertion
Algorithmus 27
Insertion-Sort(a, n)
1 for i ← 2 to n
2
do x ← a[i]
3
Insert a[i] into the sorted sequence a[1 . . i − 1].
4
j ←i−1
5
while j > 0 and a[j] > x
6
do a[j + 1] ← a[j]
7
j ←j−1
8
a[j + 1] ← x
BinaryInsertion
BinaryInsertionSort funktioniert im Prinzip wie InsertionSort, aber das Einfügen
erfolgt durch binäre anstelle linearer Suche.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.2 Sortieren von Arrays – elementare Methoden
50
aus Klausur, SS04:
Algorithmus 28
Binary-Insertion-Sort(a, n)
1 for i ← 2 to n
2
do x ← a[i]
3
l←1
4
r←i
5
while l < r
6
do m ← b(l + r)/2c
7
if a[m] ≤ x
8
then l ← m + 1
9
else r ← m
10
for j ← i downto r + 1
11
do a[j] ← a[j − 1]
12
a[r] ← x
StraightSelection
Algorithmus 29
Selection-Sort(a, n)
1 for i ← 1 to n − 1
2
do k ← i
3
x ← a[i]
4
for j ← i + 1 to n
5
do if a[j] < x
6
then k ← j
7
x ← a[k]
8
a[k] ← a[i]
9
a[i] ← x
BubbleSort
bereits behandelt, Algorithmus 14 auf Seite 37
etwas anders formuliert (konsistent mit der Systematik dieses Abschnitts)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
51
Algorithmus 30
Bubble-Sort(a, n)
1 for i ← 2 to n
2
do for j ← n downto i
3
do if a[j − 1] > a[j]
4
then exchange a[j] ↔ a[j − 1]
ShakerSort
Algorithmus 31
ShakerSort(a, n)
1 l←2
2 r←n
3 k←n
4 repeat
5
for j ← r downto l
6
do if a[j − 1] > a[j]
7
then exchange a[j − 1] ↔ a[j]
8
k←j
9
l ←k+1
10
for j ← l to r
11
do if a[j − 1] > a[j]
12
then exchange a[j − 1] ↔ a[j]
13
k←j
14
r ←k−1
15
until l > r
3.3
Sortieren von Arrays – schnelle Methoden
Heapstrukturen
Die Struktur Heap wird u.a. verwendet für eine effiziente Implementierung
von Prioritätswarteschlangen, also Warteschlangen, in denen die Kunden“
”
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
52
nach einer ihnen zugeordneten Priorität eingeordnet werden. Man kann mit
einer verhältnismäßig einfachen Modifikation einen Heap aber auch zum Sortieren verwenden. Das Verfahren gehört zu den schnellen Sortieralgorithmen
und ist bekannt unter dem Namen Heapsort.
Es gibt zwei verschiedene Klassen von Heaps, die sich darin unterscheiden,
was höhere Priorität bedeutet. Entweder kennzeichnet der größere Wert eine
höhere Priorität. In dem Fall spricht man von einem Max-Heap, im anderen Fall von einem Min-Heap. Diese zweite Variante hat eine Bedeutung im
Zusammenhang mit Simulationen: das zeitlich nächste Ereignis, also das mit
der kleinsten verbleibenden Zeit, ist als nächstes abzuarbeiten. Man kann die
beiden Varianten sehr einfach zusammenfassen durch die Einführung einer
Prozedur, in der nur eine Vergleichoperation ausgetauscht werden muß, um
von einem Min-Heap zu einem Max-Heap zu kommen.
Für den Max-Heap setzt man:
Prior-To(x, y)
return x > y Max-Heap
und für den Min-Heap:
Prior-To(x, y)
return x < y Min-Heap
Verwendet man einen Heap zum Sortieren, so führt ein Max-Heap zu aufsteigender und ein Min-Heap zu einer absteigenden Sortierung.
Die folgenden Beispiele konzentrieren sich auf den Fall des Min-Heaps.
Details zur Datenstruktur Heap:
Definition:
Ein Heap (genauer: ein Min-Heap) ist ein binärer Baum, dessen Knoten Werte zugeordnet sind (im einfachsten Fall etwa ganze Zahlen) mit folgenden zwei
speziellen Eigenschaften:
1. Der Baum ist fast horizontal abgeschnitten (s. Abb. 3.1).
2. Die Werte von Kindknoten (childs) sind stets ≥ den Werten der Elternknoten (parents).
fast horizontal abgeschnitten“ bedeutet folgendes: s. Abb. 3.1
”
Beispiel für einen Heap: Abb. 3.2
Damit liegt fest, wie eine Datenstruktur auszusehen hat, die eine HeapStruktur besitzt.
2 Fragen sind zu beantworten:
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
53
Abbildung 3.1: Heap als binärer Baum
1
3
3
2
6
4
4
7
8
9
10
8
14
12
5
6
7
9
15
7
Abbildung 3.2: Beispiel für Heap mit Werten und nummerierten Knoten
1. Wie baut man einen Heap auf oder allgemeiner: welche Operationen gibt
es auf einem Heap, die die Heap-Struktur erhalten?
2. Wie implementiert man (möglichst effizient) einen Heap?
zu 1: Die zwei Operationen sind
Einfügen (void insert(int x))
Löschen / Extrahieren (int remove())
Außerdem gibt es zwei interne Verwaltungsoperationen, auf die sich diese
Operationen abstützen:
heapUp, heapDown
(auch siftUp bzw. siftDown genannt).
Abb. 3.3 zeigt die Details der Basisoperationen in einer informellen Weise.
In Abb. 3.4 sind die wechselseitigen Beziehungen der Knoten in einem
Heap dargestellt.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
54
zu Insert
up Heap
down Heap
zu Remove
Abbildung 3.3: Heap-Struktur: Insert und Remove
zu 2: Abbildung des Baumes auf ein Array durch fortlaufendes Durchnummerieren der Knoten, beginnend mit 1
Die zwei wichtigen Anwendungen sind also
1. Prioritätswarteschlangen
2. Sortierverfahren (Heapsort)
(mit Min-Heap für absteigendes Sortieren)
Der detaillierte Pseudocode (nach CLRS, geringfügig geändert) für Heapsort:
Parent(i)
return bi/2c
Left(i)
return 2i
Right(i)
return 2i + 1
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
a i/2
parent
ai
node
2 childs
55
a2i
a2i+1
Abbildung 3.4: Wechselseitige Beziehungen der Knoten in einem Heap (Ausschnitt aus Baum)
Heapsort in Pseudocode entsprechend der Aufteilung in CLRS:
HeapifyI (a, i)
1 j ← 2i
2 x ← a[i]
3 if j < heap-size[a] and Prior-To(a[j + 1], a[j])
4
then j ← j + 1
5 while j ≤ heap-size[a] and Prior-To(a[j], x)
6
do a[i] ← a[j]
7
i←j
8
j ← 2j
9
if j < heap-size[a] and Prior-To(a[j + 1], a[j])
10
then j ← j + 1
11 a[i] ← x
Build-Heap(a)
1 heap-size[a] ← length[a]
2 for i ← blength[a]/2c downto 1
3
do HeapifyI (a, i)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
56
Heapsort(a)
1 Build-Heap(a)
2 for i ← length[a] downto 2
3
do exchange a[1] ↔ a[i]
4
heap-size[a] ← heap-size[a] − 1
5
HeapifyI (a, 1)
eine Implementierung genau nach CLRS (rekursiv):
1
2
/* Heap Sort nach CLRS */
package Kapitel03;
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class HeapSortCLRS {
private long [ ] a;
private int heapSize;
private int arrayLength; // effective array length
HeapSortCLRS (long [ ] a) {
this.a = a;
}
public void heapifyR (int i) { // rekursiv, CLRS
int l = 2*i,
r = l + 1,
x = (l <= heapSize && priorTo(a[l], a[i]))? l : i;
if (r <= heapSize && priorTo(a[r], a[x]))
x = r;
if (x != i) {
exchange(i, x);
heapifyR(x);
}
}
private boolean priorTo (long x, long y) {
// first arg prior to second arg
return x > y; // maxHeap: aufsteigend sortieren
// return x < y; // minHeap: absteigend sortieren
}
public void heapifyI (int i) { // Wirth, S. 93 = sift, iterativ
int j = 2*i;
long x = a[i];
if (j < heapSize && priorTo(a[j+1], a[j]))
j++;
while (j <= heapSize && priorTo(a[j], x)) {
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
a[i] = a[j];
i = j;
j *= 2;
if (j < heapSize && priorTo(a[j+1], a[j]))
j++;
33
34
35
36
37
}
a[i] = x;
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
57
}
public void buildHeap () {
arrayLength = a.length − 1;
heapSize = arrayLength;
for (int i = arrayLength/2; i >= 1; i−−)
heapifyR (i);
}
public void heapSort () {
buildHeap();
for (int i = arrayLength; i >= 2; i−−) {
exchange(1, i);
heapSize = heapSize − 1;
heapifyR (1);
}
}
public void exchange(int i, int j) {
long tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
public void show () {
for (int i = 1; i < a.length; i++)
System.out.print(a[i] + ” ”);
System.out.println();
}
}
class HeapSortDemoCLRS {
public static void main (String [ ] args) {
System.out.println(”Start ...”);
long dummy = Long.MIN VALUE; // not included in sorting
long [ ] a = {dummy, 30, 12, 15, 4, 7, 19, 13, 62, 18, 1, 5};
HeapSortCLRS hs = new HeapSortCLRS(a);
hs.show();
hs.heapSort();
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
hs.show();
74
}
75
76
58
}
Sortieren durch Zerlegen (QuickSort)
Quicksort, informell beschrieben:
1. Sei a ein Feld mit den Indexgrenzen von L bis R und
2. sei x ein beliebiges Feldelement.
3. Zerlege a in zwei Teilfelder a[L] . . . a[k] und a[k + 1] . . . a[R], so dass alle
Werte im linken Teil nicht größer als x und alle Werte im rechten Teil
nicht kleiner als x sind, d.h.
a[i] ≤ x links: L ≤ i ≤ k
x ≤ a[j]
rechts: k + 1 ≤ j ≤ R
4. Der linke und der rechte Teil werden rekursiv nach dem gleichen Verfahren
umgeordnet.
Der eigentliche Algorithmus:
Algorithmus 32
Quicksort(a, n)
1 qsort(a, 1, n)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
59
Algorithmus 33
qsort(a, l, r)
1 x ← a[b(l + r)/2c]
2 i←l
3 j←r
4 repeat
5
while a[i] < x
6
do i ← i + 1
7
while x < a[j]
8
do j ← j − 1
9
if i ≤ j
10
then exchange a[i] ↔ a[j]
11
i←i+1
12
j ←j−1
13
until i > j
14 if l < j
15
then qsort(a, l, j)
16 if i < r
17
then qsort(a, i, r)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
60
Der Algorithmus als Java-Methode mit Angabe der Stellen, an denen die
Zustände festgestellt werden, wie sie in der folgenden Tabelle als Beispiel
gezeigt sind.
/* adapted to notation of Wirth: */
void quickSort (int[] a, int L, int R) {
int i = L, j = R, x; // x = pivot
x = a[(L+R) >> 1]; // integer division by 2
//−−−−−−−−−−−−
showState(a, L, R, i, j, x);
//−−−−−−−−−−−−
do {
while (a[i] < x)
i++;
while (x < a[j])
j−−;
//−−−−−−−−−−−−
showState(a, L, R, i, j, x);
//−−−−−−−−−−−−
if (i <= j) {
int w = a[j]; a[j] = a[i]; a[i] = w;
i++; j−−;
//−−−−−−−−−−−−
showState(a, L, R, i, j, x);
//−−−−−−−−−−−−
}
} while (i <= j);
if (L < j)
quickSort (a, L, j);
if (i < R)
quickSort (a, i, R);
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.3 Sortieren von Arrays – schnelle Methoden
1 0
2
3
4
5
6
7
8
9
10
11
j
12
1
28
i
28
i
11
61
2
3
58 23
4
17
5
91
58 23
17
91
58 23
i
11 58 23
i
11 17 23
ij
11 17 23
ij
11 17 23
j
11 17 .
i
j
11 17 .
ij
11 17 .
i
.
.
.
17
91
j
91
17
j
58
6
11
j
11
j
28
28
91
28
58
91
28
58
i
.
91
28
.
.
.
.
.
.
.
.
58
i
58
91
28
j
.
.
.
91 28
13
i
j
14
.
.
. 58 28 91
j
i
15
.
.
. 58 28 .
i
j
16
.
.
. 58 28 .
i
j
17
.
.
. 28 58 .
j
i
Die Tabelle zeigt die jeweiligen Positionen der Variablen i und j, die als eine
Art Zeiger“ fungieren. Punkte anstelle von Zahlen bedeuten, dass in dem
”
Bereich das Array nicht bearbeitet wird, die Werte also auch nicht geändert
werden. i und j beziehen sich jeweils auf die darüberstehende Zeile.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.4 Sortieren von Sequenzen
62
Ein Vergleich der Sortiermethoden mit Arrays
Tabelle aus [Wir86, S. 102]:
min
mit
max
C=
n−1
(n2 − n + 2)/4
(n2 − n)/2 − 1
M=
2(n − 1)
(n2 − 9n − 19)/4
(n2 − 3n − 4)/2
C=
(n2 − n)/2
(n2 − n)/2
(n2 − n)/2
M=
3(n − 1)
n(ln n + 0.57)
n2 /4 + 3(n − 1)
C=
(n2 − n)/2
(n2 − n)/2
(n2 − n)/2
M=
0
0.75 · (n2 − n)
1.5 · (n2 − n)
direktes Einfügen:
direkte Auswahl:
direkter Austausch:
lt. Wirth, S. 103: Ausführungszeiten für n = 2048, wenn das zu ordnende
Array mit zufälligen Elementen gefüllt wird:
BubbleSort
(StraightExchange) 128.8
direkte Auswahl
(StraightSelection)
58.3
direktes Einfügen (StraightInsertion)
50.7
binäres Einfügen (BinaryInsertion)
37.7
HeapSort
2.2
QuickSort
1.2
3.4
Sortieren von Sequenzen
Das Standardbeispiel für das Sortieren von Sequenzen ist Mergesort: Es gilt
gleichzeitig als das prototypische Beispiel für Divide-and-Conquer-Verfahren.
Pseudo-Code für ein allgemeines Schema dieser Verfahren:
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.4 Sortieren von Sequenzen
63
Algorithmus 34
Solve(x)
1 if |x| = 1
2
then solve x directly: solution is `
3
else split x into subproblems x1 and x2
4
`1 ← solve(x1 )
5
`2 ← solve(x2 )
6
combine solutions `1 and `2 into solution `
for the original problem
7 return `
Die Größen des folgenden Algorithmus’ sind in Abb. 3.5 veranschaulicht.
Algorithmus 35
Merge-Sort(A, p, r)
1 if p < r
2
then q ← b(p + r)/2c
3
Merge-Sort(A, p, q)
4
Merge-Sort(A, q + 1, r)
5
Merge(A, p, q, r)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.4 Sortieren von Sequenzen
64
zu MERGE−SORT(A,p,r)
p+r
2
q
p
MERGE−S(A,p,q)
r
MERGE−S(A,q+1,r)
MERGE(A,p,q,r)
p
kopieren:
p
q
r
n1
n2
L, Länge n1+1
R, Länge n2+1
q
r
8
L
R
abschließen durch Sentinal
Abbildung 3.5: Merge-Sort
Algorithmus 36
Merge(A, p, q, r)
1 n1 ← q − p + 1
2 n2 ← r − q
3 create arrays L[1 . . n1 + 1] and R[1 . . n2 + 1]
4 for i ← 1 to n1
5
do L[i] ← A[p + i − 1]
6 for j ← 1 to n2
7
do R[j] ← A[q + j]
8 L[n1 + 1] ← ∞
9 R[n2 + 1] ← ∞
10 i ← 1
11 j ← 1
12 for k ← p to r
13
do if L[i] ≤ R[j]
14
then A[k] ← L[i]
15
i←i+1
16
else A[k] ← R[j]
17
j ←j+1
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.5 Sortieren in linearer Zeit
3.5
65
Sortieren in linearer Zeit
Allgemeine Analyse: Minimalzahl von Vergleichen
• allgemein beweisbare Aussage (vgl. I-Duden, S. 557):
gegeben: n Elemente, alle voneinander verschieden
Ein Sortierverfahren, das seine Information über die Anordnung der zu
sortierenden Elemente nur aus Vergleichsoperationen zwischen den Elementen bezieht, benötigt mindestens O(n log n) Vergleiche; mit anderen
Worten: es kann nicht besser als O(n log n) sein.
Begründung:
Beispiel: Elemente a1
a2
a3
grafische Darstellung durch sog. Entscheidungsbaum
Baum hat genau n! Blätter, denn ein Blatt entspricht genau einer Permutation der n Elemente a1 , a2 , . . . , an
V (n) sei die minimale Anzahl von Vergleichen, die notwendig sind, um n
Objekte zu sortieren
Ein binärer Baum der Höhe k hat höchstens 2k−1 Blätter, also
n! ≤ 2V (n)
oder (log bedeutet hier 2er-Logarithmus)
log(n!) ≤ V (n)
log(n!) wird nach unten abgeschätzt.
also:
V (n) ≥ O(n log n)
z.B. Heapsort (s. Seite 51) erreicht diese Schranke
Durch Vergleich von Elementen geht es daher nicht besser!
• Frage: Geht es mit anderen Methoden besser?
JA!: Beispiel ist BucketSort
R. Rüdiger
Algorithmik I
— 1. März 2006 —
3.5 Sortieren in linearer Zeit
66
Bucketsort
Programmbeispiel 4 (BucketSort)
1
package Kapitel03;
2
public class BucketSort {
3
private int min, max;
4
private int [ ] bucket;
5
BucketSort (int min, int max) {
6
this.min = min;
7
this.max = max;
8
bucket = new int [max − min + 1];
9
for (int i = 0; i < bucket.length; i++)
10
bucket[i] = 0;
11
}
12
public void sort(int [ ] a, int n){
13
for (int i = 1; i <= n; i++)
14
bucket[a[i] − min]++;
15
}
16
public String toString () {
17
String s = ””;
18
for (int i = 0; i < bucket.length; i++)
19
for (int j = 1; j <= bucket[i]; j++ )
20
s += (i + min) + ”\t”;
21
return s;
22
}
23
public static void main (String [ ] args) {
24
int dummy = Integer.MIN VALUE; // dummy value, not included in sorting
25
int [ ] a = {dummy, −3, −7, 10, 9, 4, 1, 6, 5, 2, 4, 5, −3};
26
int n = a.length − 1;
27
int min = −10;
28
int max = +10;
29
BucketSort bs = new BucketSort(min, max);
30
bs.sort(a, n);
31
System.out.println(bs);
32
}
33
}
34
/*
35
result:
36
−7
−3
−3
1
2
4
4
5
5
6
9
10
37
/
*
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Kapitel 4
Rekursion
4.1
Allgemeines
Rekursion ist eine der fundamentalen Techniken, die in der Informatik an verschiedenen Stellen eine zentrale Rolle spielt. Einige Algorithmen kann man in
einfacher Weise nur rekursiv formulieren wie z. B. das Durchlaufen (Traversieren) von binären Suchbäumen, die später behandelt werden (Kapitel 5.4,
Seite 97). Eine andere Stelle, in der Rekursion eine wichtige Rolle spielt, sind
die funktionalen Programmiersprachen. In einer rein funktionalen Sprache
gibt es z. B. keine while-Schleife; die Iteration wird durch Rekursion nachgebildet.
Im Folgenden sind diverse Beispiele für rekursive Programme zusammengestellt; einige wurden schon früher besprochen, z.B.:
• die Türme von Hanoi
• die Fibonacci-Zahlen (In dem Fall hat sich die rekursive Implementierung
aber als vollkommen unbrauchbar herausgestellt.)
• das binäre Suchen
• Es folgen zunächst weitere einfache Beispiele:
ggT (größter gemeinsamer Teiler, gcd = greatest common divisor) rekursiv
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.1 Allgemeines
68
Algorithmus 37
GCD(a, b)
1 if a = b
2
then return a
3
else if a > b
4
then return GCD(a − b, b)
5
else return GCD(a, b − a)
• McCarthy-91-Funktion: die Funktion ist ein Beispiel dafür, dass eine kompliziert aussehende rekursive Formulierung nicht unbedingt auch zu einem
komplizierten Ergebnis führt.
Algorithmus 38
f(n)
1 if n > 100
2
then return n − 10
3
else return f(f(n + 11))
Als Funktion in normaler mathematischer Schreibweise formuliert:
f (f (n + 11)) für n ≤ 100
f (n) =
n − 10
für n > 100
Tabelle der Funktionswerte:
f (112)
f (111)
f (110)
f (109)
f (108)
...
f (101)
f (100)
f (99)
f (98)
f (97)
...
=
=
=
=
= 102
= 101
= 100
= 99
= 98
...
= 91
f (f (111)) = f (101) = 91
f (f (110)) = f (100) = 91
f (f (109)) = f (99) = 91
f (f (108)) = f (98) = 91
...
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.2 Backtracking
69
• Zahlen in Binär- oder auch einer anderen Darstellung ausgeben: Probleme,
bei denen zum Drehen“ des Endergebnisses ein Zwischenspeicher benötigt
”
wird, lassen sich häufig elegant und kompakt rekursiv formulieren, z.B. die
Ausgabe von Zahlen in Binärdarstellung.
Programmbeispiel 5 (Binärdarstellung)
1
...
2
static String getBinR(int x) { // recursive method
3
// precondition: x >= 0
4
return ((x >= 2)? getBinR(x / 2) : ””) + x % 2;
5
}
6
static String getBinI(int x) { // non−recursive method
7
// precondition: x >= 0
8
String s = x > 0? ”” : ”0 ”;
9
while (x != 0) {
10
s = x % 2 + s;
11
x /= 2;
12
}
13
return s;
14
}
15
...
Weitere größere Beispiele:
4.2
Backtracking
Beispiel zum Backtracking-Verfahren: Spielstrategie (vgl. Gumm/Sommer,
S. 148)
• Abstraktes 2-Personen-Spiel: 2 Personen spielen gegeneinander.
• Spiel ist gegeben durch eine Menge S von möglichen Situationen. Spielregeln definieren, welche Folgepositionen von einer gegebenen Position aus
erlaubt sind.
• Zu einer Situation s sei Folge(s) die Menge der möglichen Situationen, zu
denen der Spieler, der am Zug ist ( wir“), von s aus aufgrund der Regeln
”
ziehen darf.
• V sei die Menge aller Situationen, in denen der Spieler, der am Zug ist,
verloren hat (Schachmatt z. B.), s. Abb. 4.1.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.2 Backtracking
70
S
V
Folge(s)
geschickter
Zug
s
Abbildung 4.1: 2 Personenspiel. Backtracking
• Falls s ∈ V , haben wir verloren. Falls F olge(s) ∩ V 6= ∅ dann können wir
den Gegner mit einem Zug in eine Verlustposition zwingen.
• Folgender Algorithmus beantwortet allgemein die Frage, ob der Spieler,
der am Zug ist, eine sichere Gewinnmöglichkeit hat:
• Pseudocode: ( ich“ bin am Zug)
”
Algorithmus 39
Good-PositionR (s)
= true ⇔ in Situation s gibt es eine Gewinnstrategie
1 if s ∈ V
2
then return false leider verloren
3
else if Folge(s) ∩ V 6= ∅ wähle einen den Regeln
entsprechenden Zug, der nach V führt
4
then return true
wenn ich“ keinen Fehler mache
W
”
5
else return p∈Folge(s) ¬Good-PositionR (p)
• Zeile 5 ausgeschrieben besagt: es existiert eine Situation p in Folge(s),
so dass Good-PositionR (p) = false. Äquivalent dazu kann man auch
schreiben ¬Good-PositionR (p) = true, d. h., der andere verliert.
Ein konkretes Beispiel: Zwei Spieler starten mit N Münzen. In jedem Zug
muß ein Spieler entweder genau 3, genau 5 oder genau 7 dieser Münzen entfernen. Derjenige Spieler, der schließlich nicht mehr ziehen kann, hat verloren.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.2 Backtracking
71
Implementierung:
Programmbeispiel 6 (Spielstrategie)
1
package Kapitel04;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Game {
boolean goodPosNR(int N) {
return 3 <= N % 10;
}
boolean goodPosR (int N) {
if (N < 3)
return false;
else if (3 <= N && N <= 9)
return true;
else
return !goodPosR (N − 7)
|| !goodPosR (N − 5)
|| !goodPosR (N − 3);
}
Game (int n, int m) {
System.out.println(”rekursiv:”);
for (int N = n; N <= m; N++) {
if ( goodPosR(N) ) {
System.out.println(N + ” : alles OK ”);
} else {
System.out.println(N + ” : praktisch verloren”);
}
}
}
}
class GameDemo {
public static void main (String args [ ]) {
new Game(1, 39);
}
}
/*
Ausgabe:
rekursiv:
1 : praktisch verloren
2 : praktisch verloren
3
4
5
6
7
:
:
:
:
:
R. Rüdiger
alles
alles
alles
alles
alles
OK
OK
OK
OK
OK
Algorithmik I
— 1. März 2006 —
4.3 Eine Anwendung: Mini-Parser
8 : alles OK
9 : alles OK
10 : praktisch
11 : praktisch
12 : praktisch
13 : alles OK
14 : alles OK
15 : alles OK
16 : alles OK
17 : alles OK
18 : alles OK
19 : alles OK
20 : praktisch
21 : praktisch
22 : praktisch
23 : alles OK
24 : alles OK
4.3
verloren
verloren
verloren
verloren
verloren
verloren
72
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
*/
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
alles OK
alles OK
alles OK
alles OK
alles OK
praktisch verloren
praktisch verloren
praktisch verloren
alles OK
alles OK
alles OK
alles OK
alles OK
alles OK
alles OK
Eine Anwendung: Mini-Parser
Ein weiteres (und wesentlich weiterführendes) Beispiel: ein kleiner Parser
([Ise01, S. 397]).
Als Parser bezeichnet man eine Komponente eines Compilers, deren Aufgabe
u.a. darin besteht zu prüfen, ob ein Programm / ein Text einer gegebenen
Syntax genügt.
Die Grammatik:
S
A
B
C
=
=
=
=
A "." .
letter | "(" B ")" .
A C .
{ ( "+" |"-" ) A } .
S ist das Startsymbol.
Programmbeispiel 7 (MiniParser)
1
package Kapitel04;
2
3
4
5
6
public class MiniParser {
char sym;
String word;
int pos;
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.3 Eine Anwendung: Mini-Parser
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
73
MiniParser(String word) {
pos = −1;
this.word = word;
}
void getSym() {
pos++;
if (pos < word.length()) {
sym = word.charAt(pos);
System.out.print(” ” + sym + ” ”);
} else
error();
}
void S() { // start symbol
getSym(); A();
if (sym == ’.’)
System.out.println(”\n syntax OK \n”);
else
error();
}
void error() {
System.out.println(”\n error at position ” + pos + ”!\n”);
System.exit(0);
}
void A() {
if (Character.isLetter(sym))
getSym();
else if (sym == ’(’) {
B();
if (sym == ’)’)
getSym();
else
error();
} else
error();
}
void B() {
getSym(); A();
C();
}
void C() {
while (sym == ’+’ || sym == ’−’) {
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.4 Heapsort rekursiv
getSym(); A();
48
}
49
}
public static void main (String [ ] args) {
System.out.println(”Start ...”);
MiniParser mini = new MiniParser(args[0]);
mini.S();
}
50
51
52
53
54
55
56
57
58
59
60
61
74
}
/*
Example 1: input string: ”(a+b).”
Start ...
( a + b ) .
syntax OK
62
63
64
65
66
Example 2: input string: ”((a+b)−(a−b)−(b−a+p)−(y−x)).”
Start ...
( ( a + b ) − ( a − b ) − ( b − a + p ) − ( y − x ) ) .
syntax OK
67
68
69
70
71
72
4.4
Example 3: input string: ”(a+((b)).”
Start ...
( a + ( ( b ) ) .
error at position 8!
*/
Heapsort rekursiv
Auch der Sortieralgorithmus HeapSort kann rekursiv formuliert werden durch
eine rekursive Formulierung des Algorithmus Heapify:
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.5 Das Acht-Damen-Problem
75
HeapifyR (a, i)
1 l ← Left(i)
2 r ← Right(i)
3 if l ≤ heap-size[a] and Prior-To(a[l], a[i])
4
then x ← l
5
else x ← i
6 if r ≤ heap-size[a] and Prior-To(a[r], a[x])
7
then x ← r
8 if x 6= i
9
then exchange a[i] ↔ a[x]
10
HeapifyR (a, x)
Der Rest des Algorithmus Heapsort bleibt ungeändert abgesehen davon, dass
jetzt natürlich stets diese rekursive Variante aufgerufen werden muß.
4.5
Das Acht-Damen-Problem
Das Problem wurde in dem einleitenden Kapitel bereits beschrieben.
Ein Algorithmus, um alle möglichen Lösungen zu finden:
Try(i)
1 for j ← 1 to n
2
do wähle k-ten Kandidaten
3
if annehmbar
4
then zeichne ihn auf
5
if i < n
6
then Try(i + 1)
7
else drucke Lösung
8
lösche Aufzeichnung
xi
aj
bk
ck
=
=
=
=
Position der Dame in der i-ten Spalte
j-te Zeile ist frei
k-te /-Diagonale ist frei
k-te \-Diagonale ist frei
Beispiel-Implementierung mit nur 4 × 4 Feldern:
1
2
package Einleitung;
public class AllQueens4 {
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.5 Das Acht-Damen-Problem
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
76
int count = 0, depth = −1;
boolean a [ ] = new boolean [4], b [ ] = new boolean [7],
c [ ] = new boolean [7];
int x [ ] = new int [4];
AllQueens4 () {
for (int i = 0; i < a.length; i++)
a[i] = true;
for (int i = 0; i < b.length; i++)
b[i] = true;
for (int i = 0; i < c.length; i++)
c[i] = true;
}
boolean getA(int i) {
return a[i−1];
}
void setA(int i, boolean b) {
a[i−1] = b;
}
boolean getB(int i) {
return b[i−2];
}
void setB(int i, boolean t) {
b[i−2] = t;
}
boolean getC(int i) {
return c[i+3];
}
void setC(int i, boolean b) {
c[i+3] = b;
}
int getX(int i) {
return x[i−1];
}
void setX(int i, int j) {
x[i−1] = j;
}
void print () {
count++;
System.out.print(”solution ” + count + ”: ”);
for (int k = 1; k <= 4; k++)
System.out.print(getX(k) + ” ”);
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.5 Das Acht-Damen-Problem
System.out.println();
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
77
}
private String blanks(int k) {
String s = ””;
while (k > 0) {
s += ” ”;
k−−;
}
return s;
}
void tryPosition(int i) {
depth++; // Rekursionstiefe
for (int j = 1; j <= 4; j++) {
boolean OK = getA(j) && getB(i+j) && getC(i−j);
System.out.println(blanks(depth) + ”( ” + i + ”, ” + j + ”)”
+ (OK ? ” let’s try it” : ” −−”));
if (getA(j) && getB(i+j) && getC(i−j)) {
setX(i, j);
setA(j, false); setB(i+j, false); setC(i−j, false);
if (i < 4)
tryPosition(i+1);
else {
System.out.print(blanks(depth)); print();
}
setA(j, true); setB(i+j, true); setC(i−j, true);
System.out.println(blanks(depth) + ”clear position ( ” + i + ”, ” + j + ”)”);
}
}
depth−−;
}
void tryPosition1(int i) {
for (int j = 1; j <= 4; j++) {
boolean OK = getA(j) && getB(i+j) && getC(i−j);
if (getA(j) && getB(i+j) && getC(i−j)) {
setX(i, j);
setA(j, false); setB(i+j, false); setC(i−j, false);
if (i < 4)
tryPosition1(i+1);
else {
System.out.print(blanks(depth)); print();
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.5 Das Acht-Damen-Problem
setA(j, true); setB(i+j, true); setC(i−j, true);
85
}
86
}
87
88
89
90
91
92
93
94
95
78
}
}
class AllQueensAppl {
public static void main (String [ ] args) {
AllQueens4 q = new AllQueens4();
q.tryPosition(1);
}
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
4.5 Das Acht-Damen-Problem
79
Ergebnis:
Start ...
(1, 1): let’s try it
(2, 1) -(2, 2) -(2, 3): let’s try it
(3, 1) -(3, 2) -(3, 3) -(3, 4) -clear position (2, 3)
(2, 4): let’s try it
(3, 1) -(3, 2): let’s try it
(4, 1) -(4, 2) -(4, 3) -(4, 4) -clear position (3, 2)
(3, 3) -(3, 4) -clear position (2, 4)
clear position (1, 1)
(1, 2): let’s try it
(2, 1) -(2, 2) -(2, 3) -(2, 4): let’s try it
(3, 1): let’s try it
(4, 1) -(4, 2) -(4, 3): let’s try it
solution 1: 2 4 1 3
clear position (4, 3)
(4, 4) -clear position (3, 1)
(3, 2) -(3, 3) -(3, 4) -clear position (2, 4)
clear position (1, 2)
(1, 3): let’s try it
(2, 1): let’s try it
(3, 1) -(3, 2) -(3, 3) -(3, 4): let’s try it
(4, 1) -(4, 2): let’s try it
solution 2: 3 1 4 2
clear position (4, 2)
(4, 3) -(4, 4) -clear position (3, 4)
clear position (2, 1)
(2, 2) -(2, 3) -(2, 4) -clear position (1, 3)
(1, 4): let’s try it
(2, 1): let’s try it
(3, 1) -(3, 2) -(3, 3): let’s try it
(4, 1) -(4, 2) -(4, 3) -(4, 4) -clear position (3, 3)
(3, 4) -clear position (2, 1)
(2, 2): let’s try it
(3, 1) -(3, 2) -(3, 3) -(3, 4) -clear position (2, 2)
(2, 3) -(2, 4) -clear position (1, 4)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Kapitel 5
Dynamische Datenstrukturen
5.1
Allgemeines
Kenntnisse im Umgang mit dynamischen Datenstrukturen gehören traditionell zur elementaren Grundausstattung eines jeden Programmierers.
Man kann vom Prinzip her 3 Arten von Datenstrukturen unterscheiden:
1. (rein) statische
2. halbdynamische und
3. dynamische Datenstrukturen.
Statisch bedeutet, dass die Größe der Struktur im Programmtext und damit
zur Compilezeit bereits festgelegt ist; sie kann daher zur Laufzeit nicht mehr
nachträglich geändert werden (daher statisch“). Ein Beispiel sind die (nor”
malen) Arrays in Pascal. Die Deklaration verlangt in dem Fall eine explizite
Angabe der Größe.
Halbdynamisch bedeutet, dass die Größe zwar zur Laufzeit gewählt werden
kann, dann aber nachträglich – jedenfalls effizient – nicht mehr zu ändern
ist. Ein typisches Beispiel sind die Arrays in Java. Will man die Größe eines
solchen Arrays nachträglich ändern (natürlich ohne Verlust der momentanen
Arraybelegung), so geht das nur durch Umkopieren von Werten.
Und dynamisch schließlich bedeutet, dass die Struktur zur Laufzeit beliebig
vergrößert und nach Bedarf wieder verkleinert werden kann. Die Standardbeispiele zu dieser Sorte von Strukturen aus Einführungsvorlesungen sind
regelmäßig die verketteten Listen und die binären Suchbäume.
Ein mögliches Mißverständnis besteht darin, die ersten beiden Strukturen,
insbesondere die (rein) statischen, als etwas minderwertig“ anzusehen, da
”
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.2 Verkettete Listen
81
sie nicht die gleiche Flexibilität besitzen wie die dynamischen Datenstrukturen. Das ist deswegen falsch, da man – wie immer in der Informatik – Kosten
und Nutzen in Relation zueinander zu sehen hat. Wenn man eine Struktur
benötigt, deren Größe kaum variiert und eine bestimmte Schranke auch nie
überschreitet, dann ist ein statisches Array immer die beste Wahl, weil der
Zugriff auf eine solche Struktur besonders effizient ist (direkter Zugriff auf
Hauptspeicheradressen). Unter Voraussetzungen solcher Art kann man auch
binäre Bäume auf Arrays abbilden, was zu einem der schnellen Sortierverfahren führt (Heapsort, Abschnitt 3.3).
Dynamische Strukturen benötigen umgekehrt Elemente, die die Verkettung
realisieren. Sind die Nutzdaten obendrein noch klein, z.B. jeweils ein IntegerWert pro Knoten, dann ist der prozentuale Verwaltungsaufwand extrem groß.
In vielen Anwendungen sind natürlich auch die Benutzerdaten nicht nur
Werte eines elementaren Datentyps. Variiert die Größe der Gesamtstruktur
obendrein erheblich, dann ist die Verwendung einer dynamischen Struktur
natürlich unausweichlich.
Ein weiterer möglicher Fehlschluß besteht in dem Argument, Rechner wären
heute sowieso schnell und Hauptspeicher groß, so dass man auch alles gleich
dynamisch machen kann. Das ist deswegen nicht richtig, weil die Anforderungen heutiger Software ebenso und durchaus überproportinal zugenommen
haben. (Zitat nach Martin Reiser: Die Software wird schneller langsam als
”
die Hardware schneller wird.“)
5.2
Verkettete Listen
Verkettete Listen = linked lists.
Ein typisches Momentbild einer linearen Liste zeigt Abb. 5.1.
prev
head[L]
9
key
next
16
NIL
4
1
Abbildung 5.1: Lineare Liste
Basisoperationen:
• List-Search
• List-Insert
• List-Delete
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.2 Verkettete Listen
82
List-Search:
Algorithmus 40
List-Search(L, k)
1 x ← head [L]
2 while x 6= nil and key[x] 6= k
3
do x ← next[x]
4 return x
List-Insert: Abb. 5.2
x:
1
3
4
....
head[L]
Abbildung 5.2: Einfügen in eine lineare Liste
Algorithmus 41
List-Insert(L, x)
1 next[x] ← head [L]
2 if head [L] 6= nil
3
then prev [head [L]] ← x
4 head [L] ← x
5 prev [x] ← nil
List-Delete: Abb. 5.3
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.2 Verkettete Listen
83
x
2
....
head[L]
5
Abbildung 5.3: Löschen aus einer linearen Liste
Algorithmus 42
List-Delete(L, x)
1 if prev [x] 6= nil
2
then next[prev [x]] ← next[x]
3
else head [L] ← next[x]
4 if next[x] 6= nil
5
then prev [next[x]] ← prev [x]
Eine Anwendung, in der als zugrundeliegende Struktur eine verkettete Liste
eine gute Wahl ist, ist die auf Seite 44 bereits angedeutete weitere Variante
für die selbstorganisierende Suche.
Klausuraufgabe Informatik II vom SS 2000, etwas modifiziert: Ein Array ist
hier vollkommen unbrauchbar. Die Einzelheiten zu search sind in Abb. 5.4
dargestellt.
Programmbeispiel 8 (Selbstorganisierende Suche (2))
1
package Kapitel05;
2
3
4
5
6
7
8
9
10
interface ItemInterface {
void setKey (int k);
int getKey ();
void setNext (ItemInterface next);
ItemInterface getNext ();
}
interface ListInterface {
void insert (ItemInterface item);
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.2 Verkettete Listen
84
1 q = head
p.next.k == 7
q p
p
3 p.next = p.next.next
head
10
9
4
7
2
5
null
2 head = p.next
4 head.next = q
Abbildung 5.4: Selbstorganisierende Suche
void show ();
boolean search (ItemInterface item);
11
12
13
}
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ListItem implements ItemInterface {
private int k;
private ItemInterface next;
ListItem (int k) {
this.k = k;
this.next = null;
}
public void setKey (int k){
this.k = k;
}
public int getKey (){
return k;
}
public void setNext (ItemInterface next){
this.next = next;
}
public ItemInterface getNext (){
return next;
}
public boolean equals (Object item) {
return this.k == ((ListItem)item).k;
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.2 Verkettete Listen
37
85
}
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class AdaptiveList2 implements ListInterface {
private ItemInterface head;
AdaptiveList2 () {
head = null;
}
public void insert (ItemInterface item) {
item.setNext(head);
head = item;
}
public void show () {
ItemInterface p = head;
if (p == null) {
System.out.println(”−”);
} else {
do {
System.out.print(p.getKey()+ ”\t”);
p = p.getNext();
} while (p != null);
System.out.println();
}
}
public boolean search (ItemInterface item) {
ItemInterface p, q;
if (head == null)
return false;
else if (item.equals(head))
return true;
else {
p = head;
while (p.getNext() != null) {
if (p.getNext().equals(item)) {
q = head;
head = p.getNext();
p.setNext(p.getNext().getNext());
head.setNext(q);
return true;
} else
p = p.getNext();
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.2 Verkettete Listen
return false;
78
}
79
}
80
81
86
}
82
83
84
85
86
87
class AdaptiveListDemo {
public static void main (String args [ ]) {
AdaptiveList2 p = new AdaptiveList2();
p.search(new ListItem(13));
p.show();
88
p.insert(new ListItem(19)); p.insert(new ListItem(3));
p.show();
p.search(new ListItem(19));
p.show();
89
90
91
92
93
p = new AdaptiveList2(); // Liste neu initialisiert
p.insert(new ListItem(12)); p.insert(new ListItem(8));
p.show();
p.insert(new ListItem(7)); p.search(new ListItem(7));
p.show();
94
95
96
97
98
99
p.insert(new ListItem(20)); p.insert(new ListItem(4));
p.show();
p.search(new ListItem(13));
p.show();
100
101
102
103
}
104
105
}
106
107
108
/*
result:
109
110
111
112
113
114
115
116
−
3
19
8
7
4
4
19
3
12
8
20
20
12
7
7
8
8
12
12
117
118
*/
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
5.3
87
Anwendungsbeispiel: Graphik-Editor
Dieses Programm steht hier u. a. als Beispiel für die Anwendung einer dynamischen Datenstruktur. Es enthält aber ganz wesentlich weitergehende
Elemente, wie sie für moderne Software- und Programmiertechniken typisch
sind.
Der Graphik-Editor ist hier abstrahiert: der wesentliche Punkt dieses größeren Beispiels besteht in der speziellen Klassenstruktur, die es ermöglicht, das
Programm leicht zu erweitern.
(Stichwort aus der Softwaretechnik: separation of concerns)
Dieser (abstrahierte) Grafik-Editor ist zu sehen als Java-Variante in Analogie
zu dem Beispiel Grafik-Editor in Oberon nach Reiser/Wirth.
technische Vorbereitung:
Dynamisches Laden von Klassen
MyClass.java
Diese Klasse soll nachträglich erweitert (d.h. konkretisiert“) werden.
”
1
public abstract class MyClass { // abstract class
2
abstract void print();
3
}
ReflectionsTest.java
Diese Klasse enthält die eigentliche Realisierung des Mechanismus, der zu den
reflections gehört. Die möglichen Konkretisierungen von MyClass.java sind
hier namentlich noch nicht bekannt; sie können einem größeren Programmsystem daher nachträglich hinzugefügt werden, obwohl diese Klasse bereits
compiliert wurde (und die Quelle vielleicht sogar nicht mehr zur Verfügung
steht).
1
2
3
4
5
6
7
8
9
/* dynamic loading of classes */
import java.io.*;
public class ReflectionsTest {
public static void main (String args [ ]) {
System.out.print(”Enter name of class to be loaded dynamically: ”);
String className = MyIO.getString();
MyClass myCl;
try {
// in detail:
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
Class C = Class.forName(className);
Object obj = C.newInstance();
myCl = (MyClass) obj;
// or condensed into one line:
// myCl = (MyClass) Class.forName(className).newInstance();
myCl.print();
} catch (InstantiationException e) {
System.out.println(”InstantiationException: ” + e);
} catch (IllegalAccessException e) {
System.out.println(”IllegalAccessException: ” + e);
} catch (ClassNotFoundException e) {
System.out.println(”ClassNotFoundException: ” + e);
}
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
88
}
}
// auxiliary class
class MyIO {
public static String getString() {
DataInput in = new DataInputStream(System.in);
String result = ””;
try {
result = in.readLine().trim();
} catch (IOException io) {}
return result;
}
}
NewClassA.java
Die erste Konkretisierung von MyClass.java.
1
2
3
4
5
6
7
8
9
public class NewClassA extends MyClass {
int I = 123;
NewClassA () {
System.out.println(”This is class NewClassA.”);
}
public void print() {
System.out.println(”value is: ” + I);
}
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
89
NewClassB.java
Die zweite Konkretisierung von MyClass.java.
1
2
3
4
5
6
7
8
9
public class NewClassB extends MyClass {
int I = 456;
NewClassB () {
System.out.println(”This is class NewClassB.”);
}
public void print() {
System.out.println(”value is: ” + I);
}
}
Der Graphik-Editor
Erläuterungen zum Graphik-Editor
Klassen / Interface:
Draw :
• ist die eigentliche Applikation (enthält main)
• Operationen sind abstrakt“ (aber nicht im technischen Sinn von Java)
”
Graph : im wesentlichen die Datenstruktur (Liste), die die Figuren enthält;
die Liste ist i. a. heterogen (s. Abb. 5.5)
Figure Interface
Ellipse :
• konkrete Figur: implementiert Figure
• Attribute: x, y, a, b
Rectangle :
• weitere konkrete Figur: implementiert Figure
• Attribute: x, y, w, h
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
90
heterogene Liste
null
rect
ellipse
line
ellipse
Abbildung 5.5: Heterogene Liste
Implementierung
Programmbeispiel 9 (Grafik-Editor: Benutzerschnittstelle Draw)
1
package Kapitel05.graphedit;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;
/** Class Draw: application class */
public class Draw {
private final static int Open = 1;
private final static int CreateFigure = 2;
private final static int DrawAll = 3;
private final static int PrintAreas = 4;
private final static int MoveAll = 5;
private final static int Quit = 6;
private final static String
Menu = ”\n”
+ Open + ” −> Open\n”
+ CreateFigure + ” −> CreateFigure\n”
+ DrawAll + ” −> DrawAll\n”
+ PrintAreas + ” −> PrintAreas\n”
+ MoveAll + ” −> MoveAll\n”
+ Quit + ” −> Quit\n”
+ ”\nMake your choice:”;
private static BufferedReader in =
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
91
new BufferedReader(new InputStreamReader(System.in));
private static Graph g = new Graph();
public static void CreateFigure() {
g.CreateFigure();
}
public static void DrawAll() {
g.DrawAll();
System.out.println();
}
public static void PrintAreas() {
g.PrintAreas();
}
public static void MoveAll() {
g.MoveAll();
}
public static void Open() {
g = new Graph();
}
public static void main(String[ ] arg) {
int menu = 0;
String buf;
do {
System.out.println(Menu);
try {
buf = in.readLine();
System.out.println();
menu = Integer.valueOf(buf).intValue();
} catch (Exception e) {
System.out.println(”*** error *** ” + e);
menu = 0; continue;
}
switch (menu) {
case Open: Open(); break;
case CreateFigure: CreateFigure(); break;
case DrawAll: DrawAll(); break;
case PrintAreas: PrintAreas(); break;
case MoveAll: MoveAll(); break;
case Quit: break;
default: System.out.println(”*** Invalid case! ***”);
}
} while (menu != Quit);
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
}
63
64
92
}
Programmbeispiel 10 (Grafik-Editor: Basisstruktur Graph)
1
package Kapitel05.graphedit;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;
import java.util.*;
/** Class Graph: basic class containing
basic structure */
public class Graph extends LinkedList{
private static String PACKAGE = ”Kapitel05.graphedit.figures.”;
private BufferedReader in;
private ListIterator listIt;
public Graph() {
in = new BufferedReader(new InputStreamReader(System.in));
}
public void CreateFigure() {
Figure f = newFigure();
if (f != null) {
f.Draw();
addLast(f);
}
}
public void DrawAll() {
// traverse list:
listIt = listIterator(0);
Figure f;
while (listIt.hasNext()) {
f = (Figure) listIt.next();
f.Draw();
}
}
public void PrintAreas() {
Figure f;
listIt = listIterator(0);
while (listIt.hasNext()) {
f = (Figure) listIt.next();
System.out.println(f.GetArea());
}
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
93
public void MoveAll() {
listIt = listIterator(0);
Figure f;
int dx, dy;
String buf;
try {
System.out.print(”MoveAll()...\ndx: ”);
buf = in.readLine();
dx = Integer.valueOf(buf).intValue();
System.out.print(”dy: ”);
buf = in.readLine();
dy = Integer.valueOf(buf).intValue();
while (listIt.hasNext()) {
f = (Figure) listIt.next();
f.Move(dx,dy);
}
} catch (Exception e) {
System.out.println(”error!”);
}
}
private Figure newFigure() {
String curClass;
Figure f = null;
try {
System.out.print(”newFigure()...\nenter class name: ”);
curClass = in.readLine();
curClass = PACKAGE + curClass;
65
66
67
68
69
70
/* expanded formulation showing
the types involved */
Class C = Class.forName(curClass);
Object obj = C.newInstance();
f = (Figure) obj;
71
72
73
74
75
76
77
78
// Java programmers prefer this form:
f = (Figure)Class.forName(curClass).newInstance();
f.Init();
} catch (IOException e) {
System.out.println(e);
} catch (InstantiationException e) {
System.out.println(e);
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
} catch (IllegalAccessException e) {
System.out.println(e);
} catch (ClassNotFoundException e) {
System.out.println(e);
}
return f;
79
80
81
82
83
84
}
85
86
94
}
Programmbeispiel 11 (Grafik-Editor: Interface Figure)
1
package Kapitel05.graphedit;
2
3
4
5
6
7
8
9
10
import java.io.*;
/** Interface Figure */
public interface Figure {
public abstract void Draw();
public abstract void Move(int dx, int dy);
public abstract float GetArea();
public abstract void Init();
}
Programmbeispiel 12 (Grafik-Editor: konkrete Figur Ellipse)
1
package Kapitel05.graphedit.figures;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Kapitel05.graphedit.*;
import java.io.*;
/** Class Ellipse */
public class Ellipse implements Figure {
private int x = 0, y = 0, a = 0, b = 0;
private BufferedReader in;
public Ellipse() {
in = new BufferedReader(new InputStreamReader(System.in));
}
public void Draw() {
System.out.println(”drawing ellipse...”);
System.out.println(x + ” ” + y + ” ”
+ a + ” ” + b);
}
public void Move(int dx, int dy) {
System.out.println(”moving ellipse...”);
x += dx; y += dy;
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
}
public float GetArea() {
return (float)(Math.PI * a * b);
}
public void Init() {
String buf;
boolean ok = false;
do {
try {
System.out.print(”creating ellipse...\nx: ”);
buf = in.readLine();
x = Integer.valueOf(buf).intValue();
System.out.print(”y: ”);
buf = in.readLine();
y = Integer.valueOf(buf).intValue();
System.out.print(”a: ”);
buf = in.readLine();
a = Integer.valueOf(buf).intValue();
System.out.print(”b: ”);
buf = in.readLine();
b = Integer.valueOf(buf).intValue();
ok = true;
} catch (Exception e) {
System.out.println(”error!”);
}
} while (!ok);
}
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
95
}
Programmbeispiel 13 (Grafik-Editor: konkrete Figur Rectangle)
1
package Kapitel05.graphedit.figures;
2
3
4
5
6
7
8
9
10
11
import Kapitel05.graphedit.*;
import java.io.*;
/** Class Rectangle */
public class Rectangle implements Figure {
private int x = 0, y = 0, w = 0, h = 0;
private BufferedReader in;
public Rectangle() {
in = new BufferedReader(new InputStreamReader(System.in));
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.3 Anwendungsbeispiel: Graphik-Editor
public void Draw() {
System.out.println(”drawing rectangle...”);
System.out.println(x + ” ” + y + ” ”
+ w + ” ” + h);
}
public void Move(int dx, int dy) {
System.out.println(”moving rectangle...”);
x += dx; y += dy;
}
public float GetArea() {
return w * h;
}
public void Init() {
String buf;
boolean ok = false;
do {
try {
System.out.print(”creating rectangle...\nx: ”);
buf = in.readLine();
x = Integer.valueOf(buf).intValue();
System.out.print(”y: ”);
buf = in.readLine();
y = Integer.valueOf(buf).intValue();
System.out.print(”w: ”);
buf = in.readLine();
w = Integer.valueOf(buf).intValue();
System.out.print(”h: ”);
buf = in.readLine();
h = Integer.valueOf(buf).intValue();
ok = true;
} catch (Exception e) {
System.out.println(”error!”);
}
} while (!ok);
}
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
96
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.4 Binäre Suchbäume (BST)
5.4
97
Binäre Suchbäume (BST)
BST: binary search tree
Ein Beispiel:
Der Suchbaum in Abb. 5.6 entsteht durch Einfügen der Werte
12, 3, 16, 1, 4, 6, 15, 19, 22, 7, 20, 13
12
3
1
16
4
15
6
19
13
7
22
20
Abbildung 5.6: Binärer Suchbaum
Die Regel: Eingefügt werden stets Blätter:
• kleinere Werte nach links,
• größere nach rechts,
• bereits vorhandene zurückweisen (Der Schlüssel soll eindeutig sein.)
Durchlaufen ( Traversieren“) binärer Bäume (nicht nur von BSTs):
”
4 verschiedene Arten
1. preorder
2. inorder
3. postorder
4. levelorder
im Detail:
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.4 Binäre Suchbäume (BST)
98
1. preorder:
• besuche“
”
die Wurzel,
• dann den linken Unterbaum,
• dann den rechten Unterbaum
2. inorder:
• besuche“
”
den linken Unterbaum,
• dann die Wurzel,
• dann den rechten Unterbaum
3. postorder:
• besuche“
”
den linken Unterbaum,
• dann den rechten Unterbaum,
• dann die Wurzel
4. levelorder:
besuche“ alle Knoten, wie graphisch dargestellt von oben nach unten und
”
gleichzeitig jeweils von links nach rechts, also ebenenweise“
”
Struktur eines BSTs mit Knoten und Verbindungen zwischen den Knoten:
siehe Abb. 5.7
Die Algorithmen formalisiert:
Durchlaufen (Traversieren):
Algorithmus 43
Inorder-Tree-Walk(x)
1 if x 6= nil
2
then Inorder-Tree-Walk(left[x])
3
print key[x]
4
Inorder-Tree-Walk(right[x])
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.4 Binäre Suchbäume (BST)
99
10
x
"parent"
p[x]
8
12
left[x]
right[x]
6
NIL
NIL
9
NIL
NIL
17
NIL
NIL
NIL
Abbildung 5.7: Binärer Suchbaum (Ausschnitt)
Algorithmus 44
Preorder-Tree-Walk(x)
1 if x 6= nil
2
then print key[x]
3
Preorder-Tree-Walk(left[x])
4
Preorder-Tree-Walk(right[x])
Algorithmus 45
Postorder-Tree-Walk(x)
1 if x 6= nil
2
then Postorder-Tree-Walk(left[x])
3
Postorder-Tree-Walk(right[x])
4
print key[x]
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.4 Binäre Suchbäume (BST)
100
Algorithmus 46
Tree-Minimum(x)
1 while left[x] 6= nil
2
do x ← left[x]
3 return x
Wirkung von Tree-Minimum(x): Rückgabewert = Zeiger auf das kleinste
Element im Unterbaum mit x als Wurzel
analog:
Algorithmus 47
Tree-Maximum(x)
1 while right[x] 6= nil
2
do x ← right[x]
3 return x
Zur Erläuterung des folgenden Algorithmus Tree-Successor(x):
Satz (aus CLRS, p. 260, ex. 12.6-6):
Gegeben ein BST T mit verschiedenen Schlüsseln. Wenn der rechte Unterbaum eines Knotens x in T leer ist und x einen Nachfolger y (im Sinne von
Folgewert) hat, dann ist y der niedrigste Vorfahr (ancestor ) von x, dessen
linker Kindknoten auch ein Vorfahr von x ist.
(Jeder Knoten ist / gilt hier als sein eigener Vorgänger.)
Algorithmus 48
Tree-Successor(x)
1 if right[x] 6= nil
2
then return Tree-Minimum(right[x])
3 y ← p[x]
4 while y 6= nil and x = right[y]
5
do x ← y
6
y ← p[y]
7 return y
Wirkung von Tree-Successor(x): Rückgabewert = Zeiger auf den Knoten,
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.4 Binäre Suchbäume (BST)
101
der vom Schlüsselwert her x folgt, falls ein solcher existiert, oder sonst nil.
Wie kann man einen BST ausdrucken mit Verdeutlichung der Baumstruktur,
aber ohne den Einsatz grafischer Hilfsmittel?
dazu: AuD I-Klausuraufgabe 4 vom SS 05
Tree-Search:
Algorithmus 49
Iterative-Tree-Search(x, k)
1 while x 6= nil and k 6= key[x]
2
do if k < key[x]
3
then x ← left[x]
4
else x ← right[x]
5 return x
Einfügen und Löschen:
Algorithmus 50
Tree-Insert(T, z)
1 y ← nil
2 x ← root[T ]
3 while x 6= nil
4
do y ← x
5
if key[z] < key[x]
6
then x ← left[x]
7
else x ← right[x]
8 p[z] ← y
9 if y = nil
10
then root[T ] ← z
11
else if key[z] < key[y]
12
then left[y] ← z
13
else right[y] ← z
Tree T was empty
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.5 B-Bäume
102
Algorithmus 51
Tree-Delete(T, z)
1 if left[z] = nil or right[z] = nil
2
then y ← z
3
else y ← Tree-Successor(z)
4 if left[y] 6= nil
5
then x ← left[y]
6
else x ← right[y]
7 if x 6= nil
8
then p[x] ← p[y]
9 if p[y] = nil
10
then root[T ] ← x
11
else if y = left[p[y]]
12
then left[p[y]] ← x
13
else right[p[y]] ← x
14 if y 6= z
15
then key[z] ← key[y]
16
copy y’s satellite data into z
17 return y
5.5
B-Bäume
Binäre Suchbäume können z. B. beim Einfügen von Elementen entarten, im
Extremfall bekommen sie die Gestalt einer linearen Liste. Das Problem des
Ausgleichs von Bäumen ist aufwändig.
Eine andere Struktur sind die sog. B-Bäume von Bayer und McCreight
(1970), auch Bayer-Bäume genannt; das B steht für balanced oder auch für
Bayer Diese Bäume haben den Vorteil, dass sie stets vollkommen ausgeglichen sind, d.h., alle Blätter befinden sich stets auf gleicher Höhe.
Zwei wichtige Punkte zu B-Bäumen:
• B-Bäume sind extrem wichtig für den Zugriff auf externe Speicher (Filesysteme und Datenbanken).
• Zugriff auf externe Speicher (Plattenspeicher) ist um den Faktor 106 -mal
langsamer als auf Primärspeicher (Hauptspeicher) (10 ms zu 10 ns).
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.5 B-Bäume
103
Hinweis: Es gibt verschiedene Definitionen von B-Bäumen; die folgende (gegenüber den letzten Versionen dieses Skript erheblich geänderte) Definition
basiert auf dem Buch CLRS. Insbesondere die Operation Insert ist hier besonders einfach.
Ein typisches Beispiel eines B-Baums zeigt Abb. 5.8.
9
12 15
3 6
4 5
1 2
7 8
10 11
13 14
16 17 18 19
Abbildung 5.8: B-Baum (mit t = 3) nach Einfügen der Werte 1, 2, 3, . . . , 19
Die definierenden Eigenschaften eines B-Baums:
1. Jeder Knoten x besitzt die folgenden Felder:
die Anzahl n[x] von Schlüsseln
die n[x] Schlüssel selbst: key i [x] in aufsteigend sortierter Reihenfolge
einen booleschen Wert leaf [x]: ist der Knoten ein Blatt?
2. Jeder Knoten hat n[x] + 1 Zeiger ci [x]; die Zeiger von Blättern sind undefiniert.
3. Wenn ki ein Schlüssel ist in dem Unterbaum mit Wurzel ci [x], dann gilt
k1 ≤ key 1 [x] ≤ k2 ≤ key 2 [x] ≤ · · · ≤ kn[x] ≤ key n[x] ≤ kn[x]+1
4. Alle Blätter liegen auf der gleichen Höhe.
5. Es gibt einen für jeden B-Baum charakteristischen Parameter t, den minimum degree.
Jeder Knoten außer der Wurzel hat mindestens t − 1 Schlüssel. Jeder
innere Knoten außer der Wurzel hat mindestens t Kinder.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.5 B-Bäume
104
Jeder Knoten hat höchstes 2t − 1 Schlüssel und damit 2t Kinder. Ein
Knoten ist voll, wenn er genau 2t − 1 Schlüssel enthält.
Wichtige Operationen sind – analog zu binären Suchbäumen:
• Einfügen
• Suchen
• Löschen (im Skript nicht behandelt)
Die Algorithmen im Detail (CLRS, p. 441):
B-Tree-Search(x, k)
1 i←1
2 while i ≤ n[x] and k > key i [x]
3
do i ← i + 1
4 if i ≤ n[x] and k = key i [x]
5
then return (x, i)
6 if leaf [x]
7
then return nil
8
else Disk-Read(ci [x])
9
return B-Tree-Search(ci [x], k)
B-Tree-Create(T )
1 x ← Allocate-Node()
2 leaf [x] ← true
3 n[x] ← 0
4 Disk-Write(x)
5 root[T ] ← x
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.5 B-Bäume
105
B-Tree-Split-Child(x, i, y)
1 z ← Allocate-Node()
2 leaf [z] ← leaf [y]
3 n[z] ← t − 1
4 for j ← 1 to t − 1
5
do key j [z] ← key j+t [y]
6 if not leaf [y]
7
then for j ← 1 to t
8
do cj [z] ← cj+t [y]
9 n[y] ← t − 1
10 for j ← n[x] + 1 downto i + 1
11
do cj+1 [x] ← cj [x]
12 ci+1 [x] ← z
13 for j ← n[x] downto i
14
do key j+1 [x] ← key j [x]
15 key i [x] ← key t [y]
16 n[x] ← n[x] + 1
17 Disk-Write(y)
18 Disk-Write(z)
19 Disk-Write(x)
B-Tree-Insert(T, k)
1 r ← root[T ]
2 if n[r] = 2t − 1
3
then s ← Allocate-Node()
4
root[T ] ← s
5
leaf [s] ← false
6
n[s] ← 0
7
c1 [s] ← r
8
B-Tree-Split-Child(s, 1, r)
9
B-Tree-Insert-Nonfull(s, k)
10
else B-Tree-Insert-Nonfull(r, k)
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.5 B-Bäume
106
B-Tree-Insert-Nonfull(x, k)
1 i ← n[x]
2 if leaf [x]
3
then while i ≥ 1 and k ≤ key i [x]
4
do key i+1 [x] ← key i [x]
5
i←i−1
6
key i+1 [x] ← k
7
n[x] ← n[x] + 1
8
Disk-Write(x)
9
else while i ≥ 1 and k ≤ key i [x]
10
do i ← i − 1
11
i←i+1
12
Disk-Read(ci [x])
13
if n[ci [x]] = 2t − 1
14
then B-Tree-Split-Child(x, i, ci [x])
15
if k > key i [x]
16
then i ← i + 1
17
B-Tree-Insert-Nonfull(ci [x], k)
Beispiele:
Beispiele für Einfügen: Werte von 1, 2, 3, . . . , 30:
1. t = 2, Darstellung: preorder-Traversierung des B-Baums
8 16
x
4
x x
2
6
12
x x
1
3
x x
x x
5
7
x x
x x
x x
x x
10
x x
9 x x
11 x x
14
20 24
x
18
22
x x
13
15
x x
x x
17
19
x x
x x
21
x x
x x
x x
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.5 B-Bäume
107
26 28
23 x x
x
25 x x
27 x x
29 30 x
2. t = 3
9 18
x x x
3 6
x x x
1 2 x x
4 5 x x
7 8 x x
12 15 x x x
10 11 x
13 14 x
16 17 x
21 24 27 x x
19 20 x
22 23 x
25 26 x
28 29 30
x
x
x
x x
x x
x x
x x
x x
x x
x x
3. t = 4
4 8 12 16 20 24
1 2 3 x
5 6 7 x
9 10 11
13 14 15
17 18 19
21 22 23
25 26 27
x
x x x
x x x
x x x x
x x x x
x x x x
x x x x
28 29 30
x
4. Werte von 1 bis 500:
t = 10
100 200 300 x x x x x x x x x x x x x x x x
10 20 30 40 50 60 70 80 90 x x x x x x x x x x
1 2 3 4 5 6 7 8 9 x x x x x x x x x x
11 12 13 14 15 16 17 18 19 x x x x x x x x x x
21 22 23 24 25 26 27 28 29 x x x x x x x x x x
31 32 33 34 35 36 37 38 39 x x x x x x x x x x
41 42 43 44 45 46 47 48 49 x x x x x x x x x x
51 52 53 54 55 56 57 58 59 x x x x x x x x x x
61 62 63 64 65 66 67 68 69 x x x x x x x x x x
71 72 73 74 75 76 77 78 79 x x x x x x x x x x
81 82 83 84 85 86 87 88 89 x x x x x x x x x x
91 92 93 94 95 96 97 98 99 x x x x x x x x x x
110 120 130 140 150 160 170 180 190 x x x x x x x x x x
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.5 B-Bäume
108
101
111
121
131
141
151
161
171
181
191
210 220 230
201
211
221
231
241
251
261
271
281
291
310 320 330
301
311
321
331
341
351
361
371
381
391
401
411
421
431
441
451
461
471
481
491
102
112
122
132
142
152
162
172
182
192
240
202
212
222
232
242
252
262
272
282
292
340
302
312
322
332
342
352
362
372
382
392
402
412
422
432
442
452
462
472
482
492
103
113
123
133
143
153
163
173
183
193
250
203
213
223
233
243
253
263
273
283
293
350
303
313
323
333
343
353
363
373
383
393
403
413
423
433
443
453
463
473
483
493
104
114
124
134
144
154
164
174
184
194
260
204
214
224
234
244
254
264
274
284
294
360
304
314
324
334
344
354
364
374
384
394
404
414
424
434
444
454
464
474
484
494
105
115
125
135
145
155
165
175
185
195
270
205
215
225
235
245
255
265
275
285
295
370
305
315
325
335
345
355
365
375
385
395
405
415
425
435
445
455
465
475
485
495
106
116
126
136
146
156
166
176
186
196
280
206
216
226
236
246
256
266
276
286
296
380
306
316
326
336
346
356
366
376
386
396
406
416
426
436
446
456
466
476
486
496
107
117
127
137
147
157
167
177
187
197
290
207
217
227
237
247
257
267
277
287
297
390
307
317
327
337
347
357
367
377
387
397
407
417
427
437
447
457
467
477
487
497
108 109 x x x x x x x x x x
118 119 x x x x x x x x x x
128 129 x x x x x x x x x x
138 139 x x x x x x x x x x
148 149 x x x x x x x x x x
158 159 x x x x x x x x x x
168 169 x x x x x x x x x x
178 179 x x x x x x x x x x
188 189 x x x x x x x x x x
198 199 x x x x x x x x x x
x x x x x x x x x x
208 209 x x x x x x x x x x
218 219 x x x x x x x x x x
228 229 x x x x x x x x x x
238 239 x x x x x x x x x x
248 249 x x x x x x x x x x
258 259 x x x x x x x x x x
268 269 x x x x x x x x x x
278 279 x x x x x x x x x x
288 289 x x x x x x x x x x
298 299 x x x x x x x x x x
400 410 420 430 440 450 460 470 480 490
308 309 x x x x x x x x x x
318 319 x x x x x x x x x x
328 329 x x x x x x x x x x
338 339 x x x x x x x x x x
348 349 x x x x x x x x x x
358 359 x x x x x x x x x x
368 369 x x x x x x x x x x
378 379 x x x x x x x x x x
388 389 x x x x x x x x x x
398 399 x x x x x x x x x x
408 409 x x x x x x x x x x
418 419 x x x x x x x x x x
428 429 x x x x x x x x x x
438 439 x x x x x x x x x x
448 449 x x x x x x x x x x
458 459 x x x x x x x x x x
468 469 x x x x x x x x x x
478 479 x x x x x x x x x x
488 489 x x x x x x x x x x
498 499 500 x x x x x x x x x
5. t = 2
willkürliche Zahlen:
20, 30, 6, 19, 5, 9, 17, 14, 27, 32, 7, 33
17
x x
6
x x
20 30
5 x x
7 9 14
x
19 x x
27 x x
32 33 x
R. Rüdiger
Algorithmik I
— 1. März 2006 —
5.5 B-Bäume
109
6. Mit den folgenden Werten entsteht ein perfekt ausgeglichener BST:
12, 6, 18, 9, 3, 21, 14, 1, 4, 8, 10, 13, 15, 20, 28
(dazu Abb.5.9)
Der entstehende B-Baum mit t = 2 sieht trotzdem anders aus (Abb. 5.10):
12
x x
6
x x
1 3 4
8 9 10
18
x x
13 14 15
20 21 28
12
6
18
3
9
1
4
8
14
10
21
13
15
20
28
Abbildung 5.9: Perfekt ausgeglichener BST (zum Vergleich mit Abb. 5.10)
12
6
1
3
4
18
8
9
10
13
14
15
20
21
28
Abbildung 5.10: B-Baum für t = 2
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Kapitel 6
Hashverfahren
6.1
Allgemeines
• Hashverfahren sind Speicherungs- und Suchverfahren.
• Ein Problem beim Suchen in BSTs (Binary Search Trees) ergibt sich daraus, dass die Position eines Elementes aus der Position relativ zu anderen
Einträgen bestimmt wird; sie hängt daher von der Größe des Baumes ab.
Komplexität: O(log n) entsprechend der Tiefe des Baums bei Ausgeglichenheit
• Eine Frage wäre: geht es noch besser, idealerweise O(1)?
• Idee: Berechne Position (Adresse) aus dem Wert selbst
• 1. Ansatz: Z. B.: Wörter der Länge 16 aus Groß- und Kleinbuchstaben
und Ziffern: Anzahl: (26 + 26 + 10)16 = 6216 ≈ 1029
Könnte man vielleicht alle möglichen Werte durchnummerieren (oder Bitmuster als ganze Zahlen interpretieren) und Array entsprechender Größe
bilden?
Das geht offenbar nicht, denn ein Array derartiger Größe kann nicht gebildet werden, außerdem wäre das Array für jede reale Anwendung überwiegend leer.
• 2. Ansatz:
Denke eine Funktion h : U → {0, 1, 2, . . . , m − 1} aus, genannt HashFunktion,
U = Menge der Schlüssel
R. Rüdiger
Algorithmik I
— 1. März 2006 —
6.1 Allgemeines
111
A = {0, 1, 2, . . . , m − 1} = Menge der Adressen
Das ideale h ist eineindeutig (Abb. 6.1).
T
0
U
Universum der
Schlüssel
h(k1)
K
wirkliche
Schlüssel
k1
h(k3) = h(k4)
Kollision......
k3
k4
k2
h(k2)
m−1
Abbildung 6.1: Hashfunktion
• Wenn K nicht zu groß ist, kann man ein solches eineindeutiges h möglicherweise finden. Sinnvoll ist das aber nur dann, wenn die Schlüssel vorgegeben sind, sich nicht ändern und keine neuen hinzukommen.
• Bsp. (aus Informatik-Handbuch) :
x
3 13 57 71 82 93
h(x) = x mod 13 3 0 5 6 4 2
Hier hat jeder Wert eine eigene Adresse: diese Art des Hashings heißt
Perfektes Hashing; die Situation, dass Adressen nicht zusammenfallen,
wird als Kollisionsfreiheit bezeichnet.
• Perfektes Hashing wird i. a. nicht immer erreichbar sein.
• Gutes (aber nicht unbedingt perfektes Hashing) sollte folgende Forderungen erfüllen:
1. möglichst wenige Kollisionen
2. gute (effiziente) Strategie zur Auflösung von Kollisionen
R. Rüdiger
Algorithmik I
— 1. März 2006 —
6.2 Statische Verfahren
6.2
112
Statische Verfahren
Man legt die Elemente in einem Array ab an einer Position, die mit einer
Hashfunktion berechnet wird. Diese Position sollte nach Möglichkeit in chaotischer Weise irgendwo“ sein. Die Hoffnung dabei: man trifft nicht mehrmals
”
dieselbe Stelle. Wenn es doch geschieht, muß eine solche Kollision aufgelöst
werden.
Bei dem folgenden Verfahren sucht man im Falle einer Kollision in derselben
Struktur nach einer anderen Stelle.
Basisalgorithmen (in Pseudocode):
Hash-Insert:
Algorithmus 52
Hash-Insert(T, k)
1 i←0
2 repeat j ← h(k, i)
3
if T [j] = nil
4
then T [j] ← k
5
return j
6
i←i+1
7
until i = m
8 error “hash table overflow”
Hash-Search:
Algorithmus 53
Hash-Search(T, k)
1 i←0
2 repeat j ← h(k, i)
3
if T [j] = k
4
then return j
5
i←i+1
6
until T [j] = nil or i = m
7 return nil
Das Verfahren ist statisch, weil
1. die Struktur ein statisch definiertes Array ist und weil es
R. Rüdiger
Algorithmik I
— 1. März 2006 —
6.3 Halbdynamische und dynamische Verfahren
113
2. nicht vorgesehen ist, dass die einmal verwendete Hashfunktion ausgetauscht wird.
Eine mögliche Wahl einer Hashfunktion ist:
h(k) = k mod m
Die Wahl von m ist wichtig!
Ungeschickt wäre z. B. m = Basisi des Zahlsystems, in dem der Schlüssel
dargestellt sind: die Hashfunktion liefert dann nur die i letzten Werte, der
Rest geht verloren, z. B.:
i = 2:
Basis = 10
123456789 mod 102 = 89
Nur die beiden letzten Stellen gehen ein, der Rest geht verloren.
m = Primzahl ist eine gute Wahl.
zur Kollisionsbehandlung (vgl. auch [Ise01, S. 358]):
1. Lineares Sondieren (linear probing)
h(k, i) = (h0 (k) + i) mod m mit i: Kollisionszähler
2. Quadratisches Sondieren:
h(k, i) = (h0 (k) + c1 i + c2 i2 ) mod m
3. double hashing:
h(k, i) = (h1 (k) + ih2 (k)) mod m
4. Rehashing:
neue Adresse berechnen mit neuer Hashfunktion
6.3
Halbdynamische und dynamische Verfahren
In dem folgenden halbdynamischen Verfahren geht man anders vor: die Elemente werden nicht direkt in dem zugrundeliegenden Array abgelegt. Dieses
enthält nur Zeiger auf die betreffenden Elemente. Die Position des Zeigers
wiederum wird durch eine Hashfunktion berechnet wie oben beschrieben.
Tritt nun eine Kollision auf, so wird hinter dem Element, an dem die Kollision aufgetreten ist, das neue Element angehängt. Es entsteht so potenziell
eine Reihe von linearen Listen. Diese Strategie zur Auflösung von Kollisionen
heißt auch Verkettung der Überläufer.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
6.4 Java-Klasse Hashtable
114
Bei den dynamischen Hashverfahren kann z. B. eine ähnliche Struktur verwendet werden, die aber reorganisiert wird, wenn die Größe nicht mehr ausreicht.
Weitere Einzelheiten: [Ise01, S. 366].
6.4
Java-Klasse Hashtable
Eine Anwendung der Klasse Hashtable aus der Java-API:
Programmbeispiel 14 (Hashtabelle – Demo)
1
// Die Funktion der Hashtabelle (Container mit assoziativem Zugriff)
2
// aus Jobst
3
package Kapitel06;
4
5
6
import java.util.*;
import java.io.*;
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CHashtableDemo {
Hashtable codes;
CHashtableDemo () {
codes = new Hashtable ();
}
public String readLine (InputStream in) throws IOException {
StringBuffer b = new StringBuffer (100);
int c;
while ((c = in.read ()) != −1 && c != ’\n’ && c != ’\r’)
b.append ((char)c);
if (c == ’r’)
c = in.read ();
return new String (b);
}
public void init () {
codes.put (”if ”, new Integer (1));
codes.put (”while”, new Integer (2));
codes.put (”switch”, new Integer (3));
codes.put (”do”, new Integer (4));
codes.put (”for ”, new Integer (5));
}
public void print () {
System.out.println (”Alle Elemente:”);
R. Rüdiger
Algorithmik I
— 1. März 2006 —
6.4 Java-Klasse Hashtable
for (Enumeration e = codes.elements (); e.hasMoreElements () ;) {
System.out.print (e.nextElement ());
if (e.hasMoreElements ())
System.out.print (”, ”);
else
System.out.println ();
}
System.out.println (”Fertig mit allen Elementen”);
31
32
33
34
35
36
37
38
}
public void search () {
System.out.print (”Eingabe: ”);
System.out.flush ();
String s = null;
try {
s = readLine (System.in);
Object o = codes.get (s);
if (o != null) {
Integer n = (Integer)o;
System.out.println (”code: ” + n);
} else
System.out.println (”Kein Eintrag fuer ” + s + ” gefunden”);
} catch (IOException e) {
System.out.println (”Exception ” + e);
}
}
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
115
}
57
58
59
60
61
62
63
64
65
public class HashtableDemo {
public static void main (String args [ ]) {
CHashtableDemo hdemo = new CHashtableDemo ();
hdemo.init ();
hdemo.print ();
hdemo.search ();
}
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Kapitel 7
Präsenzübungen
Die folgenden Aufgaben zu Algorithmen und Datenstrukturen I sind Prä”
senzübungen“; d. h. diese Aufgaben werden in die Vorlesung integriert. Sie
sollten diese Aufgaben bearbeiten, nach eigenem Wunsch in einer kleinen Arbeitsgruppe (empfehlenswert) oder auch allein. Wenn die meisten eine Aufgabe bearbeitet haben, dann wird diese entweder an der Tafel vorgeführt (eher
selten) oder es gibt eine ausgearbeitete Musterlösung dazu unter AuD I-Info
(im Netz, meistens). Viele der Aufgaben sind frühere Klausuraufgaben; alle
Aufgaben sind gedacht als Vorbereitung auf die abschließende Klausur. Wer
alle Aufgaben selbst durcharbeitet, hat damit faktisch (fast) eine Garantie für
einen erfolgreichen Klausurabschluß. Sie können und sollten die Zeit, in der
die Aufgaben bearbeitet werden, dazu nutzen, um fachliche Fragen beliebiger
Art zu stellen.
7.1
Präsenzübungen zur ersten Einführungsvorlesung
Übung 1 (Klausuraufgabe vom SS 95)
Das Spiel Türme von Hanoi gilt als Standardbeispiel für ein durch Rekursion
besonders elegant lösbares Problem. Überlegen Sie sich im Detail, wie das im
Skript abgedruckte Programm Türme von Hanoi arbeitet.
Wieviele Aufrufe der Methode Hanoi gibt es, wenn das Programm für 3 Scheiben gestartet wird?
Hinweis: Versuchen Sie zunächst, die Aufgabe ohne Rechner – also unter
Klausurbedingungen – zu lösen, und machen Sie sich den Mechanismus der
Rekursion dann durch Experimentieren am Rechner klar.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.2 Präsenzübungen zu Kapitel 1: Einführung und Grundlagen
7.2
117
Präsenzübungen zu Kapitel 1:
Einführung und Grundlagen
Übung 2
In der Vorlesung wurde der Sortier-Algorithmus BubbleSort angegeben in
Pseudo-Code gemäß der Notation von CLRS:
Bubblesort(A)
1 for i ← 1 to length[A]
2
do for j ← length[A] downto i + 1
3
do if A[j] < A[j − 1]
4
then exchange A[j] ↔ A[j − 1]
Vollziehen Sie ohne Rechner die einzelnen Schritte des Algorithmus im Detail
anhand eines Beispiels nach, ungefähr in der Art, wie es in der Vorlesung in
ersten Schritten angedeutet wurde. Die aufgestellte Tabelle von aufeinanderfolgenden Zuständen soll einfach durchschaubar und aussagekräftig sein; sie
soll also nicht zuviele aber auch nicht zu wenige Details enthalten.
Sie können sich vorstellen, dass jemand, der vergessen hat, wie der Algorithmus arbeitet, nach einem kurzen Studium Ihrer Tabelle die Arbeitsweise von
BubbleSort reproduzieren kann.
Übung 3
Bestimmen Sie für die folgenden Zeiträume, mit einer wie großen Anzahl an
Scheiben beim Spiel Türme von Hanoi man starten kann, so dass das Spiel
in dem gegebenen Zeitraum beendet wird.
Sie können unterstellen, dass Sie über einen recht schnellen Rechner verfügen
und dass alle Bewegungen“ nur im Hauptspeicher durchgeführt werden.
”
Zahlenbeispiel: Ihr Rechner benötigt für die Bewegung einer Scheibe 1 ns
(10−9 Sekunden).
a) 1 s
d) 100 Jahre
b) 1 min
e) 1 WA = 1 Weltalter = 1010 Jahre
c) 1 RSZ (= 1 Regelstudienzeit)1
Wie lautet jeweils der Wert, wenn Sie die Scheibenbewegungen per Hand
ausführen? (Annahme: 1 Bewegung pro Sekunde)
1
Bitte nur rechnen, nicht ausprobieren!
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.2 Präsenzübungen zu Kapitel 1: Einführung und Grundlagen
118
Geben Sie zunächst eine grobe Abschätzung der Größenordnung an (ohne
Rechner!), und berechnen Sie dann die Werte mit Rechner (hier z.B. Taschenrechner).
Übung 4
Die rekursive Berechnung der Binomialkoeffizienten nk kann – ganz analog
zur Berechnung der Fibonacci-Zahlen – dargestellt werden in Form eines
Binärbaumes.
Geben Sie dazu die Einzelheiten für ein Beispiel an, und begründen Sie anhand dieses Binärbaumes, weshalb in diesem Fall eine rekursive Implementierung schlecht ist.
Wieso ist die rekursive Implementierung der Türme von Hanoi nicht in gleicher Weise schlecht?
Übung 5
Geben Sie einen iterativ formulierten Algorithmus zur Berechnung der FibonacciZahlen an. Der Algorithmus soll gemäß CLRS-Konventionen notiert werden.
Übung 6
In der Vorlesung wurde gezeigt, dass im Spiel Türme von Hanoi für die
Gesamtzahl der Schritte T (N ), die erforderlich sind, um einen Scheibenstapel der Höhe N aus der Ausgangssituation (Stange A) in die Endsituation
(Stange C) zu übertragen, die folgende Formel gilt:
T (N ) = 2 · T (N − 1) + 1
Laut Vorlesung lautet die Lösung:
T (N ) = 2N − 1
Im Vorlesungsskript sind mehrere Methoden angegeben, wie man diese Lösung
erhält. Rechnen Sie jede der drei Methoden im Detail nach.
Übung 7
Für besonders einfache Algorithmen wie Bubblesort kann man die Anzahl
erforderlicher Vergleiche (comparisons) und Austauschoperationen (movements) explizit als Funktion der Größe n des zu sortierenden Arrays angeben. Man müßte sich in dem Fall also nicht unbedingt auf die O()-Notation
zurückziehen.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.2 Präsenzübungen zu Kapitel 1: Einführung und Grundlagen
119
In der Literatur findet man zu Bubblesort folgende Angaben:
Anzahl Vergleiche
Minimum
n(n − 1)
2
Anzahl Bewegungen
Mittel
Maximum
n(n − 1)
n(n − 1)
2
2
3n(n − 1) 3n(n − 1)
4
2
0
Begründen Sie diese Angaben.
Übung 8
Bekanntlich hat Gauß in jugendlichem Alter das Problem, die Zahlen von 1
bis 100 zu addieren, wesentlich schneller gelöst als seine Mitschüler (und der
Überlieferung nach sich dafür eine Ohrfeige eingefangen).2
Formulieren Sie die Gaußsche Lösung und die seiner Mitschüler jeweils als
einen Algorithmus
für allgemeines n, also zwei Algorithmen zur Berechnung
Pn
von s = i=1 i, und bestimmen Sie die Komplexität dieser beiden Algorithmen.
Übung 9
Beweisen Sie die folgenden Aussagen direkt, d.h. anhand der Definitionen
von O() bzw. Θ() ohne Rückgriff auf Sätze der Vorlesung:
a) 5n2 − 6n = Θ(n2 )
d) n2 log n 6= Θ(n2 )
b) n! = O(nn )
Pn 2
3
c)
i=1 i = Θ(n )
e) f (n) = O(n) gilt genau dann,
wenn f (n) = O(n + 1).
Übung 10 (aus Klausur SS 03, Aufgabe 4)
Existiert in den folgenden Aussagen jeweils ein kleinster Wert k ∈ N0 =
{0, 1, 2, . . .}, für den die Aussage richtig ist?
Falls ja, geben Sie ihn an.
a) (n2 + n!)2 = O(nk )
b)
1
= O(nk )
1 + n3
2
c)
n2 + cos4 n
= O(nk )
1 + cos4 n
d) (1 + n6 + n3 )2 = O(nk )
. . . zu Recht, denn besser als der Lehrer zu sein, ist natürlich eine Frechheit.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.2 Präsenzübungen zu Kapitel 1: Einführung und Grundlagen
120
Übung 11
Fällen Sie bitte Entscheidungen für die folgenden Situationen:
a) Sie haben zwei Rechner zur Verfügung, R1 und R2 . R1 ist ca. um einen
Faktor 100 schneller als R2 . Für die Lösung eines Problems stehen zwei
Algorithmen zur Verfügung, A1 und A2 . A1 ist ein O(n log n)-Algorithmus,
A2 ein O(n2 )-Algorithmus.
Unglücklicherweise läuft der schnelle Algorithmus A1 nur auf dem langsamen Rechner R2 . Für Werte von n bis ca. 10 wissen Sie aus Erfahrung,
daß die Wahl des Algorithmus praktisch keine Rolle spielt. Sie haben das
Problem aber zu lösen für n = 107 .
Treffen Sie eine Entscheidung: besser der schnelle Rechner mit dem langsamen Algorithmus oder der langsame Rechner mit dem schnellen Algorithmus?
b) In Ihrer Firma soll ein Problem gelöst werden, bei dem u.a. alle Permutationen der Namen aller Bewohner Deutschlands (ca. 80 Millionen) aufgelistet werden müssen. Zu diesem Zweck soll ein Quantenrechner allerneuester Technologie beschafft werden mit einer Leistung von 1080 QUOPS
(quantum operations per second). (Ein Rechner dieser Leistungsklasse
kann alle Elementarteilchen des Universums in ca. 1 Sekunde durchzählen.)
Ihr stärkster Konkurrent um den Posten eines Abteilungsleiters meint,
man könnte hier genauso gut einen Prozessor vom Ende des vergangenen
Jahrhunderts einsetzen; den neuen und natürlich extrem teuren Rechner
könnte man sich auch sparen. Hat er recht?
Hinweis: Es ist nützlich, hier die Stirlingsche Formel anzuwenden:
n! =
n n √
e
1
2πn 1 +
+O
12 n
1
n2
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.3 Präsenzübungen zu Kapitel 2: Fundamentale Datenstrukturen
7.3
121
Präsenzübungen zu Kapitel 2:
Fundamentale Datenstrukturen
Übung 12
Die folgende Variante von binary search ist um Ausgabeanweisungen erweitert worden.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
public static int binSearch2(int[ ] a, int x) {
int L = 0, R = a.length;
while (L < R) {
int m = (L + R) / 2;
System.out.println(”L = ” + L + ”\tm = ” + m + ”\tR = ” + R + ”\t”
+ a[m] + ” < ” + x + ”? ”);
if (a[m] < x)
L = m + 1;
else
R = m;
}
System.out.println(”L = ” + L + ”\t” + ”\tR = ” + R);
return (R < a.length && a[R] == x) ? R : −1;
}
...
a) Was gibt die Methode jeweils aus für die Arraybelegung
int [ ] a = {2, 4, 6, 8, 10, 12, 14};
mit Suche nach 3, 8 und 15
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.4 Präsenzübungen zu Kapitel 3: Sortieren
7.4
122
Präsenzübungen zu Kapitel 3:
Sortieren
Übung 13
a) Was gibt das folgende Programm aus?
b) Wie häufig werden Kopiervorgänge ausgeführt? (Wert von Z am Ende)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Test2 { // Klausuraufgabe, etwas umgestellt
int Z = 0;
void method2 (long [ ] a, int first, int last) {
int j, i0 = first − 1;
for (int i = first + 1; i <= last; i++) {
a[i0] = a[i]; Z++;
j = i;
while (a[i0] < a[j−1]) {
a[j] = a[j−1]; Z++;
j−−;
}
a[j] = a[i0]; Z++;
showArray(a);
}
}
void showArray(long [ ] a) {
for (int i = 0; i < a.length; i++)
System.out.print(a[i] + ”\t”);
System.out.println();
}
}
public class Application2 {
public static void main (String args [ ]) {
Test2 t = new Test2();
int n = 6;
long a [ ] = new long [n];
a[1] = 3; a[2] = 9; a[3] = 2; a[4] = 1; a[5] = 7;
int first = 1, last = a.length − 1;
t.method2(a, first, last);
System.out.println(t.Z);
}
}
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.4 Präsenzübungen zu Kapitel 3: Sortieren
123
Übung 14
Führen Sie anhand eines Beispiels die einzelnen Sortierschritte aus für InsertionSort, SelectionSort und ShakerSort; die Werte im Array seien 9, 1, 5, 4,
8, 2.
Übung 15 aus Klausuraufgabe Informatik I vom WS 98/99)
Die Datenstruktur Heap kann auf ein Array abgebildet werden. Es sei hier
unterstellt, daß – wie üblich – die Arraykomponente zum Index 0 durch ein
Sentinel vorbelegt sei, welches für die folgenden Fragen keine Rolle spielt.
Welche Werte befinden sich in den Arraykomponenten 1, 2, 3, . . . , wenn
Sie in einen anfänglich leeren Heap die folgenden Werte in der gegebenen
Reihenfolge einfügen:
a) 15 7 3 4 6 8 7 1:
1:
2:
3:
4:
5:
6:
7:
8:
b) Wie ist das Array jeweils nach jedem der folgenden Schritte belegt, wenn
Sie dreimal in Folge das Element an der Spitze des Heaps aus dem Heap
entfernen, wobei die Heapstruktur stets erhalten bleiben soll.
1.
1:
2:
3:
4:
5:
6:
2.
1:
2:
3:
4:
5:
6:
3.
1:
2:
3:
4:
5:
7:
c) Vollziehen Sie die einzelnen Schritte nach, mit denen HeapSort die obige
Zahlenfolge absteigend sortiert.
Übung 16
Geben Sie entsprechend dem Beispiel aus der Vorlesung die jeweiligen Wertebelegungen an, die ein Array während des Sortierens durch QuickSort nacheinander durchläuft.
Die anfänglichen Werte seien:
a) 2 3 7 5 1
b) 1 2 3 4 5
c) 5 4 3 2 1
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.5 Präsenzübungen zu Kapitel 4: Rekursion
7.5
124
Präsenzübungen zu Kapitel 4:
Rekursion
Übung 17
In der Vorlesung wurde der Algorithmus Effizientes Potenzieren vorgestellt
in Form einer iterativen Methode.
a) Führen Sie den Algorithmus durch anhand des Beispiels a37 .
b) Wie lautet der Algorithmus, wenn man ihn als rekursiv definierte mathematische Funktion formuliert?
Übung 18 (Klausuraufgabe 4 vom WS 04/05)
Im Folgenden ist ein Algorithmus in zwei Varianten abgedruckt, Alg41 und
Alg42 , der zu gegebenem n ∈ N0 aus einer quadratischen Matrix a eine
neue quadratische Matrix berechnet. Das Ergebnis ist für beide Varianten
dasselbe.
Alg41 (a, n)
1 if n = 0
2
then print(1)
3
return 1
4 if n = 1
5
then print(a)
6
return a
7 if n mod 2 = 0
8
then x ← Alg41 (a, n/2)
9
y ←x·x
10
print(y)
11
return y
12 x ← Alg41 (a, (n − 1)/2)
13 y ← a · x · x
14 print(y)
15 return y
Alg42 (a, n)
1 if n = 0
2
then return 1
3 if n = 1
4
then return a
5 if n mod 2 = 0
6
then return Alg42 (a, n/2) · Alg42 (a, n/2)
7 return a · Alg42 (a, (n − 1)/2) · Alg42 (a, (n − 1)/2)
a) Beschreiben Sie die generelle Wirkung des Algorithmus’ in einem kurzen
Satz.
b) Wie lautendie 2 × 2-Matrizen,
diebei der Berechnung Alg41 (a, 5) mit
a11 a12
0 1
a=
=
a21 a22
2 1
nacheinander auftreten. Die Ausgabeanweisung Print steht jeweils unmittelbar vor return.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.5 Präsenzübungen zu Kapitel 4: Rekursion
125
c) Wie viele Aktivierungen von Alg41 gibt es beim Start mit Alg41 (a, 5)
und wie viele Aktivierungen von Alg42 beim Start mit Alg42 (a, 5)? (erster Aufruf jeweils mitgerechnet)
Übung 19
a) Was wird durch den abgedruckten Algorithmus Proc(n) ausgegeben,
wenn er gestartet wird mit n = 1, n = 2 bzw. n = 3?
b) Wieviele Zeilen werden ausgegeben, wenn der Startparameter n = 6 ist?
c) Wie kann man ganz generell die Wirkung des abgedruckten Algorithmus
beschreiben?
Proc(n)
1 create array s[1 . . n]
2 for i ← 1 to n
3
do s[i] ← i
4 proc1(1, s)
proc1(d, s)
1 h ← s[d]
2 n ← length[s]
3 for i ← d to n
4
do s[d] ← s[i]
5
s[i] ← h
6
if d < n
7
then proc1(d + 1, s)
8
else ShowArray(s)
9
s[i] ← s[d]
10 s[d] ← h
Hinweis: Die Aufgabe stimmt im Prinzip inhaltlich überein mit einer früheren Klausuraufgabe (Aufgabe 2 zu AuD I im SS 2000), ist aber weitgehend
umformuliert (Pseudocode anstelle von Oberon, direkte Frage nach der Ausgabe. Die etwas mystifizierenden Namen sind beibehalten, weil diese sonst
die Antworten bereits enthalten würden.).
R. Rüdiger
Algorithmik I
— 1. März 2006 —
7.6 Präsenzübungen zu Kapitel 5: Dynamische Datenstrukturen
7.6
126
Präsenzübungen zu Kapitel 5:
Dynamische Datenstrukturen
Übung 20
In einen anfänglich leeren binären Suchbaum werden der Reihe nach folgende
Werte eingefügt:
4, 9, 7, 1, 2, 3, 10
Welche Werte werden ausgegeben bei einer
a) Inorder-
c) Postorder- bzw.
b) Preorder-
d) Levelorder-
Traversierung des Baums?
7.7
Präsenzübungen zu Kapitel 6:
Hashverfahren
Übung 21
In eine Hashtabelle werden folgende (Schlüssel, Wert)-Paare eingefügt:
Schlüssel 10 200
Wert
2
3
101
23
120 111
34 45
122
8
103
2
203 99 119 12 123
3 23 34 45 8
Die Hashfunktion sei
h(key) = key mod 7
und die Strategie zur Auflösung von Kollisionen sei Verkettung der Überläufer.
Zeichnen Sie ein Bild der so entstehenden Struktur.
R. Rüdiger
Algorithmik I
— 1. März 2006 —
Herunterladen