Leseprobe Blakowski Algorithmen und Datenstrukturen INFORMATIK Studienbrief 2-050-0208 1. Auflage 2007 HDL HOCHSCHULVERBUND DISTANCE LEARNING Verfasser: Prof. Dr. Gerold Blakowski Professor für Wirtschaftsinformatik, insbesondere Telekommunikation und Multimedia im Fachbereich Wirtschaft an der Fachhochschule Stralsund Der Studienbrief wurde auf der Grundlage des Curriculums für das Studienfach „Wirtschaftsinformatik“ verfasst. Die Bestätigung des Curriculums erfolgte durch den Fachausschuss „Grundständiges Fernstudium Wirtschaftsingenieurwesen“, dem Professoren der folgenden Fachhochschulen angehörten: HS Anhalt, FHTW Berlin, TFH Berlin, HTWK Leipzig, HS Magdeburg-Stendal, HS Merseburg, HS Mittweida, FH Schmalkalden, FH Stralsund, TFH Wildau und WH Zwickau. 1. Auflage 2007 Redaktionsschluss: März 2007 2007 by Service-Agentur des Hochschulverbundes Distance Learning mit Sitz an der FH Brandenburg. Das Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere das Recht der Vervielfältigung und Verbreitung sowie der Übersetzung und des Nachdrucks, bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. Kein Teil des Werkes darf in irgendeiner Form ohne schriftliche Genehmigung der Service-Agentur des HDL reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden. Service-Agentur des HDL (Hochschulverbund Distance Learning) Leiter: Dr. Reinhard Wulfert c/o Agentur für wissenschaftliche Weiterbildung und Wissenstransfer e. V. Magdeburger Straße 50, 14770 Brandenburg Tel.: 0 33 81 - 35 57 40 E-Mail: [email protected] Fax: 0 33 81 - 35 57 49 Inernet: http://www.aww-brandenburg.de Informatik Algorithmen und Datenstrukturen Inhaltsverzeichnis Randsymbole .............................................................................................................................. 5 Einleitung ................................................................................................................................... 7 Literaturhinweise ....................................................................................................................... 9 1 Algorithmen ............................................................................................................... 10 1.1 Einführung und Zielsetzung ......................................................................................... 10 1.2 Beschreibung eines Algorithmus .................................................................................. 11 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 Anforderungen an Algorithmen.................................................................................... 12 Allgemeinheit .............................................................................................................. 12 Ausführbarkeit ............................................................................................................. 12 Terminierung ............................................................................................................... 12 Finitheit ....................................................................................................................... 13 Determinismus und Determiniertheit............................................................................ 13 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 Entwurfsprinzipien ...................................................................................................... 13 Brute-Force (vollständige Suche) ................................................................................. 13 Backtracking................................................................................................................ 15 Dynamische Programmierung ...................................................................................... 19 Greedy-Algorithmen .................................................................................................... 21 Teile-und-Herrsche-Prinzip.......................................................................................... 23 1.5 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 Komplexität ................................................................................................................. 23 Konstanter Zeitaufwand O(1)....................................................................................... 24 Logarithmischer Zeitaufwand O(log(n)) ....................................................................... 24 Linearer Zeitaufwand O(n)........................................................................................... 26 Polynomialer Zeitaufwand O(nk) .................................................................................. 26 Exponentieller Zeitaufwand O(kn) ................................................................................ 27 Vergleich der Komplexitätsklassen .............................................................................. 28 Zusammenfassung und Aufgaben................................................................................. 29 2 Datentypen und -strukturen ...................................................................................... 29 2.1 Abstrakte Datentypen................................................................................................... 30 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 Stack (Stapel) .............................................................................................................. 31 Operationen des Stacks ................................................................................................ 31 Beispiel ....................................................................................................................... 32 Implementierung.......................................................................................................... 32 Zeitaufwand................................................................................................................. 34 Sonstiges ..................................................................................................................... 34 2.3 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 Queue (Schlange)......................................................................................................... 35 Operationen der Queue ................................................................................................ 35 Beispiel ....................................................................................................................... 36 Implementierung.......................................................................................................... 36 Zeitaufwand................................................................................................................. 38 Sonstiges ..................................................................................................................... 39 Algorithmen und Datenstrukturen Informatik 2.4 List (Liste) ...................................................................................................................39 2.5 2.5.1 2.5.2 2.5.3 2.5.4 ArrayList (Random Access List) ..................................................................................39 Operationen der ArrayList ............................................................................................40 Beispiel ........................................................................................................................41 Implementierung ..........................................................................................................41 Zeitaufwand .................................................................................................................43 2.6 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.6.6 2.6.7 LinkedList (Verkettete Liste)........................................................................................44 Operationen der LinkedList ..........................................................................................44 Beispiel ........................................................................................................................45 Implementierung ..........................................................................................................45 Zugriff mit Index..........................................................................................................49 Zeitaufwand .................................................................................................................49 Betrachtung eines gemeinsamen Abstrakten Datentyps Liste ........................................50 Implementierung eines Stacks mit einer LinkedList ......................................................50 2.7 Iteratoren......................................................................................................................50 2.8 2.8.1 2.8.2 2.8.3 2.8.4 Set (Menge) .................................................................................................................52 Operationen des Sets ....................................................................................................52 Beispiel ........................................................................................................................52 Implementierung ..........................................................................................................53 Zeitaufwand .................................................................................................................55 2.9 2.9.1 2.9.2 2.9.3 2.9.4 Map (assoziatives Array)..............................................................................................55 Operationen der Map....................................................................................................56 Beispiel ........................................................................................................................56 Implementierung ..........................................................................................................56 Zeitaufwand .................................................................................................................57 2.10 2.10.1 2.10.2 2.10.3 Tree (Baum) .................................................................................................................57 Implementierung einer Map mit einem Baum ...............................................................58 Zeitaufwand .................................................................................................................59 Zusammenfassung und Aufgaben .................................................................................59 3 Rekursion und Sortierverfahren................................................................................60 3.1 Rekursion.....................................................................................................................61 3.2 3.2.1 3.2.2 3.2.3 3.2.4 Sortierverfahren ...........................................................................................................65 Sortieren durch Einfügen..............................................................................................66 Mergesort.....................................................................................................................67 Weitere Sortierverfahren ..............................................................................................71 Zusammenfassung und Aufgaben .................................................................................75 4 Java Collections Framework......................................................................................76 4.1 4.1.1 4.1.2 Datentypen in Java .......................................................................................................77 Fundamentale Datentypen ............................................................................................77 Generische Klassen ......................................................................................................79 4.2 4.2.1 4.2.2 4.2.3 Struktur des Java Collections Frameworks....................................................................79 Interfaces des Java Collections Frameworks .................................................................79 Implementierungen im Java Collections Framework .....................................................80 Anwendungsbeispiel Liste............................................................................................83 4 Informatik 4.2.4 4.2.5 4.2.6 4.2.7 Algorithmen und Datenstrukturen Anwendungsbeispiel Map ............................................................................................ 87 Sortieren mit Methoden des Java Collections Frameworks ........................................... 90 Anwendungsbeispiel priorisierte Warteschlange .......................................................... 94 Zusammenfassung und Aufgaben................................................................................. 99 Antworten zu den Kontrollfragen und Lösungshinweise zu den Übungsaufgaben.............. 101 Literaturverzeichnis............................................................................................................... 108 Anhang (Elemente des verwendeten Pseudo-Codes) ................................................................ 109 Sachwortverzeichnis............................................................................................................... 111 Randsymbole B D K M P S Ü Z Beispiel Definition Kontrollfragen Merksatz Programm Studienziele Übungsaufgaben Zusammenfassung 5 Informatik Algorithmen und Datenstrukturen Einleitung Eine grundlegende Aufgabe der Informatik ist die Entwicklung von Programmen zur Lösung von Problemen. Das Problem wird üblicherweise dadurch formuliert, dass zu einer Eingabe die zugehörige Ausgabe festgelegt wird, z. B. Eingabe: a,b ∈ N Ausgabe: c ∈ N mit c ist der größte gemeinsame Teiler ggT von a,b. Vor dem Schreiben des Programms muss der Programmierer ein Verfahren zur Lösung des Problems finden, eine Lösungsidee entwickeln. Diese lässt sich unabhängig von der zugrunde liegenden Programmiersprache als Algorithmus beschreiben. Zur Lösung von verschiedenen Problemstellungen können generalisierte Entwurfsprinzipien für Algorithmen wiederholt angewendet werden, also Lösungsideen wiederverwendet werden. Zur Auswahl von Algorithmen ist in der Praxis die Betrachtung der Komplexität des Algorithmus wichtig. Darunter wird der Zeit- und Speicherplatzaufwand zur Ausführung eines Algorithmus verstanden. In diesem Studienbrief wird in Kapitel 1 zunächst der Begriff Algorithmus präzisiert, es werden dann Anforderungen an Algorithmen behandelt, die die Umsetzbarkeit in real ausführbare Programme sicherstellen. Danach werden ausgewählte häufig verwendete Entwurfsprinzipien anhand von Beispielen erläutert und anschließend die Klassifikation von Algorithmen gemäß ihres Zeitaufwands anhand von Beispielen vorgestellt. Algorithmen operieren auf Daten. Die Daten können dabei sehr einfach strukturiert sein, wie im obigen Beispiel des ggT-Algorithmus mit den natürlichen Zahlen a,b,c als Ein- bzw. Ausgabedaten. Oftmals sind die Daten jedoch komplex strukturiert, z. B. bei einem Algorithmus zum Sortieren einer Liste von Kunden. Dabei stellt sowohl der Kunde mit Kundennummer, Vorname, Nachname etc. als auch die Liste, die eine Sammlung von Kunden umfasst, eine Datenstruktur dar. Eine Datenstruktur kann unabhängig von der zugrunde liegenden Programmiersprache beschrieben werden, und zwar durch Angabe des Typs der gespeicherten Daten, der Operationen auf den Daten mit ihren Einund Ausgabeparametern sowie der Spezifikation, was eine Operation bewirkt. In Kapitel 2 werden häufig benötigte wieder verwendbare Datenstrukturen speziell für Sammlungen von Objekten (Sammlungsdatentypen) dargestellt sowie deren beispielhafte Umsetzung aufgezeigt, inklusive der Algorithmen zur Realisierung der Operationen. Dabei erfolgt auch eine Betrachtung des Zeitbedarfs der Operationen, da dies für die praktische Anwendung von hoher Relevanz ist. Kapitel 3 umfasst zunächst die Erläuterung der Rekursion als wichtiges Entwurfsprinzip für Algorithmen. Bei einer Rekursion wird der Algorithmus wiederholt auf sich selbst angewendet. Zum Beispiel kann die Fakultät der Zahl n, also n!, dadurch berechnet werden, dass man n mit der Fakultät (n −1)! multipliziert, also n! = n*(n − 1)!. Dieses Entwurfsprinzip, welches eine einfache Umsetzung eines Algorithmus für eine Vielzahl von Problemen ermöglicht, wird an Beispielen erläutert. Des 7 Algorithmen und Datenstrukturen Informatik Weiteren werden Algorithmen zum Sortieren von Daten und deren Komplexität bei der Ausführung betrachtet, da das Sortieren eines der am häufigsten in Rechnern ablaufenden Verfahren ist. In den wichtigsten existierenden Programmierumgebungen sind bereits Implementierungen der behandelten Datenstrukturen verfügbar. Am Beispiel des Java Collections Frameworks wird in Kapitel 4 die praktische Verwendung einer solchen Implementierung behandelt. Dies umfasst eine Erläuterung der vorhandenen Datenstrukturen mit ihren verschiedenen zugrunde liegenden Implementierungen und deren moderner programmiersprachlichen Umsetzung mit parametrisierbaren Datentypen, den so genannten generischen Klassen. Des Weiteren wird die Verwendung von Sortierverfahren im Java Collections Framework zur Sortierung von Daten nach verschiedenen Kriterien und die Realisierung einer Nachrichtenwarteschlange auf Basis einer zur Verfügung stehenden Datenstruktur beispielhaft aufgezeigt. Zum Verständnis, insbesondere von Kapitel 4, ist die Kenntnis einer aktuellen objektorientierten Programmiersprache, wie Java oder C# sinnvoll. Eine integrierte Einführung würde den Rahmen des Studienbriefs überschreiten. Verweise auf einführende Literatur und Online-Quellen sind in den Literaturhinweisen aufgeführt. Nach Durcharbeiten des Studienbriefs soll der Leser S • die Anforderungen an und grundlegende Entwurfsprinzipien von Algorithmen kennen, Algorithmen gemäß dieser Prinzipien entwerfen und die Laufzeit von Algorithmen beurteilen können. • das Konzept des abstrakten Datentyps verstehen, die wichtigsten Sammlungsdatentypen kennen, für konkrete Anwendungen gezielt den Sammlungsdatentyp aufgrund der Zugriffstruktur und die Implementierung aufgrund der Zeitanforderungen an die Operationen auswählen können. • die wichtigsten Sortierverfahren mit ihren Randbedingungen und ihrem Zeitaufwand kennen und damit gezielt einsetzen können. • das Prinzip eines Frameworks zur Realisierung der Sammlungsdatentypen und Sortieralgorithmen exemplarisch am Java Collections Framework verstehen und das Java Collections Framework für eigene Anwendungen verwenden können. Zur Vertiefung des Lehrstoffs ist es hilfreich, die Beispiele im Text detailliert nachzuvollziehen und bei den Beispielen aus Kapitel 4 auch ausführen zu lassen und dabei mit dem Java Collections Framework zu experimentieren. Zur Übersetzung und Ausführung der Beispiele benötigen Sie eine Java-Entwicklungsumgebung. Es sind zahlreiche freie Entwicklungsumgebungen für den privaten Gebrauch über das Internet erhältlich, genannt seien Eclipse [4] und NetBeans [7]. Die Programme des Studienbriefs sind unabhängig von der Entwicklungsplattform lauffähig. Vorausgesetzt wird nur das Java Development Kit 5.0 [3]. 8 Algorithmen und Datenstrukturen K Ü Informatik K 2.1 Was umfasst die Spezifikation eines ADTs? K 2.2 Erläutern Sie den zentralen Unterschied zwischen Stack und Queue! K 2.3 Worin unterscheiden sich die Implementierungen von ArrayList und LinkedList im Wesentlichen bezüglich des Zeitverhaltens? Ü 2.1 Zeigen Sie, wie mit der LinkedList eine Queue realisiert werden kann! Ü 2.2 Ergänzen Sie die ArrayList-Implementierung aus Abschnitt 2.5.3 um – eine Operation int indexOf(Object e), die den Index von Element e in der Liste zurückgibt. Falls das Element nicht in der Liste enthalten ist, soll -1 zurückgegeben werden! – eine Operation boolean contains(Object e), die true zurückgibt, wenn das Objekt in der Liste enthalten ist, false andernfalls! Ü 2.3 Skizzieren Sie die Implementierung einer Map mit Hilfe von zwei ArrayLists, wovon eine ArrayList die Keys enthält und die andere die Values. Verwenden Sie die Methode indexOf aus Ü 2.2! Geben Sie den Zeitaufwand für die Implementierung der Operation put (key k, Object e) an! 3 Rekursion und Sortierverfahren In diesem Kapitel wird das wichtige Konzept der Rekursion in Algorithmen und das Sortieren als zentrale, ressourcenaufwändige Aufgabe in vielen Anwendungen behandelt. Das Konzept der Rekursion wird an Beispielen erläutert und es wird der Zeitaufwand für diese Anwendungen diskutiert. Es folgt die Erläuterung der Funktionsweise und die Analyse des Zeitaufwands für ausgewählte Sortieralgorithmen. Der Leser soll nach dem Studium dieses Kapitels S • Rekursion als Entwurfsprinzip verstehen, zur Lösung von Problemen anwenden können sowie beurteilen können, wann der Einsatz sinnvoll ist. • ausgewählte Sortieralgorithmen bezüglich ihrer Eigenschaften verstehen und damit zielgerichtet für eigene Anwendungen auswählen können. • das Teile-und-Herrsche-Entwurfsprinzip verstehen. 60 Informatik 3.1 Algorithmen und Datenstrukturen Rekursion In den bisherigen Beispielen des Studienbriefs wurde die wiederholte Ausführung von Programmelementen durch iterative Schleifen realisiert. Die gesamte Ausführung des Algorithmus ergibt sich dabei aus einer Abfolge von Aufrufen verschiedener Methoden. Bei der Rekursion ruft sich eine Methode selbst wieder auf, wobei die Komplexität bei jedem Aufruf reduziert wird, bis eine Abbruchbedingung für die Rekursion erfüllt wird oder eine vorgegebene Anzahl von Rekursionen durchgeführt wurde. Durch Rekursion können bestimmte komplexe Probleme einfach algorithmisch umgesetzt werden. Fakultät Ein einfaches einführendes Beispiel ist die Berechnung der Fakultät: n ! = n * ( n − 1) * ( n − 2 ) *...*1. Für n = 0 gilt per Definition 0! = 1. Dies ist auch die Abbruchbedingung. Es ist leicht zu erkennen, dass folgende Rekursionsgleichung gilt n * (n − 1)! n! = 1 für n > 0 für n = 0 Daraus ergibt sich unmittelbar folgender Algorithmus: Eingabe: n ∈ Ν Ausgabe: n !∈ Ν P Verfahren: long fakultät(int n) { if (n = = 0) return 1; else return n * fakultät(n-1); } 61 Algorithmen und Datenstrukturen Informatik Bild 3.1 zeigt den Ablauf der rekursiven Berechnung von fakultät(3): Ergebnis: fakultät(3) { … return 3 * fakultät(2); 3*2 } 2*1 fakultät(2) { … return 2 * fakultät(1); } 1*1 fakultät(1) { … return 1 * fakultät(0); } 1 Bild 3.1 fakultät(0) { … return 1; // n == 0 …} Rekursive Berechnung der Fakultät von 3 Der Zeitaufwand hängt linear von der Größe der Zahl n ab. Es werden genau n+1 Aufrufe von fakultät benötigt mit jeweils konstantem Zeitaufwand. Somit beträgt der Zeitaufwand O(n). Türme von Hanoi Ein sehr bekanntes Beispiel für Rekursion ist die Lösung des Türme-vonHanoi-Problems. Die Aufgabe ist, n Steine von aufsteigender Größe von einer Ausgangsplattform auf eine Zielplattform unter Nutzung einer Zwischenplattform umzubauen. Dabei darf jedoch immer nur ein Stein auf einmal bewegt werden und niemals ein größerer auf einem kleineren Stein liegen. Bild 3.2 zeigt die Lösung für n = 3. Betrachtet man die Zwischenschritte genauer, stellt man fest, dass sich das Problem rekursiv lösen lässt. Schritt 1: Schichte zuerst die Steine 1 bis n-1 auf die Zwischenplattform (Bewegung1 bis 3 in der Abbildung). Schritt 2: Schichte Stein n von der Ausgangs- zur Zielplattform (Bewegung 4). Schritt 3: Schichte die Steine 1 bis n-1 von der Zwischenplattform zur Zielplattform (Bewegung 5 bis 7). 62 Informatik Algorithmen und Datenstrukturen Bewegung 1 2 3 Ausgangsplattform 1 Zwischenplattform Zielplattform Zwischenplattform Zielplattform 2 3 Ausgangsplattform 1 2 3 2 1 Ausgangsplattform Zwischenplattform Zielplattform 3 1 2 Ausgangsplattform Zwischenplattform 3 4 Ausgangsplattform Zielplattform 1 2 3 Zwischenplattform Zielplattform 5 1 2 3 Ausgangsplattform Zwischenplattform Zielplattform 6 2 3 1 Ausgangsplattform Zwischenplattform 7 Zielplattform 1 2 3 Ausgangsplattform Bild 3.2 Zwischenplattform Zielplattform Türme von Hanoi für n = 3 Rekursiv lässt sich das Verfahren folgendermaßen formulieren: hanoi(n,Ausgangsplattform,Zielplattform,Zwischenplattform) { P if (n= =1) { Bewege Stein von Ausgangsplattform zur Zielplattform; } else { // Schritt 1: Bewege die Steine 1 bis n-1 von der // Ausgangsplattform // zur Zwischenplattform (unter Nutzung der Zielplattform als // temporäre Zwischenplattform) hanoi(n-1,Ausgangsplattform,Zwischenplattform,Zielplattform); // Schritt 2: Bewege Stein n zur Zielplattform 63 Algorithmen und Datenstrukturen Informatik Bewege Stein n von Ausgangsplattform zur Zielplattform; // Schritt 3: Bewege die Steine 1 bis n-1 von der // Zwischenplattform zur Zielplattform (unter Nutzung der // Ausgangsplattform als temporäre Zwischenplattform) hanoi(n-1,Zwischenplattform,Zielplattform,Ausgangsplattform); } } } Bei jedem Aufruf von Hanoi wird ein Stein bewegt. Zusätzlich wird für n > 1 zweimal Hanoi aufgerufen. Also ergibt sich als Zeitaufwand für Hanoi für n Steine: 1 + 21 + 22 +…+ 2n−1. Der Zeitaufwand ist somit O(2n). Fibonacci-Zahlen An diesem Beispiel soll erläutert werden, dass beim Einsatz von Rekursion neben der einfachen Umsetzung in einen Algorithmus auch der Zeitaufwand zu berücksichtigen ist. Die Fibonacci-Zahlen sind folgendermaßen rekursiv definiert: 0 f (n), n ∈ Ν = 1 f (n − 1) + f (n − 2) für für n=0 n =1 für n≥2 Daraus leitet sich folgender rekursiver Algorithmus ab: P Eingabe: n Ausgabe: f(n) int fib(int n) { if (n<=1) return n; else return fib(n-1) + fib(n-2); } Der Zeitaufwand zur Berechnung von fib(n) ist in dieser Implementierung O(2n), da für die Berechnung von fib(n) eine Berechnung von fib(n-2) und eine Berechnung von fib(n-1) benötigt wird usw. 64 Informatik Algorithmen und Datenstrukturen Bild 3.3 verdeutlicht dies für das Beispiel fib(5). Insgesamt wird 1 * fib(5), 1 * fib(4), 2*fib(3), 3*fib(2), 5*fib(1) und 3*fib(0) berechnet. Der Nachteil dieses rekursiven Algorithmus ist, dass unnötig mehrfache Berechnungen vorgenommen werden, z. B. fib(3) und fib(2). Hier bietet sich als Alternative eine iterative Lösung an, die sukzessive fib(0), fib(1), fib(2) bis fib(n) berechnet und dabei jeweils die letzten zwei Zwischenergebnisse speichert. Damit kann der Zeitaufwand O(n) erreicht werden. fib(5) fib(4) fib(3) + 20 fib(2) + fib(1) fib(3) + fib(2) fib(2) fib(1) + fib(0) fib(1) + fib(0) + fib(1) fib(1) + fib(0) Bild 3.3 Aufrufbaum für rekursive Fibonacci-Implementierung Rekursive Algorithmen sollten immer daraufhin untersucht werden, ob die Zeitkomplexität mit den Rekursionsschritten abnimmt und keine unnötigen mehrfachen Berechnungen vorgenommen werden. 3.2 Sortierverfahren Das Sortieren von Daten ist eine häufig wiederkehrende Aufgabe in Programmen, z. B. das Sortieren von Tabellen nach Namen, Kundennummern etc. Es sind zahlreiche Algorithmen mit unterschiedlicher Laufzeiteffizienz verfügbar. Im Folgenden werden wichtige in der Praxis eingesetzte Algorithmen vorgestellt. Dabei wird exemplarisch davon ausgegangen, dass ein Array a natürlicher Zahlen der Kapazität n mit natürlichen Zahlen aufsteigend sortiert werden soll. 65 Algorithmen und Datenstrukturen 3.2.1 Informatik Sortieren durch Einfügen Beim Sortieren durch Einfügen wird zunächst ein zusätzliches leeres Array s der Länge n angelegt, in das nacheinander a[0] bis a[n-1] in sortierter Reihenfolge eingefügt werden. P Eingabe: int []a: Array der Länge n Ausgabe: int []s: Array der Länge n mit s[i]<=s[j] für 0<=i<j<=n-1 sortierenDurchEinfügen(int[] a, int[] s, int n) { // Sortiere nacheinander die Elemente des Arrays a ein for (int i=0;i<n;i++) { // i-1 Elemente sind bereits sortiert eingefügt int j = i; // Suche jetzt mit Hilfe der Laufvariable j die richtige Position für // das Element a[i]. Beginne den Vergleich mit dem größten bereits // eingefügten Element s[i-1]. while (j>0 && s[j-1] > a[i]) { // Element s[j-1] ist größer als Element a[i] // Dann muss s[j-1] nach s[j] verschoben werden, // um Platz für a[i] zu machen. s[j]=s[j-1]; // Prüfe nächsten Index j--; } // Einfügen von a[i] an der korrekten Position s[j]=a[i]; } } 66 Informatik Algorithmen und Datenstrukturen Bild 3.4 zeigt das Sortieren durch Einfügen an einem Array der Kapazität 5: Array a 4 1 8 5 Array s 3 4 4 4 4 1 1 1 8 8 8 5 5 5 4 3 1 4 1 4 3 8 3 1 4 1 8 5 4 3 5 4 1 Bild 3.4 8 3 8 5 4 8 5 8 Beispiel Sortieren durch Einfügen Der Zeitaufwand von Sortieren durch Einfügen ist O(n2). In der inneren while-Schleife müssen im schlechtesten Fall alle bereits einsortierten Elemente verschoben werden, um Platz für das einzufügende Element zu schaffen. Es ergibt sich somit ein Zeitaufwand von 1 + 2 + 3 +…+ n − 1, also O(n2). Der schlechteste Fall tritt ein, wenn a absteigend sortiert ist. Im besten Fall ist a bereits aufsteigend sortiert, dann beträgt der Zeitaufwand der inneren Schleife immer nur 1, nämlich das Einfügen an der letzten Stelle von s. Insgesamt ist der Zeitaufwand dann O(n). Sortieren durch Einfügen zeigt insgesamt ein sehr gutes Verhalten, wenn die Daten bereits weitestgehend vorsortiert sind, so dass der Zeitaufwand sich O(n) annähert. Sortieren durch Einfügen ist ein stabiles Sortierverfahren, da die Reihenfolge gleicher Elemente beim Sortieren erhalten bleibt. 3.2.2 Mergesort Mergesort ist ein stabiles Sortierverfahren, basierend auf dem Teile-undHerrsche-Entwurfsprinzip: – Zuerst wird das Problem in Teilprobleme zerlegt, die separat gelöst werden. 67 Algorithmen und Datenstrukturen Informatik – Anschließend werden die Teillösungen zu einer Gesamtlösung zusammengesetzt. Ausgangspunkt von Mergesort ist, dass zwei bereits sortierte Folgen a,b mit linearem Zeitaufwand zu einer sortierten Folge merge zusammengefügt werden: P Eingabe: int []a: Array der Länge n mit a[i]<=a[j] für 0<=i<j<=n-1 int []b: Array der Länge n mit b[i]<=b[j] für 0<=i<j<=n-1 Ausgabe: int merge[]: Array der Kapazität 2n mit merge[i]<=merge[j] für 1<=i<j<=2n und {a[0],..., a[n-1], b[0],.., b[n-1]} = {merge[0],..., merge[n-1]} Algorithmus: merge(int[] a, int[] b) { int[] merge = new int[2*n]; // Merge-Array erzeugen int index_a = 0; // Position in a int index_b = 0; // Position in b int index_merge = 0; // Position in merge // while (index_a<n && index_b<n) { // Solange a oder b noch nicht komplett durchlaufen sind if (a[index_a]<b[index_b]) { // Element aus a kleiner, deshalb in merge einsortieren merge[index_merge]=a[index_a]; index_a++; // nächstes Element aus a betrachten index_merge++; } else { // Element aus b kleiner, deshalb in merge einsortieren merge[index_merge]=b[index_b]; index_b++; // nächstes Element aus b betrachten index_merge++; } } // Jetzt noch die restlichen Elemente aus a und b einsortieren for (;index_a<n;index_a++) { merge[index_merge++] = a[index_a]; } 68 Informatik Algorithmen und Datenstrukturen for (;index_b<n;index_b++) { merge[index_merge++] = b[index_b]; } return merge; } Die Sortierung in Mergesort erfolgt dadurch, dass – die Hälften eines zu sortierenden Arrays jeweils getrennt sortiert werden und – dann mittels Merge zu einer sortierten Folge zusammengefügt werden. Eingabe: int []a: Array der Länge n, int start, int ende P Ausgabe: int []a: Array der Länge n mit a[i]<=a[j] für start<=i<j<=ende mergesort(int[]a,int start,int ende) { // start und ende sind die Indizes, die den Bereich im Array // angeben, der sortiert werden soll if (start>=ende) return; // Ende der Rekursion int mitte = (start+ende) / 2; // Array teilen mergesort(a,start,mitte); // Linken Bereich sortieren mergesort(a,mitte+1,ende); // Rechten Bereich sortieren // Die Bereiche wieder zusammenmischen mit einer // Variante von merge, die a[start,…,mitte] mit // a[mitte+1,..,ende] vermischt. merge(a,start,mitte,ende); } Bild 3.5 zeigt die Sortierung eines Arrays der Länge 7. Zunächst wird rekursiv das Array immer weiter zerlegt und dann wieder Stück für Stück mittels merge zusammengefügt: 69 Algorithmen und Datenstrukturen Informatik ms(0,6) Halbieren 11 4 1 28 15 3 ms(0,3) 11 4 ms(4,6) 1 28 ms(0,1) 11 4 1 ms(1,1) 11 4 1 11 ms(4,5) 28 ms(2,2) 1 3 15 ms(6,6) 3 9 ms(3,3) ms(4,4) ms(5,5) 28 15 3 28 3 9 15 Merge 4 15 ms(2,3) ms(0,0) 9 1 4 1 Bild 3.5 11 28 3 4 3 9 11 15 9 15 28 Beispiel für Mergesort Beim ersten Aufruf von Mergesort entsteht der Zeitaufwand für das Min schen der sortierten halben Arrays, also 2 ⋅ = n Auf der zweiten Ebene 2 zweimal der Zeitaufwand des Mischens der jeweiligen Viertel der Arrays n 4 ⋅ = n etc. Der Zeitaufwand entspricht somit der Anzahl der Aufruf4 ebenen i. i ist beschränkt auf log(n). Insgesamt ergibt sich daraus ein Worst-Case-Zeitaufwand von O(n*log(n)). Bei Verfahren, die wie Mergesort auf einem Vergleich von Elementen beruhen, ist O(n*log(n)) auch die theoretische untere Schranke für die Worst-Case- und Average-Case-Zeitkomplexität. Mergesort kann optimiert werden, indem bei kleinen zu sortierenden Arrays unterhalb eines Schwellwertes statt Mergesort Sortieren durch Einfügen verwendet wird, was bei kleinen Arrays effizienter ist als die rekursiven Aufrufe bei Mergesort. Zudem kann vor dem Mischen zuerst geprüft werden, ob das größte Element des linken Teilarrays kleiner ist als das kleinste Element des rechten Teilarrays. In diesem Fall kann das Mischen unterbleiben, da bereits eine Sortierung vorliegt. 70 Informatik Algorithmen und Datenstrukturen mergesort(int[]a,int start,int ende) { // optimiert P // start und ende sind die Indizes, die den Bereich im Array // angeben, der sortiert werden soll if (ende-start < schwellwert) { sortieren_durch_einfügen(a,start,ende); return; } int mitte = (start+ende) / 2; // Array teilen mergesort(a,start,mitte); // Linken Bereich sortieren mergesort(a,mitte+1,ende); // Rechten Bereich sortieren if (a[mitte]<a[mitte+1]) return; // Linker und rechter Bereich zusammen schon sortiert // Die Bereiche wieder zusammenmischen mit einer // Variante von merge, die a[start,…,mitte] mit // a[mitte+1,..,ende] vermischt. merge(a,start,mitte,ende); } 3.2.3 Weitere Sortierverfahren Quicksort und Bucketsort sind zwei weitere bekannte Sortieralgorithmen, die hier kurz betrachtet werden sollen. Quicksort Bei Quicksort wird zuerst ein Element p aus dem zu sortierenden Array ausgewählt. Das Array wird im nächsten Schritt so umsortiert, dass p an der gemäß der Endsortierung richtigen Position platziert wird, alle anderen Elemente mit einem kleineren Wert p unterhalb von p platziert werden und alle größeren Elemente oberhalb. Dieser Vorgang wird rekursiv wiederholt, bis das gesamte Array sortiert ist. 71 Algorithmen und Datenstrukturen P Informatik Eingabe: int []a: Array der Länge n, int start, int ende Ausgabe: int []a: Array der Länge n mit a[i]<=a[j] für start<=i<j<=ende quicksort(int a[],int start,int ende) { if (ende-start >=1) { int index = start; // Jetzt umsortieren der Elemente im Array for (int zeiger = start; zeiger < ende; zeiger++) { if (a[zeiger] <= a[ende]) { // a[zeiger] kleiner als Element am Ende! // Dieses mit größerem Element auf // kleinerem Index vertauschen. tauscheWerte(index,zeiger); index++; // index-1 entspricht Anzahl Elemente // kleiner gleich a[ende] } } // Jetzt das Element a[ende] in die // endgültige Position bringen tauscheWerte(index,ende); quicksort(a,start,index-1); quicksort(a,index+1,ende); } } 72 Informatik Algorithmen und Datenstrukturen Bild 3.6 zeigt die Positionierung des letzten Elements des Arrays an die zugehörige Position in der endgültigen Sortierung: 3 9 7 2 5 Index 9>5 3 9 7 2 Zeiger 5 3<5, tauschen Wert Index,Zeiger 3 9 7 2 5 7>5 2 3 7 9 5 2<5, tauschen Wert Index,Zeiger 3 2 7 9 5 tauschen Wert Index,Ende 2 3 5 9 7 5 ist richtig platziert, alle Elemente links davon sind kleiner, alle Elemente rechts davon größer Bild 3.6 Beispiel des Positionierens des letzten Elements Quicksort gilt als sehr schnelles Verfahren mit einem AverageZeitaufwand von O(n*log(n)), allerdings hat es einen Worst-CaseZeitaufwand von O(n2), z. B. auf bereits sortierten Listen. Quicksort ist nicht stabil. Bucketsort Im Gegensatz zu den vorher genannten Verfahren, kann mit Bucketsort ein Average-Zeitaufwand von O(n) erreicht werden. Hierzu muss die Voraussetzung erfüllt sein, dass die Werte der zu sortierenden Elemente gleichverteilt in einem Intervall [unten,oben] liegen. Es wird ein zusätzliches Array b (Bucket-Array) der Länge n angelegt, das das Intervall [unten,oben] in b-gleichgroße Intervalle unterteilt, wobei jedes b[i] mehrere Elemente aufnehmen kann, z. B. in einer Liste. Im folgenden Beispiel werden n reale Zahlen aus dem Intervall [0,..,n) sortiert. b[i] repräsentiert das Intervall [i,i+1). 73 Algorithmen und Datenstrukturen P Informatik Eingabe: float a[]: Array der Länge n Ausgabe: float a[]: Array der Länge n mit a[i]<=a[j] für 0<=i<j<n bucketsort(float a[],int n) { // Bucket-Array initial jeweils mit leerer Liste Liste [] b = new Liste[n]; // b[i] sei leere Liste for (int i=0;i<n;i++) { // Schritt 1 // Füge Elemente in passenden Bucket b[(int)a[i]].add(a[i]); } for (int i=0;i<n;i++) { // Schritt 2 SortiereListe(b[i]); // anderes Sortierverfahren } for (int i=0;i<n;i++) { // Schritt 3 b[i] auslesen und sortiert in a eintragen } } Bild 3.7 zeigt ein Beispiel für das Sortieren mit n = 5: Zu sortierendes Array 3,6 1,5 4,6 2,2 4,3 Elemente in Bucket-Array einsortieren [0,...,1) [1,...,2) 1,5 [2,...,3) 2,2 [3,...,4) 3,6 [4,...,5) 4,6 4,3 ggf. Listen in Bucket-Array sortieren [4,...,5) 4,3 4,6 Elemente sortiert aus Bucket-Array auslesen 1,5 Bild 3.7 74 2,2 3,6 Beispiel für Sortieren mit Bucketsort 4,3 4,6 Informatik Algorithmen und Datenstrukturen Der Worst-Case-Zeitaufwand tritt ein, wenn alle Elemente aus a im gleichen Bucket b[i] abgelegt werden. Dann entspricht der Zeitaufwand dem des eingesetzten Sortierverfahrens in der zweiten Schleife. Der AverageCase-Zeitaufwand beträgt jedoch aufgrund der angenommenen Gleichverteilung O(n). 3.2.4 Zusammenfassung und Aufgaben Die Verwendung von Rekursion wurde an drei Beispielen vorgestellt: Fakultät als einfaches einführendes Beispiel, die Türme von Hanoi als Beispiel für eine Anwendung der Rekursion, die eine einfache algorithmische Umsetzung ermöglicht, bei der die Komplexität in jedem Rekursionsschritt reduziert wird und ein bestmöglicher Zeitaufwand erreicht wird. Die rekursive Version der Fibonacci-Zahlen ist ein Beispiel für eine Anwendung, bei der der Zeitaufwand gegen eine rekursive Lösung spricht. Z Bei den Sortierverfahren wurden die Funktionsweise, der Zeitaufwand und Randbedingungen für den Einsatz für die Verfahren Sortieren durch Einfügen, Mergesort, Quicksort und Bucketsort erläutert (s. Tabelle 3.1). Mergesort und Quicksort sind zudem Beispiele für rekursive Teile-undHerrsche-Algorithmen. Tabelle 3.1 Zeitaufwand der Sortierverfahren Sortierverfahren Worst-CaseZeitaufwand 2 Average-CaseZeitaufwand 2 Sortieren durch Einfügen O(n ) O(n ), aber gut bei vorsortierten Folgen Mergesort O(n*log(n)) O(n*log(n)) Quicksort O(n ) bei vorsortierten Folgen O(n*log(n)) Bucketsort (mit Mergesort zur Sortierung der Buckets) O(n*log(n)) O(n) bei Gleichverteilung der Elemente 2 K 3.1 Was wird unter Rekursion verstanden? K 3.2 Erläutern Sie, wann der Einsatz jeweils von Sortieren durch Einfügen, Mergesort und Bucketsort günstig ist! Ü 3.1 Formulieren Sie einen rekursiven Algorithmus, der feststellt, ob ein Wort ein Palindrom ist, d. h. von vorne und hinten gelesen gleich ist, z. B. Rentner, Reittier! Ü 3.2 Skizzieren Sie einen iterativen Algorithmus zur Berechnung von fib(n)! Ü 3.3 Vollziehen Sie schriftlich die Sortierung der Zahlenfolge 6,3,8,2,7,5 bei den Algorithmen Sortieren durch Einfügen, Mergesort und Quicksort in den einzelnen Schritten nach! K Ü 75