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 —