94
Algorithmische Mathematik Thomas Richter * 27. Januar 2020 * Institut für Analysis und Numerik, Universität Magdeburg. Raum 16b, Gebäude 2 ([email protected])

Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

  • Upload
    others

  • View
    12

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Algorithmische Mathematik

Thomas Richter∗

27. Januar 2020

∗Institut für Analysis und Numerik, Universität Magdeburg. Raum 16b, Gebäude 2([email protected])

Page 2: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum
Page 3: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Inhaltsverzeichnis

1. Grundlagen 11.1. Vollständige Induktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.2. Python - Variablen und Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . 51.3. Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141.4. Exkurs: Aussagenlogik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

I. Diskrete Aspekte der Algorithmischen Mathematik 21

2. Sortieren 232.1. Ordnung und erstes Sortierverfahren . . . . . . . . . . . . . . . . . . . . . . . . . 232.2. Laufzeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292.3. Sortieren bei totaler Ordnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

2.3.1. Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382.3.2. Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

2.4. Stabiles Sortieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 472.5. Radix-Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48

3. Graphentheorie 533.1. Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533.2. Graphentheoretische Probleme . . . . . . . . . . . . . . . . . . . . . . . . . . . . 573.3. Darstellung und Umsetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63

3.3.1. Realisierung in Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 643.4. Zusammenhang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66

3.4.1. Tiefensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 693.4.2. Breitensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72

3.5. Suche nach kürzesten Wegen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 763.5.1. Dijkstra’s Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78

Bibliography 83

Verzeichnis der Algorithmen 85

Index 87

iii

Page 4: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Inhaltsverzeichnis

iv ©2018,2019 Thomas Richter

Page 5: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Vorwort

Dieses Skript entstand parallel zur Vorlesung Algorithmische Mathematik im Wintersemester2018/2019 sowie im Sommersemester 2019 an der Universität Magdeburg. Begleitend zu denVorlesungen im Wintersemester 2019/20 und im Sommersemester 2020 wird das Skript lau-fend überarbeitet und angepasst.

Hinweise auf Fehler und Verbesserungsvorschläge werden immer gerne entgegengenommen.

v

Page 6: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum
Page 7: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1. Grundlagen

In diesem ersten Kapitel beschäftigen wir uns mit mathematischen Beweisen und den Grund-zügen der Programmierung in Python.Was ist einmathematisches Problem,wie beweisenwireine Aussage, was ist ein Algorithmus zum Lösen eines Problems und was ist ein Programmzur Umsetzung des Algorithmus? Schließlich, wie realisieren wir ein Programm in Python?

1.1. Vollständige Induktion

Eine grundelegende Beweistechnik ist die vollständige Induktion. Ziel ist es zu beweisen, dasseine Aussage A(n), dabei ist n eine natürliche Zahl, für alle solche natürlichen Zahlen n ∈ Nmit n > n0 korrekt ist. Hierzu ein Beispiel:

Behauptung 1.1. Für alle natürlichen Zahlen n > 6 ist die Aussage

(n− 2)2 > n

wahr.

Wir können die Behauptung nachrechnen:

n = 6 : (6− 2)2 = 42 = 16 > 6 wahrn = 7 : (7− 2)2 = 52 = 25 > 7 wahrn = 8 : (8− 2)2 = 62 = 36 > 8 wahr

...

Für die ersten 3 Zahlen haben wir die Aussage bestätigt. Wir dürfen jedoch nicht schließen,dass die Aussage nun für alle natürlichen Zahlen n > 6 gilt. Es gibt unendlich viele solcheZahlen n > 6, daher steht es nicht zur Debatte, die Behauptung für alle Zahlen einzeln nach-zurechnen.

Die Grundidee der vollständigen Induktion ist die Folgende

1. Induktionsanfang: Zunächst wird bewiesen, dass die Aussage für die Zahl n0 korrekt ist.

2. Induktionsschluss: Dann wird die folgende Schlussfolgerung bewiesen: Angenommen,die Aussage sei korrekt für (alle)n ∈ N. Dann ist die Aussage auch korrekt fürn+1 ∈ N.

In Schritt 2 beweisen wir die Folgerung: Angenommen die Aussage A(n) sei für alle n > n0

wahr, dann ist auch die Aussage A(n + 1) wahr. Hiermit alleine ist noch nichts bewiesen: dennangenommen, die Aussage A(n) sei nicht wahr, dann sagen wir auch gar nichts über A(n +

1

Page 8: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.1. Vollständige Induktion

1). Aus einer falschen Aussage können wir einen beliebigen Schluss folgern. Zusammen mitSchritt 1 erhalten wir jedoch eine vollständige Kette von Aussagen.

A(n0) ist wahr ⇒ A(n0 + 1) ist wahr ⇒ A(n0 + 2) ist wahr · · · .

Wir beweisen nun Behauptung 1.1

Beweis. (i) Induktionsanfang. Für n0 = 6 gilt wie bereits oben gezeigt

(n0 − 2)2 = 42 = 16 > 6,

d.h. die Aussage ist korrekt für n0 = 6.

(ii) Induktionsschritt. Angenommen, die Aussage sei für ein beliebiges n ∈ N mit n > n0

korrekt.Wirwollen zeigen, dasswir hieraus auch die Korrektheit fürn+1 ∈ N folgern können.Hierzu formulieren wir zunächst die entsprechende Aussage(

(n+ 1) − 2)2

> (n+ 1) (1.1)

Wir formen die linke Seite äquivalent mit der binomischen Formel um((n+ 1) − 2

)2=((n− 2) + 1

)2= (n− 2)2 + 2(n− 2) + 1 = (n− 2)2 + 2n− 3.

