0% fanden dieses Dokument nützlich (0 Abstimmungen)
11 Ansichten193 Seiten

Algorithmische-Mathematik Compress

Hochgeladen von

Diana Stephenson
Copyright
© All Rights Reserved
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen
0% fanden dieses Dokument nützlich (0 Abstimmungen)
11 Ansichten193 Seiten

Algorithmische-Mathematik Compress

Hochgeladen von

Diana Stephenson
Copyright
© All Rights Reserved
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen

Stefan Hougardy

Jens Vygen

Algorithmische
Mathematik
2. Auflage
Algorithmische Mathematik
Stefan Hougardy · Jens Vygen

Algorithmische Mathematik
2., korrigierte und erweiterte Auflage
Stefan Hougardy Jens Vygen
Forschungsinstitut für Diskrete Forschungsinstitut für Diskrete
Mathematik, Universität Bonn Mathematik, Universität Bonn
Bonn, Deutschland Bonn, Deutschland

ISBN 978-3-662-57460-7 ISBN 978-3-662-57461-4 (eBook)


[Link]

Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detail-
lierte bibliografische Daten sind im Internet über [Link] abrufbar.

Springer Spektrum
© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2016, 2018
Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Jede Verwertung, die nicht
ausdrücklich vom Urheberrechtsgesetz zugelassen ist, bedarf der vorherigen Zustimmung des Verlags.
Das gilt insbesondere für Vervielfältigungen, Bearbeitungen, Übersetzungen, Mikroverfilmungen und die
Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt
auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen-
und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden
dürften.
Der Verlag, die Autoren und die Herausgeber gehen davon aus, dass die Angaben und Informationen in
diesem Werk zum Zeitpunkt der Veröffentlichung vollständig und korrekt sind. Weder der Verlag noch
die Autoren oder die Herausgeber übernehmen, ausdrücklich oder implizit, Gewähr für den Inhalt des
Werkes, etwaige Fehler oder Äußerungen. Der Verlag bleibt im Hinblick auf geografische Zuordnungen und
Gebietsbezeichnungen in veröffentlichten Karten und Institutionsadressen neutral.

Verantwortlich im Verlag: Annika Denkert

Springer Spektrum ist ein Imprint der eingetragenen Gesellschaft Springer-Verlag GmbH, DE und ist ein Teil
von Springer Nature
Die Anschrift der Gesellschaft ist: Heidelberger Platz 3, 14197 Berlin, Germany
Vorwort

Seit es Computer gibt, nimmt die Bedeutung von Algorithmen in fast allen Bereichen
der Mathematik ständig zu. An der Universität Bonn wurde daher neben Analysis und
Linearer Algebra eine dritte Grundvorlesung für das erste Semester konzipiert: die Algo-
rithmische Mathematik. Dieses Buch gibt genau die Inhalte dieser Vorlesung wieder,
die die Autoren mehrfach gehalten haben, und die etwa 30 mal 90 Minuten (zuzüglich
Übungen) umfasst. Wir setzen nirgends mehr als Schulwissen voraus; dennoch ist das
Buch für Leser ohne mathematische Vorbildung anspruchsvoll.
Im Gegensatz zu den meisten anderen einführenden Büchern über Algorithmen, die
vielleicht eher auf Informatikstudenten abzielen, legen wir von Anfang an viel Wert auf
eine rigorose mathematische Vorgehensweise. Exakte Definitionen, präzise Sätze und
genau ausgearbeitete elegante Beweise sind gerade am Anfang eines Mathematikstudi-
ums unentbehrlich. Das Buch beinhaltet aber auch viele Beispiele, Erläuterungen und
Hinweise auf weiterführende Themen.
Bei der Auswahl der Themen haben wir darauf geachtet, ein möglichst breites Spek-
trum von Algorithmen und algorithmischen Fragestellungen zu zeigen, soweit dies ohne
tiefere mathematische Kenntnisse möglich ist. Wir behandeln Grundlagen (Kap. 1–3),
numerische Fragen (Kap. 4–5), Graphen (Kap. 6–7), Sortieralgorithmen (Kap. 8), Kom-
binatorische Optimierung (Kap. 9 und 10) sowie die Gauß-Elimination (Kap. 11). Dabei
sind die verschiedenen Themen oft miteinander verzahnt; die Reihenfolge kann daher
nicht ohne Weiteres verändert werden. Neben klassischen Algorithmen und deren Ana-
lyse wird der Leser wichtige theoretische Grundlagen, viele Querverbindungen und
sogar auch Hinweise auf offene Forschungsfragen entdecken.
Algorithmen wirklich zu verstehen und mit ihnen zu arbeiten ist kaum möglich, ohne
sie auch implementieren zu können. Parallel zu den mathematischen Themen führen wir
daher in diesem Buch in die Programmiersprache C++ ein. Wir bemühen uns dabei, die
technischen Details auf das Notwendige zu beschränken – dies ist kein Programmier-
kurs! – und dennoch das Buch auch für Studienanfänger ohne Programmiererfahrung
zugänglich zu machen.
Die von uns sorgfältig konzipierten Programmbeispiele sollen einerseits die wichtigs-
ten Elemente der Sprache C++ lehren und darüber hinaus zum Selbststudium anregen.

V
VI Vorwort

Andererseits sind sie aber auch stets so gewählt, dass sie thematisch den jeweiligen Stoff
ergänzen. Natürlich kann man nicht wirklich programmieren lernen, ohne es selbst zu
tun, ebenso wenig wie man Mathematik lernen kann, ohne selbst Aufgaben und Prob-
leme zu lösen. Dazu möchten wir alle Studienanfänger von Beginn an mit Nachdruck
ermuntern.
Wir wünschen allen Lesern viel Freude an der Algorithmischen Mathematik!

Bonn Stefan Hougardy


März 2015 Jens Vygen
Vorwort zur 2. Auflage

Neben viel positiver Resonanz, die wir zur ersten Auflage unseres Buches erhalten
haben, gab es mehrfach den Wunsch, dass eine Neuauflage Übungsaufgaben enthalte.
Diesem Wunsch sind wir nun nachgekommen und haben an das Ende eines jeden Kapi-
tels einen Abschnitt mit Übungsaufgaben hinzugefügt. Ansonsten ist die zweite Auflage
inhaltlich identisch mit der ersten Auflage; wir haben lediglich einige kleinere Fehler
behoben.
Hinweise auf noch verbliebene Fehler nehmen wir gerne entgegen.

Bonn Stefan Hougardy


März 2018 Jens Vygen

Anmerkungen zu den C++-Programmen

Dieses Buch enthält eine Reihe von Programmbeispielen in C++. Der Sourcecode aller
dieser Programme kann über die Webseiten der Autoren heruntergeladen werden. Wir
benutzen in diesem Buch die in ISO/IEC 14882:2011 [6] spezifizierte C++-Version, die
auch unter dem Namen C++11 bekannt ist. Zum Kompilieren der Programmbeispiele
eignen sich alle gängigen C++-Compiler, die diese C++-Version unterstützen. Beispiels-
weise unterstützt der frei verfügbare GNU C++ Compiler g++ ab der Version 4.8.1 alle
in diesem Buch benutzten Sprachelemente von C++11. Gute Lehrbücher zu C++11 sind
z. B. [5, 26, 33]. Ausführliche Informationen zu C++11 findet man auch im Internet,
z. B. unter [Link] oder [Link]

VII
Danksagung

Wir möchten uns an dieser Stelle bei allen bedanken, die uns im Laufe der Jahre Anre-
gungen und Verbesserungsvorschläge zu diesem Buch gegeben haben. Neben den Stu-
dierenden aus unseren Vorlesungen möchten wir uns hier insbesondere bei Christoph
Bartoschek, Ulrich Brenner, Helmut Harbrecht, Stephan Held, Dirk Müller, Philipp
­Ochsendorf, Jan Schneider und Jannik Silvanus bedanken.
Für Hinweise auf verbleibende Fehler und weitere Verbesserungsvorschläge sind wir
natürlich jederzeit dankbar.

Stefan Hougardy
Jens Vygen

IX
Symbolverzeichnis

| mit der Eigenschaft, dass


∃ es gibt (mindestens) ein
∀ für alle
∅ leere Menge
⊆ Teilmenge
⊂ echte Teilmenge
∪ Vereinigung von Mengen
∩ Schnitt von Mengen


∪˙ disjunkte Vereinigung von Mengen
symmetrische Differenz
× kartesisches Produkt
∧ logisches und
∨ logisches oder
⌈·⌉ obere Gaußklammer
⌊·⌋ untere Gaußklammer
≈ ungefähr gleich


← Zuweisung im Pseudocode
Transposition

XI
Inhaltsverzeichnis

1 Einleitung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1 Algorithmen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Berechnungsprobleme. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Algorithmen, Pseudocode und C++. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4 Einfacher Primzahltest. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.5 Sieb des Eratosthenes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.6 Nicht alles ist berechenbar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.7 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2 Darstellungen ganzer Zahlen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.1 b-adische Darstellung natürlicher Zahlen . . . . . . . . . . . . . . . . . . . . . . . . 25
2.2 Exkurs: Aufbau des Hauptspeichers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.3 b-Komplementdarstellung ganzer Zahlen . . . . . . . . . . . . . . . . . . . . . . . . 31
2.4 Rationale Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.5 Beliebig große ganze Zahlen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.6 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3 Rechnen mit ganzen Zahlen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.1 Addition und Subtraktion. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.2 Multiplikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.3 Euklidischer Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.4 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4 Approximative Darstellungen reeller Zahlen. . . . . . . . . . . . . . . . . . . . . . . . . 53
4.1 b-adische Darstellung reeller Zahlen. . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.2 Maschinenzahlen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.3 Rundung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.4 Maschinenzahlenarithmetik. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.5 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60

XIII
XIV Inhaltsverzeichnis

5 Rechnen mit Fehlern. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63


5.1 Binäre Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
5.2 Fehlerfortpflanzung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
5.3 Kondition. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
5.4 Fehleranalyse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.5 Newton-Verfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
5.6 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
6 Graphen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
6.1 Grundlegende Definitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
6.2 Wege und Kreise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
6.3 Zusammenhang und Bäume. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
6.4 Starker Zusammenhang und Arboreszenzen. . . . . . . . . . . . . . . . . . . . . . 81
6.5 Exkurs: Elementare Datenstrukturen. . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
6.6 Darstellungen von Graphen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
6.7 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
7 Einfache Graphenalgorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
7.1 Graphendurchmusterung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
7.2 Breitensuche. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
7.3 Bipartite Graphen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
7.4 Azyklische Digraphen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
7.5 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
8 Sortieralgorithmen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
8.1 Das allgemeine Sortierproblem. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
8.2 Sortieren durch sukzessive Auswahl. . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
8.3 Sortieren nach Schlüsseln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
8.4 Mergesort. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
8.5 Quicksort. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
8.6 Binäre Heaps und Heapsort. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
8.7 Weitere Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
8.8 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
9 Optimale Bäume und Wege. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
9.1 Optimale aufspannende Bäume. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
9.2 Implementierung von Prims Algorithmus. . . . . . . . . . . . . . . . . . . . . . . . 127
9.3 Kürzeste Wege: Dijkstras Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . 130
9.4 Konservative Kantengewichte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
9.5 Kürzeste Wege mit beliebigen Kantengewichten. . . . . . . . . . . . . . . . . . . 135
9.6 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
Inhaltsverzeichnis XV

10 Matching und Netzwerkflüsse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139


10.1 Das Matching-Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
10.2 Bipartites Matching. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
10.3 Max-Flow-Min-Cut-Theorem. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
10.4 Algorithmen für maximale Flüsse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
10.5 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
11 Gauß-Elimination. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
11.1 Die Operationen der Gauß-Elimination. . . . . . . . . . . . . . . . . . . . . . . . . . 151
11.2 LU-Zerlegung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
11.3 Gauß-Elimination mit rationalen Zahlen. . . . . . . . . . . . . . . . . . . . . . . . . 157
11.4 Gauß-Elimination mit Maschinenzahlen. . . . . . . . . . . . . . . . . . . . . . . . . 160
11.5 Matrixnormen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
11.6 Kondition linearer Gleichungssysteme . . . . . . . . . . . . . . . . . . . . . . . . . . 165
11.7 Übungsaufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169

Literaturverzeichnis. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171

Sachverzeichnis. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Einleitung
1

In diesem Kapitel werden wir einige grundlegende Begriffe einführen und erste Algorithmen
vorstellen, analysieren und in C++ implementieren. Zudem werden wir sehen, dass nicht
alles berechenbar ist.

1.1 Algorithmen

Der Begriff Algorithmus bezeichnet eine endliche Rechenvorschrift, für die Folgendes
spezifiziert ist:

• Was wird als Eingabe für den Algorithmus akzeptiert?


• Welche Rechenschritte werden in welcher Reihenfolge durchgeführt, gegebenenfalls in
Abhängigkeit von Eingabewerten oder Zwischenergebnissen?
• Wann stoppt der Algorithmus und was wird dann ausgegeben?

Für eine formale Definition benötigt man ein konkretes Rechnermodell wie etwa die
Turing-Maschine [36] oder eine Programmiersprache wie zum Beispiel C++ [32]. Wir wol-
len hier zunächst darauf verzichten und uns auf Beispiele beschränken.
Das Wort Algorithmus geht auf Mohammed ibn Musa Alchwarizmi (ca. 780–840)
zurück. Er revolutionierte die Mathematik in der westlichen Welt durch ein Buch über
das indische Zahlensystem und Rechenvorschriften (Addition, Subtraktion, Multiplikation,
Division, Bruchrechnen, Wurzelziehen); siehe [13, 37].

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 1


S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
2 1 Einleitung

Einige Algorithmen sind jedoch viel älter; zum Beispiel wurden das Sieb des Eratosthe-
nes, der euklidische Algorithmus und die Gauß-Elimination bereits vor über 2000 Jahren
beschrieben. Wir werden diese Algorithmen noch im Detail besprechen.
Algorithmen können von Menschen ausgeführt werden, in Hardware implementiert
werden (früher in mechanischen Rechenmaschinen, heute auf Computerchips) oder in Soft-
ware, d. h. als Computerprogramm in einer Programmiersprache geschrieben werden, das
dann übersetzt und auf einem universellen Mikroprozessor ausgeführt werden kann.
Die Algorithmische Mathematik ist der Teil der Mathematik, der sich mit dem Entwurf
und der Analyse von Algorithmen beschäftigt. Seit es Computer gibt, nimmt die Bedeu-
tung von Algorithmen in fast allen Bereichen der Mathematik ständig zu, von unzähligen
Anwendungen ganz zu schweigen. Die Numerische Mathematik und weite Teile der Dis-
kreten Mathematik sind dadurch überhaupt erst entstanden. Natürlich bilden Algorithmen
auch eine Grundlage der Informatik.

1.2 Berechnungsprobleme

Mit N, Z, Q und R bezeichnen wir wie üblich die Mengen der natürlichen Zahlen (ohne die
Null), der ganzen Zahlen, der rationalen Zahlen und der reellen Zahlen. Diese werden wir
hier ebenso wenig formal definieren wie Standardbegriffe aus der naiven Mengenlehre. Wir
benötigen aber darüber hinaus einige mathematische Grundbegriffe.

Definition 1.1 Seien A und B zwei Mengen. Dann ist deren kartesisches Produkt definiert
durch A × B: = {(a, b) | a ∈ A, b ∈ B}. Eine Relation auf (A, B) ist eine Teilmenge von
A × B.

Für eine Menge A und eine Relation R ⊆ A × A schreiben wir statt (a, b) ∈ R oft auch
kurz a Rb. Beispiele sind die Relation {(x, x) | x ∈ R}, die wir mit = bezeichnen, und die
Relation {(a, b) ∈ N × N | ∃c ∈ N mit b = ac}, die wir mit „ist Teiler von“ bezeichnen.

Definition 1.2 Seien A und B zwei Mengen und f ⊆ A × B, so dass es für alle a ∈ A
genau ein b ∈ B gibt mit (a, b) ∈ f . Dann heißt f eine Funktion (oder Abbildung) von A
nach B. Wir schreiben auch f : A → B. Statt (a, b) ∈ f schreiben wir f (a) = b oder auch
f : a  → b. Die Menge A heißt Definitionsbereich, die Menge B Wertebereich von f .
Eine Funktion f : A → B heißt injektiv, wenn f (a)  = f (a  ) für alle a, a  ∈ A mit
a  = a  gilt. Eine Funktion f : A → B heißt surjektiv, wenn für jedes b ∈ B ein a ∈ A
existiert mit f (a) = b. Eine Funktion heißt bijektiv (oder eine Bijektion), wenn sie injektiv
und surjektiv ist.
1.2 Berechnungsprobleme 3

Beispiel 1.3 Die Funktion f : N → N, die durch f (x) = 2 · x definiert sei, ist injektiv,
aber nicht surjektiv. Die Funktion g : Z → N, die durch g(x) = |x| + 1 definiert sei, ist
surjektiv, aber nicht injektiv. Die Funktion h : Z → N, die durch

2 · x, falls x > 0
h(x) =
−2 · x + 1, falls x ≤ 0

definiert sei, ist injektiv und surjektiv und damit bijektiv.

Definition 1.4 Für a, b ∈ Z bezeichne {a, . . . , b} die Menge {x ∈ Z | a ≤ x ≤ b}. Falls


b < a, so ist {a, . . . , b} die leere Menge {}, die wir aber meist mit ∅ bezeichnen.
Eine Menge A heißt endlich, wenn es eine injektive Funktion f : A → {1, . . . , n} für ein
n ∈ N gibt, andernfalls unendlich. Eine Menge A heißt abzählbar, wenn es eine injektive
Funktion f : A → N gibt, andernfalls überabzählbar. Die Anzahl der Elemente in einer
endlichen Menge A bezeichnen wir mit |A|.

Durch eine Funktion f : A → B können wir ausdrücken, dass ein Computerprogramm bei
Eingabe a ∈ A das Ergebnis f (a) ∈ B ausgibt. Nahezu alle Computer rechnen intern nur
mit Nullen und Einsen. Als Eingabe und Ausgabe erlauben wir der besseren Lesbarkeit
halber aber meist auch andere Zeichenketten:

Definition 1.5 Sei A eine nichtleere endliche Menge. Für k ∈ N ∪ {0} bezeichnen wir mit
Ak die Menge der Funktionen f : {1, . . . , k} → A. Ein Element f ∈ Ak schreiben wir oft
auch als Folge f (1) . . . f (k) und bezeichnen es als Wort (oder Zeichenkette) der Länge
k über dem Alphabet A. Das einzige Element ∅ von A0 nennen wir das leere Wort (es

hat Länge 0). Wir setzen A∗ : = k∈N∪{0} Ak . Eine Sprache über dem Alphabet A ist eine
Teilmenge von A∗ .

Damit können wir definieren:

Definition 1.6 Ein Berechnungsproblem ist eine Relation P ⊆ D × E, wobei es zu jedem


d ∈ D mindestens ein e ∈ E gibt mit (d, e) ∈ P. Wenn (d, e) ∈ P, dann ist e eine korrekte
Ausgabe für das Problem P mit Eingabe d. Die Elemente von D heißen Instanzen des
Problems.
P heißt eindeutig, wenn P eine Funktion ist. Sind D und E Sprachen über einem end-
lichen Alphabet A, so sprechen wir von einem diskreten Berechnungsproblem. Sind D
und E Teilmengen von Rm bzw. Rn für m, n ∈ N, so sprechen wir von einem numerischen
Berechnungsproblem. Ein eindeutiges Berechnungsproblem P : D → E mit |E| = 2 heißt
Entscheidungsproblem.
4 1 Einleitung

Mit anderen Worten: ein Berechnungsproblem ist genau dann eindeutig, wenn es für jede
Eingabe nur eine korrekte Ausgabe gibt. Kommen zudem nur 0 oder 1 (nein oder ja) als
Ausgabe in Frage, so handelt es sich um ein Entscheidungsproblem.
Computer können naturgemäß nur diskrete Berechnungsprobleme lösen. Für numerische
Berechnungsprobleme muss man sich auf Eingaben beschränken, die durch endliche Zei-
chenketten beschreibbar sind, und kann auch nur derartige Ausgaben produzieren; man muss
also Rundungsfehler in Kauf nehmen. Wir gehen darauf später ein und betrachten zunächst
diskrete Berechnungsprobleme. Zuvor präzisieren wir aber im nächsten Abschnitt den Be-
griff des Algorithmus und erläutern die Möglichkeit, Algorithmen mittels Pseudocode oder
mittels der Programmiersprache C++ zu spezifizieren.

1.3 Algorithmen, Pseudocode und C++

Algorithmen dienen dazu, Berechnungsprobleme zu lösen. Erste formale Definitionen des


Algorithmusbegriffs entstanden in der ersten Hälfte des 20. Jahrhunderts. So wurde der
Algorithmusbegriff von Alonzo Church 1936 mittels des sogenannten Lambda-Kalküls [7]
und von Alan Turing 1937 mit Hilfe der Turing-Maschinen [36] definiert. Es gibt heutzutage
eine Vielzahl weiterer Definitionen. Alle diese konnten jedoch als äquivalent nachgewiesen
werden, d. h. unabhängig davon, welche dieser Definitionen des Algorithmusbegriffs man
verwendet, bleibt die Menge der Berechnungsprobleme, die ein solcher Algorithmus lösen
kann, stets dieselbe.
Aus praktischer Sicht ist man natürlich meistens daran interessiert, einen Algorithmus
auf einem Computer ausführen zu können. Heutige Computer benötigen ihre Anweisun-
gen als Maschinencode. Da dieser Maschinencode für Menschen nur schwer verständlich
ist, benutzt man stattdessen sogenannte höhere Programmiersprachen. Diese Programmier-
sprachen erlauben es, Anweisungen für den Computer zu erstellen, die zum einen für einen
Menschen hinreichend leicht verständlich sind und zum anderen mit Hilfe eines sogenannten
Compilers in für den Computer verständlichen Maschinencode übersetzt werden können.
Nachfolgende Abbildung veranschaulicht die Schritte vom Entwurf eines Algorithmus bis
zur Ausführung auf einem Computer.
1.3 Algorithmen, Pseudocode und C++ 5

entwerfen

Algorithmus
lesbar für Menschen
(z.B. in Pseudocode)

implementieren

Computerprogramm lesbar für Menschen


(z.B. in C++) und Computer

kompilieren

Maschinencode
lesbar für Computer
(prozessorabhängig)

ausführen

Wir benutzen in diesem Buch die Programmiersprache C++, um Algorithmen zu spezifi-


zieren. Eine endliche Folge von Anweisungen, die den Regeln einer Programmiersprache
genügen, nennt man ein Programm. Für C++ und viele weitere Programmiersprachen kann
man zeigen, dass sich alle Algorithmen als Programme in der entsprechenden Programmier-
sprache schreiben lassen. Den Begriff des Algorithmus kann man daher auch alternativ über
C++ Programme definieren. Wir werden uns hier damit begnügen, eine gewisse Teilmenge
der Sprache C++ vorzustellen. Dabei werden wir auf eine formale Definition der Seman-
tik verzichten; siehe hierfür [32]. Stattdessen erklären wir die wichtigsten Elemente dieser
Programmiersprache anhand von Beispielen. Das Umsetzen einer algorithmischen Idee in
ein Programm nennt man implementieren.
Als eine weitere Möglichkeit, um Algorithmen zu spezifizieren, benutzen wir in diesem
Buch sogenannten Pseudocode. Dieser erlaubt es, die umgangssprachliche Formulierung
eines Algorithmus so weit zu präzisieren, dass sie für einen Menschen immer noch leicht
verständlich ist und eine Umsetzung in eine Programmiersprache nicht sehr aufwändig ist.
Zu beachten ist jedoch, dass wir in Pseudocode auch umgangssprachliche Formulierungen
zulassen, so dass es insbesondere nicht möglich ist, Pseudocode automatisch in Maschinen-
code oder in ein C++-Programm umzusetzen. Die Syntax und Semantik von Pseudocode
werden wir hier nicht formal definieren, da sie weitestgehend selbsterklärend ist und uns
der Pseudocode auch nur dazu dient, die einem Algorithmus zu Grunde liegende Idee auf-
zuschreiben.
6 1 Einleitung

Anhand eines sehr einfachen Algorithmus schauen wir uns zunächst eine Umsetzung
in Pseudocode an und implementieren den Algorithmus dann als C++-Programm. Wir
betrachten das Problem, das Quadrat einer natürlichen Zahl zu bestimmen. Wir wollen also
einen Algorithmus angeben, der die Funktion f : N → N berechnet, die durch f (x) = x 2
definiert ist. In Pseudocode können wir einen Algorithmus für dieses Problem wie folgt
aufschreiben:

Algorithmus 1.7 (Quadrat einer Zahl)


Eingabe: x ∈ N.
Ausgabe: das Quadrat von x.

antwort ← x · x
output antwort

Pseudocode kann (wie jedes Computerprogramm) Variablen enthalten; hier sind das neben
der Eingabe „x“ die Variable „antwort“. Der im Pseudocode benutzte Pfeil „←“ besagt,
dass wir den Wert des Ausdrucks, der rechts von dem Pfeil steht, der Variablen, die links
von dem Pfeil steht, zuweisen. In unserem Fall erhält also die Variable mit dem Namen
„antwort“den Wert des Ausdrucks x · x zugewiesen. Danach wird mit dem Befehl output
der in „antwort“ abgespeicherte Wert ausgegeben.
In C++ kann man diesen Algorithmus wie folgt implementieren:

Programm 1.8 (Quadrat einer Zahl)


1 // s q u a r e . cpp ( C o m p u t e the S q u a r e of an I n t e g e r )
2
3 # i n c l u d e < iostream >
4
5
6 int main ()
7 {
8 std :: cout << " E n t e r an i n t e g e r : " ;
9 int x ;
10 std :: cin >> x ;
11 int r e s u l t = x * x ;
12 std :: cout << " The s q u a r e of " << x << " is " << r e s u l t << " .\ n " ;
13 }

Wir erläutern hier nur kurz die Zeilen 8 bis 12 des Programms. Eine detailliertere Erklärung
aller Programmzeilen findet man in den Boxen C++ im Detail (1.1), C++ im Detail (1.2)
sowie C++ im Detail (1.3). In Zeile 8 wird zunächst ein kurzer Text auf den Bildschirm
ausgegeben, der dazu auffordert, eine ganze Zahl einzugeben. In Zeile 9 wird eine Variable
„x“definiert, die eine ganze Zahl repräsentiert. Die eingegebene Zahl wird in Zeile 10
1.4 Einfacher Primzahltest 7

eingelesen und in der Variablen „x“ gespeichert. In Zeile 11 wird eine weitere Variable mit
dem Namen „result“ definiert, und es wird ihr der Wert x · x zugewiesen. Schließlich
werden in Zeile 12 die eingegebene Zahl und das berechnete Ergebnis ausgegeben.

C++ im Detail (1.1): Die Grundstruktur eines Programms


Ein C++-Programm ist eine Zeichenkette, die den Regeln der Sprache C++ genügt.
Jedes C++-Programm muss eine Funktion mit dem Namen main enthalten. Diese
Funktion wird bei Programmstart als Erste ausgeführt. Dabei werden die Anweisungen
ausgeführt, die hinter int main() zwischen den geschweiften Klammern { und }
eingeschlossen sind. Ein kürzest mögliches C++-Programm sieht wie folgt aus:
int main (){}

Da zwischen den geschweiften Klammern nichts steht, tut dieses Programm nichts.
Eine Anweisung in C++ muss immer mit einem Semikolon beendet werden. Anwei-
sungen kann man innerhalb einer Zeile hintereinander schreiben, der besseren Lesbar-
keit halber empfiehlt es sich aber, pro Zeile höchstens eine Anweisung anzugeben, so
wie dies zum Beispiel in Programm 1.8 gemacht wurde. Leerzeichen und Leerzeilen
können an vielen Stellen in ein C++-Programm eingefügt werden, ohne die Funktion
des Programms zu ändern. Damit kann man zum Beispiel Blöcke von zusammengehö-
renden Anweisungen einrücken, um so die Lesbarkeit zu erhöhen. Ebenfalls wichtig
für die Lesbarkeit ist das Einfügen von Kommentaren in den Programmcode. Ein
Kommentar wird durch // eingeleitet. Der Compiler ignoriert alle Zeichen ab dem
// bis zum Zeilenende.
Wichtig ist noch zu wissen, dass C++ im Gegensatz zu einigen anderen Program-
miersprachen zwischen Groß- und Kleinschreibung unterscheidet. Daher ist zum Bei-
spiel int Main(){} kein gültiges C++-Programm.

1.4 Einfacher Primzahltest

Als erstes Beispiel für ein Entscheidungsproblem wollen wir testen, ob eine gegebene na-
türliche Zahl prim ist.

Definition 1.9 Eine natürliche Zahl n heißt prim, wenn n ≥ 2 ist und es keine natürlichen
Zahlen a und b gibt mit a > 1, b > 1 und n = a · b.
8 1 Einleitung

Wir betrachten also das Entscheidungsproblem {(n, e) ∈ N × {0,1} | e = 1 ⇔ n prim}.


Man beachte, dass hier N als Sprache über einem Alphabet aufgefasst wird, zum Beispiel
wie gewohnt über dem Alphabet {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}. Wir schreiben solche Probleme
meist wie folgt:

C++ im Detail (1.2): Ein- und Ausgabe


Ein Programm, das eine Funktion berechnet, benötigt als Eingabe ein Element des
Definitionsbereichs und liefert als Ausgabe den Funktionswert zurück. Im einfachsten
Fall lassen sich die Eingabe und die Ausgabe eines Programms über Tastatur und Bild-
schirm realisieren. Die Sprache C++ stellt im Rahmen der sogenannten Standardbi-
bliothek dazu Möglichkeiten bereit. Die Standardbibliothek ist eine sehr umfangreiche
Ansammlung von nützlichen Funktionalitäten. Um darauf zurückgreifen zu können,
muss man den Compiler anweisen, die entsprechend benötigten Teile dem eigenen
Programm zur Verfügung zu stellen. Der für die Ein- und Ausgabe zuständige Teil
der Standardbibliothek heißt iostream. Er wird über die Anweisung #include
<iostream> in Zeile 3 von Programm 1.8 eingebunden.
Um auf Funktionalitäten der Standardbibliothek zugreifen zu können, muss man
jeweils ein std:: voranstellen. Die Bildschirmausgabe erfolgt mittels std::cout,
indem man hinter den Outputoperator << das schreibt, was auf den Bildschirm ausge-
geben werden soll. Einen Text gibt man aus, indem man diesen in Anführungszeichen
einschließt. Dies geschieht beispielsweise in Zeile 8 von Programm 1.8. Werte von Va-
riablen gibt man durch Angabe des entsprechenden Variablennamens aus. Man muss
nicht jede einzelne Ausgabe mittels std::cout einleiten, sondern kann mehrere
direkt hintereinander folgende Ausgaben durch Aneinanderreihung mit dem <<-
Operator bewirken. Zeile 12 von Programm 1.8 zeigt dies. Hier wird zudem durch die
Ausgabe der Zeichenfolge \n ein Zeilenvorschub erzeugt.
Das Gegenstück zu cout ist cin. Mittels std::cin >> lassen sich über die
Tastatur eingegebene Werte einlesen. Auch hier kann man mehrere Einleseoperationen
direkt hintereinander ausführen. Zum Beispiel liest man mit der Anweisung
std :: cin >> x >> y >> n u m _ i t e r a t i o n s ;

Werte für die drei Variablen x, y und num_iterations ein.

Entscheidungsproblem 1.10 (Prim?)


Eingabe: n ∈ N.
Frage: Ist n prim?

Wir wollen nun einen Algorithmus entwerfen, der dieses Problem löst, das heißt bei Eingabe
von n ∈ N entscheidet, ob n prim ist. Ein solcher Algorithmus heißt auch Primzahltest.
1.4 Einfacher Primzahltest 9

C++ im Detail (1.3): Elementare Datentypen und Operatoren


Ein wichtiges Element jeder Programmiersprache ist die Möglichkeit, Variablen zu
definieren. Jede Variable in C++ hat einen festgelegten Typ. Die für uns wichtigsten
Typen sind int, bool und double. Der Datentyp int dient zum Abspeichern von
ganzen Zahlen. Eine Variable vom Typ bool kann nur zwei unterschiedliche Werte
speichern, nämlich true und false. Variablen vom Typ double dienen zum Spei-
chern von reellen Zahlen. Um eine Variable zu definieren, d. h. um sie dem Compiler
bekannt zu machen, gibt man wie zum Beispiel in Zeile 9 von Programm 1.8 einfach
den gewünschten Datentyp und den Namen der Variablen an. Man kann der Variablen
dabei auch direkt einen Wert zuweisen, wie es z. B. in Zeile 11 von Programm 1.8
geschieht. Schließlich kann man zudem auch mehrere Variablen vom gleichen Typ
in einer Anweisung definieren, indem man die Variablen wie im folgenden Beispiel
durch Kommata abtrennt:
int i , j = 77 , n u m b e r _ o f _ i t e r a t i o n s = j + 2 , f i r s t _ r e s u l t ;

Variablen dürfen in ihren Namen alle Buchstaben, Ziffern und das Zeichen „_“ ent-
halten. Ein Variablenname darf allerdings nicht mit einer Ziffer beginnen. Man sollte
Variablennamen stets so wählen, dass sie das Verständnis des Programms erleichtern.
Eine einzelne Variable oder die Verknüpfung von Variablen durch Operatoren nennt
man einen Ausdruck. Für die Datentypen int und double stehen neben den Rechen-
operatoren +, -, * und / für die Addition, Subtraktion, Multiplikation und Division
auch die Vergleichsoperatoren ==, <, >, <=, >= und != zur Verfügung. Letztere lie-
fern ein Ergebnis vom Typ bool und entsprechen den bekannten Relationen =, <,
>, ≤, ≥ und  =. Zu beachten ist, dass ein einfaches = in C++ nicht einen Vergleich
liefert, sondern für die Zuweisung eines Wertes an eine Variable benutzt wird. Aus-
drücke vom Typ bool lassen sich mit and und or logisch verknüpfen und mit not
negieren. Die Reihenfolge, in der Ausdrücke ausgewertet werden, kann mit Hilfe von
runden Klammern () geeignet festgelegt werden.

Ein nahe liegender (aber nicht sehr guter) Algorithmus ergibt sich direkt aus der Definition.
Man testet zunächst, ob n ≥ 2 ist. Ist dies der Fall, so testet man für alle natürlichen Zahlen a
und b mit 2 ≤ a, b ≤ n2 , ob n = a · b ist.
Dies erfordert bis zu ( n2 − 1)2 Multiplikationen. Auch wenn ein heutiger Computer meh-
rere Milliarden Rechenoperationen (einschließlich Multiplikationen) pro Sekunde ausfüh-
ren kann, wird die Rechenzeit beispielsweise für eine achtstellige Primzahl schon mehrere
Stunden betragen.
10 1 Einleitung

Natürlich geht es viel besser. Wir formulieren unseren Primzahltest zunächst mit Pseu-
docode:

Algorithmus 1.11 (Einfacher Primzahltest)


Eingabe: n ∈ N.
Ausgabe: die Antwort auf die Frage, ob n prim ist.

if n = 1 then antwort ← „nein“ else antwort ← „ja“



for i ← 2 to n do
if i ist Teiler von n then antwort ← „nein“
output antwort

Wir sehen hier zwei neue Elemente im Pseudocode. Zum einen eine sogenannte if-
Anweisung. Diese Anweisung dient dazu, andere Anweisungen in Abhängigkeit von dem
Wahrheitsgehalt einer Aussage auszuführen (in unserem Fall ist die Aussage „n = 1“ bzw.
„i ist Teiler von n“). Ist die Aussage wahr, so werden die Anweisungen im then-Teil aus-
geführt, ansonsten die Anweisungen im else-Teil. Dabei darf der else-Teil auch fehlen, dies
entspricht einem leeren else-Teil.
Die zweite neue Anweisung, die wir in diesem Beispiel benutzt haben, ist die
for-Anweisung. In der for-Anweisung werden einer Variable der Reihe nach unterschied-
liche Werte zugewiesen. In unserem Beispiel haben wir die Variable i, der die Werte von

2 bis  n zugewiesen werden. Für jeden dieser an die Variable zugewiesenen Werte wird
dann der Anweisungsblock ausgeführt, der eingerückt unter der for-Anweisung steht.
Es gibt noch zwei Stellen in Algorithmus 1.11, die der Erläuterung bedürfen. Folgende
Definitionen sind sehr nützlich:

Definition 1.12 Für x ∈ R definieren wir die untere bzw. obere Gaußklammer durch

x: = max{k ∈ Z | k ≤ x},


x: = min{k ∈ Z | k ≥ x}.

Für zwei natürliche Zahlen a und b definieren wir a mod b := a − b ·  ab .

Also bezeichnet a mod b den Rest der ganzzahligen Division von a durch b. Natürlich ist
b genau dann Teiler von a, wenn a mod b = 0 ist. Die Operationen a mod b und  ab 
können mit jedem modernen Computer in wenigen Taktzyklen ausgeführt werden, und
daher gelten sie als elementare Rechenoperationen. In C++ schreibt man hierfür a%b bzw.
a/b. Allerdings ist bei negativem a oder b zu beachten, dass a/b in C++ immer zur 0 hin
gerundet wird.
1.4 Einfacher Primzahltest 11

Ferner ist nicht offensichtlich, dass das Wurzelziehen durch eine elementare Rechenope-
ration realisiert werden kann; es lässt sich hier aber leicht vermeiden: wir müssen i genau
so lange um jeweils 1 erhöhen, bis i · i > n ist.
Wie gefordert, ist der Ablauf des Algorithmus für jede zulässige Eingabe wohldefiniert.
Man sieht unmittelbar, dass der Algorithmus stets terminiert (d. h. der Ablauf endet nach
endlich vielen Schritten) und die korrekte Ausgabe berechnet. Wir sagen auch, er berechnet
die Funktion f : N → {ja,nein}, wobei f (n) = ja genau dann, wenn n prim ist. Statt
{ja,nein} schreiben wir oft {true, false} oder {1,0}.

Die Anzahl der Rechenoperationen ist hier im Wesentlichen proportional zu n. Um
dies präzise auszudrücken, benutzt man die Landau-Symbole:

Definition 1.13 Sei g : N → R≥0 . Dann definieren wir:

O(g): = { f : N → R≥0 | ∃α ∈ R>0 ∃n 0 ∈ N ∀n ≥ n 0 : f (n) ≤ α · g(n)};


(g): = { f : N → R≥0 | ∃α ∈ R>0 ∃n 0 ∈ N ∀n ≥ n 0 : f (n) ≥ α · g(n)};
(g): = O(g) ∩ (g).

Statt f ∈ O(g) sagt man oft auch: f ist O(g), oder schreibt sogar: f = O(g). Die Benutzung
des Gleichheitszeichens ist hierbei anders als üblich nicht symmetrisch; dennoch hat sich
diese Notation durchgesetzt.

Bezeichnet g die Funktion n  → n und f (n) die Anzahl der Rechenoperationen von
Algorithmus 1.11 bei Eingabe n, so ist f ∈ O(g). Wir sagen auch, der Algorithmus hat

(asymptotische) Laufzeit O( n).

In diesem Fall gilt sogar, dass der Algorithmus Laufzeit ( n) hat, weil auch nie weniger

als n Rechenschritte ausgeführt werden. Man kann den Algorithmus 1.11 aber leicht
verändern, indem man die for-Anweisung abbricht, sobald ein Teiler gefunden wurde (so
werden wir ihn gleich auch implementieren). Diese Variante des Algorithmus terminiert
dann oft viel schneller, beispielsweise nach einer konstanten Anzahl Rechenoperationen für
alle geraden Zahlen n.
Bei der O-Notation spielen konstante Faktoren keine Rolle. Wir wissen ohnehin nicht
genau, wie viele Rechenoperationen ein Computerprogramm wirklich durchführen wird.
Dies kann vom Compiler und der Hardware abhängen. Dass Algorithmus 1.11 mit Laufzeit

O( n) aber schneller ist als der naive Algorithmus, dessen Laufzeit (n 2 ) beträgt, gilt –
unabhängig von den in der O-Notation versteckten Konstanten – jedenfalls für hinreichend
große n.
Es geht noch besser. Allerdings wurde erst 2002 ein Algorithmus gefunden dessen Lauf-
zeit O((log n)k ) für eine Konstante k ist [1, 38]. Dabei bezeichnet log oder log2 den Lo-
garithmus zur Basis 2. Den natürlichen Logarithmus, d. h. den Logarithmus zur Basis e
bezeichnen wir mit ln. Für die O-Notation ist es nicht von Bedeutung, welche Basis dem
Logarithmus zu Grunde liegt, da log n = (ln n) gilt.
12 1 Einleitung

Programm 1.14 (Einfacher Primzahltest)


1 // prime . cpp ( S i m p l e P r i m a l i t y Test )
2
3 # i n c l u d e < iostream >
4
5
6 bool i s _ p r i m e ( int n )
7 {
8 // n u m b e r s less than 2 are not p r i m e :
9 if ( n < 2) {
10 return false ;
11 }
12
13 // c h e c k all p o s s i b l e d i v i s o r s up to the square root of n :
14 for ( int i = 2; i * i <= n ; ++ i ) {
15 if ( n % i == 0) {
16 return false ;
17 }
18 }
19 return true ;
20 }
21
22
23 int g e t _ i n p u t ()
24 {
25 int n ;
26 std :: cout << " This p r o g r a m c h e c k s w h e t h e r a g i v e n i n t e g e r \\ is p r i m e .\ n "
27 << " En t e r an i n t e g e r : " ;
28 std :: cin >> n ;
29 return n;
30 }
31
32
33 void w r i t e _ o u t p u t ( int n , bool a n s w e r )
34 {
35 if ( a n s w e r ) {
36 std :: cout << n << " is prime .\ n " ;
37 }
38 else {
39 std :: cout << n << " is not prime .\ n " ;
40 }
41 }
42
43
44 int main ()
45 {
46 int n = g e t _ i n p u t ();
47 w r i t e _ o u t p u t (n , i s _ p r i m e ( n ));
48 }

Das Programm 1.14 stellt eine C++-Implementierung von Algorithmus 1.11 dar. Die Funkti-
on is_prime implementiert den eigentlichen Algorithmus; die beiden anderen Funktionen
get_input und write_output sorgen lediglich für die Ein- und Ausgabe. Wie man
einer Funktion in C++ Parameter übergeben kann und wie die Funktion Werte zurückliefern
1.5 Sieb des Eratosthenes 13

kann, ist in der Box C++ im Detail (1.4) genauer erklärt. Wie auch schon im Pseudocode
des Algorithmus 1.11 haben wir in der Funktion is_prime eine if-Anweisung und eine
for-Anweisung benutzt. Auf beides gehen wir in der Box C++ im Detail (1.5) genauer ein.

C++ im Detail (1.4): Funktionen


Funktionen in C++ entsprechen in gewisser Weise mathematischen Funktionen. Eine
Funktion in C++ erhält eine bestimmte Anzahl an Argumenten als Funktionspa-
rameter übergeben und liefert einen Ergebniswert zurück. Funktionen tragen we-
sentlich zur Strukturierung und besseren Lesbarkeit von Programmen bei. Geeig-
net gewählte Funktionsnamen wie z. B. write_output und is_prime in Pro-
gramm 1.14 suggerieren dem Leser sofort, was eine Anweisung wie write_output
(n, is_prime(n)) bewirkt, ohne dass er im Detail verstehen muss, wie die Funk-
tion is_prime implementiert ist.
Die Definition einer Funktion beginnt mit einem Typbezeichner, der angibt, wel-
chen Typ das Ergebnis der Funktion hat. Soll eine Funktion kein Ergebnis zurücklie-
fern, wie z. B. die Funktion write_output in Programm 1.14, so gibt man ihr den
Rückgabetyp void. Es folgt sodann der Name der zu definierenden Funktion, auf den
eine durch Kommata getrennte Liste der übergebenen Parameter in runden Klammern
folgt. Für jeden Parameter ist der Typ und der Name anzugeben. Schließlich folgt der
Anweisungsteil der Funktion, der durch geschweifte Klammern umschlossen wird.
Um einen Funktionswert zurückzuliefern benutzt man die return-Anweisung. So-
bald eine solche erreicht wird, wird die Funktion beendet und der hinter der return-
Anweisung stehende Ausdruck ausgewertet und als Ergebnis zurückgeliefert.

Der Ausdruck ++i in Zeile 14 von Programm 1.14 bewirkt die Erhöhung der Variablen i
um 1; stattdessen könnte man auch i = i + 1 schreiben.
Wir kümmern uns hier nicht darum, wie sich das Programm 1.14 verhält, wenn die Ein-
gabe keine natürliche Zahl ist (in der Praxis sollte man allerdings meist so programmieren,
dass unerwartete Eingaben abgefangen werden). Es sei aber bereits an dieser Stelle darauf
hingewiesen, dass Programm 1.14 nur korrekt funktioniert, wenn die Eingabezahl nicht zu
groß ist. Auf dieses Problem und eine Lösung gehen wir im nächsten Kapitel ein.

1.5 Sieb des Eratosthenes

Im vorigen Abschnitt haben wir ein Entscheidungsproblem behandelt. Wir geben nun ein
Beispiel für ein allgemeineres diskretes Berechnungsproblem:
14 1 Einleitung

Berechnungsproblem 1.15 (Liste von Primzahlen)


Eingabe: n ∈ N.
Aufgabe: Berechne alle Primzahlen p mit p ≤ n.

C++ im Detail (1.5): if und for


Die allgemeine Form einer if-Anweisung in C++ sieht wie folgt aus:
if (Bedingung) {Anweisungsblock1} else {Anweisungsblock2}
Bedingung ist dabei ein Ausdruck, der ein Ergebnis vom Typ bool liefert. Die runden
Klammern um diesen Ausdruck müssen zwingend gesetzt werden. Ist das Ergebnis
dieses Ausdrucks true, so werden die Anweisungen in Anweisungsblock1 ausge-
führt, ansonsten die Anweisungen in Anweisungsblock2. Ein Beispiel für eine if-
Anweisung sehen wir in den Zeilen 35–40 von Programm 1.14. Den else-Teil einer
if-Anweisung kann man auch weglassen. Dies entspricht einem else-Teil, in dem
Anweisungsblock2 leer ist. In den Zeilen 15–17 von Programm 1.14 sieht man ein
Beispiel. Falls einer der Anweisungsblöcke in einer if-Anweisung nur eine Anwei-
sung enthält, so kann man die geschweiften Klammern um diesen Anweisungsblock
auch weglassen.
Die allgemeine Form einer for-Anweisung ist die folgende:
for (Initialisierung; Bedingung; Ausdruck) {Anweisungsblock}
Bei Erreichen einer for-Anweisung wird zunächst die Initialisierung ausgeführt.
In dieser definiert man meist eine Variable, die eine Reihe von Werten durchlaufen
soll. Sodann wird getestet, ob Bedingung den Wert true ergibt. Ist dies der Fall, so
wird der Anweisungsblock durchlaufen und anschließend wird Ausdruck ausgewertet.
Ausdruck dient meist dazu, den Wert der in der Initialisierung definierten Variablen
zu verändern. Die letzten drei Schritte werden so lange wiederholt, bis Bedingung
den Wert false ergibt. Die in den Zeilen 14–18 von Programm 1.14 definierte
for-Schleife hat als Initialisierung int i = 2, als Bedingung i * i <= n und
als Ausdruck ++i. In diesem Beispiel sehen wir auch, dass aufgrund der return-
Anweisung im Anweisungsblock der Schleife eine for-Schleife vorzeitig beendet
werden kann, d. h. auch wenn Bedingung noch erfüllt ist.

Wollen wir ein Programm für dieses Problem schreiben, so können wir die Funktion
is_prime (und auch get_input) wiederverwenden: Dies führt unmittelbar zu einem

Algorithmus mit Laufzeit O(n n). Besser ist Algorithmus 1.16, der Sieb des Eratosthe-
nes genannt wird. Im Pseudocode dieses Algorithmus benutzen wir die Variable p, die ein
Vektor der Länge n ist. Der Zugriff auf die i-te Komponente dieses Vektors erfolgt mittels
p[i].
1.5 Sieb des Eratosthenes 15

Algorithmus 1.16 (Sieb des Eratosthenes)


Eingabe: n ∈ N.
Ausgabe: alle Primzahlen, die nicht größer als n sind.

for i ← 2 to n do p[i] ← „ja“


for i ← 2 to n do
if p[i] = „ja“ then
output i
for j ← i to  ni  do p[i · j] ← „nein“.

Satz 1.17 Algorithmus 1.16 ist korrekt und hat Laufzeit O(n log n).

Beweis Wir zeigen zunächst die Korrektheit: für alle k ∈ {2, . . . , n} ist k genau dann Teil
der Ausgabe, wenn k prim ist.
Sei k ∈ {2, . . . , n}. Wenn im Algorithmus jemals p[k] ← „nein“gesetzt wird, dann ist
k = i · j mit j ≥ i ≥ 2 und k somit nicht prim. Für Primzahlen k ∈ {2, . . . , n} gilt also
stets p[k] = „ja“; somit werden sie auch korrekt ausgegeben.
Wenn k nicht prim ist, so sei i der kleinste Teiler von k, der mindestens 2 ist. Sei j: = ki .
Offenbar ist 2 ≤ i ≤ j = ki ≤  ni  und i prim. Daher ist stets p[i] = „ja“, und die
Zuweisung p[i · j] ← „nein“ wird erreicht. Ab dann ist p[k] = „nein“, und das bleibt auch
so. Wenn später i den Wert k erreicht, wird die Zahl k daher nicht ausgegeben.
Die erste Zeile benötigt Laufzeit O(n). Der restliche Teil hat höchstens Laufzeit propor-
n n n 1 n 1
tional zu i=2 i =n i=2 i ≤ n 1 x dx = n ln n, also O(n log n). 

Tatsächlich ist die Laufzeit geringer, weil die innere Schleife nur für Primzahlen i ausgeführt

wird (es wird ja zunächst getestet, ob p[i] = „ja“ ist). Wegen p≤n : p prim 1p = O(log log n)
(dies werden wir hier nicht beweisen, ein elementarer Beweis findet sich z. B. in [35]), hat
das Sieb des Eratosthenes tatsächlich sogar Laufzeit O(n log log n).
Dieser Algorithmus wurde übrigens wohl nicht zuerst von Eratosthenes (ca. 276–194
v. Chr.) entdeckt, sondern noch früher. Programm 1.18 zeigt eine Implementierung in C++.
Die Funktion sieve implementiert dabei den eigentlichen Algorithmus. In dieser Funktion
definieren wir eine Variable is_prime vom Typ vector, um uns für jede Zahl zu merken,
ob sie prim ist oder nicht. Der Datentyp vector wird in der Box C++ im Detail (1.6)
genauer erklärt.
16 1 Einleitung

Programm 1.18 (Sieb des Eratosthenes)


1 // sieve . cpp ( E r a t o s t h e n e s ’ Sieve )
2
3 # i n c l u d e < iostream >
4 # i n c l u d e < vector >
5
6
7 void w r i t e _ n u m b e r ( int n )
8 {
9 std :: cout << " " << n ;
10 }
11
12
13 void sieve ( int n )
14 {
15 std :: vector < bool > i s _ p r i m e ( n + 1 , true ); // I n i t i a l i z e s v a r i a b l e s
16
17 for ( int i = 2; i <= n ; ++ i ) {
18 if ( i s _ p r i m e [ i ]) {
19 w r i t e _ n u m b e r ( i );
20 for ( int j = i ; j <= n / i ; ++ j ) {
21 i s _ p r i m e [ i * j ] = false ;
22 }
23 }
24 }
25 }
26
27
28 int g e t _ i n p u t ()
29 {
30 int n ;
31 std :: cout << " This p r o g r a m l i s t s all p r i m e s up to a gi v e n i n t e g e r .\ n "
32 << " En t e r an i n t e g e r : " ;
33 std :: cin >> n ;
34 return n;
35 }
36
37
38 int main ()
39 {
40 int n = g e t _ i n p u t ();
41 if ( n < 2) {
42 std :: cout << " T h e r e are no p r i m e s less than 2.\ n " ;
43 }
44 else {
45 std :: cout << " The p r i m e s up to " << n << " are : " ;
46 sieve ( n );
47 std :: cout << " .\ n " ;
48 }
49 }

Ein verwandtes Problem besteht darin, zu einer gegebenen Zahl n ∈ N die Primfaktorzer-
legung zu berechnen. Die Frage, wie schnell man dieses Problem lösen kann, ist von hoher
praktischer Bedeutung, weil Verschlüsselungsverfahren wie RSA auf der Annahme basie-
ren, dass man zu einer gegebenen Zahl n = p · q, wobei p und q große Primzahlen sind,
diese Faktoren nicht effizient finden kann.
1.6 Nicht alles ist berechenbar 17

C++ im Detail (1.6): Der abstrakte Datentyp vector


Ein vector dient dazu, eine Menge von Objekten gleichen Typs zu speichern.
Dieser Datentyp ist in der Standardbibliothek definiert und kann durch Einbinden
von #include <vector> genutzt werden. Einen vector definiert man durch
std::vector< Datentyp> Vektorname;, wobei Datentyp festlegt, welchen
Typ die Objekte haben, die in dem vector gespeichert werden sollen, und Vektorna-
me der Variablenname für den vector ist. Bei der Definition eines vectors kann
man optional ein oder zwei Parameter in runden Klammern angeben. Der erste Pa-
rameter gibt dabei an, wie viele Objekte der vector zu Beginn enthalten soll, und
der zweite Parameter gibt den Wert an, mit dem diese initialisiert werden sollen. So
liefern die folgenden Definitionen:
std :: vector < bool > i s _ p r i m e ;
std :: vector < int > p r i m e _ n u m b e r ( 1 0 0 ) ;
std :: vector < int > v (1000 ,7);

einen vector mit Namen is_prime, der 0 Elemente vom Typ bool enthält,
einen vector mit Namen prime_number, der 100 Elemente vom Typ int enthält,
sowie einen vector mit Namen v, der 1000 Elemente vom Typ int enthält, die alle
den Wert 7 haben. Die Elemente eines vectors sind konsekutiv von 0 ausgehend
durchnummeriert. Der oben definierte vector v enthält also 1000 Elemente mit den
Indizes 0 bis 999. Auf den Wert eines Elements, das den Index i in einem vector
hat, kann man mittels Vektorname[i] zugreifen. Z. B. liefert v[99] das 100ste
Element des oben definierten vectors v.

1.6 Nicht alles ist berechenbar

Mit Algorithmen (bzw. C++-Programmen) kann man vieles berechnen, aber nicht alles. Wir
wollen zunächst zeigen, dass jede Sprache, insbesondere auch die der C++-Programme,
abzählbar ist. Hierzu benötigen wir:

Lemma 1.19 Sei k ∈ N und l ∈ N. Definiere f l : {0, . . . , k − 1}l → {0, . . . , k l − 1} durch



f l (w) := li=1 ai k l−i für alle w = a1 . . . al ∈ {0, . . . , k − 1}l . Dann ist f l wohldefiniert
und bijektiv.


Beweis Für w ∈ {0, . . . , k − 1}l ist 0 ≤ f l (w) ≤ li=1 (k − 1)k l−i = k l − 1, also ist f l
wohldefiniert.
Wir zeigen nun die Injektivität von f l . Seien w, w ∈ {0, . . . , k − 1}l mit w = a1 . . . al ,
w = a1 . . . al und w  = w . Dann gibt es einen kleinsten Index j mit 1 ≤ j ≤ l so

18 1 Einleitung

l
dass a j  = a j . Sei ohne Einschränkung a j > a j . Dann gilt: f l (w ) =  l−i =
i=1 ai k
 j−1  l−i   
+ a j k l− j + li= j+1 ai k l−i . Wegen i=1 ai k l−i = i=1 ai k l−i , a j k l− j ≤
j−1 j−1
i=1 ai k
 
(a j − 1)k l− j und i= j+1 ai k l−i ≤ i= j+1 (k − 1)k l−i = k l− j − 1 erhalten wir somit
l l
 j−1  j−1
f l (w ) ≤ i=1 ai k l−i + (a j − 1)k l− j + k l− j − 1 = i=1 ai k l−i + a j k l− j − 1 < f l (w).
Es gilt also f l (w )  = f l (w).
Da |{0, . . . , k − 1}l | = k l = |{0, . . . , k l − 1}|, muss f l auch surjektiv sein. 

Satz 1.20 Sei A eine nichtleere endliche Menge. Dann ist die Menge A∗ abzählbar.

Beweis Sei f : A → {0, . . . , |A| − 1} eine Bijektion. Dann definieren wir eine Funktion
 l
g : A∗ → N durch g(a1 . . . al ) := 1 + l−1 i=0 |A| +
i
i=1 f (ai )|A|
l−i . Wir behaupten,

dass g injektiv ist. Dies folgt aber direkt aus Lemma 1.19, weil g die Menge der Wörter der
 l
Länge l bijektiv auf {1 + l−1 i=0 |A| , . . . ,
i
i=0 |A| } abbildet.
i 

Korollar 1.21 Die Menge aller C++-Programme ist abzählbar.

Beweis C++-Programme sind Wörter über einem endlichen Alphabet. Die Aussage folgt
somit aus Satz 1.20, da Teilmengen abzählbarer Mengen abzählbar sind. 

Damit können wir nun zeigen:

Satz 1.22 Es gibt Funktionen f : N → {0,1}, die von keinem C++-Programm berechnet
werden.

Beweis Sei P die (nach Korollar 1.21 abzählbare) Menge der C++-Programme, die eine
Funktion f : N → {0,1} berechnen. Sei g : P → N eine injektive Funktion. Für P ∈ P sei
f P die von P berechnete Funktion.
Betrachte eine Funktion f : N → {0,1} mit f (g(P)): = 1 − f P (g(P)) für alle P ∈ P .
Eine solche existiert, da g injektiv ist. Offenbar ist f  = f P für alle P ∈ P , und somit wird
f von keinem C++-Programm berechnet. 

Dies ist im Wesentlichen Cantors Diagonalbeweis, der auch zeigt, dass R überabzählbar
ist. Man sieht, dass tatsächlich nur „wenige“ Funktionen berechenbar sind, nämlich nur
abzählbar viele unter den insgesamt überabzählbar vielen.
Unter den nicht berechenbaren Funktionen sind durchaus auch interessante, die sich
konkret angeben lassen. Wir stellen nun das berühmteste Beispiel vor.
1.6 Nicht alles ist berechenbar 19

Sei Q die Menge aller C++-Programme, die eine natürliche Zahl als Eingabe akzeptieren.
Sei g : N → Q eine surjektive Funktion. Da nach Korollar 1.21 Q abzählbar ist, existiert
eine injektive Funktion f : Q → N, und wir können g( f (Q)): = Q für Q ∈ Q setzen, und
g(n): = Q 0 für ein beliebiges Q 0 ∈ Q, wenn n ∈
/ { f (Q) | Q ∈ Q}.
Definiere eine Funktion h : N × N → {0,1} durch

1 falls g(x) bei Eingabe von y nach endlich vielen Schritten hält;
h(x, y) =
0 sonst.

Dieses Entscheidungsproblem wird als das Halteproblem bezeichnet. Die Funktion h heißt
daher auch Haltefunktion.

Satz 1.23 Die Haltefunktion h wird von keinem C++-Programm berechnet.

Beweis Angenommen, es gäbe ein solches Programm P, das h berechnet. Dann gibt es
auch ein Programm Q, das bei Eingabe von x ∈ N zunächst die Funktion h(x, x) berechnet,
und das anhält, falls h(x, x) = 0, und in eine Endlosschleife läuft, falls h(x, x) = 1.
Sei q ∈ N mit g(q) = Q. Nach Definition von h ist h(q, q) = 1 genau dann, wenn g(q)
bei Eingabe von q terminiert. Nach Konstruktion von Q ist aber h(q, q) = 0 genau dann,
wenn Q bei Eingabe von q terminiert. Dies ist ein Widerspruch; P existiert also nicht. 

Dieser Satz gilt natürlich ebenso für jede andere Programmiersprache, und auch allgemein
für Algorithmen. Man sagt auch, das Halteproblem ist nicht entscheidbar.
Es ist auch in der Praxis keineswegs immer einfach zu sehen, ob ein Programm stets
terminiert. Ein gutes Beispiel ist der folgende Algorithmus:

Algorithmus 1.24 (Collatz-Folge)


Eingabe: n ∈ N.
Ausgabe: Angabe, ob die Collatz-Folge von n den Wert 1 erreicht.

while n > 1
if n mod 2 = 0
then n ← n/2
else n ← 3 · n + 1
output „Collatz-Folge erreicht den Wert 1“
20 1 Einleitung

Bis heute ist nicht bekannt, ob dieser einfache Algorithmus immer terminiert. Diese Frage
ist als das Collatz-Problem bekannt. Eine Implementierung von Algorithmus 1.24 in C++
zeigt das Programm 1.25:

Programm 1.25 (Collatz-Folge)


1 // c o l l a t z . cpp ( C o l l a t z S e q u e n c e )
2
3 # i n c l u d e < iostream >
4
5 using myint = long long ;
6
7 myint g e t _ i n p u t ()
8 {
9 myint n ;
10 std :: cout << " This p r o g r a m c o m p u t e s the C o l l a t z s e q u e n c e for an "
11 << " i n t e g e r .\ n " << " E n t e r an i n t e g e r : " ;
12 std :: cin >> n ;
13 return n;
14 }
15
16
17 int main ()
18 {
19 myint n = g e t _ i n p u t ();
20 while ( n > 1) {
21 std :: cout << n << " \ n " ;
22 if ( n % 2 == 0) {
23 n = n / 2;
24 }
25 else {
26 n = 3 * n + 1;
27 }
28 }
29 std :: cout << n << " \ n " ;
30 }

Wir haben im Programm den Datentyp long long statt int benutzt (und mit der
using-Anweisung als myint abgekürzt), weil damit auch größere ganze Zahlen gespei-
chert werden können. Den größten darstellbaren Wert kann man mit Hilfe der Funktion
std::numeric_limits<myint>::max() ermitteln, die in <limits> definiert
ist. Das Ergebnis kann von dem verwendeten Compiler abhängen, der Wert ist aber meist
263 − 1.
Für das Programm 1.25 verhindert aber auch die Verwendung des Datentyps long
long nicht unbedingt, dass der darstellbare Zahlenbereich überschritten wird und ein soge-
nannter Überlauf stattfindet. Im nächsten Kapitel werden wir sehen, wie man noch größere
Zahlen darstellen kann.
1.7 Übungsaufgaben 21

C++ im Detail (1.7): Die while-Anweisung


Die allgemeine Form der while-Anweisung sieht wie folgt aus:
while (Bedingung ) {Anweisungsblock }
Die Bedingung wird ausgewertet und falls sie den Wert true ergibt, so wird der
Anweisungsblock ausgeführt, ansonsten wird im Programmcode hinter dem Anwei-
sungsblock fortgefahren. Dieser Ablauf wiederholt sich so lange, bis Bedingung den
Wert false ergibt. Alternativ gibt es die do while-Anweisung:
do {Anweisungsblock } while (Bedingung );
Diese verhält sich entsprechend, nur dass hier erst der Anweisungsblock ausgeführt
wird und dann die Bedingung getestet wird.

Als ein weiteres neues Sprachelement haben wir in den Zeilen 20–28 von Programm 1.25
eine while-Anweisung verwendet. Diese wird genauer in der Box C++ im Detail (1.7)
erklärt.
Für Zahlen bis 5 · 260 ist experimentell nachgewiesen worden, dass die Collatz-Folgen
endlich sind und somit obiges Programm für solche Eingaben terminiert [28].

1.7 Übungsaufgaben

1. Es seien A und B zwei nichtleere Mengen. Beweisen Sie: es gibt eine injektive Ab-
bildung f : A → B genau dann, wenn es eine surjektive Abbildung g : B → A
gibt.
2. Es seien m und n zwei natürliche Zahlen. Außerdem seien A := {1, . . . , m} und B :=
{1, . . . , n}. Bestimmen Sie in Abhängigkeit von m und n die Zahl der …
a) Abbildungen von A nach B.
b) injektiven Abbildungen von A nach B.
c) bijektiven Abbildungen von A nach B.
d) Relationen auf (A, B).
3. Es sei A eine endliche Menge und f : A → A eine Funktion. Für alle a ∈ A sei
f 0 (a) := a und f i (a) := f ( f i−1 (a)) für i ∈ N. Zeigen Sie, dass folgende Aussagen
äquivalent sind:
a) f ist injektiv.
b) f ist surjektiv.
c) Es gibt ein i ∈ N mit f i = f 0 .
Welche der sechs Implikationen gelten auch, wenn A unendlich ist?
4. Zeigen Sie, dass Z × N abzählbar ist.
22 1 Einleitung

5. Für eine Menge A bezeichnen wir mit 2 A die Potenzmenge von A, d. h. die Menge aller
Teilmengen von A. Es sei nun A eine endliche Menge.
a) Beweisen Sie |2 A | = 2|A| .
b) Sei R := {(B, C) ∈ 2 A × 2 A | B ⊆ C}. Zeigen Sie |R| = 3|A| .
6. Für natürliche Zahlen b und k sei W (b, k) die Menge aller Wörter der Länge k
über dem Alphabet {1, . . . , b}. Zeigen Sie, dass es eine bijektive Funktion ϕ von
{1, . . . , |W (b, k)|} nach W (b, k) gibt, so dass sich für alle i ∈ {1, . . . , |W (b, k)| − 1}
die Wörter ϕ(i) und ϕ(i + 1) nur an genau einer Stelle unterscheiden.
7. Modifizieren Sie das Programm [Link] (Programm 1.8) so, dass es
a) die dritte Potenz von x statt des Quadrats von x ausgibt.
b) die k-te Potenz x k ausgibt, wobei k ∈ N eine zusätzliche Eingabe ist. (Sie dürfen
davon ausgehen, dass die Eingabewerte hinreichend klein sind, so dass x k < 109
ist.)
c) testet, ob x eine Quadratzahl ist, d. h. x = a 2 für ein a ∈ N ist, und zwar ohne die
C++-Funktion std::sqrt (für eine Näherung der Quadratwurzel) zu benutzen.

8. Seien a, b ∈ N. Beweisen Sie, dass b genau dann Teiler von a ist, wenn a mod b = 0
ist.
9. Überprüfen Sie anhand von sinnvollen Beispielen, wie Ihr C++-Compiler Ausdrücke
der Form a%b und a/b auswertet, wenn a und b Variablen vom Typ int sind, von
denen mindestens eine nicht positiv ist. Geben Sie drei aufschlussreiche Beispiele an.
Finden Sie anschließend mathematische Formeln für den Wert von a%b und a/b, die
dem Verhalten Ihres Compilers entsprechen.
10. Beweisen Sie folgende Aussagen:
a) Für alle Funktionen f, g : N → R≥0 gilt: f = O(g) ⇔ g = ( f ).
b) Für alle Funktionen f, g, h : N → R≥0 mit f = O(g) und g = O(h) gilt
f = O(h).
11. Beweisen Sie:
a) log n = (ln n)
b) log(n!) = (n log n)
12. Beweisen Sie, dass für alle k, l ∈ N gilt:
a) (log

n)k = O(n 1/l )
b) 2 log n = O(n 1/l )
n
13. Es sei z ∈ R≥0 eine Konstante. Zeigen Sie, dass dann gilt: i z = (n 1+z ).
i=1
14. Schreiben Sie ein C++-Programm, das alle Primzahlzwillinge (also Paare von Primzah-
len, deren Differenz 2 ist) auflistet, die kleiner als 106 sind. Wie viele gibt es?
15. Schreiben Sie ein C++-Programm, das zu einer gegebenen natürlichen Zahl n ≥ 2 ihre

Primfaktorzerlegung berechnet. Das Programm soll Laufzeit O( n) haben.
1.7 Übungsaufgaben 23

16. Die (starke) Goldbachsche Vermutung besagt, dass jede gerade natürliche Zahl außer
2 die Summe zweier Primzahlen ist. Schreiben Sie ein C++-Programm, das zu zwei
gegebenen natürlichen Zahlen a und b mit 2 < a < b diese Vermutung für alle geraden
Zahlen in {a, . . . , b} überprüft.
17. Zeigen Sie, dass die Potenzmenge von N (vgl. Aufgabe 5) überabzählbar ist.
Hinweis: Verfahren Sie ähnlich wie im Beweis von Satz 1.22.
18. Erklären Sie, warum nur abzählbar viele unter den überabzählbar vielen Funktionen
berechenbar sind.
19. Schreiben Sie Programm 1.25 (Collatz-Folge) so um, dass es dasselbe tut, aber ohne
eine while-Anweisung auskommt.
20. Finden Sie eine Zahl zwischen 1 und 1000, bei deren Eingabe die Collatz-Folge mög-
lichst lang ist.
21. Ersetzen Sie im Programm 1.25 (Collatz-Folge) in Zeile 26 die Anweisung
n = 3 * n + 1; durch
a) n = n + 1;. Zeigen Sie, dass das Programm dann stets terminiert, und geben Sie (mit
Hilfe der O-Notation) eine möglichst gute Schranke für die Zahl der Rechenschritte
an.
b) n = n + 3;. Für welche Startwerte terminiert das Verfahren dann? Beweisen Sie die
Korrektheit Ihrer Antwort.
Darstellungen ganzer Zahlen
2

Ein Computer speichert alle Informationen mit Bits, die jeweils 0 oder 1 sein können. Ein
Byte besteht aus acht Bits. Wir sind anfangs davon ausgegangen, dass wir alle auftretenden
natürlichen Zahlen im Datentyp int speichern können. Tatsächlich ist das nicht der Fall.
Eine Variable vom Typ int entspricht einer Folge von normalerweise 4 Bytes. Damit sind
natürlich nur 232 verschiedene Zahlen darstellbar. Wir lernen in diesem Kapitel, wie ganze
Zahlen gespeichert werden.

2.1 b-adische Darstellung natürlicher Zahlen

Natürliche Zahlen werden in der Binärdarstellung (auch 2-adische Darstellung genannt)


gespeichert. Diese ist analog zur gewohnten Dezimaldarstellung. Beispielsweise ist

106 = 1 · 102 + 0 · 101 + 6 · 100


= 1 · 26 + 1 · 25 + 0 · 24 + 1 · 23 + 0 · 22 + 1 · 21 + 0 · 20

in Binärdarstellung 1101010. Statt 10 oder 2 könnte man auch jede andere natürliche Zahl
b ≥ 2 als Basis nehmen:

Satz 2.1 Seien b ∈ N, b ≥ 2, und n ∈ N. Dann existieren eindeutig bestimmte Zahlen


l ∈ N und z i ∈ {0, . . . , b − 1} für i = 0, . . . , l − 1 mit zl−1  = 0 und


l−1
n = z i bi.
i=0

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 25


S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
26 2 Darstellungen ganzer Zahlen

Das Wort zl−1 . . . z 0 heißt die b-adische Darstellung von n; man schreibt manchmal auch
n = (zl−1 . . . z 0 )b . Es gilt stets l − 1 = logb n.

Vollständige Induktion
Um eine Aussage A(n) für alle n ∈ N zu beweisen, genügt es offenbar, zu zeigen, dass
A(1) gilt (Induktionsanfang) und dass für alle i ∈ N die Aussage A(i + 1) aus A(i)
folgt (Induktionsschritt). Beim Beweis des Induktionsschritts nennt man A(i) auch
die Induktionsvoraussetzung. Diese Beweistechnik heißt (vollständige) Induktion.
Allgemeiner kann auch für eine beliebige Menge M eine Aussage A(m) für alle m ∈ M
bewiesen werden, indem man eine Funktion f : M → N angibt und zeigt, dass für
jedes m ∈ M aus der Annahme „A(m) ist falsch“ folgt, dass es ein m  ∈ M mit
f (m  ) < f (m) gibt, sodass auch A(m  ) falsch ist. Wir sprechen hier auch von
Induktion über f.


Beweis Die letzte Aussage ist sofort klar: falls n = l−1 i=0 z i b mit z i ∈ {0, . . . , b − 1} für
i
 l−1
i = 0, . . . , l − 1 und zl−1  = 0 ist, so ist bl−1 ≤ n ≤ i=0 (b − 1)bi = bl − 1 und somit
logb n = l − 1.
Die Eindeutigkeit der b-adischen Darstellung folgt nun direkt aus Lemma 1.19.
Wir beweisen die Existenz per Induktion über l(n) := 1 + logb n; vergleiche die Box
Vollständige Induktion. Ist l(n) = 1, also n ∈ {1, . . . , b − 1}, so hat n die Darstellung
0
n = i=0 z i bi mit z 0 = n.
Sei daher nun n ∈ N mit l(n) ≥ 2. Setze n  := n/b. Dann gilt l  := l(n  ) = l(n) − 1.
l  −1  i 
Nach Induktionsvoraussetzung hat n  daher eine Darstellung n  = i=0 z i b mit z i ∈
{0, . . . , b − 1} für i = 0, . . . , l  − 1 und zl  −1  = 0. Setze z i := z i−1
 für i = 1, . . . , l  und
z 0 := n mod b ∈ {0, . . . , b − 1}. Wir erhalten:
 −1
l 

l 
l−1

n = bn/b + (n mod b) = bn + z 0 = b · z i bi + z0 = 
z i−1 bi + z 0 = z i bi .
i=0 i=1 i=0

Liegt eine Zahl in b-adischer Darstellung (zl−1 . . . z 0 )b vor, so kann man diese Zahl mit dem
sogenannten Horner-Schema ausrechnen, indem man


l−1
z i · bi = z 0 + b · (z 1 + b · (z 2 + · · · + b · (zl−2 + b · zl−1 ) · · · ))
i=0

ausnutzt; dies spart einige Multiplikationen.


2.1 b-adische Darstellung natürlicher Zahlen 27

Umgekehrt liefert der Beweis von Satz 2.1 unmittelbar einen Algorithmus, um für belie-
biges b die b-adische Darstellung einer Zahl z ∈ N zu erhalten. Programm 2.2 zeigt eine
C++-Implementierung, die für 2 ≤ b ≤ 16 funktioniert. Traditionell spielen neben der Bi-
närdarstellung (b = 2) auch die Oktaldarstellung (b = 8) und die Hexadezimaldarstellung
(b = 16) eine gewisse Rolle. Für die Darstellung einer Zahl mit b > 10 benutzt man die
Buchstaben A,B,C,. . . für die Ziffern größer als 9.

Programm 2.2 (Basiskonverter)


1 // b a s e c o n v . cpp ( I n t e g e r B a s e C o n v e r t e r )
2
3 # i n c l u d e < iostream >
4 # i n c l u d e < string >
5 # i n c l u d e < limits >
6
7 const std :: s t r i n g h e x d i g i t s = " 0 1 2 3 4 5 6 7 8 9 A B C D E F " ;
8
9 std :: s t r i n g b _ a r y _ r e p r e s e n t a t i o n ( int base , int n u m b e r )
10 // r e t u r n s the r e p r e s e n t a t i o n of " n u m b e r " with base " base " , a s s u m i n g 2 <= base <=16.
11 {
12 if ( n u m b e r > 0) {
13 r e t u r n b _ a r y _ r e p r e s e n t a t i o n ( base , n u mber / base ) + h e x d i g i t s [ n u m b e r % base ];
14 }
15 else {
16 return "";
17 }
18 }
19
20
21 bool g e t _ i n p u t ( int & base , int & n u m b e r ) // call by r e f e r e n c e
22 {
23 std :: cout << " T h i s p r o g r a m c o m p u t e s the r e p r e s e n t a t i o n of a n a t u r a l n u m b e r "
24 << " w i t h r e s p e c t to a given base .\ n "
25 << " Enter a base among 2 ,... , " << h e x d i g i t s . size () << " : " ;
26 std :: cin >> base ;
27 std :: cout << " E n t e r a n a t u r a l n u m b e r a m o n g 1 ,... , "
28 << std :: n u m e r i c _ l i m i t s < int >:: max () << " : " ;
29 std :: cin >> n u m b e r ;
30 r e t u r n ( base > 1) and ( base <= h e x d i g i t s . size ()) and ( n u m b e r > 0);
31 }
32
33
34 int main ()
35 {
36 int b , n ;
37 if ( g e t _ i n p u t ( b , n )) {
38 std :: cout << " The " << b << " - ary r e p r e s e n t a t i o n of " << n
39 << " is " << b _ a r y _ r e p r e s e n t a t i o n ( b , n ) << " .\ n " ;
40 }
41 else std :: cout << " Sorry , wrong input .\ n " ;
42 }

Das Programm 2.2 benutzt den string-Datentyp aus der Standardbibliothek. Etwas mehr
Hintergrund zur Verwendung dieses Datentyps ist in der Box C++ im Detail (2.1) dargestellt.
28 2 Darstellungen ganzer Zahlen

C++ im Detail (2.1): Strings


Um den string-Datentyp der Standardbibliothek benutzen zu können, muss man
mittels #include <string> zunächst den entsprechenden Teil einbinden. Eine
Variable mit Namen s vom Typ string kann dann mittels std::string s; de-
finiert werden. Man kann, wie in Zeile 7 von Programm 2.2, einer string-Variablen
bei der Definition direkt einen Wert zuweisen, indem man diesen Wert explizit angibt.
Nützlich ist auch die Möglichkeit, eine string-Variable mit einer vorgegebenen
Anzahl gleicher Zeichen zu befüllen. Folgendes Beispiel
std :: s t r i n g s (10 , ’ A ’ );

definiert eine string-Variable s mit Wert "AAAAAAAAAA". Auf ein einzelnes


Zeichen an Position i in einer string-Variablen s kann man mittels s[i] zugreifen.
Dabei ist zu beachten, dass das erste Zeichen in einer string-Variablen an Position 0
steht. Der Ausdruck s1 + s2 erzeugt einen string, der entsteht, indem man den
string s2 an den string s1 hängt. Die Funktion [Link]() liefert die Anzahl
Zeichen zurück, die in dem string s enthalten sind.

In Programm 2.2 sehen wir ein erstes Beispiel für eine Funktion, die sich selbst aufruft.
Solche Funktionen nennt man rekursiv. Die Verwendung rekursiver Funktionen kann den
Code eleganter machen; allerdings muss man immer aufpassen, dass die Rekursionstie-
fe, d. h. die maximale Anzahl ineinander geschachtelter Aufrufe der Funktion, beschränkt
ist (hier kann sie höchstens 32 betragen, wenn die größte darstellbare Zahl des Daten-
typs int 231 − 1 ist). Eine alternative nicht-rekursive Implementierung der Funktion
b_ary_representation zeigt folgender Code-Ausschnitt:
1 std :: s t r i n g b _ a r y _ r e p r e s e n t a t i o n ( int base , int n u m b e r )
2 // r e t u r n s the r e p r e s e n t a t i o n of " n u m b e r " with base " base " , a s s u m i n g 2 <= base <=16.
3 {
4 std :: s t r i n g r e s u l t = " " ;
5 while ( n u m b e r > 0) {
6 r e s u l t = h e x d i g i t s [ n u m b e r % b a s e ] + r esult ;
7 number = number / base ;
8 }
9 return result ;
10 }

Ferner sei darauf hingewiesen, dass Variablen an die Funktion get_input nicht wie in
den anderen bisherigen Funktionen per Wert an neue lokale Variablen übergeben werden
(„call by value“), sondern dass durch das vorgestellte &-Zeichen Referenzen auf die in der
Funktion main definierten Variablen übergeben werden („call by reference“). Die Funktion
get_input hat somit keine eigenen Variablen. Vielmehr werden die beiden in der Funktion
main definierten Variablen b und n unter anderen Namen, nämlich base und number, in
der Funktion get_input weiterbenutzt.
Die string-Variable hexdigits haben wir in Zeile 7 von Programm 2.2 außerhalb
jeder Funktion definiert. Damit ist diese Variable eine globale Variable, die für jede der drei
2.2 Exkurs: Aufbau des Hauptspeichers 29

in dem Programm vorkommenden Funktionen sichtbar ist. Um zu verhindern, dass der Wert
dieser Variablen versehentlich verändert wird, ist sie durch das Voranstellen von const als
Konstante definiert worden.
Die Darstellung von Zahlen zur Basis 8 oder 16 lässt sich übrigens in C++ sehr einfach
durch Verwendung der Streammanipulatoren std::oct und std::hex erreichen. So
bewirkt zum Beispiel die Anweisung std::cout << std::hex;, dass alle folgenden
Zahlen in hexadezimaler Schreibweise ausgegeben werden. Mittels std::dec kann man
auf die dezimale Darstellung wieder zurückschalten.

2.2 Exkurs: Aufbau des Hauptspeichers

Am Beispiel von Programm 2.2 können wir gut erklären, wie der Teil des Hauptspeichers
des Computers aufgebaut wird, den das Betriebssystem für die Programmausführung frei-
gegeben hat. Jedes Byte in diesem Bereich hat eine Adresse, die üblicherweise in Hexade-
zimaldarstellung angegeben wird.
Wenn ein Programm ausgeführt wird, ist der verfügbare Teil des Hauptspeichers in vier
Bereiche gegliedert, die oft „Code“, „Static“, „Stack“ und „Heap“ genannt werden (siehe
die Abb. 2.1; Details können von Prozessor und Betriebssystem abhängen).
Im Bereich Code liegt das Programm selbst, in Maschinencode, den der Compiler aus
dem Quelltext erzeugt hat. Er gliedert sich in den Programmcode der einzelnen Funktionen.
Im Stack wird bei jedem Aufruf einer Funktion „obenauf“ Platz für das Ergebnis (wenn
nicht void) und die lokalen Variablen der Funktion (einschließlich der Funktionsargumen-
te) reserviert. Für Variablen, die per „call by reference“ übergeben wurden, wird allerdings
keine neue Variable angelegt, sondern nur die Speicheradresse der übergebenen Variable
gespeichert. Bei der Ausführung werden vom Prozessor stets zwei Adressen verwaltet, eine
für die Stelle im Code, die gerade ausgeführt wird, und eine für die Stelle im Stack, wo
der Bereich der Funktion beginnt, die gerade ausgeführt wird. Wird eine Funktion rekursiv
ausgeführt, wird bei jedem Aufruf ein eigener Bereich im Stack angelegt.
Außerdem werden für jede Funktion (außer main) zwei Rücksprungadressen gespei-
chert, eine in den Code und eine in den Stack, damit das Programm weiß, wo und mit
welchen Variablen es nach Beenden der Funktion weitermachen muss. Beim Beenden einer
Funktion wird ihr Platz im Stack wieder freigegeben und kann wiederverwendet werden.
Daneben gibt es „freien Speicher“, den sogenannten Heap, den man vor allem dann
braucht, wenn erst bei der Ausführung des Programms klar ist, wie viel Speicherplatz ge-
braucht wird. Beispielsweise liegen die in einem vector oder string gespeicherten
Daten in einem Abschnitt auf dem Heap. Es gibt aber immer eine zugehörige normale Va-
riable, die auf dem Stack liegt, und die die Speicheradresse enthält, an der der Abschnitt im
Heap mit den eigentlichen Daten beginnt. Eine Variable, die eine Speicheradresse enthält,
heißt Pointer.
30 2 Darstellungen ganzer Zahlen

Abb. 2.1 Beispielhafter 00000000


Aufbau des Hauptspeichers 00000001

...
Daten zur Verwaltung des Heaps

Heap
...
number

b_ary_repre-
base

sentation

Stack
Rücksprungadresse im Code
Rücksprungadresse im Stack
n

main
b
Ergebnis von main

Static
globale Variablen (falls vorhanden)

Code
...

...

fffffffd
fffffffe
ffffffff

Schließlich gibt es einen Bereich Static, der etwaige globale Variablen enthält (die man
allerdings weitgehend vermeiden sollte).
Die Inhalte des Speichers werden zu Beginn nicht initialisiert; man kann also beispiels-
weise nicht wissen, welchen Wert eine nicht initialisierte Variable hat. Ebenso wenig hat
man Einfluss auf die Wahl der Speicheradressen im Stack oder Heap. Zu einer Variable
x bezeichnet &x ihre Speicheradresse. Ist umgekehrt p ein Pointer, so bezeichnet *p den
Inhalt der Variable, der an der Speicheradresse p gespeichert ist.
Den Wert einer per call by reference an eine Funktion übergebenen Variable kann diese
Funktion ändern (nicht aber natürlich ihre Speicheradresse). Will man den Wert nicht ändern,
so schreibt man besser const voran, um dies für Leser des Quelltextes zu dokumentieren
und um eine irrtümliche Änderung auszuschließen.
2.3 b-Komplementdarstellung ganzer Zahlen 31

2.3 b-Komplementdarstellung ganzer Zahlen

Wir sind es gewohnt, negative Zahlen durch Voranstellen eines „−“-Zeichens zu spezifi-
zieren. In einem Computer ließe sich dies zum Beispiel dadurch realisieren, dass man das
erste Bit für das Vorzeichen reserviert und den Wert 0 mit „+“ und den Wert 1 mit „−“
identifiziert. Diese Darstellung nennt man Vorzeichen- oder Betragsdarstellung.

Beispiel 2.3 Wir betrachten Binärdarstellungen mit vier Bits, von denen das erste Bit für
das Vorzeichen reserviert ist. Die Zahl +5 wird in Binärdarstellung als 0101 dargestellt, die
Zahl −5 entsprechend als 1101. Mit dieser Vorzeichendarstellung lassen sich die Zahlen

0 entspricht 0000 sowie −0 entspricht 1000


1 entspricht 0001 −1 entspricht 1001
.. ..
. .
7 entspricht 0111 −7 entspricht 1111

darstellen. Ein Nachteil dieser Darstellung ist, dass es zwei unterschiedliche Darstellungen
für die Null gibt. Ein wesentlich gravierenderer Nachteil ist aber, dass die Addition eine
Fallunterscheidung benötigt: man kann nicht einfach die Binärdarstellungen addieren, denn
beispielsweise muss die Addition von 0010 und 1001 den Wert 0001 ergeben.

Deshalb wird diese Darstellung in den Standarddatentypen nicht verwendet. Stattdessen


identifiziert man jede negative Zahl z ∈ {−2l−1 , . . . , −1} mit z + 2l , wobei l die Anzahl
der verwendeten Bits ist. Man spricht hier von der Komplementdarstellung.

Beispiel 2.4 Mit der Komplementdarstellung mit vier Bits lassen sich die Zahlen

0 entspricht 0000 sowie −8 entspricht 1000


1 entspricht 0001 −7 entspricht 1001
.. ..
. .
7 entspricht 0111 −1 entspricht 1111

darstellen. Die Addition von 0010 (entspricht der Zahl 2) und 1001 (entspricht der Zahl −7)
ergibt 1011, was der Zahl −5 entspricht.

Allgemein kann man die b-Komplementdarstellung einer Zahl für beliebige Basen b ≥ 2
definieren. Dazu definieren wir zunächst das b-Komplement:

Definition 2.5 Seienl und b ≥ 2 natürliche Zahlen und n ∈ {0, . . . , bl − 1}. Dann ist
K bl (n) := (bl − n) mod bl das l-stellige b-Komplement von n.
32 2 Darstellungen ganzer Zahlen

K 2 nennt man auch das Zweierkomplement und K 10 das Zehnerkomplement.

l−1
Lemma 2.6 Seien b, l ∈ N mit b ≥ 2, und sei n = i=0 z i b
i mit z i ∈ {0, . . . , b − 1} für
i = 0, . . . , l − 1. Dann gilt:

(i) K bl (n + 1) = l−1i=0 (b − 1 − z i )b falls n  = b − 1; außerdem K b (0) = 0;
i l l

(ii) K b (K b (n)) = n.
l l

Beweis K bl (0) = 0 ergibt sich unmittelbar aus der Definition des b-Komplements. Für
 l−1
n ∈ {0, . . . , bl − 2} gilt zudem: K bl (n + 1) = bl − 1 − l−1 i=0 z i b =
i
i=0 (b − 1)b −
i
l−1  l−1
i=0 z i b = i=0 (b − 1 − z i )b . Damit ist (i) bewiesen.
i i

K bl (K bl (0)) = 0 ergibt sich unmittelbar aus (i). Für n > 0 gilt K bl (n) = bl − n > 0 und
somit K bl (K bl (n)) = bl − (bl − n) = n. Dies beweist (ii).

Beispiel 2.7 Das b-Komplement einer positiven Zahl lässt sich nach Lemma 2.6(i) berech-
nen, indem man zunächst 1 subtrahiert und dann stellenweise die Differenz zu b − 1 aus-
rechnet. Es gilt zum Beispiel:

K 24 ((0110)2 ) = (1010)2 4 ((4809) ) = (5191)


K 10 10 10
K 24 ((0001)2 ) = (1111)2 K 10 ((0000)10 ) = (0000)10
4

Definition 2.8 Seien l und b ≥ 2 natürliche Zahlen und n ∈ {−bl /2, . . . , bl /2 − 1}.
Dann erhält man die l-stellige b-Komplementdarstellung von n, indem man die b-adische
Darstellung von n (falls n ≥ 0) bzw. die b-adische Darstellung von K bl (−n) (falls n < 0)
vorne mit so vielen Nullen auffüllt, dass sich eine l-stellige Zahl ergibt.

Der wesentliche Vorteil der b-Komplementdarstellung ist, dass sich mit dieser ohne Fall-
unterscheidung leicht rechnen lässt. Dies ergibt sich aus dem folgenden Satz.

Satz 2.9 Seien l und b ≥ 2 natürliche Zahlen und Z := {−bl/2, . . . , bl /2 − 1}.


z falls z ≥ 0
Betrachte die Funktion f : Z → {0, . . . , bl − 1} die durch z  →
K b (−z) falls z < 0
l

definiert ist. Dann ist f bijektiv und für x, y ∈ Z gilt:

(a) Ist x + y ∈ Z , so gilt f (x + y) = ( f (x) + f (y)) mod bl;


(b) Ist x · y ∈ Z , so gilt f (x · y) = ( f (x) · f (y)) mod bl;

Beweis Für z ∈ Z gilt f (z) = (z + bl ) mod bl . Damit ist f wegen |Z | = bl bijektiv. Seien
x, y ∈ Z und p, q ∈ {0, 1} mit f (x) = x + pbl und f (y) = y + qbl . Dann gilt:
2.3 b-Komplementdarstellung ganzer Zahlen 33

(a) f (x + y) = (x + y + bl ) mod bl = ((x + pbl ) + (y + qbl )) mod bl = ( f (x) +


f (y)) mod bl .
(b) f (x · y) = (x · y +bl ) mod bl = ((x + pbl )·(y +qbl )) mod bl = ( f (x)· f (y)) mod bl .

Die 2-Komplementdarstellung wird zur Speicherung ganzer Zahlen im Computer benutzt.


Bei dieser Darstellung ist das erste Bit genau dann 1, wenn die dargestellte Zahl negativ ist.
Die Anzahl l der benutzten Bits ist nahezu immer eine Zweierpotenz und ein Vielfaches von
8. Für den Datentyp int ist beispielsweise meist l = 32 (s. u.), und es können damit die
Zahlen in {−231 , . . . , 231 − 1} dargestellt werden.

Partitionen und Äquivalenzrelationen


Sei S eine Menge. Eine Partition von S ist eine Menge von nichtleeren, paarweise
disjunkten Teilmengen von S, deren Vereinigung S ist. Beispielsweise hat die Menge
{1, 2, 3} fünf verschiedene Partitionen.
Eine Relation R ⊆ S × S heißt Äquivalenzrelation (auf S), wenn für alle a, b, c ∈ S
gilt:

• (a, a) ∈ R (Reflexivität);
• (a, b) ∈ R ⇒ (b, a) ∈ R (Symmetrie);
• ((a, b) ∈ R ∧ (b, c) ∈ R) ⇒ (a, c) ∈ R (Transitivität).

Für jede Äquivalenzrelation R auf S ist {{s ∈ S | (a, s) ∈ R} | a ∈ S} eine Partition


von S; ihre Elemente sind die Äquivalenzklassen von R. Umgekehrt induziert jede
Partition P von S vermöge R := {(a, b) ∈ S × S | ∃P ∈ P mit a, b ∈ P} eine
Äquivalenzrelation auf S.
Beispielsweise ist = eine Äquivalenzrelation auf R; ihre Äquivalenzklassen sind alle
einelementig. Auf Z kann man für jedes k ∈ N durch Rk = {(x, y) ∈ Z × Z |
k teilt |x − y|} eine andere Äquivalenzrelation definieren, die genau k (unendliche)
Äquivalenzklassen hat. Die Menge dieser Äquivalenzklassen von Rk bezeichnet man
oft mit Z/kZ und nennt sie den Restklassenring (bzgl. k).

Wenn man Über- und Unterschreitungen des zulässigen Bereichs Z ignoriert (bzw. immer
modulo bl rechnet), kann man also mit der Komplementdarstellung genauso rechnen wie mit
der b-adischen Darstellung. In beiden Fällen rechnet man de facto mit den Elementen des
Restklassenrings Z/bl Z; siehe die Box Partitionen und Äquivalenzrelationen. Lediglich
die Repräsentanten der Äquivalenzklassen sind bei der b-adischen Darstellung und der
Komplementdarstellung teilweise verschieden.
Die Subtraktion kann man auf die Addition zurückführen, Vergleiche durch vorherige
Subtraktion auf Testen des ersten Bits.
34 2 Darstellungen ganzer Zahlen

Die Komplementdarstellung haben sich schon Konstrukteure von mechanischen Rechen-


maschinen seit dem 17. Jahrhundert zunutze gemacht. Eine der weltweit größten Sammlun-
gen mechanischer Rechenmaschinen kann im Bonner Arithmeum besichtigt werden.
Bereichsüberschreitungen werden normalerweise nicht automatisch abgefangen. Man
muss also beim Programmieren selbst aufpassen und ggfs. vorher testen, ob ein (meist
unerwünschter) Überlauf auftreten kann.
Die folgende Tabelle gibt die wichtigsten C++-Datentypen für ganze Zahlen wieder. Die
Darstellung der Zahlen erfolgt in der 2-Komplementdarstellung, sodass sich die jeweils
kleinste und größte darstellbare Zahl aus der Anzahl der Bits ergibt.

Datentyp Anzahl Bits unter gcc Anzahl Bits unter gcc


5.1.0 Windows 10 4.8.5 Linux 4.13.1
short 16 16
int 32 32
long 32 64
long long 64 64

Über sizeof (Datentyp) bzw. sizeof Variable kann man die Anzahl der Bytes
ermitteln, die ein Datentyp oder eine Variable benötigt. Das Ergebnis ist vom Typ size_t,
welcher nichtnegative ganze Zahlen hinreichender Größe speichern kann. Es gibt deshalb
keinen Grund, sich blind darauf zu verlassen, dass zum Beispiel ein int wirklich 4 Bytes
hat.
Wir haben gesehen, dass man mit den Standarddatentypen nur ganze Zahlen innerhalb
eines bestimmten Intervalls speichern kann. Das ist oft problematisch; zum Beispiel kön-
nen in einer Collatz-Folge auch größere Zahlen als 264 auftreten, selbst wenn die Eingabe
kleiner ist (vgl. Programm 1.25). Wenn man das nicht abfängt, wird meist einfach modulo
2l gerechnet, wobei l die Anzahl der Bits des Datentyps ist.

2.4 Rationale Zahlen

Rationale Zahlen kann man natürlich speichern, indem Zähler und Nenner in getrennten
Variablen gespeichert werden. Sinnvoll ist es, hierfür einen neuen Datentyp zu definieren;
in C++ sagen wir auch: eine Klasse. Klassen bilden das wichtigste Konstrukt der Sprache
C++, das wir deshalb hier etwas genauer erläutern.
Eine Klasse kann das Speichern von Daten in Variablen ermöglichen und Funktionen
bereitstellen, die auf diesen Daten operieren. Ferner können noch Typen und Konstanten
definiert werden. Entscheidend ist die klare Trennung von interner Datenverwaltung und
dem Interface nach außen. Man betrachte Programm 2.10, das aus zwei Dateien besteht.
2.4 Rationale Zahlen 35

In der Datei fraction.h wird eine Klasse namens Fraction definiert, die sich zum
Speichern rationaler Zahlen eignet. Das Programm [Link] zeigt ein Beispiel, wie
diese Klasse verwendet werden kann: ganz ähnlich wie ein Standarddatentyp.
Die Mitglieder einer Klasse sind entweder public oder private. Der public Teil
stellt das Interface nach außen dar. Der private Teil sollte die Daten enthalten (hier
Zähler und Nenner); darüber hinaus kann er Funktionen enthalten, die nur als Subroutinen
der public Funktionen benötigt werden, aber nicht selbst von außen sichtbar sein sollen.
Auf alles, was private ist, hat man von außen keinen direkten Zugriff. Dies schützt vor
Programmierfehlern.
Eine Klasse kann vier Arten von Funktionen enthalten:

• Konstruktoren, mit denen ein Objekt der Klasse erzeugt werden kann, ganz so, wie
eine Variable eines Standarddatentyps deklariert (und evtl. auch initialisiert) wird. Ein
Konstruktor heißt immer genauso wie die Klasse selbst. In fraction.h ist der (in
diesem Fall einzige) Konstruktor in Zeile 9 bis 12 definiert. Falls man keinen Konstruktor
definiert, wird vom Compiler ein Standardkonstruktor definiert, der die zu einer Klasse
gehörenden Objekte initialisiert.
• einen Destruktor, der aufgerufen wird, wenn die Lebenszeit eines Objektes der Klasse
endet, ebenso wie Variablen einer Funktion nach deren Beendigung wieder freigege-
ben werden. Da hier kein Destruktor explizit definiert ist (er müsste ∼Fraction heißen),
übernimmt diese Rolle ein vom Compiler generierter Standarddestruktor.
• weitere Funktionen, die immer auf einem bestehenden Objekt der Klasse operieren.
Sie können auch nur in Zusammenhang mit einem Objekt aufgerufen werden. Sie kön-
nen dieses Objekt verändern (wie etwa reciprocal()) oder auch nicht (wie etwa
numerator()); im zweiten Fall sollte man const hinter die Deklaration schreiben.
Ansonsten verhalten sie sich wie normale Funktionen.
• Operatoren, die für bestimmte Standarddatentypen vordefiniert sind, können auch für
eine neue Klasse definiert werden (hier gezeigt für < und +). Operatoren verhalten sich
ebenso wie Funktionen, außer dem speziellen Namen und des komfortableren Aufrufs
(siehe etwa Zeile 16 von [Link]) gibt es keinen Unterschied.

Ebenso wie andere Funktionen können Operatoren neben dem Objekt der Klasse, mit dem
sie assoziiert sind, ein weiteres Objekt derselben Klasse als Parameter bekommen und auch
ein neues Objekt der Klasse als Ergebnis erzeugen. Auf deren Daten und Funktionen kann
mittels „.“ auch zugegriffen werden. Ein Beispiel ist die hier gezeigte Addition.
Konstruktoren und Destruktoren können explizit oder implizit auf verschiedene Arten
aufgerufen werden. Die Zeilen 14 und 16 von [Link] zeigen zwei explizite Auf-
rufe des Konstruktors, ein weiterer findet in den Zeilen 44–46 von fraction.h statt.
36 2 Darstellungen ganzer Zahlen

Programm 2.10 (Rationale Zahlen und harmonische Zahlen)


1 // f r a c t i o n . h ( C l a s s F r a c t i o n )
2
3 # i n c l u d e < stdexcept >
4
5 class F r a c t i o n {
6 public :
7 using i n t t y p e = long long ;
8
9 F r a c t i o n ( i n t t y p e n , i n t t y p e d ): _ n u m e r a t o r ( n ) , _ d e n o m i n a t o r ( d )
10 { // c o n s t r u c t o r
11 if ( d < 1) e r r o r _ z e r o ();
12 }
13
14 i n t t y p e n u m e r a t o r () const // r e t u r n n u m e r a t o r
15 {
16 return _numerator ;
17 }
18
19 i n t t y p e d e n o m i n a t o r () const // r e t u r n d e n o m i n a t o r
20 {
21 return _denominator ;
22 }
23
24 void r e c i p r o c a l () // r e p l a c e s a / b by b / a
25 {
26 if ( n u m e r a t o r () == 0) {
27 e r r o r _ z e r o ();
28 } else {
29 std :: swap ( _numerator , _ d e n o m i n a t o r );
30 if ( d e n o m i n a t o r () < 0) {
31 _numerator = - _numerator ;
32 _denominator = - _denominator ;
33 }
34 }
35 }
36
37 bool operator <( const F r a c t i o n & x ) const // c o m p a r i s o n
38 {
39 r e t u r n n u m e r a t o r () * x . d e n o m i n a t o r () < x . n u m e r a t o r () * d e n o m i n a t o r ();
40 }
41
42 F r a c t i o n o p e r a t o r +( const F r a c t i o n & x ) const // a d d i t i o n
43 {
44 r e t u r n F r a c t i o n ( n u m e r a t o r () * x . d e n o m i n a t o r () +
45 x . n u m e r a t o r () * d e n o m i n a t o r () ,
46 d e n o m i n a t o r () * x . d e n o m i n a t o r ());
47 }
48
49 // f u r t h e r o p e r a t i o n s may be added here
50
51 private :
52 inttype _numerator ;
53 inttype _denominator ;
54
55 void e r r o r _ z e r o ()
56 {
57 throw std :: r u n t i m e _ e r r o r ( " D e n o m i n a t o r < 1 not a l l o w e d in F r a c t i o n . " );
58 }
59 };
2.4 Rationale Zahlen 37

1 // h a r m o n i c . cpp ( H a r m o n i c N u m b e r s )
2
3 # i n c l u d e < iostream >
4 # i n c l u d e < stdexcept >
5 # include " fraction .h"
6
7 int main ()
8 {
9 try {
10 std :: cout << " T h i s p r o g r a m c o m p u t e s H ( n ) = 1 / 1 + 1 / 2 + 1 / 3 + . . . + 1 / n .\ n "
11 << " E n t e r an i n t e g e r n : " ;
12 F r a c t i o n :: i n t t y p e n ;
13 std :: cin >> n ;
14 F r a c t i o n sum (0 ,1);
15 for ( F r a c t i o n :: i n t t y p e i = 1; i <= n ; ++ i ) {
16 sum = sum + F r a c t i o n (1 , i );
17 }
18 std :: cout << " H ( " << n << " ) = "
19 << sum . n u m e r a t o r () << " / " << sum . d e n o m i n a t o r () << " = "
20 << s t a t i c _ c a s t < double >( sum . n u m e r a t o r ()) / sum . d e n o m i n a t o r () << " \ n " ;
21 }
22 catch ( std :: r u n t i m e _ e r r o r e ) {
23 std :: cout << " R U N T I M E E R R O R : " << e . what () << " \ n " ;
24 r e t u r n 1;
25 }
26 }

Auch der Outputoperator << lässt sich umdefinieren. Wir könnten damit die Ausgabe
in Zeile 19 des Programms [Link] durch << sum ersetzen. Allerdings ist der
erste Operand von << in diesem Fall ein ostream. Daher kann der neue Operator nicht
innerhalb der Klasse Fraction definiert werden, sondern muss als eine normale Funktion
außerhalb von Fraction definiert werden. Eine mögliche Realisierung kann wie folgt
aussehen:
1 // o u t p u t o p e r a t o r for c l a s s F r a c t i o n
2
3 std :: o s t r e a m & operator < <( std :: o s t r e a m & os , const F r a c t i o n & f )
4 {
5 os << f . n u m e r a t o r () << " / " << f . d e n o m i n a t o r ();
6 r e t u r n os ;
7 }

Das Programm 2.10 zeigt auch erstmals ein Beispiel, wie man mit Fehlern umgeht, die
während der Laufzeit eines Programms auftreten. Dazu dienen sogenannte exceptions,
deren Benutzung in der Box C++ im Detail (2.2) genauer erklärt wird. In Zeile 29 von
fraction.h benutzen wir die swap-Funktion, die den Inhalt der beiden übergebenen
Variablen vertauscht.
38 2 Darstellungen ganzer Zahlen

C++ im Detail (2.2): Fehlerbehandlung mittels try-catch


Während der Laufzeit eines Programms können eine Vielzahl von Fehlern auftreten,
bei denen an der Programmstelle, an der dieser Fehler entdeckt wird, nicht unbedingt
klar ist, wie mit dem Fehler umgegangen werden soll. Typische Fehler, die zur Lauf-
zeit auftreten, sind zum Beispiel die Division durch 0 oder der Bereichsüberlauf eines
int-Typs. Die Idee der Fehlerbehandlung von C++ ist, dass der Programmteil, der
einen Fehler bemerkt, diesen geeignet an die Stelle im Programm zurückmelden kann,
an der man weiß, wie mit dem Fehler umzugehen ist. Die Funktionalitäten zur Feh-
lerbehandlung sind in der Standardbibliothek in dem Header stdexcept definiert,
der daher zunächst mittels #include <stdexcept> eingebunden werden muss.
Wird ein Laufzeitfehler bemerkt, so kann dieser mittels
throw std::runtime_error (Fehlermeldung );
an den aufrufenden Programmteil gemeldet werden. Fehlermeldung ist dabei ein
string, der den aufgetretenen Fehler beschreibt. Ein Beispiel hierfür sehen wir in
Zeile 57 von fraction.h. Ein mittels throw gemeldeter Laufzeitfehler kann mit
einem try-catch-Konstrukt geeignet behandelt werden. Dieses hat die allgemeine
Form:
try {Programmteil} catch (std::runtime_error e)
{Fehlerbehandlung }
Die Zeilen 9–25 des Programms [Link] zeigen ein Beispiel. Falls in Pro-
grammteil ein Fehler auftritt, der mittels throw gemeldet wird, so wird Programm-
teil beendet und getestet, ob es eine zum Fehlertyp passende catch-Anweisung
gibt und der Teil Fehlerbehandlung ausgeführt. Neben runtime_error sind in
stdexcept eine Reihe weiterer Fehlertypen definiert. Allen ist gemein, dass man
mittels .what() den in der throw-Anweisung angegebenen Fehlermeldungstext
erhält.
Sobald eine throw-Anweisung durchlaufen wird, wird das Programm mit dem
Fehlerbehandlungsteil einer passenden catch-Anweisung fortgesetzt. Ist eine solche
catch-Anweisung nicht vorhanden, so wird rekursiv zu der aufrufenden Funktion
zurückgesprungen, bis eine solche catch-Anweisung angetroffen wird. Enthält keine
der aufrufenden Funktionen eine solche catch-Anweisung, so wird das Programm
beendet.

Schließlich sehen wir in Zeile 20 von [Link] eine Umwandlung einer ganzen Zahl
in den Datentyp double. Allgemein kann man mittels static_cast<Datentyp>
(Ausdruck) einen Ausdruck in einen anderen Datentyp umwandeln.
Die Klasse Fraction ist noch nicht gut implementiert. Immerhin wird abgefangen,
wenn ein Nenner null wird. Nichts unternommen wurde hingegen bisher gegen auftretende
Bereichsüberläufe von inttype. Genau solche passieren auch sehr schnell; beispielsweise
funktioniert das Programm [Link] nur für n ≤ 20 korrekt.
2.5 Beliebig große ganze Zahlen 39

Natürlich könnte und sollte man die auftretenden Brüche immer kürzen. Darauf gehen
wir im nächsten Kapitel ein. Auch das verhindert Überläufe aber nicht immer. Man könnte
diese mit throw abfangen, aber besser ist es natürlich, wenn Zähler und Nenner beliebig
groß werden könnten.

2.5 Beliebig große ganze Zahlen

Wir wollen nun zeigen, wie eine Klasse aussehen kann, die beliebig große ganze Zahlen
realisiert. Dazu müssten wir eigentlich alle Standardrechenoperationen sowie die Vergleichs-
operatoren zur Verfügung stellen, die es auch für die Standarddatentypen wie int gibt. Der
Einfachheit halber beschränken wir uns hier aber auf die Operatoren +, +=, < und eine Aus-
gabefunktion. Der += Operator erlaubt Ausdrücke der Form a = a + b; abkürzend als
a += b; zu schreiben.
Das Programm 2.11 besteht der Übersichtlichkeit halber aus drei Dateien. Die Datei-
en largeint.h und [Link] definieren einen neuen Typ (Klasse) namens
LargeInt.
Die Datei largeint.h enthält die Deklaration der Klasse (bestehend aus den Teilen
public und private). Die Datei [Link] enthält die Implementierung aller zur
Klasse gehörigen Funktionen. Diese Trennung in Headerdatei und Implementierungsdatei
ist zumindest bei umfangreicheren Klassen üblich und praktisch.
Die Klasse LargeInt speichert eine beliebig große natürliche Zahl in einem vector
namens _v, wobei jeder Eintrag des vectors eine Dezimalstelle der Zahl enthält. Zudem
gibt es einen konstanten string mit Namen digits, der für die Umwandlung in die Zif-
ferndarstellung benötigt wird. Es ist aber nicht nötig, dass jedes Objekt vom Typ LargeInt
eine Kopie dieses strings enthält. Deshalb ist das Schlüsselwort static vorangestellt,
das dazu führt, dass digits nur einmal für alle Objekte der Klasse angelegt wird. Die
Initialisierung einer static-Variablen einer Klasse muss außerhalb der Klasse erfolgen.
In unserem Fall geschieht dies in Zeile 5 von [Link]. Der Benutzer der Klasse
muss aber alle diese Details nicht kennen; denn diese sind im private-Teil. Von außen
sind nur folgende Funktionen sichtbar:

• Mit dem Konstruktor kann man eine neue Variable vom Typ LargeInt anlegen. Diesem
wird als Argument die zu speichernde Zahl übergeben. Ein Konstruktor ruft automatisch
die Konstruktoren für die in der Klasse enthaltenen Variablen auf; hier den Konstruktor
von vector.
• Die Funktion decimal liefert einen string mit der Dezimaldarstellung der Zahl
zurück.
• Der Vergleichsoperator < wird implementiert. Hierbei wird getestet, ob der gespeicherte
Wert kleiner ist als der im Argument.
• Der Operator += wird implementiert und auch bei der Realisierung des Operators +
verwendet. Damit können wir Zahlen vom Typ LargeInt addieren.
40 2 Darstellungen ganzer Zahlen

Programm 2.11 (Beliebig große ganze Zahlen)


1 // l a r g e i n t . h ( D e c l a r a t i o n of C l a s s L a r g e I n t )
2
3 # i n c l u d e < vector >
4 # i n c l u d e < string >
5
6
7 class L a r g e I n t {
8 public :
9 using i n p u t t y p e = long long ;
10
11 L a r g e I n t ( i n p u t t y p e ); // constructor
12 std :: s t r i n g d e c i m a l () const ; // decimal representation
13 bool operator <( const L a r g e I n t &) const ; // comparison
14 L a r g e I n t o p e r a t o r +( const L a r g e I n t &) const ; // addition
15 const L a r g e I n t & o p e r a t o r +=( const L a r g e I n t &); // addition
16
17 private :
18 std :: vector < short > _v ; // s t o r e s i n g l e digits , last digit in _v [0]
19 s t a t i c c o n s t std :: s t r i n g d i g i t s ;
20 };

1 // l a r g e i n t . cpp ( I m p l e m e n t a t i o n of C l a s s L a r g e I n t )
2
3 # include " largeint .h"
4
5 const std :: s t r i n g L a r g e I n t :: d i g i t s = " 0 1 2 3 4 5 6 7 8 9 " ;
6
7 L a r g e I n t :: L a r g e I n t ( i n p u t t y p e i ) // c o n s t r u c t o r , c a l l s c o n s t r u c t o r of v e c t o r
8 {
9 do {
10 _v . p u s h _ b a c k ( i % 10);
11 i /= 10;
12 } while ( i > 0);
13 }
14
15
16 std :: s t r i n g L a r g e I n t :: d e c i m a l () const // r e t u r n s d e c i m a l r e p r e s e n t a t i o n
17 {
18 std :: s t r i n g s ( " " );
19 for ( auto i : _v ) { // range for s t a t e m e n t : i runs over all
20 s = digits [i] + s; // e l e m e n t s of _v
21 }
22 return s;
23 }
24
25
26 bool L a r g e I n t :: operator <( const L a r g e I n t & arg ) const // c h e c k s if < arg
27 {
28 if ( _v . size () == arg . _v . size ()) {
29 auto it2 = arg . _v . r b e g i n ();
30 for ( auto it1 = _v . r b e g i n (); it1 != _v . rend (); ++ it1 , ++ it2 ) {
31 if (* it1 < * it2 ) r e t u r n t r u e ;
32 if (* it1 > * it2 ) r e t u r n f a l s e ;
33 }
34 return false ;
35 }
36 r e t u r n _v . size () < arg . _v . size ();
37 }
38
39
40 L a r g e I n t L a r g e I n t :: o p e r a t o r +( const L a r g e I n t & arg ) const // a d d i t i o n
41 {
42 L a r g e I n t r e s u l t (* this );
43 r e s u l t += arg ;
44 return result ;
2.5 Beliebig große ganze Zahlen 41

45 }
46
47
48 const L a r g e I n t & L a r g e I n t :: o p e r a t o r +=( const L a r g e I n t & arg ) // a d d i t i o n
49 {
50 if ( arg . _v . size () > _v . size ()) {
51 _v . r e si z e ( arg . _v . s i z e () , 0);
52 }
53 auto it1 = _v . begin ();
54 for ( auto it2 = arg . _v . begin (); it2 != arg . _v . end (); ++ it2 , ++ it1 ) {
55 * it1 += * it2 ;
56 }
57 short carry = 0;
58 for ( auto & i : _v ) {
59 i += carry ;
60 carry = i / 10;
61 i %= 10;
62 }
63 if ( carry != 0) _v . p u s h _ b a c k ( carry );
64 r e t u r n * this ;
65 }

1 // f a c t o r i a l . cpp ( C o m p u t i n g n ! by A d d i t i o n )
2
3 # i n c l u d e < iostream >
4 # i n c l u d e < limits >
5 # include " largeint .h"
6
7
8 L a r g e I n t f a c t o r i a l ( L a r g e I n t :: i n p u t t y p e n )
9 // c o m p u t e s n ! warning , slow : r u n t i m e O ( n ^3 log n )
10 {
11 L a r g e I n t r e s ult (1);
12 for ( L a r g e I n t :: i n p u t t y p e i = 2; i <= n ; ++ i ) {
13 L a r g e I n t sum (0);
14 for ( L a r g e I n t :: i n p u t t y p e j = 1; j <= i ; ++ j ) {
15 sum += r e s u l t ;
16 }
17 r e s u l t = sum ;
18 }
19 return result ;
20 }
21
22
23 int main ()
24 {
25 L a r g e I n t :: i n p u t t y p e n ;
26 std :: cout << " T h i s p r o g r a m c o m p u t e s n ! , for a n a t u r a l n u m b e r n up to "
27 << std :: n u m e r i c _ l i m i t s < L a r g e I n t :: inputtype >:: max () << " \ n "
28 << " E n t e r a n a t u r a l n u m b e r n : " ;
29 std :: cin >> n ;
30 std :: cout << n << " ! = " << f a c t o r i a l ( n ). d e c i m a l () << " \ n " ;
31 }

Der Konstruktor für LargeInt fügt dem vector _v mittels _v.push_back jeweils
ein Element an das Ende an. In den Zeilen 50 und 51 von [Link] sehen wir zwei
weitere neue vector-Funktionen. Die Anzahl der Elemente, die in einem vector enthal-
ten sind, liefert die Funktion size() zurück. Mittels resize kann man die Größe eines
vectors ändern. Das erste Argument dieser Funktion gibt die Anzahl Elemente, das optio-
nale zweite Argument einen Initialwert für durch resize neu hinzugefügte Elemente an.
42 2 Darstellungen ganzer Zahlen

Sowohl die Addition als auch der Vergleich zweier LargeInts erfordern, dass man über
alle Elemente der entsprechenden vectoren läuft. Dies könnte man mittels Indexzugriff
auf die einzelnen vector-Elemente realisieren. Eine universellere Methode basiert aber
auf der Verwendung sogenannter Iteratoren. Deren Benutzung wird in der Box C++ im
Detail (2.3) erklärt.

C++ im Detail (2.3): Iteratoren und bereichsbasiertes for


Die Standardbibliothek stellt neben dem abstrakten Datentyp vector eine Reihe wei-
terer sogenannter Containertypen zur Verfügung. Auf die Elemente eines vectors
kann man mittels Indexoperator [] zugreifen. Für andere Containertypen ist das aber
nicht unbedingt der Fall. Um Code zu erstellen, der möglichst unabhängig von dem
verwendeten Containertyp ist, kann man auf die Elemente eines Containers mithilfe
von Iteratoren zugreifen. Die Funktion begin() liefert einen Iterator, der auf das
erste Element des Containers verweist, die Funktion end() liefert einen Iterator,
der auf die Stelle hinter dem letzten Element des Containers verweist. In Zeile 53
von [Link] haben wir einen Iterator mit Namen it1 definiert, dem der
Wert _v.begin() zugewiesen wird. Als Typ haben wir dabei auto benutzt. Die-
ses Schlüsselwort veranlasst den Compiler, selbstständig den passenden Typ anhand
des zugewiesenen Wertes zu ermitteln. Einen Iterator kann man mittels ++ und ––
inkrementieren und dekrementieren, damit verweist er dann auf das nächste oder das
vorherige Element. Benutzt man statt begin und end die Funktionen rbegin()
und rend(), so kann man die Elemente des Containers mit ++ in umgekehrter Rei-
henfolge durchlaufen. Dies nutzen wir z. B. in Zeile 30 von [Link]. Mittels
*iter erhält man das aktuelle Element, auf das der Iterator iter verweist.
Häufig kommt es vor, dass man über alle Elemente eines Containers laufen möchte.
Zu diesem Zweck gibt es in C++ eine spezielle Version der for-Anweisung, die
bereichsbasierte for-Anweisung genannt wird. Diese haben wir beispielsweise in den
Zeilen 19–21 von [Link] benutzt. Die dort definierte Variable i durchläuft
alle Elemente des Vektors _v. Alternativ hätten wir die Schleife in den Zeilen 19–21
auch wie folgt schreiben können:
for ( auto it = _v . begin (); it != _v . end (); ++ it ) {
s = d i g i t s [* it ] + s ;
}

Zu beachten ist, dass in einer bereichsbasierten for-Schleife die Elemente des


Containers durchlaufen werden, und daher nicht wie bei Iteratoren der Dereferenzie-
rungsoperator * benutzt wird. Sollen die von der Variablen durchlaufenen Elemente
auch verändert werden können, so muss die Variable mittels & als Referenz auf die
Elemente definiert werden. Dies haben wir in der for-Schleife in den Zeilen 58–62
von [Link] benutzt.
2.6 Übungsaufgaben 43

Der Additionsalgorithmus für zwei LargeInt-Zahlen wird durch den +=-Operator


in den Zeilen 48–65 von [Link] realisiert. Dieser Operator wird sodann in den
Zeilen 40–45 benutzt, um den +-Operator zu realisieren. Dazu legen wir zunächst in Zeile 42
eine Kopie des links vom +-Operator stehenden Objekts an. Zu diesem Zweck müssen wir
auf das Objekt verweisen, mit dem der +-Operator aufgerufen wurde. Dazu benutzen wir
this, das jeweils die Adresse des Objektes enthält, über das die entsprechende Funktion
aufgerufen wurde.
Gibt es verschiedene Variablen vom Typ LargeInt, wie etwa in der Funktion main
in der Datei [Link], so belegen diese natürlich verschiedene Speicherbereiche.
Bei jedem Durchlauf der for-Schleife wird in Zeile 12 von [Link] eine neue
LargeInt-Variable mit Namen sum angelegt. Am Ende der for-Schleife wird diese
wieder gelöscht, indem der Destruktor von LargeInt aufgerufen wird.
In unserem Fall geschieht das alles automatisch: da wir keinen eigenen Destruktor defi-
niert haben, wird (automatisch) der Standarddestruktor aufgerufen; dieser ruft jeweils ein-
fach den Destruktor von vector auf. Da vector Daten auf dem Heap anlegt, hat diese
Klasse einen komplizierteren Destruktor, der den benutzten Speicherplatz wieder freigibt.
Die Implementierung von [Link] ist nicht sehr effizient und könnte natürlich
mit Hilfe der Multiplikation vereinfacht und verbessert werden. Diese und andere Grund-
rechenarten können in der Klasse LargeInt ergänzt werden. Momentan ist die Laufzeit
dominiert von (n 2 ) Additionen, bei denen der größere Summand (n log n) Stellen hat,
d. h. wir haben (n 3 log n) elementare Rechenoperationen. Implementiert man die Mul-
tiplikation, so kann man n! mit (n) Multiplikationen ausführen, bei denen jeweils ein
Faktor höchstens n log n Stellen hat und der andere höchstens log n; damit ergibt sich (bei
Implementierung der Multiplikation nach Schulmethode) eine Laufzeit von O(n 2 log2 n).
Eine solche neue Klasse ist sehr leicht einsetzbar. Um beispielsweise die Collatz-
Folge (Programm 1.25) für beliebig große Zahlen zu implementieren, muss man in je-
nem Programm nur Zeile 5 durch using myint = LargeInt; ersetzen und natürlich
#include "largeint.h" einfügen. Auch unsere Klasse Fraction kann durch Aus-
tausch einer einzigen Zeile (ersetze Zeile 7 in fraction.h durch using inttype =
LargeInt;) mit beliebigen Brüchen arbeiten. Natürlich müssen zuvor die noch fehlenden
Operationen, insbesondere die Multiplikation, implementiert werden.

2.6 Übungsaufgaben

1. Geben Sie die Darstellung der Zahl (3324)5 bezüglich der Basis 7 an und wandeln Sie
die Oktalzahl (6272)8 in das Binärsystem um.
2. Bestimmen Sie für die Dezimalzahl −25 die 8-stellige und die 16-stellige
2-Komplementdarstellung.
44 2 Darstellungen ganzer Zahlen

3. Es sei z die 2-Komplementdarstellung einer negativen Zahl mit l Bits. Welche Zahl
entsteht, wenn man in z jede 0 durch eine 1 und jede 1 durch eine 0 ersetzt?
4. Für welche negativen Zahlen x ist die 2-Komplementdarstellung mit l Bits bis auf die
Vorzeichenstelle identisch mit der 2-Komplementdarstellung von −x?
5. Es sei z die 2-Komplementdarstellung einer negativen Zahl mit l Bits. Wie sieht die
Darstellung derselben Zahl aus, wenn 2l Stellen für die Komplementdarstellung zur
Verfügung stehen?
Bemerkung: Eine solche Umwandlung kann zum Beispiel notwendig werden, wenn
eine Variable vom Typ short in eine Variable vom Typ int umgewandelt wird.
6. Zeigen Sie, dass die Existenz der b-adischen Darstellung im Beweis von Satz 2.1 auch
aus Lemma 1.19 gefolgert werden kann.
l−1
7. Es sei (al−1 al−2 . . . a0 )−10 := i=0 ai (−10) , wobei
i ai ∈ {0, . . . , 9} sei für i ∈
l−1
{0, . . . , l − 1}. Man nennt al−1 . . . a0 Darstellung von i=0 ai (−10)i zur Basis −10,
falls al−1  = 0 gilt oder l = 1 und a0 = 0 gelten.
a) Schreiben Sie (19375573910)−10 als Dezimalzahl.
b) Geben Sie eine Darstellung von (9230753)10 zur Basis −10 an.
c) Zeigen Sie, dass es für jede ganze Zahl x eine Darstellung zur Basis −10 gibt.
d) Ist die Darstellung aus Aufgabenteil (c) immer eindeutig?

8. Für eine natürliche Zahl n sei q(n) := l−1 i=0 z i , wobei z l−1 . . . z 0 die Dezimaldarstel-
lung von n sei. Man nennt q(n) die Quersumme von n. Für t ∈ N definieren wir qt (n)
rekursiv durch q1 (n) = q(n) und qt+1 (n) = q(qt (n)). Es sei außerdem q ∗ (n) = qt0 (n),
wobei t0 die kleinste natürliche Zahl sei, für die qt0 (n) ≤ 9. Zeigen Sie, dass für natür-
liche Zahlen a und b gilt: q ∗ (q ∗ (a) · q ∗ (b)) = q ∗ (a · b).
9. Die Addition und Multiplikation zweier rationaler Zahlen, bei denen Nenner und Zähler
jeweils höchstens l Stellen haben, lässt sich in O(l 2 ) durchführen.
10. Implementieren Sie Subtraktion, Multiplikation und Division in der Klasse Fraction.
11. Ergänzen Sie in der Klasse Fraction einen Test, der einen Überlauf verhindert und
eine Fehlerbehandlung mittels throw-catch durchführt.
12. Es sei x0 : = 1 und xi := 1 + xi−1 1
für i ∈ N. Berechnen Sie mit Hilfe der Klasse
Fraction die ersten fünfzig Glieder der Folge (xi )i∈N . Wogegen konvergiert die
Folge? Welche Eigenschaften beobachten Sie bei Zähler und Nenner der Folgenglieder?
13. Erweitern Sie die Klasse LargeInt um die Multiplikation.
14. Erweitern Sie die Klasse LargeInt um einen Test, ob eine Zahl gerade ist, sowie um
die ganzzahlige Division durch 2.
15. Ändern Sie Programm 1.25 so, dass die Collatz-Folge für alle im Datentyp long long
darstellbaren natürlichen Zahlen korrekt berechnet wird. Benutzen Sie hierzu die Klasse
LargeInt und Aufgabe 14.
Rechnen mit ganzen Zahlen
3

Wir haben in Kap. 1 die Grundrechenarten als elementare Rechenoperationen bezeichnet


und angenommen, dass sie in konstanter Zeit durchgeführt werden können. Dies ist oft eine
praktische Annahme, die allerdings für beliebig große Zahlen natürlich nicht der Reali-
tät entspricht. Wir wollen sehen, wie schnell man tatsächlich mit ganzen Zahlen rechnen
kann. Elementare Rechenschritte (im engeren Sinne) sind dabei nur Rechenoperationen und
Vergleiche auf ganzen Zahlen beschränkter Größe.
Moderne Computer können Rechenoperationen auf Zahlen mit bis zu 64 Bit in wenigen
Taktzyklen durchführen. Für die asymptotische Laufzeit ist dies aber hier irrelevant, da man
alles aus Rechenoperationen mit nur einem Bit zusammensetzen kann.

3.1 Addition und Subtraktion

In Kap. 2 haben wir bereits gesehen, wie man die Addition zweier positiver ganzer Zahlen
durchführen kann. In Programm 2.11 wurde eine entsprechende Implementierung gezeigt.
Der Algorithmus hat offenbar Laufzeit O(l), wobei l die maximale Anzahl der Stellen der
beteiligten Zahlen ist. Indem man entweder die Vorzeichendarstellung oder die Komple-
mentdarstellung negativer Zahlen benutzt, kann man diesen Algorithmus leicht auf negative
Zahlen bzw. die Subtraktion erweitern. Da man die Komplementdarstellung in O(l) Zeit
berechnen kann, bleibt die Laufzeit dieser Erweiterung weiterhin O(l). Wir halten fest:

Proposition 3.1 Für zwei ganze Zahlen x und y können die Summe x + y und die Differenz
x − y in Laufzeit O(l) berechnet werden, wobei l = 1 + log2 (max{|x|, |y|, 1}) ist. 

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 45


S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
46 3 Rechnen mit ganzen Zahlen

3.2 Multiplikation

Für die Multiplikation und Division haben die nahe liegenden Algorithmen (Schulmethode)
Laufzeit O(l 2 ). Es geht aber besser.
Karatsuba [22] hat als Erster eine Möglichkeit gefunden, die Multiplikation asymptotisch
schneller durchzuführen. Die Idee besteht darin, die l-stelligen zu multiplizierenden Zahlen x
und y in zwei circa 2l -stellige Zahlen zu zerlegen, etwa durch x = x  · B + x  und y =
y  · B + y  , wobei B eine Potenz der Basis ist, bezüglich der die Zahlen dargestellt sind.
Es reicht dann, die Produkte p := x  · y  , q := x  · y  und r := (x  + x  ) · (y  + y  )
auszurechnen (rekursiv mit demselben Algorithmus), um dann mittels folgender Gleichung
das gesuchte Produkt x · y zu erhalten:

x · y = (x  · y  )B 2 + (x  · y  + x  · y  )B + x  · y  = p B 2 + (r − p − q)B + q . (3.1)

Wir gehen im Folgenden davon aus, dass die Basis 2 zur Zahlendarstellung benutzt wird
und stellen Karatsubas Algorithmus in Pseudocode vor. Selbstverständlich können wir uns
auf natürliche Zahlen beschränken.

Algorithmus 3.2 (Karatsubas Algorithmus)


Eingabe: x, y ∈ N, in Binärdarstellung.
Ausgabe: x · y, in Binärdarstellung.

if x < 8 and y < 8 then output x · y (direkte Multiplikation)


else
l ← 1 + log2 (max{x, y}), k ←  2l 
B ← 2k
x  ←  Bx , x  ← x mod B
y  ←  By , y  ← y mod B
p ← x  · y  (rekursiv)
q ← x  · y  (rekursiv)
r ← (x  + x  ) · (y  + y  ) (rekursiv)
output p B 2 + (r − p − q)B + q

Satz 3.3 Die Multiplikation zweier in Binärdarstellung gegebener natürlicher Zahlen x und
y ist mit Algorithmus 3.2 in Laufzeit O(l log2 3 ) möglich, wobei l = 1 + log2 (max{x, y})
ist.

Beweis Die Korrektheit des Algorithmus folgt direkt aus der Gl. (3.1).
3.2 Multiplikation 47

Für die Laufzeit zeigen wir zunächst, dass es eine Konstante c ∈ N gibt, so dass die Zahl
T (l) der elementaren Rechenschritte (im engeren Sinne) von Algorithmus 3.2 in Abhän-
gigkeit von l (der maximalen Zahl der Stellen von x und y) wie folgt abgeschätzt werden
kann:
T (l) ≤ c für l ≤ 3
(3.2)
T (l) ≤ c · l + 3T ( 2l + 1) für l ≥ 4.
Um die zweite Ungleichung nachzuweisen, muss man feststellen, dass die drei rekursi-
ven Aufrufe des Algorithmus mit Zahlen erfolgen, die jeweils höchstens  2l + 1 Stellen
haben. Dies ist für x  und y  offensichtlich; sie haben sogar höchstens k =  2l  Stellen.
Ferner ist 1 + log2 x   ≤ 1 + log2 Bx  = 1 + log2 x − k ≤ l − k =  2l , also hat x  und
analog y  höchstens  2l Stellen. Somit haben auch x  + x  und y  + y  jeweils höchstens
 2l + 1 Stellen. Dies ist weniger als l für alle l ≥ 4, weshalb die Rekursion terminiert.
Alle anderen Operationen brauchen nur O(l) Schritte; hier nutzt man aus, dass B eine
Zweierpotenz ist und x und y (ebenso wie alle Zwischenergebnisse) in Binärdarstellung
angegeben sind. Ferner benutzt man natürlich Proposition 3.1.
Die Rekursion (3.2) kann man wie folgt lösen. Wir behaupten zunächst, dass für alle
m ∈ N ∪ {0} gilt:
T (2m + 2) ≤ c · (4 · 3m − 2 · 2m − 1) (3.3)
Dies folgt einfach durch Induktion: Für m = 0 ist T (3) ≤ c nach (3.2). Für m ∈ N ist,
ebenfalls nach (3.2), T (2m + 2) ≤ c · (2m + 2) + 3T (2m−1 + 2), was nach Induktionsvor-
aussetzung abgeschätzt werden kann durch

T (2m + 2) ≤ c · (2m + 2 + 3 · (4 · 3m−1 − 2 · 2m−1 − 1)) = c · (4 · 3m − 2 · 2m − 1),

womit (3.3) bewiesen ist.


Für ein beliebiges l ∈ N mit l > 2 sei nun m := log2 (l − 2) < 1 + log2 l. Dann gilt
T (l) ≤ T (2m + 2) < 4c · 3m < 12c · 3log2 l = 12c · l log2 3 . 

Man beachte, dass log2 3 kleiner als 1,59 ist und der Algorithmus somit (besonders
bei zwei ungefähr gleich langen Zahlen) wesentlich schneller ist als die Schulmethode mit
Laufzeit O(l 2 ).
In der Praxis multipliziert man auch größere Zahlen direkt (mindestens bis zu 32 oder
64 Binärstellen: so große Zahlen werden von heutigen Prozessoren in wenigen Taktzyklen
multipliziert) und wählt erst bei noch größeren Zahlen den rekursiven Ansatz von Karatsuba.
Noch schnellere Multiplikationsalgorithmen wurden von Schönhage und Strassen [31]
sowie Fürer [18] gefunden. Die (ganzzahlige) Division lässt sich auf die Multiplikation
zurückführen und ebenso schnell durchführen; wir gehen darauf in Abschn. 5.5 weiter ein.
48 3 Rechnen mit ganzen Zahlen

3.3 Euklidischer Algorithmus

Der euklidische Algorithmus (den Euklid um 300 [Link]. in Buch VII seiner Elemente
beschrieb) erlaubt das effiziente Berechnen des größten gemeinsamen Teilers zweier Zahlen,
und damit das Kürzen von Brüchen.

Definition 3.4 Für a, b ∈ N ist ggT(a, b) der größte gemeinsame Teiler, d. h. die größte
natürliche Zahl, die sowohl Teiler von a als auch Teiler von b ist. Gilt ggT(a, b) = 1, so
heißen a und b teilerfremd. Ferner setzen wir ggT(a, 0) := ggT(0, a) := a für alle a ∈ N
(jede natürliche Zahl ist Teiler von 0) und ggT(0,0) := 0.
Für a, b ∈ N ist kgV(a, b) das kleinste gemeinsame Vielfache , d. h. die kleinste natürliche
Zahl, von der sowohl a als auch b Teiler sind.

Lemma 3.5 Für alle a, b ∈ N gilt:


(a) a · b = ggT(a, b) · kgV(a, b);
(b) ggT(a, b) = ggT(a mod b, b).

Beweis

(a) Für jeden gemeinsamen Teiler x von a und b (insbesondere also für x = ggT(a, b)) ist
ab
x ein gemeinsames Vielfaches von a und b, woraus „≥“ folgt.
Seien p, q ∈ N teilerfremd mit qp = kgV(a,b)
ab . Dann sind pa pb
q ∈ Z und q ∈ Z, d. h. q ist
pab
Teiler von a und b. Somit ist ggT(a, b) ≥ q = kgV(a,b) ≥ kgV(a,b)
ab
.
(b) Ist x Teiler von a und b, so offenbar auch von a − b b  = a mod b. Ist umgekehrt x
a

Teiler von a mod b und von b, so auch von (a mod b) + b ab  = a. 

Lemma 3.5(b) ergibt direkt die Korrektheit des

Algorithmus 3.6 (Euklidischer Algorithmus)


Eingabe: a, b ∈ N.
Ausgabe: ggT(a, b).

while a > 0 and b > 0 do


if a < b then b ← b mod a else a ← a mod b
output max{a, b}

Beispiel 3.7 ggT(6314, 2800) = ggT(2800, 714) = ggT(714, 658) = ggT(658, 56) =
ggT(56, 42) = ggT(42, 14) = ggT(14, 0) = 14.
3.3 Euklidischer Algorithmus 49

Der Euklidische Algorithmus ist in der Funktion gcd im Programm 3.8 rekursiv imple-
mentiert. Diese Funktion könnte man auch in unsere Klasse LargeInt integrieren (sofern
der %-Operator für LargeInt implementiert ist). Die um eine solche ggT-Funktion er-
weiterte Klasse LargeInt kann man dann als Typ für Zähler und Nenner in Fraction
benutzen und alle Brüche stets (am Ende jeder Rechenoperation) kürzen.

Programm 3.8 (Euklidischer Algorithmus)


1 // e u c l i d . cpp ( E u c l i d e a n A l g o r i t h m )
2
3 # i n c l u d e < iostream >
4
5 u s i n g m y i n t = long long ;
6
7 m y i n t gcd ( m y i n t a , m y i n t b ) // c o m p u t e g r e a t e s t c o m m o n d i v i s o r
8 { // u s i n g E u c l i d e a n a l g o r i t h m
9 if ( b == 0) {
10 return a;
11 }
12 else {
13 r e t u r n gcd ( b , a % b );
14 }
15 }
16
17
18 int main ()
19 {
20 myint a , b;
21 std :: cout << " This p r o g r a m c o m p u t e s the g r e a t e s t c o m m o n d i v i s o r .\ n "
22 << " E n t e r two n a t u r a l numbers , s e p a r a t e d by b l a n k : " ;
23 std :: cin >> a >> b ;
24 std :: cout << " gcd ( " << a << " , " << b << " ) = " << gcd (a , b ) << " \ n " ;
25 }

Die Anzahl der Iterationen (bzw. rekursiven Aufrufe) des Euklidischen Algorithmus kann
leicht durch 2 + log2 max{a, 1} + log2 max{b, 1} beschränkt werden, da sich in jeder
Iteration (bis auf evtl. die erste in gcd) mindestens eine der beiden Zahlen mindestens
halbiert.
Für eine genauere Laufzeitanalyse des Euklidischen Algorithmus 3.6 benötigen wir zu-
nächst die Fibonacci-Zahlen Fi für i = 0, 1, 2, . . ., gegeben durch F0 := 0, F1 := 1
und
Fn+1 := Fn + Fn−1 für n ∈ N.
Die ersten Fibonacci-Zahlen sind also: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55. Die Fibonacci-Zahlen
waren übrigens schon mehr als 400 Jahre vor Fibonacci (ca. 1170–1240) in Indien bekannt.

Lemma 3.9 Für alle n ∈ N ∪ {0} ist


 √ n  √ n 
1 1+ 5 1− 5
Fn = √ − .
5 2 2
50 3 Rechnen mit ganzen Zahlen

Beweis Induktion über n. Die Aussage ist für n = 0 und n = 1 offensichtlich. Für n ≥ 2
ist

Fn = Fn−1 + Fn−2
⎛ √ n−1  √ n−2  √ n−1  √ n−2 ⎞
1 ⎝ 1+ 5 1+ 5 1− 5 1− 5 ⎠
=√ + − −
5 2 2 2 2
⎛ √ n−2  √   √ n−2  √ ⎞
1 ⎝ 1+ 5 1+ 5 1− 5 1− 5
=√ · +1 − · +1 ⎠
5 2 2 2 2
 √ n  √ n 
1 1+ 5 1− 5
=√ − ,
5 2 2
√ √  √ 2 √ √  √ 2
da 1+ 5
2 +1= 3+ 5
2 = 1+ 5
2 und 1− 5
2 +1= 3− 5
2 = 1− 5
2 . 

Lemma 3.10 Falls a > b > 0 und Algorithmus 3.6 k ≥ 1 Iterationen der while-Schleife
durchläuft, so gilt a ≥ Fk+2 und b ≥ Fk+1 .

Beweis Induktion über k. Für k = 1 gilt die Aussage wegen b ≥ 1 = F2 und a > b ⇒
a ≥ 2 = F3 .
Für den Induktionsschritt sei nun k ≥ 2. Wegen a > b ≥ 1 und k ≥ 2 wird die nächste
Iteration mit den Zahlen b und a mod b arbeiten. Auf diese können wir die Induktionsvoraus-
setzung anwenden, da k −1 Iterationen verbleiben und b > a mod b > 0 (wegen k −1 > 0).
Wir erhalten b ≥ Fk+1 und a mod b ≥ Fk . Es gilt somit a = a/b · b + (a mod b) ≥
b + (a mod b) ≥ Fk+1 + Fk = Fk+2 .

Satz 3.11 (Satz von Lamé) Falls a ≥ b und b < Fk+1 für ein k ∈ N, so durchläuft
Algorithmus 3.6 weniger als k Iterationen der while-Schleife.

Beweis Falls b = 0, so durchläuft der Algorithmus gar keine Iteration, falls a = b > 0 nur
eine. Die restlichen Fälle folgen direkt aus Lemma 3.10. 

Bemerkung 3.12 Falls ggT(Fk+2 , Fk+1 ) für k ∈ N berechnet wird, so sind genau k Iteratio-
nen notwendig, denn ggT(F3 , F2 ) = ggT(2,1) braucht eine Iteration und ggT(Fk+2 , Fk+1 )
für k ≥ 2 wird auf ggT(Fk+1 , Fk+2 − Fk+1 ) = ggT(Fk+1 , Fk ) zurückgeführt. Satz 3.11 ist
also bestmöglich.

Korollar 3.13 Falls a ≥ b > 1, so durchläuft Algorithmus 3.6 höchstens logφ b



Iterationen, wobei φ = 1+2 5 . Die Gesamtlaufzeit von Algorithmus 3.6 zur Berechnung
von ggT(a, b) ist O((log a)3 ).
3.4 Übungsaufgaben 51

Beweis Wir zeigen zunächst durch Induktion über n, dass Fn > φ n−2 für alle n ≥ 3 gilt.
Für n = 3 haben wir F3 = 2 > φ. Wegen 1 + φ = φ 2 erhalten wir unter Ausnutzung der
Induktionsvoraussetzung für n ≥ 3:

Fn+1 = Fn + Fn−1 > φ n−2 + φ n−3 = φ n−3 (φ + 1) = φ n−1 .

Für die erste Aussage des Korollars sei k := 1 + logφ b ≥ 2. Wir haben b ≤ φ k−1 < Fk+1
und die Aussage folgt mit Satz 3.11. In jeder Iteration muss x mod y für zwei Zahlen x und
y mit a ≥ x ≥ y berechnet werden. Dies geht mit der Schulmethode in O((log a)2 ) womit
sich eine Gesamtlaufzeit von O((log a)3 ) ergibt. 

Bemerkung 3.14 Die Konstante φ = 1+ 5
≈ 1,618 . . . wird der goldene Schnitt genannt.
√ 2
Es gilt 1− 5
2 ≈ −0,618 . . .. Deshalb ist nach Lemma 3.9 Fn ≈ √1 · φn .
5

3.4 Übungsaufgaben

1. Implementieren Sie Karatsubas Algorithmus für die Klasse LargeInt.


2. Zeigen Sie, dass man mit Hilfe von Karatsubas Algorithmus zwei natürliche Zahlen
mit k- bzw. l-stelliger Binärdarstellung (mit k ≤ l) in O(k log2 (3)−1l) Zeit multiplizieren
kann.
3. Es seien a, b, c, d ∈ R Konstanten mit c, d ≥ 0 und 2 ≤ b < a. Außerdem sei
T : N → R≥0 eine monoton steigende Funktion, welche die folgenden Bedingungen
erfüllt:
• T (n) ≤ d für n ≤ b und
• T (n) ≤ c · n + a · T  nb für n > b.
Zeigen Sie, dass dann T (n) = O(n logb (a) ) gilt.
4. Zeigen Sie, dass es für alle natürlichen Zahlen m und n ganze Zahlen s und t gibt, für
die ggT(n, m) = s · m + t · n gilt.
5. Es seien p eine Primzahl und a, b ∈ N. Zeigen Sie: Falls p Teiler von ab ist, so ist p
Teiler von a oder p ist Teiler von b.
Hinweis: Benutzen Sie Aufgabe 4.
6. Zeigen Sie, dass jede natürliche Zahl n ≥ 2 auf eindeutige Weise als n = p1 · . . . · pk
geschrieben werden kann, wobei k ∈ N und p1 , . . . , pk Primzahlen sind mit p1 ≤
· · · ≤ pk .
Hinweis: Benutzen Sie Aufgabe 5.
7. Integrieren Sie den Euklidischen Algorithmus in die Klassen Fraction und
LargeInt.
52 3 Rechnen mit ganzen Zahlen

8. Zeigen Sie, dass man die Laufzeit von Algorithmus 3.6 auch durch O((log a)(log b) +
(log b)3 ) beschränken kann.
9. Beweisen Sie die Korrektheit der folgenden Funktion zur Berechnung des größten ge-
meinsamen Teilers zweier natürlicher Zahlen. Diese Funktion benutzt nur die Division
durch 2 (die in Binärdarstellung natürlich sehr einfach und schnell realisierbar ist).
Welche Laufzeit hat diese Funktion?
1
2 myint gcd2 ( myint a , myint b ) // c o m p u t e g r e a t e s t c o m m o n d i v i s o r
3 { // (" b i n a r y E u c l i d e a n a l g o r i t h m ")
4 if ( b == 0) r e t u r n a ;
5 else if ( a % 2 == 0) {
6 if ( b % 2 == 0) r e t u r n 2 * gcd2 ( b / 2 , a / 2);
7 e l s e r e t u r n gcd2 (b , a / 2);
8 }
9 else {
10 if ( b % 2 == 0) r e t u r n gcd2 (a , b / 2);
11 else if ( a < b ) r e t u r n gcd2 (a , ( b - a ) / 2);
12 e l s e r e t u r n gcd2 (b , ( a - b ) / 2);
13 }
14 }

10. Bestimmen Sie die kleinste Zahl c ∈ R, für die Fn ≤ cn für alle Fibonacci-Zahlen Fn
mit n ∈ N gilt.
11. Schreiben Sie ein Programm, das zu einer Eingabe k ∈ N die Fibonacci-Zahl Fk
berechnet, und das auch für große k (mindestens bis 100) korrekt funktioniert.
12. Zeigen Sie, dass sich jede natürliche Zahl n als Summe von paarweise verschiedenen
Fibonacci-Zahlen schreiben lässt. Zeigen Sie, dass O(log n) Summanden für eine solche
Darstellung ausreichen.
13. Beweisen Sie die folgenden Aussagen über die Fibonacci-Zahlen:
(a) Fn+1 · Fn−1 − Fn2 = (−1)n für n ≥ 1.
(b) Fn+k = Fk · Fn+1 + Fk−1 · Fn für k ≥ 1, n ≥ 0.
(c) Fk·n ist ein Vielfaches von Fn für k ≥ 1.
(d) ggT(Fm , Fn ) = FggT(m,n) für m, n ≥ 1.
Hinweis: Beweisen Sie zunächst unter Benutzung von (b) und (c), dass
ggT(Fm , Fn ) = ggT(Fn , Fr ), falls m mit Rest r durch n teilbar ist.
Approximative Darstellungen reeller Zahlen
4

Durch die Kombination der Klassen LargeInt (Programm 2.11) und Fraction (Pro-
gramm 2.10) – erweitert um die noch fehlenden Operationen – kann man mit rationalen
Zahlen rechnen, ohne Rundungsfehler zu machen. Allerdings sind die einzelnen Rechen-
operationen vergleichsweise langsam, weil Zähler und Nenner – auch wenn man immer mit
dem Euklidischen Algorithmus kürzt – groß werden können.
Wesentlich schneller ist das Rechnen mit Standarddatentypen wie double; allerdings
muss man hier Rundungsfehler in Kauf nehmen und kontrollieren.
Komplexe Zahlen lassen sich natürlich (näherungsweise) speichern, indem man
(Näherungen von) Real- und Imaginärteil speichert, ähnlich wie man rationale Zahlen als
Paare ganzer Zahlen (Zähler und Nenner) darstellen kann. Analog zur Klasse Fraction
kann man auch hierfür eine Klasse definieren. Die C++-Standardbibliothek enthält sogar
schon einen Typ complex<double>, den man für komplexe Zahlen benutzen kann. Wir
beschränken uns im Folgenden der Einfachheit halber auf reelle Zahlen; alles überträgt sich
aber natürlich direkt auf komplexe Zahlen.

4.1 b-adische Darstellung reeller Zahlen

Die b-adische Darstellung natürlicher Zahlen lässt sich wie folgt auf reelle Zahlen erweitern:

Satz 4.1 (b-adische Darstellung reeller Zahlen) Sei b ∈ N mit b ≥ 2. Sei x ∈ R\{0}.
Dann existieren eindeutig bestimmte Zahlen E ∈ Z, σ ∈ {−1,1} und z i ∈ {0, 1, . . . , b − 1}
für i ∈ N ∪ {0} mit {i ∈ N | z i  = b − 1} unendlich, z 0  = 0 und

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 53


S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
54 4 Approximative Darstellungen reeller Zahlen

∞ 

x = σ · bE · z i · b−i . (4.1)
i=0

(4.1) heißt die (normalisierte) b-adische Darstellung von x.

∞
Wir benutzen hier die übliche Notation für Reihen (unendliche Summen) i=0 ai :=
n
limn→∞ i=0 ai . Man beachte, dass der Grenzwert existiert, weil die Folge monoton wach-
send und durch b von oben beschränkt ist.

Beweis Sei x ∈ R\{0}. Offenbar muss σ = −1 gelten falls x < 0 und σ = 1 falls x > 0.
Für z i ∈ {0, 1, . . . , b − 1} (i ∈ N ∪ {0}) mit {i ∈ N | z i  = b − 1}  = ∅ und z 0  = 0 gilt

 ∞

1 ≤ z i · b−i < (b − 1) · b−i = b.
i=0 i=0

Somit muss E := logb |x| gewählt werden.


Setze a0 := b−E |x| und definiere rekursiv für i ∈ N ∪ {0}:

z i := ai und ai+1 := b(ai − z i ).

Man beachte 1 ≤ a0 < b und 0 ≤ ai < b für i ∈ N, also z i ∈ {0, . . . , b − 1} für alle i und
z 0  = 0.

n
Behauptung Es gilt a0 = z i · b−i + an+1 b−n−1 für alle n ∈ N ∪ {0}.
i=0
Wir zeigen diese Behauptung per Induktion über n. Als Induktionsanfang prüfen wir die
Gleichung für n = 0: hier ist in der Tat a0 = z 0 b−0 + a1 b−1 .
Für den Induktionsschritt sei n ∈ N. Die Induktionsannahme (für n − 1) besagt a0 =

n−1 
n−1
z i · b−i + an b−n . Mittels an+1 = b(an − z n ) erhalten wir a0 = z i · b−i + z n b−n +
i=0 i=0
an+1 b−n−1 , also ist die Behauptung bewiesen.
Damit haben wir
 n 

−i
x = σ · b · a0 = σ · b · lim
E E
zi · b ,
n→∞
i=0

also (4.1).
Angenommen, {i ∈ N | z i  = b − 1} wäre endlich, d. h. es gäbe ein n 0 ∈ N mit z i = b − 1
für alle i > n 0 . Dann wäre


n0 ∞
 
n0
a0 = z i · b−i + (b − 1) · b−i = z i · b−i + b−n 0
i=0 i=n 0 +1 i=0

Wegen der Behauptung folgt an 0 +1 = b, ein Widerspruch. Damit ist die Existenz bewiesen.
4.2 Maschinenzahlen 55

Es bleibt, die Eindeutigkeit zu zeigen. Die Eindeutigkeit von σ und E wurde bereits
gezeigt. Angenommen, wir haben zwei Darstellungen

 ∞

−i
x = σ ·b · E
yi · b = σ ·b ·
E
z i · b−i
i=0 i=0

mit den genannten Eigenschaften. Sei n der kleinste Index mit yn  = z n . Ohne Beschränkung
der Allgemeinheit sei yn + 1 ≤ z n . Dann erhalten wir

 ∞
x
= yi · b−i
σb E
i=0

n−1 ∞

≤ yi · b−i + (z n − 1)b−n + yi b−i
i=0 i=n+1

n−1 ∞

< yi · b−i + (z n − 1)b−n + (b − 1)b−i
i=0 i=n+1

n−1
= z i · b−i + z n b−n
i=0
∞
≤ z i · b−i
i=0
x
= ,
σ bE
einen Widerspruch. 

Beispiel 4.2 Für b = 2 ist die b-adische Darstellung von


1
3 = 2−2 · (20 + 2−2 + 2−4 + 2−6 + · · · ) = (0,01)2
8,1 = 23 · (20 + 2−7 + 2−8 + 2−11 + 2−12 + 2−15 + 2−16 · · · ) = (1000,00011)2 .

4.2 Maschinenzahlen

Da in einem Rechner nur endlich viele Stellen einer reellen Zahl dargestellt werden können,
nutzt man in Anlehnung an Satz 4.1 die folgende Darstellung:

Definition 4.3 Seien b, m ∈ N, b ≥ 2. Eine b-adische m-stellige normalisierte Gleit-


kommazahl hat die Form
56 4 Approximative Darstellungen reeller Zahlen

m−1 

−i
x =σ ·b · E
zi · b
i=0
m−1 −i mit z  = 0 und
mit dem Vorzeichen σ ∈ {−1,1}, der Mantisse i=0 z i · b 0
z i ∈ {0, . . . , b − 1} für alle i ∈ N ∪ {0}, sowie dem Exponenten E ∈ Z.

Beispiel 4.4 Für b = 10 hat die Zahl 136,5 die normalisierte 5-stellige Darstellung 136,5 =
1,3650 · 102 . In C++-Notation schreibt sich das als 1.365e+2.
Die Zahl 13 besitzt für b = 10 und m ∈ N keine Darstellung als m-stellige normalisierte
Gleitkommazahl. Die Zahl 0 besitzt überhaupt keine Darstellung als normalisierte Gleit-
kommazahl.

Für die Darstellung im Rechner sind geeignete Werte für b, m und den zulässigen Bereich
{E min , . . . , E max } für E zu wählen. Dies bestimmt den Maschinenzahlbereich

F(b, m, E min , E max )

als die Menge der b-adischen m-stelligen normalisierten Gleitkommazahlen mit Exponent
E ∈ {E min , . . . , E max }, zuzüglich der Zahl 0. Die Elemente eines (festen) Maschinenzahl-
bereichs heißen Maschinenzahlen.
Im 1985 verabschiedeten IEEE-Standard 754 [20] (IEEE steht für The Institute of Elec-
trical and Electronics Engineers) wird beispielsweise ein Datentyp double mit b = 2, m =
53 und E ∈ {−1022, . . . , 1023} definiert. Wir schreiben Fdouble := F(2, 53, −1022,1023).
Zur Darstellung dieser Zahlen reichen 64 Bits aus: ein Bit für das Vorzeichen, 52 Bits
z 1 · · · z 52 für die Mantisse sowie 11 Bits für den Exponenten E. Da wegen der Normali-
sierung das nullte Bit z 0 immer den Wert 1 hat, kann dieses weggelassen werden (es wird
daher „hidden bit“ genannt).
Für den Exponenten wird die sogenannte Bias-Darstellung benutzt: Eine Konstante (Bias)
wird hinzuaddiert, um alle Exponenten positiv zu machen. Im konkreten Fall double wird
1023 hinzuaddiert. Für den C++-Datentyp double schreibt der C++-Standard nicht vor,
wie er zu realisieren ist. Üblicherweise wird er aber gemäß dem IEEE-Datentyp double
implementiert. Davon gehen wir im Folgenden aus.
Die Abspeicherung in Bits für die normalisierte Gleitkommadarstellung sieht wie folgt
aus:
1 Bit 11 Bits 52 Bits
1+σ 
 
E + 1023  z 1 z 2 . . . z 52
2

Die „freien“ Exponenten −1023 und 1024 ( = 0 und 2047 in Bias-Darstellung) werden
benutzt, um ±0 und ±∞ (Exponent größer als 1023) zu kodieren sowie für subnormale
Zahlen (Exponent kleiner als −1022) und NaN (Not a Number), das als Ergebnis von
unzulässigen Rechenoperationen wie z. B. log(−1) dient.
4.3 Rundung 57

Die größte so darstellbare Zahl ist

max Fdouble = std::numeric_limits<double>::max()


= 1, 1 . . . 1 ·21023 = (2 − 2−52 ) · 21023 = 21024 − 2971 ≈ 1,797693 · 10308 .
52

Die kleinste positive Maschinenzahl in Fdouble ist

min{ f ∈ Fdouble , f > 0} = std::numeric_limits<double>::min()


= 1,0 · 2−1022 ≈ 2,225074 · 10−308 .

Wir bezeichnen mit range(F) := [min{ f ∈ F, f > 0}, max F] das kleinste Intervall, in
dem alle darstellbaren positiven Zahlen liegen.
Der C++Datentyp float (für englisch floating point) benutzt hingegen meistens nur
32 Bits und damit einen wesentlich geringeren Darstellungsbereich und eine viel gerin-
gere Genauigkeit. Er ist deshalb nur selten zu empfehlen. Umgekehrt hat der Datentyp
long double meistens eine höhere Genauigkeit als double. Häufig werden 80 Bits für
die Darstellung von long double benutzt (64 Bits für die Mantisse und 16 Bits für den
Exponenten), da die FPUs (Floating Point Units) in heutigen Prozessoren vielfach eine sol-
che Darstellung von Gleitkommazahlen verwenden. Auf solchen Gleitkommazahlen kann
eine FPU die elementaren Rechenoperationen in wenigen Taktzyklen durchführen.
Der Datentyp double ist der Standarddatentyp in C++ für die approximative Darstellung
reeller Zahlen.

4.3 Rundung

Da es nur endlich viele Maschinenzahlen gibt, muss man sich mit gerundeten Darstellungen
von reellen Zahlen zufrieden geben:

Definition 4.5 Sei F = F(b, m, E min , E max ) ein Maschinenzahlbereich. Eine Abbildung
rd : R → F heißt Rundung (zu F), wenn für alle x ∈ R gilt:

|x − rd(x)| = min |x − a|.


a∈F

Man beachte, dass es mehrere Rundungen gibt. Zum Beispiel kann in F(10, 2, 0, 2) die Zahl
12,5 auf 12 oder 13 gerundet werden. Bei kaufmännischer Rundung wird x im Zweifel zur
betragsmäßig größeren Zahl gerundet. IEEE 754 fordert aber: x wird so gerundet, dass die
letzte Stelle gerade wird, also zum Beispiel rd(12,5) = 12 und rd(13,5) = 14. Ein Grund
58 4 Approximative Darstellungen reeller Zahlen

dafür ist, dass bei Rechnungen wie (· · · (((12 + 0,5) − 0,5) + 0,5) − . . . abwechselnd auf-
und abgerundet wird (bei kaufmännischer Rundung divergierte diese Reihe).

Definition 4.6 Es seien x, x̃ ∈ R, wobei


 x̃ eine Näherung von x sei. Dann heißt |x − x̃| der
 x−x̃ 
absolute Fehler. Falls x  = 0 heißt  x  der relative Fehler.

Die Maschinengenauigkeit ist der maximale relative Fehler, der bei einer Rundung von
Zahlen im darstellbaren Bereich auftreten kann:

Definition 4.7 Sei F ein Maschinenzahlbereich. Die Maschinengenauigkeit von F wird


mit eps(F) bezeichnet und ist definiert durch
 
 x − rd(x) 
eps(F) := sup    | x ∈ R, |x| ∈ range(F), rd Rundung zu F .
x 

Satz 4.8 Für jeden Maschinenzahlbereich F = F(b, m, E min , E max ) mit E min < E max gilt
1
eps(F) = .
1 + 2bm−1
 
Beweis Für x = b E min · 1 + 21 b−m+1 ist x ∈ range(F) und für jede Rundung rd zu F gilt:
 
 x − rd(x)  b E min · 21 b−m+1
  = |x − rd(x)| =   =
1
,
 x  x b E min · 1 + 21 b−m+1 1 + 2bm−1

woraus eps(F) ≥ 1+2b1 m−1 folgt.


Für die andere Richtung genügt es, positive Zahlen x zu betrachten. Sei also x ∈ range(F)
∞
und x ∈/ F (sonst gibt es keinen Rundungsfehler), und sei x = b E · i=0 z i · b−i die nor-
malisierte b-adische Darstellung von x. Dann sind
m−1 

m−1 
−i −i −m+1
x =b · E
zi · b und x =b · E
zi · b +b
i=0 i=0

in F und es gilt x < x < x .


Für jede Rundung rd zu F gilt |x − rd(x)| ≤ 21 (x − x ) = 21 · b E · b−m+1 . Wegen
z 0 > 0 gilt x = b E + |x − b E | ≥ b E + |x − rd(x)|, und somit wie gefordert
 
 x − rd(x)  |x − rd(x)| 1 1 1
  ≤ = ≤ = .
  b E + |x − rd(x)| bE bE + m−1
x 1+ 1+ 1
|x−rd(x)|
1 2b
E ·b−m+1
2 ·b


4.4 Maschinenzahlenarithmetik 59

Beispiel 4.9 Für den durch IEEE 754 definierten Typ double gilt:
1
eps(Fdouble ) = ≈ 1,11 · 10−16 .
1 + 253
Man beachte, dass std::numeric_limits<double>::epsilon() die kleinste
Zahl x liefert mit x > 0 und 1 + x ∈ Fdouble . Es gilt daher x = 2−52 ≈ 2 · eps(Fdouble ).

Definition 4.10 Sei F ein Maschinenzahlbereich und s ∈ N. Eine Maschinenzahl f ∈ F hat


(mindestens) s signifikante Stellen in der b-adischen Gleitkommadarstellung, falls f  = 0
und für jede Rundung rd und jede Zahl x ∈ R mit rd(x) = f gilt:
logb | f | +1−s
|x − f | ≤ 1
2 ·b .

Beispiel 4.11 Jede Maschinenzahl f ∈ Fdouble \ {−max Fdouble , 0, max Fdouble } hat
mindestens 52 log10 2 = 15 signifikante Stellen in der Dezimaldarstellung, denn für x ∈ R
∞
mit normalisierter Gleitkommadarstellung x = σ · 2 E · i=0 z i · 2−i und rd(x) = f ist we-
gen 2 ≤ | f |
E

1 E −52
|x − f | ≤ ·2 ·2 ≤ 2−53 | f | < 1
2 · 10 log10 | f | +1−52 log10 2
.
2
Nach #include <iomanip> kann man mit std::cout << std::
setprecision(15); festlegen, dass bei nachfolgenden Ausgaben vom Typ double
diese Stellen auch ausgegeben werden.

4.4 Maschinenzahlenarithmetik

Sei ◦ eine der vier Operationen {+, −, ·, /}. Wie Beispiel 4.13 verdeutlicht, gilt für x, y ∈ F
nicht notwendigerweise x ◦ y ∈ F. Stattdessen wird auf dem Rechner eine Ersatzoperati-
on  ausgeführt, sodass x  y ∈ F gilt. Wir nehmen im Folgenden an, dass:

Annahme 4.12 x  y = rd(x ◦ y) für eine Rundung rd zu F und alle x, y ∈ F.

Dies wird zum Beispiel vom IEEE-Standard 754 gefordert. Zudem wird dies dort auch für die
Wurzelfunktion, nicht aber für zum Beispiel sin(x), exp(x) etc. vorausgesetzt. Annahme 4.12
garantiert, dass man als Ergebnis einer Rechenoperation das auf die nächste Maschinenzahl
gerundete exakte Ergebnis von x ◦ y erhält. Man beachte aber, dass zur Berechnung von
x  y nicht zuerst x ◦ y exakt berechnet werden muss. Vielmehr kann man die Rechnung
abbrechen, sobald hinreichend viele Stellen berechnet sind.
60 4 Approximative Darstellungen reeller Zahlen

Beispiel 4.13 Sei F = F(10, 2, −5,5).


Wähle x = 4,5 · 101 = 45 und y = 1,1 · 100 = 1,1. Dann sind

x ⊕y = rd(x + y) = rd(46,1) = rd(4,61 · 101 ) = 4,6 · 101


x y = rd(x − y) = rd(43,9) = rd(4,39 · 101 ) = 4,4 · 101
x y = rd(x · y) = rd(49,5) = rd(4,95 · 101 ) ∈ {4,9 · 101 , 5,0 · 101 }
x y = rd(x/y) = rd(40,90) = rd(4,09 · 101 ) = 4,1 · 101 .

Gemäß Annahme 4.12 gilt für den relativen Fehler


   
x ◦ y − x  y  
  =  x ◦ y − rd(x ◦ y)  ≤ eps(F),
 x◦y   x◦y 
falls |x ◦ y| ∈ range(F).
Das Kommutativgesetz für Addition und Multiplikation gilt auch in der Maschinenzahlena-
rithmetik. Assoziativ- und Distributivgesetz gelten jedoch im Allgemeinen nicht:

Beispiel 4.14 Im Maschinenzahlbereich F = F(10,2, −5,5) seien x = 4,1 · 100 und y =


8,2 · 10−1 und z = 1,4 · 10−1 . Dann sind

(x ⊕ y) ⊕ z = 4,9 ⊕ 0,14 = 5,0


x ⊕ (y ⊕ z) = 4,1 ⊕ 0,96 = 5,1
x  (y ⊕ z) = 4,1  0,96 = 3,9
(x  y) ⊕ (x  z) = 3,4 ⊕ 0,57 = 4,0

Folglich können mathematisch äquivalente Ausdrücke bei Ausführung in Maschinenzahle-


narithmetik zu wesentlich unterschiedlichen Ergebnissen führen, auch wenn der Input als
Maschinenzahlen darstellbar ist. Dies kann dramatische Folgen haben.

4.5 Übungsaufgaben

1. Schreiben Sie die Zahlen 125,125 und 99,9 und 17 jeweils in normalisierter 2-adischer
Darstellung.
2. Wir betrachten einen Ein-Byte-Rechner mit Gleitkomma-Arithmetik. Bei der Zahlen-
darstellung werden ein Bit für das Vorzeichen, vier Bits für die Mantisse und drei Bits
für den Exponenten (mit denen die Exponenten (−2, −1, 0, 1, 2) und 3 kodiert werden
können) verwendet. Die führende Eins in der Mantisse wird nicht abgespeichert.
a) Welche Darstellung haben die Zahlen 6,5 und −0,375?
b) Wie viele Zahlen können in diesem Gleitkomma-Format dargestellt werden?
4.5 Übungsaufgaben 61

c) Geben Sie die minimale und maximale darstellbare Zahl z min und z max sowie die
betragsmäßig kleinste darstellbare Zahl ungleich Null an.
d) Skizzieren Sie alle darstellbaren Zahlen auf einer Zahlengeraden.
e) Nicht darstellbare Zahlen werden auf die nächstgelegene darstellbare Zahl gerundet.
Bestimmen Sie den maximalen absoluten und relativen Rundungsfehler für reelle
Zahlen im Bereich [z min , z max ].
3. Was ist die kleinste natürliche Zahl, die nicht in Fdouble ist?
4. Geben Sie zwei positive Zahlen x, y ∈ Fdouble an mit x − y ∈ / Fdouble .
5. Es sei rd : R → Fdouble eine Rundungsfunktion.
a) Welchen Wert hat rd( 13 )? Geben Sie einen möglichst einfachen Ausdruck hierfür an.
b) Wie groß ist der relative Fehler, der bei dieser Rundung auftritt?
6. Es sei F = F(b, m, E min , E max ) ein Maschinenzahlbereich mit m > 1. Zeigen Sie:
a) eps(F) ∈ / F.
b) Sei rd eine Rundung zu F und x ∈ range(F). Zeigen Sie, dass dann ein ε ∈ R existiert
mit |ε| ≤ eps(F) und rd(x) = x · (1 + ε).
7. Wenn eps die Maschinengenauigkeit eines Maschinenzahlbereichs ist, wie viele signifi-
kante Dezimalstellen hat eine von 0 verschiedene Zahl in diesem Maschinenzahlbereich
dann mindestens?
8. Was ist die größte natürliche Zahl n, für die folgendes gilt: Für alle x, y ∈ Fdouble mit
0 ≤ x ≤ y ≤ 2n x ist rd(x + y)  = y?
9. Kompilieren Sie das folgende Programm. Wie erklären Sie sich die Ergebnisse, die es
berechnet?

1 # i n c l u d e < iostream >


2 # i n c l u d e < iomanip >
3
4 int main ()
5 {
6 float w = 0;
7 d o u b l e x = 0 , y = 0 , z = 0;
8
9 for ( long i =1; i < = 1 0 0 0 0 0 0 0 0 ; ++ i ) w +=1;
10 for ( long i =1; i < = 1 0 0 0 0 0 0 0 0 ; ++ i ) x +=1;
11 for ( long i =1; i < = 1 0 0 0 0 0 0 0 ; ++ i ) y +=.1;
12 for ( long i =1; i < = 1 0 0 0 0 0 0 ; ++ i ) z +=.1;
13
14 std :: cout << std :: s e t p r e c i s i o n (15)
15 << " w = " << w << " \ n "
16 << " x = " << x << " \ n "
17 << " y = " << y << " \ n "
18 << " z = " << z << " \ n " ;
19 }

10. Zeigen Sie, wie man für zwei 2-adische m-stellige normalisierte Gleitkommazahlen x
und y, deren Exponenten in Binärdarstellung höchstens m Bits haben, eine entspre-
chende Darstellung von x ⊕ y in der Laufzeit O(m) berechnen kann.
62 4 Approximative Darstellungen reeller Zahlen

11. Gibt es in Beispiel 4.14 andere Zahlen x, y, z ∈ F, für die der Unterschied im Hinblick
auf Assoziativgesetz bzw. Distributivgesetz größer als 0,1 ist? Gibt es solche, bei denen
alle Zwischenergebnisse in range(F) liegen?
12. Es sei F = F(b, m, E min , E max ) ein Maschinenzahlbereich. Was ist max{|x ⊕ (y ⊕
z) − (x ⊕ y) ⊕ z| | x, y, z ∈ F mit x + y, y + z, x + y + z ∈ range(F)}?
13. Zeigen Sie analog zu Beispiel 4.14, dass auch für die Multiplikation das Assoziativge-
setz in der Maschinenzahlenarithmetik nicht gilt.
Rechnen mit Fehlern
5

Bei numerischen Berechnungsproblemen muss man immer mit Fehlern rechnen. Wegen der
nur endlich vielen Maschinenzahlen entstehen Fehler nicht nur bei der Eingabe von Daten
(zum Beispiel ist die Zahl 0,1 nicht exakt mit endlich vielen Stellen binär darstellbar; vgl.
Beispiel 4.2), sondern auch bei den elementaren Rechenoperationen +, −, · , /. Auch die
gewünschte Ausgabe kann eine nicht darstellbare Zahl sein. Man unterscheidet drei Arten
von Fehlern:

Datenfehler Um ein Rechenverfahren zu starten, benötigt man zunächst Eingabedaten. Bei


numerischen Berechnungsproblemen sind diese im Allgemeinen ungenau (z. B. Messdaten)
oder lassen sich nicht genau im Rechner darstellen. Anstelle von einem Eingabewert x hat
man also einen gestörten Eingabewert x̃. Selbst bei exakter Rechnung kann man damit kein
exaktes Ergebnis erwarten. Zudem pflanzen sich Fehler durch Rechenoperationen fort und
können sich selbst dann extrem verstärken, wenn exakt gerechnet wird; siehe Abschn. 5.2.

Rundungsfehler Wegen des begrenzten Maschinenzahlbereichs müssen alle Zwischener-


gebnisse gerundet werden (siehe Abschn. 4.4). Dies kann ebenfalls zu extremen Verfäl-
schungen des Endergebnisses führen, weil auch solche Fehler sich fortpflanzen.

Verfahrensfehler Viele Algorithmen liefern selbst bei exakter Rechnung nach endlich vielen
Schritten keine exakten Lösungen, sondern nähern sich einer solchen nur beliebig genau an,
d. h. sie berechnen eine (theoretisch unendliche) Folge, die gegen den gewünschten Wert
konvergiert. Das ist sogar unvermeidbar, wenn das Ergebnis irrational ist. Der beim Abbruch
eines Näherungsverfahrens verbleibende Fehler wird Verfahrensfehler genannt. Wir sehen
ein Beispiel in Abschn. 5.1.

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 63


S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
64 5 Rechnen mit Fehlern

In der Praxis kommen weitere Fehlerquellen hinzu. Zu nennen sind insbesondere Modell-
fehler, wenn nämlich das mathematische Modell das tatsächlich zu lösende Problem nicht
genau beschreibt, sowie natürlich durch Implementierungsmängel verursachte Fehler (bei-
spielsweise nicht abgefangene Bereichsüberschreitungen).

5.1 Binäre Suche

Hat man eine streng monoton wachsende Funktion f : R → R, und möchte man f −1 (x)
für ein x ∈ R berechnen, so bietet sich binäre Suche an, sofern man Schranken L und U hat
mit f (L) ≤ x ≤ f (U ). Hierzu muss man die Funktion f nicht explizit kennen, sondern
es reicht, dass man sie auswerten kann. Man setzt also nur eine Subroutine voraus, die bei
Eingabe x den Wert f (x) berechnet, über die man aber sonst nichts weiß. Man sagt auch:
f ist durch ein Orakel gegeben.
Die Grundidee der binären Suche ist, ein zu durchsuchendes Intervall in zwei Hälften zu
teilen, und festzustellen, in welcher Hälfte weitergesucht werden muss.

Beispiel 5.1 Wir können beispielsweise Quadratwurzeln mit binärer Suche berechnen, nur
unter Verwendung√der Multiplikation (in der Auswertung der Funktion f (x) = x 2 ). Wollen
wir zum Beispiel 3 berechnen, so sehen wir mit den Anfangsschranken L = 1 und U = 2:

√3 ∈ [1, 2]
1,5 = 2,25
2 ⇒ √3 ∈ [1,5, 2]
1,752 = 3,0625 ⇒ √3 ∈ [1,5, 1,75]
1,6252 = 2,640625 ⇒ √3 ∈ [1,625, 1,75]
1,6875 = 2,84765625
2 ⇒ √3 ∈ [1,6875, 1,75]
1,718752 = 2,9541015625 ⇒ √3 ∈ [1,71875, 1,75]
1,7343752 = 3,008056640625 ⇒ 3 ∈ [1,71875, 1,734375]

Die Folge der Intervallmittelpunkte konvergiert offenbar gegen die korrekte Lösung (wenn
auch recht langsam – nach sechs Iterationen kennen wir erst eine Nachkommastelle √ – dazu
später mehr). Natürlich können wir aber nie exakt irrationale Zahlen wie 3 berechnen.
Bezeichnet [li , u i ] das Intervall und m i = li +u i
2 dessen Mittelpunkt √ in der i-ten Iteration
(also m 1 = 1,5; m 2 = 1,75 usw.), so wissen wir a priori, dass √ |m i − 3| ≤ 21 |u i − li | =
−i −i
2 |u 1 − l1 | = 2 |U − L| ist, also beispielsweise |m 6 − 3| ≤ 2 = 64 −6 1
= 0,015625.
Dies bezeichnet man als a-priori-Schranke.
Im Nachhinein kann man oft eine bessere Schranke angeben, a-posteriori-Schranke
√ |m i2 −3| |m i2 −3|
genannt. Hier ist zum Beispiel |m i − 3| = √ ≤
m i +li . Für i = 6 ergibt sich
√ mi + 3
0,008056640625
|m 6 − 3| ≤ 3,453125 ≈ 0,002333.
5.1 Binäre Suche 65

Die diskrete Variante der binären Suche ist mindestens ebenso nützlich:

Algorithmus 5.2 (Binäre Suche (diskret))

Eingabe: Ein Orakel zur Berechnung einer monoton wachsenden Funktion f : Z → R.


Zahlen L , U ∈ Z mit L ≤ U , sowie y ∈ R mit y ≥ f (L).
Ausgabe: das maximale n ∈ {L , . . . , U } mit f (n) ≤ y.

l←L
u ←U +1
while u > l + 1 do
m ← l+u 2
if f (m) > y then u ← m else l ← m
output l

Satz 5.3 Algorithmus 5.2 arbeitet korrekt und terminiert nach O(log(U − L + 2)) Itera-
tionen.

Beweis Die Korrektheit folgt aus der Tatsache, dass jederzeit L ≤ l ≤ u − 1 ≤ U sowie
f (l) ≤ y sowie (u > U oder f (u) > y) gilt, d. h. die korrekte Antwort liegt stets in
{l, . . . , u − 1}.
Die Laufzeit folgt unmittelbar aus der Beobachtung, dass sich u − l − 1 in jeder Iteration
auf höchstens
      
l +u l +u l +u l +u−1
max − l − 1, u − − 1 ≤ max − l − 1, u − −1
2 2 2 2
u −l −1

2
verringert, d. h. mindestens halbiert. 

Die binäre Suche ist insbesondere nützlich zum Auffinden von Daten in sortierten Beständen.
Die Standardbibliothek stellt nach Einbinden von #include <algorithm> Funktio-
nen zur binären Suche zur Verfügung. Unter anderem gibt es die Funktion
binary_search, mit der man zum Beispiel in einem sortierten vector schnell tes-
ten kann, ob ein bestimmtes Element vorkommt.
66 5 Rechnen mit Fehlern

5.2 Fehlerfortpflanzung

Sind die gespeicherten Zahlen zu einem bestimmten Zeitpunkt fehlerbehaftet (aufgrund von
Datenfehlern oder Rundungsfehlern, s. o.), so pflanzen sich die Fehler durch Rechenope-
rationen auf den Zahlen weiter fort. Man spricht von Fehlerfortpflanzung. Das folgende
Lemma gibt Auskunft dar über, wie sich Fehler bei exakter Ausführung der Grundrechen-
arten fortpflanzen.

Lemma 5.4 Seien x, y ∈ R \ {0} und x̃, ỹ ∈ R Näherungen mit εx := x−x x̃ und ε y :=
y− ỹ x◦y−(x̃◦ ỹ)
y (d. h. |εx | bzw. |ε y | sind die relativen Fehler). Dann gilt für ε◦ := x◦y mit ◦ ∈
{+, −, ·, /} :
x y
ε+ = εx · + εy · ,
x+y x+y
x y
ε− = εx · − εy · ,
x−y x−y
ε· = εx + ε y − εx · ε y ,
εx − ε y
ε/ = .
1 − εy

Beweis Es gilt: x̃ = x · (1 − εx ) und ỹ = y · (1 − ε y ). Wir berechnen:

x + y − (x̃ + ỹ) x + y − (1 − εx ) · x − (1 − ε y ) · y εx · x εy · y
ε+ = = = + ,
x+y x+y x+y x+y

ε− analog,

x · y − x̃ · ỹ x · y − (1 − εx ) · x · (1 − ε y ) · y
ε· = =
x·y x·y
= 1 − (1 − εx ) · (1 − ε y ) = εx + ε y − εx · ε y,

x/y − x̃/ ỹ x/y − ((1 − εx ) · x)/((1 − ε y ) · y)


ε/ = =
x/y x/y
1 − εx 1 − ε y − 1 + εx εx − ε y
=1− = = . 
1 − εy 1 − εy 1 − εy

Bei · und/addieren oder subtrahieren sich die relativen Fehler im Wesentlichen (sofern sie
klein sind). Bei + und − kann der relative Fehler jedoch sehr verstärkt werden, falls |x ± y|
sehr klein in Relation zu |x| und |y| ist. Dieser Effekt heißt Auslöschung, und es gilt, ihn so
weit wie möglich zu vermeiden.
5.3 Kondition 67

Die absoluten Fehler addieren (oder subtrahieren) sich bei Addition und Subtraktion,
können sich aber bei Multiplikation und Division sehr verstärken. Die Kombination aus
Punkt- und Strichrechnung ist daher besonders gefährlich.

Beispiel 5.5 Betrachte folgendes lineare Gleichungssystem:

10−20 x + 2y = 1
−20 −20
10 x + 10 y = 10−20

Subtrahiert man die zweite von der ersten Gleichung, erhält man

(2 − 10−20 )y = 1 − 10−20 .

Rundung auf die nächsten darstellbaren Zahlen (in Fdouble ) ergibt 2y = 1, also y = 0,5.
Dies ist noch annähernd korrekt. Einsetzen in die erste Gleichung liefert aber x = 0, was
völlig falsch ist (besser wäre es hier, in die zweite Gleichung einzusetzen). Die korrekte
1−10−20
Lösung ist x = 2−101 −10 = 0,5000. . . und y = 2−10 −20 = 0,4999. . .
Wir werden uns in Kap. 11 noch genauer mit der Lösung linearer Gleichungssysteme
befassen.

5.3 Kondition

Wir haben gesehen, dass bei bestimmten Rechenoperationen kleine (relative) Fehler in der
Eingabe zu großen (relativen) Fehlern im Ergebnis führen. Dies wird durch folgende Defi-
nition erfasst:

Definition 5.6 Sei P ⊆ D × E ein numerisches Berechnungsproblem mit D, E ⊆ R. Sei


d ∈ D eine Instanz von P mit d  = 0. Dann ist die (relative) Kondition von d, oft genannt
κ(d), definiert als
      
e |
| e−e   
lim sup inf d−d  e ∈ E, (d, e) ∈ P d ∈ D, e ∈ E, (d , e ) ∈ P, 0 < |d −d | < ε
   
ε→0 | d |

(wobei hier 00 := 0 sei).


Bezeichnen wir mit κ(d) die Kondition einer Instanz d ∈ D, so hat das Problem P selbst
die (relative) Kondition
sup {κ(d) | d ∈ D, d  = 0}.
68 5 Rechnen mit Fehlern

Die Kondition beschreibt den unvermeidbaren Fehler in der Ausgabe, der durch einen
Fehler in der Eingabe induziert wird; genauer das maximale Verhältnis dieser Fehler unter
der Annahme, dass der Eingabefehler klein ist.
Die Definition lässt sich auf mehrere Arten auf mehrdimensionale Probleme erweitern;
hierauf gehen wir in Abschn. 11.6 noch ein.
Obige Definition der relativen Kondition bezieht sich auf das Verhältnis der relativen
Fehler. Analog kann man die absolute Kondition mittels des Verhältnisses der absoluten
Fehler definieren. Diese wird aber eher selten betrachtet.

Proposition 5.7 Sei f : D → E ein eindeutiges numerisches Berechnungsproblem mit


D, E ⊆ R, und sei d ∈ D eine Instanz mit d  = 0 und f (d)  = 0. Dann ist ihre Kondition
gleich   
|d| | f (d  ) − f (d)|   
· lim sup d ∈ D, 0 < |d − d | < ε .
| f (d)| ε→0 |d  − d|

Korollar 5.8 Sei f : D → E ein eindeutiges numerisches Berechnungsproblem, wobei


D, E ⊆ R und f differenzierbar sei. Dann ist die Kondition einer Instanz d ∈ D mit d  = 0
und f (d)  = 0 gleich
| f  (d)| · |d|
κ(d) = .
| f (d)|

Diese elegante Form lässt sich auch auf mehrdimensionale differenzierbare Funktionen
verallgemeinern, worauf wir hier aber nicht eingehen können.
Je kleiner die Kondition, desto besser. Insbesondere ist eine Kondition von höchstens
1 gut, weil sich kleine Fehler bei der Lösung des Problems nicht verstärken müssen. Man
spricht von gut konditionierten Problemen.

Beispiel 5.9
√ 1
√ ·x
• Sei f (x) = x. Dann ist die Kondition gemäß Korollar 5.8 gleich 2 √xx = 21 . Somit ist
die Wurzelfunktion gut konditioniert.
• Sei f (x) = x + a (wir betrachten also nur die Fortpflanzung eines Fehlers im ersten
Summanden x). Dann ist die Kondition | x+a x
|. Die Addition ist also schlecht konditioniert,
falls |x + a| klein im Verhältnis zu |x| ist (Auslöschung).
• Sei f (x) = a · x. Dann ist die Kondition |a|·|x|
|a·x| = 1. Die Multiplikation ist also immer
gut konditioniert.
5.4 Fehleranalyse 69

5.4 Fehleranalyse

Ein Algorithmus heißt numerisch stabil (für eine bestimmte Instanz oder generell), wenn
jeder seiner Rechenschritte gut konditioniert ist. Es kann nur dann ein numerisch stabiler
Algorithmus existieren, wenn das Problem gut konditioniert ist. Ebenso wenig wie gut
konditioniert ist numerisch stabil exakt definiert.
Bei der Fehleranalyse unterscheidet man:

• Bei der Vorwärtsanalyse untersucht man, wie sich der relative Fehler im Laufe einer
Rechnung akkumuliert.
• Bei der Rückwärtsanalyse wird jedes Zwischenergebnis als exaktes Ergebnis für ge-
störte Daten interpretiert, und es wird abgeschätzt, wie groß die Eingangsdatenstörung
sein müsste, damit das berechnete Ergebnis korrekt wäre.

Beispiel 5.10 Betrachte die Addition zweier Zahlen im Maschinenzahlbereich F:

• Vorwärtsanalyse: x ⊕ y = rd(x + y) = (x + y) · (1 + ε) mit |ε| ≤ eps(F).


• Rückwärtsanalyse: x ⊕ y = x · (1 + ε) + y · (1 + ε) = x̃ + ỹ mit |ε| ≤ eps(F) und
x̃ = x · (1 + ε) und ỹ = y · (1 + ε).

Aus der Kombination von Rückwärtsanalyse und Kondition kann man manchmal auch
eine Abschätzung für den Fehler im Ergebnis erhalten. Zum Beispiel ist im Beispiel 5.1
1,734375 die korrekte Lösung für die Eingabe 1,7343752 = 3,008056640625√= 3(1 + ε)
 
mit ε ≤ 0,00269. Wegen der Kondition 21 folgt, dass der relative Fehler  3−1,734375
√ 
3
ungefähr durch 21 ·0,00269 beschränkt werden kann. Dies stimmt jedoch nicht immer genau,
da die Kondition nur den Grenzwert für Fehler nahe null widerspiegelt.
Eine andere Möglichkeit der Fehleranalyse bietet die Intervallarithmetik. Hier rechnet
man statt mit Zahlen x, die man nicht genau kennt, immer mit Intervallen [a, b], in denen x
liegt. Schon bei den Eingabedaten kann man x durch das Intervall {y ∈ R | rd(y) = rd(x)}
ersetzen. Bei der Addition zweier Intervalle ergibt sich dann [a1 , b1 ] ⊕ [a2 , b2 ] = [rd− (a1 +
a2 ), rd+ (b1 + b2 )], wobei rd− und rd+ Ab- bzw. Aufrundung zur nächsten Maschinenzahl
bezeichnen. So hat man zu jedem Zeitpunkt Fehlerschranken; allerdings ist das natürlich
mit höherem Rechenaufwand verbunden.
Für Intervallarithmetik in C++ sollte man eine Klasse definieren, mit der solche Intervalle
gespeichert werden können und die die benötigten Rechenoperationen zur Verfügung stellt.
Statt rd− und rd+ kann man schnellere, eventuell stärker ab- oder aufrundende Funktionen
verwenden (wodurch die Fehlerschranken natürlich ungenauer werden), etwa Division durch
bzw. Multiplikation mit 1 + b−m in F(b, m, E min , E max ).
70 5 Rechnen mit Fehlern

5.5 Newton-Verfahren

Wir geben ein weiteres allgemeines Näherungsverfahren an, das der binären Suche oft über-
legen ist. Allerdings ist dieses Verfahren nur für differenzierbare Funktionen durchführbar,
deren Ableitung in einer Umgebung des gesuchten Wertes berechenbar und ungleich null
ist, und es konvergiert auch nur unter bestimmten Voraussetzungen.
Gesucht sei eine Nullstelle einer differenzierbaren Funktion f : R → R. Beginnend mit
einem „geratenen“ Startwert x0 setzen wir

f (xn )
xn+1 ← xn − für n = 0, 1, . . .
f  (xn )

Natürlich muss man hier f  (xn )  = 0 voraussetzen. Dieses Verfahren geht auf Isaac Newton
zurück und heißt deshalb Newton-Verfahren. Unter bestimmten Voraussetzungen konver-
giert es sehr schnell.
Wir geben ein Beispiel: Um die (positive) Quadratwurzel aus einer Zahl a ≥ 1 zu
berechnen, können wir die positive Nullstelle der Funktion f : R → R mit f (x) = x 2 − a
berechnen. Das Newton-Verfahren für diesen Spezialfall ist dann durch

xn2 − a 1 a
xn+1 ← xn − = xn + für n = 0, 1, . . .
2xn 2 xn

gegeben, wobei man zum Beispiel mit x0 = 1 starten kann. Dieses Verfahren zum Wurzel-
ziehen wurde von Heron von Alexandria vor fast 2000 Jahren beschrieben, war aber schon
weitere rund 2000 Jahre früher in Mesopotamien bekannt und heißt deshalb auch babyloni-
sches Wurzelziehen.

Beispiel 5.11 Für a = 3 mit dem Startwert x0 = 1 erhalten wir:

x1 = 2 =2
x2 = 7
4 = 1,75
x3 = 97
56 = 1,732142857. . .
x4 = 18817
10864 = 1,732050810. . .


Die Folge scheint viel schneller gegen 3 = 1,732050807568877. . . zu konvergieren als
bei der binären Suche (Beispiel 5.1), und das ist tatsächlich auch der Fall. Dies wollen wir
nun präzisieren.

Definition 5.12 Sei (xn )n∈N eine konvergente Folge reeller Zahlen und x ∗ := lim xn .
n→∞
Existiert eine Konstante c < 1 und ein n 0 ∈ N, so dass

|xn+1 − x ∗ | ≤ c · |xn − x ∗ |
5.5 Newton-Verfahren 71

für alle n ≥ n 0 gilt, so hat die Folge (mindestens) Konvergenzordnung 1.


Sei p > 1. Existiert eine Konstante c ∈ R, so dass

|xn+1 − x ∗ | ≤ c · |xn − x ∗ | p

für alle n ∈ N gilt, so hat die Folge (mindestens) Konvergenzordnung p.

Wir sagen, eine Folge (und ein Algorithmus, der eine solche Folge berechnet) konvergiert
linear bzw. quadratisch, wenn die Konvergenzordnung 1 bzw. 2 ist.

Beispiel 5.13 Die Folge der Intervalllängen bei der binären Suche (siehe Beispiel 5.1)
konvergiert linear (hier kann c = 21 gewählt werden).

Wir zeigen nun, dass das babylonische Wurzelziehen quadratisch konvergiert: der Fehler
quadriert sich in jedem Schritt.

Satz 5.14 Es sei a ≥ 1 und x0 ≥ 1. Für die durch xn+1 = 1


2 xn + a
xn f ür n = 0, 1, . . .
definierte Folge (xn )n∈N gilt:

(a) x1 ≥ x2 ≥ . . . ≥ a

(b) Die Folge konvergiert quadratisch gegen lim xn = a; genauer gilt für alle n ≥ 0:
n→∞

√ 1 √
xn+1 − a ≤ (xn − a)2 (5.1)
2
(c) Für alle n ≥ 1 gilt:
√ a √
xn − a ≤ xn − ≤ 2(xn − a).
xn

Beweis

(a) Mit dem Satz vom arithmetischen und geometrischen Mittel ( x y ≤ x+y
2 folgt direkt
√ √ √ 2
aus x + y − 2 x y = ( x − y) ≥ 0 für alle x, y ≥ 0) erhalten wir für alle n ≥ 0:

√ a 1 a
a= xn · ≤ xn + = xn+1 . (5.2)
xn 2 xn

Außerdem ist für n ≥ 1:


1 a 1   (5.2)
xn+1 − xn = · xn + − xn = a − xn2 ≤ 0.
2 xn 2xn

(b) Für alle n ≥ 0 ist:


72 5 Rechnen mit Fehlern

√ 1 a √ 1  2 √  1  √ 2
xn+1 − a= xn + − a= xn + a − 2 axn = xn − a .
2 xn 2xn 2xn

1  √  √  √ 
Wegen xn − a ≤ 1 folgt xn+1 − a ≤ 1
2 xn − a für n ≥ 1, was lim xn =
√ x n n→∞
a beweist. Aus xn ≥ 1 folgt (5.1).
(c) Es gilt für n ∈ N:
√ (a) a (a) √
xn − a ≤ xn − = 2xn − 2xn+1 ≤ 2(xn − a). 
xn

Satz 5.14(c) legt ein Abbruchkriterium nahe: für eine gewünschte absolute Fehlerschranke

ε > 0 bricht man ab, wenn xn − xan ≤ ε. Dies impliziert xn − a ≤ ε. Solange die Abbruch-

bedingung nicht gilt, ist xn − a > 2ε , d. h. das Abbruchkriterium ist schlimmstenfalls um
einen Faktor 2 zu stark.

Man beachte, dass gemäß (5.1) die Konvergenz besonders schnell ist, sobald |xn − a| ≤
1 ist. Dies √
kann man leicht direkt erreichen,
√ indem man √ b := a · 2−2 log4 a setzt, mit dem

Verfahren b berechnet, und a = b · 2 log 4 a sowie b ∈ [1,2) ausnutzt.
Das Newton-Verfahren eignet sich übrigens auch für die schnelle Division großer ganzer
Zahlen. Um ab zu berechnen, kann man die Nullstelle von f : x  → x1 − b mit dem Newton-
Verfahren hinreichend genau berechnen und das Ergebnis dann mit a multiplizieren. Eine
Iteration ist durch
x −b
1
xn+1 ← xn − n 1 = xn + xn (1 − bxn )
− x2
n

gegeben; man beachte, dass man hier keine Division braucht. Da das Newton-Verfahren
auch hier quadratisch konvergiert, genügen O(log log n) Iterationen zur Berechnung von
n signifikanten Stellen. Für die Multiplikationen benutzt man ein schnelles Multiplika-
tionsverfahren; siehe Abschn. 3.2.

5.6 Übungsaufgaben

1. Betrachten Sie folgendes Problem: Es sei eine Funktion f : [0, 1] → R gegeben, so


dass für alle x, y, α ∈ [0, 1] gilt: f (αx + (1 − α)y) ≤ α f (x) + (1 − α) f (y). Die
Funktion sei über ein Orakel gegeben, das zu einem beliebigen Wert x ∈ [0, 1] den
Wert f (x) ausgibt. Außerdem sei ein  > 0 gegeben. Gesucht ist ein x ∗ ∈ [0, 1], für
das es ein x̃ mit |x ∗ − x̃| <  gibt, so dass für alle x ∈ [0, 1] gilt: f (x̃) ≤ f (x). Zeigen
 
Sie, dass O(log 1 + 1 ) Abfragen von Funktionswerten reichen, um ein solches x ∗
zu berechnen.
Hinweis: Modifizieren Sie die binäre Suche geeignet.
5.6 Übungsaufgaben 73

2. Eine Lösung der quadratischen Gleichung ax 2 + bx + c = 0 mit a, b, c ∈ R und a  = 0


ist √
−b + b2 − 4ac
x1 = .
2a
Ist b > 0 und b2 deutlich größer als 4ac, so kommt es zur Auslöschung bei der Be-
rechnung von x1 . Geben Sie eine numerisch stabilere Formel zur Berechnung von x1
an.
3. Schreiben Sie die folgenden Ausdrücke so um, dass für die angegebenen Argumente
Auslöschung vermieden wird:
(a) x1 − x+1
1
für x  1 (d. h. x ist wesentlich größer als 1).

(b) 3 1 + x − 1 für x ≈ 0.
(c) 1−cos x
sin x für x ≈ 0.
(d) √1 für x  1.
x− x 2 −1
4. Berechnen Sie die Kondition der folgenden Funktionen und geben Sie an, für welche
x  = 0 die Funktionsauswertung qualitativ gut bzw. schlecht konditioniert ist.
(a) f (x) = arcsin(x), x ∈ [−1, 1]

(b) f (x) = 3 x, x ≥ 0
(c) f (x) = y x = e x ln y für ein festes y > 0
(d) f (x) = 1
x
(e) f (x) = ln(1 + |x − 1|)
5. Für einen Vektor x ∈ Rn von Stichproben bezeichne

1
n
x̄ = xi
n
i=1

den Mittelwert der Stichprobe. Zur Berechnung der Varianz der Stichprobe stehen die
Formeln
 n 
1   1  2
n n
1
s1 =
2
(xi − x̄) , s2 =
2 2
xi − n x̄
2 2
und s32 = (xi − x̄ 2 )
n−1 n−1 n−1
i=1 i=1 i=1

zur Verfügung.
(a) Zeigen Sie, dass s12 = s22 = s32 gilt.
(b) Welche der drei Formeln ist im Hinblick auf die numerische Stabilität die günstigste?

6. Zeigen Sie, dass das babylonische Wurzelziehen mit jedem Startwert x0 ∈ R \ {0}
konvergiert. Was ist der Grenzwert?
74 5 Rechnen mit Fehlern

7. Man kann die Quadratwurzel einer Zahl a ≥ 0 auch mit dem Newtonverfahren an-
gewandt auf f (x) = 1 − xa2 berechnen. Dabei können wir uns auf Eingaben a und
Startwerte x0 mit 1 ≤ a < 4 und 1 ≤ x0 ≤ 2 beschränken.
(a) Wie sieht eine Iteration dieses Verfahrens aus? Berechnen Sie x1 , x2 , x3 für a = 3
und x0 = 1.
(b) Beweisen Sie, dass auch diese Variante quadratisch konvergiert.
(c) Ist das Verfahren besser als das babylonische Wurzelziehen?
8. Beweisen Sie die Ungleichung vom arithmetischen und geometrischen Mittel:
1 n n 1
i=1 x i ≥ ( i=1 x i ) für alle n ∈ N und x 1 , . . . , x n ≥ 0.
n
n
Hinweis: Beweisen Sie die Ungleichung zunächst für Zweierpotenzen n. Führen Sie
dann eine umgekehrte Induktion von n auf n − 1, um sie für die restlichen Zahlen zu
beweisen.
9. Zeigen Sie: Falls eine Folge Konvergenzordnung p ≥ 2 hat, so hat sie auch Konver-
genzordnung p − 1.
10. Die Folge (xi )i∈N rationaler Zahlen, die durch x1 := 1 und xi+1 := 1 + x1i (i ∈ N)

gegeben ist, konvergiert (vgl. Aufgabe 2.12) und es gilt x1 < x3 < . . . < 1+2 5 <
. . . < x4 < x2 (dies dürfen Sie ohne Beweis verwenden). Welche Zahlen treten als
Nenner der Folgenglieder xi auf? Schließen Sie daraus, dass die Folge nicht quadratisch
konvergiert.
11. Für welche Startwerte x0 konvergiert das in Abschn. 5.5 beschriebene Newton-Verfahren
für die Division?
Graphen
6

Sehr viele diskrete Strukturen können durch Graphen beschrieben werden. Auch in unzäh-
ligen Anwendungen tauchen Graphen auf natürliche Weise auf. Daher bilden Graphen die
wohl wichtigste Struktur der Diskreten Mathematik.

6.1 Grundlegende Definitionen

Definition 6.1 Ein ungerichteter Graph ist ein Tripel (V, E, ψ), wobei V und E endliche
Mengen sind, V  = ∅, und ψ : E → {X ⊆ V | |X | = 2}. Ein gerichteter Graph (oder
auch Digraph) ist ein Tripel (V, E, ψ), wobei V und E endliche Mengen sind, V  = ∅, und
ψ : E → {(v, w) ∈ V × V | v  = w}. Ein Graph ist ein gerichteter oder ungerichteter
Graph. Die Elemente aus V heißen Knoten, die Elemente aus E Kanten.

Beispiel 6.2 Durch ({1, 2, 3, 4, 5}, {a, b, c, d, e}, {a  → (2,5), b  → (4,5), c  → (1,2),
d  → (2,1), e  → (1,3)}) ist ein gerichteter Graph mit fünf Knoten und fünf Kanten gegeben.
Er wird in Abb. 6.1a durch eine Zeichnung im R2 veranschaulicht. Knoten entsprechen
Punkten, Kanten entsprechen Strecken (oder Kurven) zwischen Knoten. Im gerichteten Fall
gibt ein Pfeil auf einer Kante die Richtung vom ersten zum zweiten Knoten an. Abb. 6.1b
zeigt einen ungerichteten Graphen mit sieben Knoten und sechs Kanten.

Zwei Kanten e  = e mit ψ(e) = ψ(e ) heißen parallel. Ein Graph ohne parallele Kanten
wird einfacher Graph genannt. Für einfache Graphen identifiziert man e mit ψ(e), das
heißt, man schreibt G = (V (G), E(G)) mit E(G) ⊆ {{v, w} | v, w ∈ V (G), v  = w}
beziehungsweise E(G) ⊆ {(v, w) | v, w ∈ V (G), v  = w}. Man benutzt diese verein-
fachte Notation vielfach auch für nicht einfache Graphen; E(G) ist dann eine Multimenge.

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 75


S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
76 6 Graphen

a b
b f
4 5 6 4
5
e
a 7 a 3
d
3 c b
e 1 2 1 2
d c

Abb. 6.1 Ein gerichteter und ein ungerichteter Graph

Beispielsweise ist der Ausdruck „e = (x, y) ∈ E(G)“ eine Abkürzung für „e ∈ E(G)
mit ψ(e) = (x, y)“. Abb. 6.1 zeigt einen einfachen Digraphen und einen nicht einfachen
ungerichteten Graphen.
Für eine Kante e = {x, y} beziehungsweise e = (x, y) sagt man, dass e die beiden
Knoten x und y verbindet. Die Knoten x und y heißen in diesem Fall benachbart (oder
adjazent). Die Knoten x und y werden die Endknoten von e genannt; x ist Nachbar von y,
und y ist Nachbar von x. Die Knoten x und y sind mit der Kante e inzident. Falls e = (x, y),
so sagen wir: e beginnt in x und endet in y, oder: e geht von x nach y.
Folgende Notation ist sehr praktisch:

Definition 6.3 Sei G ein ungerichteter Graph und X ⊆ V (G). Dann definiere

δ(X ) := {{x, y} ∈ E(G) | x ∈ X, y ∈ V (G) \ X }.

Die Nachbarschaft von X ist die Menge N (X ) := {v ∈ V (G) \ X | δ(X ) ∩ δ({v})  = ∅}.
Sei G ein Digraph und X ⊆ V (G). Dann definiere

δ + (X ) := {(x, y) ∈ E(G) | x ∈ X, y ∈ V (G) \ X },

δ − (X ) := δ + (V (G) \ X ) und δ(X ) := δ + (X ) ∪ δ − (X ).


Für einen Graphen G und x ∈ V (G) setze δ(x) := δ({x}), N (x) := N ({x}), δ + (x) :=
δ ({x}) und δ − (x) := δ − ({x}). Der Grad eines Knotens x ist |δ(x)|, das heißt: die Anzahl
+

der mit x inzidenten Kanten. Im gerichteten Fall bezeichnen |δ − (x)| beziehungsweise |δ + (x)|
den Eingangs- beziehungsweise Ausgangsgrad von x und es gilt |δ(x)| = |δ + (x)|+|δ − (x)|.

Falls mehrere Graphen auf der gleichen Knotenmenge betrachtet werden, ist es häufig er-
forderlich anzugeben, auf welchen Graphen man sich bezieht; dies macht man dann durch
ein Subskript; zum Beispiel δG (x).


Satz 6.4 Für jeden Graphen G gilt: x∈V (G) |δ(x)| = 2|E(G)|.
6.2 Wege und Kreise 77

Beweis Jede Kante ist mit genau zwei Knoten inzident. Auf der linken Seite der Gleichung
zählt man daher jede Kante genau zweimal. 

 + (x)|
 − (x)|.
Satz 6.5 Für jeden Digraphen G gilt: x∈V (G) |δ = x∈V (G) |δ

Beweis Auf beiden Seiten wird jede Kante genau einmal gezählt. 

Aus Satz 6.4 folgt sofort:

Korollar 6.6 In jedem Graphen ist die Anzahl der Knoten mit ungeradem Grad gerade. 

Definition 6.7 Ein Graph H ist Teilgraph (oder Subgraph oder Untergraph) des Gra-
phen G, falls V (H ) ⊆ V (G) und E(H ) ⊆ E(G). (Damit ist natürlich gemeint, dass auch
ψ(e) in G und H identisch ist für alle e ∈ E(H )). Man sagt auch, dass G den Graphen H
enthält. Falls V (G) = V (H ), so heißt H ein aufspannender Teilgraph.
H heißt induzierter Teilgraph, falls E(H ) = {{x, y} ∈ E(G) | x, y ∈ V (H )} bezie-
hungsweise E(H ) = {(x, y) ∈ E(G) | x, y ∈ V (H )}. Ein induzierter Teilgraph H von G
ist bereits vollständig durch seine Knotenmenge V (H ) spezifiziert. Man sagt daher auch,
dass H der von V(H) induzierte Teilgraph von G ist und notiert diesen als G[V (H )].

Zwei Arten, Subgraphen zu bilden, kommen oft vor: sei G ein Graph. Für v ∈ V (G)
definieren wir G − v := G[V (G) \ {v}]. Für e ∈ E(G) definieren wir G − e :=
(V (G), E(G) \ {e}).

6.2 Wege und Kreise

Definition 6.8 Ein Kantenzug (von x1 nach xk+1 ) in einem Graphen G ist eine Folge
x1 , e1 , x2 , e2 , . . . , xk , ek , xk+1 mit k ∈ N∪{0} und ei = (xi , xi+1 ) ∈ E(G) beziehungsweise
ei = {xi , xi+1 } ∈ E(G) für i = 1, . . . , k. Falls x1 = xk+1 , so handelt es sich um einen
geschlossenen Kantenzug.
Ein Weg ist ein Graph P = ({x1 , . . . , xk+1 }, {e1 , . . . , ek }) mit k ≥ 0 und der Eigenschaft,
dass x1 , e1 , x2 , e2 , . . . , xk , ek , xk+1 ein Kantenzug ist (mit k+1 verschiedenen Knoten). Man
sagt auch, dass P ein x1 -xk+1 -Weg ist, oder dass P x1 und xk+1 verbindet. Die Knoten x1
und xk+1 werden die Endknoten des Weges P genannt; die Knoten in V (P) \ {x1 , xk+1 }
sind die inneren Knoten von P.
Ein Kreis ist ein Graph C = ({x1 , . . . , xk }, {e1 , . . . , ek }) mit k ≥ 2, so dass die Folge
x1 , e1 , x2 , e2 , . . . , xk , ek , x1 ein geschlossener Kantenzug ist (mit k verschiedenen Knoten).
78 6 Graphen

Die Länge eines Weges oder Kreises ist die Anzahl seiner Kanten. Ist ein Weg oder Kreis
ein Teilgraph von G, so spricht man von einem Weg oder Kreis in G.

In einem Graphen G heißt ein Knoten y von einem Knoten x aus erreichbar, wenn es einen
x-y-Weg in G gibt. In ungerichteten Graphen definiert dies eine Äquivalenzrelation, denn
die Transitivität folgt aus:

Lemma 6.9 Sei G ein Graph und x, y ∈ V (G). Es gibt genau dann einen x-y-Weg in G,
wenn es einen Kantenzug von x nach y in G gibt.

Beweis „⇒“: Nach Definition entspricht ein x-y-Weg einem Kantenzug von x nach y.
„⇐“: Angenommen, in einem Kantenzug von x nach y kommt ein Knoten v mehrfach
vor. Dann eliminiere alle Knoten und Kanten nach dem ersten bis einschließlich zum letzten
Auftreten von v. Iteriert man dies, bleibt ein x-y-Weg übrig. 

Zwei Graphen heißen kanten- bzw. knotendisjunkt, wenn sie keine Kante bzw. keinen
Knoten gemeinsam haben.

Lemma 6.10 Sei G ein ungerichteter Graph mit |δ(v)| gerade für alle v ∈ V (G) oder ein
gerichteter Graph mit |δ − (v)| = |δ + (v)| für alle v ∈ V (G). Dann existiert ein k ≥ 0 und
paarweise kantendisjunkte Kreise C1 , . . . , Ck so dass E(G) = E(C1 ) ∪ · · · ∪ E(Ck ).

Beweis Wir führen eine Induktion über |E(G)|. Die Aussage ist trivial, wenn E(G) = ∅.
Sonst genügt es, einen Kreis zu finden, denn nach Löschen von dessen Kanten bleibt ein
Graph, der die Voraussetzungen des Lemmas erfüllt. Sei also e = {x, y} bzw. e = (x, y)
eine Kante. Wegen der Voraussetzung muss es eine andere Kante e geben, die mit y inzident
ist bzw. y verlässt. Wir machen dann mit dem anderen Endpunkt von e weiter. So können
wir einen Kantenzug konstruieren, in dem alle Kanten verschieden sind und werden nach
spätestens |V (G)| Schritten einen Knoten treffen, den wir bereits besucht hatten. Dies ergibt
dann einen Kreis. 

Für zwei Mengen A und


.
B ist deren symmetrische Differenz definiert als AB := (A \
B) ∪ (B \ A). Mit A ∪ B bezeichnen wir die disjunkte Vereinigung, in der Elemente aus
AB einfach und Elemente aus A ∩ B doppelt vorkommen.

Lemma 6.11 Sei G ein Graph, und seien P ein s-t-Weg in G und Q ein t-s-Weg in G, und
P  = Q. Dann enthält (V (P) ∪ V (Q), E(P) ∪ E(Q)) einen Kreis.

.
Beweis Betrachte C := E(P)E(Q) im ungerichteten Fall und C := E(P) ∪ E(Q) im
gerichteten Fall, und sei H := (V (G), C). Im ungerichteten Fall hat jeder Knoten in H
6.3 Zusammenhang und Bäume 79

geraden Grad. Im gerichteten Fall gilt für jeden Knoten v ∈ V (G), dass |δ − +
H (v)| = |δ H (v)|.
Nach Lemma 6.10 ist E(H ) = C somit in beiden Fällen die disjunkte Vereinigung von
Kantenmengen von Kreisen. In keinem .
Kreis kann eine Kante von G doppelt vorkommen.
Wegen P  = Q gilt ∅  = C ⊆ E(P) ∪ E(Q) und es folgt die Behauptung. 

6.3 Zusammenhang und Bäume

Wir betrachten nun zusammenhängende Graphen und insbesondere minimal zusammen-


hängende Teilgraphen.
Sei F eine Familie von Mengen bzw. Graphen. Dann ist F ∈ F inklusionsminimal bzw.
minimal, falls keine echte Teilmenge beziehungsweise kein echter Teilgraph von F in F ist.
Entsprechend ist F ∈ F inklusionsmaximal bzw. maximal, falls F keine echte Teilmenge
beziehungsweise kein echter Teilgraph eines Elements in F ist. Man beachte, dass inklu-
sionsminimale bzw. -maximale Mengen in F nicht notwendigerweise kardinalitätsminimal
beziehungsweise -maximal sind.

Definition 6.12 Sei G ein ungerichteter Graph. G heißt zusammenhängend, falls es für
je zwei Knoten x, y ∈ V (G) einen x-y-Weg in G gibt. Ansonsten heißt G unzusammen-
hängend. Die maximalen zusammenhängenden Teilgraphen von G sind die Zusammen-
hangskomponenten von G. Ein Knoten v heißt Artikulationsknoten, falls v nicht der einzige
Knoten ist und G − v mehr Zusammenhangskomponenten hat als G. Eine Kante e heißt
Brücke, falls G − e mehr Zusammenhangskomponenten hat als G.

Beispielsweise hat der ungerichtete Graph in Abb. 6.1b drei Zusammenhangskomponenten,


zwei Brücken (e und f ) und einen Artikulationsknoten (5). Die Zusammenhangskomponen-
ten sind die von den Äquivalenzklassen der Erreichbarkeitsrelation induzierten Teilgraphen.

Satz 6.13 Ein ungerichteter Graph G ist genau dann zusammenhängend, falls δ(X )  = ∅
für alle ∅ ⊂ X ⊂ V (G).

Beweis „⇒“: Sei ∅ ⊂ X ⊂ V (G) und x ∈ X , y ∈ V (G) \ X . Da G zusammenhängend


ist, gibt es einen x-y-Weg in G. Da x ∈ X aber y ∈ / X , muss der Weg eine Kante {a, b}
enthalten mit a ∈ X und b ∈/ X . Dann ist also δ(X )  = ∅.
„⇐“: Angenommen G ist unzusammenhängend. Seien a und b Knoten in V (G), die
nicht durch einen a-b-Weg verbunden sind. Sei X die Menge der von a aus erreichbaren
Knoten. Wegen a ∈ X und b ∈ / X gilt ∅ ⊂ X ⊂ V (G). Nach Definition von X gilt zudem
δ(X ) = ∅. Dies ist ein Widerspruch. 
80 6 Graphen

Bemerkung 6.14 Nach Satz 6.13 kann man prüfen, ob ein gegebener Graph G zusam-
menhängend ist, indem man für jede Menge X mit ∅ ⊂ X ⊂ V (G) testet, ob δ(X )  = ∅
ist. Da es 2|V (G)| − 2 solcher Mengen X gibt, erfordert dieser Ansatz aber exponentiellen
Rechenaufwand. Wir werden bald einen viel besseren Algorithmus kennen lernen.

Definition 6.15 Ein ungerichteter Graph heißt Wald, falls er keinen Kreis enthält. Ein
Baum ist ein zusammenhängender Wald. Ein Knoten vom Grad 1 in einem Baum heißt
Blatt.

Die Zusammenhangskomponenten eines Waldes sind also Bäume.

Satz 6.16 Jeder Baum mit mindestens zwei Knoten enthält mindestens zwei Blätter.

Beweis Betrachte einen maximalen Weg im Baum. Dieser hat eine Länge ≥ 1 und seine
Endknoten müssen wegen der Kreisfreiheit Blätter sein. 

Lemma 6.17 Sei G ein Wald mit n Knoten, m Kanten, und p Zusammenhangskomponenten.
Dann ist n = m + p.

Beweis Induktion über m. Trivial für m = 0. Ist m ≥ 1, so existiert eine Zusammenhangs-


komponente von G, die mindestens zwei Knoten enthält und somit nach Satz 6.16 auch ein
Blatt v. Das Entfernen der mit v inzidenten Kante erhöht die Zahl der Zusammenhangskom-
ponenten um eins. Nach Induktionsannahme gilt daher n = (m − 1) + ( p + 1) = m + p. 

Satz 6.18 Sei G ein ungerichteter Graph auf n Knoten. Dann sind folgende Aussagen
äquivalent:

(a) G ist ein Baum.


(b) G hat n − 1 Kanten und enthält keinen Kreis.
(c) G hat n − 1 Kanten und ist zusammenhängend.
(d) G ist ein minimaler zusammenhängender Graph mit Knotenmenge V (G) (das heißt: G
ist zusammenhängend und jede Kante von G ist eine Brücke).
(e) G ist minimaler Graph mit Knotenmenge V (G) und δ(X )  = ∅ für alle ∅ ⊂ X ⊂ V (G).
(f) G ist maximaler Wald mit Knotenmenge V (G) (das heißt: das Hinzufügen einer belie-
bigen Kante erzeugt einen Kreis).
(g) Je zwei Knoten in G sind durch genau einen Weg in G verbunden.
6.4 Starker Zusammenhang und Arboreszenzen 81

Beweis
(a)⇒(g) In einem Wald sind je zwei Knoten wegen Lemma 6.11 durch höchstens einen
Weg verbunden, und in einem zusammenhängenden Graphen durch mindestens
einen.
(g)⇒(d) Nach Definition ist G zusammenhängend. Angenommen, es gäbe eine Kante e, so
dass G − e zusammenhängend ist. Dann wären die Endknoten von e in G durch
zwei verschiedene Wege verbunden, im Widerspruch zu (g).
(e)⇔(d) folgt unmittelbar aus Satz 6.13.
(d)⇒(f) Wenn jede Kante eine Brücke ist, enthält G keine Kreise. Wenn G zusammenhän-
gend ist, erzeugt das Hinzufügen einer beliebigen Kante immer einen Kreis.
(f)⇒(b) Die Maximalität impliziert den Zusammenhang und somit gemäß Lemma 6.17
|E(G)| = |V (G)| − 1.
(b)⇒(c) folgt direkt aus Lemma 6.17.
(c)⇒(a) Enthält ein zusammenhängender Graph einen Kreis, so können wir eine Kante
entfernen, ohne den Zusammenhang zu zerstören. So entfernen wir k Kanten und
erhalten schließlich einen zusammenhängenden Wald mit n −1−k Kanten. Wegen
Lemma 6.17 muss dann aber k = 0 sein.


Korollar 6.19 Ein ungerichteter Graph ist genau dann zusammenhängend, wenn er einen
aufspannenden Baum enthält.

Beweis Dies gilt wegen (d)⇔(a) aus Satz 6.18. 

6.4 Starker Zusammenhang und Arboreszenzen

Definition 6.20 Für einen Digraphen G betrachten wir gelegentlich den zugrundeliegen-
den ungerichteten Graphen, d. h. einen ungerichteten Graphen G  mit V (G) = V (G  )
so dass es eine Bijektion φ : E(G) → E(G  ) gibt mit φ((v, w)) = {v, w} für alle
(v, w) ∈ E(G). Man nennt G auch eine Orientierung von G  .
Ein Digraph G heißt (schwach) zusammenhängend, falls der zugrundeliegende unge-
richtete Graph zusammenhängend ist. G heißt stark zusammenhängend, falls es für alle
s, t ∈ V (G) einen Weg von s nach t und einen Weg von t nach s gibt. Die starken Zusam-
menhangskomponenten eines Digraphen sind die maximalen stark zusammenhängenden
Teilgraphen.

Der Digraph in Abb. 6.1a ist (schwach) zusammenhängend und hat vier starke Zusam-
menhangskomponenten.
82 6 Graphen

Wir wollen zunächst einen Knoten r festhalten und prüfen, ob alle Knoten von r aus
erreichbar sind.

Satz 6.21 Sei G ein Digraph und r ∈ V (G). Genau dann gibt es für jedes v ∈ V (G) einen
r -v-Weg, wenn δ + (X )  = ∅ für alle X ⊂ V (G) mit r ∈ X .

Beweis Analog zum Beweis von Satz 6.13. 

Definition 6.22 Ein Digraph ist ein Branching, falls der zugrundeliegende ungerichtete
Graph ein Wald ist und jeder Knoten höchstens eine eingehende Kante hat. Ein zusammen-
hängendes Branching heißt Arboreszenz. Eine Arboreszenz mit n Knoten hat nach Satz 6.18
n − 1 Kanten, somit hat sie genau einen Knoten r mit δ − (r ) = ∅. Dieser Knoten heißt die
Wurzel der Arboreszenz. Für eine Kante (v, w) eines Branchings heißt w ein Kind von v,
und v der Vorgänger von w. Knoten ohne Kinder heißen Blätter.

Satz 6.23 Sei G ein Digraph mit n Knoten und r ∈ V (G). Dann sind folgende Aussagen
äquivalent:

(a) G ist eine Arboreszenz mit Wurzel r (d. h. ein zusammenhängendes Branching mit
δ − (r ) = ∅).
(b) G ist ein Branching mit n − 1 Kanten, und δ − (r ) = ∅.
(c) G hat n − 1 Kanten, und jeder Knoten ist von r aus erreichbar.
(d) Jeder Knoten ist von r aus erreichbar, aber das Entfernen einer beliebigen Kante
zerstört diese Eigenschaft.
(e) G erfüllt δ + (X )  = ∅ für alle X ⊂ V (G) mit r ∈ X , das Entfernen einer beliebigen
Kante von G zerstört jedoch diese Eigenschaft.
(f) δ − (r ) = ∅ und für jedes v ∈ V (G) gibt es einen eindeutig bestimmten Kantenzug von
r nach v.
(g) δ − (r ) = ∅ und |δ − (v)| = 1 für alle v ∈ V (G) \ {r }, und G enthält keinen Kreis.

Beweis
(a)⇔(b) und (c) ⇒(d) Diese Implikationen folgen mit Satz 6.18, wenn man den G zugrun-
deliegenden ungerichteten Graphen betrachtet.
(b)⇒(c) Wir haben |δ − (v)| = 1 für alle v ∈ V (G) \ {r }. Also haben wir für jedes v einen
r -v-Weg (beginne in v und folge immer der eingehenden Kante bis zu r ).
(d)⇔(e) Dies folgt mit Satz 6.21.
(d)⇒(f) Aus der Minimalität in (d) folgt δ − (r ) = ∅. Angenommen, es gäbe für irgendein
v zwei verschiedene Kantenzüge P und Q von r nach v. Dann muss v  = r sein.
Wir wählen v, P und Q so, dass die Summe der Längen der beiden Kantenzüge
minimal ist. Dann müssen sich P und Q schon in der letzten (in v endenden) Kante
6.5 Exkurs: Elementare Datenstrukturen 83

unterscheiden. Das aber bedeutet, dass wir eine dieser beiden Kanten entfernen
können, ohne die Erreichbarkeit aller Knoten von r aus zu zerstören.
(f)⇒(g) Wenn jeder Knoten von r aus erreichbar ist und |δ − (v)| > 1 für ein v ∈ V (G)\{r },
dann gibt es zwei Kantenzüge von r nach v. Wenn G einen Kreis C enthält, dann
sei v ∈ V (C), und betrachte den r -v-Weg P. Dann ergibt P gefolgt von C einen
zweiten Kantenzug von r nach v.
(g)⇒(b) Wenn |δ − (v)| ≤ 1 für alle v ∈ V (G) gilt, dann ist jeder Teilgraph, dessen zugrun-
deliegender ungerichteter Graph ein Kreis ist, sogar selbst ein (gerichteter) Kreis.
Somit folgt mit (g), dass G Branching ist.

Korollar 6.24 Ein Digraph G ist genau dann stark zusammenhängend, wenn er für jedes
r ∈ V (G) eine aufspannende Arboreszenz mit Wurzel r enthält.

Beweis Dies folgt direkt aus Satz 6.23 (d)⇔(a). 

6.5 Exkurs: Elementare Datenstrukturen

Eine Datenstruktur ist ein Objekt (in C++: eine Klasse), in dem eine Menge von anderen Ob-
jekten („Elementen“) des jeweils gleichen Typs gespeichert werden kann, und die bestimmte
Operationen zur Verfügung stellt. Typische Operationen sind zum Beispiel:

• Einrichten einer leeren Datenstruktur (Konstruktor)


• Einfügen eines Elementes
• Auffinden eines Elementes
• Entfernen eines Elementes
• Durchlaufen aller Elemente
• Löschen einer Datenstruktur (Destruktor)

Die vielleicht einfachste Datenstruktur ist ein Array (Feld), das eine feste Anzahl von auf-
einanderfolgenden gleichartigen Speicherplätzen bereitstellt. Jeder dieser Plätze hat einen
Index, mit dem auf die Plätze lesend und schreibend zugegriffen werden kann (das nennt
man random access). Arrays können auch mehrdimensional sein, z. B. um Matrizen zu spei-
chern. In C++ kann ein solches Array nach Einbinden von #include <array> durch
std::array<Typ, Anzahl>; definiert werden. Dabei gibt Typ den Typ der Elemente
an, die in dem Array gespeichert werden sollen und Anzahl deren Anzahl.
84 6 Graphen

Hinter vector in C++ verbirgt sich auch ein solches Array. Allerdings ist die Länge eines
mit vector implementierten Arrays variabel, weshalb eine Operation wie push_back
zur Verfügung gestellt werden kann. Falls nötig, wird dabei intern das Array in ein größeres
Array umkopiert.
In einem Array (und auch in vector) ist zum Auffinden eines Objektes (und damit
auch zum Löschen) normalerweise das Durchlaufen aller Einträge erforderlich; es sei denn,
der Index der Speicherstelle ist bekannt. Unterliegen die gespeicherten Elemente einer be-
stimmten Ordnung, und sind die Einträge des Arrays dementsprechend sortiert, so kann
man ein Objekt mit binärer Suche (Algorithmus 5.2) viel schneller, nämlich in O(log n)
Zeit auffinden, wobei n die Länge des Arrays ist. Allerdings ist dann das Einfügen aufwän-
dig, wenn die Reihenfolge erhalten werden soll: hierbei muss man mit O(n) Zeitaufwand
rechnen. Auch beim Löschen eines Elementes (selbst wenn dessen Index bekannt ist) muss
man mit O(n) Zeitaufwand rechnen, sofern man keine Lücken im Array dulden möchte und
die Reihenfolge der übrigen Elemente erhalten möchte.
Letzteres kann man sehr effizient mit Listen bewerkstelligen. Auch die Elemente einer
Liste sind immer in einer bestimmten Reihenfolge gegeben. Bei einer Liste können die
einzelnen Elemente aber an beliebigen voneinander unabhängigen Stellen im Speicher ste-
hen. Zu jedem Element merkt man sich einen Pointer auf die Speicherstelle des nächsten
Elementes (des Nachfolgers). Beim letzten Element einer Liste ist dies der Null-Pointer (in
C++ heißt er nullptr), der das Ende einer Liste markiert. Zusätzlich braucht man noch
einen Pointer auf das erste Element einer Liste, damit man die Liste auch durchlaufen kann.
Listen können einfach verkettet oder doppelt verkettet sein. Bei doppelt verketteten Listen
(vgl. Abb. 6.2) merkt man sich noch zusätzlich zu jedem Element einen Pointer auf den
Vorgänger. Dies erlaubt das schnelle Löschen eines Elementes, dessen Speicherplatz man

first last

item 1 item 2 item 3


data prev next data prev next data prev next

Abb. 6.2 Eine doppelt verkettete Liste mit drei Elementen. Jedes Element besteht aus einem Da-
teneintrag, einem Pointer auf das vorangehende Element und einem Pointer auf das nachfolgende
Element. Punkte, von denen kein Pfeil ausgeht, symbolisieren den nullptr. Die Elemente werden
im Heap gespeichert. Die Variablen first und last sind Pointer, die im Stack liegen. Lässt man alles,
was grün ist, weg, so erhält man eine einfach verkettete Liste
6.5 Exkurs: Elementare Datenstrukturen 85

kennt: die Anzahl der Rechenschritte ist durch eine Konstante beschränkt, die unabhängig
von der Größe der Liste ist. Man sagt auch: die Laufzeit ist O(1).
Listen haben den Nachteil, dass man auf ihnen keine binäre Suche durchführen kann.
Ferner ist das Durchlaufen einer Liste (wenn auch nur um einen konstanten Faktor) lang-
samer als bei einem Array, weil der Zugriff auf aufeinanderfolgende Speicherbereiche bei
heutigen Computern wesentlich schneller geht als bei weit auseinander liegenden.
Eine weitere elementare Datenstruktur wird Stack (Stapelspeicher) genannt. Darin wer-
den Elemente immer „obenauf“ eingefügt, und es kann auch nur immer das jeweils „obers-
te“ (also zuletzt eingefügte) Element entfernt werden. Man spricht auch von einem LIFO-
Speicher (last in, first out). Dies leisten z. B. die Operationen push_back und pop_back
von vector. Ein Teil des Hauptspeichers ist als Stack organisiert; siehe Abschn. 2.2.
Umgekehrt zu einem Stack verhält sich eine Queue (Warteschlange). Hier wird auch
immer an einem Ende eingefügt, die Elemente werden aber vom anderen Ende entfernt,
d. h. es handelt sich um einen FIFO-Speicher (first in, first out).
Eine Queue kann sehr gut als (einfach verkettete) Liste implementiert werden, wie Pro-
gramm 6.25 zeigt. Da der Typ der Objekte, die wir in der Queue speichern wollen, von der
Anwendung abhängt, legen wir ihn nicht fest, sondern nutzen das template-Konstrukt.
Dieses wird in der Box C++ im Detail (6.1) genauer erklärt. Die Befehle new und delete
fordern Speicher der passenden Größe (auf dem Heap; vgl. Abschn. 2.2) an bzw. geben ihn
wieder frei. Die Klasse Queue enthält eine Unterklasse Item, wobei sich ein Item aus
einem Element vom Typ T (dem Template-Parameter) und einem Pointer auf das folgende
Item zusammensetzt.

Programm 6.25 (Queue)


1 // queue . h ( Queue )
2
3 t e m p l a t e < t y p e n a m e T > class Queue { // T is a type to be s p e c i f i e d by user
4 public :
5 ~ Queue () // d e s t r u c t o r
6 {
7 clear ();
8 }
9
10 bool i s _ e m p t y () const
11 {
12 r e t u r n _ f r o n t == n u l l p t r ;
13 }
14
15 void clear ()
16 {
17 while ( not i s _ e m p t y ()) {
18 p o p _ f r o n t ();
19 }
20 }
21
22 void p u s h _ b a c k ( const T & o b j e c t ) // i n s e r t o b j e c t at end of q u e u e
23 {
86 6 Graphen

24 Item * cur = new I t e m ( o b j e c t ); // get new m e m o r y for I t e m at a d d r e s s cur ,


25 // i n i t i a l i z e with object and n u l l p t r
26 if ( i s _ e m p t y ()) {
27 _ f r o n t = cur ;
28 }
29 else {
30 _back - > _next = cur ; // p - > n is a b b r e v i a t i o n for (* p ). n
31 }
32 _back = cur ;
33 }
34
35 T p o p _ f r o n t () // d e l e t e and r e t u r n f i r s t o b j e c t of q u e u e
36 { // A T T E N T I O N : queue must not be empty !
37 I t e m * cur = _ f r o n t ;
38 if ( _ b a c k == _ f r o n t ) {
39 _front = nullptr ;
40 _back = nullptr ;
41 }
42 else {
43 _ f r o n t = _front - > _next ;
44 }
45 T o b j e c t = cur - > _ o b j e c t ;
46 d e l e t e cur ; // f r e e memory for 1 I t e m at a d d r e s s cur
47 return object ;
48 }
49
50 private :
51 s t r u c t Item { // s t r u c t is a c l a s s w h e r e by
52 Item ( const T & o b j e c t ) : _ o b j e c t ( o b j e c t ) {} // d e f a u l t e v e r y t h i n g is p u b l i c
53
54 T _object ;
55 Item * _next = n u l l p t r ; // p o i n t e r to the n e x t I t e m ( or n u l l p t r )
56 };
57
58 I t e m * _ f ro n t = n u l l p t r ; // _ f r o n t and _ b a c k are p o i n t e r s to
59 Item * _back = n u l l p t r ; // v a r i a b l e s of type Item , or the
60 }; // n u l l p t r if q u e u e is e m p t y

Die in Programm 6.25 gezeigte Implementierung einer Queue dient lediglich als einfa-
ches Beispiel dafür, wie man einen abstrakten Datentyp realisieren kann. Unsere Imple-
mentierung ist allerdings nicht sehr effizient, da bei jedem Hinzufügen und Entfernen ei-
nes Elementes neuer Speicher auf dem Heap angefordert bzw. wieder freigegeben wird.
Eine wesentlich effizientere Implementierung einer Queue ist in der Standardbibliothek
enthalten. Nach Einbinden von #include <queue> kann man eine Queue mittels
std::queue<Datentyp> definieren. Eine solche unterstützt noch weit mehr Opera-
tionen als die von uns in Programm 6.25 implementierten. Im nächsten Kapitel sehen wir
eine Anwendung des abstrakten Datentyps Queue.
6.6 Darstellungen von Graphen 87

C++ im Detail (6.1): Klassen-Templates


Mit Hilfe sogenannter Template-Parameter kann man die Verwendbarkeit einer
Klasse deutlich steigern und dadurch mehrfach vorkommenden, sehr ähnlichen Code
vermeiden. Die Benutzung von Template-Parametern haben wir schon bei dem ab-
strakten Datentyp vector gesehen: durch vector<Typname> kann man einen
vector definieren, dessen Elemente vom Typ Typname sind. Um eine Klasse mit
einem Template-Parameter zu versehen, muss man wie in Zeile 3 von Programm 6.25
lediglich template<typename T> vor das Schlüsselwort class schreiben. Da-
bei darf anstelle von T ein beliebiger Bezeichner stehen. Innerhalb der Klassendefini-
tion von Queue können nun Objekte vom Typ T definiert und benutzt werden. Durch
Queue<Typname> q; wird eine Queue definiert, deren Elemente den angegebe-
nen Typ haben. Dabei darf Typname nicht nur ein Standarddatentyp von C++ sein,
sondern ein beliebiger Typ bzw. Klasse. Man kann auch mehr als einen Template-
Parameter bei einer Klassendefinition angeben. Diese müssen dann durch Komma
voneinander getrennt werden.

6.6 Darstellungen von Graphen

Man kann Graphen einfach speichern, indem man die Anzahl n der Knoten und die Anzahl m
der Kanten sowie für jedes i ∈ {1, . . . , m} die Nummern der Endknoten der i-ten Kante
speichert (in Bezug auf eine feste Nummerierung der Knoten und Kanten). Dies ist jedoch
nicht sehr komfortabel. Mit anderen Datenstrukturen kann man schneller entscheiden, wel-
che Kanten mit einem bestimmten Knoten inzident sind; dies wird in fast allen Algorithmen
auf Graphen benötigt.

Definition 6.26 Sei G = (V, E, ψ) ein Graph mit n Knoten und m Kanten. Die Matrix
A = (ax,y )x,y∈V ∈ Zn×n heißt Adjazenzmatrix von G, wobei

ax,y = |{e ∈ E | ψ(e) = {x, y} bzw. ψ(e) = (x, y)}|.

Die Matrix A = (ax,e )x∈V,e∈E ∈ Zn×m heißt Inzidenzmatrix von G, wobei



1, falls x ein Endknoten von e ist
ax,e =
0, sonst,
88 6 Graphen

falls G ungerichtet, und ⎧



⎪−1, falls e in x beginnt

ax,e = 1, falls e in x endet


⎩0, sonst,
falls G gerichtet

Beispiel 6.27

4
Adjazenzmatrix Inzidenzmatrix
d
1 2 3 4 a b c d e
1 0 0 1 0 3 1 1 0 −1 0 0
2 1 0 0 0 2 −1 1 0 0 1
c b
3 0 2 0 1 e 3 0 −1 1 −1 −1
4 0 0 0 0 4 0 0 0 1 0
1 2
a

Ein wesentlicher Nachteil dieser Matrizen ist deren hoher Speicherbedarf. Um dies zu
präzisieren, erweitern wir die Landau-Symbole (Definition 1.13) auf Graphen.
Sei G die Menge aller Graphen, f : G → R≥0 und g : G → R≥0 . Wir sagen, f = O(g),
wenn α > 0 und n 0 ∈ N existieren, so dass für alle G ∈ G mit |V (G)| + |E(G)| ≥ n 0
gilt, dass f (G) ≤ α · g(G). Mit anderen Worten: f ist höchstens um einen konstanten
Faktor größer als g, eventuell bis auf endlich viele Ausnahmen (statt G kann man dann
diese Notation auch für beliebige abzählbare Mengen benutzen). Analog erweitert man
die - und -Notation. Die Funktion f wird meist die Laufzeit eines Algorithmus oder
einen Speicherbedarf beschreiben; g wird oft nur von der Anzahl der Knoten und Kanten
abhängen.
Damit kann man sagen: Der Speicherbedarf von Adjazenzmatrix bzw. Inzidenzmatrix
beträgt (n 2 ) bzw. (nm), wobei n = |V (G)| und m = |E(G)| ist. Dies ist z. B. für
Graphen mit (n) Kanten (wie sie oft auftreten) viel mehr als nötig.
Bei der Darstellung mit Adjazenzlisten ist der Speicherbedarf oft geringer. Hier merkt
man sich für jeden Knoten eine Liste aller mit ihm inzidenten Kanten (oder in einfachen
Graphen manchmal auch nur eine Liste der adjazenten Knoten). Bei gerichteten Graphen
hat man natürlich zwei Listen, getrennt nach eingehenden und ausgehenden Kanten.
6.6 Darstellungen von Graphen 89

Beispiel 6.28 Eine Adjazenzlistendarstellung des Digraphen aus Beispiel 6.27 hat die fol-
gende Form. Das Symbol  markiert jeweils das Ende einer Liste.

δ + (1) → c →  δ − (1) → a →  a = (2,1)


b = (3,2)
δ + (2) → a →  δ − (2) → e → b → 
c = (1,3)
δ + (3) → b → d → e →  δ − (3) → c →  d = (3,4)
δ + (4) →  δ − (4) → d →  e = (3,2)

Die Adjazenzlisten können je nach Bedarf einfach verkettete oder doppelt verkettete Listen
oder auch Arrays sein. Im Allgemeinen muss man davon ausgehen, dass die Kanten in den
Adjazenzlisten in beliebiger Reihenfolge stehen.
Für die meisten Zwecke ist eine Adjazenzliste die geeignete Datenstruktur, vor allem
da sie das Durchlaufen von δ(v) bzw. δ + (v) und δ − (v) für jeden Knoten v in linearer Zeit
ermöglicht, und weil der Speicheraufwand proportional zur Anzahl der Knoten und Kanten
ist (wenn man wie üblich annimmt, dass ein Pointer bzw. Knoten- oder Kantenindex nur
konstant viel Speicherplatz benötigt). Daher werden wir diese Form bei allen folgenden
Algorithmen benutzen. Der Zugriff auf die nächste Kante in einer Kantenliste oder einen
Endpunkt einer Kante gelten daher als elementare Rechenoperationen.
Wir zeigen nun eine Implementierung in C++. Die Klasse Graph erlaubt das Speichern
von gerichteten und ungerichteten Graphen. In der Variablen dirtype wird festgehalten,
um welche Art von Graph es sich handelt. Es gibt zwei verschiedene Konstruktoren für die
Graphenklasse. Der eine Konstruktor hat als ersten Parameter die Anzahl der Knoten, der
andere Konstruktor hat einen Dateinamen als ersten Parameter. Beide Konstruktoren haben
einen zweiten Parameter, der angibt, ob der zu erzeugende Graph gerichtet oder ungerichtet
ist.
Die Klasse Graph enthält Unterklassen Node für Knoten und Neighbor für die Nach-
barn eines Knoten. Dabei speichern wir im Falle von Digraphen lediglich die über ausgehen-
de Kanten erreichbaren Knoten als Nachbarn. Dies erlaubt es, manche Graphenalgorithmen
so zu implementieren, dass sie sowohl auf Digraphen als auch auf ungerichteten Graphen
funktionieren, ohne dass diese beiden Fälle separat behandelt werden müssen.
90 6 Graphen

C++ im Detail (6.2): Kommandozeilenparameter


Die Funktion main kann zwei Parameter haben, die dann wie in dem Programm
[Link] aussehen sollten. Wird das Programm mit „programmname
par1 . . . parn“ aufgerufen, so ist argc = n+1, argv[i] = pari für i = 1, . . . , n,
und argv[0]= programmname. Der Typ char* wird für C-style strings benutzt
(die man eigentlich nur noch als Argument für die main-Funktion braucht): ein Zeiger
auf ein Zeichen, dem andere Zeichen folgen können; diese Zeichen gehören so lange
zum String, bis das Null-Zeichen (ASCII-Code 0) kommt (dieses gehört natürlich
nicht mehr zum String).
Ist T ein Typ und p ein Pointer vom Typ T*, so bezeichnet p[i] eine Variable vom
Typ T, die an der Speicheradresse p + i · sizeof(T) steht.

Das Programm [Link] liest einen Graphen aus einer Datei aus. Der Name der
Datei wird dabei als Kommandozeilenparameter dem Programm übergeben, siehe hierzu
die Box C++ im Detail (6.2). Bei der angegebenen Datei gehen wir davon aus, dass in der
ersten Zeile die Anzahl der Knoten des Graphen steht. Dabei wird angenommen, dass die
Knoten des Graphen konsekutiv mit 0 beginnend durchnummeriert sind. Für jede Kante des
Graphen enthält die Datei eine eigene Zeile, die die Nummern der beiden Endknoten und
optional als dritten Wert das Gewicht der Kante enthält.

Programm 6.29 (Graphen)


1 // graph . h ( D e c l a r a t i o n of Class Graph )
2 # ifndef GRAPH_H
3 # define GRAPH_H
4
5 # i n c l u d e < iostream >
6 # i n c l u d e < vector >
7
8 class Graph {
9 public :
10 using N o d e I d = int ; // v e r t i c e s are n u m b e r e d 0 ,... , n u m _ n o d e s () -1
11
12 class N e i g h b o r {
13 public :
14 N e i g h b o r ( G r a p h :: N o d e I d n , d o u b l e w );
15 d o u b l e e d g e _ w e i g h t () const ;
16 G r a p h :: N o d e I d id () const ;
17 private :
18 G r a p h :: N o d e I d _id ;
19 double _edge_weight ;
20 };
21
22 class Node {
23 public :
24 void a d d _ n e i g h b o r ( G r a p h :: N o d e I d nodeid , d o u b l e w e i g h t );
25 const std :: vector < Neighbor > & a d j a c e n t _ n o d e s () const ;
26 private :
27 std :: vector < Neighbor > _ n e i g h b o r s ;
28 };
29
30 enum D i r T y p e { directed , u n d i r e c t e d }; // e n u m d e f i n e s a type with p o s s i b l e v a l u e s
31 Graph ( NodeId num_nodes , D i r T y p e d i r t y p e );
6.6 Darstellungen von Graphen 91

32 Graph ( char const * filename , D i r T y p e d i r t y p e );


33
34 void a d d _ n o d e s ( N o d e I d n u m _ n e w _ n o d e s );
35 void a d d _ e d g e ( N o d e I d tail , N o d e I d head , d o u b l e w e i g h t = 1 . 0 ) ;
36
37 N o d e I d n u m _ n o d e s () const ;
38 const Node & g e t _ n o d e ( N o d e I d ) const ;
39 void print () const ;
40
41 const D i r T y p e d i r t y p e ;
42 static const NodeId invalid_node ;
43 static const double infinite_weight ;
44
45 private :
46 std :: vector < Node > _ n o d e s ;
47 };
48
49 # endif // G R A P H _ H

1 // graph . cpp ( I m p l e m e n t a t i o n of Class Graph )


2
3 # include < fstream >
4 # include < sstream >
5 # include < stdexcept >
6 # include < limits >
7 # include " graph . h "
8
9 const G r a p h :: N o d e I d G r a p h :: i n v a l i d _ n o d e = -1;
10 c o n s t d o u b l e Graph :: i n f i n i t e _ w e i g h t = std :: n u m e r i c _ l i m i t s < double >:: max ();
11
12
13 void Graph :: a d d _ n o d e s ( N o d e I d n u m _ n e w _ n o d e s )
14 {
15 _ n o d e s . r e s i z e ( n u m _ n o d e s () + n u m _ n e w _ n o d e s );
16 }
17
18 G r a p h :: N e i g h b o r :: N e i g h b o r ( G r a p h :: N o d e I d n , d o u b l e w ): _id ( n ) , _ e d g e _ w e i g h t ( w ) {}
19
20 G r a p h :: G r a p h ( N o d e I d num , D i r T y p e d t y p e ): d i r t y p e ( d t y p e ) , _ n o d e s ( num ) {}
21
22 void G r a p h :: a d d _ e d g e ( N o d e I d tail , N o d e I d head , d o u b l e w e i g h t )
23 {
24 if ( tail >= n u m _ n o d e s () or tail < 0 or head >= n u m _ n o d e s () or head < 0) {
25 throw std :: r u n t i m e _ e r r o r ( " Edge cannot be added due to u n d e f i n e d e n d p o i n t . " );
26 }
27 _ n o d e s [ t a i l ]. a d d _ n e i g h b o r ( head , w e i g h t );
28 if ( d i r t y p e == G r a p h :: u n d i r e c t e d ) {
29 _ n o d e s [ h e a d ]. a d d _ n e i g h b o r ( tail , w e i g h t );
30 }
31 }
32
33 void Graph :: Node :: a d d _ n e i g h b o r ( G r a p h :: N o d e I d nodeid , d o u b l e w e i g h t )
34 {
35 _ n e i g h b o r s . p u s h _ b a c k ( G r a p h :: N e i g h b o r ( nodeid , w e i g h t ));
36 }
37
38 const std :: vector < Graph :: Neighbor > & Graph :: Node :: a d j a c e n t _ n o d e s () const
39 {
40 return _neighbors ;
41 }
42
43 G r a p h :: N o d e I d G r a p h :: n u m _ n o d e s () const
44 {
45 r e t u r n _ n o d e s . s i z e ();
46 }
47
48 const Graph :: Node & Graph :: g e t _ n o d e ( N o d e I d node ) const
92 6 Graphen

49 {
50 if ( node < 0 or node >= s t a t i c _ c a s t < int >( _ n o d e s . size ())) {
51 throw std :: r u n t i m e _ e r r o r ( " I n v a l i d n o d e i d in G r a p h :: g e t _ n o d e . " );
52 }
53 r e t u r n _ n o d e s [ n o d e ];
54 }
55
56 G r a p h :: N o d e I d G r a p h :: N e i g h b o r :: id () const
57 {
58 r e t u r n _id ;
59 }
60
61 d o u b l e G r a p h :: N e i g h b o r :: e d g e _ w e i g h t () const
62 {
63 return _edge_weight ;
64 }
65
66 void Graph :: print () const
67 {
68 if ( d i r t y p e == G r a p h :: d i r e c t e d ) {
69 std :: cout << " D i g r a p h " ;
70 } else {
71 std :: cout << " U n d i r e c t e d graph " ;
72 }
73 std :: cout << " with " << n u m _ n o d e s () << " vertices , n u m b e r e d 0 ,... , "
74 << n u m _ n o d e s () - 1 << " .\ n " ;
75
76 for ( auto n o d e i d = 0; n o d e i d < n u m _ n o d e s (); ++ n o d e i d ) {
77 std :: cout << " The f o l l o w i n g edges are " ;
78 if ( d i r t y p e == G r a p h :: d i r e c t e d ) {
79 std :: cout << " l e a v i n g " ;
80 } else {
81 std :: cout << " i n c i d e n t to " ;
82 }
83 std :: cout << " v e r t e x " << n o d e i d << " :\ n " ;
84 for ( auto n e i g h b o r : _ n o d e s [ n o d e i d ]. a d j a c e n t _ n o d e s ()) {
85 std :: cout < < n o d e i d < < " - " << n e i g h b o r . id ()
86 << " w e i g h t = " << n e i g h b o r . e d g e _ w e i g h t () << " \ n " ;
87 }
88 }
89 }
90
91 Graph :: Graph ( char const * filename , D i r T y p e d t y p e ): d i r t y p e ( d t y p e )
92 {
93 std :: i f s t r e a m file ( f i l e n a m e ); // open file
94 if ( not file ) {
95 throw std :: r u n t i m e _ e r r o r ( " C a n n o t o p e n file . " );
96 }
97
98 G r a p h :: N o d e I d num = 0;
99 std :: s t r i n g l i n e ;
100 std :: g e t l i n e ( file , line ); // get first line of file
101 std :: s t r i n g s t r e a m ss ( line ); // c o n v e r t l i n e to a s t r i n g s t r e a m
102 ss >> num ; // for which we can use >>
103 if ( not ss ) {
104 throw std :: r u n t i m e _ e r r o r ( " I n v a l i d f i l e f o r m a t . " );
105 }
106 a d d _ n o d e s ( num );
107
108 while ( std :: g e t l i n e ( file , line )) {
109 std :: s t r i n g s t r e a m ss ( line );
110 G r a p h :: N o d e I d head , tail ;
111 ss >> tail >> head ;
112 if ( not ss ) {
113 throw std :: r u n t i m e _ e r r o r ( " I n v a l i d f i l e f o r m a t . " );
114 }
115 double weight = 1.0;
6.7 Übungsaufgaben 93

116 ss >> w e i g h t ;
117 if ( tail != head ) {
118 a d d _ e d g e ( tail , head , w e i g h t );
119 }
120 else {
121 throw std :: r u n t i m e _ e r r o r ( " I n v a l i d f i l e f o r m a t : l o o p s not a l l o w e d . " );
122 }
123 }
124 }

1 // t e s t g r a p h . cpp ( R e a d D i g r a p h from File and Print )


2
3 # i n c l u d e " graph . h "
4
5 int main ( int argc , char * argv [])
6 {
7 if ( argc > 1) {
8 Graph g ( argv [1] , G r a p h :: d i r e c t e d );
9 g . print ();
10 }
11 }

6.7 Übungsaufgaben

1. Wie viele Kanten kann ein einfacher unzusammenhängender ungerichteter Graph


mit n Knoten höchstens haben?
2. Es sei G ein Graph und X ⊆ V (G). Zeigen Sie: X enthält genau dann eine ungerade
Anzahl von Knoten mit ungeradem Grad in G, wenn |δ(X )| ungerade ist.
3. Es sei G ein ungerichteter Graph, in dem es genau zwei Knoten mit ungeradem Grad
gibt. Zeigen Sie, dass es dann einen Weg zwischen diesen beiden Knoten gibt.
4. Es sei G ein zusammenhängender einfacher ungerichteter Graph mit mehr als einem
Knoten. Zeigen Sie, dass G einen Knoten v enthält mit

1  2|E(G)|
|δ(w)| ≥ .
|δ(v)| |V (G)|
w∈N (v)

5. Es sei S eine Menge mit n Elementen und A = {A1 , . . . , An } eine Menge von paar-
weise verschiedenen Teilmengen von S. Zeigen Sie, dass es dann ein s ∈ S geben
muss, für das auch die Mengen Ai ∪ {s} (i = 1, . . . , n) paarweise verschieden sind.
Hinweis: Falls nicht, betrachten Sie einen ungerichteten Graphen G mit Knotenmen-
ge A, in dem es für jedes s ∈ S genau eine Kante {Ai , A j } gibt mit Ai A j = {s}.
6. Zeigen Sie, dass es in jedem ungerichteten Graphen G eine Menge X ⊆ V (G) gibt
mit |δ(X )| ≥ 21 |E(G)|. Zeigen Sie, dass es in jedem gerichteten Graphen G eine
Menge X ⊆ V (G) gibt mit |δ + (X )| ≥ 41 |E(G)|. Zeigen Sie, dass beide Schranken
asymptotisch scharf sind.
7. Es sei G ein einfacher ungerichteter Graph. Zeigen Sie, dass es
a) zwei disjunkte Mengen A und B gibt, mit A ∪ B = V (G) so dass in G[A] und in
G[B] alle Knoten geraden Grad haben.
94 6 Graphen

b) zwei disjunkte Mengen C und D gibt, mit C ∪ D = V (G) so dass in G[C] alle
Knoten geraden Grad und in G[D] alle Knoten ungeraden Grad haben.
3
8. Zeigen Sie, dass jeder ungerichtete Graph mit n Knoten und mehr als 21 n 2 Kanten
einen Kreis der Länge höchstens 4 besitzt.
Hinweis: Betrachten Sie die Kardinalität der Menge {v} ∪ N (v) ∪ N (N (v)) für einen
Knoten v.
9. Es sei G ein einfacher ungerichteter Graph, in dem je zwei Kreise kantendisjunkt
sind. Zeigen Sie, dass G einen Knoten besitzt, der Grad höchstens zwei hat.
10. Es seien (V, F1 ) und (V, F2 ) zwei Wälder mit |F1 | < |F2 |. Zeigen Sie, dass es eine
Kante e ∈ F2 \ F1 gibt, so dass (V, F1 ∪ {e}) ein Wald ist.

11. Zeigen Sie, dass ein Baum G mit mehr als einem Knoten genau 2+ max{0, |δ(v)|−
v∈V (G)
2} Blätter hat.
12. Es sei G ein stark zusammenhängender gerichteter Graph dessen zugrundeliegender
ungerichteter Graph mindestens einen Kreis ungerader Länge enthält. Zeigen Sie,
dass dann G einen gerichteten Kreis ungerader Länge enthält.
13. Angenommen, ein gerichteter Graph hat k starke Zusammenhangskomponenten.
Wie viele Kanten muss man dann mindestens und wie viele höchstens hinzufügen,
um den Graphen stark zusammenhängend zu machen?
14. Beweisen Sie:
a) Seien (V, F1 ) und (V, F2 ) zwei Branchings mit 2|F1 | < |F2 |. Dann gibt es eine
Kante e ∈ F2 \ F1 , so dass (V, F1 ∪ {e}) ein Branching ist.
b) Die Aussage aus (a) wird falsch, wenn man die Bedingung „2|F1 | < |F2 |“ durch
„2|F1 | ≤ |F2 |“ ersetzt.
15. Implementieren Sie analog zu der Klasse Queue eine Klasse Stack, die einen
Stack realisiert. Die Klasse vector soll hierbei nicht verwendet werden.
16. Bei vector kann man mit push_back bzw. pop_back ein Element am Ende
hinzufügen bzw. das letzte Element entfernen. Wenn der für den vector reservierte
Speicherplatz für eine push_back-Operation nicht mehr ausreicht, wird (woanders
im freien Speicher) Speicherplatz für doppelt so viele Elemente reserviert, und die
vorhandenen Daten werden hierhin umkopiert, bevor dann push_back ausgeführt
wird. Die Funktion pop_back ändert nichts am reservierten Speicherplatz.
Zeigen Sie, dass eine Folge von n push_back- und pop_back-Operationen (in
beliebiger Reihenfolge) auf einem anfänglich t Elemente enthaltenden vector
insgesamt in O(t + n) Zeit ausgeführt werden kann.
17. Zeigen Sie, dass für jeden gerichteten Graphen G die Anzahl der Zusammenhangs-
komponenten des zugrundeliegenden ungerichteten Graphen plus der Rang der In-
zidenzmatrix von G gleich |V (G)| ist.
18. Implementieren Sie eine Funktion in der Klasse Graph, die – falls der Graph gerich-
tet ist – den zugrundeliegenden ungerichteten Graphen berechnet. Implementieren
Sie eine weitere Funktion, die den maximalen einfachen Teilgraphen berechnet.
Einfache Graphenalgorithmen
7

Wir werden nun erste Graphenalgorithmen kennen lernen. Dabei wird es darum gehen,
einen Graphen zu „erkunden“ und zum Beispiel festzustellen, welche Knoten von einem
bestimmten Ausgangsknoten aus erreichbar sind. Die vorgestellten Algorithmen liefern aber
noch weitere Informationen und haben zahlreiche Anwendungen.

7.1 Graphendurchmusterung

Wir stellen zunächst den allgemeinen Algorithmus zur Graphendurchmusterung in Pseudo-


code vor.

Algorithmus 7.1 (Graphendurchmusterung)


Eingabe: ein Graph G, ein Knoten r ∈ V (G).
Ausgabe: die Menge R ⊆ V (G) der von r aus erreichbaren Knoten und eine Menge F ⊆
E(G), sodass (R, F) eine Arboreszenz mit Wurzel r beziehungsweise ein Baum
ist.

R ← {r }, Q ← {r }, F ← ∅
while Q  = ∅ do
Wähle ein v ∈ Q
+
if ∃ e = (v, w) ∈ δG (v) bzw. e = {v, w} ∈ δG (v) mit w ∈ V (G) \ R
then R ← R ∪ {w}, Q ← Q ∪ {w}, F ← F ∪ {e}
else Q ← Q \ {v}
output(R, F)

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 95


S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
96 7 Einfache Graphenalgorithmen

Satz 7.2 Algorithmus 7.1 arbeitet korrekt und kann mit Laufzeit O(n + m) implementiert
werden, wobei n = |V (G)| und m = |E(G[R])| ≤ |E(G)|.

Beweis Wir behaupten zunächst, dass zu jedem Zeitpunkt (R, F) ein Baum beziehungswei-
se eine Arboreszenz mit Wurzel r in G ist. Dies gilt sicherlich zu Beginn des Algorithmus.
Die einzige Veränderung erfährt (R, F), wenn ein Knoten w und eine Kante e von v nach
w hinzugefügt wird, wobei v bereits in R war, w hingegen nicht. Somit bleibt (R, F) ein
Baum beziehungsweise eine Arboreszenz mit Wurzel r .
Angenommen, am Ende des Algorithmus gäbe es einen von r aus erreichbaren Knoten w,
der nicht in R ist. Sei P ein r -w-Weg und sei {x, y} beziehungsweise (x, y) eine Kante in P
mit x ∈ R und y ∈ / R. Da x ∈ R, muss x auch zu einem bestimmten Zeitpunkt in Q gelegen
haben. Der Algorithmus endet nicht, solange x nicht aus Q entfernt wurde. Dies geschieht
aber nur, falls es keine Kante (x, y) beziehungsweise {x, y} gibt mit y ∈ / R. Widerspruch.
Wir analysieren nun noch die Laufzeit. Kanten und Knoten außerhalb von G[R] werden
niemals betrachtet, außer dass alle Knoten anfangs als „nicht zu R gehörig“ initialisiert
werden. Für jeden besuchten Knoten x merken wir uns innerhalb seiner Liste δ(x) bezie-
hungsweise δ + (x) stets die aktuelle Position, bis zu der die Kanten bereits betrachtet wurden.
Zu Beginn (beim Einfügen in Q) ist dies die Anfangsposition der Liste. Damit wird jede
Kante von G[R] maximal zweimal betrachtet (im gerichteten Fall sogar nur einmal).
Jeder von r aus erreichbare Knoten wird genau einmal in Q eingefügt und aus Q entfernt.
Wir benötigen für Q also eine Datenstruktur, in der Einfügen, Entfernen, und Auswahl eines
Elementes jeweils in konstanter Zeit möglich sind. Beispielsweise haben Stacks und Queues
diese Eigenschaft. Die Gesamtlaufzeit für alle Operationen mit Q ist somit O(m), denn
|R| ≤ |E(G[R])| + 1, da G[R] zusammenhängend ist. 

Wir erhalten auch:

Korollar 7.3 Die Zusammenhangskomponenten eines ungerichteten Graphen G können in


O(n + m) Zeit bestimmt werden, wobei n = |V (G)| und m = |E(G)|.

Beweis Man starte den Algorithmus zur Graphendurchmusterung in einem beliebigen


Knoten. Falls R = V (G), so ist der Graph zusammenhängend. Andernfalls ist G[R] eine
Zusammenhangskomponente von G, und man iteriere mit G[V (G) \ R]. Die Initialisierung
aller Knoten muss nur einmal am Anfang erfolgen. 

Hat ein Graphenalgorithmus Laufzeit O(n + m) mit n = |V (G)| und m = |E(G)|, so sagen
wir auch, dass der Algorithmus lineare Laufzeit hat.
Algorithmus 7.1 kann unterschiedlich implementiert werden. Insbesondere für die Wahl
von v ∈ Q gibt es mehrere Möglichkeiten. Wählt man jeweils den Knoten v ∈ Q, der
als letzter zu Q hinzugefügt wurde (LIFO; also ist Q ein Stack), so wird der Algorithmus
Tiefensuche oder DFS (für depth first search) genannt. Falls man jeweils den Knoten aus
7.2 Breitensuche 97

a b c
f f f
3 0 3 0 3 0

d e d e d e
g g g

2 1 2 1 2 1
a a a
c b c b c b
4 4 4
Abb. 7.1 a ein Digraph; b mögliches Ergebnis von DFS; c Ergebnis von BFS; beide gestartet im
Knoten 0, Kanten aus F sind fett

Q wählt, der als erstes zu Q hinzugefügt wurde (FIFO; also ist Q eine Queue), so handelt
es sich um Breitensuche, auch BFS (für breadth first search) genannt. Ein Baum oder
eine Arboreszenz (R, F), die von DFS beziehungsweise BFS berechnet werden kann, heißt
DFS-Baum beziehungsweise BFS-Baum.

Beispiel 7.4 Abb. 7.1 zeigt ein Beispiel für DFS und BFS. Diese beiden Varianten haben
interessante Eigenschaften; wir untersuchen BFS gleich genauer.

7.2 Breitensuche

Zunächst schauen wir uns eine Implementierung der Breitensuche an. Mit der Variable dist
speichern wir die Länge des Weges im BFS-Baum zu jedem Knoten. Dies wird eigentlich
nicht benötigt, aber wir kommen in Satz 7.6 darauf zurück.

Programm 7.5 (Breitensuche)


1 // bfs . cpp ( B r e a d t h F i r s t S e a r c h )
2
3 # include " graph .h"
4 # include " queue .h"
5
6 G r a p h bfs ( c o n s t G r a p h & graph , G r a p h :: N o d e I d s t a r t _ n o d e )
7 {
8 std :: vector < bool > v i s i t e d ( g r a p h . n u m _ n o d e s () , f a l s e );
9 std :: vector < int > dist ( graph . n u m _ n o d e s () , -1);
10 G r a p h b f s _ t r e e ( g r a p h . n u m _ n o d e s () , g r a p h . d i r t y p e );
11 Queue < G r a p h :: NodeId > q u e u e ;
12
98 7 Einfache Graphenalgorithmen

13 std :: cout << " The f o l l o w i n g v e r t i c e s are r e a c h a b l e f r o m v e r t e x "


14 << s t a r t _ n o d e << " :\ n " ;
15 q u e u e . p u s h _ b a c k ( s t a r t _ n o d e );
16 v i s i t e d [ s t a r t _ n o d e ] = true ;
17 dist [ s t a r t _ n o d e ] = 0;
18
19 w h i l e ( not q u e u e . i s _ e m p t y ()) {
20 auto c u r _ n o d e i d = q u e u e . p o p _ f r o n t ();
21 std :: cout << " V e r t e x " << c u r _ n o d e i d << " has d i s t a n c e "
22 << dist [ c u r _ n o d e i d ] << " .\ n " ;
23 for ( auto n e i g h b o r : g r a p h . g e t _ n o d e ( c u r _ n o d e i d ). a d j a c e n t _ n o d e s ()) {
24 if ( not v i s i t e d [ n e i g h b o r . id ()]) {
25 v i s i t e d [ n e i g h b o r . id ()] = true ;
26 dist [ n e i g h b o r . id ()] = dist [ c u r _ n o d e i d ] + 1;
27 b f s _ t r e e . a d d _ e d g e ( c u r _ n o d e i d , n e i g h b o r . id ());
28 q u e u e . p u s h _ b a c k ( n e i g h b o r . id ());
29 }
30 }
31 }
32
33 return bfs_tree ;
34 }
35
36
37 int main ( int argc , char * argv [])
38 {
39 if ( argc > 1) {
40 Graph g ( argv [1] , Graph :: d i r e c t e d ); // read d i g r a p h from file
41 G r a p h b f s _ t r e e = bfs ( g , 0);
42 std :: cout << " The f o l l o w i n g is a BFS - t r e e r o o t e d at 0:\ n " ;
43 b f s _ t r e e . p r i n t ();
44 }
45 }

Für zwei Knoten v und w in einem Graphen G bezeichne dist(v, w) die Länge eines
kürzesten v-w-Weges in G. Wir nennen dist(v, w) auch den Abstand von v nach w in G.
Falls es keinen v-w-Weg in G gibt, so setzen wir dist(v, w) := ∞. In ungerichteten Graphen
gilt stets dist(v, w) = dist(w, v) für alle v, w ∈ V (G).

Satz 7.6 Jeder BFS-Baum enthält einen kürzesten Weg von einem Knotenr zu allen anderen,
von r aus erreichbaren Knoten. Die Werte dist(r, v) für alle v ∈ V (G) können in linearer
Zeit bestimmt werden.

Beweis Programm 7.5 ist eine Implementierung von Algorithmus 7.1 mit Q als Queue, also
BFS. Die berechneten Werte dist[i] geben offenbar zu jedem Zeitpunkt die Abstände
von r zu allen Knoten in bfs_tree an (dieser Graph, im Folgenden T genannt, hieß in
Algorithmus 7.1 (R, F)). Es bleibt somit zu zeigen:

dist G (r, v) = dist T (r, v) für alle v ∈ V (G).

Da „≤“ offensichtlich ist (T ist ein Teilgraph von G), nehmen wir an, es gäbe einen Knoten
v ∈ V (G) mit dist G (r, v) < dist T (r, v). Sei v so gewählt, dass dist G (r, v) minimal ist.
7.3 Bipartite Graphen 99

Sei P ein kürzester r -v-Weg in G, und (u, v) bzw. {u, v} dessen letzte Kante. Dann ist
dist G (r, u) = dist T (r, u) und somit

dist T (r, v) > dist G (r, v) = dist G (r, u) + 1 = dist T (r, u) + 1. (7.1)

Zu jedem Zeitpunkt des Algorithmus gilt:

(a) dist T (r, x) ≤ dist T (r, y) + 1 für alle x ∈ R und y ∈ Q.


(b) dist T (r, x) ≤ dist T (r, y) für alle x, y ∈ Q, falls x vor y zu Q hinzugefügt wurde.

Dies zeigt man leicht durch Induktion über die Anzahl der durchlaufenen Schritte.
Wegen (a) und (7.1) wird v erst zu R hinzugefügt, nachdem u aus Q entfernt wurde.
Aufgrund der Kante (u, v) bzw. {u, v} müsste dann aber dist T (r, v) ≤ dist T (r, u) + 1
gelten, ein Widerspruch. 

Eigenschaften wie (a) und (b), die während eines Algorithmus stets gelten, bezeichnet
man auch als Invarianten. Sie sind oft sehr nützlich, um die Korrektheit eines Algorithmus
zu zeigen.

7.3 Bipartite Graphen

Folgende spezielle Arten von ungerichteten Graphen kommen häufig vor:

Definition 7.7 Ein vollständiger Graph ist ein einfacher ungerichteter Graph, in dem
je zwei Knoten durch eine Kante verbunden sind. Einen vollständigen Graphen auf einer
n-elementigen Knotenmenge bezeichnet man oft mit Kn .
Eine Bipartition eines ungerichteten Graphen G besteht aus zwei disjunkten Knoten-
mengen A und B, deren Vereinigung V (G) ist, sodass jede Kante in E(G) genau einen
Endknoten in A hat. Ein Graph heißt bipartit, falls er eine Bipartition besitzt. Wenn wir
die Notation G = (A∪B,˙ E(G)) benutzen, so meinen wir damit, dass die Mengen A und B
eine Bipartition bilden.
Ein vollständiger bipartiter Graph ist ein Graph G mit Bipartition V (G) = A∪B ˙ und
E(G) = {{a, b} | a ∈ A, b ∈ B}. Falls |A| = n und |B| = m, so wird er oft mit Kn,m
bezeichnet.
Ein ungerader Kreis ist ein Kreis ungerader Länge.

Satz 7.8 (König [24]) Ein ungerichteter Graph ist genau dann bipartit, wenn er keinen un-
geraden Kreis enthält. In linearer Zeit kann man in einem gegebenen ungerichteten Graphen
entweder eine Bipartition oder einen ungeraden Kreis finden.
100 7 Einfache Graphenalgorithmen

Beweis Wir zeigen zunächst, dass ein bipartiter Graph keinen Kreis ungerader Länge ent-
˙ und sei C ein Kreis der Länge k in G,
hält. Sei also G bipartit mit Bipartition V (G) = A∪B,
mit Knotenmenge {v1 , . . . , vk } und Kanten {vi , vi+1 } für i = 1, . . . , k, wobei vk+1 := v1
sei. Ohne Beschränkung der Allgemeinheit sei v1 ∈ A. Folglich gilt v2 ∈ B, v3 ∈ A
usw., das heißt vi ∈ A genau dann, wenn i ungerade. Wegen vk+1 = v1 ∈ A folgt, dass k
gerade ist.
Nun zeigen wir die zweite Aussage des Satzes (daraus folgt dann auch die erste). Sei
G ohne Beschränkung der Allgemeinheit zusammenhängend. (Andernfalls finden wir eine
Bipartition für jede Zusammenhangskomponente und fügen diese zusammen.)
Wir wenden BFS auf G mit einem Startknoten r an, erhalten einen aufspannenden Baum
T = (V (G), F), und setzen

A := {v ∈ V (G) | dist T (r, v) ist gerade} und B := V (G)\A.

Falls e ∈ δ(A) für alle e ∈ E(G), so ist A∪B ˙ Bipartition. Ansonsten gibt es eine Kante
e = {v, w} mit v, w ∈ A oder v, w ∈ B. Sei Ce der Kreis in (V (G), F ∪{e}).˙ Ce entsteht
aus e, dem u-v-Weg in T und dem u-w-Weg in T, wobei u der letzte gemeinsame Knoten
auf dem r -v-Weg in T und dem r -w-Weg in T ist. Wir haben dist T (r, v) + dist T (r, w) =
2 dist T (r, u) + |E(Ce )| − 1. Da auf der linken Seite der Gleichung eine gerade Zahl steht,
muss Ce ein ungerader Kreis sein. 

Man hätte in diesem Beweis übrigens statt T einen beliebigen aufspannenden Baum nehmen
können.
Satz 7.8 liefert eine gute Charakterisierung der Eigenschaft „bipartit“: man kann nicht nur
leicht beweisen, dass ein Graph diese Eigenschaft hat (indem man eine Bipartition angibt),
sondern auch, dass ein Graph die Eigenschaft nicht hat (indem man einen ungeraden Kreis
angibt). Beide „Beweise“ sind leicht überprüfbar.

7.4 Azyklische Digraphen

Eine wichtige Klasse gerichteter Graphen wird durch folgende Definition beschrieben:

Definition 7.9 Ein Digraph heißt azyklisch, falls er keinen (gerichteten) Kreis enthält.
Sei G ein Digraph mit n Knoten. Eine topologische Ordnung von G ist eine Ordnung
der Knoten V (G) = {v1 , . . . , vn }, sodass i < j für jede Kante (vi , v j ) ∈ E(G) gilt.
7.5 Übungsaufgaben 101

Wir benötigen ein Lemma:

Lemma 7.10 Ist G ein Digraph mit δ + (v)  = ∅ für alle v ∈ V (G), so kann man in
O(|V (G)|) Zeit einen Kreis in G finden.

Beweis Man startet einfach in einem beliebigen Knoten und folgt jeweils einer beliebigen
ausgehenden Kante, bis sich ein Knoten wiederholt. 

Folgender Satz liefert eine gute Charakterisierung:

Satz 7.11 Ein Digraph hat genau dann eine topologische Ordnung, wenn er azyklisch ist.
Zu einem gegebenen Digraphen G kann man in linearer Zeit entweder eine topologische
Ordnung von G oder einen Kreis in G finden.

Beweis Hat G eine topologische Ordnung, so kann G offenbar keinen Kreis enthalten.
Wir zeigen deshalb die zweite Aussage des Satzes (daraus folgt dann auch die erste). Sei
n := |V (G)| und m := |E(G)|. Zuerst berechnen wir den Ausgangsgrad a(v) := |δ + (v)|
für alle v ∈ V (G) und speichern alle Knoten v mit a(v) = 0 in einer Liste L 0 . Dies geht in
linearer Zeit.
Ist L 0 leer, so führt Lemma 7.10 in O(n) Schritten zu einem Kreis. Andernfalls wählen
wir einen Knoten aus L 0 und nennen ihn vn . Wir entfernen vn aus L 0 und führen für alle
(u, vn ) ∈ δ − (vn ) folgendes durch: verringere a(u) um eins, und falls dann a(u) = 0 ist,
füge u in L 0 ein. Dies geht offenbar in Zeit O(1 + |δ − (vn )|).
Dann sind L 0 und a(v) für v ∈ V (G) \ {vn } korrekt für G − vn . Wir verringern also n
um eins und iterieren solange n > 0 ist.
Damit erhalten wir in der Gesamtzeit O(n + m) entweder einen Kreis oder eine topolo-
gische Ordnung v1 , . . . , vn von G. 

7.5 Übungsaufgaben

1. Implementieren Sie die Tiefensuche. Fügen Sie zur Klasse Graph eine Funktion hinzu,
die die Anzahl der Zusammenhangskomponenten berechnet, falls der Graph ungerichtet
ist. Sie soll lineare Laufzeit haben.
2. Es sei G ein zusammenhängender ungerichteter Graph, r ∈ V (G), und T ein durch Tie-
fensuche ausgehend von r gefundener aufspannender Baum. Für u, v ∈ V (G) bezeich-
ne Puv den u-v-Weg in T . Zeigen Sie: Für alle Kanten {x, y} ∈ E(G) gilt x ∈ V (Pr y )
oder y ∈ V (Pr x ).
102 7 Einfache Graphenalgorithmen

3. Geben Sie ein Verfahren für das folgende Problem an: Zu einem gegebenen Baum T
hat man Zeit O(|V (T )|) für ein Präprozessing. Danach soll, wenn zwei Knoten x und
y von T gegeben sind, in Zeit O(distT (x, y)) der x-y-Weg in T ausgegeben werden.
4. Geben Sie einen Algorithmus an, der in linearer Zeit einen Kreis in einem gegebenen
ungerichteten Graphen G findet oder entscheidet dass G ein Wald ist.
5. Es sei G ein einfacher ungerichteter Graph mit |E(G)| ≥ |V (G)| + 4. Zeigen Sie, dass
G zwei kantendisjunkte Kreise enthalten muss. Zeigen Sie zudem, dass es Graphen G
mit |E(G)| = |V (G)| + 3 gibt, die keine zwei kantendisjunkte Kreise enthalten.
6. Es sei T ein Baum mit n Knoten. Ein Knoten v ∈ V (T ) heißt Zentralknoten, wenn
die Zusammenhangskomponenten, die nach Herausnahme von v entstehen, jeweils
höchstens n2 Knoten enthalten. Zeigen Sie, dass ein Baum stets einen Zentralknoten
enthält. Geben Sie außerdem ein möglichst effizientes Verfahren an, um einen solchen
Knoten zu finden (mit Laufzeitanalyse).
7. Das Komplement Ḡ eines ungerichteten Graphen G ist der Graph mit V (Ḡ) = V (G), in
dem zwei Knoten genau dann durch eine Kante verbunden sind, wenn sie es in G nicht
sind. Geben Sie einen Algorithmus mit linearer Laufzeit an, der zu einem gegebenen
Graphen überprüft, ob sein Komplement bipartit ist.
8. Zeigen Sie, dass ein ungerichteter Graph G genau dann bipartit ist, wenn jeder Teilgraph
H von G eine Menge X ⊆ V (H ) von paarweise nicht benachbarten Knoten mit |X | ≥
|V (H )|
2 enthält.
9. Es sei G ein bipartiter Graph mit n Knoten und k Zusammenhangskomponenten. Wie
viele Mengen X ⊆ V (G) gibt es, sodass δ(X ) = E(G) ist?
10. Zu einem gegebenen ungerichteten Graphen G soll eine inklusionsminimale Teilmenge
F ⊆ E(G) berechnet werden, sodass (V (G), E(G) \ F) bipartit ist. Zeigen Sie, dass
ein solches F in linearer Zeit berechnet werden kann.
11. Implementieren Sie eine Funktion, die zu einem gegebenen gerichteten Graphen G
entweder eine topologische Ordnung von G oder einen Kreis in G berechnet.
12. Es sei l ∈ N und G ein gerichteter Graph, der keinen Kantenzug x1 , e1 , x2 , . . . , xl , el ,
xl+1 enthält. Zeigen Sie, dass G mindestens |V (G)| + 1 − l verschiedene topologische
Ordnungen besitzt.
13. Es sei G ein gerichteter Graph, und G derjenige einfache gerichtete Graph, dessen
Knoten die starken Zusammenhangskomponenten von G sind und der genau dann eine
+ −
Kante (X, Y ) enthält, wenn δG (V (X )) ∩ δG (V (Y ))  = ∅. Zeigen Sie, dass G eine
topologische Ordnung besitzt.
Sortieralgorithmen
8

Sehr oft müssen gespeicherte Daten sortiert werden. Hierfür gibt es im Wesentlichen zwei
Gründe: zum einen arbeiten bestimmte Algorithmen Objekte in einer bestimmten Reihenfol-
ge ab, zum anderen kann man in einem sortierten Datenbestand mit random access einzelne
Objekte viel schneller finden (mit binärer Suche, Algorithmus 5.2).

8.1 Das allgemeine Sortierproblem

Definition 8.1 Sei S eine Menge. Eine Relation R ⊆ S × S heißt partielle Ordnung (von
S), wenn für alle a, b, c ∈ S gilt:

• (a, a) ∈ R (Reflexivität);
• ((a, b) ∈ R ∧ (b, a) ∈ R) ⇒ a = b (Antisymmetrie);
• ((a, b) ∈ R ∧ (b, c) ∈ R) ⇒ (a, c) ∈ R (Transitivität).

Statt (a, b) ∈ R schreiben wir oft a Rb. Eine partielle Ordnung R ist eine totale Ordnung
von S, wenn (a, b) ∈ R oder (b, a) ∈ R für alle a, b ∈ S gilt.

Beispielsweise ist die gewohnte Ordnung „≤“ eine totale Ordnung auf R. Andererseits ist
die Teilmengenrelation „⊆“ eine partielle, aber im Allgemeinen keine totale Ordnung auf
einer beliebigen Familie von Mengen. In einem gerichteten Graphen G ist R := {(v, w) ∈
V (G) × V (G) | w ist von v aus erreichbar} genau dann eine partielle Ordnung, wenn G
azyklisch ist.

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 103
S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
104 8 Sortieralgorithmen

Für endliche Mengen S kann eine totale Ordnung  durch Nummerierung der Elemente
angegeben werden, d. h. durch eine Bijektion f : S → {1, . . . , n} wobei f (s) = |{a ∈ S |
a  s}| für alle s ∈ S. Damit können wir definieren:

Berechnungsproblem 8.2 (Allgemeines Sortierproblem)


Eingabe: eine endliche Menge S mit einer partiellen Ordnung  (durch ein Orakel gegeben).
Aufgabe: Berechne eine Bijektion f : {1, . . . , n} → S mit f ( j)  f (i) für alle 1 ≤ i <
j ≤ n.

Wir nehmen also zunächst an, dass wir keine weiteren Kenntnisse über die partielle Ordnung
haben, als dass wir für je zwei Elemente a und b fragen können, ob a  b ist. Natürlich
können wir auch die Eigenschaften der partiellen Ordnung ausnutzen, insbesondere die
Transitivität.
Die Eingabe ist in der Regel durch irgendeine Bijektion h : {1, . . . , n} → S und das
Orakel gegeben. Gesucht ist dann eine Permutation π (d. h. eine Bijektion π : {1, . . . , n} →
{1, . . . , n}), so dass f : i → h(π(i)) die gewünschte Eigenschaft hat.
Manchmal kann man schneller sortieren, wenn die Eingabe anders gegeben ist (Satz 7.11
ist ein Beispiel dafür), und/oder die partielle Ordnung zusätzliche Eigenschaften hat (z. B.
eine totale Ordnung ist); hierauf gehen wir ab Abschn. 8.3 ein.

8.2 Sortieren durch sukzessive Auswahl

Ein naives Sortierverfahren besteht darin, alle n! Permutationen auszuprobieren und jeweils
alle n(n−1)
2 geordneten Paare (i, j) mit 1 ≤ i < j ≤ n daraufhin zu testen, ob f ( j)  f (i)
gilt. Dies ist natürlich sehr ineffizient.
Wir zeigen nun ein besseres Verfahren: Sortieren durch sukzessive Auswahl. Hier
bestimmt man f (i) := s sukzessive für i = 1, . . . , n, und zwar so, dass für alle
t ∈ S \ { f (1), . . . , f (i − 1)} gilt: t  s ⇒ t = s.

Algorithmus 8.3 (Sortieren durch sukzessive Auswahl)


Eingabe: eine Menge S = {s1 , . . . , sn }; eine partielle Ordnung  auf S (durch ein Orakel
gegeben).
Ausgabe: eine Bijektion f : {1, . . . , n} → S mit f ( j)  f (i) für alle 1 ≤ i < j ≤ n.
8.2 Sortieren durch sukzessive Auswahl 105

for i ← 1 to n do f (i) ← si
for i ← 1 to n do
for j ← i to n do
if f ( j)  f (i) then swap( f (i), f ( j))
output f

Die Iterationen mit i = j sind offenbar überflüssig, sie erleichtern aber den Beweis des
folgenden Satzes. Der swap-Befehl vertauscht den Inhalt zweier Variablen. Wir zeigen:

Satz 8.4 Algorithmus 8.3 löst das allgemeine Sortierproblem 8.2 korrekt und hat Laufzeit
O(n 2 ).

Beweis Die Laufzeit ist offensichtlich. Da Funktionswerte von f nur vertauscht werden
(mit dem swap-Befehl), ist f stets eine Bijektion.
Wir zeigen, dass für alle 1 ≤ i ≤ j ≤ n nach Iteration (i, j) folgendes gilt:

(a) f (k)  f (h) für alle 1 ≤ h < i und h < k ≤ n;


(b) f (k)  f (i) für alle i < k ≤ j.

Für Iteration (1, 1) sind beide Bedingungen leer.


Bedingung (a) kann allenfalls durch Erhöhen von i zerstört werden; dass dies nicht der
Fall ist, folgt aber aus Bedingung (b) für j = n, da i erst erhöht wird, wenn j = n ist.
Es bleibt zu zeigen, dass die Bedingung (b) stets erhalten bleibt. Hierzu betrachten wir
den Zustand zu Beginn von Iteration (i, j). Gilt f ( j)  f (i), so wird f hier nicht verändert,
und (b) gilt offenbar weiterhin.
Gilt f ( j)  f (i), so betrachten wir einen Index k mit i < k ≤ j. Für k = j gilt wegen
f (i)  f ( j) (Antisymmetrie), dass (b) nach dem swap-Befehl gilt. Für k < j galt zuvor
(b), also f (k)  f (i) und somit (wegen der Transitivität) f (k)  f ( j). Der swap-Befehl
zerstört (b) also nicht.
Am Ende des Algorithmus, d. h. nach Iteration (n, n), impliziert (a) die Korrektheit. 

Die Funktion sort1 in Programm 8.5 zeigt eine Implementierung dieses Algorithmus. Die
Implementierung und das Anwendungsbeispiel sind so gewählt, dass wir ein paar weitere
Möglichkeiten, die C++ bietet, illustrieren können.
Man beachte insbesondere das abstrakte Interface der Funktion sort1. Man bekommt
lediglich zwei Iteratoren: ein Verweis first auf das erste Element (das kann z. B. ein
Pointer oder ein Index sein), ein Verweis last auf die Stelle hinter dem letzten Element,
sowie eine Funktion comp, mit der man zwei Elemente vergleichen kann. Ein Iterator (siehe
die Box C++ im Detail (2.3)) muss mindestens drei Operationen zur Verfügung stellen:
106 8 Sortieralgorithmen

* Dereferenzierung (mit derselben Syntax wie bei Pointern);


++ Weitergehen zum Folgeelement;
!= Vergleich (insbesondere mit last, damit erkannt wird, wenn keine weiteren Elemente
mehr kommen).

Die Vergleichsfunktion comp könnte wirklich eine boolesche Funktion sein, ist hier aber eine
Variable vom Typ BirthdayComparison: einer Klasse, die den Operator () zur Ver-
fügung stellt; dieser hat zwei Argumente und verhält sich wie eine boolesche Funktion; sie
vergleicht die Argumente b1 und b2 und liefert genau dann true, wenn b1b2∧b1 =b2
ist. Das Vergleichsorakel als Klasse zu implementieren, bietet die Möglichkeit, zum Ver-
gleich noch andere Informationen (hier die Variable _today) zu nutzen, was manchmal
notwendig ist.

Programm 8.5 (Sortieren durch sukzessive Auswahl)


1 // sort . cpp ( S o r t i n g by S u c c e s s i v e S e l e c t i o n )
2
3 # include < iostream >
4 # include < string >
5 # include < vector >
6 # include < ctime >
7 # include < random >
8 # include < iomanip >
9
10 t e m p l a t e < c l a s s Iterator , c l a s s Compare >
11 void s o r t 1 ( I t e r a t o r first , I t e r a t o r last , c o n s t C o m p a r e & comp )
12 // I t e r a t o r must have o p e r a t o r s * , ++ , and !=
13 {
14 for ( I t e r a t o r c u r r e n t = f i r s t ; c u r r e n t != last ; ++ c u r r e n t ) {
15 Iterator cur_min = current ;
16 for ( I t e r a t o r i = c u r r e n t ; i != last ; ++ i ) {
17 if ( comp (* i , * c u r _ m i n )) {
18 cur_min = i;
19 }
20 }
21 std :: swap (* cur_min , * c u r r e n t );
22 }
23 }
24
25
26 s t r u c t Date {
27 Date () // r a n d o m c o n s t r u c t o r
28 {
29 t i m e _ t r d a t e = d i s t r i b u t i o n ( g e n e r a t o r );
30 _ t i m e = * l o c a l t i m e (& r d a t e );
31 }
32
33 Date ( time_t date ): _time (* l o c a l t i m e (& date )) {} // c o n s t r u c t o r
34
35 s t a t i c std :: u n i f o r m _ i n t _ d i s t r i b u t i o n < time_t > d i s t r i b u t i o n ;
36 s t a t i c std :: d e f a u l t _ r a n d o m _ e n g i n e g e n e r a t o r ;
37 static time_t today ;
38 tm _ t i m e ;
39 };
40
8.2 Sortieren durch sukzessive Auswahl 107

41 t i m e _ t D a t e :: t o d a y = time ( n u l l p t r );
42 std :: u n i f o r m _ i n t _ d i s t r i b u t i o n < time_t > Date :: d i s t r i b u t i o n (1 , Date :: today );
43 std :: d e f a u l t _ r a n d o m _ e n g i n e Date :: g e n e r a t o r ( Date :: today );
44
45
46 std :: o s t r e a m & operator < <( std :: o s t r e a m & os , c o n s t Date & date )
47 {
48 s t a t i c c o n s t std :: s t r i n g m o n t h n a m e [12] = { " Jan " , " Feb " , " Mar " ,
49 " Apr " , " May " , " Jun " , " Jul " , " Aug " , " Sep " , " Oct " , " Nov " , " Dec " };
50 os << m o n t h n a m e [ d a t e . _ t i m e . t m _ m o n ] << " " << std :: setw (2)
51 << date . _time . t m _ m d a y < < " , " << date . _time . t m _ y e a r + 1 9 0 0 ;
52 r e t u r n os ;
53 }
54
55
56 class BirthdayComparison {
57 public :
58 B i r t h d a y C o m p a r i s o n ( c o n s t D a t e & t o d a y ) : _ t o d a y ( t o d a y ) {}
59 bool o p e r a t o r ()( c o n s t Date & b1 , c o n s t Date & b2 ) c o n s t
60 {
61 r e t u r n d a y _ n u m ( b1 ) < d a y _ n u m ( b2 );
62 }
63 private :
64 int d a y _ n u m ( c o n s t Date & date ) c o n s t
65 {
66 r e t u r n (32 * (12 + d a t e . _ t i m e . t m _ m o n - _ t o d a y . _ t i m e . t m _ m o n ) +
67 date . _time . t m _ m d a y - _today . _time . tm_mday ) % (12*32);
68 }
69 Date c o n s t & _ t o d a y ;
70 };
71
72
73 int main ()
74 {
75 std :: cout << " T o d a y is " << Date ( Date :: today ) << " .\ n "
76 << " How m a n y r a n d o m b i r t h d a y s do you want to sort ? " ;
77 int n ;
78 std :: cin >> n ;
79 std :: vector < Date > d a t e s ( n );
80 std :: cout << " Here are " << n << " r a n d o m d a t e s :\ n " ;
81 for ( auto d : d a t e s ) {
82 std :: cout << d << " ";
83 }
84 std :: cout << " \ n \ n " ;
85
86 B i r t h d a y C o m p a r i s o n c o m p a r i s o n (( Date ( Date :: today )));
87 std :: cout << " S o r t i n g ... " << std :: endl ;
88 c l o c k _ t t i m e r = c l o c k ();
89 s o r t 1 ( d a t e s . b e g i n () , d a t e s . end () , c o m p a r i s o n );
90 t i m e r = c l o c k () - t i m e r ;
91
92 std :: cout << " The u p c o m i n g b i r t h d a y s are , s t a r t i n g t o d a y :\ n " ;
93 for ( auto d : d a t e s ) {
94 std :: cout << d << " ";
95 }
96 std :: cout << " \ n \ n " << " S o r t i n g took "
97 << s t a t i c _ c a s t < double >( timer )/ C L O C K S _ P E R _ S E C << " s e c o n d s .\ n " ;
98 }
108 8 Sortieralgorithmen

Hätten wir für unsere Klasse Queue noch einen Iterator (eine Teilklasse mit einer Varia-
ble vom Typ Item*, einem Konstruktor mit Argument vom Typ Item*, Operatoren ++,
!= und *) sowie Funktionen begin() und end(), die einen _front bzw. nullptr
enthaltenden Iterator zurückgeben, ergänzt, so könnte man auch eine (z. B. analog mit Zu-
fallsdaten befüllte) Queue vom Typ Queue<Date> mit exakt derselben Funktion sort1
sortieren.

C++ im Detail (8.1): Zufallszahlen und Uhrzeit


In C++ gibt es mehrere Möglichkeiten, um Pseudo-Zufallszahlen zu erzeugen.
Die einfachste Version benutzt die Funktion rand(), welche nach Einbinden von
#include <cstdlib> zur Verfügung steht; vgl. Programm 8.20. Diese Funk-
tion liefert eine ganze Zahl zwischen 0 und RAND_MAX, wobei RAND_MAX eine
vordefinierte Konstante mit Wert mindestens 32767 ist. Die von rand() erzeugten
Pseudo-Zufallszahlen können allerdings weit von der Gleichverteilung abweichen.
Eine etwas aufwändigere Möglichkeit, die mehr Einfluss auf die erzeug-
ten Zufallszahlen bietet und Pseudo-Zufallszahlen von besserer Qualität er-
zeugt, steht nach Einbinden von #include <random> zur Verfügung. Mit
std::default_random_engine kann man einen Zufallszahlengenerator de-
finieren, der Zufallszahlen gemäß einer wählbaren Verteilung erzeugt. Bei der De-
finition des Zufallszahlengenerators kann optional ein Argument angegeben wer-
den, das zur Initialisierung des Generators benutzt wird. Dies haben wir bei-
spielsweise in Zeile 43 in dem Programm 8.5 gemacht. Wir haben als Vertei-
lung std::uniform_int_distribution in Zeile 42 gewählt. Damit lassen
sich gleichverteilt ganze Zahlen erzeugen, die in dem angegebenen Intervall liegen.
Als Templateparameter muss ein Integertyp angegeben werden; wir haben den Typ
time_t benutzt, den wir weiter unten erklären. Zeile 29 von Programm 8.5 zeigt,
wie eine einzelne Zufallszahl erzeugt wird.
Nach Einbinden von #include <ctime> stehen eine Reihe von nützlichen
Funktionen zur Verfügung, um die aktuelle Uhrzeit zu ermitteln und Zeitdifferenzen
zu berechnen. Ein Aufruf von time(nullptr) liefert die Anzahl Sekunden zurück,
die seit dem 1. 1. 1970 vergangen sind. Der Ergebnistyp ist time_t. Die Funktion
localtime (siehe z. B. Zeile 30 von Programm 8.5) konvertiert einen Wert vom
Typ time_t in einen Wert vom Typ tm. Dabei ist tm eine Klasse, die unter ande-
rem die Werte tm_year, tm_mon und tm_mday enthält, womit man Jahr, Monat
und Tag eines Datums erhält. Wir benutzen dies z. B. in den Zeilen 50 und 51 des
Programms 8.5.
Um die genaue Laufzeit eines Programms zu bestimmen, reicht eine sekunden-
genaue Auflösung meist nicht aus. Die Funktion clock() liefert die von einem
Programm verbrauchte CPU-Zeit mit einer Genauigkeit von mindestens einer Milli-
sekunde als Wert vom Typ clock_t zurück. Die Umwandlung in Sekunden erfolgt
durch Division durch CLOCKS_PER_SEC, siehe Zeile 97 von Programm 8.5.
8.2 Sortieren durch sukzessive Auswahl 109

Meist erlaubt ein Iterator auch die Operatoren –– (hierzu muss eine Liste aber dop-
pelt verkettet sein) und ==. Manchmal (z. B. bei vector, nicht aber bei Listen) ist auch
random access möglich, d. h. man kann zum Beispiel die Operation +=i für ein beliebi-
ges int i auf Iteratoren anwenden (wobei man aufpassen muss, dass man den Bereich
nicht verlässt). Random access ist für einige Algorithmen unentbehrlich. Erforderlichenfalls
kann man natürlich vorab die Daten in einen vector umkopieren und nach der Sortierung
zurückkopieren.
Die Standardbibliothek stellt nach Einbinden von #include <algorithm> die
Funktion std::sort bereit. Das Interface dieser Funktion hat dieselbe Struktur wie un-
ser sort1. (Bei std::sort kann man auch das dritte Argument weglassen; dann wird
einfach mit < verglichen. Allerdings braucht std::sort random access.)
Damit haben wir ein wesentliches Merkmal der C++-Standardbibliothek kennen gelernt:
die Trennung von Datenstrukturen und Algorithmen. Algorithmen wie die Sortierfunktionen
std::sort und sort1 funktionieren weitgehend unabhängig von der Datenstruktur, in
der die Objekte gespeichert werden.
Das Programm 8.5 zeigt auch die Benutzung von Pseudo-Zufallszahlen und Uhrzeiten;
siehe die Box C++ im Detail (8.1). Programm 8.5 benutzt für die Ausgabe den Streamma-
nipulator setw, der in iomanip definiert ist und mit dem man festlegen kann, wie viele
Stellen die unmittelbar nachfolgende Ausgabe mindestens haben soll.
Wir zeigen nun, dass die Laufzeit von Algorithmus 8.3 für das allgemeine Sortierproblem
 
bestmöglich ist. Für 0 ≤ k ≤ n schreiben wir nk := k!(n−k)! n!
(mit 0! := 1). Für eine
S
endliche Menge S und 0 ≤ k ≤ |S| schreiben wir k := {A ⊆ S | |A| = k}. Man beachte
   
| kS | = |S|
k .

Satz 8.6 Für jeden Algorithmus für das Problem 8.2 und jedes n ∈ N gibt es eine Eingabe
 
(S, ) mit |S| = n, für die der Algorithmus mindestens n2 Orakelaufrufe braucht.

Beweis Für S = {1, . . . , n} und (a, b) ∈ S×S mit a = b betrachte R := {(a, b)}∪{(x, y) ∈
S × S | x = y}. Der Algorithmus muss dann das Orakel für (a, b) oder für (b, a) fragen,
weil er sonst nicht wissen kann, in welcher Reihenfolge er a und b platzieren darf. Da (a, b)
   
unbekannt ist, muss er für alle n2 möglichen Paare aus 2S mindestens einmal das Orakel
fragen. 

Dies ist exakt die Anzahl der Orakelaufrufe des Algorithmus 8.3, wenn man mit j nicht bei
i sondern bei i + 1 beginnt, was wie gesagt äquivalent ist. Damit ist der Algorithmus in
diesem Sinne bestmöglich.
110 8 Sortieralgorithmen

8.3 Sortieren nach Schlüsseln

Um schnellere Sortierverfahren zu erhalten, muss man also Zusatzannahmen machen. In den


meisten Anwendungen kann die partielle Ordnung durch so genannte Schlüssel beschrieben
werden: für jedes Element s der zu sortierenden Menge S hat man einen Schlüssel k(s) ∈ K ,
wobei K eine Menge mit einer totalen Ordnung ≤ ist (oft N oder R mit der gewohnten
Ordnung). Die partielle Ordnung  auf S ist dann durch a  b ⇔ (a = b ∨ k(a) < k(b))
gegeben. Eine so entstehende partielle Ordnung nennen wir durch Schlüssel induziert. Ein
Beispiel sind die Geburtstage in Programm 8.5; hier ist day_num eine solche Funktion k.
Damit ergibt sich folgendes Problem:

Berechnungsproblem 8.7 (Sortieren nach Schlüsseln)


Eingabe: eine endliche Menge S, sowie eine Funktion k : S → K und eine totale
Ordnung ≤ auf K .
Aufgabe: Berechne eine Bijektion f : {1, . . . , n} → S mit k( f (i)) ≤ k( f ( j)) für alle
1 ≤ i < j ≤ n.

Ist K = {1, . . . , m} mit der natürlichen Ordnung, und sind die Schlüssel k(s) ∈ K für
alle s ∈ S explizit bekannt, so kann man das Sortierproblem in der Laufzeit (und mit dem
Speicheraufwand) O(|S| + m) lösen, indem man Listen k −1 (i) für i = 1, . . . , m erzeugt
und diese aneinanderhängt (Bucketsort). Für m = O(|S|) ist das bestmöglich.
Im Folgenden wollen wir aber nichts weiter über (K , ≤) voraussetzen. Wir nehmen
wieder an, dass wir nur über ein Orakel Informationen über die Schlüssel erhalten können:
für a, b ∈ S können wir fragen, ob k(a) < k(b) ist (äquivalent: ob a  b und a = b ist).
Natürlich funktioniert Algorithmus 8.3 auch für das Sortieren nach Schlüsseln. Beispiele
für weitere einfache Verfahren für dieses Problem sind:

• Sortieren durch Einfügen: Man sortiert die ersten i Elemente, sukzessive für i = 1, . . . , n,
indem man das i-te Element an einer richtigen Stelle in die sortierte Liste der ersten i − 1
Elemente einfügt.
• Bubblesort: Man geht die Liste bis zu (n − 1)-mal durch und vertauscht dabei jeweils
zwei benachbarte Elemente, wenn sie nicht in der richtigen Reihenfolge stehen; nach i
Durchgängen stehen die größten i Elemente in richtiger Reihenfolge am Ende der Liste.

Diese Verfahren sind manchmal schneller, für beide gilt aber auch: für jedes n gibt es
 
Instanzen, bei denen n2 Vergleiche benötigt werden (wobei n die Anzahl der Elemente ist).
Im nächsten Abschnitt lernen wir einen besseren Algorithmus kennen.
8.4 Mergesort 111

8.4 Mergesort

Wir wollen nun Sortieralgorithmen mit Laufzeit O(n log n) studieren. Mergesort beruht
auf dem „Divide-and-Conquer“-Prinzip: Teile das Ausgangsproblem in kleinere Teilpro-
bleme, löse diese separat (meist rekursiv), und füge die Lösungen der Teilprobleme zu einer
Gesamtlösung zusammen.

Algorithmus 8.8 (Mergesort)


Eingabe: eine Menge S = {s1 , . . . , sn }; eine durch Schlüssel induzierte partielle Ordnung 
auf S (durch ein Orakel gegeben).
Ausgabe: eine Bijektion f : {1, . . . , n} → S mit f ( j)  f (i) für alle 1 ≤ i < j ≤ n.

if |S| > 1
.
then sei S = S1 ∪ S2 mit |S1 | =  n2  und |S2 | =  n2 
sortiere S1 und S2 (rekursiv mit Mergesort)
Verschmelze die Sortierungen von S1 und S2 zu Sortierung von S .

.
Das Partitionieren von S in S1 ∪ S2 ist offensichtlich einfach zu realisieren. Danach wird
Mergesort rekursiv für S1 und S2 aufgerufen. Das Verschmelzen der Sortierungen lässt sich
leicht in O(|S|) Zeit bewerkstelligen: Hierzu geht man die sortierten Listen gleichzeitig
durch, vergleicht jeweils die ersten („kleinsten“) Elemente der beiden Listen, entfernt das
kleinere aus seiner Liste und fügt es ans Ende einer neuen Liste ein.
Man kann dann die Elemente der neuen Liste am Ende in die Speicherplätze der alten
Liste kopieren und die neue Liste löschen. Kennt man die Pointer einer Liste (und nicht
nur Iteratoren), so kann man auf das Erzeugen einer neuen Liste ganz verzichten und beim
Verschmelzen nur die Pointer umhängen. Deshalb eignet sich Mergesort besonders gut zum
Sortieren von Listen (random access wird nicht benötigt).

Satz 8.9 Mergesort sortiert n Elemente in der Laufzeit O(n log n).

Beweis Bezeichne T (n) die Laufzeit von Mergesort für n Elemente. Offenbar gibt es ein
c ∈ N mit T (1) ≤ c und

T (n) ≤ T ( n2 ) + T ( n2 ) + cn für alle n ≥ 2 .

Behauptung T (2k ) ≤ c(k + 1)2k für alle k ∈ N ∪ {0}.


Wir zeigen die Behauptung per Induktion über k. Für k = 0 ist sie trivial. Für k ∈ N ist
(mit der Induktionsvoraussetzung)
112 8 Sortieralgorithmen

T (2k ) ≤ 2 · T (2k−1 ) + c 2k ≤ 2 · c k 2k−1 + c 2k = c(k + 1)2k .

Damit ist die Behauptung gezeigt, und wir erhalten für alle n ∈ N mit k := log2 n <
1 + log2 n:

T (n) ≤ T (2k ) ≤ c(k + 1)2k < 2c(2 + log2 n)n = O(n log n).
Mergesort wurde bereits 1936 in Hardware und 1945 von John von Neumann als einer der
ersten Algorithmen überhaupt in Software implementiert [23]. Auch Mergesort ist gewis-
sermaßen bestmöglich, nämlich im folgenden Sinne:

Satz 8.10 Selbst wenn man die Eingaben auf total geordnete Mengen beschränkt, benötigt
jeder Sortieralgorithmus, der Informationen über die Ordnung nur durch paarweise Ver-
gleiche von zwei Elementen gewinnt, zum Sortieren einer n-elementigen Menge mindestens
log2 (n!) Vergleiche.

Beweis Wir betrachten Eingabemengen S = {1, . . . , n}, die einer totalen Ordnung unter-
liegen. Gesucht ist eine der n! Permutationen auf S. Ein Vergleich „a < b ?“ von zwei
Elementen a und b liefert für einen Teil der Permutationen „wahr“, für einen anderen Teil
„falsch“. Der größere dieser beiden Teile enthält mindestens n!
2 viele Permutationen. Jeder
weitere Vergleich halbiert den größeren der beiden Teile höchstens. Nach k Vergleichen gibt
es also noch mindestens 2n!k viele Permutationen, die vom Algorithmus nicht unterschieden
werden können. Jeder Sortieralgorithmus benötigt also mindestens k Vergleiche mit 2k ≥ n!,
das heißt k ≥ log2 (n!). 

Bemerkung 8.11 Es gilt n! ≥  n2 n/2 , also log2 (n!) ≥ n/2 log n2  = (n log(n)).
Somit ist Mergesort ein asymptotisch optimales Sortierverfahren. Allerdings benötigt Mer-
gesort im Allgemeinen mehr als log2 (n!) Vergleiche. Man kann beweisen, dass es keinen
Sortieralgorithmus gibt, der stets mit log2 (n!) Vergleichen auskommt. Beispielsweise gibt
es keinen Sortieralgorithmus, der 12 Elemente mit nur 29 = log2 (12!) Vergleichen sor-
tiert. Für jeden Algorithmus gibt es Instanzen mit 12 Elementen, für die er mindestens 30
Vergleiche benötigt [23]. Mergesort braucht zum Sortieren einer 12-elementigen Menge
mindestens 20 und höchstens 33 Vergleiche.

8.5 Quicksort

Quicksort, 1960 von Antony Hoare vorgeschlagen, basiert ebenfalls auf dem „Divide-and-
Conquer“-Prinzip, spart jedoch den Aufwand des Merge-Schrittes, den Mergesort benötigt:
8.5 Quicksort 113

Algorithmus 8.12 (Quicksort)


Eingabe: eine Menge S = {s1 , . . . , sn }; eine durch Schlüssel induzierte partielle Ordnung 
auf S (durch ein Orakel gegeben).
Ausgabe: eine Bijektion f : {1, . . . , n} → S mit f ( j)  f (i) für alle 1 ≤ i < j ≤ n.

if |S| > 1
then wähle x ∈ S beliebig
S1 := {s ∈ S | s  x} \ {x}
S2 := {s ∈ S | x  s} \ {x}
Sx := S \ (S1 ∪ S2 )
Sortiere S1 und S2 (soweit nicht leer) rekursiv mit Quicksort
Füge die sortierten Mengen S1 , Sx und S2 aneinander

Ein Vorteil von Quicksort gegenüber Mergesort beim Sortieren von Daten in einem Array
(oder vector) ist, dass kein zusätzlicher temporärer Speicherplatz benötigt wird. Ande-
rerseits können wir nur folgende, schlechtere Laufzeitschranke beweisen:

Satz 8.13 Der Quicksort-Algorithmus 8.12 ist korrekt und hat Laufzeit O(n 2 ).

Beweis Die Korrektheit folgt sofort aus der Erkenntnis, dass S1 = {s ∈ S | k(s) < k(x)}
und S2 = {s ∈ S | k(s) > k(x)} ist, wobei k(s) wieder den Schlüssel von s bezeichnet.
Sei T (n) die Laufzeit von Quicksort für n Elemente. Dann gibt es offenbar ein c ∈ N
mit T (1) ≤ c und

T (n) ≤ c n + max (T (l) + T (n − 1 − l)) für alle n ≥ 2,


0≤l≤n−1

wobei T (0) := 0 sei (l bezeichnet hier die Kardinalität von S1 ).

Behauptung T (n) ≤ c n 2 .
Der Beweis erfolgt per Induktion über n; der Induktionsanfang n = 1 ist trivial. Für den
Induktionsschritt berechnen wir:

T (n) ≤ c n + max (T (l) + T (n − 1 − l))


0≤l≤n−1

≤ c n + max (c l 2 + c (n − 1 − l)2 )
0≤l≤n−1

= c n + c (n − 1)2
< c n2.


114 8 Sortieralgorithmen

Bemerkung 8.14 Die Aussage des Satzes ist bestmöglich, da die Laufzeit (n 2 ) ist, falls
die Menge bereits sortiert ist und als x immer das erste Element ausgewählt wird.
Natürlich kann man x auch anders wählen, z. B. zufällig. Idealerweise ist x ein Median
von S, d. h. |S1 | ≤ |S| |S|
2 und |S2 | ≤ 2 . Man kann einen Median einer n-elementigen Menge
in O(n) Zeit finden, aber der Aufwand dafür ist nicht so gering, dass sich das immer lohnte,
obwohl sich dadurch die Laufzeit auf O(n log n) verringerte.

Der Hauptgrund für das Interesse am Quicksort-Algorithmus (mit einer simplen Wahl von
x) ist seine meist exzellente Laufzeit in der Praxis. Wir können das theoretisch teilweise
erklären, indem wir seine erwartete Laufzeit betrachten, wenn jede Wahl von x unabhängig
voneinander erfolgt und jeweils jedes Element mit gleicher Wahrscheinlichkeit gewählt
wird. Wir haben dann einen randomisierten Algorithmus, den wir Random-Quicksort nennen
wollen. Den Erwartungswert seiner Laufzeit bezeichnen wir mit T (n).

Satz 8.15 Der Erwartungswert T (n) der Laufzeit von Random-Quicksort mit n Elementen
ist O(n log n).

Beweis Es gibt ein c ∈ N mit T (1) ≤ c und

1  2 
n n−1

T (n) ≤ c n + T (i − 1) + T (n − i) = c n + T (i) ,
n n
i=1 i=1

für n ≥ 2, wobei T (0) := 0, denn für i = 1, . . . , n wählen wir als x mit Wahrscheinlichkeit
1
n das in einer festen sortierten Reihenfolge an i-ter Stelle stehende Element aus.

Behauptung T (n) < 2cn(1 + ln n).


Wir zeigen dies per Induktion über n. Für n = 1 ist die Aussage trivial. Für n ≥ 2 erhalten
wir mit der Induktionsvoraussetzung:

2 
n−1
T (n) ≤ c n + 2ci(1 + ln i)
n
i=1

2c n
≤ cn + 2x(1 + ln x) dx
n 1
 n
2c x2
= cn + x 2 (1 + ln x) −
n 2 1
c
= c n + 2cn(1 + ln n) − cn −
n
< 2cn(1 + ln n) 
8.6 Binäre Heaps und Heapsort 115

8.6 Binäre Heaps und Heapsort

Eine Prioritätswarteschlange ist eine Datenstruktur zum Speichern von Elementen mit
Schlüsseln, die mindestens folgende Funktionen bereitstellt:

• init: Einrichten einer leeren Warteschlange (Konstruktor);


• insert (s, k(s)): Einfügen des Elements s mit Schlüssel k(s) in die Warteschlange;
• s = extract_min: Entfernen eines Elements s mit kleinstem Schlüssel und Rückgabe
von s;
• clear: Löschen der Warteschlange (Destruktor).

Nutzt man als Datenstruktur ein Array oder eine Liste, so benötigt insert O(1) Lauf-
zeit und extract_min O(n) Laufzeit, falls n Elemente in der Prioritätswarteschlange
sind. Nutzt man ein sortiertes Array oder eine sortierte Liste, so benötigt insert O(n)
und extract_min O(1) Laufzeit. Binäre Heaps erlauben, sowohl insert als auch
extract_min in O(log n) Zeit durchzuführen.
Manchmal werden noch weitere Operationen benötigt, vor allem:

• decrease_key (s, k(s)): Verringern des Schlüssels k(s) von s;


• s = find_min: Rückgabe eines Elements s mit kleinstem Schlüssel (ohne es zu ent-
fernen);
• remove (s): Entfernen des Elements s aus der Warteschlange.

Definition 8.16 Ein Heap für eine endliche Menge S mit Schlüsseln k : S → K und einer
totalen Ordnung ≤ von K ist eine Arboreszenz A mit einer Bijektion f : V (A) → S, so
dass die Heapordnung

k( f (v)) ≤ k( f (w)) f üralle (v, w) ∈ E(A) (8.1)

gilt.

Abb. 8.1 zeigt ein Beispiel. Die Funktion find_min liefert einfach f (r ), wobei r die
Wurzel der Arboreszenz ist. Um die anderen Funktionen effizient implementieren zu kön-
nen, beschränken wir uns auf bestimmte, sehr einfach darzustellende Arboreszenzen wie in
Abb. 8.1 für n = 10 gezeigt:

Proposition 8.17 Sei n ∈ N. Dann ist der Graph Bn mit V (Bn ) = {0, . . . , n − 1} und
E(Bn ) = {(i, j) | i ∈ V (Bn ), j ∈ {2i + 1, 2i + 2} ∩ V (Bn )} eine Arboreszenz mit Wurzel
0. Kein Weg in Bn ist länger als log2 n.
116 8 Sortieralgorithmen

Abb. 8.1 Ein Binärheap für


3
eine Menge mit zehn
0
Elementen, deren Schlüssel
(blau) 3, 6, 8, 9, 10, 11, 12, 12, 6 9
13, 15 sind 1 2

12 8 13 10
3 4 5 6
15 12 11
7 8 9

Beweis Offenbar ist Bn azyklisch mit δ − ( j) = {( j−1


2 , j)} für j ∈ {1, . . . , n − 1}, also
Arboreszenz mit Wurzel 0. Für jeden Weg mit Knoten v0 , . . . , vk gilt vi+1 + 1 ≥ 2(vi + 1),
also n ≥ vk + 1 ≥ 2k . 

Arboreszenzen diesen Typs heißen auch vollständige Binärbäume. Ein Heap (B, f ) für S
heißt Binärheap, wenn B = B|S| .
Programm 8.18 zeigt eine Implementierung mit allen oben genannten Funktionen. Beim
Einfügen eines Elements in einen Heap mit bislang n Elementen wird es zunächst dem
Knoten n zugeordnet, und dann dieser Knoten mit seinem Vorgänger vertauscht, solange
das neue Element einen kleineren Schlüssel hat (sift_up). Beim Löschen eines Elements
aus einem Heap mit n Elementen wird zunächst das am Knoten n − 1 stehende Element
an den frei gewordenen Knoten kopiert und dann sift_up oder sift_down angewandt,
um die Heapordnung wiederherzustellen. Siehe Abb. 8.2.

Programm 8.18 (Heap)


1 // heap . h ( B i n a r y H e a p )
2
3 # i n c l u d e < vector >
4 # i n c l u d e < stdexcept >
5
6 template < typename T> // a s s u m e t h a t T has the < o p e r a t o r
7 c l a s s Heap {
8 public :
9 bool i s _ e m p t y () c o n s t
10 {
11 r e t u r n _data . size () == 0;
12 }
13
14 c o n s t T & f i n d _ m i n () c o n s t
15 {
16 if ( i s _ e m p t y ()) {
17 t h r o w std :: r u n t i m e _ e r r o r ( " Empty heap ; Heap :: f i n d _ m i n f a i l e d . " );
18 }
19 r e t u r n _data [0];
20 }
21
22 T e x t r a c t _ m i n ()
23 {
8.6 Binäre Heaps und Heapsort 117

a 3 b 3
0 0

6 9 6 9
1 2 1 2

12 8 13 10 12 5 13 10
3 4 5 6 3 4 5 6
15 12 11 5 15 12 11 8
7 8 9 10 7 8 9 10

c 3 d 8
0 0
5 9 5 9
1 2 1 2

12 6 13 10 12 6 13 10
3 4 5 6 3 4 5 6
15 12 11 8 15 12 11
7 8 9 10 7 8 9

e 5 f 5
0 0
8 9 6 9
1 2 1 2

12 6 13 10 12 8 13 10
3 4 5 6 3 4 5 6
15 12 11 15 12 11
7 8 9 7 8 9

Abb. 8.2 In den in Abb. 8.1 gezeigten Heap wird ein Element mit Schlüssel 5 eingefügt: a zunächst
wird es in den Knoten 10 geschrieben; b dann werden die Inhalte der Knoten 4 und 10 vertauscht;
c eine weitere Vertauschung der Inhalte der Knoten 1 und 4 stellt dann die Heapordnung wieder her.
Wird dann das Element an der Wurzel gelöscht, so wird zunächst (d) der Inhalt des Knotens 10 an
die Wurzel geschrieben, und dieser dann (e), (f) nach unten durchgereicht

24 T r e s u l t = f i n d _ m i n ();
25 remove (0);
26 return result ;
27 }
28
29 int i n s e r t ( c o n s t T & o b j e c t )
30 {
31 _ d a t a . p u s h _ b a c k ( o b j e c t );
32 s i f t _ u p ( _ d a t a . size () - 1);
33 r e t u r n _data . size () - 1;
34 }
35
36 protected : // a c c e s s i b l e only for d e r i v e d c l a s s e s
37 void r e m o v e ( int i n d e x )
38 {
39 e n s u r e _ i s _ v a l i d _ i n d e x ( i n d e x );
40 swap ( _data [ index ] , _data [ _data . size () - 1]);
118 8 Sortieralgorithmen

41 _ d a t a . p o p _ b a c k ();
42 s i f t _ u p ( i n d e x );
43 s i f t _ d o w n ( i n d e x );
44 }
45
46 void d e c r e a s e _ k e y ( int i n d e x )
47 {
48 e n s u r e _ i s _ v a l i d _ i n d e x ( i n d e x );
49 s i f t _ u p ( i n d e x );
50 }
51
52 v i r t u a l void swap ( T & a , T & b ) // v i r t u a l f u n c t i o n s can be
53 { // o v e r r i d d e n by d e r i v e d c l a s s e s
54 std :: swap (a , b );
55 }
56
57 T & g e t _ o b j e c t ( int i n d e x )
58 {
59 e n s u r e _ i s _ v a l i d _ i n d e x ( i n d e x );
60 r e t u r n _ d a t a [ i n d e x ];
61 }
62
63 private :
64 void e n s u r e _ i s _ v a l i d _ i n d e x ( int i n d e x )
65 {
66 if ( i n d e x >= s t a t i c _ c a s t < int >( _data . size ()) or i n d e x < 0)
67 t h r o w std :: r u n t i m e _ e r r o r ( " Index error in heap o p e r a t i o n " );
68 }
69
70 s t a t i c int p a r e n t ( int i n d e x ) // do not call with index ==0!
71 {
72 r e t u r n ( i n d e x - 1) / 2;
73 }
74
75 s t a t i c int left ( int i n d e x ) // left child may not exist !
76 {
77 r e t u r n (2 * i n d e x ) + 1;
78 }
79
80 s t a t i c int r i g h t ( int i n d e x ) // r i g h t c h i l d may not e x i s t !
81 {
82 r e t u r n (2 * i n d e x ) + 2;
83 }
84
85 void s i f t _ u p ( int i n d e x )
86 {
87 w h i l e (( i n d e x > 0) and ( _ d a t a [ i n d e x ] < _ d a t a [ p a r e n t ( i n d e x ) ] ) ) {
88 swap ( _data [ index ] , _data [ parent ( index )]);
89 i n d e x = p a r e n t ( i n d e x );
90 }
91 }
92
93 void s i f t _ d o w n ( int i n d e x )
94 {
95 int s m a l l e s t = i n d e x ;
96 w h i l e ( true ) {
97 if (( left ( index ) < s t a t i c _ c a s t < int >( _data . size ())) and
98 ( _data [ left ( index )] < _data [ s m a l l e s t ]))
99 {
100 s m a l l e s t = left ( index );
101 }
102 if (( r i g h t ( i n d e x ) < s t a t i c _ c a s t < int >( _data . size ())) and
8.6 Binäre Heaps und Heapsort 119

103 ( _ d a t a [ r i g h t ( i n d e x )] < _ d a t a [ s m a l l e s t ]))


104 {
105 s m a l l e s t = r i g h t ( i n d e x );
106 }
107 if ( i n d e x == s m a l l e s t ) r e t u r n ;
108 swap ( _data [ s m a l l e s t ] , _ d a t a [ i n d e x ]);
109 index = smallest ;
110 }
111 }
112
113 std :: vector <T > _ d a t a ; // h o l d s the o b j e c t s in heap order
114 };

Satz 8.19 Die Funktionen der Klasse Heap in Programm 8.18 sind korrekt. Die Laufzeit
jeder Funktion ist durch O(log n) beschränkt, wenn der Heap n Elemente enthält.

Beweis Die Laufzeit folgt sofort aus Proposition 8.17. Für die Korrektheit ist folgendes zu
zeigen:

Behauptung Wenn ein Binärbaum (Bn , f ) für (S, k) die Heapeigenschaft hat und dann
k( f (i)) verringert bzw. erhöht wird für ein i ∈ {0, 1 . . . , n − 1}, dann stellt sift_up(i)
bzw. sift_down(i) die Heapeigenschaft wieder her.
Falls k( f (i)) verringert wird, so ist höchstens für die Kante in δ − (i) die Heapeigenschaft
verletzt. Diese wird von sift_up aber wiederhergestellt, wobei in jeder Iteration höchstens
für die Kante (parent(index), index) die Heapeigenschaft verletzt ist.
Analog ist, falls k( f (i)) erhöht wird, höchstens für die Kanten in δ + (i) die Heap-
eigenschaft verletzt, und wird von sift_down wiederhergestellt. 

Die Funktionen remove und decrease_key können nur sinnvoll aufgerufen werden,
wenn man die Nummer des Knotens im Heap kennt, in dem das betreffende Objekt ge-
speichert ist. Dies wird aber in der Regel nicht der Fall sein, weil sich das ja auch ständig
durch swap ändert. Wir werden deshalb bald noch eine von Heap abgeleitete Klasse vor-
stellen, die sich zusätzlich Knotennummern merkt und diese bei swap mit vertauscht. Für
die Kernfunktionen insert und extract_min ist das aber unnötig.
Mit Hilfe von Binärheaps erhalten wir einen weiteren Algorithmus für das Sortieren
mit Schlüsseln, der Laufzeit O(n log n) hat. Wie in Programm 8.20 anhand eines Beispiels
gezeigt, werden zunächst die Objekte in einen Heap einsortiert und dann sukzessive durch
extract_min wieder herausgeholt. Dieser Algorithmus wird Heapsort genannt.

Programm 8.20 (Heapsort)


1 // h e a p s o r t . cpp ( E x a m p l e for H e a p s o r t )
2
3 # i n c l u d e < iostream >
4 # i n c l u d e < cstdlib >
5 # i n c l u d e " heap . h "
120 8 Sortieralgorithmen

6
7 int main ()
8 {
9 int n ;
10 std :: cout << " How many n u m b e r s do you want to sort ? " ;
11 std :: cin >> n ;
12
13 Heap < int > heap ;
14 int n e w _ n u m b e r ;
15
16 std :: cout << " I will sort the f o l l o w i n g " << n << " n u m b e r s :\ n " ;
17 for ( int i = 0; i < n ; ++ i ) {
18 n e w _ n u m b e r = rand () % 900 + 100;
19 std :: cout << n e w _ n u m b e r << " " ;
20 h e a p . i n s e r t ( n e w _ n u m b e r );
21 }
22
23 std :: cout << " \ n " << " S o r t e d :\ n " ;
24 w h i l e ( not ( heap . i s _ e m p t y ())) {
25 std :: cout << heap . e x t r a c t _ m i n () << " " ;
26 }
27 std :: cout << " \ n " ;
28 }

Satz 8.21 Heapsort sortiert n Elemente in der Laufzeit O(n log n).

Beweis Folgt aus Satz 8.19. 

8.7 Weitere Datenstrukturen

Es gibt auch noch mächtigere Datenstrukturen, mit denen man zusätzlich in O(log n) Zeit
ein Element mit einem bestimmten Schlüssel finden kann (was die oben angekündigte In-
dizierung erspart), binäre Suche durchführen kann, sowie zu jedem Element den Vorgänger
und Nachfolger bzgl. einer den Schlüsseln entsprechenden Reihenfolge finden kann. Solche
balancierten Suchbäume (geläufige Namen sind search tree, AVL-tree, red-black tree) sind
ebenso wie Heaps auch in der C++-Standardbibliothek implementiert und heißen dort map.
Eine weitere nützliche Datenstruktur bilden Hash-Tables (in C++ unordered_map).
Zum Speichern einer Menge S ⊆ U wählt man hier eine Funktion g : U → {0, . . . , k−1}, so
dass meistens wenige Elemente denselben Funktionswert haben. Für jedes i = 0, . . . , k − 1
werden die (meist wenigen) Elemente von g −1 (i) in einer anderen Datenstruktur gespeichert,
beispielsweise in einer Liste. Bei der Suche nach einem Element u ∈ U muss man dann nur
g(u) ausrechnen und eine kurze Liste durchsuchen.
Die Bücher [34] und [8] enthalten weitere Informationen zu Datenstrukturen.
8.8 Übungsaufgaben 121

8.8 Übungsaufgaben

1. Wir wollen Binärstrings (d. h. Wörter über {0, 1}) lexikographisch sortieren. Wenn s und
t zwei Binärstrings der Länge m sind, dann heißt s lexikographisch kleiner als t, wenn
es einen Index j ∈ {1, . . . , m} gibt, so dass s und t an den ersten j − 1 Stellen identisch
sind, s an der Stelle j den Wert 0 hat und t an der Stelle j den Wert 1 hat. Zeigen
Sie, dass man n Binärstrings der Länge m in Zeit O(mn) (also in linearer Laufzeit)
lexikographisch sortieren kann.
2. Die Funktion sort1 ist nicht exakt eine Implementierung von Algorithmus 8.3. Was
ist der Unterschied? Beweisen Sie, dass die Funktion dennoch korrekt funktioniert.
3. Es sei S eine endliche Menge mit einer partiellen Ordnung „“. Beweisen Sie, dass
dann folgende Aussagen äquivalent sind:
(a)  ist durch Schlüssel induziert;
(b) (a  b ∧ b  c ∧ a = c) ⇒ a  c für alle a, b, c ∈ S;
(c) {(x, y) ∈ S × S | x = y ∨ (x  y ∧ y  x)} ist eine Äquivalenzrelation.
4. Es sei α < 2. Zeigen Sie, dass die Laufzeit der Algorithmen „Sortieren durch Einfügen“
und „Bubblesort“ nicht O(n α ) ist, wobei n die Anzahl der zu sortierenden Elemente
ist.
5. Zu einer gegebenen Folge x1 , . . . , xn ∈ Z soll festgestellt werden, ob es i, j, k ∈
{1, . . . , n} gibt mit xi + x j + xk = 0. Zeigen Sie, wie man dieses Problem in O(n 2 )
Zeit lösen kann.
6. Modifizieren Sie den Algorithmus Mergesort so, dass die gegebene Menge S nicht mehr
in zwei, sondern in b (mit 3 ≤ b ≤ n) Teilmengen S1 , . . . , Sb aufgeteilt wird. Diese
Teilmengen sollen natürlich möglichst ähnliche Größen haben, d. h. je zwei von ihnen
sollen sich in ihren Größen höchstens um 1 unterscheiden. Die Mengen S1 , . . . , Sb
werden dann rekursiv sortiert und die sortierten Teilmengen anschließend zu einer
sortierten Gesamtmenge verschmolzen.
(a) Zeigen Sie, dass das Verschmelzen der Teillösungen in Zeit O(n log b) möglich ist.
(b) Führen Sie eine asymptotische Laufzeitanalyse des Verfahrens durch.
7. Bestimmen Sie die maximale Anzahl der Vergleiche, die bei Mergesort bzw. Quick-
sort benötigt werden, um fünf Elemente a1 , . . . , a5 zu sortieren. Vergleichen Sie die
Resultate mit der unteren Schranke für das Sortieren von fünf Elementen.
8. Implementieren Sie die Algorithmen Mergesort und Quicksort. Vergleichen Sie die
durchschnittliche und die maximale Laufzeit beider Implementierungen auf 105 zufällig
erzeugten Listen von jeweils 105 double Werten aus dem Intervall [0, 1].
9. Es seien k, n ∈ N mit k ≤ n. Der k-Median einer n-elementigen Menge M =
{a1 , . . . , an } von Zahlen ist dann die Zahl a ∈ M, so dass |{b ∈ M | b < a}| = k − 1
und |{b ∈ M | b > a}| = n − k. Betrachten Sie folgendes Verfahren, um den k-Median
von M = {a1 , . . . , an } zu bestimmen:
Falls n = 1, dann gebe man a1 zurück. Andernfalls sei A := {a ∈ M | a < a1 },
falls {a ∈ M | a < a1 } = ∅, und A := {a1 } sonst. Außerdem sei B := M \ A. Falls
122 8 Sortieralgorithmen

|A| ≥ k, so gebe man den (rekursiv berechneten) k-Median von A zurück. Sonst gebe
man den (ebenfalls rekursiv berechneten) (k − |A|)-Median von B zurück.
Man zeige, dass dieses Verfahren korrekt arbeitet und bestimme die durchschnittliche
Laufzeit über alle möglichen Permutationen von a1 , . . . , an betrachtet.
10. Es sei G ein Graph mit parallelen Kanten. Zeigen Sie, dass man in O(m) Zeit einen
maximalen Teilgraphen von G berechnen kann, der einfach ist, wobei m die Anzahl
der Kanten in G ist.
11. Es seien n Elemente mit Schlüsseln gegeben. Zeigen Sie, dass man in Zeit O(n) einen
Binärheap für diese Elemente aufbauen kann.
12. Zeigen Sie dass der Algorithmus Heapsort (n log n) Laufzeit benötigt, um n paarweise
verschiedene Elemente zu sortieren.
Optimale Bäume und Wege
9

In der Kombinatorischen Optimierung sucht man in einer endlichen Menge von Objekten
mit einer kombinatorischen Struktur ein optimales Element. Die Objekte (d. h., die zulässi-
gen Lösungen) können meist als Teilmengen einer endlichen Grundmenge U repräsentiert
werden. Oft ist U die Kantenmenge eines Graphen. Die Objekte können dann z. B. s-t-Wege
(für gegebene Knoten s und t) oder aufspannende Bäume sein; diese beiden Fälle werden
wir gleich betrachten. Hat man eine Gewichtsfunktion c : U → R, so heißt eine zulässige

Lösung X ⊆ U optimal, wenn ihr Gewicht (man sagt auch: ihre Kosten) c(X ) := u∈X c(u)
minimal ist.
Die in Kap. 6 vorgestellte Klasse Graph (Programm 6.29) ist auch für gewichtete Gra-
phen ausgelegt. Die Funktion add_edge hat ein optionales drittes Argument, mit dem das
Kantengewicht angegeben werden kann.
Sei G ein Graph mit Kantengewichten c : E(G) → R. Für einen Teilgraphen H von G

bezeichnen wir mit c(E(H )) = e∈E(H ) c(e) das Gewicht von H ; manchmal spricht man
auch von Kosten oder Länge.

9.1 Optimale aufspannende Bäume

Wir betrachten zunächst das folgende kombinatorische Optimierungsproblem.

Berechnungsproblem 9.1 (Minimum-Spanning-Tree-Problem)


Eingabe: ein zusammenhängender ungerichteter Graph G mit Kantengewichten c : E(G)
→ R.
Aufgabe: Berechne einen aufspannenden Baum in G mit minimalem Gewicht.

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 123
S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
124 9 Optimale Bäume und Wege

Eine optimale Lösung wird oft MST für minimum spanning tree genannt. Entsprechend
wird Problem 9.1 auch als MST-Problem bezeichnet. Folgender einfacher Algorithmus geht
auf Kruskal [25] zurück und liefert stets eine optimale Lösung. Er wird auch als Greedy-
Algorithmus bezeichnet, weil er „gierig“ immer die billigstmögliche Kante hinzunimmt.

Algorithmus 9.2 (Kruskals Algorithmus)


Eingabe: ein zusammenhängender ungerichteter Graph G mit Kantengewichten
c : E(G) → R.
Ausgabe: ein aufspannender Baum (V (G), T ) in G mit minimalem Gewicht.

Sortiere E(G) = {e1 , . . . , em }, sodass c(e1 ) ≤ c(e2 ) ≤ · · · ≤ c(em )


T ←∅
for i ← 1 to m do
if (V (G), T ∪ {ei }) ist Wald then T ← T ∪ {ei }

Satz 9.3 Kruskals Algorithmus 9.2 ist korrekt.

Beweis Der Graph (V (G), T ), den Kruskals Algorithmus berechnet, ist offensichtlich ein
aufspannender Wald. Angenommen, (V (G), T ) wäre nicht zusammenhängend. Dann gäbe
es gemäß Satz 6.13 ein ∅ = X ⊂ V (G) mit δ(X ) ∩ T = ∅. Da G zusammenhängend ist,
muss es aber in G eine Kante e ∈ δ(X ) geben. Diese hätte dann aber zu T hinzugefügt
werden müssen, als sie an der Reihe war.
Das Ergebnis (V (G), T ) von Kruskals Algorithmus ist also stets ein aufspannender
Baum. Wir zeigen nun die Optimalität. Sei (V (G), T ∗ ) ein aufspannender Baum minimalen
Gewichts, und zwar so gewählt, dass |T ∗ ∩ T | maximal ist.
Angenommen, T ∗ = T (sonst ist T optimal). Da |T | = |T ∗ | = |V (G)| − 1 (nach
Satz 6.18), ist dann T ∗ \ T = ∅. Sei j ∈ {1, . . . , m} der kleinste Index mit e j ∈ T ∗ \ T.
Da Kruskals Algorithmus e j nicht gewählt hat, muss es einen Kreis C geben mit E(C) ⊆
{e j } ∪ (T ∩ {e1 , . . . , e j−1 }). Natürlich ist e j ∈ E(C).
(V (G), T ∗ \{e j }) ist unzusammenhängend, d. h. es gibt ein X ⊂ V (G) mit δG (X )∩T ∗ =
{e j }. Nun ist |E(C) ∩ δG (X )| gerade (das gilt für jeden Kreis und jedes X ), also mindestens
zwei. Sei ei ∈ (E(C) ∩ δG (X )) \ {e j }. Man beachte i < j und somit c(ei ) ≤ c(e j ).
Sei T ∗∗ := (T ∗ \ {e j }) ∪ {ei }. Dann ist (V (G), T ∗∗ ) ein aufspannender Baum mit
c(T ∗∗ ) = c(T ∗ ) − c(e j ) + c(ei ) ≤ c(T ∗ ), also auch optimal. T ∗∗ hat aber eine Kante mehr
(nämlich ei ) mit T gemeinsam als T ∗ , im Widerspruch zur Wahl von T ∗ . 
9.1 Optimale aufspannende Bäume 125

Für die meisten kombinatorischen Optimierungsprobleme liefert der Greedy-Algorith-


mus im Allgemeinen keine optimale Lösung; vielmehr kann das Ergebnis sehr schlecht sein.
Für das Minimum-Spanning-Tree-Problem eignet er sich jedoch gut. Er ist auch recht
schnell, wenn man sich bei der Implementierung Mühe gibt:

Satz 9.4 Kruskals Algorithmus 9.2 kann mit Laufzeit O(m log n) implementiert werden,
wobei n = |V (G)| und m = |E(G)|.

Beweis Wir können m ≤ n 2 annehmen, denn andernfalls entfernen wir vorab parallele
Kanten (und behalten für jedes Knotenpaar nur jeweils die billigste); dies geht mit Bucketsort
in O(m + n 2 ) Zeit.
Das Sortieren der m Kanten geht nach Kap. 8 dann in O(m log m) = O(m log n) Zeit.
Die for-Schleife wird m-mal durchlaufen. Um schnell testen zu können, ob (V (G), T ∪
{ei }) ein Wald ist, merken wir uns die Zusammenhangskomponenten von (V (G), T ) wie
folgt. Jeder Zusammenhangskomponente weisen wir einen Index zu, und merken uns die
Anzahl ihrer Knoten. Zusätzlich speichern wir an jedem Knoten den Index der Zusammen-
hangskomponente, in der er liegt. Dann kann man in O(1) Zeit testen, ob (V (G), T ∪ {ei })
ein Wald ist, da dazu nur getestet werden muss, ob die Endknoten von ei in unterschiedlichen
Zusammenhangskomponenten liegen. Fügt man ei hinzu, so müssen zwei Zusammenhangs-
komponenten vereinigt werden. Dazu addiert man die beiden Knotenanzahlen und läuft mit
Algorithmus 7.1 vor dem Hinzufügen von ei über die Knoten der kleineren Komponente und
weist diesen den Index der größeren Komponente zu. Da ein Knoten maximal log2 n mal
die Komponente wechseln kann (denn ihre Größe verdoppelt sich jedes Mal mindestens),
beträgt die Gesamtlaufzeit für die for-Schleife O(m log n). 

Wir geben nun einen zweiten Beweis für die Korrektheit. Für eine Instanz (G, c) des MST-
Problems heiße eine Kantenmenge F gut, wenn es einen gewichtsminimalen aufspannenden
Baum (V (G), T ) gibt mit F ⊆ T . Dann gilt:

Lemma 9.5 Sei (G, c) eine Instanz des MST-Problems, F ⊂ E(G) gut, und e ∈ E(G) \ F.
Die Menge F ∪ {e} ist genau dann gut, wenn ein X ⊂ V (G) existiert mit δG (X ) ∩ F = ∅,
e ∈ δG (X ), und c(e) ≤ c( f ) für alle f ∈ δG (X ).

Beweis
„⇒“: Ist F ∪ {e} gut, dann sei (V (G), T ) ein gewichtsminimaler aufspannender Baum
mit F ∪ {e} ⊆ T . Dann ist (V (G), T \ {e}) unzusammenhängend; sei also X die
Knotenmenge einer Zusammenhangskomponente, d. h. δG (X ) ∩ T = {e}. Gäbe es
ein f ∈ δG (X ) mit c( f ) < c(e), so wäre (V (G), (T \ {e}) ∪ { f }) ein aufspannender
Baum mit geringerem Gewicht, im Widerspruch zur Optimalität.
126 9 Optimale Bäume und Wege

„⇐“: Sei umgekehrt X ⊂ V (G) mit δG (X ) ∩ F = ∅, e ∈ δG (X ), und c(e) ≤ c( f ) für alle


f ∈ δG (X ). Da F gut ist, gibt es einen gewichtsminimalen aufspannenden Baum
(V (G), T ) mit F ⊆ T. Falls e ∈ T, so ist F ∪ {e} gut, und wir sind fertig. Sonst
enthält (V (G), T ∪ {e}) einen Kreis, und dieser enthält neben e eine weitere Kante
f ∈ δG (X ). Sei T  := (T \ { f }) ∪ {e}. Dann ist (V (G), T  ) ein aufspannender
Baum, der wegen c(T  ) = c(T ) − c( f ) + c(e) ≤ c(T ) ebenfalls gewichtsminimal
ist. Ferner ist F ∪ {e} ⊆ T  (denn f ∈
/ F), also ist F ∪ {e} gut.


Dieses Lemma impliziert sofort die Korrektheit von Kruskals Algorithmus, denn T ist zu
jedem Zeitpunkt des Algorithmus gut.
Das Lemma zeigt auch eine andere Möglichkeit auf, das MST-Problem zu lösen. Wir
stellen nun einen weiteren bekannten Algorithmus vor, der schon 1930 von Jarník [21]
gefunden wurde, heute aber nach seinem Wiederentdecker Prim [30] benannt ist.

Algorithmus 9.6 (Prims Algorithmus)


Eingabe: ein zusammenhängender ungerichteter Graph G mit Kantengewichten c : E(G)
→ R.
Ausgabe: ein aufspannender Baum (V (G), T ) in G mit minimalem Gewicht.

Wähle v ∈ V (G) beliebig


X ← {v}
T ←∅
while X = V (G) do
Wähle ein e = {x, y} ∈ δG (X )mit kleinstem Gewicht; sei x ∈ X und y ∈
/ X
T ← T ∪ {e}
X ← X ∪ {y}

Satz 9.7 Prims Algorithmus 9.6 ist korrekt. Mithilfe von Binärheaps kann er so implemen-
tiert werden, dass seine Laufzeit O(m log n) ist, wobei n = |V (G)| und m = |E(G)|.

Beweis Zu jedem Zeitpunkt von Prims Algorithmus ist (X, T ) offenbar ein Baum und T
nach Lemma 9.5 gut, also am Ende ein aufspannender Baum minimalen Gewichts.
Um die behauptete Laufzeit zu erreichen, verwalten wir stets die Knoten v ∈ V (G)\X mit
δ(v)∩δ(X ) = ∅ in einem Heap, wobei v den Schlüssel min{c(e) | e ∈ δ(v)∩δ(X )} trägt. Die
Laufzeit wird dann von n insert-, n extract_min- und höchstens m decrease_key-
Operationen dominiert (vgl. Programm 9.8). Da n ≤ m + 1 ist, folgt die Behauptung mit
Satz 8.19. 
9.2 Implementierung von Prims Algorithmus 127

9.2 Implementierung von Prims Algorithmus

Wir zeigen nun eine Implementierung von Prims Algorithmus und zugleich eine fast
identische Implementierung des alsbald vorzustellenden Kürzeste-Wege-Algorithmus von
Dijkstra. Dabei werden sowohl die Klasse Graph (Programm 6.29) benutzt, als auch ein
von Heap<HeapItem> (vgl. Programm 8.18) abgeleiteter NodeHeap, der Knoten des
Graphen mit Schlüsseln speichert. Man beachte die zusätzlich gespeicherten Daten und die
swap-Funktion, die diejenige in Heap überschreibt. Im NodeHeap wird diese neue swap-
Funktion auch von den geerbten Funktionen sift_up und sift_down aufgerufen; dies
ist möglich, weil die in Heap bereits vorhandene swap-Funktion ein identisches Interface
hat und als virtual deklariert war. Damit können nun auch die Operationen remove und
insbesondere decrease_key benutzt werden.

Programm 9.8 (Prims Algorithmus und Dijkstras Algorithmus)


1 // p r i m d i j k s t r a . cpp ( Prim ’s A l g o r i t h m and D i j k s t r a ’ s A l g o r i t h m )
2
3 # include " graph .h"
4 # i n c l u d e " heap . h "
5
6 struct HeapItem
7 {
8 H e a p I t e m ( G r a p h :: N o d e I d nodeid , d o u b l e key ): _ n o d e i d ( n o d e i d ) , _key ( key ) {}
9 G r a p h :: N o d e I d _ n o d e i d ;
10 d o u b l e _key ;
11 };
12
13 bool operator <( c o n s t H e a p I t e m & a , c o n s t H e a p I t e m & b )
14 {
15 r e t u r n ( a . _key < b . _key );
16 }
17
18
19 c l a s s N o d e H e a p : p u b l i c Heap < HeapItem > {
20 public :
21 N o d e H e a p ( int n u m _ n o d e s ): _ h e a p _ n o d e ( num_nodes , n o t _ i n _ h e a p )
22 { // c r e a t e s a heap with all nodes having key = i n f i n i t e w e i g h t
23 for ( auto i = 0; i < n u m _ n o d e s ; ++ i ) {
24 i n s e r t ( i , G r a p h :: i n f i n i t e _ w e i g h t );
25 }
26 }
27
28 bool i s _ m e m b e r ( G r a p h :: N o d e I d n o d e i d ) c o n s t
29 {
30 e n s u r e _ i s _ v a l i d _ n o d e i d ( n o d e i d );
31 r e t u r n _ h e a p _ n o d e [ n o d e i d ] != n o t _ i n _ h e a p ;
32 }
33
34 d o u b l e g e t _ k e y ( G r a p h :: N o d e I d n o d e i d )
35 {
36 r e t u r n g e t _ o b j e c t ( _ h e a p _ n o d e [ n o d e i d ]). _key ;
37 }
38
39 G r a p h :: N o d e I d e x t r a c t _ m i n ()
40 {
41 G r a p h :: N o d e I d r e s u l t = Heap < HeapItem >:: e x t r a c t _ m i n (). _ n o d e i d ;
128 9 Optimale Bäume und Wege

42 _heap_node [ result ] = not_in_heap ;


43 return result ;
44 }
45
46 void i n s e r t ( G r a p h :: N o d e I d nodeid , d o u b l e key )
47 {
48 e n s u r e _ i s _ v a l i d _ n o d e i d ( n o d e i d );
49 H e a p I t e m item ( nodeid , key );
50 _ h e a p _ n o d e [ n o d e i d ] = Heap < HeapItem >:: i n s e r t ( i t e m );
51 }
52
53 void d e c r e a s e _ k e y ( G r a p h :: N o d e I d nodeid , d o u b l e n e w _ k e y )
54 {
55 e n s u r e _ i s _ v a l i d _ n o d e i d ( n o d e i d );
56 g e t _ o b j e c t ( _ h e a p _ n o d e [ n o d e i d ]). _key = n e w _ k e y ;
57 Heap < HeapItem >:: d e c r e a s e _ k e y ( _ h e a p _ n o d e [ n o d e i d ]);
58 }
59
60 void r e m o v e ( G r a p h :: N o d e I d n o d e i d )
61 {
62 e n s u r e _ i s _ v a l i d _ n o d e i d ( n o d e i d );
63 Heap < HeapItem >:: r e m o v e ( _ h e a p _ n o d e [ n o d e i d ]);
64 _heap_node [ nodeid ] = not_in_heap ;
65 }
66
67 private :
68
69 void e n s u r e _ i s _ v a l i d _ n o d e i d ( G r a p h :: N o d e I d n o d e i d ) c o n s t
70 {
71 if ( n o d e i d < 0 or n o d e i d >= s t a t i c _ c a s t < int >( _ h e a p _ n o d e . size ()))
72 t h r o w std :: r u n t i m e _ e r r o r ( " i n v a l i d n o d e i d in N o d e H e a p " );
73 }
74
75 void swap ( H e a p I t e m & a , H e a p I t e m & b )
76 {
77 std :: swap (a , b );
78 std :: swap ( _ h e a p _ n o d e [ a . _ n o d e i d ] , _ h e a p _ n o d e [ b . _ n o d e i d ]);
79 }
80
81 s t a t i c c o n s t int n o t _ i n _ h e a p ;
82 std :: vector < int > _ h e a p _ n o d e ;
83 };
84
85 int c o n s t N o d e H e a p :: n o t _ i n _ h e a p = -1;
86
87
88 struct PrevData {
89 G r a p h :: N o d e I d id ;
90 double weight ;
91 };
92
93
94 G r a p h mst ( c o n s t G r a p h & g )
95 { // Prim ’s A l g o r i t h m . A s s u m e s that g is u n d i r e c t e d and c o n n e c t e d .
96 Graph tree ( g . n u m _ n o d e s () , G r a p h :: u n d i r e c t e d );
97 N o d e H e a p heap ( g . n u m _ n o d e s ());
98 std :: vector < PrevData > prev ( g . n u m _ n o d e s () , { G r a p h :: i n v a l i d _ n o d e , 0 . 0 } ) ;
99
100 c o n s t G r a p h :: N o d e I d s t a r t _ n o d e i d = 0; // s t a r t at v e r t e x 0
101 heap . d e c r e a s e _ k e y ( s t a r t _ n o d e i d , 0);
102
103 w h i l e ( not heap . i s _ e m p t y ()) {
9.2 Implementierung von Prims Algorithmus 129

104 G r a p h :: N o d e I d n o d e i d = h e a p . e x t r a c t _ m i n ();
105 if ( n o d e i d != s t a r t _ n o d e i d ) {
106 tree . a d d _ e d g e ( p r e v [ n o d e i d ]. id , nodeid , prev [ n o d e i d ]. w e i g h t );
107 }
108 for ( auto n e i g h b o r : g . g e t _ n o d e ( n o d e i d ). a d j a c e n t _ n o d e s ()) {
109 if ( heap . i s _ m e m b e r ( n e i g h b o r . id ()) and
110 n e i g h b o r . e d g e _ w e i g h t () < heap . g e t _ k e y ( n e i g h b o r . id ()))
111 {
112 prev [ n e i g h b o r . id ()] = { nodeid , n e i g h b o r . e d g e _ w e i g h t ()};
113 heap . d e c r e a s e _ k e y ( n e i g h b o r . id () , n e i g h b o r . e d g e _ w e i g h t ());
114 }
115 }
116 }
117 r e t u r n tree ;
118 }
119
120
121 G r a p h s h o r t e s t _ p a t h s _ t r e e ( c o n s t G r a p h & g , G r a p h :: N o d e I d s t a r t _ n o d e i d )
122 { // D i j k s t r a ’ s A l g o r i t h m . The g r a p h g can be d i r e c t e d or u n d i r e c t e d .
123 Graph tree ( g . n u m _ n o d e s () , g . d i r t y p e );
124 N o d e H e a p heap ( g . n u m _ n o d e s ());
125 std :: vector < PrevData > prev ( g . n u m _ n o d e s () , { G r a p h :: i n v a l i d _ n o d e , 0 . 0 } ) ;
126
127 heap . d e c r e a s e _ k e y ( s t a r t _ n o d e i d , 0);
128
129 w h i l e ( not heap . i s _ e m p t y ()) {
130 d o u b l e key = heap . f i n d _ m i n (). _key ;
131 if ( key == G r a p h :: i n f i n i t e _ w e i g h t ) {
132 break ; // break exits the while loop i m m e d i a t e l y
133 }
134 G r a p h :: N o d e I d n o d e i d = h e a p . e x t r a c t _ m i n ();
135 if ( n o d e i d != s t a r t _ n o d e i d ) {
136 tree . a d d _ e d g e ( p r e v [ n o d e i d ]. id , nodeid , prev [ n o d e i d ]. w e i g h t );
137 }
138 for ( auto n e i g h b o r : g . g e t _ n o d e ( n o d e i d ). a d j a c e n t _ n o d e s ()) {
139 if ( heap . i s _ m e m b e r ( n e i g h b o r . id ()) and
140 ( key + n e i g h b o r . e d g e _ w e i g h t () < heap . g e t _ k e y ( n e i g h b o r . id ( ) ) ) )
141 {
142 prev [ n e i g h b o r . id ()] = { nodeid , n e i g h b o r . e d g e _ w e i g h t ()};
143 heap . d e c r e a s e _ k e y ( n e i g h b o r . id () , key + n e i g h b o r . e d g e _ w e i g h t ());
144 }
145 }
146 }
147 r e t u r n tree ;
148 }
149
150
151 int main ( int argc , char * argv [])
152 {
153 if ( argc > 1) {
154 Graph g ( argv [1] , Graph :: u n d i r e c t e d );
155 std :: cout << " The f o l l o w i n g is the u n d i r e c t e d i n p u t g r a p h :\ n " ;
156 g . p r i n t ();
157
158 std :: cout << " \ nThe f o l l o w i n g is a m i n i m u m w e i g h t s p a n n i n g tree :\ n " ;
159 G r a p h t = mst ( g );
160 t . p r i n t ();
161
162 Graph h ( argv [1] , Graph :: d i r e c t e d );
163 std :: cout << " \ nThe f o l l o w i n g is the d i r e c t e d i n p u t g r a p h :\ n " ;
164 h . p r i n t ();
165
130 9 Optimale Bäume und Wege

166 std :: cout << " \ nThe f o l l o w i n g is a s h o r t e s t p a t h s tree :\ n " ;


167 G r a p h u = s h o r t e s t _ p a t h s _ t r e e ( h , 0);
168 u . p r i n t ();
169 }
170 }

9.3 Kürzeste Wege: Dijkstras Algorithmus

Wir wollen nun ein weiteres wichtiges kombinatorisches Optimierungsproblem betrachten:


das Kürzeste-Wege-Problem. Für einen Graphen G mit Kantengewichten c : E(G) → R
bezeichnen wir mit

dist(G,c) (x, y) := min{c(E(P)) | P ist ein x-y-Weg in G}

den Abstand von x nach y in (G, c). Für einen Weg P nennen wir c(E(P)) die Länge von
P (bzgl. c).

Berechnungsproblem 9.9 (Kürzeste-Wege-Problem)


Eingabe: ein Graph G mit Kantengewichten c : E(G) → R sowie Knoten s, t ∈ V (G).
Aufgabe: Berechne einen kürzesten s-t-Weg in (G, c) oder entscheide, dass t von s aus in
G nicht erreichbar ist.

Man kann sich auf gerichtete Graphen beschränken, denn man kann jede ungerichtete Kante
e = {v, w} durch zwei gerichtete Kanten (v, w) und (w, v), beide mit Gewicht c(e), ersetzen.
Wenn c(e) = 1 für alle e ∈ E(G) ist, wir als Länge eines Weges also wie bisher nur die
Anzahl der Kanten betrachten, so haben wir mit Satz 7.6 bereits eine Lösung: Breitensuche
löst diesen Spezialfall des Kürzeste-Wege-Problems in linearer Zeit. Anders sieht es aus,
wenn die Kanten unterschiedliche Gewichte (Kosten, Längen) haben.
Es zeigt sich, dass das Problem erheblich schwieriger ist, falls negative Kantengewichte
vorkommen. In vielen Anwendungen ist dies aber nicht der Fall; etwa wenn die Kanten-
gewichte Fahrtzeiten oder Kosten modellieren. Wir setzen daher zunächst voraus, dass alle
Kantengewichte nichtnegativ sind. Folgender berühmte Algorithmus von Dijkstra [10] be-
rechnet dann kürzeste Wege von einem Knoten s zu allen von s aus erreichbaren Knoten.
Er wird sehr häufig in der Praxis eingesetzt.

Algorithmus 9.10 (Dijkstras Algorithmus)


Eingabe: ein Digraph G mit Kantengewichten c : E(G) → R≥0 , ein Knoten s ∈ V (G).
Ausgabe: eine Arboreszenz A := (R, T ) in G, so dass R alle von s aus erreichbaren Knoten
enthält und dist (G,c) (s, v) = dist(A,c) (s, v) für alle v ∈ R gilt.
9.3 Kürzeste Wege: Dijkstras Algorithmus 131

R ← ∅, Q ← {s}, l(s) ← 0
while Q = ∅ do
Wähle ein v ∈ Q mit l(v) minimal
Q ← Q \ {v}
R ← R ∪ {v}
+
for e = (v, w) ∈ δG (v) mit w ∈
/ R do
if w ∈
/ Q or l(v) + c(e) < l(w) then l(w) ← l(v) + c(e), p(w) ← e
Q ← Q ∪ {w}
T ← { p(v) | v ∈ R \ {s}}

Eine Arboreszenz, die eine korrekte Ausgabe darstellt, wird Kürzeste-Wege-Baum (mit
Wurzel s in (G, c)) genannt. Genauer ist für einen Graphen G und c : E(G) → R ein
Kürzeste-Wege-Baum mit Wurzel s ein Teilgraph H von G, sodass H ein Baum bzw. eine
Arboreszenz mit Wurzel s ist und H einen kürzesten s-v-Weg für alle von s aus erreichbaren
Knoten v enthält.
Ein Kürzeste-Wege-Baum mit Wurzel s liefert unmittelbar eine Lösung für das Kürzeste-
Wege-Problem, und zwar gleich für alle t ∈ V (G). Eine Implementierung von Dijkstras
Algorithmus ist in Programm 9.8 enthalten.

Satz 9.11 Dijkstras Algorithmus 9.10 ist korrekt und hat Laufzeit O(n 2 + m), wobei n =
|V (G)| und m = |E(G)|.

Beweis Seien zu jedem Zeitpunkt des Algorithmus T := { p(v) | v ∈ (R ∪ Q) \ {s}} und


A := (R ∪ Q, T ). Dann gelten folgende Invarianten am Ende jeder Iteration der while-
Schleife:

+ −
(a) p(w) ∈ δG (R) ∩ δG (w) für alle w ∈ Q \ {s};
(b) A[R] ist Arboreszenz mit Wurzel s in G;
(c) l(v) = dist (A,c) (s, v) für alle v ∈ R ∪ Q;
+
(d) Für alle e = (v, w) ∈ δG (R) ist w ∈ Q und l(v) + c(e) ≥ l(w);
(e) l(v) = dist(G,c) (s, v) für alle v ∈ R.

Man beachte, dass aus (b), (c), (d), (e) und Q = ∅ am Ende direkt die Korrektheit folgt.
(a)–(c) gelten offensichtlich stets. (d) gilt auch am Ende der Iteration, in der v zu R
hinzugefügt wird; anschließend ändert sich l(v) nicht mehr, und kein l(w) wird jemals
erhöht.
132 9 Optimale Bäume und Wege

Es bleibt zu zeigen, dass (e) erhalten bleibt, wenn ein Knoten v zu R hinzugefügt wird.
Wegen (c) gilt stets l(v) ≥ dist (G,c) (s, v); deshalb zeigen wir die umgekehrte Ungleichung;
sei v ∈ V (G) \ {s}. Wir betrachten den Zeitpunkt unmittelbar bevor v zu R hinzugefügt
wird. Sei P ein s-v-Weg in G. Wir zeigen, dass die Länge von P mindestens l(v) ist. Sei
x der letzte Knoten auf P, der zu diesem Zeitpunkt schon zu R gehört (insbesondere ist
+
x = v), und e = (x, y) ∈ δG (x) ∩ E(P) die folgende Kante (also y ∈ / R; eventuell ist
y = v). Dann ist y ∈ Q (wegen (d)) und

c(E(P)) ≥ dist(G,c) (s, x) + c(e) = l(x) + c(e) ≥ l(y) ≥ l(v),

was zu zeigen war. Bei der ersten Ungleichung nutzt man aus, dass alle Kantengewichte
nichtnegativ sind; die Gleichung folgt daraus, dass (e) für x galt; die vorletzte Ungleichung
folgt aus (d); und die letzte aus der Wahl von v im Algorithmus.
Zur Laufzeit: Die while-Schleife hat offenbar höchstens n Iterationen, die Wahl von v
kann trivial in O(n) Zeit erfolgen, und in der inneren for-Schleife wird jede Kante nur
einmal betrachtet. Die Gesamtlaufzeit O(n 2 + m) folgt. 

Bemerkung 9.12 Die Invarianten (b), (c) und (e) im Beweis von Satz 9.11 implizieren, dass
wir den Algorithmus abbrechen können, sobald t ∈ R ist, wenn wir nur an einem kürzesten
s-t-Weg interessiert sind. Dies kann die Laufzeit in der Praxis verbessern, führt aber nicht
zu einer besseren asymptotischen Laufzeit.

Ähnlich wie Prims Algorithmus lässt sich auch Dijkstras Algorithmus durch die Benutzung
von Heaps effizienter implementieren:

Satz 9.13 Mithilfe von Binärheaps kann man Dijkstras Algorithmus 9.10 so implementie-
ren, dass er Laufzeit O(m log n) hat.

Beweis Benutze für die Menge Q einen Binärheap, wobei v natürlich den Schlüssel l(v)
hat. Dann haben wir jeweils bis zu n extract_min- und insert-Operationen, und
bis zu m decrease_key-Operationen (vgl. Programm 9.8). Die Laufzeit folgt dann mit
Satz 8.19. 

Varianten von Dijkstras Algorithmus werden für die Lösung der meisten Kürzeste-Wege-
Probleme in der Praxis benutzt. Falls die Kantengewichte aber auch negativ sein können,
so funktioniert Dijkstras Algorithmus im Allgemeinen nicht mehr. In diesem Fall benötigen
wir andere, langsamere Algorithmen.
9.4 Konservative Kantengewichte 133

9.4 Konservative Kantengewichte

Definition 9.14 Sei G ein Graph und c : E(G) → R. Dann heißt c konservativ, falls es in
(G, c) keinen Kreis mit negativem Gewicht gibt.

Für einen Weg P und Knoten x, y ∈ V (P) bezeichnen wir mit P[x,y] den Teilgraphen von
P, der ein x-y-Weg ist. Folgendes Lemma ist von zentraler Bedeutung, weil die Aussage
äquivalent zur Existenz eines Kürzeste-Wege-Baumes ist.

Lemma 9.15 Sei G ein Graph und c : E(G) → R konservativ. Seien s, w ∈ V (G) mit
s = w. Sei P ein kürzester s-w-Weg, und sei e = (v, w) die letzte Kante in P. Dann ist
P[s,v] ein kürzester s-v-Weg.

Beweis Angenommen, es gäbe einen s-v-Weg Q mit c(E(Q)) < c(E(P[s,v] )), also
c(E(Q)) + c(e) < c(E(P)).
Falls w ∈
/ V (Q), so ist (V (Q) ∪ {w}, E(Q) ∪ {e}) ein kürzerer s-w-Weg als P; ein
Widerspruch. Ansonsten ist C := (V (Q [w,v] ), E(Q [w,v] ) ∪ {e}) ein Kreis in G und

c(E(C)) = c(E(Q [w,v] )) + c(e)


= c(E(Q)) + c(e) − c(E(Q [s,w] ))
< c(E(P)) − c(E(Q [s,w] ))
≤0

was im Widerspruch dazu steht, dass c konservativ ist. 

Diese Eigenschaft verliert man im Allgemeinen, falls beliebige Kantengewichte zugelassen


sind. Die Korrektheit des folgenden Algorithmus wird für den Fall gerichteter Graphen einen
zweiten Beweis für Lemma 9.15 liefern:

Algorithmus 9.16 (Moore-Bellman-Ford-Algorithmus)


Eingabe: ein Digraph G mit konservativen Kantengewichten c : E(G) → R, ein Knoten
s ∈ V (G).
Ausgabe: eine Arboreszenz A := (R, T ) in G, sodass R alle von s aus erreichbaren Knoten
enthält und dist (G,c) (s, v) = dist(A,c) (s, v) für alle v ∈ R gilt.
134 9 Optimale Bäume und Wege

n ← |V (G)|
l(v) ← ∞ für alle v ∈ V (G) \ {s}
l(s) ← 0
for i ← 1 to n − 1 do
for e = (v, w) ∈ E(G) do
if l(v) + c(e) < l(w) then l(w) ← l(v) + c(e), p(w) ← e
R ← {v ∈ V (G) | l(v) < ∞}
T ← { p(v) | v ∈ R\s}

Satz 9.17 Der Moore-Bellman-Ford-Algorithmus 9.16 arbeitet korrekt und hat Laufzeit
O(mn), wobei n = |V (G)| und m = |E(G)|.

Beweis Die Laufzeit ist offensichtlich. Sei zu jedem Zeitpunkt des Algorithmus A :=
(R, T ) mit R := {v ∈ V (G) | l(v) < ∞} und T := { p(v) | v ∈ R \ {s}}. Wir zeigen
zunächst, dass dann folgende Invarianten stets gelten:

(a) Für alle y ∈ R \ {s} mit p(y) = e = (x, y) gilt l(y) ≥ l(x) + c(e);
(b) A ist eine Arboreszenz mit Wurzel s in G;
(c) l(v) ≥ dist (A,c) (s, v) für alle v ∈ R.

Zum Beweis von (a) beobachten wir, dass bei der letzten Änderung von l(y) auch p(y) auf
e = (x, y) gesetzt wurde und dann l(y) = l(x) + c(e) galt; seitdem kann sich l(x) allenfalls
verringert haben.
Aus (a) folgt insbesondere l(v) ≥ l(w) + c(E(P)) für jeden w-v-Weg P in A. Wir
zeigen nun (b). Wenn es vorkommen sollte, dass das Einfügen einer Kante e = (v, w) einen
Kreis in A erzeugt, dann gab es schon unmittelbar vorher einen w-v-Weg P in A, also galt
l(v) ≥ l(w) + c(E(P)). Andererseits galt l(w) > l(v) + c(e), denn sonst würde p(w) nicht
auf e gesetzt. Somit ist c(E(P)) + c(e) < 0, und demnach ist (V (P), E(P) ∪ {e}) ein Kreis
mit negativem Gewicht; ein Widerspruch, denn c ist konservativ. Somit erfüllt A stets die
Bedingung (g) von Satz 6.23; d. h. (b) gilt.
(a) und (b) und l(s) = 0 implizieren sofort (c) und somit l(v) ≥ dist(G,c) (s, v) für alle
v ∈ R. Um den Beweis der Korrektheit abzuschließen, zeigen wir die

Behauptung Für alle k ∈ {0, . . . , n − 1}, für alle v ∈ V (G) und für alle s-v-Wege P in G
mit |E(P)| ≤ k gilt nach k Iterationen l(v) ≤ c(E(P)).

Da kein Weg in G mehr als n − 1 Kanten hat, impliziert die Behauptung die Korrektheit
des Algorithmus.
Den Beweis der Behauptung führen wir per Induktion über k, wobei die Aussage für
k = 0 trivial ist, denn nach null Iterationen ist l(s) = 0 = dist (G,c) (s, s).
Sei also k ∈ {1, . . . , n − 1}, und sei v ∈ V (G) \ {s} ein Knoten und P ein s-v-
Weg in (G, c) mit |E(P)| ≤ k. Sei e = (u, v) die letzte Kante dieses Weges. Nach
9.5 Kürzeste Wege mit beliebigen Kantengewichten 135

Induktionsvoraussetzung gilt l(u) ≤ c(E(P[s,u] )) nach k − 1 Iterationen, und in Ite-


ration k wird unter anderem die Kante e betrachtet, wodurch l(v) ≤ l(u) + c(e) ≤
c(E(P[s,u] )) + c(e) = c(E(P)) sichergestellt wird. 

Dieser Algorithmus stammt von Moore [27], aufbauend auf Arbeiten von Bellman [3] und
Ford [14]. Er ist bis heute der schnellste Algorithmus für das Kürzeste-Wege-Problem in
gerichteten Graphen mit konservativen Gewichten.
Der Moore-Bellman-Ford-Algorithmus ist nicht auf ungerichtete Graphen mit konserva-
tiven Kantengewichten anwendbar, da durch das Ersetzen einer Kante {v, w} mit negativem
Gewicht durch zwei Kanten (v, w) und (w, v) desselben Gewichts ein Kreis (der Länge 2)
mit negativem Gewicht entsteht.
Man beachte auch, dass alle Algorithmen in diesem Kapitel für beliebige reelle Kanten-
gewichte funktionieren, sofern man Vergleiche und die Grundrechenarten (hier braucht man
sogar nur Additionen) mit diesen durchführen kann. In der Praxis muss man sich natürlich
auf rationale Zahlen oder Maschinenzahlen beschränken. Zur Vermeidung von Rundungs-
fehlern beschränkt man sich sogar oft auf ganze Zahlen.

9.5 Kürzeste Wege mit beliebigen Kantengewichten

Für jedes Berechnungsproblem soll die Art, wie die Eingabe genau kodiert ist (mit Nullen
und Einsen und/oder als Folge reeller Zahlen), festgelegt sein, auch wenn wir dies in der
Regel nicht konkret darlegen, weil alle vernünftigen Kodierungen äquivalent sind.

Definition 9.18 Ein Algorithmus, dessen Eingabe aus Nullen und Einsen besteht, heißt
polynomiell (man sagt auch: er hat polynomielle Laufzeit), wenn es ein k ∈ N gibt, so
dass seine Laufzeit O(n k ) ist, wobei n die Länge der Eingabe ist. Ein Algorithmus, dessen
Eingabe auch reelle Zahlen enthalten kann, heißt streng polynomiell, wenn er für rationale
Eingaben polynomiell ist und es ein k ∈ N gibt, sodass die Anzahl der Rechenschritte
(inklusive Vergleiche und Grundrechenarten mit reellen Zahlen) O(n k ) ist, wobei n die
Anzahl der Bits und reellen Zahlen in der Eingabe ist.

Bisher hatten alle in diesem Buch behandelten Algorithmen außer denen in Kap. 1 und 5
polynomielle Laufzeit. Die in diesem Kapitel bisher vorgestellten Algorithmen (und auch
die in Kap. 6) waren alle sogar streng polynomiell.
Erlauben wir beliebige Kantengewichte, so ist aber kein polynomieller Algorithmus für
das Kürzeste-Wege-Problem bekannt. Immerhin geht es etwas besser als durch Ausprobieren
aller Wege, wie Held und Karp [19] beobachtet haben:
136 9 Optimale Bäume und Wege

Satz 9.19 Das allgemeine Kürzeste-Wege-Problem kann in O(m+n 2 2n ) Zeit gelöst werden,
wobei n = |V (G)| und m = |E(G)|.

Beweis Sei (G, c, s, t) eine Instanz des Kürzeste-Wege-Problems. Wir können wie im Be-
weis von Satz 9.4 vorab parallele Kanten aussortieren. Für A ⊆ V (G) mit s ∈ A und a ∈ A
sei l A (a) die Länge eines kürzesten s-a-Weges, dessen Knotenmenge A ist, oder ∞, wenn
kein solcher existiert.
Es gilt l{s} (s) = 0 und l A (s) = ∞ für alle A ⊃ {s}. Für A ⊆ V (G) mit s ∈ A und
a ∈ A \ {s} gilt außerdem
 
l A (a) = min l A\{a} (b) + c(e) | b ∈ A \ {a}, e = (b, a) ∈ δ − (a) bzw. {b, a} ∈ δ(a) .

Die Berechnung aller dieser höchstens 2n−1 n Zahlen geht in O(2n n 2 ) Zeit, indem man l A (a)
für aufsteigende Werte von |A| berechnet.
Wir haben schließlich dist (G,c) (s, t) = min{l A (t) | {s, t} ⊆ A ⊆ V (G)}, und mithilfe
einer dieses Minimum annehmenden Menge A und der vorab berechneten Zahlen kann ein
kürzester Weg in O(n 2 ) Zeit ermittelt werden. 

Natürlich ist dieser Algorithmus nicht polynomiell, und spätestens für n ≥ 50 unbrauchbar.
Ein schnellerer Algorithmus ist aber nicht bekannt. Ein polynomieller Algorithmus für das
allgemeine Kürzeste-Wege-Problem existiert genau dann, wenn P =NP ist; dies ist die viel-
leicht wichtigste offene Frage der Algorithmischen Mathematik. Hier steht P für die Menge
aller Entscheidungsprobleme, für die es einen polynomiellen Algorithmus gibt, während
NP die Menge aller Entscheidungsprobleme mit folgender Eigenschaft ist: es gibt ein Po-
lynom p sodass für alle n ∈ N und alle Instanzen mit n Bits, für die die korrekte Antwort
„ja“ lautet, eine Folge von höchstens p(n) Bits (Zertifikat genannt) existiert, die dies „be-
weist“: es gibt einen polynomiellen Algorithmus, der Paare von Instanzen und angeblichen
Zertifikaten korrekt überprüft.
Beispielsweise liegt das Entscheidungsproblem, ob ein gegebener ungerichteter Graph
einen aufspannenden Kreis (oft auch Hamiltonkreis genannt) enthält, in NP, denn ein
Hamiltonkreis selbst dient als Zertifikat. Auch für dieses Problem gibt es nur dann einen
polynomiellen Algorithmus, wenn P = NP sein sollte. Dasselbe gilt für tausende wichtiger
Berechnungsprobleme, weshalb die Frage so bedeutend ist.
Eines davon ist das berühmte Traveling-Salesman-Problem: hier ist ein minimal ge-
wichteter Hamiltonkreis in einem gegebenen vollständigen Graphen mit Kantengewich-
ten gesucht. Man beachte, dass das Traveling-Salesman-Problem mit den im Beweis von
Satz (9.19) berechneten Zahlen l A (a) auch gelöst werden kann: die Länge eines kürzes-
ten Hamiltonkreises ist offenbar min{l V (G) (t) + c(e) | e = {t, s} ∈ δ(s) bzw. e = (t, s) ∈
δ − (s)}. Auch hier ist der resultierende Algorithmus von Held und Karp mit Laufzeit O(n 2 2n )
immer noch der mit der besten bekannten asymptotischen Laufzeit. Dennoch ist es (mit an-
deren Algorithmen) gelungen, Instanzen mit vielen tausend Knoten optimal zu lösen [2].
9.6 Übungsaufgaben 137

9.6 Übungsaufgaben

1. Es sei (G, c) eine Instanz des Minimum-Spanning-Tree-Problems, bei der c(e) = c(e )
für je zwei verschiedene Kanten e und e gilt. Zeigen Sie, dass es dann nur eine optimale
Lösung geben kann.
2. Es sei G ein einfacher ungerichteter Graph mit Kantengewichten c : E(G) → R
und v ∈ V (G). Gesucht ist ein aufspannender Baum, in dem v kein Blatt ist und der
unter allen aufspannenden Bäumen, in denen v kein Blatt ist, minimales Gewicht hat.
Zeigen Sie, dass ein solcher Baum in O(n log n) Zeit berechnet werden kann, wobei
n := |V (G)|.
3. Es sei G ein einfacher ungerichteter Graph mit Kantengewichten c : E(G) → R. Die
Menge F ⊆ E(G) enthalte alle Kanten des Graphen, die in irgend einem MST von G
enthalten sind. Zeigen Sie, dass F in O(n log n) Zeit berechnet werden kann, wobei
n := |V (G)|.
4. Beweisen Sie die folgende Aussage: Entfernt man aus einem zusammenhängenden
ungerichteten Graphen mit Kantengewichten sukzessive eine schwerste Kante, deren
Herausnahme nicht den Zusammenhang des Graphen zerstört, so bleibt am Ende ein
MST übrig.
5. Angenommen, alle Kantengewichte sind ganzzahlig und liegen zwischen 0 und einer
Konstante C. Zeigen Sie, dass es dann einen Algorithmus mit linearer Laufzeit für das
Kürzeste-Wege-Problem gibt.
6. Zeigen Sie, dass Dijkstras Algorithmus keinen kürzesten Weg berechnet, wenn manche
Kanten negatives Gewicht haben, selbst wenn die Gewichtsfunktion konservativ ist.
7. Es sei G ein gerichteter Graph und c : E(G) → R. Zeigen Sie, dass c genau dann
konservativ ist, wenn es eine Abbildung π : V (G) → R gibt, sodass für alle Kanten
e = (v, w) ∈ E(G) gilt: c(e) + π(v) − π(w) ≥ 0.
8. Betrachten Sie die folgende Variante des Moore-Bellman-Ford-Algorithmus: Numme-
riere die Knoten des gegebenen Graphen G in einer beliebigen Reihenfolge, es sei also
V (G) = {v1 , . . . , vn }. Betrachte nun in jeder Iteration die Kanten in folgender Reihen-
folge: Durchlaufe die Knoten von v1 nach vn und betrachte für jeden dabei besuchten
Knoten vi alle Kanten (vi , v j ) ∈ E(G) mit i < j, um l(v j ) neu zu setzen. Durchlau-
fe anschließend alle Knoten von vn nach v1 und betrachte für jeden dabei besuchten
Knoten vi alle Kanten (vi , v j ) ∈ E(G) mit j < i, um l(v j ) neu zu setzen. Zeigen
Sie, dass, wenn man in jeder Iteration alle Kanten in dieser Reihenfolge betrachtet,  n2 
Iterationen ausreichend sind.
9. Betrachten Sie folgende Version des Moore-Bellman-Ford-Algorithmus: Solange es
eine Kante (v, w) mit l(w) > l(v) + c((v, w)) gibt, wähle eine beliebige solche Kante
aus und setze l(w) = l(v)+ c((v, w)). Zeigen Sie, dass diese Vorgehensweise bei einer
ungeschickten Wahl der Kantenreihenfolge eine exponentielle Zahl von Knotenlabel-
Änderungen notwendig machen kann.
138 9 Optimale Bäume und Wege

10. Gegeben seien ein gerichteter Graph G mit Kantengewichten c : E(G) → R≥0 und
s, t ∈ V (G). Gesucht ist ein s-t-Weg, dessen längste Kante möglichst kurz ist. Zeigen
Sie, dass dieses Problem in O(n log n) Zeit lösbar ist mit n := |V (G)|.
Hinweis: Modifizieren Sie Dijkstras Algorithmus geeignet.
11. Zeigen Sie, wie man zu einem gegebenen ungerichteten Graphen G mit n Knoten und
m Kanten einen kürzesten Kreis in G in Zeit O(nm) berechnen kann.
12. Zeigen Sie, dass Lemma 9.15 im Allgemeinen nicht gilt, wenn c nicht konservativ ist.
Matching und Netzwerkflüsse
10

In diesem Kapitel wollen wir zwei weitere grundlegende kombinatorische Optimierungs-


probleme lösen: maximale Matchings in bipartiten Graphen und maximale Flüsse in Netz-
werken. Es wird sich herausstellen, dass das erste Problem ein Spezialfall des zweiten ist.
Insofern ist nicht überraschend, dass eine grundlegende Technik, augmentierende Wege, bei
beiden der Schlüssel zur Lösung ist.

10.1 Das Matching-Problem

Definition 10.1 Sei G ein ungerichteter Graph. Eine Menge von Kanten M ⊆ E(G) heißt
Matching in G, falls |δG (v) ∩ M| ≤ 1 für alle v ∈ V (G) gilt. Ein perfektes Matching ist
ein Matching mit |V (G)|
2 Kanten.

Berechnungsproblem 10.2 (Matching-Problem)


Eingabe: ein ungerichteter Graph G.
Aufgabe: Berechne ein Matching in G mit maximaler Kardinalität.

Proposition 10.3 Ein inklusionsmaximales Matching kann in O(m + n) Zeit berechnet


werden, wobei n = |V (G)| und m = |E(G)|.

Beweis Der Greedy-Algorithmus, der die Kanten der Reihe nach durchgeht und sie genau
dann zur anfangs leeren Menge M hinzunimmt, wenn M dabei ein Matching bleibt, leistet
das Gewünschte. 

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 139
S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
140 10 Matching und Netzwerkflüsse

Satz 10.4 Ist M ein inklusionsmaximales Matching und M ∗ ein kardinalitätsmaximales


Matching, so gilt |M| ≥ 21 |M ∗ |.

Beweis Für ein Matching M sei VM := {x ∈ V (G) | δG (x) ∩ M  = ∅} die Menge der
von M überdeckten Knoten. Dann gilt |VM | = 2|M|. Ist M inklusionsmaximal, so hat jedes
e ∈ E(G), und somit erst recht jedes e ∈ M ∗ , mindestens einen Endpunkt in VM . Da keine
zwei Kanten in M ∗ einen Endpunkt gemeinsam haben, folgt |M ∗ | ≤ |VM | = 2|M|. 

Die Schranke in Satz 10.4 ist bestmöglich, wie beispielsweise ein Weg der Länge drei zeigt.
Der Greedy-Algorithmus liefert also ein Matching, das höchstens um einen Faktor 2 kleiner
ist als ein optimales. Man spricht auch von einem 2-Approximationsalgorithmus.
Folgende Definition ist die Grundlage nahezu aller Matching-Algorithmen:

Definition 10.5 Sei G ein ungerichteter Graph und M ein Matching in G. Ein
M-augmentierender Weg in G ist ein Weg P in G mit |E(P) ∩ M| = |E(P) \ M| − 1,
dessen Endpunkte zu keiner Kante von M inzident sind.
Jeder M-augmentierende Weg hat also ungerade Länge, und seine Kanten gehören
abwechselnd zu E(G) \ M und zu M. Folgendes zentrale Resultat charakterisiert kardi-
nalitätsmaximale Matchings; es geht auf Petersen [29] und Berge [4] zurück.

Satz 10.6 Sei G ein ungerichteter Graph und M ein Matching in G. Genau dann gibt es
ein Matching M in G mit |M | > |M|, wenn es einen M-augmentierenden Weg in G gibt.

Beweis Wenn P ein M-augmentierender Weg ist, dann ist M E(P) ein größeres Matching.
Wenn umgekehrt M ein Matching mit |M | > |M| ist, dann ist (V (G), M M ) ein
Graph, in dem jeder Knoten Grad höchstens 2 hat. M M kann also zerlegt werden in Kan-
tenmengen von Kreisen gerader Länge, deren Kanten abwechselnd zu M und M gehören,
und paarweise knotendisjunkten Wegen, deren Kanten ebenfalls abwechselnd zu M und M
gehören. Für mindestens einen dieser Wege P muss |E(P) ∩ M| < |E(P) ∩ M | gelten;
dieser ist dann M-augmentierend. 

10.2 Bipartites Matching

Wir wollen nun einen auf van der Waerden und König zurückgehenden Algorithmus be-
schreiben, der das Matching-Problem in bipartiten Graphen optimal löst. Es gibt auch für
allgemeine Graphen einen ähnlich schnellen Algorithmus, der aber erheblich komplizierter
ist.
10.2 Bipartites Matching 141

Algorithmus 10.7 (Bipartiter Matching-Algorithmus)


Eingabe: ein bipartiter Graph G.
Ausgabe: ein Matching M in G mit maximaler Kardinalität.

M ←∅
X ← {v ∈ V (G) | δ(v)  = ∅}
.
Berechne eine Bipartition V (G) = A ∪ B von G[X ]
while true do
. .
V (H ) ← A ∪ B ∪ {s, t}
E(H ) ← ({s} × (A ∩ X )) ∪ {(a, b) ∈ A × B | {a, b} ∈ E(G) \ M}
∪ {(b, a) ∈ B × A | {a, b} ∈ M} ∪ ((B ∩ X ) × {t})
if t ist in H nicht von s aus erreichbar then stop
Sei P ein s-t-Weg in H
M ← M {{v, w} ∈ E(G) | (v, w) ∈ E(P)}
X ← X \ V (P)

Satz 10.8 Der Bipartite Matching-Algorithmus 10.7 arbeitet korrekt und hat Laufzeit O(nm),
wobei n = |V (G)| und m = |E(G)|.

Beweis Ist P ein s-t-Weg in H , so ist (V (P)\{s, t}, {{v, w} ∈ E(G) | (v, w) ∈ E(P)}) ein
M-augmentierender Weg, also wird M dann im Algorithmus korrekt vergrößert. X enthält
stets die nicht isolierten Knoten von G, die nicht von M überdeckt werden.
Umgekehrt hat jeder M-augmentierende Weg P in G Endpunkte ā ∈ A∩X und b̄ ∈ B∩X ,
und dann ist (V (P)∪{s, t}, {(s, ā), (b̄, t)}∪{(a, b) | {a, b} ∈ E(P)\ M}∪{(b, a) | {a, b} ∈
E(P) ∩ M}) ein s-t-Weg in H . Deshalb ist nach Satz 10.6 M kardinalitäts maximal, wenn
der Algorithmus stoppt.
Die Laufzeit folgt daraus, dass in jeder der höchstens n2 Iterationen mit Algorithmus 7.1
in O(m) Zeit ein s-t-Weg gefunden oder festgestellt wird, dass keiner existiert. 
Folgender berühmte Satz wurde zuerst von Frobenius [17] bewiesen:

.
Satz 10.9 (Heiratssatz) Ein bipartiter Graph G = (A ∪ B, E(G)) mit |A| = |B| besitzt
genau dann ein perfektes Matching, wenn |N (S)| ≥ |S| für alle S ⊆ A.

Beweis Falls G ein perfektes Matching M besitzt, dann gilt |N G (S)| ≥ |N(V (G),M) (S)| =
|S| für alle S ⊆ A. Die Bedingung ist also notwendig. Wir zeigen per Induktion über |A|,
dass die Bedingung auch hinreichend ist. Falls |A| = 1, so ist dies offensichtlich. Sei also
|A| ≥ 2.
Falls |N (S)| ≥ |S| + 1 für alle ∅  = S ⊂ A gilt, so sei e = {a, b} eine beliebige Kante in
G. Nach Induktionsvoraussetzung besitzt der Graph G := G −a −b ein perfektes Matching
142 10 Matching und Netzwerkflüsse

M ; denn |N G (S)| ≥ |N G (S)| − 1 ≥ |S| für alle ∅  = S ⊆ A \ {a}. Dann aber ist M ∪ {e}
ein perfektes Matching in G.
Wir können nun also davon ausgehen, dass es ein ∅  = S ⊂ A mit |N (S)| = |S|
gibt. Nach Induktionsvoraussetzung hat G[S ∪ N (S)] ein perfektes Matching M. Be-
trachte nun den Graphen G := G[(A \ S) ∪ (B \ N (S))]. Für jedes T ⊆ A \ S ist
|N G (T )| = |N G (T ∪ S) \ N G (S)| = |N G (T ∪ S)| − |S| ≥ |T ∪ S| − |S| = |T |. Also hat G
nach Induktionsvoraussetzung ein perfektes Matching M . Damit ist M ∪ M ein perfektes
Matching in G.

10.3 Max-Flow-Min-Cut-Theorem

Definition 10.10 Gegeben sei ein Digraph G mit u : E(G) → R≥0 (wobei u(e) die Ka-
pazität der Kante e heißt) und zwei ausgezeichnete Knoten s ∈ V (G), die Quelle, und
t ∈ V (G), die Senke. Das Tupel (G, u, s, t) bezeichnet man als Netzwerk.
Ein s-t-Fluss in (G, u) ist eine Funktion f : E(G) → R≥0 mit f (e) ≤ u(e) für alle
e ∈ E(G) und
f (δ − (v)) = f (δ + (v)) für alle v ∈ V (G) \ {s, t} (10.1)
sowie
val( f ) := f (δ + (s)) − f (δ − (s)) ≥ 0 .
Wir bezeichnen val( f ) als den Wert von f .


Wir haben wieder die praktische Notation f (A) := e∈A f (e) benutzt. Die Bedingung
(10.1) wird als Flusserhaltungsbedingung bezeichnet. Abb. 10.1 gibt ein Beispiel.

Berechnungsproblem 10.11 (Fluss-Problem)


Eingabe: ein Netzwerk (G, u, s, t).
Aufgabe: Berechne einen s-t-Fluss in (G, u) mit maximalem Wert.
Ein s-t-Fluss mit maximalem Wert (man sagt auch kurz: ein maximaler Fluss) existiert
stets, denn wir maximieren eine stetige Funktion über einer nichtleeren kompakten Menge.
Wir werden dies aber auch noch konstruktiv (algorithmisch) beweisen.

Lemma 10.12 Sei (G, u, s, t) ein Netzwerk und f ein s-t-Fluss in (G, u). Dann gilt für
alle A ⊂ V (G) mit s ∈ A und t ∈
/ A:

(a) val( f ) = f (δ + (A)) − f (δ − (A))


(b) val( f ) ≤ u(δ + (A)).
10.3 Max-Flow-Min-Cut-Theorem 143

a b
2 0
a b a b
9 5 6 5 5 6

s 1 1 3 t s 0 1 2 t

7 5 3 2
c d c d
4 4
Abb. 10.1 a ein Netzwerk (die Zahlen an den Kanten sind ihre Kapazitäten); b ein s-t-Fluss darin,
mit Wert 8.

Beweis

(a): Wegen der Flusserhaltungsbedingung für v ∈ A \ {s} gilt:


 
val( f ) = f (δ + (s)) − f (δ − (s)) = f (δ + (v)) − f (δ − (v))
v∈A
= f (δ + (A)) − f (δ − (A)) .

(b): Wegen 0 ≤ f (e) ≤ u(e) für alle e ∈ E(G) folgt aus (a):

val( f ) = f (δ + (A)) − f (δ − (A)) ≤ u(δ + (A)) . 

Dies motiviert folgende Definition:

Definition 10.13 Sei (G, u, s, t) ein Netzwerk. Ein s-t-Schnitt in G ist eine Kantenmenge
δ + (X ) für ein X ⊂ V (G) mit s ∈ X und t ∈
/ X . Die Kapazität eines s-t-Schnittes δ + (X )
+
ist u(δ (X )) (also die Summe der Kapazitäten seiner Kanten).
Nach Lemma 10.12 ist der maximale Wert eines s-t-Flusses höchstens die minimale
Kapazität eines s-t-Schnittes. Tatsächlich gilt Gleichheit, wie wir nun algorithmisch zeigen
wollen. Dazu erweitern („augmentieren“) wir einen s-t-Fluss sukzessive entlang von s-t-
Wegen.

Definition 10.14 Sei (G, u, s, t) ein Netzwerk und f ein s-t-Fluss. Für e = (x, y) ∈ E(G)
bezeichne ← −
e eine neue (nicht zu G gehörende) gegenläufige Kante von y nach x. Dann
ist der Residualgraph
. ←
G f definiert durch V (G f ) := V (G) und E(G f ) := {e ∈ E(G) |

f (e) < u(e)} ∪{ e | e ∈ E(G), f (e) > 0}.. Ein f-augmentierender Weg ist ein s-t-Weg in
G f . Die Residualkapazitäten u f : E(G) ∪ {← e− | e ∈ E(G)} → R≥0 sind definiert durch

u f (e) := u(e) − f (e) und u f ( e ) := f (e) für alle e ∈ E(G).
144 10 Matching und Netzwerkflüsse

Abb. 10.2 zeigt ein Beispiel. Man beachte, dass G f parallele Kanten enthalten kann, auch
wenn G einfach war. Alle Kanten im Residualgraphen haben positive Residualkapazität.

Lemma 10.15 Sei (G, u, s, t) ein Netzwerk, f ein s-t-Fluss in (G, u), und P ein f-
augmentierender Weg. Sei 0 ≤ γ ≤ mine∈E(P) u f (e). Dann definieren wir das Ergebnis
der Augmentierungvon f entlang P um γ als f : E(G) → R≥0 mit f (e) := f (e) + γ für
e ∈ E(G) ∩ E(P), f (e) := f (e) − γ für e ∈ E(G) mit ← e− ∈ E(P), und f (e) := f (e)
für alle anderen Kanten e von G. Dieses f ist dann ein s-t-Fluss, dessen Wert um γ höher
ist als der von f .

Beweis Folgt direkt aus der Definition der Residualkapazitäten. 

Satz 10.16 Sei (G, u, s, t) ein Netzwerk. Ein s-t-Fluss f in (G, u) hat genau dann maxi-
malen Wert, wenn es keinen f-augmentierenden Weg gibt.

Beweis Falls es einen f-augmentierenden Weg P gibt, so kann f gemäß Lemma 10.15
entlang P um mine∈E(P) u f (e) zu einem Fluss mit größerem Wert augmentiert werden.
Falls es keinen f-augmentierenden Weg gibt, so ist t von s aus in G f nicht erreichbar.
Sei R die Menge der von s aus in G f erreichbaren Knoten. Dann gilt f (e) = u(e) für alle
+ −
e ∈ δG (R) und f (e) = 0 für alle e ∈ δG (R), da andernfalls (nach Definition von G f ) eine
Kante des Residualgraphen aus R hinausführte. Nach Lemma 10.12(a) gilt daher:
+ − +
val( f ) = f (δG (R)) − f (δG (R)) = u(δG (R)) ,

a b
2 0
5 a b a b
5 6 5 5 6
4
s 1 1 2 1 t s 0 0 1 t
4 3
4 3
3 c d 2 c d
4 4

Abb. 10.2 a der Residualgraph G f mit Residualkapazitäten u f für das Netzwerk (G, u, s, t) und
den s-t-Fluss f aus Abb. 10.1; fett ein f -augmentierender Weg P (mit Knoten s, c, b, d, t); b der
durch die Augmentierung von f entlang P um 1 resultierende s-t-Fluss. Dieser hat maximalen Wert,
wie der durch die gelbe Menge induzierte s-t-Schnitt beweist
10.4 Algorithmen für maximale Flüsse 145

was wegen Lemma 10.12(b) bedeutet, dass f maximalen Wert hat. 


Wir erhalten auch einen berühmten Satz von Dantzig, Ford und Fulkerson [9, 15]:

Satz 10.17 (Max-Flow-Min-Cut-Theorem) In jedem Netzwerk (G, u, s, t) ist der maxi-


male Wert eines s-t-Flusses gleich der minimalen Kapazität eines s-t-Schnittes.

Beweis Nach Lemma 10.12(b) ist der Wert eines s-t-Flusses höchstens so groß wie die
Kapazität eines s-t-Schnittes. Im Beweis von Satz 10.16 wurde gezeigt, dass zu einem
maximalen s-t-Fluss f ein s-t-Schnitt δ + (R) existiert mit val( f ) = u(δ + (R)). 

10.4 Algorithmen für maximale Flüsse

Obige Ergebnisse legen folgenden Algorithmus nahe, der von Ford und Fulkerson [16]
stammt:

Algorithmus 10.18 (Ford-Fulkerson-Algorithmus)


Eingabe: ein Netzwerk (G, u, s, t) mit ganzzahligen Kapazitäten.
Ausgabe: ein s-t-Fluss f , der maximalen Wert hat und der zudem ganzzahlig ist.

f (e) ← 0 für alle e ∈ E(G)


while es gibt einen f -augmentierenden Weg P do
γ ← mine∈E(P) u f (e)
Augmentiere f entlang P um γ

Satz 10.19 Der Ford-Fulkerson-Algorithmus 10.18 ist korrekt und kann mit Laufzeit O(mW )
implementiert werden, wobei m = |E(G)| und W der Wert eines maximalen s-t-Flusses ist.
Insbesondere ist W ≤ u(δ + (s)).

Beweis Der Algorithmus erhöht den Wert von f in jeder Iteration um eine natürliche
Zahl γ (alle Residualkapazitäten sind stets ganzzahlig) und terminiert deshalb nach höchs-
tens W Iterationen, und zwar wegen Satz 10.16 mit einem s-t-Fluss maximalen Werts. Nach
Lemma 10.12(a) ist W ≤ u(δ + (s)). Jede Iteration kann mittels Graphendurchmusterung in
O(m) Zeit durchgeführt werden. 
146 10 Matching und Netzwerkflüsse

Folgendes Ergebnis wollen wir auch festhalten:

Korollar 10.20 Sei (G, u, s, t) ein Netzwerk mit ganzzahligen Kapazitäten. Dann existiert
ein s-t-Fluss, der maximalen Wert hat und ganzzahlig ist.

Beweis Der Ford-Fulkerson-Algorithmus berechnet einen solchen. 

Der Bipartite Matching-Algorithmus 10.7 ist übrigens ein Spezialfall des Ford-Fulkerson-
Algorithmus; jener entspricht nämlich genau diesem, wenn man ihn auf den in der ersten
Iteration konstruierten Digraphen H mit Kapazitäten 1 auf allen Kanten anwendet. Man
kann auch den Heiratssatz 10.9 aus dem Max-Flow-Min-Cut-Theorem herleiten.
Der Ford-Fulkerson-Algorithmus ist im Allgemeinen nicht polynomiell, wie das Beispiel
in Abb. 10.3 zeigt.
Für irrationale Kapazitäten ist der Ford-Fulkerson-Algorithmus gar nicht geeignet: hier
wäre nicht einmal garantiert, dass er überhaupt terminiert, und der Wert von f konvergierte
noch nicht einmal unbedingt gegen das Optimum.
Edmonds und Karp [12] haben den Algorithmus von Ford und Fulkerson verbessert,
indem sie den augmentierenden Weg jeweils durch Breitensuche in G f finden. Dies ergibt
einen streng polynomiellen Algorithmus, wie wir nun zeigen.

Lemma 10.21 Sei G ein gerichteter Graph und s und t zwei Knoten. Sei F die Vereini-
gung der Kantenmengen aller kürzesten s-t-Wege in G, und e ∈ F. Dann ist F auch die
Vereinigung der Kantenmengen aller kürzesten s-t-Wege in (V (G), E(G) ∪ {←
e−}).

Beweis Sei k := dist G (s, t), und P ein s-t-Weg mit |E(P)| = k und e ∈ E(P). Angenom-
men, es gäbe einen s-t-Weg Q in .
(V (G),
.
E(G) ∪ {←e−}) mit |E(Q)| ≤ k und ←e− ∈ E(Q).
←−
Betrachte H := (V (G), ({ f, g} ∪ E(P) ∪ E(Q))\{e, e }), wobei f und g zwei neue Kanten
von t nach s seien. H erfüllt die Voraussetzungen von Lemma 6.10; es gibt also zwei kan-
tendisjunkte Kreise in H , die f bzw. g enthalten. Diese ergeben dann zwei s-t-Wege in G
mit Gesamtlänge höchstens |E(H )| − 2 = |E(P)| + |E(Q)| − 2 ≤ 2k − 2, im Widerspruch
zur Definition von k. 

Abb. 10.3 Eine Instanz, bei a


der für U ∈ N der Ford- U U
Fulkerson-Algorithmus 2U
Iterationen benötigt, wenn s 1 t
immer entlang von Wegen mit
drei Kanten augmentiert wird.
U U
Man beachte, dass die Eingabe b
hier mit O(log U ) Bits kodiert
wird
10.5 Übungsaufgaben 147

Satz 10.22 Wählt man im Ford-Fulkerson-Algorithmus immer einen augmentierenden Weg


mit möglichst wenigen Kanten, so terminiert der Algorithmus nach höchstens 2|E(G)||V (G)|
Iterationen, auch bei beliebigen Kapazitäten u : E(G) → R≥0 .

Beweis Nach Lemma 10.21 kann sich dist G f (s, t) nie verringern. Um zu zeigen, dass
dist G f (s, t) höchstens 2|E(G)| Iterationen lang konstant bleibt, beobachten wir, dass wäh-
rend eines solchen Zeitraums die Vereinigung der Kantenmengen aller kürzesten s-t-Wege
in G f gemäß Lemma 10.21 nie erweitert, aber in jeder Iteration um diejenigen Kanten
e ∈ E(P) dezimiert wird, für die u f (e) = γ ist.

Dies ist als Edmonds-Karp-Algorithmus bekannt. Seine Laufzeit ist also O(m 2 n), wobei
n = |V (G)| und m = |E(G)|. Damit erhalten wir auch einen konstruktiven Beweis dafür,
dass in jedem Netzwerk ein maximaler s-t-Fluss existiert. Sind aber die Kapazitäten in der
Eingabe als Maschinenzahlen gegeben, so muss nicht unbedingt ein maximaler s-t-Fluss
existieren, der aus Maschinenzahlen besteht.
Das Flussproblem ist übrigens ein spezielles Lineares Programm: es wird eine lineare
Zielfunktion über einem durch lineare Ungleichungen beschriebenen Raum maximiert. Man
kann sogar allgemeine Lineare Programme in polynomieller Zeit lösen, aber das ist wesent-
lich komplizierter. Wir betrachten im Folgenden einen weiteren sehr wichtigen Spezialfall:
das Lösen linearer Gleichungssysteme.

10.5 Übungsaufgaben

1. Es sei G ein ungerichteter Graph und seien M und M zwei Matchings in G mit
|M | > |M|. Beweisen Sie, dass es in G mindestens |M | − |M| knotendisjunkte
M-augmentierende Wege gibt.
2. Ein Graph heißt k-regulär, wenn jeder Knoten Grad k hat. Man beweise, dass ein unge-
richteter k-regulärer bipartiter Graph k paarweise disjunkte perfekte Matchings besitzt.
Man folgere daraus, dass die Kantenmenge eines ungerichteten bipartiten Graphen mit
maximalem Grad k in höchstens k Matchings partitioniert werden kann.
3. Geben Sie einen ungerichteten 3-regulären Graphen an, der kein perfektes Matching
besitzt.
4. Sei k ∈ N eine Konstante. Zeigen Sie, dass man zu einem gegebenen ungerichteten
Graphen G in polynomieller Laufzeit ein Matching M bestimmen kann, für das es
keinen M-augmentierenden Weg in G gibt, der weniger als k Kanten hat. Um welchen
Faktor kann ein solches Matching höchstens kleiner sein als ein kardinalitätsmaximales
Matching? Zeigen Sie, dass die von Ihnen bewiesene Schranke bestmöglich ist.
148 10 Matching und Netzwerkflüsse

.
5. Es sei G ein einfacher bipartiter Graph mit Bipartition V (G) = A ∪ B und |A| = |B| =
k. Jeder Knoten in G habe Grad mindestens 2k . Zeigen Sie, dass G dann ein perfektes
Matching besitzt.
6. Zeigen Sie, dass eine Folge von Mengen A1 , . . . , An genau dann ein Repräsentanten-
system hat, d. h. Elemente ai ∈ Ai für i = 1, . . . , n mit ai  = a j für alle i  = j, wenn

für alle k ∈ N und alle Indizes 1 ≤ i 1 < · · · < i k ≤ n gilt, dass | kj=1 Ai j  ≥ k.
7. Finden Sie ein kardinalitätsmaximales Matching in einem bipartiten Graphen G, wenn
Sie einen ganzzahligen maximalen s-t-Fluss in dem Graphen H mit Einheitskapazitäten
haben, der zu Beginn des Bipartiten Matching-Algorithmus berechnet wird. Zeigen
Sie, dass der Hauptteil des Bipartiten Matching-Algorithmus als Spezialfall des Ford-
Fulkerson-Algorithmus aufgefasst werden kann.
8. Beweisen Sie, dass es in jedem Netzwerk einen maximalen s-t-Fluss f gibt, für den
(V (G), {e ∈ E(G) | f (e) > 0}) azyklisch ist. Zeigen Sie, dass es jedoch nicht immer
einen maximalen s-t-Fluss gibt, dessen Residualgraph azyklisch ist.
9. Zeigen Sie, dass im Fall von irrationalen Kapazitäten der Ford-Fulkerson-Algorithmus
nicht immer terminiert. √
Hinweis: Benutzen Sie das folgende Netzwerk mit den Kantenkapazitäten r := 5−1 2
und q ≥ 1−r 1
. Es gilt somit 1 = r + r 2 .
v1 q v2 q v3
q q
s t
1 q r q 1

q q q q
w1 w2 w3

10. Betrachten Sie das Fluss-Problem eingeschränkt auf Instanzen (G, u, s, t), für die G −t
eine Arboreszenz ist. Zeigen Sie, dass man das so eingeschränkte Problem in linearer
Zeit lösen kann.
Hinweis: Benutzen Sie Tiefensuche.
11. Zeigen Sie, dass man die Kantenmenge F aus Lemma 10.21 in linearer Zeit berechnen
kann.
12. Zeigen Sie, wie man in einem gegebenem Netzwerk (G, u, s, t) einen s-t-Schnitt mi-
nimaler Kapazität in polynomieller Zeit bestimmen kann.
Gauß-Elimination
11

Wir beschäftigen uns in diesem Kapitel mit der Lösung linearer Gleichungssysteme, das
heißt Systeme der Form

α11 ξ1 + α12 ξ2 + · · · + α1n ξn = β1


.. .. ..
. . .
αm1 ξ1 + αm2 ξ2 + · · · + αmn ξn = βm

(oder kurz Ax = b), wobei A = (αi j )1≤i≤m, 1≤ j≤n ∈ Rm×n und b = (β1 , . . . , βm ) ∈ Rm
gegeben sind und x = (ξ1 , . . . , ξn ) ∈ Rn gesucht ist. Mit anderen Worten: wir lösen
folgendes numerische Berechnungsproblem:

Berechnungsproblem 11.1 (Lineare Gleichungssysteme)


Eingabe: eine Matrix A ∈ Rm×n und ein Vektor b ∈ Rm .
Aufgabe: Berechne einen Vektor x ∈ Rn mit Ax = b oder entscheide, dass keiner existiert.

Aus der Linearen Algebra wissen wir, dass dieses Problem eng verwandt dazu ist, den
Rang, und im Falle m = n die Determinante und – falls A nichtsingulär ist – die Inverse
A−1 von A zu berechnen; vgl. die Box Rang und Determinante. Alle diese Probleme
löst das Gaußsche Eliminationsverfahren, das wir in diesem Kapitel studieren. Wir haben
Problem 11.1 für reelle Zahlen definiert, aber man kann alles auf beliebige Körper (zum
Beispiel C) erweitern, sofern man darin rechnen kann.
Neben der Linearen Algebra spielen Algorithmen zur Lösung linearer Gleichungssyste-
me eine fundamentale Rolle bei der numerischen Lösung von Differentialgleichungssyste-
men, in der linearen und nichtlinearen Optimierung, und demzufolge auch in zahlreichen
Anwendungen.

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 149
S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
150 11 Gauß-Elimination

Rang und Determinante


Sei A ∈ Rm×n . Dann ist der Rang von A als die maximale Anzahl linear unabhängiger
Spaltenvektoren von A definiert. Dieser Wert ist gleich der maximalen Anzahl linear
unabhängiger Zeilenvektoren von A. Die Determinante einer quadratischen Matrix
A ∈ Rn×n mit A = (αi j )1≤i, j≤n ist definiert als

 
n
det(A) := sign(σ ) αiσ (i) ,
σ ∈Sn i=1

wobei Sn die Menge aller Permutationen auf der Menge {1, · · · , n} ist und sign(σ ) das
Vorzeichen der Permutation σ angibt. Eine Matrix A ∈ Rn×n heißt nichtsingulär, falls
es eine Matrix B ∈ Rn×n gibt mit AB = I . Ansonsten heißt A singulär. Eine Matrix A
ist genau dann singulär, falls det(A) = 0. Falls es eine Matrix B mit AB = I gibt, so
nennt man B die Inverse von A und bezeichnet sie mit A−1 . Die Inverse einer Matrix A
ist eindeutig und es gilt A A−1 = A−1 A = I . Zudem gilt (AB)−1 = B −1 A−1 für zwei
nichtsinguläre Matrizen A und B. Sind A und B zwei Matrizen in Rn×n , so kann man
durch Nachrechnen leicht beweisen, dass det(AB) = det(A) det(B) gilt. Unmittelbar
aus der Definition der Determinante ergibt sich der Laplacesche Entwicklungssatz.
Dieser besagt, dass für jedes j ∈ {1, . . . , n} gilt:


n
det(A) = (−1)i+ j αi j det(Ai j ) .
i=1

Dabei bezeichnet Ai j die Matrix, die aus A durch Streichen der i-ten Zeile und j-ten
Spalte entsteht.

Vektoren sind hier immer Spaltenvektoren, es sei denn sie werden transponiert (d. h. für x ∈
Rn ist x  ein Zeilenvektor). Für x, y ∈ Rn ist x  y ∈ R ihr Standardskalarprodukt (manchmal
auch inneres Produkt genannt). Für x ∈ Rm und y ∈ Rn ist x y  ∈ Rm×n ihr äußeres Produkt.
Mit I bezeichnen wir eine Einheitsmatrix, also eine quadratische Matrix (δi j )1≤i, j≤n mit
δi j = 1 für i = j und δi j = 0 für i  = j. Die Spalten von I heißen Einheitsvektoren und
werden mit e1 , . . . , en bezeichnet. Die Dimension der Einheitsvektoren und -matrizen wird
stets aus dem Kontext hervorgehen. Analog kann 0 die Zahl Null oder einen Vektor oder
eine Matrix mit lauter Nullen bezeichnen.
11.1 Die Operationen der Gauß-Elimination 151

11.1 Die Operationen der Gauß-Elimination

Das grundlegende Verfahren der Linearen Algebra wird allgemein nach Carl Friedrich Gauß
benannt, war aber schon mehr als 2000 Jahre vor ihm in China bekannt. Es basiert auf
folgenden Operationen:

(1) Vertauschen von Zeilen


(2) Vertauschen von Spalten
(3) Addition eines Vielfachen einer Zeile zu einer anderen

Mit Zeile ist jeweils eine Zeile von A und der entsprechende Eintrag von b gemeint. Mit
Spalte ist jeweils eine Spalte von A und der entsprechende Eintrag von x gemeint. Diese
Operationen ändern die Lösungsmenge nicht (außer dass (2) die Reihenfolge der Variablen
ändert). Für die ersten beiden Operationen ist das offensichtlich. Für die dritte gilt:

Lemma 11.2 Sei A ∈ Rm×n und b ∈ Rm . Seien p, q ∈ {1, . . . , m} mit p  = q und δ ∈ R.


Das δ-fache der p-ten Zeile zur q-ten Zeile von A zu addieren bedeutet, A durch G A zu
ersetzen, wobei G := I + δeq ep ∈ Rm×m .
G ist nichtsingulär, und es gilt G −1 = I − δeq ep und {x | Ax = b} = {x | G Ax = Gb}.
Ferner haben A und G A denselben Rang und im Falle m = n dieselbe Determinante.

Beweis Die erste Aussage ist offensichtlich. Wir prüfen (I + δeq ep )(I − δeq ep ) = I 2 −
δ 2 eq ep eq ep = I . Ist Ax = b, so auch G Ax = Gb. Da G nichtsingulär ist, folgt auch die
Umkehrung. Die Nichtsingularität impliziert auch, dass G A den gleichen Rang wie A hat.
Im Falle m = n ist det(G A) = (det G)(det A) = det A. 

Der Hauptteil der Gauß-Elimination besteht darin, die gegebene Matrix mit diesen Opera-
tionen in eine obere Dreiecksform zu bringen:

Definition 11.3 Eine Matrix A = (αi j ) ∈ Rm×n heißt obere Dreiecksmatrix, falls αi j = 0
für alle i > j gilt. A heißt untere Dreiecksmatrix, falls αi j = 0 für alle i < j gilt. Eine
quadratische Dreiecksmatrix A ∈ Rm×m heißt normiert, falls αii = 1 für alle i = 1, . . . , m.
Die Matrix G in Lemma 11.2 ist eine normierte Dreiecksmatrix.

Proposition 11.4 Eine normierte untere Dreiecksmatrix ist nichtsingulär und hat Determi-
nante 1. Ihre Inverse ist ebenfalls eine normierte untere Dreiecksmatrix.

Beweis Sei A ∈ Rm×m eine normierte untere Dreiecksmatrix. Wir zeigen die Aussage per
Induktion über m. Für m = 1 ist die Aussage offensichtlich. Sonst schreiben wir A = ( cB 01 ),
152 11 Gauß-Elimination

wobei B natürlich wieder eine normierte untere Dreiecksmatrix ist und 0 der Vektor mit m−1
Nullen. Wir erhalten det A = det B, und prüfen (unter Verwendung der Induktionsannahme)
−1
durch Ausrechnen nach, dass A−1 = ( −cB B −1 01 ). 

Ebenso ist die Inverse einer nichtsingulären oberen Dreiecksmatrix wieder eine obere Drei-
ecksmatrix. Wir bringen A in obere Dreiecksform, indem wir sukzessive folgendes Lemma
anwenden (und ggfs. vorher Zeilen und/oder Spalten vertauschen um α pp  = 0 zu gewähr-
leisten).

Lemma 11.5 Sei A = (αi j ) ∈ Rm×n und p ∈ {1, . . . , min{m, n}} mit αii  = 0 für alle
i = 1, . . . , p und αi j = 0 für alle j < p und i > j. Dann gibt es eine normierte untere
Dreiecksmatrix G ∈ Rm×m sodass G A = (αi j ) mit αii  = 0 für alle i = 1, . . . , p und
αi j = 0 für alle j ≤ p und i > j. Ein solches G und die Matrix G A können in O(mn)
Rechenschritten berechnet werden.

αi p
Beweis Wir subtrahieren das α pp -fache der Zeile p von Zeile i für i = p + 1, . . . , m. Nach
m αi p m αi p
Lemma 11.2 erhalten wir G = i= p+1 (I − α pp ei e p ) = I −
 
i= p+1 α pp ei e p . 

Insgesamt braucht diese erste Phase der Gauß-Elimination r Iterationen und also
O(mn(r + 1)) elementare Rechenoperationen, wobei r ≤ min{m, n} der Rang von A
ist. Der Rest ist einfach, denn lineare Gleichungssysteme mit Dreiecksmatrizen lassen sich
leicht mit O(n min{m, n}) Rechenoperationen lösen:

Lemma 11.6 Sei A = (αi j ) ∈ Rm×n obere Dreiecksmatrix und b = (β1 , . . . , βm ) ∈ Rm .


Für alle i = 1, . . . , m gelte entweder (i ≤ n und αii  = 0) oder (βi = 0 und αi j = 0
für alle j = 1, . . . , n). Dann ist die Menge der Lösungen von Ax = b gegeben durch
x = (ξ1 , . . . , ξn ) mit

ξi ∈ R beliebig für alle i mit i > m oder αii = 0, und


⎛ ⎞
1 ⎝ n
ξi = βi − αi j ξ j ⎠ für i = min{m, n}, . . . , 1 mit αii  = 0 . (11.1)
αii
j=i+1

Beweis Durch Umformen von (11.1) erhält man


n
αii ξi + αi j ξ j = βi ,
j=i+1
11.1 Die Operationen der Gauß-Elimination 153

was genau der i-ten Zeile des linearen Gleichungssystems Ax = b entspricht. Andere
Zeilen sind komplett null. Man beachte, dass zur Berechnung von ξi mit (11.1) nur Werte
von Variablen mit höherem Index benötigt werden. 

Eine analoge Aussage gilt natürlich auch für untere Dreiecksmatrizen. Es kann auch vor-
kommen, dass die i-te Zeile von A vollständig null ist, aber βi  = 0 ist. Dann hat das
Gleichungssystem offenbar keine Lösung.
Das Verfahren in Lemma 11.6 bildet den Abschluss der Gauß-Elimination. Wir halten
fest:

Satz 11.7 Lineare Gleichungssysteme Ax = b mit A ∈ Rm×n und b ∈ Rm können mit


O(mn(r + 1)) elementaren Rechenoperationen gelöst werden, wobei r der Rang von A ist.

Beweis Wir bringen A durch die Operationen (1)–(3) auf obere Dreiecksform; siehe
Lemma 11.5. Gibt es dann eine Zeile der Matrix, die komplett null ist, aber deren ent-
sprechender Eintrag der rechten Seite nicht null ist, so stellen wir fest, dass Ax = b keine
Lösung besitzt. Sonst wenden wir Lemma 11.6 an. 

Wir werden den Algorithmus unten auch noch formal beschreiben.

Beispiel 11.8 Wir lösen das lineare Gleichungssystem

3ξ1 + ξ2 + 4ξ3 − 6ξ4 = 3


6ξ1 + 2ξ2 + 6ξ3 = 2
9ξ1 + 4ξ2 + 7ξ3 − 5ξ4 = 0
ξ2 − 3ξ3 + ξ4 = −5

wie folgt:
3 1 4 −6 3 3 1 4 −6 3 3 1 4 −6 3
6 2 6 0 2 0 0 −2 12 −4 0 1 −5 13 −9
→ →
9 4 7 −5 0 0 1 −5 13 −9 0 0 −2 12 −4
0 1 −3 1 −5 0 1 −3 1 −5 0 1 −3 1 −5

3 1 4 −6 3 3 1 4 −6 3
0 1 −5 13 −9 0 1 −5 13 −9
→ →
0 0 −2 12 −4 0 0 −2 12 −4
0 0 2 −12 4 0 0 0 0 0
Beim ersten, dritten und vierten Schritt wurde Lemma 11.5 angewandt, beim zweiten Schritt
wurden die zweite und dritte Zeile vertauscht. Die vierte Gleichung ist redundant. Am Ende
kann ξ4 beliebig gewählt werden; wählt man ξ4 = 0, so erhält man mit (11.1) eine Lösung
x = (−2, 1, 2, 0) .
154 11 Gauß-Elimination

Statt am Ende Lemma 11.6 anzuwenden, kann man die Matrix auch durch weitere An-
wendung von Lemma 11.5 (diesmal von unten nach oben) in Diagonalgestalt bringen, d. h.
in die Form ( D0 B0 ), wobei D eine Diagonalmatrix (d. h. eine quadratische untere und obere
Dreiecksmatrix) ist. Dies wird als Gauß-Jordan-Verfahren bezeichnet.

11.2 LU-Zerlegung

Die Gauß-Elimination gewinnt weitere Informationen:

Definition 11.9 Eine LU-Zerlegung einer Matrix A ∈ Rm×n besteht aus einer normierten
unteren Dreiecksmatrix L und einer oberen Dreiecksmatrix U mit LU = A.

Proposition 11.10 Jede nichtsinguläre Matrix hat höchstens eine LU-Zerlegung.

Beweis Aus A = L 1 U1 = L 2 U2 folgt U1 U2−1 = L −11 L 2 , da mit A auch U2 nichtsingulär ist.


Das Inverse einer oberen Dreiecksmatrix und das Produkt zweier oberer Dreiecksmatrizen
ist wieder eine obere Dreiecksmatrix. Das Inverse einer normierten unteren Dreiecksmatrix
und das Produkt zweier normierter unterer Dreiecksmatrizen ist wieder eine normierte untere
Dreiecksmatrix (siehe Proposition 11.4). Aus U1 U2−1 = L −1 1 L 2 folgt daher, dass links und
rechts die Einheitsmatrix stehen muss. 

Eine LU-Zerlegung existiert aber nicht immer. Beispielsweise hat die Matrix ( 01 01 ) keine
LU-Zerlegung. Daher benötigen wir eine allgemeinere Definition:

Definition 11.11 Sei n ∈ N und σ : {1, . . . , n} → {1, . . . , n} eine Permutation. Die zu σ


gehörende Permutationsmatrix (der Ordnung n) ist die Matrix Pσ = (πi j ) ∈ {0,1}n×n mit
πi j = 1 genau dann wenn σ (i) = j.
Eine voll pivotisierte LU-Zerlegung einer Matrix A ∈ Rm×n besteht aus einer normier-
ten unteren Dreiecksmatrix L, einer oberen Dreiecksmatrix U und zwei Permutationen σ und
τ mit A = Pσ LU Pτ sowie der Eigenschaft, dass ein Diagonalelement υii von U = (υi j )
nur dann null ist, wenn die Zeilen i bis m von U alle komplett null sind.

Eine Permutationsmatrix ist also eine Matrix, die aus einer Einheitsmatrix I = Pid durch
Vertauschungen von Zeilen (oder Spalten) entsteht (mit id bezeichnen wir eine Identitäts-
funktion). Die Multiplikation einer Matrix von links mit Pσ bewirkt eine Vertauschung der
Zeilen gemäß der Permutation σ (die i-te Zeile wird zur σ (i)-ten Zeile), während die Mul-
tiplikation einer Matrix von rechts mit Pτ die Vertauschung der Spalten gemäß τ bewirkt.
Es gilt natürlich:
11.2 LU-Zerlegung 155

Proposition 11.12 Für alle n ∈ N bilden die Permutationsmatrizen der Ordnung n mit der
Multiplikation eine Gruppe (mit I als neutralem Element). Für jede Permutationsmatrix P
gilt P P  = I . 

Nichtsinguläre Matrizen A haben immer eine teilpivotisierte LU-Zerlegung, d. h. eine voll


pivotisierte mit τ = id. Eine voll pivotisierte LU-Zerlegung existiert sogar für jede Matrix
und kann mit der Gauß-Elimination berechnet werden, wie wir gleich sehen. Kennt man eine
solche Zerlegung, so kann man den Rang, die Determinante (bei quadratischen Matrizen)
sowie die Inverse (einer nichtsingulären Matrix) leicht bestimmen. Zum Beispiel haben wir:

Satz 11.13 Sei Ax = b ein lineares Gleichungssystem mit A ∈ Rm×n . Ist eine voll pi-
votisierte LU-Zerlegung von A bekannt, so kann man das lineare Gleichungssystem mit
O(m max{m, n}) Rechenoperationen lösen.

Beweis Sei A = Pσ LU Pτ eine voll pivotisierte LU-Zerlegung von A. Lineare Gleichungs-
systeme mit den Matrizen Pσ und Pτ (trivial) sowie L und U (Lemma 11.6) können je-
weils mit O(m max{m, n}) Rechenoperationen gelöst werden. Deshalb lösen wir sukzes-
sive Pσ z  = b (d. h. ζi = βσ (i) für i = 1, . . . , m), dann Lz  = z  , dann U z  = z  ,
und schließlich Pτ x = z  (d. h. ξτ ( j) = ζ j für j = 1, . . . , n). Es kann passieren, dass
das Gleichungssystem U z  = z  keine Lösung hat; genau dann hat auch Ax = b keine
Lösung. 

Eine voll pivotisierte LU-Zerlegung ist besonders praktisch, wenn man viele Gleichungs-
systeme mit derselben Matrix A, aber verschiedenen rechten Seiten b lösen möchte. Ebenso
können wir mit der (voll pivotisierten) LU-Zerlegung natürlich den Rang (das ist der Rang
von U ) und im Falle einer quadratischen Matrix die Determinante berechnen; dabei müssen
wir nur die Diagonalelemente von U betrachten (vergleiche Korollar 11.16).
Wir beschreiben nun die Gauß-Elimination formal, und zwar so, dass sie explizit eine voll
pivotisierte LU-Zerlegung berechnet. In der Praxis weiß man oft a priori, dass A nichtsingulär
ist und kann dann auf Spaltenvertauschungen verzichten (d. h. Pτ = I ). Sie können aber
dennoch aus Gründen numerischer Stabilität sinnvoll sein; siehe dazu Abschn. 11.4.
Der Algorithmus behält die Invariante Pσ LU Pτ = A bei, wobei σ und τ stets Permu-
tationen sind und L = (λi j ) eine normierte untere Dreiecksmatrix ist. Der Algorithmus
stoppt, wenn U die geforderten Eigenschaften hat. Wir bezeichnen mit υi· bzw. υ·i die i-te
Zeile bzw. Spalte von U ; analog für L.

Algorithmus 11.14 (Gauß-Elimination)


Eingabe: eine Matrix A ∈ Rm×n .
Ausgabe: eine voll pivotisierte LU-Zerlegung (σ, L , U, τ ) von A. Der Rang r von A.
156 11 Gauß-Elimination

σ (i) ← i (i = 1, . . . , m)
L = (λi j ) ← I ∈ Rm×m
U = (υi j ) ← A
τ (i) ← i (i = 1, . . . , n)
r ←0
while es gibt p, q > r mit υ pq  = 0 do
Wähle p ∈ {r + 1, . . . , m} und q ∈ {r + 1, . . . , n} mit υ pq  = 0
r ←r +1
if p  = r then swap(υ p· , υr · ), swap(λ p· , λr · ), swap(λ· p , λ·r ), swap(σ ( p), σ (r ))
if q  = r then swap(υ·q , υ·r ), swap(τ (q), τ (r ))
for i ← r + 1 to m do
λir ← υυrrir
for j ← r to n do υi j ← υi j − λir υr j .

Das zu Beginn jeder Iteration ausgewählte υ pq heißt Pivotelement. Ist die Ausgangsma-
trix nichtsingulär, so kann man stets q = r + 1 wählen, also auf Spaltenvertauschungen
verzichten. Wir sprechen dann von Teilpivotisierung (oder partieller Pivotisierung).
Im Algorithmus gilt offenbar nach jeder Iteration: λi j  = 0 impliziert i = j (und so-
mit λi j = 1) oder υi j = 0 (wobei U = (υi j )). Daher können in einer platzsparenden
Implementierung L und U in derselben Matrix gespeichert werden.

Satz 11.15 Die Gauß-Elimination (Algorithmus 11.14) arbeitet korrekt und benötigt
O(mn(r + 1)) elementare Rechenoperationen, wobei r der Rang von A ist.

Beweis Für die Korrektheit zeigen wir zunächst, dass die Invariante Pσ LU Pτ = A stets
gilt. Zu Beginn ist dies offensichtlich.
Bei einer Zeilenvertauschung ( p  = r ) passiert folgendes: Das Vertauschen von Spal-
ten der Matrix L und entsprechender Zeilen von U ändert nicht das Produkt LU , da
(L Pπ )(Pπ U ) = LU . Das Vertauschen von Zeilen von L wird durch die Änderung von
σ ausgeglichen; das Produkt Pσ L bleibt dabei gleich.
Bei einer Spaltenvertauschung (q  = r ) werden Spalten von U vertauscht; dies wird durch
die Änderung von τ ausgeglichen; das Produkt U Pτ bleibt gleich.
Es bleibt die Änderung der Werte von λir und υi j ( j = r, . . . , n) in der for-Schleife zu
prüfen; wir müssen zeigen, dass LU hier konstant bleibt. Dafür wird Lemma 11.5 angewandt,
d. h. für i = r + 1, . . . , m jeweils Lemma 11.2. Dabei wird U von links mit I − λir ei er
multipliziert. Gleichzeitig wird aber λir gesetzt, was gleichbedeutend damit ist, L von rechts
mit I + λir ei er zu multiplizieren (denn zu diesem Zeitpunkt ist λ·i = ei , und vorher war
λir = 0). Wegen (I + λir ei er )(I − λir ei er ) = I ändert sich das Produkt LU nicht. Die
Invariante Pσ LU Pτ = A ist damit gezeigt.
11.3 Gauß-Elimination mit rationalen Zahlen 157

Offenbar sind σ und τ stets Permutationen. Außerdem ist L stets eine normierte untere
Dreiecksmatrix: es werden nur Einträge unterhalb der Diagonalen verändert, und Vertau-
schungen von Zeilenpaaren und entsprechenden Spaltenpaaren finden immer simultan statt.
Nach Iteration r gilt υrr  = 0 und υir = 0 für alle i = r + 1, . . . , m, und diese Wer-
te ändern sich danach nicht mehr. Wenn der Algorithmus terminiert, ist υi j = 0 für alle
i = r + 1, . . . , m und alle j = 1, . . . , n.
Daraus folgt auch, dass r am Ende der Rang von U ist, und also auch der Rang von A
(denn die Permutationsmatrizen und L sind nichtsingulär). Die Laufzeit ist offensichtlich. 

Wir halten noch fest:

Korollar 11.16 Die Determinante einer gegebenen Matrix A ∈ Rn×n lässt sich in O(n 3 )
Rechenschritten berechnen.

Beweis Wir benutzen Algorithmus 11.14. Jede Zeilen- oder Spaltenvertauschung bedeu-
tet die Multiplikation der Determinante mit −1. Haben wir k Zeilen- und l Spalten-
vertauschungen durchgeführt, so ergibt sich det A = (det Pσ )(det L)(det U )(det Pτ ) =
n
(−1)k (det U )(−1)l = (−1)k+l i=1 υii . Die Laufzeit folgt aus Satz 11.15. 

Korollar 11.17 Sei A ∈ Rn×n gegeben. Dann lässt sich in O(n 3 ) Rechenschritten A−1
berechnen oder entscheiden, dass A singulär ist.

Beweis Wir berechnen eine voll pivotisierte LU-Zerlegung mit Algorithmus 11.14 in O(n 3 )
Schritten (Satz 11.15). Ist der Rang von A gleich n (d. h. A ist nichtsingulär), so lösen wir die
Gleichungssysteme Ax = ei für i = 1, . . . , n gemäß Satz 11.13 jeweils in O(n 2 ) Schritten.
Die Lösungen ergeben die Spalten von A−1 . 

11.3 Gauß-Elimination mit rationalen Zahlen

Wir haben beim bisherigen Studium der Gauß-Elimination angenommen, dass wir mit be-
liebigen reellen Zahlen exakt rechnen können. Das ist natürlich nicht der Fall. In der Praxis
gibt es daher zwei Möglichkeiten: exaktes Rechnen mit rationalen Zahlen oder Rechnen mit
Maschinenzahlen unter Inkaufnahme von Rundungsfehlern.
Wir betrachten zunächst erstere. Wir nehmen rationalen Input an, also A ∈ Qm×n und
b ∈ Qm . Natürlich sind dann alle Zahlen in den Rechnungen rational, denn es werden
ja nur die Grundrechenarten benutzt. Es ist aber nicht offensichtlich, dass die Anzahl der
benötigten Bits, um die im Algorithmus vorkommenden Zahlen exakt zu speichern, nicht
158 11 Gauß-Elimination

stärker als polynomiell wächst (man kann beispielsweise mit n Multiplikationen aus der
n
Zahl 2 die Zahl 22 berechnen, für deren Binärdarstellung man 2n + 1 Bits braucht).

Beispiel 11.18 Die Zahlen selbst können durchaus exponentiell wachsen, wie die Matrix
⎛ ⎞
1 −1 0 · · · 0 0 2
⎜ ⎟
⎜−1 2 −1 . . . 0 0 2⎟
⎜ ⎟
⎜ ⎟
⎜−1 0 2 . . . 0 0 2⎟
⎜ ⎟
⎜ ⎟
An := ⎜ .. .. . . . . . . .. .. ⎟ ∈ Zn×n
⎜ . . . . . . .⎟
⎜ ⎟
⎜ . ⎟
⎜−1 0 0 . . 2 −1 2⎟
⎜ ⎟
⎝−1 0 0 · · · 0 2 2⎠
−1 0 0 · · · 0 0 2

zeigt; ihre LU-Zerlegung ist


⎛ ⎞ ⎛1 −1 0 · · · 0 2

1 0 ··· 0
⎜−1 1 0 ⎟⎜ . ⎟
⎜ ⎟⎜⎜ 0 1 −1 . . 0 4 ⎟ ⎟
⎜ . .. ⎟ ⎜ ⎟
⎜ −1 1 . . .⎟ ⎜ ..


⎟⎜ 0 1
⎟⎜
. 0 8 ⎟ ⎟
An = ⎜ .. .. .. ⎟ ⎜ .. . . . . . . .. .. ⎟ .

⎜ .
. . . ⎟ ⎜.
⎟⎜ . . . . . ⎟ ⎟
⎜ . .. ⎟⎜ ⎟
⎜ . . 1 0 ⎟⎜ .. ⎟
⎜ ⎟⎜ . 1 −1 2n−2⎟
⎝ −1 1 0⎠ ⎝ ⎟
0 1 2n−1⎠
−1 ··· −1 1 0 ··· 0 2n

Edmonds [11] zeigte aber, dass die Gauß-Elimination tatsächlich ein polynomieller (und
daher auch ein streng polynomieller) Algorithmus ist.
Zunächst zeigen wir, dass die Binärdarstellungen von Zähler und Nenner der Determi-
nante einer quadratischen rationalen Matrix höchstens doppelt so viele Bits benötigen wie
die Einträge der Matrix insgesamt:

p
Lemma 11.19 Sei n ∈ N und A = (αi j ) ∈ Qn×n mit αi j = qii jj , pi j ∈ Z und qi j ∈ N.

Sei k := i,n j=1 log2 (| pi j | + 1) + log2 (qi j ) . Dann existieren p ∈ Z und q ∈ N mit
p
det A = q und log2 (| p| + 1) + log2 (q) ≤ 2k.

 
Beweis Wähle q := i,n j=1 qi j und p := q · det A. Dann ist log2 q = i,n j=1 log2 qi j < k
und p ∈ Z. Mit dem Laplaceschen Entwicklungssatz und Induktion über n erhält man
n n n n n
|det A| ≤ i=1 j=1 |αi j |. Hieraus folgt |det A| ≤ i=1 j=1 | pi j | < i, j=1 (1 + | pi j |),
also log2 | p| = log2 q +log2 |det A| < log2 q +log2 i,n j=1 (1+| pi j |) = i,n j=1 (log2 qi j +
log2 (1 + | pi j |)) < k. 
11.3 Gauß-Elimination mit rationalen Zahlen 159

Beispiel 11.18 zeigt, dass die Schranke bis auf einen konstanten Faktor scharf ist.
Wir werden diese Abschätzung für Subdeterminanten der gegebenen Matrix A ∈
Qm×n benutzen; das sind Determinanten von Teilmatrizen A I J := (αi j )i∈I, j∈J mit
I ⊆ {1, . . . , m}, J ⊆ {1, . . . , n} und |I | = |J |  = 0. Es gilt nämlich:

Lemma 11.20 Alle im Laufe der Gauß-Elimination (Algorithmus 11.14) vorkommenden


Zahlen sind 0 oder 1 oder Einträge von A oder, bis auf das Vorzeichen, Quotienten von
Subdeterminanten von A.

Beweis Zum Beweis betrachten wir eine Iteration r . Hier werden Einträge υi j von U mit
i > r und j ≥ r verändert. Im Falle j = r ist der neue Eintrag null. Sonst ist i > r und
j > r , und am Ende der Iteration gilt
 
   det U{1,...,r,i}{1,...,r, j} 
υi j  =  , (11.2)
 det U 
{1,...,r }{1,...,r }

wie man mit dem Laplaceschen Entwicklungssatz erhält, wenn man die letzte Zeile (mit
Index i) der im Zähler stehenden Matrix betrachtet; diese hat höchstens einen von Null
verschiedenen Eintrag, nämlich υi j .
Die zur Berechnung von λir herangezogenen Werte von υir und υrr sind für r = 1
Einträge, also auch Subdeterminanten, von A. Für r > 1 gilt, dass sie letztmals in Iteration
r − 1 verändert wurden, und wir haben gemäß (11.2)
     
       
λir  =  det U{1,...,r −1,i}{1,...,r }  /  det U{1,...,r }{1,...,r }  =  det U{1,...,r −1,i}{1,...,r } .
 det U   det U   det U 
{1,...,r −1}{1,...,r −1} {1,...,r −1}{1,...,r −1} {1,...,r }{1,...,r }
(11.3)
Wir zeigen nun noch, dass Zähler und Nenner in diesen Brüchen (11.2) und (11.3) nicht nur
Subdeterminanten von U , sondern auch von A sind.
Wegen der Invariante Pσ LU Pτ = A ist stets LU = Pσ A Pτ und somit am Ende von
Iteration r :

(Pσ A Pτ ){1,...,r,i}{1,...,r, j} = (LU ){1,...,r,i}{1,...,r, j}


= L {1,...,r,i}{1,...,m} U{1,...,m}{1,...,r, j}
= L {1,...,r,i}{1,...,r,i} U{1,...,r,i}{1,...,r, j} ,

wobei die letzte Gleichung daraus folgt, dass die Spalten von L jenseits der ersten r noch
die Spalten der Einheitsmatrix sind. Es folgt für alle i > r und alle j > r :

det Pσ A Pτ ){1,...,r,i}{1,...,r, j} = det L {1,...,r,i}{1,...,r,i} det U{1,...,r,i}{1,...,r, j}


= det U{1,...,r,i}{1,...,r, j}

und analog
160 11 Gauß-Elimination

det Pσ A Pτ ){1,...,r }{1,...,r } = det U{1,...,r }{1,...,r } .


Auf der linken Seite steht jeweils (bis auf das Vorzeichen) eine Subdeterminante von A. 

Damit erhalten wir:

Satz 11.21 Die Gauß-Elimination (Algorithmus 11.14) ist ein polynomieller Algorithmus.

Beweis Nach Lemma 11.20 sind alle Zahlen, die im Algorithmus vorkommen, 0 oder 1
oder Einträge von A oder (bis auf das Vorzeichen) Quotienten von Subdeterminanten von
A. Jede Subdeterminante von A kann nach Lemma 11.19 mit 2k Bits gespeichert werden, der
Quotient also mit höchstens 4k Bits, wobei k die Anzahl der Bits der Eingabe ist. Damit die
Zahlen wirklich nicht mehr Platz brauchen, müssen alle Brüche stets gekürzt werden. Dies
geht nach Korollar 3.13 mit dem Euklidischen Algorithmus in polynomieller Zeit. 

11.4 Gauß-Elimination mit Maschinenzahlen

Das Rechnen mit rationalen Zahlen mit beliebig großem Zähler und Nenner ist recht lang-
sam. Deshalb löst man lineare Gleichungssysteme in der Praxis meistens mit Maschinen-
zahlen wie dem Datentyp double und nimmt Rundungsfehler in Kauf. Beispiel 5.5 sowie
das folgende Beispiel zeigen, dass sich bei der Gauß-Elimination Rundungsfehler durch
Auslöschung verstärken und zu völlig falschen Ergebnissen führen können.
 
Beispiel 11.22 Die Matrix A = 2−k −1 hat die LU-Zerlegung
1 1
     −k 
2−k −1 1 0 2 −1
= .
1 1 2k 1 0 2k + 1

Für k ∈ N mit k > 52 wird dies beim Rechnen in Fdouble gerundet zu


 −k     −k   −k 
2 −1 1 0 2 −1 2 −1
= = ;
1 1 2k 1 0 2k 1 0

das heißt die gerundete LU-Zerlegung ergibt als Produkt eine Matrix, die von A völlig
verschieden ist. Besser geht es mit Teilpivotisierung: man vertauscht erste und zweite Zeile
und erhält  −k     
2 −1 01 1 0 1 1
= ,
1 1 10 2−k 1 0 −1 − 2−k
was auch nach Rundung fast korrekt ist.
11.4 Gauß-Elimination mit Maschinenzahlen 161

Die Gauß-Elimination ohne geeignete Pivotsuche ist also numerisch instabil. Daher ver-
sucht man, durch geschickte Wahl des Pivotelementes die numerische Stabilität zu erhalten.
Als Strategie wurde z. B. vorgeschlagen, das Element υ pq (möglichst mit q = r + 1, das
|υ |
heißt ohne Spaltenvertauschung) zu wählen, für das max jpq|υ pj | möglichst groß ist. Für keine
Pivotstrategie konnte aber bisher wirklich nachgewiesen werden, dass sie immer zu einem
numerisch stabilen Verfahren führen. Immerhin konnte Wilkinson [11] eine Abschätzung
der Rückwärtsstabilität geben, die das meist gute Verhalten in der Praxis teilweise erklärt.
Aus einer Abschätzung der Rückwärtsstabilität und der Kondition erhält man auch eine
Abschätzung der Vorwärtsstabilität, wie wir noch erläutern werden.
Wir führen hier die Rückwärtsanalyse nur für die einfachere zweite Phase der Gauß-
Elimination (Lemma 11.6) durch:

Satz 11.23 Sei A = (αi j ) ∈ Rm×n obere Dreiecksmatrix und b = (β1 , . . . , βm ) ∈ Rm ,


wobei die Einträge von A und b Maschinenzahlen in einem Maschinenzahlbereich F seien. F
habe Maschinengenauigkeit eps := eps(F) < 13 . Für alle i = 1, . . . , m gelte entweder (i ≤
n und αii  = 0) oder (βi = 0 und αi j = 0 für alle j = 1, . . . , n). Führt man das Verfahren
aus Lemma 11.6 mit Maschinenzahlenarithmetik durch, und sind die Absolutbeträge aller
Zwischenergebnisse null oder in range(F), so erhält man einen Vektor x̃, für den es eine
obere Dreiecksmatrix à = (α̃i j ) gibt mit à x̃ = b und

|αi j − α̃i j | ≤ max{3, n} eps (1 + eps)n−1 |αi j |

für i = 1, . . . , m und j = 1, . . . , n.

Beweis Wir betrachten der Reihe nach die Indizes i = min{m, n}, . . . , 1 mit αii  = 0. Wir
haben gemäß (11.1):  
rd (βi − si+1 )
ξ̃i = rd ,
αii
wobei  
sk = rd rd(αik ξ̃k ) + sk+1 (11.4)

(k = i + 1, . . . , n), sn+1 := 0 und rd eine Rundung zu F sei.

Behauptung Sei k ∈ {i + 1, . . . , n + 1}. Dann gibt es  αi j ∈ R ( j = k, . . . , n) mit sk =


n

α ξ̃
j=k i j j und |
αij − αij | ≤ (n + 2 − k) eps(1 + eps) n+1−k |α | für j = k, . . . , n.
ij
Wir zeigen diese Behauptung per Induktion über n + 1 − k. Für k = n + 1 ist nichts zu
zeigen.

Für k ≤ n haben wir gemäß der Induktionsvoraussetzung sk+1 = nj=k+1  α i j ξ̃ j mit

|α i j − αi j | ≤ (n + 1 − k) eps(1 + eps) |αi j | für j = k + 1, . . . , n. Sei rd eine Rundung
n−k

zu F. Nach (11.4) folgt für geeignete 1 , 2 ∈ R mit | 1 |, | 2 | ≤ eps (vgl. Definition 4.7):
162 11 Gauß-Elimination

 
sk = rd rd(αik ξ̃k ) + sk+1
 
= (αik ξ̃k )(1 + 1 ) + sk+1 (1 + 2 )
⎛ ⎞
n
= ⎝(αik ξ̃k )(1 + 1 ) + α i j ξ̃ j ⎠ (1 +
 2)
j=k+1

n
= αik (1 + 1 )(1 + 2 )ξ̃k + 
α i j (1 + 2 )ξ̃ j
j=k+1

Wir setzen αik := αik (1 + 1 )(1 + 2 ) und αi j := α i j (1 + 2 ) für j = k + 1, . . . , n. Dann


αik − αik | ≤ |αik || 1 + 2 + 1 2 | ≤ |αik |(2 eps + eps2 ) < |αik |2 eps(1 + eps).
ist |
Für j = k + 1, . . . , n berechnen wir

αi j − αi j | = |(
| α i j − αi j )(1 + 2 ) + 2 αi j |
≤ |
α i j − αi j |(1 + | 2 |) + | 2 ||αi j |
≤ (n + 1 − k) eps(1 + eps)n−k |αi j |(1 + | 2 |) + | 2 ||αi j |
 
≤ (n + 1 − k) eps(1 + eps)n+1−k + eps |αi j |
 
< (n + 2 − k) eps(1 + eps)n+1−k |αi j |

Es folgt die Behauptung.


Für k = i + 1 impliziert sie, dass es α̃i j ∈ R ( j = i + 1, . . . , n) gibt mit si+1 =
n
j=i+1 α̃i j ξ̃ j und |α̃i j − αi j | ≤ (n + 1 − i) eps(1 + eps)
n−i |α | für j = i + 1, . . . , n.
ij
Für i = min{m, n}, . . . , 1 mit αii  = 0 haben wir für geeignete 1 , 2 ∈ R mit | 1 |, | 2 | ≤
eps:
   
rd (βi − si+1 ) (βi − si+1 ) (1 + 1 )
ξ̃i = rd = (1 + 2 ) .
αii αii
Wir setzen daher noch α̃ii := αii /((1 + 1 )(1 + 2 )) und erhalten wegen 1 − | | ≤ 1+1 ≤
1+ 23 | | für ∈ R mit | | ≤ 13 wie gewünscht |α̃ii −αii | ≤ ((1+ 23 eps)(1+ 23 eps)−1)|αii | <
3 eps(1 + eps)|αii |. Falls i = n = 1, ist 1 = 0 und also sogar |α̃ii − αii | ≤ 23 eps |αii |.
Zudem gilt 
βi − nj=i+1 α̃i j ξ̃ j
ξ̃i = .
α̃ii
Insgesamt folgt à x̃ = b. 

Der Term (1+eps)n−1 ist für Fdouble und alle praktisch relevanten Größen von n so nahe an
1, dass er vernachlässigt werden kann. Für Fdouble und n = 106 haben wir beispielsweise
|αi j − α̃i j | ≤ 2−33 |αi j |. Die zweite Phase der Gauß-Elimination ist also rückwärtsstabil.
11.5 Matrixnormen 163

Eigentlich ist man aber am relativen Fehler von x̃ interessiert. Diesen kann man mit Hilfe
der Kondition aus der Rückwärtsstabilität erhalten, wie wir zeigen werden. Wir benötigen
dazu aber etwas Vorbereitung.

11.5 Matrixnormen

Um den Begriff der Kondition auf mehrdimensionale Probleme zu verallgemeinern, braucht


man Normen. Durch Abbildungen können Fehler gedämpft oder verstärkt werden. Wir
konzentrieren uns hier auf lineare Abbildungen, wie sie für die Lösung linearer Gleichungs-
systeme relevant sind.

Definition 11.24 Ist V ein Vektorraum über R (etwa V = Rn oder V = Rm×n ; analog
über C), so ist eine Norm auf V eine Abbildung · : V → R mit

• x > 0 für alle x  = 0;


• αx = |α| · x für alle α ∈ R und alle x ∈ V ;
• x + y ≤ x + y für alle x, y ∈ V (Dreiecksungleichung).

Eine Matrixnorm ist eine Norm auf Rm×n .

Beispiel 11.25 Bekannte Vektornormen (jeweils für x = (ξi ) ∈ Rn ) sind:


n
• x 1:= i=1 |ξi | (Betragssummennorm, 1 -Norm, Manhattannorm);
• x ∞ := max1≤i≤n |ξi | (Maximumnorm, ∞ -Norm).

Matrixnormen (jeweils für A = (αi j ) ∈ Rm×n ) sind z. B.:


m
• A := max1≤ j≤n i=1 |α | (Spaltensummennorm);
1
n i j
• A ∞ := max1≤i≤m j=1 |αi j | (Zeilensummennorm).

Im Folgenden beschränken wir uns auf quadratische Matrizen.

Definition 11.26 Eine Matrixnorm · M auf Rn×n heißt verträglich mit der Vektornorm
· auf Rn , falls für alle A ∈ Rn×n und alle x ∈ Rn gilt:
M
Ax ≤ A · x .

Eine Matrixnorm · : Rn×n → R heißt submultiplikativ, wenn


164 11 Gauß-Elimination

AB ≤ A · B

für alle A, B ∈ Rn×n .

Nicht alle Matrixnormen · : Rn×n → R sind submultiplikativ:


   z. B. definiert A :=
max |a | eine Matrixnorm mit  11 10  =  20  = 2 > 1·1 =  11 ·
 
1≤i, j,≤n i j 00 10 00 00
 1 0 . Diese Matrixnorm ist auch nicht mit der Vektornorm · ∞ verträglich, wie das
10        
Beispiel  1 1 1  =  2  = 2 > 1 · 1 =  1 1  ·  1  zeigt.
00 1 ∞ 0 ∞ 00 1 ∞

Satz 11.27 Sei · eine Vektornorm auf Rn . Dann ist | · | : Rn×n → R, definiert durch
  
Ax   
|A| := max  x ∈ Rn
\ {0} = max Ax | x ∈ Rn , x = 1
x

für alle A ∈ Rn×n , eine submultiplikative, mit · verträgliche Matrixnorm.

Beweis Wir zeigen zunächst, dass | · | eine Norm ist:

• Falls A  = 0, gibt es einen Eintrag αi j  = 0, und dann ist Ae j  = 0, also Ae j > 0. Es


Ae j
folgt |A| = maxx=0 Ax x ≥ ej > 0.
α Ax Ax
• Für alle α ∈ R und A ∈ Rn×n ist |α A| = maxx=0 x = |α| maxx=0 x =
|α| · |A| .
• Für alle A, B ∈ Rn×n ist

|A + B| = max (A + B)x
x =1

≤ max ( Ax + Bx )
x =1

≤ max Ax + max Bx
x =1 x =1

= |A| + |B| .

| · | ist submultiplikativ, denn für alle A, B ∈ Rn×n mit B  = 0 ist


ABx
|AB| = max
x=0 x
 
A(Bx) Bx
= max ·
Bx=0 Bx x
Ay Bx
≤ max · max
y=0 y x=0 x
= |A| · |B| .

| · | ist verträglich mit · , denn für alle A ∈ Rn×n und alle x ∈ Rn gilt:
11.6 Kondition linearer Gleichungssysteme 165

Ay
|A| · x = max x ≥ Ax .
y=0 y

Wir nennen die in Satz 11.27 definierte Matrixnorm |cdot| die von · induzierte Norm.
Die von einer Vektornorm induzierte Matrixnorm ist offenbar die kleinstmögliche mit dieser
Vektornorm verträgliche Matrixnorm.

Beispiel 11.28 Die Betragssummennorm x  → x 1 (x ∈ Rn ) induziert die Spaltensum-


mennorm A  → A 1 (A ∈ Rn×n ), denn es gilt stets
⎧   ⎫
⎨ n   n ⎬
 n   n
max{ Ax 1 | x 1 = 1} = max  α ξ   |ξ | = 1 = max |αi j |
⎩  ij j  j
⎭ 1≤ j≤n
i=1  j=1   j=1 i=1

(das erste Maximum wird für einen Einheitsvektor angenommen).


Die Maximumnorm x  → x ∞ induziert die Zeilensummennorm A  → A ∞ , denn es
gilt stets
⎧   ⎫
⎨  n  ⎬
  n
max{ Ax ∞ | x ∞ = 1} = max max  αi j ξ j   |ξ j | ≤ 1 ∀ j = max |αi j | .
⎩ 1≤i≤n   ⎭ 1≤i≤n
j=1 j=1

11.6 Kondition linearer Gleichungssysteme

Wir erinnern uns an die Definition der Kondition aus Abschn. 5.3 und verallgemeinern
Definition 5.6 auf mehrdimensionale Probleme, indem wir überall den Absolutbetrag durch
eine Norm ersetzen. Die Kondition hängt natürlich von der Wahl der Norm ab.
Der Einfachheit halber konzentrieren wir uns hier auf lineare Gleichungssysteme Ax = b,
bei denen A ∈ Rn×n nichtsingulär ist; damit ist das Berechnungsproblem eindeutig, denn
es ist die eindeutige Lösung x = A−1 b gesucht.
Halten wir zunächst die Matrix A fest und betrachten b als Eingabe, so haben wir:

Proposition 11.29 Sei A ∈ Rn×n nichtsingulär und · : Rn → R eine feste Vektornorm.


Die Kondition des Problems b  → A−1 b (bzgl. dieser Norm) ist

κ(A) := A−1 · A ,

wobei · auch die von der Vektornorm · induzierte Matrixnorm bezeichnet.


166 11 Gauß-Elimination

Beweis Gemäß der Definition berechnen wir die Kondition wie folgt:
⎧ ⎧ −1  ⎫ ⎫
⎨ ⎨ A b−A
−1 b  ⎬  ⎬
−1 
sup lim sup
A b  b ∈ Rn , 0 < b − b <  b ∈ Rn , b  = 0
⎩ →0 ⎩ b−b   ⎭  ⎭
b 
 −1  
A (b − b ) A(A−1 b)   
= sup ·  b, b ∈ R n
, b  = 0, b  = b
b − b A−1 b
 −1     
A x  Ay 
= sup  x  = 0 · sup y  = 0
x y 
= A−1 · A . 

κ(A) heißt auch die Kondition der Matrix A. Wegen A−1 · A ≥ A−1 A = I = 1
ist sie immer mindestens 1.
Kennen wir die Kondition von A (oder zumindest eine obere Schranke), so können wir die
Genauigkeit einer Näherungslösung x̃ abschätzen. Hierzu berechnen wir den Residuenvektor
r := A x̃ − b. Mit x̃ = A−1 (b + r ) und x = A−1 b erhalten wir

x − x̃ A−1 b − A−1 (b + r ) A−1r · Ax A−1 · r r


= = ≤ A = κ(A) .
x x b · x b b
Ist der Fehler zu groß, kann man es mit einer Nachiteration versuchen. Hierzu löst man
(ebenfalls näherungsweise) das lineare Gleichungssystem Ax = r und subtrahiert die Lö-
sung von x̃.

Beispiel 11.30 Wir betrachten das lineare Gleichungssystem aus Beispiel 5.5:
 −20    
10 2 ξ1 1
= .
10−20 10−20 ξ2 10−20

Gauß-Elimination (ohne Pivotisierung) mit Maschinenzahlenarithmetik in Fdouble ergibt


die Näherungslösung ξ2 = 21 und ξ1 = 0. Diese hat den sehr kleinen Residuenvektor
      
10−20 2 0 1 0
− = ,
10−20 10−20 1
2 10−20 − 21 · 10−20

was aber nicht bedeutet, dass die Lösung gut ist, denn die Kondition der Matrix ist sehr
schlecht:
 −20   −20    
10 2  10 2   −1 2·1020
 2−10−20 2−10−20 

κ =   ·   ≈ 2 · 1020 .
10−20 10−20  10−20 10−20   1 −1 
−20 −20
2−10 2−10

Multipliziert man die zweite Zeile mit 1020 , so wird die Kondition viel besser, denn
11.6 Kondition linearer Gleichungssysteme 167

 −20   −20    −1 
10 2  10 2   
2 

κ = 

 ·  2−10−20 2−10−20
 ≈ 3
1 1 1 1   1
−20
−10−20 
2−10 2−10−20

(jeweils bzgl. Zeilensummennorm). Allerdings ist jetzt auch der Residuenvektor groß, denn
 −20       
10 2 0 1 0
− = .
1 1 1
2 1 − 1
2

Dennoch hilft eine gute Kondition bei rückwärtsstabilen Verfahren. Möchten wir allgemein
eine Instanz I eines numerischen Berechnungsproblems lösen, und wissen wir, dass die von
einem bestimmten Algorithmus berechnete Näherungslösung x̃ die korrekte Lösung für eine
˜
Instanz I˜ ist mit I −I I klein (Rückwärtsstabilität), so kann der relative Fehler x̃−x
x in erster
˜
Näherung durch κ(I ) I −I I abgeschätzt werden. Dies unterstellt ein (natürlich nicht immer
gegebenes) lineares Verhalten und ist sonst nur für hinreichend kleine Fehler annähernd
korrekt. Man spricht auch von linearisierter Fehlertheorie.
Ein Beispiel liefert Satz 11.23. Hier ist allerdings nicht die rechte Seite gestört, sondern
die Matrix selbst. Die Kondition der Instanz ist dann nicht (wie in Proposition 11.29) einfach
die Kondition der Matrix. Wir benötigen folgende allgemeinere Aussage.

Satz 11.31 Seien A, à ∈ Rn×n mit A nichtsingulär und à − A A−1 < 1 (bzgl. einer
festen Vektornorm und der davon induzierten Matrixnorm). Dann ist à nichtsingulär. Seien
−1
weiter b, b̃ ∈ Rn mit b  = 0. Dann gilt für x := A−1 b und x̃ := Ã b̃:

x̃ − x κ(A) b̃ − b à − A
≤ + .
x 1 − Ã − A A −1 b A

Beweis Es gilt für alle y ∈ Rn :

A−1 · Ãy ≥ A−1 Ãy


= y + A−1 ( Ã − A)y
≥ y − A−1 ( Ã − A)y (11.5)
≥ −1 · Ã − A · y
y − A 
= 1 − Ã − A A−1 y .

Nach Voraussetzung ist die rechte Seite für alle y  = 0 positiv, und somit auch Ãy > 0
für alle y  = 0; also ist à nichtsingulär.
Setzen wir y = x̃ − x in (11.5) ein und benutzen
168 11 Gauß-Elimination

Ã(x̃ − x) = b̃ − Ãx
= (b̃ − b) − ( Ã − A)x
≤ b̃ − b + Ã − A · x

b̃ − b Ax à − A
= A · + · x
b A A

b̃ − b à − A
≤ A + x ,
b A

so erhalten wir

A−1 b̃ − b à − A
x̃ − x ≤ A + x ,
1 − Ã − A A−1 b A

wie gefordert. 

Satz 11.23 und 11.31 ergeben zusammen eine sehr gute Abschätzung für den relativen Fehler
der Lösung, die wir in der zweiten Phase der Gauß-Elimination erhalten, wenn wir mit
Maschinenzahlen rechnen, zumindest wenn die Matrix kleine Kondition hat.
In Beispiel 11.30 haben wir bereits gesehen, dass man die Kondition einer Matrix oft
durch Multiplikation einer Zeile mit einer (von null verschiedenen) Zahl verbessern kann.
Ebenso kann man Spalten mit Konstanten ungleich null multiplizieren. Allgemein wählt
man nichtsinguläre Diagonalmatrizen D1 und D2 , so dass D1 AD2 besser konditioniert ist
als A; dann löst man statt Ax = b das Gleichungssystem (D1 AD2 )y = D1 b und setzt
anschließend x := D2 y. Dies nennt man Vorkonditionierung.
Tatsächlich können D1 und D2 beliebige nichtsinguläre Matrizen sein, aber leider ist noch
nicht einmal bei der Beschränkung auf Diagonalmatrizen klar, wie man D1 und D2 am besten
wählen sollte. Man strebt normalerweise an, dass die Summen der Absolutbeträge in allen
Zeilen und Spalten von D1 AD2 ähnlich sind; dies nennt man Äquilibrierung. Beschränkt
man sich auf Vorkonditionierung mit einer Diagonalmatrix von links (d. h. D2 = I ), führen
gleiche Zeilenbetragssummen tatsächlich zur besten so erreichbaren Kondition bezüglich
der Maximumnorm:


n 
n
Satz 11.32 Sei A = (αi j ) ∈ Rn×n eine nichtsinguläre Matrix mit |αi j | = |α1 j | für
j=1 j=1
i = 1, . . . , n. Dann gilt für jede nichtsinguläre Diagonalmatrix D bzgl. der Maximumnorm:

κ(D A) ≥ κ(A) .

Beweis Die Diagonalelemente von D bezeichnen wir mit δ1 , . . . , δn . Dann gilt


11.7 Übungsaufgaben 169


n
DA ∞ = max |δi | |αi j | = A ∞ max |δi | .
1≤i≤n 1≤i≤n
j=1

Hier haben wir ausgenutzt, dass die Zeilensummen alle gleich sind.
Die Inverse von D hat die Diagonaleinträge δ11 , . . . , δ1n . Sei A−1 = (αi j )i, j=1,...,n . Dann
ist:
(D A)−1 ∞ = A−1 D −1 ∞
n |α  | 
 ij max1≤i≤n nj=1 |αi j | A−1 ∞
= max ≥ = .
1≤i≤n |δ j | max1≤k≤n |δk | max1≤k≤n |δk |
j=1

Die Multiplikation dieser beiden Ungleichungen ergibt:

κ(D A) = DA ∞ A−1 D −1 ∞ ≥ A ∞ A−1 ∞ = κ(A) . 

Zusammenfassend ist die Gauß-Elimination (ggfs. mit Vorkonditionierung und/oder Nach-


iteration) ein brauchbares Verfahren für die Lösung allgemeiner linearer Gleichungssysteme
und verwandter Probleme. Benötigt man allerdings eine exakte Lösung, so muss man mit
rationalen Zahlen exakt rechnen (Abschn. 11.3). Für Gleichungssysteme mit speziellen Ei-
genschaften gibt es andere, oft bessere Verfahren, auf die wir hier aber nicht näher eingehen
können.

11.7 Übungsaufgaben

1. Zeigen Sie, dass ein lineares Gleichungssystem entweder keine, genau eine, oder
unendlich viele Lösungen besitzt. Geben Sie für jeden der drei Fälle ein Beispiel an.
2. Es sei Ax = 0 ein lineares Gleichungssystem, wobei die Einträge der Matrix A ganz-
zahlig seien. Angenommen, es gibt außer x = 0 eine weitere Lösung. Zeigen Sie, dass
es dann außer x = 0 eine weitere ganzzahlige Lösung gibt.
3. Zeigen Sie, dass der Rechenaufwand für den Laplaceschen Entwicklungssatz (n!)
beträgt.
4. Geben Sie eine 3 × 3-Matrix an, die (a) keine (b) unendlich viele LU-Zerlegungen
besitzt.
5. Zeigen Sie, dass es für jede nichtsinguläre Matrix A, deren Diagonaleinträge alle 1 sind,
eine LU-Zerlegung gibt, so dass auch U normiert ist.
6. Zeigen Sie, dass nichtsinguläre Matrizen immer eine teilpivotisierte LU-Zerlegung
haben.

7. Eine Matrix A heißt diagonaldominant, wenn |αii | ≥ j=i |αi j | für alle i gilt.
Betrachten Sie die Gauß-Elimination, wenn die Eingabematrix A diagonaldominant
170 11 Gauß-Elimination

ist. Zeigen Sie, dass man dann keine Zeilen- und Spaltenvertauschungen benötigt, und
dass U stets diagonaldominant ist.
8. Geben Sie analog zu Beispiel 11.18 nichtsinguläre Matrizen An ∈ {−2, −1, 0, 1, 2}n×n
mit LU-Zerlegungen An = L n Un an, so dass die Einträge von L n exponentiell (in n)
wachsen.
9. Zeigen Sie, dass für eine ganzzahlige quadratische Matrix A, deren Einträge mit insge-
samt k Bits gespeichert werden können, | det A| ≤ 2k gilt.
10. Es sei k die Anzahl der Bits, die zum Speichern aller Einträge einer Eingabematrix
A ∈ Qm×n benötigt wird (insbesondere ist k ≥ mn). Zeigen Sie, dass die Anzahl
elementarer Rechenschritte (Operationen auf einzelnen Bits) der Gauß-Elimination mit
ständigem Kürzen von Brüchen durch O(k 4,5 ) beschränkt werden kann.
11. Es sei A ∈ Qn×n nichtsingulär und b ∈ Qn . Sei k die zum Speichern aller Einträge von A
und b benötigte Anzahl Bits. Zeigen Sie, dass dann die Einträge der eindeutigen Lösung
x des linearen Gleichungssytems Ax = b mit jeweils höchstens 2k Bits gespeichert
werden können.
12. Es sei A = (αi j ) ∈ Rn×n . Zeigen Sie, dass durch A := n maxi, j |αi j | eine
Matrixnorm definiert wird.

13. Es seien n, m ∈ N. Zeigen Sie, dass die durch A  → mn maxi=1 m maxn
j=1 |αi j | für alle
A∈R m×n definierte Abbildung eine submultiplikative Matrixnorm ist.
14. Es sei A eine nichtsinguläre Matrix. Zeigen Sie, dass dann gilt:
  
A − B  1
inf  B singulär ≥ .
A κ(A)
Literaturverzeichnis

1. Agrawal M, Kayal N, Saxena N. PRIMES is in P. Ann Math. 2004;160:781–93.


2. Applegate DL, Bixby RE, Chvátal V, Cook WJ. The traveling salesman problem. A computational
study. Princeton: Princeton University Press; 2006.
3. Bellman R. On a routing problem. Q Appl Math. 1958;16:87–90.
4. Berge C. Two theorems in graph theory. Proc Natl Acad Sci USA. 1957;43:842–4.
5. Breymann U. Der C++ Programmierer. 5. Aufl. München: Hanser; 2018.
6. C++ Standard. ISO/IEC 14882:2011. [Link]
2012/[Link]. Zugegriffen: 26. März 2018.
7. Church A. An unsolvable problem of elementary number theory. Am J Math. 1936;58:345–63.
8. Cormen TH, Leiserson CE, Rivest RL, Stein C. Introduction to algorithms. 3. Aufl. Cambridge:
MIT Press; 2009.
9. Dantzig GB, Fulkerson DR. On the max-flow min-cut theorem of networks. In: Kuhn HW, Tucker
AW, Herausgeber. Linear inequalities and related systems. Princeton: Princeton University Press;
1956. S. 215–21.
10. Dijkstra EW. A note on two problems in connexion with graphs. Numer Math. 1959;1:269–71.
11. Edmonds J. Systems of distinct representatives and linear algebra. J Res Natl Bur Stand.
1967;B71:241–5.
12. Edmonds J, Karp RM. Theoretical improvements in algorithmic efficiency for network flow
problems. J ACM. 1972;19:248–64.
13. Folkerts M. Die älteste lateinische Schrift über das indische Rechnen nach al-Hwārizmı̄.
˘
München: Verlag der Bayerischen Akademie der Wissenschaften; 1997.
14. Ford LR. Network flow theory. Paper P-923, The Rand Corporation, Santa Monica; 1956.
15. Ford LR, Fulkerson DR. Maximal flow through a network. Can J Math. 1956;8:399–404.
16. Ford LR, Fulkerson DR. A simple algorithm for finding maximal network flows and an application
to the Hitchcock problem. Can J Math. 1957;9:210–8.
17. Frobenius G. Über zerlegbare Determinanten. Sitzungsberichte der Königlich Preussischen Aka-
demie der Wissenschaften 1917;XVIII:274–77.
18. Fürer M. Faster integer multiplication. SIAM J Comput. 2009;39:979–1005.
19. Held M, Karp RM. A dynamic programming approach to sequencing problems. J SIAM.
1962;10:196–210.

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 171
S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
172 Literaturverzeichnis

20. IEEE Standard for Binary Floating-Point Arithmetic. ANSI/IEEE Std 754-1985. [Link]
[Link]/web-2008/standards/[Link]. Zugegiffen: 26. März 2018.
21. Jarník V. O jistém problému minimálním. Práce Moravské Pr̆írodovĕdecké Spolec̆nosti
1930;6:57–63.
22. Karatsuba A, Ofman Y. Multiplication of multidigit numbers on automata. Sov Phys Dokl.
1963;7:595–6.
23. Knuth DE. The art of computer programming, vol. 1–4A. 3rd ed. Boston: Addison-Wesley; 2011.
24. König D. Über Graphen und ihre Anwendung auf Determinantentheorie und Mengenlehre. Math
Ann. 1916;77:453–65.
25. Kruskal JB. On the shortest spanning subtree of a graph and the traveling salesman problem.
Proc AMS. 1956;7:48–50.
26. Lippman SB, Lajoie J, Moo BE. C++ primer. 5. Aufl. Boston: Addison Wesley 2013
27. Moore EF. The shortest path through a maze. Proceedings of an International Symposium on the
Theory of Switching; Teil II. Cambridge: Harvard University Press; 1959. S. 285–92.
28. Oliveira e Silva T. Empirical verification of the 3x+1 and related conjectures. In: Lagarias JC,
Herausgeber. The ultimate challenge: the 3x+1 problem. Prov Am Math Soc; 2010. S. 189–207.
29. Petersen J. Die Theorie der regulären Graphs. Acta Math. 1891;15:193–220.
30. Prim RC. Shortest connection networks and some generalizations. Bell Syst Tech J.
1957;36:1389–401.
31. Schönhage A, Strassen V. Schnelle Multiplikation großer Zahlen. Computing. 1971;7:281–92.
32. Stroustrup B. The C++ programming language. 4. Aufl. Boston: Addison-Wesley; 2013.
33. Stroustrup B. Programming: principles and practice using C++. 2. Aufl. Boston: Addison Wesley;
2014.
34. Tarjan RE. Data structures and network algorithms. Philadelphia: SIAM; 1983.
35. Treiber D, Zur Reihe der Primzahlreziproken. Elem Math. 1995;50:164–6.
36. Turing AM. On computable numbers, with an application to the Entscheidungsproblem. Proc
Lond Math Soc 1937;42(2):230–65, 43:544–6.
37. Vogel K, Herausgeber. Mohammed ibn Musa Alchwarizmi’s Algorismus. Das früheste Lehrbuch
zum Rechnen mit indischen Ziffern. Aalen: Zeller; 1963.
38. Waldecker R, Rempe-Gillen L. Primzahltests für Einsteiger. 2. Auflage Springer 2016.
39. Wilkinson JH. Error analysis of direct methods of matrix inversion. J ACM. 1961;8:281–330.
Sachverzeichnis

! =, 9 Adjazenzlisten, 88
N, 2 Adjazenzmatrix, 87
Q, 2 Äquivalenzklassen, 33
R, 2 Äquivalenzrelation, 33, 78
Z, 2 Alchwarizmi, Mohammed ibn Musa, 1
←, 6 algorithm, 65, 109
*, 30 Algorithmische Mathematik, 2
++, 13, 42 Algorithmus, 1
+=, 39 eine Funktion berechnender, 11
--, 42 numerisch stabiler, 69
<, 9 polynomieller, 135
<<, 8, 37 randomisierter, 114
<=, 9 streng polynomieller, 135
==, 9 terminierender, 11
>, 9 Allgemeines Sortierproblem, 104
>=, 9 Alphabet, 3
>>, 8 and, 9
%, 10 Anweisung in C++, 7
&, 28 Approximationsalgorithmus, 140
//, 7 Äquilibrierung, 168
#include, 8
Arboreszenz, 82
\n, 8
Arithmeum, 34
Array, 83
A Artikulationsknoten, 79
a/b, 10 Assoziativgesetz, 60
Abbildung, 2 asymptotische Laufzeit, 11
Abbruchkriterium, 72 aufspannender Teilgraph, 77
Abstand, 98, 130 augmentierender Weg
abzählbare Menge, 3 M-, 140
Addition, 45 f -, 143
adjazenter Knoten, 76 Augmentierung, 144

© Springer-Verlag GmbH Deutschland, ein Teil von Springer Nature 2018 173
S. Hougardy und J. Vygen, Algorithmische Mathematik,
[Link]
174 Sachverzeichnis

Ausdruck, 9 Byte, 25
Ausgabe, korrekte, 3 b-adische Darstellung
Ausgangsgrad, 76 natürlicher Zahlen, 26
Auslöschung, 66 reeller Zahlen, 54
äußeres Produkt, 150 b-Komplement, 31
auto, 42 b-Komplementdarstellung, 32
azyklischer Digraph, 100
a-posteriori-Schranke, 64
a-priori-Schranke, 64 C
C++, 1, 5
Programm, 7
B C-style strings, 90
babylonisches Wurzelziehen, 70 Call
Basiskonverter, 27 by reference, 28, 30
Baum, 80 by value, 28
begin(), 42 Cantor, Georg, 18
Beliebig große ganze Zahlen, 40 Cantors Diagonalbeweis, 18
Bellman, Richard, 135 catch, 38
benachbarter Knoten, 76 char, 90
Berechnungsproblem, 3 Charakterisierung, gute, 100
diskretes, 3, 13 Church, Alonzo, 4
eindeutiges, 3 cin, 8
numerisches, 3 class, 34
bereichsbasiertes for, 42 clock(), 108
Berge, Claude, 140 clock_t, 108
Betragsdarstellung, 31 CLOCKS_PER_SEC, 108
Betragssummennorm, 163 Code, 29
BFS, 97 Collatz-Folge, 19, 20
Bias-Darstellung, 56 Collatz, Lothar, 20
Bijektion, 2 Collatz-Problem, 20
bijektive Funktion, 2 Compiler, 4
Binärbaum, vollständiger, 116 Computerprogramm, 2
Binärdarstellung, 25, 27 const, 29
Binärheap, 116 Containertypen, 42
binäre Suche, 64 cout, 8
diskret, 65 cstdlib, 108
binary_search, 65 ctime, 108
bipartiter Graph, 99
bipartiter Matching-Algorithmus, 141
Bipartition, 99 D
Bit, 25 Dantzig, George, 145
Blatt, 80, 82 Darstellung, b-adische
bool, 9 natürlicher Zahlen, 26
Branching, 82 reeller Zahlen, 54
break, 130 Datenfehler, 63
Breitensuche, 97 Datenstruktur, 83
Brücke, 79 dec, 29
Bubblesort, 110 default_random_engine, 108
Bucketsort, 110 Definition einer Variablen, 9
Sachverzeichnis 175

Definitionsbereich, 2 Eratosthenes, 15
delete, 85 erreichbarer Knoten, 78
Destruktor, 35 Euklid, 48
Determinante, 149, 150 Euklidischer Algorithmus, 2, 48, 49
Dezimaldarstellung, 25
DFS, 96
Diagonalbeweis von Cantor, 18 F
diagonaldominante Matrix, 169 false, 9
Diagonalmatrix, 154, 168 Fehler
Differenz, symmetrische, 78 absoluter, 58
Digraph, 75 relativer, 58
azyklischer, 100 Fehlerfortpflanzung, 66
stark zusammenhängender, 81 Fehlertheorie, linearisierte, 167
zusammenhängender, 81 Fibonacci, Leonardo, 49
Dijkstra, Edsger, 130 Fibonacci-Zahlen, 49
Dijkstras Algorithmus, 127, 130 FIFO-Speicher, 85
disjunkte Vereinigung, 78 float, 57
diskretes Berechnungsproblem, 3 Fluss
Distributivgesetz, 60 maximaler, 142
Divide-and-Conquer, 111 s-t-, 142
Division, 47, 72 Fluss-Problem, 142
ganzzahlige, 10 Flusserhaltungsbedingung, 142
do while, 21 for
double, 9, 59 Anweisung
Dreiecksmatrix in C++, 14
normierte, 151 in Pseudocode, 10
obere, 151 bereichsbasiertes, 42
untere, 151 Ford-Fulkerson-Algorithmus, 145
Ford, L.R., jr., 135, 145
FPU, 57
E Frobenius, Ferndinand Georg, 141
Edmonds, Jack, 146, 158 Fulkerson, D.R., 145
Edmonds-Karp-Algorithmus, 147 Funktion, 2
eindeutiges Berechnungsproblem, 3 bijektive, 2
einfacher Graph, 75 in C++, 13
einfacher Primzahltest, 10 injektive, 2
Eingangsgrad, 76 rekursive, 28
Einheitsmatrix, 150 surjektive, 2
Einheitsvektor, 150 Funktionsparameter, 13
else-Teil
in C++, 14
in Pseudocode, 10 G
end(), 42 Gauß, Carl Friedrich, 151
Endknoten, 76, 77 Gauß-Elimination, 2, 151, 155
endliche Menge, 3 rückwärtsstabile Phase, 162
enthaltener Graph, 77 Gauß-Jordan-Verfahren, 154
entscheidbares Problem, 19 Gaußklammer, 10
Entscheidungsproblem, 3, 7 gegenläufige Kante, 143
eps, 58 Gewicht, 123
176 Sachverzeichnis

ggT, 48 Hoare, Antony, 112


Gleichungssystem, lineares, 149 Horner-Schema, 26
Gleitkommadarstellung, normalisierte, 55
Gleitkommazahl, normalisierte, 55
globale Variable, 28 I
Goldbachsche Vermutung, 22 IEEE-Standard, 56
Goldener Schnitt, 51 if-Anweisung
Grad, 76 in C++, 14
Graph, 75 in Pseudocode, 10
bipartiter, 99 Implementieren, 5
einfacher, 75 include, 8
enthaltener, 77 Induktion
gerichteter, 75 über f , 26
inklusionsmaximaler, 79 vollständige, 26
inklusionsminimaler, 79 Induktionsanfang, 26
kantendisjunkter, 78 Induktionsschritt, 26
knotendisjunkter, 78 Induktionsvoraussetzung, 26
regulärer, 147 induzierte Norm, 165
ungerichteter, 75 induzierter Teilgraph, 77
unzusammenhängender, 79 injektive Funktion, 2
vollständiger, 99 inklusionsmaximaler Graph, 79
bipartiter, 99 inklusionsminimaler Graph, 79
zugrundeliegender ungerichteter, 81 innerer Knoten, 77
inneres Produkt, 150
zusammenhängender, 79
Instanz eines Problems, 3
Graphen, 90
int, 9, 34
Graphendurchmusterung, 95
Intervallarithmetik, 69
Greedy-Algorithmus, 124
Invariante, 99
größter gemeinsamer Teiler, 48
Inverse, 150
gut konditioniert, 68
inverse Matrix, 149
gute Charakterisierung, 100
inzidenter Knoten, 76
Inzidenzmatrix, 87
iomanip, 109
H
iostream, 8
Haltefunktion, 19
Iterator, 42, 105
Halteproblem, 19
Hamiltonkreis, 136
Harmonische Zahlen, 36 J
Hash-Tables, 120 Jarník, Vojtěch, 126
Headerdatei, 39
Heap, 29, 115, 116
binärer, 115 K
Heapordnung, 115 Kante, 75
Heapsort, 119 beginnt in, 76
Heiratssatz, 141 endet in, 76
Held, Michael, 135 gegenläufige, 143
Heron von Alexandria, 70 geht von … nach…, 76
hex, 29 inzidente, 76
Hexadezimaldarstellung, 27 parallele, 75
Sachverzeichnis 177

kantendisjunkte Kreise, 78 Kürzeste-Wege-Baum, 131


kantendisjunkte Graphen, 78 Kürzeste-Wege-Problem, 130
Kantengewicht, konservatives, 133
Kantenzug, 77
geschlossener, 77 L
Kapazität, 142, 143 1 -Norm, 163
Karatsuba, A.A., 46 ∞ -Norm, 163
Karatsubas Algorithmus, 46 Lambda-Kalkül, 4
Karp, Richard, 135, 146 Landau-Symbole, 11, 88
kartesisches Produkt, 2 Länge
kaufmännische Rundung, 57 eines Kreises, 78
kgV, 48 eines Teilgraphen, 123
Kind, 82 eines Weges, 78, 130
Klasse, 34 eines Wortes, 3
kleinstes gemeinsames Vielfaches, 48 Laplacescher Entwicklungssatz, 150
K n , 99 Laufzeit, 11
K n,m , 99 asymptotische, 11
Knoten, 75 lineare, 96
adjazenter, 76 polynomielle, 135
benachbarter, 76 leere Menge, 3
erreichbarer, 78 leeres Wort, 3
innerer, 77 LIFO-Speicher, 85
inzidenter, 76 limits, 20
knotendisjunkte Graphen, 78 Lineare Gleichungssysteme, 149
kombinatorische Optimierung, 123 lineare Konvergenz, 71
Kommandozeilenparameter, 90 lineare Laufzeit, 96
Kommentare in C++, 7 Lineares Programm, 147
Kommutativgesetz, 60 linearisierte Fehlertheorie, 167
Komplement eines Graphen, 102 Liste, 84
Komplementdarstellung, 31 doppelt verkettete, 84
komplexe Zahl, 53 einfach verkettete, 84
Kondition, 165 von Primzahlen, 14
absolute, 68 ln, 11
einer Matrix, 166 localtime, 108
relative, 67 log, 11
König, Dénes, 99, 140 long, 34
konservatives Kantengewicht, 133 long double, 57
Konstante, 29 long long, 20, 34
Konstruktor, 35 Lösung, optimale, 123
Konvergenz, lineare, 71 Lösung, zulässige, 123
Konvergenz, quadratische, 71 LU-Zerlegung, 154
Konvergenzordnung, 71 teilpivotisiert, 155
korrekte Ausgabe, 3 voll pivotisiert, 154
Kosten, 123
Kreis, 77
ungerader, 99 M
Kruskal, Joseph, 124 main, 7
Kruskals Algorithmus, 124 Manhattannorm, 163
Kürzen von Brüchen, 48 Mantisse, 56
178 Sachverzeichnis

map, 120 Netzwerk, 142


Maschinencode, 4 Neumann, John von, 112
Maschinengenauigkeit, 58 new, 85
Maschinenzahl, 56 Newton, Isaac, 70
Maschinenzahlbereich, 56 Newton-Verfahren, 70
Matching, 139 nichtsinguläre Matrix, 150, 151
perfektes, 139 Norm, 163
überdeckter Knoten, 140 induzierte, 165
Matching-Algorithmus, bipartiter, 141 normalisierte Gleitkommazahl, 55
Matching-Problem, 139 normierte Dreiecksmatrix, 151
Matrix not, 9
diagonaldominante, 169 NP, 136
inverse, 149 Null-Pointer, 84
nichtsinguläre, 150 nullptr, 84
singuläre, 150 Nullstelle, 70
Matrixnorm, 163 numeric_limits, 20, 57
submultiplikative, 163 numerisch stabiler Algorithmus, 69
verträgliche, 163 numerisches Berechnungsproblem, 3
max, 20
Max-Flow-Min-Cut-Theorem, 145
maximaler Graph, 79 O
Maximumnorm, 163 O-Notation, 11, 88
Median, 114 obere Dreiecksmatrix, 151
Menge oct, 29
abzählbare, 3 Oktaldarstellung, 27
endliche, 3 -Notation, 11, 88
leere, 3 operator, 35
maximale, 79 Operatoren in C++, 9, 35
minimale, 79 optimale Lösung, 123
überabzählbare, 3 Optimierung, kombinatorische, 123
unendliche, 3 or, 9
Mergesort, 111 Orakel, 64
minimaler Graph, 79 Ordnung
minimum spanning tree, 124 partielle, 103
Minimum-Spanning-Tree-Problem, 123 durch Schlüssel induzierte, 110
mod, 10 topologische, 100
Modellfehler, 64 totale, 103
Moore-Bellman-Ford-Algorithmus, 133 Orientierung, 81
Moore, E.F., 135 ostream, 37
MST, 124 Outputoperator, 8
Multiplikation, 46
gut konditionierte, 68
P
P, 136
N P = NP, 136
Nachbar, 76 parallele Kante, 75
Nachbarschaft, 76 Parameter einer Funktion, 13
Nachiteration, 166 partielle Ordnung, 103
NaN, 56 durch Schlüssel induziert, 110
Sachverzeichnis 179

Partition, 33 R
perfektes Matching, 139 rand(), 108
Permutation, 104 RAND_MAX, 108
Permutationsmatrix, 154 random, 108
Petersen, Julius, 140 random access, 83
Pivotelement, 156 Random-Quicksort, 114
Pivotisierung, partielle, 156 randomisierter Algorithmus, 114
Pointer, 29 Rang, 149, 150
polynomielle Laufzeit, 135 range, 57
Rationale Zahlen, 36
polynomieller Algorithmus, 135
rbegin(), 42
pop_back, 85
reelle Zahlen, 53
Potenzmenge, 21
Referenz, 28
Prim, 8 regulärer Graph, 147
Prim, Robert, 126 rekursive Funktion, 28
Primfaktorzerlegung, 16 Relation, 2
Prims Algorithmus, 126, 127 rend(), 42
Primzahl, 7 Residualgraph, 143
Primzahltest, 8 Residualkapazitäten, 143
einfacher, 10, 12 Residuenvektor, 166
Primzahlzwilling, 22 resize, 41
Prioritätswarteschlange, 115 Restklassenring, 33
private, 35 return, 13
Problem, gut konditioniertes, 68 RSA, 16
Produkt Rückwärtsanalyse, 69
inneres, 150 rückwärtsstabil, 162
kartesisches, 2 Rundung, 57
Produkt, äußeres, 150 Rundung, kaufmännische, 57
Programm, 5 Rundungsfehler, 4, 63
implementieren, 5 runtime_error, 38
lineares, 147
Programmiersprache, 4 S
Pseudo-Zufallszahlen, 109 Satz vom arithmetischen und geometrischen
Pseudocode, 5, 10 Mittel, 71
public, 35 Schlüssel, 110
push_back, 41 Schnitt
goldener, 51
s-t-, 143
Senke, 142
Q setprecision, 59
Quadrat einer Zahl, 6 setw, 109
quadratische Konvergenz, 71 short, 34
Quadratwurzel, 64, 70 Sieb des Eratosthenes, 2, 15, 16
Quelle, 142 signifikante Stellen, 59
Quersumme, 44 singuläre Matrix, 150
Queue, 85 size(), 41
queue, 86 sizeof, 34
Quicksort, 112, 113 size_t, 34
180 Sachverzeichnis

sort, 109 then-Teil in Pseudocode, 10


Sortieren -Notation, 11, 88
durch Einfügen, 110 this, 43
durch sukzessive Auswahl, 104, 106 throw, 38
nach Schlüsseln, 110 Tiefensuche, 96
Spaltensummennorm, 163 time(), 108
Spaltenvektor, 150 time_t, 108
Sprache, 3, 8 tm, 108
sqrt, 22 topologische Ordnung, 100
Stabilität, 69 totale Ordnung, 103
Stack, 29, 85 Transponieren, 150
Standardbibliothek, 8, 17 Traveling-Salesman-Problem, 136
Standardskalarprodukt, 150 true, 9
stark zusammenhängender Digraph, 81 try, 38
Static, 30 Turing, Alan, 4
static, 39 Turing-Maschine, 1, 4
static_cast, 38 Typ einer Variablen, 9
std::, 8 typename, 87
stdexcept, 38
Stellen, signifikante, 59
Streammanipulator, 29, 109 U
streng polynomieller Algorithmus, 135 überabzählbare Menge, 3
string, 27, 28 überdeckter Knoten, 140
struct, 86 Überlauf, 20, 34
Subdeterminante, 159 unendliche Menge, 3
Subgraph, 77 uniform_int_distribution, 108
submultiplikative Matrixnorm, 163 unordered_map, 120
subnormale Zahl, 56 untere Dreiecksmatrix, 151
Subtraktion, 45 Untergraph, 77
Suchbaum, balancierter, 120 using, 20
Suche, binäre, 64
diskret, 65
surjektive Funktion, 2 V
swap, 37 Variable
symmetrische Differenz, 78 globale, 28
in C++, 9
in Pseudocode, 6
T Typ, 9
Teilbarkeitsrelation, 2 vector, 17, 65
Teiler, größter gemeinsamer, 48 Verbindung
teilerfremde Zahlen, 48 Kante, 76
Teilgraph, 77 Weg, 77
aufspannender, 77 Vereinigung, disjunkte, 78
echter, 79 Verfahrensfehler, 63
induzierter, 77 verträgliche Matrixnorm, 163
Teilmenge, echte, 79 Vielfaches, kleinstes gemeinsames, 48
Teilpivotisierung, 156 virtual, 127
template, 85 void, 13
Template-Parameter, 87 vollständige Induktion, 26
Sachverzeichnis 181

vollständiger bipartiter Graph, 99 Wurzel, 82


vollständiger Graph, 99 Wurzelziehen
Vorgänger, 82 babylonisches, 70
Vorkonditionierung, 168
Vorwärtsanalyse, 69
Vorzeichendarstellung, 31 Z
Zahlen
harmonische, 36
W komplexe, 53
van der Waerden, B.L., 140 reelle, 53
Wald, 80 subnormale, 56
Weg, 77 teilerfremde, 48
augmentierender Zehnerkomplement, 32
M-, 140 Zeichenkette, 3
f -, 143 Zeilensummennorm, 163
Wert, 142 Zeilenvektor, 150
Wertebereich, 2 Zertifikat, 136
what(), 38 zugrundeliegender ungerichteter Graph, 81
while-Anweisung in C++, 21 zulässige Lösung, 123
Wilkinson, J.H., 161 Zusammenhangskomponente, 79
Wort, 3 starke, 81
leeres, 3 Zweierkomplement, 32

Das könnte Ihnen auch gefallen