d 4 2 i 9 9 b 2 1 f 6 3 s a 1 8 c 3 6 e 5 5 g 7 4 h 8 7 Bachelorstudiengang Informatik/IT-Sicherheit Algorithmen und Datenstrukturen [AlgoDat] Autoren: Dr. rer. nat. Werner Massonne Prof. Dr.-Ing. Felix C. Freiling Friedrich-Alexander-Universität Erlangen-Nürnberg Algorithmen und Datenstrukturen [AlgoDat] Studienbrief 1: Die Programmiersprache C Studienbrief 2: Basiskonzepte und Datenstrukturen Studienbrief 3: Sortierung und Mengen Studienbrief 4: Balancierte Bäume Studienbrief 5: Algorithmen auf Graphen Autoren: Dr. rer. nat. Werner Massonne Prof. Dr.-Ing. Felix C. Freiling 1. Auflage Friedrich-Alexander-Universität Erlangen-Nürnberg © 2015 Felix Freiling Friedrich-Alexander-Universität Erlangen-Nürnberg Department Informatik Martensstr. 3 91058 Erlangen 1. Auflage (3. Dezember 2015) Didaktische und redaktionelle Bearbeitung: Bärbel Wolf-Gellatly Das Werk einschließlich seiner Teile ist urheberrechtlich geschützt. Jede Verwendung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung der Verfasser unzulässig und strafbar. Das gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen. Um die Lesbarkeit zu vereinfachen, wird auf die zusätzliche Formulierung der weiblichen Form bei Personenbezeichnungen verzichtet. Wir weisen deshalb darauf hin, dass die Verwendung der männlichen Form explizit als geschlechtsunabhängig verstanden werden soll. Das diesem Bericht zugrundeliegende Vorhaben wurde mit Mitteln des Bundesministeriums für Bildung, und Forschung unter dem Förderkennzeichen 160H11068 gefördert. Die Verantwortung für den Inhalt dieser Veröffentlichung liegt beim Autor. Inhaltsverzeichnis Seite 3 Inhaltsverzeichnis Einleitung zu den Studienbriefen I. Abkürzungen der Randsymbole und Farbkodierungen . . . . . . . . . II. Zu den Autoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . III. Modullehrziele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Studienbrief 1 Die Programmiersprache C 1.1 Lernergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Advance Organizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Motivation, Geschichte, wesentliche Merkmale . . . . . . . . . . . . . 1.4 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5 Elementare Datentypen, Operatoren und Ausdrücke . . . . . . . . . . 1.5.1 Elementare Datentypen . . . . . . . . . . . . . . . . . . . . . 1.5.2 Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5.3 Arithmetische Operatoren, Vergleiche und logische Operatoren 1.5.4 Typumwandlung . . . . . . . . . . . . . . . . . . . . . . . . . 1.5.5 Inkrement- und Dekrement-Operatoren . . . . . . . . . . . . 1.5.6 Bit-Manipulation . . . . . . . . . . . . . . . . . . . . . . . . . 1.5.7 Zuweisungsoperatoren und bedingte Ausdrücke . . . . . . . 1.5.8 Vorrang und Reihenfolge, Seiteneffekte . . . . . . . . . . . . 1.6 Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6.1 Bedingte Anweisungen . . . . . . . . . . . . . . . . . . . . . 1.6.2 Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.7 Programmstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.7.1 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.7.2 Gültigkeitsbereiche und Speicherklassen . . . . . . . . . . . Initialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.7.3 Präprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8 Zeiger und Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.1 Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.2 Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.3 Zeiger als Funktionsparameter . . . . . . . . . . . . . . . . . 1.8.4 Adressarithmetik . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.5 Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.6 Zeiger auf Zeiger . . . . . . . . . . . . . . . . . . . . . . . . Mehrdimensionale Felder . . . . . . . . . . . . . . . . . . . . Kommandozeilenparameter . . . . . . . . . . . . . . . . . . . 1.8.7 Zeiger auf Funktionen . . . . . . . . . . . . . . . . . . . . . . 1.9 Strukturen und Verbunde . . . . . . . . . . . . . . . . . . . . . . . . 1.9.1 Strukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . typedef und sizeof . . . . . . . . . . . . . . . . . . . . . . . . Dynamische Datenstrukturen . . . . . . . . . . . . . . . . . . 1.9.2 Verbunde . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10 C-Standardbibliothek . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10.1 Eingabe und Ausgabe . . . . . . . . . . . . . . . . . . . . . . Formatierte Ausgabe . . . . . . . . . . . . . . . . . . . . . . 1.10.2 Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . . 1.10.3 Weitere Bibliotheken . . . . . . . . . . . . . . . . . . . . . . . <ctype.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . . <string.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . . <math.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . . <stdlib.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . . <time.h> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . <limits.h> und <float.h> . . . . . . . . . . . . . . . . . . . . 9 6 7 8 9 9 9 11 17 17 18 20 21 22 23 25 26 27 27 30 33 33 35 37 39 40 40 42 43 44 45 46 47 49 50 51 51 55 55 58 58 59 59 63 64 64 64 64 65 65 66 Seite 4 Inhaltsverzeichnis 1.11 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 1.12 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Studienbrief 2 Basiskonzepte und Datenstrukturen 2.1 Lernergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Advance Organizer . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4 Asymptotische Aussagen . . . . . . . . . . . . . . . . . . . . . 2.5 Maschinenmodell, Zeitmodell und Problemgrößen . . . . . . . 2.6 Worst Case, Best Case, Average Case und amortisierte Kosten 2.7 Pseudocode . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.8 Probabilistische Algorithmen . . . . . . . . . . . . . . . . . . . 2.9 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.10 Effizienz, Komplexitätsklassen P und NP . . . . . . . . . . . . 2.11 Strukturierte Datentypen . . . . . . . . . . . . . . . . . . . . 2.11.1 Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . Suchbäume . . . . . . . . . . . . . . . . . . . . . . . Implementierung von Bäumen . . . . . . . . . . . . . Durchmusterung und Grundoperationen . . . . . . . . Balancierte Bäume . . . . . . . . . . . . . . . . . . . 2.11.2 Graphen . . . . . . . . . . . . . . . . . . . . . . . . . Repräsentation von Graphen . . . . . . . . . . . . . . 2.12 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . 2.13 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Studienbrief 3 Sortierung und Mengen 3.1 Lernergebnisse . . . . . . . . . . . . . . . . 3.2 Advance Organizer . . . . . . . . . . . . . . 3.3 Sortieren . . . . . . . . . . . . . . . . . . . 3.3.1 Minsort . . . . . . . . . . . . . . . . 3.3.2 Eine untere Schranke . . . . . . . . 3.3.3 Bubblesort . . . . . . . . . . . . . . 3.3.4 Heapsort . . . . . . . . . . . . . . . 3.3.5 Mergesort . . . . . . . . . . . . . . 3.3.6 Quicksort . . . . . . . . . . . . . . . Schlechtester Fall . . . . . . . . . . Mittlerer Fall . . . . . . . . . . . . . 3.3.7 Bucketsort . . . . . . . . . . . . . . 3.4 Mengen . . . . . . . . . . . . . . . . . . . . 3.4.1 Suchen in geordneten Mengen . . . Lineare Suche . . . . . . . . . . . . Binärsuche . . . . . . . . . . . . . . Interpolationssuche . . . . . . . . . 3.4.2 Digitale Suchbäume . . . . . . . . . 3.4.3 Hashing . . . . . . . . . . . . . . . . Hashing mit Verkettung . . . . . . . Hashing mit offener Adressierung . Perfektes und universelles Hashing . Hashing vs. balancierte Suchbäume 3.4.4 Das Union-Find-Problem . . . . . . . Erste Lösung . . . . . . . . . . . . . Zweite Lösung . . . . . . . . . . . . 3.4.5 Priority Queues . . . . . . . . . . . . 3.5 Zusammenfassung . . . . . . . . . . . . . . 3.6 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 . 101 . 101 . 101 . 102 . 102 . 104 . 104 . 107 . 109 . 110 . 110 . 112 . 114 . 115 . 116 . 116 . 117 . 118 . 120 . 121 . 124 . 126 . 128 . 129 . 129 . 131 . 134 . 136 . 137 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 73 73 73 74 75 78 79 80 81 84 85 85 87 89 89 93 94 96 98 99 Studienbrief 4 Balancierte Bäume 139 4.1 Lernergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 Inhaltsverzeichnis 4.2 4.3 4.4 4.5 4.6 4.7 Seite 5 Advance Organizer . . . . . . . . . . . . . . . . . . Einführung . . . . . . . . . . . . . . . . . . . . . . AVL-Baum . . . . . . . . . . . . . . . . . . . . . . . B-Bäume . . . . . . . . . . . . . . . . . . . . . . . 4.5.1 Knotenstruktur und Suche in einem B-Baum 4.5.2 Einfügen . . . . . . . . . . . . . . . . . . . 4.5.3 Löschen . . . . . . . . . . . . . . . . . . . 4.5.4 Varianten von B-Bäumen . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 139 140 143 144 145 147 150 151 152 Studienbrief 5 Algorithmen auf Graphen 155 5.1 Lernergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 5.2 Advance Organizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 5.3 Topologisches Sortieren . . . . . . . . . . . . . . . . . . . . . . . . . 155 5.4 Systematische Durchmusterung von Graphen . . . . . . . . . . . . . 159 5.5 Eine nähere Betrachtung von DFS . . . . . . . . . . . . . . . . . . . . 163 5.5.1 Zusammenhangskomponenten . . . . . . . . . . . . . . . . . 166 Bestimmung von Zusammenhangskomponenten . . . . . . . 167 Bestimmung von starken Zusammenhangskomponenten . . 168 Mehrfache Zusammenhangskomponenten . . . . . . . . . . . 170 5.6 Minimale aufspannende Bäume . . . . . . . . . . . . . . . . . . . . . 171 5.7 Greedy-Algorithmen und Matroide . . . . . . . . . . . . . . . . . . . . 173 5.8 Kürzeste Wege, der Algorithmus von Dijkstra . . . . . . . . . . . . . . 175 5.8.1 Bellman-Ford-Algorithmus . . . . . . . . . . . . . . . . . . . . 179 5.8.2 Dynamische Programmierung, der Algorithmus von FloydWarshall . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180 Transitive Hüllen . . . . . . . . . . . . . . . . . . . . . . . . . 183 5.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 5.10 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 Liste der Lösungen zu den Kontrollaufgaben Verzeichnisse I. Abbildungen II. Definitionen . III. Exkurse . . . IV. Tabellen . . . V. Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 . 189 . 189 . 190 . 190 . 190 Stichwörter 193 Liste der Lösungen zu den Übungen 197 Anhang 203 Seite 6 Einleitung zu den Studienbriefen Einleitung zu den Studienbriefen I. Abkürzungen der Randsymbole und Farbkodierungen Definition D Exkurs E Quelltext Q Übung Ü Zu den Autoren Seite 7 II. Zu den Autoren Felix Freiling ist seit Dezember 2010 Inhaber des Lehrstuhls für ITSicherheitsinfrastrukturen an der Friedrich-Alexander-Universität ErlangenNürnberg. Zuvor war er bereits als Professor für Informatik an der RWTH Aachen (2003-2005) und der Universität Mannheim (2005-2010) tätig. Schwerpunkte seiner Arbeitsgruppe in Forschung und Lehre sind offensive Methoden der IT-Sicherheit, technische Aspekte der Cyberkriminalität sowie digitale Forensik. In den Verfahren zur Online-Durchsuchung und zur Vorratsdatenspeicherung vor dem Bundesverfassungsgericht diente Felix Freiling als sachverständige Auskunftsperson. Werner Massonne erwarb sein Diplom in Informatik an der Universität des Saarlandes in Saarbrücken. Er promovierte anschließend im Bereich Rechnerarchitektur mit dem Thema „Leistung und Güte von Datenflussrechnern“. Nach einem längeren Abstecher in die freie Wirtschaft arbeitet er inzwischen als Postdoktorand bei Professor Freiling an der Friedrich-Alexander-Universität. Seite 8 Einleitung zu den Studienbriefen III. Modullehrziele Dieses Modul vertieft das große Themengebiet „Algorithmen und Datenstrukturen“, beschäftigt sich also mit der strukturierten Umsetzung konkreter Problemstellungen in Form von Programmen. In den Programmiermodulen der ersten beiden Semester, insbesondere im Modul „Programmierkonzepte“, haben Sie bereits viele Grundkenntnisse über Abstraktion, Datenstrukturen, Algorithmen, Algorithmenentwurf und Algorithmenanalyse erworben. Die Grundbegriffe werden in diesem Modul vorausgesetzt, ebenso die allgemeinen Techniken der Programmierung. In diesen Modulen stand die Programmiersprache Java im Vordergrund. Java beinhaltet bereits „höhere“ Datenstrukturen und Operationen auf diesen Datenstrukturen. Für den Programmierer bringt dies den Vorteil, sich nicht mehr mit den Details ihrer Implementierung beschäftigen zu müssen. Andererseits spielen gerade diese Implementierungen eine wichtige Rolle bei der Einschätzung des Laufzeitverhaltens von Programmen unter dem Gesichtspunkt ihrer Effizienz1 . Effizienzanalysen von Programmen oder Algorithmen werden in der Regel nicht auf der Ebene abstrahierender Programmiersprachen wie Java ausgeführt, sondern mit Hilfe von „maschinennaher“ Programmiersprachen, die es erlauben, direkte Rückschlüsse auf die Laufzeit eines Programms auf einer realen Rechnerarchitektur zu ziehen. Üblicherweise wird nicht mal eine reale Programmiersprache verwendet, sondern sogenannter Pseudocode. Auch in diesem Modul werden wir teilweise Pseudocode verwenden, wenn dadurch die Darstellung von Algorithmen „eleganter“, d. h. kompakter und verständlicher wird. Reale Programmiersprachen erzeugen einen gewissen programmiertechnischen „Overhead“, der bei Pseudocode weggelassen werden kann, um sich auf den Kern eines Algorithmus konzentrieren zu können. Als eigentliche Basissprache verwenden wir in diesem Modul jedoch die reale Programmiersprache C. Die Übersetzung von Pseudocode in ein reales C-Programm wird Ihnen keine großen Schwierigkeiten bereiten, sobald Sie den C-Programmierkurs im ersten Studienbrief dieses Moduls absolviert haben. Dieser Studienbrief setzt Grundkenntnisse im Bereich elementarer Datenstrukturen voraus. Sollten Sie bereits über Programmiererfahrung in C verfügen, so können Sie diesen Studienbrief komplett überspringen. Im zweiten Studienbrief werden die Begrifflichkeiten und Methoden herausgearbeitet, die es erlauben, Algorithmen auf einer klar definierten Basis zu bewerten und zu vergleichen. Die genaue Definition dieses Umfeldes ist wichtig, um nicht „Äpfel mit Birnen“ vergleichen zu müssen. Im Vordergrund stehen hierbei ein einheitliches Maschinenmodell und ein einheitliches Zeitmodell. Es werden darauf aufbauend Methoden zur asymptotischen Analyse von Programmlaufzeiten vorgestellt. Der zweite Teil des Studienbriefs befasst sich mit fundamentalen Datenstrukturen, die in der Informatik eine ganz bedeutende Rolle spielen und einen Zentralpunkt der Algorithmenentwicklung darstellen. In den Studienbriefen 3 bis 5 werden einige der bedeutendsten Algorithmen vorgestellt und analysiert. Studienbrief 3 beschäftigt sich mit den Themen Sortieren und Mengen. Auf das Sortieren von Daten entfällt ein erheblicher Anteil der Rechenkapazität von Computern. Eine effiziente Verwaltung und ein effizienter Zugriff auf Mengen, die letztendlich Datenbestände darstellen, sind von ebenso fundamentaler Bedeutung bei der Programmentwicklung wie die Sortieralgorithmen. Studienbrief 4 beschäftigt sich mit Suchbäumen. Suchbäume sind Datenstrukturen, die insbesondere für die Ablage und Verwaltung sehr großer Datenmengen (z. B. in Datenbanken) geeignet sind. Wir werden einige gebräuchliche Suchbäume kennenlernen sowie die Algorithmen für deren dynamische Konstruktion. In Studienbrief 5 beschäftigen wir uns mit Graphen. Die Darstellung von Daten und Datenabhängigkeiten in Form von Graphen ist in der Informatik allgegenwärtig, die Implementierung effizienter Graphenalgorithmen daher fundamental. Die Einsatzmöglichkeiten von Graphenalgorithmen sind sehr groß. Beispielsweise beruht die Berechnung von Routen durch ein Navigationssystem auf Graphenalgorithmen. Das gesamte Modul ist mit vielen praktischen Übungen angereichert, anhand derer Sie die Implementierung der vorgestellten Algorithmen und Datenstrukturen in der Programmiersprache C einüben können. Die konkrete Implementierung eines Algorithmus vertieft das Verständnis und vermittelt ein Gefühl für den praktischen Umgang mit Algorithmen. 1 Was unter dem Begriff „Effizienz“ genau zu verstehen ist, wird im Verlauf des Moduls definiert. Im Moment reicht die intuitive Vorstellung, dass Effizienz so viel wie „Schnelligkeit“ bedeutet. Studienbrief 2 Basiskonzepte und Datenstrukturen Seite 73 Studienbrief 2 Basiskonzepte und Datenstrukturen 2.1 Lernergebnisse Sie sind mit den Grundbegriffen zur Algorithmenanalyse vertraut. Die Konzepte zur Analyse und Bewertung von Algorithmen sind Ihnen bekannt, Maschinenund Zeitmodell können Sie benennen und begründen. Darüber hinaus können Sie einige fortgeschrittene Analysemethoden für rekursive Algorithmen benennen und anwenden. Sie können Algorithmen in einem Pseudocode formulieren. Sie können deterministische und probabilistische Algorithmen definieren und ihre grundsätzlichen Unterschiede aufzeigen. Des Weiteren können Sie den Begriff „Effizienz“ definieren und einige Effizienzklassen benennen und begründen. Zwei grundlegend wichtige Datenstrukturen, Bäume und Graphen, können Sie definieren, charakterisieren und implementieren. 2.2 Advance Organizer In Studienbrief 1 wurde mit dem Erlernen der Programmiersprache C das Handwerkszeug für die konkrete Implementierung von Algorithmen geschaffen. In diesem Studienbrief erlernen Sie die Grundtechniken zur Entwicklung und Analyse bzw. Bewertung von Algorithmen. Algorithmen lösen konkrete Probleme. Zur Lösung eines Problems können viele unterschiedliche Algorithmen formuliert werden. Daher ist es wichtig, diese objektiv in ihrer Leistungsfähigkeit miteinander vergleichen und einstufen zu können. Die Grundlagen dazu werden im ersten Teil dieses Studienbriefs vermittelt. Der zweite Teil des Studienbriefs beschäftigt sich mit Datenstrukturen. Die strukturierte Anordnung von Daten ist fundamental wichtig, um darauf einen leistungsfähigen Algorithmus zu implementieren. Hier werden die Datenstrukturen vorgestellt und definiert, auf denen die Algorithmen in den folgenden Studienbriefen aufbauen. 2.3 Einführung Allgmein wird behauptet, dass der Begriff „Algorithmus“ auf Herrn Abu Dscha’far Muhammad ibn Musa al-Chwarizmi (s. Bild 2.1) zurückgeht, der um das Jahr 800 n. Chr. im Haus der Weisheit in Bagdad lehrte. In Wahrheit jedoch leitet sich der Begriff vom altgriechischen αλ γOριθ µει(ν) ab, was so viel wie „wahre Laufzeiten geschickt in einer O-Notation verstecken“ bedeutet. Abb. 2.1: Muhammad ibn Musa al-Chwarizmi Seite 74 Studienbrief 2 Basiskonzepte und Datenstrukturen 2.4 Asymptotische Aussagen asymptotische Analyse Die Methodik der asymptotischen Aussagen bezüglich der Laufzeit von Algorithmen wurden bereits im Modul „Programmierkonzepte“ vorgestellt. Diese sogenannte O-Notation bildet die Grundlage zur Effizienzanalyse von Algorithmen. Die grundlegenden Definitionen werden an dieser Stelle noch einmal wiederholt, und es werden einige Regeln für den Umgang mit der O-Notation vorgestellt. Definition 2.1: O-Notation D Seien f , g : N0 → R+ , dann gilt: O( f ) = {g : ∃c > 0 : ∃n0 > 0 : ∀n > n0 : g(n) ≤ c · f (n)} Ω( f ) = {g : ∃c > 0 : ∃n0 > 0 : ∀n > n0 : g(n) ≥ c · f (n)} Θ( f ) = O( f ) ∩ Ω( f ) o( f ) = {g : ∀c > 0 : ∃n0 > 0 : ∀n > n0 : g(n) ≤ c · f (n)} ω( f ) = {g : ∀c > 0 : ∃n0 > 0 : ∀n > n0 : g(n) ≥ c · f (n)} obere und untere Schranken O( f ) ist die Menge aller Funktionen, die asymptotisch nach oben durch f beschränkt sind. Man spricht daher von einer oberen Schranke. Ω( f ) ist die Menge von Funktionen, die asymptotisch mindestens so stark wachsen wie f . Dies bezeichnet man als untere Schranke. Θ( f ) ist die Menge aller Funktionen, die asymptotisch „genauso stark“ wachsen wie f . Hier spricht man von einer scharfen Schranke bzw. einer Entsprechung. Die o- und ω-Symbolik werden weniger häufig benutzt. Der Effekt des Allquantors für c ist, dass sich f und g (für genügend große n) beliebig weit unterscheiden müssen. So ist bspw. 3n2 ∈ O(n2 ), aber 3n2 ∈ / o(n2 ). Hingegen 2 gilt n · log2 n ∈ o(n ). Man kann folgende, etwas hinkende Analogie zu den reellen Zahlen benutzen, um die Bedeutung der O-Notation optisch besser darzustellen. Im mathematischen Sinn ist das natürlich falsch: g ∈ O( f ) g ∈ Ω( f ) g ∈ Θ( f ) g ∈ o( f ) g ∈ ω( f ) ≈ ≈ ≈ ≈ ≈ g≤ g≥ g= g< g> f f f f f Obwohl die O-Notation eigentlich auf Mengen basiert, benutzt man Schreibweisen wie n2 + 5n = n2 + O(n) = O(n2 ). Das Gleichheitssymbol bezeichnet hier mathematisch korrekt ein „enthalten in“. Solche Gleichungen sind immer von links nach rechts zu lesen. Es folgen einige Rechenregeln für die O-Notation. Die Beweise sind eher Fleißarbeit und deswegen Teil der Übungen. 2.5 Maschinenmodell, Zeitmodell und Problemgrößen Seite 75 Satz 2.1 S Seien f , h : N0 → R+ , dann gilt: a · f (n) f (n) + h(n) f (n) + h(n) O( f (n)) · O(h(n)) ∈ ∈ ∈ ⊆ Θ( f (n)) ∀a > 0 Ω( f (n)) O( f (n)) , wenn h(n) ∈ O( f (n)) O( f (n) · h(n)) Kontrollaufgabe 2.1 Gilt 2n+1 K ∈ O(2n )? In diesem Modul werden meist Kombinationen von Polynomen und logarithmischen Funktionen in den Laufzeitanalysen vorkommen, weswegen der folgende Satz nun bewiesen wird, um ein wenig den praktischen Umgang mit der ONotation zu zeigen. Satz 2.2 S Sei p(n) = ∑ki=0 ai ni ein Polynom und sei ak > 0, dann gilt: p(n) ∈ Θ(nk ) Beweis: Es ist zu zeigen, dass p(n) ∈ O(nk ) und p(n) ∈ Ω(nk ) gilt. 1) Für alle n > 0 gilt: k k k p(n) ≤ ∑ |ai |ni ≤ nk ∑ |ai | = nk ( ∑ |ai |) i=0 i=0 ⇒ p(n) ∈ O(nk ) da i=0 k ∑ |ai | konstant. i=0 2) Sei A = ∑k−1 i=0 |ai |, dann gilt für positive n: p(n) ≥ ak nk − Ank−1 = a ak k k n + nk−1 n−A , 2 2 und damit p(n) ≥ a2k nk für n > 2A ak . Wir wählen c = k Definition von Ω(n ) und erhalten so p(n) ∈ Ω(nk ). ak 2 und n0 = 2A ak in der 2.5 Maschinenmodell, Zeitmodell und Problemgrößen Um die Komplexität von Algorithmen untersuchen bzw. vergleichen zu können, benötigen wir feste Bezugsgrößen. Es macht für uns wenig Sinn, reale Laufzeiten (z. B. in Sekunden) auf einem realen Computer zu messen, denn dann wären die Analysen technologieabhängig und somit nicht allgemeingültig und nicht übertragbar. Zudem wären die Analysen sehr kompliziert und von vielen kleinen Details abhängig, die bei einer grundlegenden Effizienzanalyse gar nicht interessieren. Als Beispiel sei nur das mehrstufige Cache-System einer modernen CPU genannt, das einen erheblichen Einfluss auf die reale Laufzeit eines Programms hat, am grundsätzlichen Verhalten des Programms allerdings nichts ändert. Wir benötigen Seite 76 Studienbrief 2 Basiskonzepte und Datenstrukturen also ein Modell, das einerseits einfach, aber andererseits konkret genug ist, um die damit erhaltenen Ergebnisse auf reale Hardware übertragen zu können. O-Notation Das Ziel der Effizienzanalyse von Algorithmen ist die Gewinnung asymptotischer Aussagen, wobei wir uns der O-Notation bedienen. Die Definition der O-Notation impliziert die Betrachtung großer Problemgrößen und die Vernachlässigung von Proportionalitätskonstanten und nichtdominanten Termen. Gewinnen wir bspw. die Aussage, dass ein Algorithmus die asymptotische Laufzeit Θ(n2 ) hat, so wissen wir, dass die Laufzeit des Algorithmus mit der Problemgröße n quadratisch wächst. Die Abbildung dieser asymptotischen Laufzeit auf eine reale Maschine ändert nichts am quadratischen Laufzeitverhalten, könnte also bspw. 2, 56n2 + 753n + 423, 38 Millisekunden aufgrund der Eigenschaften der realen CPU betragen. Maschinenmodell Wir wählen als Maschinenmodell ein einfache Variante des von-Neumann-Modells, das Ihnen aus dem Modul „Rechnerstrukturen“ bekannt ist und nennen diese RAM (Random Access Machine). Die Ein-/Ausgabeeinheit und Teile der Speichereinheit wie die Festplatten interessiert uns in der Regel nicht, unsere RAM besteht also nur aus der CPU und dem Hauptspeicher. Programm und Eingaben liegen vor dem Programmstart im Hauptspeicher und nach dem Programmlauf ebenfalls. Unsere RAM ist eine sequentielle Maschine, verfügt also nur über eine einzige Recheneinheit. Der Hauptspeicher ist in (unendlich vielen) Zellen s[0], s[1], . . . fester Größe organisiert, auf die „wahlfrei“ zugegriffen werden kann, d. h. der Zugriff ist beliebig, und alle einzelnen Schreib-/Leseoperationen auf eine Speicherzelle benötigen dieselbe Zeit. Die CPU unserer RAM beherrscht alle üblichen binären und unären arithmetischen und logischen Befehle auf Daten, deren Größe denen einer Speicherzelle entspricht, sowie Kontrollflussbefehle (bedingte und unbedingte Sprünge). Die RAM hat einen Registersatz beliebiger, aber fester Größe. Die arithmetischen und logischen Befehle arbeiten ausschließlich auf Registern, demzufolge hat ein Register auch die Größe einer Speicherzelle. Register können mit Konstanten geladen werden, und der Hauptspeicherzugriff erfolgt ausschließlich indirekt über Register, d. h. die Befehle R j = s[Ri ] bzw. s[Ri ] = R j für beliebige Register Ri und R j sind die einzigen unterstützten Hauptspeicherbefehle.1 Zeitmodell Für unsere RAM definieren wir ein ganz einfaches Zeitmodell. Wir postulieren, dass alle Befehle der RAM dieselbe Ausführungszeit haben. Die Ausführungszeit eines CPU-internen Befehls entspricht damit auch der Zeit eines lesenden oder schreibenden Speicherzugriffs. Die Laufzeit eines RAM-Programms entspricht demnach genau der Anzahl der ausgeführten RAM-Befehle. Kontrollaufgabe 2.2 K Beherrscht die RAM den Datentransferbefehl Ri = R j ? Warum interessiert es uns nicht, dass auf einer realen CPU bspw. Additionund Divisionsbefehle keineswegs dieselbe Laufzeit haben? Wie viele Zeiteinheiten benötigt das Umkopieren eines Blocks von 100 Speicherzellen mindestens? 1 Da die Größe eines Registers begrenzt ist, können durch die indirekte Adressierung auch nur endlich viele Speicherzellen adressiert werden. Teile des unendlich großen Hauptspeichers können somit nicht adressiert werden. Das soll uns aber nicht weiter interessieren. Vereinfachend gehen wir immer davon aus, dass der Hauptspeicher genügend groß ist, und dass genügend viele Zellen indirekt über Register adressiert werden können. 2.5 Maschinenmodell, Zeitmodell und Problemgrößen Seite 77 Exkurs 2.1: Parallele Algorithmen E Die RAM ist ein sequentielles Maschinenmodell, und wir betrachten in diesem Modul auch ausschließlich sequentielle Algorithmen. Die Welt der parallelen Algorithmen steht außerhalb unserer Betrachtungen, und es ist auch kein einfaches Maschinenmodell denkbar, das den gesamten Bereich der parallelen Algorithmen abdeckt. In der modernen Computerwelt existiert die Parallelität auf vielerlei Ebenen. Innerhalb eines Prozessorkerns kann es möglich sein, durch einen Befehl (gleiche) Operationen auf mehreren Daten auszuführen (SIMD für Single Instruction - Multiple Data). Innerhalb eines physikalischen Prozessors können mehrere unabhängige Prozessorkerne existieren, die gleichzeitig unterschiedliche Programme oder unterschiedliche Threads eines Programms ausführen können, und innerhalb eines Rechners können mehrere dieser physikalischen Prozessoren existieren. Alle diese Prozessoren und Prozessorkerne teilen sich einen gemeinsamen Hauptspeicher, kommunizieren und synchronisieren sich also über diesen. Ein übliches Maschinenmodell dieser sogenannten Shared-Memory-Architekturen ist die PRAM (Parallel Random Access Machine). Die PRAM wird zur Entwicklung und Analyse paralleler Algorithmen benutzt, und es existieren davon einige Unterarten, die verschiedene, gleichzeitig mögliche Operationen auf dem gemeinsamen Hauptspeicher erlauben. Bei einem Rechnerverbund, bei dem jeder Rechner über einen eigenen Hauptspeicher verfügt, spricht man von einem verteilten System. Hochleistungsrechner kommunizieren dabei über schnelle, dedizierte Netzwerke, lose gekoppelte Systeme über verschiedene Arten von Netzwerken, bspw. über das Internet. Die Maschinenmodelle sind jeweils unterschiedlich. Wir haben bisher schon mehrfach den intuitiven Begriff der Problemgröße benutzt, ohne zu sagen, was genau damit gemeint ist. Allgemein ist die Problemgröße die Größe der Eingabe eines Programms. Sollen bspw. n Zahlen sortiert werden, so hat die Problemgröße offensichtlich etwas mit n zu tun. Aber es gibt zwei mögliche Arten der Interpretation. Ist die Problemgröße die Anzahl der Bits, die erforderlich ist, um die n Zahlen zu codieren, oder ist die Problemgröße die Zahl n (eventuell multipliziert mit einem konstanten Faktor) selbst? Problemgröße Wir gehen davon aus, dass eine Speicherzelle – wie bei einem realen Rechner – eine „kleine“ Integer-Zahl aufnehmen kann. Sollen also n kleine Integer-Zahlen sortiert werden, so stehen diese n Zahlen in n Speicherzellen. Die Problemgröße ist also n Speicherzellen bzw. n · m Bit, wenn m die Größe einer Speicherzelle in Bit ist. Vereinfachend sagen wir, dass die Problemgröße n ist. Die Problemgröße ist also immer die Anzahl der Eingabedaten eines Programms. Wir betrachten in diesem Modul ausschließlich Algorithmen, bei denen jeder Wert einer Eingabe und auch die Zwischenergebnisse immer in eine Speicherzelle hineinpassen. Zusammen mit der Forderung, dass alle RAM-Befehle in einer Zeiteinheit ausgeführt werden können, bezeichnen wir das als uniformes Komplexitätsmaß. uniformes Komplexitätsmaß Sollen n „große“ Zahlen sortiert werden, so müssen diese auf mehr als n Speicherzellen aufgeteilt werden. Die Problemgröße ist dann größer als n. Ein leicht verständliches Beispiel, bei dem das uniforme Komplexitätsmaß nicht angebracht ist, ist ein Programm zur Zerlegung einer Zahl in ihre Primfaktoren. Angenommen, eine Zahl n = p · q mit zwei großen Primzahlen p und q soll in diese zerlegt werden. Das Verfahren dazu ist enorm aufwendig, obwohl dem Programm nur Bit-Komplexitätsmaß Seite 78 Studienbrief 2 Basiskonzepte und Datenstrukturen eine einzige Zahl als Eingabe übergeben wird. Das uniforme Komplexitätsmaß ist hier offensichtlich unsinnig. Hier macht es erheblich mehr Sinn, die Länge der binären Codierung von n als Problemgröße anzusehen, und man spricht daher vom Bit-Komplexitätsmaß. Die Bit-Komplexität entspricht dem Komplexitätsbegriff bei Turing-Maschinen in der theoretischen Informatik. E Exkurs 2.2: „Schnelle“ Multiplikation großer Zahlen An dieser Stelle wird es Zeit, das Augenzwinkern von Herrn al-Chwarizmi auf Seite 73 aufzuklären. Die Laufzeit eines Algorithmus zur Multiplikation ganzer Zahlen nach der bekannten Schulmethode ist recht offensichtlich Θ(n2 ), wenn n die Bit-Komplexität der Problemgröße – in etwa entspricht das der Anzahl der Ziffern der zu multiplizierenden Zahlen – angibt. Schönhage und Strassen beschreiben in [und Volker Strassen, 1971] ein Verfahren, das eine Laufzeit von O(n · log2 n · log2 log2 n) hat, also schneller ist als die Schulmethode, und das bis zum Jahr 2007 das effizienteste, bekannte Multiplikationsverfahren war. Wegen der großen, in der O-Notation verborgenen, Konstanten ist der Schönhage-Strassen-Algorithmus allerdings erst für Zahlen mit mehreren 10000 Ziffern absolut schneller als die Schulmethode. Asymptotische Aussagen sollte man also stets etwas kritisch betrachten, manchmal eher praktisch als akademisch. 2.6 Worst Case, Best Case, Average Case und amortisierte Kosten Worst Case, Best Case und Average Case D Die Laufzeit eines Algorithmus hängt von den Eingaben ab. Wenn wir die Problemgröße mit n fixieren, dann muss es keineswegs so ein, dass alle Eingaben der Größe n dieselbe Laufzeit bewirken. Wir werden in diesem Modul mehrfach Algorithmen kennenlernen, deren Laufzeit nicht von der Problemgröße, sondern von der Art der Eingaben bzw. von ihrer Gruppierung abhängt. Ein Sortieralgorithmus, der bspw. (in Zeit O(n)) erkennt, wenn ihm bereits ein sortiertes Feld übergeben wird, erzielt einen wunderbaren besten Fall (best case). Derselbe Algorithmus kann aber im schlechtesten Fall (worst case) eine quadratische Laufzeit haben und im mittleren Fall (average case) eine Laufzeit von O(log2 n · n). Wie wir noch sehen werden, sind die Analysemethoden für die drei Fälle oftmals unterschiedlich. Wir wollen an dieser Stelle die drei Begriffe formal definieren. Definition 2.2: Laufzeit Sei P ein RAM-Programm. Seien weiterhin n ∈ N die Größe einer Eingabe für P und In die Menge aller möglichen Eingaben der Größe n für P. Als T (i) bezeichnen wir die Laufzeit von P für Eingabe i ∈ In . Dann gilt: worst case: T (n) = max{T (i) : i ∈ In } best case: T (n) = min{T (i) : i ∈ In } average case: T (n) = |I1n | · ∑i∈In T (i) amortisierte Kosten Darüber hinaus spricht man von amortisierten Kosten, wenn die Laufzeit einer ganzen Folge von Eingaben der Größe n betrachtet wird. Wenn diese Folge eine Länge von m hat, dann sind die amortisierten Kosten für eine Eingabe der Folge definiert als Tmm , wobei Tm die Gesamtlaufzeit der Folge ist. In der Regel werden dabei für Tm die worst-case-Kosten der gesamten Folge angenommen. Die amortisierten 2.7 Pseudocode Seite 79 Kosten können unter Umständen geringer ausfallen als die worst-case-Kosten einer einzelnen Eingabe. 2.7 Pseudocode Das im vorangegangenen Abschnitt eingeführte Maschinen- und Zeitmodell der RAM erlaubt es uns, Programme, die im Maschinencode der RAM geschrieben sind, zu analysieren. Wir wollen unsere Algorithmen allerdings nicht in Maschinencode formulieren, sondern in einer Hochsprache. Was uns also fehlt, ist ein Modell, dass die Ausführung eines Hochsprachenprogramms auf eine RAM abbildet. Wir könnten für eine beliebige Hochsprache einen Compiler für RAM-Code entwickeln und die erzeugten RAM-Programme dann analysieren, aber das ist angesichts der Größe der RAM-Programme zu kompliziert und zu unübersichtlich. Stattdessen verwenden wir eine einfache Hochsprache und vereinfachen das Zeitmodell der RAM noch weiter. Es ist in der Welt der Algorithmenanalyse unüblich, Programme in einer realen höheren Programmiersprache zu formulieren. Die Formulierung erfolgt vielmehr in sogenanntem Pseudocode, der von vielen Implementierungsdetails wie z. B. Variablendeklarationen, Typkonvertierungen usw. abstrahiert und dadurch kurz und prägnant ist. Pseudocode ist im Kern ähnlich zu einer (imperativen) Programmiersprache wie C oder PASCAL. Die Übersetzung eines Pseudocode-Programms in eine dieser Sprachen oder umgekehrt ist für einen geübten Programmierer einfach zu bewerkstelligen. Ein Pseudocode-Programm besteht aus Zuweisungen an Variablen, einfachen Berechnungen, Schleifen und Funktionen. Darüber hinaus ist der freie Umgang mit mathematischen Notationen erlaubt, deren direkte Umsetzung in einer realen Hochsprache leicht möglich ist. Ein Beispiel (die Bedeutung des Programms ist an dieser Stelle unwichtig): Quelltext 2.1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Pseudocode und Hochsprache Q union (int A,B,C) { if (GROESSE[A] <= GROESSE[B]) { z = MAPIN[A]; y = MAPIN[B]; } else { z = MAPIN[B]; y = MAPIN[A]; } for (alle x in Liste[z]) IST_IN[x] = y; Liste[y] = Liste[y] vereinigt Liste[z]; MAPOUT[y] = C; MAPIN[C] = y; GROESSE[C] = GROESSE[A] + GROESSE[B]; } Jeder Autor verwendet im Prinzip seinen eigenen Pseudocode. Der in diesem Modul verwendete Pseudocode ist C-ähnlich, auf eine formale Einführung wollen wir allerdings verzichten. Syntax und Semantik sollten leicht verständlich sein, und Sie werden in den Übungen genügend Gelegenheit haben, Pseudocode in C zu übersetzen. Das Zeitmodell für Pseudocode ist denkbar einfach: Einfache Anweisungen benötigen eine konstante Zeit, Funktionsaufrufe eine konstante Zeit zuzüglich der Zeit zur Ausführung des Funktionsrumpfes. Die Konstanten können sich bei unterschiedlichen Anweisungen durchaus unterscheiden. Eine Unterscheidung wird Zeitmodell für Pseudocode Seite 80 Studienbrief 2 Basiskonzepte und Datenstrukturen allerdings nur dann gemacht, wenn es sinnvoll ist. Oftmals ist eine Unterscheidung nicht erforderlich, um Laufzeitabschätzung zu machen. Beispielsweise dominiert bei Sortieralgorithmen oftmals die Anzahl der Vergleiche die Laufzeit, sodass es nicht erforderlich ist, Konstanten für sonstige Anweisungen überhaupt zu betrachten. Die Gesamtlaufzeit eines Pseudocode-Programms ist dann die Summe der Laufzeiten der ausgeführten Anweisungen. Es ist natürlich nicht erlaubt, wahre Laufzeiten im Pseudocode in „einfachen Anweisungen“ zu verstecken, deren Laufzeit als konstant angenommen wird. Grundlage dessen, was als konstante Laufzeit einer Anweisung angenommen wird, ist der (fiktiv) aus einer Anweisung erzeugte RAM-Code. Intuitiv sollte meist klar sein, welche Anweisungen ein Äquivalent in RAM-Code mit konstanter Laufzeit haben, die Details einer solchen Übersetzung werden allerdings erst im Modul „Systemnahe Programmierung“ herausgearbeitet. Eine PseudocodeAnweisung der Art sortiere Feld A ist übrigens unproblematisch, solange sie als Funktionsaufruf interpretiert wird. Die Laufzeit besteht dann aus einer Konstanten zum Aufruf der Funktion plus der Zeit für das Sortieren des Feldes, die keineswegs konstant ist. Drei besondere Funktionen werden gelegentlich in Pseudocode benötigt und mit konstanter Laufzeit angesetzt: alloc und free zur Allokierung und Freigabe von Speicher so wie random zur Generierung einer Zufallszahl. Zumindest die Anweisungen zur Speicherverwaltung haben in der Realität nicht automatisch eine konstante Laufzeit. Die korrekte Analyse von Programmen, die solche Anweisungen enthalten, ist daher sehr wesentlich. 2.8 Probabilistische Algorithmen Man spricht von einem probabilistischen oder randomisierten Algorithmus, wenn darin eine random-Anweisung zur Anwendung kommt, ansonsten spricht man von einem deterministischen Algorithmus. In C haben wir bereits die Bibliotheksfunktion rand kennengelernt, die eine ganzzahlige Zufallszahl im Intervall von 0 bis RAND_MAX generiert.2 Las Vegas- und Monte Carlo-Algorithmen Die Laufzeit eines probabilistischen Algorithmus hängt von einer zufälligen Wahl ab, die innerhalb des Algorithmus getroffen wird. Somit ist die Laufzeit für eine feste Eingabe keine Zahl, sondern eine Zufallsvariable, die von einer zufälligen Wahl abhängt. Wie geht man damit um? Wenn wir wissen, dass der Algorithmus auf jeden Fall terminiert und das korrekte Ergebnis liefert, dann können wir die Terminierung abwarten. Solche Algorithmen werden auch als Las Vegas-Algorithmen bezeichnet. Wir können allerdings auch einen Timer in die RAM einbauen, der den Algorithmus nach einer fest voreingestellten Zeit stoppt. Allerdings kann dann nicht mehr sichergestellt werden, dass ein bis dahin berechnetes Zwischenergebnis auch korrekt ist. Ist das Zwischenergebnis allerdings mit hoher Wahrscheinlichkeit korrekt, so hat auch diese Vorgehensweise eine realistische Daseinsberechtigung. Solche Algorithmen werden als Monte Carlo-Algorithmen bezeichnet. Ein Las Vegas-Algorithmus wird in Abs. 3.4.3 vorgestellt, ein bekanntes Beispiel für einen Monte Carlo-Algorithmus ist der Miller-Rabin-Primzahltest3 . 2 3 Auf die Problematik, dass es sich hierbei um Pseudo-Zufallszahlen handelt, soll hier nicht weiter eingegangen werden. Wir gehen im Folgenden davon aus, dass wir perfekte Zufallszahlen generieren können. siehe z. B. bei http://de.wikipedia.org/wiki/Miller-Rabin-Test. 2.9 Rekursion Seite 81 Als Erweiterung von Def. 2.2 definieren wir die randomisierte Laufzeit eines probabilistischen RAM-Programms P wie folgt: Definition 2.3: Randomisierte Laufzeit D worst case: Tr (n) = max{time0r (i) : i ∈ In } average case: Tr (n) = |I1n | · ∑i∈In time0r (i) Dabei ist time0r (i) der Erwartungswert der Laufzeit von P für Eingabe i ∈ In . Diese Definition macht allerdings nur bei Las Vegas-Algorithmen Sinn. Für Monte Carlo-Algorithmen, bei denen die Laufzeit beschnitten wird, benutzt man eine „umgekehrte Laufzeitdefinition“. Ohne dies formal auszuführen, wird bei Monte Carlo-Algorithmen eine Laufzeitschranke so definiert, dass nach Ablauf dieser Zeit der Algorithmus mit hoher Wahrscheinlichkeit ein korrektes Ergebnis liefert. Kontrollaufgabe 2.3 K Man kann die Zahl π bestimmen, indem man ein Quadrat mit Seitenlänge 1 um den Einheitskreis legt und immer wieder zufällig darauf „feuert“. Aus dem Verhältnis der „Treffer“ außerhalb und innerhalb des Kreises lässt sich π „errechnen“. Handel es sich hier um einen Las Vegas- oder einen Monte Carlo-Algorithmus? 2.9 Rekursion Die Laufzeit rekursiver Algorithmen ist oftmals intuitiv leicht ersichtlich, formal aber weniger leicht beschreibbar. In diesem Abschnitt wollen wir uns ein wenig näher mit der Laufzeitanalyse rekursiver Algorithmen beschäftigen. Eine grundsätzliche Methode, die insbesondere bei der Klasse der divide-and-conquerAlgorithmen zur Anwendung kommt, ist die Substitutionsmethode. Zunächst werden wir diese vorstellen, dann das sogenannte Master-Theorem (aber nicht bewiesen), das ein starkes mathematisches Werkzeug zur Analyse rekursiver Algorithmen darstellt. Zunächst müssen wir festlegen, wie überhaupt die Laufzeit eines rekursiven Algorithmus definiert werden soll. Allgemein gehen wir davon aus, dass der Algorithmus in Form einer Funktion vorliegt, die sich selbst aufruft. Wir zählen, wie oft die Funktion (rekursiv) aufgerufen wird und addieren die Kosten dieser Funktionsaufrufe. Es wird festgelegt, dass jedem einzelnen Funktionsaufruf die Kosten zur Ausführung des Funktionsrumpfes einschließlich der Kosten zur „Versorgung“ der rekursiven Aufrufe zugeordnet werden, aber abzüglich der Kosten für die rekursiven Aufrufe. Die Gesamtkosten ergeben sich dann aus der Summe der so ermittelten Kosten aller Funktionsaufrufe. Beispiel 2.1 In Studienbrief 1 wurde ein rekursives Verfahren zur Bestimmung der Fibonacci-Zahlen vorgestellt. Der Rumpf der Funktion ist sehr einfach und besteht nur aus einer Addition und zwei Funktionsaufrufen. Die Kosten Laufzeit rekursiver Algorithmen B Seite 82 Studienbrief 2 Basiskonzepte und Datenstrukturen eines Funktionsaufrufs sind offensichtlich O(1). Allerdings ist die Gesamtzahl der Funktionsaufrufe sehr hoch, denn jeder Funktionsaufruf initiiert seinerseits zwei neue Funktionsaufrufe. Die Gesamtzahl der Funktionsaufrufe wächst daher geometrisch in n, d. h. die Laufzeit des Algorithmus ist Θ(2n ).4 Rekursionsgleichungen Die Laufzeiten rekursiver Algorithmen können oft mit Hilfe von Rekursionsgleichungen der allgemeinen Form f (n) = F( f (1), f (2), . . . , f (n − 1)) beschrieben werden. Die Kosten (Laufzeit) f bei Problemgröße n errechnet sich also durch Anwendung einer Funktion F aus den Kosten kleinerer Problemgrößen. Zudem werden Anfangswertbedingungen für f festgelegt. Solch eine Rekursionsgleichung steht oft in ganz engem Bezug zur rekursiven Funktion selbst, bspw. wäre im Fall unseres Fibonacci-Algorithmus eine Funktion f (n) = x · ( f (n − 1) + f (n − 2)) (x ≥ 1) mit f (1) = f (2) = c eine naheliegende Kostenbeschreibung. Allerdings hilft eine Rekursionsgleichung noch nicht wirklich weiter, denn ihr ist meist nicht auf den ersten Blick die „wahre“ Laufzeit anzusehen. Wir benötigen eine geschlossene Form der Kostenfunktion, zu deren Lösung nicht der Algorithmus selbst ausgeführt werden soll. Es gilt also Rekursionsgleichungen zu lösen! Substitution Die wichtigste Methode zur Lösung einer Rekursionsgleichung ist die Substitutionsmethode. Diese besteht aus zwei Schritten: 1. Erraten eines Lösungsansatzes. 2. Beweis des Ansatzes durch Induktion; Gewinnung der Konstanten. Beispiel 2.2 B Wir nehmen an (erraten), dass der vorgestellte Fibonacci-Algorithmus exponentielle Laufzeit hat, also eine Laufzeit von f (n) = Θ(cn ) für eine Konstante c > 1. Die Kostenfunktion ist f (n) = x · ( f (n − 1) + f (n − 2)), wobei wir der Einfachheit halber x = 1 setzten, um x nicht durch die ganze Rechnung mitschleppen zu müssen. Wir zeigen zunächst, dass f (n) ≥ acn für geeignete Konstanten a, c > 0 gilt. Der Induktionsschritt5 ist dann wie folgt: f (n) = f (n − 1) + f (n − 2) ≥ acn−1 + acn−2 = acn · Damit die Annahme f (n) ≥ acn gilt, muss also c+1 c2 c+1 c2 ≥ 1 sein. Die Lösung der quadratischen Gleichung = 0 führt dazu, dass c ≤ sein muss, damit der Induktionsschritt gilt. c2 − c − 1 √ 5+1 2 ∼ 1.618 Analog zeigt man durch Umkehrung von „≥“ zu „≤“, dass f (n) ≤ a0 d n für √ √ d ≥ 5+1 gilt. Zusammengefasst gilt also f (n) = Θ(cn ) für c = 5+1 2 2 . Mastertheorem Der Fibonacci-Algorithmus ist kein klassischer divide-and-conquer-Algorithmus, weil die Problemgröße in der Rekursion nicht um einen konstanten Faktor verrin4 5 Selbstverständlich existieren sehr viel effizientere Verfahren zur Bestimmung von Fibonacci-Zahlen. Dies ist an dieser Stelle allerdings nicht von Bedeutung. Für den Induktionsanfang wählt man ein geeignetes a und n = 1. 2.9 Rekursion Seite 83 gert wird. Typische Rekursionsgleichungen von divide-and-conquer-Algorithmen haben die Form T (n) = a · T (n/b) + Θ(nk ), wobei a ≥ 1 und b > 1 gelten. (n/b kann hierbei auch dn/be oder bn/bc bedeuten.) Diese Rekursionsgleichung beschreibt die Laufzeit eines Algorithmus, der die Eingabe der Größe n in a Teilprobleme der Größe n/b zerlegt, also die Teilprobleme durch a rekursive Aufrufe löst und aus den erhaltenen Teillösungen die Gesamtlösung zusammensetzt. Θ(nk ) beschreibt – wie oben festgelegt – die Kosten zur Ausführung des Funktionsrumpfes, also die Zeit zum Zerlegen in Teilprobleme und zum Zusammenfügen der Teillösungen ohne die Kosten der rekursiven Aufrufe. Für Rekursionsgleichungen dieser Art gilt das sogenannte Mastertheorem, das ein sehr starkes Mittel für ihre Lösung darstellt: Satz 2.3 S Für eine Rekursionsgleichung der Form T (n) = a · T (n/b) + Θ(nk ) mit a ≥ 1, b > 1 und k ≥ 0 gilt: , falls a < bk Θ(nk ) k T (n) = Θ(n · log2 n) , falls a = bk Θ(nlogb a ) , falls a > bk Beispiel 2.3 B Nehmen wir einen rekursiven Algorithmus an, bei dem die Problemgröße gedrittelt wird mit der Rekursionsgleichung T (n) = a · T (n/3) + n (also b = 3 und k = 1) Der interessante Wert bk ist 3. Die Laufzeit des Algorithmus wird entscheidend durch die Konstante a geprägt. Schauen wir uns zunächst das Ergebnis für a = 2, a = 3 und a = 4 an: , falls a = 2 Θ(n) Θ(n · log2 n) , falls a = 3 T (n) = Θ(nlog3 4 ) ∼ Θ(n1.26 ) , falls a = 4 Wegen Θ(nk ) = n in der Rekursionsgleichung ist die Laufzeit bereits durch Ω(n) nach unten beschränkt. Für a < 3 wird die Laufzeit nicht mehr „schlimmer“, sie wird also nicht durch die Rekursion geprägt, sondern durch die Ausführung des Funktionsrumpfes. Für a > 3 wächst die Laufzeit polynomiell und für a = 3 „ausgeglichen“, d. h. die logarithmische Rekursionstiefe prägt die Laufzeit. Eine praktische Anwendung des Mastertheorems wird Ihnen in den Übungen zu Studienbrief 3 begegnen. Seite 84 Studienbrief 2 Basiskonzepte und Datenstrukturen 2.10 Effizienz, Komplexitätsklassen P und NP Wir haben schon mehrfach den Begriff effizienter Algorithmus verwendet. Wann wird ein Algorithmus überhaupt als effizient bezeichnet? Allgmein lautet die Definition so, dass ein Algorithmus A effizient ist, wenn es ein Polynom p gibt und für eine Problemgröße n die Laufzeit des Algorithmus O(p(n)) ist. Um Kompatibilität mit der theoretischen Informatik, insbesondere der Komplexitätstheorie, herzustellen, ist hierbei n die Bit-Komplexität der Eingabe, denn eine RAM kann eine Turing-Maschine in polynomieller Zeit simulieren und umgekehrt. Die Implementierungsdetails eines effizienten Algorithmus, d. h. das zugrunde liegende Maschinenmodell ist demnach irrelevant. Effiziente Algorithmen und polynomielle Laufzeit Ein Problem kann nunmehr effizient gelöst werden, wenn dafür ein Algorithmus gefunden wird, der eine polynomielle Laufzeit hat. Die Klasse der Probleme, für die es eine effiziente Lösung gibt, wird als P bezeichnet. Die Definition scheint großzügig gewählt zu sein, weil auch Polynome ganz beachtliche Wachstumsraten haben können, aber es gibt viele Probleme, für die keine effiziente Lösung bekannt ist. Das vielleicht prominenteste Problem dieser Art ist das Travelling-SalesmanProblem: Ein Handlungsreisender muss n Städte genau einmal besuchen und dabei den kürzesten Gesamtweg finden. Für dieses Problem ist bisher kein effizienter RAM-Algorithmus gefunden worden. Es bleibt wohl nichts anderes übrig, als alle n! möglichen Routen durchzuprobieren, und dies führt zu einer exponentiellen Laufzeit. P 6= NP? Das Problem des Handlungsreisenden lässt sich gut parallelisieren; die zu untersuchenden Routen lassen sich bspw. auf die Prozessoren einer PRAM verteilen. Jeder Prozessor schreibt die von ihm gefundene kürzeste Route in eine gemeinsame Speicherzelle, wobei der kleinste Wert gewinnt. Da eine PRAM aber nur über endlich viele Prozessoren verfügt, führt dies auch nicht zu einem effizienten Algorithmus. Man benötigte so etwas wie eine beliebig große PRAM mit einem separaten Prozessor für jede Route. Praktisch ist das zwar nicht realisierbar, aber theoretische ist dieses Vorgehen sehr interessant, weil damit eine neue Klasse von Problemen definiert werden kann, nämlich NP. NP steht für „nichtdeterministisch polynomiell“. Informal kann man sich eine Lösung für ein Problem aus NP – wie beschrieben – so vorstellen, dass alle Lösungswege parallel durchprobiert werden, wobei zumindest der gewinnende polynomielle Laufzeit hat. Dass P ⊆ NP gilt, ist offensichtlich, aber die Frage, ob NP ⊆ P gilt, ist wohl das berühmteste offene Problem der Informatik. Es wird zwar angenommen, dass P 6= NP ist, aber bewiesen ist dies keineswegs. NP-hart und NP-vollständig Man bezeichnet ein Problem ph als NP-hart, wenn jedes Problem aus der Klasse NP auf dieses Problem polynomiell reduzierbar ist. Das heißt, dass für jedes Problem p ∈ NP ein Verfahren mit polynomieller Laufzeit existiert, das p auf ph abbildet. Durch die Lösung von ph kann somit auch p effizient gelöst werden. Ist ph selbst in NP, so bezeichnet man dieses Problem als NP-vollständig. Das Travelling-SalesmanProblem ist ein NP-vollständiges Problem, und es existieren ganze Sammlungen NP-vollständiger Probleme6 . Die Klasse der NP-vollständigen Probleme ist eine echte Teilmenge von NP. Wenn gezeigt werden könnte, dass nur ein einziges dieser NP-vollständigen Probleme effizient gelöst werden könnte, dann wäre P = NP bewiesen, weil damit alle Probleme aus NP auf dieses polynomiell reduzierbar und damit effizient lösbar wären. Da bis heute kein effizient lösbares NP-vollständiges Problem gefunden wurde, nimmt man an, dass P 6= NP gilt. weitere Komplexitätsklassen „Oberhalb“ von NP existieren weiter Komplexitätsklassen von Problemen, bspw. EXPT IME und NEXPT IME. Erstere ist die Klasse der Probleme, die in deterministischer, exponentieller Zeit gelöst werden können, letztere ist die Klasse der 6 siehe z. B. bei http://de.wikipedia.org/wiki/Karps_21_NP-vollst%C3%A4ndige_Probleme. 2.11 Strukturierte Datentypen Probleme, die in nichtdeterministischer, exponentieller Zeit gelöst werden können. Es gilt zwar NP ⊆ EXPT IME, aber nicht EXPT IME ⊆ NP. 2.11 Strukturierte Datentypen Strukturierte Datentypen dienen zur strukturierten Gruppierung von Speicherzellen. Einige strukturierte Datentypen haben wir bereits im Studienbrief 1 kennengelernt, andere sind Ihnen aus dem Modul „Programmierkonzepte“ bekannt. Die Operationen darauf und die konkrete Realisierung in C sind bekannt oder leicht nachvollziehbar. Diese strukturierten Datentypen werden hier nur kurz der Vollständigkeit halber aufgelistet, der ganz überwiegende Teil dieses Abschnittes beschäftigt sich dann mit Bäumen und Graphen. Array oder Feld: Ein Array besteht aus Elementen des gleichen Datentyps. Struktur: Eine Struktur besteht aus einer festen Anzahl von Elementen verschie- dener Datentypen. File: Ein File besteht aus einer unbegrenzten Folge von Elementen. Stapel (auch Stack bzw. Keller): Ein Stapel repräsentiert eine dynamische Fol- ge von Elementen mit den Operationen „leeren“, „einfügen hinten“ (push) und „streichen hinten“ (pop) nach dem LIFO (Last In First Out)-Prinzip. Schlange (Queue): Eine Schlange repräsentiert eine dynamische Folge von Ele- menten mit den Operationen „leeren“, „einfügen hinten“ und „streichen vorne“ nach dem FIFO (First In First Out)-Prinzip. Stapel und Schlangen7 können mit Feldern implementiert werden. Bei einem Stapel existiert ein Zeiger auf das letzte Element, bei einer Schlange ein Zeiger auf das erste und ein Zeiger auf das letzte Element. Schlangen können auch zyklisch sein. Liste: Eine Liste repräsentiert eine Folge, die beliebig veränderbar ist. Jedes Listenelement enthält neben der Information einen oder mehrere Verweise auf benachbarte Elemente. Die wichtigsten Listen sind: • einfach verkettete Liste • doppelt verkettete Liste • zyklisch verkettete Liste Auf Listen werden die Operationen „einfügen“ und „streichen“ ausgeführt. Je nach Art der Implementierung (meist durch Felder oder dynamische Datenstrukturen) sind die Kosten der Elementaroperationen unterschiedlich. 2.11.1 Bäume In Studienbrief 1 wurde bereits der binäre Suchbaum als Beispiel für eine rekursive Datenstruktur informal eingeführt. Bäume sind mit die wichtigsten Datenstrukturen der Informatik und werden oftmals zur Speicherung von Daten benutzt, 7 Eine spezielle Variante der Schlangen, nämlich Warteschlangen mit Prioritäten (Priority Queues), werden in Abs. 3.4.5 behandelt. Seite 85 Seite 86 Studienbrief 2 Basiskonzepte und Datenstrukturen insbesondere auch bei Datenbanken. Ein Baum kann formal wie folgt definiert werden. Definition 2.4: Baum D Bäume bestehen aus Knoten und Blättern. Sei V = {v1 , v2 , . . . } eine unendliche Menge von Knoten und B = {b1 , b2 , . . . } eine unendliche Menge von Blättern, dann ist die Menge der Bäume über V und B wie folgt definiert: 1. Jedes Element bi ∈ B ist ein Baum. bi ist die Wurzel dieses Baums. 2. Wenn T1 , . . . , Tm (m ≥ 1) Bäume mit paarweise verschiedenen Mengen von Knoten und Blättern sind und v ∈ V ein neuer Knoten ist, dann ist das (m + 1)-Tupel T = hv, T1 , . . . , Tm i ein Baum. Der Knoten v ist die Wurzel des Baums, m ist der Grad von v, Ti ist der i-te Unterbaum von T , und die Wurzel von Ti ist das i-te Kind von v. v heißt Elter8 dieses Kindes. Die Kinder eines Elterknotens heißen Geschwister. Der Grad bzw. die Ordnung eines Baums ist das Maximum aller Grade seiner Knoten. Binärbaum Abb. 2.2 zeigt einen Baum in der üblichen graphischen Darstellung. Nach Konvention wird die Wurzel eines Baums als oberster Knoten gezeichnet, die Blätter sind demnach unten zu finden. Die Pfeile verweisen auf die Kinder eines Knotens. Der Baum hat Grad 4. Bäume mit Grad 2 werden als Binärbäume bezeichnet. Abb. 2.2: Ein Baum In einem Baum existieren Pfade, die stets von „oben“ nach „unten“ verlaufen, also von Richtung Wurzel in Richtung Blätter. Ein Pfad führt von einem Elterknoten zu einem seiner Kinder, dann ggf. zu einem dessen Kinder usw. Die Tiefe eines 8 In der Literatur ist auch der Begriff Vater üblich, dem neuen Trend folgend wählen wir jedoch die Unisex-Bezeichnung Elter. 2.11 Strukturierte Datentypen Seite 87 Baums wird durch den längsten möglichen Pfad von der Wurzel zu einem Blatt bestimmt. Der Beispielbaum von oben hat bspw. die Tiefe 3. Definition 2.5: Tiefe und Pfade D Sein B ein Baum und p = (v0 , . . . , vt )(t ≥ 1) eine Folge von Knoten und Blättern9 , für die gilt: vn+1 (0 ≤ n ≤ t − 1) ist Kind von vn , dann ist p ein Pfad in B der Länge t. Sei v die Wurzel von B und sei k ein Knoten oder ein Blatt von B, dann ist die Länge des Pfades von v nach k die Tiefe von k. Die Tiefe T des Baums B ist das Maximum aller Tiefen der Blätter des Baums. Suchbäume Ein Baum wird zum Suchbaum, wenn seinen Knoten und Blättern Schlüssel zugeordnet werden. Schlüssel entstammen einer Wertemenge W (z. B. Integer-Zahlen), die eine Ordnungsrelation besitzt. Es kann also jeweils für zwei Schlüssel angegeben werden, ob ein Schlüssel kleiner, größer oder gleich dem anderen ist. Die im Baum abgelegten Schlüssel unterliegen einer Ordnung. Bei einem Binärbaum ist diese leicht definierbar: Der Schlüssel eines Knotens ist größer als der (oder gleich dem) Schlüssel der Wurzel seines linken Unterbaums und kleiner als der (oder gleich dem) Schlüssel der Wurzel seines rechten Unterbaums. Abb. 2.3 zeigt einen solchen binären Suchbaum. Diese Methodik kann verallgemeinert werden: Definition 2.6: Suchbaum Sei B ein Baum der Ordnung n und sei S eine Schlüsselmenge. B ist ein n-närer Suchbaum, wenn gilt: 1. Jedem Knoten und jedem Blatt ist eine Schlüsselmenge S0 ⊆ S zugeordnet. S0 ist eine geordnete Menge < s1 , . . . , sk > mit 1 ≤ k ≤ n − 1, für die gilt: s1 ≤ s2 ≤ · · · ≤ sk . 2. Jeder Knoten v mit Grad m (2 ≤ m ≤ n) verfügt über eine geordnete Schlüsselmenge < s1 , . . . , sm−1 > von m − 1 Schlüsseln. Für alle Schlüssel si (1 ≤ i ≤ m − 1) gilt: si ist ≥ alle Schlüssel der Wurzel des i-ten Unterbaums von v. Zudem ist Schlüssel sm−1 ≤ alle Schlüssel des m-ten Unterbaums von v. 3. Jeder Knoten v mit Grad 1 verfügt über genau einen Schlüssel s, wobei s ≥ alle Schlüssel der Wurzel des einzigen Unterbaums von v gilt. Abb. 2.4 zeigt einen Suchbaum der Ordnung 4, der dieser Definition entspricht. Alle Knoten und Blätter haben maximal 3 Schlüssel, und die Schlüssel eines Knotens „teilen“ die Schlüssel ihrer Unterbäume. 9 Nach Def. 2.4 kann nur vt ein Blatt sein D Seite 88 Studienbrief 2 Basiskonzepte und Datenstrukturen Abb. 2.3: Binärer Suchbaum 10 20 4 1 7 17 19 Abb. 2.4: Suchbaum mit Ordnung m=4 20 60 10 5 7 30 35 50 11 12 15 21 24 33 38 43 45 70 90 52 55 65 67 69 80 85 96 Kontrollaufgabe 2.4 K Warum entspricht der Binärbaum in Abb. 2.3 nicht zu 100% dieser Definition? Wie könnte der Baum bspw. umgebaut werden, damit er der Definition entspricht? Oder anders herum: Welcher relativ willkürlicher Passus der Definition, der eher implementierungstechnische Gründe bei n-nären Suchbäumen hat, müsste geändert werden, damit der Baum der Definition entspricht? Speichern von Informationen in Suchbäumen Die Schlüssel können als Informationen aufgefasst werden, die in einem Suchbaum gespeichert sind. Wir unterscheiden knotenorientierte und blattorientierte Suchbäume. Bei einem knotenorientierter Baum sind die Informationen über alle Knoten und Blätter des Baums verteilt. Bezüglich Informationsgehalt wird also nicht zwischen Knoten und Blättern unterschieden. Im Gegensatz dazu sind bei einem blattorientierten Suchbaum die Informationen ausschließlich in den Blättern vorhanden, d. h. die Schlüssel in den Knoten bilden lediglich Wegweiser zu den Blättern. knotenorientierter Suchbaum Betrachten wir zunächst den Fall eines knotenorientierten Suchbaums. Wenn die Schlüssel Informationen darstellen, dann bedeuten doppelte Schlüssel auch doppelte Informationen. Das mehrfache Vorhandensein von Schlüsseln macht bei manchen Anwendungen Sinn, bei anderen nicht. Im Folgenden werden wir immer von ungleichen Schlüsseln bei knotenorientierten Bäumen ausgehen. Formal ist dazu in der Definition die Forderung zu ergänzen, dass alle Schlüsselmengen aller Knoten und Blätter disjunkt sind. blattorientierter Suchbaum Bei blattorientierten Bäumen ist es etwas komplizierter, wenn einerseits keine doppelten Informationen gespeichert werden sollen, aber andererseits einzelne Blättern und Knoten identische Schlüssel haben dürfen. Dazu werden zwei Schlüsselmengen SK ⊆ S für Knoten und SB ⊆ S für Blätter definiert, die nicht disjunkt, 2.11 Strukturierte Datentypen Seite 89 sondern insbesondere auch identisch sein können. Die Schlüsselmengen der einzelnen Knoten müssen allerdings paarweise disjunkt sein, ebenso die der einzelnen Blätter. Implementierung von Bäumen Die Implementierung eines Baums kann mittels Felder oder dynamisch mit Strukturen realisiert werden, wie wir es bereits in Studienbrief 1 kennengelernt haben. Ist die maximale Größe eines Baums zur Kompilierungszeit bekannt, so bietet sich die Speicherung in Feldern an, weil dadurch der Zusatzaufwand zur dynamischen Speicherallokation eingespart werden kann. Die Implementierung kann bei einem (vollständigen) binären Baum wie folgt durchgeführt werden: Wenn M = {v1 , . . . , vn , b1 , . . . , bn+1 } die Menge der Knoten und Blätter ist, so wird eine beliebige injektive Funktion pos : M → N festgelegt, für die gilt: 1. pos(x), x ∈ M gibt die Position (Index) im Feld an. 2. inh(pos(x)) ist der „Inhalt“ des Knotens oder Blatts, also z. B. der Schlüssel. 3. LK(pos(x)) ist die Position des linken Kindes von x. 4. RK(pos(x)) ist die Position des rechten Kindes von x. Das Feld zur Speicherung des binären Baums aus Abb. 2.3 könnte bspw. wie folgt aufgebaut sein: pos(x) 1 2 3 4 5 6 7 inh(pos(x)) 10 4 20 1 7 17 19 LK(pos(x)) 2 4 6 - RK(pos(x)) 3 5 7 - Tabelle 2.1: Baum, gespeichert in einem Feld Da die Funktion pos nach N abbildet, können fehlende Kinder durch leere 0Verweise in LK bzw. RK angezeigt werden. Insbesondere haben Blätter zwei 0Verweise und können darüber identifiziert werden. Diese Implementierung ist leicht auf n-näre Bäume erweiterbar. Im Folgenden gehen wir allerdings stets von einer Implementierung von Bäumen durch dynamische Strukturen aus, wobei Zeiger auf Folgeknoten verweisen. Durchmusterung und Grundoperationen Folgende Grundoperationen werden auf Suchbäumen ausgeführt: • Suche nach einem Schlüssel • Einfügen eines Schlüssels • Entfernen eines Schlüssels Des Weiteren ist die Durchmusterung bzw. strukturierte Durchsuchung eines Baums eine wichtige Operation, die allerdings nicht auf Suchbäume beschränkt ist. Wegen des rekursiven Baumaufbaus ist eine Durchmusterung leicht durch Aufruf der folgenden rekursiven Funktion möglich, der die Wurzel des Baums Durchmusterung von Suchbäumen Seite 90 Studienbrief 2 Basiskonzepte und Datenstrukturen als Parameter übergeben wird. Hier werden bspw. die Schlüssel systematisch ausgedruckt. Quelltext 2.2 Q 1 durchmustern(knoten s) { drucke Schluessel von s aus; 3 for {alle Kinder k von links nach rechts} 4 durchmustern(k); 5 } 2 Präordnung und Postordnung Die „Aktion“, also in dem Fall das Ausdrucken der Schlüssel, findet zuerst an der Wurzel statt, dann an der Wurzel des am weitesten links stehenden Unterbaums usw. Man spricht daher von einer Präordnung, weil die Aktion vor dem rekursiven Aufruf der Unterbäume stattfindet. Die Präordnung kann auch mit der Aktionsreihenfolge wK1 K2 . . . Km gekennzeichnet werden, wobei w die Wurzel und Ki die Kinder symbolisieren. Vertauscht man im Listing Zeile (2) mit den Zeilen (3) und (4), so wird der Baum in Postordnung durchmustert, man schreibt K1 K2 . . . Km w. In Abb. 2.5 sind die Knoten eines Baums in der Reihenfolge nummeriert, in denen die Aktionen bei Postordnung und Präordnung ausgeführt werden. Abb. 2.5: Durchmusterung mit Präordnung und Postordnung 1 12 Präordnung Postordnung 28 31 lexikographische Ordnung K 75 44 52 10 11 63 87 11 9 12 10 96 Speziell bei binären Bäumen existiert noch der Begriff der symmetrischen oder lexikographischen Ordnung, die der Aktionsreihenfolge LwR entspricht, wobei L bzw. R für den linken bzw. rechten Unterbaum stehen. Kontrollaufgabe 2.5 Wie muss das Listing verändert werden, um eine Durchmusterung eines Binärbaums in lexikographischer Ordnung zu erzeugen? Die Laufzeit der Durchmusterung ist offensichtlich Θ(n) (n gleich Anzahl Knoten und Blätter), da jeder Knoten und jedes Blatt Θ(1) „Besuche“ hat. Auf einen formalen Beweis soll an dieser Stelle verzichtet werden. Nun zu den drei Grundoperationen. Wir betrachten hier knotenorientierte Suchbäume, denn beim Einfügen und Streichen ist für blattorientierte Suchbäume nicht 2.11 Strukturierte Datentypen Seite 91 allgemein formulierbar, was zu tun ist. Oder anders ausgedrückt: Es hängt weitgehend von äußeren Parametern ab, wann und warum ein Schlüssel in einem Knoten ergänzt bzw. gelöscht wird. Wir nehmen weiterhin an, dass die Schlüssel w im Baum vom Typ Integer sind. Die Suche nach einem Schlüssel (s. nachfolgendes Listing) ist dem Durchmusterungsverfahren sehr ähnlich, nur dass hier nicht alle Kinder eines Knotens besucht werden, sondern nur der Unterbaum, in dem der Schlüssel enthalten sein kann. Dieser Unterbaum ist wegen der Struktur des Suchbaums eindeutig. Da wir nicht gefordert haben, dass jeder innere Knoten die maximal mögliche Anzahl von Kindern haben muss, besteht keine Gewähr, dass es diesen Unterbaum tatsächlich gibt; existiert er nicht, so wird die Suche erfolglos abgebrochen (Zeile 12). Das Ergebnis ist vom Typ knoten, womit ein Zeiger auf einen Knoten gemeint ist. Dieser Zeiger zeigt bei einer erfolgreichen Suche auf den Knoten bzw. das Blatt, das den Schlüssel enthält. Die Laufzeit einer Suche entspricht offensichtlich der Länge des Suchpfades und damit maximal der Tiefe des Baums. Quelltext 2.3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Q knoten suche(knoten s; int w) { knoten v; if (w in Schluesselmenge von s) return s; else if (s ist Blatt) return NULL else { v = Wurzel des Unterbaums von s, in dem der Schluessel gemaess der Schluesselordnung enthalten sein kann; if (v == NULL) return NULL else return suche(v,w); } } Die Grundoperationen „einfügen“ und „streichen“ betrachten wir an dieser Stelle nur für binäre knotenorientierte Suchbäume mit unterschiedlichen Schlüsseln.10 Die Verfahren werden für spezielle Suchbäume in Studienbrief 4 gesondert betrachtet, weil sie stark von der genauen Definition dieser Bäume abhängen. In einem 10 Suche nach einem Schlüssel Die Regel, dass Kinder immer „linksbündig“ an einen Knoten gehängt werden sollen, vernachlässigen wir hierbei. Zur Umsetzung wären ggf. geringfügige Transformationen auf Blattebene beim Einfügen oder Streichen eines Schlüssels notwendig, die für die Betrachtung der generellen Verfahren allerdings unwichtig sind. Einfügen eines Schlüssels Seite 92 Studienbrief 2 Basiskonzepte und Datenstrukturen binären Suchbaum enthält jeder Knoten bzw. jedes Blatt genau einen Schlüssel. Das Einfügen eines Schlüssels geschieht folgendermaßen: Quelltext 2.4 Q 1 2 3 4 5 6 7 einfuege(knoten s; int w) { knoten v; if(suche(s,w) == NULL){ v = Knoten, in dem die Suche endete; haenge neues Blatt mit Schluesel w an v } } Wenn der Schlüssel bereits vorhanden ist, ist nichts zu tun (Zeile 3). Ansonsten endet die Suche in einem Knoten oder in einem Blatt. Wir gehen davon aus, dass durch eine kleine Erweiterung des Suchverfahrens ein Zeiger v auf diesen Knoten bzw. dieses Blatt gewonnen werden kann. An v wird ein neues Blatt mit dem neuen Schlüssel angehängt. Es ist zu beachten, dass auch in dem Fall, dass die Suche in einem Knoten endet, kein Unterbaum an der Einfügestelle existieren kann, denn sonst hätte die Suche nicht in diesem Knoten geendet. Bei dieser Vorgehensweise wird also immer ein neues Blatt an den Baum angehängt. Es wird niemals ein innerer Knoten eingefügt. Die Laufzeit entspricht offensichtlich auch der Länge des Suchpfades und damit maximal der Tiefe des Baums, da Erzeugung und Einhängen eines Unterbaums konstante Zeit benötigen. Streichen eines Schlüssels Nun zum Streichen eines Schlüssels (s. nachfolgendes Listing). Wenn der Schlüssel nicht vorhanden ist, muss er auch nicht gelöscht werden (Zeile 3). Ansonsten liefert die Suche einen Zeiger auf ein Blatt oder auf einen Knoten. Ist der Schlüssel in einem Blatt, so wird dieses gestrichen (Zeile 5). Ist der Schlüssel in einem Knoten, so wird ein kleiner Trick angewandt. Es wird der am weitestem rechts stehende Knoten x im linken Unterbaum von v gesucht (Zeilen 7 und 8), sofern dieser Unterbaum vorhanden ist. Ansonsten wird der am weitesten links stehenden Knoten im rechten Unterbaum gesucht (Zeile 9). Einer der beiden Unterbäume muss vorhanden sein, denn sonst wäre der Knoten ein Blatt. Ist der linke Unterbaum vorhanden, und ist x der darin am weitesten rechts stehende Knoten, so ist dessen Schlüssel kleiner als der Schlüssel in v, aber größer als alle anderen Schlüssel im linken Unterbaum von v. Für den rechten Unterbaum gilt dies analog. Nun sind zwei Fälle zu unterscheiden: 1. Ist x ein Blatt, so wird dessen Schlüssel nach v übertragen, womit die Ordnung im Baum erhalten bleibt. Blatt x wird dann gelöscht (Zeilen 11 und 12). Abb. 2.6 zeigt das Verfahren an einem Beispiel; hier wird der Schlüssel 30 aus dem Baum gelöscht. 2. Ist x ein Knoten, so wird der Schlüssel von x nach v transferiert, und anschließend wird der Schlüssel von x im Unterbaum gelöscht, dessen Wurzel x ist. Dies erfolgt rekursiv, bis die Blattebene erreicht ist (Zeilen 15 und 16). Bei diesem Verfahren müssen niemals innere Knoten aus einem Baum gelöscht werden. Die Laufzeit des Verfahrens entspricht offensichtlich maximal der Tiefe des Baums. 2.11 Strukturierte Datentypen Seite 93 Quelltext 2.5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Q streiche(knoten s; int w) { knoten v,x; if((v = suche(s,w)) != NULL){ if(v ist ein Blatt) loesche v; else { if (linker Unterbaum von v ist vorhanden) x = rechtester Knoten im linken Unterb. von v; else x = linkester Knoten im rechten Unterb. von v; if (x == BLATT) { ersetze Schluessel in v durch den in x; loesche x; } else { ersetze Schluessel in v durch den in x; streiche(x, Schlussel von x); } } } } 10 9 10 30 33 25 12 Abb. 2.6: Streichen in einem binären Suchbaum 27 ⇒ 9 27 25 33 12 Kontrollaufgabe 2.6 Beschreiben Sie die den Löschvorgang des Schlüssels 30 im Baum aus Abb. 2.6, wenn der Knoten mit Schlüssel 27 nicht existiert. Balancierte Bäume Wir nennen einen Suchbaum geeignet11 , wenn alle drei Grundoperationen in Laufzeit O(log2 n) ausgeführt werden können, wobei n die Summe der Knoten und Blätter des Baums ist. Die bisherige Definition bzw. die im vorangegangenen Abschnitt vorgestellten Grundoperationen führen nicht automatisch zu geeigneten Suchbäumen, was leicht am Beispiel eines Binärbaums gezeigt werden kann. Ein durch fortlaufendes Einfügen erzeugter Binärbaum könnte in der Form „entartet“ sein, dass jeder Knoten nur ein Kind hat. Der Baum ist dann eigentlich kein 11 Es existiert in der Literatur (z. B. [Schöning, 1997]) der Begriff eines optimalen Suchbaums, der über Zugriffsverteilungen und gewichtete Pfadlängen definiert ist. Der einfachere und schwächere Begriff „geeigneter Suchbaum“ soll uns an dieser Stelle aber reichen. K Seite 94 Studienbrief 2 Basiskonzepte und Datenstrukturen Baum mehr, sondern eine Liste, bei der im schlimmsten Fall alle Knoten nacheinander durchsucht werden müssen, was zu einer Laufzeit von O(n) führt. Man könnte darauf vertrauen, dass solche Entartungen sehr selten auftreten, oder man könnte von Zeit zu Zeit den Baum komplett neu aufbauen. Der sicher elegantere Weg ist es allerdings, balancierte Bäume zu benutzen. Geeignete Suchbäume sind balanciert. Definition 2.7: Balancierter Baum D Sei B ein Baum und sei n die Anzahl der Knoten und Blätter des Baums. B heißt balanciert, wenn für die Tiefe T des Baums gilt: T = O(log2 n). höhenbalancierter Baum Das heißt informal, dass Knoten in der Regel mehr als ein Kind und alle Blätter in etwa dieselbe Tiefe haben. Man spricht deswegen auch von einem höhenbalancierten Baum. Beispiele für höhenbalancierte Bäume sind AVL-Bäume und B-Bäume. Darauf werden wir in Studienbrief 4 zurückkommen. gewichtsbalancierter Baum Neben höhenbalancierten Bäumen existiert noch gewichtsbalancierte Bäume. Dabei haben alle Unterbäume eines Knotens in etwa dieselbe Anzahl an Knoten und Blättern. Im Fall eines binären Suchbaums B wird eine beschränkte Balance α vorgegeben und die Wurzelbalance ρ(B) wie folgt definiert: |Br | l| ρ(B) = |B |B| = 1 − |B| , wobei |B| die Anzahl der Blätter des Baums und |Bl | (|Br |) die Anzahl der Blätter des linken (rechten) Unterbaums bezeichnen. Ein Baum B ist von beschränkter Balance α, wenn für jeden Unterbaum B0 von B, dessen Wurzel ein Knoten ist, α ≤ ρ(B0 ) ≤ 1−α gilt. Diese Bäume werden als√BB[α]Bäume bezeichnet, und es kann gezeigt werden, dass diese für 14 < α ≤ 1 − 22 eine logarithmische Tiefe haben (s. z. B. [Mehlhorn, 1986]). Wenn ein balancierter Suchbaum vorliegt, so ist die Suche darin mit dem genannten Verfahren wegen der logarithmischen Tiefe in Zeit O(log2 n) möglich. Schwieriger ist das Einfügen und Entfernen von Schlüsseln. Einerseits haben die Knoten und Blätter begrenzte Kapazität, d. h. die maximale Anzahl an Schlüsseln ist dort fest vorgegeben. Andererseits sind keine leeren Knoten und Blätter erlaubt. Demzufolge müssen balancierte Suchbäume beim Einfügen und Entfernen von Schlüsseln ggf. umgebaut werden, d. h. es müssen Knoten und Blätter eingefügt bzw. entfernt werden, ohne dass die Balance-Eigenschaft verloren geht. Die in Studienbrief 4 vorgestellten Suchbäume sind geeignete Suchbäume, und es werden jeweils die Algorithmen vorgestellt, die das Einfügen und Entfernen von Schlüsseln in Zeit O(log2 n) ermöglichen. 2.11.2 Graphen Ein Graph ist eine dem Baum verwandte Datenstruktur, dem aber insbesondere die strenge Hierarchie eines Baums fehlt. Es gibt weder eine Wurzel am Anfang, noch gibt es Blätter am Ende. Ein Graph besteht nur aus Knoten, und Kanten verweisen von Knoten zu nachfolgenden Knoten. Abb. 2.7 zeigt einen Graphen mit 5 Knoten und 6 Kanten. Die Kanten werden als Pfeile zwischen Quell- und Zielknoten gezeichnet. Viele Strukturen der realen Welt lassen sich als Graphen repräsentieren. Als Beispiel sei eine Straßennetzkarte genannt. Die Straßen selbst entsprechen den Kanten, und Städte oder Straßenkreuzungen (je nach Detailgehalt 2.11 Strukturierte Datentypen Seite 95 der Karte) entsprechen den Knoten. Auf einer solchen, als Graph repräsentierten, Straßennetzkarte, können bspw. kürzeste Fahrtrouten berechnet werden, wenn die Kanten mit Kilometerangaben versehen werden. Andere Anwendungsgebiete von Graphen sind die Netzanalyse in der Elektrotechnik, die Erkennung molekularer Strukturen in der organischen Chemie, die Segmentierung von Computerprogrammen, der Entwurf integrierter Schaltkreise (VLSI-Entwurf) usw. Formal sind Graphen wie folgt definiert: Definition 2.8: Graphen 1. Ein endlicher Graph G = (V, E) besteht aus einer endlichen Menge von Knoten V = {v1 , . . . , vn } (n = |V |) und einer endlichen Menge von Kanten E, E ⊆ V ×V . (v, w) ∈ E heißt Kante von v nach w. Der Knoten v heißt Quelle, der Knoten w Ziel der Kante (v, w). D 2. Ein Pfad p =< v0 , . . . , vk > in G von Knoten x nach Knoten y ist eine Folge v0 . . . vk von Knoten mit v0 = x und vk = y, und (vi , vi+1 ) ∈ E für 0 ≤ i < k. k heißt Länge (oder Kardinalität) des Pfades. Ein Pfad heißt Zyklus, wenn x = y gilt für k ≥ 1. 3. Ein Pfad p =< v0 , . . . , vk > heißt einfach, wenn vi 6= v j gilt für i 6= j (außer vielleicht v0 = vk ). Ein einfacher, zyklischer Pfad, der alle Knoten des Graphen beinhaltet, heißt Hamilton-Zyklus. 4. Ein Graph G heißt azyklisch, wenn es keinen Zyklus in G gibt. 5. Ein Graph G heißt ungerichtet, wenn mit (v, w) ∈ E stets (w, v) ∈ E gilt. Ansonsten heißt er gerichtet. Ein Paar von Kanten (v, w) und (w, v) bei einem ungerichteten Graphen schreiben wir abkürzend als {v, w}. 6. Der Ingrad eines Knotens v ist die Anzahl der Knoten w, von denen eine Kante zu v existiert, der Outgrad eines Knotens v ist die Anzahl der Knoten w, zu denen eine Kante von v existiert: indeg(v) = |{w, (w, v) ∈ E}| outdeg(v) = |{w, (v, w) ∈ E}| Bei einem ungerichteten Graph sind Ingrad und Outgrad jedes Knotens identisch und bezeichnen den Grad des Knotens. 7. Ein Graph G heißt gewichtet, wenn jeder Kante (i, j) eine Zahl ci, j ∈ R zugeordnet ist. ci j heißt das Gewicht von Kante (i, j).12 8. Sei G = (V, E) ein Graph. Der Graph G0 = (V 0 , E 0 ) heißt Untergraph von G, wenn V 0 ⊆ V und E 0 ⊆ E gilt. Beispiel 2.4 B Bild 2.7 zeigt einen (ungewichteten) gerichteten, azyklischen Graphen mit der Knotenmenge V = {1, 2, 3, 4, 5} und den Kanten E = {(1, 2), (1, 3), (2, 4), (2, 5), (3, 5), (4, 5)}. Wir legen künftig n = |V | und e = |E| als Bezeichnung der Anzahl von Knoten bzw. Kanten eines Graphen fest. Des Weiteren bezeichnen wir künftig einen azyklischen, ungerichteten Graphen als DAG (directed acyclic graph). 12 Wir legen fest, dass bei einem ungerichteten Graph stets ci j = c ji für alle Kanten (i, j) gilt. DAG Seite 96 Studienbrief 2 Basiskonzepte und Datenstrukturen Abb. 2.7: Ein Graph 1 4 2 5 3 Die enge Verwandtschaft von Bäumen und Graphen zeigt die folgende Definition. Ein Baum ist somit eine Sonderform eines gerichteten Graphen. D Definition 2.9: Wälder und Bäume 1. Ein gerichteter Graph A = (V, E) heißt gerichteter Wald, wenn gilt: a) A ist azyklisch. b) indeg(v) ≤ 1 für alle v ∈ V . Ein Knoten v ∈ V heißt Wurzel von A, wenn indeg(v) = 0 gilt. 2. Ein gerichteter Wald heißt Baum, wenn er genau eine Wurzel hat. 3. Sei G = (V, E) ein gerichteter Graph. Ein gerichteter Wald A = (V, E 0 ) mit E 0 ⊆ E heißt aufspannender Wald von G. Ist A ein Baum, so heißt er aufspannender Baum (spanning tree). B Beispiel 2.5 Der Graph in Abb. 2.7 ist offensichtlich kein Wald, da Knoten „5“ Ingrad 3 hat. Beispielsweise durch Streichen der Kanten (3,5) und (4,5) kann ein aufspannender Baum des Graphen erzeugt werden. Repräsentation von Graphen Für Graphenalgorithmen spielt es eine große Rolle, wie die Repräsentation eines Graphen im Speicher eines Rechners realisiert wird, wie also Knoten und Kanten real gespeichert werden. Üblich ist die Repräsentation als Adjazenzmatrix oder als Adjazenzliste. D Definition 2.10: Adjazenzmatrix Sei G = (V, E) ein Graph und sei n = |V |. Eine Adjazenzmatrix ist eine (n × n)boolesche Matrix A = (ai j )1≤i, j≤n , für die gilt: ai, j = 1 0 , falls (i, j) ∈ E , falls (i, j) ∈ /E 2.11 Strukturierte Datentypen Seite 97 Definition 2.11: Adjazenzliste D Sei G = (V, E) ein Graph und sei n = |V |. Eine Adjazenzliste (Nachbarschaftsliste) von G besteht aus n linearen Listen, wobei gilt: Liste i(1 ≤ i ≤ n) enthält alle j mit (i, j) ∈ E. Die n Listenköpfe sind in einem Feld13 der Größe n gespeichert. Beispiel 2.6 B In Abb. 2.8 sind Adjazenzmatrix und Adjazenzliste eines Graphen dargestellt. Die Knoten sind der Deutlichkeit halber durchnummeriert. Bei einem ungerichteten Graph wäre die Adjazenzmatrix symmetrisch. 1 2 3 4 5 1 4 2 5 3 1 2 3 4 5 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0 0 1 2 3 4 5 Abb. 2.8: Adjazenzmatrix und Adjazenzliste 5 1 5 3 Eine dritte Art der Repräsentation haben wir bereits im Zusammenhang mit Bäumen kennengelernt: Adjazenzfelder. In jedem Knoten sind die ausgehenden Kanten jeweils in einem Feld gespeichert. Diese Art der Repräsentation ist allerdings i. Allg. nur bei einem kleinen, maximalen Outgrad sinnvoll. Es ist offensichtlich, dass eine Adjazenzmatrix O(n2 ) Speicherplatz benötigt. Eine Adjazenzliste benötigt O(n + e) Speicherplatz. Da e ≤ n2 gilt, ergibt das ebenfalls einen Speicherplatzbedarf von O(n2 ). Bei „dünnen“ Graphen (e n2 ) wird allerdings sehr viel weniger Speicher als bei einer Adjazenzmatrix benötigt. Adjazenzmatrix Welche Art der Repräsentation gewählt wird, hängt in erster Linie davon ab, welche Algorithmen für einen Graphen implementiert werden sollen, welche Zugriffe und Operationen also auf einem Graphen ausgeführt werden sollen. Es ist die Repräsentation zu wählen, welche die benötigten Implementierungen möglichst effizient unterstützt. Oftmals ist die Adjazenzliste die geeignetere Repräsentation, allerdings ist hier die Bestimmung der Vorgänger eines Knotens schwieriger als bei einer Adjazenzmatrix. Die Frage, ob eine bestimmte Kante in einem Graphen vorhanden ist, ist bei einer Adjazenzmatrix in Zeit O(1) zu beantworten, bei der Adjazenzliste muss eine der Teillisten durchsucht werden, was allgemein eine Laufzeit von O(e) ergibt. Einige graphentheoretische Probleme lassen sich mit Methoden der linearen Algebra lösen, was die Repräsentation als Adjazenzmatrix favorisiert. Adjazenzlisten sind in der Regel dann vorteilhaft, wenn Graphen gemäß ihrem strukturellen Aufbau durchsucht werden sollen, also gemäß ihrer Nachbarschaftsbeziehungen. Bei einer Adjazenzmatrix muss immer eine komplette Zeile durchsucht werden, um die Folgeknoten eines Knotens zu bestimmen, was eine Laufzeit 13 Dieses Feld ist gemäß einer „Durchnummerierung“ der Knoten geordnet. Adjazenzliste Seite 98 Studienbrief 2 Basiskonzepte und Datenstrukturen von O(n) ergibt. Sollen Graphen dynamisch verändert werden, insbesondere Knoten aus einem Graphen gestrichen oder zu diesem hinzugefügt werden, so liegen die Vorteile stark auf Seiten der Adjazenzliste. Da beide Repräsentationen ihre Vor- und Nachteile haben, kann es sinnvoll sein, beide Repräsentationen zur Verfügung zu haben bzw. eine der Repräsentationen mit anderen Datenstrukturen (z. B. Suchbäume oder Hashtabellen (s. Abs. 3.4.3)) anzureichern. Ist ein Graph gewichtet, so müssen die Gewichte zusätzlich in die Repräsentation aufgenommen werden. Bei einer Adjazenzmatrix ist dies oft durch Verwendung einer Matrix mit reellen Zahlen statt einer booleschen Matrix möglich, ansonsten können Zeiger in der Adjazenzmatrix auf Gewichte verweisen. Bei einer Adjazenzliste werden die Gewichte zusätzlich zu den Verweisen in den Listenelementen gespeichert. 2.12 Zusammenfassung Die Definition eines eindeutigen, realitätsnahen und einfachen Modells ist für eine objektive Effizienzanalyse von Algorithmen fundamental wichtig. Von diesem Modell ausgehend können verschiedene Methoden angewandt werden, um in einem konkreten Fall, also für einen konkreten Algorithmus, Effizienzaussagen zu treffen und diesen mit anderen Algorithmen zu vergleichen. Die hier eingeführte Modellbildung bezieht sich auf sequentielle Algorithmen, kann aber auf verschiedenste Weise für parallele Algorithmen erweitert werden. Reale Rechnerhardware arbeitet mit einigen wenigen elementaren Datentypen wie Integer- oder Fließkommazahlen. Strukturierte Datentypen bilden komplexere Datenmodelle auf diese elementaren Datentypen ab. Strukturierte Datentypen, oft auch als abstrakte Datentypen bezeichnet, definieren neben der Struktur der Daten im Rechner selbst auch die Methoden, mit denen auf diesen Daten gearbeitet werden kann. Die Effizienz dieser Methoden ist äußerst wichtig, um darauf aufbauend effiziente Algorithmen formulieren und implementieren zu können. 2.13 Übungen Seite 99 2.13 Übungen Übung 2.1 Ü Beweisen Sie formal die Aussagen von Satz 2.1. Übung 2.2 Ü Zeigen Sie, dass das Travelling-Salesman-Problem ein Problem der Klasse NP ist. Wählen Sie zur Formulierung des Algorithmus (in Pseudocode) als Modell einer nichtdeterministischen Registermaschine eine synchrone priority CRCW (Concurrent Read Concurrent Write) PRAM: Die PRAM verfügt über beliebig viele Prozessoren, die auf einen gemeinsamen Speicher zugreifen. Alle Prozessoren arbeiten synchron dasselbe Programm ab. Dabei ist sowohl der gleichzeitige lesende als auch der gleichzeitige schreibende Zugriff auf dieselbe Speicherzelle erlaubt. Werden gleichzeitig mehrere schreibende Zugriffe auf eine Speicherzelle getätigt, so wird vereinbart, dass der kleinste zu schreibende Wert Priorität hat. Die Prozessoren der PRAM haben fest vorgegebene Prozessornummern procnum, die mit 0 beginnen. Die Parallelität im Algorithmus wird durch for-pardo-Konstrukte ausgedrückt. Zeigen Sie anschließend, wie der Algorithmus auch für das schwächere Modell einer CREW (Concurrent Read Exclusive Write) PRAM formuliert werden kann. Bei einer CREW PRAM ist das gemeinsame Schreiben auf eine Speicherzelle verboten, das gemeinsame Lesen ist jedoch erlaubt. Übung 2.3 Erweitern Sie den Binärbaum aus Übung 1.13 zu einem n-nären Suchbaum, wobei n ≥ 2 frei wählbar ist. Es reicht, einen Baum mit Integer-Schlüsseln zu implementieren. Anforderungen an Balancen sind keine gestellt. Der Einfachheit halber halten wir uns nicht ganz genau an Definition 2.6: Die Anzahl der Schlüssel in einem Knoten darf größer sein als die Anzahl seiner Kinder. Die Kinder hängen weiterhin linksbündig an einem Knoten. Durch die Auflockerung der Schlüsselordnung gelingt es, Suchbäume mit gut gefüllten Knoten zu erzeugen, ohne detaillierte Verfahren festzulegen, wie Bäume beim Einfügen neuer Schlüssel ggf. umstrukturiert werden müssen. Mit einem Suchbaum (B-Baum), der Definition 2.6 genau einhält, werden wir uns in Studienbrief 4 beschäftigen. Ü Seite 100 Ü Studienbrief 2 Basiskonzepte und Datenstrukturen Übung 2.4 Implementieren Sie die Grundoperationen aus Abs. 2.11.1 für einen binären, knotenorientierten Suchbaum in C. Würfeln Sie in beliebiger Reihenfolge Integer-Schlüssel zum Einfügen und Streichen. Geben Sie das Ergebnis strukturiert aus. Tipp: Sie können auf Teil 1 von Übung 1.14 aufsetzen. Ü Übung 2.5 Formulieren Sie die Suche gemäß Abs. 2.11.1 für blattorientierte Suchbäume in Pseudocode. Welche Probleme können Sie erkennen, wenn Sie Schüssel in einem blattorientierten binären Suchbaum einfügen oder streichen wollen? Ü Übung 2.6 Schreiben Sie ein Programm, das einen Graphen mit 20 Knoten generiert, wobei beliebige Kanten per Zufallsgenerator erzeugt werden. Speichern Sie den Graphen als Adjazenzliste und als Adjazenzmatrix. Geben Sie Adjazenzliste und Adjazenzmatrix in einer übersichtlichen Form auf dem Bildschirm aus. Liste der Lösungen zu den Kontrollaufgaben Seite 187 Liste der Lösungen zu den Kontrollaufgaben Lösung zu Kontrollaufgabe 2.1 auf Seite 75 2n+1 = 2 · 2n . Damit gilt nach Satz 2.1: O(2 · 2n ) = O(2) · O(2n ) = O(2n ). Damit gilt 2n+1 ∈ O(2n ). Lösung zu Kontrollaufgabe 2.2 auf Seite 76 Der Datentransferbefehl Ri = R j kann bspw. durch den logischen Befehl Ri = R j and R j „simuliert“ werden. Die Laufzeit eines Divisionsbefehls ist zwar länger als die eines Additionsbefehls, aber dennoch konstant bzw. – in Abhängigkeit von der Implementierung in Hardware – nach oben durch einen konstanten Wert beschränkt. Bei einer asymptotischen Laufzeitanalyse spielt die Konstante keine Rolle, bei einer absoluten Zeitmessung natürlich schon. Es gibt bei realen CPUs Befehle, die komplette Schleifen nach einem Schleifenzähler abarbeiten. Solche Befehle beherrscht die RAM natürlich nicht, da diese Befehle keine konstante Laufzeit haben. Hierzu sind mindestens 200 Befehle erforderlich, weil nicht direkt von einer Speicherzelle in eine andere Speicherzelle geschrieben werden kann. Es ist also der Umweg über ein Register erforderlich. Lösung zu Kontrollaufgabe 2.3 auf Seite 81 Generell weder noch, da das Verfahren wegen der unendlich vielen Nachkommastellen von π nie mit einem korrekten Ergebnis terminieren kann. Will man jedoch π auf eine feste Anzahl von Nachkommastellen genau bestimmen, dann ist das Verfahren ein Monte Carlo-Algorithmus. Lösung zu Kontrollaufgabe 2.4 auf Seite 88 Die Definition besagt für einen Binärbaum, dass bei einem Knoten, der nur ein Kind hat, dieses immer das linke Kind ist. Man könnte also bspw. die 19 an die Stelle der 17 schreiben, und die 17 als linkes Kind an die 19 anhängen. Wenn ein Knoten weniger Kinder hat als es die Ordnung des Baums erlaubt, dann könnten die Kinder eigentlich an beliebigen Positionen angehängt werden, solange das der Schlüsselordnung genügt. In unserer Definition sind die Kinder „linksbündig“ angehängt. Bei einer Implementierung eines n-nären Suchbaums werden die Verweise auf die Kinder in der Regel in Feldern gespeichert. Die Linksbündigkeit macht dann die Zugriffe auf die Kinder effizienter, weil nicht das gesamte Feld durchforstet und auf NULL-Zeiger überprüft werden muss. Ansonsten müsste eine andere Datenstruktur als ein Feld für die Speicherung der Verweise gewählt oder ein Feld mit den Indizes der nicht NULL-Zeiger angelegt werden. Lösung zu Kontrollaufgabe 2.5 auf Seite 90 Quelltext 5.19 1 2 durchmustern(knoten s) { durchmustern(linkes Kind); Q Seite 188 Liste der Lösungen zu den Kontrollaufgaben 3 drucke Schluessel von s aus; durchmustern(rechtes Kind); 4 5 } Lösung zu Kontrollaufgabe 2.6 auf Seite 93 Der am weitesten rechts stehende Knoten im linken Unterbaum des Knotens v mit Schlüssel 30 ist der Knoten x mit Schlüssel 25. Der Schlüssel 30 von v wird mit Schlüssel 25 überschrieben, und anschließend wird Schlüssel 25 im Unterbaum mit Wurzel x gelöscht. Dort ist der Knoten mit Schlüssel 12 der am weitesten rechts stehende Knoten. Dieser ist ein Blatt; sein Schlüssel wird also nach x transferiert, und das Blatt wird gelöscht. Verzeichnisse Seite 189 Verzeichnisse I. Abbildungen Abb. 1.1: Abb. 1.2: Abb. 1.3: Abb. 2.1: Abb. 2.2: Abb. 2.3: Abb. 2.4: Abb. 2.5: Abb. 2.6: Abb. 2.7: Abb. 2.8: Abb. 3.1: Abb. 3.2: Abb. 3.3: Abb. 3.4: Abb. 3.5: Abb. 3.6: Abb. 3.7: Abb. 3.8: Abb. 3.9: Abb. 3.10: Abb. 3.11: Abb. 4.1: Abb. 4.2: Abb. 4.3: Abb. 4.4: Abb. 4.5: Abb. 4.6: Abb. 4.7: Abb. 4.8: Abb. 4.9: Abb. 4.10: Abb. 4.11: Abb. 4.12: Abb. 4.13: Abb. 5.1: Abb. 5.2: Abb. 5.3: Abb. 5.4: Abb. 5.5: Abb. 5.6: Abb. 5.7: Abb. 5.8: Abb. 5.9: Abb. 5.10: Abb. 5.11: Feld a im Hauptspeicher . . . . . . . . . . . . . . . . . Verkettete Liste . . . . . . . . . . . . . . . . . . . . . Binärbaum . . . . . . . . . . . . . . . . . . . . . . . . Muhammad ibn Musa al-Chwarizmi . . . . . . . . . . . Ein Baum . . . . . . . . . . . . . . . . . . . . . . . . . Binärer Suchbaum . . . . . . . . . . . . . . . . . . . . Suchbaum mit Ordnung m=4 . . . . . . . . . . . . . . Durchmusterung mit Präordnung und Postordnung . . Streichen in einem binären Suchbaum . . . . . . . . . Ein Graph . . . . . . . . . . . . . . . . . . . . . . . . . Adjazenzmatrix und Adjazenzliste . . . . . . . . . . . . Heap als Baum und als Feld . . . . . . . . . . . . . . . Wiederherstellung der Heap-Eigenschaft . . . . . . . . Schema von Bucketsort . . . . . . . . . . . . . . . . . Binärsuche . . . . . . . . . . . . . . . . . . . . . . . . Digitaler Suchbaum . . . . . . . . . . . . . . . . . . . Komprimierter digitaler Suchbaum . . . . . . . . . . . Hashtafel . . . . . . . . . . . . . . . . . . . . . . . . . Hashing mit Verkettung . . . . . . . . . . . . . . . . . Hashing mit offener Adressierung . . . . . . . . . . . . Partitionierung und union . . . . . . . . . . . . . . . . Pfadkomprimierung . . . . . . . . . . . . . . . . . . . AVL-Baum . . . . . . . . . . . . . . . . . . . . . . . . . Fall 1: Rechtsrotation . . . . . . . . . . . . . . . . . . . Fall 2: Links-Rechtsrotation . . . . . . . . . . . . . . . Fall 3: Linksrotation . . . . . . . . . . . . . . . . . . . Fall 4: Rechts-Linksrotation . . . . . . . . . . . . . . . Schlüsselordnung . . . . . . . . . . . . . . . . . . . . B-Baum mit Ordnung m=4 . . . . . . . . . . . . . . . Einfügen in einen B-Baum . . . . . . . . . . . . . . . . Split eines Knotens . . . . . . . . . . . . . . . . . . . . Rückführung des Löschens auf einen Schlüssel im Blatt Übernahme von Schlüsseln . . . . . . . . . . . . . . . Verschmelzung von Schlüsseln . . . . . . . . . . . . . Übernahme und Verschmelzung . . . . . . . . . . . . Topologische Sortierung . . . . . . . . . . . . . . . . . Kantenklassen, dfsnum und compnum . . . . . . . . . Starke Zusammenhangskomponenten . . . . . . . . . Durchmusterung mit DFS . . . . . . . . . . . . . . . . Graph mit MST . . . . . . . . . . . . . . . . . . . . . . Entfernungen in einem Graphen . . . . . . . . . . . . Kürzeste Wege von Knoten s aus . . . . . . . . . . . . Relaxierung einer Kante . . . . . . . . . . . . . . . . . Kürzeste Wege nach Bellman-Ford . . . . . . . . . . . Folge von δk (1, 4) . . . . . . . . . . . . . . . . . . . . . Bestimmung von δk (i, j) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 56 71 73 86 88 88 90 93 96 97 104 105 113 117 118 120 121 121 125 129 133 140 141 141 142 142 144 144 146 146 148 149 150 151 156 164 166 169 171 175 177 180 181 182 182 II. Definitionen Definition 2.1: Definition 2.2: Definition 2.3: O-Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Laufzeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Randomisierte Laufzeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 78 81 Seite 190 Verzeichnisse Definition 2.4: Definition 2.5: Definition 2.6: Definition 2.7: Definition 2.8: Definition 2.9: Definition 2.10: Definition 2.11: Definition 3.1: Definition 3.2: Definition 3.3: Definition 3.4: Definition 3.5: Definition 4.1: Definition 4.2: Definition 5.1: Definition 5.2: Definition 5.3: Definition 5.4: Definition 5.5: Definition 5.6: Definition 5.7: Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tiefe und Pfade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Suchbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Balancierter Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wälder und Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adjazenzmatrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adjazenzliste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundoperationen auf Mengen . . . . . . . . . . . . . . . . . . . . Digitaler Suchbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . Perfekte Hashfunktion . . . . . . . . . . . . . . . . . . . . . . . . . Universelle Hashfunktionen . . . . . . . . . . . . . . . . . . . . . . Ackermann-Funktion . . . . . . . . . . . . . . . . . . . . . . . . . . AVL-Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . B-Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Topologisches Sortieren . . . . . . . . . . . . . . . . . . . . . . . . Induzierter Untergraph . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenhangskomponenten . . . . . . . . . . . . . . . . . . . . Mehrfache Zusammenhangskomponenten . . . . . . . . . . . . . Teilmengensysteme, Matroide und zugehörige Kostenfunktion Kürzeste (billigste) Wege in Graphen . . . . . . . . . . . . . . . . transitive Hülle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 87 87 94 95 96 96 97 115 118 126 127 133 140 143 155 162 167 170 173 175 183 III. Exkurse Exkurs 1.1: Exkurs 1.2: Exkurs 2.1: Exkurs 2.2: C-Compiler . . . . . . . . . . . . . . . . . . Strukturen als Parameter . . . . . . . . . Parallele Algorithmen . . . . . . . . . . . „Schnelle“ Multiplikation großer Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 53 77 78 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 34 59 65 65 89 IV. Tabellen Tabelle Tabelle Tabelle Tabelle Tabelle Tabelle 1.1: 1.2: 1.3: 1.4: 1.5: 2.1: Vorrang und Assoziativität der Operatoren Deklaration und Definition . . . . . . . . . Umwand- lungszeichen . . . . . . . . . . . String-Funktionen . . . . . . . . . . . . . . Mathematische Funktionen . . . . . . . . . Baum, gespeichert in einem Feld . . . . . . . . . . . V. Literatur G. M. Adelson-Velskii and E. M. Landis. An algorithm for the organization of information. Soviet Math. Doklady. 3, S. 1259-1263, 1962. Vorlesung Algorithmen. Kap. 7 Graphenalgorithmen. Universität Tübingen, 2005. Rudolf Bayer and Edward M. McCreight. Organization and maintenance of large ordered indices. Acta Informatica, Volume 1, 1972. R. E. Bellman. On a routing problem. Quarterly of Applied Mathematics 16(1), Brown University, 1958. O. Bittel. Algorithmen und Datenstrukturen. Vorlesungsskript, FH-Konstanz, 2007. Martin Dietzfelbinger, Kurt Mehlhorn, and Peter Sanders. Algorithmen und Datenstrukturen: Die Grundwerkzeuge. Springer, 2014. Literatur Seite 191 Edsger W. Dijkstra. A note on two problems in connexion with graphs. Numerische Mathematik 1, S. 269-271, 1959. Robert W. Floyd. Algorithm 97 (shortest path). Communications of the ACM 5, 1962, 6, S. 345, 1962. Brian W. Kernighan and Dennis M. Ritchie. The C Programming Language. Prentice Hall, 1978. Brian W. Kernighan and Dennis M. Ritchie. Programmieren in C. Hanser Fachbuch, 1990. Donald Ervin Knuth. The Art of Computer Programming, Volume III: Sorting and Searching. Addison-Wesley, 1973. Joseph B. Kruskal. On the shortest spanning subtree of a graph and the traveling salesman problem. Proceedings of the American Mathematical Society, 1956. Kurt Mehlhorn. Data Structures and Algorithms 2: Graph Algorithms and NP-completeness. Springer Verlag, 1984. Kurt Mehlhorn. Datenstrukturen und Algorithmen 1: Sortieren und Suchen. Teubner Verlag, 1986. Kurt Mehlhorn and Peter Sanders. Algorithms and Data Structures. Springer Verlag, 2010. Uwe Schöning. Algortithmen - kurz gefasst. Spektrum Hochschultaschenbuch, 1997. Axel Stutz and Peter Klingebiel. Übersicht über die C Standard-Bibliothek. http://www2.hs-fulda.de/~klingebiel/c-stdlib/index.htm, 1999. Robert E. Tarjan. Efficiency of a good but not linear set union algorithm. Journal of the ACM (JACM) Volume 22 Issue 2, 1975. Ronald Rivest Thomas H. Cormen, Charles E. Leiserson and Clifford Stein. Algorithmen - Eine Einführung. Oldenbourg Wissenschaftsverlag, 2010. Arnold Schönhage und Volker Strassen. Schnelle Multiplikation großer Zahlen. Computing 7, Springer Verlag, S. 281–292, 1971. Stichwörter Seite 193 Stichwörter BB[α]-Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 Adjazenzfeld . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Adjazenzliste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Adjazenzmatrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 amortisierte Kosten . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 asymptotische Analyse . . . . . . . . . . . . . . . . . . . . . . . . 74 aufspannender Baum . . . . . . . . . . . . . . . . . . . . . . . . . 96 average case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Baum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 best case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Binärbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 Bit-Komplexitätsmaß . . . . . . . . . . . . . . . . . . . . . . . . . 77 blattorientierter Suchbaum . . . . . . . . . . . . . . . . . . . . 88 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 Rekursionsgleichungen . . . . . . . . . . . . . . . . . . . . . . . 82 Schlüssel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 Schlange . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 Shared Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 spanning tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 Stapel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 Struktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 strukturierte Datentypen . . . . . . . . . . . . . . . . . . . . . 85 Substitution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 Suchbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85, 87 Turing-Maschine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 uniformes Komplexitätsmaß . . . . . . . . . . . . . . . . . . 77 verteilte Systeme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 DAG . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 deterministische Algorithmen . . . . . . . . . . . . . . . . 80 worst case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 effizienter Algorithmus . . . . . . . . . . . . . . . . . . . . . . . 84 Zeitmodell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 geeigneter Suchbaum . . . . . . . . . . . . . . . . . . . . . . . . . 93 gewichtsbalancierter Baum . . . . . . . . . . . . . . . . . . . 94 Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 höhenbalancierter Baum . . . . . . . . . . . . . . . . . . . . . . 94 knotenorientierter Suchbaum . . . . . . . . . . . . . . . . . 88 Las Vegas-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . 80 lexikographische Ordnung . . . . . . . . . . . . . . . . . . . . 90 Liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 Maschinenmodell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 Mastertheorem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 Monte Carlo-Algorithmus . . . . . . . . . . . . . . . . . . . . 80 NP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 NP-hart . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 NP-vollständig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 O-Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74, 76 P . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 parallele Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . 77 Postordnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 Präordnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 probabilistische Algorithmen . . . . . . . . . . . . . . . . . 80 Problemgröße . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 Pseudocode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 RAM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 randomisierte Algorithmen . . . . . . . . . . . . . . . . . . . 80 Anhang Seite 195 Anhang Prioritäten und Assoziativitäten der Operatoren in C Operatoren () [] -> . ! ~ ++ -- + - * & (type) sizeof * / % + << >> < <= > >= == != & ∧ | && || ?: = += -= *= /= %= &= ∧ = |= <<= >>= , Assoziativität von links von rechts von links von links von links von links von links von links von links von links von links von links von rechts von rechts von links