Algorithmische-Mathematik Compress
Algorithmische-Mathematik Compress
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
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.
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!
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.
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
△
∪˙ 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
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:
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].
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
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∗ .
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.
entwerfen
Algorithmus
lesbar für Menschen
(z.B. in Pseudocode)
implementieren
kompilieren
Maschinencode
lesbar für Computer
(prozessorabhängig)
ausführen
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:
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:
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.
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.
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 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
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:
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
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:
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
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.
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.
Im vorigen Abschnitt haben wir ein Entscheidungsproblem behandelt. Wir geben nun ein
Beispiel für ein allgemeineres diskretes Berechnungsproblem:
14 1 Einleitung
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
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
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
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.
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:
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
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.
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.
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:
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:
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
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.
in Binärdarstellung 1101010. Statt 10 oder 2 könnte man auch jede andere natürliche Zahl
b ≥ 2 als Basis nehmen:
l−1
n = z i bi.
i=0
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
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.
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
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.
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
...
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
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
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.
Beispiel 2.4 Mit der Komplementdarstellung mit vier Bits lassen sich die Zahlen
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
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:
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.
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, a) ∈ R (Reflexivität);
• (a, b) ∈ R ⇒ (b, a) ∈ R (Symmetrie);
• ((a, b) ∈ R ∧ (b, c) ∈ R) ⇒ (a, c) ∈ R (Transitivität).
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
Ü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.
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
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
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.
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
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.
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
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.
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.
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
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
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.
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
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.
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.
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.
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:
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
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.
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
∞
x = σ · bE · z i · b−i . (4.1)
i=0
∞
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
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.
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:
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
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
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:
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).
Die Maschinengenauigkeit ist der maximale relative Fehler, der bei einer Rundung von
Zahlen im darstellbaren Bereich auftreten kann:
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
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 ).
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:
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
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?
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:
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.
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).
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:
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
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,
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.
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:
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.
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.
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.
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
|xn+1 − x ∗ | ≤ c · |xn − x ∗ | 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.
√ 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
√ 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
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.
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.
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
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
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
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.
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}).
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.
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.
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.
Satz 6.13 Ein ungerichteter Graph G ist genau dann zusammenhängend, falls δ(X ) = ∅
für alle ∅ ⊂ X ⊂ V (G).
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.
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.
Satz 6.18 Sei G ein ungerichteter Graph auf n Knoten. Dann sind folgende Aussagen
äquivalent:
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.
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 .
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.
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:
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
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.
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
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
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.
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
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.
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 }
6.7 Übungsaufgaben
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
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)
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.
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.
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:
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)
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.
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
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.
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
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.
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).
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:
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.
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.
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:
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
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.
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.
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
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.
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
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
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
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:
≤ 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).
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.
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
Eine Prioritätswarteschlange ist eine Datenstruktur zum Speichern von Elementen mit
Schlüsseln, die mindestens folgende Funktionen bereitstellt:
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:
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
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
12 8 13 10
3 4 5 6
15 12 11
7 8 9
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.
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
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.
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).
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.
© 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.
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
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
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.
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
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.
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
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).
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.
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)|.
+ −
(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
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
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
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
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.
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
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.
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
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.
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
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.
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 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
(b): Wegen 0 ≤ f (e) ≤ u(e) für alle e ∈ E(G) folgt aus (a):
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 .
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
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)).
Obige Ergebnisse legen folgenden Algorithmus nahe, der von Ford und Fulkerson [16]
stammt:
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
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.
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.
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
(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:
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
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
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:
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:
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:
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:
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.
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
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.
Eine LU-Zerlegung existiert aber nicht immer. Beispielsweise hat die Matrix ( 01 01 ) keine
LU-Zerlegung. Daher benötigen wir eine allgemeinere Definition:
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 .
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.
σ (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.
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 .
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
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:
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 :
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 :
und analog
160 11 Gauß-Elimination
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.
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
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:
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)
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
α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 |
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
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
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 .
AB ≤ A · B
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
|A + B| = max (A + B)x
x =1
≤ max ( Ax + Bx )
x =1
≤ max Ax + max Bx
x =1 x =1
= |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.
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:
κ(A) := A−1 · A ,
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
Beispiel 11.30 Wir betrachten das lineare Gleichungssystem aus Beispiel 5.5:
−20
10 2 ξ1 1
= .
10−20 10−20 ξ2 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
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) .
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
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
© 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
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