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