Algorithmen und Datenstrukturen I
Transcrição
Algorithmen und Datenstrukturen I
1 Algorithmen und Datenstrukturen I von Peter Naeve Statistik und Informatik Fakultät für Wirtschaftswissenschaften Universität Bielefeld c Naeve, Version 2.0, Wintersemester 1999/2000 P. Vorwort Die nachstehenden Kapitel sind in der Regel schon ein wenig mehr als verkleinerte Kopien der in der Vorlesung gezeigten Folien. Insbesondere durch viele gliedernde Überschriften wurde versucht, sie in das Gerüst einer sinnvollen Struktur innerhalb eines Kapitels zu bringen. Das führt zur Zeit noch dazu, daß der eine oder andere Gliederungspunkt leer erscheint. An einigen Punkten sprießen aber schon spärlich erklärende oder interpretierende Ausführungen. Hier erwies es sich — zumindest für den Verfasser — als sinnvoll, erst einmal die Formeln hinzuschreiben. Die jeweiligen erläuternden Texte zu den Formeln hat ein Teil der Leser vielleicht ja noch aus der Vorlesung im Ohr. Das was hier vorgelegt wird, ist also sicher immer noch nur ein Torso eines Vorlesungsskriptes. Der Autor weiß dies nur zu gut. Im Vorwort einer früheren Version hatte er geschrieben: Die Versionsnummer 1.0 ist aber doch so etwas wie eine Geburtsanzeige. Anders als im richtigen Leben muß man aber das Kind nicht so annehmen wie es ist. Der Autor ist für Verbesserungsvorschläge dankbar. Leider sind bei vorhergehenden Ausgaben diese nur sehr spärlich eingegangen. Der Autor versichert, daß er keinen Hinweis auf einen noch so kleinen Fehler persönlich nehmen wird. Nun tritt das Skript in der Version 2.0 auf. Dieser Quantensprung in der Versionsnummer ist gerechtfertigt durch die vielen kritischen Anmerkungen von Peter Wolf, der im WS 97/98 dem Autor durch die Übernahme der Veranstaltung Algorithmen und Datenstrukturen erst ein Freisemester ermöglichte. Wie es seine Art ist, brachte er seine Kritik gleich immer in der Gestalt von Verbesserungsvorschlägen vor, die ich oft direkt als Formulierung übernehmen konnte. Aber nach wie vor ist vieles zu verbessern. Die studentischen Leser sollten sich immer noch eingeladen fühlen, auch die kleinste Anmerkung beim Autor anzubringen. Wem der Weg in den 9. Stock zu weit ist, kann sich auch per email äußern. [email protected] Bisher ist noch keiner erschienen. So habe ich — im November 98 — selber einige winzige Verbesserungen angebracht. Bis heute ist es im Wesentlichen bei diesem Zustand geblieben. Peter Naeve Bielefeld, November 2000 Contents 1 Einführung 1.1 Viele Fragen — gibt’s auch Antworten? . . . . . . . . . . 1.2 Algorithmus und Notation . . . . . . . . . . . . . . . . . . 1.2.1 Definitionen . . . . . . . . . . . . . . . . . . . . . . 1.2.2 Notationen . . . . . . . . . . . . . . . . . . . . . . 1.3 Ein Problem und viele Algorithmen und Datenstrukturen 1.3.1 Weißt du wieviel Bitlein . . . . . . . . . . . . . . . . 1.3.2 Komplex Komplexe . . . . . . . . . . . . . . . . . 1.3.3 Ein Überdeckungsproblem . . . . . . . . . . . . . . 1.3.4 Es gibt viele Wege zu einer fairen Teilung . . . . . 1.4 Eine magische Aufgabe . . . . . . . . . . . . . . . . . . . . 1.5 Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Algorithmen und Effizienz 2.1 Laufzeitverhalten . . . . . . . . . . . . . 2.1.1 Das O()-Konzept . . . . . . . . . 2.1.2 Regeln . . . . . . . . . . . . . . . 2.2 Verbesserung des Laufzeitverhalten . . . 2.2.1 Das Problem . . . . . . . . . . . 2.2.2 Nenn’ nur die (Element-)Summe 2.2.3 Die Lehren . . . . . . . . . . . . 2.2.4 Und wo ist nun der Teilvektor? . 2.3 Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 2 2 3 3 5 6 8 14 15 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 17 18 18 18 18 22 22 24 3 Sortieren 3.1 Einführung . . . . . . . . . . . . . . . . . . . . . 3.1.1 Sortieren tut Not . . . . . . . . . . . . . . 3.1.2 Sortierprinzipien . . . . . . . . . . . . . . 3.1.3 Bedingungen . . . . . . . . . . . . . . . . 3.1.4 Vorgehen . . . . . . . . . . . . . . . . . . 3.2 Algorithmen fürs Sortieren durch Auswahl . . . . 3.2.1 Lösungsskizze . . . . . . . . . . . . . . . . 3.2.2 Der Algorithmus . . . . . . . . . . . . . . 3.3 Algorithmen fürs Sortieren durch Einfügen . . . 3.3.1 Lösungsskizze . . . . . . . . . . . . . . . . 3.3.2 Ein erster Algorithmus . . . . . . . . . . . 3.3.3 . . .und seine Verbesserungen . . . . . . . . 3.3.4 Shell-Sort . . . . . . . . . . . . . . . . . . 3.4 Algorithmen fürs Sortieren durch Zählen . . . . . 3.5 Sortieren ohne Vergessen der Ausgangsanordnung 3.5.1 Lösung durch Duplizieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 25 25 27 27 27 27 27 28 28 28 28 29 31 31 32 32 . . . . . . . . . 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 3.5.2 Behalte die ursprünglichen Indizes . . . . Beurteilung von Sortieralgorithmen . . . . . . . . 3.6.1 Vergleiche . . . . . . . . . . . . . . . . . . 3.6.2 Bewegungen . . . . . . . . . . . . . . . . . Algorithmen fürs Sortieren durch Austausch . . . Algorithmen fürs Sortieren durch Mischen . . . . 3.8.1 Zur Analyse von Algorithmen am Beispiel Exkurs: Bandsortieren . . . . . . . . . . . . . . . Algorithmen fürs Sortieren durch Verteilen . . . Quicksort . . . . . . . . . . . . . . . . . . . . . . 3.11.1 Die Zerlegung . . . . . . . . . . . . . . . . 3.11.2 Eine erste Lösung . . . . . . . . . . . . . 3.11.3 . . .und ihre rekursive Fassung . . . . . . . Heap-Sort . . . . . . . . . . . . . . . . . . . . . . 3.12.1 Definition eines Heap . . . . . . . . . . . 3.12.2 Die Idee . . . . . . . . . . . . . . . . . . . 3.12.3 . . . und ihre Umsetzung . . . . . . . . . . 3.12.4 Reparatur eines Heap – die Funktion sift 3.12.5 . . . und ein Beispiel . . . . . . . . . . . . . Exkurs: Bandsortieren . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . von mergesort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Korrekte Algorithmen durch formale Spezifikation 4.1 Zwei Wege zu Funktionen . . . . . . . . . . . . . . . . 4.1.1 Explizite Spezifikation . . . . . . . . . . . . . . 4.1.2 Implizite Spezifikation . . . . . . . . . . . . . . 4.1.3 Beweisnot . . . . . . . . . . . . . . . . . . . . . 4.1.4 Ein einfaches Beispiel . . . . . . . . . . . . . . 4.2 Von der Funktion zum Programm . . . . . . . . . . . . 4.2.1 Die beliebte Fakultät . . . . . . . . . . . . . . . 4.2.2 Die Fakultät einmal anders betrachtet . . . . . 4.3 und zu den Operationen . . . . . . . . . . . . . . . . . 4.3.1 Der Formalismus . . . . . . . . . . . . . . . . . 4.3.2 Ein altes Beispiel . . . . . . . . . . . . . . . . . 4.3.3 Hoare Tripel . . . . . . . . . . . . . . . . . . . 4.4 Beweisregeln . . . . . . . . . . . . . . . . . . . . . . . 4.5 Die korrekte Fakultät . . . . . . . . . . . . . . . . . . 4.6 Wann ist ein Quadrat ein Palindrom? . . . . . . . . . 4.7 Der Beginn des Versuches einer Rechtfertigung . . . . 4.7.1 Die Wut um den verlorenen Groschen . . . . . 4.7.2 Updating A Sequential File . . . . . . . . . . . 4.7.3 Sprache . . . . . . . . . . . . . . . . . . . . . . 4.7.4 Die Struktur des Problems . . . . . . . . . . . 4.7.5 Die Herren hätten den Groschen auch verloren 4.8 Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 33 33 35 36 37 41 42 43 43 43 44 45 45 45 46 46 47 48 48 49 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 51 51 52 53 53 54 54 55 55 55 56 56 56 57 57 59 59 61 63 64 65 65 0 Einführung Chapter 1 Einführung Inhaltsangabe 1.1 1.2 Viele Fragen — gibt’s auch Antworten? . . . . . . . . . . . . . . . . . . . Algorithmus und Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.1 Definitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.2 Notationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Ein Problem und viele Algorithmen und Datenstrukturen . . . . . . . . 1.3.1 Weißt du wieviel Bitlein . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.2 Komplex Komplexe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.3 Ein Überdeckungsproblem . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.4 Es gibt viele Wege zu einer fairen Teilung . . . . . . . . . . . . . . . . . . . 1.4 Eine magische Aufgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5 Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1 1 2 2 2 3 3 5 6 8 14 15 Viele Fragen — gibt’s auch Antworten? • Was ist ein Algorithmus? • Was ist eine Datenstruktur? • Wie schreibt (notiert) man einen Algorithmus auf? • Wie schreibt (notiert) man eine Datenstuktur auf? • Wie findet man einen Algorithmus? • Was macht ein vorgelegter Algorithmus? • Wie findet man die geeignete Datenstruktur? • Wie beweist man die Korrektheit eines Algorithmus? • Wie mißt man das Verhalten (Zeitverbrauch, Platzbedarf) eines Algorithmus? • Wie macht man den Algorithmus effizient? • Wie implementiert man einen Algorithmus auf einem bestimmten Rechner, in einer bestimmten Sprache? 1 2 Einführung Hoffentlich ist der Leser am Ende der Lektüre überzeugt, daß sich tatsächlich auch Antworten zu den Fragen in diesem Text finden lassen. Vor einem sei er aber gleich gewarnt. Der Verfasser glaubt nicht, daß es zu jeder Frage “die Antwort” gibt. Man kann die Inhalte — ohne damit eine Reihenfolge festzulegen — auch wie folgt auflisten. • Algorithmen, deren Kenntnis ein Muß ist. • Datenstrukturen, deren Kenntnis ein Muß ist. • Beurteilung von Algorithmen. • Entwurfsprinzipien für Algorithmen. • Korrektheitsbeweis für Algorithmen. • Algorithmen und Hardware-Architektur. • Notation für den Entwurf von Algorithmen und Datenstrukturen. Wenden wir uns als erstes der Notationsproblematik zu. 1.2 Algorithmus und Notation 1.2.1 Definitionen Was ist eigentlich ein Algorithmus? Hat es etwas zu bedeuten, daß in einem vor kurzem auf dem Markt gekommenen Lexikon der Informatik, das sich speziell an Studenten wendet, kein Eintrag unter Algorithmus findet? Sollten wir das restliche Papier doch lieber für wichtigere Dinge verwenden? Ich glaube nicht. Beginnen wir daher mit einer Definition, die auf Dijkstra [2] zurückgeht. Definition 1: Algorithmus An algorithm is the description of a pattern of behavior, expressed in terms of a well-understood, finite repertoire of named (so called “primitive”) actions of which it is assumed a priori that they can be done (i. e. can be caused to happen). △ Ein suchender Blick in die Literatur bringt sicher weitere Definitionsversuche ans Licht. Für unsere Zwecke wollen wie uns aber mit dieser Definition von Dijkstra begnügen. Warum? Sie gefällt dem Verfasser am besten und erfüllt ihren Zweck, wie die Leser unschwer am Ende des Skriptes erkannt haben werden. 1.2.2 Notationen In der Literatur wurden bereits viele Algorithmen aufgeschreiben. Schaut man sie sich näher an, dann sieht man, daß die Autoren eine Vielzahl von Aufschreibformen ge(er)funden haben. Betrachten wir einmal den berühmten Euklidischen Algorithmus [3]. 1. Euklidischer Algorithmus wie in den Elementen notiert. ([3]: Siebentes Buch §2 (A.1)) 2. Euklidischer Algorithmus wie von Knuth [4] notiert. Algorithmus A: Euclid’s Algorithm Given two positive integers m and n, find their greatest common divisor, i.+e., the largest positive integer which evenly divides both m and n. A1 : [Find remainder.] Divide m by n and let r be the remainder. (We will have 0 ≤ r < n.) Einführung 3 A2 : [Is it zero?] If r = 0, the algorithm terminates; n is the answer. A3 : [Interchange.] Set m ← n, n ← r, and go back to A1. 3. Euklidischer Algorithmus in Pseudocode. Algorithmus 1: [1] [2] [3] [4] [5] Euclid’s Algorithm : while n > 0 do : rem ← m mod n : m←n : n ← rem : gcd ← m Die nachstehende Fassung macht klar, warum wir von einem Pseudocode sprechen. Mit wenigen Zusätzen ist der Algorithmus in korrektes Pascal gebracht. Man braucht nur statt “ ← ” ein “:=”zu lesen. Die in dieser Sprache so geliebten “;” als explizite Delimiter wurden weggelassen. 4. Euklidischer Algorithmus in Pascal. Algorithmus 1: Euclid’s Algorithm [1] : function gcd(m,n: integer ): integer [2] : var rem: integer [3] : begin [4] : while n > 0 do [5] : begin [6] : rem ← m mod n [7] : m←n [8] : n ← rem [9] : end [10] : gcd ← m [11] : end Wir werden aber nicht Pascal im Hinterkopf haben, C täte es auch. Da das Programmieren eines konkreten Computers nicht im Mittelpunkt steht, benutzen wir eben einen Pseudocode. In diesen formulieren wir, was uns wichtig erscheint, ohne uns von der Syntaxbesessenheit eines Compilers verrückt machen zu lassen. 1.3 1.3.1 Ein Problem und viele Algorithmen und Datenstrukturen Weißt du wieviel Bitlein . . . Aufgabe 1: Gebe die bit-Summe (Anzahl der auf 1 gesetzten bits) in einem Wort der Länge n an. ♥ 4 Einführung Antwort 1: Das “ finite repertoire of named (so called “primitive”) actions” enthalte: • Die “bürgerliche” Addition, geschrieben als “+”. • Den Vergleich, geschrieben als “=”. • Die Zuweisung, geschrieben als “ ← ”. • Den Zugriff über einen Index. • Kontrollstrukturen wie for . . . to . . . do und if . . . then . Mehr oder weniger explizit wird eine Datenstruktur des Types “Reihe” (Anton lebe hoch) unterstellt. Darstellung: Knuth style Algorithmus B: BS (Bit-Summe) Dieser Algorithmus berechnet die Bit-Summe — Anzahl der gesetzten Bits — von n Bits. Die Bits stehen auf den Positionen b1 ,. . . ,bn . Die Summe wird auf s abgelegt. B1 : [Initialisierung] s ← 0 B2 : [Summation] Prüfe für alle i von 1 bis n, ob bi =1, wenn ja, so erhöhe s um 1. Darstellung: Pseudocode Algorithmus 2: BS (Bit-Summe) [1] : s ← 0 [2] : for i ← 1 to n do [3] : if bi = 1 then s ← s + 1 Bemerkungen: • naheliegend • n Vergleiche • Addition wird nur ausgeführt für gesetzte bits • man muß auf die bits zugreifen können Was wäre von der folgenden Konstruktion in Zeile [3] des obigen Algorithmus zu halten? Variation: Algorithmus 1.2 [3] : s ← s+bi Was ist bi ? bi = true bzw. false bi = 1 bzw. 0 Steht in der zweiten Interpretation der Addition Tür und Tor offen? Einführung 5 Antwort 2 Gegeben sei die logische Operation ∧, die parallel auf den einzelnen bit-Positionen arbeitet. Wir erweitern oder verändern — wie man es sehen will — also das bekannte Repertoire. B steht im Folgendem für die Bitfolge. Algorithmus 3: Lösung 2 [1] : s←0 [2] : while B 6= 0 do [3] : begin [4] : s←s+1 [5] : B ← B ∧ (B − 1) [6] : end Wie man sieht, wird nur mit Skalaren hantiert. Die Reihe der “bits” taucht nur implizit auf. Die Handlung ∧ setzt ihre Existenz zwar voraus, wir müssen aber nicht mit ihr Umgehen, d.h., auf ein bit zugreifen können. Antwort 3: Bilde Tabelle mit allen Möglichkeiten. Siehe nachstehendes Beispiel für ein Wort der Länge 4 bit. Wort 0000 0010 0100 0110 1000 1010 1100 1110 bit-Summe 0 1 1 2 1 2 2 3 Wort 0001 0011 0101 0111 1001 1011 1101 1111 bit-Summe 1 2 2 3 2 3 3 4 Bemerkungen 1. Nur die 2n bit-Summen müssen gespeichert werden, wenn es möglich ist, das bit-Muster als Adresse zu interpretieren. Wie macht man dies? 2. Schneller Algorithmus aber großer Verbrauch von Speicherplatz. 1.3.2 Komplex Komplexe Aufgabe 2: Multiplikation zweier komplexer Zahlen z1 und z2 Die Lösung hängt ab von der Darstellung einer komplexen Zahl. ♥ 6 Einführung Komplexe Zahl in Kartesischen Koordinaten ℑ(z) 6 α β ℜ(z) ? Dabei ist z = (ℜ(z), ℑ(z)) = (α, β). Sei z1 = (α1 , β1 ), z2 = (α2 , β2 ), z3 = (α3 , β3 ) und z3 = z1 × z2 , dann gilt: α3 = α1 × α2 − β1 × β2 β3 = α1 × β2 + β1 × α2 . Komplexe Zahl in Polarkoordinaten ℑ(z) 6 q |z| arc(z) ℜ(z) ? Dabei ist z = (|z|, arc(z)). Sei z1 = (|z1 |, arc(z1 )), z2 = (|z2 |, arc(z2 )), z3 = (|z3 |, arc(z3 )) und z3 = z1 × z2 , dann gilt: |z3 | = |z1 | × |z2 | arc(z3 ) = arc(z1 ) + arc(z2 ). 1.3.3 Ein Überdeckungsproblem Das Problem und seine Präzisierung Aufgabe 3: Überdecke die nachstehende große Figur (ein Quadrat mit einer Kantenlänge von 8 Einheiten, bei dem an zwei gegenüberliegenden Ecken je ein Quadrat von 1 Einheit Kantenlänge entfernt wurde) mit möglichst wenig kleinen Figuren (ein Rechteck mit den Kantenlängen 1 und 2 Einheiten). ♥ Einführung 7 Es sieht so aus, als ob man mit 31 Plättchen auskommen kann, denn die große Figur hat eine Fläche von 64 − 2 = 62 Einheiten2 und die kleine Figur eine Fläche von 2 Einheiten2 . Wir wollen die Aufgabe präzisieren. Offensichtlich kann man die Minimalzahl 31 nur erreichen, wenn sich die kleinen Figuren nicht überlappen. Aufgabe 4: Wie muß man die 31 kleinen Figuren auf der großen Figur so plazieren, daß diese vollständig überdeckt ist. ♥ Der Lösungsbaum und seine vollständige Enumeration Damit die Plättchen ”‘ordentlich”’ hingelegt werden können, geben wir der großen und der kleinen Figur ein Raster. Wir können unser systematisches Vorgehen in dem nachstehenden `P X P `X rH X `X ``` ! @H P PX ``` H XX ! @ H r r r r rr r r rr r r r r r r r! rX r r r r r@ r r r rH rP r rP rP r r rX rX r r` r r` rr X P H P !@HP XX X P X H ! rrr r r r r r r r r r r! r r r r r@ r@ r r rH rH rP r rP rP rX r rX rX rr Baum organisieren. Kein Plättchen Ein Plättchen Zwei Plättchen Bemerkung: Der Aufbau eines Teilbaumes wird abgebrochen, wenn alle 31 Plättchen verbraucht oder kein weiteres Plättchen mehr gelegt werden kann. Ehe wir in die Einzelheiten des Algorithmus gehen, der uns den vollständigen Baum aufbaut bzw. nach einer Lösung durchsucht, sollten wir noch einmal aus anderer Perspektive auf das Problem sehen, nicht aber ohne bereits einmal das Zauberwort “Backtracking” fallen gelassen zu haben. Der Zauber wird hoffentlich irgendwann einmal gelöst. Sieh da, ein Schachbrett Markiere die große und die kleine Figur wie folgt. 8 Einführung @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ Zähle die markierten und die unmarkierten Felder: 32 markierte Felder 30 unmarkierte Felder. Zur Abdeckung steht immer ein Paar markiertes, unmarkiertes Feld zur Verfügung. Die Aufgabe läßt sich also gar nicht lösen. 1.3.4 Es gibt viele Wege zu einer fairen Teilung Aufgabe 5: Zwei Freunde erhalten 8l Wein in einem Gefäß, das genau 8l faßt. Sie wollen den Wein unter sich zu gleichen Teilen aufteilen. Ihnen stehen außer dem 8l-Gefäß nur noch zwei Gefäße mit einem Fassungsvermögen von 5l bzw. 3l zur Verfügung. Wie sollten sie vorgehen? ♥ Trial and Error Wohl kaum einer wird der Versuchung widerstehen, schnell und unsystematisch einen Lösungsversuch nach der Methode “Trial and error” zu probieren. Auf einem Zettel — nicht ganz so ordentlich wie hier — wird er notieren. Er wird aber hoffentlich sich ein Minimum an Notation einfallen lassen, z.B.: • Gef. I: das Gefäß, das maximal 8 l faßt. • Gef. II: das Gefäß, das maximal 5 l faßt. • Gef. III: das Gefäß, das maximal 3 l faßt. Außerdem wird er durch Nachfragen erfahren haben, daß ein Gefäß immer entweder ganz ausgegossen oder ganz gefüllt werden muß. Tätigkeit 3l 5l 3l 3l 3l 2l 1l 5l 1l 3l von von von von von von von von von von I → III I → II III → I II → III III → I II → III I → III I → II II → III III → I Gef. I 8 5 0 3 3 6 6 5 1 1 4 Gef. II 0 0 5 5 2 2 0 0 5 4 4 Gef. III 0 3 3 0 3 0 2 3 2 3 0 Bemerkung nicht, war schon mal, (Schritt 1) wieder bei (6,0,2) aufgesetzt geschafft Ist es aber auch eine gute, sprich schnelle Lösung, die wir gefunden haben? Einführung 9 Wer sieht den Baum? Auch dieses Problem läßt sich auf einen Baum zurückführen und dann mit Hilfe des Zauberwortes “Backtracking” lösen. Dazu vereinbaren wir die folgende Notation: gebe den Inhalt der 3 Gefäße als Trippel an, Position 1 für das 8l-Gefäß, Position 2 für das 5l-Gefäß. Bezeichnen wir das Tripel mit (x,y,z), dann können wir uns — nicht nur für Konstuktion des Baumes sinnvoll — die möglichen Aktionen systematisch wie in der nachstehenden Tabelle auflisten. Quelle x>0 Zustände y=5 z<3 y<5 z=3 z<3 (x,5,z) (x,y,3) (x,y,z) (x,y,z) Aktion → (0,5,3) → (0,5,3) → (x−(5−y),5,z) → (x−(3−z),y,3) y>0 x 3−z ≥ y 3−z < y z z<3 (x,y,z) → (x+y,0,z) (x,y,z) → (x,0,z+y) (x,y,z) → (x,y−(3−z),3) z>0 x 5−y ≥ z 5−y < z y y<5 (x,y,z) → (x+z,y,0) (x,y,z) → (x,y+z,0) (x,y,z) → (x,5,z−(5−y)) Die nachstehende Graphik zeigt einen Ausschnitt aus dem wachsenden Baum. 800 PPP PP PP PP 350 503 H H H HH H H HH HH H 053 800 530 053 323 800 Q A Q A Q Q A Q 053 503 620 350 Gesucht ist der kürzeste Weg von der Wurzel zu einem Knoten mit dem Tripel (4,4,0). Es leuchtet ein, daß ein Weg, auf dem ein Knoten mit dem Tripel (800) liegt, auf keinen Fall der kürzeste sein kann. Dasselbe gilt für Wege, auf denen man Knoten mit identischem Inhalt antrifft. So sind Wege, die mit der Sequenz (800) → (350) → (800) beginnen, auf keinen Fall aussichtsreiche Kandidaten. Ein Beispiel für den zweiten Fall sind Wege, die mit der Sequenz (800) → (350) → (053) → (350) beginnen. Die nachstehenden Sequenzen belegen, daß es viele Kandidaten (Wege mit sich nicht wiederholenden Tripeln) gibt. (800) → (350) → (053) → (503) → (530) → (233) → (251) → (701) → (710) → (413) → (440) (800) → (350) → (323) → (620) → (602) → (503) → (530) → (233) → (251) → (701) → (710) → (413) → (440) (800) → (350) → (323) → (620) → (602) → (152) → (143) → (440) Wem gebührt die Krone? Der Algorithmus wird’s schon richten, würde der Wiener sagen. Hoffen wir’s. Zustände im R3 Jedes Tripel beschreibt den Zustand der drei Gefäße. Man kann jedes Tripel auch als einen Punkt im R3 auffassen. Da der Verfasser auch ein (hoffentlich leichter) Fall von Anagraphiker ist, sei dies hier an Hand von 2-dimensionalen Projektionen demonstriert. Zuerst eine Projektion auf die (x,y)-Ebene. 10 Einführung y 6 5 s s s s 4 s s s si f s s s s s s s s s s s s 4 s 5 s 6 s 7 3 2 1 1 2 3 sh x 8 Nun die auf die (y,z)-Ebene. z 6 3 s s s s s s 2 s s s s s s 1 s s s s s s s 1 s 2 s 3 si f 4 s 5 sh y Der Ausgangspunkt (das Tripel {800}) ist in den Darstellungen jeweils der von einem einfachen Kreis, das Ziel (das Tripel {440}) der durch einen doppelten Kreis umfangene Punkt. In Kästchen eingeschlossen sind die Punkte, die vom Ausgangspunkt in einem Schritt (Guß) erreicht werden können. Führt man weitere Schritte aus, so erkennt man, daß nur immer — dank unserer Regeln — Punkte auf dem Rand erreicht werden können. Graphen trinken Wein Führe Abkürzungen ein. 800 701 602 503 413 323 233 152 Auf dem Weg zum Graphen. a c e g i k m o 710 620 530 440 350 251 143 053 b d f h j l n p Einführung 11 d hj l p c g k o b f j n aj e i m Wer ist von wem aus erreichbar? a b c d e f g h i j k l m n o p : : : : : : : : : : : : : : : : g a a a a a a a b a d c f g e g j c b e d g f i g k g j g h j j i g j g j p j h p j m l o n j l k o m n p p p p p p 12 Einführung j Y H * * H 6 MB@H MB I @ BA]@ MJ HH B B @H BAJ@ H HH @ B B B @ H H AJ@ H @ B H H @ @ H B B A J H @ @ HH@ B B B AH @ B AJ HH B H ? B j @ @ J Y H Y H H @ H AK H 6 HB 6 6 B 6 @ I H A@ B HAHJ @ @ B B B HH @ @ A J A H H @@ B A J B HH B@ A @ H H H B@ B A B@ A J H @ B AHBH@ A BH@ J R @ R? H ? ? ? @ H U A * Y H H B HH B @ J B H A H H H H HHB AB @ J B BH @J B BHH AH HH HH B HH H @J B AB H HH B HH B B HH A @J HH H BN j JBN ? j H jABN H @ Der vollständige Graph. Da man wenig sieht, hier eine Auflösung in Teilgraphen. e e c e e e e e e c e e von a e e MBB eB B e B e ce e 6 e B ce Be von e e e e e e e e e e e e e e ce e e MBB e eB e e AKA B e e AB e e Y H H HHAB ce e HABe e von i e ce e e e e e e 6 e -e e H HH je H ce eH ? von b e e ce e e e e e e e e 6 e -e e H HH e eH je H e ce ce von f e e e e e e 6 e e e e ce e e e von j e ce e * -e e e ? e e e ce ? ce e von c e e e e e e e e e * e e e e e ? e c e von g e e e e e ce e e H Y H HH e e He e e e e c e von k ? e e e e e ce e @ BHH H eB @ e H je H B @ Re @ e B e@ B ? BNBe ce e von d e e e e e ce e e @ AB e eBA@ e e BA @ e e B AAUe @ Re @ B e BNBe e ce von h e ce e -e B e e eB e B ? e e eB e B ce e e BNBe von l Einführung e e e ce von m 13 ce e e 6 MBB e eB e I @ B @ e @e B e Y H HH@ B e eH@ HBe e e ce e 6 @ I @ e @e e Y H HH@ 6 e eHH @e e e e ce von n e e ce e e e e e e e e ce von o e e e 6 e e e ? e e e c e e e e e e e e e ce von p e e e Die Lösung des Verteilungsproblems entspricht der Antwort auf die Frage nach dem kürzesten Weg vom Knoten “a” zum Knoten “h” im Graphen. Eine ganz andere Sicht Das Problem läßt sich auch mit Hilfe einer Adjazenzmatrix darstellen. Eine “1” bedeutet, daß eine Verbindung zwischen den die Zeile bzw. die Spalte bezeichnenden Knoten besteht, z.B. kann man von a nach g kommen. Eine “0” — in der Darstellung weggelassen — bedeutet, es gibt keine direkte Verbindung. a a b c d e f g h i j k l m n o a 1 1 1 1 1 1 1 b c d e f g 1 h i 1 1 1 j 1 1 k 1 1 1 1 1 n o 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 c d e 1 g f 1 1 1 1 1 b p 1 1 1 a m 1 1 1 l h i 1 1 j 1 k l m n o 1 1 1 1 1 1 1 a b c d e f g h i j k l m n o p p Die Lösung ergibt sich dann durch Matrixmultiplikation. Betrachten wir ein Beispiel. h 2 h 1 Die Adjazenzmatrix für diesen Graphen ist dann I @ @@ @@ @@ @ @ R h @ - 0 1 C= 1 0 0 1 3 1 1 0 14 Einführung Man kann leicht C 2 und C 3 berechnen 1 1 C2 = 0 2 1 0 1 1 1 2 2 1 1 2 C3 = 2 1 0 2 c22,2 = 2 besagt, daß es 2 Wege der Länge 2 vom Knoten 2 zum Knote 2 gibt, nämlich 2 → 1 → 2 und 2 → 3 → 2. Die n-te Potenz der Adjazenzmatrix gibt also jeweils die Anzahl der Wege der Länge n zwischen zwei Knoten an. . . . und noch eine Wir interessieren uns für den kürzesten Weg und seine Länge von einer Position i zum Ziel. Es gibt i = 1, . . . , n Positionen (Knoten mit den Inhalten {x,y,z}). Das Ziel sei o.B.d.A. die Position n. Sei fi die Länge des kürzesten Weges von der Position i ins Ziel. Dann gilt fi = min(1 + fj ) fn = 0 j von i aus erreichbar i6=j Man muß also ein nichtlineares Gleichungssystem lösen. Dies kann man durch sukzessive Approximation lösen. Die k-te Approximation ergibt sich dabei zu: (k−1) (k) = min(1 + fj fi i6=j ) Eine geeignete Anfangsbedingung ist fn(0) = 0 (0) fi = +∞ i 6= n Wenden wir das Konzept auf unser Problem an. In der der bisher eingeführten Notation suchen wir den kürzesten Weg von der Position h zur Position a. 1 2 3 4 5 6 7 8 a b c d e f g +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ 2 2 2 2 2 2 +∞ +∞ +∞ +∞ +∞ 3 3 3 3 3 +∞ 3 3 3 3 3 +∞ +∞ +∞ +∞ +∞ +∞ 6 6 +∞ +∞ +∞ +∞ +∞ 7 4 4 4 4 7 h 0 0 0 0 0 0 0 0 i j k l m n o p +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ 5 5 5 2 2 2 2 2 2 +∞ +∞ +∞ 1 1 1 1 1 1 1 6 6 4 4 4 4 5 5 5 1 1 1 1 1 1 1 +∞ +∞ +∞ +∞ 7 Wie nicht anders zu erwarten, in 7 Schritten sind wir am Ziel. 1.4 Eine magische Aufgabe In der Ausgabe des Sterns (38/99) wurde zur Unterhaltung folgendes unvollständiges magisches Quadrat präsentiert: Algorithmen und Effizienz 15 10 16 5 19 15 9 4 6 21 14 Diese Rätselaufgabe kann als ein Beispiel für die Veranstaltung Algorithmen und Datenstrukturen Verwendung finden. Beantworten Sie folgende Fragen und beobachten Sie sich auf dem Weg der Beantwortung. a.) Was ist ein magisches Quadrat? b.) Wie lautet die Lösung? c.) Wie könnte man Zwischenergebnisse auf dem Weg zur Lösung notieren und den Lösungsweg verständlich beschreiben? d.) Wie sind Sie auf die Lösung gekommen? Welche Sackgassen haben Sie betreten? e.) Was hat Ihnen bei Ihren Überlegungen aus dem Studium geholfen? f.) Welche Arten von Werkzeugen wünschen Sie sich für die Lösung solcher (5 × 5)-QuadratProbleme? g.) Welche Algorithmen und Datenstrukturen werden für die Umsetzung solcher Werkzeuge benötigt? h.) Was ändert sich durch die Verallgemeinerung von (5 × 5) nach (n × n)? i.) Welche unterschiedlichen Ansichten zu Unterstützungswerkzeugen werden Werkzeugersteller und Werkzeugverwender haben? j.) Wie sollte eine Software-Lösung aussehen? k.) Was konnten Sie für Ihre Lösung aus den vorstehenden Abschnitten entnehmen? 1.5 Literatur [1] Bellman e.a.: Algorithms, Graphs and Computers Academic Press, 1970 [2] Dijkstra E. W.: A Short Introduction to the Art of Programming EWD 316, Aug. 1971 [3] Euklid: Die Elemente Wissenschaftliche Buchgesellschaft, 1962 [4] Knuth D. E.: The Art of Computer Programming, vol I Addison-Wesley, 1973 [5] Reingold e.a.: Combinatorial Algorithms Theory and Practice Prentice Hall, 1977 [6] Zilahi-Szabó M. G.: Kleines Lexikon der Informatik Oldenbourg Verlag, 1995 16 Algorithmen und Effizienz Chapter 2 Algorithmen und Effizienz Inhaltsangabe 2.1 Laufzeitverhalten . . . . . . . . . . . . . . . 2.1.1 Das O()-Konzept . . . . . . . . . . . . . . . 2.1.2 Regeln . . . . . . . . . . . . . . . . . . . . . 2.2 Verbesserung des Laufzeitverhalten . . . . 2.2.1 Das Problem . . . . . . . . . . . . . . . . . 2.2.2 Nenn’ nur die (Element-)Summe . . . . . . 2.2.3 Die Lehren . . . . . . . . . . . . . . . . . . 2.2.4 Und wo ist nun der Teilvektor? . . . . . . . 2.3 Literatur . . . . . . . . . . . . . . . . . . . . . 2.1 2.1.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 18 18 18 18 22 22 24 Laufzeitverhalten Das O()-Konzept Was wollen wir unter Effizienz verstehen? Genauer, wie wollen wir Effizienz messen? n bezeichne ein Maß für die Größe des input oder des zu lösenden Problems, und die Laufzeit T des Lösungsalgorithmuss (Lösungsprogramm) sei eine Funktion von n, d.h., T = T (n). Sicherlich ist die Größe der Laufzeit ein Anhaltspunkt für die Effizienz eines Algorithmus. Wir wollen hier aber nicht die Laufzeiten absolut vergleichen, sondern uns mit einem Vergleich ihrer Größenordnung begnügen. Dies ermöglicht die nachstehende Definition. Annahme: • n∈N • T (n) ≥ 0 für alle n Definition 1: O() T (n) heißt O(f (n)), wenn es eine Konstante c gibt, so daß gilt T (n) ≤ cf (n). △ Beachte, daß es bei der O()-Betrachtung nicht auf den Wert der Konstanten c ankommt. Gibt es also zwei Algorithmen, für die z.B. gilt, daß T1 (n) = 2n2 und für den zweiten Algorithmus T2 (n) = 3n2 , so sind doch beide O(n2 ). In der Literatur finden sich noch weitere Größenvergleichsindikatoren, auf die wir hier aber nicht weiter eingehen wollen. 17 18 Algorithmen und Effizienz 2.1.2 Regeln Zur Ermittlung von T (n) bzw. der Abschätzung des O()-Verhaltens ist der nachstehende Satz von Regeln hilfreich. Regel 1: Sequenzen Die Laufzeiten der einzelnen Bestandteile (Anweisungsblöcke) der Sequenz addieren sich. Das O()-Verhalten der Sequenz ist durch das Maximum der O()-Verhalten der Blöcke gegeben. Regel 2: Schleifen Die Laufzeit einer Schleife ist höchstens die Laufzeit des Schleifenrumpfes multipliziert mit der Anzahl k der Iterationen. Das O()-Verhalten des Schleifenrumpfes sei O(r), dann ist das O()Verhalten der Schleife O(kr). Regel 3: Geschachtelte Schleifen Geschachtelte Schleifen werden von innen nach außen unter Beachtung von Regel 1 abgearbeitet. Regel 4: if-then-else Die Laufzeit eines if-then-else-Konstruktes ist nicht größer als das Maximum der Laufzeiten des if-Blockes und des else-Blockes plus der Testzeit. Das O()-Verhalten entspricht dem Maximum der O()-Verhalten der Blöcke. Regel 5: Rekursion Die Laufzeit erfüllt eine Rekursion gleicher Stufe. 2.2 2.2.1 Verbesserung des Laufzeitverhalten Das Problem Das folgende Problem wie auch die Lösungsvorschläge sind dem Buch Programming Pearls von Bentley [8] entnommen. Aufgabe 1: Gegeben sei ein Vektor der Länge n, die Elemente sind reelle Zahlen. Gesucht ist die größte Elementsumme, die sich bei Betrachtung von Teilvektoren (benachbarte Elemente) finden läßt. ♥ Zwei Fälle fallen sofort ins Auge: 1. Sind alle Elemente des Vektors positiv, so ist der Vektor selbst der gesuchte Teilvektor. 2. Sind alle Elemente des Vektors negativ, wollen wir den leeren Vektor (enthält kein Element) ins Spiel bringen. Wir ordnen ihm die Elementsumme 0 zu. 3 8 −3 2 −9 0 2 −4 7 6 −4 3 In diesem Beispiel finden wir als größte Elementsumme 13. Wenn es interessiert, der Teilvektor steht auf den Positionen 9 bis 10. 2.2.2 Nenn’ nur die (Element-)Summe Wir wollen die Notierung des (oder der Teilvektoren?) Teilvektors erst einmal außer Betracht lassen. Es soll also nur die maximale Elementsumme ermittelt werden. Welcher Teilvektor sie liefert, z.B. durch Angabe der Indexpositionen, soll uns erst später beschäftigen. Algorithmen und Effizienz 19 Die erste Lösung: O(n3 ) Hier gehen wir nach der Methode brute force vor. Wir überprüfen alle Teilvektoren, die sich bilden lassen auf ihre Elementsumme hin. Wir müssen dazu alle Indexpaare (i, j) bilden mit i ≤ j und i, j = 1, . . . n. Die nachstehende Lösung liegt auf der Hand. Algorithmus 1: O(n3 ) [1] : MaxSoFar ← 0.0 [2] : for i ← 1 to n do [3] : for j ← i to n do [4] : Sum ← 0.0 [5] : for k ← i to j do Sum ← Sum + X[k] [6] : [7] : MaxSoFar ← max(MaxSoFar, Sum) Das Laufzeitverhalten wird von den 3 geschachtelten Schleifen bestimmt. Jede hat maximal n Schritte. Daraus ergibt sich für das Verhalten des Algorithmus O(n3 ). Die nachstehende kommentierte Fassung des Algorithmus verdeutlicht dies. Algorithmus 1: O(n3 ) mit Berechnungshinweisen [1] : O(1) MaxSoFar ← 0.0 [2] : O(n3 ) for i ← 1 to n do [3] : O(n2 ) for j ← i to n do [4] : O(1) Sum ← 0.0 [5] : O(n) for k ← i to j do [6] : [7] : Sum ← Sum + X[k] O(1) MaxSoFar ← max(MaxSoFar, Sum) O(1) Die zweite Lösung: O(n2 ) Ein wenig Nachdenken zeigt, daß der obige Algorithmus bei der Bestimmung der Elementsumme der Teilvektoren (i, j) und (i, j + 1) bei der Summierung des zweiten Teilvektors wieder beim Element i beginnt, obwohl doch gilt, Sum(i,j+1) = Sum(i,j) + Xj+1 . Der nächste Algorithmus vermeidet diese unnötigen Additionen, indem er sich geschickt Zwischenergebnisse merkt. Anders überlegt, muß die Teilfolge an irgendeiner Position beginnen. Wir können daher an der Stelle i beginnen, die folgenden Elemente Schritt für Schritt addieren und ein sich einstellendes neues Maximum notieren. Dieses Vorgehen führt zu zwei Schleifen. Algorithmus 2: O(n2 ) — der Erste [1] : MaxSoFar ← 0.0 [2] : for i ← 1 to n do [3] : Sum ← 0.0 [4] : for j ← i to n do [5] : Sum ← Sum + X[j] [6] : MaxSoFar ← max(MaxSoFar, Sum) 20 Algorithmen und Effizienz Hier sind es nur noch 2 geschachtelte Schleifen, von denen jede im ungünstigsten Fall n Schritte erfordert. Also ergibt sich für den Algorithmus ein Verhalten O(n2 ). Die obige Idee läßt sich leicht verallgemeinern. Die nachstehende Formel enthält die Lösung. Sum(i,j+k) = Sum(i,j) + Xj+1 + . . . Xj+k Verschafft man sich die Sequenz Sumi = X1 + . . . + Xi für i = 1, . . . n, so kann man sich die Elementsummen aller Teilvektoren durch geeignete Differenzbildung in dieser Sequenz beschaffen. Hier die Umsetzung. Algorithmus 3: O(n2 ) — der Zweite [1] : CumArray[0] ← 0.0 [2] : for i ← 1 to n do [3] : CumArray[i] ← CumArray[i−1] + X[i] [4] : MaxSoFar ← 0.0 [5] : for i ← 1 to n do [6] : for j ← i to n do [7] : Sum ← CumArray[j] − CumArray[i − 1] [8] : MaxSoFar ← max(MaxSoFar, Sum) Auch hier sind es nur noch 2 geschachtelte Schleifen, von denen jede im ungünstigsten Fall n Schritte erfordert. Die Schleife zur Ermittlung der Sequenz Sumi ist nur von der Ordnung O(n). Von größerem Gewicht sind die beiden geschachtelten Schleifen. Also ergibt sich für den Algorithmus ein Verhalten O(n2 ). Die dritte Lösung: O(n log n) Eine ganz andere Betrachtung führt zum nächsten Algorithmus. Die Skizze zeigt die Lösungsidee. A B MA MB MC Nach der Methode ”‘divide et impera”’ vorgehend stellen wir fest, daß der gesuchte Teilvektor mit der maximalen Elementsumme entweder im Teil A (dann als MA bezeichnet) liegt oder im Teil B (MB ) oder aber durch unseren Teilungsschritt gerade zerstückelt wurde, es wäre der 3. Fall (MC ). Gesetzt den Fall wir haben die drei Maximumskandidaten MA , MB , MC , dann ist es keine große Schwierigkeit mehr, den Algorithmus zu formulieren. • Bestimme für die drei Fälle die maximalen Summen und bestimme hieraus das Maximum. • Bestimme das Maximum jedes der Teils (A, B) nach demselben Prinzip. Wir schreiben den Algorithmus schrittweise verfeinernd auf. Die entsprechenden Partien werden durch die Klammerung << . . . >> hervorgehoben. Wir verwenden außerdem eine rekursive Hinschreibe. Algorithmus 4: O(n log n) [1] : recursive function MaxSum(l, u) Algorithmen und Effizienz 21 [2] : {Handle Fehlaufruf ab: zero-element vector} [3] : if l > u then return 0.0 [4] : {Handle Rekursionsende ab: one-element vector} [5] : if l = u then return max(0.0,X[i]) [6] : {Ermittle Teilungsstelle: A is X[l .. m], B is X[m+1 .. u]} [7] : m ← (l + u)/2 [8] : {Bestimme MaxToLeft [9] : << Bestimme größte Summe direkt links von der Grenze>> [10] : {Bestimme MaxToRight [11] : << Bestimme größte Summe direkt rechts von der Grenze>> [12] : {Bestimme Gesamtmaximum} [13] : MaxCrossing ← MaxToLeft + MaxToRight [14] : MaxInA ← MaxSum(l,m) [15] : MaxInB ← MaxSum(m+1,u) [16] : return max(MaxCrossing,MaxInA,MaxInB) Bei der Division in Schritt 7 handelt es sich um die ganzzahlige Division. Bestimme größte Summe direkt links von der Grenze Sum ← 0.0 MaxToLeft ← 0.0 Bestimme größte Summe direkt links von der Grenze Bestimme größte Summe direkt links von der Grenze for i ← m downto l do Bestimme größte Summe direkt links von der Grenze Sum ← Sum + X[i] Bestimme größte Summe direkt links von der Grenze MaxToLeft ← max(MaxToLeft,Sum) Bestimme größte Summe direkt rechts von der Grenze Sum ← 0.0 MaxToRight ← 0.0 Bestimme größte Summe direkt rechts von der Grenze Bestimme größte Summe direkt rechts von der Grenze for i ← m + 1 to u do Bestimme größte Summe direkt rechts von der Grenze Sum ← Sum + X[i] Bestimme größte Summe direkt rechts von der Grenze MaxToRight ← max(MaxToRight,Sum) Bei jedem Rekursionsschritt durchlaufen wir den ganzen Vektor einmal linear, also ein Verhalten von O(n). Es gibt aber nur größenordnungsmäßig log n solche Schritte, denn eine Folge der Länge n = 2m kann m-mal — also log n-mal — halbiert werden. Daraus ergibt sich ein Laufzeitverhalten von O(n log n). 22 Algorithmen und Effizienz Die vierte Lösung: O(n) Daß es auch mit einem Durchgang geht, entnimmt man der nachstehenden Skizze. MaxEndingHere MaxSoFar Man kann immer gedanklich zwei augenblickliche Maxima unterscheiden. Zum einen die maximale Elementsumme des Teilvektors, der mit dem Element Xi endet und zum anderen das Maximum aller bisher betrachteten Teilvektoren. Natürlich können diese beiden auch zusammenfallen. Der folgende Algorithmus setzt diese Idee um. Algorithmus 5: O(n) [1] : MaxSoFar ← 0.0 [2] : MaxEndingHere ← 0.0 [3] : {loop invariant: MaxSoFar, MaxEndingHere are accurate for X[1 .. i-1]} [4] : for i ← 1 to n do [5] : MaxEndingHere ← max(MaxEndingHere+X[i],0.0) [6] : MaxSoFar ← max(MaxSoFar,MaxEndingHere) Das Laufzeitverhalten von O(n) erkennt man sofort. Oder? 2.2.3 Die Lehren Was haben wir gelernt an generalisierbaren Dingen? • Speichere (Zwischen)zustände, um erneute Berechnung zu vermeiden. • Führe vorbereitende Berechnungen aus und lege sie in Datenstrukturen ab. • Teile und herrsche. • Gehe von der Lösung für i zur Lösung von i+1 über. • Prüfe die Verwendung kumulierter Größen. • Suche nach einer unteren Grenze für das Laufzeitverhalten. 2.2.4 Und wo ist nun der Teilvektor? Der gesuchte Teilvektor wird durch den ”‘linken”’ bzw. ”‘rechten”’ Index angegeben. Sie werden als bestl bzw. bestr bezeichnet. Es werden nachstehend jeweils für die meisten Algorithmen nur die Zeilen angegeben, in denen eine Änderung vorgenommen wird oder die angefügt werden müssen. Durch bestl =0 und bestr =0 wird ein leerer Teilvektor beschrieben. Die erste Lösung: O(n3 ) Algorithmus 1: O(n3 ) [1] : MaxSoFar ← 0.0 [8] : ♦ bestl ← 0 if MaxSoFar< Sum then [9] : MaxSoFar ← Sum [10] : bestl ← i ♦ bestr ← j ♦ bestr ← 0 Algorithmen und Effizienz 23 Die zweite Lösung: O(n2 ) Die beiden O(n2 )-Algorithmen sind nach dem gleichen Schema umzurüsten. Algorithmus 2: [1] : [7] : O(n2 ) — der Erste MaxSoFar ← 0.0 ♦ bestl ← 0 MaxSoFar ← Sum [9] : bestl ← i Algorithmus 3: [9] : bestr ← 0 ♦ bestr ← 0 if MaxSoFar< Sum then [8] : [5] : ♦ ♦ bestr ← j O(n2 ) — der Zweite MaxSoFar ← 0.0 ♦ bestl ← 0 if MaxSoFar< Sum then [10] : MaxSoFar ← Sum [11] : bestl ← i ♦ bestr ← j Die dritte Lösung: O(n log n) Für die rekursive Lösung wollen wir voraussetzen, daß es möglich ist, als Resultat eine komplexere Struktur als einen Skalar zurückzugeben. In ”‘Anton”’-Terminologie wollen wir hier einen Verbund betrachten. Er bestehe aus den Komponenten Sum, bestl, bestr, auf die in der Form resultat$Sum usw. zugegriffen wird. Insgesamt sind die kleinen Veränderungen so zahlreich, daß wir den Algorithmus vollständig neu hinschreiben. Natürlich haben wir nicht das Prinzip geändert. Algorithmus 4: O(n log n) [1] : recursive function MaxSum(l, u) [2] : bestl ← 0 [3] : if l > u then return 0.0,bestl,bestr [4] : if l = u then Sum ← max(0.0,X[i]) [5] : if Sum > 0.0 then bestl ← l [6] : return Sum,bestl,bestr [7] : m ← (l + u)/2 [8] : Sum ← 0.0 [9] : for i ← m downto l do [10] : Sum ← Sum + X[i] [11] : if Sum > MaxToLeft then MaxToLeft ← Sum ♦ [12] : Sum ← 0.0 bestr ← 0 ♦ ♦ ♦ MaxToLeft ← 0.0 bestr ← u ♦ MaxToRight ← 0.0 bestl ← m+1 ♦ ♦ bestl ← i bestr ← m [13] : for i ← m + 1 to u do [14] : Sum ← Sum + X[i] [15] : if Sum > MaxToRight then MaxToRight ← Sum ♦ bestr ← i 24 Algorithmen und Effizienz [16] : if bestl > bestr then bestl ← 0 ♦ bestr ← 0 [17] : Sum ← MaxToLeft + MaxToRight [18] : MaxInA ← MaxSum(l,m) [19] : MaxInB ← MaxSum(m+1,u) [20] : if MaxInA$Sum > Sum then [21] : Sum ← MaxInA$Sum ♦ bestl ← MaxInA$bestl ♦ bestr ← MaxInA$bestr ♦ bestr ← MaxInB$bestr [22] : if MaxInB$Sum > Sum then [23] : Sum ← MaxInB$Sum ♦ bestl ← MaxInB$bestl [24] : return Sum,bestl,bestr Man beachte, daß die Variable MaxCrossing in der Zeile [17] nicht mehr auftaucht. Ihre Ersetzung durch die Variable Sum macht die nachstehende Fallunterscheidung etwas einfacher in der Hinschreibe. Die vierte Lösung: O(n) Beim linearen Algorithmus sind die Veränderungen so zahlreich, daß wir ihn vollständig neu hinschreiben. Natürlich haben wir nicht das Prinzip geändert. Algorithmus 5: O(n) [1] : MaxSoFar ← 0.0 ♦ bestl ← 0 ♦ bestr ← 0 [2] : MaxEndingHere ← 0.0 ♦ j ← 1 [3] : for i ← 1 to n do [4] : MaxEndingHere ← MaxEndingHere+X[i] [5] : if MaxEndingHere<0 then [6] : [7] : MaxEndingHere ← 0 ♦ j ← i+1 if MaxSoFar<MaxEndingHere then [8] : MaxSoFar ← MaxEndingHere [9] : bestl ← j 2.3 ♦ bestr ← i Literatur [8] Bentley J.: Programming Pearls Addison-Wesley, 1989 [9] Weiss M. A.: Data Structures and Algorithm Analysis Algorithmen und Effizienz 25 The Benjamin/Cummings Publishing Company, 1992 Algorithmen und Datenstrukturen II von Peter Naeve Statistik und Informatik Fakultät für Wirtschaftswissenschaften Universität Bielefeld c Naeve, Version 2.0, Wintersemester 1999/2000 P. 26 Algorithmen und Effizienz Contents 27 Chapter 3 Sortieren Inhaltsangabe 1.1 1.2 Viele Fragen — gibt’s auch Antworten? . . . . . . . . . . . . . . . . . . . Algorithmus und Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.1 Definitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.2 Notationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Ein Problem und viele Algorithmen und Datenstrukturen . . . . . . . . 1.3.1 Weißt du wieviel Bitlein . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.2 Komplex Komplexe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.3 Ein Überdeckungsproblem . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.4 Es gibt viele Wege zu einer fairen Teilung . . . . . . . . . . . . . . . . . . . 1.4 Eine magische Aufgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5 Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1 3.1.1 1 2 2 2 3 3 5 6 8 14 15 Einführung Sortieren tut Not Sortieren???? In der guten alten Zeit war ein Sortierprogramm ein wichtiger Bestandteil eines Betriebssystem. Sie liegt noch gar nicht soweit zurück — für einige stehengebliebenen DOS-Anhänger besteht sie immer noch. Gibt es doch unter diesem Betriebssystem ein Kommando sort. Das Kommando sort opt file sortiert den Inhalt der Datei file wie es in den Optionen opt festgelegt ist. UNIX kennt ein ein ähnlich arbeitendes Kommando. Die vermeintlich bessere Zukunft Windows läßt einen allerdings in Bezug aufs Sortieren im Stich. Es gibt scheint’s nichts zu sortieren. Bill Gates hat die Welt für uns bereits sortiert. Ein Problem und eine naive Lösung Eines der ersten Probleme, die man im kommerziellen Bereich mit Hilfe von Computern anging, ist in der Literatur bekannt unter dem Namen ”‘Fortschreiben einer sequentiellen Datei”’ (engl. ”‘updating a sequential file”’ oder auch ”‘posting”’ genannt). ”‘Sequentiell”’ erinnert daran, daß das erste Medium zur Speicherung von größeren Datenmengen das Magnetband war. Seine Geometrie erlaubt nur eine sequentielle (zuerst nur in einer Richtung; rückwärts lesende Magnetbandstationen sind eine spätere Entwicklung) Verarbeitung. Die Fortschreibung einer Datei steht immer an, wenn ein Bestand (Bestanddatei, engl. ”‘master file”’) wie zum Beispiel die Konten einer Sparkasse, eine 25 26 Sortieren Bibliothek, ein Lager oder der Personalstand eines Unternehmens Veränderungen unterworfen ist. Es werden Beträge eingezahlt, abgebucht, Bücher werden ausgeliehen, vorgemerkt, Konserven werden angeliefert, überschreiten ihr Verfalldatum, Mitarbeiter werden krank, entlassen, heiraten usw. Alle diese Vorkommnisse werden zusammenfassend als Bewegungen (engl. ”‘transaction”’) bezeichnet. Sie werden in einer Bewegungsdatei (engl. ”‘transaction file”’) zusammengefaßt. Von Zeit zu Zeit paßt man den Bestand (die Bestandsdatei) diesen Bewegungen an. Man schreibt, wie man sagt, den Bestand fort. Welchen Problemen man dabei bei ”‘naivem”’ Vorgehen begegnet, verdeutlicht die nachstehende Zeichnung. Dabei haben wir die einzelnen ”‘Sätze”’ mit Vornamen als Schlüssel belegt: Bestandsdatei DIRK LARS PAUL ANNA KARL EMIL SVEN EMIL PAUL I Schreib/Lesestation DORA LARS SVEN ANNA LARS Bewegungsdatei Wie man sieht, wird gerade der Satz LARS fortgeschrieben. Die nächste Bewegung bezieht sich auf den Satz SVEN. Dazu ist es notwendig, das Band der Bestandsdatei bis zum Satz SVEN in Pfeilrichtung zu bewegen. Nach der Fortschreibung von SVEN kommt eine Bewegung, die sich auf ANNA bezieht. Der entsprechende Eintrag in der Bestandsdatei ist aber bereits an der Schreib-/Lesestation vorbei. Es muß also das Band zurückgespult werden. Die Auswirkungen auf die Effizienz des Verfahrens sind verheerend, da das Rückspulen zeitaufwendig und, wie sich zeigt, unnötig ist. Die pfiffige Lösung Sortiert man vor der Bearbeitung die Sätze in der Bewegungsdatei so, daß die Schlüssel in der gleichen Weise sortiert sind wie in der Bestandsdatei, so entfällt das Rückspulen. Die nächste Abbildung verdeutlicht die neue Situation1 : Bestandsdatei ANNA DIRK EMIL KARL LARS PAUL SVEN PAUL SVEN I Schreib/Lesestation ANNA DORA EMIL LARS LARS Bewegungsdatei Man überzeugt sich auch leicht, daß beim richtigen Vorrücken der Bänder keine Notwendigkeit zum Rückspulen mehr besteht. Aber wie rückt man Bestandsband und Bewegungsband richtig vor? Das Vorrücken muß so synchronisiert werden, daß kein benötigter Satz bereits die Schreib-/Lesestation passiert hat. Es darf also nicht passieren, daß nach der Bearbeitung der ersten Bewegung LARS das Bestandsband bewegt wird. Erst wenn man auf dem Bewegungsband bei PAUL angekommen ist, darf das Bestandsband von LARS zu PAUL vorrücken. Das Problem ”‘Fortschreiben einer sequentiellen Datei”’ ist also durch die Idee der gleichartigen Sortierung auf ein Synchronisationsproblem zurückgeführt worden. Dies wollen wir hier nicht studieren. Wir bleiben in diesem Kapitel beim — jetzt wohl als wichtig erkanntem — Sortieren. 1 Wir haben sinnvollerweise jetzt die Bestandsdatei nach (alphabetisch) aufsteigendem Schlüssel sortiert. Sortieren 3.1.2 27 Sortierprinzipien Man betrachte die Folge: 5 12 8 9 1 2, sie soll aufsteigend (oder absteigend) sortiert werden. Das Ergebnis der Sortierung ist: 1 2 5 8 9 12. Wie sind wir zu diesem Ergebnis gekommen? Prinzipien: 1. Sortieren durch Auswahl Suche das kleinste (oder das größte oder beide) Element, separiere es von dem Rest. Mit dem Rest verfahre analog, usw. 2. Sortieren durch Einfügen Die Elemente werden nacheinander betrachtet und jeweils an dem angebrachten Platz relativ zum sortierten Teil eingefügt. 3. Sortieren durch Zählen Jedes Element wird mit jedem anderen verglichen und die Zahl der jeweils kleineren Elemente wird gezählt, diese Zahl legt den Platz des Elements in der sortierten Folge fest. 4. Sortieren durch Austausch Wenn zwei Elemente gefunden werden, die nicht richtig (in Bezug zu der sortierten Folge) stehen, so werden sie ausgetauscht. 5. Sortieren durch Mischen Mischen von sortierten Teilfolgen bis es nur noch eine Teilfolge ( = sortierter Folge) gibt. 6. Sortieren durch Verteilen Die gute alte Sortiermaschine. 3.1.3 Bedingungen • Sortieren im Platz oder nicht? • Internes oder externes Sortieren? • Stabiles Sortieren (bei Gleichheit wird die Reihenfolge beibehalten) oder nicht? Wir werden grundsätzlich internes Sortieren betrachten und dabei in der Regel im Platz sortieren. Die Frage der Stabilität bleibt erst einmal unberücksichtigt. 3.1.4 Vorgehen Die vorstehenden Prinzipien wollen wir nun versuchen, in explizite Algorithmen zu überführen. Die Entwicklung wird nach der Methode der schrittweisen Verfeinerung vorgenommen. Wesentlich werden Konzepte der strukturierten Programmierung (Dijkstra) verwendet. Es wird im Folgenden angenommen, daß der Vektor a, der aus den Elementen a1 , . . ., an besteht, zu sortieren ist. 3.2 3.2.1 Algorithmen fürs Sortieren durch Auswahl Lösungsskizze a1 bis ai−1 bereits sortiert 6 Austausch von ai und ak 6 28 Sortieren 3.2.2 Der Algorithmus k x Algorithmus 1: Index des kleinsten Elementes unter ai bis an augenblickliches Minimum Top Level: Sortieren durch Auswahl [1] : for i ← 1 to n−1 do [2] : << bestimme den Index k des kleinsten Elementes der ai ,. . . ,an >> [3] : << vertausche ai und ak >> Die nächsten Schritte sind fast selbsterklärend, bestimme den Index k . . . x ← ai ♦ k ← i for j ← i + 1 to n do bestimme den Index kbestimme ... den Index k . . . if aj < x then bestimme den Index k . . . x ← aj ♦ k ← j wenn man beachtet, daß man mit x = ak aus der Schleife kommt. vertausche ai . . . ak ← ai ♦ ai ← x Damit ergibt sich insgesamt Algorithmus 1: [1] [2] [3] [4] [5] [6] Sortieren durch Auswahl : for i ← 1 to n−1 do : x ← ai ♦ k ← i : for j ← i + 1 to n do : if aj < x then : x ← aj ♦ k ← j : ak ← ai ♦ ai ← x 3.3 3.3.1 Algorithmen fürs Sortieren durch Einfügen Lösungsskizze Bestimme Index k der ai unter den bereits sortierten a1 bis ai−1 zukommt. a1 bis ai−1 bereits sortiert 1. rechts Feld- 6 2. ai nimmt Platz von ak ein 3.3.2 Ein erster Algorithmus k Algorithmus 2: Index des neuen Elementes unter a1 bis ai Top Level: Sortieren durch Einfügen [1] : for i ← 2 to n do [2] : <<füge ai geeignet in den sortierten Teilvektor a1 , . . . , ai−1 ein>> Sortieren 29 Die Elemente auf den Plätzen 1 bis i − 1 sind sortiert, für das Element auf dem Platz i muß nun sein Platz — nennen wir ihn k — unter den i Elementen gefunden werden. Hat man den Platz gefunden, so muß er für das neue Element frei gemacht werden. füge ai geeignet in . . . << bestimme den Index k unter den ersten i-Plätzen >> << verschiebe die Elemente k, . . . , i−1 um einen Platz nach rechts, füge ai ein >> füge ai geeignet inbestimme ... den Index k . . . k ← 1 for j ← 1 to i−1 do bestimme den Index kbestimme ... den Index k . . . if ai > aj then k ← k+1 verschiebe die Elemente . . . h ← ai for j ← i−1 downto k do verschiebe die Elementeverschiebe ... die Elemente . . . aj+1 ← aj verschiebe die Elemente . . . ak ← h Algorithmus 2: [1] : Sortieren durch Einfügen for i ← 2 to n do [2] : k←1 [3] : for j ← 1 to i−1 do [4] : if ai > aj then k ← k+1 [5] : h ← ai [6] : for j ← i−1 downto k do [7] : [8] : 3.3.3 aj+1 ← aj ak ← h . . .und seine Verbesserungen Eine nähere Inspektion des gefundenen Algorithmus zeigt, daß eine Reihe von Verbesserungen möglich sind. • Abbruch der Suchschleife nach dem ersten false • Mischen von Platzsuche und Verschieben Lösung mit besserem Abbruch Algorithmus 3: [1] : Sortieren durch Einfügen, Variante 1 for i ← 2 to n do [2] : k←1 [3] : while ai > ak and k < i do [4] : k ← k+1 [5] : h ← ai [6] : for j ← i−1 downto k do [7] : [8] : aj+1 ← aj ak ← h 30 Sortieren Lösung mit noch besserem Abbruch Aber die Bedingung in der while -Schleife ist unnötig kompliziert, es geht auch einfacher. Der nachstehende Algorithmus beendet die while - Schleife immer, da (ai >ai )==false ist. Algorithmus 4: Sortieren durch Einfügen, Variante 2 [1] : for i ← 2 to n do [2] : k←1 [3] : while ai > ak do [4] : k ← k+1 [5] : h ← ai [6] : for j ← i−1 downto k do [7] : [8] : aj+1 ← aj ak ← h Lösung mit besserem Abbruch und Mischung der Schritte Algorithmus 5: Sortieren durch Einfügen, Variante 3 [1] : for i ← 2 to n do [2] : j←i−1 [3] : h ← ai [4] : while aj > h and j > 0 do [5] : aj+1 ← aj [6] : j←j−1 [7] : aj+1 ← h Abbruch durch Marke Auch hier geht es durch die Einführung einer Marke (sentinel = Wächter) besser. Algorithmus 6: Sortieren durch Einfügen, Variante 4 [1] : for i ← 2 to n do [2] : j←i−1 [3] : a0 ← ai [4] : while aj > a0 do [5] : aj+1 ← aj [6] : j←j−1 [7] : aj+1 ← a0 Sortieren 3.3.4 31 Shell-Sort Eine Variante des Sortierens durch Einfügen. Es werden Folgen sortiert durch Einfügen, deren Elemente eine Distanz von h haben. Problem: Bestimmung der Schrittweite h. Der nachstehende Algorithmus konstruiert eine Folge abnehmender h-Werte. Begonnen wird mit einem großen h. Dies führt zu großen Sprüngen ursprünglich falsch stehender Elemente — sie kommen schneller auf den ihn gebührenden Platz. Der letzte Durchgang mit einem h=1 repariert zur Not alles. Es gibt aber die beruhigende Erkenntnis, daß mit einem h-Wert gewonnene Teilordnungen durch einen Durchlauf mit einem anderen h-Wert nicht zerstört wird, siehe Knuth [13]. j h v Algorithmus 7: Index des kleinsten Elementes unter ai bis an Schrittweite augenblickliches Minimum Shell-Sort [1] : h←1 [2] : repeat h ← 3*h + 1 until h > n [3] : repeat [4] : h ← h div 3 [5] : for i ← h+1 to n do [6] : v ← ai ♦ j ← i [7] : while aj−h > v do [8] : aj ← aj−h ♦ j ← j − h [9] : if j ≤ h then goto lab lab:aj ← v [10] : [11] : until h = 1 Kommentar: Schritt 6: Schritt 7−9: Schritt 10: 3.4 ai wird auf v gemerkt shift nach rechts um h v an passender Stelle einfügen Algorithmen fürs Sortieren durch Zählen Algorithmus 8: Top Level: Sortieren durch Zählen [1] : for i ← 1 to n do [2] : < bestimme Platz von ai in sortierter Folge > Überlegung: • wenn keiner kleiner ist als ein bestimmtes aj , so ist dieses das 1. Element der sortierten Folge • wenn genau einer kleiner ist als ein bestimmtes ak , so ist dieses das 2. Element in der sortierten Folge • usw. Algorithmus 8: Top Level: Sortieren durch Zählen 32 Sortieren [1] : for i ← 1 to n do [2] : < bestimme für ai die Anzahl der Elemente, die kleiner als ai sind > Überlegung: • a1 a2 a3 a4 a1 o < > < a2 > o < < a3 < > o > a4 > > < o a5 < < > < • Feld ij ist immer komplementär zu Feld ji • man braucht nur die obere bzw. untere Dreiecksmatrix zu durchlaufen c Algorithmus 8: Vektor der Zählergebnisse Sortieren durch Zählen [1] : for i ← 1 to n do [2] : ci ← 1 [3] : for i ← 1 to n−1 do [4] : for j ← i+1 to n do [5] : if ai < aj then cj ← cj + 1 [6] : else ci ← ci + 1 Der Algorithmus sortiert nicht, sondern liefert nur einen Indexvektor für den Platz im sortierten Vektor ab. Durch for i ← 1 to n do bci ← ai erzeugt man die sortierte Version von a im Vektor b. 3.5 3.5.1 Sortieren ohne Vergessen der Ausgangsanordnung Lösung durch Duplizieren Diese Technik sei am Algorithmus für das Sortieren durch Einfügen demonstriert. Lösung (im Vektor b) mit Erhalt der Ausgangslage (im Vektor a) und Verwendung einer Marke. Algorithmus 9: Sortieren durch Einfügen, Variante 5 [1] : b1 ← a1 [2] : for i ← 2 to n do [3] : j←i−1 [4] : b0 ← ai [5] : while bj > b0 do [6] : bj+1 ← bj [7] : j←j−1 [8] : bj+1 ← b0 Sortieren 3.5.2 33 Behalte die ursprünglichen Indizes Kann man nicht im Platz — besonders interessant, wenn z.B. ai mehr als ein Skalar ist — sortieren, ohne die Information über den Ausgang zu verlieren? Antwort: ja. Man verschaffe sich einen Vektor der Indizes (Plätze) in der Ausgangsfolge. Genauer • spanne den Vektor der Indizes auf. ci ← i • sortiere die ci mit den ai mit Diese Technik wird nachstehend beim Verfahren des Sortierens durch Auswahl demonstriert. Algorithmus 10: [1] : [2] : [3] : Sortieren durch Auswahl, Variante 1 for i ← 1 to n do ci ← i for i ← 1 to n−1 do [4] : x ← ai ♦ k ← i [5] : for j ← i + 1 to n do [6] : if aj < x then [7] : x ← aj [8] : ak ← ai ♦ ai ← x [9] : m ← ci ♦ ci ← ck ♦ ck ← m 3.6 ♦ k←j Beurteilung von Sortieralgorithmen Ehe wir noch weitere Algorithmen auf den Leser einstürmen lassen, ist es wohl angebracht, einen ersten — und hier dann auch einzigen — Gedanken auf die Frage zu verwenden: Welches der vielen Angebote sollte man denn nun auswählen? Sicher wird dabei das Kriterium Laufzeit eine Rolle spielen. Neben der schon bekannten O()-Betrachtung geht man oft so vor, daß man sich die wichtigsten Handlungen heraussucht und angibt, wie oft man sie bei den verschiedenen Algorithmen ausführen muß. Wie anstrengend das Sortieren ist, hängt sicher auch von den Daten ab. Man hat das Gefühl, daß es sicher nicht so aufwendig sein dürfte, einen bereits sortierten array zu sortieren. Oder merkt es der Algorithmus gar nicht? Die Datenabhängigkeit der Laufzeit versucht man in der Informatik so zu berücksichtigen, daß man zu gegebener Problemgröße jeweils das sogenannte worst case, best case und average case Verhalten angibt. Wir werden dies einmal an drei Sortieralgorithmen (Sortieren durch Zählen, Sortieren durch Einfügen, Sortieren durch Auswahl) demonstrieren. Für das Sortieren sind sicher 1. Vergleiche: zwei Elemente werden verglichen 2. Bewegungen: Elemente werden auf einen anderen Platz geschoben die wichtigen Aktionen. 3.6.1 Vergleiche Sortieren durch Zählen Listen wir uns zur Erinnerung noch einmal den Algorithmus von Seite 32 auf. Algorithmus 8: Sortieren durch Zählen 34 Sortieren [1] : for i ← 1 to n do [2] : ci ← 1 [3] : for i ← 1 to n−1 do [4] : for j ← i+1 to n do [5] : if ai < aj then cj ← cj + 1 [6] : else ci ← ci + 1 Die beiden Schleifen in Zeile [3] und [4] regeln das Durchlaufen der oberen Dreiecksmartix (siehe Seite 32). Für jede Position in der oberen Dreiecksmatrix wird ein Vergleich durchgeführt. Es gibt zu gegebenem n genau (n2 − n)/2 Positionen. Unabhängig von der Anordnung der Daten, werden immer diese Positionen durchlaufen. Bei diesem Algorithmus unterscheiden sich also die drei Verhalten nicht. Sortieren durch Auswahl Auch hier zur Erinnerung erst einmal der Algorithmus von Seite 28. Algorithmus 1: Sortieren durch Auswahl [1] : for i ← 1 to n−1 do [2] : x ← ai ♦ k ← i [3] : for j ← i + 1 to n do [4] : if aj < x then [5] : x ← aj [6] : ak ← ai ♦ ai ← x ♦ k←j Die beiden Schleifen in den Zeilen [1] und [3] des Algorithmus steuern wiederum die Anzahl der Vergleiche. Wir haben die gleiche Situation wie beim Algorithmus für das Sortieren durch Zählen. Alle drei Fälle sind gleich und haben den Wert (n2 − n)/2. Sortieren durch Einfügen Von den vielen Varianten, die wir betrachtet haben, wollen wir die nachstehende untersuchen. Algorithmus 6: Sortieren durch Einfügen, Variante 4 [1] : for i ← 2 to n do [2] : j←i−1 [3] : a0 ← ai [4] : while aj > a0 do [5] : aj+1 ← aj [6] : j←j−1 [7] : aj+1 ← a0 Dieser Algorithmus reagiert auf die Anordnung der Daten. Es ist daher eine Fallunterscheidung notwendig. Folge aufsteigend sortiert: Bei jeder sortierten Teilfolge der Länge k = 1, . . . , n−1 wird nach einem Vergleich abgebrochen. Damit werden für den vollständigen Sortiervorgang n − 1 Vergleiche benötigt. Sortieren 35 Folge absteigend sortiert: Bei jeder sortierten Teilfolge der Länge k = 1, . . . , n − 1 werden k + 1 Vergleiche benötigt. (Beachte den Vergleich mit a0 ). Der vollständige Sortiervorgang benötigt dann n−1 n X X n(n + 1) n2 + n − 2 (k + 1) = k= −1= 2 2 k=1 k=2 Vergleiche. sonst Wir wollen annehmen, daß alle möglichen Anordnungen der n Werte gleichwahrscheinlich sind. Bei einer sortierten Teilfolge der Länge k = 1, . . . , n − 1 werden wir dann im Mittel nach der Hälfte der insgesammt k + 1 möglichen Vergleiche unsere Entscheidung bezüglich des Platzes getroffen haben. (Man bekommt eine erste Ahnung: ganz ohne Statistik geht es nicht.) Damit ergibt sich für die vollständige Sortierung n−1 X k=1 n k+1 X k n(n + 1) 1 n2 + n − 2 = = − = 2 2 4 2 4 k=2 Zusammenfassung Damit können wir für die drei Algorithmen zusammenfassend die benötigten Anzahlen der Vergleiche angeben als: best case worst case average case durch Zählen n2 −n 2 n2 −n 2 n2 −n 2 durch Auswahl n2 −n 2 n2 −n 2 n2 −n 2 durch Einfügen n−1 n2 +n−2 2 n2 +n−2 4 Sortieren 3.6.2 Bewegungen Sortieren durch Zählen Der vorliegende Algorithmus sieht gar keine Bewegungen von Elementen vor. Berücksichtigen wir den Vorschlag auf Seite 32, dann haben wir in jeder Situation n Bewegungen. Sortieren durch Auswahl In Bezug auf die Anzahl der Bewegungen ist die Anordnung der Daten von Bedeutung. Es ist daher jetzt eine Fallunterscheidung angebracht. Folge aufsteigend sortiert: Bei jedem Durchgang sind 3 Bewegungen (für die Initialisierung und den am Ende durchgeführten Austauschen) erforderlich. Das führt beim vollständigen Sortiervorgang zu insgesamt 3(n − 1) Bewegungen. Folge absteigend sortiert: Bei jedem Durchgang sind immer die eben erwähnten 3 Bewegungen durchzuführen. Dazu kommen für die Durchgänge i = 1, . . . , (n/2) noch jeweils n − 2i + 1 Bewegungen. Nach diesen stehen alle Elemente am richtigen Ort. Dies sieht man an nachstehender Beweisskizze. a1 i= an i= an i= an > a2 > a3 1 n−i−i+1 = n − 1 < a2 > a3 3 n−i−i+1 = n − 3 < an−1 < a3 5 n−i−i+1 = n − 5 < an−1 < an−3 > a4 > ... > an−3 > an−2 > an−1 > an > a4 > ... > an−3 > an−2 > an−1 < a1 > a4 > ... > an−3 > an−2 < a2 < a1 < a4 > ... > an−3 < a3 < a2 < a1 36 Sortieren Damit hat man bei einem vollständigen Sortiervorgang 3(n − 1) + n/2 X (n − 2i + 1) = 3(n − 1) + i=1 = 3(n − 1) + n/2 X n i= (n + 1) − 2 2 i=1 n n ( + 1) n2 n (n + 1) − 2 2 2 = 3(n − 1) + 2 2 4 sonst Wir wollen annehmen, daß alle möglichen Anordnungen der n Werte gleich wahrscheinlich sind. Und dann dürfen uns hier die Kräfte verlassen. Die weitere Analyse erfordert doch eine so gehörige Portion Statistik, daß wir hier getrost auf die einschlägige Literatur verweisen wollen (siehe z.B. das angegebene Buch von Naeve [14]). Sortieren durch Einfügen Bei jedem Schritt ist eine Initialisierungsbewegung zu erledigen. Nach jedem Vergleich erfolgt auch eine Bewegung. Der Vergleich, der zum Abbruch führt, zieht das Einfügen nach sich. Wir haben also für jeden Schritt Anzahl Bewegungen = Anzahl Vergleiche + 1 Insgesamt haben wir für einen vollständigen Sortiervorgang n − 1 Schritte vorzunehmen. Damit ergibt sich für die Gesamtzahl der Bewegungen Gesamtzahl Bewegungen = Gesamtzahl Vergleiche + n−1 Zusammenfassung Damit können wir für die drei Algorithmen zusammenfassend die benötigten Anzahlen der Bewegungen angeben als: Sortieren best case worst case average case n n n durch Zählen durch Auswahl durch Einfügen 3.7 3(n − 1) 3(n − 1) + n2 4 3(n − 1) + n(ln n + γ) n2 +5n−6 4 2 n +3n−4 2 2(n − 1) Algorithmen fürs Sortieren durch Austausch Vorüberlegungen: • betrachte benachbarte Paare, stehen sie falsch, dann tausche aus • beginnt man bei den Indexpositionen (1,2) und fährt dann gleitend, d.h. (2,3) usw. fort, so rutschen die größeren Elemente (aufsteigend sortieren) nach rechts (n) und die kleineren nach links • man geht solange über die Folge, bis kein Austausch mehr stattfindet • ist bei einem Durchgang k die Indexposition der Paare (k,k+1) für die der letzte Austausch stattgefunden hat, so stehen alle Elemente mit Indizes >k richtig (im nachstehenden Beispiel durch * angedeutet). Im ersten Durchgang werden (7 ↔ 5), (7,12), (12 ↔ 11), (12 ↔ 3), (12,15) gegenübergestellt. Durch ↔ wird ein Austausch der Plätze angedeutet. 7 5 5 5 3 3 * 5 7 7 3 5 5 * 12 11 3 7 7 7 * 11 3 11 11 11 11 * 3 12 12 12 12 12 * 15 15 15 15 15 15 Ausgangsfolge 1. Durchgang 2. Durchgang 3. Durchgang 4. Durchgang sort. Folge Sortieren 37 Damit ergibt sich — direkt hingeschrieben — der nachstehende, in der Literatur Bubble-Sort genannte Algorithmus. k l Algorithmus 11: Index des letzten Austausches alter Index des zuletzt nach rechts gewanderten Elementes Sortieren durch Austausch [1] : k←n [2] : while k 6= 1 do [3] : l←1 [4] : for j ← 1 to k−1 do [5] : if aj > aj+1 then [6] : l ← j ♦ h ← aj [7] : aj ← aj+1 ♦ aj+1 ← h [8] : k←l Eine Illustration zur Namensgebung: 15 3 15 15 15 15 12 12 12 12 3 11 11 11 3 7 7 ◦ ◦ 11 ◦ ◦ 12◦ 5 7 3.8 ◦ ◦ ◦ ◦ ◦ ◦ ◦ ◦ ◦ 11◦ 7 7 5 5 ◦ ◦ ◦ ◦ ◦ 3 5 ◦ ◦ ◦ ◦ ◦ 5 3 Algorithmen fürs Sortieren durch Mischen Dieser Algorithmus entstammt der Arbeit On the composition of well-structured programs von N. Wirth[18]. Aufgabe 1: Sortiere n Elemente a1 , . . . ,an aufsteigend. ♥ N Erleichterung: Gehe von n = 2 aus. Damit schaffen wir uns das Problem von Teilfolgen unterschiedlicher Länge vom Hals. Lösungsidee: Ausgehend von sortierten 1-Tupeln (trivial) bilde nacheinander sortierte 2-Tupel, 4Tupel usw. bis ein sortiertes n-Tupel vorliegt. Man benötigt noch einmal den Platz für n Elemente, da man jeweils beim Erzeugen von 2×k-Tupeln aus k-Tupeln n Elemente von einem Ausgangsarray zu einem Zielarray mischen muß. Wir wollen für einen zu sortierenden Vektor der Länge 8 unser geplantes Vorgehen durch Angabe der beteiligten Indizes von Ausgangs- und Zielarray verdeutlichen. Erzeugung sortierter 2-Tupel aus (sortierten) 1-Tupeln: 38 Sortieren von sortiert nach (1, 8) aufwärts (1, 2) (2, 7) abwärts (7, 8) (3, 6) aufwärts (3, 4) (4, 5) abwärts (5, 6) Erzeugung sortierter 4-Tupel aus sortierten 2-Tupeln: von sortiert nach (1, 2, 7, 8) aufwärts (1, 2, 3, 4) (3, 4, 5, 6) abwärts (5, 6, 7, 8) Erzeugung sortierter 8-Tupel aus sortierten 4-Tupeln: von sortiert nach (1, 2, 3, 4, 5, 6, 7, 8) aufwärts (1, 2, 3, 4, 5, 6, 7, 8) Die Tupel sind dabei jeweils durch Unterstreichung gekennzeichnet. In einem Tupel sind die Elemente auf- oder absteigend sortiert. Es werden immer zwei Tupel gemischt, die eine gegenläufige Sortierung besitzen. Der nachstehende Algorithmus geht also von folgender Situation aus. In der Zeichnung sind ein Teil der benötigten Steuervariablen und ihre Verwendung bereits eingetragen. i k • ◦ ◦ • XX XXX XXX XX XXX XX? ? 9 z X • • ◦ ◦ j l Den zusätzlichen Platz denken wir uns als an+1 , . . . , a2n bezeichnet. Das Mischen erfolgt dann im Wechsel von (1,. . . ,n) nach (n+1,. . . ,2n) und umgekehrt. Als Konvention wollen wir einführen, daß die Indizes der zu mischenden Folgen immer mit i bzw. j bezeichnet werden und die der aufnehmenden Folgen mit k bzw. l. Auch wollen wir die Folgen entsprechend als i-, j-, h- bzw. l-Folgen bezeichnen. Die zunehmende Länge der sortierten Teilfolgen sei mit p bezeichnet. Die Richtung in der sortiert wird, gibt die boolesche Variable up (siehe die Planfigur) an. up p Algorithmus 12: gibt Mischrichtung an, up=true: von a1 , . . . , an nach an+1 . . . , a2n gibt Mischrichtung an, up=false: von an+1 , . . . , a2n nach a1 . . . , an Länge der sortierten Teilfolgen Top Level: Sortieren durch Mischen [1] : up ← true ♦ p ← 1 [2] : repeat [3] : << initialisiere i,j,k,l >> [4] : << mische i- und j-Folgen zu k- und l-Folgen >> [5] : up ← not up ♦ p ← 2*p [6] : until p = n Sortieren 39 Man kann auf den Zeiger l explizit verzichten, wenn man k entsprechend initialisiert und durch eine △ △ Inkrementvariable h (1 = k, -1 = l) richtig fortschreibt. m zählt die in einem Durchgang noch zu mischenden Elemente. m h Anzahl der noch zu mischenden Elemente gibt Mischrichtung an, h=1: von a1 , . . . , an nach an+1 . . . , a2n gibt Mischrichtung an, h=−1: von an+1 , . . . , a2n nach a1 . . . , an mische i- und j-Folgen zu k- und l-Folgen m ← n ♦ h ← 1 repeat mische i- und j-Folgen zu k- und l-Folgen mische i- und j-Folgen zu k- und l-Folgen Mischung zweier Folgen >> mische i- und j-Folgen zu k- und l-Folgen << mische zwei Folgen >> mische i- und j-Folgen zu k- und l-Folgen << schreibe Steuervariablen fort>> << initialisiere für mische i- und j-Folgen zu k- und l-Folgen until m = 0 Der Mischprozeß besteht aus dem eigentlichen Mischen — solange die i-Folge (Restlänge q) und die j-Folge (Restlänge r) noch beide Elemente enthalten — und dem Kopieren des Restes. mische zwei Folgen << mische solange beide Folgen noch voll>> << kopiere Rest >> mische zwei Folgen p q Anzahl der noch zu mischenden Elemente in i-Folge Anzahl der noch zu mischenden Elemente in j-Folge initialisiere für Mischungen zweier Folgen m ← m− 2*p ♦ q ← p ♦ r ← p mische solange beide Folgen noch voll while (q <> 0) and (r <> 0) do if ai < aj then mische solange beide Folgen nochmische voll solange beide Folgen noch voll := i + 1 ♦ q := q − 1 mische solange beide Folgen noch voll mische solange beide Folgen noch voll else ak := aj ♦ k := k + h ♦ j := j − 1 ♦ r := r − 1 kopiere Rest if q = 0 then {kopiere Rest des j-ten Laufs} while r > 0 do kopiere Rest kopiere Rest ak := aj ♦ k := k + h ♦ j := j − 1 ♦ r := r − 1 kopiere Rest else {r = 0, kopiere Rest des i-ten Laufs} kopiere Rest while q > 0 do kopiere Rest ak := ai ♦ k := k + h ♦ i := i + 1 ♦ q := q − 1 initialisiere i,j,k,l if up then i := 1 ♦ j := n ♦ k := n + 1 ♦ l := 2*n initialisiere i,j,k,l initialisiere i,j,k,l else initialisiere i,j,k,l ak := ai ♦ k := k + h ♦ i k := 1 ♦ l := n ♦ i := n + 1 ♦ j := 2*n 40 Sortieren schreibe Steuervariablen fort h := −h ♦ t := k ♦ k := l ♦ l := t Damit kommen wir zu dem in Pascal-Notation zusammengefaßtem Algorithmus, wobei die begin und end durch Einrücken ersetzt wurden. Algorithmus 12: Sort durch Mischen [1] : procedure mergesort; [2] : [3] : var i, j, k, l, t: index; h, m, p, q, r: integer; up: Boolean; [4] : {beachte, daß a 2*n Elemente besitzt} [5] : up := true, p := 1; [6] : repeat {für p = 1, 2, 4, . . .} [7] : [8] : [9] : [10] : if up then i := 1; j := n; k := n + 1; l := 2*n else k := 1; l := n; i := n + 1; j := 2*n [11] : h := 1; m := n; [12] : repeat {mische einen Lauf von i und j nach k} [13] : m := m − 2*p; q := p; r := p [14] : while (q <> 0) and (r <> 0) do [15] : if a[i].key < a[j].key then [16] : a[k] := a[i]; k := k + h; i := i + 1; q := q − 1 [17] : else [18] : [19] : a[k] := a[j]; k := k + h; j := j − 1; r := r − 1 if q = 0 then {kopiere Rest des j-ten Laufs} [20] : while r > 0 do [21] : [22] : a[k] := a[j]; k := k + h; j := j − 1; r := r − 1 else {r = 0, kopiere Rest des i-ten Laufs} [23] : while q > 0 do [24] : [25] : a[k] := a[i]; k := k + h; i := i + 1; q := q − 1 h := −h; t := k; k := l; l := t [26] : until m = 0; [27] : up := not up; p := 2*p [28] : until p >= n; Zwei Problem sind noch offen. Wo steht eigentlich die sortierte Folge, wenn der Algorithmus endet? Was machen wir in dem sehr wahrscheinliche Fall, wenn n keine Zweierpotenz ist? Nachstehende Veränderungen unseres Algorithmus sollten diese Problem lösen. Zuerst sorgen wir dafür, daß der sortierte Array immer auf den Plätzen 1 bis n steht. Der Algorithmus ist um die folgenden Zeilen zu ergänzen. Algorithmus 12: [29] : [30] : Verbesserung: Array auf Platz 1 bis n? if not up then for i := 1 to n do a[i] := a[i+n] Sortieren 41 Will man beliebige n zulassen, so ist der Schritt [13] wie unten angegebn zu ersetzen und die nachfolgenden Schritte einzufügen. Algorithmus 12: [13] : [14] : [15] : [16] : 3.8.1 Verbesserung: Berücksichtigung beliebiger n if m >= p then q := p else q := m; m := m − q; if m >= p then r := p else r := m; m := m − r; Zur Analyse von Algorithmen am Beispiel von mergesort Nachdem die Grundidee und eine filigrane Umsetzung des Sortierens durch Mischen beschrieben worden sind, wollen wir noch einige Überlegungen zu seiner Effizienz anstellen. Genauer wollen wir der Frage nachgehen, wie viele Schlüsselvergleiche nötig sind. Dabei werden wir herausfinden, daß die Anzahl der Vergleiche mit ca. n log n zu Buche schlägt und damit geringer ist als die der oben analysierten Algorithmen (Sortieren durch Auswahl, Einfügen und Zählen). Dieses Ergebnis ist bedeutsam, da n log n eine magische Grenze für Sortieralgorithmen, die auf Vergleichen beruhen, darstellt. Die nachstehenden Überlegungen lehnen sich an das Buch von Sedgewick und Flajolet an. Um nicht im Kleinklein zu ertrinken, wollen wir zur Beantwortung der Fragestellung eine rekursive Formulierung von mergesort betrachten: Algorithmus 13: [1] : Top Level: Sortieren durch Mischen, rekursive Fassung mergesort(a) [2] : << falls Länge von a gerade 1 ist, liefere a ab >> [3] : << teile a in der Mitte in die Hälften al und ar >> [4] : al ← mergesort(al); ar ← mergesort(ar) [5] : << bereite Mischschritt vor >> [6] : << mische al und ar zu a und liefere a ab >> Wesentlich interessiert uns der Mischschritt, da in diesem der zu beobachtende Vergleichsschritt steckt. In einer Schleife werden auf a in sortierter Weise die Elemente der schon sortierten Vektoren al und ar abgelegt. mische al und ar zu a und liefere a ab for k=1 to n do if al[il] < ar[ir]) then mische al und ar zu a und liefere mische a ab al und ar zu a und liefere a ab mische al und ar zu a und liefere a ab mische al und ar zu a und liefere a ab a[k] ← al[il]; il ← il+1 else a[k] ← ar[ir]; ir ← ir+1 mische al und ar zu a und liefere a ab return(a) Für diese Schleife sind einige Vorbereitungen notwendig. Damit keine Probleme bei der Mischung dadurch auftreten, daß das Ende von al oder ar erreicht ist, wollen wir al und ar mit der größten verfügbaren Zahl (MaxZahl) um ein Element verlängern. Außerdem sind il und ir zu initialisieren. bereite Mischschritt vor il ← ir ← 1 al[length(al)+1] ← MaxZahl 42 Sortieren bereite Mischschrittbereite vor Mischschritt vor ar[length(ar)+1] ← MaxZahl Die weiteren Verfeinerungen dürfte der Leser selbst aufschreiben können. Nun zu der Frage: Wie viele Vergleiche benötigt mergesort? Wir definieren Cn als die Anzahl der Vergleiche für einen Inputvektor der Länge n. Dann benötigen wir C⌊n/2⌋ Vergleiche für die Sortierung des einen Teils von a und C⌈n/2⌉ Vergleiche für die des anderen Teils. Zusätzlich sind in dem Mischschritt n Vergleiche erforderlich. Also gilt: Cn = C⌊n/2⌋ + C⌈n/2⌉ + n für n>1 und C1 = 0. Nehmen wir an, daß sich n darstellen läßt als Potenz von 2: n = 2l . Dann vereinfacht sich die Beziehung zu: C2l = C2l−1 · 2 + 2l Dividieren wir beide Seiten durch 2l , erhalten wir: C2l C l−1 = 2l−1 + 1 =: A 2l 2 Diese Rekursion können wir verfolgen, bis wir bei C1 angekommen sind: C2l−2 C20 C l−1 + 1 + 1 = ... = 0 + 1 + ...+ 1 = l A = 2l−1 + 1 = 2 2l−2 2 Zusammenfassend folgt: Cn = C2l = l · 2l = (log2 n) · n Falls n keine Potenz von 2 ist, ergibt sich ein etwas schlechteres Ergebnis und eine ungemütlichere Rechnerei. 3.9 Exkurs: Bandsortieren ... mit 3 Bändern Die klassische — aber nicht ganz korrekte — Antwort auf die Frage, wieviele Bänder braucht man zum Sortieren einer Datei, die sich auf einem Magnetband befindet, ist 3. Die nachstehende Zeichnung zeigt schematisch wie man dabei vorging. Im Verteil-Schritt wurden die sortierten Teilfolgen auf 2 Bänder verteilt, von dort wurden sie dann auf ein Band gemischt. Mit den dann an Zahl geringeren aber an Länge größeren entstandenen sortierten Teilfolgen wurden dann diese beiden Schritte wiederholt bis man nur noch eine Teilfolge (den sortierte Bestand) hatte. * Verteilen H HH HH j H HH HH HH j Mischen * * Verteilen H HH HH j H HH HH HH j Mischen * Sortieren 43 ... mit 4 Bändern Hat man 4 Bänder zur Verfügung, so kann man den Verteilungsschritt mit dem Mischschritt mischen. Das Schema wird an der nachstehenden Zeichnung deutlich. @ @ @ @ @ @ Mischen@ / Verteilen Mischen@ / Verteilen Mischen@ / Verteilen @ @ @ @ @ @ R @ R @ R @ Kommt dies etwa jemand bekannt vor? Richtig, es ist genau das Schema, das wir beim Algorithmus durch Mischen vorausgesetzt haben. 3.10 Algorithmen fürs Sortieren durch Verteilen Die gute alte Sortiermaschine als Software! 3.11 Quicksort Sei a1 , a2 , . . . , an−1 , an die zu sortierende Folge. Unterstellen wir, daß es uns gelingt, die Folge so umzuordnen, daß wir zwei Teilfolgen a1 , . . . , ak und am , . . . , an mit k ≤ m bekommen. Für die Teilfolgen gelte, daß alle Elemente der einen Teilfolge kleiner sind als alle Elemente der anderen Teilfolge. Über die Ordnung innerhalb einer Teilfolge ist nichts bekannt. Für die Elemente auf den Indexpositionen j mit k < j < m gilt ak ≤ aj und aj ≤ am . Außerdem ist ai = aj für k < i, j < m. Man erkennt, daß man durch fortgesetzte Anwendung dieser Idee zu einer sortierten Folge kommt. Es gilt zwei Probleme zu lösen. Zum einen muß man die Zerlegung in die zwei Teilfolgen bewerkstelligen, zum anderen sollte man merken, wann man mit der Teilerei aufhören sollte. Dieses Problem ist sicher das leichtere. Besteht eine Teilfolge nur aus einem Element, so ist sie sortiert, bei zwei Elementen kann man durch einfaches Vertauschen der Plätze eine sortierte Folge — durch direktes sortieren also — erzeugen. Wenden wir uns nun dem schwierigerem Problem der Zerlegung (Partition) zu. 3.11.1 Die Zerlegung Die Teilfolge wird durch die Indexpositionen l und r im Array a beschrieben. Der Algorithmus soll uns zwei Indexpositionen i und j liefern, die die beiden zu erstellenden Teilfolgen bestimmen. Die eine Teilfolge soll dabei durch den Indexbereich (l, j) und die andere durch (i, r) beschrieben sein. Gilt j < l bzw. r < i, so heißt dies, daß die entsprechende Teilfolge leer ist. Der Algorithmus bestimmt einen Teilungspunkt und stellt von rechts nach links und links nach rechts durch den Array laufend die oben beschriebene Ordnung her. l r i j Algorithmus 13: linker Eckpunkt der zu zerlegenden Folge rechter Eckpunkt der zu zerlegenden Folge linker Eckpunkt der rechten Teilfolge rechter Eckpunkt der linken Teilfolge Top Level: Partition 44 Sortieren [1] : procedure partition(a,l,r,i,j) [2] : <<bestimme den Teilungspunkt>> [3] : <<stelle Ordnung her>> k x Index des Teilungspunktes Wert von ak bestimme den Teilungspunkt k ← (l+r) div 2 x ← ak bestimme den Teilungspunkt stelle Ordnung her i ← l ♦ j ← r while i ≤ j do stelle Ordnungstelle her Ordnung her <<wenn Austausch nötig, mache ihn>> wenn Austausch nötig, mache ihn while ai < x do i ← i+1 while aj >x do j ← j−1 wenn Austausch nötig, machewenn ihn Austausch nötig, mache ihn if i ≤ j then wenn Austausch nötig, mache ihn <<vertausche ai , aj >> wenn Austausch nötig, mache ihn i ← i+1 ♦ j ← j−1 Die Verfeinerung von << vertausche . . . >> sei trotz vieler schmerzlicher Erfahrungen mit dieser Aufgabe in Klausuren zur Einführung in die Informatik dem Leser anvertraut. Damit erhalten wir die nachstehende Funktion durch Einsetzen der Verfeinerungsblöcke. Algorithmus 13: Partition [1] : procedure partition(a,l,r,i,j) [2] : k ← (l+r) div 2 [3] : x ← ak [4] : i ← l ♦ j ← r [5] : while i ≤ j do [6] : while ai < x do i ← i+1 [7] : while aj > x do j ← j−1 [8] : if i ≤ j then [9] : h ← ai ♦ ai ← aj [10] : i ← i+1 ♦ j ← j−1 3.11.2 ♦ aj ← h Eine erste Lösung Damit kommen wir zu einer ersten Lösung. Wir gehen dabei von der Verfügbarkeit von Mengenoperationen wie ∪ etc. aus. Algorithmus 14: Quicksort [1] : s ← {(1,n)} [2] : while s 6= ∅ do [3] : <<wähle ein Paar, d.h. ein Element (l,r)aus >> Sortieren 45 [4] : s ← s \ {ausgewähltes Element} [5] : if r−l ≤ 1 then [6] : [7] : <<sortiere direkt>> else [8] : partition(a,l,r,i,j) [9] : s ← s ∪ {(l,j),(i,r)} sortiere direkt if r−l = 1 then if al < ar then <<vertausche al und ar >> sortiere direkt sortiere direkt else skip 3.11.3 . . .und ihre rekursive Fassung Man kann den Algorithmus auch kurz und bündig in einer rekursiven Fassung angeben. Algorithmus 15: Quicksort [1] : procedure quicksort(a,l,r) [2] : if l < r then [3] : partition(a,l,r,i,j) [4] : quicksort(a,l,j) [5] : quicksort(a,i,r) 3.12 Heap-Sort 3.12.1 Definition eines Heap Eine Struktur folgender Art h1 H HH HH h2 h3 @ @ @ @ h4 h5 h6 h7 @ @ @ @ @ @ @ @ h8 h9 h10 h11 h12 h13 h14 h15 wobei für alle Elemente hi eine der Heap-Bedingungen h2i (1) hi ≤ h2i+1 (2) gilt, heißt Heap. hi ≥ h2i h2i+1 46 Sortieren 3.12.2 Die Idee Die Heap-Struktur läßt sich leicht für ein Sortierverfahren verwenden. Nehmen wir an, es liegt ein Heap der Heapbedingung (2) vor. Dann ist das maximale Element des Arrays auf der Indexposition 1 zu finden. Tauscht man es mit dem letzten Element des Arrays aus, so sind 1. alle Elemente auf den kleineren Indexpositionen kleiner als dieses Element und 2. sie bilden einen Heap mit der Heapbedingung (2), die nur an der ersten Stelle gestört sein kann, dann aber aber mit wenig Aufwand wieder repariert werden kann. Setzen wir diese Idee in einen Algorithmus um. Heapsort Algorithmus A: A1 : [Heap-Erzeugung] Gebe dem zu sortierenden Vektor eine Heapstruktur. A2 : [Sortierschritt] Führe beginnend mit k=n bis k=1 folgenden Aufgaben aus. Störe die Heapstruktur, d.h. tausche das erste mit dem k-ten Element aus. Stelle die Heap-Struktur innerhalb der ersten k−1 Elemente wieder her. 3.12.3 . . . und ihre Umsetzung Heap-Sort Algorithmus 16: [1] : <<Heap-Erzeugung>> [2] : <<Sortierteil>> Setzen wie die beiden Schritte um. Heap-Erzeugung Die Elemente auf Indexpositionen größer gleich [n/2] + 1 bilden einen Heap. Das ist trivial, denn für keinen Index j aus dieser Menge. existiert die Indexposition 2 × j bzw 2 × j + 1 innerhalb des Arrays. Nehmen wir sukzessive ein Element nach links (Indexposition 1) fortschreitend zum Heap hinzu. Innerhalb dieses “neuen” Heaps steht es immer auf “obersten” Indexposition, obwohl es meistens von der Größe her dort nicht stehen dürfte. Es ist also eine Reparatur nötig, die von der Procedur sift erledigt wird. Heap-Erzeugung l ← (n div 2) + 1 while l > 1 do Heap-Erzeugung Heap-Erzeugung Heap-Erzeugung l←l−1 sift(l,n) Sortierteil Sortierteil r ← n while r > 1 do Sortierteil Sortierteil Sortierteil x ← a1 ♦ a1 ← ar r ← r − 1 ♦ sift(1,r) ♦ ar ← x Sortieren 47 Das Ergebnis Damit haben wir den nachstehenden Algorithmus entwickelt: Algorithmus 16: Heap-Sort [1] : l ← (n div 2) + 1 [2] : while l > 1 do [3] : l←l−1 [4] : sift(l,n) [5] : r←n [6] : while r > 1 do [7] : x ← a1 ♦ a1 ← ar [8] : r ← r − 1 ♦ sift(1,r) 3.12.4 ♦ ar ← x Reparatur eines Heap – die Funktion sift Die Erstellung einer Heapstruktur funktioniert nach der Melodie: ich habe fast eine Heapstruktur, sie ist nur an der ersten Stelle zerstört. Daher wollen wir die Reparaturarbeiten jetzt abarbeiten. Nehmen wir an, wir haben einen gestörten Heap. Genauer: alles bildet einen Heap, nur das Element auf der ersten Position steht falsch. Der nachstehende Algorithmus sucht den passenden Platz für dieses Element, so daß der ganze array hinterher wieder ein Heap ist. In der Sportlersprache: das Element wird soweit wie nötig nach hinten durchgereicht. Der nachstehende Algorithmus geht davon aus, die Heap-Bedingung (2) liegt dem Heap zugrunde. Algorithmus 17: sift [1] : procedure sift(l,r) [2] : i ← l ♦ j ← 2×i ♦ x ← ai [3] : while j ≤r do [4] : if j < r then [5] : if aj < aj+1 then j ← j+1 [6] : if x ≥ aj then goto found [7] : ai ← aj [8] : ♦ i ← j ♦ j ← 2×i found: ai ← x Die Variation für den anderen Fall, daß Heap-Bedingung (1) dem Heap zugrunde liegt, ist rasch hingeschrieben. Algorithmus 18: sift [1] : procedure sift(l,r) [2] : i ← l ♦ j ← 2×i ♦ x ← ai [3] : while j ≤r do [4] : if j < r then [5] : if aj > aj+1 then j ← j+1 [6] : if x ≤ aj then goto found [7] : ai ← aj [8] : ♦ i ← j ♦ j ← 2×i found: ai ← x 48 3.12.5 Sortieren . . . und ein Beispiel Erstellung eines Heap: Verwendet wird die Heap-Bedingung (2). Mit einem * ist jeweils das oberste Element des bisherigen Heap gekennzeichnet. Das Indextrippel (i,2i,2i+1) ist jeweils durch ein + markiert. 1 21 21 2 33 33 3 41 41 4 15 15 21 33 41 21 33 21 *33 + 49 *41 + 41 *15 + 49 21 21 + 49 41 41 + 41 49 49 + 21 + 35 49 35 41 41 5 27 *27 + 27 6 *22 22 7 38 38 8 49 49 9 35 35 10 17 17 + 17 22 38 27 22 + 22 38 + 38 49 + 15 35 + 35 15 35 17 35 + 33 17 17 49 + 33 + 35 27 + 27 22 38 27 22 38 15 + 15 35 + 21 + 33 27 + 27 22 38 15 33 17 22 38 22 38 33 + 21 17 27 15 + 15 17 17 Sortieren: Verwendet wird weiterhin die Heap-Bedingung (2). Mit einem * ist das entfernte (größte) Element des Heap gekennzeichnet. Das Indextrippel (i,2i,2i+1) ist jeweils durch ein + markiert. 1 49 17 + 41 2 35 35 + 35 41 21 + 38 35 35 15 35 35 3 41 41 + 17 + 38 38 + 21 + 21 4 33 33 5 27 27 6 22 22 7 38 38 8 15 15 9 21 21 10 17 *49 33 27 21 *49 27 27 38 + 17 17 15 33 33 + 33 22 + 22 22 15 15 21 *41 *49 49 *41 49 27 17 + 17 15 33 22 + 22 *38 41 49 27 . . . und so weiter und so weiter . . . 3.13 Exkurs: Bandsortieren Beim Sortieren durch Mischen wird die Zahl der Durchläufe ganz wesentlich auch von der Länge der sortierten Teilfolgen in der Ausgangskonstellation beeinflußt. Bei einem internen Speicher, der eine Kapazität von N zu sortierenden Elementen hat, kann man — so scheint es — nur sortierte Teilfolgen der Länge N durch internes Sortieren bilden. Man kann zeigen, daß mit Hilfe der Heapstruktur in dieser Situation sortierte Teilfolgen mit einer mittleren Länge von 2 × N erzeugt werden können. Algorithmus 19: Sortieren 49 [1] : erzeuge im internen Speicher Heap mit kleinstem Element an der Spitze. [2] : repeat [3] : entferne Minimum und speichere es auf Band [4] : füge neues Element ein und stelle Heap wieder her [5] : until (Minimum < kleiner letztes “ausgelesene” Element) 3.14 Literatur [11] Alagić S., Arbib M. A.: The Design of Well-structured and Correct Programs [12] Gries D.: The Science of Programming [13] Knuth D. E.: The Art of Programming vol 3, chap. 5 [14] Naeve P.: Stochastik für Informatik R. Oldenbourg Verlag, 1995 [15] Sedgewick R., Flajolet P.: An Introduction to the Analysis of Algorithms Addison-Wesley, 1996 [16] Wirth N. : Algorithmen und Datenstrukturen [17] Wirth N. : Algorithms + Data Structures = Programs [18] Wirth N. : On the composition of well-structured programs Computing Review 50 Sortieren