Dies setzen wir in (1.1) ein und erhalten((n+ 1) − 2

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

Wir ziehen auf beiden Seiten 2n− 3 ab und erhalten

(n− 2)2 > −n+ 4.

Jetzt nutzen wir die Induktionsannahme, also die Annahme, dass die Aussage (n − 2)2 > n

korrekt ist. Unter dieser Annahme erhalten wir

n > −n+ 4 ⇒ 2n > 4 ⇒ n > 2, (1.2)

was für n > n0 = 6 sicher richtig ist.

Wir haben also gezeigt, dass die Behauptung

(n− 2)2 > n

für alle n ∈ Nmit n > 6 korrekt ist. Aber was ist mit n ∈ {1, 2, 3, 4, 5}? Wir könnten versuchen,die entsprechende Aussage auch für diese kleineren Werte zu beweisen.

Beweis. (Beweisversuch für Behauptung 1.1 im Fall n0 = 1) (i) Induktionsanfang. Für n0 = 1 gilt

(n0 − 2)2 = (−1)2 = 1 > 1,

d.h. die Aussage ist korrekt für n0 = 1.

2 ©2018,2019 Thomas Richter

Page 9: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.1. Vollständige Induktion

(ii) Induktionsschritt. Wir gehen wieder davon aus, dass die Aussage für ein n ∈ N mit n >n0 = 1 korrekt ist und wir wollen die Korrektheit der Aussage für n + 1 folgern. Alle Schrit-te aus dem vorherigen Beweis sind weiterhin gültig. Schließlich, in Schritt (1.2) erhalten wirjedoch die Bedingung

n > 2,

welche jedoch für n = 1 nicht korrekt ist. Wir können somit keinen Induktionsschritt n = 1 7→2 = n+ 1 durchführen.

Der Beweis schlägt fehl. Wir können nicht beweisen, dass die Aussage für alle n > 1 gültig ist.

Wir setzen die Untersuchung mit n0 = 2 fort.

Beweis. (Beweisversuch für Behauptung 1.1 im Fall n0 = 2) (i) Induktionsanfang. Für n0 = 2 gilt

(n0 − 2)2 = (0)2 = 0 6> 2.

Der Induktionsanfang ist bereits nicht korrekt, d.h. die Aussage kann nicht für alle n > 2stimmen.

Das gleiche Ergebnis erhalten wir für n0 = 3, denn (n0 − 2)2 = 13 6> 3. Für n0 = 4 schließlichgelingt es, sowohl Induktionsanfang als auch Induktionsschritt zu beweisen. Dieses einfacheeinführende Beispiel zeigt, dass es bei der vollständigen Induktion in der Tat wesentlich ist,dass sowohl Induktionsanfang A(n0), als auch eine fortlaufende Kette A(n) ⇒ A(n + 1) fürn > n0 gezeigt werden. Dann ist die vollständige Induktion ein mächtiges Werkzeug, mitwelchem eine Sequenz aus unendlich vielen Aussagen oft sehr einfach bewiesenwerden kann.

Bemerkung 1.2 (Direkter Beweis). Eine weitere Beweistechnik ist die direkte Methode. Hier wirdversucht, die Aussage sofort für alle Fälle, das heißt in unserem Fall für alle Zahlen n ∈ N mit n >n0 = 4 zu beweisen. In diesem Fall erfordert der direkte Beweis bereits einige Kenntnisse der Analysis.Es gilt durch elementare Umformung

(n− 2)2 > n ⇒ n2 − 4n+ 4 > n ⇒ n2 − 5n+ 4 > 0

Der Term auf der linken Seite kann als quadratisches Polynom p(n) aufgefasst werden und lässt sich inLinearfaktoren zerlegen als

p(n) = n2 − 5n+ 4 = (n− 1)(n− 4) > 0

Das Polynom hat also die beiden Nullstellen p(1) = 0 und p(4) = 0. Für n > 4 gilt

n− 1 > 3 > 0 und n− 4 > 0 ⇒ (n− 1)(n− 4) > 0,

da das Produkt positiver Zahlen wieder positiv ist. Hiermit ist die Behauptung bewiesen.

Zum Abschluss geben wir noch ein weiteres einfaches Beispiel für die vollständige Induktionan.

[email protected] 3

Page 10: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.1. Vollständige Induktion

Satz 1.3 (Gaußsche Summenformel). Für n ∈ N giltn∑

i=1

i =n(n+ 1)

2,

wobei wir die Konvention0∑

i=1

i = 0

verwenden.

Beweis. Beweis mit vollständiger Induktion

(i) Induktionsanfang: Für n = 0 gilt

0∑i=1

i = 0, sowie 0(0+ 1)

2= 0,

die Aussage ist somit wahr.

(ii) Induktionsschritt: Angenommen, die Aussage sei für alle n ∈ N wahr. Für n + 1 lautet dieAussage

n+1∑i=1

i =(n+ 1)(n+ 2)

2

Wir formen die linke Seite um, indem wir den größten Summanden abspalten. Es gilt alsoäquivalent

⇔n∑

i=1

i+ (n+ 1) =(n+ 1)(n+ 2)

2.

Nun verwendenwir die Induktionsvorraussetzung, also die angemommene Gültigkeit der Sum-menformel für n. D.h., wir folgern unter dieser Annahme, dass

⇒ n(n+ 1)

2+ (n+ 1) =

(n+ 1)(n+ 2)

2.

Wir formen äquivalent um, in dem wir beide Seiten mit 2 multiplizieren und die Klammernauflösen

⇔ n2 + n+ 2n+ 2 = n2 + 3n+ 2.

Diese Aussage ist für alle n gültig. Wir haben also unter der Annahme der Gültigkeit derSummenformel für n deren Gültikeit für n+ 1 nachgewiesen.

Zusammenmit dem Induktionsanfang für n = 0 schließt dies den Beweis der Summenformel.

Direkter Beweis der Summenformel. Für die Summenformel gibt es auch einen direkten Be-weis. Dieser beruht auf der Beobachtung, dass sich die Summanden paarweise anordnen las-sen. Für gerade n = 2mmit einemm ∈ N gilt

n∑i=1

i = 1+ 2+ 3+ · · ·+m+ (m+ 1) + (m+ 2) + · · ·+ 2m

4 ©2018,2019 Thomas Richter

Page 11: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.2. Python - Variablen und Schleifen

In endlichen Summen (n ∈ N ist eine feste natürliche Zahl) dürfen wir die Reihenfolge derSummanden beliebig ändern (Kommutativität der Addition in den natürlichen Zahlen), d.h.wir schreiben

n∑i=1

i = 1+ 2+ 3+ · · ·+m+ 2m+ (2m− 1) + · · ·+ (m+ 1),

oder, wieder mit dem Summensymbol

n∑i=1

i =

m∑i=1

i+

m−1∑i=0

(2m− i).

In der zweiten Summe verschieben wir den Indexn∑

i=1

i =

m∑i=1

i+

m∑i=1

(2m− i+ 1),

und wir fassen beide Summen zusammenn∑

i=1

i =

m∑i=1

i+ (2m− i+ 1) =

m∑i=1

2m+ 1 = m(2m+ 1).

Mit n = 2m alsom = n/2 folgtn∑

i=1

i =n(n+ 1)

2

Es bleibt, als Übung, auch den Fall ungerader n ∈ N zu behandeln.

Dieses Beispiel zeigt, dass der Induktionsbeweis, wenn auch komplexer in seiner Logik, invielen Fällen weit eleganter ist.

1.2. Python - Variablen und Schleifen

Wir stellen uns die Aufgabe, die Fakultät einer ganzen Zahl n ∈ N, also n ∈ {0, 1, 2, 3, . . . }1 zuberechnen.

Definition 1.4 (Fakultät). Es sei n ∈ N mitN = {0, 1, 2, . . . }. Wir definieren

0! := 1 sowie n! :=n∏

i=1

i für n > 1.

mit dem endlichen Produkt von n Zahlen a1, . . . ,an ∈ R, definiert alsn∏

i=1

ai := a1 · a2 · · ·an.

1oder gehört die 0 nicht zu den natürlichen Zahlen? In Büchern findet man beides. Bei uns gehört die 0 zu dennatürlichen Zahlen.

[email protected] 5

Page 12: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.2. Python - Variablen und Schleifen

Wirwollen dieseAufgabe auf demComputer in Python umsetzen. Dabei gehenwir davon aus,dass Python keine Funktion zur Berechnung der Fakultät kennt, sondern nur die Grundre-chenarten,+,−, ·, /. Wir müssen einenAlgorithmus zur Berechnung der Fakultät entwickeln.

Definition 1.5 (Algorithmus). Ein Algorithmus ist eine Vorschrift zum Lösen (oder zur Appro-ximation, also Berechnung einer näherungsweisen Lösung) einer mathematischen Aufgabe. Ein Algo-rithmus besteht aus einzelnen Anweisungen, die

• entweder grundlegende Vorschriften der verwendeten Programmiersprache sind• oder weitere Algorithmen sind, die bereits auf Basis der Programmiersprache definiert wurden.

Ein Algorithmus, der eine mathematische Abbildung f : X→ Y umsetzt, benötigt eine Eingabe x ∈ Xund liefert eine Ausgabe y ∈ Y.

In dieser Vorlesung verwenden wir zur Programmierung die Programmiersprache Python.Neben einigen grundlegenden Informationen in diesem Skript gibt es im Internet eine Vielzahlvon exzellenten (und bestimmt auch eine Vielzahl von sehr schlechten) Skripten, Büchern undTutorien in Python, wir geben hier nur zwei Beispiele [2, 5].

Ein Programm zur Berechnung von n! kann den folgenden Aufbau haben:

• Wir bestimmen die Zahl n ∈ N, z.B. als Eingabe des Benutzers

• Wir berechnen Stück für Stück das Produkt der Zahlen von 1 bis n, also zunächst 1, dann1 · 2, dann 1 · 2 · 3, usw

Die Zahl n ∈ N und auch die Zwischenwerte 1! = 1, 2! = 1 · 2 speichern wir in Variablen.

Definition 1.6 (Variablen und Datentypen (in Python)). Eine Variable ist eine Bezeichnung füreinen Speicherplatz, der im Programmablauf verschiedene Werte annehmen kann.Variablen unterscheiden sich in der Art ihres Datentyps. Datentypen können ganze Zahlen, Fließ-kommazahlen zur Approximation von reellen Zahlen, Zeichenketten und vieles mehr sein. Wir be-trachten zunächst nur Zahlen.

Die wichtigste Operation ist die Zuweisung

1 x=4

2 y=1/2

3 z=8∗x−y

Das = ist keine mathematische Gleichheit sondern als Zuweisungsoperator zu lesen: Zunächst wirdder Ausdruck auf der rechten Seite ausgewertet, im Anschluss wird das Ergebnis in die Variable auf derlinken Seite geschrieben.

Variablen können erst verwendet werden, nachdem sie deklariert wurden, also mit einem ers-ten Wert initialisiert wurde. Die Zuweisung

1 x=x+1

funktioniert erst, nachdem x einen Wert erhalten hat. In Python können auch mehrere Wertegleichzeitig zugewiesen werden, z.B.

6 ©2018,2019 Thomas Richter

Page 13: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.2. Python - Variablen und Schleifen

1 x,y=4,6

2 x,y=y,x

Aber Vorsicht, es werden zunächst alle Ausdrücke der rechten Seite ausgewertet. Erst im An-schlusswerden die Variablen auf der linken Seite neu definiert.WelchenWert haben x,y,z nachEnde des folgenden Programms?

1 x,y=4,6

2 x,y,z=y,x,x−y

Zur Berechnung der Fakultät müssen nMultiplikationen durchgeführt werden, dabei soll derAlgorithmus für beliebige Werte von n ∈ N das richtige Ergebnis liefern. Ein zentrales Ele-ment von Programmiersprachen sind Schleifen zur wiederholten Ausführung von einfachenOperationen.

Definition 1.7 (while - Schleife).

1 while BED:

2 Anweisung 1

3 Anweisung 2

4 ...

5 Anweisung n

Falls die Bedingung BED in Zeile 1wahr ist, so werden die Anweisungen 1 bis n in den kommenden Zei-len alle ausgeführt und im Anschluss springt das Programm wieder in Zeile 1. Falls die Bedingung BEDin Zeile 1 falsch ist, so wird das Programm hinter dem Block mit den Anweisungen 1 bis n fortgeführt.In Python muss die Bedingung durch ein “:” abgeschlossen werden. Weiter gehören in Python all die-jenigen Anweisungen zur while-Schleife, die durch ein gleichmäßiges Einrücken markiert sind.Definition 1.8 (Bedingungen). Bedingungen können z.B. einfache mathematische Vergleiche sein

• x<y ist wahr genau dann, wenn x echt kleiner als y ist• x>y ist wahr genau dann, wenn x echt größer als y ist• x<=y ist wahr genau dann, wenn x kleiner oder gleich y ist• x>=y ist wahr genau dann, wenn x größer oder gleich y ist• x==y ist wahr genau dann, wenn x gleich y ist• x=y! ist wahr genau dann, wenn x ungleich y ist• True ist immer wahr• False ist immer falsch

Definition 1.9 (Logische Verknüpfungen). Bedingungen können mit den logischen Schlüsselwor-ten and, or und not verknüpft werden. Dabei sind die Verknüpfungen zu lesen als “und”, “oder” und“nicht”. Einige Beispiele für Bedingungen

• Ein Monat ist größer gleich 1 und kleiner gleich 12(jahr >= 1)and (jahr <= 12).

[email protected] 7

Page 14: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.2. Python - Variablen und Schleifen

• Der Eintritt ist frei für Kinder bis 6 Jahre sowie für Rentner ab 67 Jahren sowie für alle, die heuteGeburtstag haben(alter < = 6)or (alter > 66)or (heutegeburtstag)

• Bedingungen können beliebig geschachtelt werden: der Eintritt ist frei für alle zwischen 10 und20 Jahren die heute keinen Geburtstag haben sowie für alle Kinder bis 6 Jahren(alter <= 6)or ( ( (alter>=10)and (alter<=20))and not(heutegeburtstag))

• Der Eintritt ist frei für Kinder bis 6 Jahre sowie für Menschen die größer sind als 180cm(alter <=6 )or (groesse >= 180)

• Der Eintritt ist frei, für Kinder bis 6 oder für Menschen die größer sind als 180cm, nicht aber fürKinder bis 6, die größer als 180cm sind (sogenanntes exklusives oder) ( (alter <=6)and not(↪→ groesse >= 180))or ( not(alter<=6)and (groesse >= 180))

Auch wenn viele der verwendeten Klammern nicht notwendig sind (in Python binden die logi-schen Verknüpfungen schwach) sollten Klammern zur besseren Lesbarkeit und zum Kennzeich-nen logischer Blöcke verwendet werden.

Beispiel 1.10 (while - Schleife). Man überlege sich die Wirkung der folgenden while-Schleifen.Dabei sind die Blöcke jeweils getrennt voneinander zu betrachten.

1 n=1

2 while n<10:

3 n=n+1

4 print(n)

5

6 n=1

7 while n<10:

8 n=n+1

9 n=2∗n10 print(n)

11

12 n=1

13 while n<10:

14 n=n+1

15

16 n=2∗n17 print(n)

18

19 n=1

20 while n<10:

21 n=n+1

22 n=2∗n23 print(n)

Wird eine Variable einfach als Anweisung angegeben (z.B. Zeile 4, 10, usw) so wird der Wertder Variable ausgegeben. Mit diesen Bausteinen können wir einen ersten Programm zur Be-rechnung der Fakultät erstellen

8 ©2018,2019 Thomas Richter

Page 15: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.2. Python - Variablen und Schleifen

Algorithmus 1.1: Einfache Berechnung der Fakultät1 # Berechnung der Fakultaet

2 n=10 # Eingabe

3

4 i = 1 # Zaehlvariable

5 fak = 1 # Zwischenergebnis i! sowie Endergebnis n!

6

7 while i <= n:

8 fak = fak∗i # Zwischenergebnis i! berechnen als (i−1)! ∗ i9 i = i+1 # i weiterzaehlen

10

11 print(fak) # Ausgabe

Bemerkung 1.11 (Kommentare). Das Symbol # leitet in Python einen Kommentar ein. Ab diesemSymbol wird der Text nicht als Programmbestandteil interpretiert. Zu besseren Lesbarkeit - auch zumErklären der praktischen Programmieraufgaben - solltenmöglichst viele Kommentare verwendet werden,um das Programm zu erklären.

Was fehlt? Wir können die Eingabe im Programmablauf berechnen und wir können die Aus-gabe schöner gestalten.

WichtigAnweisungen in Programmiersprachen sind Ein- undAusgabeoperationen. In Pythondient die Funktion print der Ausgabe. Einige Beispiele

1 print(’Hallo!’)

2

3 print(’Die Fakultaet von ’,n,’ ist ’,n,’!=’,fak)

Die Eingabe erfolgt in Python mit der Funktion input. Normalerweise entscheidet Pythonselbst, welcher Datentyp gemeint ist. Die Funktion input ist hier eine Ausnahme. Zum einleseneiner ganzen Zahl verwenden wir

1 n = int(input(’Bitte eine ganze Zahl eingeben: ’))

Dabei steht int für integer, hiermit werden ganze Zahlen bezeichnet.

Satz 1.12. Der folgende Algorithmus berechnet für jede Zahl n ∈ N die Fakultät n! und liefert nacheiner endlichen Anzahl von Schritten das Ergebnis:

1 # Berechnung der Fakultaet

2 n = int(input(’Bitte natuerliche Zahl eingeben: ’)) # Eingabe

3

4 fak = 1 # (Zwischen)−Ergebnis und Laufvariable5 i = 1

6 while i <= n: # Abbruch der Schleife?

7 fak = fak∗i # i! berechnen

8 i = i+1 # i weiterzaehlen

9

10 print(’Die Fakultaet von ’,n,’ ist ’,fak) # Ausgabe

[email protected] 9

Page 16: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.2. Python - Variablen und Schleifen

Beweis. Der formale Beweis der Korrektheit eines Programms ist nicht einfach und erforderteine vertiefte Einführung in die theoretische Informatik2. Wir geben hier nur eine “Beweis-idee”, die auf dem folgenden Ansatz beruht: Wir definieren klare Vorbedingungen, die zuBeginn des Algorithmus gelten sollen. Im Anschluss teilen wir den Algorithmus in verschie-dene Bereiche, gekennzeichnet durch Zeilennummern, auf. Wir formulieren und BeweisenFolgerungen, die für das Programm jeweils nach einem Abschnitt gelten.

Vorbedingung Wir gehen davon aus dass die in Zeile 2 eingelesene Zahl zulässig ist, alson ∈ NmitN = {0, 1, 2, . . . }.

Folgerung 1: in Zeile 5 des Programms gilt

(1.a) i 6 n oder (fak = n! sowie i = n+ 1)

(1.b) fak = (i− 1)!

Es gilt i==1 sowie fak==1. Wegen (1− 1)! = 0! = 1 ist Bedingung (1.b) somit wahr.

ZumNachweis von Bedingung (1.a)machen wir eine Fallunterscheidung.Wir wissen aus derVorbedingung, dassn ∈ N alson ist eine ganze Zahl, insbesonderen > 0. Angenommen nun,Fall 1, es gilt n = 0. Dann ist 1 = i 6 n = 0 falsch, aber fak = 1 = 0! sowie 1 = i = n+1 = 0+1wahr. Fall 2, es gilt n > 1, dann ist 1 = i 6 n stets wahr. Somit gilt in beiden (also in allen,denn n kann nur > 0 oder = 0 sein) Fällen Bedingung (1.a).

Folgerung 2: in Zeile 6. des Programms gelten stets (d.h. vor sowie nach Ausführung derZeilen 7 und 8) die beiden folgenden Invarianten:

(2.a) i 6 n oder (fak = n! sowie i = n+ 1)

(2.b) fak = (i− 1)!

Wir führen den weiteren Beweis per Induktion nach der Laufvariable i.

(i) Induktionsanfang.Wegen Folgerung 1 gelten die beide Bedingungen vor dem ersten Durch-lauf der Schleife, denn (1.a) und (2.a) sowie (1.b) und (2.b) sind die gleichen Bedingungen.

(ii) Induktionsschritt. Es gelten die Bedingungen (2.a) und (2.b) in Zeile 6 vor Ausführung derZeilen 7 und 8. Wir wollen zeigen, dass die Bedingungen auch nach Durchlauf der Zeilen 7und 8 noch gelten. Wir machen die folgende Fallunterscheidung

1. Es gilt i > n, die Zeilen 7 und 8 werden nicht ausgeführt. D.h., Bedingungen (2.a) und(2.b) gelten weiterhin.

2. Es gilt i 6 n, die Zeilen 7 und 8 werden ausgeführt und es gilt zunächst fak = (i − 1)!sowie i 6 n. Wir gehen beide Zeilen durch. In Zeile 7 wird die rechte Seite ausgewertet,also gilt mit Verwendung von (2.b) (Induktion)

fak · i = (i− 1)! · i = i!

und das Ergebnis wird der Variable fak zugeordnet. Es gilt nach Zeile 7 somit fak = i!.2Interessierte finden im Internet (und natürlich der Bibliothek) entsprechende Literatur. Stichwörter sind z.B.formale Programmverifikation, denotationelle Semantik, Hoare Kalkül

10 ©2018,2019 Thomas Richter

Page 17: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.2. Python - Variablen und Schleifen

in Zeile 8 wird in der rechten Seite i um eins erhöht. Somit gilt bei Verwendung (2.a)(Induktion)

i+ 1 6 n+ 1

Das Ergebnis wird der Variable i zugeordnet, somit gilt im Anschluss einerseits

i 6 n+ 1

sowiefak = (i− 1)!

d.h. auf jeden Fall ist (2.b) wahr. Weiter folgt aus i 6 n + 1 mit der Fallunterscheidung:Fall 1, i 6 n dass (2.a)wahr ist und mit Fall 2, i = n+ 1 das ebenso (2.a)wahr ist, dennnun ist fak = (i− 1)! = n!.

TerminierungWirmüssen noch nachweisen, dass derAlgorithmus eine endliche Laufzeit hat.Zeilen 1-5 sowie 10 werden nur genau einmal ausgeführt. Wir müssen nachweisen, dass dieSchleife in Zeilen 6-8 nach einer endlichen Zahl von Schritten abbricht.

Behauptung: Nachm ∈ N Durchläufen der Schleife gilt i = m+ 1. Wir beweisen dies schnellmit Induktion. Zu Beginn, d.h. nach 0 Durchläufen gilt i = 1. Es sei nun für einm die Aussagewahr, d.h. es gelte i = m + 1. Nach einem weiteren Durchlauf wird in Zeile 8, nur diese istrelevant, da Zeile 7 die von uns betrachteten Variablen m und i nicht ändert, die rechte Seitei+ 1 berechnet. Nachm+ 1 Durchläufen gilt also wieder

(i+ 1) = (m+ 1) + 1 ⇒ i = m+ 1.

Hiermit ist die Aussage i = m + 1 bewiesen. Nach m = n Schritten gilt i = n + 1 und dieSchleife bricht in Zeile 6 ab, d.h. das Programm terminiert.

Im Beweis haben wir bereits eine Schwäche des Programms kennengelernt. Wir prüfen nicht,ob die Eingabe n zulässig ist, d.h. ob n eine natürliche Zahl ist. Wir könnten das Programmverbessern, indem wir bei falscher Eingabe mit einer Fehlermeldung reagieren.

if-AnweisungAbfragen, die denweiteren Programmablauf je nach Ergebnis steuern sind zen-tral in jeder Programmiersprache. Einige Beispiele in Python:

1 if BED1:

2 Anweisungen 1

3

4 if BED2:

5 Anweisungen 2

6 else:

7 Anweisungen 3

8

9 if BED4:

10 Anweisungen 4

11 elif BED5:

12 Anweisungen 5

13 else:

14 Anweisungen 6

[email protected] 11

Page 18: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.2. Python - Variablen und Schleifen

Die if-Anweisung prüft die Bedingung. Ist die Bedingung wahr, so werden die Anweisungenim folgenden eingerückten Block ausgeführt. Ansonsten wird dieser Block übersprungen unddas Programm geht im Anschluss weiter.

In Python gibt es die optionalen Blöcke else: sowie elif: Der else: Block kann als ansonstengelesen werden. D.h.: ist die Bedingung wahr, so wird der Block gleich nach if: ausgeführt,ist die Bedingung aber nicht wahr, dann wird der Block nach else: ausgeführt. Die weiterenelif: Blöcke (elif für “else if”) können als weitere if-Anweisungen Verstandenwerden. D.h. imletzten Beispiel wird zunächst BED4 geprüft, ist diese wahr, so werden die Anweisungen 4 durch-geführt. Ist BED4 aber falsch, so wird geprüft ob BED5wahr ist. In dem Fall werden Anweisungen 5durchgeführt. Ist auch BED5 falsch, so (und nur dann) werden Anweisungen 6 durchgeführt.

Wir könnten unser Programm also erweitern und die Eingabe ergänzen mit

1 n = int(input(’Bitte natuerliche Zahl eingeben: ’)) # Eingabe

2

3 if n < 0:

4 print(’Fehlerhafte Eingabe. Negative Zahlen sind nicht erlaubt’)

5 print(’Keine Garantie fuer den Programmablauf!’)

Wenn die Eingabe nicht zulässig ist, so wäre es natürlich sinnvoll, das Programm unmittel-bar zu beenden. Die meisten modernen Programmiersprachen bieten hierfür einen einfachenMechanismus, Assertion genannt.

1 n = int(input(’Bitte natuerliche Zahl eingeben: ’)) # Eingabe

2

3 assert n>=0, ’Fehlerhafte Eingabe. Negative Zahlen sind nicht erlaubt’

Falls dieAssertion, also Behauptungwahr ist, so läuft das Programm einfachweiter. Falls dieAs-sertion jedoch falsch ist, so bricht das Programm ab mit einer Fehlermeldung. Einerseits wirddie selbst definierte Fehlermeldung ausgegeben, darüber hinaus gibt Python weitere Infor-mationen an, z.B. die Zeilennummer, um die Fehlersuche zu vereinfachen. Bei komplexerenProgrammen macht es Sinn, den Befehl assert umfassend zu verwenden.

Neben der while-Schleife ist die zweite wichtige Struktur die for-Schleife.

Die while-Schleife führt einen Programmblock aus, solange eine bestimmte Bedingung wahrist. Die for-Schleife durchläuft einen Programmblock für alle Elemente einer Liste. Ein Bei-spiel

1 for i in [0,1,2,4,8]:

2 print(i)

Die Variable i nimmt nacheinander und in der vorgegebenen Reihenfolge die Werte der Liste[0,1,2,4,8] an und für jeden dieser Werte wird der folgende eingerückte Block durchgeführt.Oft nutzt man die for-Schleife zum Durchlaufen aller Zahlen eines bestimmten Bereichs. Hierstellt Python den praktischen Befehl range zur Verfügung. Man teste die folgenden kurzenSchleifen

12 ©2018,2019 Thomas Richter

Page 19: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.2. Python - Variablen und Schleifen

1 for i in range(0,10):

2 print(i)

3

4 for i in range(5,9):

5 print(i)

6

7 for i in range(5,1):

8 print(i)

9

10 for i in range(5,1,−1):11 print(i)

12

13 for i in range(0,0.3,1):

14 print(i)

range(a,b,s) liefert Zahl für Zahl Werte a, a+ s, a+ 2s bis die Zahl b nicht erreicht wird. Ist spositiv, so ist die letzte Zahl < b, ist die Zahl s negativ, so ist die letzte Zahl > b.

Listen sind ein weiterer sehr allgemeiner Datentyp in Python. Wir verwenden zunächst nurListen von Zahlen. Diese werden als

1 L = [1,4,20,−5,4]

definiert. Listen können z.B. in for-Schleifen durchlaufen werden. Python stellt viele Befehlebereit um Listen zu modifizieren. Wir gehen davon aus dass durch L eine Liste gegeben ist,unter Umständen die leere Liste L=[].

• len(L) gibt die Anzahl der Element in L zurück.

• L[i], wobei i eine Zahl zwischen 0 und len(L)−1 ist, gibt den i-ten Eintrag der Liste zu-rück. Dabei zählt Python ab der 0.

• L[i,j], wobei i und j Zahlen zwischen 0 und len(L)−1 sind, gibt die Teilliste von Elementi bis Element j− 1 zurück. Bei i > jwird die leere Liste zurückgegeben.

• L.append(x) hängt an die Liste hinten denWert x an. Dies kann auch durch L[len(L):]=[x]erreicht werden.

• L.append(M) hängt an die Liste L hinten die gesamte zweite Liste M an. Dies kann auchdurch L[len(L):]=M erreicht werden.

• L.insert(i,x) fügt das Element x an der Position i ein (das neue Element x steht im An-schluss an der i-ten Stelle).

• L.remove(x) entfernt das erste Auftreten des Element x in L und gibt einen Fehler zurück,falls L das Element x gar nicht enthält.

• L.count(x) gibt zurück, wie oft das Element x in der Liste L enthalten ist.

• L.index(x) gibt den Index zurück an dem das Element x zum ersten mal in der Liste Lauftaucht. Ist das Element nicht enthalten, so wird ein Fehler zurückgegeben.

[email protected] 13

Page 20: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.3. Funktionen

Wir können die for-Schleife nutzen um die Fakultät zu berechnen.

Algorithmus 1.2: Berechnung der Fakultät mit for-Schleife1 # Berechnung der Fakultaet

2 n = int(input(’Bitte natuerliche Zahl eingeben: ’)) # Eingabe

3 assert n>=0,’Nur positive Zahlen!’

4

5 fak = 1 # (Zwischen)−Ergebnis6

7 for i in range(1,n+1): # Schleife von 1 bis n (einschliesslich)

8 fak = fak∗i # i! berechnen

9

10 print(’Die Fakultaet von ’,n,’ ist ’,fak) # Ausgabe

Da die Funktion range(a,b) die Werte von a bis b− 1 liefert, müssen wir hier die Grenze n+ 1verwenden.

1.3. Funktionen

Einwichtiges Strukturelement in Programmiersprachen sind Funktionen. Sie erlauben es, Ein-heiten zusammenzufassen, um sie z.B. mehrfach auszuführen, einmal programmierte Blöckein verschiedenen Programmen zu nutzen, oder einfach bessere Struktur zu schaffen. Der Syn-tax in Python ist wie folgt

Funktionen in Python Funktionen werden in Python folgendermaßen definiert:

1 def funktionsname(Param1, Param2, ...):

2 Anweisung 1

3 Anweisung 2

4 ...

5 return rueckgabe

Das Schlüsselwort def startet die Funktionsdefinition, gefolgt von einemNamen der Funktion.Eine Funktion kann (muss aber nicht) Parameter erhalten. Im folgenden eingerückten Blockwerden Anweisungen ausgeführt und schließlich kann (muss aber nicht) eine Rueckgabe mitdem Schlüsselwort return geliefert werden.

Einmal definiert, können Funktionen über ihren Namen aufgerufen werden.

Wir setzen dies am Beispiel der Fakultät um:

Algorithmus 1.3: Funktion zur Berechnung der Fakultät1 def fakultaet(n): # Definition einer Funktion mit einem Parameter n

2 assert n>=0,’Nur positive Zahlen!’

3

4 fak = 1 # ab hier Berechnung Fakultaet

5 for i in range(1,n+1):

6 fak = fak ∗ i

14 ©2018,2019 Thomas Richter

Page 21: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.3. Funktionen

7 return fak # Rueckgabe des Ergebnis

8

9 ### Jetzt sind ausserhalb der Funktion

10 ### Ausgabe von 0!, 1!, ..., 8!

11 for i in range(0,9):

12 print(’Die Fakultaet von ’,i,’ ist ’,fakultaet(i))

Funktionen können sich gegenseitig und auch sich selbst aufrufen. Der Selbstaufruf einerFunktion ist ein wichtiges Element der Programmierung, Rekursion genannt.

Definition 1.13 (Rekursion). Unter einer Rekursion versteht man den Selbstaufruf einer Funktion.Ein rekursiv verwendete Funktion bedarf stets einer Verankerung, die zumAbbruch der Rekursion führtund so eine Endlosschleife verhindert.

Rekursion eignet sich z.B. zur Berechnung der Fibonacci-Zahlen, definiert als

f0 := 1, f1 := 1 sowie für n > 2 gilt fn := fn−1 + fn−2,

umgesetzt als

Algorithmus 1.4: Rekursive Berechnung der Fibonacci-Zahlen1 def fibonacci(n):

2 if n <= 1:

3 return 1

4 else:

5 return fibonacci(n−1) + fibonacci(n−2)

Für die Werte n = 0 und n = 1 wird als Ergebnis 1 zurückgegeben. Ansonsten ruft sich dieFunktion gleich zweimal rekursiv selbst auf. Die bisherige Programmierweise wird iterativgenannt, der Programmtext wird Zeile für Zeile hintereinander aufgerufen.

Auch am Beispiel der Fakultät lässt sich die Rekursion gut einsetzen. Denn neben der Defini-tion

n! =

n∏i=1

i

können wir die Fakultät auch rekursiv einführen:

0! = 1 sowie für n > 1 n! = (n− 1)! · n.

Die Fakultät von n ist die Fakultät von n− 1 multipliziert mit n.

Algorithmus 1.5: Rekursive Berechnung der Fakultät1 def fakultaet(n): # rekursive Definition der Fakultaet

2 if n < 0: # Zulaessig?

3 print(’nur positive Zahlen’)

4 return −15 elif n == 0: # Verankerung der rekursiven Definition

6 return 1

7 else: # Rekursion

8 return fakultaet(n−1)∗n

[email protected] 15

Page 22: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.3. Funktionen

Satz 1.14 (Fakultät mit Rekursion). Die in Algorithmus 1.5 berechnet für jede Eingabe n ∈ N dieFakultät in endlich vielen Schritten.

Beweis. Wir führen den Korrektheitsbeweis per Induktion.

• Für n < 0 ist die Bedingung in Zeile 2 wahr, es werden also Zeilen 3 und 4 ausgeführt.Das Programm liefert korrekterweise eine Fehlermeldung und die Funktion wird mitdem Ergebnis −1 beendet.

• Behauptung: Für n = 0 wir das korrekte Ergebnis, also 0! = 1 berechnet. Beweis: Diesist die eigentliche Verankerung der Induktion. Die Bedingung in Zeile 2 ist falsch, derfolgende Block wird nicht ausgeführt. Die Bedingung in Zeile 5 ist wahr, es wird Zeile 6ausgeführt und die Funktion liefert das korrekte Ergebnis 0! = 1.

• Annahme:Die Funktion berechnet für alle Eingaben 0, 1, . . . ,n−1das korrekte Ergebnis,also die Fakultät von n−1. Behauptung:Unter dieser Annahme berechnet die Funktiondas korrekte Ergebnis für die Eingabe n.

Beweis: Es sei nun n > 0 beliebig gegeben. Die Bedingungen in Zeilen 2 und 5 sindfalsch, d.h. die Blöcke in Zeilen 3-4 sowie in Zeile 6 werden nicht ausgeführt. Stattdessenwird der else:-Block, also Zeile 8 ausgeführt. Hier wird der Ausdruck Fakultaet(n−1)∗↪→ n ausgewertet. Laut Induktion gilt Fakultaet(n−1)=(n−1)!. Es wird also das korrekteErgebnis (n− 1)!n = n! zurückgegeben.

Es bleibt, die Terminierung des Programms für beliebige Eingaben nachzuweisen.Wirmachenwieder eine Fallunterscheidung.

• Für n < 0 sowie für n = 0wird die Funktion mit einer festen Rückgabe beendet.

• Fürn > 0 ruft sich die Funktion rekursivmit demEingabewertn−1 auf.Nachn Schrittenwird Fakultaet(0) aufgerufen und die Berechnung bricht ab.

Bemerkung 1.15 (Rekursion). Rekursion kann sehr elegantes Programmieren ermöglichen.Das zeigtzum Beispiel die sehr kurze Funktion zur Berechnung der Fibonacci-Zahlen. Rekursion hat jedoch ofteinen erheblichenNachteil: sie kann inHinsicht auf Rechenzeit und auch auf Speicherbedarf zu enormenAufwand führen. Man versuche z.B. mit dem Programm oben größere Fibonacci-Zahlen zu berechnen.Wir werden diese Aspekte - Laufzeit und Speicherbedarf - noch eingehend untersuchen.

Bemerkung 1.16 (Bezeichnung von Variablen und Funktionen in Python). Im Gegensatz zueiner normal Sprachemit einemSatz an grammatikalischenRegeln und einemüblicherweise großen aberrecht festem Vokabular leben Programmiersprachen davon, dass ihr Umfang durch selbst geschriebeneFunktionen stets erweitert werden kann. Wir haben schon verschiedene Funktionen zur Berechnung derFakultät oder zur Berechnung der Fibonacci-Zahlen geschrieben.Mit dem Schlüsselwort def könnenwirden Funktionen dabei einen beliebigen Namen geben. Damit die Programme auch im Nachhinein undvon anderen Nutzern verstanden werden können, gibt es Regeln, die (meist) nicht verpflichtend imSinne der Funktionalität, aber im Sinne der Lesbarkeit eingehalten werden sollten:

• Funktionen und auch Variablen dürfen keine Namen benutzen, die bereits reserviert sind, z.B.print, if, else, ...

16 ©2018,2019 Thomas Richter

Page 23: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.4. Exkurs: Aussagenlogik

• Für Funktionsnamen undVariablennamenwerden nurKleinbuchstaben verwendet, etwa ergebnis↪→ = 5, def fakultaet(n):

• Längere Namen werden durch einen _ verknüpft, etwa def fakultaet_rekursiv(n):

Eine ausführliche Darstellung von Regeln guter Programmierung findet sich im Python Style Gui-de [7].

1.4. Exkurs: Aussagenlogik

Wir geben eine stark verkürzte Einführung in die Aussagenlogik. Unter eine Aussage verste-hen wir dabei einen Satz, der üblicherweise wahr oder falsch sein kann. Hier beginnt jedochbereits das erste Problem. Die Aussage Alle Hunde sind weiß ist sicher falsch, der Satz Hundesind Tiere ist wahr. Aber der Satz im Februar schneit es ist nicht entscheidbar. Wir interessierenuns hier aber weniger für die Anwendung der Logik auf Sätze unserer Sprache - dies ist einewichtige Disziplin in der Philosophie - sondern uns geht es um einige Grundlagen der Logikzum besseren Verständnis von Algorithmen.

Wir gehen nun formaler vor und bezeichnen unter einer elementaren Aussage oder einer ele-mentaren Formel einen Ausdruck, der entweder wahr oder falsch sein kann, stellen uns abernicht die Frage, warum. Wir benennen solche Formeln mit Großbuchstaben.

Definition 1.17 (Junktor). Ein Junktor ist eine logische Verknüpfung zwischen Aussagen. Wir de-finieren die Wirkung eines Junktors durch die vollständige Angabe einer Wahrheitstafel. Die für unswesentlichen Junktoren sind

• die Negation ¬A, gesprochen nicht A,

• die Konjunktion A∧ B, gesprochen A und B,

• die Disjunktion A∨ B, gesprochen A oder B,

• die Implikation A→ B, gesprochenwenn A dann B,

• die Äquivalenz A↔ B oder A = B, gesprochen A genau dann wenn B

mit den Wahrheitstafeln

A B ¬A A∧ B A∨ B A→ B A↔ B

w w f w w w ww f f f w f ff w w f w w ff f w f f w w

Eine Merkregel: und ∧ ist unten offen, oder ∨ ist oben offen.

[email protected] 17

Page 24: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.4. Exkurs: Aussagenlogik

Imwesentlichen sind die Junktoren definiert, wiewir es im Sprachgebrauch erwartenwürden.Einzig die Implikation erfordert ein wenig Nachdenken, aus einer falschen Aussage kann jedebeliebige Aussage gefolgert werden. Ein einfaches Beispiel: AussageA sei es gilt 0 = 1, AussageB sei es gilt n = n+ 1 für alle Zahlen n ∈ N. Wir folgern mathematisch korrekt A→ B, denn

0 = 1∣∣∣+ n ⇒ 0+ n = n = 1+ n.

Der Schluss A → B ist mathematisch korrekt, das Ergebnis jedoch nicht. Warum? Weil dieAussage A schon falsch war. Dieses Prinzip findet z.B. bei der Beweistechnik der InduktioneineAnwendung.Wir folgern stetsAussagenA(n)→ A(n+1)und gehen stets davon aus, dassdie Aussage A(n) bereits bewiesen ist. Den Induktionsanfang, also den Beweis der WahrheitvonA(1) benötigenwir umdie Schlusskette zu verankern. ZurVerknüpfungAussagenwerdenKlammern verwendet, etwa

(A∧ (B∨ ¬C))→ (C↔ (A∧ ¬D)).

Dabei verwenden wir die Konvention, dass die Negation ¬ stärker bindet als die sonstigenJunktoren, es gilt also stets

(¬A)∧ B↔ ¬A∧ B,

aber im Allgemeinen¬A∧ B 6↔ ¬(A∧ B).

(Es kann natürlich einzelne Belegungen von A und Bmit wahr oder falsch geben, so dass auchdiese Äquivalenz wahr ist).

Definition 1.18 (Erfüllbare und unerfüllbare Aussagen, Tautologien). Es sei A eine aus den nelementaren Aussagen A1, . . . ,An verknüpfte Aussage. Wir nennen A

• erfüllbar, falls es mindestens eine Kombination von Wahrheitswerten (A1, . . . ,An) ∈ {w, f}n

gibt, so dass die Aussage A wahr ist.• unerfüllbar, falls es keine Kombination von Wahrheitswerten (A1, . . . ,An) ∈ {w, f}n gibt, so

dass die Aussage A wahr ist.• eine Tautologie, falls A wahr ist, unabhängig davon ob die elementaren Aussagen A1, . . . ,An

wahr und falsch sind.Satz 1.19 (De Morgansche Gesetze). Die folgenden Aussagen sind Tautologien

(i) ¬(A∧ B)↔ ¬A∨ ¬B

(ii) ¬(A∨ B)↔ ¬A∧ ¬B

Beweis. Einfache Tautologien können durch Testen aller verschiedenen Belegungen der ele-mentaren Aussagen nachgewiesen werden.

A B A∧ B ¬(A∧ B) ¬A ¬B ¬(A∨ B)

w w w f f f fw f f w f w wf w f w w f wf f f w w w w

18 ©2018,2019 Thomas Richter

Page 25: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.4. Exkurs: Aussagenlogik

Für beliebigeKombinationenderWahrheitswerte fürAundB stimmendie Ergebnisse überein,es liegt als eine Tautologie vor. Die zweite Äquivalenz ist auf die gleiche Art zu zeigen.

Man nennt diesen Zugang, das Testen aller mögliche Kombinationen einen brute-force Zugang.Beweise mittels brute-force lassen sich einfach mit Computerunterstützung durchführen. Einentsprechender Algorithmus in Python-Pseudocode (kein lauffähiges Programm) könnte fol-gendermaßen aussehen:

Algorithmus 1.6: Test auf Tautologie1 # Test der Aussage für eine gegebene Belegung

2 def aussage_test(A1, · · ·An):

3 if AussageKorrekt für Belegung A1, . . . ,An:

4 return wahr

5 else:

6 return falsch

7

8 # Rekursive Funktion zum Test aller Belegungen

9 def test_tautologie(i, A1, . . . ,An):

10 # Abbruch negativ, falls Aussage falsch

11 if not(AussageTest(A1, . . . ,Ai−1,w,Ai+1,An)) or not(AussageTest(↪→ A1, . . . ,Ai−1, f,Ai+1,An)):

12 return falsch

13

14 # Abbruch positiv falls alle Kombinationen getestet

15 if i == n:

16 return wahr

17

18 # rekursiver Aufruf

19 return test_tautologie(i+1,A1, . . . ,Ai,w,Ai+2, . . . ,An) and test_tautologie(i

↪→ +1,A1, . . . ,Ai, f,Ai+2, . . . ,An)

20

21 # Aufruf der rekursiven Funktion mit der Belegung

22 # A1, . . . ,An = w.23 ist_tautologie = test_tautologie(1,w, · · · ,w)

Dieser Algorithmus setzt wieder auf das Prinzip der Rekursion. Das Verfahren lässt sich leichtabwandeln zumTest auf Erfüllbarkeit oder nicht-Erfüllbarkeit von Aussagen.Wir werden spä-ter auf diesesVerfahren zurückkommenund es hinsichtlichAufwand analysieren:wie oftwirddie Funktion AussageTest aufgerufen, wenn wir eine Aussage aus n elementaren Aussagen un-tersuchen?

Wir fassen nun noch ohne Beweis einige weitere Tautologien zusammen

[email protected] 19

Page 26: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

1.4. Exkurs: Aussagenlogik

Satz 1.20 (Tautologien). Die folgenden zusammengesetzten Aussagen sind Tautologien:

(1) A→ A

(2) A∨ ¬A

(3) (A→ B)↔ (¬B→ ¬A)

(4) (A∨ (B∨ C))↔ ((A∨ B)∨ C)

(5) (A∧ (B∧ C))↔ ((A∧ B)∧ C)

(6) ¬(¬A) = A

(7) ((A→ B)∧ (B→ C))→ (A→ C)

(8) (A∨ (B∧ C))↔ ((A∨ B)∧ (A∨ C))

(9) (A∧ (B∨ C))↔ ((A∧ B)∨ (A∧ C))

Von besonderer Bedeutung ist (3), das Prinzip des Widerspruchbeweis: die Folgerung wennA dann B ist genau dann richtig, wenn die Folgerung wenn nicht A dann nicht B wahr ist. DieTautologien (4)-(5) entsprechen dem Assoziativgesetz (wir lassen daher gewisse Klammernweg), (6) ist die doppelte Verneinung, (7) die Transitivität der Implikation, (8)-(9) sind Dis-tributivgesetze.

Eine Alternative zum brute-force Verfahren ist das Ausnutzen bereits bekannter TautologienzurVereinfachung zusammengesetzterAussagen.Dieser Zugang lässt sich jedoch nicht immeranwenden und ist weitaus schwerer in einem Algorithmus umzusetzen. Ein Beispiel hierzu.Wir testen die Aussage(

B∧ ¬(((A→ E)∧ (E→ C))→ (A→ C)

))→ ¬

(C∧

(B∨ ¬(D∧A∧ E)

))(1.3)

welche zunächst lang undkompliziert zu sein scheint.Wir identifizieren jedoch (fett unterlegt)die Tautologie (7) aus Satz 1.20. Wir vereinfachen daher (1.3) zu(

B∧ ¬(w))→ ¬

(C∧

(B∨ ¬(D∧A∧ E)

)), (1.4)

verwenden ¬w = f sowie B∧ f = f und fahren fort mit

f→ ¬(C∧

(B∨ ¬(D∧A∧ E)

)), (1.5)

schließlich können wir aus f, also “falsch”, jede Aussage implizieren, also (f→ A)↔ w, somitist gezeigt, dass es sich bei (1.3) um eine Tautologie handelt.

20 ©2018,2019 Thomas Richter

Page 27: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Teil I.

Diskrete Aspekte derAlgorithmischen Mathematik

21

Page 28: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum
Page 29: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2. Sortieren

In diesemKapitel befassenwir unsmit dem Sortierproblem: gegeben sei eine Sequenza1, . . . ,an ∈M von n ∈ N Elementen einer MengeM. Die Aufgabe ist es, diese Elemente in eine bestimmteReihenfolge zu bringen. Hierzu werden wir zunächst den Begriff derOrdnung einführen, wel-ches die Reihenfolgemathematisch definiert. ImAnschluss untersuchenwir verschiedeneMe-thoden zum Sortieren von Sequenzen. Besondere Beachtung wird die Effizienz der Verfahrenfinden. Wie viele Schritte werden benötigt, um eine Sequenz mit n Elementen in die richtigeReihenfolge zu bringen? Und weiter, wie viele Speicher benötigt der Algorithmus, um dieseSortierung durchzuführen?

2.1. Ordnung und erstes Sortierverfahren

Definition 2.1 (Relation). Es seien A,B Mengen. Eine Relation R ist eine Teilmenge der kartesi-schen Produktmenge

A× B = {(a,b) |a ∈ A,b ∈ B}.

Definition 2.2 (Ordnung). Es sei S eine Menge und R ⊂ S× S eine Relation mit den Eigenschaften1. Reflexivität. Es ist (a,a) ∈ R für alle a ∈ S2. Antisymmetrie. Falls (a,b) ∈ R und (b,a) ∈ R dann gilt zwingend a = b

3. Transitivität. Falls (a,b) ∈ R und (b, c) ∈ R dann gilt (a, c) ∈ Rheißt partielle Ordnung. Alternativ zu (a,b) ∈ R schreiben wir a � b.

Falls zusätzlich gilt (a,b) ∈ R oder (b,a) ∈ R für alle a,b ∈ S, dann heißt R eine totale Ordnung.Wir schreiben dann a 6 b.

Beispiel 2.3 (Ordnungen).

• Es seiR = N,R = Z,R = Q oderR = R. Dannwerden6 sowie durch> jeweils totaleOrdnungengegeben.

• Es sei R = N,R = Z,R = Q oder R = R. Dann werden < sowie durch > keine Ordnungengegeben, denn die Reflexivität ist verletzt.

• Es sei R = N,R = Z,R = Q oder R = R. Dann ist durch = eine partielle Ordnung gegeben.Dies ist etwas verwunderlich (und auch keine praktisch sinnvolle Ordnung), aber die Gesetzesind alle erfüllt. Diese Ordnung ist jedoch keine totale Ordnung, da z.B. weder 3 = 5 noch5 = 3 gilt.

23

Page 30: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.1. Ordnung und erstes Sortierverfahren

• Es sei R die Menge aus Obstkörben, in denen jeweils entweder jeweils Birnen oder Äpfel liegen.Für zwei Körbe K1,K2 ∈ R definieren wir die partielle Ordnung

K1 � K2 ⇔

K1 und K2 Apfelkörbe und es gilt: Anzahl in K1 6 Anzahl in K2

K1 und K2 Birnenkörbe und es gilt: Anzahl in K1 6 Anzahl in K2

Ein Korb K1 ist also kleiner als ein Korb K2, wenn in beiden Körben entweder nur Äpfel odernur Birnen sind und wenn in K1 dann weniger (oder gleich viele) Früchte sind wie in K2. Apfelund Birnen können wir nicht vergleichen, daher ist dies keine totale Ordnung

• Es sei R = C. Wir führen die lexikographische Ordnung ein

a,b ∈ C : a 6 b ⇔ (Re(a) < Re(b)) ∨((Re(a) = Re(b)) und (Im(a) 6 Im(b))

).

Eine komplexe Zahl a ∈ C ist also auf jeden Fall kleiner als b ∈ C, wenn ihr Realteil kleiner ist.Sind Realteile gleich, dann entscheidet der Imaginärteil. Dies ist eine totale Ordnung.

• Der Name lexikographische Ordnung kommt daher, da sich diese Ordnung leicht auf Spracheübertragen lässt. Es sei nun S eine Menge von Wörtern. Es seien u,w ∈ S zwei Wörter mit nu

und nw Buchstaben. Es ist durch

u 6 w ⇔(es gibt ein i 6 min(nu,nw) u1 = w1, . . . ,ui−1 = wi−1 und ui < wi

)∨((nu 6 nw) und (ui = wi für alle i = 1, . . . ,nu)

), (2.1)

wobei wir mit ui und wi den i-ten Buchstaben meinen und auf der Menge der Buchstaben dieübliche Ordnung vorgegeben sei. Es gilt also z.B.

Baum 6 Haus, Baum 6 Baumhaus, Baum 66 Baustelle, Baumhaus 66 Baum

Definition 2.4 (Allgemeines Sortierproblem). Gegeben sei eine MengeM mit n ∈ N Elementenund partieller Ordnung �. Gesucht ist eine Bijektion

f : {1, . . . ,n}→M

mit(f(j) 6= f(i))→ (f(j) 6� f(i)) ∀i, j : 1 6 i < j 6 n.

Das allgemeine Sortierproblem ist als Widerspruch formuliert: Falls das Element i vor demElement j steht und falls die Elemente verschieden sind, dann darf das j-te Element nicht vordem i-ten in bzgl. der Ordnung stehen. Bezogen auf die Apfel/Birnen-Ordnung bedeutet dies,dass die Sequenz

(3 Birnen), (22 Äpfel), (8 Birnen), (14 Birnen), (41 Äpfel)

sortiert ist, ebenso wie die Sequenz

(3 Birnen), (8 Birnen), (14 Birnen), (22 Äpfel), (41 Äpfel),

24 ©2018,2019 Thomas Richter

Page 31: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.1. Ordnung und erstes Sortierverfahren

nicht aber die Sequenz

(3 Birnen), (41 Äpfel), (8 Birnen), (14 Birnen), (22 Äpfel).

Denn 2-tes und 5-tes Element stehen bezogen auf die Ordnung falsch zueinander. Über Ele-mente, die nicht in Relation stehen, also Birnen und Äpfel, gibt das Sortierproblem keine An-gaben an die Reihenfolge.

Wir betrachten hierzu den folgenden Algorithmus, Sortieren durch Auswahl, im englischen se-lection sortAlgorithmus 2.1: Selection SortGegeben Liste L sowie eine (partielle) Ordnung

1 def selection_sort(L):

2 n = len(L)

3 for i in range(n):

4 for j in range(i+1,n):

5 if L[j] � L[i]:

6 L[i],L[j] = L[j],L[i]

Satz 2.5 (Sortiereigenschaft von Selection Sort). Algorithmus 2.1 liefert im Ergebnis die eine sor-tierte Liste gemäß Definition 2.4.

Beweis. Wirwerden die Korrektheit des Algorithmusmit vollständiger Induktion nachweisen.Hierzu betrachten wir zunächst den allgemeinen Ablauf. Es erfolgt immer eine Schleife überdie Variable i von 0 bis n− 1, dann eine Schleife über die Variable j von i+ 1 bis hin zu n− 1.Es werden immer alle diese Kombinationen von i und j durchlaufen, unabhängig von mög-lichen Tauschvorgängen in Zeile 6. Wir definieren daher einen Zustand (i, j) in dem sich derAlgorithmus nach Durchlauf der Zeilen 5. und 6., d.h. nach einemmöglichen Tausch befindet.Der Ablauf ist also

(0, 0), (0, 1), . . . , (0,n− 1), (1, 1), (2, 1), . . . , (2,n− 1), . . . , . . . , (n− 1,n− 1).

Im Folgenden beweisen wir mit vollständiger Induktion nach (i, j) (in dieser Reihenfolge),dass die beiden folgenden Bedingungen stets gelten:

(I) L[k] = L[h] oder L[k] 6� L[h] ∀h,k : 0 6 h < i und h < k < n(II) L[k] = L[i] oder L[k] 6� L[i] ∀k : i < k 6 j.

Haben wir Bedingung (II) für alle i = 0, . . . ,n − 1 und j = i + 1, . . . ,n − 1 gezeigt, so folgtnach Definition 2.4 gerade, dass die Liste L sortiert ist.

Induktionsanfang (i, j) = (0, 0) Es ist i = 0 und somit existiert keine natürliche Zahl h ∈ Nmit 0 6 h < 0. Ebenso folgt aus j = 0, dass es keine natürliche Zahl k ∈ N geben kann mit0 < k 6 0. Die Bedingungen sind trivialerweise erfüllt (denn wir stellen ja überhaupt keineForderung).

Induktionsschritt (i, j) 7→ (i, j + 1) Es sei also j < n − 1, ansonsten folgt sofort der Schritt(i, j)→ (i+ 1, i+ 2), der unten gezeigt wird.

[email protected] 25

Page 32: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.1. Ordnung und erstes Sortierverfahren

Wir nehmen also an, dass beide Bedingungen für (i, j) erfüllt sind. In diesem Schritt werdeneventuell die Einträge L[j+1] und L[i] getauscht. Auf jedem Fall gilt nach diesem Schritt

L[j+1] = L[i] oder L[j+1] 6� L[i].

Bedingung (I) hängt nicht von j ab. Hier werden Elemente h < i mit Elementen k in Bezuggesetzt. Der mögliche Tausch von L[i]mit L[j+1] für j > i ändert das Element L[h] sicher nicht,somit bleibt auch die Bedingung erhalten.

Bedingung (II) hängt direkt vom Index j ab. Wir untersuchen hier zwei Fälle:Fall 1: Angenommen, in Zeile 5 galt L[j+1] 6� L[i]. D.h., es ist kein Tausch notwendig. Danngilt mit der Induktionsannahme für (i, j) das

L[k] = L[i] oder L[k] 6� L[i] ∀i < k 6 j

sowie auchL[j+1] 6� L[i],

da wir gerade diesen Fall vorausgesetzt haben. D.h., (i, j)→ (i, j+ 1) gilt.

Fall 2: Angenommen, vor in Zeile 5 galt vor Tausch L[j+1] � L[i]. Dann tauschen wir L[j+1]und L[i]. Wir untersuchen zwei Teilfälle:

Fall 2a: Es sei k = j+ 1. Es gilt nach Schritt 5

L[k] = L[j+1] = L[i]alt,

wobei wir mit L[i]alt den Zustand vor Schritt 5 meinen. Also gilt L[i] = L[j+1]alt � L[i]alt =L[j+1] = L[k]. Wegen der Antisymmetrie folgt nun entweder L[k] = L[i], oder aber, wennL[k] 6= L[i] dann ist L[k] 6� L[i] (es können für verschiedene Elemente nicht gleichzeitigL[k] � L[i] gelten).

Fall 2b: Es sei nun i < k < j+ 1. Aus der Induktionsannahme

L[k] = L[i] oder L[k] 6� L[i]alt 6� L[j+1]alt = L[i].

(Hätten wir eine totale Ordnung, so wäre dies im Fall L[k] 6= L[i] äquivalent zu L[k] >L[i]alt > L[i], also L[k] > L[i], somit L[k] 66 L[i]. Wir haben aber nur eine partielle Ord-nung und brauchen ein weiteres Argument). Angenommen es wäre

L[k] � L[i] = L[j+1]alt � L[i]alt.

Dann folgt mit der Transitivität auch L[k] � L[i]alt im Widerspruch zur Induktionsannahme(II) (da ja k 6 j < j+ 1). Somit L[k] 6� L[i] im Fall L[k] 6= L[i].

(iv) Induktionsschritt (i,n) 7→ (i+1, i+2)Die Reihenfolge der Elemente ändert sich in diesemSchritt nicht. Bedingung (II) ist leer. Der Index i in Bedingung (I) ist um eins größer, d.h. nunist auch h = i zugelassen und wir müssen für diesen Fall zeigen, dass

L[k] 6= L[i] oder L[k] 6� L[h] = L[i] ∀i < k 6= n.

Diese Bedingung ist aber exakt Bedingung (I) zum Index (i,n), d.h. bereits gezeigt.

26 ©2018,2019 Thomas Richter

Page 33: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.1. Ordnung und erstes Sortierverfahren

Bemerkung 2.6 (Call by Reference, call by Value). Die Funktion SelectionSort aus Algorith-mus 2.1 liefert keinen Rückgabewert per return-Anweisung. Dennoch erfüllt die Funktion, z.B. im Sinnevon

1 L=[3,1,5]

2 selection_sort(L)

3 print(L)

ihre Aufgabe und ändert die Reihenfolge der Element in L. In der Programmierung unterscheidet manzwischen zwei verschiedenen Varianten bei der Übergabe von Parametern. Wir betrachten das folgendekurze Programm

1 def tuwas(z):

2 z = z+1

3

4 x=5

5 print(x)

6 tuwas(x)

7 print(x)

Was passiert? x erhält den Wert 5, wird ausgegeben an die Funktion tuwas übergeben. Dort erhält x denNamen z, dieses z wird um eins erhört. Im Anschluss hat x immer noch den Wert 5. Wir nennen diesesVerhalten Call by Value, d.h. der Wert der Variable x wird übergeben, innerhalb der Funktion tuwaswird jedoch eine neue Variable z angelegt und der Wert von x wird bei der Übergabe in z kopiert.Ein zweites ähnliche Beispiel

1 def tunochwas(S):

2 S[0],S[1] = S[1],S[0]

3

4 L=[1,2]

5 print(L)

6 tunochwas(L)

7 print(L)

Jetzt legen wir eine Liste L an, diese wird an die Funktion tunochwas übergeben. Dort heißt die Variablenun S und in S werden erstes und 2tes Element vertauscht. Erstaunlicherweise wird nun zunächst dieursprüngliche Reihenfolge 1, 2 im Anschluss die vertauschte Reihenfolge 2, 1 ausgegeben. Hier scheintdie Variable S in der Funktion tunochwas das gleiche Objekt zu behandeln wie die Variable L. Wir nen-nen dieses Verhalten Call by Reference. Übergeben wird nicht der Wert der Variable, sondern eineReferenz auf diese Variable (wir können uns einfach die Speicherstelle vorstellen). Die Variable erhältnur einen neuen Namen.Ein drittes Beispiel, leicht abgewandelt:

1 def tunochwas(S):

2 S = [S[1],S[0]]

3

4 L=[1,2]

5 print(L)

[email protected] 27

Page 34: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.1. Ordnung und erstes Sortierverfahren

6 tunochwas(L)

7 print(L)

Wir haben nur die Art geändert, wie wir die Reihenfolge ändern. Jetzt scheint das Prinzip Call byReference nicht mehr zu gelten. Denn vor und nach dem Funktionsaufruf wir die alte Reihenfolgeausgegeben. Was geht schief? Nichts, es wird wieder per Call by Reference nur eine Referenz auf Lübergeben und nun S genannt. Aber in Zeile 2 des Programm wird nun eine neue Liste - wieder mitdem Namen S - erzeugt. Dieses Objekt hat nichts mehr mit der Liste L zu tun, die unter dem Namen Sübergeben wurde.In Python liegt es am Typ der Variable, wie die Übergabe erfolgt. Für uns reicht im Moment: Listenwerden als Referenz übergeben, einfache Zahlen per Wert. Falls wir eine Funktion nutzen wollen umeine Variable zu ändern, so sind wir immer auf der sicheren Seite, wenn wir die return-Anweisungverwenden um ein Ergebnis zurückzugeben, also z.B.:

1 def tunochwas(S):

2 S = [S[1],S[0]]

3 return S

4

5 L=[1,2]

6 print(L)

7 L = tunochwas(L)

8 print(L)

In diesem Fall wird bei der Zuweisung in Zeile 7 immer eine Kopie der Variable erstellt. Es gibt abergute Gründe, das ModellCall by Reference einzusetzen, z.B. wenn wir mit sehr großen Datenmengenhantieren. Die Liste kann ja viele Millionen Einträge haben, diese zu kopieren, nur weil wir 2 Elementetauschen kann sehr ineffizient sein.In Python werden Datentypen entweder immutable (unveränderlich) oder mutable (veränderlich)genannt. immutable-Typen, wie Zahlen oder Strings, können nie ihre Wert ändern. Die Anweisung x↪→ = x+1 ändert die Variable x nicht sondern erstellt eine neue Variable und nennt diese wieder x.immutable-Typen werden per Call by Value übergeben. mutable-Typen hingegen sind veränderlichund werden per Call by Reference übergeben.

Bemerkung 2.7 (Zufallszahlen in Python). Um Testlisten für Sortierverfahren zu erstellen ist espraktisch, Zufallszahlen zu wählen. In Python gibt es hierfür verschiedene Befehle. Diese Befehle sindjedoch nicht Teil der Python-Grundbefehle, sondern müssen zunächst im Programm importiertwerdenmittels

1 import random

Im Anschluss stehen verschiedene Befehle zur Verfügung:1 import random

2

3 x = random.randint(−5,2) # liefert eine zufällige ganze Zahl

4 # zwischen a und b (jeweils inkl.)

5

6 f = random.random() # liefert eine zufällige Fliesskommazahl

28 ©2018,2019 Thomas Richter

Page 35: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.2. Laufzeit

7 # zwischen 0 und 1

8

9 L = [1,2,’Haus’,4,−5,6]10 e = random.choice(L) # liefert ein zufaelliges Element aus L

Bemerkung 2.8 (Zeitmessung). Wirwollen oft Zeitenmessen, umAlgorithmen auf Effizienz zu prü-fen. Python hält hierfür praktische Funktionen in der Bibliothek time bereit. Einbinden der Funktionenper

1 import time

Wir benötigen zunächst aus dieser Bibliothek nur die Funktion time.perf_counter(), welche die aktu-elle Zeit (in Sekunden) als Fließkommazahl zurück gibt. Die in einer Funktion, z.B. selection_sortverbrachte Zeit kann mittels zweier Aufrufe von clock ermittelt werden:

1 ...

2 t1 = time.perf_counter()

3 selection_sort(L)

4 t2 = time.perf_counter()

5 print(’Es sind ’,t2−t1,’ Sekunden vergangen’)6 ...

time.perf_counter() liefert die sogenannte CPU-Zeit. Das ist die Zeit, die der Prozessor mit dem Pro-gramm beschäftigt ist. Diese Zeit kann von der in Realität verstrichenen Zeit abweichen, z.B. wennder Computer gleichzeitig mit anderen Programmen beschäftigt ist. Man unterscheidet zwischen derwall time, welche die Zeit ist, die in Realität vergeht und der cpu time, welche die Zeit angibt, die derProzessor zur Ausführung der Anweisungen benötigt.

In älteren Python-Versionenmuss gegebenenfalls die Funktion time.clock() anstelle von time.perf_counter↪→ () verwendet werden.

2.2. Laufzeit

Selection Sort ist eines der wenigen Sortierverfahren, welches nur eine partielle Ordnung be-nötigt. Das Problem einer partiellen Ordnung ist, dass es Elemente a,b ∈M geben kann, dieüberhaupt nicht in Ordnung zueinander stehen müssen, d.h. a 6� b und b 6� a. Um zu ent-scheiden, an welcher Stelle ein Element a steht, muss es mit allen anderen Elementen einzelnverglichen werden.

Satz 2.9 (Aufwand des allgemeinen Sortierproblems). Es seiM eineMenge mit n Elementen und� eine partielle Ordnung. Im ungünstigsten Fall benötigt das allgemeine Sortierproblem mindestens

n2 − n

2

Vergleiche.

[email protected] 29

Page 36: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.2. Laufzeit

Beweis. Jeder Algorithmus zum Sortieren muss sicherstellen, dass die Ordnung gegeben ist,vergleiche Definition 2.4, d.h. ob

f(j) 6� f(i) ∀i, j : 1 6 i < j 6 n.

Wir betrachten eineMengeMmitn Elementen, von denen eventuell keine zwei verschiedenenzueinander in Ordnung stehen, d.h.

∀x,y ∈Mmit x 6= y gilt (x 6� y) und (y 6� x).

Hieraus folgern wir, dass keinerlei Transitivität innerhalb der Menge gelten kann, die unterUmständendie Sortierung erleichtern vereinfachenwürde.Umzu entscheiden, ob ein Elementan Stelle i falsch steht, testen wir, ob es eine Element an Stelle j > i gibt mit xj � xi. Das ersteElement erfordert somit n − 1 Vergleiche, das zweite n − 2, usw. Insgesamt ergibt sich derAufwand als (gerechnet in der Anzahl an Vergleichen)

(n− 1) + (n− 2) + · · ·+ 1 =n(n− 1)

2=n2 − n

2.

Der mögliche Ausschluss einer Verwendung der Transitivität ist wichtig. Denn Angenommenes läge eine totale Ordnung6 vor. Dann gilt für alle x,y ∈M entweder x 6 y oder y 6 x (odernatürlich x = y). Ein Test auf Sortiertheit kann hier viel einfacher durchgeführt werden:

Algorithmus 2.2: Test auf Sortiertheit (totale Ordnung)Gegeben Liste Lmit n Elementen und totaler Ordnung 6

1 def test_sortiert(L):

2 n = len(L)

3 for i in range(n−1):4 if not(L[i] <= L[i+1]):

5 return False

6 return True

Dieser Algorithmus benötigt nur n − 1 Vergleiche. Denn durch die totale Ordnung und dieTransitivität folgt aus L[i] <= L[i+1] für alle 0 6 i < n − 1 auch L[i] <= L[j] für alle 0 6 i 6j < n. Und da die Ordnung total ist, ist L[i] 6 L[j] äquivalent zu L[i] = L[j] oder L[j] 66 L[i], d.h.genau die Bedingung an das allgemeine Sortierproblem in Definition 2.4. Dieser Algorithmusbesagt natürlich noch nicht, dass wir das Sortierproblem bei totaler Ordnung in weniger alsn2−nVergleichen durchführen können.Wir werden aber in Abschnitt 2.3 in der Tat Verfahrenkennenlernen, die das Problem sehr viel schneller lösen. Der Grundwird stets die Transitivitätsein sowie dasWissen, dass für zwei beliebige Elementea,b ∈M immera 6 b oder aberb 6 agilt.

Wir untersuchen im folgenden den Aufwand von Algorithmen. Hierzu definieren wir:

Definition 2.10 (Aufwand, Komplexität). Es sei ein Algorithmus gegeben, welcher eine Eingabeder Größe n ∈ N eine Ausgabe zuordnet. Mit dem Aufwand oder der Komplexität

A(n) ∈ N

bezeichnen wir die Anzahl von Operationen, die der Algorithmus zur Ausführung benötigt.

30 ©2018,2019 Thomas Richter

Page 37: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.2. Laufzeit

Bemerkung 2.11 (Operationen und Aufwand). Eine klare Definition des Begriffs Aufwand istschwierig und hängt von den genauen Umständen ab. Die Addition zweier Zahlen z = x+ y benötigteine Operation (die Addition) hat also Aufwand 1. Die Addition von n Zahlen

y =

n∑i=1

xi

benötigtn−1Additionen und hat damit den Aufwandn−1. Wie sieht es aber aus mit der Addition vonzwei sehr langen Zahlen mit vielen tausend Stellen? Falls wir einen Computer haben, der zwei Zahlenunabhängig von der ihrer Länge in einem Schritt addieren kann, so ist der Aufwand weiterhin eins.Allgemein wird dies jedoch nicht so sein und der Aufwand steigt mit der Länge der Zahlen. Im Allge-meinen werden wir hier davon ausgehen, dass die Grundoperationen (z.B. Addition, Multiplikation,Vergleich zweier Werte, Tauschen von Zahlen, ...) alle in der gleichen Zeit mit konstantem Aufwanddurchgeführt werden können.

Wir beginnen mit Selection Sort.

Satz 2.12 (Laufzeit von Selection Sort). Algorithmus 2.1 hat stets die Laufzeitn2 − n

2.

Beweis. Die Laufzeit kann einfach abgelesen werden.

Selection Sort ist also im Sinne von Satz 2.9 ein optimaler Algorithmus wenn wir nur einepartielle Ordnung zugrunde legen. Zur genaueren Kategorisierung des Aufwands betrachtenwir im folgenden zwei einfache Algorithmen

1 x=0

2 for i in range(n):

3 for j in range(i,n):

4 x = x+j

und1 x=0

2 for i in range(n):

3 x = x+i

4 for j in range(i,n):

5 x = x+j

Wir bestimmen den jeweiligen Laufzeiten durch Abzählen der Iterationen:

A1(n) = 1+

n∑i=1

n∑j=i

1 = 1+

n∑i=1

(n− i+ 1)

= 1+ n2 −

n∑i=1

i+ n = 1+ n2 + n−n(n+ 1)

2=

1

2n2 +

1

2n+ 1

A2(n) = 1+

n∑i=1

1+

n∑j=i

1

= n+A1(n) =1

2n2 +

3

2n+ 1

[email protected] 31

Page 38: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.2. Laufzeit

Dabei haben wir Additionen sowie die Initialisierung, jeweils in Zeile 1, als eine Operationgezählt. Der Aufwand von Algorithmus 2 ist höher. Es gilt:

n 10 100 1 000 10 000

A1(n) 56 5 051 500 501 50 005 001A2(n) 66 5 151 501 501 50 015 001

A2(n) −A1(n) 10 100 1 000 10 000(A2(n) −A1(n))/A1(n) 15% 2% 0.2% 0.02%

Wir zeigen den Aufwand, die Differenz zwischen beiden Verfahren und die relative Differenz.Es gilt natürlich A2(n) − A1(n) = n, aber die relative Differenz sinkt mit der Problemgröße.Für großen ∈ N ist der Unterschied zwischen beiden Verfahren zu vernachlässigen.Wir inter-essieren uns daher insbesondere für den wesentlichen asymptotischen Aufwand. Hierzu führenwir im Vorgriff auf die Analysis schnell den Begriff der Konvergenz gegen Unendlich und derKonvergenz gegen Null ein.Definition 2.13 (Konvergenz gegenUnendlich und gegenNull). Es sei g : R→ R eine Funktion.Wir sagen “g konvergiert für x gegen Unendlich gegen Unendlich”, in Symbolen

g(x) −−−→x→∞ ∞,

falls es zu jedem Wert C > 0 ein xC ∈ R gibt, so dass

g(x) > C, für alle x > xCgilt. Wir sagen “g konvergiert für x gegen Null gegen Null”, in Symbolen

g(x) −−−→x→0

0,

falls es für jedes ε > 0 ein δ > 0 gibt, so dass

−ε < g(x) < ε, für alle − δ < x < δ

gilt.

In der Analysis ist der Begriff der Konvergenz viel weiter gefasst. Fürs erste genügen uns jedochdiese beiden Spezialfälle.

Beispiel 2.14 (Konvergenz). Die Funktion f(x) = x2 konvergiert für x→∞ gegen unendlich. Dennes sei C > 0 beliebig gegeben, ohne Einschränkung C >> 1. Dann gilt für alle x > C > 1

x2 = x · x > 1 · x > C.

Dies ist gerade die geforderte Konvergenz gegen Unendlich.Ebenso gilt für die Funktion f(x) = x2 die Konvergenz f(x) → 0 für x → 0. Denn zu beliebigem0 < ε 6 1 und gilt für alle |x| < ε

f(x) = x2 = x · x < ε · ε 6 ε,

und da x2 > 0 gilt entsprechend f(x) > −ε. Also konvergiert f(x) gegen Null.

32 ©2018,2019 Thomas Richter

Page 39: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.2. Laufzeit

Wir definieren:

Definition 2.15 (Landau-Symbole). Es sei g(n) eine Funktion mit g → ∞ für n → ∞. Dann istf ∈ O(g) genau dann, wenn es eine Konstante C > 0 und ein n0 ∈ N gibt, so dass gilt∣∣∣∣ f(n)g(n)

∣∣∣∣ 6 C, für alle n > n0.

Weiter ist f ∈ o(g) genau dann, wenn es zu jedem ε > 0 ein n0 ∈ N gibt, so dass∣∣∣∣ f(n)g(n)

∣∣∣∣ < ε, für alle n > n0,

Sei nun g(h) eine Funktion mit g(h) → 0 für h → 0. Dann ist f ∈ O(g) genau dann, wenn es eineKonstante C > 0 gibt sowie ein n0 ∈ N, so dass∣∣∣∣ f(h)g(h)

∣∣∣∣ 6 C, für alle n > n0,

und f ∈ o(g) genau dann, wenn es für jedes ε > 0 ein n0 ∈ N gibt, so dass

limh→0

∣∣∣∣ f(h)g(h)

∣∣∣∣ < ε, für alle n > n0.

Die Landau-Symbole dienen dem Vergleich von Konvergenzgeschwindigkeiten. Wir sagen zuf(n) ∈ O(g), dass “f(n) nicht schneller wächst als g(n)”, zu f(n) ∈ o(g), dass “f(n) langsamerwächst als g(n)”, zu f(h) ∈ O(g), dass “f(h) mindestens genauso schnell gegen Null gehtwie g(h)” und zu f(h) ∈ oc(g), dass “f(h) schneller gegen Null geht als g(h)”. Die Symbolikfür n → ∞ wird verwendet zum Messen von Aufwand (hier wollen wir den Aufwand imWachstum nach oben abschätzen), wohingegen die Symbolik f(h) für h→ 0 für die Messungdes Fehlers dient (der Fehler soll nach oben beschränkt werden).

Für die beiden Algorithmen oben gilt

A1(n) ∈ O(n2), A2(n) ∈ O(n2).

Oft schreiben wir einfach

A1(n) = O(n2), A2(n) = O(n2).

Beide haben die gleiche asymptotische Komplexität, auch wenn die Laufzeit nicht identischist. Daher ist die Notation mit “=” nicht optimal, wir erwarten Transitivität, diese gilt jedochnicht, da A1(n) 6= A2(n).

Ein paar weitere Beispiele zum nachrechnen (jeweils n→∞ und h→ 0)

log(n) = o(n)sin(h) = O(h)

sin(n) = O(1)

h2 + h = O(h)

n2 + n = O(n2)√n = o(n)

[email protected] 33

Page 40: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.2. Laufzeit

Oftmöchtemandie führendeKonstante bei der Bestimmungder Laufzeit angeben.Das Landau-Symbol wird dann für den zweiten Term verwendet, d.h. für alle Terme, die wir vernachlässi-gen wollen. Für die beiden obigen Algorithmen gilt dann jeweils

A1(n) =n2

2+ O(n), A2(n) =

n2

2+ O(n).

Auch Selection Sort hat den Aufwand O(n2).

Laufzeiten, best-case, worst-case, average-case Viele Algorithmen haben einezufällige Komponente. Wir betrachten das folgende Beispiel

Algorithmus 2.3: Sequentielle SucheGegeben sei eine Liste L=[s1,...,sn] mit n ∈ N Einträgen. Gesucht ist die Position eines ihrerEinträge l.

1 for i in range(n):

2 if L[i] == l:

3 return i

4 return −1

Es gilt:

Satz 2.16 (Aufwand der sequentiellen Suche). Wir gehen davon aus, dass die Einträge li der Listepaarweise verschieden sind und dass das gesuchte Element l Teil der Liste ist. Die best-case Komple-xität des Verfahrens ist

Abest(n) = 1

die worst-case Komplexität des Verfahrens ist

Aworst(n) = n

die average-case Komplexität des Verfahrens im Falle eine Gleichverteilung ist

Aavg(n) =n+ 1

2.

Beweis. Die ersten beiden Fälle ergeben sich bei l1 = l, bzw. bei ln = l. Wir gehen nun davonaus, dass sich das Element l an beliebiger Stelle befindet und zwar mit gleicher Wahrschein-lichkeit an jeder der n Stellen, jeweils 1/n. Falls sich l an Stelle i befindet, so ist die LaufzeitAi(n) = i. Hierdurch ergibt sich:

Aavg(n) =1

n

n∑i=1

i =n(n+ 1)

2n=n+ 1

2.

Wir betrachten nun Sortieren durch Einfügen

34 ©2018,2019 Thomas Richter

Page 41: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Algorithmus 2.4: Insertion SortGegeben eine Liste Lmit n Elementen und eine (partielle) Ordnung �.

1 def insertion_sort(L):

2 for i in range(n):

3 j=i

4 while (j>0) and not(L[j−1] � L[j]):

5 L[j−1],L[j] = L[j],L[j−1]6 j = j−1

Im Gegensatz zu Selection Sort hängt bei Insertion Sort die Laufzeit von der Eingabe ab. Fallsdie Ordnung alle Elemente erfasst und die Daten bereits sortiert sind, d.h. L[j−1]�L[j], sobricht die while-Schleife immer sofort bei j = i ab. Zeilen 5 und 6 werden nie ausgeführt undinsgesamt sind nur n − 1 Vergleiche notwendig. Dies ist kein Widerspruch zur Aussage vonSatz 2.9, denn dieser sagt etwas über den ungünstigsten Fall aus.

Die best-case Laufzeit istO(n). Imworst-case sind dieDaten gegenläufig sortiert. Es kann gezeigtwerden, dass dies auch im Laufe des Verfahrens so bestehen bleibt. Damit ergibt sich eineworst-case Laufzeit von O(n2). Es kann in aufwändiger Analyse auch gezeigt werden, dass dieaverage-case Laufzeit O(n2) ist.

2.3. Sortieren bei totaler Ordnung

Wir gehen im Folgenden davon aus, dass eine totale Ordnung6 auf derMengeM gegeben ist.Für je zwei Elemente gilt also entweder a 6 b oder b 6 a. Gelten beide Bedingungen, so ista = b. Bei Analyse einer partiellen Ordnungmussten wir oft mit der Negation, also f(i) 6� f(j)arbeiten. Dies bedeutet nicht gleichzeitig f(j) � f(i). Im Fall einer totalen Ordnung ist dieseinfacher. Aus f(i) 66 f(j) folgt f(j) 6 f(i). Wir definieren die Negation

f(i) 66 f(j) ⇔: f(i) > f(j).

Wir betrachten noch einmal Selection Sort aus Algorithmus 2.1. In Zeilen 4-5 werden die Ele-mente mit Indizes i und j getauscht, falls L[j] <= L[i] gilt. Hiermit wird - im Sinne vom Beweiszu Satz 2.5 - erreicht, dass nach dieser Schleife gilt:

L[k] 6= L[h] oder L[k] 6� L[h] ∀0 6 h < i und h < k < n.

Bei einer totalen Ordnung lässt sich diese Bedingung einfacher schreiben

L[h] 6 L[k] ∀0 6 h < i und h < k < n.

Wollen wir diese Bedingung induktiv von i auf i+1 übertragen, so müssen wir nur den neuenIndex h = i überprüfen, also ob

L[i] 6 L[k] ∀i < k < n

gilt, denn alle Indizes 0, . . . , i−1 bleiben in L unverändert.Wir können den Selection-Sort leichtmodifizieren:

[email protected] 35

Page 42: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Algorithmus 2.5: Min-Sort; Selection Sort bei totaler OrdnungGegeben Liste L und totale Ordnung

1 def selection_sort_total(L):

2 n = len(L)

3 for i in range(n):

4 m = i

5 for j in range(i+1,n):

6 if L[j] <= L[m]:

7 m = j

8 L[i],L[m] = L[m], L[i]

In Zeilen 4-7 des Verfahrens wird derjenige Indexmmit i 6 m < n gesucht, dessen Elementminimal ist. Anschließend wird dieses minimale Element mit Index i vertauscht. Dieser Algo-rithmus ist effizienter, da anstelle von O(n2) Tauschvorgängen nur O(n) Elemente getauschtwerden müssen. Nach wie vor sind jedoch O(n2) Vergleiche notwendig.

Diese Vereinfachung ist bei einer partiellen Ordnung nicht möglich. Hier existiert nicht unbe-dingt ein kleinstes Element, da überhaupt nicht alle Elemente miteinander in Relation stehen.Auch der Beweis lässt sich bei einer totalen Ordnung leichter durchführen.

Satz 2.17 (Selection Sort bei totaler Ordnung). Algorithmus 2.5 löst das Sortierproblem bei totalerOrdnung.

Beweis. Wir zeigen induktiv, dass nach Schritt i gilt:

L[h] 6 min{L[h+1], L[h+2], . . . , L[n−1]} ∀0 6 h 6 i. (2.2)

Nach Schritt i = n− 1 ist dann das Sortierproblem gelöst, denn dies impliziert insbesondere

L[h] 6 L[h+1] ∀0 6 h < n− 1.

(i) Aus der Transitivität folgt, dass die Zeilen 4-7 einen Indexm ∈ {i, . . . ,n} bestimmen mit

L[m] = min{L[i], . . . , L[n−1]},

also L[m]6L[j] für alle j = i, . . . ,n − 1. Denn durch jeden möglichen Tausch m 7→ m ′ 7→ m ′′

bleibt stets L[m ′′]6 L[m ′]6 L[m] erhalten.

(ii)Wir zeigen nun mit Induktion die Bedingung (2.2). Zunächst sei i = 0. Die Bedingung istleer, also korrekt.

(iii) i 7→ i + 1. In diesem Schritt ändert sich höchstens das Element L[i+1] und wird gegebe-nenfalls mit einem L[m] für i+ 1 6 m < n getauscht. D.h., nach Induktionsvoraussetzung giltBedingung (2.2) für alle Indizes 0 6 h 6 i. Es bleibt zu zeigen, dass nach diesem Schritt auch

L[i+1] 6 min{L[i+2], . . . , L[n−1]}

gilt. Der neue Index i + 1, also der alte Indexm wurde jedoch gerade also das Minimum derMenge

L[m] = min{L[i+1], . . . , L[n−1]}

bestimmt. Somit ist die Bedingung gezeigt.

36 ©2018,2019 Thomas Richter

Page 43: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Wir kommen nun mit Bucket-Sort kurz zu einem Sortierverfahren welches einem komplettanderen Prinzip folgt und unter Umständen weit schneller ist als die lineare Laufzeit O(n).Definition 2.18 (Induzierte Ordnung). Es seien M,N eine Mengen. Die Menge N sei mit der(totalen) Ordnung 6 versehen. Weiter sei k : M → N eine Bijektion. Dann nennen wir aufM dieOrdnung 6 ′ gegeben durch

a 6 ′ b ⇔ k(a) 6 k(b)

eine von N induzierte Ordnung.Satz 2.19 (Bucket Sort). Es sei L eine Liste mit n Einträgen einer Menge M, N eine Menge mite Elementen und 6 auf L die von N mittels k : M → N induzierte Ordnung. Dann das folgendeVerfahren, Bucket-Sort genannt, lineare Laufzeit O(n+ e):

1. Stelle e “Eimer” E1, . . . ,Ee auf2. Laufe über alle n Elemente der Liste L und füge jedes Elemente x ∈ M in den entsprechenden

Eimer Ek(x) ein3. Laufe über alle e Eimer in korrekter Reihenfolge und hänge die Ergebnisse aneinander.

Jeder Eintrag x1, . . . , xn der Liste Lwird in den “Eimer” mit Index k(xi) gelegt. Dies geschiehtin O(n) Operationen. Im Anschluss werden in O(e) Operationen die Elemente der Eimer zueiner sortierten Liste zusammengefügt. Im Fall e = O(n) ist dieses Verfahren linear und somitoptimal. Denn ein Sortieralgorithmus kann nicht schneller als linear sein, da ja jedes Elementmindestens einmal angesehen werden muss, um die Sortierung sicher zu stellen. Im Gegen-satz zu den oben angesprochenen einfachen Verfahren hat Bucket Sort den Nachteil, dass esnicht ohne erheblichen Speicheraufwand auskommt. Es müssen e Listen - also die “Eimer” -vorgehalten werden und in jedem dieser Eimer können maximal n Elemente stehen (falls alleEinträge der Liste L gleich sind). Das Verfahren hat die Laufzeit O(n+ e) und auch den Spei-cheraufwandO(n+e). Fallsn� e, so istBucket Sort in dieserArt nicht sinnvoll durchzuführen.Angenommen n Zahlen einer Menge vom Typ unsigned int, (ein übliches Zahlenformat mit 32Bits, d.h. 32 Stellen

M = {0, 1, . . . , 232 − 1} = {0, 1, . . . , 4 294 967 295}

Dann müssen stets e = 4 294 967 295 “Eimer” vorgehalten werden und die Laufzeit beträgt

Abucket(n) = 4 294 967 295+ n.

Für n = 1 000 ist der quadratische Aufwand von selection sort klar vorzuziehen (es werden nur0.01% der Operationen von Bucket Sort benötigt). Hinzu kommt der hier natürlich viel zu ho-he Speicheraufwand. In abgewandelter Form hat Bucket Sort jedoch einen hohen praktischenNutzen. Es werden nicht e “Eimer” verwendet, sondern eine kleine Zahl e ′ � n. Die Auftei-lung in die “Eimer” sorgt für eine Vorsortierung. Die Elemente werden möglichst gut auf e ′Teillisten verteilt und diese können dann - als kleinere Probleme - effizient sortiert werden. DieSchwierigkeit besteht in einer gutenAufteilung der Daten auf die “Eimer”.Wenn amEnde fastdie ganze Liste in der gleichen Teilliste landet, kann das Verfahren nicht effizient sein.

Das Prinzip, die Eingabe aufzuteilen und getrennt voneinander zu bearbeiten wirdDivide andConquer (teile und herrsche) genannt. Auf diesem Prinzip basieren zwei weitere wichtige Sor-tierverfahren,Merge Sort und Quicksort.

[email protected] 37

Page 44: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

2.3.1. Merge Sort

Merge Sortwurde von John von Neumann1 eingeführt. Die grundlegende Idee vonMerge Sortist es, dass es 2 kurze listen einfacher sortiert werden können als eine doppelt so lange unddass es sehr einfach ist, zwei jeweils sortierte Listen L und Rmit nl und nr Elementen zu einersortierten Liste der Länge n = nl + nr zusammenzusetzen. Wir geben hierzu einen entspre-chenden Algorithmus an.

Algorithmus 2.6: Zusammenfügen sortierter ListenGegeben, zwei sortierte Listen L,R

1 def merge(L,R):

2 S = [] # Ergebnisliste

3 il = 0 # Laufvariablen in Listen L und R

4 ir = 0

5 # Beide Listen gleichzeitig durchlaufen bis ein Ende erreicht wird

6 while (il < len(L)) and (ir < len(R)):

7 if L[il] <= R[ir]: # kleineres Elemnent einfuegen

8 S.append(L[il])

9 il = il + 1

10 else:

11 S.append(R[ir])

12 ir = ir + 1

13

14 # Jetzt ist eine Laufvariablen am Ende. Die verbleibenden Elemente

15 # der anderen Liste werden angehaengt

16 for i in range(il,len(L)):

17 S.append(L[i])

18 for i in range(ir,len(R)):

19 S.append(R[i])

20 return S # Ergebnis zurueckgeben

Eswerden insgesamt nl+nr Schritte durchgeführt. Aber wir kommenwir zu sortierten ListenL und R? Einfach indemwir das Prinzip rekursiv weiter denken: vier Listen der Länge n/4 sindschneller zu sortieren als 2 der Längen/2, erst recht als eine der Längen, usw. Listen der Länge1 sind trivial zu soritieren. Denn ein Element ist stets sortiert.

Wir startenMerge Sort daher gedanklich bei n Listen der Länge 1. Diese werden zu Listen derLänge 2, diese zu Listen der Länge 4 zusammengeführt, usw. Das Verfahren kann rekursivformuliert werden:

Algorithmus 2.7: Merge SortGegeben, Liste Lmit n ∈ N Elementen und totaler Ordnung 6

1 def merge_sort(L):

2 n = len(L)

3 if n <= 1: # leere Liste, 1er Liste immer sortiert

4 return L

1Ungarisch/US-Amerikanischer Mathematiker und Informatiker (wenn es damals schon Informatiker gab). Vonihm stammen etliche wichtige Beiträge in verschiedenen Bereichen der Mathematik.

38 ©2018,2019 Thomas Richter

Page 45: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

5 m = int(n/2)

6 S1 = merge_sort(L[0:m]) # Rekursiver Aufruf linke Haelfte

7 S2 = merge_sort(L[m:n]) # Rekursiver Aufruf rechte Haelfte

8 L = merge(S1,S2)

9 return L

Merge Sort hat zusätzlichen Speicheraufwand beim Kombinieren der beiden Listen.

Satz 2.20 (Aufwand vonMerge Sort). Merge Sort hat in jedem Fall den Aufwand O(n logn).

Beweis. Es sei T(n) der Aufwand für Algorithmus 2.7 bei einer Eingabe der Länge n. Hierfürgilt

T(n) = T(bn2c) + T(dn

2e) + n, (2.3)

wobei b·c das Abrunden auf die nächstkleinere ganze Zahl bezeichnet und d·e das entspre-chende Aufrunden. Weiter bezeichnet der Summand n den Aufwand beim Zusammenfügen,die beiden anderen Terme den Aufwand im rekursiven Aufruf. Die Größe der Teilliste kannsich um eins unterscheiden. Falls n = 1 so ist der Aufwand T(1) = 1. Wir zeigen induktiv fürk ∈ N

T(2k) 6 (k+ 1)2k.

Für k = 0 also n = 1 ist T(1) = 1 6 1 · 20 = 1.

Die Behauptung sei für 2k−1 wahr. Dann gilt mit (2.3)

T(2k) 6 2 · T(2k−1) + 2kInd.6 2 · k · 2k−1 + 2k = (k+ 1)2k.

Nun gibt es ein minimales k0 ∈ Nmit 2k0−1 6 n 6 2k0 . Dann folgt

T(n) 6 T(2k0) = (k0 + 1)2k0 6 2(k0 + 1)n 6 2(2+ log2(n)

)n,

alsoT(n) = 2n log2(n) + 4n = O(n log(n)).

2.3.2. Quicksort

Mit Merge Sort haben wir eine Sortierverfahren analysiert, welches immer - also unabhängigvon dem Wert von n Elementen in einer Liste - den Aufwand 2n log2(n) also O(n log(n)) be-sitzt. Der Schlüssel für Merge Sort ist das hierarchische Aufteilen der Liste {s1, . . . , sn} in zweikleinere Listen und das anschließende Zusammenfügen von zwei kleineren sortierten Listenzu einer größeren. Aus diesem Aufbau resultiert die logarithmische Komponente in der Lauf-zeit. Ein Feld der Länge n lässt sich log2(n)mal in jeweils zwei (etwa) gleichgroße Teile zerle-gen.

Einweiteres Sortierverfahren, welches auf demPrinzipDivide and Conquer beruht istQuicksort.Ein Feld s1, . . . , sn wird sukzessive in kleinere Felder aufgeteilt. Dabeiwird in jedemSchritt ein

[email protected] 39

Page 46: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Pivot-Element p gewählt, z.B. p = s1. Das Feld wird dann so umgestellt, dass links vom Pivot-Element nur Elemente si 6 p stehen und rechts vom Pivot-Element nur Elemente sj > p.Das Pivot-Element stehe nach diesem Partitionierungsschritt an der Stelle ip ∈ {1, . . . ,n}. ImAnschlusswird das Feld in zwei Teile {1, . . . , ip}und {ip+1, . . . ,n}unterteilt unddasVerfahrenwird rekursiv auf beide Teile angewandt. Bei der Partitionierung kommt es prinzipiell nichtdarauf an, ob Elemente si = p links oder rechts eingeordnet werden. Bei der Wahl des Pivot-Elements sind wir frei. Einfache Verfahren verwenden entweder das erste oder letzte Elementder Liste.

Der Algorithmus besteht also aus zwei Teilen:

Algorithmus 2.8: Quicksort Eingabe von Liste Lmit n ∈ N Elementen sowie zwei Indizes0 6 l, r < n

1 def quick_sort(L,l,r):

2 if not(l<r):

3 return # Beendet Funktion (ohne Rueckgabe)

4 j = partitioniere(L,l,r) # Partitionieren der Liste

5 quick_sort(L,l,j) # Sortiere links

6 quick_sort(L,j+1,r) # Sortiere rechts

Algorithmus 2.9: Partitioniere Eingabe einer Liste Lmit n ∈ N Elementen sowie von zweiIndizes 0 6 l < r < n

1 def partitioniere(L,l,r):

2 p = L[r] # Pivot−Element3 jl = l # linker Index

4 jr = r−1 # rechter Index neben Pivot

5

6 while True: # ’Endlos’−Schleife7 while (jl<r−1) and (L[jl]<p): # suche >= Pivot

8 jl = jl + 1

9

10 while (l<jr) and (L[jr]>=p): # suche < Pivot

11 jr = jr − 1

12 if (jl<jr): # Tausche, falls f

13 L[jl],L[jr] = L[jr],L[jl]

14

15 if not(jl<jr): # Ende der while−Schleife16 break

17

18 if (L[jl]>=p): # Falls linker index = Pivot, tauschen

19 L[jl],L[r] = L[r],L[jl]

20 return jl

21 else:

22 return jr

Die Funktionen arbeiten Beide auf dem Prinzip desCall by Reference und ändern stets die Liste,legen aber keine neue Liste an.

40 ©2018,2019 Thomas Richter

Page 47: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Diese Art der Partitionierung wird Hoare Schema genannt. Wir arbeiten mit zwei Indizes, diegegenläufig das Feld bearbeiten.

[email protected] 41

Page 48: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Wir machen ein Beispiel. Es ist n = 7, d.h. die Liste hat die Elemente s0, . . . , s6. Das Pivot-Element ist jeweils markiert. Wir wählen wie in Algorithmus 2.9 jeweils das rechte Element.

s0 s1 s2 s3 s4 s5 s6

7 3 0 4 8 9 5

7 3 O 4 8 g J← Pirot

- Pos je nach socleI3 0 I 8 9 J

spogjenaohsudeI3 O I89 J

c - s Element getauodt

"4 3 I I89 5 = Indites meal sale

far je LEie3=7zp=5 ⇒ Tank

-4 3 O 5 8 g zD= je =3

Jett : 2 Listen

3 0 J 8 9 F Taels Assad soak

- - ohneteood .

- - Dealt ; Tausch je end

4 3 O 5 7 9 8 Pinot

I3 0

"'

£ 8 i¥I Sade olneteosdg

O 3 4 .

1

I89 jewels : Tausch

I je and Pirot

i ¥!, I

iIIE. Sade ohne

' I Teased .

O 3 4 5 7 89 → Fertig

42 ©2018,2019 Thomas Richter

Page 49: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Satz 2.21 (Laufzeit von Quicksort). Dieworst-case Laufzeit vonQuicksort zur Sortierung von nElementen beträgt

Tworst(n) =n2

2+ O(n)

Operationen. Die best-case Laufzeit von Quicksort zur Sortierung von n Elementen beträgt

Tbest(n) = 2n log2(n) + O(n)

Operationen.

Beweis. Zur Partitionierung von k = r− l+ 1 Elementen sind k− 1 Vergleiche notwendig. DieAnzahl der Tauschprozesse hängt von der Verteilung der Elemente ab. Wir notieren also

Tpart(k) = k− 1.

Der Aufwand von Quicksort zum Sortieren von k Elementen lässt sich rekursiv schreiben als

Tquick(k) = Tpart(k) + Tquick(k′) + Tquick(k− k

′),

wobei k ′ < k von dem Pivot-Element und der Verteilung der Elemente abhängt. Im ungüns-tigsten Fall gilt k ′ = 1 und es gilt:

Tquick(k) = Tpart(k) + Tquick(1) + Tquick(k− 1).

Wir setzen Tquick(1) = 1 und erhalten

Tquick(n) =

n∑k=1

Tpart(k) + n · Tquick(1) =

n∑k=1

(k− 1) + n =n(n− 1)

2+ n =

n2

2+ O(n).

Im worst-case gilt für Quicksort quadratische Laufzeit.Im optimalen Fall halbiert sich die Intervallgröße stets so dass

Tquick(k) = Tpart(k) + Tquick

(⌈k

2

⌉)+ Tquick

(⌊k

2

⌋)6 Tpart(k) + 2Tquick

(⌈k

2

⌉).

Wir wählen ein k0 ∈ Nmit2k0−1 6 n 6 2k0 . (2.4)

Dann giltTquick(n) 6 Tquick(2

k0) 6 2k0 + 2Tquick(2k0−1). (2.5)

Wir zeigen induktivTquick(2

k0) 6 (k0 + 1)2k0

Für k0 = 0 ist die Aussage bei Tquick(20) = Tquick(1) = 1 klar. Nun gelte die Aussage also für

k0 − 1. Dann ist mit (2.5)

Tquick(2k0) 6 2k0 + 2Tquick(2

k0−1)ind6 2k0 + 2k02

k0−1 = (k0 + 1)2k0 .

Hieraus folgtTquick(n) 6 2(log2(n) + 2)n = 2n log2(n) +O(n).

[email protected] 43

Page 50: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Bemerkung 2.22. Der Faktor 2 in der Laufzeitabschätzung Tbest(n) = 2n log2(n) kommt von derrecht groben Abschätzung in Schritt (2.4). Alle n ∈ N mit 2k0−1 < n 6 2k0 werden als 2k0 ab-geschätzt. Im ungünstigsten Fall, d.h. für n = 2k0−1 ist dies etwa ein Faktor 2. Angenommen es istn = 2k0 , so lässt sich der Aufwand abschätzen als Tbest(n) = n log2(n).Ohne diese Bemerkung mag das Ergebnis von Satz (2.23) iritierend wirken: der durchschnittliche Auf-wand wäre mit 1.386n log2(n) geringer als der minimale Aufwand.

Wir zeigen für n = 10 000 und n = 20 000 die Laufzeiten für jeweils 5 000 verschiedene Zah-lenlisten.

0.0006

0.0008

0.001

0.0012

0.0014

0.0016

0 1000 2000 3000 4000 5000

Zei

t

Experiment

0.0015

0.002

0.0025

0.003

0.0035

0 1000 2000 3000 4000 5000

Zei

t

Experiment

Die Laufzeiten sind weit gestreut. Bei n = 10 000 ist die Häufung bei T ≈ 0.00077, bei n =20 000 liegt die Häufung bei T ≈ 0.0016. Es gilt:

0.00077

2 log2(10 000)10 000≈ 2.9 · 10−9,

0.00165

2 log2(20 000)20 000≈ 2.9 · 10−9.

Mit der Konstantec ≈ 2.9 · 10−9

zeigt sich für die “meisten” Experimente also die erwartete logarithmische Laufzeit.

44 ©2018,2019 Thomas Richter

Page 51: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Satz 2.23 (Durchschnittliche Laufzeit von Quicksort). Es seien s1, . . . , sn paarweise unterschied-liche Elemente. Angenommen, alle n! Permutationen der s1, . . . , sn sind gleich wahrscheinlich. Dannist die durchschnittliche Laufzeit

Tavg(n) ≈ 1.386n log2(n) + O(n).

Beweis. Wenn die alle Permutationen mit gleicher Wahrscheinlichkeit vorkommen, so ist dasPivot-Element k ′ ∈ {1, . . . ,k} zufällig und jede neue Partitionierung in {1, . . . ,k ′} und {k ′ +1, . . . ,k} tritt mit gleicher Wahrscheinlichkeit auf. Für Pivot-Element 1 6 k ′ 6 k gilt dann

Tquick(k) = Tpart(k) + Tquick(k′) + Tquick(k− k

′ − 1).

Da alle k ′mit gleicherWahrscheinlichkeit vorkommenberechnet sich die durchschnittliche Lauf-zeit eines Schritts als

Tquick(k) = k− 1+1

k

k∑k ′=1

(Tquick(k

′) + Tquick(k− k′ − 1)

)Die beiden Teile der Summe können zusammengefasst werden

T(k) = k− 1+2

k

k−1∑k ′=0

T(k ′) +1

kT(k),

wobei wir T(0) := 0 setzen. Zum Lösen dieser Gleichung multiplizieren wir beide Seiten mitk

kT(k) = k(k− 1) + 2

k−1∑k ′=0

T(k ′) + T(k).

Eine entsprechende Gleichung gilt für k− 1

kT(k) = k(k− 1) + 2

k−1∑k ′=0

T(k ′) + T(k)

(k− 1)T(k− 1) = (k− 1)(k− 2) + 2

k−2∑k ′=0

T(k ′) + T(k− 1)

Wir ziehen die Gleichungen voneinander ab und erhalten

kT(k) − (k− 1)T(k− 1) = 2(k− 1) + T(k− 1) + T(k)

also(k− 1)T(k) = kT(k− 1) + 2(k− 1) ⇒ T(k)

k=T(k− 1)

k− 1+

2

k.

Wir setzen Tk := T(k)/k und erhalten

Tk = Tk−1 +2

k= Tk−2 +

2

k+

2

k− 1= T1 + 2

k∑i=1

1

i

[email protected] 45

Page 52: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.3. Sortieren bei totaler Ordnung

Bei der Summe handelt es sich um die Partialsumme der harmonischen Reihe. Für diese gilt∫k1

1

xdx 6

k∑i=1

1

i6 1+

∫k1

1

xdx.

Das Integral lässt sich über den Logarithmus berechnen als

k∑i=1

1

i6 1+ ln(k).

Hiermit erhalten wir die Abschätzung (bei T1 = 1)

Tk 6 1+ 2 ln(k).

Mit T(k) = kTk folgtT(k) 6 k(1+ 2 ln(k)).

Schließlich gilt ln(k) = log2(k) ln(2) ≈ 0.693 log2(k), also

T(k) 6 1.386k log2(k) + O(k).

Schließlich geben wir noch ein allgemeines Resultat bezüglich der Laufzeit von vergleichsba-sierten Sortierverfahren, d.h. von Verfahren, welche die Sortierung auf Basis direkter VergleicheL[i]<=L[j] herstellen. Mit Bucket-Sort haben wir bereits ein mögliches alternatives Verfahrenkennengelernt. Hier kommt statt Vergleich ein direktes Einsortieren in den richtigen Bucketzum Einsatz. Wir haben jedoch noch nicht gezeigt, wie sich so ein Einsortieren - ohne Ver-gleich - effizient implementieren lässt.

Satz 2.24 (Vergleichsbasierte Sortierverfahren). Es sei S = {s1, . . . , sn} eine Menge (paarweiseverschieden) von n Elementen mit totaler Ordnung 6. Jedes vergleichsbasierte Sortierverfahrenbenötigt im schlechtesten Fall (d.h. bei ungünstiger Reihenfolge der s1, . . . , sn) mindestens

O(n log(n)

)Vergleiche.

Mit diesem Satz ist gemeint, dass ein optimales vergleichsbasiertes Verfahren im ungünstigs-ten Fall, d.h. bei schlechter Vorsortierung der Elemente si, immerO(n log(n))Vergleiche benö-tigt. Für jedes Verfahren gibt es also mindestens eine Liste von Elementen {s1, . . . , sn} so dassdiese Zahl von Vergleichen notwendig ist.

Beweis. (i) n verschiedene Elemente si können in n! Permutationen, also verschiedenen Rei-henfolgen auftreten. Denn, für die erste Stelle gibt es n Möglichkeiten, für die zweite Stellebleiben n− 1Möglichkeiten, usw. Wir nennen P die Menge dieser Permutationen. Genau einedieser Permutationen liefert die sortierte Liste.

(ii) Ein Vergleich sa < sb teilt dieMenge der P Permutationen in zwei Teilmengen P = Pa∪Pb,diejenigen mit sa < sb und diejenigen mit sa > sb. Im ungünstigen Fall, bleibt die größere

46 ©2018,2019 Thomas Richter

Page 53: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.4. Stabiles Sortieren

der beiden Mengen übrig. Selbst ein optimaler Algorithmus wird also mit jedem Vergleichhöchstens zu einer Halbierung der Menge der Permutationen führen.

(iii) Es ist N0 := #P = n! die Anzahl der Permutationen zu Beginn. Im ungünstigstem Fallgilt also pro Schritt Ni = Ni−1/2, also

Ni = 2−in!

Eine eindeutige Reihenfolge steht somit nach

log2(n!)

Schritten fest. Denn dann gilt

2− log2(n!)n! =1

2log2(n!)n! =

n!

n!= 1,

sodass die verbleibende Teilmenge nur noch ein (somit sortiertes) Element hat.

(iv) Es giltlog2(n!) 6 n log2 n.

Mit Induktion. n = 1 klar.

log2((n+ 1)!) = log2(n!) + log(n+ 1) 6 n log2(n) + log(n+ 1) 6 (n+ 1) log2(n+ 1).

2.4. Stabiles Sortieren

Wir führen zunächst einen neuen Begriff ein.

Definition 2.25 (Stabiles Sortieren). Ein Sortierverfahren heißt stabil, wenn gleiche Werte nachDurchlauf des Verfahrens in der gleichen Reihenfolge sind wie vorher.

Beispiel 2.26. Wir betrachten die Liste L=[3,5,2,5] und schreiben diese aber als L=[3,5,2,5] um diebeiden Einträge mit dem Wert 5, d.h. Einträge 1 und 3 zu unterscheiden. Sortieren dieser Liste liefertentweder das Ergebnis [2,3,5,5] oder aber die Liste [2,3,5,5]. Im ersten Fall ist die Sortierung stabil,denn die Einträge 5 und 5 tauschen die Reihenfolge nicht. Im zweiten Fall liegt keine stabile Sortierungvor.

Es gilt nun, die bekannten Sortierverfahren Selection Sort, Algorithmus 2.1, Insertion Sort, Al-gorithmus 2.4, Min-Sort, Algorithmus 2.5, Merge Sort, Algorithmus 2.7 und Quicksort, Algo-rithmus 2.8 auf Stabilität zu analysieren.

Wir betrachten hier nur das zuletzt untersuchteQuicksort. Unsere Implementierung der Funk-tion def partitioniere(L,l,r) ist nicht stabil. Denn in Zeile 19 tauschen wir die Indizes jl und r,also das aktuelle Element und das Pivot-Element auch dann, wenn L[jl] == p gilt. Wir ändernalso die Reihenfolge von zunächst gleichen Elementen.

[email protected] 47

Page 54: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.5. Radix-Sort

2.5. Radix-Sort

Wir betrachten zumAbschluss noch ein Sortierverfahren,welches auf dermehrfachenAnwen-dung von stabilen Verfahren aufbaut und welches die Komplexität On besitzt. Bei Radix-Sortwird das Prinzip einer lexikographischen Ordnung angewandt. Wir betrachten hier als Grund-menge die natürlichen Zahlen N, die Methode lässt sich jedoch einfach erweitern. Eine Zahla ∈ N stellen wir in 10er Basis dar als

a = adad−1 · · ·a1a0 =d∑

i=0

10iai,

d.h. a0 ist die 1-er Stelle, a1 die 10-er Stelle, usw. Gegebenenfalls gilt az = az+1 = . . . ,ad = 0ab einem Index z > 0.

Die übliche Ordnung 6 auf N kann auch als lexikographische Ordnung formuliert werden:

a = b ⇔ ai = bi ∀i = 0, . . . ,d

a < b ⇔ ∃k ∈ {0, . . . ,d}mit ak < bk und ai = bi für i = k+ 1, . . . ,n.

Es gilt also z.B. 1029 < 1043, also

9︸︷︷︸a0

·100 + 2︸︷︷︸a1

·101 + 0︸︷︷︸a2

·102 + 1︸︷︷︸a3

·103 < 3︸︷︷︸b0

·100 + 4︸︷︷︸b1

·101 + 0︸︷︷︸b2

·102 + 1︸︷︷︸b3

·103,

da a1 = 2 < 4 = ba und 0 = a2 = b2 = 0 sowie 1 = a3 = b3 = 1. Anders gesprochen: es wirdzunächst die Ordnung an d-ter Stelle (hier 3-ter Stelle) überprüft, dann an d−1-ter Stelle, usw.bis hin zur 0-ten Stelle.

Dieses Prinzip lässt sich auf viele andere Zahlensystem oder auch komplett anders gestalteteMengen übertragen. Im Folgenden betrachten wir jedoch nur einfache Zahlen-Ziffern.

Idee von Radix-Sort: Gegeben MengeM mit Elementen s ∈ S, welche eine lexikographischeOrdnung bzgl. Ihrer Stellen maximal d Stellen s0, . . . , sd besitzt.

Eine Liste L=[s1,s2,...,sn] dieser Menge werden nach folgendem Prinzip sortiert:

1 for i in range (d+1) :2 Sor t i e re L bezügl ich S te l l e i s t ab i l

Beispiel 2.27. Wir betrachten die folgenden Zahlen und sortieren rückwärts nach den Ziffern.

48 ©2018,2019 Thomas Richter

Page 55: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.5. Radix-Sort

Schritt: vorher 1 2 3138 471 413 138471 413 713 167368 713 927 274413 453 628 368628 274 138 368798 646 646 413713 167 453 453646 927 167 471274 138 368 628368 368 368 646167 628 471 713927 798 274 798453 368 798 927

Man beachte, dass stets stabil sortiert wurde, d.h. Einträge mit jeweils gleicher Ziffer werden nie ge-tauscht.

Wir müssen also zunächst untersuchen, ob die bekannten Sortierverfahren stabil sind. Weiterkann Radix-Sort nur dann Sinn machen, wenn wir für die Sortierung nach einzelnen Ziffernein besonders schnelles Verfahren gefundenwird. Denn bei Radix-Sort müssen wir dieMengeder Elemente insgesamt dmal sortieren.

Satz 2.28. Merge-Sort und Insertion-Sort sind stabile Sortierverfahren.Quicksort ist im allgemeinennicht stabil. Es existieren jedoch stabile Varianten von Quicksort.

Beweis: Gegeben sei eine Menge Smit totaler Ordnung sowie eine Liste L=[s1,...,sn]mit Ele-menten dieser Menge. Insertion-Sort ist in Algorithmus 2.4 angegeben.

Wir betrachten jeweils einen Durchlauf der inneren Schleife, also Zeilen 4-6 des Algorithmus.Angenommen, für zwei Elemente L[k] und L[l]mit k < l gelte L[k]==L[l].

Angenommen, mindestens eines der Elemente hat einen Index größer i, also k > i oder l > i.Dann wird mindestens eines der Elemente sicher nicht getauscht, denn potentiell getauschtwerden nur Indizes j 6 i. Die Reihenfolge bleibt somit nach Durchlauf der Schleife bestehen.

Nun gelte 0 6 k < l 6 i. Falls die Schleife in Zeile 3 wegen L[j−1]<=L[j] bei j > l abbricht,so werden die Elemente mit Index k und l sicher nicht getauscht, denn aus k < l < j folgtk < j− 1.

Angenommen, der Index j = l wird erreicht. Nun folgen gegebenenfalls Tauschvorgänge,bei denen L[l] schrittweise mit Elementen L[l]=L[j]−>L[j−1]−>L[j−2]−>...−>L[s] getauschtwird.

Falls Abbruch bei s > k + 1, so wird die Reihenfolge der beiden Elemente nicht getauscht.Falls s = k+ 1 erreicht wird so kommt es wegen L[s]=L[l]=L[k]=L[s−1] zu einem Abbruch derSchleife.

Elemente mit gleichemWert werden also nie getauscht.

[email protected] 49

Page 56: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.5. Radix-Sort

Schnelles Sortieren kleiner Mengen

Für eine effiziente Umsetzung von Radix-Sort benötigen wir Verfahren zur Sortierung der Lis-ten nach einzelnen Ziffern. Speziell an dieser Aufgabe ist, dass nur eine sehr geringe Anzahlan verschiedenenWerten in jeder Ziffermöglich sind. Die Listen können beliebig lang sein, dieZiffern haben aber z.B. nur dieWerte {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, oder im Binärsystem lediglich dieWerte {0, 1}.

Bucket-Sort ist prinzipiell ein sehr schnelles Sortierverfahren. Zum Sortieren einer Liste von nElementen werdenO(n+m) Operationen benötigt, wobeim die Anzahl von möglicherweiseverschiedenen Elementen ist. Fallsm groß ist, so ist dieses Verfahren nicht effizient.

Sortieren wir jedoch nur nach einer Ziffer, so gibt es nur 10 unterschiedlicheWerte {0, 1, . . . , 9}.Es sind also nur 10 Eimer notwendig. Wir könnten somit auf die folgende Art nach der l-tenZiffer sortieren.

Algorithmus 2.10: Sortieren mit Bucket-Sort nach Ziffer l1 def bucketsort(L,l): # Liste L, Stelle l

2

3 B=[] # B ist eine Liste von Listen

4 for d in range(10): # Wir fuegen 10 leere Listen an

5 B.append([])

6

7 for i in range(n):

8 z = ziffer(L[i],l) # l−te Stelle von L[i]9 B[z].append(L[i]) # Fuege L[i] in richtigen Bucket ein

10

11 S=[] # Leere Ergebnis−Liste12 for i in range(10): # Durchlaufe die Buckets in korrekter Reihenfolge

13 for l in B[i]: # Durchlaufe die Elemente im Bucket

14 S.append(l) # Fuege Element an

15

16 return S

Die Funktion ziffer(..,..) soll dabei die entsprechende Ziffer zurück geben und muss nochgeschrieben werden, z.B. durch (einfache Funktion ohne jede Sicherheits-Abfrage)

1 def ziffer(x,l): # Rueckgabe der l−ten Ziffer von x2 return ( int(x/(10∗∗l)) % 10 )

Der Nachteil von Bucket-Sort ist der zusätzliche Speicheraufwand zum Speichern der Buckets,sowie das zweimalige Kopieren aller Elemente, zunächst in den Bucket, dann in die Ergebnis-liste.

Ein Vorteil von diesemAlgorithmus ist die Stabilität. Die Elemente werden in gleicher Reihen-folge in die Buckets gelegt, in der sie nachher auch entnommen werden.

Eine gute Alternative zum Sortieren nach Ziffern ist Counting-Sort. In einem ersten Durchgangwird gezählt, wie oft jede Stelle vorkommt. Im zweitenDurchgang können die Elemente gleichan der richtigen Stelle eingefügt werden.

50 ©2018,2019 Thomas Richter

Page 57: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.5. Radix-Sort

Algorithmus 2.11: Sortieren mit Counting-Sort nach Ziffer l1 def countingsort(L,l):

2 C = [0]∗10 # Vorkommen jeder Ziffer

3

4 for i in range(n): # Zaehle vorkommen der Ziffern

5 z = ziffer(L[i],l)

6 C[z]=C[z]+1

7

8 I = [0]∗10 # Start−Index fuer jede Ziffer9 for i in range(1,10):

10 I[i] = I[i−1] + C[i−1]11

12 S = [0]∗n # Ergebnisliste der Laenge n

13 for i in range(n):

14 z = ziffer(L[i],l)

15 S[I[z]] = L[i] # L[i] an richtige Stelle

16 I[z] = I[z]+1 # Index erhoehen

17

18 return S

Wirwählen hier wiederholt die Notation L=[0]∗n. Dies erstellt eine Liste mit dem Eintrag 0 undder Länge n. Man teste in Python den Effekt von Anwendungen der Art print([1,2,3]∗5).

ZumAbschluss vergleichen wir die verschiedenen Sortierverfahren für große Listen von Zah-len zwischen 0 und 108 und geben die gemessenen Laufzeiten an. Wir müssen bei Radix-Sortsomit stets 8 Durchgänge von Counting-Sort ausführen.

N Radix Quick Merge

20 0.0000046 0.0000036 0.00001740 0.0000089 0.0000081 0.00003180 0.0000110 0.0000146 0.000069

160 0.0000172 0.0000342 0.000126320 0.0000293 0.0000692 0.000241640 0.0000540 0.0001485 0.000529

1 280 0.0001024 0.0003106 0.0010212 560 0.0001962 0.0005980 0.0006425 120 0.0001327 0.0004300 0.00130010 240 0.0002474 0.0008956 0.00347820 480 0.0004856 0.0018293 0.00522640 960 0.0010229 0.0037729 0.01019781 920 0.0016786 0.0061296 0.016180

163 840 0.0028981 0.0113089 0.030600327 680 0.0055382 0.0235784 0.062167655 360 0.0111745 0.0500609 0.128783

1 310 720 0.0227151 0.1042850 0.2682302 621 440 0.0455057 0.2149980 0.5505685 242 880 0.0919180 0.4507670 1.160730

[email protected] 51

Page 58: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

2.5. Radix-Sort

x

x log(x)Merge-SortQuick-SortRadix-Sort

1× 1071× 10610000010000100010010

10.0000000

1.0000000

0.1000000

0.0100000

0.0010000

0.0001000

0.0000100

0.0000010

0.0000001

Der Unterschied zwischen der Laufzeit O(n) und O(n log(n)) scheint nicht sonderlich großund wird nur für sehr große Listen deutlich.

52 ©2018,2019 Thomas Richter

Page 59: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3. Graphentheorie

Unter einemGraphwerdenwir eine Paar bestehend ausKnoten undKanten verstehen. Knotenkönnen z.B. Länder sein, Kanten gemeinsame Grenzen. Oder Knoten sind Straßenbahnhalte-stellen, Kanten sind die Schienenverbindungen dazwischen. An diesen beiden Beispielsgra-phen lassen sich einige wichtige Probleme der Graphentheorie beschreiben:

• Wie komme ich am schnellsten von der Haltestelle A zur Haltestelle B, wenn ich nurentlang der Schienen fahre? Dies ist das übliche Problem der Routenplanung.

• Wie kann ich die Länder auf einer Landkarte mit möglichst wenig verschiedenen Farbenso einfärben, dass je zwei Länder mit gemeinsamer Grenze eine unterschiedliche Farbehaben? Dies ist das Färbeproblem.

• Wie plane ich eine möglichst schnelle Route über alle Haltestellen im Straßenbahnnetz?Dies ist das Problem des Handlungsreisenden (Traveling Salesman).

• Ist es möglich durch Grenzübertritte von jedem Land der Karte zu jedem anderen Landzu kommen? Dieses Problem nennt man den Zusammenhang im Graphen.

In den folgenden Abschnitten werden wir diese - und weitere - Probleme der Graphentheorieanalysieren und Algorithmen zu deren Umsetzung untersuchen und implementieren. Einegroße Herausforderung vieler Probleme der Graphentheorie ist die Komplexität. Die von unsbeschriebenen Suchverfahren besaßen alle eine Komplexität von n (Radix-Sort), n log(n) (z.B.Merge-Sort oder Quicksort im durchschnittlichen Fall, aber auch jedes optimale vergleichsba-sierteVerfahren) odern2 (einfacheVerfahrenwie Bubble Sort). In derGraphentheorie tauchenProbleme auf, für die bisher überhaupt kein Algorithmus bekannt ist, der das Problem in po-lynomialer Laufzeit, d.h. mit der Komplexität np für ein festes p ∈ N löst.1 Das Problem desHandlungsreisenden kann z.B. exakt mit dem Aufwand n! gelöst werden, für die praktischeUmsetzung ist dieser Aufwand jedoch schon bei kleinem n viel zu groß.

Im Rahmen dieser Vorlesung wird nur ein kurzer Einblick in die Graphentheorie mit einigengrundlegenden Problemen, Anwendungen und Algorithmen gegeben. Zum Weiterlesen gibtes viele empfehlenswerte Bücher, z.B. Diestel [1], Korte und Vygen [4] oder Schrijver [6].

3.1. Grundlagen

Definition 3.1 (Graph). Ein Graph (V,E) ist ein Paar, wobei V 6= ∅ und E ⊂ V × V Mengen sind.Die Elemente aus V heißen die Knoten, die Elemente aus E die Kanten.Ein Graph heißt ungerichteter Graph, falls für x,y ∈ V und (x,y) ∈ E auch (y, x) ∈ E folgt.Ansonsten heißt er gerichteter Graph.1Mit n werden wir in der Graphentheorie meist die Anzahl an Knoten oder die Anzahl an Kanten im Graphenbezeichnen, oder aber auch Sie summe beider Werte.

53

Page 60: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.1. Grundlagen

Abbildung 3.1.: Karte von Europa.Diese Karte entstammt den Artikel https://de.wikipedia.org/wiki/Europa, lizenziert unter der Creati-ve Commons Attribution-Share Alike 3.0 Unported Lizenz(https://creativecommons.org/licenses/by-sa/3.0/legalcode). DieAutoren sind auf der Wikipedia-Seite genannt.

Ein Graph heißt vollständiger Graph, falls er für alle x,y ∈ V mit x 6= y eine Kante (x,y) ∈ Ebesitzt.Bemerkung 3.2 (Gerichtete und ungerichtete Graphen). Wir werden uns im wesentlichen mitungerichtetenGraphen beschäftigen. Zu jeder Kante (x,y) ∈ E existiert also auch eine Kante (y, x) ∈ E.Alternativ können wir in einem ungerichteten Graphen eine Kante als eine zweielementige Teilmenge{x,y} anstelle eines geordneten Paares auffassen. Denn für Mengen gilt {x,y} = {y, x}. Wir werdenjedoch meist die Schreibweise (x,y) verwenden und jeweils im Zusammenhang klarstellen, ob wir einengerichteten oder einen ungerichteten Graphen betrachten.

Beispiel 3.3. Wir betrachten einige Beispiel.1. Gegeben sei die Karte in Abbildung 3.1. Wir bilden einen Graphen nach folgendem Prinzip:V = {x | x ist ein Land Europas}, E = {(x,y) ∈ V×V | x und y haben eine gemeinsame Landgrenze}.Zunächst handelt es sich natürlich nach einem Graphen entsprechend der obigen Definition. Die-ser Graph ist

• ungerichtet, da - falls eine Grenze zwischen Land x und Land y gibt - es auch eine Grenzezwischen Land y und Land x gibt.

54 ©2018,2019 Thomas Richter

Page 61: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.1. Grundlagen

• nicht vollständig, da es nicht zwischen allen Ländern eine Grenze gibt, z.B. nicht zwischenPortugal und Albanien.

2. Wir bilden den Graphen nun nach folgendem Prinzip:

V = {x | x ist Land Europa}, E = {(x,y) ∈ V×V | es fließt ein Fluss von Land x nach Land y}.

Auch hierbei handelt es sich um einen Graphen bestehend aus Knoten - den Ländern - und denKanten, einer Teilmenge der Produktmenge V × V . Die genauen Eigenschaften lassen sich nurmit hinreichend geographischen Kenntnissen klären, aber vermutlich

• ist dieser Graph nicht vollständig, da es z.B. keinen Fluss gibt, der von den Niederlandennach Polen fließt.

• Ebenso ist der Graph vermutlich ein ungerichteter Graph, da es z.B. keinen größeren Flussgibt, der von Portugal nach Spanien fließt, viele jedoch in anderer Richtung.

3. Schließlich betrachten wir einen Graphen nach dem Prinzip:

V = {x | x ist Hauptstadt in Europa}, E = {(x,y) ∈ V × V | man kann von x nach y reisen}.

Eine Kante zwischen zwei Städten besteht laut dieser Definition nicht nur dann, wenn es einedirekte Reiseverbindung gibt, sondern wenn eine Reise überhaupt möglich ist. Wieder handelt essich um einen Graphen, dieser ist (derzeit) ungerichtet und vollständig.

Bemerkung 3.4. Diese von uns verwendete Definition ist eine Vereinfachung. Zwischen zwei Knotenx,y ∈ V kann es in “unseren Graphen” nur eine einzige Kante e = (x,y) ∈ E geben. Der Begriff desGraphen kann so erweitert werden, dass auch parallele Kanten zwischen zwei Knoten möglich sind(man spricht dann von einemMultigraphen).

Wir werden uns hier im wesentlichen mit ungerichteten Graphen befassen. Falls es eine Kante(x,y) ∈ E gibt, so ist die Kante (y, x) auch Element von E. Oder anders ausgedrückt, wirkönnen eineKante als ungeordnetes Paar {x,y} = {y, x} auffassen.Wirwerden jedochweiterhin(x,y) schreiben.

Es seien nun x,y ∈ V zwei Knoten, e = (x,y) ∈ E eine Kante. Wir sagen: “x und y sind dieEndknoten von e”, “Die Knoten x und y sind mit der Kante e inzident” und “die Knoten x undy sind benachbart oder auch adjazent”.

Definition 3.5 (Nachbarschaft). Sei (V,E) ein ungerichteter Graph. Für einen Knoten x ∈ V istdurch

N(x) = {y ∈ V \ {x} | (x,y) ∈ E} ⊂ V

undδ(x) = {(x,y) ∈ E} ⊂ E

die Nachbarschaft definiert.Zwei Knoten x,y ∈ V heißen benachbart, wenn es eine Kante e = (x,y) ∈ E gibt.Zwei Kanten e1, e2 ∈ E heißen benachbart, wenn es einen Knoten x ∈ V gibt, der Teil beider Kantenist.

Definition 3.6 (Grad). DerGrad eines Knotens x ist die Anzahl der mit x inzidenten Kanten |δ(x)|.

[email protected] 55

Page 62: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.1. Grundlagen

In Bezug auf Beispiel 3.3 (1.) gilt z.B.

N(Niederlande) = {Belgien, Deutschland}

sowie

δ(Niederlande) = {(Niederlande, Belgien), (Niederlande, Deutschland)},

d.h. der Grad der Niederlande ist 2.

Die Nachbarschaft von x besteht also aus allen Knoten y ∈ V , die durch eine Kante (x,y) mitx verbunden sind. Der Begriff kann erweitert werden zur Nachbarschaft einer Teilmenge vonKnoten X ⊂ V :

δ(X) = {e = (x,y) ∈ E | x ∈ X, y ∈ V \ X},

N(X) =⋃x∈X

N(x) \ X.

Definition 3.7 (Kantenzüge). Sei (V,E) ein Graph. Ein Kantenzug von x ∈ V nach y ∈ V ist eineFolge von Knoten xi ∈ V für i = 0, . . . ,k und Kanten ei = (xi−1, xi) ∈ E für i = 1, . . . ,k mit

x = x0e1−→ x1

e2−→ x2e3−→ · · · xk−1

ek−→ xk = y.

Im Fall x = y handelt es sich um einen geschlossenen Kantenzug

x = x0e1−→ x1

e2−→ x2e3−→ · · · xk−1

ek−→ xk = x.

Wir betrachten wieder den Graphen aus Beispiel 3.3 (1.) dann ist durch

Belgien→ Frankreich→ Spanien (3.1)

ein Kantenzug gegeben, nicht aber durch

Belgien→ Deutschland→ Spanien,

da es keine Kante Deutschland→ Spanien gibt. Auch durch

Belgien→ Niederlande→ Belgien→ Deutschland, (3.2)

ist ein Kantenzug gegeben. Dass der Knoten Belgien doppelt vorkommt ist kein Problem.

Definition 3.8 (Wege und Kreise). Es sei

x0e1−→ x1

e2−→ x2e3−→ · · · xk−1

ek−→ xk

ein Kantenzug mit paarweise verschiedenen Knoten {x0, . . . , xk}. Der TeilgraphP := (V ′,E ′) = ({x0, . . . , xk}, {e1, . . . , ek}) ⊂ (V,E)

wird Weg genannt. Wird ein Weg P mit mindestens 3 Knoten (d.h. k > 2) um eine Kante ek+1 =(xk, x0) erweitert, so dass

x0e1−→ x1

e2−→ x2e3−→ · · · xk−1

ek−→ xkek+1−−−→ x0

ein geschlossener Kantenzug ist, so heißt er KreisC := (V,E) = ({x0, . . . , xk}, {e1, . . . , ek, ek+1}).

Die Länge eines Weges oder Kreises ist die Anzahl seiner Kanten.

56 ©2018,2019 Thomas Richter

Page 63: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.2. Graphentheoretische Probleme

Bemerkung 3.9 (Kantenzüge und Wege). Der Unterschied zwischen Kantenzügen und Wegenscheint zunächst sehr klein. Mit einem Kantenzug meinen wir eine Abfolge von Knoten und Kanten,unter einem Weg verstehen wir einen Graphen, der durch die Knoten und Kanten eines Kantenzugsgebildet wird - wenn kein Knoten doppelt vorkommt.

Betrachten wir den Kantenzug (3.1) so ist der zugehörige (gerichtete) Graph gegeben durch

V = {Belgien,Frankreich,Spanien}, E = {(Belgien,Frankreich), (Frankreich,Spanien)},

ein Teilgraph des ursprünglichen Graphen aus Beispiel 3.3.

Man sagt “der Weg P verbindet die Knoten x0 und xk”. Die Suche von verbindenden Wegen mitspeziellen Eigenschaften (z.B.möglichst kurz) ist eine der großenAufgaben der Graphentheo-rie. In allgemeinen Graphen muss es zwischen zwei Knoten x,y ∈ V nicht unbedingt einenWeg P geben.

Definition 3.10 (Zusammenhängend). Ein Knoten y ∈ V heißt von x ∈ V aus erreichbar, falls eseinen Weg gibt der x und y verbindet.

Ein Graph (V,E) heißt zusammenhängend, falls es zu je zwei Knoten x,y ∈ V einen Weg gibt, der xund y verbindet. Ansonsten heißt der Graph unzusammenhängend.

Graph 1 aus Beispiel 3.3 ist z.B. nicht zusammenhängend. Der Knoten Malte ist von Polen durchkeinen Weg erreichbar. Graph 3 hingegen ist zusammenhängend, da es eine Möglichkeit gibt,von jeder Hauptstadt in jede andere Hauptstadt zu reisen.

Satz 3.11. Ein ungerichteter Graph G = (V,E) ist genau dann zusammenhängend, falls für alle nichtleeren, echten Knotenteilmengen

X ⊂ V mit X 6= ∅ und X 6= V

δ(X) 6= ∅ gilt.

3.2. Graphentheoretische Probleme

Im Folgenden werden einige wichtige Probleme der Graphentheorie vorgestellt.

A. Das Färbeproblem Wie viele verschiedene Farben sind notwendig um eine Landkarte sozu färben, dass zwei Länder mit gemeinsamer Grenze (von positiver Länge, also keine Punkt-Grenzen) stets eine unterschiedliche Farbe haben?

[email protected] 57

Page 64: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.2. Graphentheoretische Probleme

Karte der deutschen Bundesländer inkl. Färbung der Länder mit insgesamt vier Farben, sodass zwei benachbarte Länder nie die gleiche Farbe haben. [Bild: public domain, veröffentlichtauf https://programmingwiki.de/Färbungsprobleme]

Die Lösung dieses Problems heißt der Vier-Farben-Satz. Es genügen also stets vier Farben.

Satz 3.12 (Vier-Farben-Satz). Es genügen stets vier Farben, um die Länder einer Landkarte in dereuklidischen Ebene so zu färben, dass keine zwei benachbarten Länder die gleiche Farbe besitzen.

Dieser Satz wurde bereits gegen 1850 formuliert, konnte aber erst über hundert Jahre späterbewiesen werden. Die Beweise zum Vier-Farben-Satzwerden mit Hilfe von Computern durch-geführt: zunächst wird die Anzahl möglicher typischer Konfigurationen reduziert (nach wievor auf über 1000 verschiedene). Auf diesenKonfigurationenwird das FärbeproblemmitHilfedes Computers gelöst.

Die Kartenfärbung kann mit Hilfe eines Graphen beschrieben werden.

58 ©2018,2019 Thomas Richter

Page 65: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.2. Graphentheoretische Probleme

Die Knoten des Graphen sind die Bun-desländer V = {x1, . . . , x16}, eine Kantee = {xa, xb} ∈ E ⊂ V × V existiert zwi-schen Bundesländern xa, xb mit gemein-samer Grenze (positiver Länge).Gesucht ist eine Färbung der Knoten f :V → {1, . . . ,M} mit M ∈ N möglichstklein, so dass f(xa) 6= f(xb) für alle e ={xa, xb} ∈ E.[Bild: public domain, modifiziert. Veröf-fentlicht auf https://programmingwiki.de/Färbungsprobleme]

Definition 3.13 (Kantengewichte). Es sei G = (V,E) ein Graph. Eine Abbildung

f : E→ R

wird Gewichtsfunktion genannt. Für e ∈ E wird durch ce = f(e) das Kantengewicht bezeichnet.

Wir betrachten meist nur positive Kantengewichte, also

f : E→ R>0.

Für einige Algorithmen wird diese Annahme ganz wesentlich sein.

B. Routenplanung Gesucht ist die kürzeste (oder schnellste oder schönste) Route zwischenzwei Städten.

[email protected] 59

Page 66: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.2. Graphentheoretische Probleme

Gegeben ist ein gewichteter Graph G =(V,E, f) sowie zwei Knoten x,y ∈ V . Ge-sucht ist einWeg

P = ({x = x0, . . . , xk = y}, {e1, . . . , ek})

der x und y verbindet und das kleinstesummierte Kantengewicht hat

k∑i=1

cek→ min.

[Bild: von Fremantleboy unterCreative-Commons-Lizenz 2.52, veröffentlicht auf https://www.wikipedia.de.]

C. Traveling Salesman - Problem des Handlungsreisenden Gesucht ist die kürzeste (oderschnellste oder schönste) Route, auf welcher eine gegebene Menge von Städten jeweils genaueinmal besucht wird.

[Bild: von Fremantleboy unter CC BT 2.5 Lizenzauf www.wikipedia.de.]

Gegeben ist ein vollständiger gewichteterGraph (V,E, f). Gesucht ist der Kreis

C = ({x1, . . . , xk}, {e1, . . . , ek}),

welcher alle Knoten des Graphen verbin-det und dessen summiertes Gewicht

k∑i=1

cek→ min

minimal ist.

2https://creativecommons.org/licenses/by/2.5/deed.de

60 ©2018,2019 Thomas Richter

Page 67: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.2. Graphentheoretische Probleme

Wir werden sehen, dass dieses Problem sehr schwer ist. Bisher ist kein effizienter Algorithmusbekannt, der dieses Problem in einer polynomiellen Laufzeit, d.h. mit der Komplexität (np)für ein festes p ∈ N löst. Es ist überhaupt nicht bekannt, ob sich dieses Problem effizient lösenkann. Hier verbirgt sich ein wichtiges Problem der Komplexitätstheorie auf welches wir nochzurückkommen. Das Problem hat vielfältige Anwendungen, z.B. bei der Routenplannung fürdie Auslieferung von Paketen oder Briefen.

Definition 3.14 (Euler-Tour). Es sei G = (V,E) ein Graph. Ein Kantenzug

x0e1−→ x1 · · ·

ek−→ xk,

der jede Kante e ∈ E genau einmal verwendet heißtEuler-Tour. Ist Fall eines geschlossenenKantenzugsx0 = xk nennen wir die Tour einen Euler-Kreis.

Ein Euler-Kreis ist nicht unbedingt ein Kreis, da jede Kante zwar nur genau einmal vorkom-men darf, die Knoten jedoch mehrfach auftauchen dürfen.

D. Königsberger Brückenproblem Ist es möglich, über jede der sieben Brücken genau einmalzu gehen und dann wieder am Ausgangspunkt anzukommen?

[Bilder: unter der Creative Commons Attribution-Share Alike 3.0 Lizenz auf www.wikipedia.deveröffentlicht. ]

Es gilt:

Satz 3.15. Es seiG = (V,E) ein zusammenhängender Graph. Es existiert genau dann ein Euler-Kreisin G, wenn alle Knoten x ∈ G einen geraden Grad haben.

E. GraphpartitionierungGegeben ist ein GraphG = (V,E)mit n ∈ NKnoten. Gesuch ist eineAufteilung (=Partitionierung) des Graphens in p ∈ N Teilgraphen Gi = (Vi,Ei) mit Vi ⊂ Vund Ei ⊂ E und den folgenden Eigenschaften

• Die Teilgraphen haben gleich viele Knoten

|Vi| =|V |

p

• Bei der Erstellung des Teilgraphen werden möglichst wenige Kanten geschnitten, d.h.die Menge

E ′ := {e = (x,y) ∈ E | x ∈ Vi und y ∈ Vj i 6= j}

ist möglichst klein.

[email protected] 61

Page 68: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.2. Graphentheoretische Probleme

Die beiden Ziele sind üblicherweise nicht exakt zu erreichen. Ein Graph mit 100 Knoten kannnatürlich nicht in 3 Graphen mit exakt gleicher Knotenzahl geteilt werden. Aber selbst wenneine theoretisch optimale Partitionierung existiert, so sind imallgemeinen Fall keine effizientenAlgorithmen bekannt, diese auch zu berechnen. Die Graphpartitionierung ist ein sogenanntesNP-vollständiges Problem. Es ist nicht nur kein Algorithmus bekannt, der das Problem in einer(in der Anzahl n der Knoten) polynomiellen Zeit optimal löst, selbst zur Überprüfung einermöglichen “Lösung” aufOptimalität ist keinAlgorithmusmit polynomieller Laufzeit bekannt.

Das Problem hat viele Anwendungen im parallelen Rechnen, also dem Ausnutzen von Paral-lelrechnern zur Beschleunigung von Algorithmen:

• Wie teile ich eine Aufgabe (der Größe n) gleichmäßig auf p Recheneinheiten auf, dieihre jede Teilaufgabe parallel (also gleichzeitig) bearbeiten.

• Dabei soll die Kommunikation zwischen denRecheneinheitenmöglichst gering gehaltenwerden, da diese zusätzlichen Aufwand bedeutet.

Da zur exakten Lösung der Graph-Partitionierung keine Algorithmen existieren, müssen heu-ristische Verfahren zur Approximation eingesetzt werden. Für die praktische Anwendung istdies im allgemeinen auf ausreichend. Wenn eine Aufgabe der Größe n = 1 000, 000 auf 1 000Recheneinheiten partitioniert werden kann, so kann die Rechenzeit theoretisch um den Fak-tor 1 000 reduziert werden. Ist die Graph-Partitionierung aber nicht exakt, angenommen dieTeilgraphen haben zwischen 900 und 1 100 Knoten, so ist die Zeitersparnis nach immer noch1 000 000 : 1100 ≈ 910.

F. Bandbreiten-Problem Wir definieren:

Definition 3.16 (Bandweite eines Graphen). Es sei G = (V,E) ein Graph mit n Knoten undeiner eineindeutigen Nummerierung f : V → {1, . . . ,n} (d.h. jedem Knoten ist eine eineindeutigeNummer zugeordnet). Unter der Bandbreite des Graphen bezeichnen wir die maximale Differenz derKnotennummern in einer Kante, d.h. die Kenngröße

maxe=(x,y)∈E

|f(x) − f(y)|.

Unter dem Bandbreiten-Problem verstehen wir nun die folgende Aufgabe: Gesucht ist eineeineindeutige Nummerierung f : V → {1, . . . ,n} der Knoten, so dass die Bandbreite des Gra-phen möglichst minimal ist.

Auch das Bandbreiten-Problem ist ein Problem, für welches keine effizienten Algorithmen zurexakten Lösung bekannt sind, es gehört zur Klasse der NP-schweren Probleme.

Das Bandbreiten-Problem taucht als Teilaufgabe der numerischenMathematik auf. Hier müs-sen oft lineare Gleichungssysteme mit einer speziellen Struktur gelöst werden:

Definition 3.17 (Dünn besetzte Matrix, Bandmatrix). Es seiA ∈ Rn×n eine Matrix. Wir nennendie Matrix A = (aij)ij eine Bandmatrix mit Bandbreitem ∈ N, falls

aij = 0 ∀|i− j| > m,

d.h. wenn alle Einträge einer Matrix außerhalb des Bandes mit Breite 2m (m nach rechts undm nachlinks von der Diagonale aus) verschwinden. Generell nennen wir eine Matrix dünn besetzt, wenn dieAnzahl der nicht-Null Elemente klein ist, d.h.

|{(i, j) ∈ {1, . . . ,n}2 | aij 6= 0}|� n2.

62 ©2018,2019 Thomas Richter

Page 69: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.3. Darstellung und Umsetzung

Die Definition von dünn besetzt ist nicht eindeutig. Eine strenge Definition bedeutet, dass injeder Matrix-Zeile nur eine feste (unabhängig von n) Anzahl an Elementen ungleich Nullsind.

Zu jeder Matrix A ∈ Rn×n kann nach folgendem Prinzip ein Graph erstellt werden

V = {1, 2, . . . ,n}, E = {(i, j) ∈ V × V | aij 6= 0}.

Die Lösung des Bandbreiten-Problems bedeutet für die Matrix, dass sie bei entsprechenderUmsortierung in eine Bandbreite mit möglichst geringer Bandbreite m transformiert wird.Eine effiziente Durchführung der Gauß-Elimination bei einer Bandmatrix benötigt O(nm2)Operationen anstelle von O(n3) Operationen bei einer allgemeinen Matrix. Ist n groß und mverhältnismäßig klein, so ist der Unterschied wesentlich.

3.3. Darstellung und Umsetzung

Zum Beschreiben eines Graphen (V,E) mit nV ∈ N Knoten V = {x1, . . . , xnV} und nE ∈ N

Kanten E = {e1, . . . , enE} eignet sich z.B. die Adjazenzmatrix A ∈ RnV×nV mit der Eigenschaft

A = (aij)nV

i,j=1, aij =

{1 (xi, xj) ∈ E0 sonst

.

6

5

4

3

21

Es ist ein ungerichteter Graph mit 6 Knoten und 8Kanten.

A =

0 1 1 1 0 11 0 0 1 1 11 0 0 1 0 01 1 1 0 0 00 1 0 0 0 01 1 0 0 0 0

Zur Speicherung des Graphen in einer Adjazenz-matrix sind 36 Matrixeinträge notwendig.

In praktischen Anwendungen und gerade bei der Implementierung von Verfahren auf demComputer ist die Adjazenzmatrix nicht effizient. Betrachtet man z.B. das Straßensystem zwi-schen den etwa 2 000 Städten in Deutschland (den Knoten eines Graph) so stellt man fest, dassjede Stadt nur mir einigen wenigen Städten direkt (also mit einer Kante) verbunden ist. DieSpeicherung der Adjazenzmatrix würde 2 0002 = 4 000 000 Einträge benötigen.

Eine alternative Speicherung eines Graphen ist die Inzidenzmatrix A ∈ RnV×NE mit der Ei-genschaft

A = (aij)nV ,nE

i,j=1 , aij =

{1 (xi, x

′i) = ej ∈ E

0 sonst.

[email protected] 63

Page 70: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.3. Darstellung und Umsetzung

Ein Matrixeintrag aij ist somit 1, falls es eine Kante ej gibt, welche den Knoten xi als End-punkt hat. Diese Art der Darstellung hat meist einen noch größeren Speicherbedarf, da in denmeisten Graphen jeder Knoten mindestens zu einer Kante gehört.

Wir geben die Inzidenzmatrix (Knoten-Kanten-Matrix) an. Hierzu benennen wir zunächstdie 8 Kanten. Dabei setzen wir im ungerichteten Graphen eine Kante (x1, x2) mit der Kante(x2, x1) gleich.

e1 = (3, 4) e2 = (1, 3) e3 = (1, 4) e4 = (2, 4),

e5 = (2, 5) e6 = (1, 2) e7 = (1, 6) e8 = (2, 6)

Die Inzidenzmatrix ergibt sich als

A =

0 1 1 0 0 1 1 00 0 0 1 1 1 0 11 1 0 0 0 0 0 01 0 1 1 0 0 0 00 0 0 0 1 0 0 00 0 0 0 0 0 1 1

Die Inzidenzmatrix benötigt mit nV ×nE Einträgen im Allgemeinen noch weit mehr Speicherals die Adjazenzmatrix. Eine effiziente Speicherung eines Graphen erfolgt mit der Adjazenz-liste. Hier wird für jeden Knoten die Menge der Knoten (oder alternativ Kanten) gespeichert,die mit diesem Knoten verbunden sind. Die Adjazenzliste ist eine dünne Struktur (im engl.sparse) zur Speicherung des Graphen.

Wir geben schließlich die Adjazenzliste des Graphen an

1 7→ 2, 3, 4, 6

2 7→ 1, 4, 5, 6

3 7→ 1, 4

4 7→ 1, 2, 3

5 7→ 2

6 7→ 1, 2

(3.3)

Gespeichert werden müssen schließlich 6 Listen mit insgesamt 16 Einträgen.

3.3.1. Realisierung in Python

Es gibt Erweiterungsbibliotheken für Python zur effizienten Handhabung von Graphen. Hiersind wichtige Algorithmen bereits implementiert, so dass die vorgestellten Probleme schnellund effizient behandelt werden können. ImRahmen dieser Vorlesungwerdenwir diese Biblio-theken zunächst nicht verwenden, sondern die Darstellung von Graphen selbst implementie-ren. Hierzu wählen wir Adjazenzlisten als sparsame und effiziente Form. Für uns besteht einGraph aus

• n Knoten

• für jeden der n Knoten wählen wir eine Liste um die möglichen Kanten zu den weiterenn− 1 Knoten zu speichern

64 ©2018,2019 Thomas Richter

Page 71: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.3. Darstellung und Umsetzung

In Python können Listen als Elemente wieder Listen enthalten. Wir setzen als Beispiel denGraphen aus (3.3) um. Die Adjazenzliste dieses Graphen, bestehend aus 6 Knoten, kann durchdie folgende doppelte Liste dargestellt werden:

1 n = 6 # Anzahl der Knoten

2 G = [[1,2,3,5],[0,3,4,5],[0,3],[0,1,2],[1],[0,1]]

3

4 print(G) # Ausgabe

Man beachte, dass die Indizes im Vergleich zu (3.3) um eins verschoben sind. Dies ist not-wendig, da Python bei Null zu zählen beginnt. In der Liste G[0] stehen die Kanten vom erstenKnoten gespeichert. In Python hat dieser den Index 0, im Beispiel (3.3) den Index 1. Im Fol-genden werden wir stets bei 0 anfangen. Das kurze Python-Beispiel zeigt, dass die Anzahl derKnoten nicht unbedingt gespeichert werden muss. Denn es sollte stets n == len(G) gelten.

Wir werden stets darauf achten, dass die Einträge in jeder Liste G[i] für i = 0, . . . ,n−1 sortiertsind. Dies können wir einfach mit den verschiedenen Sortierverfahren erreichen, die wir inKapitel 2 diskutiert haben. Darüber hinauswerdenwir in der Adjazenzliste nie eine Kante voneinem Knoten i zu sich selbst speichern. Schließlich betrachten wir nur ungerichtete Graphen.Wir geben beispielsweise einige grundlegende Algorithmen an:

1. Die Funktion test_ungerichtet prüft, ob der gegebene Graph ungerichtet ist

2. Die Funktion graph_aufraemen sortiert die Adjazenzliste, entfernt doppelte Kanten undentfernt selbstverweise, d.h. Kanten der Art (i, i)

1 # Testet, ob der Graph G ungerichtet ist

2 # Rueckgabe: True falls ungerichtet , False falls gerichtet

3 def test_ungerichtet(G):

4 for k in range(len(G)): # alle Knoten durchlaufen

5 for j in G[k]: # Nachbarn von k, d.h. Kanten(k,j) durchlaufen

6 if not(k in G[j]): # Es gibt keine Kante (j,k)

7 return False # ... Graph ist nicht ungerichtet

8 return True # Graph ist gerichtet

9

10 # Transformiert Graph in Standard−Form11 # − Nachbarn sortiert

12 # − keine doppelten Kanten

13 # − keine Kante zu eigenem Knoten

14 def graph_aufraeumen(G):

15 for k in range(len(G)): # Alle Knoten durchlaufen

16 SORT(G[k]) # Sortieren der Nachbarn

17 # SORT ist ein beliebiges Sortierverfahren

18

19 S = [] # neue, bereinigte Liste für Nachbarn

20 j = −1 #

21 for i in G[k]: # Alle Nachbarn durchlaufen

22 if i != j: # Neuer Nachbar?

23 j = i # dann merken

24 if i!=k: # Nicht der aktuelle Knoten?

[email protected] 65

Page 72: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

25 S.append(i) # dann einfuegen

26

27 G[k] = S # neue Liste speichern

3.4. Zusammenhang

Ein Graph G = (V,E) ist zusammenhängend, wenn es zu je zwei Knoten x,y ∈ V einen Wegin G gibt, der x und y verbindet. Im Folgenden betrachten wir Algorithmen zur Suche vonWegen, sowie zur Bestimmung aller Knoten y ∈ V , die von einem x ∈ V aus erreichbar sind.

Definition 3.18 (Zusammenhangskomponente). Es sei G = (V,E) ein ungerichteter Graph. Diemaximalen zusammenhängenden Teilgraphen von G sind die Zusammenhangskomponenten vonG.Beispiel 3.19. Der folgende Graph mit 5 Knoten und 3 Kanten

1

2

3

4

5

hat zwei Zusammenhangskomponenten G1 und G2:

G1 = (V1 = {1, 2, 3}, E1 = {(1, 2), (2, 3)}), G2 = (V2 = {4, 5}, E2 = {(4, 5)}).

Im Folgenden werden wir Algorithmen untersuchen, um die Zusammenhangskomponenteneines Graphen zu bestimmen. Der Grundalgorithmus ist die Durchmusterung von Graphen.Ausgehend von einem Knoten x ∈ V suchen wir alle Knoten y ∈ V , die von x aus erreichbarsind.

Wir benötigen einige weitere Begriffe.

Definition 3.20 (Baum und Wald). Ein ungerichteter Graph heißt Wald, falls er keinen Kreis ent-hält. Ein Baum ist ein zusammenhängender Wald. Knoten vom Grad 1 in einem Baum heißen Blatt.

Die Zusammenhangskomponenten eines Waldes sind Bäume.

Bäume eignen sich, um die Zusammenhangskomponenten eines Graphen darzustellen. DerGraph aus Beispiel 3.19 ist ein Wald. Jede der beiden Zusammenhangskomponenten ist einBaum. Die Knoten 1, 3, 4, 5 sind Blätter.

Das grundlegende Verfahren zur Bestimmung von Zusammenhangskomponenten in einemGraphen G = (V,E) ist die Graphdurchmusterung.Wir geben zunächst einen Algorithmus an, der auch umgangsprachlich formulierten Pseudo-Code enthält. Zeilen, die nicht in Python formuliert sind, sind rot unterlegt.

66 ©2018,2019 Thomas Richter

Page 73: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

Algorithmus 3.1: GraphdurchmusterungGegeben ungerichteter Graph G als Adjazenzliste (doppelte Liste) mit n=len(G) Knoten sowieausgezeichneter Knoten x ∈ {0, . . . ,n−1}. Ausgabe: Liste K aller Knoten, die von x aus erreich-bar sind sowie die Adjazenzliste Z eines aufspannenden Baumes.

1 def graph_durchmusterung(G,x):

2 n = len(G) # Anzahl der Knoten im Graph

3 Z=[] # Adjazenzliste der Zusammenhangskomponente

4 for i in range(n): # leere Listen fuer Nachbarn einfuegen

5 Z.append([])

6 K=[x] # Knoten in der Zusammenhangskomponente

7 Q=[x] # Hilfsliste

8 while len(Q)>0:

9 wähle y aus Q # Ein Element y auswaehlen. Details spaeter!

10 falls ein Nachbar z von y existiert , der noch nicht in K ist:

11 K.append(z) # Knoten z hinzufuegen

12 Z[y].append(z) # Kante (y,z) hinzufuegen

13 Z[z].append(y) # Kante (z,y) hinzufuegen

14 Q.append(z) # von z aus weitersuchen

15 else:

16 Q.remove(y) # y hat keine neuen Nachbarn mehr

Die grundlegende Idee von diesem Algorithmus ist es, die Zusammenhangskomponente ZStück für Stück aufzubauen. Hierzu wird eine temporäre Liste Q geführt. Zunächst wird derKnoten x in Q eingefügt. Dann wird in Zeile 9 jeweils ein Knoten y aus der Liste Q entnommen.Wir wissen, dass dieser Knoten y von x aus erreichbar ist. Ein Nachbar z von y ist also auchvon x aus erreichbar und wird daher den Listen Q sowie Z hinzugefügt. Hat ein Knoten y keineneuen Nachbarn mehr, also solche die schon in Z enthalten sind, so wird der Knoten aus dertemporären Liste Q entfernt.

Der Algorithmus ist in den beiden Zeilen 9 und 10 in Pseudo-Code formuliert, da er hier nocheine gewisse Freiheit enthält:

• in Zeile 9 wählen wir einen Knoten der Hilfsliste Q, spezifizieren aber nicht, welchen.

• ebenso wählen wir in Zeile 10 eine Kante e = (y, z) unter der Bedingung dass e ∈ Esowie z 6∈ Z, spezifizieren aber nicht die konkrete Realisierung dieser Wahl.

Wir werden später zwei verschiedene Realisierungen der Graphdurchmusterung diskutieren,die Tiefensuche sowie die Breitensuche. Die konkreten Verfahren unterscheiden sich in der ex-akten Wahl der von Knoten und Kante in Zeilen 9 und 10.

Satz 3.21 (Graphdurchmusterung). Der Algorithmus zur Graphdurchmusterung eines GraphenG = (V,E) zum Knoten x ∈ V liefert eine maximale Zusammenhangskomponente Z mit x ∈ Z.Bei optimaler Implementierung benötigt der Algorithmus lineare Laufzeit (bezogen auf die Anzahl derKnoten und Kanten)

O(|V |+ |E1|),

wobei E1 die Menge der Kanten in der Zusammenhangskomponente Z von x ist.

[email protected] 67

Page 74: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

Beweis. (i)Wir zeigen zunächst, dass der durch die Knoten K und die Adjazenzliste Z gegebeneGraph zu jedem Zeitpunkt im Verfahren ein Baum ist. Zu Beginn des Verfahrens gilt:

1 K=[x]

2 Z=[[] ... []]

d.h. K enthält nur den Knoten x und Z enthält keine Kanten. Dieser Graph - bestehend auseinem Knoten - ist somit zusammenhängend und kreisfrei, also ein Baum.

Im Verfahren wird der Graph bestehend aus der Liste K und der Adjazenzliste Z nur in denZeilen 11-13 erweitert. Der Knotenliste Kwird ein Knoten z nur dann hinzugefügt, wenn diesermit einem Knoten y ∈ K durch eine Kante e = (y, z) ∈ E verbunden ist, aber noch nicht in Kenthalten ist. Diese Kante, sowie die gegenläufige Kante e ′ = (z,y) ∈ E (dieser Schritt ist nichtnotwendig) werden der Adjazenzliste Z hinzugefügt. Der resultierende Teilgraph K,Z ist somitnach wie vor zusammenhängend und - da z nicht in K lag - immer noch kreisfrei. Somit liegtein Baum vor.

(ii) Angenommen, nach Abschluss des Verfahrens gäbe es einen Knoten r ∈ V \ K, der abervon einem Knoten x ∈ K erreichbar wäre. Dies bedeutet: es gibt einen Weg P in G = (V,E),der x und r verbindet. Es sei (a,b) ∈ E eine Kante auf diesem Weg mit a ∈ K und b ∈ V \ K.Da a ∈ Kmuss es einen Durchlauf des Algorithmus gegeben haben mit a ∈ Q (es werden nurKnoten in K eingetragen, die zuvor inQ lagen). Dieser Knoten awird jedoch nur dann ausQentfernt, wenn es keine solche Kante (a,b) gibt. Dies ist ein Widerspruch, also kann es keineerreichbaren Knoten r ∈ V \ K geben.

(iii) Es bleibt die Bestimmung der Komplexität.

In den Zeilen 1-7 des Algorithmus entsteht Aufwand der Ordnung (n) zur Initialisierung derErgebnis-Adjazenzliste Z. DerwesentlicheAufwand besteht in den Zeilen 7-15, d.h. in der while↪→ -Schleife: für jedenKnoteny ∈ Q (der auch immery ∈ K in derKnotenliste der Zusammen-hangskomponente ist) durchlaufen wir potentiell alle Kanten (y, z). Dabei betrachten wir jedeKante höchstens einmal, denn sobald (y, z) berücksichtigt wurde, muss der Knoten z nichtweiter betrachtet werden (siehe Zeile 10). Wurden einmal alle von y ausgehenden Kantenbetrachtet, so wird in Zeile 16 der Knoten y aus Q entfernt. Da y bereits in der Zusammen-hangskomponente y ∈ K ist, wird ein Knoten y nie zweimal der ListeQ hinzugefügt. Hierausfolgt, dass wir jede Kante imGraphen höchsten einmal betrachten. Die Anzahl der Durchläufevon Zeilen 8-16 ist somit maximal O(|E1|). Falls die Zeilen 9 und 10 im Algorithmus in linearerLaufzeit O(1) implementiert werden, so ergibt sich die postulierte Komplexität

O(|V |+ |E1|)

Satz 3.22 (Zusammenhangskomponenten). Alle Zusammenhangskomponenten eines ungerichte-ten Graphen können in linearer Laufzeit

O(|V |+ |E|)

bestimmt werden.

68 ©2018,2019 Thomas Richter

Page 75: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

Beweis. Man startet das Verfahren mit einem beliebigen Knoten x ∈ V . Als Ergebnis erhältman in der Laufzeit

O(|V |+ |E1|)

eine Zusammenhangskomponente Z1 = (K1,E1). Wir führen das Verfahren weiter mit einemKnoten x ∈ V \ V1 (falls vorhanden). Die entsprechende Zusammenhangskomponente Z2 =(V2,E2) können wir in Laufzeit

O(|E2|)

bestimmen, da die anfängliche Initialisierung der Liste der besuchten Kanten nur einmal er-folgen muss. Nachm Schritten erhalten wir die Laufzeit

O(|V |+ |E1|+ |E2|+ · · ·+ |Em|) = O(|V |+ |E|),

da|E1|+ · · ·+ |Em| 6 |E|.

Definition 3.23 (Breitensuche undTiefensuche). Wählt man bei der Graphdurchmusterung jeweilsden Knoten y ∈ Q, der zuletzt zu Q hinzugefügt wurde, so nennt man das Verfahren Tiefensuche.Wählt man den Knoten y ∈ Q, der als erstes zu Q hinzugefügt wurde, so nennt man das VerfahrenBreitensuche.

Die Grundideen der beiden Varianten beschreiben zwei verschiedene Datenstrukturen, last infirst out sowie first in first out. Die Erste Datenstruktur wird mit einem Stack realisiert. Wie aufeinen Bücherstapel werden Elemente oben abgelegt und auch wieder von oben entfernt. Diezweite Variante, first in first out beschreibt eineWarteschlange, auf englischQueue. Das Elementwelches zuerst in die Warteschlange zugefügt wurde, wir auch als erstes wieder entfernt. Stel-len wir uns beide Datenstrukturen als Listen vor, so erfolgt bei einem Stack das Hinzufügenund Entfernen auf der gleichen Seite, bei einer Warteschlange erfolgen Hinzufügen und Ent-fernen auf unterschiedlichen Seiten. Bei der Realisierung von Stacks und Queues müssen wirdarauf achten, dass das Hinzufügen von Elementen sowie das entnehmen von Elementen inkonstanter LaufzeitO(1) erfolgt. Ansonsten gilt die Aussage zur Komplexität in Satz 3.21 nichtmehr.

3.4.1. Tiefensuche

Bemerkung 3.24 (Stacks in Python). Stacks lassen sich in Python einfach durch Listen realisieren.Es sei L eine Liste. Das anhängen eines neuen Elements x, d.h. das Ablegen von x oben auf demStapel wird mit der Funktion L.append(x) in optimaler Laufzeit erledigt. Das Entnehmen des letztenElements, d.h. das Entfernen des obersten Elements des Stapels erledigt die Funktion z=L.pop().Diese Funktion liefert das oberste Element zurück und entfernt es aus der Liste.

1 # ein Beispiel zum Abtippen

2 L = [1,5]

3 L.append(4)

4 L.pop()

5 L.pop()

6 print(L)

[email protected] 69

Page 76: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

Mit diesen Strukturen können wir nun die Tiefensuche in Python implementieren. In Zeile 9des Algorithmus wählen wir nun also jeweils das letzte Element der Liste Q, d.h. y=Q[len(Q)−1]↪→ . Alternativ können wir das Element mit dem Befehl y=Q.pop() entnehmen. Dieser Befehllöscht jedoch sofort das letzte Element aus der Liste. In Zeile 16 von Algorithmus 3.1 wirddas Element y jedoch erst dann aus Q gelöscht, wenn y keine weiteren Nachbarn mehr hat,die noch nicht in Z liegen. Auch wenn die Verwendung von y=Q.pop() eventuell effizienter ist,wählen wir hier die Variante y=Q[len(Q)−1]. In Zeile 10 von Algorithmus 3.1 muss ein Nachbarvon y gefunden werden, der noch nicht in Z, bzw. in K liegt. Um hier nicht immer die ganzeAdjazenz G[y] von vorne aufs Neue durchsuchen zu müssen, führen wir eine Indexliste I ein,in der wir uns für jeden Knoten y in Gmerken, welche Indizes der Adjazenz bereits untersuchtwurden. Schließlich ändern wir die Bedeutung der Liste K. Wir speichern nun nicht mehr,welche Knoten wir bereits hinzugefügt haben, sondern wir speichern eine Liste der Länge n=↪→ len(G), welche am Anhang stets den Wert False hat und ändern den Wert K[y]=True sobaldwir einen Knoten Y hinzufügen. Dies geschieht, da wir in Zeile 10 von Algorithmus 3.1 fürjeden Knoten testen müssen, ob dieser bereits in K enthalten ist. Eine Suche würde im bestenFall O(log(n)) Operationen benötigen, im schlechtesten Fall (lineare Suche) O(n). Die direkteAbfrage K[y]==True oder K[y]==False ist potentiell effizienter.

70 ©2018,2019 Thomas Richter

Page 77: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

Algorithmus 3.2: Tiefensuche1 def tiefensuche(G,x):

2 n = len(G) # Anzahl der Knoten

3 Z = [] # Adj.liste des Baumes, also

4 for i in range(n): # der Zusammenhangskomponente

5 Z.append([]) # erstellen.

6 K = [False]∗n # Zum Erfassen der Knoten in Z

7 K[x] = True # Ausgangsknoten x merken

8

9 Q = [x] # Startknoten auf den Stack

10 I = [0]∗n # Positionen in Adjazenzliste speichern

11

12 while len(Q)>0: # Solange Q noch Knoten erhaelt:

13 y = Q[len(Q)−1] # letztes Element aus Q entnehmen

14

15 while I[y]<len(G[y]): # Suche Nachbarn z von y

16 z = G[y][I[y]] # z ist der Index nes y−ten Nachbarn17 if not(K[z]): # z ist noch nicht in Z?

18 break # dann: Abbruch der Schleife

19 I[y]=I[y]+1 # Index hochzaehlen

20

21 if I[y]<len(G[y]): # Nachbar z gefunden

22 Z[y].append(z) # Nachbarn in Adjazenzliste Z eintragen

23 Z[z].append(y) # (gerichteter Graph, auch andere Kante)

24 K[z] = True # Knoten z merken

25 Q.append(z) # Knoten auf Stack legen

26 else: # kein weiterer Nachbar von y?

27 Q.pop() # dann: y von Stack entfernen

28 return Z,K # Rueckgabe Adjazenzliste und Knotenliste

Das Ergebnis der Tiefensuche ist einerseits die Liste K, die für jeden Knoten speichert, ob erin der gefundenen Zusammenhangskomponente mit dem Startknoten x ist oder nicht. Dannwird die Adjazenzliste der Zusammenhangskomponente Z zurückgegeben. Z ist ein Baum.

Der Baum Z enthältWege zu allen Knoten, die von x aus erreichbar sind. Durch einen einfachenTrick kanndie Tiefensuche genutztwerden, einenWeg imNachhinein auch anzuzeigen. In denZeilen 22 und 23 der Tiefensuche werden die neuen Kanten zum Baum Z hinzugefügt. Wennwir Zeile 22 streichen, also nur die Kante von dem neuen Knoten z zu dem vorherigen Knoteny speichern, so hat der Baum die folgende Eigenschaft: Von jedem Knoten z in Z geht nur eineeinzige Kante aus. Dies ist die Kante zu dem Knoten y von dem z aus das erste mal erreicht wurde.Dasist klar, denn der Block von Zeile 22 bis 25 wird nur dann ausgeführt, wenn vorher K[z]==Falsegalt.

Mit dieser kleinen Modifikation kann der gesuchte Weg nun einfach ausgegeben werden, in-dem wir ihn rückwärts durchlaufen. Angenommen gesucht ist ein Weg von x nach y und an-genommen, so ein Weg existiert auch, d.h. beide Knoten liegen in der gleichen Zusammen-hangskomponente, die mit der Tiefensuche erstellt wurde. Dann geben wir zunächst y ausund gehen von y aus zu dem einzigen Knoten, der in der Adjazenz Z[y] gespeichert wird. Dies

[email protected] 71

Page 78: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

ist gerade der vorherige Knoten auf dem Weg von x nach y. Wir wiederholen, bis wir beimKnoten x ankommen.

Der zugehörige Algorithmus ist sehr kurz

1 # Z erstellen , x und y in Z gegeben

2

3 print(y)

4 while not(y==x):

5 y=Z[y][0]

6 print(y)

3.4.2. Breitensuche

Zur Realisierung der Breitensuche sind nur wenige Modifikationen notwendig. In Zeile 9 vonAlgorithmus 3.1 ist nun das Element zu entnehmen, welches wir zuerst in die Liste Q eingefügthaben. Dies ist natürlich ganz einfach mit dem Befehl y=Q[0] zu erledigen. Ein Problem ergibtsich in Zeile 16 der Graphdurchmusterung. Hier müsste das erste Elemente der Liste entferntwerden. Listen in Python sehen dies nicht effizient vor: um das erste Element zu entfernenmüssen wir alle weiteren Elemente kopieren, d.h. Q[0]=Q[1], Q[1]=Q[2], usw, im Anschluss dieListe verkürzen. Das Kopieren bringt einen hohen Aufwand O(n) mit sich. Stattdessen ver-wenden wir die Datenstruktur queue.

Bemerkung 3.25 (Queues in Python). Queues können in Python durch eine Bibliothek bereit gestelltwerden. In Python 3 werden die entsprechenden Funktionen mittels

1 import queue

geladen, in Python 2 mit1 import Queue

Ab hier ist die Verwendung gleich. Die wesentlichen Methoden dienen dem• Erstellen einer Variable vom Typ queue mittels Q = queue.Queue()• dem Hinzufügen eines Elementes x am Ende der Queue mittels Q.put(x)• dem Entnehmen eines Elements y am Anfang der Queue mittels y=Q.get().• der Abfrage der Länge der Queue mittels Q.qsize(), d.h. das Ermitteln der Anzahl der in der

Queue gespeicherten Elemente. Man teste das folgende Programm1 import queue # Achtung, in Python 2 import Queue

2

3 Q = queue.Queue() # Queue erstellen

4 Q.put(10)

5 Q.put(5)

6 Q.qsize()

7 Q.get()

8 Q.get()

72 ©2018,2019 Thomas Richter

Page 79: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

9

10 S = [] # Stack (=Liste) erstellen

11 S.append(10)

12 S.append(5)

13 len(S)

14 S.pop()

15 S.pop()

Wir können nun die Breitensuche auf der Basis der Queue realisieren. In Zeile 7 von Algorith-mus 3.1 müssen wir das Startelement mittels Q.put(x) in die Queue einfügen. In Zeile 8 mussdie Schleife while len(Q)>0: durch while Q.qsize()>0: ersetzt werden. In Zeile 9 von wählen wirdas Element y als y=Q.get(). Diese Anwendung entfernt das Element aus der Queue Q. (Manlese hierzu auch den entsprechenden Abschnitt bei der Tiefensuche). Bei der Breitensuche istdieses Verhalten jedoch nicht schlimm. Denn: wir arbeiten mit einer Warteschlange, d.h. wirfügen in Zeile 14 einen Knoten z in dieWarteschlange Q ein und entnehmen diesen dann sofortim nächsten Durchlauf in Zeile 9 wieder. Dies können wir effizienter gestalten, in dem wir zueinem Knoten y in einer Schleife alle Nachbarn in die Warteschlange einfügen.

Algorithmus 3.3: Breitensuche1 def breitensuche(G,x):

2 n = len(G) # Anzahl der Knoten

3 Z = [] # Adj.liste der Zusammenhangskomponente

4 for i in range(n):

5 Z.append([])

6 K = [False]∗n # Knoten in Zusammenhangskomponente

7 K[x] = True # Ausgangsknoten x merken

8

9 Q = queue.Queue() # Erstellen einer Queue

10 Q.put(x) # Startknoten

11

12 while Q.qsize()>0:

13 y = Q.get() # Erstes Element entnehmen

14

15 for z in G[y]: # Alle Nachbarn von y durchlaufen

16 if not(K[z]): # Falls Nachbar z noch nicht in Z

17 Z[y].append(z) # Nachbarn in Adjazenzliste Z eintragen

18 Z[z].append(y) # (gerichteter Graph)

19 K[z] = True # Knoten z merken

20 Q.put(z) # Knoten an Queue anhaengen

21

22 return Z,K # Rückgabe Adjazenzliste und Knotenliste

Visualisierung der Breitensuche

[email protected] 73

Page 80: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

Wir zeigen von oben links nach unten rechts die sechs Schritte der Breitensuche vom Knotenx aus:

• Das aktuelle Suchelement y ist jeweils blau eingekreist

• Knoten, die bereits in Z sind werden gelb markiert

• Kanten in Z sind grün markiert

• Die Zahlen markieren die Reihenfolge der Element in der Queue. Der Knoten mit derjeweils kleinsten Zahl wird im nächsten Schritt als aktives Element y, d.h. als blau mar-kiertes gewählt

• Dabei überspringen wir Schritte in denen es keine neue Nachbarn mehr gibt. Dies trifftauf den Knoten 2 im 3-ten Schritt sowie auf den Knoten 5 im 5ten Schritt zu.

Die Breitensuche hat eine zur Suche kürzester Wege wichtige Eigenschaft. Hierzu überlegeman sich anhand der vorherigen Abbildung, in welcher Reihenfolge neue Knoten dem BaumZ hinzugefügt werden: im ersten Schritt werden alle Knoten der Liste Q hinzugefügt, die un-mittelbar von x aus erreichbar sind. Danach werden alle diese Knoten aus y ∈ Q bearbeitetund es werden jeweils die neuen Knoten hinzugefügt, die wieder in genau einem Schritt vony aus erreichbar sind. Wir machen die folgenden Modifikationen im Algorithmus um uns ineiner Indexliste L die Abstand von x zu merken:

• Nach Zeile 2 wird eine Liste erzeugt mittels L = [−1]∗n. Der Index−1 bedeutet, dass derentsprechende Knoten nicht erreichbar ist von x

• Dann wird der Startknoten mittels L[x]=0 eingetragen. Denn der Startknoten ist unmit-telbar, ohne Weg, bzw. mit einemWeg der Länge 0 erreichbar.

• Innerhalb der if-Anweisung in Zeilen 17-20, d.h. z.B. gleich zu Beginn in Zeile 17 wirddie Liste aktualisiert mittels L[z]=L[y]+1. Denn falls die Länge von x nach y L[y] ist, so istdie Länge bis z - welche noch nicht erreicht wurde eine zusätzliche Kante.

74 ©2018,2019 Thomas Richter

Page 81: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.4. Zusammenhang

Visualisierung der Breitensuche mit kürzesten Wegen

Zusätzlich zur vorherigen Visualisierung haben wir den Index L[] angegebenen. Anstelle desEintrags −1 lassen wir den Index frei, falls der entsprechenden Knoten (noch) nicht erreichtwurde.

Satz 3.26 (Breitensuche). Der durch Breitensuche ausgehend von x ∈ V erzeugte BaumZ = (V ′,E ′)enthält einen kürzesten Weg von Knoten x ∈ V ′ zu allen anderen, von x aus erreichbaren Knoteny ∈ V ′. Die Länge des Weges dist(x,y) kann in linearer Laufzeit bestimmt werden.

Beweis. Die Abbildung zeigt die Schwierigkeit des Beweises.Wir betrachten z.B. denWeg P imGraphen G vom Knoten x bis zum Knoten, der in der mittleren Abbildung der unteren Reihemit der Nummer 8 gekennzeichnet ist. Zu diesem Knoten gibt es im GraphenG zwei kürzesteWege, jeweils bestehend aus 3 Kanten; einer geht über den mit 6 gekennzeichneten Knoten,einer über den Knoten 7. Der Beweis kann also nicht auf dem Prinzip “Der kürzeste Weg zwi-schen Knoten x und y in G verläuft auch in Z”. Nur einer der möglicherweise uneindeutigenkürzesten Wege geht durch Z.

Der Beweis wird als Widerspruchsbeweis geführt. Angenommen, es gäbe zwei Knoten x,y ∈Z ⊂ V mit

distG(x,y) < distZ(x,y). (3.4)

Der Weg in Z hab die Knoten P = (x0, x1, . . . , xn).

Da Z = (V ′,E ′) mit V ′ ⊂ V und E ′ ⊂ E ein Teilgraph von G ist, so liegt der Weg P natürlichauch in G. Somit gilt stets

distG(x,y) 6 distZ(x,y),

da es gegebenenfalls noch kürzere Wege in G gibt.

[email protected] 75

Page 82: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.5. Suche nach kürzesten Wegen

Angenommen xk sei der erste Knoten im Weg P mit der Eigenschaft

distG(x, xk) = distZ(x, xk) aber distG(x, xk+1) < distZ(x, xk+1). (3.5)

So ein Knotenmuss existieren, wenn Bedingung (3.4) gelten soll. Wir können ohne Einschrän-kung annehmen, dass y = xk+1.

Für xk gilt somitdistG(x, xk) = distZ(x, xk).

Weiter gilt distZ(x, xk+1) = distZ(x, xk) + distZ(xk, xk+1) = distZ(x, xk) + 1, da der Weg Pein kürzester Weg in Z ist. Annahme (3.5) bedeutet dann

distG(x, xk+1) < distZ(x, xk+1) = distZ(x, xk) + 1 = distG(x, xk) + 1, (3.6)

also müsste distG(x, xk+1) 6 distG(x, xk) sein.

Im modifizierten Algorithmus gibt es einen Zustand, in dem der Knoten xk der Queue Q hin-zugefügt wird und einen Index L[xk]= distG(x, xk) = distZ(x, xk) erhält.

Da Knoten xk nun in der Queue Q liegt, wird dieser einmal in Zeile 13 entnommen y=Q.get()=↪→ xk. ImAnschluss werden in den Zielen 15-20 alle Nachbarn z von y=xk betrachtet, d.h. auchder gesuchte Knoten z=xk+1.

Angenommen dieser Knoten z=xk+1 sei bisher noch nie besucht, also K[xk+1]==False in Zeile16. So wird dieser Hinzugefügt und die Länge als L[xk+1]=L[xk]+1 im Widerspruch zu (3.6)bestimmt.

Nun angenommen, es gelte K[xk+1]==True. Dieses bedeutet, dass der Knoten xk+1 bereits be-sucht wurde. Somit hat dieser ist diesem Knoten auch schon eine Länge zugeordnet. Da diesvorher geschehen ist, also nicht vomKnoten xk ausmuss dann L[xk+1]<=L[xk]+1 gelten,wiederim Widerspruch zu (3.6).

3.5. Suche nach kürzesten Wegen

Die Breitensuche durchmustert einen GraphenG = (V,E) und findet so die Zusammenhangs-komponenten. Ausgehend von einemKnoten x ∈ V wird ein Baum aufgespannt, der von x ausdie Wege mit der kleinsten Kantenanzahl zu allen erreichbaren Knoten y ∈ V erhält. DiesesErgebnis scheint schon nahe an demProblemder Routenplanung zu sein: suche den kürzesten(oder schnellsten, oder schönsten)Weg von Köln an den Tegernsee. Alle Kanten haben jedochdie gleiche Wertigkeit. Übertragen auf die Routenplanung bedeutet dies, dass alle Abschnitte,z.B. zwischenAutobahnausfahrten die gleicheWertigkeit besitzen, unabhängig von der Längeder Strecke, oder der zugelassenen Höchstgeschwindigkeit.

Wir führen hierzu gewichtete Graphen ein, um den Knoten und Kanten Gewichtsfunktionenzuzuordnen.

Definition 3.27 (Gewichteter Graph). Ein Quadrupel G = (V,E, fV , fE) bestehend aus KnotenV ⊂ N, Kanten E ⊂ V ×V sowie zwei Gewichtsfunktion fV : V → R und FE : E→ R heißt gewich-teter Graph. Oft betrachtet man auch Tripel G = (V,E, fE) oder G = (V,E, fV) mit ausschließlichKanten- oder Knotengewichten.

76 ©2018,2019 Thomas Richter

Page 83: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.5. Suche nach kürzesten Wegen

DieUnterscheidung zwischen gerichteten undungerichtetenGraphen, vollständigenGraphen,Bäumen und Wäldern wird durch das zusätzliche Gewicht nicht geändert.

Zur Speicherung von gewichteten Graphen werden zusätzlich zur Adjazenzliste die Gewichts-listen gespeichert. Ein Beispiel:

Algorithmus 3.4: Darstellung eines gewichteten Graphen1 # Adjazenzliste mit 9 Knoten

2 Z=[[1,2,3],[0,5,6],[0,9],[0,5],[5,7],[1,3,4,8],[1,8,9],[4,8],[5,6,7],[2,6]]

3 # Liste von Kantengewichten − in der Ordnung der Adjazenz

4 fE=[[3,3,2],[3,9,8],[3,2],[2,5],[5,2],[9,5,5,5],[8,1,8],[2,6],[5,1,6],[2,1]]

5 # Liste der Knotengewichte (nicht in der Abbildung)

6 fV=[1.2,2.0,−0.5,1.2,1.6,−0.8,0.5,3,1,0.5]7

8 print(’Knoten 1 hat Kante zu Knoten’,Z[1][0],’mit Kantengewicht’,fE[1][0])

9 print(’Knoten 1 hat Gewicht’,fV[1])

10 print(’Knoten’,Z[1][0],’hat Gewicht’,fV[Z[1][0]])

Definition 3.28 (Kürzester Weg). Gegebensei ein gewichteter Graph G = (V,E, fE) (mitKantengewichten) sowie zwei Knoten x,y ∈V . Gesucht ist der Weg P = (V ′,E ′, fW) ⊂ G

P := (V ′,E ′) =

({x = x0, . . . , xk = y}, {e1, . . . , ek})

mit kleinstem summierten Kantengewicht∑e∈P

fW(e)→ min!

Wir suchen z.B. den kürzesten Weg vonKnoten 0 nach Knoten 8. 3

3

5

6

1

2

9

5

5

8

2

2

03

1

5

4

6

7

8

9

2

1

Bemerkung 3.29 (Kreisemit negativemGewicht). Kantengewichte fW(e) können imAllgemeinennegativ oder positiv sein. Wir beschränken uns hier auf den einfachen Fall von ausschließlich positivenKantengewichten fW : E → R>0. Angenommen, ein Graph (mit auch negativen Kantengewichten)hätte einen Kreis mit negativer Kantengewichtsumme. Durch mehrfaches durchlaufen dieses Kreiseskönnen wir Wege mit beliebig kleiner Gewichtssumme erzeugen. Dieses Beispiel ist extrem, es zeigtjedoch bereits, dass dieser allgemeine Fall schwerer ist.

[email protected] 77

Page 84: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.5. Suche nach kürzesten Wegen

3.5.1. Dijkstra’s Algorithmus

Bei der Suche nach kürzesten Wegen haben wir Landkarten vor Augen. Diese beinhalten zu-sätzlich zu den Knoten und Kanten - also Städten und Bahnlinien - auch geometrische In-formationen, die uns beim Problem stark helfen: Wollen wir von Hamburg nach Kiel fahren,so werden wir nicht in einen Zug nach Berlin oder Hannover steigen. Wir suchen hier nacheinfachen Algorithmen, die ohne solche Zusatzinformationen auskommen.

Starten wir in einem Punkt x ∈ V , so können wir also nicht zielgerichtet in Richtung y ∈ V los-legen sondern werden den Graphen - ähnlich der Durchmusterung - allgemein durchlaufen.Hierbei werden wir für alle Knoten xk ∈ V jeweils kürzesteWege von x berechnen. Treffenwirbei dieser Suche auf den Knoten y, so sind wir fertig.

Der Algorithmus zur Breitensuche bildet eine gute Grundlage. Er würde das kürzeste-Wege-Problem lösen, wenn alle Kanten die Gewichte fE ≡ 1 hätten. Von einem Knoten x ∈ G =(V,E) aus wird ein Baum Z = (V ′,E ′) erzeugt, der die kürzesten Wege - gerechnet in derAnzahl der Kanten - zu allen erreichbaren Knoten y erhält. Dabei geht die Breitensuche fol-gendermaßen vor:

• Wir beginnen mit dem Startknoten x und fügen diesen in den Ergebnisbaum ein Z.

• Dieser Baum Zwird Schritt für Schritt um alle Knoten aus V erweitert, die noch nicht inZ sind, aber von einem Knoten aus Z erreicht werden können.

• So kann sichergestellt werden, dass im i-ten Schritt genau die Knoten hinzugefügt wer-den, die in mindestens i Schritten von x aus erreichbar sind.

• Wird ein Knoten z angetroffen der bereits in Z enthalten ist, so muss er kein zweites malberücksichtigt werden. Denn der erste Weg zu z ist entweder kürzer oder aber genausolang. Ist er genauso lang, so ist es nicht wichtig, welcher Weg gespeichert wird.

Der letzte Punkt wird durch die Wahl der queue zur Realisierung von Q in der Breitensuchesichergestellt. Es ist auch der entscheidende Punkt, der die Breitensuche von der Suche nachkürzestenWegen in gewichtetenGraphen unterscheidet. Es kann durchaus sein, dass von zweiWegen von x nach y derjenigemit der kleineren Kantenanzahl dermit dem größeremGesamt-gewicht ist. In der Abbildung zu Definition 3.28 betrachte man z.B. die Wege 0 3−→ 1

9−→ 55−→ 8

mit Gesamtgewicht 3+9+5 = 17 sowie 0 3−→ 22−→ 9

1−→ 61−→ 8mit 4 Kanten aber dem geringeren

Gesamtgewicht 3+ 2+ 1+ 1 = 7.

Die notwendigen Änderungen sind nicht sehr umfangreich: Wir fügen einen Knoten v erstdann zum Ergebnisgraphen Z hinzu, wenn wir uns sicher sind, dass wir v nicht - auf einemanderenWeg - schneller erreichen können. Umdies zu erreichen arbeitenwirmit 2 temporärenListen: Q ist nach wie vor die Suchliste aller Knoten, die wir bereits erreicht haben und vondenen wir noch weitere Knoten suchen werden. Sobald ein Knoten w der Liste Q hinzugefügtwird, so speichern wir in L[w] die Länge des bisher schnellsten Weges. Wird w ein zweitesmal gefunden, z.b. mittels einer Kante (v,w), so speichern wir nur den schnelleren Weg undmerken uns die Kante (v,w), indem wir den Wert P[w]=v merken, d.h. wir speichern, von woaus wir nach w gelangt sind.

Ein Knoten wird erst dann endgültig dem Ergebnisgraphen R hinzugefügt, wenn der Knotenselbst der Liste Q entnommen wird.

78 ©2018,2019 Thomas Richter

Page 85: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.5. Suche nach kürzesten Wegen

Ein Algorithmus zum Lösen des Problems (bei positiven Kantengewichten) heißt Dijkstra’sAlgorithmus.

[email protected] 79

Page 86: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.5. Suche nach kürzesten Wegen

Algorithmus 3.5: Dijkstra’s AlgorithmusGegeben ein gewichteter Graph G = (V,E, fW) sowie ein Knoten x ∈ V

1 # Eingabe: Adjazenzliste G sowie Kantengewichte fE.

2 # Startknoten x sowie Ziel y

3 def dijkstra(G,fE,x,y):

4 n=len(G) # Anzahl der Knoten

5

6 Q=[x] # Temporaere Liste

7 R=[False]∗n # weitere temp. Liste

8

9 L=[−1]∗n # Laenge zu allen Knoten

10 L[x]=0

11

12 P=[−1]∗n # Speichert den Weg rueckwaerts

13

14 while len(Q)>0:

15 wahle v aus Q mit L[v] minimal

16 Q.remove(v)

17 R[v]=True

18

19 for j in range(len(G[v])): # Nachbarn von v durchlaufen

20 if not(R[G[v][j]]): # j−ter Nachbar noch nicht bearbeitet21 w=G[v][j]

22 lw=L[v]+fW[v][j] # Weglaenge zu w

23 if (L[w]<0) or (lw<L[w]): # neuer Weg kuerzer?

24 L[w]=lw # neue Weglaenge merken

25 Q.append(w) # von w aus weitersuchen

26 P[w]=v # Merke den Rueckweg von w nach v

27 return P

Der Algorithmus erzeugt eine Liste P, die zu jedem Knotenw denjenigen Knoten v angibt, vondem aus w von x auf dem kürzesten Weg erreicht wurde. Der durch P beschriebene Graphist eine sogenannte Arboreszenz. Eine Arboreszenz ist ein zusammenhängendes Branching. Dabeiist ein Branching ein gerichteter Graph, dessen zugehöriger ungerichteter Graph ein Wald ist(Kreisfrei) und für den jeder Knoten nur eine einzelne eingehende Kante besitzt.Was ist wich-tig für uns? Zu jedem Knoten v ∈ R \ {x} gibt es nur eine Kante e ∈ T mit e = (v ′, x). Wollenwir nun den kürzesten Weg von x nach y bestimmen, so durchlaufen wir die entsprechendenKanten von y aus einfach rückwärts:

1 ## Gegeben P aus dijkstra (von x aus startend)

2 print(’Weg von’,x,’nach’,y)

3 if P[y]==−1:4 print(’Der Knoten’,y,’ist von’,x,’aus nicht erreichbar’)

5 else:

6 print(y)

7 while not(P[y]==x):

8 print(y)

9 y=P[y]

80 ©2018,2019 Thomas Richter

Page 87: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.5. Suche nach kürzesten Wegen

10 if y==−1:11 Assert False,’Das haette nicht passieren duerfen!’

Suchen wir nur den Weg von x nach y so kann der Algorithmus abgebrochen werden, falls inZeile 15 v = y gilt.

Zeile 15 ist noch nicht ausformuliert. Hier unterscheidet sich Dijkstra von den beiden anderenVarianten der Durchmusterung, also Tiefensuche oder Breitensuche. Nicht die Reihenfolgeentscheidet, wo es weitergeht, sondern die bisherigen Gewichte. Knoten v in Q die schnell er-reicht sind, also geringe Werte von L[v] besitzen, sind gute Kandidaten für die weitere Suche.Für eine praktische Umsetzung von Dijkstra’s Algorithmus muss diese Zeile effizient imple-mentiert werden. Es bietet sich an, dass die Liste Q gleich sortiert gespeichert wird, so dassimmer v=Q[0] gewählt werden kann. Wird einfach immer die ganze Liste Q von vorne durch-sucht, so ist der Aufwand hierzu O(|Q|), im schlimmsten Fall O(|V |). Besser geht es, wenn dieEinträge in Q bereits sortiert abgelegt werden. Dann ist Q[0] stets der Knoten mit dem gerings-ten Gewicht. Diese Modifikation erfordert jedoch die Suche nach dem Richtigen Index beimEinfügen. Wird hier die Idee der binären Suche gewählt, so singt der Aufwand auf log2(|V |).

Satz 3.30 (Dijkstr’as Algorithmus). DerDijkstra Algorithmus löst das kürzeste-Wege-Problem.Der Algorithmus kann in

O(|V |AQ(V) + |E|)

Operationen durchgeführt werden, wobei AQ(V) der Aufwand zur Suche des Knoten mit minimalemGewicht ist, sowie zum Löschen eines Knotens aus Q.

Beweis. Der Beweis der Korrektheit, dass also kürzesteWege von x ∈ V zu jedem erreichbarenKnoten y ∈ V gefunden werden findet sich z.B. in Hougardy & Vygen [3].

Wir gehen kurz auf den Aufwand ein. Wir speichern Q und R in Listen. R[v] hat den Wert Trueoder False, die Überprüfung ist somit in einem Schritt möglich.

Die while-Schleife wird höchstens |V | mal durchgeführt, da nur |V | Knoten der Menge Q hin-zugefügt werden können. Wird einmal v in Q betrachtet, so wird der Knoten v der Liste auchentnommen und niewieder hinzugefügt. Jeder Knoten kann also nur einmal durchlaufenwer-den.

In jedem Durchgang werden die folgenden Schritte durchgeführt:

• Suche des Elements v in Qmit L[v]minimal. Dies erfordert jeweilsAQ(|V |)Operationen.(Wie oben diskutiert).

• Löschen von v ∈ Q erfordert ebenso AQ(|V |) Operationen.

• Hinzufügen von v in R erfolgt in O(1) Operationen.

• Die Abfrage w 6∈ R in Zeile 23 benötigt nur O(1) Operation

• In Zeile 22werden alle Kanten von v aus durchlaufen. Da dieser Schritt maximal für alleKnoten des Graphen durchlaufen wird wird die innere Schleife 24-29 somit maximalO(|E|)mal durchlaufen. Die Schritte 24-29 können jeweils in O(1)Operationen durchge-führt werden.

Insgesamt ergibt sich somit der Aufwand O(|V |AQ(|V |) + |E|).

[email protected] 81

Page 88: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

3.5. Suche nach kürzesten Wegen

Bei optimaler Implementierung von Q mittels eines Suchbaums gilt AQ(|V |) = log2(|V |) undder Gesamtaufwand ergint sich als |V | log2(|V |) + |E|. Die hier verwendete Implementierunghat einen quadratischen Aufwand, also |V |2 + |E|.

82 ©2018,2019 Thomas Richter

Page 89: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Bibliography

[1] R. Diestel. Graphentheorie. 5. Auflage. http://diestel-graph-theory.com/german/. Sprin-ger Spectrum, 2017.

[2] P. Gries, J. Campbell, and J. Montojo. Practical Programming. An Introduction to ComputerScience Using Python 3. Ed. by L. Beighley. 2nd edition. The Pragmatic Programmers. Dal-las, Texas. Raleigh, North Carolina: Pragmatic Bookshelf, 2013. url: http://pragprog.com.

[3] S. Hougardy and J. Vygen. Algorithmische Mathematik. 2. Auflage. Springer Spektrum,2018.

[4] B. Korte and J. Vygen.Kombinatorische Optimierung. DeutscheÜbersetzung der 6. Auflage.Springer, 2018.

[5] M. Markert et al. Das Python3.3-Tutorial auf Deutsch. Release 3.3. 2017. url: https://py-tutorial-de.readthedocs.io/de/python-3.3/.

[6] A. Schrijver. Combinatorial Optimization. Polyhedra and Efficiency. Springer, 2003.[7] G. van Rossum, B. Warsaw, and N. Coghlan. Style Guide for Python Code. url: https:

//www.python.org/dev/peps/pep-0008/.

83

Page 90: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Bibliography

84 ©2018,2019 Thomas Richter

Page 91: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Verzeichnis der Algorithmen

1.1. Einfache Berechnung der Fakultät . . . . . . . . . . . . . . . . . . . . . . . . . . 91.2. Berechnung der Fakultät mit for-Schleife . . . . . . . . . . . . . . . . . . . . . . 141.3. Funktion zur Berechnung der Fakultät . . . . . . . . . . . . . . . . . . . . . . . 141.4. Rekursive Berechnung der Fibonacci-Zahlen . . . . . . . . . . . . . . . . . . . 151.5. Rekursive Berechnung der Fakultät . . . . . . . . . . . . . . . . . . . . . . . . . 151.6. Test auf Tautologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

2.1. Selection SortGegeben Liste L sowie eine (partielle) Ordnung . . . . . . . . . . . . . . . . . . 25

2.2. Test auf Sortiertheit (totale Ordnung)Gegeben Liste L mit n Elementen und totaler Ordnung 6 . . . . . . . . . . . . . 30

2.3. Sequentielle SucheGegeben sei eine Liste L=[s1 ,..., sn]mit n ∈ N Einträgen. Gesucht ist die Positioneines ihrer Einträge l. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

2.4. Insertion SortGegeben eine Liste L mit n Elementen und eine (partielle) Ordnung �. . . . . . 35

2.5. Min-Sort; Selection Sort bei totaler OrdnungGegeben Liste L und totale Ordnung . . . . . . . . . . . . . . . . . . . . . . . . . 36

2.6. Zusammenfügen sortierter ListenGegeben, zwei sortierte Listen L,R . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

2.7. Merge SortGegeben, Liste L mit n ∈ N Elementen und totaler Ordnung 6 . . . . . . . . . . 38

2.8. Quicksort Eingabe von Liste L mit n ∈ N Elementen sowie zwei Indizes0 6 l, r < n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

2.9. Partitioniere Eingabe einer Liste L mit n ∈ N Elementen sowie von zweiIndizes 0 6 l < r < n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

2.10. Sortieren mit Bucket-Sort nach Ziffer l . . . . . . . . . . . . . . . . . . . . . . . 502.11. Sortieren mit Counting-Sort nach Ziffer l . . . . . . . . . . . . . . . . . . . . . 51

3.1. GraphdurchmusterungGegeben ungerichteter Graph G als Adjazenzliste (doppelte Liste) mit n=len(G↪→ ) Knoten sowie ausgezeichneter Knoten x ∈ {0, . . . ,n − 1}. Ausgabe: ListeK aller Knoten, die von x aus erreichbar sind sowie die Adjazenzliste Z einesaufspannenden Baumes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67

3.2. Tiefensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 713.3. Breitensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 733.4. Darstellung eines gewichteten Graphen . . . . . . . . . . . . . . . . . . . . . . 773.5. Dijkstra’s Algorithmus

Gegeben ein gewichteter Graph G = (V,E, fW) sowie ein Knoten x ∈ V . . . . . 80

85

Page 92: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Verzeichnis der Algorithmen

86 ©2018,2019 Thomas Richter

Page 93: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Index

Äpfel und Birnen, 24Äquivalenz, 17assert, 12

adjazent, 55Adjazenzliste, 64Adjazenzmatrix, 63Algorithmus, 6Arboreszenz, 80Assertion, 12Aufwand, 29Ausgabe, 6Aussage, 17

erfüllbar, 18unerfüllbar, 18

Aussagenlogik, 17

Bandbreiten-Problem, 62Bandmatrix, 62Baum, 66benachbarte Knoten, 55Beweis

direkte Methode, 3Branching, 80Breitensuche, 69, 73Bucket Sort, 37

Call by Reference, 27Call by Value, 27cpu time, 29

dünn besetzte Matrix, 62Datentyp, 6direkter Beweis, 3Disjunktion, 17divide and conquer, 37

Eingabe, 6Eingabe und Ausgabe, 9Einrücken, 7elementare Aussage, 17

elementare Formal, 17Endknoten, 55

Färbeproblem, 57Fibonacci-Zahlen, 15Funktionen, 14

Graph, 53Bandbreite, 62gerichtet, 54gewichtet, 59ungerichtet, 54vollständig, 54

Graphdurchmusterung, 66Graphen

Durchmusterung, 66Graphpartitionierung, 61

Heuristiken, 62

immutable, 28Implikation, 17Indizenzmatrix, 63Induktion, 1Induktionsbeweis, 10Induzierte Ordnung, 37Insertion Sort, 34inzident, 55iterativ, 15

Junktor, 17

Königsberger Brückenproblem, 61Kante, 53Kantengewicht, 59Kantenzug, 56

geschlossen, 56kartesisches Produkt, 23Knoten, 53Kommentare, 9Komplexität, 53

average-case, 34

87

Page 94: Algorithmische Mathematikrichter/WS19/am1/skript.pdf · Algorithmische Mathematik ThomasRichter∗ 27.Januar2020 ∗Institut für Analysis und Numerik, Universität Magdeburg.Raum

Index

best-case, 34worst-case, 34

Konjunktion, 17Konvergenz, 32Kreis, 56

Laufzeit, 16lexikographische Ordnung, 48lexikographische Ordnung, 24Logik, 17logische Verknüpfungen, 7

MatrixBandmatrix, 62dünn besetzt, 62

Merge Sort, 37mutable, 28

Negation, 17NP-schwer, 62NP-vollständig, 62

Ordnung, 23induziert, 37lexikographisch, 24, 48

paralleles Rechnen, 62Parallelisierung, 62Problem des Handlungsreisenden, 60Python

elif, 12else, 12for, 12if, 11range, 12Einrücken, 7Funktionen, 14Graphen, 64I/O, 9import, 28Kommentare, 9Liste, 12Queue, 72random, 28Stacks, 69time, 29

Queue, 69Quick Sort, 37

Radix-Sort, 48Rekursion, 15, 19Relation, 23

Schleifen, 7Selection Sort

totale Ordnung, 35Sortieren, 23

stabil, 47Sortieren durch Einfügen, 34Speicherbedarf, 16stabiles Sortieren, 47Stack, 69

Tautologie, 18Terminierung, 11Tiefensuche, 69Transitivität, 20Traveling Salesman, 60

Variable, 6vergleichsbasiertes Sortieren, 46Verknüpfungen, 7vollstandige Induktion, 1

Wald, 66wall time, 29Weg, 56

Zeitmessung, 29Zufallszahlen, 28Zusammenhangskomponente, 66Zuweisung, 6

88 ©2018,2019 Thomas Richter