Sie sind auf Seite 1von 269

Computerorientierte Mathematik I

mit Java
(Bachelor Version)

Rolf H. Möhring

Technische Universität Berlin


Institut für Mathematik

Wintersemester 2009/10
ii
Vorbemerkungen

Die Computerorientierte Mathematik stellt die Informatikgrundausbildung an der TU Berlin für die
Studiengänge Mathematik, Techno- und Wirtschaftsmathematik sowie Naturwissenschaften in
der Informationsgesellschaft und Informationstechnik im Maschinenwesen dar. Zugleich bildet
sie das Bindeglied zwischen Informatik und Mathematik. Dies äußert sich vor allem in der Auswahl
der Themen und der algorithmischen Fragen, die in der Veranstaltung behandelt werden.
Die Veranstaltung streckt sich über zwei Semester und vermittelt den Stoff in insgesamt 8 Semes-
terwochenstunden Vorlesung und 8 Semesterwochenstunden Übung. Die Vorlesung umfasst folgende
Punkte

- Grundlagen des Entwurfs und der Analyse von Algorithmen

- Standardalgorithmen und Datenstrukturen

- Grundlagen von prozeduralen und objektorientierten Programmiersprachen, insbesondere Java

- Einführung in Aufbau und Funktionsweise von Rechnern (einschließlich Schaltkreistheorie)

- Aspekte der Rechnernutzung in der Mathematik: Zahlendarstellung, Computerarithmetik, grund-


legende Algorithmen der Diskreten und der Numerischen Mathematik

Die Übung vertieft diesen Stoff in praktischen und theoretischen Aufgaben. Der praktische Teil enthält
eine Einführung in die Rechnerbenutzung an Linux Rechnern und das Erlernen der Programmierspra-
che Java. Sie untergliedert sich in folgende Punkte:

- Einführung in die Rechnerbenutzung

- Einführung in die Programmiersprache Java

- Einüben von Techniken für das Erstellen und Testen von Programmen und Algorithmen

- Realisierung von Algorithmen auf dem Rechner

- Besprechung der Übungsaufgaben

- Praktische Übungen am Rechner

iii
iv VORBEMERKUNGEN

Es werden keinerlei Vorkenntnisse aus dem Bereich der Informatik vorausgesetzt. Hinsichtlich der
Mathematik sind Kenntnisse im Umfang der Schulmathematik ausreichend.
Dieses Skript steht online zu Verfügung unter http://www.math.tu-berlin.de/coga/teaching/coma/Skript-
I-Java/
Inhaltsverzeichnis

Vorbemerkungen iii

Inhaltsverzeichnis v

1 Einleitung 1
1.1 Computer und Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Algorithmen versus Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . 7
1.4 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

2 Probleme, Algorithmen, Programme 11


2.1 Temperatur Umrechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.1.1 Das Problem: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.1.2 Der Algorithmus: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.1.3 Das Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2 Einkommensteuerberechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2.1 Das Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2.2 Der Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.2.3 Das Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.3 Primzahl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.3.1 Das Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.3.2 Der Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.4 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

3 Ausdrücke, Anweisungen, Kontrollstrukturen 31

v
vi INHALTSVERZEICHNIS

3.1 Variablen und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31


3.2 Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.2.1 Ausdrücke, genaue Erklärung . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.2.2 Ausdrücke und einfache Anweisungen . . . . . . . . . . . . . . . . . . . . . 36
3.2.3 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.3 Definitionen und Deklarationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.4 Strukturierte Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.4.1 Zusammengesetzte Anweisung, Verkettung . . . . . . . . . . . . . . . . . . 39
3.4.2 Selektion, Bedingte Anweisung . . . . . . . . . . . . . . . . . . . . . . . . 44
3.4.3 Wiederholung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.4.4 Mächtigkeit von Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . 55
3.5 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58

4 Objekte, Typen, Datenstrukturen 59


4.1 Datentypen und Operationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.2 Strukturierte Datentypen (Datenstrukturen) . . . . . . . . . . . . . . . . . . . . . . 64
4.3 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
4.3.1 Strings in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
4.3.2 Manipulation von Strings: ein erster Ansatz . . . . . . . . . . . . . . . . . . 67
4.3.3 Manipulation von Strings mit Exception Handling . . . . . . . . . . . . . . 69
4.4 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.4.1 Arrays in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.4.2 Mehrdimensionale Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.5 Records . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.6 Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.6.1 Einschub: Javadoc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.6.2 Eine Klasse für Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
4.7 Stacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
4.8 Queues (Warteschlangen) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
4.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
4.10 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

5 Algorithmen auf Arrays 105


INHALTSVERZEICHNIS vii

5.1 Suchen einer Komponente vorgegebenen Wertes . . . . . . . . . . . . . . . . . . . . 105


5.1.1 Sequentielle Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
5.1.2 Binäre Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.2 Matrizenmultiplikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
5.3 Kürzeste Wege in gerichteten Graphen . . . . . . . . . . . . . . . . . . . . . . . . . 117
5.3.1 Graphen und Wege . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
5.3.2 Zwei konkrete Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . 119
5.3.3 Die Bellman Gleichungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
5.3.4 Der Einfluss negativer Zykel . . . . . . . . . . . . . . . . . . . . . . . . . . 126
5.3.5 Der Bellman-Ford Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . 127
5.3.6 Die Ermittlung negativer Zykel . . . . . . . . . . . . . . . . . . . . . . . . 132
5.3.7 Die Ermittlung kürzester Wege . . . . . . . . . . . . . . . . . . . . . . . . . 133
5.4 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

6 Abstraktion von Methoden und Daten 137


6.1 Funktionale (Prozedurale) Abstraktion . . . . . . . . . . . . . . . . . . . . . . . . . 137
6.1.1 Funktionen und Prozeduren . . . . . . . . . . . . . . . . . . . . . . . . . . 137
6.1.2 Parameter und Datenfluss . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
6.1.3 Gültigkeitsbereiche von Identifiern (Scope) . . . . . . . . . . . . . . . . . . 143
6.1.4 Abarbeitung von Funktionsaufrufen . . . . . . . . . . . . . . . . . . . . . . 146
6.1.5 Der Run-Time-Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
6.2 Modulare Abstraktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
6.3 Abstraktion durch Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
6.3.1 Definition von Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
6.3.2 Static-Felder und Methoden . . . . . . . . . . . . . . . . . . . . . . . . . 157
6.3.3 Unterklassen und Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . 157
6.3.4 Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
6.3.5 Sichtbarkeit von Klassen, Methoden und Feldern . . . . . . . . . . . . . . . 160
6.3.6 Weitere Modifizierer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
6.3.7 Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
6.3.8 Klassen in Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
6.3.9 Implementationen des Interface ActionListener . . . . . . . . . . . . . . . . 164
viii INHALTSVERZEICHNIS

6.3.10 Der Lebenszyklus von Objekten . . . . . . . . . . . . . . . . . . . . . . . . 168


6.4 Beispiele von Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
6.4.1 Bruchrechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
6.4.2 Erzeugung von Zufallszahlen . . . . . . . . . . . . . . . . . . . . . . . . . . 173
6.5 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180

7 Rekursion 181
7.1 Beispiele für Rekursive Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . 184
7.1.1 Berechnung des ggT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
7.1.2 Die Türme von Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
7.1.3 Die Ackermann Funktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
7.1.4 Ulams Funktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
7.2 Wo Rekursion zu vermeiden ist . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
7.2.1 Berechnung der Fakultät . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
7.2.2 Berechnung des größten gemeinsamen Teilers . . . . . . . . . . . . . . . . . 193
7.2.3 Die Türme von Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
7.2.4 Berechnung der Fibonacci-Zahlen . . . . . . . . . . . . . . . . . . . . . . . 193
7.2.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
7.3 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195

8 Die Analyse von Algorithmen 197


8.1 Analysearten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
8.2 Die Asymptotische Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
8.2.1 Obere Schranken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
8.2.2 Untere Schranken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
8.3 A posteriori Analyse, Laufzeitmessungen . . . . . . . . . . . . . . . . . . . . . . . 203
8.4 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204

9 Sortieren in Arrays 205


9.1 Direkte Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
9.1.1 Sortieren durch Austauschen: Bubblesort . . . . . . . . . . . . . . . . . . . 206
9.1.2 Sortieren durch direktes Auswählen: Selection Sort . . . . . . . . . . . . . . 208
9.1.3 Sortieren durch direktes Einfügen: Insertion Sort . . . . . . . . . . . . . . . 210
INHALTSVERZEICHNIS ix

9.2 Mergesort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212


9.2.1 Mischen sortierter Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
9.2.2 Sortieren durch rekursives Mischen: Mergesort . . . . . . . . . . . . . . . . 215
9.2.3 Die Analyse von Mergesort . . . . . . . . . . . . . . . . . . . . . . . . . . 216
9.3 Beschleunigung durch Aufteilung: Divide and Conquer . . . . . . . . . . . . . . . . 219
9.3.1 Aufteilungs-Beschleunigungs-Sätze . . . . . . . . . . . . . . . . . . . . . . 219
9.3.2 Multiplikation von Dualzahlen . . . . . . . . . . . . . . . . . . . . . . . . . 223
9.4 Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
9.4.1 Der Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
9.4.2 Der Rekursionsaufwand von Quicksort . . . . . . . . . . . . . . . . . . . . 229
9.4.3 Der Worst Case Aufwand von Quicksort . . . . . . . . . . . . . . . . . . . . 230
9.4.4 Der mittlere Aufwand von Quicksort . . . . . . . . . . . . . . . . . . . . . . 230
9.5 Heapsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
9.5.1 Die Grobstruktur von Heapsort . . . . . . . . . . . . . . . . . . . . . . . . . 235
9.5.2 Die Implementation des Heaps . . . . . . . . . . . . . . . . . . . . . . . . . 236
9.5.3 Die Implementation von Heapsort . . . . . . . . . . . . . . . . . . . . . . . 240
9.5.4 Die Analyse von Heapsort . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
9.6 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245

10 Untere Komplexitätsschranken für das Sortieren 247


10.1 Das Entscheidungsbaum-Modell . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
10.2 Analyse des Entscheidungsbaums . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
10.3 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252

Literaturverzeichnis 253

Index 256
x INHALTSVERZEICHNIS
Kapitel 1

Einleitung

1.1 Computer und Algorithmen

Wir leben im Zeitalter der Computerrevolution. Sie hat vergleichbare Auswirkungen für die Gesell-
schafts- und Sozialordnung wie die Industrielle Revolution. War die Industrielle Revolution im We-
sentlichen eine Steigerung der körperlichen Kräfte des Menschen, so ist die Computerrevolution eine
Steigerung der geistigen Kräfte, eine Verstärkung des menschlichen Gehirns.
Die Bedeutung des Computers hat zur Informatik (Computer Science) als neue wissenschaftliche Dis-
ziplin geführt. Sie behandelt alle Aspekte des Computereinsatzes und der Rechnerentwicklung.
Wenn man sich einmal fragt, was einen Computer so revolutionär macht, so kann man in erster Nähe-
rung zur Beantwortung dieser Frage sagen, dass ein Computer eine Maschine ist, die geistige Routi-
nearbeiten durchführt, indem sie einfache Operationen (Basisoperationen) mit hoher Geschwindigkeit
ausführt. Ein Beispiel ist etwa das Suchen eines Namens in einer Liste oder das Sortieren einer Menge
von Namen in alphabetischer Reihenfolge.
Dies bedeutet, dass ein Computer natürlich nur solche Aufgaben erledigen kann, die durch solche
einfachen Operationen beschreibbar sind. Außerdem muss man dem Computer mitteilen können, wie
die Aufgabe durchzuführen ist. Eine solche Beschreibung der Aufgabe für den Computer nennt man
Algorithmus.
Ein Algorithmus ist also eine Handlungsvorschrift, und keine Problembeschreibung. Etwas genauer:
Ein Algorithmus ist eine präzise, das heisst in einer festgelegten Sprache abgefasste, endliche Be-
schreibung eines allgemeinen Verfahrens unter Verwendung ausführbarer elementarer Verarbeitungs-
schritte zur Lösung einer gestellten Aufgabe.
In der Informatik muss man diese umgangssprachliche Beschreibung weiter präzisieren durch die An-
gabe eines geeigneten Modells für den Computer (Maschinenmodell) und die Angabe der möglichen
elementaren Schritte (d. h. der Angabe einer Programmiersprache). Mögliche Modelle in der Infor-
matik sind die Turing-Maschine, die Random-Access-Maschine (RAM) und viele andere. Diese Ma-

Version vom 17. Juni 2010

1
2 KAPITEL 1. EINLEITUNG

schinenmodelle werden genauer in der Theorie der Berechenbarkeit untersucht. Es hat sich gezeigt,
dass alle gängigen Maschinenmodelle zueinander äquivalent sind in dem Sinne, dass man das eine
Modell durch das andere simulieren kann. Man kann also eine Turingmaschine so programmieren,
dass sie eine RAM darstellt und umgekehrt.
Die konkrete Ausführung bzw. Abarbeitung des Algorithmus nennt man einen Prozess. Die Einheit,
die den Prozess ausführt heißt Prozessor. Ein Prozess besitzt zu jedem Zeitpunkt einen Zustand, der
den aktuellen Stand der Ausführung angibt.
Man beachte, dass ein Prozessor nicht nur ein Computer, sondern auch ein Mensch oder ein Gerät
sein kann.
Beispiel:

Prozess Algorithmus Typische Schritte im Algorithmus


Pullover stricken Strickmuster 2 rechts, 2 links
Modellflugzeug bauen Montageanleitung leime Teil A an Flügel B
Kuchen backen Backrezept 3 Eier unter rühren
Beethovensonate spielen Notenblatt einzelne Noten

Ein Computer ist also nichts anderes als ein spezieller Prozessor. Ein gängiger Computer hat drei
Hauptkomponenten wie sie Abbildung 1.1 zeigt.

Ein-, Ausgabe- Zentraleinheit Speicher


geräte CPU (memory)

(3) (1) (2)

Abbildung 1.1: Hauptkomponenten eines Computers.

Es sind:

1. Die Zentraleinheit (CPU oder Central Processing Unit). Sie führt die Basisoperationen aus.

2. Der Speicher (Memory). Er enthält

• die auszuführenden Operationen des Algorithmus,


• die Information (Daten bzw. Objekte), auf der die Operationen wirken (wesentliches Kenn-
zeichen des von-Neumann Rechners).

3. Die Ein-Ausgabegeräte (Input and Output Devices), über die der Algorithmus und die Daten,
die in den Hauptspeicher gebracht werden und über die der Computer die Ergebnisse seiner
Tätigkeit mitteilt.

Diese Komponenten bilden die Hardware. Das sind die physikalischen Einheiten, aus denen sich
ein Computer zusammensetzt. Im Rahmen der Vorlesung werden Ihnen die Computer sowohl als
theoretisches Modell in der Vorlesung, als auch als praktische Hardware begegnen. Die praktische
1.1. COMPUTER UND ALGORITHMEN 3

Hardware wird durch UNIX-Workstations oder Linux PCs dargestellt. Die typischen Schritte in den
Algorithmen für diese Computer sind Statements der Programmiersprache Java, die Sie im Rahmen
der Veranstaltung erlernen werden. In theoretischer Hinsicht wird in der Vorlesung ein idealisiertes
Rechnermodell (ähnlich zu einer RAM) betrachtet, das Java-Statements verarbeiten kann, aber idea-
lisiert in der Hinsicht ist, dass vorab keine Beschränkungen durch Wortlänge, größte zu verarbeitende
Zahl usw. angenommen werden. Sie spielen erst bei konkreten Implementationen eine Rolle.
Kommen wir zurück zur Frage, was denn den Computer so revolutionär macht, und fragen nach
kennzeichnenden Merkmalen. Dann ergeben sich:

1. Geschwindigkeit
Selbst komplexe Algorithmen mit vielen Basisoperationen können schnell ausgeführt werden.
Man beachte jedoch, dass trotz hoher Computergeschwindigkeit Aufgaben bleiben, die zu zeit-
intensiv sind, um durchführbar zu sein (z. B. die Bestimmung einer Gewinnstrategie beim
Schachspielen). Im Rahmen der Komplexitätstheorie wird der Schwierigkeitsgrad von Proble-
men untersucht, den sie für die Behandlung auf dem Computer darstellen.

2. Zuverlässigkeit
Die Wahrscheinlichkeit für elektronische Fehler sind äußerst gering. Meist sind Abstürze oder
Inkorrektheiten in Programmen auf Programmierfehler oder logische Fehler zurückzuführen.
In einem gewissen Sinne ist ein Computer also ein billiger und gehorsamer Diener. Er führt
blindlings Befehle aus und wiederholt sie, wenn nötig, beliebig oft ohne Beschwerde. Diese
Stärke ist aber zugleich auch eine Schwäche, da die Anweisungen blindlings ausgeführt werden,
egal ob sie nun den beabsichtigten Ablauf korrekt beschreiben oder nicht.

3. Speicher
Ein Rechner kann riesige Informationsmengen speichern und schnell darauf zugreifen, da seine
Speichertechnik den beliebigen Zugriff (Random Access) gestattet. Hier besteht ein großer Un-
terschied zum menschlichen Gehirn, das in der Regel assoziativ arbeitet, d. h. beim Auffinden
von Informationen wird nicht auf die Adresse der Information zurückgegriffen, sondern man
benutzt die Assoziation mit anderen Informationen (“Eselsbrücken”).

4. Kosten
Die Kosten der Computer sind in vielen Bereichen niedrig im Vergleich zu äquivalenter mensch-
licher Arbeit.

5. Vernetzung
Die Vernetzung von Computern (insbesondere im Internet) eröffnet völlig neuartige und schnel-
le Möglichkeiten zur Kommunikation und zum Zugang zu Information aus vielfältigen Quellen.

Die Punkte 1.–5. werden deutlich, wenn man sich ein modernes Flugreservierungssystem vorstellt,
bei dem parallel in vielen Ländern Reisebüros auf zentralen Dateien Buchungen und Stornierungen
vornehmen. Man stelle sich einmal vor, wie man das ohne Computer realisieren müsste.
4 KAPITEL 1. EINLEITUNG

1.2 Programmiersprachen

Die Ausführung eines Algorithmus auf einem Prozessor setzt voraus, dass der Prozessor den Algo-
rithmus interpretieren können muss, d. h. er muss

• verstehen, was jeder Schritt bedeutet und

• die jeweilige Operation ausführen können.

Dies ist erreichbar durch die schrittweise Verfeinerung (ein wichtiges Instrument für die Program-
miermethodik) bis hin auf das Verständnisniveau des Prozessors. So kann z. B. bei der Anleitung zum
Stricken die Anweisung “2 links-2 rechts” Teil der Verfeinerung der Anweisung “Zopfmuster” sein.
Bei Computern als Prozessor muss der Algorithmus in einer Programmiersprache ausgedrückt wer-
den. Die Schritte im Algorithmus heißen dann Anweisung oder Befehl (statement). Ihr Detailliertheits-
grad und ihre konkrete Formulierung ist abhängig von der verwendeten Programmiersprache.
Bei den einfachsten Sprachen (Maschinensprachen) kann jede Anweisung direkt vom Computer in-
terpretiert werden. Dies bedeutet, dass Anweisungen jeweils nur kleine Teile des Algorithmus aus-
drücken und man lange Programme für komplexe Aufgaben schreiben muss. Die Programmierung in
der Maschinensprache ist also langwierig und mühsam und dadurch auch fehleranfällig.
Zur Vereinfachung der Programmierung wurden andere Sprachen entwickelt, die sogenannten höher-
en Programmiersprachen. Sie sind komfortabler, da eine Anweisung bereits einen größeren Algorith-
musteil abdecken kann, was wiederum die Erstellung vom Programmen erleichtert.
Programme in höheren Programmiersprachen können nicht direkt durch die CPU eines Computers
interpretiert werden. Der gängige Weg, dies zu erreichen besteht darin, Programme aus höheren Pro-
grammiersprachen in die Maschinensprachen zu übersetzen, bevor sie ausgeführt werden (evtl. über
mehrere Zwischensprachen). Diese Übersetzung kann selbst wiederum von einem Computer aus-
geführt werden und ist damit ein automatisierter Teil der schrittweisen Verfeinerung, vgl. Abbildung
1.2.
Der Übergang zwischen Problem- und Maschinen-orientierten Sprachen ist fließend. So weist bei-
spielsweise die Programmiersprache C einerseits den Sprachumfang und die Notation einer höheren
Programmiersprache auf, verfügt aber andererseits über viele Eigenschaften einer Assemblersprache
für eine maschinennahe Programmierung. Am Maschinen-orientierten Ende der Skala ist die auf vie-
len Rechnern vorhandene so genannte Mikroprogrammierung zu erwähnen, mit deren Hilfe elemen-
tare Algorithmen zwischen der Ebene der internen Maschinendarstellung und der Assemblerebene
realisiert werden können. Wir verdeutlichen dies anhand eines sehr einfachen Beispiels. Die Addition
zweier Zahlen a und b und die Zuweisung des Ergebnisses an eine Variable c kann in einer Program-
miersprache folgendermaßen formuliert sein:

c = a+b

Die Notation ähnelt der von der Mathematik her bekannten Formelschreibweise und ist unmittelbar
verständlich. Die Formulierung in einer Assemblersprache könnte wie folgt aussehen:
1.2. PROGRAMMIERSPRACHEN 5

Algorithmus

Hauptthema
der Programmierung (Codierung)
Vorlesung
Programm in höherer
Programmiersprache

automatisierte Übersetzung

Programm in
Maschinensprache

Interpretation durch CPU (Decodierung)

gewünschter Ablauf wird ausgeführt

Abbildung 1.2: Übersetzung von Programmen.

MOVE R1,a (hole a aus dem Speicher und schreibe a in das Register R1)
MOVE R2,b (hole b aus dem Speicher und schreibe b in das Register R2)
ADD R2,R1 (addiere den Inhalt von Register R1 zum Inhalt von Register R2)
MOVE c,R2 (schreibe den Inhalt von Register R2 unter dem Namen
c in den Speicher)

Zusätzlich zur eigentlichen Additionsoperation müssen nun noch die Lese- und Schreiboperationen
auf dem Speicher berücksichtigt werden. Für alle Assembleroperationen sind sehr genaue Kenntnisse
über die Organisation des verwendeten Rechners erforderlich, etwa die Funktionsweise, Anzahl und
Benennung der Register. Für den nur am Ergebnis c interessierten Programmierer ist weder von Inter-
esse, welche Speicheroperationen erforderlich sind, noch, was denn ein Register überhaupt ist. Schon
dieses extrem einfache Beispiel zeigt die Unübersichtlichkeit von Assemblerprogrammen.
Auf der Ebene der Mikroprogrammierung können die Schritte der Assemblerprogramme weiter zer-
legt werden. Dies kann bis hinunter zu Operationen auf einzelnen Bits gehen. Die Mikroprogrammie-
rung erlaubt die Programmierung auch der kleinsten Teilfunktionen eines Rechners.
Auf der untersten Maschinenebene erhält man nur noch intern verschlüsselte Darstellungen, mit denen
nur noch sehr geduldige und bis ins kleinste mit “ihrem” Rechner vertraute Spezialisten umgehen
können. Ein fiktives Beispiel für einen Maschinencode könnte lauten:
6 KAPITEL 1. EINLEITUNG

0000 0111 0110 1011 1001 1111 1010 0010 1110 1010 1010
0101 1110 1010 0101 0100 0101 0100 0010 1010 1010 1111
0001 1010 1010 1010 1011 0101 0101 0101 0101 0101 0101
1010 1010 1010 1010 1010 1010 1010 1010 1010 1010 1001

Am anderen Ende der Skala gibt es verschiedene Versuche über die höheren Programmiersprachen
hinaus in Richtung auf die Problemformulierung in natürlicher Sprache.
Bei der Übersetzung von einer höheren Programmiersprache in die Maschinensprache unterscheidet
man zwischen interpretieren und kompilieren. Beim Interpretieren wird

• jede Anweisung einzeln übersetzt,

• vor der Übersetzung der nächsten Anweisung zunächst die vorige Anweisung ausgeführt,

• bei jedem Lauf des Programms wieder neu übersetzt.

Beim Kompilieren wird

• das Programm als ganzes übersetzt durch den sogenannten Compiler. Ein Compiler ist also ein
Programm, das ein anderes Programm aus dem Quelltext (source code) in maschinenlesbare
Form übersetzt (object code).

• Der object code steht dann in Maschinensprache für jeden Aufruf zur Verfügung.

Die auf den ersten Personal Computern verfügbare Programmiersprache BASIC wurde speziell im
Hinblick auf Interpretation entworfen. Im allgemeinen sind interpretierte Sprachen “strukturschwä-
cher”, haben jedoch den Vorteil, dass man Programme sehr schnell zum Laufen bekommt.
Kompilieren erfordert mehr Speicherplatz als interpretieren. Die Fehlersuche ist im Allgemeinen müh-
samer, dafür laufen kompilierte Programme jedoch wesentlich schneller.
Die im Rahmen der Vorlesung gelehrte Programmiersprache Java wird sowohl kompiliert wie inter-
pretiert, allerdings auf verschiedenen Niveaus. Der Programmtext wird vom Java-Compiler in eine
Zwischensprache, den Java Bytecode kompiliert. Dieser wird dann durch die Java Virtual Machine
(JVM) interpretiert und ausgeführt. Dadurch muss nur die JVM für verschiedene Rechnerarchitektu-
ren (Windows, Unix, Macintosh ...) angepasst werden. Der einmal kompilierte Code kann dann auf
allen Architekturen ausgeführt werden.
Diese Systemunabhängigkeit macht Java zur WWW-Sprache par excellence und führt zur Zeit auch
zu eigenständigen systemunabhängigen Applikationen (z. B. Bürosoftware in Java).
Bei der Übersetzung eines Programms erfolgt stets eine Syntaxanalyse. Dabei erlaubt die Interpretati-
on die Suche einfacher Fehler, während die Kompilation meistens eine weitergehende Syntaxanalyse
und auch eine partielle Überprüfung der Semantik erlaubt.
Es gibt eine ganze Hierarchie von Programmiersprachen, die von einfachen Sprachen (Maschinen-
sprache) über mittleres Niveau (FORTRAN, BASIC) bis zu hohem Niveau reichen (Pascal, C, C++,
Java).
1.3. ALGORITHMEN VERSUS PROGRAMMIERSPRACHEN 7

Diese Sammlung von Programmen auf einem Rechner nennt man die Software. Auch bei der Software
gibt es eine Hierarchie, die am unteren Ende mit der Hardware verknüpft ist: die Software-Hardware-
Hierarchie (vgl. Abbildung 1.3).

Anwendungssoftware
z. B. Textverarbeitung, Statistik-
Programmier- paket

umgebung Systemsoftware
z. B. Betriebssystem, Editor,
Compiler, Eclipse

Computerhardware
z. B. CPU, Speicher, Ein-, Aus-
gabegeräte

Abbildung 1.3: Die Software-Hardware Hierarchie.

Auf der mittleren Ebene ist das Betriebssystem (operating system) besonders wichtig. Es dient der

• Verwaltung und Steuerung der Ein-Ausgabe-Einheiten, z. B. Drucker,

• Speicherung von Informationen (z. B. auf Diskette),

• Unterstützung der gleichzeitigen Benutzung von Computern durch mehrere Benutzer,

• Bereitstellung von Kommandooberflächen (Shells) für die Benutzer zur Kommunikation mit
dem Rechner (Start von Programmen, Kopieren von Files usw.).

In der Vorlesung wird als Betriebssystem UNIX verwendet. Eine Einführung in UNIX wird in der
Übung gegeben.

1.3 Algorithmen versus Programmiersprachen

Wie wir gesehen haben, erfordert die Durchführung eines Prozesses auf einem Computer, dass

• ein Algorithmus entworfen wird, der beschreibt, wie der Prozess auszuführen ist,

• der Algorithmus als Programm in einer geeigneten Programmiersprache ausgedrückt wird,

• der Computer das Programm ausführt.


8 KAPITEL 1. EINLEITUNG

Die Rolle von Algorithmen ist grundlegend. Ohne Algorithmus gibt es kein Programm und ohne
Programm gibt es nichts auszuführen.
Algorithmen sind unabhängig von einer konkreten Programmiersprache und einem konkreten Com-
putertyp, auf denen sie ausgeführt werden. Ein wesentlicher Teil der Vorlesung besteht darin, den
Entwurf von Algorithmen unabhängig von der “Tagestechnologie” zu entwerfen und zu studieren.
Dabei spielt natürlich die Modellierung und Mathematisierung der zugrunde liegenden Anwendung
eine wichtige Rolle.
Überspitzt gesagt sind Algorithmen wichtiger als Computer und Programmiersprachen. Programmier-
sprachen sind nur Mittel zum Zweck, um Algorithmen in Form von Prozessen auszuführen. Natürlich
sind auch Computer und Programmiersprachen wichtig, da sie z. B. die Ausführgeschwindigkeit eines
Programms und den Aufwand zur Erstellung des Programms bestimmen, aber sie sind letztlich nur
Mittel zur effektiveren Darstellung und Ausführung von Algorithmen.
Wegen dieser grundlegenden Bedeutung von Algorithmen gibt es viele Gebiete der Angewandten
Mathematik und der Informatik, die sich mit Algorithmen beschäftigen. Dies sind z. B.

– Entwurf (Design) von Algorithmen Dies ist im allgemeinen eine schwierige Tätigkeit, die viel
Kreativität und Einsicht erfordert (es gibt keinen Algorithmus zum Entwurf von Algorithmen).
Dieses Thema ist ein wesentlicher Gegenstand der Vorlesung.

– Berechenbarkeit Gibt es Prozesse, für die kein Algorithmus existiert? Die Antwort auf diese
Frage und das Studium dessen, was Berechenbarkeit ist, d. h. auf einem Algorithmus ausführbar
ist oder nicht, ist Gegenstand dieses Gebietes.

– Komplexität von Algorithmen Dieses Gebiet befasst sich mit der Untersuchung des Aufwands
an Laufzeit und Speicherplatz und der Ermittlung in Form von unteren Komplexitätsschranken
für Problemklassen und der Entwicklung von “schnellen” Algorithmen zu ihrer Lösung. Im
Rahmen der Vorlesung wird dies bereits an einfachen Beispielen (Sortieren) erläutert.

– Korrektheit von Algorithmen Hier werden Methoden entwickelt um nachzuweisen, dass ein
Algorithmus korrekt arbeitet. Diese Methoden sind teilweise wieder automatisierbar (automati-
sches Beweisen!).

1.4 Literaturhinweise
Neuere einführende Werke, die die gesamte Informatik oder zumindest große Teile davon behandeln, sind
[AU94, Bro06, GL02, GS06]. Die hier gegebene Einleitung lehnt sich an [GL02] an. Speziell für die theoreti-
sche Informatik sei auf die umfangreichen Handbücher [vL90a, vL90b] verwiesen.
Im Hinblick auf den Entwurf und die Analyse von Algorithmen sind in den letzten Jahren eine ganze Reihe guter
Lehrbücher erschienen. Besonders empfehlenswert ist [CLRS01], weitere gute Bücher sind [Meh88, Mei98,
OW02, Sed03] und die neu aufgelegten Klassiker [Knu97, Knu98a, Knu98b].
Die zunehmende Bedeutung von Java spiegelt sich zur Zeit in einer Flut von Büchern über Java und objektori-
entiertes Programmieren wieder.
1.4. LITERATURHINWEISE 9

Für Anfänger geeignet und besonders ausführlich und anschaulich ist [DD04]. Für Umsattler von einer anderen
Programmiersprache nach Java ist [Fla05] zu empfehlen. Das Buch [Job05] behandeln Java vom “übergeordne-
ten” Standpunkt und beleuchtet die Syntax in vielen Einzelheiten. Sehr empfehlenswert ist das sowohl als Buch
als auch online verfügbare Tutorial [Krü06]. Eine gelungene Verbindung von Datenstrukturen, Standardalgo-
rithmen und Java bringen [GT05], wobei jedoch Java Kenntnisse vorausgesetzt werden, [Pre00], das ebenfalls
online verfügbar ist, und [SS06].
Die Analyse von Algorithmen verlangt Grundkenntnisse aus der Diskreten Mathematik (vor allem aus der Kom-
binatorik und der Graphentheorie). Die nötigen Techniken sind u. a. in [CLRS01] enthalten. Weiterführende,
empfehlenswerte Bücher sind [Aig04, GR03, Wii87].
10 KAPITEL 1. EINLEITUNG
Kapitel 2

Probleme, Algorithmen, Programme:


Einige Beispiele

2.1 Temperatur Umrechnung

2.1.1 Das Problem:

Temperaturangaben in Fahrenheit sollen in Celsius umgerechnet werden.

2.1.2 Der Algorithmus:

Dafür nutzt man den Zusammenhang zwischen beiden Temperaturskalen. Beide Skalen haben eine
äquidistante Unterteilung mit folgenden Entsprechungen 1 :
0 Grad Fahrenheit ∼ = −17 97 Grad Celsius,
100 Grad Fahrenheit ∼ = 37 7 Grad Celsius.
9
Hieraus lässt sich die Temperatur C in Celsius als affin-lineare Funktion der Temperatur F in Fahren-
heit berechnen, vgl. Abbildung 2.1:

5
C = (F − 32) .
9
Dies resultiert in den folgenden Algorithmus.

1 Der anekdotischen Überlieferung zufolge kamen diese auf folgende Weise zustande. Fahrenheit wollte eines Tages eine

“normierte” Temperaturskala entwickeln. Es war gerade Winter und ziemlich kalt (nämlich −17 79 Grad Celsius). Da er sich
keine kältere Temperatur vorstellen konnte, normierte er diese zu 0. Anschließend wollte er die normale Körpertemperatur
des Menschen zu 100 normieren. Da er aber an diesem Tage leichtes Fieber hatte, wurden daraus 37 79 Grad Celsius.

Version vom 17. Juni 2010

11
12 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME
C

37.78

0
20 40 60 80 100 F

−17.78

Abbildung 2.1: Die Celsius- und Fahrenheitskala.

Algorithmus 2.1 (Temperatur Umrechnung)


1. Einlesen von F
2. Umrechnung in C
3. Ausgabe von C

2.1.3 Das Programm

Wir sehen uns jetzt ein zugehöriges Java Programm an. Dieses erzeugt ein Applet, das in Abbil-
dung 2.2 im Applet Launcher auf meinem Macintosh Rechner dargestellt ist.

Abbildung 2.2: Das Temperatur Applet.

Zunächst der Programmtext:

Programm 2.1 Temperatur.java


//Temperatur.java (1)
// (1)
2.1. TEMPERATUR UMRECHNUNG 13

// Transforms Fahrenheit to Celsius (1)


import java.awt.*; (2)
import java.applet.Applet; (3)
import java.awt.event.ActionListener; (3)
import java.awt.event.ActionEvent; (3)

public class Temperatur extends Applet (4)


{ (5)
// variables for the problem
double fahrenheit, celsius; (6)

// objects for the graphical user interface


Label inputPrompt, outputPrompt1, outputPrompt2; (7)
TextField input, output; (7)

// setup the graphical user interface components


// and initialize labels and text fields
public void init() { (8)
// define labels and textfields
inputPrompt = new Label("Geben Sie eine Temperatur " (9)
+ "in Fahrenheit an und dr"ucken Sie Return."); (9)
outputPrompt1 = new Label("Die Temperatur in " (9)
+ "Celsius betr"agt"); (9)
outputPrompt2 = new Label(" Grad."); (9)

input = new TextField(10); (10)


input.addActionListener(new ActionListener() { (11)
public void actionPerformed(ActionEvent e) { (11)
calculateTemperature(); (11)
} (11)
}); // action will be on input field (11)

output = new TextField(10); (12)


output.setEditable(false); (13)
// disable editing in output field (13)

// add labels and textfields to the applet


add(inputPrompt); (14)
add(input); (14)
add(outputPrompt1); (14)
add(output); (14)
add(outputPrompt2); (14)
}
14 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME

// process user’s action on the input text field


public void calculateTemperature() { (15)
// get input number
fahrenheit = Double.parseDouble(input.getText()); (16)

// calculate celsius and round it to 1/100 degrees


celsius = 5.0 / 9 * (fahrenheit - 32); (17)
// use Math class for round
celsius = Math.round(celsius * 100); (18)
celsius = celsius / 100.0; (19)

// show result in textfield output


output.setText(Double.toString(celsius)); (20)
} (21)
} (22)

Wir erläutern jetzt die Bedeutung der einzelnen Anweisungen gemäß der Nummerierung am rechten
Rand.

(1) Alles nach // bis zum Ende einer Zeile ist Kommentar. Die Kommentare enthalten den Namen
des Programms (Temperatur.java) und Informationen darüber, was das Programm tut.
Kommentare sollten auf Englisch sein, man weiß nie, wer das Programm einmal verwenden
muss.
(2) – (3) Das Programm benutzt Teile der von SUN entwickelten Bibliothek für Java. Die benötig-
ten Teile müssen dem Compiler per import-Anweisung mitgeteilt werden. Durch die Zeile
import java.awt.* in (2) werden alle Bibliotheksdateien geladen, die zum awt, dem abstract
window toolkit gehören. Die Import-Anweisungen (3) laden die Applet-Klasse, und zwei Klas-
sen zur Verarbeitung von Aktionen (ActionListener und ActionEvent).
Jede Klasse besteht grob gesprochen aus einer Kollektion von gleich gearteten Daten und Me-
thoden, die auf diesen Daten arbeiten. Inzwischen gibt es Tausende von verfügbaren Klassen in
Java, und die Klassenbibliothek wird laufend erweitert. Eine Beschreibung aller jeweils in Java
verfügbaren Klassen findet man unter http://java.sun.com/j2se/1.6.0/docs/index.html.
(4) Deklariert das Hauptprogramm Temperatur. Jedes Hauptprogramm ist in Java selber eine
Klasse, was durch das Schlüsselwort class zum Ausdruck gebracht wird. Temperatur erwei-
tert (Schlüsselwort extends) die vordefinierte Klasse Applet. Dies bedeutet, dass das lauffähi-
ge Programm Temperatur als Applet zur Verfügung steht, also über das WWW geladen und
mit Appletviewern oder HTML-Browsern ausgeführt werden kann, vgl. Abbildung 2.2 und Ab-
bildung 2.3.2
Das Schlüsselwort public bringt zum Ausdruck, dass die Klasse Temperatur auch von anderen
Klassen benutzt werden darf. Alle durch import geladenen Klassen sind ebenfalls public,
d. h. die von ihnen bereit gestellten Methoden können von Temperatur benutzt werden.
2 Man kann natürlich auch in Java eigenständige Programme schreiben; hierauf wird in den Übungen näher eingegangen.
2.1. TEMPERATUR UMRECHNUNG 15

(5) – (22) Die geschweiften Klammern {. . .} enthalten den Block der Klasse Temperatur. In ihm
wird festgelegt, was die Klasse leisten soll.
(6) Definiert die Variablen celsius, fahrenheit vom Datentyp double. Dieser bezeichnet Gleit-
kommazahlen doppelter Präzision (daher die Bezeichnung double), also im Rechner darstell-
bare reelle Zahlen mit großer Genauigkeit.
celsius, fahrenheit können also reelle Werte annehmen, aber keine anderen wie z. B. Buch-
staben oder Strings.
Die Bezeichner celsius und fahrenheit sind mnemonisch gewählt, d. h. aus den Namen lässt
sich leicht auf die Bedeutung schließen. Man sollte stets mnemonische Bezeichner verwenden.
(7) Deklariert die Objekte für das graphische User Interface, drei Label und zwei Textfelder. Label
sind Strings von Zeichen, und Textfelder sind rechteckige einzeilige Felder, die Text enthalten
können.
(8) Leitet die Definition der public Methode init() an. Diese wird automatisch beim Start des
Applets durch den Browser oder Appletviewer aufgerufen. Die Klammern () bedeuten, dass
die Methode keinen Input erwartet. Das Schlüsselwort void gibt an, dass die Methode kei-
nen Outputwert erzeugt. Der Block der Methode wird wieder durch geschweifte Klammern
begrenzt.
(9) Definiert die Anfangseigenschaften der Labels. Für jedes in (7) deklarierte Label wird durch
new Label (...)
ein neues Objekt vom Typ Label erzeugt und mit dem in den Klammern angegebenen String in-
itialisiert. Die Quotes ". . ." dienen als Begrenzer eines Strings, das + konkateniert zwei Strings
zu einem.
(10) Hierdurch wird ein neues Textfeld der Länge 10 (d. h. für 10 sichtbare Zeichen) eingerichtet und
der Variablen input zugewiesen. Dieses Textfeld ist jetzt unter dem Namen input ansprechbar.
(11) Diese (später näher erklärten 3 ) Anweisungen bewirken, dass das Textfeld input Aktionen
auslösen kann. Dies geschieht durch das Drücken der <Return> Taste. Der dem Textfeld hin-
zugefügte ActionListener startet beim Drücken der <Return> Taste die Methode mit dem
vorgeschriebenen Namen actionPerformed, die wiederum die von uns weiter unten in dem
Zeilen (15) – (20) definierte Methode calculateTemperature() startet.
(12) Richtet analog zu (10) das Textfeld output ein.
(13) Nutzt die Methode setEditable() der Klasse TextField, um output nicht-editierbar zu
machen.
(14) Hier werden durch die Methode add() der Klasse Applet alle Labels und Textfelder dem Ap-
plet hinzugefügt. Da keine weiteren Layoutanweisungen erfolgen, werden sie auf der verfügba-
ren Appletfläche zeilenweise zentriert angeordnet. Dabei sind mehrere Komponenten pro Zeile
möglich.
3 Hier wird eine anonyme Klasse definiert, die das Interface ActionListener präzisiert, vgl. Kapitel 6.3.8.
16 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME

(15) Leitet die Definition der public Methode calculateTemperature() ein, die die eigentliche
Rechnung und die Ein- und Ausgabe erledigt.
(16) Zunächst wird die in input angegebene Zahl der Variablen fahrenheit zugewiesen. Dies
erfolgt durch zwei ineinander geschachtelte Methoden. Die in input eingegebene Zahl ist
zunächst nur ein String. Dieser wird durch
input.getText()
eingelesen, anschließend durch die Methode parseDouble() der Klasse Double, also durch
Double.parseDouble() zu einer double Variablen konvertiert. Dieser Wert wird der double
Variablen fahrenheit zugewiesen.
Man könnte die Zeile (16) äquivalent auch in die folgenden Anweisungen zerlegen:

String str = input.getText(); // erzeugt String str


double temp = Double.parseDouble(str);
// erzeugt double temp aus str
fahrenheit = temp; // weist den Wert von temp
// der Variablen fahrenheit zu

(17) Ist eine Zuweisung. Der Wert des Ausdruck (5.0 / 9) * (fahrenheit - 32) wird berech-
net und der Variablen celsius zugewiesen. Da / sowohl die ganzzahlige Division mit Rest, als
auch die reellwertige Division bezeichnet, muss man (z. B. durch Angabe von 5.0 statt 5) dem
Compiler klar machen, dass hier die reellwertige Division gemeint ist. Beachte: 5 / 9 ergibt
den Wert 0, aber 5.0 / 9 den Wert 0.5555555555555556.
Hier findet die eigentliche Umrechnung statt. Alle anderen Anweisungen im Programm bezie-
hen sich auf das User-Interface und die Ein- und Ausgabe.
(18) – (19) Um viele Nachkommastellen bei der Ausgabe zu vermeiden, wird das Ergebnis auf 2
Nachkommastellen gerundet. Dies geschieht durch Verwendung der Methode round() der
Klasse Math, die viele mathematische Standardfunktionen bereitstellt. round() rundet eine
double Zahl auf die nächstliegende ganze Zahl.
(20) Zeigt das Resultat im Ausgabefeld output. Die Methode toString() der Klasse Double
wandelt den double Wert celsius in einen String um, der dann mit der Methode setText()
der Klasse TextField für das Textfeld output benutzt wird.

Wir haben im Programm Temperatur.java einige wichtige Begriffe kennen gelernt: Variable, Zu-
weisung, Klasse, Methode.
Eine Variable ist (in erster Näherung) ein Name (Platzhalter) für Objekte (Daten) eines Typs, z. B. des
Typs “reelle Zahl”.
Sie belegt im Speicher einen bestimmten (dem Benutzer unbekannten) Speicherplatz, dessen Größe
vom vereinbarten Typ des Objekts abhängt. Dieser Speicherplatz ist unter dem Namen der Variablen
ansprechbar.
Eine Zuweisung aktualisiert den Wert (Inhalt) des Speicherplatzes.
2.1. TEMPERATUR UMRECHNUNG 17

Beispiel 2.1 Die Deklaration


double fahrenheit, celsius;
erzeugt im Speicher die (automatisch mit 0 initialisierten) Objekte

celsius 0
fahrenheit 0 .

Die Eingabe von 95 bewirkt folgendes. Durch


fahrenheit = Double.parseDouble(input.getText());
wird der Variablen fahrenheit der eingelesene Wert zugewiesen.

celsius 0
fahrenheit 95.0

Die Zuweisung
celsius = (5.0 / 9) * (fahrenheit - 32)
bewirkt die Auswertung des Ausdrucks (5.0 / 9) * (fahrenheit - 32) zu

5
(5.0/9) ∗ (95 − 32) = · 63 = 35
9
und die Zuweisung an die Variable celsius.

celsius 35.0
fahrenheit 95.0

Man beachte: Ein Ausdruck hat einen Wert. Eine Zuweisung hat einen Effekt.
Eine Klasse besteht aus Variablen (auch Komponenten oder Datenfelder genannt) und Methoden (auch
Funktionen genannt). In Klassen sollen gleichartige Daten zusammengefasst werden und alle Me-
thoden, sie zu verarbeiten, bereitgestellt werden. Die Komponenten und Methoden können durch das
Schlüsselwort public auch anderen Klassen zur Verfügung gestellt werden, bzw. durch das Schlüssel-
wort private außerhalb der Klasse verboten werden. Der Zugriff auf eine public Methode oder
Komponente einer anderen Klasse erfolgt durch den “.”, wie in
Double.parseDouble() oder Math.round()
Klassen sind in Java auch Datentypen, man kann also Variablen diesen Typs definieren, wie z. B.
TextField input;
Eine solche Variable wird auch als Instanz der Klasse oder Objekt bezeichnet. Instanzen (Objekte)
können die Methoden und Komponenten der Klasse benutzen. Der Zugriff geschieht wieder mit dem
“.” wie in
18 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME

input.getText()
Methoden einer Klasse sind in anderen Programmiersprachen als Funktionen bekannt. Sie können ein
oder mehrere Argumente eines bestimmten Typs übergeben bekommen wie in
Math.round(celsius * 100)
und liefern Werte eines Rückgabetyps, bei Math.round vom Typ long, der für (lange) ganze Zahlen
steht. Argumenttypen und Rückgabetyp müssen in der Deklaration vereinbart werden, für Math.round
ist diese in der Klasse Math gegeben durch
long round(double a)
Dabei steht a für den zu übergebenden Wert vom Typ double.
Diese Bemerkungen sind nur eine sehr oberflächliche Einführung in Methoden und Klassen. Sie wer-
den wesentlich detaillierter in Kapitel 6 behandelt.
Der Programmtext muss in einer Datei abgespeichert werden, die genauso heißt wie die implementier-
te Klasse und den Zusatz .java hat, also in der Datei Temperatur.java. Für jede Klasse ist dabei
eine gesonderte Datei Klassenname.java anzulegen. Mit dem Befehl4
javac Temperatur.java
wird die Datei Temperatur.java kompiliert. Dies erzeugt die Datei Temperatur.class, die das
Java Programm als Byte-Code enthält, sowie eine Hilfsklasse Temperatur$1.class 5 .
In dieser Form kann es über das Internet durch Browser aus html-Seiten geladen und ausgeführt
werden, siehe Abbildung 2.3.

Abbildung 2.3: Darstellung des Applet Temperatur“ im Internet Explorer.



Dafür benötigt man eine html-Datei, die das Laden auslöst. Sie kann wie folgt aussehen.
4 in meiner Unix shell
5 Diese zweite .class Datei rührt von der anonymen Klasse her, die den ActionListener präzisiert.
2.2. EINKOMMENSTEUERBERECHNUNG 19

Datei startApplet.html

<title>Temperatur</title>
<hr>
<applet codebase=. code="Temperatur.class" width=550 height=70>
</applet>
<hr>
<a href="Temperatur.java">The source.</a>

codebase gibt das directory an, in dem der code Temperatur.class zu finden ist. codebase=.
bedeutet also, dass sich dieser in der gleichen directory wie Temperatur.html befindet. Durch
width=550 und height=70 werden die Weite und Höhe des Applets (gemessen in Pixel) festgelegt.
Diese Werte können im Applet selbst6 bzw. im Appletviewer durch Veränderung der Fenstergröße, in
dem das Applet angezeigt wird, verändert werden. Abbildung 2.4 zeigt eine verkleinerte Darstellung
durch Änderung der Fenstergröße im Appletviewer. Sie zeigt deutlich, dass die Objekte zeilenweise
zentriert auf das Applet gelegt werden.

Abbildung 2.4: Darstellung des Applet Temperatur“ bei Veränderung der Fenstergröße im Applet-

viewer.
Durch Laden der html-Datei startApplet.html in einen html-Browser (z. B. Internet Explorer)
oder einen Appletviewer steht dann das Applet ausführbar zur Verfügung.

2.2 Einkommensteuerberechnung

2.2.1 Das Problem

In einem (hypothetischen) Steuermodell sind Einkommensteuern S in Abhängigkeit des zu versteuern-


den Einkommens E zu zahlen. Das Steuermodell verwendet den sogenannten Stufen- oder Scheiben-
tarif, in dem das Einkommen E in Scheiben unterteilt wird, die mit unterschiedlichen Sätzen besteuert
werden. Hier sind es 3 Scheiben:
E ≤ 20.000 e 0 e Steuer
20.000 e < E ≤ 60.000 e 20% von E − 20.000
E > 60.000 e 8.000 + 40% von E − 60.000
6 durch die Anweisung setSize(x, y) in der Methode init()
20 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME

2.2.2 Der Algorithmus

Aus dem Scheibentarif ergibt sich die Einkommensteuer S als stückweise lineare, monoton steigende
Funktion von E, vgl. Abbildung 2.5.

Steuer S in Te

24

8
Einkommen E in Te

0 20 40 60 80 100

Abbildung 2.5: Die Einkommensteuer Kurve.

Dies resultiert in den folgenden Algorithmus.

Algorithmus 2.2 (Steuer Berechnung)


1. Einlesen von E
2. Berechnung von S
3. Ausgabe von S

2.2.3 Das Programm

Ein entsprechendes Java Programm ist:

Programm 2.2 Steuer.java


// Steuer.java
//
// calculates taxes depending on the income in a
// piecewise linear function
import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class Steuer extends Applet {

// constants for tax calculation


final double noTaxBound = 20000, lowRate = .2,
2.2. EINKOMMENSTEUERBERECHNUNG 21

lowTaxBound = 60000, highRate = .4;

double income, tax;

// setup the graphical user interface components


// and initialize labels and text fields
Label inputPrompt, outputString;
TextField input;
Panel p1, p2; // use subdivision of applet into 2 panels
// for better layout

public void init() {


//set layout for applet so that everything is left alligned
setLayout(new FlowLayout(FlowLayout.LEFT));

p1 = new Panel();
p2 = new Panel();

// define layout and color for panel p1


p1.setLayout(new FlowLayout(FlowLayout.LEFT));
p1.setBackground(Color.pink);

inputPrompt = new Label("Geben Sie das zu versteuernde "


+ "Einkommen an und dr"ucken Sie Return. EURO");
input = new TextField(10);
input.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
calculateTax();
}
}); // action will be on input field

p1.add(inputPrompt); // put prompt on panel


p1.add(input); // put input on panel

// same for panel p2


p2.setLayout(new FlowLayout(FlowLayout.LEFT));
p2.setBackground(Color.yellow);

// this time we write the output into a label


outputString = new Label("Es sind " + tax
+ " EURO Steuern zu zahlen. Ihr Finanzamt. ");
p2.add(outputString);

// add panels to the applet


22 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME

add(p1);
add(p2);
}

// process user’s action on the input text field


public void calculateTax() {
income = Double.parseDouble(input.getText());

if (income <= noTaxBound) { // no tax


tax = 0;
} else if (income <= lowTaxBound) { // low rate applies
tax = lowRate * (income - noTaxBound);
} else {
tax = lowRate * (lowTaxBound - noTaxBound)
// low rate applies
+ highRate * (income - lowTaxBound);
// high rate applies
}

// round to cent
tax = Math.round(tax * 100) / 100.0;

outputString.setText("Es sind " + tax


+ " EURO Steuern zu zahlen. Ihr Finanzamt.");
outputString.invalidate();
// marks outputString for updating when it changes

validate(); // tells applet to check for invalidations


// and update layout if necessary
}
}

Das entspechende Applet ist in Abbildung 2.6 dargestellt.

Abbildung 2.6: Das Steuer Applet.


2.2. EINKOMMENSTEUERBERECHNUNG 23

Hier sehen wir zusätzlich:

• Die Definition von Konstanten.

• Die if-Anweisung.

• Panels

• Layout-Anweisungen

Konstanten sollte man immer über Bezeichner ansprechen, da man so ihre Werte bei einer möglichen
Änderung nur an einer Stelle ändern muss. 7 Konstanten werden in Java über das Schlüsselwort final
gekennzeichnet.8 Sie müssen bei der Definition initialisiert werden.
Als Bezeichner (Identifikator) für Klassen, Methoden, Variablen, Konstanten usw. kommen in Java
bestimmte Strings in Frage. Allgemein bestehen Strings aus 16-bit Unicode Zeichen. Diese Zeichen
sind bei den ersten 128 Zeichen mit dem ASCII-Zeichensatz und mit den ersten 256 Zeichen mit dem
ISO8859-1 (Latin 1) Zeichensatz verträglich. Bei 16 Bit sind insgesamt ca. 34000 Zeichen darstellbar,
so dass auch Zeichensätze exotischer Sprachen (äthiopisch, Bengali, Tamil . . .) darstellbar sind.
Als Bezeichner kommen solche Strings in Frage, die mit einem Unicode-Buchstaben beginnen und
nur aus Unicode-Buchstaben und Unicode-Ziffern bestehen. Zulässig sind also
input_1, Möhring,
nicht jedoch
3-Felder, no Tax Bound.
Die if-Anweisung (if-statement) hat in Java die Form
if (Bedingung) Anweisung1 else Anweisung2
wobei der else-Teil fehlen kann. Dabei ist Bedingung ein logischer Ausdruck. Logische Ausdrücke
sind in Java vom Typ boolean und können die Werte true und false für die beiden Wahrheitswerte
wahr und falsch annehmen.
Ist im if-statement der Wert von Bedingung gleich true, so wird Anweisung1 ausgeführt, ansonsten
(Bedingung ist “false”) Anweisung2 (bzw. nichts, falls der else-Teil fehlt).
Anweisung1 bzw. Anweisung2 können wieder if-statements sein, wodurch eine Verschachtelung wie
im obigen Programm auftritt.
Die Klasse Panel (aus java.awt) stellt eine Möglichkeit dar, die Oberfläche eines Applets zu un-
tergliedern. In unserem Fall werden zwei Panels p1 und p2 definiert. Für beide Panels wird mit
setLayout() ein Layout festgelegt, nämlich ein linksbündiges Flowlayout. Ohne Layout Spezifi-
zierung wird das zentrierte Flowlayout genommen (wie in Temperatur.java). Darüber hinaus
werden mit der Methode setBackground() Farben für diese Panels definiert. Die Argumente dieser
7 Ein Negativbeispiel war die Umstellung der Postleitzahlen; die Anzahl der Zeichen hierfür war meist nie als Konstante

deklariert worden, was einen immensen Umstellungsaufwand erforderte und ganze Programmsysteme lahmlegte.
8 Genau genommen sind Konstanten in Java Variable, deren Wert nicht mehr geändert werden darf.
24 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME

Methode sind Konstanten der Klasse Color, werden also (Zugriff auf Komponenten!) mit Color.pink
angesprochen.
Auch das gesamte Applet verwendet ein linksbündiges Flowlayout. Die Layoutanweisungen sind
nicht absolut, sondern ändern sich mit der Größe des Applets. Das linksbündige Flowlayout schreibt
nur vor, dass die Komponenten in der Reihenfolge der add() Befehle Zeile für Zeile eingefügt wer-
den.
Die Ausgabe erfolgt diesmal über ein Label outputString, das in init() eingerichtet und in-
itialisiert wird, und in calculateTax() mit der Methode setText() aktualisiert wird. Um die
Länge dem veränderten Text anzupassen, wird das Objekt outputString zunächst durch die An-
weisung outputString.invalidate() als “verändert” markiert, und mit dem Aufruf der Methode
validate() für das Applet wird dieses angewiesen, outputString zu aktualisieren.

2.3 Primzahl

2.3.1 Das Problem

Zu einer gegebenen natürlichen Zahl n ∈ N, n > 0 soll die kleinste Primzahl p bestimmt werden, die
größer als n ist.9

2.3.2 Der Algorithmus

Der Algorithmus zur Lösung dieses Problems basiert auf der folgenden Idee.
Prüfe die Zahlen n + 1, n + 2, n + 3, . . ., ob sie durch eine kleinere natürliche Zahl k > 1 ohne Rest teil-
bar ist. Die erste Zahl z, die das nicht ist, ist die gesuchte Primzahl. Wir beschreiben dies nachstehend
in sogenanntem Pseudocode:

Algorithmus 2.3 (Primzahl)


Lese die Zahl n ein
Setze z := n
Wiederhole
Erhöhe z um 1
überprüfe ob z Primzahl ist
bis z Primzahl ist
Gebe z aus

Der Test, ob z eine Primzahl ist, geschieht nach folgender Idee:


Teile z durch alle Zahlen k = 2, 3, . . . (bis k · k > z) und prüfe ob der Rest 0 ist. Ist dies für ein k der
Fall, so ist z keine Primzahl, andernfalls (d. h. keines der k teilt z ohne Rest) ist z eine Primzahl.
9 N bezeichnet die Menge der natürlichen Zahlen 0, 1, 2, . . .
2.3. PRIMZAHL 25

Diese Idee führt zu dem folgenden Algorithmus. Hierin ist teilerGefunden eine sogenannte Boolesche
Variable, also eine Variable, die nur die Werte wahr oder falsch annimmt. Die geschweiften Klammern
{. . .} enthalten Kommentare.

Algorithmus 2.4 (Primzahltest)


Setze k := 2
Setze teilerGefunden := falsch {noch kein Teiler gefunden}
Solange (teilerGefunden = falsch) und (k · k ≤ z) führe aus
Teile z ganzzahlig durch k. Sei rest der entstehende Rest
Falls rest = 0 so setze teilerGefunden := wahr
Setze k := k + 1
{z ist Primzahl falls teilergefunden = falsch beim Austritt aus der Schleife}

Algorithmus 2.3 und 2.4 werden jetzt in ein Java Programm umgesetzt.

Programm 2.3 Primzahl.java


// Primzahl.java
//
// input: natural number n
// output: smallest prime number p with p > n
//
// method: apply prime test to n+1, n+2, ... until prime is found,
// prime testing of z is done by testing numbers k = 2 ... with
// k * k <= z if they are factors of z
//

import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class Primzahl extends Applet {


Label inputPrompt; // declare Label
TextField input, output; // declare textfields for input and output

int n, // the input integer read from terminal


z, // candidate for the prime, set to n+1, n+2 etc.
k; // possible divisor of z
boolean divisorFound; // Boolean, indicates that a divisor
// k of z has been found

// setup the graphical user interface components


// and initialize labels and text fields
26 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME

public void init() {


// set layout
setLayout(new FlowLayout(FlowLayout.LEFT));

inputPrompt = new Label("Geben Sie die Zahl n ein "


+ "und dr"ucken Sie Return.");
input = new TextField(10);
input.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// call method for finding next prime
findPrime();
}
}); // action will be on input field

// output will be text in a field


output = new TextField(60);
output.setEditable(false);
output.setBackground(Color.yellow);

add(inputPrompt); // put prompt on applet


add(input); // put input on applet
add(output); // put output on applet
}

// function for finding next prime number


public void findPrime() {
// get input number
n = Integer.parseInt(input.getText());
z = n;
do {
z++;
k = 2;
divisorFound = false;
// so far no divisor of z has been found
while (!divisorFound && k*k <= z) {
// check if k is a divisor of z
if (z % k == 0) {
divisorFound = true;
}
k++;
} //end while
} while (divisorFound);

// define the output string


2.3. PRIMZAHL 27

String str = "Die n"achstgr"o"sere Primzahl nach "


+ n + " ist " + z + ’.’;

// put the string into the output field


output.setText(str);
}
}

Das entsprechende Applet ist in Abbildung 2.7 dargestellt.

Abbildung 2.7: Das Primzahl Applet.

Neu sind hier folgende Konstrukte für Schleifen:


while (Fortsetzungsbedingung) Anweisung
bzw.
do Anweisung while (Fortsetzungsbedingung)
Im ersten Fall wird Anweisung so oft ausgeführt, wie Fortsetzungsbedingung gilt. Diese Bedingung
wird vor jedem Eintritt in die Schleife überprüft. Evtl. wird die Schleife also auch kein mal durchlau-
fen.
Im zweiten Fall erfolgt die Überprüfung der Fortsetzungsbedingung nach jeder Abarbeitung von An-
weisung. Anweisung wird also mindestens einmal ausgeführt.
Neu sind ferner folgende Operatoren:
! logische Negation
== Test auf Gleichheit
&& logisches und
% Rest bei ganzzahliger Division, 8 % 3 ergibt 2
Bei der Ausgabe wird zunächst der String str durch Konkatenation von Zeichenketten und Werten
von Variablen erzeugt. Dieser wird dann als String im Textfeld output ausgegeben.
Betrachten wir das Programm für das Zahlenbeispiel n = 50. Tabelle 2.1 dokumentiert die Verände-
rung der Werte der Variablen n, z, k, divisorFound während des Programmablaufs (von oben nach
unten).
Tabelle 2.1: Werte der Variablen in Programm 2.3
28 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME

n z k divisorFound Aktion im Programm


0 0 0 false Default Initialisierung bei Definition
50 0 0 false nach Einlesen in n
50 50 0 false nach z = n
50 51 0 false nach z++
50 51 2 false nach k = 2
50 51 2 false nach divisorFound = false
50 51 2 false Eintritt in while-Schleife
z % k = 51 % 2 6= 0
50 51 3 false nach k++
neuer Eintritt in while-Schleife
z % k = 51 % 3 = 0
50 51 3 true “then” statement ausgeführt
50 51 4 true nach k++
kein neuer Eintritt in while-Schleife
neuer Eintritt in do-Schleife
50 52 4 true nach z++
50 52 2 true nach k = 2
50 52 2 false nach divisorFound = false
50 52 2 false Eintritt in while-Schleife
z % k = 52 % 2 = 0
50 52 2 true “then” statement ausgeführt
50 52 3 true nach k++
kein neuer Eintritt in while-Schleife
neuer Eintritt in do-Schleife
50 53 3 true nach z++
50 53 2 true nach k = 2
50 53 2 false nach divisorFound = false
50 53 2 false Eintritt in while-Schleife
z % k = 53 % 2 6= 0
50 53 3 false nach k++
50 53 3 false neuer Eintritt in while-Schleife
z % k = 53 % 3 6= 0
50 53 4 false nach k++
50 53 4 false neuer Eintritt in while-Schleife
z % k = 53 % 4 6= 0
50 53 5 false nach k++
50 53 5 false neuer Eintritt in while-Schleife
z % k = 53 % 5 6= 0
50 53 6 false nach k++
2.3. PRIMZAHL 29

n z k divisorFound Aktion im Programm


50 53 6 false neuer Eintritt in while-Schleife
z % k = 53 % 6 6= 0
50 53 7 false nach k++
50 53 7 false neuer Eintritt in while-Schleife
z % k = 53 % 7 6= 0
50 53 8 false nach k++
50 53 8 false k*k = 8 ∗ 8 > 53
⇒ kein neuer Eintritt in while-Schleife
divisorFound hat Wert false
⇒ kein neuer Eintritt in do-Schleife
⇒ z = 53 ausgegeben

Die Korrektheit des Programms folgt aus den Vorüberlegungen10 :


- Die while-Schleife testet alle in Frage kommenden natürlichen Zahlen k ≤ z ob sie Teiler
von z sind.

- Die do-Schleife terminiert mit einem z, für das kein Teiler gefunden wurde.

- Da es zu jeder Zahl n eine Primzahl p mit p > n gibt, terminiert auch die do-Schleife nach
endlich vielen Schritten (keine Endlosschleife).

Der Algorithmus kann auf verschiedene Weisen verbessert und schneller gemacht werden.

1. Die if-Anweisung

if (z % k == 0) { divisorFound = true;}

kann ersetzt werden durch die (allerdings schwer lesbare) Zuweisung

divisorFound = (z % k == 0);

Dabei ist z % k == 0 ein Boolescher Ausdruck, dessen Wert der Variablen divisorFound
zugewiesen wird.

2. Es kommen nur ungerade Zahlen als Primzahlen in Frage. Daher kann man als ersten Wert von
z die erste ungerade Zahl > n nehmen und dann stets z um 2 erhöhen.

3. Da z ungerade gewählt wird, kommen nur ungerade Zahlen k als Teiler von z in Frage.
Der Einbau dieser Änderungen (Anfang und Ende wie in Programm 2.3):
10 Dabei wird vorausgesetzt, dass nur natürliche Zahlen eingegeben werden. Für andere typzulässige Eingaben wie zum
Beispiel negative ganze Zahlen wird nichts ausgesagt. Solche Eingaben könnte man natürlich durch entsprechende Fallun-
terscheidungen “abfangen”.
30 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME

. . .
if (n % 2 == 0) z = n - 1; else z = n;
// this makes z + 2 smallest odd number > n
do {
z += 2;
k = 3;
divisorFound = false;
while (!divisorFound && k*k <= z) {
divisorFound = (z % k == 0);
k += 2;
}\\endwhile
} while (divisorFound);
. . .

Dabei ist k += 2 äquivalent zu k = k + 2.

4. Eine andere Verbesserung besteht darin, in der while-Schleife eine unnötige Erhöhung der
Variablen k zu vermeiden. Dies kann erreicht werden durch:

if (z % k == 0) {
divisorFound = true;
} else {
k += 2;
}

2.4 Literaturhinweise
Die Beispiele sind [OSW02] entnommen. Dort finden sich insgesamt 100 Beispielaufgaben für einfache Pro-
gramme und zugehörige Lösungsalgorithmen (allerdings in Pascal).
Weitere Beispiele für einfache Java Programme findet man in [DD04, Krü06].
Kapitel 3

Ausdrücke, Anweisungen,
Kontrollstrukturen

Wir behandeln nun übersichtsartig die wichtigsten Sprachelemente in (höheren) Programmiersprachen


für die Formulierung von Algorithmen.

3.1 Variablen und Objekte

Ausdrücke beinhalten üblicherweise Variable, auf deren Wert bei der Auswertung zugegriffen wird.
In Java gibt es zwei unterschiedliche Arten von Variablen (bzw. Datentypen) nämlich Standardtypen
und Referenztypen.
Die Standardtypen sind die “eingebauten” Typen

boolean 1 Bit
char 16 Bit ganze Zahl ohne Vorzeichen
entspricht dem Zahlenwert eines Unicode-Zeichen
byte 8 Bit ganze Zahl mit Vorzeichen
short 16 Bit ganze Zahl mit Vorzeichen
int 32 Bit ganze Zahl mit Vorzeichen
long 64 Bit ganze Zahl mit Vorzeichen
float 32 Bit Fließkommazahl
double 64 Bit Fließkommazahl

Bei diesen Typen wird unter dem Namen der Variablen stets der Wert angesprochen. In der Sequenz

int a = 1;
int b = 2;
a = a + b;
Version vom 17. Juni 2010

31
32 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

wird also in a + b auf die Werte 1 und 2 zugegriffen.


Alle anderen Typen in Java sind Referenztypen. Hierzu gehören

• Klassentypen

• Schnittstellentypen

• Arraytypen

• Aufzählungstyp

Wir behandeln zunächst nur die Klassentypen. Jede Klasse stellt einen Datentyp dar. Die Werte von
Variablen eines Klassentyps heißen in Java Objekte. In

TextField input, output;

werden zwei Variablen der Klasse TextField deklariert.


Bei Referenztypen wird unter dem Namen der Variablen nicht der Wert (also das Objekt), sondern die
Adresse (Referenz) angesprochen, an der das Objekt im Speicher abgelegt ist. Die Variable input ist
kein Objekt, sondern zeigt lediglich auf eins. Man braucht daher Methoden, um

• die Objekte im Speicher zu erzeugen,

• die Werte/Daten einzugeben,

• auf die Werte/Daten zuzugreifen.

Die Erzeugung der Objekte erfolgt (bis auf wenige Ausnahmen) mit der new-Anweisung und dem
Aufruf eines Konstruktors. In

output = new TextField(10);

ist TextField(10) ein Konstruktor (von mehreren) der Klasse TextField, der ein Textfeld mit 10
Zeichen konstruiert und im Speicher einrichtet. Ein anderer Konstruktor ist z. B.

output = new TextField("Coma I", 10);

der das Textfeld gleich mit dem String Coma I belegt und eine Länge von 10 Zeichen vereinbart.
Jede Klasse verfügt üblicherweise über mehrere Konstruktoren für Objekte, darunter einen Default-
Konstruktor ohne Argumente.
Die Eingabe von Daten erfolgt dann z. B. über die Methode setText() wie in

output.setText("Hallo!");

Der lesende Zugriff auf Daten entsprechend über die Methode getText()
3.1. VARIABLEN UND OBJEKTE 33

output.getText();

Ausnahmen gibt es bei der Klasse String (als Zugeständnis an C-Programmierer). Hier ist neben der
Java-konformen Konstruktion

String str = new String("Hallo");

auch die direkte Zuweisung möglich

String str = "Hallo";

Ferner wird unter dem Namen einer Variablen vom Typ String in Ausdrücken stets der Wert (also
der String selbst) angesprochen.
Für alle Standardtypen gibt es in Java korrespondierende Klassen (sogenannte Wrapper-Classes):

• Integer für int,

• Double für double,

• Boolean für boolean,

• Character für char.

Die Sequenz

int a = 64;
Integer myInt = new Integer(a);

erzeugt ein Integer Objekt mit Wert von a (also 64). Durch

int b = myInt.intValue();

wird der int-Variablen b der Wert des Objektes myInt zugewiesen, auf den mit der Methode intValue()
zugegriffen wird. Mit

Integer yourInt=Integer.valueOf(4);
int c = myInt.intValue() + yourInt.intValue();

wird der int-Variablen c die Summe von myInt und yourInt zugewiesen. Da dies in Berechnun-
gen sehr umständlich ist, wird ab Java 1.5 das Einpacken (boxing) und Auspacken (unboxing) von
Wrapper-Klassen automatisch durch den Compiler vorgenommen. Man nennt dieses Verhalten auto-
boxing. Vereinfacht kann also

int d = myInt + yourInt;


34 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

geschrieben werden, um die Summe der beiden Integer-Variablen zu berechnen.


Zur Illustration des Unterschiedes zwischen Adresse (Referenz) und Wert betrachte man folgende
Deklaration:

TextField input, output;


String str = new String("Hallo!");

Hierfür werden im Speicher Plätze für drei Referenzen angelegt. Die ersten beiden werden mit null
initialisiert, während die dritte eine Referenz auf den String Hallo enthält. Dabei steht die Konstante
null für die “leere” Adresse. Im allgemeinen nehmen Referenzen natürlich weniger Speicherplatz als
die eigentlichen Daten ein. Dies wird durch die unterschiedliche Größe der Speicherplätze angedeutet.
Die Referenz von str auf die eigentlichen Daten (den Wert) wird durch den Pfeil dargestellt.

input null
output null

str Hallo!

Durch

input = new TextField(str, 10);


output = new TextField("Wie geht’s?", 10);
str = "Gut!";

werden zwei neue TextField-Objekte mit Adressen input und output erzeugt, die die Werte
Hallo! bzw. Wie geht’s? haben. Ferner wird der Variablen str der neue Wert Gut! zugewiesen.
Es ergibt sich folgendes Speicherbild:

input Hallo!
output Wie geht’s?

str Gut!

Durch die Zuweisung input = output wird input die Adresse von output (nicht der Wert!) zuge-
ordnet. Danach werden unter input und output dasselbe TextField-Objekt mit Wert Wie geht’s?
angesprochen. Das Objekt mit Wert Hallo! kann jedoch nicht mehr (mit input) angesprochen wer-
den. 1 Es entsteht folgendes Speicherbild:

input Hallo!
output Wie geht’s?

str Gut!
1 Das Objekt ist “herrenlos”, da keine Referenz mehr darauf verweist. Es wird automatisch vom Garbage Collector

gelöscht.
3.2. AUSDRÜCKE 35

Um in input denselben Wert wie in output abzuspeichern, muss man die Methoden getText() und
setText() verwenden:

input.setText(output.getText());

Den Unterschied zwischen Referenz und Wert zeigt auch folgendes Codefragment.

Integer intA = new Integer(10);


Integer intB = new Integer(10);
String compare = (intA == intB) + " " + intA.equals(intB);

Zunächst werden zwei verschiedene Integer Objekte intA und intB mit dem selben Wert 10 erzeugt.
Der Boolesche Ausdruck (intA == intB) vergleicht die Referenzen von intA und intB, während
intA.equals(intB) mittels der Methode equals() der Klasse Integer die Werte von intA und
intB vergleicht. Der String compare bekommt also den Wert false true zugewiesen. 2

3.2 Ausdrücke

Ein Ausdruck ist grob gesprochen eine Formel oder Rechenregel, die stets einen Wert (Resultat) spe-
zifiziert. Der Ausdruck besteht aus Operatoren und Operanden.
Beispiele für Operanden sind Konstanten, Variablen, Funktionen; Beispiele für Operatoren sind die
arithmetischen Operatoren + - * / und die logischen Operatoren ! && || (nicht, und, oder). Ope-
ratoren sind immer in Zusammenhang mit zugehörigen Wertebereichen (im Programmiersprachen
Jargon: Datentypen oder Typen) zu sehen, z. B. ganze Zahlen oder Gleitkommazahlen. Sie werden
daher in Zusammenhang mit Datentypen in Kapitel 4 noch eingehender diskutiert.
Beispiele für Ausdrücke in Java sind:

a > b logischer Ausdruck


a * b / c != c + d * e logischer Ausdruck
(a + b) * 3 / 2 arithmetischer Ausdruck

Haben a,. . .,e die Werte 1, 2, . . . , 6, so liefern die Ausdrücke die Werte false, true, bzw. 4.
Der Vorrang von Operatoren ist im Zweifelsfall durch Klammern zu regeln. Bei gleichwertigen Ope-
ratoren erfolgt die Auswertung von links nach rechts.

3.2.1 Ausdrücke, genaue Erklärung

In Java unterscheidet man wie in C lvalues und rvalues. Beide sind Ausdrücke, jedoch können lvalues
in Zuweisungen nur links vom Zuweisungszeichen = stehen. Genauer: lvalues bezeichnen alles, was
2 Eine einzige Ausnahme von der Unterscheidung zwischen Adresse und Wert gibt es bei Strings, vgl. Abschnitt 4.3.
36 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

Inhalt einer Speicheradresse ist (also insbesondere Variablen, während rvalues allgemein den Wert
eines Ausdrucks bezeichnet.
Ein Ausdruck hat ganz allgemein 3 Eigenschaften:

• Einen Wert oder Rückgabewert, der sich durch vollständige Auswertung des Ausdrucks ergibt.

• Einen Typ, nämlich den Typ seines Wertes. Bei Funktionsaufrufen ist dies der Typ des Rückga-
bewertes (Rückgabetyp).

• Einen Effekt. Darunter versteht man einen Effekt auf Speicherinhalte. Ergibt sich dieser nicht
aus der Zuweisung eines Wertes an einen lvalue, so nennt man dies einen Seiteneffekt.3 Sei-
teneffekte entstehen vor allem bei Funktionsaufrufen (Änderung von Parametern oder globalen
Variablen), aber auch bei vielen Operatoren.

3.2.2 Ausdrücke und einfache Anweisungen

Eine wichtige Regel in der C-Programmierung (und damit auch in Java) ist: Ein Ausdruck wird zu
einer (einfachen) Anweisung durch Anfügen eines Semikolons. In diesem Fall wird der Rückgabewert
unterdrückt, und Zuweisungen an lvalues und ggf. Seiteneffekte sind die einzigen Effekte.
Weitere einfache Anweisungen sind: Aufruf einer void-Funktion oder eine Definition, jeweils abge-
schlossen durch ein Semikolon. Die leere Anweisung besteht nur aus einem Semikolon.

3.2.3 Beispiele

1. a = b
Dies ist ein Zuweisungsausdruck mit dem Zuweisungsoperator =. Dabei wird dem lvalue a (der
einen Speicherplatz bezeichnet) der Wert des rvalue b zugewiesen. Es wird also ein Speicherin-
halt verändert; dies zählt aber nicht als Seiteneffekt, da es eine Zuweisung an einen lvalue ist.
Der Wert des Ausdrucks ist der Wert, der der linken Seite zugewiesen wird. Der Typ ist der
Typ der linken Seite.
Die einfache Anweisung
a = b;
weist also a den Wert von b zu. Der Wert des Ausdrucks a = b, also b, wird unterdrückt.

2. c += b = a
+= ist ein Zuweisungsoperator mit folgender Bedeutung: x += y ist äquivalent zu x = x +
y. In dem Ausdruck c += b = a ist c der lvalue und b = a der rvalue. Die Auswertung des
rvalue weist b (als Seiteneffekt!) den Wert von a zu. Der Wert des rvalues ist der Wert von a.
Zu diesem wird der Wert von c addiert. (wegen des += Operators) und die Summe dem lvalue
c zugewiesen. Ein Beispiel mit konkreten Werten:
3 Manchmal wird auch die Zuweisung an einen lvalue als Seiteneffekt bezeichnet.
3.3. DEFINITIONEN UND DEKLARATIONEN 37

a) vorher a 1
b 5
c 2

b) nachher a 1
b 1 ← Seiteneffekt!
c 3
Solche Seiteneffekte sind zwar möglich, aber sollten tunlichst vermieden werden, da sie zu
unlesbaren Programmen führen. Die Zuweisungen
b = a; c += b;
leisten das Selbe und sind klarer.

3. Zuweisungen in logischen Ausdrücken


Ein deutliches Beispiel für die Eigenschaften von Ausdrücken ist
if ((a = b) > c) c = a;
für int Variablen a,b,c. Die Zuweisung a = b liefert einen Wert (nämlich den von b). Ist die-
ser größer als der Wert von c, so wird c = a ausgeführt. Durch die Auswertung des Ausdrucks
a = b wird als Seiteneffekt a der Wert von b zugewiesen.

4. Der Inkrementoperator ++ (entsprechend --)


Er kann als Präfix (++a) oder Postfix (a++) auf einen arithmetischen Ausdruck a angewendet
werden. Ist a ein lvalue, so wird der Wert von a um 1 erhöht. Der Wert von a++ ist der Wert
von a; der Wert von ++a ist der Wert von a plus 1. Also:

a++; bedeutet: erst benutzen, dann erhöhen


++a; bedeutet: erst erhöhen, dann benutzen

Ein Beispiel mit Werten a = 2 und b = 4:

a = b++; weist a den Wert 4 zu und erhöht als Seiteneffekt den Wert von b auf 5.
a = ++b; weist a den Wert 5 zu und erhöht als Seiteneffekt den Wert von b auf 5.
a++; weist a den Wert von 3 zu, dies ist kein Seiteneffekt, da a ein lvalue ist.
++a; weist a den Wert von 3 zu, dies ist kein Seiteneffekt, da a ein lvalue ist.

3.3 Definitionen und Deklarationen

In Java unterscheidet man zwischen Deklaration und Definition. Deklarationen führen Identifier ein
und assoziieren mit ihnen Typen, so dass der Compiler aufgrund dieser Deklaration die Typver-
träglichkeit überprüfen kann (type checking). In Java erfolgt dabei automatisch eine Default-Initia-
lisierung.
38 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

Definitionen sind Deklarationen, die zugleich (bei Variablen und Konstanten) den Identifiern Spei-
cherplatz zuordnen oder (bei Funktionen) den Rumpf der Funktion aufführen. Jeder Identifier muss
deklariert sein, bevor er benutzt werden kann. Beispiele sind:

int a; deklariert die Integer Variable a.


double x = 3.14; definiert die Gleitkomma Variable x und
initialisiert sie zu 3.14.
int Square(int x) definiert die Funktion f (x) = x2 . In den {...}
{return x*x;} Klammern steht der Funktionsrumpf.
int Square(int x); deklariert eine Funktion Square. Dem Compiler
sind dadurch Name, Rückgabetyp und
Argumenttyp bekannt.

Weitere Beispiele werden in Kapitel 6.1.3 diskutiert.

3.4 Strukturierte Anweisungen

Neben den einfachen Anweisungen gibt es die strukturierten Anweisungen:

• zusammengesetzte Anweisung (Verbundanweisung)

• bedingte Anweisung

• wiederholende Anweisung

und daraus abgeleitete Anweisungen (z. B. die selektive Anweisung). Sie werden mit den nachfol-
gend beschriebenen Kontrollstrukturen gebildet, die in allen höheren Programmiersprachen existieren.
Sie heißen Kontrollstrukturen, da mit ihnen kontrolliert“ wird, wie Programme intern in Abhängig-

keit von Bedingungen gesteuert werden, d. h., je nach Bedingung unterschiedliche Anweisungen
ausführen, teilweise auch wiederholt. Man sagt auch, die Kontrollstrukturen legen den Programm-

fluss“ fest. Neben den Kontrollstrukturen bilden Methoden und Klassen weitere wichtige Bausteine
zum Steuern und Strukturieren von Programmen. Auf sie wird ausführlich in Kapitel 6 eingegangen.
Für die Kontrollstrukturen werden wir neben einer umgangsprachlichen Beschreibung und einem Bei-
spiel drei programmiersprachenunabhängige Darstellungen angeben, und zwar als:

• Pseudocode,

• Struktogramm,

• Flussdiagramm.

Danach wird auf die entsprechende Realisierung der Kontrollstrukturen in Java eingegangen.
Struktogramme (auch Nassi-Shneiderman-flowcharts genannt) und Flussdiagramme sind graphische
Beschreibungen von Algorithmen unter Verwendung der genannten Kontrollstrukturen.
3.4. STRUKTURIERTE ANWEISUNGEN 39

Pseudocode leistet das gleiche in einer an Pascal orientierten, mit normalem Text durchsetzten Be-
schreibung. Die folgende Tabelle stellt die Pseudocode Äquivalente bereits eingeführter Java Kon-
strukte zusammen.

Pseudocode Java Bedeutung


:= = Zuweisung
= == Test auf Gleichheit
6= != Test auf Ungleichheit
not ! logisches nicht
and && logisches und
or || logisches oder
{. . .} /*...*/ bzw. //... Kommentare

3.4.1 Zusammengesetzte Anweisung, Verkettung

Die zusammengesetzte Anweisung (compound statement) besteht in der Hintereinanderschaltung (Ver-


kettung) von Anweisungen (einfachen oder strukturierten) M1 , . . . , Mt , die in der gegebenen Reihen-
folge sequentiell abgearbeitet werden.

Beispiel 3.1 (Hypothekberechnung) Es ist der monatliche Betrag a zu berechnen, der zur Ablösung
einer Annuitätshypothek in Höhe von b e bei einer Laufzeit von n Jahren und jährlicher Verzinsung
zum Zinssatz von z % anfällt.

Dazu stellen wir folgende Überlegung an. Der Betrag b wächst in n Jahren bei der angegebenen
Verzinsung auf b · (1 + z)n , falls keine Rückzahlungen erfolgen.
Sehen wir uns an, wie sich die monatlichen Zahlungen verzinsen. Im ersten Jahr wird 12a “angespart”.
Dieser Betrag wird allerdings erst ab dem zweiten Jahr verzinst.4 Er wächst also nach n Jahren auf
12a(1 + z)n−1 e. Entsprechend ergeben die Zahlungen im zweiten Jahr am Ende 12a(1 + z)n−2 e,
usw. Insgesamt ist der Wert aller Zahlungen nach n Jahren auf

12a · (1 + z)n−1 + 12a · (1 + z)n−2 + . . . + 12a

angewachsen.
Die monatlichen Zahlungen von a müssen, wenn sie zum selben Zinssatz verzinst werden, nach n
Jahren auf den selben Betrag von b · (1 + z)n führen. Durch Gleichsetzung beider Beträge lässt sich a
berechnen. Betrachten wir nun die zeitliche Entwicklung des Darlehens so wie der Zahlungen:

4 Dies ist eine gerichtlich umstrittene Praxis vieler Banken. An sich müssten die Zahlungen bereits—wie bei

Sparguthaben—für Teile eines Jahres verzinst werden. Die entsprechende Rechnung bleibt als Übung überlassen.
40 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

Jahr: Zahlungen
0: b
1: b(1 + z) − 12a
2: (b(1 + z) − 12a)(1 + z) − 12a = b(1 + z)2 − (12a(1 + z) + 12a)
3: b(1 + z)3 − (12a(1 + z)2 + 12a(1 + z) + 12a)
..
.
n: b(1 + z)n − (12a(1 + z)n−1 + 12a(1 + z)n−2 + . . . + 12a(1 + z) + 12a)
| {z } | {z }
Wert der Schulden Wert der aller Einzahlungen

Nach n Jahren muss der Wert der Schulden dem Wert aller Einzahlungen entsprechen. Die Gleichset-
zung der Beträge ergibt dann:

b · (1 + z)n = 12a · (1 + z)n−1 + 12a · (1 + z)n−2 + . . . + 12a


= 12a[(1 + z)n−1 + . . . + (1 + z)1 + (1 + z)0 ]
(1 + z)n − 1
= 12a
(1 + z) − 1
(1 + z)n − 1
= 12a
z

Hieraus folgt
b (1 + z)n · z
a= ·
12 (1 + z)n − 1

Dies resultiert in folgenden Algorithmus zur Berechnung von a.

Algorithmus 3.1 (Hypothek Abtrag)


M1 Lese b ein (in e)
M2 Lese z ein (in %)
M3 Lese n ein (in Jahren)
M4 Berechne r := 1 + z
M5 Berechne R := rn
M6 Berechne a := (b/12)(R · z)/(R − 1)
M7 Gebe a aus

Die Verkettung erlaubt keine Verzweigung. Sie hat daher (ohne Verwendung weiterer Kontrollstruktu-
ren) nur einen beschränkten Anwendungsbereich. Viele sogenannte “programmierbare” Taschenrech-
ner der ersten Generation waren nur so programmierbar.
Der Pseudocode für die Verkettung lautet:

begin
M1 ;
3.4. STRUKTURIERTE ANWEISUNGEN 41

M2 ;
..
.
Mt ;
end

Das Struktogramm für die Verkettung ist in Abbildung 3.1 angegeben, das Flussdiagramm in Ab-
bildung 3.2.

M1
M2
..
.
Mt

Abbildung 3.1: Struktogramm der Verkettung.

−→ M1 −→ M2 −→ . . .−→ Mt −→

Abbildung 3.2: Flussdiagramm der Verkettung.

In Java werden zusammengesetzte Anweisungen durch geschweifte Klammern dargestellt:

{
M1 ;
M2 ;
..
.
Mt ;
}

Ein Beispiel für ein Java Programm mit nur einer zusammengesetzten Anweisung ist Programm
2.1 (Temperatur.java). Hier folgt ein weiteres für die Hypothekberechnung. Im Unterschied zum
Temperatur-Programm wird hier ein ActionListener an jedes Feld angebunden.

Programm 3.1 Hypothek.java


// Hypothek.java
//
// calculates monthly mortgage rate
import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
42 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

public class Hypothek extends Applet {

double amount, // total amount


ratePerMonth, // monthly rate
interestRate, // interest rate in %
interestFactor, // 1 + interest rate
power; // interestFactor^period
int period; // number of years

Label amountPrompt,
interestRatePrompt,
periodPrompt,
startPrompt; // declare Labels
TextField amountField,
interestRateField,
periodField; // declare textfields for input

// setup the graphical user interface components


// and initialize labels and text fields
public void init() {
setLayout(new FlowLayout(FlowLayout.LEFT));
setFont(new Font("Times", Font.PLAIN, 14));

amountPrompt = new Label("Geben sie den Gesamtbetrag "


+ "des Darlehens in EURO an:");
amountField = new TextField(10);
amountField.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
calculateMortgage();
}
});

interestRatePrompt = new Label("Geben Sie den Zinssatz "


+ "in % an:");
interestRateField = new TextField(10);
interestRateField.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
calculateMortgage();
}
});

periodPrompt = new Label("Geben Sie die Laufzeit "


+ "in Jahren an:");
periodField = new TextField(10);
3.4. STRUKTURIERTE ANWEISUNGEN 43

periodField.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
calculateMortgage();
}
});

startPrompt = new Label("Dr"ucken Sie Return "


+ "zum Start der Berechnung.");

add(amountPrompt);
add(amountField);
add(interestRatePrompt);
add(interestRateField);
add(periodPrompt);
add(periodField);
add(startPrompt);
}

// display the result as graphics


public void paint(Graphics g) {
g.drawString("Ihre monatliche Rate betraegt EURO "
+ ratePerMonth, 15, 150);
g.drawString("f"ur EURO " + amount + " bei " + period
+ " Jahren und " + interestRate
+ " % Zinsen.", 15, 170);
}

// process user’s action on the input text fields


public void calculateMortgage() {
// get input numbers
amount = Double.parseDouble(amountField.getText());
interestRate = Double.parseDouble(
interestRateField.getText());
period = Integer.parseInt(periodField.getText());

interestFactor = (100 + interestRate) / 100;


power = Math.pow(interestFactor, period);
ratePerMonth = (amount / 12) * power
* (interestFactor - 1)/(power - 1);

// round to Cent
ratePerMonth = (Math.round(ratePerMonth * 100)) / 100.0;
44 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

repaint(); // calls method paint for the whole applet


}
}

Hier wird die Funktion pow() aus der Klasse Math für die Potenzierung verwendet. pow(a,b) be-
rechnet den Wert ab und gibt ihn als Rückgabewert zurück.
Um die Berechnung in jedem Eingabefeld auslösen zu können, wird an jedes Eingabefeld ein Action-
Listener hinzugefügt, der in allen Fällen dieselbe Methode calculateMortgage() aufruft.
Ferner nutzen wir die Graphikmöglichkeiten von Java zur Ausgabe. Die Anweisungen repaint()
und init() veranlassen das Applet, sich selbst als Graphikobjekt zu sehen und die Methode paint()
für sich selbst aufzurufen. In ihr wird die Methode drawString(str, x, y) verwendet, die den
String str an der Position (x,y) zeichnet. Dabei sind x,y int-Variablen, die die Anzahl der Pixel
von der linken oberen Ecke der Appletfläche angeben (nach rechts bzgl. x und nach unten bzgl. y. Das
fertige Applet ist in Abbildung 3.3 dargestellt.

Abbildung 3.3: Das Hypothek Applet.

3.4.2 Selektion, Bedingte Anweisung

Die Selektion ermöglicht das Ansteuern einer Alternative in Abhängigkeit von Daten. Der Prototyp
der Selektion ist die if-Anweisung.

Die if-Anweisung

Der Pseudocode für die if-Anweisung lautet:

if B
then S1
else S2
3.4. STRUKTURIERTE ANWEISUNGEN 45

Dabei ist B ein Boolescher Ausdruck und sind S1 , S2 beliebige Anweisungen (insb. wieder strukturierte
Anweisungen). Der Prozessor berechnet den Wahrheitswert (true bzw. false) von B und steuert in
Abhängigkeit davon S1 bzw. S2 an. Der else-Teil darf fehlen.

Beispiel 3.2 (Betrag) Der Betrag |a − b| der Differenz a − b zweier reeller Zahlen a, b ist zu berech-
nen.
Eine Lösung in Pseudocode lautet:

if a > b
then berechne a − b
else berechne b − a

Das Struktogramm der if-Anweisung ist in Abbildung 3.4 dargestellt, das Flussdiagramm in Ab-
bildung 3.5.

B B
true false true

S1 S2 S1

Abbildung 3.4: Struktogramm der if-Anweisung.

+ − −
B B
+

S1 S2 S1

Abbildung 3.5: Flussdiagramm der if-Anweisung.

In Java wird die if-Anweisung folgendermaßen gebildet:

if (B) S1 else S2

Auch hier darf der else-Teil fehlen. Die Codekonvention schlägt vor, if und else Teile immer mit
{...} zu klammern und dabei folgendes Einrückmuster zu verwenden:
46 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

if (B1 ) {
Anweisungen
} else if (B2 ) {
Anweisungen
} else {
Anweisungen
}

Die selektive Anweisung

Sollen mehr als die zwei Fälle true und false unterschieden werden, so ist dies mit der selektiven
Anweisung oder Selektion möglich.
Diese lässt sich als “geschachtelte” Variante der if-Anweisung auffassen. Sind etwa c1 , . . . cn die Wer-
te, die der Ausdruck B annehmen kann, und soll beim Wert ci die Anweisung Si ausgeführt werden,
so lässt sich die entsprechende Selektion wie folgt in Pseudocode realisieren:

if B = c1
then S1
else if B = c2
then S2
else if . . .
..
.
else if B = cn
then Sn

Hierfür existiert in Pseudocode die folgende Kurzform:

case B of
c1 : S1 ;
c2 : S2 ;
..
.
cn : Sn ;
end

In Java existiert dafür die switch-Anweisung:

switch (expression) {
case i: {
Anweisungen
}
case j: {
Anweisungen
3.4. STRUKTURIERTE ANWEISUNGEN 47

}
default: {
Anweisungen
}
}

Das zugehörige Struktogramm ist in Abbildung 3.6 dargestellt, das Flussdiagramm in Abbildung 3.7.

c1

c2 B

S1
S2 cn
...

Sn

Abbildung 3.6: Struktogramm der Selektion.

c1 c2 cn

S1 S2 ... Sn

Abbildung 3.7: Flussdiagramm der Selektion.

Bei ineinandergeschachtelten if-Anweisungen kann es wegen fehlender else Teile zu Unklarheiten


bei der Zuordnung der else zu den if kommen. Falls dies nicht durch Klammerung mit {...}
geregelt wird, bezieht sich ein “hängendes” else immer auf das letzte if, auf das eine Zuordnung
möglich ist. Bei

if (Bedingung1 ) Anweisung1
48 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

if (Bedingung2 ) Anweisung2
else Anweisung3

bezieht sich das else also auf das zweite if.

3.4.3 Wiederholung

Die Wiederholung ermöglicht die wiederholte Durchführung einer Anweisung (meist mit veränderten
Werten von Variablen). Die Häufigkeit der Wiederholung wird dabei durch eine Boolesche Bedingung
kontrolliert. Der Prototyp der Selektion ist die while-Anweisung.

Die while-Anweisung

Der Pseudocode für die while-Anweisung lautet:

while B do S

Dabei ist B ein Boolescher Ausdruck und ist S eine beliebige Anweisung (insb. wieder eine struk-
turierte Anweisung). Der Prozessor berechnet den Wahrheitswert (true bzw. false) von B vor jeder
Ausführung von S. Ist B = true so wird S ausgeführt, sonst nicht.
Falls der Wahrheitswert von B immer true bleibt, so wird S stets wieder ausgeführt. Man gerät dann
in eine sog. Endlosschleife. Sie stellt bei Anfängern einen häufig gemachten Programmierfehler dar.
Das Struktogramm der while-Anweisung ist in Abbildung 3.8 dargestellt, das Flussdiagramm in
Abbildung 3.9.

Abbildung 3.8: Struktogramm der while-Anweisung.

In Java wird die while-Anweisung wie folgt gebildet:

while ( B ) S

Die Codekonvention schlägt folgende einrückende Schreibweise vor:

while (B) {
Anweisungen S
}
3.4. STRUKTURIERTE ANWEISUNGEN 49


B
+

Abbildung 3.9: Flussdiagramm der while-Anweisung.

Beispiel 3.3 (Größter gemeinsamer Teiler) Für zwei positive natürliche Zahlen x, y ist der größte
gemeinsame Teiler ggT (x, y) zu berechnen.

Der größte gemeinsame Teiler von zwei positiven natürlichen Zahlen x und y ist die größte natürliche
Zahl, die x und y teilt. So ist ggT (12, 16) = 4 und ggT (12, 17) = 1.
Zur Berechnung des ggT nutzen wir folgende mathematische Aussage:

Lemma 3.1 Sind a, b ∈ N und ist a > b > 0, so ist


ggT (a, b) = ggT (a − b, b).

Beweis: Zeige:

1. Ist c ein Teiler von a und b, so auch von a − b und b.


2. Ist c ein Teiler von a − b und b, so auch von a und b.

Aus 1. und 2. folgt: a, b und a − b, b haben dieselben Teiler, und damit auch denselben größten ge-
meinsamen Teiler.
zu 1: Sei c ein Teiler von a und b. Dann existieren k1 , k2 ∈ N mit k1 · c = a und k2 · c = b. Hieraus folgt:
a − b = k1 · c − k2 · c = (k1 − k2 ) · c
Da a > b, ist k1 − k2 > 0. Also ist a − b ein Vielfaches von c und somit c ein Teiler von a − b. Da c
nach Annahme auch ein Teiler von b ist, ist c ein Teiler von a − b und b. Also gilt 1.
zu 2: Sei c ein Teiler von a − b und b. Dann existieren `1 , `2 ∈ N mit `1 · c = a − b und `2 · c = b.
Hieraus folgt:
a = (a − b) + b = `1 · c + `2 · c = (`1 + `2 ) · c
Also ist c ein Teiler von a. Da c nach Annahme auch ein Teiler von b ist, ist c ein Teiler von a und b.
Also gilt 2.

Dies führt zu folgendem Algorithmus zur Berechnung von ggT (x, y) (in Pseudocode).
50 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

Algorithmus 3.2 (Größter gemeinsamer Teiler)


a := x; b := y;
while a 6= b do
if a > b then a := a − b
else b := b − a;
{ggT (a, b) = ggT (x, y)}
{ggT (x, y) = a = b}
ggT (x, y) := a;

Wir überlegen uns zunächst, dass dieser Algorithmus korrekt arbeitet.

Satz 3.1 Algorithmus 3.2 berechnet für zwei beliebige positive natürliche Zahlen x, y ihren größten
gemeinsamen Teiler.

Beweis: Lemma 3.1 garantiert, dass am Ende der if-Anweisung bei jedem Durchlauf der while-
Schleife die Bedingung ggT (a, b) = ggT (x, y) gilt.
Da a oder b in jedem Durchlauf der while-Schleife um mindestens 1 kleiner wird und a und b positiv
bleiben, kann die while-Schleife höchstens max{x, y} mal durchlaufen werden. Also terminiert die
while-Schleife.
Beim Austritt aus der while-Schleife gilt a = b, also ggT (a, b) = a = b. Zusammen mit der oben ge-
zeigten Gleichheit ggT (a, b) = ggT (x, y) folgt die Behauptung.

Ein Zahlenbeispiel: Für x = 28, y = 12 ergeben sich folgende Werte für a und b.

a: 28 → 16 → 4 → 4 → 4
b: 12 → 12 → 12→ 8 → 4

Nach dem 4. Schleifendurchlauf ist a = b = 4, und der Algorithmus terminiert mit ggT (28, 12) = 4.
Ein entsprechendes Java Programm lautet:

Programm 3.2 GGT.java


// GGT.java
//
// calculates greatest common divisor gcd(x,y)
// of natural numbers x and y
import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class GGT extends Applet {


3.4. STRUKTURIERTE ANWEISUNGEN 51

int x, y, a, b; // natural numbers

Label xPrompt,
yPrompt,
startPrompt; // declare Labels
String message; // declare String for Result
TextField xField,
yField; // declare textfields for input

// setup the graphical user interface components


// and initialize labels and text fields
public void init() {
setLayout(new FlowLayout(FlowLayout.LEFT));
setFont(new Font("Times", Font.PLAIN, 14));
setSize(380, 150);

xPrompt = new Label("Geben sie eine nat"urliche Zahl "


+ "x > 0 ein:");
xField = new TextField("24", 10);
xField.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
compute_gcd();
}
});

yPrompt = new Label("Geben Sie eine nat"urliche "


+ "Zahl y > 0 ein:");
yField = new TextField("36", 10);
yField.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
compute_gcd();
}
});

startPrompt = new Label("Dr"ucken Sie Return zum "


+ "Start der Berechnung.");

add(xPrompt);
add(xField);
add(yPrompt);
add(yField);
add(startPrompt);
52 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

message = "Der ggT von 24 und 36 ist 12.";


}

// display the result as graphics


public void paint(Graphics g) {
g.drawString(message, 15, 130);
}

// process user’s action on the input text fields


public void compute_gcd() {
// get input numbers
x = Integer.parseInt(xField.getText());
y = Integer.parseInt(yField.getText());

if (x <= 0 || y <= 0) {
message = "Bitte nur positive ganze Zahlen "
+ "eingeben!";
} else {
a = x;
b = y;
while (a != b) {
if (a > b) {
a = a - b;
} else {
b = b - a;
}
// gcd(x,y) == a
message = "Der ggT von " + x + " und " + y
+ " ist " + a + ".";
}
}
repaint();
}
}

Man beachte, dass unzulässige Eingaben von ganzen Zahlen (0 oder negative Zahlen) durch den Be-
nutzer im Programm abgefangen werden. Dies ist notwendig, da das Programm sonst in eine Endlos-
schleife gerät. Das fertige Applet ist in Abbildung 3.10 dargestellt.
Der vorgestellte Algorithmus zur Berechnung des größten gemeinsamen Teiler lässt sich noch weiter
beschleunigen wie im folgenden Korollar aus Lemma 3.1 deutlich wird.
3.4. STRUKTURIERTE ANWEISUNGEN 53

Abbildung 3.10: Das GGT Applet.

Korollar 3.1 Seien a, b ∈ N und ist a > b > 0, so gilt

ggT (a, b) = ggT (a mod b, b).

Beweis: Da a > b, lässt sich a eindeutig zur Basis b darstellen, als a = (a ÷ b)b + a mod b, wobei ÷
die ganzzahlige Division bezeichnet. Definiere nun c := a ÷ b. Es ist offensichtlich, dass c ∈ N. Falls
a mod b = 0, so teilt b a und die Behauptung folgt direkt. Andernfalls, für a mod b 6= 0 gilt a − (c −
1)b > b und a − cb < b. Nun definieren wir die Folge (ai )ci=0 durch ai := a − ib, ∀i ∈ {0, 1, . . . , c},
sodass a0 = a gilt. Somit folgt, dass für alle i ∈ {0, 1, . . . c−1} die ai echt größer als b sind. Wir können
daher Lemma 3.1 auf a und b anwenden und erhalten ggT (a, b) = ggT (a − b, b) = ggT (a1 , b). Hier-
auf können wir wieder das Lemma 3.1 anwenden, bis schließlich ggT (ac−1 , b) = ggT (ac , b) Nun gilt,
dass ac < b und wir können das Lemma nicht mehr anwenden. Zusammenfassend folgt also, dass
ggT (a, b) = ggT (ac , b) = ggT (a − cb, b) = ggT (a mod b, b) und somit die Behauptung.

Daher ergibt sich der folgende Pseudocode zur Berechnung von ggT (x, y):

Algorithmus 3.3 (Größter gemeinsamer Teiler)


a := x; b := y;
while a mod b 6= 0 do
if a > b then a := a mod b
else b := b mod a;
{ggT (x, y) = ggT (a, b)}
{ggT (x, y) = a = b}
ggT (x, y) := a;

Varianten der while-Anweisung

Bei der while-Anweisung wird S nie ausgeführt, wenn B bereits zu Anfang den Wert false hat. Oft
möchte man jedoch die Anweisung S mindestens einmal (unabhängig von B) ausführen. Dies ist mit
54 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

der while-Anweisung wie folgt möglich:

S;
while B do S

Da dies häufig vorkommt, gibt es hierfür die repeat-Anweisung als eigene Kontrollstruktur.

repeat S while B

oder, äquivalent dazu,

repeat S until not B

Der Prozessor führt erst S aus, dann wird B geprüft (post checking im Gegensatz zu pre checking bei
der while-Anweisung). Falls B noch erfüllt ist, so wird S erneut ausgeführt.
Das Struktogramm der repeat-Anweisung ist in Abbildung 3.11 dargestellt, das Flussdiagramm in
Abbildung 3.12.

not B

Abbildung 3.11: Struktogramm der repeat-Anweisung.

+
B

Abbildung 3.12: Flussdiagramm der repeat-Anweisung

In Java gibt es hierfür die do-while-Anweisung:

do
S
while ( B );
3.4. STRUKTURIERTE ANWEISUNGEN 55

Laut Codekonvention soll sie wie folgt geschrieben werden:

do {
Anweisungen S
} while (B);

Manchmal muss man eine feste Anzahl von Wiederholungen (Iterationen) von S machen (z. B. n mal).
Dies kann realisiert werden durch die Mitführung einer Kontrollvariablen z, die nicht in S vorkommt.
Diese wird üblicherweise als Zähler bezeichnet.

z := n;
while z > 0 do
z := z − 1;
S

Kurzform:

repeat S n times

Das Struktogramm hierfür ist in Abbildung 3.13 dargestellt.

n times

Abbildung 3.13: Struktogramm der n-maligen Wiederholung.

In Java gibt es dafür das for-statement als eigene Kontrollstruktur, siehe Übung.

3.4.4 Mächtigkeit von Kontrollstrukturen

Mit den eingeführten Kontrollstrukturen Verkettung, Selektion und Wiederholung kann alles berech-
net werden, was im intuitiven Sinne berechenbar ist. Der Nachweis hiervon wird in der Theorie der
Berechenbarkeit geführt.
Tatsächlich lässt sich dies bereits mit weniger Kontrollstrukturen erreichen, z. B. reichen die Verket-
tung und die while-Schleife bereits aus. (Übung: hierdurch ist die if-Anweisung simulierbar). Verket-
tung und Iteration (n-malige Wiederholung) reichen jedoch nicht aus. Man braucht die Möglichkeit,
beliebige Boolesche Ausdrücke B als Abbruchbedingung zu wählen.
Die gleiche Mächtigkeit erreicht man alternativ auch mit der Verkettung, der Selektion und der goto-
Anweisung. Die goto-Anweisung ermöglicht es, zu beliebigen Stellen im Programm zu “springen”.
56 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

In älteren Programmiersprachen (Basic, Fortran) und in Assembler gehören goto-Anweisungen zum


Standard, in moderneren Sprachen sind sie verpönt, da die Programme durch sie meist unstrukturiert
werden und schwer zu verstehen und zu überprüfen sind.
In Java gibt es kein goto Statement, jedoch sind Sprünge partiell mit den abgeschwächten“ goto

Anweisungen break und continue erlaubt, bei denen auch Sprungstellen definiert werden können.
Wir erläutern hier nur zwei spezielle Situationen um aus geschachtelten if- oder while-Anweisungen
heraus zu springen. Hierbei sind keine selbst definierten Sprungstellen erforderlich.
Zum Verlassen von Schleifen “in der Mitte” gibt es die Anweisungen break und continue. Die
while Anweisung wird dann zu

while (true) {
...
if (!Fortsetzungsbedingung ) break;
...
}

bzw. zu

while ( Fortsetzungsbedingung ){
...
if (!Bedingung ) continue;
weitere Anweisungen
}

Im ersten Fall wird die while-Schleife ganz verlassen, im zweiten Fall werden die “weiteren Anwei-
sungen” übersprungen und es erfolgt der nächste Durchlauf der while-Schleife.
break kann auch in der if- bzw. switch-Anweisung auftreten, vgl. Übung.
Zum Abschluss geben wir in den Abbildungen 3.14 und 3.15 die verbesserte Version des Algorithmus
zur Berechnung der nächst größeren Primzahl zu einer gegebenen Zahl (vgl. Kapitel 2.3) in Form von
Struktogrammen an.
3.4. STRUKTURIERTE ANWEISUNGEN 57

Lese Zahl n ein


Durchlaufe alle ungeraden Zahlen z > n der Reihe nach
k kein Teiler von z und k · k ≤ z
Teste alle ungeraden Zahlen k ab k = 3 bis k · k ≤ z
ob sie Teiler von z sind
kein Teiler von z gefunden
Gebe z aus

Abbildung 3.14: Struktogramm des Primzahl-Algorithmus (Grobversion).

Lese Zahl n ein

n gerade
true false
z := n − 1 z := n
z := z + 2
k := 3
divisorFound := false
(divisorFound = false) and (k · k ≤ z)
z modulo k = 0
true false
divisorFound := true k := k + 2
divisorFound = false
Gebe z aus

Abbildung 3.15: Struktogramm des Primzahl-Algorithmus (Feinversion).


58 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN

3.5 Literaturhinweise
Kontrollstrukturen werden in nahezu allen Büchern über Programmiersprachen behandelt. Geschichtliche Hin-
weise zur Entstehung der Kontrollstrukturen und Vergleiche zwischen verschiedenen Programmierstilen und
Sprachen finden sich in [Hor95].
Bzgl. Java verweisen wir auf [CW98, DD04, Küh99].
Die Behandlung von Kontrollstrukturen, Ausdrücken, Typechecking usw. durch Compiler ist ausführlich in
[ALSU06] dargestellt.
Eine Einführung in die Theorie der Formalen Sprachen ist in [Wii87] enthalten. Tiefer geht [HMU06].
Kapitel 4

Methoden und Klassen:


Einführung und Beispiele

4.1 Datentypen und Operationen

Die Syntax einer algorithmischen Sprache beschreibt die formalen Regeln, mit denen ein Algorithmus
formuliert werden kann. Sie erklärt jedoch nicht die Bedeutung der Daten und Operationen, die in
einem in einer bestimmten algorithmischen Sprache geschriebenen Algorithmus zulässig sind. Dies
ist ein Problem der Semantik.
Für Daten eines vorgegebenen Typs ergibt sich die Semantik aus den möglichen Werten und den
zugelassenen Operationen auf diesen Werten. Beide zusammen bilden einen Typ oder Datentyp.
Definition: Ein Datentyp (kurz Typ) besteht aus

• dem Wertebereich (domain) des Typs

• einer Menge von Operationen (Methoden) auf diesen Werten

Jede Programmiersprache verfügt über eingebaute elementare (Standard-) Datentypen. Andere müssen
als sogenannte abstrakte oder selbst definierte Datentypen mit den Ausdrucksmitteln der Program-
miersprache definiert werden.

Elementare Datentypen

Java stellt elementare Datentypen mit zugehörigen Operationen bereit für

• ganze Zahlen: int, long, short, byte.

• Gleitkommazahlen: float, double.


Version vom 17. Juni 2010

59
60 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

• Zeichen: char.

• Wahrheitswerte: boolean

Beispiel 4.1 (Der Java Typ int)


Wertebereich: Die endliche Teilmenge

{Nmin , Nmin + 1, Nmin + 2, . . . , 0, 1, 2, . . . , Nmax }

der ganzen Zahlen, wobei Nmin = −231 und Nmax = 231 − 1 ist.
Operationen: = Zuweisung
+ Addition
- Subtraktion
* Multiplikation
/ Ganzzahlige Division
% Rest bei ganzzahliger Division
== Test auf Gleichheit, Ergebnis ist vom Typ boolean
!= Test auf Ungleichheit, Ergebnis ist vom Typ boolean
und viele andere mehr (vgl. Übung).

Abstrakte Datentypen

Beispiel 4.2 (Ein selbstdefinierter Typ Fraction)


Wertebereich: Alle Brüche der Form r = p/q wobei p und q int Werte sind und q > 0 ist.
Operationen:
Fraction(a,b) Erzeuge den Bruch r aus gegebenen int Zahlen a,b
toString() Schreibe den Bruch r in der Form “p/q”
doubleValue() Berechne den entsprechenden double Wert
simplify() Reduziere r auf die Form p/q, so dass p und q teilerfremd sind
getNumerator() Gebe den Zähler (numerator) zurück
getDenominator() Gebe den Nenner (denominator) zurück
multiply(s) Multipliziere Bruch mit Bruch s
equals(s) Teste, ob Bruch = s ist (2/4 = 1/2)

Der Typ Fraction ist in Java nicht vorhanden, kann aber (über Klassen, vgl. Kapitel 6.4.1) implemen-
tiert werden.
Wir werden jedoch bereits in unserem Pseudocode die Schreibweise (und Ausdrucksweise) der Java
Klassen übernehmen. toString(),. . ., equals() sind Methoden der Klasse Fraction.
Die Deklaration von Variablen vom Typ Fraction erfolgt gemäß

Fraction r, s;
4.1. DATENTYPEN UND OPERATIONEN 61

Man nennt dann r, s Instanzen (Objekte) der Klasse. Auf die zugehörigen Objektmethoden wird mit

r.toString();

usw. zugegriffen. Diese Anweisung bewirkt das Schreiben des Bruches r als String in der Form a/b.
Die Erzeugung (auch Instantiierung genannt) erfolgt wie bei Objekten üblich mit new.

Fraction r = new Fraction(3, 4);

bedeutet also die Erzeugung der Variablen (des Objektes) r vom Typ Fraction mit dem Wert 3/4.
In Java sind also folgende Anweisungen denkbar (bei Umsetzung obiger Methoden):

Fraction r = new Fraction(3, 4) // r := 3/4


Fraction s = new Fraction(4, 8); // x := 4/8
s.simplify(); // s = 1/2
r.multiply(s); // r := r · s = 34 · 21 = 38
if(r.equals(s)) ... // Test auf Gleichheit

Beispiel 4.3 (Skatkarte)


Wertebereich:={Karo 7, . . . , Karo Ass, . . . , Kreuz 7, . . . , Kreuz Ass}, d. h. 32 Werte, die die Spielkar-
ten im Skat Spiel darstellen.
Funktionen:
farbe() Gibt die Farbe (Kreuz, Pik, Herz oder Karo) einer Karte an
wert() Gibt den Wert (7,. . .,10, Bube, Dame, König oder Ass)
einer Karte an
Skatkarte() Konstruktor, zieht eine zufällige Karte
Skatkarte(f,w) Konstruktor, erzeugt eine Karte mit Farbe f und Wert w

Dann weist die Sequenz

Skatkarte karte = new Skatkarte();


w = karte.wert();

der Variablen w den Wert einer zufälligen Skatkarte zu.


Auch dieser Typ ist in Java nicht vorhanden, könnte aber ähnlich wie der Typ Fraction als Klasse
implementiert werden.
Wichtig ist die Unterscheidung zwischen (abstrakten) Datentypen und Implementationen (z. B. in
Java) solcher Datentypen. Algorithmenentwicklung basiert nur auf abstrakten Datentypen. Die Um-
setzung abstrakter Datentypen in eine Implementation erfolgt entweder erst danach, oder ist unnötig,
da bereits Implementationen existieren, die man verwenden kann (Wiederverwendbarkeit wird gerade
von Java besonders unterstützt).
Datentypen sind extrem wichtig für die Compilierung, da der Compiler dann
62 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

• beim Compilieren allen definierten Objekten den erforderlichen Speicherplatz zuweisen kann,

• die Typinformation zur Überprüfung der Zulässigkeit von Programmstatements benutzen kann
(syntaktisch und partiell auch semantisch),

• den Typ des Wertes eines Ausdrucks bereits (weitgehend) ermitteln kann, ohne den Rechen-
prozess durchführen zu müssen (zum Beispiel ist die Multiplikation einer int Zahl mit einer
double Zahl vom Typ double, siehe unten.).

Statische und dynamische Datentypen

Unter statischen Datentypen verstehen wir Datentypen, die bereits zur Kompilierzeit bekannt sind.
Dies setzt voraus, dass der Datentyp jedes Identifiers im Programm deklariert bzw. definiert wird. Es
gibt jedoch auch dynamische Datentypen (sog. Generics), die erst zur Laufzeit festgelegt werden und
dem Benutzer somit größere Freiheiten bei der Programmierung einräumen.
In Java sind Arrays und Records Beispiele für statische Datentypen, während Listen, Queues und
Stacks dynamisch implementiert werden können.Wir betrachten nun ein Beispiel für einen selbst-
definierten (abstrakten) und statischen Datentypen, den wir anschließend dynamisch (generisch) ma-
chen werden.

Beispiel 4.4 (Tupel) Wir konstruieren zunächst einen statischen Datentyp, der es ermöglicht Tupel
von int-Werten zu speichern.

public class IntPair{


private int first;
private int second;

public IntPair(int first, int second){


this.first = first;
this.second = second;
}

//more methods
}

Falls wir nun einen Datentyp schreiben möchten, der ein Tupel mit double-Werten darstellt, so
müssen wir den bereits vorhandenen Datentyp zwar nur geringfügig verändern, aber den kompletten
Code nochmal schreiben. Dies können wir umgehen, indem wir einen dynamischen Datentyp eines
Tupels erstellen, dessen Typen erst zur Laufzeit festgelegt werden.

public class Pair<T, U>{


private T first;
private U second;
4.1. DATENTYPEN UND OPERATIONEN 63

public Pair(T first, U second){


this.first = first;
this.second = second;
}

//more methods
}

Durch public class Pair<T, U> definieren wir die beiden generischen Typen T, U, die erst bei
der Erzeugung neuer Objekte festgelegt werden. Es gilt jedoch zu beachten, dass T und U Referenzty-
pen sein müssen. Außerdem darf beim Konstruktor nicht der Zusatz <T,U> stehen.
Das Tupel (3, 3.14) kann nun folgendermaßen erzeugt werden:

Pair p = new Pair<Integer, Double>(3, 3.14);

Es sei nochmals ausdrücklich darauf hingewiesen, dass hier die Wrapperklassen Integer, Double
verwendet werden müssen, da int, double elementare Datentypen sind!

Casting

Bei der Typüberprüfung (type checking) unterscheidet man zwischen strikter Typüberprüfung wie
in Pascal und nicht strikter Typüberprüfung wie in Java. Bei strikter Typüberprüfung ist das “Mischen”
von Typen stark eingeschränkt. In Java sind Typumwandlungen nur explizit durch das sogenannte
casting oder type casting möglich. Besonders wichtig wird die Typumwandlung im Zusammenhang
mit der Vererbung bei Klassen, vgl. Abschnitt 6.3.3.
Ist intVar eine int Variable und floatVar eine float Variable, so bedeutet:

intVar = (int) floatVar;

eine Zuweisung mit expliziter Typumwandlung.


Ein Beispiel liefert die Division. Da / bei ganzen Zahlen die ganzzahlige Division bezeichnet, hat
5/8 den ganzzahligen Wert 0 und 5.0/8 den gebrochenen Wert 0.625. Statt 5.0/8 kann man auch
(double) 5 / (double) 8 schreiben, was vor allem für Ausdrücke wie (double) x / (double)
y wichtig ist, in denen x, y ganzzahlige Werte annehmen, man aber die reelle Division meint.
Bei offensichtlichen Operationen wie

floatVar = floatVar * intVar;

findet eine implizite Typumwandlung (float) intVar statt.


In Pseudocode werden Typvereinbarungen in der Form
64 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

TypName identifier;

geschrieben.

4.2 Strukturierte Datentypen (Datenstrukturen)

Die bisherigen Beispiele waren Beispiele für einfache (unstrukturierte oder primitive) Datentypen.
Neben einfachen Datentypen gibt es sogenannte zusammengesetzte oder strukturierte Datentypen. Sie
setzen sich aus bereits eingeführten Datentypen gemäß bestimmter Strukturierungsmerkmale zusam-
men. Stehen die Strukturierungsmerkmale im Vordergrund (und nicht der Typ der “Grunddaten”), so
redet man von Datenstrukturen.
Strukturierte Typen oder Datenstrukturen haben (neben Wertebereich und Operationen)

• Komponenten-Daten, die atomar oder wieder strukturiert sein können


• Regeln, die das Zusammenwirken der Komponenten zur gesamten Struktur definieren.

Programmiersprachen haben i. a. nur wenige Datenstrukturen als eingebaute Typen (alle haben z. B.
Arrays). Die meisten muss der Programmierer selber implementieren, wobei ihm Java mächtige Kon-
struktionsmöglichkeiten (vor allem Klassen und Vererbung) bereitstellt.
Wir behandeln zunächst die wichtigsten Datenstrukturen aus abstrakter Sicht. Die Implementation in
Java wird erst jeweils dann erfolgen, wenn die nötigen Konstruktionsmöglichkeiten besprochen sind.
Abbildung 4.1 gibt eine hierarchische Übersicht über einige der wichtigsten Datenstrukturen.

Datenstrukturen

Linear Nichtlinear

Direkter Zugriff Sequentieller Zugriff Set Enum

Homogene Heterogene Last-In First-In


Allgemein
Komponenten Komponenten First-out First-Out

Array Record Liste Stack Queue

Abbildung 4.1: Klassifikation einiger wichtiger Datenstrukturen.

Eine lineare Datenstruktur hat (bei mindestens 2 Komponenten) eine Ordnung auf den Komponenten
mit folgenden Eigenschaften:
4.3. STRINGS 65

• Es gibt eine eindeutige erste Komponente.


• Es gibt eine eindeutige letzte Komponente.
• Jede Komponente (außer der ersten) hat einen eindeutigen Vorgänger.
• Jede Komponente (außer der letzten) hat einen eindeutigen Nachfolger.

Direkter Zugriff (auch random access genannt) bedeutet, dass man auf jede beliebige Komponente
zugreifen kann, ohne vorher auf andere Komponenten zugreifen zu müssen. Beispiele sind:

– Ein Regalbrett mit Büchern; auf jedes Buch kann direkt zugegriffen werden.
– CDs mit direkter Ansteuerung von Musikstücken.

Sequentieller Zugriff bedeutet, dass man auf die i-te Komponente nur zugreifen kann, nachdem man
vorher auf die Komponenten 1, 2, . . . , i − 1 zugegriffen hat. Beispiele sind:

– Ein Stapel von Büchern; um das i-te zu nehmen, müssen erst die i − 1 obersten entfernt werden.
– Musikstücke auf einem Tonband.

Bevor wir uns den in der Abbildung 4.1 genannten strukturierten Datentypen zuwenden, werden wir
zuvor auf die Datenstruktur String eingehen, die in vielen Programmiersprachen eine besondere Rolle
einnimmt.

4.3 Strings

Strings sind Zeichenketten. Sie sind extrem wichtig für die EDV, werden aber sehr unterschiedlich in
den verschiedenen Programmiersprachen behandelt.
Kennzeichen der Datenstruktur String sind:

• variable Komponentenzahl,
• Komponenten sind homogen vom Typ char,
• direkter Zugriff auf Komponenten,
• typische Stringoperationen wie:
– Verkettung,
– Finden von Substrings,
– Vergleich bezüglich lexikographischer Ordnung,
– Einfügen/Lesen von Zeichen an bestimmter Stelle.

Der Wertebereich von Strings ist die Menge der Zeichenketten aus char Zeichen (einschließlich der
leeren Zeichenkette).
66 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

4.3.1 Strings in Java

In Java gibt es drei Klassen für Strings, String, StringBuilder und StringBuffer.
Die Klasse String hat als Objekte Zeichenketten, die sich nach der Erzeugung nicht ändern. Sie ist
speziell optimiert für die Verwaltung konstanter Zeichenketten.
Die Klassen StringBuffer und StringBuilder haben als Objekte Zeichenketten, die sich im Ver-
lauf des Programms ändern können (kürzer werden, wachsen, Teilstrings ändern, . . .). Beispielsweise
werden Operationen aus dieser Klasse zur Implementation des “+” Operators der Klasse String ver-
wendet.
Im Gegensatz zu C oder C++ sind Strings in Java keine Arrays von char.

char[] string = { ’H’, ’a’, ’l’, ’l’, ’o’ };

ist nicht dasselbe wie

String str = "Hallo";

Allerdings gibt es in der Syntax viele Zugeständnisse an C-Programmierer, zum Beispiel die Zuwei-
sung

String str = "Hallo!";

die gleichzeitig neben der Java-konformen Erzeugung mit new existiert: 1

String str = new String("Hallo!");

Es gibt viele Methoden für Strings, die für alle gängigen Stringoperationen ausreichen. Man schaue
sich dafür die Klassen String, StringBuilder und StringBuffer in der Java-Dokumentation an.
Als Beispiel führen wir drei Konstruktoren der Klasse String auf.

public String(); // konstruiert den leeren String


public String(String value); // konstruiert einen String mit Wert value
public String(char[] value); // wandelt ein char-Array in einen String um.
1 Tatächlich
gibt es zwischen beiden Arten der Erzeugung einen diffizilen Unterschied. Zwei mit new erzeugte Strings
str1, str2 mit gleichem Wert Hallo! haben (wie bei Referenztypen zu erwarten) unterschiedliche Adressen. Dagegen
ergeben Zuweisungen
String str1 = "Hallo!";
...
String str2 = "Hallo!";
desselben Strings an verschiedene Variablen auch dieselbe Adresse, da der Compiler überprüft, ob es die Stringkonstante
Hallo! bereits gibt und sie dann wiederverwendet. Wenn man also stets diese Art Erzeugung (ohne new) verwendet, lässt
sich Gleichheit von Strings auch mit (str1 == str2) überprüfen (weiteres Zugeständnis an C-Programmierer), während
allgemein bei Objekten mit == nur Gleichheit der Adressen geprüft wird. Gleichheit der Werte muss man mit der Methode
equals() überprüfen, also str1.equals(str2).
4.3. STRINGS 67

4.3.2 Manipulation von Strings: ein erster Ansatz

Als Beispiel für den Umgang mit Arrays und Strings betrachten wir zwei Java Programme für das
Einlesen einer Folge von Zahlen aus einer TextArea in ein Array. Das erste Programm macht sehr
spezielle Annahmen über die gegebene Zahlenfolge, und fängt noch keine Fehler ab, die aus der
Verletzung dieser Annahmen resultieren. Das zweite Programm beinhaltet ein Exception Handling
für den Umgang mit Fehlern.
Gegeben ist in beiden Fällen eine Folge von int-Zahlen n, a0 , a1 , . . . , am , die durch die sogenannten
white space, d. h. ein oder mehrere Leerzeichen (blanks), Tabulatoren und Zeilenumbrüche getrennt
sind, etwa

4 10 20 30
40 50

Die erste Zahl gibt die Länge des Arrays an, in das die Zahlen a0 , a1 , . . . , an−1 aus der Folge eingelesen
werden sollen. Daher muss m ≥ n − 1 sein. Ferner wird vorausgesetzt, dass außer int-Zahlen und
white space keine anderen Strings in der TextArea stehen.
Im ersten Programm StringDemo wird zusätzlich angenommen, dass nach jeder Zahl ein Leerzei-
chen ’’ steht. Dieses Leerzeichen wird als Trennsymbol verwendet, um das Ende einer int-Zahl zu
erkennen.
StringDemo verwendet Methoden der Klassen String und StringBuffer, u. a.

split(regex) // Teilt den String an den Stellen regex in einzelne Strings


// und gibt diese in einem String-array zurück
aus der Klasse String, und

append(str) // Hängt den String str an das StringBuffer-Objekt an


toString() // Konvertiert ein StringBuffer-Objekt in ein String-Objekt

aus der Klasse StringBuffer.

Programm 4.1 StringDemo.java


// StringDemo.java
//
// Demonstrates use of the classes Strings and StringBuffer
// together with arrays
//
// A string of integer numbers in which each number is terminated by a blank
// is converted into an array of integers
//
// Assumptions:
// input is a sequence of numbers n, a0, a1, a2 ...
68 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

// n specifies the length of an array


// a0, a1, ... are the array entries (must be at least n)
// numbers are terminated by a blank with possibly more white space
// between them (return, tab or newline)
//
// Take care! You will get exceptions if these assumptions are violated.

import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class StringDemo extends Applet {

TextArea input, output;


Button startBtn;

int[] vec;

// setup the graphical user interface components


public void init() {
//set layout
setLayout(new FlowLayout(FlowLayout.LEFT));
// set font
setFont(new Font("Times", Font.PLAIN, 24));

input = new TextArea("4 10 20 30 40 ", 5, 30);


add(input); // put input on applet

startBtn = new Button("Start");


startBtn.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
runDemo();
}
});
add(startBtn);

output = new TextArea(10, 30);


output.setEditable(false);
add(output);
}

// process user’s action on the input text area


public void runDemo() {
4.3. STRINGS 69

String inputStr = input.getText();


// divide the inputStr in different Strings separated by a whitespace
String[] args = inputStr.split(" ");
// read in the number of numbers in the TextField
int n = Integer.parseInt(args[0]);
// define the int-array of length n
vec = new int[n];

// write the n numbers into the array


for (int i = 0; i < n; i++) {
vec[i] = Integer.parseInt(args[i+1]);
}

// Construct the output string in a StringBuffer object


StringBuffer outputBuf = new StringBuffer();
for (int i = 0; i < vec.length; i++) {
outputBuf.append(i + ": "
+ Integer.toString(vec[i]) + "\n");
}
// show string str in output
output.setText(outputBuf.toString());

}
}

4.3.3 Manipulation von Strings mit Exception Handling

Um mögliche Fehler abzufangen, verwenden wir das Exception-Handling mit try und catch mit
folgender Syntax (Darstellung gemäß Codekonvention):2

try {
statements
} catch(Exceptionklasse1 Exceptionvariable1 ) {
Statements für Exceptionbehandlung
...
} catch(Exceptionklassek Exceptionvariablek ) {
Statements für Exceptionbehandlung
}
Falls nötig, kann ein finally Block angehängt werden.

finally {
weitere Anweisungen für Exceptionbehandlung
2 Zur vollständigen Syntax siehe die Java Dokumentation.
70 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

Tritt bei Abarbeitung des try Blocks eine Exception auf, so wird in den zugehörigen catch Block
gesprungen und dieser durchgeführt. Anschließend wird der optionale finally Block durchgeführt
und danach der umgebende Programmtext weiter abgearbeitet.
Um Exceptions mit try und catch abfangen zu können, muss man in selbst geschriebenen Metho-
den auch dafür sorgen, dass Exceptions erzeugt werden. Dies geschieht mit der throw Anweisung
im Rumpf der Methode. Im Kopf der Methode wird zusätzlich durch throws ExceptionKlasse dem
Compiler kenntlich gemacht, dass die Methode Ausnahmen erzeugen kann, die in anderen Programm-
teilen mit try und catch abgefangen werden können. Als Beispiel betrachten wir die Berechnung
des größten gemeinsamen Teilers als Methode:

int ggT(int a, int b) throws IllegalArgumentException {


if ((a <= 0) || (b <= 0)) {
throw new IllegalArgumentException(
"No negative numbers allowed.");
}
// insert code for computing the ggT

Hierin ist IllegalArgumentException eine in Java bereits vorhandene Klasse, aus der in der new-
Anweisung new IllegalArgumentException("No negative numbers") ein Konstruktor ver-
wendet wird, dem man Strings als Argumente übergeben kann. Diese Strings können in einer catch-
Anweisung entsprechend für Fehlermeldungen verwendet werden.
Wird die throw-Anweisung ausgeführt, so wird die Methode danach beendet und der Rest des Rump-
fes nicht ausgeführt.
In unserem Beispiel für Strings können drei Typen von Exceptions auftreten, die ebenfalls in Java als
Klassen vorhanden sind:

NumberFormatException Der gelesene String stellt keine int-Zahl dar


NoSuchElementException Das erste oder nächste Token existiert nicht
NegativeArraySizeException Die erste gelesene Zahl ist negativ

Sie werden entsprechend abgefangen und in Mitteilungen an den Benutzer umgesetzt. Um den Typ
der Exception zu ermitteln, reicht es, den entsprechenden Fehler beim Ablauf zu erzeugen und die
Meldung der Laufzeitumgebung auf dem Bildschirm zu studieren.3

Programm 4.2 StringExceptionHandlingDemo.java


// StringExceptionHandlingDemo.java
//
// Demonstrates use of exception handling
//
3 Unter Unix/Linux die Shell, in der der Appletviewer gestartet wurde.
4.3. STRINGS 71

// A string of integer numbers that are separated by white space


// is converted into an array of integers
//
// Assumptions:
// input is a sequence of numbers n, a0, a1, a2 ...
// n specifies the length of an array
// a0, a1, ... are the array entries, there must be at least n
// numbers are separated by white space
//
// now we do exception handling

import java.awt.*;
import java.applet.Applet;
import java.util.*; // for class StringTokenizer
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class StringExceptionHandlingDemo extends Applet {

TextArea input, output;


Button startBtn;

int[] vec;

// setup the graphical user interface components


public void init() {
//set layout
setLayout(new FlowLayout(FlowLayout.LEFT));
//set Font
setFont(new Font("Times", Font.PLAIN, 24));

input = new TextArea("4 10 20 30 40 ", 5, 30);


add(input); // put input on applet

startBtn = new Button("Start");


startBtn.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
runDemo();
}
});
add(startBtn);

output = new TextArea(10, 30);


add(output);
72 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

// process user’s action on the input text field


public void runDemo() {
output.setText("");
String inputStr = input.getText();
// divide the inputStr in different Strings separated by a whitespace
String[] args = inputStr.split(" ");

try {
// read in the number of numbers in the TextField
int n = Integer.parseInt(args[0]);
// define the int-array of length n
vec = new int[n];
// throws NegativeArraySizeException
// if str is a negative int

// write the n numbers into the array


for (int i = 0; i < n; i++) {
vec[i] = Integer.parseInt(args[i+1]);
// throws IndexOutOfBoundsException
// if n>args.length
// throws NumberFormatException
// if str is not an int
}

// Construct the output string in a StringBuffer object


StringBuffer outputBuf = new StringBuffer();
for (int i = 0; i < vec.length; i++) {
outputBuf.append(i + ": "
+ Integer.toString(vec[i]) + "\n");
}
output.setText(outputBuf.toString());

} catch (IndexOutOfBoundsException e) {
output.setText("Zu wenige Zahlen eingegeben!.");

} catch (NumberFormatException e) {
output.setText("Bitte nur ganze Zahlen eingeben.");

} catch (NegativeArraySizeException e) {
output.setText("Die erste Zahl muss "
+ "eine positive ganze Zahl sein.");
}
4.4. ARRAYS 73

}
}

4.4 Arrays

Das Array ist die verbreitetste Datenstruktur; in einigen Programmiersprachen (Fortran, Basic, Algol
60) sogar die einzige.
Kennzeichen der Datenstruktur Array sind:

• feste Komponentenzahl (die in Java erst zur Laufzeit festgelegt werden muss),

• direkter Zugriff auf Komponenten mittels Indizes,

• homogener Grundtyp,

• Indizes können berechnet werden.

Ist k die Anzahl der Komponenten und A der Wertebereich des Grundtyps, so ist der Wertebereich X
des Arrays das kartesische Produkt A × . . . × A (k-fach).
Die k Komponenten haben in der Regel ganze Zahlen (meist 0, 1, . . . , k − 1) als Index. Mathematisch
entspricht also X den Vektoren der Länge k mit Komponenten aus A, d. h.

X = {(a0 , a1 , . . . , ak−1 ) | ai ∈ A, i = 0, . . . , k − 1}

Zu den Operationen auf Arrays gehören (abstrakt formuliert):

value(a, i) Ermittelt den Wert der Komponente eines Arrays a mit Index i, also den
Wert der (i + 1)-ten Komponente. Ist a = (a0 , . . . , ak−1 ), so liefert
value(a, i) den Wert ai .
store(i, v) Weist der Komponente von a mit Index i den Wert v zu. Danach ist ai = v.
a := b Zuweisung von Arrays. Danach gilt ai = bi , i = 0, . . . , k − 1.
a=b Test auf Gleichheit. Liefert den Wert true genau dann, wenn ai = bi für
i = 0, . . . , k − 1.

4.4.1 Arrays in Java

In Java existiert bereits ein eingebauter Array Typ als Referenztyp. Durch die Anweisung
int[] x = new int[10]; (auch int x[] = ...)
wird ein Array mit 10 Komponenten mit Komponententyp int definiert, das unter dem Namen x
ansprechbar ist. Die Komponenten haben die Indizes 0,1,...,9. In Java sind 0,1,...,n-1 die
einzig möglichen Indizes eines Arrays mit n Komponenten.
74 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

Arraytypen in Java sind Referenztypen. Arrayvariable (x in obiger Anweisung) verweisen daher auf
Array-Objekte, die wie üblich mit new erzeugt werden. Diese Objekte verfügen über ein Feld length,
das die Anzahl der Komponenten angibt. Hierauf wird (wie immer bei Objekten) mit dem . zugegrif-
fen, also x.length.4
Der Operation value(a,i) entspricht in Java der Feldzugriff gemäß der Syntax
<Feldzugriff >::=<Referenzausdruck>[<Ausdruck>]
Dabei wird der Referenzausdruck zuerst ausgewertet und liefert die Referenz (Adresse) des Arrays.
Der Ausdruck in den Klammern [. . .] muss einen int Wert liefern, der den Index der Komponente
berechnet. Zur Laufzeit wird in Java automatisch überprüft, ob der berechnete Index wirklich im
vereinbarten Bereich des Arrays liegt. Ist dies nicht der Fall, so erfolgt eine Exception-Meldung durch
die Laufzeitumgebung (ArrayIndexOutOfBoundsException).
Durch
a = x[3];
wird also der Variablen a der Wert der 4-ten Komponente von x zugewiesen. Entsprechend wird durch
x[i] = v;
der (i + 1)-ten Komponente von x der Wert von v zugewiesen.
Bei der Deklaration von Arrays kann eine Initialisierung ohne Benutzung von new vorgenommen
werden, wie in
int[] x = { 1, 2, 3, 4 };
Zuweisungen von Werten können über for Schleifen realisiert werden. So initialisiert

int n = 10;
int[] x = new int[n];
int[] y;
for (int i = 0; i < x.length; i++) x[i] = i*i;

das Array x mit den Werten

0 1 4 9 16 25 36 49 64 81
0 1 2 3 4 5 6 7 8 9

y ist zunächst undefiniert. Die Zuweisung y = x ist zwar zulässig, weist aber nur y die Adresse von x
zu. Beide Variablen zeigen dann auf dasselbe Array. Will man in y eine Kopie anlegen, so muss man
dies entweder selber realisieren oder spezielle Methoden wie clone() verwenden.
Bei dem dem int Array x lässt sich einfach eine Kopie mit einer for-Schleife herstellen:

y = new int[n];
for (int i = 0; i < n; i++) y[i] = x[i];
4 Aber nicht x.length(), da length keine Methode ist.
4.4. ARRAYS 75

Diese einfache Art geht jedoch nicht mehr bei Objekten als Grundtyp, da durch die Zuweisung y[i]
= x[i] nur die Referenzen zugewiesen würden und nicht die Werte. Hier hilft die Methode clone(),
über die die meisten Referenztypen (darunter Arraytypen) verfügen. 5 Ist etwa

String[] stringArr = {"Hallo!", "Wie gehts?", "Gut."};

ein Array aus drei Strings, so lässt sich mit der Anweisung

String[] copyOfstringArr = (String[]) stringArr.clone();

eine echte“ Kopie von stringArr herstellen. Die Anweisung stringArr.clone() kopiert den

vom Array stringArr belegten Speicherinhalt in ein allgemeines Objekt, das mit der Castanwei-
sung (String[]) auf den richtigen Typ (Array von Strings) gecastet werden muss. Jetzt sprechen
stringArr und copyOfstringArr verschiedene Speicherbereiche an und können unabhängig von-
einander geändert und verwendet werden.
Als kompliziertes Beispiel für den Komponentenzugriff gemäß der Regel
<Feldzugriff >::=<Referenzausdruck>[<Ausdruck>]
betrachten wir das Fragment

int[] x = {1, 2, 3, 4},


y = {5, 6, 7, 8},
z;
y[x [0]] = 9; // ergibt y[1] == 9
(z = y)[(y = x)[0] + y[1]] = 0;

In der letzten Zeile wird zunächst der Referenzausdruck (z = y) ausgewertet. Er ist ein Zuweisungs-
ausdruck, der den Wert von y, also die Adresse von y ergibt, wobei diese als Seiteneffekt z zugewiesen
wird. Hiernach wird unter z also das Array y angesprochen!
In der Auswertung von [<Ausdruck>] geschieht Ähnliches mit dem Referenzausdruck y = x. Da-
her wird (y = x)[0] zu x[0], also zu 1 ausgewertet, und der gesamte Ausdruck in [...] zu 1 +
x[1] = 1 + 2 = 3. Als Seiteneffekt zeigt y jetzt auf x.
Die gesamte Zeile bewirkt also eine Zuweisung an z[3], so dass y und z (einschließlich der Änderung
von y und der beiden Seiteneffekte) am Ende die Werte

y 1 2 3 4 z 5 9 7 0
0 1 2 3 0 1 2 3

haben.
5 Genauer: alle Typen, die das Interface Clonable implementieren, vgl. Abschnitt 6.3.7.
76 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

4.4.2 Mehrdimensionale Arrays

Der Komponententyp eines Arrays kann natürlich wieder ein strukturierter Typ sein. Insbesondere
sind so Arrays von Arrays möglich. So deklariert

double[][] table = new double[3][2];

ein Array table mit 5 Komponenten, wobei jede Komponente wiederum ein Array mit 10 Kompo-
nenten vom Grundtyp double ist.6
table ist ein Beispiel eines 2-dimensionalen Arrays. Man stellt es sich am besten in Form eines
Speicherbildes vor:

table[0]
table table[1]
table[2]

table[i] greift dann auf die (i + 1) Komponente von table zu (also das Array table[i]), und
table[i][j] auf die ( j + 1)-te Komponente des Arrays table[i].
Ein zweidimensionales Array kann aus Arrays unterschiedlicher Länge bestehen. So erzeugt

int[][] table = new int[5][]; // 5 Zeilen


for (int i = 0; i < table.length; i++) {
int[] tmp = new int[i+1];
for (int j = 0; j < tmp.length; j++)
tmp[j] = i+j; // initialisiere tmp[j]
table[i] = tmp;
}

das Array

table[0] 0
table[1] 1 2
table table[2] 2 3 4
table[3] 3 4 5 6
table[4] 4 5 6 7 8
0 1 2 3 4

Dies ist möglich, da Arraytypen Referenztypen sind. Der Komponententyp des Arrays table ist der
Referenztyp int[], und dieser kann Referenzen auf int-Arrays unterschiedlicher Länge haben.
Das Array im Beispiel ließe sich auch durch direkte Initialisierung erzeugen:
6 Bei der Stellung der Klammern [][] sind auch double[] table[] bzw. double table[][] zulässig.
4.5. RECORDS 77

int[][] table = { { 0 },
{ 1, 2 },
{ 2, 3, 4 },
{ 3, 4, 5, 6 },
{ 4, 5, 6, 7, 8 }
};

Als abschließendes Beispiel dieser Einführung von Arrays betrachten wir das Umdrehen eines Arrays.

Beispiel 4.5 (Umdrehen eines Arrays) Ein 1-dimensionales Array mit n int Komponenten soll um-
gedreht werden.

4 1 3 2 −→ 2 3 1 4

int[] vector = new int[n];


// Initialisierung der Komponenten
int temp; // Hilfsvariable
int limit = vector.length/2; // obere Grenze in der for Schleife
for (int i = 0; i < limit; i++){
temp = vector[i];
vector[i] = vector[n-1-i]; // Zugriff auf Komponente n-1-i
vector[n-1-i] = temp; // Zuweisung an Komponente n-1-i
}

Hier wird in vector[n-1-i] der Index n-1-i berechnet und dann auf die entsprechende Kompo-
nente von vector zugegriffen bzw. ihr etwas zugewiesen.
Eine andere Implementation (unter voller Ausnutzung der Möglichkeiten von for-Schleifen) ist:

for (int i = -1, j = vector.length; ++i < --j;){


temp = vector[i];
vector[i] = vector[j];
vector[j] = temp;
}

4.5 Records

Kennzeichen der Datenstruktur Record sind:

• feste Komponentenzahl,

• direkter Zugriff auf Komponenten mittels Namen,

• heterogene Komponententypen, dafür keine Berechnung von Indizes.


78 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

Die Komponenten von Records heißen auch Felder, oder Datenfelder. Record Typen werden in der
Pseudosprache wie folgt deklariert

type StudentRec = record


String name;
String adresse;
Integer matrikelnr;
String fach;
end record

Selektion von Komponenten findet mit dem Punkt (.) statt.


StudentRec student;
student.matrikelnr := 127538;
In Java gibt es keine eigene Datenstruktur Record, sie kann jedoch als Spezialfall einer Klasse aufge-
fasst werden. Klassen enthalten zusätzlich zu den Datenfeldern Konstruktoren und Methoden (Ope-
rationen), die auf den Datenfeldern operieren. Details über Klassen werden in Kapitel 6.3 behandelt.
Eine rudimentäre Klasse für Studenten könnte wie folgt aussehen:

public class Student {


public String name;
public String adresse;
public int matrikelnr;
public String fach;
public int fachsemester;
// Konstruktor fuer Neueinschreibung
public Student(String aktName, String aktAdr, int aktMatrNr,
String aktFach) {
name = aktName;
adresse = aktAdr;
matrikelnr = aktMatrNr;
fach = aktFach;
fachsemester = 1;
}
public void changeAddress(String newAdr){
adresse = newAdr;
}
// weitere Konstruktoren und Methoden
}

Eine typische Verwenung der Klasse Student ist ein Array, dessen Komponenten Studenten sind, etwa
alle Studenten der Coma:

Student[] comaTeilnehmer = new Student[200];


comaTeilnehmer[27] = new Student("Hans Meier", "unbekannt", 20307, "TWM");
4.6. LISTEN 79

4.6 Listen

Eine Liste ist eine lineare Datenstruktur. Ihre Komponenten werden Items oder Listenelemente ge-
nannt. Das erste Element heißt Anfang oder Kopf (head) der Liste, das letzte Element heißt Ende
(tail). Kennzeichen der Datenstruktur Liste sind:

• lineare Datenstruktur,

• veränderliche Länge (Listen können wachsen und schrumpfen),

• homogene Komponenten (elementar oder strukturiert),

• sequentieller Zugriff auf Komponenten durch einen (impliziten) Listenzeiger, der stets auf ein
bestimmtes Element der Liste zeigt, und nur immer ein Listenelement vor oder zurück gesetzt
werden kann7 .

Listen werden immer dann angewendet, wenn man an beliebigen Stellen noch Elemente einfügen oder
löschen möchte. Ein großer Vorteil von Listen gegenüber Arrays ist, dass Listen dynamisch impleme-
tiert sind. Ein Beispiel sind Güterwaggons eines Zuges an einer Verladestation (= Listenzeiger), die
nur einen Waggon zur Zeit beladen kann. Listen findet man auch in der Textverarbeitung. Hier sind
Zeilen Listen von Wörtern, Paragraphen Listen von Zeilen, Kapitel Listen von Paragraphen usw.
Typische Listenoperationen sind das Einfügen in eine Liste, der sequentielle Übergang zum nächs-
ten Element, das Löschen eines Elementes usw. Wir werden sie nachstehend als Java Methoden
einer generischen Klasse vom Typ T, LinkedList<T> wiedergeben, die ihrerseits auf der Klasse
ListNode<T> vom selben generischen Typ T für Listenelemente beruht. Dies greift dem später ein-
geführten Klassenkonzept von Java vor (vgl. Kapitel 6.3). Der hier gewonnene Vorteil ist, dass diese
Methoden bereits genutzt werden können, ohne ihre Implementationsdetails zu kennen.

4.6.1 Einschub: Javadoc

Wir verwenden außerdem eine weitere, wichtige Methode um Java-Code zu dokumentieren. Neben
den Kommentar-Zeichen

// alles ab hier bis Ende der Zeile ist Kommentar


/* ... */ alles zwischen /* und */ ist Kommentar

gibt es in Java die zusätzlichen Kommentar-Klammern

/** ... */

Sie wirken wie /* und */, aber dienen als Input für das Dokumentationsprogramm javadoc. Ruft
man dieses Programm mit
7 sowie auf die Stelle hinter dem letzten Element, wodurch der Zustand “end of list” gekennzeichnet wird.
80 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

javadoc *.java

auf, so erstellt es für jede .java Datei der momentanen Directory eine entsprechende .html Datei, in
der Informationen abgelegt werden, die aus den /** ... */ Kommentaren und den Deklarationen
von Klassen, Methoden, Konstruktoren, Feldern usw. gewonnen werden. Dazu müssen die Kommen-
tare den Deklarationen vorausgehen und sie sinnvoll dokumentieren. Zusätzlich erstellt javadoc in
der Datei tree.html eine Einordnung der eigenen Klassen in die Klassenhierarchie von Java (durch
die man sich dann auch per html-Browser bewegen kann), in der Datei AllNames.html einen Index
aller Klassen, Methoden, Konstruktoren, Feldern usw. aus den analysierten .java Dateien und noch
vieles, vieles mehr.
Dieses Dokumentationswerkzeug ist sehr mächtig und bietet einen guten Zugriff auf Klassen, die
Klassenhierarchie, und die Methoden einer Klasse. Die /** ... */ Kommentare können mit html-
Formatierungsbefehlen angereichert werden, wie etwa

<code> ... </code> erzeugt Schreibmaschinentext

Außerdem sind spezielle tags verwendbar um die html-Seiten entsprechend zu strukturieren, z. B.:

@see für Verweise


@author Nennung des Autors
@version Versionsnummer
@param Erläuterung der Parameter
@return Erläuterung der Rückgabe von Methoden
@exception Erläuterung von Ausnahmen

Als Beispiel schaue man sich die folgenden, entsprechend dokumentierten Dateien ListNode und
LinkedList.java an. Ausschnitte der von javadoc aus LinkedList.java erzeugten html-Datei
LinkedList.html sind in Abbildung 4.2 und Abbildung 4.3 in Browsersicht dargestellt. Weitere
Informationen über javadoc erhält man in der Javadokumentation oder mit man javadoc in den
Unix/Linux Manual Pages.

4.6.2 Eine Klasse für Listen

Programm 4.3 ListNode.java


/**
* A generic class for list nodes.
* A list node consists of a data component and a link to the next list node
*/
public class ListNode<T> {
/**
* data component is of generic type T
*/
private T _data;
4.6. LISTEN 81

Abbildung 4.2: Anfang der von javadoc erzeugten Datei LinkedList.html.

/**
* next points to the next list node
*/
private ListNode<T> _next;

/**
* Construct a list node containing a specified object
82 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

Abbildung 4.3: Methodenteil der von javadoc erzeugten Datei LinkedList.html.

* @param obj the object for the list node


*/
public ListNode(T obj) {
_data = obj;
_next = null; // leave next uninitialized
}

/**
* Return the data in this node.
4.6. LISTEN 83

* @return the data of this node.


*/
public T getData() {
return _data;
}

/**
* Set the data of the node.
* @param obj the data object for the node.
*/
public void setData(T obj) {
_data = obj;
}

/**
* Return the next node.
* @return the reference to the next node.
*/
public ListNode<T> getNext() {
return _next;
}

/**
* Set the next node.
* @param next the next node.
*/
public void setNext(ListNode<T> next) {
_next = next;
}
}

Programm 4.4 SimpleList.java


import java.util.NoSuchElementException;

/**
* The <code>SimpleList</code> class implements a dynamically growable
* list of objects. <code>SimpleList</code> administers a cursor to
* point to the active list node and another one to point to the
* node before the current node
*
* @see ListNode
*
* Each list is a collection of ListNodes along with two
* implicit list cursors in the range 1..n+1, where n is
84 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

* the current length of the list


*/
public class SimpleList<T> {

/**
* a list pointer to the first node
*/
private ListNode<T> _head;

/**
* a list pointer to the current node
*/
private ListNode<T> _curr;

/**
* a list pointer to node preceding the current node
*/
private ListNode<T> _prev;

/**
* Default Constructor, constructs an empty list.
*/
public SimpleList() {
_head = _prev = _curr = null;
}

/**
* Tests if this list has no entries.
*
* @return <code>true</code> if the list is empty; <code>false</code>
* otherwise
*/
public boolean isEmpty() {
return (_head == null);
}

/**
* Set the list cursor to the first list element.
*/
public void reset() {
_curr = _head;
_prev = null;
}
4.6. LISTEN 85

/**
* Test if the list cursor stands behind the last element of the list.
*
* @return <code>true</code> if the cursor stands behind the last
* element of the list; <code>false</code> otherwise
*/
public boolean isAtEnd() {
return (_curr == null);
}

/**
* increment the list cursor to the next list node
* Throws <code>NoSuchElementException</code> if
* <code>endOfList() == true</code>
*/
public void increment() throws NoSuchElementException {
if(isAtEnd()) {
throw new NoSuchElementException(
"No further list node.");
}
_prev = _curr;
_curr = _curr.getNext();
}

/**
* Return the value of the current node.
* Throws <code>NoSuchElementException</code> if
* there is no current node
*/
public T currentData() throws NoSuchElementException {
if (_curr == null) {
throw new NoSuchElementException(
"No current list node.");
}
return _curr.getData();
}

/**
* Inserts a new list node before the current node.
* If the list is empty insert at front.
* The cursor points to the new list node.
*
* @param someData the object to be added.
86 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

*/
public void insertBefore(T someData) {
ListNode<T> newNode = new ListNode<T>(someData);
if (isEmpty()) {
_head = _curr = newNode;
} else {
newNode.setNext(_curr);
_curr = newNode;
if (_prev != null) {
_prev.setNext(newNode);
} else {
_head = newNode;
}
}
}

/**
* Inserts a new list node after the current node. The cursor points
* to the new list node.
* Throws <code>NoSuchElementException</code> if
* there is no current node
*
* @param someData the object to be added.
*/
public void insertAfter(T someData) throws NoSuchElementException {
ListNode<T> newNode = new ListNode<T>(someData);
if (isEmpty()) {
_head = _curr = newNode;
} else {
if (_curr == null) {
throw new NoSuchElementException(
"Cursor not on a valid element.");
}
newNode.setNext(_curr.getNext());
_curr.setNext(newNode);
_prev = _curr;
_curr = newNode;
}
}

/**
* Delete the current node from the list.
* Throws <code>NoSuchElementException</code> if
* there is no current node
4.6. LISTEN 87

*/
public void delete() throws NoSuchElementException {
if (_curr == null) {
throw new NoSuchElementException(
"No element for deletion.");
}
if (_curr == _head) {
_head = _curr = _curr.getNext();
} else {
_curr = _curr.getNext();
_prev.setNext(_curr);
}
}

/**
* Returns a string representation of this list.
*
* @return a String representing this list.
*/
public String toString( ) {
StringBuilder strBui = new StringBuilder() ;

for (reset(); ! isAtEnd(); increment())


strBui.append(currentData().toString());

return strBui.toString() ;
}

Als Demonstration der Listenoperationen behandeln wir folgendes Beispiel.

Beispiel 4.6 (Einlesen eines Strings in eine Liste)


Es sollen folgende Aktionen ausgeführt werden:

– Einlesen eines Strings in eine Liste von Character,

– Ausgabe der Liste auf dem Bildschirm,

– Löschen des Anfangs der Liste bis zu einem vorgegebenen Zeichen (ergibt die leere Liste, falls
das Zeichen nicht vorkommt),

– Ausgabe der gekürzten Liste.


88 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

Dies leistet das nachstehende Java Applet. Es wird eine SimpleList<Character> erstellt und ein-
zelne char-Zeichen, die aus dem String ausgelesen werden, hinzugefügt. Diese werden automatisch
mittels Autoboxing in entsprechenden “Wrapperobjekte” der Klasse Character eingepackt.

Programm 4.5 ListDemo.java


/*
* ListDemo.java
*
* Represents strings as list of char and manipulates them
*/
import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class ListDemo extends Applet {

// setup the graphical user interface components


// and initialize labels and text fields
Label inputPrompt1, inputPrompt2;
TextField input1, input2, output1, output2;
Panel p1, p2, p3, p4, p5;

public void init() {


//set layout
setLayout(new GridLayout(5, 1));
//set Font
setFont(new Font("Times", Font.PLAIN, 24));

p1 = new Panel();
p2 = new Panel();
p3 = new Panel();
p4 = new Panel();
p5 = new Panel();

p1.setLayout(new FlowLayout(FlowLayout.LEFT));
inputPrompt1 = new Label("Schreiben Sie einen String "
+ "und beenden Sie ihn mit Return.");
p1.add(inputPrompt1);

p2.setLayout(new FlowLayout(FlowLayout.LEFT));
input1 = new TextField("Hello World", 40);
input1.addActionListener(new ActionListener(){
4.6. LISTEN 89

public void actionPerformed(ActionEvent e){


runDemo();
}
});
p2.add(input1);

p3.setLayout(new FlowLayout(FlowLayout.LEFT));
output1 = new TextField("Der String ist: Hello World", 40);
p3.add(output1);
output1.setEditable(false);

p4.setLayout(new FlowLayout(FlowLayout.LEFT));
inputPrompt2 = new Label("Geben Sie das Zeichen an, "
+ "bis zu dem geloescht wird:");
p4.add(inputPrompt2);
input2 = new TextField("W", 2);
input2.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
runDemo();
}
});
p4.add(input2);

p5.setLayout(new FlowLayout(FlowLayout.LEFT));
output2 = new TextField("Gekuerzte Liste: World", 40);
p5.add(output2);
output2.setEditable(false);

add(p1);
add(p2);
add(p3);
add(p4);
add(p5);
}

// process user’s action


public void runDemo() {
// list for representing string
SimpleList<Character> myList = new SimpleList<Character>();
// read input string
String str = input1.getText();

char ch;
90 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

// represent str as a list of Character


// use length() and charAt() methods of class String
for (int i = 0; i < str.length(); i++) {
ch = str.charAt(i); // get the char
myList.insertAfter(ch);
// insert it in the list
// used autoboxing for char -> Character
}
// use list methods to write string in first output field
if (myList.isEmpty()) {
output1.setText("Der String ist leer.");
} else {
// use toString() method of class SimpleList
output1.setText("Der String ist: "
+ myList.toString());
}

// read char until which will be deleted


ch = input2.getText().charAt(0);

if (! myList.isEmpty()) { // nothing to do otherwise


myList.reset();
while (myList.currentData() != ch) {
// used autoboxing again
myList.delete();
// leave while loop if myList is empty
if (myList.isEmpty()) break;
}
}

// write remaining string in second output field


if (myList.isEmpty()) {
output2.setText("Die gekuerzte Liste ist leer.");
} else {
// use toString() method of class SimpleList
output2.setText("Gekuerzte Liste: "
+ myList.toString());
}
}
}
4.7. STACKS 91

4.7 Stacks

Stacks sind eine eingeschränkte Form von Listen, bei denen das Einfügen und Löschen nur am Kopf
(genannt top) möglich ist. Als Liste gesehen kann der Listenzeiger also nur auf das erste Element
zeigen. Ein Beispiel ist ein Bücherstapel in einem engen Karton, man hat immer nur auf das obere
Buch Zugriff. Man nennt daher Stacks auch Last-In, First-Out oder LIFO Listen.
Wie bei Listen sehen wir uns die Stack Operationen ausgedrückt als Methoden einer Java Klasse an.8

Programm 4.6 Stack.Java


import java.util.NoSuchElementException;

/**
* The <code>SimpleStack</code> class implements a Stack
* of objects of Type T.
*
* @see ListNode
*/
public class SimpleStack<T> {
/**
* a list pointer to the first node
*/
private ListNode<T> top;

/**
* Constructs an empty stack.
*
*/
public SimpleStack() {
top = null;
}

/**
* Tests if this stack has no entries.
*
* @return <code>true</code> if the stack is empty;
* <code>false</code> otherwise
*/
public boolean isEmpty() {
return (top == null);
}

8 Es gibt in Java eine eigene Klasse Stack im Package java.util, die auf der Klasse Vector basiert. Aus didaktischen

Gründen verwenden wir hier eine eigene Klasse.


92 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

/**
* Return the value of the top node.
*/
public T top() throws NoSuchElementException {
if (top == null) {
throw new NoSuchElementException("No top node.");
}
return top.getData();
}

/**
* Inserts a new stack node at the top with <code>someData</code>
* as data
*
* @param someData the data object of the new node
*/
public void push(T someData) {
ListNode<T> newNode = new ListNode<T>(someData);
if (isEmpty()) {
top = newNode;
} else {
newNode.setNext(top);
top = newNode;
}
}

/**
* Delete the top node from the stack.
*/
public void pop() throws NoSuchElementException {
if (top == null) {
throw new NoSuchElementException("No element "
+ "for deletion.");
}
top = top.getNext();
}

/**
* Returns a string representation of this stack, top element first
*
* @return a String representing this stack.
*/
public String toString( ) {
StringBuilder strBui = new StringBuilder();
4.7. STACKS 93

// use methods from class ListNode to traverse this stack


for (ListNode<T> curr = top; curr != null; curr = curr.getNext())
strBui.append(curr.getData().toString());

return strBui.toString() ;
}
}

Diese Klasse verwaltet wieder Objekte generischen Typs T. Gegeben einen String ‘‘abc’’ können
wir beispielsweise einen Stack befüllen und wieder in einen String schreiben.

SimpleStack<Character> myStack = new SimpleStack<Character>();


for (int i = 0; i < str.length(); i++){
myStack.push(str.charAt(i));
}
StringBuffer strBuf = new StringBuffer();
while(! myStack.isEmpty()) {
strBuf.append(myStack.top());
myStack.pop();
}
String result = strBuf.toString();

Dies ergibt die folgende Folge von Belegungen der Instanz myStack:

leer a b c b a leer Top


a b a
a

Nach Abarbeitung der while-Schleife hat result den Wert cba.


Stacks sind fundamental für viele Aufgabenstellungen der Informatik, z. B.

• Laufzeitverwaltung von Funktions- und Prozeduraufrufen,

• Realisierung von Rekursion,

• Auswertung von Ausdrücken in Postfixnotation, z. B. in HP-Taschenrechnern, etwa


Eingabe: abc+*
Stackfolge: leer a b c c+b (c+b)*a Top
a b a
a
94 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

Als ein weiteres Beispiel betrachten wir korrekte Klammerausdrücke. Sie sind wichtig für die korrekte
Darstellung von arithmetischen und logischen Ausdrücken [(,) Klammern], und zur Bildung korrek-
ter Java Programme [{,} Klammern]. Der Compiler muss in der Lage sein, Klammerausdrücke auf
Korrektheit zu überprüfen und “korrespondierende” Klammerpaare zu finden, etwa:

(()()) korrekt
(() nicht korrekt
{{}{}{}}} nicht korrekt

Zunächst überlegt man sich, wie man korrekte Klammerausdrücke aus einfacheren (d. h. mit weniger
Klammern) zusammensetzen kann.
Hierzu gibt es drei wichtige Regeln. Es bezeichnen A, B im Weiteren immer korrekte Klammeraus-
drücke. Dann gilt bzgl. der Zusammensetzung von Klammerausdrücken:

R1 A = () ist korrekt

R2 Man kann korrekte Klammerausdrücke hintereinander schreiben und erhält einen neuen korrek-
ten Klammerausdruck.
A, B korrekt ⇒ AB korrekt

R3 Man kann einen korrekten Klammerausdruck A wieder klammern und erhält einen neuen kor-
rekten Klammerausdruck.
A korrekt ⇒ (A) korrekt

Beispiel 4.7 Wir wollen zeigen, dass (())() ein korrekter Klammerausdruck ist. Definiere B := (). Nach
R1 gilt, dass B korrekt ist. Mit R3 folgt, dass (()) = (B) =: A korrekt ist. Aus R2 folgt, dass AB = (())()
korrekt ist und somit die Behauptung.

Satz 4.1 Jeder korrekte Klammerausdruck mit mehr als 2 Klammern kann mit einer dieser Möglichkeiten
aus kleineren konstruiert werden.

Beweis: Sei A ein korrekter Klammerausdruck mit mehr als 2 Klammern. Dann gehört zur ersten
Klammer auf ( eine entsprechende Klammer zu ).
Ist dies die letzte Klammer in A, so ist A von der Form (B) wobei B wieder ein korrekter Klammer-
ausdruck ist (da sonst A nicht korrekt wäre).
Ist die “Klammer zu” nicht die letzte Klammer in A, so sei B der Teilausdruck vom Anfang von A
bis einschließlich dieser Klammer, und C der Rest. Dann sind B und C korrekte Klammerausdrücke
(sonst wäre A nicht korrekt).
Die Fallunterscheidung zeigt also: A = (B) oder A = BC, wobei B bzw. B,C kürzere korrekte Klam-
merausdrücke als A sind. Also trifft eine der beiden Regeln zu.
4.7. STACKS 95

Beispiel 4.8 (Erkennung von korrekten Klammerausdrücken)


Klammerausdrücke sind auf Korrektheit zu überprüfen und einander entsprechende Klammern sind
zu paaren.

So ist z. B. (()()))() nicht korrekt, aber {{}{{}}} korrekt. Die entsprechende Paarung des zweiten
Ausdrucks ist

{ { } { { } } }

Dies geschieht mit dem folgenden Algorithmus.

Algorithmus 4.1 (Erkennung von korrekten Klammerausdrücken)


1. Lese die Folge der Klammern von links nach rechts.
2. Falls “(”, so pushe diese auf den Stack.
3. Falls “)” so poppe eine “(” vom Stack, erkläre diese als zur momentan gelesenen “)”
gehörig.
4. Erkläre den Klammerausdruck als korrekt, falls der Stack am Ende leer ist,
jedoch zwischendurch nie vom leeren Stack gepoppt wird.

Als konkretes Beispiel betrachten wir (1 (2 (3 )4 (5 )6 )7 (8 )9 )10 , wobei die Klammern zur besseren
Identifizierung mit Indizes versehen sind. Dann ergibt sich in Algorithmus 4.1 die in Abbildung 4.4
dargestellte Stackfolge.

0 1 2 3 4 5 6 7 8 9 10
leer (1 (2 (3 (2 (5 (2 (1 (8 (1 leer
(1 (2 (1 (2 (1 (1
(1 (1
⇓ ⇓ ⇓ ⇓ ⇓
Paar Paar Paar Paar Paar
(3 )4 (5 )6 (2 )7 (8 )9 (1 )10

Abbildung 4.4: Ein Beispiel zu Algorithmus 4.1.

Sie zeigt, dass der Ausdruck korrekt ist mit der folgenden Klammerung:

(1 (2 (3 )4 (5 )6 )7 (8 )9 )10
96 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

Bei dem Beispiel (1 )2 )3 (4 )5 ergibt sich die Stackfolge

0 1 2 3
leer (1 leer Error

Also wird (1 )2 )3 (4 )5 als nicht-korrekter Klammerausdruck erkannt.

Satz 4.2 (Erkennung korrekter Klammerausdrücke) Algorithmus 5.1 erkennt genau die korrekten
Klammerausdrücke als korrekt und identifiziert zueinander gehörende Klammerpaare richtig.

Beweis: Der Beweis wird durch Induktion nach der Anzahl k der Klammern im Ausdruck geführt.
Induktionsanfang: k = 2. Offenbar wird bei k = 2 Klammern genau () als korrekt erkannt, und die
Klammerkorrespondenz hergestellt.
Induktionsvoraussetzung: Die Methode arbeitet für alle Klammerausdrücke der Länge < k korrekt
(k > 2).
Induktionsschluss auf die Länge k:

1. Ist A ein korrekter Klammerausdruck, so sind die Stack Bedingungen erfüllt und korrespondie-
rende Klammern werden richtig ermittelt.
Sei A korrekt mit der Länge k > 2. Dann gibt es aufgrund der Syntax für korrekte Klammeraus-
drücke (vgl. 4.1) 2 Fälle: a) A = B ·C oder b) A = (B), wobei B,C kürzere korrekte Klammer-
ausdrücke sind, auf die dann die Induktionsvoraussetzung zutrifft.

a) Da auf B,C die Induktionsvoraussetzung zutrifft, werden sie als korrekt erkannt und wer-
den die zugehörigen Korrespondenzen richtig ermittelt.
Die Stackfolge für A ergibt sich als Konkatenation der Stackfolgen von B und C. Also
gelten auch in A die Stack Bedingungen.
Da der Stack am Ende von B leer ist, können auch innerhalb von A Klammern aus B nur
mit Klammern aus B korrespondieren. Diese Korrespondenzen werden nach Induktions-
voraussetzung richtig erkannt. Das gilt entsprechend auch für C.
b) Da auf B die Induktionsvoraussetzung zutrifft, wird B als korrekt erkannt und werden die
zugehörigen Korrespondenzen richtig ermittelt. Dies bedeutet, dass sich in A die äußeren
Klammern entsprechen müssen, da alle Klammern in B bereits für die Korrespondenzen
innerhalb von B “verbraucht” werden.
Die Stackfolge für A ergibt sich also aus der Stackfolge von B durch Anhängen der ersten
“(” von A als unterste Komponente des Stacks. Also gelten auch in A die Stack Bedingun-
gen.
Da der Stack am Ende von B genau noch die erste “(” von A enthält, werden die äußeren
Klammern als korrespondierend erkannt. Da innerhalb von B stets die erste “(” von A als
unterste Komponente im Stack enthalten ist, werden die Korrespondenzen innerhalb von
B auch nach Induktionsvoraussetzung richtig erkannt.
4.7. STACKS 97

Abbildung 4.5 illustriert die Stackfolgen für die beiden auftretenden Fälle.

2. Sind die Stack Bedingungen erfüllt, so ist A ein korrekter Klammerausdruck.


Betrachte die erste Stelle `, an der der Stack nach der zugehörigen Push/Pop Operation leer ist.
Es gibt 2 Fälle: a) ` < k oder b) ` = k.

a) Aus ` < k folgt, dass sich A zerlegen lässt in einen ersten Teil B := a1 a2 . . . a` und einen
zweiten Teil C := a`+1 . . . ak ; ai ∈ {(, )}. Da die Stack Bedingungen in A erfüllt sind, sind
sie nach der Wahl von ` auch in B und C erfüllt, und die Induktionsvoraussetzung trifft
wegen ` < k auf B und C zu. Also sind B und C korrekt, und damit nach den Syntaxregeln
auch A = BC.
b) Aus ` = k folgt, dass in der Stackfolge die zuerst gelesene “(” von A bis zum Schluss auf
dem Stack bleibt. Da die Stack Bedingungen erfüllt sind, muss die letzte Klammer von A
eine “)” sein, die dann mit der ersten Klammer korrespondiert. Also ist A von der Form
A = (B).
Die Stackfolge von B ist dann gleich der Stackfolge von A ohne die erste Klammer “(”
von A. Also folgt, dass auch die Stack Bedingungen für B erfüllt sind. Da B kürzer als A
ist, ist B nach Induktionsvoraussetzung korrekt, und damit nach den Syntaxregeln auch A.

Aus 1 und 2 folgt die Behauptung.

Fall a)

leer (B1 leer (C1 leer

Stackfolge für B Stackfolge für C

Fall b)
leer (1 (2 (1 leer
(1

Stackfolge für B mit (1 als zusätzlicher,


unterer Komponente

Abbildung 4.5: Die Stackfolgen aus dem Beweis von Satz 4.2.

Wir betrachten jetzt eine Implementation von Algorithmus 4.1 für Strings, der außer den Klammern (
und ) auch andere Zeichen enthalten kann. Der Test auf korrekte Klammern bezieht sich auf ( und ).
Dazu verwenden wir ein Hilfsarray
partner : Array von int
98 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

Am Ende soll partner[i] die zum Zeichen an Position i im String zugehörige Klammer angeben.
Dabei soll gelten (mit k > 0):

 k charAt(k) und charAt(i) bilden ein Paar (..) oder
partner[i] := charAt(i) und charAt(k) bilden ein Paar (..),
−1 charAt(i) 6= (,).

Für den String ((a+b)(-1))/(2+c) ergibt sich

10 5 -1 -1 -1 1 9 -1 -1 6 0 -1 16 -1 -1 -1 12
0 2 4 6 8 10 12 14 16

als Belegung für das Array partner.


Um diese Belegung von partner zu erreichen wird statt der “(” in einem Stack jeweils die Position
i im String abgespeichert, d. h. man definiert den benötigten Stack als Stack von Integer Objekten.
Abbildung 4.6 zeigt ein Struktogramm für die Verfeinerung von Algorithmus 4.1 mit diesen Daten-
strukturen. Eine Implementierung in Java gibt Programm 4.7.

Ermittle die Länge n des gegebenen Strings str


Initialisiere das Array partner zu -1,...,-1
Definiere den Stack s {Einrichten des leeren Stacks}
i := 0 {Initialisierung der Zählvariablen}
korrekt := true {Boolesche Variable; bleibt true bis festgestellt wird, dass
str nicht korrekt ist}
i < n and korrekt
’(’
str.charAt(i)
’)’

s.isEmpty()
true false
m := s.top()
s.push(i)
{merke Position der
{merke die Position
“(” in der Variablen
der “(” im Stack} korrekt := false
m}
{es kann bei “)”
nicht vom Stack ge- s.pop()
poppt werden} {entferne “(”}
partner[i] := m
partner[m] := i
i := i+1
korrekt and s.isEmpty()
true false
Gebe partner aus {Der Ausdruck Fehlermeldung: Der Ausdruck ist an
ist korrekt} Stelle i-1 inkorrekt

Abbildung 4.6: Struktogramm für Algorithmus 4.1.


4.7. STACKS 99

Programm 4.7 StackDemo.java


import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

/**
* reads string of parantheses and other chars
* uses stack to check for correct parantheses rules
* @see SimpleStack.java
*/
public class StackDemo extends Applet {

// setup the graphical user interface components


// and initialize labels and text fields
Label inputPrompt;
TextField input;
TextArea output;

public void init() {


//set layout
setLayout(new FlowLayout(FlowLayout.LEFT));
setFont(new Font("Times", Font.PLAIN, 24));
Font fixedWidthFont = new Font("Courier", Font.PLAIN, 24);

inputPrompt = new Label("Schreiben Sie einen String und "


+ "beenden Sie ihn mit Return.");
add(inputPrompt);

input = new TextField("((a+b)-(c-d))/(x-y)", 50);


input.setFont(fixedWidthFont);
input.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
runDemo();
}
});
add(input);

output = new TextArea(10, 50);


output.setFont(fixedWidthFont);
add(output);
}
100 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

public void runDemo() {


String str;
StringBuilder outputBuf = new StringBuilder();

// read the string


str = input.getText();

// define partner and initialize to -1 ... -1


int[] partner = new int[str.length()];
int i;
for (i = 0; i < str.length(); i++) {
partner[i] = -1;
}

// define Stack of Integer objects


SimpleStack<Integer> checkStack = new SimpleStack<Integer>();

// main loop, use stack for checking correctness


i = 0;
boolean correct = true;
int m;
while (i < str.length() && correct) {
switch (str.charAt(i)) {
case ’(’ : {
// remember position i of ’(’ on stack
checkStack.push(i); // autoboxing
break;
}
case ’)’ : {
if (checkStack.isEmpty()) {
// no corresponding ’(’
correct = false;
} else {
// corresponding ’(’ is in position m
m = checkStack.top(); // autoboxing
// remove the corresponding ’(’
checkStack.pop();
// positions i and m have
// coresponding ’(’ and ’)’
partner[i] = m;
partner[m] = i;
}
break;
}
4.7. STACKS 101

default : { // neither ’(’ nor ’)’ at position i


; // nothing to do,
// partner[i] is already -1
}
}//end switch
i++;
}//end while: if not correct, then at
// position i-1 (counting from 0)

// output correct pairs or report error


if (correct && checkStack.isEmpty()) {
// correct paranthesises
// output correct pairs
outputBuf.append("Der String ist korrekt mit "
+ "folgender Klammerung:\n");
outputBuf.append(str);

i = 0;
while (true) {
// look for next ’(’
while (str.charAt(i) != ’(’
&& i < str.length() - 1) {
i++;
}
// leave while loop if at end of str
if (i == str.length() - 1) break;
// newline on current ’(’
outputBuf.append(’\n’);
// indent until current ’(’
for (m = 0; m < i ; m++) {
outputBuf.append(’ ’);
}//end for
// write current ’(’
outputBuf.append(str.charAt(i));
for (m = i + 1; m < partner[i] ; m++) {
// indent until corresponding ’)’
outputBuf.append(’ ’);
}//end for
// write corresponding ’)’
outputBuf.append(str.charAt(partner[i]));
i++; // increase i for next while loop
}//end while
} else {
outputBuf.append("Die Klammerung ist an Position "
102 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

+ i + " nicht korrekt.\n");


// indicate the wrong position
outputBuf.append(str + ’\n’);
// indent until wrong position
for (m = 0; m < i - 1; m++) {
outputBuf.append(’ ’);
}//end for
// write ’!’ at wrong position
outputBuf.append(’!’);
}
output.setText(outputBuf.toString());
}
}

4.8 Queues (Warteschlangen)

Queues sind wie Stacks eingeschränkte Listen, bei denen das Einfügen nur am Ende (rear oder tail)
und das Löschen nur am Kopf (front oder head) möglich ist. Sie bilden also die geeignete Datenstruk-
tur für das, was man im täglichen Leben unter “Warteschlange” versteht. Man nennt daher Queues
auch First-In, First-Out oder FIFO Listen.
Wie bei Stacks drücken wir die Operationen als Methoden einer Java-Klasse Queue<T> vom generi-
schen Typen T aus.

/**
* Tests if this queue has no entries.
*
* @return <code>true</code> if the queue is empty; <code>false</code>
* otherwise
*/
abstract public boolean isEmpty();

/**
* Return the value of the current node.
*/
abstract public T front() throws NoSuchElementException;

/**
* Inserts a new queue node at the rear.
*
* @param <code>someData</code> the object to be added.
*/
abstract public void enqeue(T someData);
4.9. ZUSAMMENFASSUNG 103

/**
* Delete the front node from the list.
*/
abstract public void dequeue() throws NoSuchElementException;

Queues haben viele Anwendungen, z. B. in Rechnerbetriebssystemen (Verwaltung wartender Jobs,


Pufferung von Ein/Ausgabe) und bei der Simulation von “Wartesituationen” in vielen Algorithmen
und Modellen betrieblicher Abläufe (Abfertigung an Bankschaltern u. a.)

4.9 Zusammenfassung

Die diskutierten Beispiele von Datenstrukturen zeigen:

• Datentypen und zugehörige Operationen bilden eine Einheit und können nicht getrennt gesehen
werden.

• Die Semantik ergibt sich erst durch den Wertebereich und die Operationen (mit den zugehörigen
Axiomen)

Es gibt verschiedene Methoden, um Datentypen zu definieren:

a) Die konstruktive Methode


Hierzu gehört die Definition von Arrays in Java. Die Definition “höherer” Datentypen erfolgt
aus bereits eingeführten Datentypen nach folgendem Muster.

einfache Datentypen niedrigste Stufe, z. B. int


| ↑
Konstruktor Selektor
↓ |
strukturierte Objekte 1. Stufe z. B. int[] vec; // Vektoren
| ↑
Konstruktor Selektor
↓ |
strukturierte Objekte 2. Stufe z. B. double[][] table; // Matrizen
| ↑
Konstruktor Selektor
↓ |
..
.

Aus der Mathematik ist diese fortgesetzte Abstraktion z. B. von Mengen bekannt:
104 KAPITEL 4. OBJEKTE, TYPEN, DATENSTRUKTUREN

Elemente einer Grundmenge


↓ ↑
Mengen von Elementen (Menge)
↓ ↑
Mengen von Mengen (Potenzmenge)

b) Die axiomatische Methode


Beispiele hierzu sind die Datenstrukturen Liste, Stack, Queue. Die Definition geschieht implizit
mittels Operationen und deren Eigenschaften in Form von Axiomen.
Auch diese Methode ist in der Mathematik gebräuchlich, z. B.

– Peano Axiome für natürliche Zahlen


– Inzidenzbeziehungen in der Geometrie usw.

Der Vorteil der axiomatischen Methode besteht in der Abstraktion von der Implementation (Im-
plementationsdetails sind unwesentlich). Dies erlaubt eine genauere Spezifikation und einfache-
re Korrekheitsbeweise. Ein (leichter) Nachteil besteht darin, dass i. A. verschiedene Interpreta-
tionen (Modelle) einer abstrakten Datenstruktur möglich sind. Daher ist die Übereinstimmung
von Modell und Spezifikation i. A. nicht leicht überprüfbar, insbesondere für ungeübte Benut-
zer.

Zusammenfassend lässt sich feststellen, dass die konstruktive Methode sich bereits als Implementa-
tionsvorschrift verstehen lässt, während die axiomatische Methode wesentlich mehr Freiheitsgrade
erlaubt.

4.10 Literaturhinweise
Datenstrukturen werden in nahezu allen Büchern über Entwurf und Analyse von Algorithmen behandelt. Ihre
Realisierung in Java wird ausführlich in [GT05] erläutert.
Die hier gegebene Darstellung lehnt sich an [HR94] an. Dort wird im Gegensatz zu den meisten Büchern
ausführlich auf die Umsetzung von der abstrakten Spezifikation zu Programmen eingegangen, allerdings in
C++. Die hier gegebenen Definitionen bzw. Deklarationen der Java Funktionen für Listen, Stack und Queues
sind eine leichte Modifikation der dort gegebenen Darstellung in C++.
Kapitel 5

Algorithmen auf Arrays

Wir behandeln jetzt drei Aufgabenstellungen mit zunehmendem Schwierigkeitsgrad, deren Lösung zu
Algorithmen auf Arrays führt:

• Suchen eines Wertes in einem Array,

• Multiplikation von Matrizen,

• Berechnung kürzester Wege in Graphen.

5.1 Suchen einer Komponente vorgegebenen Wertes

Hier handelt es sich um folgendes Basisproblem in Arrays:


Gegeben: Ein Array mit n ganzen Zahlen, eine ganze Zahl x.
Gesucht: Der Index i einer Komponente mit Wert x (falls vorhanden).

5.1.1 Sequentielle Suche

Eine einfache Lösung dieses Problems basiert auf folgender Idee. Durchlaufe die Komponenten des
Arrays sequentiell bis die aktuelle Komponente i den Wert x enthält. Gebe dann i aus. Ist x in keiner
Komponente, gebe −1 aus.
Diese Methode heißt sequentielle Suche. Sie erfordert im schlimmsten Fall n Vergleiche bis der Index
i gefunden ist, bzw. festgestellt wird, dass keine Komponente den Wert x hat.
Eine Java Methode hierfür ist:

Programm 5.1 sequentialSearch

Version vom 17. Juni 2010

105
106 KAPITEL 5. ALGORITHMEN AUF ARRAYS

/**
* searches for an element in an int array
* by sequential search
*
* @param a the int array
* @param x the int to search for
* @return the index of the first component containing <code>x</code>
* or <code>-1</code> if <code>x</code> is not in the array
*/
public int sequentialSearch(int[] a, int x){
int k = 0; // variable to hold the index or -1
while (k < a.length) {
if (a[k] == x) {
return k; // found x
}
k++;
}
return -1;
}

Beim i-ten Wiedereintritt in die for Schleife gilt die Zusicherung (Assertion)

i < n und a[0], . . . , a[i − 1] 6= x

Beim Austritt aus der Schleife gilt

a[i] = x oder i = a.length

Ist i = n, so ist x nicht im Array.


Diese Überlegungen zeigen die Korrektheit des Algorithmus. Zusicherungen bei Schleifen nennt man
auch Schleifeninvarianten. Sie spielen bei Korrektheitsbeweisen eine wichtige Rolle.

5.1.2 Binäre Suche

Liegt das Array a in sortierter Form vor (mit n = a.length), gilt also a[0] ≤ a[1] ≤ . . . ≤ a[n−1], so lässt
sich die Suche wesentlich beschleunigen durch die Verwendung der binären Suche oder Bisection.
Die Idee der binären Suche ist wie folgt:

– Wähle den mittleren Index i und vergleiche x und a[i].

– Ist a[i] = x, so ist der Index i gefunden.

– Ist a[i] < x, so kann x nur in der “linken Hälfte” von a, d. h. in a[0] . . . a[i − 1] sein. Wende das
Verfahren auf die linke Hälfte an.
5.1. SUCHEN EINER KOMPONENTE VORGEGEBENEN WERTES 107

– Ist a[i] > x, so kann x nur in der “rechten Hälfte” von a, d. h. in a[i + 1] . . . a[n − 1] sein. Wende
das Verfahren auf die rechte Hälfte an.

– Verfahre so weiter bis x gefunden, oder der noch zu durchsuchende Teil, in dem x sein könnte,
leer ist.

Als Beispiel betrachten wir a mit den Werten

3 5 6 8 10 12 13 16
0 1 2 3 4 5 6 7

und x = 10. Zuerst wird i in der Mitte gewählt, etwa i = 3. Dann ist a[i] = 8 < x = 10, und es wird in
der rechten Hälfte von a weitergesucht.

10 12 13 16
4 5 6 7

i wird wieder in der Mitte gewählt, etwa i = 5. Dann ist a[i] = 12 > x = 10, und es wird in der linken
Hälfte des verbleibenden Arrays weiter gesucht, also in

10
4

Die einzig verbleibende Wahl von i = 4 findet dann den gesuchten Wert. Wäre jetzt x 6= a[i], so stellte
man an dieser Stelle fest dass x nicht im Array enthalten sein kann.
Eine Java Methode hierfür ist:

Programm 5.2 binarySearch


/**
* searches for an element in an int array
* by binary search
*
* @param a the int array
* @param x the int to search for
* @return the index of the first component containing <code>x</code>
* or <code>-1</code> if <code>x</code> is not in the array
*/
public int binarySearch(int[] a, int x){
int k; // variable to hold the index or -1
int i, j; // lower and upper array bounds
i = 0; // initial array bounds
j = a.length - 1; // initial array bounds
while (i <= j) {
k = (i + j) / 2; // choose k in the middle
if (a[k] == x) {
return k; // found x
108 KAPITEL 5. ALGORITHMEN AUF ARRAYS

}
if (x > a[k]) {
i = k + 1;
} else {
j = k - 1; // update bounds
}
}
return -1;
}

Vor jedem Eintritt in die Schleife gilt (falls x im Array ist) die Invariante

a[i] < x ≤ a[ j] und 0 ≤ i ≤ j ≤ n − 1.

Da in jedem Schleifendurchlauf i erhöht oder j erniedrigt wird, terminiert das Programm mit a[k] = x
oder i > j. Im ersten Fall wird der richtige Index k zurückgegeben. Im zweiten Fall (also bei i > j) ist
der Bereich, in dem x sein könnte, wegen der Schleifeninvariante leer und es wird −1 zurückgegeben.
Also arbeitet der Algorithmus korrekt. Bezüglich der nötigen Anzahl von Vergleichen ergibt sich:

Satz 5.1 (Aufwand der binären Suche) Für die Anzahl C(n) von Vergleichen bei der binären Suche
in einem Array mit n Komponenten gilt1 C(n) ≤ blog nc + 1.

Beweis: Der Beweis erfolgt durch vollständige Induktion nach n.


Induktionsanfang: Ist n = 1, so ist nur 1 Vergleich erforderlich. Also stimmt die Behauptung wegen
blog 1c + 1 = 0 + 1 = 1.
Induktionsvoraussetzung: Die Behauptung sei richtig für alle Arrays der Länge < n, n ≥ 2.
Schluss auf n: Nach dem ersten Vergleich mit a[k] muss nur in einer der Hälften weitergesucht werden.
Beide Hälften haben eine Länge ≤ bn/2c und erfordern daher nach Induktionsvoraussetzung

C(bn/2c) ≤ blog(bn/2c)c + 1

Vergleiche. Insgesamt sind dann 1 +C(bn/2c) Vergleiche erforderlich. Einsetzen ergibt:

C(n) ≤ 1 +C(bn/2c)
≤ 1 + blog(bn/2c)c + 1
≤ 1 + log(n/2) + 1 da bn/2c ≤ n/2
= 1 + log(n/2) + log 2
= 1 + log((n/2) · 2) da log(a · b) = log a + log b
= 1 + log n
1 log n bedeutet hier (wie stets in der Informatik) den Logarithmus von n zur Basis 2. dae ist a nach oben gerundet, bac
ist a nach unten gerundet, also d5,2e = 6 und b5,2c = 5.
5.2. MATRIZENMULTIPLIKATION 109

Also ist C(n) ≤ 1 + log n. Da C(n) ganzzahlig ist, folgt C(n) ≤ b1 + log nc = blog nc + 1.

Es sind also wesentlich weniger Vergleiche nötig als bei der sequentiellen Suche. Bei n = 1.000.000 ≈
220 = 1048576 reichen C(n) = 20 Vergleiche bei der binären Suche aus, während die sequentielle
Suche 1.000.000 Vergleiche braucht.

5.2 Matrizenmultiplikation

Ein n-dimensionaler Vektor (genauer: Spaltenvektor) mit Elementen aus einer Menge I ist ein n-Tupel
 
x1
 x2 
x=  mit xi ∈ I, i = 1, . . . , n.
 
..
 . 
xn

Die Menge aller solchen Vektoren wird mit Vn (I) bezeichnet. Meist ist I = R, d. h. Vn (I) = Vn (R).
Dafür schreibt man auch kurz Rn .
Eine m × n-Matrix mit Elementen aus einer Menge I ist eine Zusammenfassung
 
a11 a12 . . . a1 j ... a1n

 a21 a22 . . . a2 j ... a2n 

 .. .. .. 
 . . . 
A=
 ai1 ai2 . . . ai j . . .

 ain 

 .. .. 
 . . 
am1 am2 . . . am j . . . amn

von m · n Elementen aus I. Mm,n (I) bezeichnet die Menge aller m × n-Matrizen von Elementen aus I.
Meist ist I = R oder I ⊆ R.
Jede Spalte von A bildet einen Vektor A· j , den sogenannten Spaltenvektor ( j = 1, . . . , n); analog bildet
jede Zeile Ai· einen Zeilenvektor (i = 1, . . . , m).
Offensichtlich lassen sich Vektoren und Matrizen in Java durch 1- bzw. 2-dimensionale Arrays dar-
stellen.

double[] vector = new double[n];


double[][] matrix = new double[m][n];

definieren entsprechende Array-Objekte.


Matrizen und Vektoren können wie folgt miteinander multipliziert werden:
110 KAPITEL 5. ALGORITHMEN AUF ARRAYS

Multiplikation einer m × n-Matrix A mit einem n-Vektor x:


    a x + a x + ··· + a x 
a11 . . . a1n x1 11 1 12 2 1n n
 .. ..   ..  =  a 21 x 1 + a22 x2 + · · · + a 2n xn

 . .  .   . . . . . . . . . . . . . . . . . . . . . . . . . .
 

am1 . . . amn xn am1 x1 + am2 x2 + · · · + amn xn

Beispiele:

1. Multiplikation einer 2 × 3 Matrix mit einem 3-Vektor. Ergebnis ist ein 2-Vektor.
 
  1    
1 2 1 1 · 1 + 2 · 0 + 1 · (−1) 0
· 0  = =
2 0 −1 2 · 1 + 0 · 0 + −1 · (−1) 3
−1

2. Multiplikation einer 1 × 3 Matrix mit einem 3-Vektor. Ergebnis ist eine Zahl.
 
 1
1 2 1 ·  0  = 1 · 1 + 2 · 0 + 1 · (−1) = 0
−1

Multiplikation einer m × p-Matrix A mit einer p × n-Matrix B:


 
a11 . . . a1p
 .. ..   b   
 . .  11 . . . b1 j . . . b1n c11 . . . c1n
  . .. ..  =  .. .. 
  ..
 ai1 . . . aip  
 . .   . . 
 .. ..  b
 . .  p1 . . . b p j . . . b pn cm1 . . . cmn
am1 . . . amp
mit
ci j = ai1 b1 j + ai2 b2 j + · · · + aip b p j
p
= ∑ aik bk j
k=1
Die Formel zur Berechnung von ci j zeigt, dass ci j aus der i-ten Zeile von A und der j-ten Spalte von
B gebildet wird. Die illustriert Abbildung 5.1.
Beispiele:

1. Multiplikation einer 2 × 3 Matrix mit einer 3 × 3 Matrix. Ergebnis ist eine 2 × 3 Matrix.
 
  2 1 3
1 2 1  −1 0 0 
2 0 −1
1 1 0
 
1 · 2 + 2 · (−1) + 1 · 1 , 1·1+2·0+1·1 , 1·3+0+0
=
2 · 2 + 0 · 0 + (−1) · 1 , 2 · 1 + 0 · 0 + (−1) · 1 , 2 · 3 + 0 + 0
 
1 2 3
=
3 1 6
mit

cij = ai1 b1j + ai2 b2j + · · · + aip bpj


p
'
= aik bkj
k=1

Die MATRIZENMULTIPLIKATION
5.2. Formel zur Berechnung von cij zeigt, daß cij aus der i-ten Zeile von A und der j-ten
111
Spalte von B gebildet wird. Die illustriert Abbildung 6.1.

i
· = ij

Abbildung
Abbildung6.1: Schemader
5.1: Schema derMatrixmultiplikation.
Matrixmultiplikation.

Beispiele:
2. Multiplikation einer 1 × 3 Matrix mit einem 3-Vektor. Ergebnis ist eine Zahl.
1. Multiplikation einer 2 × 3 Matrix  mit einer 3 × 3 Matrix. Ergebnis ist eine 2 × 3 Matrix.
2
 
  = 1 · 2 + 2 · 0 + 1 · 1 = 3
( 1, 2,2 1 1 30
1 2 1  
 −1 0 01
2 0 −1
1 1 0
3. Multiplikation eines 3 Vektors mit einer 1 × 3 Matrix. Ergebnis ist eine 3 × 3 Matrix. (
1 · 2 + 2 · (−1) + 1 · 1 , 1·1+2·0+1·1 , 1·3+0+0
=  
2 2· 2 + 0 · 0 + (−1) · 1 ,2 ·21 · 12+
· 20 ·20· +
1 (−1) · 1 2 , 4 2 ·23 + 0 + 0
   
 0  1,(2, 1

=  0·1 0·2 0·1  =  0 0 0 
1 2 3
= 1 1·1 1·2 1·1 1 2 1
3 1 6
Diese Beispiele zeigen, dass die Matrixmultiplikation die Multiplikation von Matrizen mit Vektoren
als Spezialfall enthält. Stellt man sich die n Spalten der p × n-Matrix B als p-Spaltenvektoren B· j ,
j = 1, . . . , n, vor, so gilt

A · B = A · B·1 , . . . , B·n

= A · B·1 , . . . , A · B·n .

In Java lässt sich die Matrizenmultiplikation folgendermaßen realisieren:

double[][] a = new double[m][p]; // Array f"ur Matrix A


double[][] b = new double[p][n]; // Array f"ur Matrix B
double[][] c = new double[m][n]; // Array f"ur Ergebnismatrix C
int i, j, k;

// multiplication
for (i = 0; i < m; i++) {
112 KAPITEL 5. ALGORITHMEN AUF ARRAYS

for (j = 0; j < n; j++) {


c[i][j] = 0;
for (k = 0; k < p; k++) {
c[i][j] += a[i][k] * b[k][j];
}//end for k
}//endfor j
}//endfor i

Wir bauen diese Multiplication jetzt in eine Klasse Matrix für Matrizen ein. Jedes Matrix Objekt
hat die drei (privaten) Felder matrix (für die Einträge), numberOfRows und numberOfColumns (für
Anzahl der Zeilen und Spalten). In den Methoden und Konstruktoren der Klasse Matrix verwenden
wir die Variable mit dem reservierten Namen this. Sie bezeichnet eine implizite Referenz auf das
Objekt, das gerade im Konstruktor konstruiert wird bzw. eine Methode für sich aufruft. Sind a, b
Objekte der Klasse Matrix, und ruft a die Methode a.multiply(b) für sich auf, so bezeichnet
also this im Rumpf von multiply() die Referenz auf das Objekt a. Diese implizite Referenz kann
nur innerhalb der Klasse durch die Variable this angesprochen werden. Mehr dazu findet sich in
Kapitel 6.3.

Programm 5.3 Matrix


import java.lang.IllegalArgumentException;
import java.lang.ArrayIndexOutOfBoundsException;

/**
* This is a class for rectangular double matrices
*/
public class Matrix {
/**
* the private representation of a matrix is a
* 2-dimensional double array
*/
private double[][] matrix;

/**
* number of rows
* is 0 for empty matrix
*/
private int numberOfRows;

/**
* number of columns
* is 0 for empty matrix
*/
private int numberOfColumns;
5.2. MATRIZENMULTIPLIKATION 113

/**
* default constructor, creates empty matrix
*/
public Matrix() {
matrix = null;
numberOfRows = 0;
numberOfColumns = 0;
}

/**
* constructor, constructs a matrix with prescribed number
* of rows and columns and all entries equal to zero
*
* Throws <code>IllegalArgumentException</code> if
* <code>m</code> or <code>n</code> is negative
*
* @param m the number of rows
* @param n thenumber of columns
*/
public Matrix(int m, int n) throws IllegalArgumentException {
if ((m <= 0) || (n <= 0)) {
throw new IllegalArgumentException("Anzahl der "
+ "Zeilen und Spalten muss positiv sein.");
}
// this is a reference to the matrix to be constructed
// we could also say matrix = new double[m][n] etc.,
// but we use this to make the reference clear
this.matrix = new double[m][n];
this.numberOfRows = m;
this.numberOfColumns = n;
}

/**
* @return the number of rows of this matrix
*/
public int getNumberOfRows() {
// here this refers to the object that applies the method
// i.e., in myMatrix.getNumberofRows(), this refers to myMatrix
// we could also say return numberOfRows,
// but we use this to make the reference clear
return this.numberOfRows;
}

/**
114 KAPITEL 5. ALGORITHMEN AUF ARRAYS

* @return the number of coulumns of this matrix


*/
public int getNumberOfColumns() {
return this.numberOfColumns;
}

/**
* returns the entry at a matrix position
*
* Throws <code>IllegalArgumentException</code> if
* <code>i</code> or <code>j</code> is negative
*
* Throws <code>ArrayIndexOutOfBoundsException</code> if
* <code>i</code> or <code>j</code> is too big
*
* @param i the row index
* @param j the colum index
* @return the entry in row i and column j
*/
public double getEntry(int i, int j) throws
IllegalArgumentException,
ArrayIndexOutOfBoundsException {
if ((i < 0) || (j < 0)) {
throw new IllegalArgumentException("Negative Indizes");
}
if (this.matrix == null) {
throw new IllegalArgumentException("Matrix ist leer.");
}
if ((i > this.getNumberOfRows()) ||
(j > this.getNumberOfColumns())) {
throw new ArrayIndexOutOfBoundsException(
"Index zu gro"s.");
}
return this.matrix[i][j];
}

/**
* sets the entry at position (i,j) of this matrix
*
* Throws <code>IllegalArgumentException</code> if
* <code>i</code> or <code>j</code> is negative or
* if this matrix is empty
*
* Throws <code>ArrayIndexOutOfBoundsException</code> if
5.2. MATRIZENMULTIPLIKATION 115

* <code>i</code> or <code>j</code> is too big


*
* @param i the row index
* @param j the colum index
* @param x the value of the entry
*/
public void setEntry(int i, int j, double x) throws
IllegalArgumentException,
ArrayIndexOutOfBoundsException {
if ((i < 0) || (j < 0)) {
throw new IllegalArgumentException("Negative Indizes");
}
if (this.matrix == null) {
throw new IllegalArgumentException("Matrix ist leer.");
}
if ((i > this.getNumberOfRows()) ||
(j > this.getNumberOfColumns())) {
throw new ArrayIndexOutOfBoundsException(
"Index zu gro"s.");
}
this.matrix[i][j] = x;
}

/**
* multiplies this matrix with another matrix
*
* Throws <code>IllegalArgumentException</code> if
* dimensions are not compatible or
* if this matrix is empty
*
* @param m the other matrix
*/
public void multiply(Matrix m) throws IllegalArgumentException {
if (this.getNumberOfColumns() != m.getNumberOfRows()) {
throw new IllegalArgumentException("Falsche "
+ "Dimensionen");
}
if (this.matrix == null) {
throw new IllegalArgumentException("Multiplikation "
+ "mit leerer Matrix.");
}

// array for result


double[][] c = new double[this.getNumberOfRows()]
116 KAPITEL 5. ALGORITHMEN AUF ARRAYS

[m.getNumberOfColumns()];
int i, j, k;

// multiplication
for (i = 0; i < this.getNumberOfRows(); i++) {
for (j = 0; j < m.getNumberOfColumns(); j++) {
c[i][j] = 0;
for (k = 0; k <
this.getNumberOfColumns(); k++){
c[i][j] += this.matrix[i][k]
* m.getEntry(i, j);
}//end for k
}//endfor j
}//endfor i
// store the result in this matrix,
// update this column number
this.matrix = c;
this.numberOfColumns = m.getNumberOfColumns();
}
}

Wie üblich werden jetzt Matrizen mit

Matrix a = new Matrix(10, 15);

erzeugt. Mit Anweisungen der Form

a.setEntry(2, 1, 3.14);

werden Komponenten gesetzt, und zwei Matrizen a und b werden mit

a.multiply(b);

miteinander multipliziert, wobei dann das Ergebnis in der Matrix a steht.


Die algebraische Struktur der Menge der Matrizen (Rechenstruktur!) wird in der Linearen Algebra
ausführlich behandelt.
Wir werden im weiteren die Kurzschreibweisen

A · x bzw. Ax für die Matrix-Vektor Multiplikation, und


A · B bzw. AB für die Matrizenmultiplikation

verwenden (geeignete “Abmessungen” vorausgesetzt).


6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 151

6.35.3. K ürzesteWEGE
KÜRZESTE Wege in gerichteten
IN GERICHTETEN GRAPHENGraphen 117

6.3.1
5.3 Graphen
Kürzesteund Wege
Wege in gerichteten Graphen
Ein gerichteter Graph
5.3.1 Graphen oder
und Digraph (directed graph) ist ein Paar G = (V, E) bestehend aus
Wege

–Ein
Dergerichteter V der oder
Menge Graph Bei(directed
Digraph
Knoten. uns istgraph)
meististVein=Paar 2, =
{1, G . . (V, E) bestehend
. , n} aus
oder (in Implementatio-
nen) V = {0, 1, . . . , n − 1}.
– Der Menge V der Knoten. Bei uns ist meist V = {1, 2, . . . , n} oder (in Implementationen) V =
– Der{0, − 1}.
1, . . . , nE
Menge der (gerichteten) Kanten oder Bögen zwischen Knoten. Bei uns ist
E ⊆ V × V \ {(i, i)|i ∈ V }; e = (i, j) bedeutet, daß die Kante e vom Knoten i zum
– Der Menge E der (gerichteten) Kanten oder Bögen zwischen Knoten. Bei uns ist E ⊆ V ×V \
Knoten j∈
{(i, i)|i gerichtet ist.
V }; e = (i, j) bedeutet, dass die Kante e vom Knoten i zum Knoten j gerichtet ist.

Beispiel 6.4
Beispiel 5.1 GG=
= (V, E)mit
(V, E) mit V {1,
V= {1,
= 2, 3,2, 3, 4,
4, 5} und5} und

= {(1,
E =E{(1, (1,(1,
2),2), 3), (2,3),
3),(2, (2, 4),
3),(2, 4),(3, 5),(4,(4,
(3,5), (4,(4,
1),1), 3), (5,
3),4)}
(5, 4)}
ist ein
ist ein Graphmit
Graph mit55 Knoten
Knoten und
und8 gerichteten Kanten.
8 gerichteten Er ist inEr
Kanten. Abbildung 5.2 dargestellt.
ist in Abbildung 6.11 dargestellt.

2 4

3 5

Abbildung 6.11:5.2:
Abbildung Zeichnung
Zeichnung des Graphen
des Graphen ausaus Beispiel
Beispiel 5.2. 6.11.

Graphen
Graphen habenviele
haben vieleAnwendungen.
Anwendungen. SieSie
eignen sich sich
eignen zur Beschreibung von
zur Beschreibung von

– Verbindungen zwischen Orten in einem Netzwerk (Straßennetz, U-Bahnnetz,. . .),


– Verbindungen zwischen Orten in einem Netzwerk (Straßennetz, U-Bahnnetz,. . .),
– Hierarchien,
– Hierarchien,
– Syntax- und Flussdiagrammen,
– Syntax- und Flußdiagrammen,
– Arbeitsabläufen,
– Arbeitsabläufen,
und vielem anderen mehr.
und In
vielem anderen
solchen mehr.haben die Kanten (i, j) des Graphen meist eine Bewertung, die je nach An-
Anwendungen
wendung als Länge oder Entfernung von i nach j (Straßennetz), Kosten (Arbeitsablauf) oder ähnlich
In solchen Anwendungen haben die Kanten (i, j) des Graphen meist eine Bewertung, die je
interpretiert wird.
nach Anwendung als Länge oder Entfernung von i nach j (Straßennetz), Kosten (Arbeitsab-
lauf) oder ähnlich interpretiert wird.
118 KAPITEL 5. ALGORITHMEN AUF ARRAYS

152 KAPITEL 6. ALGORITHMEN AUF ARRAYS


Ein solcher bewerteter Graph ist beschreibbar durch eine Matrix A = (ai j )i, j=1,...,n mit

Ein solcher bewerteter Graph ist beschreibbar


(Länge) durch eine (i,
Matrix A= (i,(a
j)ij∈)i,j=1,...,n mit

 Bewertung von Kante j) falls E,

ai j = 0 falls i = j,

Bewertung (Länge) von Kante (i, j) falls (i, j) ∈ E,
∞ sonst.
aij = 0 falls i = j,

 ∞ sonst.
Wir nennen A die Bewertungsmatrix des Graphen G. Zur Abspeicherung von A im Rechner wird statt
· max 2
einenennen
∞Wir sehr große Zahl
A die M (z. B. M := n des
Bewertungsmatrix (i, j)∈E |aG.
Graphen i j| + 1) verwendet.
Zur Abspeicherung von A im Rechner
wird statt ∞ eine sehr große Zahl M (z. B. M := n · max(i,j)∈E |aij | + 1) verwendet.6
Beispiel 5.2 (Fortsetzung von Beispiel 5.1) Für den Graphen aus Beispiel 5.1 legen wir die in Ab-
Beispiel
bildung 5.3 6.5
links(Fortsetzung von angegebenen
neben den Kanten Beispiel 6.4)Kantenbewertungen
Für den Graphen aus Beispiel
fest. 6.4 legen wir
Die zugehörige Matrix A
die in Abbildung 6.12 links
ist rechts daneben angegeben. neben den Kanten angegebenen Kantenbewertungen fest. Die
zugehörige Matrix A ist rechts daneben angegeben.

−1

3  
−1
2 4 0 −1 2 ∞ ∞
 ∞ 0 2 3 ∞ 
 
1 2 0 1 ⇒ A=
 ∞ ∞ 0 ∞ 1 

 −1 ∞ 0 0 ∞ 
2 ∞ ∞ ∞ 1 0
3 1 5

Abbildung 6.12: Bewerteter Graph aus Beispiel 6.12.


Abbildung 5.3: Bewerteter Graph aus Beispiel 5.3.
Falls nur der Graph G beschrieben werden soll (d. h. ohne Kantenbewertungen), so reicht
auch die Matrix +
1 ohne
Falls nur der Graph G beschrieben werden soll (d. h. ∈ E,
(i, j) Kantenbewertungen), so reicht auch die
A = (aij ) mit aij =
Matrix 0 sonst.
1 (i, j) ∈ E,
A = (ades
Diese Matrix heißt Adjazenzmatrix i j ) mit ai j = G. In ihr ist durch Zugriff auf aij in kon-
Graphen 0 sonst.
stanter Zeit (d. h. unabhängig von der Größe des Graphen) feststellbar, ob i und j durch
eine Matrix
Diese Kante verbunden sind. Dafürdes
heißt Adjazenzmatrix benötigt
Graphen man G. jedoch quadratischen
In ihr ist durch ZugriffSpeicherplatz (n × n Zeit
auf ai j in konstanter
Matrix).
(d. h. unabhängig von der Größe des Graphen) feststellbar, ob i und j durch eine Kante verbunden
Es sind
sind. auch
Dafür andere
benötigt Datenstrukturen
man zur Speicherung
jedoch quadratischen von (n
Speicherplatz × n Matrix).
Graphen möglich, z. B. für jeden
Knoten i eine Liste der Knoten j, die mit i durch eine Kante (i, j) verbunden sind. Diese
Es sind auch andere Datenstrukturen zur Speicherung von Graphen möglich, z. B. für jeden Knoten
benötigen weniger Platz, erfordern jedoch mehr Zeit, um festzustellen, ob (i, j) ∈ E ist oder
i eine Liste der Knoten j, die mit i durch eine Kante (i, j) verbunden sind. Diese benötigen weni-
nicht, da Listen nur sequentiellen Zugriff erlauben.
ger Platz, erfordern jedoch mehr Zeit, um festzustellen, ob (i, j) ∈ E ist oder nicht, da Listen nur
Bei manchenZugriff
sequentiellen Anwendungen
erlauben.spielen die Richtungen der Kanten keine Rolle. In diesem Fall
ist mit einer Kante (i, j) auch die Kante (j, i) im Graphen, und beide haben dieselbe Bewer-
Bei manchen
tung. In derAnwendungen
Darstellung desspielen
Graphendie Richtungen der Kanten
wird dann statt keinegerichteten
der beiden Rolle. In diesem
KantenFall
eineist mit
einer6 Wir
Kante (i, j) auch die Kante ( j, i) im Graphen, und beide haben dieselbe Bewertung.
werden hier nur diese einfache, aber viel Speicherplatz verbrauchende Datenstruktur verwenden. Eine
In der
Darstellung
oft benutzte des Graphen
Alternative wirdindann
besteht einemstatt dervon
Array beiden
Listen.gerichteten Kantendieeine
Dabei entsprechen ungerichtete
Indices gezeichnet,
der Komponenten
den Knoten des Graphen, und die Komponente i enthält eine Liste aller Knoten j, zu denen von i aus eine
2 Wir werden
Kante (i, j) existiert.
hier nur diese einfache, aber viel Speicherplatz verbrauchende Datenstruktur verwenden. Eine oft benutzte
Alternative besteht in einem Array von Listen. Dabei entsprechen die Indices der Komponenten den Knoten des Graphen,
und die Komponente i enthält eine Liste aller Knoten j, zu denen von i aus eine Kante (i, j) existiert.
5.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 119

also statt . Man spricht dann von ungerichteten Kanten. Ein Graph
mit nur ungerichteten Kanten heißt ungerichteter Graph.
Im folgenden werden wir die Kantenbewertung als Entfernung interpretieren, bzgl. der wir kürzeste
Wege zwischen je zwei Knoten berechnen wollen.
Ein Weg von i nach j ist eine endliche Folge von Knoten und Kanten

i = i1 , (i1 , i2 ), i2 , (i2 , i3 ), . . . , ik−1 , (ik−1 , ik ), ik = j .

i1 i2 i3 i` ik−1 ik

Dabei sind Knotenwiederholungen zugelassen, und auch der triviale Weg, der nur aus einem Knoten
besteht (man “geht” dann von i nach i, indem man in i bleibt).
Ein Weg heißt elementar, wenn keine Knotenwiederholungen auftreten. Ein Weg von i nach j heißt
Zykel, falls i = j gilt. Die Länge eines Weges ist die Summe der Entfernungen der Kanten auf dem
Weg. Die Länge des trivialen Weges ist 0. Ein Weg von i nach j heißt kürzester Weg, falls alle anderen
Wege von i nach j eine mindestens ebenso große Länge haben.
Unser Ziel ist nun die Berechnung der kürzesten Weglängen ui j zwischen je zwei Knoten i, j (mit
ui j = ∞ falls kein Weg von i nach j existiert) sowie zugehöriger kürzester Wege.

5.3.2 Zwei konkrete Anwendungen

Als erste Anwendung betrachten wir den Ausschnitt aus dem BVG-Netz von Berlin in Abbildung
5.4. Dieses Netz wird in Abbildung 5.5 als bewerteter ungerichteter Graph wiedergegeben, da jede
Kante in beiden Richtungen “bereist” werden kann, und die Reisezeit für beide Richtungen gleich ist.
Die Bewertung einer Kante ist für beide Richtungen gleich und entspricht der mittleren Reisezeit in
Minuten (ohne Umsteigezeiten und Wartezeiten).
Die Bewertungsmatrix A dieses Graphen ist in Tabelle 5.1 angegeben. Als kürzester Weg von der
Yorkstraße zum Mathematikgebäude ergibt sich

Yo −→ Be −→ Zoo −→ ER −→ MA

mit der Länge 5 + 5 + 2 + 5 = 17 Minuten.


Umsteige- und Wartezeiten können berücksichtigt werden, indem man die Stationen “aufbläht” zu
mehreren Knoten, die den verschiedenen Linien entsprechen, und den Kanten dazwischen die Umsteige-
und Wartezeiten zuordnet bzw. die Kantenbewertungen um diese Werte vergrößert. Dies ist in Abbil-
dung 5.6 für die Station Zoologischer Garten ausgeführt.
Bei Entfernungen als Kantenbewertungen treten nur nicht-negative Zahlen auf. Oft sind jedoch auch
negative Kantenbewertungen sinnvoll, wie die folgende Anwendung des Devisentausches zeigt.
120 KAPITEL 5. ALGORITHMEN AUF ARRAYS
154 KAPITEL 6. ALGORITHMEN AUF ARRAYS

S-Friedrichstr.

MA-Gebäude
Bismarckstr. S-Tiergarten S1
Fußwege
Ernst-
Reuter-Pl.

Zoologischer
Garten

U2 Wittenbergpl.
U1 Möckernbrücke

Bus 119
U9

Yorckstr.
U7

Berliner Str.

Abbildung 6.13: Ausschnitt aus dem BVG-Netz.


Abbildung 5.4: Ausschnitt aus dem BVG-Netz.

Tabelle 6.3: Bewertungsmatrix des Graphen zum BVG-Netz.

Bi ER MA Ti Fr Zoo Be Yo Mö Wi
Tabelle 5.1: Bewertungsmatrix des Graphen zum BVG-Netz.
Bi 0 2 ∞ ∞ ∞ ∞ 7 ∞ ∞ ∞
ER 2 0 5 ∞ ∞ 2 ∞ ∞ ∞ ∞
MA
Bi

ER5
MA
0 10
Ti ∞
Fr 11
Zoo∞ Be∞ Yo∞ Mö

Wi
BiTi 0∞ ∞ 2 10
∞ 0∞ 7∞ 3 ∞ ∞ 7∞ ∞∞ ∞∞ ∞
ERFr 2∞ ∞ 0 ∞5 7∞ 0∞ ∞ 2 ∞ ∞10 ∞∞ ∞∞ ∞
MAZoo ∞∞ 52 11
0 310 ∞∞ 0 11 5 ∞∞ ∞∞ ∞2 ∞
TiBe 7
∞ ∞ ∞ ∞
10 ∞0 ∞7 5 3 0 ∞5 ∞∞ ∞∞ ∞
Yo ∞ ∞ ∞ ∞ 10 ∞ 5 0 2 15
Fr ∞ ∞ ∞ 7 0 ∞ ∞ 10 ∞ ∞
Mö ∞ ∞ ∞ ∞ ∞ ∞ ∞ 2 0 7
Zoo
Wi ∞∞ ∞ 2 11
∞ ∞3 ∞∞ 2 0 ∞ 5 15 ∞7 ∞0 2
Be 7 ∞ ∞ ∞ ∞ 5 0 5 ∞ ∞
Yo ∞ ∞ ∞ ∞ 10 ∞ 5 0 2 15
Mö ∞ ∞ ∞ ∞ ∞ ∞ ∞ 2 0 7
Wi ∞ ∞ ∞ ∞ ∞ 2 ∞ 15 7 0
5.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 121

6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 155

7
6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN Fr 155
MA
10
5
Bi 2 Ti
ER 11 7 Fr
3
MA
2
10
5 Zoo 10
Bi 2 Ti
ER 11 2
3 Wi
2
7
7 Zoo 10
Mö
2
15
Wi
7 2
5
7 Mö
Yo
15
5
2
5
Be
Yo
Abbildung 6.14: Der Graph
5 für das BVG-Netz.
Abbildung 5.5: Der Graph für das BVG-Netz.
Be Ti

ER
Abbildung 6.14: 3 für das BVG-Netz.
2 Der Graph

Zoo
Ti
10
ER 5 2
10
2 3 Wi

10 Zoo
5

10 5 2
Wi
Be

5
Abbildung 6.15: Zoologischer Garten mit Umsteige- und Wartezeiten.

Be

Abbildung 6.15: Zoologischer Garten mit Umsteige- und Wartezeiten.


Abbildung 5.6: Zoologischer Garten mit Umsteige- und Wartezeiten.
122 KAPITEL 5. ALGORITHMEN AUF ARRAYS

Gegeben ist ein gerichteter Graph, bei dem die Knoten Devisenbörsen darstellen und eine Kante (i, j)
dem Tausch von Währung i in die “Zielwährung” j entspricht. Die Bewertung der Kante (i, j) gibt
den Preis (Kurs) für eine Einheit der Zielwährung j bzgl. der Währung i an. Daher haben beide
Kantenrichtungen unterschiedliche Werte. Ein Beispiel ist in Abbildung 5.7 angegeben (Kurse vom
November 1998).

Abbildung 5.7: Ein Graph für den Devisentausch (Kurse November 2003).

In dieser Anwendung möchte man für gegebene “Heimatwährung” i und Zielwährung j eine Um-
tauschfolge (also einen Weg) bestimmen, so dass das Produkt der Bewertungen entlang des Weges
(also der Preis für eine Einheit der Zielwährung) möglichst klein wird.
Die Problem lässt sich folgendermaßen auf ein Kürzeste-Wege Problem transformieren. Ersetze die
Kantenbewertung ai j der Kante (i, j) durch log ai j . Dann ist (nach den Logarithmengesetzen) das Pro-
dukt entlang eines Weges gleich der Summe der Logarithmen der Kanten. Man erhält also ein Kürzes-
tes Wege Problem mit āi j := log ai j als Bewertung der Kanten. Dabei können negative Bewertungen
āi j auftreten, nämlich genau dann, wenn ai j < 1 ist.
Als Konsequenz negativer Kantenbewertungen können auch Zykel negativer Länge auftreten. Diese
machen, wie wir sehen werden, bei der Berechnung kürzester Wege Schwierigkeiten. Beim Devisen-
tausch haben sie jedoch eine besondere Bedeutung: Sie entsprechen einem Gewinn bringenden Zykel,
auf dem man durch Umtausch seinen Einsatz vermehren kann. Im Graphen aus Abbildung 5.7 existiert
ein solcher Zykel, vgl. Abbildung 5.8. Der Tausch entlang des Zykels ermöglicht den Kauf einer DM
für 2,79 · 0,51 · 0,61 = 0,9960 DM, also eine Vermehrung des eingesetzten Kapitals um den Faktor
1/0,9960 ' 1,004161.
Solche Zykel können tatsächlich bei Devisengeschäften vorkommen, allerdings nur kurzfristig, da
sich Kurse dauernd aufgrund von Angebot und Nachfrage ändern. Die Rechner der Devisenhändler
bemerken solche Zykel sofort und ordern entsprechend bis die steigende Nachfrage durch das Steigen
der Preise den Faktor wieder größer als 1 werden lässt.
5.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 123

Abbildung 5.8: Ein gewinnbringender Zykel beim Devisentausch.

5.3.3 Die Bellman Gleichungen

Wir werden jetzt eine Methode kennen lernen, mit der man die kürzesten Weglängen (und auch die
kürzesten Wege selbst) zwischen je 2 Knoten iterativ berechnen kann (sofern sie existieren). Die
Grundidee basiert darauf, die Anzahl der Kanten auf den betrachteten Wegen in jeder Iteration um 1
zu vergrößern. Dazu definieren wir für i, j ∈ V und m ∈ N die Größe

 Länge eines kürzesten Weges von i nach j
(m)
ui j := mit höchstens m Kanten, falls dieser existiert,
∞ falls kein solcher Weg existiert.

Diese Größe ist wohldefiniert, da es für festes m nur endlich viele Wege mit höchstens m Kanten
zwischen i und j gibt, und somit unter diesen endlich vielen auch einen kürzesten.
In Beispiel 5.4 ergeben sich für i = 1 und j = 5 folgende Werte:
(1) (2) (3) (4) (5)
u15 = ∞, u15 = 3, u15 = 2, u15 = 2, u15 = 2 .

Allgemein gilt:

Lemma 5.1 Für festes i und j und m ≥ 2 ist


(1) (2) (m) (m+1)
ui j ≥ ui j ≥ . . . ≥ ui j ≥ ui j .

Beweis: Beim Übergang von m auf m + 1 vergrößert sich die Menge der Wege unter denen man einen
(m) (m+1)
kürzesten Weg ermittelt. Daher gilt ui j ≥ ui j .

(m)
Die ui j lassen sich durch Rekursionsgleichungen berechnen, die im folgenden Satz 5.2 angegeben
sind. Sie besagen anschaulich, dass sich die kürzeste Länge eines Weges von i nach j mit höchstens
124 KAPITEL 5. ALGORITHMEN AUF ARRAYS

m + 1 Kanten aufspalten lässt in die kürzeste Länge eines Weges mit höchstens m Kanten von i bis zu
einem Knoten k und die Länge ak j der Kante (k, j). Die Minimumsbildung in der Rekursionsgleichung
dient dem Finden des richtigen Knoten k.
Im Beweis dieses Satzes spielt das Prinzip der optimalen Substruktur eine wichtige Rolle. Es besagt
in diesem Fall, das ein kürzester Weg zwischen zwei Knoten sich aus kürzesten Teilwegen zusammen
setzt. Kurz: jeder Teilweg eines kürzesten Weges ist selber ein kürzester Weg zwischen den Endpunk-
ten des Teilweges. Abbildung 5.9 veranschaulicht dieses Prinzip. Es gilt in verschiedenen Bereichen
der Mathematik und gibt immer Anlass zu Algorithmen, die große“ optimale Strukturen aus optima-

len Teilstrukturen zusammen setzen.

i r s j

kürzester Weg von i nach j

r s

kürzester Weg von r nach s

Abbildung 5.9: Das Prinzip der optimalen Substruktur bei kürzesten Wegen.

(m)
Satz 5.2 (Bellman Gleichungen) Die ui j erfüllen die folgenden Rekursionsgleichungen (die sog.
Bellman Gleichungen).
(1) (m+1) (m)
ui j = ai j , ui j = min [uik + ak j ] für m ≥ 1 .
k=1,...,n

Beweis: Für m = 1 kommen nur Wege mit höchstens einer Kante in Frage. Für i 6= j also gerade die
Länge der Kante (i, j), falls diese existiert, bzw. ∞ andernfalls. Für i = j kommt nur der triviale Weg
(1)
mit 0 Kanten in Frage. Also gilt in allen Fällen ui j = ai j .
(m+1)
Betrachte nun ui j . Wir zeigen zunächst
(m+1) (m)
ui j ≥ min [uik + ak j ] . (5.1)
k=1,...,n

(m+1)
Diese Ungleichung ist trivialerweise erfüllt, falls ui j = ∞ ist, also kein Weg von i nach j mit
höchstens m + 1 Kanten existiert.
Also nehmen wir an, dass ein solcher Weg existiert. Sei dann W ein kürzester Weg von i nach j mit
höchstens m + 1 Kanten (dieser existiert, da wegen der Beschränkung auf maximal m + 1 Kanten nur
endlich viele Wege in Frage kommen). Wir unterscheiden den Fall, dass W keine bzw. mindestens
eine Kante enthält.
(1) W habe mindestens eine Kante, etwa
5.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 125

W := i i1 i2 ip ` j

(m+1)
Dann hat W die Länge ui j . (Dabei kann W einen Zykel enthalten oder auch i = j sein, so dass der
ganze Weg einen Zykel bildet.)
Das Anfangsstück

W 0 := i i1 i2 ip `

von W ist ein kürzester Weg von i nach ` mit höchstens m Kanten; denn gäbe es einen kürzeren, so
würde man durch Anhängen der Kante (`, j) an W 0 einen kürzeren Weg mit höchstens m + 1 Kanten
von i nach j erhalten, im Widerspruch zur Annahme, dass W ein kürzester Weg von i nach j ist.
(m)
Also ist die Länge von W 0 gleich ui` und somit die Länge von W gleich
(m+1) (m) (m)
ui j = ui` + a` j ≥ min [uik + ak j ] ,
k=1,...,n

da ` bei der Minimumsbildung vorkommt. Also ist Ungleichung (5.1) erfüllt.


(2) Hat W keine Kanten, so ist i = j und W besteht nur aus dem einzigen Knoten i = j. Dann ist seine
(m+1) (m+1) (m) (1) (m)
Länge uii = 0. Aus Lemma 5.1 folgt uii ≤ uii ≤ uii = aii = 0 und somit uii = 0. Also gilt
(m+1) (m) (m)
uii = uii + aii ≥ min [uik + ak j ] ,
k=1,...,n

da i bei der Minimumsbildung vorkommt. Folglich gilt Ungleichung (5.1) auch in diesem Fall.
Jetzt zeigen wir umgekehrt die Ungleichung
(m+1) (m)
ui j ≤ min [uik + ak j ] . (5.2)
k=1,...,n

Diese Gleichung ist wieder trivialerweise erfüllt, falls die rechte Seite ∞ ist. Ist sie endlich, so sei r
der Index, für den das Minimum angenommen werde, d. h.
(m) (m)
min [uik + ak j ] = uir + ar j .
k=1,...,n

Dabei unterscheiden wir die Fälle r 6= j bzw. r = j.


(m) (m)
(1) Sei r 6= j. Da uir + ar j < ∞, sind uir < ∞ und ar j < ∞. Also existiert ein kürzester Weg

W= i j1 j2 jq r j

(m)
von i nach r mit höchstens m Kanten und Länge uir und es existiert wegen r 6= j und ar j < ∞ im
Graphen die Kante (r, j). Der Weg W kann Zykel enthalten und es kann auch i = r sein (dann hat W
entweder 0 Kanten oder der gesamte Weg W bildet einen Zykel). Das folgende Argument gilt für all
diese Fälle.
Da die Kante (r, j) existiert, ist
126 KAPITEL 5. ALGORITHMEN AUF ARRAYS

W 0 := i j1 j2 jq r j

(m)
ein Weg von i nach j mit höchstens m + 1 Kanten und seine Länge ist uir + ar j . Diese kann natürlich
nicht kleiner als die kürzeste Länge eines Weges von i nach j mit höchstens m + 1 Kanten sein, d. h.
(m) (m+1)
uir + ar j ≥ ui j .

Also gilt Ungleichung (5.2).


(2) r = j. Dann ist
(m) (m) (m) (m+1)
min [uik + ak j ] = ui j + a j j = ui j ≥ ui j
k=1,...,n

wegen a j j = 0 und Lemma 5.1.


Aus den Ungleichungen (5.1) und (5.2) folgt die Behauptung.

5.3.4 Der Einfluss negativer Zykel

Es stellt sich nun die Frage, wie groß man die Anzahl m der Kanten maximal wählen muss, um
kürzeste Weglängen zu berechnen. Hier gibt es Schwierigkeiten falls der Graph Zykel negativer Länge
(kurz: negative Zykel) enthält. In diesem Fall können die Wege mit höchstens m Kanten einen solchen
(m)
Zykel (sogar mehrfach) enthalten, so dass das Ergebnis ui j keinem elementaren Weg (also einem
ohne Knotenwiederholungen) mehr entspricht, wie das folgende Beispiel zeigt.

Beispiel 5.3 (Der Einfluss negativer Zykel) Betrachte den Graph aus Abbildung 5.10. Um die kürzes-
te Weglänge von 1 nach n zu berechnen, müssen m := n − 1 Kanten berücksichtigt werden. Dann gibt
(m) (m)
zwar u1n die richtige kürzeste Weglänge an, aber die Werte u1 j sind für j = 2, . . . , n − 2 ungleich der
kürzesten Länge eines elementaren Weges von 1 nach j (die 0 beträgt). Zum Beispiel ergibt sich für
j=4  
(3) (4) (5) (6) (7) (8) (n−1) n−3
u14 = u14 = 0, u14 = u14 = −1, u14 = u14 = −2, . . . , u14 = − .
2
Die Ursache ist natürlich der negative Zykel 2 −→ 3 −→ 2 der Länge −1.

−1

1 2 3 4 n
0 0 0 0 0

Abbildung 5.10: Ein Graph mit einem Zykel negativer Länge.


5.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 127

Wir untersuchen daher zunächst den Fall, dass der gegebene Graph G keine negativen Zykel hat. Dies
lässt immer noch negative Kantenbewertungen zu, nur dürfen sich diese nicht entlang eines Zykels zu
einer negativen Zahl summieren.

Lemma 5.2 Hat G keinen negativen Zykel und gibt es einen Weg von i nach j, so existiert ein kürzester
Weg von i nach j, der elementar ist.

Beweis: Da G keine negativen Zykel enthält, kann die Wegnahme von Zykel aus einem Weg die
Weglänge höchstens verkürzen. Also kann man sich bei der Suche nach kürzesten Wegen auf elemen-
tare Wege beschränken. Da es nur endlich viele elementare Wege von i nach j gibt, existiert hierunter
auch ein kürzester.

Wir definieren nun



 Länge eines kürzesten elementaren Weges von i nach j
ui j := falls kein Weg von i nach j existiert,
∞ falls ein Weg von i nach j existiert.

Dann gilt (vgl. Lemma 5.1):

Satz 5.3 Hat G keine negativen Zykel, so ist für festes i und j
(1) (2) (n−1)
ui j ≥ ui j ≥ . . . ≥ ui j = ui j .

Die Bellman Gleichungen berechnen also in n − 2 Iterationen die kürzesten Weglängen ui j .

(m)
Beweis: Da G keine negativen Zykel hat, folgt aus Lemma 5.2, dass ui j nie kleiner als ui j werden
kann. Elementare Wege können bei n Knoten höchstens n − 1 Kanten haben. Die Monotonieeigen-
(m)
schaft der ui j aus Lemma 5.1 ergibt dann die Behauptung.

5.3.5 Der Bellman-Ford Algorithmus

Satz 5.3 lässt sich direkt in den folgenden Algorithmus umsetzen, der nach seinen Entdeckern Bellman-
Ford Algorithmus genannt wird. Wir geben ihn direkt als Java Methode an, die aus der Entfernungsma-
trix A des Graphen die Matrix U der kürzesten Weglängen ermittelt und zurück gibt. Dabei speichert
(m) (m+1)
u die ui j ab und dient temp zur Berechnung der ui j für festes i.

Programm 5.4 bellman


public double[][] bellman(double[][] a){
128 KAPITEL 5. ALGORITHMEN AUF ARRAYS

int n = a.length; // number of nodes


double[][] u = new double[n][n];
// initialize u
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
u[i][j] = a[i][j];
}
}
// main loop
for (int m = 2; m < n; m++){
for (int i = 0; i < n; i++) {
double[] tmp = new double[n]; // tmp array
// for calculating min{...} in Bellman equation
for (int j = 0; j < n; j++) {
// save current distance from i to j
tmp[j] = u[i][j];
for (int k = 0; k < n; k++) {
if (tmp[j] > u[i][k] + a[k][j]) {
tmp[j] = u[i][k] + a[k][j];
}
}// end for k
}// end for j
// store tmp in u[i]
for ( int j = 0; j < n; j++ ) {
u[i][j] = tmp[j];
}
}// endfor i
}// endfor m
return u;
}

Die Korrektheit des Bellman-Ford Algorithmus folgt direkt aus Satz 5.3 und den Bellman Gleichun-
gen (Satz 5.2). Für den Aufwand überlegt man sich aus der Schachtelung der Schleifen:

Satz 5.4 Der Bellman-Ford Algorithmus in der Version von Programm 5.4 benötigt (n − 2) · n · n · n
Vergleiche und höchstens (n − 2) · n · (n(1 + n) + n) Zuweisungen, d. h. seine Laufzeit liegt in der
Größenordnung von n4 Operationen.

(m)
Ist man nur an den ui j interessiert, nicht jedoch an den ui j (d. h. den kürzesten Weglängen mit höchs-
(m)
tens m Kanten), so kann man wegen der Monotonieeigenschaft der ui j in u[i,j] direkt jedesmal
den Wert von temp[j] abspeichern. Dann gilt nach der m-ten Iteration der äußeren Schleife
(m) (m+`)
ui j ≥ u[i,j] = ui j ≥ ui j
5.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 129

für ein ` mit 0 ≤ ` ≤ n − 1 − m. Dies liegt daran, dass kürzere Weglängen mit einer Kante mehr direkt
in u[i,j] abgespeichert werden und für weitere Rechnungen schon benutzt werden.
Dies hat im Programm den Vorteil, dass die Zwischenspeicherung in temp überflüssig wird und da-
(m)
durch weniger Zuweisungen nötig sind. Dafür wird in der m-ten Iteration jedoch nicht ui j berechnet,
(m) (n−1)
sondern ein Wert zwischen ui j und ui j = ui j .
Der Algorithmus vereinfacht sich dann zu

Programm 5.5 bellmanShort


public double[][] bellmanShort(double[][] a){
int n = a.length; // number of nodes
double[][] u = new double[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
u[i][j] = a[i][j];
}
for ( int m = 2; m < n; m++ ) {
for ( int i = 0; i < n; i++ ) {
for ( int j = 0; j < n; j++ ) {
for ( int k = 0; k < n; k++ ) {
if ( u[i][j] > u[i][k] + a[k][j] ) {
u[i][j] = u[i][k] + a[k][j];
}
}
}
}
}
return u;
}

Dieser Algorithmus hat allerdings immer noch eine Laufzeit von der Größenordnung n4 . Tatsächlich
gibt es, wenn man nur an den ui j interessiert ist, andere einfache Algorithmen, deren Laufzeit von der
Größenordnung n3 ist, vgl. die Literaturhinweise am Ende des Kapitels.

Beispiel 5.4 (Fortsetzung von Beispiel 5.2) Für den Graphen aus Beispiel 5.2 sollen die kürzesten
Weglängen mit dem Bellman-Ford Algorithmus berechnet werden (in der Version von Programm
5.4).
(m)
Dabei bezeichnet U (m) die Matrix (ui j )i, j=1,...,n und U die Matrix (ui j )i, j=1,...,n . Gemäß Satz 5.2 ist
 
0 −1 2 ∞ ∞
 ∞ 0 2 3 ∞ 
U (1) = A = 
 
 ∞ ∞ 0 ∞ 1  
 −1 ∞ 0 0 ∞ 
∞ ∞ ∞ 1 0
130 KAPITEL 5. ALGORITHMEN AUF ARRAYS

(2)
Dann ergibt sich u11 aus den Bellman Gleichungen gemäß
(2) (1)
u11 = min [u1k + ak1 ]
k=1,...,n
= min[0 + 0, −1 + ∞, 2 + ∞, ∞ − 1, ∞ + ∞] = 0 .

Dies entspricht einer Verknüpfung der ersten Zeile von U (1) mit der ersten Spalte von A. Diese Ver-
knüpfung, wir wollen sie mit “⊗” bezeichnen, ist gerade die rechte Seite der Bellman Gleichungen,
also  
0
 ∞ 
(1)   (1)
U1· ⊗ A·1 = (0, −1, 2, ∞, ∞) ⊗  ∞  = k=1,...,n
 min [uk + ak1 ] .
 −1 

Entsprechend ist
 
−1
 0 
(2) (1)  
u12 = U1· ⊗ A·2 = (0, −1, 2, ∞, ∞) ⊗  ∞ 

 ∞ 

= min[0 − 1, −1 + 0, 2 + ∞, ∞ + ∞, ∞ + ∞] = −1 ,
 
2
 2 
(2) (1)  
u13 = U1· ⊗ A·3 = (0, −1, 2, ∞, ∞) ⊗  0 

 0 

= min[0 + 2, −1 + 2, 2 + 0, ∞ + 0, ∞ + ∞] = 1 .

(1)
Hier wurde gegenüber u13 = 2 eine kürzere Weglänge über 2 Kanten ermittelt. Weiterhin ist
 

 3 
(2) (1)  
u14 = U1· ⊗ A·4 = (0, −1, 2, ∞, ∞) ⊗ 
 ∞ 

 0 
1
= min[0 + ∞, −1 + 3, 2 + ∞, ∞ + 0, ∞ + 1] = 2 ,
 

 ∞ 
(2) (1)  
u15 = U1· ⊗ A·5 = (0, −1, 2, ∞, ∞) ⊗ 
 1 

 ∞ 
0
= min[0 + ∞, −1 + ∞, 2 + 1, ∞ + ∞, ∞ + 0] = 3 .
5.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 131

(2)
Also ist die erste Zeile von U (2) gleich (0, −1, 1, 2, 3). Die anderen ui j berechnen sich entsprechend,
z. B.
 
0
 ∞ 
(2) (1)  
u21 = U2· ⊗ A·1 = (∞, 0, 2, 3, ∞) ⊗   ∞ 

 −1 

= min[∞ + 0, 0 + ∞, 2 + ∞, 3 − 1, ∞ + ∞] = 2 .

Insgesamt erhält man


 
0 −1 1 2 3
 2 0 2 3 3 
U (2) = 
 
 ∞ ∞ 0 2 1 

 −1 −2 0 0 1 
0 ∞ 1 1 0
und schließlich  
0 −1 1 2 2
 2 0 2 3 3 
(4)
 
U =U =
 1 0 0 2 1 .

 −1 −2 0 0 1 
0 −1 1 1 0

Das Beispiel zeigt, dass die Bellman Gleichungen eine starke Analogie zur Matrixmultiplikation auf-
weisen. Bei der Matrixmultiplikation C := A · B ist der Eintrag ci j der Ergebnismatrix C gegeben als
n
ci j = ∑ [aik · bk j ] .
k=1

Hier ist “·” die innere und “+” die äußere Operation.
In den Bellman Gleichungen C := A ⊗ B ist der Eintrag ci j der Ergebnismatrix C gegeben als

ci j = min [aik + bk j ] ,
k=1,...,n

also mit “+” als innerer und “min” als äußerer Operation.
Speziell ist
U (1) = A
U (2) = U (1) ⊗ A = A⊗A
U (3) = U (2) ⊗ A = A⊗A⊗A
..
.
U (n−1) = U (n−2) ⊗ A = A · · ⊗ A} =: An−1 .
| ⊗ ·{z
n−1 mal
132 KAPITEL 5. ALGORITHMEN AUF ARRAYS

Die Analogie zwischen Matrixmultiplikation und Bellman Gleichungen wird besonders deutlich,
wenn man sich nur dafür interessiert, ob i und j mit einem Weg mit höchstens m Kanten verbun-
(m)
den sind oder nicht. Dann kann ui j als Boolesche Variable definiert werden, d. h.

(m) true es gibt einen Weg von i nach j mit höchstens m Kanten,
ui j =
false sonst.

Ausgehend von A = (ai j ) mit 


true falls (i, j) ∈ E
ai j =
false sonst
erhält man
(m+1) (m) (m) (m)
ui j = [ui1 ∧ a1 j ] ∨ [ui2 ∧ a2 j ] ∨ . . . ∨ [uin ∧ an j ]
was genau der Matrixmultiplikation entspricht (∧ entspricht ·, ∨ entspricht +).

5.3.6 Die Ermittlung negativer Zykel

Für die Berechnung der kürzesten Weglängen ui j hat es sich als notwendig erwiesen, dass der Graph
G keine negativen Zykel enthält. Es stellt sich daher die Frage, wie man die Gültigkeit dieser Voraus-
setzung effizient überprüfen kann.

Satz 5.5 (Test auf negative Zykel) Die folgenden Aussagen sind äquivalent:

1. Der Graph G enthält einen negativen Zykel.

2. Der Graph G enthält einen elementaren negativen Zykel.


(n)
3. Es gibt einen Knoten i mit uii < 0

Insbesondere kann die Existenz negativer Zykel durch Überprüfen der Diagonalelemente in U (n) fest-
gestellt bzw. ausgeschlossen werden.

Beweis: (1) ⇒ (2): Sei Z ein negativer Zykel in G. Ist Z nicht bereits elementar, so durchlaufe man
von einem beliebigen Startknoten aus Z solange, bis der erste Knoten zum zweiten Mal erreicht wird.
Sei i dieser Knoten. Dann zerfällt Z in den elementaren Zykel Z1 von i nach i und den Rest Z2 ,
der ebenfalls ein Zykel ist, aber nicht notwendigerweise elementar. Die Länge von Z ist gleich der
Summe der Längen von Z1 und Z2 . Also muss Z1 oder Z2 negativ sein. Ist es Z1 , so ist ein elementarer
negativer Zykel gefunden. Ist es Z2 , so muss dasselbe Argument ggf. wiederholt werden. Da der
Graph G endlich ist, endet man nach einer endlichen Anzahl von Anwendungen dieses Argumentes
bei einem negativen elementaren Zykel.
(2) ⇒ (3): Sei Z ein elementarer negativer Zykel in G und i ein Knoten in Z. Da Z elementar ist,
enthält Z höchstens n Kanten (sonst würde ein Knoten doppelt vorkommen). Also gilt
(n)
uii ≤ Länge(Z) < 0,
5.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 133

da Z einen Weg von i nach i mit höchstens n Kanten bildet.


(n) (n)
(3) ⇒ (1): Sei uii < 0. Nach Definition von uii existiert dann ein Weg von i nach i (also ein Zykel
(n)
Z) mit höchstens n Kanten, dessen Länge gleich uii ist. Also ist Z ein negativer Zykel (der trotz der
Beschränkung auf n Kanten nicht elementar zu sein braucht).

(m)
Es existieren also genau dann negative Zykel, sobald ein uii < 0 wird für 1 ≤ m ≤ n. Da dies bei
(m)
der Berechnung von uii festgestellt werden kann, kann man abbrechen, sobald dies zum ersten-
mal eintritt, und eine entsprechende Fehlermeldung ausgeben. Der einzige Mehraufwand (neben der
Überprüfung) besteht in einer zusätzlichen Iteration. Diese wird notwendig, da bei n Knoten ein ele-
mentarer Zykel maximal n Kanten haben kann (vgl. die zweite Richtung des Beweises).

5.3.7 Die Ermittlung kürzester Wege

Bisher haben wir nur die kürzesten Weglängen ui j berechnet. Natürlich möchte man auch einen zu-
gehörigen kürzesten Weg ermitteln.
Dazu nutzt man die folgende, im Beweis von Satz 5.2 durchgeführte Überlegung aus: Bei der Berech-
(m+1) (m) (m+1)
nung der Bellman Gleichungen ui j = mink [uik + ak j ] entspricht (im Fall ui j < ∞) der Index
k0 , für den das Minimum angenommen wird, einem Knoten k0 , so dass (k0 , j) die letzte Kante auf
einem kürzesten Weg von i nach j ist. Merkt man sich also im Bellman-Ford Algorithmus jeweils
diesen Index, so lässt sich aus dieser Information ein kürzester Weg rekonstruieren.
Die Realisierung im Programm erfolgt durch ein n × n Array

int[][] tree;

mit der Initialisierung 


i falls (i, j) ∈ E
tree[i][j] := .
-1 sonst

In jedem Durchlauf der äußeren Schleife des Algorithmus wird dann tree[i][j] der Wert k zuge-
wiesen, wenn bei u[i][j] eine Änderung auftritt und das zugehörige Minimum bei k angenommen
wird.
Der Algorithmus (in der Version von Programm 5.5) wird dann zu3 :

Programm 5.6 bellmanTree


public double[][] bellmanTree( double[][] a ){
int n = a.length; // number of nodes
3 Indieser Variante wird nur die tree-Matrix zurückgegeben. Natürlich würde man entweder eine geeignete Ergebnis-
Klasse definieren, die die Rückgabe von u und tree gleichzeitig erlaubt, oder Graphen als Objekte ansehen, die entspre-
chende Felder u und tree aktualisieren.
134 KAPITEL 5. ALGORITHMEN AUF ARRAYS

double[][] u = new double[n][n];


for ( int i = 0; i < n; i++ )
for ( int j = 0; j < n; j++ ) u[i][j] = a[i][j];
for ( int m = 2; m < n; m++ )
for ( int i = 0; i < n; i++ )
for ( int j = 0; j < n; j++ )
for ( int k = 0; k < n; k++ )
if ( u[i][j] > u[i][k] + a[k][j] ) {
u[i][j] = u[i][k] + a[k][j];
tree[i][j] = k;
}
return tree;
}

Das Array tree enthält nach Ausführung der Methode die Information über die kürzesten Wege (falls
keine negativen Zykel gefunden wurden). Um dies genauer zu erläutern, brauchen wir den Begriff des
gerichteten Baums.
Ein gerichteter Baum ist ein Digraph T = (V, E) mit folgenden Eigenschaften:

– Es gibt genau einen Knoten r, in dem keine Kante endet (die Wurzel von T ).

– Zu jedem Knoten i 6= r gibt es genau einen Weg von der Wurzel r zu i.

Dies bedeutet, dass keine zwei Wege in den gleichen Knoten einmünden. Der Graph kann sich ausge-
hend von der Wurzel also nur verzweigen. Daher kommt auch der Name Baum.

Satz 5.6 Hat G keine negativen Zykel, so ist bei Termination von Programm 5.6 für jeden Knoten i
der Graph Ti := (V, Ei ) mit der Knotenmenge V = {0, . . . , n − 1} und der Kantenmenge

Ei = {(tree[i][j],j) | j = 0, 1, . . . , n − 1; tree[i][j] 6= −1}

ein gerichteter Baum mit i als Wurzel.


Ein Weg von i nach j in Ti ist ein kürzester Weg von i nach j in G. Ti heißt daher auch Kürzester-
Wege-Baum zum Knoten i.

Beweis: Jeder Knoten j 6= i hat in Ti (wenn er von i aus in G erreichbar ist) genau einen Vorgängerkno-
ten, nämlich tree[i][j]. Also können in Ti keine zwei Kanten in einem Knoten zusammentreffen.
Daher existiert zu jedem Knoten j, der von i aus in G erreichbar ist, ein eindeutiger Weg von i nach j
in Ti . Da G keinen negativen Zykel hat, hat i keinen Vorgängerknoten. Also bildet Ti einen Baum mit
Wurzel i.
Sei i = i0 , i1 , . . . , i`+1 = j die Folge der Knoten auf dem Weg von i nach j in Ti . Dann ist

i` = tree[i][ j], i`−1 = tree[i][i` ], . . . , i = tree[i][i2 ] .


5.4. LITERATURHINWEISE 135

i i1 i2 i` j
ai,i1 ai1 ,i2 ai2 ,i3 ai`−1 ,i` ai` , j

Da die Werte im Array tree gerade bei der Minimumsbildung aktualisiert werden, folgt
ui j = ui,i` + ai` , j
ui,i` = ui,i`−1 + ai`−1 ,i`
..
.
ui,i2 = ui,i1 + ai1 ,i2
ui,i1 = ai,i1 .

Daher ist ui j = ai,i1 +ai1 ,i2 +. . .+ai`−1 , j und somit der Weg in Ti ein kürzester Weg von i nach j in G.

Beispiel 5.5 (Fortsetzung von Beispiel 5.4) Für den Graphen aus Beispiel 5.4 erhält man
 
−1 1 2 2 3
 4 −1 2 2 3 
 
tree =  4 1 −1 5 3 .
 4 1 4 −1 3 
4 1 4 5 −1
Als E1 ergibt sich aus der ersten Zeile von tree die Kantenmenge
E1 = {(1, 2), (2, 3), (2, 4), (3, 5)} ,
und somit der in Abbildung 5.11 dargestellte Kürzeste-Wege Baum T1 . Die Kanten aus E1 entsprechen
folgenden Bellman Gleichungen:
(4)
u11 = 0
(4) (3)
u12 = u11 + a12 k=1 j=2
(4) (3)
u13 = u12 + a23 k=2 j=3
(4) (3)
u14 = u12 + a24 k=2 j=4
(4) (3)
u15 = u13 + a35 k=3 j=5

5.4 Literaturhinweise
Suchverfahren in Arrays werden in nahezu allen Büchern über Entwurf und Analyse von Algorithmen behan-
delt. Besonders ausführlich gehen [Knu98b, Meh88, OW02] hierauf ein.
Die Lösung linearer Gleichungssysteme ist Gegenstand aller Bücher über lineare Algebra. Ein empfehlenswer-
tes Buch, das lineare Algebra und Numerik verbindet, ist [GMW91].
Die Berechnung kürzester Wege findet sich sowohl in Büchern über Entwurf und Analyse von Algorithmen
(z. B. in [CLRS01, OW02]), als auch in Büchern über Graphenalgorithmen und/oder kombinatorische Optimie-
rung (etwa [Jun94]). Eine sehr gute Darstellung verschiedener kürzeste Wegealgorithmen gibt [Tar83].
136 KAPITEL 5. ALGORITHMEN AUF ARRAYS

2 4
−1 3

1 2

1
3 5

Abbildung 5.11: Der Kürzeste-Wege Baum zum Knoten 1 in Beispiel 5.5.


Kapitel 6

Abstraktion von Methoden und Daten

6.1 Funktionale (Prozedurale) Abstraktion

Funktionale Abstraktion erlaubt die “Auslagerung” häufig auftretender ähnlicher oder gleicher Pro-
grammteile auf eigene “Untereinheiten” des Hauptprogramms. Diese Untereinheiten oder Unterpro-
gramme existieren in allen Programmiersprachen unter verschiedenen Namen: procedures, functions,
subroutines. In Java sind alle Unterprogramme Funktionen, egal ob sie Werte zurückgeben oder nicht.
Die Java-interne Bezeichnung für Funktion ist Methode.
Funktionen sind ein Werkzeug zur Abstraktion, da man ihr Input-Output Verhalten (was tut die Funk-
tion) von ihrer Implementation (wie tut sie es) trennen kann. Sie bilden daher ein Werkzeug sowohl für
den Algorithmenentwurf (Aufteilung des Algorithmus in “kleine” Einheiten die alle Funktionen sind)
als auch für die Schaffung wiederverwendbarer Software (gut implementierte Funktionen können in
unterschiedlichen Aufgabenbereichen eingesetzt werden).
In beiden Fällen ist alles, was der Programmierer braucht, die Spezifikation der Funktion (d. h. eine
Beschreibung dessen, was die Funktion tut). Die Implementation selbst ist für ihn irrelevant, sofern
sie die Spezifikation erfüllt.

Beispiele sind mathematische Funktionen wie sqrt(x) (berechnet x) oder pow(x,n) (berechnet
xn ), Funktionen zur Handhabung von Strings und viele andere mehr. In allen Fällen interessiert nur
das Verhalten, aber nicht die Implementation.

6.1.1 Funktionen und Prozeduren

Funktionen fallen in zwei Kategorien, solche die einen einzelnen Funktionswert zurückgeben (in Pas-
cal “functions”) und solche, die keinen Wert zurückgeben (in Pascal “procedures”).
In Java hat jede Funktion einen Rückgabetyp, der in der Definition der Funktion angegeben werden

Version vom 17. Juni 2010

137
138 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

muss. Die allgemeine Definition hat die Form1

Rückgabetyp FunktionsName(formale Parameterliste )


{
Funktionsrumpf
}

Dabei gelten folgende semantische Regeln:

• Ist der Rückgabetyp void, so wird kein Wert zurückgegeben (wie bei einer Pascal Prozedur).

• Ist der Rückgabetyp verschieden von void, so wird pro Aufruf genau ein Wert vom Rückgabetyp
mit einer return Anweisung im Rumpf zurückgegeben. 2

• Als Rückgabetyp sind alle Typen erlaubt.

• Die formale Parameterliste ist optional. Falls vorhanden, so besteht sie aus einer durch Kommas
getrennten Folge
De f1 , De f2 , . . . , De fk
von Variablendeklarationen ohne Initialisierungen. Jede Definition De fi deklariert genau eine
Variable.

Der Aufruf einer Methode erfolgt mit Werten“ für die formalen Parameter. Diese Werte werden aktu-

elle Parameter oder Aufrufparameter genannt. Sie müssen natürlich typverträglich mit den formalen
Parametern sein und auch in derselben Anzahl und Reihenfolge auftreten.

6.1.2 Parameter und Datenfluss

Der Datenfluss bezeichnet die Art des Datenaustausches zwischen der Funktion und dem sie aufrufen-
den Programm. Für jeden Parameter in der formalen Parameterliste gibt es dabei drei Möglichkeiten

• Fluss nur in die Funktion,

• Fluss nur aus der Funktion heraus,

• Fluss sowohl in die Funktion, als auch aus der Funktion heraus.

Die Arten des Datenflusses sind geeignet zu kommentieren (zum Beispiel mit den @param und @return
Verweisen in den javadoc Kommentaren). Zur Realisierung dieses Datenflusses stehen in Program-
miersprachen verschiedene Methoden zur Parameterübergabe zur Verfügung:

• Call by value (Wertparameter).


1 Modifiziererwie public, private usw. werden in Abschnitt 6.3.5 behandelt.
2 Einzige Ausnahme: Eine throw Anweisung zur Erzeugung einer Exception beendet die Abarbeitung einer Funktion
und gibt eine Exception zurück.
6.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 139

• Call by reference (Variabler Parameter).

Call by value (Wertparameter): Eine Kopie des aktuellen Parameters wird beim Aufruf an die
Funktion übergeben. Die Funktion arbeitet mit der Kopie und ändert den aktuellen Parameter des
aufrufenden Programms nicht.

Beispiel 6.1 (Berechnung der Fakultät) Für eine natürliche Zahl n > 0 ist

n! := 1 · 2 · 3 · . . . · n

zu berechnen. Dies leistet die folgende Java Funktion.

int factorial (int n) {


int product = 1;
for (int i = 2; i <= n; i++) product *= i;
return product;
}

factorial ist also eine Funktion mit einem Wertparameter n, die einen Wert zurück gibt. Schema-
tisch ist dies in Abbildung 6.1 dargestellt. Der Datenfluss erfolgt nur in die Funktion über den Wert-
parameter n. Eine solche Funktion entspricht am ehesten der in der Mathematik üblichen Vorstellung
einer Funktion.

n
( by value )
Funktionswert

Abbildung 6.1: Datenfluss bei der Funktion factorial.

Das aufrufende Programm kann diese Funktion in beliebigen Ausdrücken verwenden, z. B. in

x = factorial(5*a) + b;

Beim Aufruf wird 5*a berechnet und dem Parameter n zugewiesen, der im Rumpf von factorial
wie eine Variable verwendet wird. Die return Anweisung gibt den Funktionswert zurück, und dieser
wird der Variablen x zugewiesen.
Call by reference (Variabler Parameter): Die Adresse des aktuellen Parameters wird beim Aufruf
an die Funktion übergeben. Die Funktion arbeitet im Rumpf auf dem Speicherplatz des aktuellen
Parameters und kann (aber muss nicht) diesen dadurch modifizieren.
140 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

Diese Art der Parameterübergabe ist in Java nicht möglich (aber z. B. in Pascal und C++). In Java
werden grundsätzlich alle Parameter durch call by value übergeben. Da jedoch alle Datentypen außer
den elementaren Referenztypen sind, lässt sich der call by reference durch einen call by value mit
einem Referenztyp weitgehend simulieren.3
Für den Datenfluss nur aus der Funktion heraus betrachten wir folgendes Beispiel.

Beispiel 6.2 (Initialisierung eines Arrays) Ein Array ist mit den ersten Quadratzahlen zu initialisie-
ren. Dies leistet folgende Funktion

void initializeWithSquares(int[] vec) {


for (int i = 0; i < vec.length; i++) {
vec[i] = i*i;
}
}

Die Anweisungen

int[] myArray = new int[7];


initializeWithSquares(myArray);
System.out.println(myArray[3]);

bewirken die Initialisierung des Arrays myArray mit 0, 1, 4, 9, 16, 25, 36, 49. Die Zahl 9
wird auf die Konsole geschrieben.

Die Funktion gibt keinen Wert zurück. Schematisch ist dies in Abbildung 6.2 dargestellt. Der Daten-
fluss erfolgt nur aus der Funktion. 4
Als Variante hiervon betrachten wir eine Funktion, die zu gegebener Zahl n ein Array der ersten n
Quadratzahlen erzeugt und (die Referenz auf) das erzeugte Array als Funktionswert zurück gibt.

int[] squareNumbers(int n) {
int[] vec = new int[n];
for (int i = 0; i < vec.length; i++) {
vec[i] = i*i;
}
return vec;
}
3 Gelegentlichwird dies fälschlicherweise als call by reference bezeichnet. Ein call by reference beinhaltet jedoch eine
automatische Dereferenzierung, daher sind als Parameter nur lvalues (z. B. Variablennamen) erlaubt. Beim call by value
können jedoch rvalues als aktuelle Parameter (z. B. Ausdrücke) übergeben werden, und genau dies geschieht in der An-
weisung
initializeWithSquares(squareNumbers(n));
mit der Funktion squareNumbers() von Seite 140.
4 Zumindest im Wesentlichen. Natürlich fließt die Referenz auf das Array myArray und über myArray.length auch die

Zahl der Komponenten als Information in die Funktion. Aber die Werte der Komponenten von myArray sind unerheblich.
6.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 141

( vec )

Abbildung 6.2: Datenfluss bei der Funktion initializeWithSquares().

Der Funktionswert kann dann in Ausdrücken der Form

int[] myArray = squareNumbers(8);

verwendet werden, wodurch der Arrayvariablen myArray das Array der ersten 8 Quadratzahlen (ge-
nauer: die Referenz des in der Funktion squareNumbers() erzeugten Arrays der ersten 8 Quadrat-
zahlen) zugewiesen wird.
Der zugehörige Datenfluss ist in Abbildung 6.3 dargestellt.

( n )
Array Referenz

Abbildung 6.3: Datenfluss bei der Funktion squareNumbers().

Als Beispiel für den Datenfluss in eine und aus einer Funktion betrachten wir die Addition zweier
Vektoren.

Beispiel 6.3 (Addition von Vektoren) Zwei Arrays der Länge n sollen komponentenweise addiert
werden und in einem Ergebnisarray zurückgegeben werden. Dies leistet folgende Funktion

/**
* Adds array a to array b and stores the result in c
* PRE: All arrays have the same length
*/
void arrayAdd(int[] a, int[] b, int[] c) {
for (int i = 0; i < a.length; i++) {
c[i] = a[i] + b[i];
142 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

}
}

Die Anweisungen

int[] vec1 = {1, 2, 3, 4}, vec2 = {4, 3, 2, 1}, sum = new int[4];
arrayAdd(vec1, vec2, sum);

bewirken dann, dass sum == {5, 5, 5, 5} gilt.

Der Datenfluss der Funktion ist in Abbildung 6.4 dargestellt.

( a b c )

Abbildung 6.4: Datenfluss bei der Funktion arrayAdd().

Natürlich wäre es analog zur Funktion squareNumbers auch möglich gewesen, das Ergebnis als
Funktionswert zurückzugeben.
Die Rückgabe interessierender Größen als Funktionswert ist prinzipiell immer möglich, da man eigene
Klassen für die interessierenden Größen definieren kann und eine Referenz auf ein Objekt dieser
Klasse zurückgeben kann. Dies illustriert das folgende Beispiel.

Beispiel 6.4 (Minimale und maximale Komponente eines Arrays) In einem Array von ganzen Zah-
len sollen der minimale und der maximale Wert ermittelt und zurückgegeben werden. Um 2 Werte
zurückgeben zu können, definieren wir eine entsprechende Klasse IntPair.

public class IntPair{


public int first;
public int second;
public IntPair(int x, int y) { // constructor
first = x;
second = y;
}
}

Der zu dieser Klasse gehörige Datentyp wird in der Funktion arrayMinMax als Rückgabetyp benutzt.
6.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 143

IntPair arrayMinMax(int[] vec){


int min, max;
min = max = vec[0];
for (int i = 0; i < vec.length; i++) {
if (vec[i] < min) {
min = vec[i];
}
if (vec[i] > max) {
max = vec[i];
}
}
IntPair pair = new IntPair(min, max);
return pair;
}

Die Anweisungen

int[] vector = { 10, 20, 3, 17, 9 };


IntPair xy = arrayMinMax(vector);

im aufrufenden Programmteil bewirken dann, dass xy.first == 3 und xy.second == 20 gilt.

6.1.3 Gültigkeitsbereiche von Identifiern (Scope)

In Unterprogrammen können Identifier verwendet werden, die auch in anderen Programmteilen oder
Unterprogrammen auftreten. Dies geschieht zwangsläufig, wenn Programme von mehreren Personen
entwickelt werden oder Fremdsoftware benutzt wird. Jede Programmiersprache braucht daher Re-
geln, die festlegen, welcher Identifier wann gemeint ist, und wie lange ein ihm eventuell zugeordneter
Speicherplatz mit dem Identifier angesprochen wird.
In Java werden solche Gültigkeitsbereiche oder Scopes (wie auch in Pascal) durch den Programmtext
festgelegt. Man spricht daher auch von statischen Scoperegeln.5 Man unterscheidet in Java zwischen
Klassenscope (class scope) und Blockscope (block scope).
Der Klassenscope ist der Bereich zwischen den Klammern {...}, die den Programmtext der Klasse
begrenzen. In ihm sind alle Identifier von class members, also Variablen (Feldern) und Funktionen
(Methoden) der Klasse bekannt, und zwar unabhängig davon, wo sie in der Klasse definiert werden.6
Identifier mit Klassenscope sind außerdem in allen Unterklassen der Klasse, in der sie deklariert wer-
den, bekannt.
Der Blockscope wird durch die Blöcke definiert. Dies sind strukturierte Anweisungen einschließ-
lich durch {...} geklammerter Programmteile als compound statement. Stellt man sich alle Blöcke
5 Andere Programmiersprachen wie LISP oder APL verwenden dynamische Scoperegeln, bei der die Hierarchie der

Aufrufe zur Laufzeit festlegt, welcher Identifier gemeint ist.


6 Man benötigt also keine forward-Deklaration wie in Pascal oder Funktionsprototypen wie in C++.
144 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

als mit {...} geklammert vor, so bilden die Blöcke in einem korrekten Programm einen korrekten
Klammerausdruck. Es kann gezeigt werden, dass je 2 Blöcke entweder disjunkt hintereinander im
Programmtext liegen oder einer ist vollständig im anderen enthalten.
{...} ... {...} bzw. {. . . { . . . } . . . }
| {z } | {z } | {z }
Block 1 Block 2 Block 1
| {z }
Block 2
Der Scope eines Identifiers, der innerhalb eines Blocks definiert wurde (man nennt das lokal definiert),
ist der gesamte Block ab der Definition.
{ . . . { . . . De f . . . { . . . { . . . } . . . } . . . { . . . } . . . } . . . }
| {z }
Scope von Def

Identifier (einer Klasse oder eines Blockes) bleiben in allen in der Klasse oder dem Block direkt oder
indirekt enthaltenen Blöcke gültig, sofern keine Überdeckung durch Neudefinition in einem “tieferen”
Block auftritt.
Neudefinition eines Identifiers (mit völlig anderer Bedeutung!) in anderen Blöcken ist beschränkt
möglich. Erfolgt die Neudefinition in einem Block B1 , der innerhalb eines Blocks B2 liegt, in dem der
Identifier bereits definiert war, so tritt Überdeckung auf. Der Scope der ersten, “äußeren” Definition
wird vom Scope der zweiten, “inneren” Definition überdeckt.
{ . . . { . . . De f . . . { . . . Neude f . . . { . . . } . . . } . . . { . . . } . . . } . . . }
| {z } | {z }| {z }
Def Neudef Def

Eine solche Überdeckung durch Neudefinition ist in Java nur für Identifier mit Klassenscope möglich
(also Klassenvariable oder Funktionen), nicht jedoch für Identifier mit Blockscope7 . Man benötigt die
Neudefinition bei der Vererbung, muss sie also bei Identifiern mit Klassenscope erlauben. Ansonsten
wird Überdeckung durch Neudefinition in Java jedoch verboten und führt zu einem Compiler-Fehler.
Formale Parameter in Funktionsdefinitionen haben als Scope den gesamten äußersten Block der Funk-
tion. Sie unterliegen den Blockscope Regeln.
Funktionen können (im Gegensatz zu Pascal) nicht geschachtelt werden. Allerdings ist es möglich,
Klassen zu schachteln (vgl. Abschnitt 6.3.3).
Das Programmfragment

for (int i = 0; i < n; i++) {


...
}
while (i < 10) {
i++;
...
}
7 im Gegensatz zu vielen anderen Programmiersprachen wie C++ oder Pascal.
6.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 145

ist also nur korrekt, wenn i eine Klassenvariable ist, während das Fragment

for (int i = 0; i < n; i++) {


for (int i = 0; i < n; i++) {
...
}
}

falsch ist, da i in der ersten for-Schleife durch i in der zweiten for-Schleife unzulässig überdeckt
wird. Dagegen ist

{ ... { ... int a; ... } ... double a; ... }

erlaubt, da die int-Variable a am Ende ihres Blockes ihre Gültigkeit verliert, also nicht durch die
double-Variable a überdeckt wird.
Das folgende Beispiel demonstriert den Unterschied zwischen statischen und dynamischen Scope-
regeln.

Beispiel 6.5 (Statische versus dynamische Scoperegeln) Im Programmfragment

public class Test extends Applet {


// other variables
int a;

void P() {
System.out.println(a);
}

void Q() {
double a;
a = 3.14;
P();
}

public void init() {


a = 1;
Q();
}
}

wird der Identifier a zweimal definiert, als Klassenvariable vom Typ int, und als lokale double
Variable in Q(). Die lokale Neudefinitionen überdeckt die Klassenvariable a innerhalb des Blockes
von Q().
146 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

In init() wird der Klassenvariablen a der Wert 1 zugewiesen. Der Aufruf von P() innerhalb von
Q() bezieht sich ebenfalls auf die Klassenvariable a, da sie im Block von P() Gültigkeit hat. Das
Programm gibt also 1 aus.
Bei Verwendung der dynamischen Scoperegeln (LISP, APL) würde der Scope aus der Aufrufhierarchie
ermittelt. init() ruft Q() auf, und Q() ruft P() auf. Daher würde P() auf die in Q() definierte
double-Variable a zugreifen und 3.14 ausgeben.
Aus den Scope Regeln ergibt sich, dass jede Funktion auf Klassenvariable (auch globale Variable
genannt) zugreifen kann und ihre Werte verwenden bzw. ändern kann. Dies stellt eine zusätzliche
Form des Datenflusses dar (neben Parametern und Funktionswert). Da diese Art des Datenflusses nicht
aus der Parameterliste ersichtlich ist, sollte die Verwendung globaler Variablen stets gut dokumentiert
werden, sofern sie nach außen public sind.
Funktionen werden außer durch Namen auch durch ihre Parameterlisten unterschieden (aber nicht
durch den Rückgabetyp!). Es können also in einem Scopebereich mehrere Funktionen den gleichen
Namen haben, sofern sie sich in ihren Parameterlisten (Anzahl, Typ, Reihenfolge der Typen) unter-
scheiden.

6.1.4 Abarbeitung von Funktionsaufrufen

Der Aufruf einer Funktion erfolgt mit den sogenannten aktuellen Parametern, die mit den in der
Definition der Funktion aufgeführten formalen Parametern typkompatibel sein müssen.
Bei Wertparametern darf der aktuelle Parameter ein Ausdruck sein, bei variablen Parametern muss es
eine Variable sein.
Beim Aufruf der Funktion werden Speicherplätze für die formalen Parameter angelegt, die unter den
Namen dieser Parameter im Rumpf der Funktion angesprochen werden. Wertparameter werden ausge-
wertet und ihr Wert in den Speicherplatz des zugehörigen formalen Parameters kopiert. Bei variablen
Parametern wird die Referenz (Adresse) der übergebenen Variablen ermittelt und im Speicherplatz
des zugehörigen formalen Parameters abgelegt. Im Rumpf arbeitet man dann bei Nennung dieses
Parameters stets auf dem Speicherplatz der übergebenen Variablen.
Da es in Java nur Wertparameter gibt, kann zwar eine Referenz auf ein Objekt als Wert übergeben
werden, aber man arbeitet nicht automatisch auf dem Speicherplatz des übergebenen Objektes. Das
Objekt kann natürlich verändert werden, aber dazu benötigt man die für das Objekt verfügbaren Me-
thoden.
Durch ein return-Statement wird ein Wert zurückgegeben und die Abarbeitung der Funktion been-
det. Ansonsten (bei void-Funktionen) endet die Abarbeitung der Funktion mit der Ausführung des
Rumpfes oder einer return Anweisung ohne Rückgabe eines Wertes.
Nach Abarbeitung der Funktion werden alle Speicherplätze für formale Parameter, lokale Variablen
usw. gelöscht, und im aufrufenden Programm wird an der Stelle nach dem Aufruf der Funktion wei-
tergemacht. Da bei variablen Parametern auf dem Speicherplatz des aktuellen Parameters gearbeitet
wurde, bleiben im Funktionsrumpf vorgenommene Änderungen erhalten.
6.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 147

Beispiel 6.6 (Fortsetzung von Beispiel 6.3) Betrachte den Aufruf arrayAdd(vec1, vec2, sum)
auf Seite 141.
Vor dem Aufruf ist die Situation im Speicher wie folgt (wobei Referenzen als Pfeile dargestellt wer-
den).

1 2 3 4 4 3 2 1 0 0 0 0

vec1 vec2 sum

Unmittelbar nach der Parameterübergabe ergibt sich folgendes Bild im Speicher.

1 2 3 4 4 3 2 1 0 0 0 0

vec1 vec2 sum

a b c

Bei den Zuweisungen c[i] = a[i] + b[i] wird also das Array sum verändert. Nach Abarbeitung
der Funktion werden die für die formalen Parameter angelegten Speicherplätze wieder freigegeben
und man erhält folgende Situation im Speicher.

1 2 3 4 4 3 2 1 5 5 5 5

vec1 vec2 sum

Wird bei der Abarbeitung einer Funktion ein neues Objekt mit new erzeugt, so steht dieses auch
nach Abarbeitung der Funktion zur Verfügung (sofern die Referenz auf diese Objekte im aufrufenden
Programmsegment noch bekannt ist).
Dies geschieht z. B. beim Aufruf der Funktion squareNumbers() in

int[] myArray = squareNumbers(8);

wobei das im Funktionsrumpf erzeugte Array über die Zuweisung der zurückgegebenen Referenz jetzt
über die Arrayreferenzvariable myArray ansprechbar ist.
Dies liegt daran, dass durch new erzeugter Speicherplatz in einem gesonderten Bereich (dem soge-
nannten Heap) angelegt wird, der getrennt von dem Bereich ist, in dem lokale Variable von Funktionen
angelegt werden (dem sogenannten Stack).
148 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

6.1.5 Der Run-Time-Stack

Wir sehen uns jetzt die Organisation der Speicherplatzverwaltung beim Ein- und Austritt in Scope-
blöcke etwas genauer an.
Jeder Scopeblock hat zur Laufzeit ein sogenanntes Environment in Form eines Activation Record mit

1. Einträgen für lokale Identifier (inklusive formale Parameter bei Funktionen),

2. Pointern auf class-Identifier bzw. Identifier aus übergeordneten Blöcken, die nicht im momen-
tanen Block neu definiert werden,

3. der Adresse der Anweisung im übergeordneten Block, mit der nach Verlassen des Blocks wei-
tergemacht wird (Rücksprungadresse).

Beim Eintritt in den Scopeblock werden diese Records mit den entsprechenden Einträgen auf einem
Stack, dem sogenannten Run-Time-Stack, abgelegt.
Die Adressen innerhalb eines Activation Records ergeben sich dann durch die Anfangsadresse des
Records plus dem jeweiligen Offset innerhalb des Records, der zur Compilierzeit bekannt ist.
Zur Einrichtung der Pointer auf Identifier aus übergeordneten Blocks gibt es mehrere Möglichkeiten.
Eine gängige besteht in der Einrichtung eines Zeigers (static pointer), der auf den Record des nächsten
Scopeblocks in der statischen Hierarchie zeigt. Dadurch kann der definierende Scopeblock eines Iden-
tifiers über eine Kette von Pointern erreicht werden.
Neben diesem Run-Time-Stack ist zur Analyse der Aufrufe von Funktionen bzw. des Ein- und Aus-
tritts in Scopeblöcke der sogenannte Aufrufbaum von Bedeutung, der die Aufrufhierarchie zur Laufzeit
darstellt. In Zusammenhang mit der Rekursion (Kapitel 7) wird er auch Rekursionsbaum genannt.
Beide Begriffe sollen nun an folgendem Beispiel erläutert werden.

Beispiel 6.7 (Aufrufbaum und Run-Time-Stack) Betrachte die Applet Klasse in Abbildung 6.5.
Die Scopeblöcke sind mit A (Klassenscope) und B–D (Blöcke aufgrund von Methoden bzw. com-
pound statements) gekennzeichnet. Dabei wird die Methode init() zuerst aufgerufen.
Der Aufrufbaum hat die in Abbildung 6.6 angegebene Gestalt.8 (Durch rekursive Aufrufe kann der
Aufrufbaum im Prinzip unendlich groß werden.)
Wir sehen uns jetzt den Run-Time-Stack an. In Block A werden alle Klassenvariablen und Funktionen
definiert, also double x,y, int z; und die Funktionen f(), g(), und init(). Diese werden nicht
auf dem Run-Time-Stack, sondern als globale Größen in einen anderen Bereich des Speichers, dem
sogenannten Heap abgelegt, vgl. Abbildung 6.7.
Beim Aufruf von init() werden keine lokalen Größen definiert. Im Activation Record wird nur
die Rücksprungadresse der static Pointer abgelegt. Da noch kein übergeordneter Block existiert, fin-
8 Die Wurzel ist dabei der oberste Knoten, und alle gerichteten Kanten verlaufen von “höheren” zu “tieferen” Kno-
ten. Daher verzichtet man bei derart dargestellten Bäumen auf die Angabe der Kantenrichtung durch Pfeile wie sonst bei
gerichteten Graphen üblich.
6.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 149

class RunTimeStack extends Applet { 


double x = 1, y = 2; 


int z = 3; 








void f(double a) {


 


int i = 4;  


 

 
... 





 
x = x + i*a;  

B


System.out.println("B: a = " + a + ", x = " + x 





+ ", y = " + y);

 


 

...

 


 


}








 
void g(int x) { 



 
int y = 5; 







... 







{ 








double i = 6;  
 


 
 


...

  


 
 


f(x);

  



System.out.println("D: i = " + i D 

 A

 
 

+ ", y = " + y + ", x = " + x); 
  

C

 

... 

 





} 







...

 


 

int i = 7;

 


 


f(y);  


 

 
System.out.println("C: i = " + i + ", y = " 





 
+ y + ", x = " + x); 





 
... 







}  





 

void init() {



 

... 
 


 


g(z)

 
 


System.out.println("E: x = " + x + ", y = " E 


 
+ y + ", z = " + z); 





 
... 







} 


}
Abbildung 6.5: Ein Programm mit seinen Scopes.
150 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

E ← Aufruf von init()

C ← Aufruf von g() in init()

D B ← Eintritt in Block D und Aufruf von f() in C

B ← Aufruf von f() in D

Abbildung 6.6: Der Aufrufbaum zum Programm aus Abbildung 6.5.

H EAP x double Wert 1


y double Wert 2
z int Wert 3
f function
g function
init function

Abbildung 6.7: Der Heap zum Programm aus Abbildung 6.5.


6.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 151

det man alle übergeordneten Identifier im Heap (gekennzeichnet durch H EAP im Stack), vgl. Abbil-
dung 6.8.
Der Aufruf g(z) in init() bewirkt den Eintritt in den Block C und die Parameteridentifikation von x
(definiert in C) mit z (global definiert). Da noch kein übergeordneter Block existiert, erübrigt sich die
Einrichtung eines static Pointers. Alle übergeordneten Identifier findet man im Heap (gekennzeichnet
durch H EAP im Stack), vgl. Abbildung 6.8.
 
Rücksprungadresse 1 x int (Wert 3 von z)
E


static pointer: H EAP 
y int (Wert 5)
C

 Rücksprungadresse 2
static pointer: H EAP


Rücksprungadresse 1
E
static pointer: H EAP

Abbildung 6.8: Der Stack nach dem Aufruf von init() (links) und g(z) in init() (rechts).

Abbildung 6.9 beschreibt den Run-Time-Stack beim Eintritt in den Scopeblock D aus Block C (links)
und beim Eintritt in den Scopeblock B aus Block D (rechts).
 

 i double (Wert 6) 
 a double (Wert 3 von x)
i int (Wert 4)
 
D B

 Rücksprungadresse 3 
 Rücksprungadresse 4
static pointer: static pointer: H EAP
 
 

 x int (Wert 3 von z) 
 i double (Wert 6)
y int (Wert 5)
 
C D

 Rücksprungadresse 2 
 Rücksprungadresse 3
static pointer: H EAP static pointer:
 
 
Rücksprungadresse 1 x int (Wert 3 von z)
E


static pointer: H EAP 
y int (Wert 5)
C

 Rücksprungadresse 2
static pointer: H EAP


Rücksprungadresse 1
E
static pointer: H EAP

Abbildung 6.9: Der Stack beim Eintritt in den Scopeblock D aus C (links) und in B aus D (rechts).

Im Statement x = x + i*a in Block B ist also mit x die Klassenvariable x und nicht die an f
übergebene Variable x gemeint, da der static pointer auf den Heap zeigt. Also wird x + i*a zu
1 + 4 ∗ 3 = 13 ausgewertet.
Nach Abarbeitung des Blocks B wird der entsprechende Activation Record auf dem Stack gelöscht.
Die Rücksprungadresse 3 gibt an, wo im Programm weitergemacht wird. Die dann entstehende Situa-
152 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

tion ist in Abbildung 6.10 links angegeben, die nach Abarbeitung von Block D rechts.
 

 i double (Wert 6) 
 x int (Wert 3 von z)
y int (Wert 5)
 
D C

 Rücksprungadresse 3 
 Rücksprungadresse 2
static pointer: static pointer: H EAP
 
 
x int (Wert 3 von z) Rücksprungadresse 1
E



y int (Wert 5) static pointer: H EAP
C

 Rücksprungadresse 2
static pointer: H EAP


Rücksprungadresse 1
E
static pointer: H EAP

Abbildung 6.10: Der Stack nach dem Austritt aus Scopeblock B (links) und aus D (rechts).

Die Situation beim Eintritt in Block B aus Block C ist in Abbildung 6.11 angegeben. Jetzt wird x +
i*a zu 13 + 4 ∗ 5 = 33 ausgewertet. Danach werden B, C und E abgearbeitet und die zugehörigen
Activation Records gelöscht.


 a double (Wert 5 von y)
i int (Wert 4)

B

 Rücksprungadresse 5
static pointer: H EAP



 i int (neu im Scope, Wert 7)


 x int (Wert 3 von y)
C y int (Wert 5)
Rücksprungadresse 2




static pointer: H EAP


Rücksprungadresse 1
E
static pointer: H EAP

Abbildung 6.11: Der Stack nach dem Eintritt in Scopeblock B aus C.

Das Programm schreibt gemäß der Abarbeitung der Scopeblöcke in der Reihenfolge B – D – B – C –
E mit den println Anweisungen die Zeilen

B: a = 3.0, x = 13.0, y = 2.0


D: i = 6.0, y = 5, x = 3
B: a = 5.0, x = 33.0, y = 2.0
C: i = 7, y = 5, x = 3
E: x = 33.0, y = 2.0, z = 3
6.2. MODULARE ABSTRAKTION 153

auf den Bildschirm. Sie geben die Werte der gerade sichtbaren Variablen im jeweiligen Block an
dieser Stelle an.

Aus den Regeln der Abarbeitung von Scopeblöcken ergibt sich bezüglich des Aufrufbaumes und des
Stacks folgender Satz.

Satz 6.1 (Eigenschaften des Run-Time-Stack und des Aufrufbaumes)


1. Der Aufrufbaum wird in der Reihenfolge LRW (linker Teilbaum vor rechter Teilbaum vor Wur-
zel) abgearbeitet.

2. Die maximale Anzahl von Activation Records auf dem Run-Time-Stack, die ein Maß für die
Größe des zur Laufzeit beanspruchten Speicherplatzes darstellt, ist gleich der Höhe des Aufruf-
baumes + 1.9

Im Zusammenhang mit der Rekursion (Kapitel 7) nennt man die Zahl

Höhe(Aufrufbaum) + 1

auch die Rekursionstiefe.

6.2 Modulare Abstraktion

Die durch Funktionen gewonnene Abstraktion beschränkt sich weitgehend auf das Input-Output Ver-
halten von Programmteilen, also auf den Datenfluss. Oft möchte man jedoch weiter gehen und auch
ganze Datenstrukturen mit mehreren zugehörigen Variablen, Funktionen und Typen abstrahieren, so
wie bei den in Kapitel 4 besprochenen Datenstrukturen.
Dies geschieht in vielen Programmiersprachen in sogenannten Modulen. Sie stellen Verallgemei-
nerungen von Funktionen dar, indem sie eine Kollektion miteinander zusammenhängender Objekte
(z. B. mehrere Funktionen, Typen, Konstanten, Variable) zu einer separat compilierten Einheit zu-
sammenfasst. Dies ist schematisch in Abbildung 6.12 dargestellt.
 
  
 Modul Variable 

 Anweisungen  



 Modul Variable 


lokale Variable Funktion
   
Funktion f Modul M

 lokale Variable 
 
 Funktion 

... Funktion
  
 


 


...

Abbildung 6.12: Funktion versus Modul.

Module sind so konzipiert, dass andere Module oder Programme Teile oder das Modul als Ganzes
nutzen können. Man nennt die Nutzer Klienten des genutzten Moduls.
9 Die Höhe eines Baumes ist die maximale Kantenzahl auf einem Weg von der Wurzel bis zu einem Blatt.
154 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

Beispiele für Module sind Bibliotheken in C oder Implementationen von abstrakten Datentypen. Mo-
dule werden meist beschrieben durch eine Spezifikation, die das Verhalten und die Eigenschaften des
Moduls festlegt. Diese wird getrennt von der Implementation, die die zugehörigen Programme enthält.
Die Spezifikation ist der öffentliche (public) Teil des Moduls. Klienten können (oder sollten) nur die
dort deklarierten Begriffe nutzen.10 Im privaten (private) Teil sind die Rümpfe der Funktionen und
zusätzliche private Variable enthalten, die nach außen verborgen bleiben (sollten). Man spricht daher
auch von Einkapselung (encapsulation).
Wirksame encapsulation setzt voraus, dass die Programmiersprache getrennte Kompilation von Pro-
grammteilen erlaubt oder Konstrukte ermöglicht, die Daten als privat erklären.
Getrennte Compilierung hat viele Vorteile:

1. Module sind wiederverwendbar (reusable, off-the-shelf components).


2. Module können bei Änderung rekompiliert werden, ohne die Klienten rekompilieren zu müssen.
3. Klienten können geändert werden, ohne das Modul ändern zu müssen.
4. Module können nur als Objektcode zur Verfügung gestellt werden, so dass Details der Imple-
mentation verborgen bleiben (encapsulation).

6.3 Abstraktion durch Klassen

Java bietet ein eigenes Konstrukt zur Abstraktion von Datentypen mit den zugehörigen Operationen:
Klassen. Eine Klasse (class) ist ein durch den Programmierer definierter strukturierter Typ. Seine
Komponenten heißen class members. Dies können Variablen, Funktionen und auch wieder Klassen
sein. In der Java Terminologie werden sie Klassenvariablen oder Felder, Methoden bzw. innere Klas-
sen genannt. Da Klassen Typen sind, kann man Variable dieses Typs definieren. Jedes Objekt hat
dann (im Prinzip11 ) als Komponenten alle Felder und Methoden der Klasse (und aller Oberklassen,
von denen sie abgeleitet wird, vgl. Abschnitt 6.3.3.

6.3.1 Definition von Klassen

Wir betrachten zunächst die eingeschränkte Definition

class KlassenName {
Def 1 ;
Def 2 ;
..
.
Def r ;
}
10 Sprachabhängig kann auch der Zugriff auf private Daten möglich sein. Er sollte jedoch unterbleiben.
11 Ausnahmen sind als static deklarierte Felder und Methoden, vgl. dazu Abschnitt 6.3.2.
6.3. ABSTRAKTION DURCH KLASSEN 155

Dabei ist class das Schlüsselwort, das die Klassendefinition einleitet, und Def 1 , . . . ,Def r sind Defini-
tionen von Variablen (Feldern der Klasse), Funktionen (Methoden der Klasse) oder inneren Klassen,
vgl. Abschnitt 6.3.8.
Unter den Methoden sind Konstruktoren von besonderer Bedeutung. Sie haben immer denselben Na-
men wie die Klasse und folgende Form

KlassenName(formale Parameterliste ){. . .}

Es gibt weder einen expliziten Rückgabetyp, noch das Schlüsselwort void. Im gewissen Sinne ist der
Name des Konstruktors der Rückgabetyp.
In der Regel haben Klassen mehrere Konstruktoren mit verschiedenen Parameterlisten. Der Aufruf
eines Konstruktors erfolgt mit new. Er erzeugt ein neues Objekt der Klasse und gibt eine Referenz auf
dieses Objekt als Wert zurück.
Wir betrachten diese Begriffe an einer Variation der auf Seite 142 definierten Klasse IntPair.

Programm 6.1 IntPair


class IntPair {
int first;
int second;

IntPair(int x, int y) {
first = x; second = y;
}

int sum() {
return first + second;
}
}

Diese Klasse hat 2 Felder first und second, eine Methode sum und einen Konstruktor IntPair.
Durch

IntPair xy = new IntPair(3, 7);

wird ein neues Objekt dieser Klasse erzeugt, dessen Felder first und second die Werte 3 und 7
haben.
Der Zugriff eines Objektes auf seine Felder und Methoden erfolgt durch den . gemäß object.feld
bzw. object.methode().

xy.first = -4;

ändert also den Wert von first zu -4,


156 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

xy.second = xy.sum();

ändert den Wert von second zum Ergebnis des Aufrufes von sum() für das Objekt xy, also zu −4 +
7 = 3.
Der Zugriff eines Objektes auf eine Methode übergibt der Methode implizit eine Referenz auf das Ob-
jekt als ersten Parameter. Diese implizite Referenz kann innerhalb der Klasse durch die Variable mit
dem reservierten Namen this angesprochen werden. Die Definition von sum in der Klasse IntPair
hätte also auch als

int sum() {
return this.first + this.second;
}

geschrieben werden können. Außerhalb der Klasse des Objektes ist dies jedoch nicht möglich.
Die Verwendung von this gehört zur Namenskonvention von Java, insbesondere in Konstruktoren.
Dort sollen nach Konvention die Parameter, mit denen Felder gesetzt werden, dieselben Bezeichner
wie die Feldnamen bekommen. Die Unterscheidung zwischen Parametern und Feldern ist dann nur
mit this möglich. Bei Befolgung der Namenskonvention wird der Konstruktor der Klasse IntPair
zu

IntPair(int first, int second) {


this.first = first;
this.second = second;
}

Klassen können auch ohne die Definition von Konstruktoren geschrieben werden. Dann verfügen sie
automatisch über den sogenannten leeren Konstruktor oder Default Konstruktor, der keine Argumente
hat. Dies gilt jedoch nicht mehr, sobald ein Konstruktor definiert wird. Da Default Konstruktoren für
die Vererbung (vgl. Abschnitt 6.3.3) immens wichtig sind, sollte man Klassen, die Konstruktoren
haben, immer zusätzlich mit einem Default Konstruktor ausstatten.
In der Klasse IntPair wäre folgender Default Konstruktor sinnvoll, der das Paar (0,0) erzeugt.

public IntPair() {
first = 0;
second = 0;
}

Oft ist auch ein Copy Konstruktor sinnvoll. Ein solcher Konstruktor erstellt eine identische Kopie
des ihm übergebenen Objektes derselben Klasse. Wir erläutern es an der Klasse IntPair:

public IntPair(IntPair xy) {


first = xy.first;
second = xy.second;
}
6.3. ABSTRAKTION DURCH KLASSEN 157

Statt eines Copy Konstruktors kann man auch das Interface Cloneable implementieren, siehe Ab-
schnitt 6.3.7.

6.3.2 Static-Felder und Methoden

In manchen Situationen möchte man Felder oder Methoden für die Klasse als Ganzes anlegen. Dies
ist z. B. sinnvoll bei der Definition von Konstanten, die für alle Objekte der Klasse gleich sind, oder
Methoden, die unabhängig von den Objekten der Klasse sind.
Dies kann erreicht werden durch Verwendung des Modifizierers static vor der Definition.
In der Klasse Integer wird etwa der maximale Wert MAX VALUE definiert als12

public static final int MAX_VALUE = Ox7fffffff;

Ebenso sind in der Klasse Math von mathematischen Funktionen alle Methoden als static definiert,
etwa

public static native double sin(double a);

für die Sinus-Funktion.


static Methoden und Felder können nicht auf Instanzen der Klasse zugreifen. Die Benutzung von
static Methoden und Feldern innerhalb der Klasse geschieht über ihre Namen, außerhalb können
sie (sofern sichtbar) durch Nennung des Klassennamens und den . angesprochen werden, also etwa

myColor = Color.orange;

für die static Konstante orange der Klasse Color, und

x = Math.max(y, z);

für die static Funktion max der Klasse Math.


Der etwas seltsame Name static ist historisch bedingt und meint den Gegensatz zu dynamic. Iden-
tifier ohne den Zusatz static sind automatisch dynamic. Für sie wird Speicherplatz bei Betreten des
Scopeblocks eingerichtet (vgl. Abschnitt 6.1.5) und nach Verlassen des Scopeblocks wieder vernich-
tet. Bei als static deklarierten Identifiern bleibt dieser Speicherplatz auch zwischen Verlassen und
erneutem Wiedereintritt in den Scopeblock erhalten. Sie sind in diesem Sinne nicht dynamic“.

6.3.3 Unterklassen und Vererbung

Klassen können andere Klassen erweitern. Die neue, erweiterte Klasse heißt Unterklasse oder Sub-
klasse, die vorgegebene Klasse heißt Oberklasse. Man sagt auch, dass die Unterklasse von der Ober-
klasse abgeleitet wird. Die Syntax hierfür ist
12 Ox7fffffff ist Hexadezimalnotation. Die entsprechende Dezimalzahl ist 2.147.483.647 = 231 − 1.
158 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

class NameUnterklasse extends NameOberklasse {...}

Die Unterklasse hat dabei Zugriff auf alle Felder und Methoden der Oberklasse, diese werden ge-

erbt“. Die Unterklasse darf Methoden und Felder der Oberklasse neu definieren und zusätzliche Me-
thoden und Felder einführen.
Hierdurch entstehen ganze Hierarchien von Klassen13 . Die Java Bibliothek liefert viele Beispiele
hierfür. Wir haben dieses Prinzip schon in der Applet-Programmierung verwendet, alle Klassen waren
Erweiterungen der Oberklasse Applet.
Alle Klassen von Objekten sind in Java Unterklassen der Klasse Object. Dadurch lassen sich Daten-
strukturen für Objekte sehr allgemein definieren, vgl. die Beispiele Stack und Liste in Abschnitt 4.7
und Abschnitt 4.7.
Für den Zugriff auf Felder und Methoden der Oberklasse dient die Referenz super. Entsprechend
können Konstruktoren der Superklasse mit super(...) und den entsprechenden aktuellen Parame-
tern angesprochen werden. Hierzu ein Beispiel14 :
Die Klasse

class Square {
double width;

Square(double width) {
this.width = width;
}

double area() {
return width * width;
}
}

wird erweitert durch die Klasse

class Rectangle extends Square {


double height;

Rectangle(double width, double height) {


super(width);
this.height = height;
}
13 In Java 1.1.4 gab es 21 Pakete mit 503 vordefinierten Klassen und ca. 5000 Methoden, in Java 1.3 gab es bereits 76
Pakete mit 1841 Klassen und ca. 20000 Methoden. Wieviele gibt es in Java 1.4?
14 das auch zeigt, dass die logische Hierarchie (Quadrat ist “Unterklasse” von Rechteck) nicht mit der Vererbungshierar-

chie übereinstimmen muss. Bei der Vererbung bedeutet Spezialisierung immer Hinzufügen bzw. Überschreiben von Feldern
und Methoden.
6.3. ABSTRAKTION DURCH KLASSEN 159

double area() {
return width * height;
}
}

Im Konstruktor Rectangle wird mit super(width) der Konstruktor von Square aufgerufen, ei-
ne Zuweisung super.width = width statt dieses Aufrufes resultiert (da kein expliziter Konstruk-
toraufruf der Klasse Square erfolgt) in einen impliziten Aufruf von super(), also Square(). Ein
solcher Konstruktor existiert aber nicht in der Klasse Square, so dass der Compiler einen Fehler
meldet. 15
Die Methode area() wird in der Klasse Rectangle überschrieben. Für beide Klassen steht daher
derselbe Name für die (unterschiedliche!) Berechnung der Fläche zur Verfügung.
Um jetzt (z. B. in einer umfangreichen Graphik) verschiedene Quadrate und Rechtecke abzuspeichern,
kann man ein Array

Square[] vec = new Square[n];

definieren. Da jedes rectangle Objekt durch die Vererbung auch vom Typ Square ist, können
gleichzeitig Rechtecke und Quadrate im Array verwaltet werden, also etwa

vec[0] = new Square(1);


vec[1] = new Rectangle(2, 3);

Der Durchlauf

for (int i = 0; i < vec.length; i++) {


System.out.println(vec[i].area());
}

schreibt dann nacheinander die Fläche der Rechtecke und Quadrate auf den Bildschirm. Dabei wird
automatisch die richtige area() Methode gewählt!
Will man als Programmierer die Klasse eines Objektes ermitteln, so geht dies mit der Methode
getClass() der Klasse Object. Diese Methode liefert eine Referenz auf ein Objekt der Klasse
Class zurück, das u. a. den Namen der Klasse des betrachteten Objektes enthält, und zwar mit dem
package Präfix der Klasse (vgl. Abschnitt 6.3.4).

Object o = new Object();


String str = o.getClass().getName();

weist also der Variablen str den Wert "java.lang.Object" zu. Im obigen Beispiel schreibt die
Anweisung
15 Es ist daher guter Programmierstil, jede Klasse mit einem parameterlosen Defaultkonstruktor zu versehen.
160 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

for (int i = 0; i < 2; i++) {


System.out.println(vec[i].getClass().getName());
}

nacheinander Square und Rectangle auf den Bildschirm. Der package Präfix entfällt hier, da die
Klassen Square und Rectangle keinem Package angehören.

6.3.4 Packages

Ein Programm in Java ist eine Menge von Dateien der Form Klassenname.java, wobei Klassenna-
me.java genau den Programmtext der Klasse Klassenname enthält16 . Die Funktionsweise des Pro-
gramms wird durch die Interaktion der Klassen (Verwendung der Methoden aus einer anderen Klasse,
Vererbung, usw.) festgelegt.
Um Klassen zu ähnlich gelagerten Aufgaben zusammenfassen zu können, gibt es die Möglichkeit zur
Definition von Paketen (packages). Ein package ist eine Menge von Klassen in einem gemeinsamen
Verzeichnis. Der Pfad zu diesem Directory stimmt mit der Bezeichnung des packages überein. Das
package java.lang liegt (in einem UNIX System) in einem Verzeichnis .../java/lang relativ zu
den durch die Environment Variable CLASSPATH festgelegten Verzeichnis-Pfaden.
Das Herstellen eigener Packages geschieht mit der Anweisung

package PackageName;

vor jeder Klassendefinition des Packages, also z. B. mit

package java.awt;
public class TextField extends TextComponent {
..
.
}

bei der Definition der Klasse TextField des Paketes java.awt.

6.3.5 Sichtbarkeit von Klassen, Methoden und Feldern

Damit Klassen in einem Java Programm interagieren können, muss die Sichtbarkeit der Namen nach
außen festgelegt werden. Gutes Softwaredesign macht nur ausgewählte, wohl überlegte Methoden
nach außen sichtbar, verbirgt aber alle Methoden und Felder, die nur als interne Hilfsmittel dienen.
Zur Regelung der Sichtbarkeit zwischen Klassen dienen die Modifizierer public, protected und
private, die der Definition vorangestellt werden. Dabei darf nur einer dieser Modifizierer auftreten.
Sie haben folgende Bedeutung.
Sichtbarkeitsmodifizierer für Felder und Methoden:
16 bis auf zusätzliche Klassen, die nach außen nicht sichtbar sind, vgl. Abschnitt 6.3.8.
6.3. ABSTRAKTION DURCH KLASSEN 161

public Feld ist überall sichtbar (Klasse muss ebenfalls public sein).
leer Default, Feld ist in dem Package sichtbar.
protected Wie Default, und das Feld ist auch in den Subklassen anderer
Packages sichtbar, die aus dieser Klasse abgeleitet wurden
(protected ist also de facto weniger geschützt als der Default!).
private Feld ist nur in dieser Klasse sichtbar.

Klassen haben als Sichtbarkeitsmodifizierer nur

public Klasse ist in anderen Packages sichtbar.


leer Default, Klasse ist in dem Package sichtbar.

6.3.6 Weitere Modifizierer

Für Felder einer Klasse wird die Art der Verwendung durch folgende Modifizierer festgelegt:

static Eins pro Klasse, nicht eins pro Objekt (vgl. Abschnitt 6.3.2).
final Wert kann nicht verändert werden.
transient Solche Felder werden nicht mit dem Objekt abgespeichert.
Reserviert für zukünftige Verwendung.
volatile Diese Daten können an verschiedene Steuerthreads übergeben werden,
so dass das Laufzeitsystem Lesen und Beschreiben solcher Felder
synchronisieren muss.

Bei Methoden unterscheidet man

final Kann nicht überschrieben werden.


static Eine pro Klasse, nicht eine für jedes Objekt.
abstract Muss überschrieben werden (um einen Nutzen zu haben).
native Nicht in Java geschrieben (kein Rumpf, sonst aber normal und
vererbbar, statisch usw.). Der Rumpf wird in einer anderen Sprache
geschrieben. Hierzu dient das JNI (Java Native Interface),
das in einer eigenen JNI-Specification festgelegt ist.
synchronized Es kann in dieser Methode jeweils nur ein Thread ausgeführt werden.
Der Zugriff auf diese Methode wird überwacht (vgl. Threads in der
Übung).

Bei Klassen gibt es schließlich

final Klasse kann nicht erweitert werden.


abstract Klasse muss erweitert werden, wobei alle abstrakten Methoden überschrieben
werden müssen.

Sinnvolle Kombinationen dieser Modifizierer wie


162 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

native private static ...

sind möglich.

6.3.7 Interfaces

Java sieht keine Mehrfachvererbung vor, so dass eine Klasse nur eine andere erweitern kann. Um den-
noch Methoden aus weiteren Klassen benutzen zu können, stellt Java Interfaces bereit. Ein Interface
wird wie eine Klasse definiert

Modifizierer interface InterfaceName {...}

und kann mehrere andere Interfaces erweitern. Im Gegensatz zu Klassen haben Interfaces keine Kon-
struktoren, sondern nur statische Konstanten und abstrakte Methoden. Bei den Methoden wird also
lediglich der Methodenkopf angegeben und mit einem Semikolon beendet.
Klassen können Interfaces über das Schlüsselwort implements nutzen, und zwar mehrere Interfaces
gleichzeitig.

class myClass extends Applet


implements ActionListener { ... }

Dies bedeutet, dass die Methodennamen aus dem Interface zur Verfügung stehen, die Methoden aber
alle noch in der Klasse implementiert werden müssen, d. h. die Methodenköpfe werden mit einem
Rumpf versehen.
Interfaces schreiben also Namen und Parameterlisten für Methoden vor, die Implementation muss
jedoch in der Klasse erfolgen.
Wir erläutern dies am Beispiel der Klasse Stack aus Abschnitt 4.7.

/**
* The <code>StackInterface</code> defines an interface
* for a stack of objects.
*
* @see ListNode
*/
public interface StackInterface {
/**
* Tests if this stack has no entries.
*
* @return <code>true</code> if the stack is empty;
* <code>false</code> otherwise
*/
boolean isEmpty();
6.3. ABSTRAKTION DURCH KLASSEN 163

/**
* Return the value of the current node.
* @throws NoSuchElementException
*/
Object top() throws NoSuchElementException;

/**
* Inserts a new stack node at the top.
*
* @param <code>someData</code> the object to be added.
*/
void push(Object someData);

/**
* Delete the current node from the list.
* @throws NoSuchElementException
*/
void pop() throws NoSuchElementException;
}

Man beachte, dass die Methoden eines Interfaces implizit public und abstract (sofern nicht final)
sind. Diese Modifizierer brauchen also nicht hinzugefügt werden.
Die Implementation des Stacks auf Seite 91) mit diesem Interface geschieht dann wie folgt:

public class Stack implements StackInterface {


// Hinzufügen von Feldern für die Implementation
// Implementation der Methoden
// Hinzufügen von Konstruktoren
}

Die Klassenbibliothek von Java macht ausführlich von Interfaces Gebrauch. Ein bereits genanntes
Beispiel ist das Interface ActionListener (vgl. Abschnitt 6.3.8, andere sind Cloneable, Comparable
und Runnable. Diese definieren die Methoden clone() (vgl. Abschnitt 5), compareTo() (für Ver-
gleiche von Objekten) und run() (für nebenläufige Prozesse in Form von Threads).
Wir geben ein Beispiel für Cloneable mit der Klasse IntPair aus Abschnitt 6.3.1:

import java.lang.Cloneable;

class IntPair implements Cloneable {


...

public Object clone() {


164 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

return new IntPair(this.first, this.second);


}
}

Die überschriebene Methode clone() gibt ein allgemeines Objekt zurück und muss daher mit casting
verwendet werden:

IntPair p = new IntPair(1,2);


IntPair q = (IntPair) p.clone();

6.3.8 Klassen in Klassen

Klassen können als Komponenten außer Datenfeldern und Methoden auch Klassen haben. Sie werden
wie Datenfelder oder Methoden über den . angesprochen und können Modifizierer haben. Sind sie
nicht static, so werden sie innere Klassen genannt.
Klassen können ferner (wie lokale Variable) lokal in Methoden verwendet werden. Sie heißen dann
lokale Klassen. Sie werden wie gewöhnliche Klassen deklariert.
Bezüglich der Sichtbarkeit von Klassen in Klassen gelten die gleichen Scope-Regeln wie beim Klas-
senscope bzw. Blockscope. Zusätzlich können Komponentenklassen, die public sind, von anderen
Klassen importiert und genutzt werden.
Müssen lokale Klassen nicht über einen Klassennamen angesprochen werden, so kann man sie als
anonyme Klassen direkt nach der Angabe einer Oberklasse oder eines Interfaces definieren, ohne
sie zu benennen. Wir haben hiervon bereits regen Gebrauch bei den ActionListenern gemacht, siehe
Abschnitt 6.3.9.

6.3.9 Implementationen des Interface ActionListener

Wir erläutern jetzt Varianten der Implementation des Interface ActionListener. Zur Illustration
benutzen wir das Applet Temperatur (Abschnitt 2.1).
Die dort benutzten Anweisungen

TextField input = new TextField(10);


input.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// perform the action
calculateTemperature();
}
});

zur Anbindung eines ActionListeners lassen sich jetzt wie folgt erklären.
ActionListener ist ein Interface, das als einzige Methode die abstrakte Methode
6.3. ABSTRAKTION DURCH KLASSEN 165

public void actionPerformed(ActionEvent e);

enthält. Die Methode

public void addActionListener(ActionListener l) {...}

der Klasse TextField verlangt die Angabe eines ActionListener-Objektes l. Dieses geschieht
mit new ActionListener(). Da ActionListener ein Interface ist, braucht man eine Klasse, die
“implements ActionListener” durchführt, d. h. die Methode actionPerformed(ActionEvent)
definiert.
Genau dies leistet die anonyme Klasse

{
public void actionPerformed(ActionEvent e){
// perform the action
calculateTemperature();
}
}

Natürlich könnte man den ActionListener auch in einer eigenen Klasse implementieren, oder durch
das Applet implementieren lassen. Wir betrachten zunächst eine Implementation als innere Klasse:

public class Temperatur extends Applet {


...

public void init() {


...
TextField input = new TetField(10);
input.addActionListener(new MyActionListener());
...
}

class MyActionListener extends ActionListener {


public void actionPerformed(ActionEvent e) {
// perform the action
calculateTemperature();
}
}

public void calculateTemperature() {


...
}
}
166 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

Hier ist MyActionListener als innere Klasse auf Klassenniveau definiert, unterliegt also dem Klas-
senscope und kann daher an beliebiger Stelle definiert werden. Es wäre auch möglich, sie als lokale
Klasse direkt innerhalb der Methode init() zu definieren; dann würde sie jedoch dem Blocksco-
pe unterliegen und muss vor der Anweisung input.addActionListener(...) erfolgen, da sonst
MyActionListener() nicht bekannt ist.
Die Anweisung

new MyActionListener()

ruft den Default Konstruktor der inneren Klasse MyActionListener auf. Dieser musste nicht defi-
niert werden, da Klassen ohne Definition von Konstruktoren automatisch über den Default Konstruk-
tor verfügen.
Natürlich ist auch die Implementation des Interface ActionListener als eigene (nicht innere) Klasse
MyActionListener möglich. Dann ist jedoch der Informationstransfer schwieriger zu gestalten. Da
die Methode actionPerformed() in der Klasse MyActionListener angesiedelt ist, muss man dem
einem Konstruktor der Klasse MyActionListener zumindest die TextFields input und output des
Applets Temperatur übergeben, damit die Methode actionPerformed() auf sie zugreifen kann.
Wir lösen dies, indem wir das gesamte Applet Temperatur übergeben.
Innerhalb der Klasse Temperatur erfolgt die Anbindung des ActionListener an das TextField input
mit der Anweisung

input.addActionListener(new MyActionListener(this));

die einen Konstruktor der Klasse MyActionListener aufruft, dem ein Objekt der Klasse Temperatur
übergeben werden kann. Die Klasse MyActionListener sieht dann folgendermaßen aus.

import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

class MyActionListener implements ActionListener {

private Temperatur tempApplet;

public MyActionListener(Temperatur tempApplet) {


this.tempApplet = tempApplet;
}

public void actionPerformed(ActionEvent e) {


// perform the action
calculateTemperature();
}
6.3. ABSTRAKTION DURCH KLASSEN 167

// process user’s action on the input text field


public void calculateTemperature() {

// get input number


double fahrenheit =
Double.parseDouble(tempApplet.input.getText());

// calculate celsius and round it to 1/100 degrees


double celsius = 5.0 / 9 * (fahrenheit - 32);
// use Math class for round
celsius = Math.round(celsius * 100);
celsius = celsius / 100.0;

// show result in textfield output


tempApplet.output.setText(Double.toString(celsius));
}
}

Die Methode calculateTemperature() ist jetzt in dieser Klasse, die Klasse Temperatur enthält nur
die Methode init() und auch nicht mehr die Variablen double und fahrenheit.
Zum Abschluss betrachten wir die Implementation des ActionListener durch das Applet Temperatur
selbst. Dann muss die Klasse Temperatur das Interface ActionListener implementieren und daher
die Methode actionPerformed() definieren.

public class Temperatur extends Applet implements ActionListener {


...

public void init() {


...
input = new TextField(10);
// register this applet as ActionListener for
// TextField input
input.addActionListener(this);
...
}

// duties of this Applet as ActionListener


public void actionPerformed(ActionEvent e) {
// body of method calculateTemperature()
}
}
168 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

6.3.10 Der Lebenszyklus von Objekten

Objekte werden durch Aufruf von Konstruktoren von Klassen mit new erzeugt und auf dem Heap
angelegt, unterliegen also nicht der Speicherverwaltung auf dem Run-Time-Stack. Hierdurch ist es
möglich, auch innerhalb von Methoden Objekte zu erzeugen und sie z. B. durch Rückgabe einer
Referenz (wie in squareNumbers auf Seite 140) an den aufrufenden Programmteil zu übergeben.
Objekte bleiben so lange erhalten, wie es eine Referenz auf sie gibt. Das Java Run-Time-Environment
überwacht dies und stellt den Speicherplatz, der durch ein nicht mehr referenziertes Objekt belegt
wird, wieder zur Verfügung. Dieser Automatismus erlaubt jedoch keinen Einfluss auf den Zeitpunkt
der Rückgabe.
Die Überwachung und Rückgabe nicht mehr referenzierter Objekte bezeichnet man in allen Program-
miersprachen als Garbage Collection. Java hat also eine eingebaute Garbage Collection, um die sich
der Programmierer nicht kümmern muss.
Der Nachteil dieses Automatismus besteht in gewissen Einbußen an Laufzeit, die zudem zeitlich un-
kontrollierbar auftreten können. Hat man jedoch gerade mal Zeit für die Garbage Collection, so kann
man sie mit der Methode

System.gc();

starten.

6.4 Beispiele von Klassen

6.4.1 Bruchrechnung

Die folgende Klasse Fraction (vgl. auch Abschnitt 4.2) stellt Datenstrukturen und Methoden zum
Rechnen mit Brüchen zur Verfügung.

import java.lang.Cloneable;

/**
* The <code>Fraction</code> class implements fractions.
* Each fraction is a pair numerator/denominator of longs
* in simplified form, i.e. gcd(numerator,denominator) = 1
*/
public class Fraction implements Cloneable {

/**
* the numerator
*/
private long num;
6.4. BEISPIELE VON KLASSEN 169

/**
* the denominator. It is always > 0
*/
private long denom;

/**
* Default constructor, constructs 0 as fraction 0/1
*/
public Fraction() {
num = 0;
denom = 1;
}

/**
* Constructs Fraction object from a long
* @param a yields the fraction a/1
*/
public Fraction(long a) {
num = a;
denom = 1;
}

/**
* Constructor with two long argument num and denom,
* constructs the fraction num/denom and simplifies it.
* @param num the numerator
* @param denom the denominator
* Throws <code>ArithmeticException</code> if
* <code>denom == 0</code>
*/
public Fraction(long num, long denom) throws ArithmeticException{
if (denom == 0) throw new ArithmeticException(
"Division by zero in constructor" );
else {
this.num = num;
this.denom = denom;
this.simplify();
}
}

/**
* simplifies this fraction
*/
private void simplify() {
170 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

if (num == 0) {
this.num = 0;
this.denom = 1;
} else {
long gcd = gcd(this.num, this.denom);
this.num = this.num/gcd;
this.denom = this.denom/gcd;
if ( this.denom < 0 ) {
this.num = -this.num;
this.denom = -this.denom;
}
}
}

/**
* Calculates the greatest common divisor of |a| and |b|
* @param a
* @param b
* @return the greatest common divisor of
* |<code>a</code>| and |<code>b</code>|
*/
private static long gcd(long a, long b) throws ArithmeticException {
if ( a == 0 || b == 0 ) throw new ArithmeticException(
"Zero argument in gcd calculation");
a = Math.abs(a);
b = Math.abs(b);
while (a != b) {
if (a > b) a = a - b;
else b = b - a;
}
return a;
}

/**
* Returns a string representation of this fraction.
* @return fraction in the form "num/denum"
*/
public String toString() {
return Long.toString(this.num) + "/"
+ Long.toString(this.denom);
}

/**
* Returns the double value of this fraction.
6.4. BEISPIELE VON KLASSEN 171

* @return num/denum
*/
public double doubleValue() {
return (double) this.num / (double) this.denom;
}

/**
* Checks equality with other fraction r.
* @param r the fraction to be compared with
* @return <code>true</code> if this fraction equals <code>r</code>.
*/
public boolean equals(Fraction r) {
return (this.num == r.num) && (this.denom == r.denom);
}

/**
* Get the nominator of this fraction
* @return the numerator of this fraction
*/
public long getNumerator() {
return num;
}

/**
* Get the denominator of this fraction
* @return the denominator of this fraction
*/
public long getDenominator() {
return denom;
}

/**
* Multiplies this fraction with other fraction r and
* simplifies the result.
* @param r the fraction to be multiplied with.
*/
public void multiply(Fraction r) {
this.num = this.num * r.num;
this.denom = this.denom * r.denom;
this.simplify();
}

/**
* Adds fraction r to this fraction and
172 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

* simplifies result.
* @param r the fraction to be added.
*/
public void add(Fraction r) {
// use a/b + c/d = (ad + cb)/bd and simplify()
this.num = this.num * r.denom + r.num * this.denom;
this.denom = this.denom * r.denom;
this.simplify();
}

/**
* clones this fraction by implementing Cloneable
*/
public Object clone() {
return new Fraction(this.num, this.denom);
}
}

Die Folge

System.out.println( "Demo der Klasse Fraction:" );


Fraction r, s, t;
r = new Fraction(3, 8);
s = new Fraction(1, 6);
double x = r.doubleValue();
System.out.println( "Wert von " + r.toString() + " ist " + x );
t = (Fraction) r.clone();
t.add(s);
System.out.println(r.toString() + " + "
+ s.toString() + " = " + t.toString());
r = new Fraction(3, 4);
t = (Fraction) r.clone();
t.multiply(s);
System.out.println(r.toString() + " * "
+ s.toString() + " = " + t.toString());
System.out.println(r.toString() + " == " + s.toString()
+ " ergibt " + r.equals(s));

von Anweisungen schreibt dann

Demo der Klasse Fraction:


Wert von 3/8 ist 0.375
3/8 + 1/6 = 13/24
6.4. BEISPIELE VON KLASSEN 173

3/4 * 1/6 = 1/8


3/4 == 1/6 ergibt false

auf den Bildschirm.

6.4.2 Erzeugung von Zufallszahlen

Zufallszahlen sind ein Standardwerkzeug für die Simulation vieler technischer Abläufe. Die Aufgabe
eines Zufallszahlengenerators ist es, wiederholt (d. h. in der Regel sehr lange Folgen von) Zahlen im
Interval [0, 1] zu generieren, die den Charakter zufälliger Ziehungen haben.17
Erfahrungen (und Überlegungen der Wahrscheinlichkeitstheorie) zeigen, dass sich mit der Funktion

f (x) = (a · x) mod m

mit a = 16807 und m = 231 − 1 “gute” Zufallszahlen generieren lassen. Man startet mit beliebigem
x0 ∈ {0, 1, . . . , m − 1} (der sogenannten seed) und erzeugt gemäß xn+1 = f (xn ) eine Folge

x0 , x1 , x2 , . . . , xn , xn+1 , . . .

von Zahlen aus {0, 1, . . . , m − 1}. Die zugehörige Folge


xn
xn0 := , n = 0, 1, 2, . . .
m
liefert dann “Zufallszahlen” im Interval [0, 1].
Diese Folge ist natürlich bei festem Startwert x0 alles andere als zufällig, da man alle Werte berechnen
kann. Außerdem wird irgendwann ein Wert xr zum zweiten mal auftreten und die Folge wird sich von
da ab wiederholen. Man spricht daher auch von Pseudozufallszahlen. Dennoch verhalten sich lange
Anfangsstücke dieser Folge angenähert zufällig, so dass man sie in Simulationen gut nutzen kann.
Die unten stehende Klasse implementiert Generatoren für Zufallszahlen als Objekte einer Klasse
RandomNumber. Die Konstruktoren dieser Klasse erlauben entweder das Setzen der Startzahl x0 , oder
einen “zufälligen” Start, indem x0 als Systemzeit genommen wird. Als Methoden haben die Objek-
te das Erzeugen der nächsten Zufallszahl aus dem Intervall [0, 1] mit nextDoubleRand(), bzw. mit
nextIntRand(int,int) das Erzeugen einer zufälligen gleichverteilten ganzen Zahl aus dem Be-
reich {lo, lo+1 . . . , hi}.

/**
* The <code>RandomNumber</code> class offers facilities
* for pseudorandom number generation.
* <p>
* An instance of this class is used to generate a stream of
* pseudorandom numbers. The class uses a long seed, which is
17 Genauer, im Intervall [0, 1] gleichverteilt sind. Teilt man also [0, 1] in n gleichlange Teilintervalle und erzeugt man

N  n2 Zufallszahlen, so sollten in jedes Teilintervall ungefähr gleich viele (also ∼ N/n) Zufallszahlen fallen.
174 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

* modified using a linear congruential formula. See <ul>


* <li>Donald Knuth, <i>The Art of Computer Programming,
* Volume 2</i>, Section 3.2.1. for general information about
* random number gerneration and
* <li>S. Park and K. Miller, Random number generators: Good
* ones are hard to find, <i>Comm. ACM</i> 31 (1988) 1192-1201
* for the specific one implemented here.
* </ul>
* @see java.util.Random
* @see java.lang.Math#random()
*/
public class RandomNumber {
private static final long MULTIPLIER = 16807;
private static final long MODULUS = 2147483647;
// Quotient of MODULUS / MULTIPLIER
private static final long QUOT = 127773;
// Remainder of MODULUS / MULTIPLIER
private static final long REM = 2836;

/**
* The current seed of the generator.
*/
private long currentSeed;

/**
* Constructs a RandomNumber object and initializes it
* with <code>System.currentTimeMillis()</code>
*/
public RandomNumber() {
currentSeed = System.currentTimeMillis() % MODULUS;
}

/**
* Constructs a RandomNumber object and initializes it
* with the value <code>seed</code>
* @param seed A value that permits a controlled
* setting of the start seed.
*/
public RandomNumber(long seed) {
currentSeed = Math.abs(seed) % MODULUS;
}

/**
6.4. BEISPIELE VON KLASSEN 175

* Generates the next random number in the interval [0,1]


* @return The next random number in [0,1].
*/
public double nextDoubleRand() {
long temp = MULTIPLIER*(currentSeed%QUOT) -
REM*(currentSeed/QUOT);
currentSeed = (temp > 0) ? temp : temp + MODULUS;
return (double) currentSeed / (double) MODULUS;
}

/**
* Generates a random int value between the given limits.
* @param lo The lower bound.
* @param hi The upper bound.
* @return An integer value in {lo,...,hi}
* @throws InvalidOperationException if lo > hi
*/
public int nextIntRand(int lo, int hi)
throws InvalidOperationException {
if (lo > hi)
throw new InvalidOperationException(
"invalid range: " + lo + " > " + hi);
return (int) (nextDoubleRand() * (hi - lo + 1) + lo);
}
}

Die Implementation nutzt currentSeed als Variable, die die momentane Zufallszahl enthält. Die
Definition dieser Variablen als private sorgt dafür, dass diese Variable nur innerhalb der Klasse
benutzt werden kann.
Die Formel xk+1 = xk · a mod n wird hier zu

currentSeed = (currentSeed * MULTIPLIER) % MODULUS

Die modulo Berechnung wird, um einen Überlauf bei currentSeed * MULTIPLIER zu verhindern,
zerlegt in

MULTIPLIER * (currentSeed % QUOT) - REM * (currentSeed/QUOT);

wobei QUOT = MODULUS / MULTIPLIER und REM = MODULUS % MULTIPLIER ist. Zum Resultat
muss, falls es nicht positiv ist, noch MODULUS hinzu addiert werden, um es in den gewünschten Bereich
0 ≤ currentSeed ≤ MODULUS − 1
zu bringen18 (dies geschieht als bedingte Anweisung).
18 Beweis als Übung.
176 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

Die Methode nextIntRand() nutzt die Tatsache, dass bei der Konvertierung von double zu int
nach unten gerundet wird. Es gilt also

0 ≤ nextDoubleRand() < 1
⇒ 0 ≤ nextDoubleRand() * (hi - lo + 1 ) < hi − lo + 1
⇒ lo ≤ nextDoubleRand() * (hi - lo + 1 ) < hi + 1
⇒ lo ≤ (int)(nextDoubleRand() * (hi - lo + 1) + lo) < hi + 1
⇒ lo ≤ (int)(nextDoubleRand() * (hi - lo + 1) + lo) ≤ hi

Die Gleichverteilung der Zufallszahlen auf [0, 1] übersetzt sich daher auf die Gleichverteilung auf
{lo, lo + 1, . . . , hi}.
Eine mögliche Verwendung der Klasse RandomNumber zeigt das folgende Applet, das die Güte der
Zufallszahlen für die Simulation eines Würfelspiels testet. Dabei wird 360000 mal mit zwei Würfeln
gewürfelt. Für jeden Wurf wird die Summe der Augenzahlen ermittelt. Über diese Summe wird eine
Statistik geführt und ausgegeben.

Programm 6.2 RollDice.Java


/**
* This class investigates the odds for rolling
* two dice by randomly generating such rolls
* and calculating the sum of their values
*/
import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public final class RollDice extends Applet {

Label seedPrompt, no_rollsPrompt, progressMsg;


TextField seedFld, no_rollsFld;
TextArea output;

final static int MAX_VALUE = 6; // number of sides of the dice


static int rolls; // number of rolls
static long[] rollCount = new long[2 * MAX_VALUE + 1];
// rollCount[i] == number of times
// that i was obtained as sum of the two dice

static long seed; // seed for random number generator

public void init() {


setLayout(new FlowLayout(FlowLayout.LEFT));
6.4. BEISPIELE VON KLASSEN 177

setFont(new Font("Times", Font.PLAIN, 24));


Font courier = new Font("Courier", Font.PLAIN, 24);

no_rollsPrompt = new Label(


"Bitte Anzahl der Würfe eingeben: ");
add(no_rollsPrompt);

no_rollsFld = new TextField("360000", 10);


add(no_rollsFld);
no_rollsFld.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
rollDice();
}
});

seedPrompt = new Label(


"Bitte Startzahl für die Zufallszahlen eingeben: ");
add(seedPrompt);

seedFld = new TextField("0", 10);


add(seedFld);
seedFld.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
rollDice();
}
});

output = new TextArea(12, 30);


output.setFont(courier);
add(output);
}

public void rollDice() {


try {
output.setText("");

// get seed
seed = Long.parseLong(seedFld.getText());
// may throw NumberFormatException
rolls = Integer.parseInt(no_rollsFld.getText());
// get no of rolls
// may throw NumberFormatException
if (rolls < 0){
output.setText("Anzahl der Würfe "
178 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

+ "muss positiv sein");


return;
}

for (int i = 0; i <= 2 * MAX_VALUE; i++) {


rollCount[i] = 0;
}

RandomNumber dice1, dice2 ; // the two dice


// make them different by different seeds
if (seed != 0) {
dice1 = new RandomNumber(seed);
dice2 = new RandomNumber(
seed*seed + 77*seed + 113);
} else {
dice1 = new RandomNumber();
dice2 = new RandomNumber(
System.currentTimeMillis() + 10000);
}

// throw the dice rolls many times


for (int i = 0; i < rolls; i++) {
rollCount[dice1.nextIntRand(1, MAX_VALUE)
+ dice2.nextIntRand(1, MAX_VALUE)]++;
}

// generate the output nicely formatted


String outputStr = format(rollCount);
output.setText(outputStr);
} catch (NumberFormatException e) {
output.setText("Bitte nur ganze Zahlen eingeben.");
}
}

private static String format(long[] rollCount) {


// generate the 3 colums of out put as 3 arrays of Strings
// first column are the indices of rollCount
String[] index = new String[rollCount.length];
for (int i = 0; i < rollCount.length; i++) {
index[i] = Integer.toString(i);
}
// second column are the counts, ie. rollCount itself
String[] count = new String[rollCount.length];
for (int i = 0; i < rollCount.length; i++) {
6.4. BEISPIELE VON KLASSEN 179

count[i] = Long.toString(rollCount[i]);
}
// third column are the normalized frequencies
// with decimal point
String[] frequency = new String[rollCount.length];
for (int i = 0; i < rollCount.length; i++) {
long freq = (long) ((rollCount[i] / (double) rolls)
* 10000);
StringBuffer strBuf = new StringBuffer();
strBuf.append(freq / 100 + "."); // decimal point
if (freq % 100 == 0) strBuf.append("00");
else if (freq % 100 < 10) strBuf.append("0"
+ freq % 100);
else strBuf.append(freq % 100);
frequency[i] = strBuf.toString();
}

// determine maximum Stringlength of every


// column for indentation
int maxIndex = maxLength(index);
int maxCount = maxLength(count);
int maxFreq = maxLength(frequency);

// generate the rows of the output


StringBuffer outputBuf = new StringBuffer();
outputBuf.append("sum count frequency\n");
for (int i = 2; i < rollCount.length; i++) {
// first column
outputBuf.append(indent(maxIndex, index[i]) + ": ");
// now the number of times that i is rolled
outputBuf.append(indent(maxCount, count[i]) + " ");
// now the frequencies
outputBuf.append(indent(maxFreq, frequency[i])
+ " %");
if (i < rollCount.length - 1) outputBuf.append("\n");
}
return outputBuf.toString();
}

private static int maxLength(String[] arr) {


int max = arr[0].length();
for (int i = 1; i < arr.length; i++)
if (max < arr[i].length()) max = arr[i].length();
180 KAPITEL 6. ABSTRAKTION VON METHODEN UND DATEN

return max;
}

private static String indent(int max, String str) {


StringBuffer strBuf = new StringBuffer();
for (int i = 0; i < max - str.length(); i++)
strBuf.append(" ");
strBuf.append(str);
return strBuf.toString();
}
}

Der folgende Output für rolls = 360.000 zeigt, dass der Zufallszahlengenerator eine sehr gleichmäßige
Verteilung liefert. Jede Augenzahl i = 2, 3, . . . , 12 erscheint mit der zu erwartenden Häufigkeit von
(i − 1) · 10000 für i = 2, . . . , 7 bzw. (12 − i + 1) · 10000 für i = 8, 9, . . . , 12.

sum count frequency


2: 9942 2.76 %
3: 19899 5.52 %
4: 29768 8.26 %
5: 39745 11.04 %
6: 50104 13.91 %
7: 60243 16.73 %
8: 50225 13.95 %
9: 40202 11.16 %
10: 29888 8.30 %
11: 19920 5.53 %
12: 10064 2.79 %

6.5 Literaturhinweise
Die hier gegebene Darstellung lehnt sich stark an [HR94] an. Dies gilt insbesondere für den Zufallszahlenge-
nerator und die Klasse Fraction.
Zufallsgeneratoren und Methoden zum Testen des Zufallsverhaltens werden ausführlich in [Knu98a] behandelt.
Der hier implementierte Generator geht auf [PM88] zurück.
Weitere Beispiele für Klassen und eine ausführliche Beschreibung aller (hier nicht aufgeführten) Feinheiten
und Variationen von Klassen in Java finden sich in [Küh99].
Kapitel 7

Rekursion

Ein Objekt heißt rekursiv, wenn es sich selbst als Teil enthält oder mit Hilfe von sich selbst definiert
ist. Rekursion kommt nicht nur in der Mathematik, sondern auch im täglichen Leben vor. Wer hat
etwa noch nie Bilder gesehen, die sich selbst enthalten?
214 KAPITEL 8. REKURSION

Abbildung 8.1: Rekursion im Bild.


w !
Abbildung 8.1: Rekursion im Bild
T1 !" T2 !" #$
! " ! " # $
! " ! " # $
Rekursion kommt speziell in mathematischen
! " =⇒
Definitionen zur
Geltung. Bekannte Beispiele sind die
# $
! "
! " T1 !" T2 !"
! "
natürlichen Zahlen, Baumstrukturen und gewisse Funktionen: ! " ! "
! " ! "
! " ! "
! "
! "
1. Natürliche Zahlen
Abbildung 8.2: Rekursion bei binären Bäumen.
! ! ! ! ! ! ! !
Die Menge N der natürlichen Zahlen kann wie folgt rekursiv definiert werden.
!# $! !# $! !# ! $! $!
# $ #$ # # $ $
#
! $! !# $!
– 0 ∈ N. # $ # $
#

– Ist n ∈ N, so auch der 8.3:


Abbildung Nachfolger n+
Die nichtleeren 1 von
binären n. mit höchstens 3 Knoten.
Bäume

– N ist die kleinste Menge mit diesen Eigenschaften.


Zur Illustration beweisen wir die Äquivalenz der Definitionen bei binären Bäumen. Als Bewei-
stechnik verwenden wir vollständige Induktion, die bei der Rekursion als “Standardtechnik”
2. Binäre Bäume
angesehen werden kann.

Version
Satz 8.1 Ein nicht-leerer Digraph T ist genau dann ein binärer Baum (im Sinne der Defini- vom 17. Juni 2010
tion gerichteter Bäume auf Seite 168), wenn er aus einer Wurzel w und zwei (möglicherweise
leeren) Teilbäumen T1 und T2 besteht, die ihrerseits binär sind.

181
Beweis: ⇒ (durch vollständige Induktion nach der Knotenzahl n von T ):
182 KAPITEL 7. REKURSION

Binäre Bäume sind gerichtete Bäume (vgl. Seite 134), bei denen jeder Knoten Anfangspunkt
von höchstens zwei Kanten ist. Die Menge der binären Bäume kann wie folgt rekursiv definiert
werden (siehe Satz 7.1).

– Der leere Baum ohne Knoten ist ein binärer Baum (genannt der leere Baum).
– Sind T1 und T2 binäre Bäume, so ist auch der Graph bestehend aus einer Wurzel w und
den Teilbäumen T1 und T2 ein binärer Baum, vgl. Abbildung 7.2. Ist einer der Teilbäume
leer, so fehlt die zu ihm führende Kante.
– Dies sind alle binären Bäume.
w
T1 T2

=⇒
T1 T2

Abbildung 7.2: Rekursion bei binären Bäumen.

Die nicht leeren binären Bäume mit höchstens 3 Knoten sind in Abbildung 7.3 dargestellt.

Abbildung 7.3: Die nicht-leeren binären Bäume mit höchstens 3 Knoten.

3. Die Fakultät n! einer natürlichen Zahl n kann rekursiv definiert werden als

1, falls n = 0
n! =
n · (n − 1)! , falls n > 0

Zur Illustration beweisen wir die Äquivalenz der Definitionen bei binären Bäumen. Als Beweistechnik
verwenden wir vollständige Induktion, die bei der Rekursion als “Standardtechnik” angesehen werden
kann.

Satz 7.1 Ein nicht-leerer Digraph T ist genau dann ein binärer Baum (im Sinne der Definition gerich-
teter Bäume auf Seite 134), wenn er aus einer Wurzel w und zwei (möglicherweise leeren) Teilbäumen
T1 und T2 besteht, die ihrerseits binär sind.

Beweis: ⇒“ (durch vollständige Induktion nach der Knotenzahl n von T ):



183

Induktionsanfang n = 1: Ist n = 1, so besteht T nur aus der Wurzel w und den leeren Teilbäumen T1
und T2 .
Induktionsschluss auf n > 1 Knoten: Sei w die Wurzel von T . Wegen n > 1 hat T mindestens einen
nicht-leeren Teilbaum, etwa T1 . Zu zeigen ist, dass T1 die rekursive Definition erfüllt, d. h. aus einer
Wurzel und zwei binären Teilbäumen besteht. Dies folgt sofort aus der Induktionsvorausetzung, da
T1 weniger als n Knoten hat. Für den evtl. vorhandenen nicht-leeren zweiten Teilbaum T2 geht der
Beweis analog.
⇐“ (durch vollständige Induktion nach der Knotenzahl n von T ):

T erfülle die rekursive Definition. Zu zeigen ist:

1. T hat eine Wurzel w.

2. In jeden Knoten v 6= w geht genau eine Kante hinein.

3. Jeder Knoten v 6= w ist auf einem gerichteten Weg von w aus erreichbar.

4. Von jedem Knoten gehen maximal 2 Kanten aus.

Induktionsanfang n = 1: Dann sind 1.–4. trivialerweise erfüllt.


Induktionsschluss auf n > 1 Knoten: Da T die rekursive Definition erfüllt, besteht T aus einer Wurzel
w und ein oder zwei nicht-leeren binären Teilbäumen T1 und T2 . Also ist 1. erfüllt und gilt 4. für w. Sei
T1 nicht-leer und w1 die Wurzel von T1 . Auf T1 trifft die Induktionsvoraussetzung zu, d. h. 1.–4. gelten
für T1 . Dies folgt ebenso für T2 , falls T2 nicht-leer ist. Also gilt 2. in ganz T , da 2. in den Teilbäumen
gilt und in ihren Wurzeln w1 bzw. w2 genau eine Kante endet. Entsprechend folgen 3. und 4.

Das Wesentliche der Rekursion ist die Möglichkeit, eine unendliche Menge von Objekten durch eine
endliche Aussage zu definieren. Auf die gleiche Art kann eine unendliche Zahl von Berechnungen
durch ein endliches rekursives Programm beschrieben werden, ohne dass das Programm explizite
Schleifen enthält. Rekursive Algorithmen sind hauptsächlich dort angebracht, wo das Problem, die
Funktion oder die Datenstruktur bereits rekursiv definiert ist.
Ein notwendiges und hinreichendes Werkzeug zur Darstellung rekursiver Programme sind Funktionen
(also in Java Methoden), die sich selbst oder gegenseitig aufrufen können. Enthält eine Funktion
f() einen expliziten Aufruf ihrer selbst, so heißt f() direkt rekursiv; enthält f() einen Aufruf einer
zweiten Funktion g(), die dann ihrerseits f() (direkt oder indirekt) aufruft, so heißt f() indirekt
rekursiv. Das Vorhandensein von Rekursion muss daher nicht direkt aus der Funktion ersichtlich sein.
Wie Wiederholungsanweisungen bergen auch rekursive Funktionen die Gefahr nicht abbrechender
Ausführung und verlangen daher die Betrachtung des Problems der Terminierung. Grundlegend ist
daher die Bedingung, dass der rekursive Aufruf einer Funktion von einer Bedingung B abhängt, die
irgendwann nicht mehr erfüllt ist.
184 KAPITEL 7. REKURSION

7.1 Beispiele für Rekursive Algorithmen

Zur Beachtung vorab: manchmal sollte man aus Komplexitätsgründen statt Rekursion lieber Iteration
verwenden. Hierauf wird in Kapitel 7.2 ausführlich eingegangen.

7.1.1 Berechnung des ggT

Die Berechnung des ggT erfolgte in Beispiel 3.3 iterativ durch eine while-Schleife. Aufgrund des
dort gezeigten Lemma 3.1 gilt auch die folgende rekursive Darstellung des ggT für natürliche Zahlen
x ≥ y ≥ 1.

y, falls x mod y = 0
ggT (x, y) =
ggT (y, x mod y) , falls y > 0
Diese rekursive Darstellung führt direkt zu der folgenden rekursiven Java Methode.

Programm 7.1 ggT rekursiv


int ggT(int a, int b){
// assume x, y >= 1
int max = Math.max(a, b);
int min = Math.min(a, b);
int remainder = max % min;
if (remainder == 0) {
return min;
} else {
return ggT(min, remainder);
}
}

Der Aufrufbaum (Rekursionsbaum) ist in Abbildung 7.4 angegeben. Er entartet hier zur Liste, da
keine Verzweigung bei den Aufrufen auftritt.
Die Rekursionstiefe ist die Höhe des Rekursionsbaums plus 1, also 5. Der Run-Time Stack enthält
daher beim “tiefsten” Aufruf 5 Activation Records, die in Abbildung 7.5 dargestellt sind.
Wir werden jetzt die Korrektheit des Programms beweisen und die maximale Rekursionstiefe abschätzen.
Bei der Korrektheit unterscheiden wir zwischen partieller Korrektheit (beim Terminieren des rekursi-
ven Algorithmus liegt das korrekte Resultat vor) und totaler Korrektheit (bei jedem Aufruf terminiert
die Abarbeitung mit dem korrekten Resultat).

Satz 7.2 Das Programm 7.1 berechnet den größten gemeinsamen Teiler korrekt. Ist ggT(a, b) ein
Aufruf des Programms und ist M := max{a, b}, so gilt für die Rekursionstiefe (und die Gesamtzahl
der Aufrufe) RggT (a, b):
RggT (a, b) ≤ 1 + 2 · log M
7.1. BEISPIELE FÜR REKURSIVE ALGORITHMEN 185

ggT(76,238)

ggT(76,10)

ggT(10,6)

ggT(6,4)

ggT(4,2)

Abbildung 7.4: Der Rekursionsbaum von Programm 7.1.

Beweis: Sei a > b und sei t := a div b und r := a mod b. Dann gilt:

a = t ·b+r ≥ b+r (da t ≥ 1)


> r+r (da b > r)

Also ist r < a2 . Somit wird in der Paarfolge (a, b), (b, r), (r, ·) usw., die beim rekursiven Aufruf von
ggT(a, b) entsteht, das größere der beiden Elemente a, b nach zwei Aufrufen mehr als halbiert. Also
kann es höchstens 1 + 2 · log M Aufrufe geben.
Der Algorithmus terminiert also bei jedem Aufruf (sogar schnell). Die Korrektheit des dann geliefer-
ten Ergebnisses folgt aus Lemma 3.1, da ja die Operation a mod b auf die fortgesetzte Subtraktion
zurückgeführt werden kann. Die t-malige Anwendung von Lemma 3.1 auf a = t · b + r ergibt nämlich
gerade ggT (a, b) = ggT (b, r).

7.1.2 Die Türme von Hanoi

Dieses Problem geht auf eine hinterindische Sage zurück: In einem im Dschungel verborgenen hinter-
indischen Tempel sind Mönche seit Beginn der Zeitrechnung damit beschäftigt, einen Stapel von 50
goldenen Scheiben mit nach oben hin abnehmendem Durchmesser, die durch einen goldenen Pfeiler
in der Mitte zusammengehalten werden, durch sukzessive Bewegungen jeweils einer einzigen Scheibe
auf einen anderen goldenen Pfeiler umzuschichten. Dabei dürfen sie einen dritten Pfeiler als Hilfspfei-
ler benutzen, müssen aber darauf achten, dass niemals eine Scheibe mit größerem Durchmesser auf
eine mit kleinerem Durchmesser zu liegen kommt. Eine Lösung für 3 Scheiben ist in Abbildung 7.6
dargestellt.
Die Sage berichtet, dass das Ende der Welt gekommen ist, wenn die Mönche ihre Aufgabe beendet
haben.
186 KAPITEL 7. REKURSION

a=4
b=2
max = 4
min = 2 ⇒ return 2
remainder = 0
Rücksprungadresse 5
static pointer: H EAP
a=6
b=4
max = 6
min = 4 ⇒ Aufruf ggT(4,2)
remainder = 2
Rücksprungadresse 4
static pointer: H EAP
a = 10
b=6
max = 10
min = 6 ⇒ Aufruf ggT(6,4)
remainder = 4
Rücksprungadresse 3
static pointer: H EAP
a = 76
b = 10
max = 76
min = 10 ⇒ Aufruf ggT(10,6)
remainder = 6
Rücksprungadresse 2
static pointer: H EAP
a = 76





 b = 238



 max = 238
1. Aufruf min = 76 ⇒ Aufruf ggT(76,10)
remainder = 10




Rücksprungadresse 1




static pointer: H EAP

Abbildung 7.5: Der Run-Time-Stack von Programm 7.1.

Wir wollen uns nun ganz allgemein überlegen, wie man n Scheiben abnehmender Größe von einem
Pfeiler i auf einen Pfeiler j (1 ≤ i, j ≤ 3, i 6= j) entsprechend der angegebenen Vorschrift umschichten
kann.
Sei k der dritte zur Verfügung stehende Hilfspfeiler. Dann kann man das Problem, n Scheiben vom
8.1. BEISPIELE FÜR REKURSIVE ALGORITHMEN 219
7.1. BEISPIELE FÜR REKURSIVE ALGORITHMEN 187

Abbildung 7.6: Umschichten von 3 Scheiben.


Abbildung 8.6: Umschichten von 3 Scheiben.

vomPfeiler
Pfeileri zum Pfeiler
i zum j mit jHilfe
Pfeiler mitdes Pfeilers
Hilfe k umzuschichten,
des Pfeilers folgendermaßen
k umzuschichten, lösen:
folgendermaßen lösen:
ManMan schichtetdie
schichtet obersten nn−−1 1Scheiben
dieobersten vomvom
Scheiben Pfeiler i zumi Pfeiler
Pfeiler k mit Hilfe
zum Pfeiler k mitdesHilfe
Pfeilers dann
desj;Pfeilers
bringt man die auf dem Pfeiler i verbliebene einzige (anfangs unterste) Scheibe (als
j; dann bringt man die auf dem Pfeiler i verbliebene einzige (anfangs unterste) Scheibe (als einzige Scheibe)
auf den Pfeiler j. Nun ist der Pfeiler i frei und man kann die n − 1 Scheiben vom Pfeiler k auf den
einzige Scheibe) auf den Pfeiler j. Nun ist der Pfeiler i frei und man kann die n − 1 Scheiben
Pfeiler j mit Hilfe des Pfeilers i umschichten. Dies ist in Abbildung 7.7 dargestellt.
vom Pfeiler k auf den Pfeiler j mit Hilfe des Pfeilers i umschichten. Dies ist in Abbildung 8.7
Wir definieren nun eine Methode getMoves() so, dass der Aufruf getMoves(n,i,j,k) die Zugfol-
dargestellt.
ge ausgibt, mit der n Scheiben vom Pfeiler i zum Pfeiler j mit Hilfe des Pfeilers k so umgeschichtet
Wirwerden,
definieren
dass nun eineeine
niemals Methode
größere move() so, daß Scheibe
auf eine kleinere der Aufruf move(n,i,j,k)
zu liegen kommt. die Zugfolge aus-
gibt, mit der n Scheiben vom Pfeiler i zum Pfeiler j mit Hilfe des Pfeilers k so umgeschichtet
Der oben erläuterte rekursive Zusammenhang zwischen getMoves(n,...) und getMoves(n-1,...)
werden, daß niemals eine größere auf eine kleinere Scheibe zu liegen kommt.
führt dann zu folgender Methode:
Der oben erläuterte rekursive Zusammenhang zwischen move(n,...) und move(n-1,...)
führt dann getMoves(int
String zu folgender Methode:
numberOfDisks, int origin, int destination, int auxPile){
if (numberOfDisks <= 0) return ""; // no moves to return
String move( int numberOfDisks, int origin, int destination, int auxPile ) {
StringBuffer moves = new StringBuffer();
if ( numberOfDisks <= 0 ) return ""; // no moves to return

StringBuffer moves = new StringBuffer();


220 KAPITEL 8. REKURSION
188 KAPITEL 7. REKURSION

n n−1

i j k i j k

n
n−1

Abbildung 8.7: Umschichten von n Scheiben.


Abbildung 7.7: Umschichten von n Scheiben.
// move numberOfDisks - 1 smallest disks from origin to auxPile
// with destination as auxiliary file
// move themove(
moves.append( numberOfDisks - 1 -smallest
numberOfDisks disks
1, origin, auxPile, destination) );
// from origin to auxPile
// //
write thedestination
with move of the as
largest disk on
auxiliary cout
pile
moves.append( origin + " --> " + destination
moves.append(getMoves(numberOfDisks - 1,+ origin,
"\n" );
auxPile, destination));
// move the numberOfDisks - 1 smallest disks from auxPile
// //
to destination with origin
append the move of the as auxiliary
largest diskfile
to StringBuffer moves
// moves.append(origin
(the largest disk on destination does not interfere)
+ " --> " + destination + "\n");
moves.append( move( numberOfDisks - 1, auxPile, destination, origin) );
// move the numberOfDisks - 1 smallest disks from auxPile
return moves.toString();
// to destination with origin as auxiliary file
}
// (the largest disk on destination does not interfere)
moves.append(getMoves(numberOfDisks
Diese Methode - 1, auxPile,
gibt bei Aufruf von move(3,1,2,3) folgende Zugfolge zurück:
destination, origin));
1 --> 2
1 --> 3
return moves.toString();
} 2 --> 3
1 --> 2
3 --> 1
Diese Methode gibt bei Aufruf von getMoves(3,1,2,3) folgende Zugfolge zurück:
3 --> 2
1 --> 2
1 --> 2
Die1rekursive
--> 3 Lösung ist hier besonders elegant, da der Rekursionsansatz einfach ist, während
2 --> 3
1 --> 2
3 --> 1
3 --> 2
7.1. BEISPIELE FÜR REKURSIVE ALGORITHMEN 189

1 --> 2

Die rekursive Lösung ist hier besonders elegant, da der Rekursionsansatz einfach ist, während die
Herleitung einer iterativen Lösung (die der Angabe einer expliziten “Strategie” zum Bewegen der
Scheiben gleichkommt) wesentlich schwieriger ist (vgl. Übung).
Der Rekursionsbaum für den Aufruf von getMoves(3,1,2,3) ist in Abbildung 7.8 dargestellt. Er
hat die Höhe 3. Also hat der Aufruf die Rekursionstiefe 4. Die Zahlen an den Aufrufen geben die
Reihenfolge der Aufrufe an. Die zugehörige Belegung des Run-Time-Stack ist in Abbildung 7.9 wie-
dergegeben. Sie entspricht dem Durchlauf des Baumes in WLR-Ordnung (erst die Wurzel, dann linker
Teilbaum nach derselben Regel, dann rechter Teilbaum nach derselben Regel).

1
getM(3,1,2,3)

2 9
getM(2,1,3,2) getM(2,3,2,1)

3 6 10 13
getM(1,1,2,3) getM(1,2,3,1) getM(1,3,1,2) getM(1,1,2,3)

4 5 7 8 11 12 14 15
g(0,..) g(0,..) g(0,..) g(0,..) g(0,..) g(0,..) g(0,..) g(0,..)

Abbildung 7.8: Rekursionsbaum zu den Türmen von Hanoi. Aus Platzgründen ist getMoves(...)
mit getM(...) oder g(...) abgekürzt.

leer 1 2 3 4 3 5 3 2 6 7 6 8 6 2 1 9 10 11 usw.
1 2 3 2 3 2 1 2 6 2 6 2 1 1 9 10
1 2 1 2 1 1 2 1 2 1 1 9
1 1 1 1 1

Abbildung 7.9: Der Run-Time-Stack zu getMoves(3,1,2,3). Die Zahlen sind die Nummern der
Aufrufe aus Abbildung 7.8

Allgemein gilt:

Satz 7.3 Beim Aufruf von move(n,1,2,3) ist die Rekursionstiefe R(n) = n + 1, die Gesamtanzahl
der rekursiven Aufrufe T (n) = 2n+1 − 1, und die Anzahl der Züge Z(n) = 2n − 1.

Beweis: Der Beweis erfolgt durch Induktion nach der Anzahl n der Scheiben. Dabei ist der Indukti-
onsanfang für n = 0 und n = 1 klar.
190 KAPITEL 7. REKURSION

Zum Schluss auf n > 1 betrachten wir die Arbeitsweise von move(). Es wird zweimal move( n -
1, ...) aufgerufen und ein zusätzlicher Zug "origin --> destination" hinzugefügt. Auf die
beiden Aufrufe move( n - 1, ...) trifft die Induktionsvoraussetzung bzgl. n − 1 zu. Also gilt:

R(n) = 1 + R(n − 1) = 1 + n ,
T (n) = 1 + 2 · T (n − 1) = 1 + 2(2n−1+1 − 1) = 2n+1 − 1 ,
Z(n) = 1 + 2 · Z(n − 1) = 1 + 2(2n−1 − 1) = 2n − 1 .

7.1.3 Die Ackermann Funktion

Als Beispiel für eine rekursive Funktionsdefinition komplexerer Art betrachten wir das Beispiel der
Ackermann Funktion a(m, n), die als Extrapolation der Folge immer stärker wachsenden Funktionen
Summe, Produkt, Potenz, . . . aufgefasst werden kann. Sie ist wie folgt definiert.


 n+1, falls m = 0
a(m, n) = a(m − 1, 1) , falls m > 0, n = 0
a(m − 1, a(m, n − 1)) , falls m, n > 0

Auch diese Definition lässt sich unmittelbar in eine Java Methode übersetzen:

int a(int m, int n) {


// assume m >= 0 and n >= 0
if (m == 0) return n+1;
else if (n == 0) return a(m-1,1);
else return a(m-1,a(m,n-1));
}

Für die Methode A ist es bereits viel schwieriger zu sehen, wie (und dass überhaupt) jede Berechnung
nach endlich vielen Schritten terminiert. Dies ist zwar der Fall, wie Satz 7.4 zeigt, aber in der Praxis
scheitert die Berechnung bereits für relativ kleine Argumente an der riesigen Rekursionstiefe.
Die Ackermann Funktion wächst sehr stark, und zwar, wie in der Theorie der rekursiven Funktionen
oder der Berechenbarkeit gezeigt wird, stärker als jede sogenannte primitiv rekursive Funktion (das
sind Funktionen mit einem “einfachen” Rekursionsschema). Es gilt z. B. (vgl. Übung):

a(0, n) > n a(3, n) > 2n


2
··
a(1, n) > n + 1 a(4, n) > 22 n mal
a(2, n) > 2n a(5, 4) > 10 10000
7.1. BEISPIELE FÜR REKURSIVE ALGORITHMEN 191

So erfordert bereits die Ausrechnung von a(1, 3) “per Hand” folgenden Aufwand:

a(1, 3) = a(0, a(1, 2))


= a(0, a(0, a(1, 1)))
= a(0, a(0, a(0, a(1, 0))))
= a(0, a(0, a(0, a(0, 1))))
= a(0, a(0, a(0, 2)))
= a(0, a(0, 3))
= a(0, 4)
= 5

Satz 7.4 Für alle m, n ∈ N terminiert der Aufruf a(m, n) nach endlich vielen Schritten.

Beweis: Der Beweis erfolgt durch zwei ineinander geschachtelte Induktionen über m (äußere Induk-
tion) und, bei festem m, über n (innere Induktion).
Induktionsanfang (m = 0): Dann erfolgt unabhängig von n nur ein Aufruf. Also wird terminiert.
Induktionsvoraussetzung: Die Behauptung sei richtig für alle k mit 0 ≤ k < m, und für alle n, d. h. der
Aufruf a(k, n) terminiert nach endlich vielen Schritten.
Schluss auf m durch Induktion über n (innere Induktion):
Induktionsanfang (n = 0): Dann wird für a(m, 0) der Wert von a(m − 1, 1) zurückgegeben. Hierfür
terminiert der Algorithmus nach Induktionsvorausetzung der äußeren Induktion.
Induktionsvoraussetzung: Der Aufruf a(m, `) terminiert für (festes) m und alle ` < n.
Schluss auf n: Der Aufruf von a(m, n) erzeugt den Aufruf von a(m − 1, a(m, n − 1). Nach innerer
Induktionsvoraussetzung terminiert der Aufruf a(m, n − 1) und liefert eine Zahl k. Dies erzeugt den
Aufruf a(m − 1, k), der nach äußerer Induktionsvorausetzung terminiert.

7.1.4 Ulams Funktion

Diese Funktion wurde mehrfach von Mathematikern untersucht (Klam, Collatz, Kakutani, vgl. [LV92]).
Sie stellt ein Beispiel für einen einfachen Algorithmus dar, für den bis heute nicht bekannt ist, ob er
bei allen Eingaben terminiert.
Der Algorithmus erzeugt, ausgehend von einer natürlichen Zahl a0 > 0, eine Folge von Zahlen a0 , a1 , . . . , an , . . .
gemäß der Vorschrift 
an /2 , falls an gerade ist,
an+1 =
3an + 1 , sonst,
und bricht ab, sobald an = 1 gilt.
192 KAPITEL 7. REKURSION

Zum Beispiel führt a0 = 3 zur Folge 3, 10, 5, 16, 8, 4, 2, 1. Es ist offen, ob der Algorithmus bei
beliebigen Input stets zum Abbruch führt, d. h. ob die Länge der Folge immer endlich ist.
Wir kleiden dies in eine rekursive Funktion. Sei a ∈ N, a > 0


 0, falls a = 1 ,
ulam(a) := 1 + ulam(a/2) , falls a gerade, a ≥ 2 ,
1 + ulam(3a + 1) , falls a ungerade , a ≥ 2 .

Offenbar berechnet ulam(a) gerade die Länge der Folge mit a0 = a.


Obwohl wir nicht wissen, ob diese Funktion für jede natürliche Zahl als Input einen Funktionswert
liefert, ist die Definition als Java Methode natürlich zulässig. Es ist jedoch die (zumindest theoretische)
Möglichkeit nicht auszuschließen, dass ein Aufruf der Funktion ulam für bestimmte Argumente eine
nicht abbrechende Folge von rekursiven Aufrufen in Gang setzt. Dies ist zugleich ein Beispiel für den
Fall, dass eine formal zulässige Funktionsdefinition nicht auch inhaltlich vernünftig sein muss. Man
sollte sich stets vergewissern, ob der durch eine Funktionsdefinition beschriebene Berechnungsprozess
für beliebige Argumente abbricht, also auch für solche, an die man vielleicht zunächst nicht gedacht
hat.

7.2 Wo Rekursion zu vermeiden ist

Rekursive Algorithmen eignen sich besonders, wenn das zugrunde liegende Problem oder die zu be-
handelnden Daten rekursiv definiert sind. Das bedeutet aber nicht, dass eine solche rekursive Definiti-
on eine Garantie dafür bietet, dass ein rekursiver Algorithmus der beste Weg zur Lösung des Problems
ist.
Der Aufwand bei rekursiven Aufrufen wird im wesentlichen durch den Aufrufbaum (Rekursions-
baum) bestimmt. Seine Höhe plus 1 ist die Rekursionstiefe. Sie bestimmt die maximale Größe des
Run-Time-Stacks, der durch den ersten Aufruf verursacht wird, und bestimmt damit den benötigten
Speicherplatz.
Die Gesamtanzahl der Knoten im Aufrufbaum bestimmt die Anzahl aller rekursiven Aufrufe. Sie ist
ein Maß für die benötigte Laufzeit.
Als Generalregel sollte man daher Rekursion immer dann vermeiden, wenn der benötigte Speicher-
platz (also die Rekursionstiefe) oder die benötigte Laufzeit (also die Anzahl der Knoten des Rekursi-
onsbaums) zu groß werden. Wir erläutern dies an einigen Beispielen.

7.2.1 Berechnung der Fakultät

Die Implementation der Fakultätsfunktion gemäß der rekursiven Definition



1, falls n = 0 ,
f ak(n) :=
n · f ak(n − 1) , falls n > 0 ,
7.2. WO REKURSION ZU VERMEIDEN IST 193

führt zur Rekursionstiefe n + 1, die deutlich zu groß ist. Der Rekursionsbaum entartet zudem zu einer
Liste (keine Verzweigungen), was meist ein Zeichen dafür ist, dass es auch einen einfachen iterativen
Algorithmus gibt (vgl. Beispiel 6.1).

7.2.2 Berechnung des größten gemeinsamen Teilers

Der rekursive Algorithmus in Kapitel 7.1.1 führt nach Satz 7.2 zu einer Rekursionstiefe von 1 + 2 ·
log max{n, m}. Dies ist akzeptabel. Allerdings ist der Rekursionsbaum wieder eine Liste, so dass man
auch einen iterativen Algorithmus verwenden könnte (vgl. Algorithmus 3.2).

7.2.3 Die Türme von Hanoi

Die Rekursionstiefe ist bei n Scheiben n + 1, und die Gesamtanzahl der Aufrufe ist 2n+1 − 1. Dies
erscheint sehr groß, ist es aber im Verhältnis zu der Zahl der auszugebenden Züge nicht. Eine iterative
Lösung wäre vorzuziehen, wenn sie denn einfach zu finden wäre. Die rekursive Struktur des Problems
legt jedoch die Verwendung der Rekursion nahe.

7.2.4 Berechnung der Fibonacci-Zahlen

Die Folge f0 , f1 , f2 , . . . der Fibonacci-Zahlen wächst nach dem Gesetz

f0 := 0, f1 := 1,
fn+1 := fn + fn−1 für n > 0 .

Sie stellen eine schnell wachsende Folge von Zahlen dar, die das Wachstum einer Population von sich
schnell vermehrenden Lebewesen (Bakterien, Kaninchen) modelliert. Ist fn die Anzahl der “Neuge-
burten” in Periode n, und reproduzieren sich in einer Periode genau die in den beiden vorherigen
Perioden “geborenen” Mitglieder (“gebärfähiges Alter”) so entsteht die Folge der Fibonacci-Zahlen
als Folge der Geburtenzahlen.
Die rekursive Definition führt auf folgende rekursive Methode:

int fib(int n) {
if (n == 0) return 0;
else if (n == 1) return 1;
else return fib(n-1) + fib(n-2);
}

Beim Aufruf von fib(5) ergibt sich der in Abbildung 7.10 dargestellte Rekursionsbaum.
Beim Aufruf von fib(n) ist also die Rekursionstiefe n, und für die Gesamtanzahl T f ib (n) der rekur-
siven Aufrufe gilt
194 KAPITEL 7. REKURSION

fib(5)

fib(4) fib(3)

fib(3) fib(2) fib(2) fib(1)

fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)

fib(1) fib(0)

Abbildung 7.10: Rekursionsbaum zu der Fibonacci-Folge.

Satz 7.5 T f ib (n) wächst mindestens exponentiell. Genauer gilt

T f ib (n) ≥ 2bn/2c für n ≥ 2 .

Beweis: Dies beweist man durch vollständige Induktion wie folgt:


Induktionsanfang:
n = 0 : T f ib (0) = 1, 2b0/2c = 20 = 1
n = 1 : T f ib (1) = 1, 2b1/2c = 20 = 1
Schluss von n auf n + 1:

T f ib (n + 1) ≥ 2 · T f ib (n − 1)
≥ 2 · 2b(n−1)/2c
= 2b(n+1)/2c .

Die erste Ungleichung folgt aus der Tatsache, dass f ib(n − 1) zweimal aufgerufen wird; die zweite
folgt aus der Induktionsvoraussetzung.

Der Aufwand ist also ähnlich wie bei den Türmen von Hanoi. Während jedoch bei den Türmen von
Hanoi stets andere Teilprobleme in den rekursiven Aufrufen berechnet werden, tauchen hier dieselben
Teilprobleme wiederholt auf.
Eine solche Mehrfachberechnung identischer Teilprobleme sollte man unbedingt vermeiden. Bei den
Fibonacci-Zahlen geht dies durch folgende iterative Variante.
Der letzte else Teil in fib wird dabei ersetzt durch

int fib(int n) {
// assume n >= 0
if (n == 0) return 0;
7.3. LITERATURHINWEISE 195

if (n == 1) return 1;
int currentFib = 1, prevFib = 0;
for ( int i = 1; i < n; i++ ){
currentFib = currentFib + prevFib;
prevFib = currentFib - prevFib;
}
return currentFib;
}

Dabei spielen die Variablen currentFib und prevFib die Rolle von fn und fn−1 , die in beiden
Zuweisungen zu fn+1 = fn + fn−1 und fn = fn+1 − fn−1 aktualisiert werden.
Man vergleiche einmal die Laufzeit beider Varianten für Zahlen n ab n = 35.

7.2.5 Zusammenfassung

Die Folgerung aus diesen Überlegungen ist, dass man auf die Verwendung von Rekursion immer dann
verzichten sollte, wenn es eine offensichtliche Lösung mit Iteration gibt. Das bedeutet aber nicht, dass
Rekursion um jeden Preis zu umgehen ist. Es gibt viele gute Anwendungen für Rekursion, wie die
folgenden Kapitel zeigen werden.
Die Tatsache, dass Implementationen von rekursiven Funktionen auf nicht-rekursiven Maschinen exis-
tieren, beweist, dass gegebenenfalls jedes rekursive Programm in ein rein iteratives umgeformt werden
kann. Dies verlangt jedoch das explizite Verwalten eines Rekursions-Stacks. Durch diese Operatio-
nen wird das Grundprinzip eines Programms oft so sehr verschleiert, dass es schwer zu verstehen ist.
Zusammenfassend lässt sich sagen, dass Algorithmen, die ihrem Wesen nach eher rekursiv als iterativ
sind, tatsächlich als rekursive Funktionen formuliert werden sollten.

7.3 Literaturhinweise
Die hier gegebene Darstellung lehnt sich in Teilen an [Wir00] an. Von dort ist auch Abbildung 8.1 entnommen.
Die Darstellung der Türme von Hanoi folgt [OW04].
Der Artikel [LV92] gibt vertiefende Informationen (und Berechnungen) zu Ulams Funktion.
Weitere Beispiele für Rekursion finden sich in nahezu allen Büchern über Algorithmen und Datenstrukturen.
196 KAPITEL 7. REKURSION
Kapitel 8

Die Analyse von Algorithmen

Ziel der Analyse ist die Effizienzuntersuchung und die Entwicklung effizienterer Algorithmen.
Die Analyse soll zunächst rechnerunabhängig durchgeführt werden. Man braucht also ein geeignetes
Rechnermodell.
Die Annahmen über das Rechnermodell können wichtige Konsequenzen haben in Bezug auf die Fra-
ge, wieviel Zeit man zur Lösung eines Problems braucht. Es existieren zwar formale Rechnermodelle
(z. B. Turingmaschinen oder Maschinen mit wahlfreiem Speicherzugriff (random access machines)),
wir werden jedoch fast immer einen “ganz gewöhnlichen Rechner” zugrunde legen. Damit ist ge-
meint, dass Befehle eines Programms zeitlich nacheinander (sequentiell) ausgeführt werden, und dass
die Kosten eines Algorithmus im Wesentlichen abhängen von der Anzahl der erforderlichen Opera-
tionen. Wir nehmen an, dass ein sogenanntes RAM (Random Access Memory) zur Verfügung steht,
also ein Speicher mit wahlfreiem Zugriff. Mit diesem Speicher ist es möglich, jede beliebige Variable
eines unstrukturierten Datentyps (also int, double usw. und ebenso unstrukturierte Komponenten
eines strukturierten Datentyps mit random access (also Arraykomponenten und Felder von Objekten)
in einer fest vorgegebenen Zeitspanne zu speichern bzw. auszulesen.
Bei der Analyse von Algorithmen besteht die erste Aufgabe darin festzustellen, welche Operatio-
nen verwendet werden und wie hoch ihre relativen Kosten sind. Typische Operationen sind z. B.
die vier Grundrechenarten Addition, Subtraktion, Multiplikation und Division, angewendet auf ganze
Zahlen. Andere elementare Operationen sind arithmetische Operationen mit Gleitkommazahlen, Ver-
gleichsbefehle, Wertzuweisungen an Variablen und ggf. Funktionsaufrufe. Elementare Operationen
benötigen typischerweise nie mehr als eine gewisse feste Zeitspanne zur Ausführung; wir sagen, dass
ihre Ausführungszeit durch eine Konstante beschränkt ist. Dies trifft nicht mehr zu für strukturierte
Daten, z. B. für Vergleich von Strings, die abhängig von der Länge der Strings ist, oder die Suche des
maximalen Elementes in einem Array.

Version vom 17. Juni 2010

197
198 KAPITEL 8. DIE ANALYSE VON ALGORITHMEN

8.1 Analysearten

Man unterscheidet folgende Arten der Analyse.

• A priori Analyse (rechnerunabhängig)

– worst-case Komplexität: Dies ist eine obere Schranke für die Ausführungszeit (in Form
von Anzahl der auszuführenden Operationen) in Abhängigkeit der Größe des Inputs, ge-
messen in relevanten Parametern (z. B. die Anzahl der zu sortierenden Objekte, Stellen-
zahl von Zahlen, etc.)
– Mittlere Komplexität: Dies ist eine obere Schranke für die mittlere Ausführungszeit bei
gewissen (Wahrscheinlichkeits-) Annahmen über das Auftreten der Problemdaten
– Untere Komplexitätsschranken: Hierunter versteht man die Ermittlung unterer Schranken
für die (worst-case oder mittlere) Ausführungszeit.

Im Idealfall liegen obere und und untere Schranke “dicht” zusammen. Dies ist meist jedoch sehr
schwer zu erreichen. Hierfür hat sich eine eigene Disziplin, die Komplexitätstheorie entwickelt.

• A posteriori Analyse (rechnerabhängig)


Hierunter versteht man das Testen einer Implementation des Algorithmus an hinreichend großen
Datensätzen, so dass “alle” Verhaltensweisen des Algorithmus auftreten. Man erstellt dann eine
Sammlung statistischer Daten über Ziel- und Speicherbedarf in Abhängigkeit des Datenmateri-
als.

Als Beispiel betrachten wir drei Programmsegmente

(a) (b) (c)


x = x+y; for (i=0; i<n; i++) for (i=0; i<n; i++)
x = x+y; for (j=0; j<n; j++)
x = x+y;

In allen Fällen nehmen wir an, dass die Anweisung x = x+y in keiner anderen Schleife steht als in den
hier gezeigten. Daher ist im Segment (a) die Häufigkeit dieser Anweisung 1. Im Segment (b) ist sie n,
und im Segment (c) ist sie n2 . Diese Häufigkeiten 1, n, n2 unterscheiden sich um Größenordnungen.
Der Begriff “Größenordnung” ist uns allen vertraut; so unterscheiden sich z. B. Gehen, Autofahren
und Fliegen durch verschiedene Größenordnungen bzgl. der Entfernung, die ein Mensch in einer Stun-
de zurücklegen kann. Im Zusammenhang mit der Algorithmenanalyse bezieht sich die Größenordnung
einer Anweisung auf die Häufigkeit ihrer Ausführung, während die Größenordnung eines Algorith-
mus sich auf die Summe der Häufigkeit aller seiner Anweisungen bezieht.
Die a priori Analyse beschäftigt sich hauptsächlich mit der Bestimmung von Größenordnungen.
Glücklicherweise gibt es eine bequeme mathematische Notation, die diesem Begriff entspricht.
8.2. DIE ASYMPTOTISCHE NOTATION 199

8.2 Die Asymptotische Notation

Bei der a priori Analyse der Rechenzeit werden alle Faktoren außer acht gelassen, die von der Ma-
schine oder der Programmiersprache abhängig sind. Man konzentriert sich ganz auf die Bestimmung
der Größenordnung der Häufigkeit von Anweisungen. Es gibt mehrere Arten der mathematischen
Notation, die sich hierfür anbieten. Eine davon ist die O-Notation:

8.2.1 Obere Schranken

Sei g : N → N. Dann bezeichnet

O(g) := { f : N → N | ∃ c > 0 und ∃ n0 ∈ N mit f (n) ≤ c · g(n) ∀n ≥ n0 }

die Menge aller Funktionen f : N → N, für die zwei positive (i. A. von f abhängige) Konstanten
c ∈ R und n0 ∈ N existieren, so dass für alle n ≥ n0 die Ungleichung f (n) ≤ c · g(n) gilt. Gehört
eine Funktion f : N → N zu dieser Menge, so schreibt man f ∈ O(g) (gesprochen: f ist von der

Größenordnung groß Oh von g“). Andere gebräuchliche Schreibweisen hierfür sind f = O(g) bzw.
auch f (n) ∈ O(g(n)) und f (n) = O(g(n)).1
Nehmen wir an, wir ermitteln die Rechenzeit f (n) für einen bestimmten Algorithmus. Die Variable n
kann z. B. die Anzahl der Ein- und Ausgabewerte sein, ihre Summe, oder auch die Größe eines dieser
Werte. Da f (n) maschinenabhängig ist, genügt eine a posteriori Analyse nicht. jedoch kann man mit
Hilfe einer a priori Analyse ein g(n) bestimmen, so dass f (n) = O(g(n)).
Wenn wir sagen, dass ein Algorithmus eine Rechenzeit O(g(n)) hat, dann meinen wir damit Folgen-
des: Wenn der Algorithmus auf unterschiedlichen Computern mit den gleichen Datensätzen läuft, und
diese die Größe n haben, dann werden die resultierenden Laufzeiten immer kleiner sein als eine Kon-
stante mal g(n). Bei der Suche nach der Größenordnung von f (n) werden wir darum bemüht sein, das
kleinste“ g(n) zu finden, so dass f (n) ∈ O(g(n)) gilt.

Ist f (n) z. B. ein Polynom, so gilt:

Satz 8.1 Für ein Polynom f (n) = am nm + . . . + a1 n + a0 vom Grade m gilt: f (n) ∈ O(nm ).

Beweis: Wir benutzen die Definition von f (n) und eine einfache Ungleichung:

f (n) ≤ |am |nm + . . . + |a1 |n + |a0 |


|am−1 | |a0 |
= (|am | + + . . . + m )nm
n n
≤ (|am | + . . . + |a0 |)nm für n ≥ 1 .

Setzt man c = |am | + . . . + |a0 | und n0 = 1, so folgt unmittelbar die Behauptung.

1 In der Analysis wird O(g) für reelle Funktionen g eingeführt. Da uns hier nur Laufzeitfunktionen interessieren, be-

schränken wir uns auf Funktionen f : N → N.


200 KAPITEL 8. DIE ANALYSE VON ALGORITHMEN

Die asymptotische O-Notation vernachlässigt Konstanten und Terme niedriger Ordnung (wie z. B. die
Terme ak nk mit k < m im Polynom). Dafür gibt es zwei gute Gründe:

– Für große n ist die Größenordnung allein maßgebend. Z. B. ist bei 104 · n und n2 die erste
Laufzeit für große n (n ≥ 104 ) zu bevorzugen.

– Konstanten und Terme niedrigerer Ordnung hängen von vielen Faktoren ab, z. B. der gewählten
Sprache oder der verwendeten Maschine, und sind daher meist nicht maschinenunabhängig.

Um Größenordnungen unterscheiden zu können, gibt es die o-Notation (lies: klein oh Notation). Sei
g : N → N. Dann bezeichnet

o(g) := { f : N → N | ∀ c > 0 ∃ n0 ∈ N mit f (n) < c · g(n) ∀n ≥ n0 }

die Menge aller Funktionen f : N → N, für die zu jeder positiven Konstanten c ∈ R ein n0 ∈ N existiert,
so dass für alle n ≥ n0 die Ungleichung f (n) < c · g(n)gilt. Gehört eine Funktion f : N → N zu
dieser Menge, so schreibt man f ∈ o(g) (gesprochen: f ist klein oh von g“). Andere gebräuchliche

Schreibweisen hierfür sind analog zur O-Notation f = o(g) bzw. auch f (n) ∈ o(g(n)) und f (n) =
o(g(n)). Man sagt dann auch, dass f von (echt) kleinerer Größenordnung als g ist.
Die gängigsten Größenordnungen für Rechenzeiten sind:

O(1) < O(log n) < O(n) < O(n log n) < O(n2 )
< O(nk ) für k ∈ N fest, k ≥ 3
< O(nlog n ) < O(2n )

Dabei bedeutet O( f (n)) < O(g(n)), dass f (n) = o(g(n)), f (n) also von kleinerer Größenordnung als
g(n) ist.
Um den Unterschied dieser Größenordnungen zu überprüfen, ziehen wir ein nützliches Kriterium der
Analysis heran:

Lemma 8.1 Seien f , g : N → N. Dann gilt

f (n)
f (n) ∈ o(g(n)) ⇐⇒ lim = 0.
n→∞ g(n)

Beweis: ⇒: Sei ε > 0 beliebig. Da f (n) ∈ o(g(n)) existiert zu ε ein n0 ∈ N mit f (n) < εg(n) für alle
f (n) f (n)
n ≥ n0 . Hieraus folgt g(n) < ε für alle n ≥ n0 . Da ε beliebig ist, gilt limn→∞ g(n) = 0.
f (n) f (n)
⇐: Es gelte limn→∞ g(n) = 0 . Dann gibt es zu jedem c > 0 ein n0 ∈ N mit g(n) < c für alle n ≥ n0 .
Also ist f (n) < c · g(n) für alle n ≥ n0 . Da dies für alle c > 0 gilt, folgt f (n) ∈ o(g(n)).
8.2. DIE ASYMPTOTISCHE NOTATION 201

f (n)
Die Überprüfung von limn→∞ g(n) = 0 führt oft auf Ausdrücke der Form ∞
∞ , deren Konvergenz mit der
Regel von de L’Hopital“ überprüft werden kann:

d
f (n) d n f (n)
lim = lim d
n→∞ g(n) n→∞
d n g(n)

sofern der Limes der rechten Seite existiert. Dieses Kriterium kann bei Bedarf natürlich iterativ ange-
wendet werden.
Wir erläutern es exemplarisch an zwei Fällen:

1. log n ∈ o(n), da
d ln n 1
d
dn log n d n ln 2 ln 2· 1n 1
lim d
= lim = lim = lim = 0.
n→∞ n→∞ d n n→∞ 1 n→∞ n ln 2
dnn dn

2. nk ∈ o(2n ) für festes k, da


d k
dnn knk−1 k!
lim = lim = . . . = lim = 0.
n→∞ d 2n n→∞ (ln 2)2 n n→∞ (ln 2)k 2n
dn

Betrachten wir noch einmal die oben aufgeführten gängigen Größenordnungen

O(1) < O(log n) < O(n) < O(n log n) < O(n2 ) < O(nk ) < O(nlog n ) < O(2n ).

O(1) bedeutet, dass die Anzahl der Ausführungen elementarer Operationen unabhängig von der Größe
des Inputs durch eine Konstante beschränkt ist. Die ersten sechs Größenordnungen haben eine wich-
tige Eigenschaft gemeinsam: sie sind durch ein Polynom beschränkt. (Sprechweisen: polynomial be-
schränkt, polynomial, schnell, effizient). O(n), O(n2 ) und O(n3 ) sind selbst Polynome, die man—bzgl.
ihrer Grade—linear, quadratisch und kubisch nennt.
Es gibt jedoch keine ganze Zahl m, so dass nm eine Schranke für 2n darstellt, d. h. 2n 6∈ O(nm ) für jede
feste ganze Zahl m. Die Größenordnung von 2n ist O(2n ), da nk ∈ o(2n ).
Man sagt, dass ein Algorithmus mit der Schranke O(2n ) einen exponentiellen Zeitbedarf hat. Für große
n ist der Unterschied zwischen Algorithmen mit exponentiellem bzw. durch ein Polynom begrenztem
Zeitbedarf ganz beträchtlich. Es ist eine große Leistung, einen Algorithmus zu finden, der statt eines
exponentiellen einen durch ein Polynom begrenzten Zeitbedarf hat.
Tabelle 8.1 zeigt, wie die Rechenzeiten der sechs typischen Funktionen anwachsen, wobei die Kon-
stante gleich 1 gesetzt wurde. Wie man feststellt, zeigen die Zeiten vom Typ O(n) und O(n log n)
ein wesentlich schwächeres Wachstum als die anderen. Für sehr große Datenmengen ist dies oft das
einzig noch verkraftbare Wachstum.
Folgendes Beispiel verdeutlicht den drastischen Unterschied zwischen polynomialem und exponenti-
ellem Wachstum. Es zeigt, dass selbst enorme Fortschritte in der Rechnergeschwindigkeit bei expo-
nentiellen Laufzeitfunktionen hoffnungslos versagen.
202 KAPITEL 8. DIE ANALYSE VON ALGORITHMEN

Tabelle 8.1: Wachstum verschiedener Größenordnungen.

log n n n log n n2 n3 2n
0 1 0 1 1 2
1 2 2 4 8 4
2 4 8 16 64 16
3 9 24 64 512 256
4 16 64 256 4096 65536
5 32 160 1024 32768 4294967296

Beispiel 8.1 (Vergleich der Größenordnungen polynomial/exponentiell) Sei f (n) die


Laufzeitfunktion für ein Problem, und sei n0 die Problemgröße, die man mit der jetzigen Technologie
in einer bestimmten Zeitspanne t lösen kann. (Z. B. Berechnen kürzester Wege in einem Graphen mit
n Knoten, t = 60 Minuten. Dann ist n0 die Größe der Graphen, für die in einer Stunde die kürzesten
Wege berechnet werden können.)
Wir stellen jetzt die Frage, wie n0 wächst, wenn die Rechner 100 mal so schnell werden. Sei dazu n?
die Problemgröße, die man auf den schnelleren Rechnern in der gleichen Zeitspanne t lösen kann.
Offenbar erfüllt n0 die Gleichung f (n0 ) = t bei der alten Technologie, und f (n0 ) = t/100 bei der
neuen Technologie. Da n? bei der neuen Technologie f (n? ) = t erfüllt, ergibt sich

f (n? ) = 100 · f (n0 ) .

Ist die Laufzeitfunktion f (n) polynomial, etwa f (n) = nk , k fest, so folgt



nk? = 100 · nk0 , also n? = 100 · n0 ,
k


d. h., man kann jetzt eine um den Faktor k 100 größere Probleme in derselben Zeit lösen.
Ist dagegen die Laufzeitfunktion f (n) exponentiell, etwa f (n) = 2n , so folgt

2n? = 100 · 2n0 , also n? = n0 + log(100) ≤ n0 + 7 ,

d. h., man kann jetzt nur eine um den additiven Term log(100) größere Probleme in derselben Zeit
lösen. Der Fortschritt macht sich also kaum bemerkbar.

8.2.2 Untere Schranken

Die O-Notation dient der Beschreibung oberer Schranken. Größenordnungen für untere Schranken
werden mit der Ω-Notation ausgedrückt, die analog zur O-Notation definiert wird.
Sei g : N → N. Dann bezeichnet

Ω(g) := { f : N → N | ∃ c > 0 und ∃ n0 ∈ N mit f (n) ≥ c · g(n) ∀n ≥ n0 }


8.3. A POSTERIORI ANALYSE, LAUFZEITMESSUNGEN 203

die Menge aller Funktionen f : N → N, für die zwei positive (i. A. von f abhängige) Konstanten c ∈ R
und n0 ∈ N existieren, so dass für alle n ≥ n0 die Ungleichung f (n) ≥ c · g(n) gilt.
Manchmal kommt es vor, dass für die Laufzeit f (n) eines Algorithmus gilt: f (n) = Ω(g(n)) und
f (n) = O(g(n)). Dafür benutzen wir folgende Schreibweise.
Sei g : N → N. Dann ist
Θ(g) := O(g) ∩ Ω(g)
die Menge aller Funktionen f : N → N für die f ∈ O(g) und f ∈ Ω(g) gilt. Es ist also f ∈ Θ(g)
(gelesen: f ist in Theta von g“), wenn es positive Konstante c1 , c2 ∈ R und n0 ∈ N gibt, so dass für

alle n ≥ n0 gilt: c1 · g(n) ≤ f (n) ≤ c2 · g(n).

Beispiel 8.2 (Sequentielle Suche) Sei f (n) die maximale Anzahl von Vergleichen bei der sequenti-
ellen Suche in einem unsortierten Array mit n Komponenten (vgl. Kapitel 5.1.1). Dann ist f (n) =
O(n), da man je mit n Vergleichen auskommt. Andererseits muss man aber auch jede Komponen-
te überprüfen, denn ihr Wert könnte ja der gesuchte Wert sein. Also ist f (n) = Ω(n) und damit
f (n) = Θ(n).

Beispiel 8.3 (Matrizenmultiplikation) Bei der Matrixmultiplikation von n × n Matrizen ergibt sich
die Berechnung eines Eintrags ci j von C = A·B gemäß ci j= ∑nk=1 aik ·bk j (vgl. Kapitel 5.2. Sie erfordert
also n Multiplikation und n − 1 Additionen.
Insgesamt sind für ganz C also n2 Einträge ci j zu berechnen, und somit n2 (n + n − 1) = 2n3 − n2 =
O(n3 ) arithmetische Operationen insgesamt auszuführen..
Da jeder Algorithmus für die Matrixmultiplikation n2 Einträge berechnen muss, folgt andererseits,
dass jeder Algorithmus zur Matrixmultiplikation von zwei n×n Matrizen Ω(n2 ) Operationen benötigt.
Es klafft zwischen Ω(n2 ) und O(n3 ) also noch eine Komplexitätslücke“.2

8.3 A posteriori Analyse, Laufzeitmessungen

Nehmen wir an, wir haben ein Programm zur Lösung eines Problems entworfen, kodiert, als kor-
rekt bewiesen und am Rechner die Fehlersuche erfolgreich durchgeführt. Wie können wir ein Leis-
tungsprofil erstellen, welches exakt den Rechenzeit- und Speicherbedarf dieses Programms angibt?
Um exakte Zeiten zu erhalten, muss unser Computer über eine Uhr verfügen, von der die Zeit per
Programm abgelesen werden kann. Mit dieser Möglichkeit der Zeitmessung können viele Faktoren
der Programmausführung überprüft werden. Der wichtigste Test eines Programms besteht darin, zu
zeigen, dass die frühere Analyse bzgl. der Größenordnung richtig war. Mit Hilfe der tatsächlich ge-
messenen Zeiten sollten wir in der Lage sein, die exakte Laufzeitfunktion in Abhängigkeit von der
benutzten Programmiersprache und dem Rechner zu bestimmen.
Zur Zeitmessung existieren UNIX-utilities wie /bin/time3 aber auch Java Methoden wie die Me-
thode
2 Die schnellsten bekannten Algorithmen zur Matrixmultiplikation kommen mit O(n2,376 ) Operationen aus.
3 vgl. die entsprechenden Manual pages durch Aufruf von man time.
204 KAPITEL 8. DIE ANALYSE VON ALGORITHMEN

public static native long System.currentTimeMillis()


der Klasse System. Sie gibt die aktuelle Zeit seit dem 1. Januar 1970 (UCT) in Millisekunden als
long-Wert zurück. Dadurch lassen sich einfach Zeitmessungen durchführen wie in der folgenden
Funktion für die rekursive Berechnung der Fibonacci-Zahlen (vgl. Abschnitt 7.2.4):

/**
* measures time for the recursive calculation of the Fibonacci
* numbers
* @param <code>n</code> the argument for the Fibonacci number
* @return the time in milliseconds for the calculation of the
* <code>n</code>-th Fibonacci number
*/
long fibonacciTime( int n ) {
long start = System.currentTimeMillis();
fib( 40 ); // no assignment since we don’t need the value
long end = System.currentTimeMillis();
return end - start;
}

8.4 Literaturhinweise
Eine gute Einführung in asymptotische Notation mit vielen Beispielen und Abschätzungstechniken findet sich
in [CLRS01].
Kapitel 9

Sortieren in Arrays

Sortieralgorithmen gehören zu den am häufigsten angewendeten Algorithmen in der Datenverarbei-


tung. Man hatte daher bereits früh ein großes Interesse an der Entwicklung möglichst effizienter Sor-
tieralgorithmen. Zu diesem Thema gibt es umfangreiche Literatur, nahezu jedes Buch über Algo-
rithmen und Datenstrukturen beschäftigt sich mit Sortieralgorithmen, da sie besonders geeignet sind,
Anfängern Programmiermethodik, Entwurf von Algorithmen und Aufwandsanalyse zu lehren.
Wir erläutern die Sortieralgorithmen vor folgendem Hintergrund. Gegeben ist ein Datentyp Item.

public class Item {


int key;
// data components and methods
}

Objekte dieses Typs sind “Karteikarten”, z. B. aus einer Studentenkartei. Jede Karteikarte enthält
neben den Daten (data components) einen Schlüssel (key) vom Typ int, z. B. die Matrikelnummer,
nach denen sortiert oder gesucht werden kann. Die gesamte Kartei wird durch ein Array vec mit
Grundtyp Item dargestellt. Wir reden daher im weiteren auch von Komponenten oder Elementen statt
Karteikarten.
Die Wahl von int als Schlüsseltyp ist willkürlich. Hier kann jeder andere Typ verwendet werden,
für den eine vollständige Ordnungsrelation definiert ist, also zwischen je zwei Werten a, b genau eine
der Relationen a < b, a = b, a > b gilt. Dies können z. B. auch Strings mit der lexikographischen
Ordnung sein (Meier < Mueller), nur müsste dann der “eingebaute” Vergleich “<” von ganzen Zahlen
durch eine selbstdefinierte Funktion zum Vergleich von Strings ersetzt werden.1

1 Dieskann mit dem Interface Comparable (vgl. Abschnitt 6.3.7) realisiert werden. Die Klasse Item muss da-
zu die Methode public int compareTo(Object o) implementieren. Dabei ist das Item x kleiner als oder
gleich dem Item y wenn x.compareTo((Object)y) <= 0 gilt. Alle Sortieralgorithmen nutzen dann die Abfrage
x.compareTo((Object)y) <= 0 für den Vergleich von x und y.

Version vom 17. Juni 2010

205
206 KAPITEL 9. SORTIEREN IN ARRAYS

9.1 Direkte Methoden

Direkt bedeutet: Sortieren der Komponenten “am Ort”. Ein typisches Beispiel dafür ist Bubblesort.

9.1.1 Sortieren durch Austauschen: Bubblesort

Bubblesort durchläuft das Array mehrmals von hinten nach vorn und lässt durch paarweise Vergleiche
das kleinste Element der restlichen Menge zum linken Ende des Arrays wandern. Stellt man sich das
Array senkrecht angeordnet vor, und die Elemente als Blasen, so steigt bei jedem Durchlauf durch das
Array eine Blase auf die ihrem Gewicht (Schlüsselwert) entsprechende Höhe auf (vgl. Abbildung 9.1)

j vec[j].key i=1 2 3 4 5 6 7
0 63 12 12 12 12 12 12 12
1 24 63 18 18 18 18 18 18
2 12 24 63 24 24 24 24 24
3 53 18 24 63 35 35 35 35
4 72 53 35 35 63 44 44 44
5 18 72 53 44 44 63 53 53
6 44 35 72 53 53 53 63 63
7 35 44 44 72 72 72 72 72

Abbildung 9.1: Phasen bei Bubblesort.

Informell lässt sich Bubblesort wie folgt beschreiben:

1. Gegeben ist vec[] mit n Komponenten.


2. Das Array vec wird n − 1 mal von hinten nach vorn durchlaufen. Ein Durchlauf heißt Phase.
3. Phase i läuft von Komponente j = n − 1 bis j = i und vergleicht jeweils vec[j].key mit
vec[j-1].key. Ist vec[j-1].key > vec[j].key so werden vec[j-1] und vec[j] ge-
tauscht.

Die Korrektheit des Algorithmus ergibt sich direkt aus folgender Schleifeninvarianten, die nach jeder
Phase gilt:
Am Ende der Phase i ist vec[i-1].key der i-kleinste Schlüssel in vec und es gilt: (9.1)
vec[0].key ≤ vec[1].key ≤ . . . ≤ vec[i-1].key ≤ vec[j].key für j = i, i +
1, . . . , n − 1.
Dies ist klar für die 1. Phase. Nimmt man die Richtigkeit für Phase i an (Induktionsvoraussetzung), so
findet Phase i + 1 das kleinste Element in vec[i]...vec[n-1] und bringt es durch ggf. fortgesetzte
Austauschoperationen an die Position i. Also gilt die Invariante auch nach Phase i (Induktionsschluss).
Für i = n − 1 folgt sofort die Korrektheit des Algorithmus.
Dies resultiert in folgender Java Methode.
9.1. DIREKTE METHODEN 207

/**
* Sorts with bubblesort algorithm
* @param vec the array to be sorted
* @exception NullPointerException if <code>vec</code>
* is not initialized
*/
public static void bubbleSort(Item[] vec)
throws NullPointerException {
if (vec == null) throw new NullPointerException();

int n = vec.length;
Item temp;
int bottom; // bottom for each pass
for (bottom = 1; bottom < n; bottom++) {
for (int i = n-1; i >= bottom; i--) {
if (vec[i-1].key > vec[i].key) {
temp = vec[i-1];
vec[i-1] = vec[i];
vec[i] = temp;
}
}
}
}

Wir berechnen nun den Worst Case Aufwand von Bubblesort nach der Anzahl der Vergleiche. Dazu
betrachten wir die Vergleiche pro Phase. Sei n die Anzahl der Komponenten des Arrays.
Phase 1 : n − 1 Vergleiche
Phase 2 : n − 2 Vergleiche
...
Phase i : n−i Vergleiche
...
Phase n − 1 : 1 Vergleich
Also gilt für die Anzahl C(n) der Vergleiche bei n Komponenten
n−1
n(n − 1)
C(n) = 1 + 2 + . . . + (n − 2) + (n − 1) = ∑i= .
i=1 2

Folglich ist C(n) ∈ O(n2 ). Da die Vergleiche unabhängig von der Eingabe durchgeführt werden (auch
2
bei einem bereits sortierten Array) gilt: C(n) = n(n−1)
2 ≥ n3 für n ≥ 3, also C(n) ∈ Ω(n2 ) und damit
C(n) ∈ Θ(n2 ).
Offensichtlich kann dieser Algorithmus verbessert werden, wenn man sich merkt, ob in einer Phase
überhaupt ein Austausch stattgefunden hat. Findet kein Austausch statt, so ist das Array sortiert und
man kann abbrechen.
208 KAPITEL 9. SORTIEREN IN ARRAYS

Eine weitere Verbesserung besteht darin, sich in einer Phase die Position (Index) k des letzten Austau-
sches zu merken. In den darauf folgenden Phasen müssen vec[0]...vec[k] nicht mehr überprüft
werden.
Schließlich kann man noch die Phasen abwechselnd von hinten nach vorn und von vorn nach hinten
laufen lassen (Shakersort), um Asymmetrie zwischen “leichten” Elementen (gehen gleich ganz nach
oben) und “schweren” Elementen (sinken jeweils nur um eine Position ab) zu durchbrechen.
Diese Verbesserungen bewirken aber nur eine Verringerung der mittleren Anzahl der Vergleiche. Für
den Worst Case lassen sich stets (Übung) Beispiele finden, die C(n) = n(n−1)
2 Vergleiche benötigen.
Es gilt also:

Satz 9.1 Bubblesort erfordert Θ(n2 ) Vergleiche im Worst Case.

Neben der Anzahl C(n) der Vergleiche ist für die Laufzeit auch die Anzahl A(n) der Zuweisungen
(Assignments) von Arraykomponenten von großer Bedeutung, da sie außer den Schlüsseln noch wei-
tere (ggf. große) Datenmengen enthalten. In Java ist dies selten der Fall, da vor allem Referenztypen
vorkommen.
Offenbar kann jeder Vergleich einen Austausch und damit 3 Zuweisungen verursachen. Es gilt also
A(n) ≤ 3 ·C(n). Beispiele zeigen, dass A(n) = 3 ·C(n) vorkommt. Also folgt:

Satz 9.2 Bubblesort erfordert Θ(n2 ) Zuweisungen im Worst Case.

9.1.2 Sortieren durch direktes Auswählen: Selection Sort

Wir geben zunächst eine informelle Beschreibung:

1. Gegeben ist vec[] mit n Komponenten.

2. Das Array vec wird n − 1 mal von vorn nach hinten durchlaufen. Ein Durchlauf heißt Phase
(pass).

3. Phase bottom sucht den Index minIndx einer Komponente mit kleinstem Schlüsselwert im
Bereich

vec[bottom], vec[bottom+1],...,vec[n-1],

mittels sequentieller Suche, und tauscht diese Komponente an die Stelle bottom. Es werden
also vec[bottom] und vec[minIndx] vertauscht.

Die Korrektheit basiert hier auf derselben Invarianten wie bei Bubblesort. Es folgt eine Java Methode:

/**
* Sorts with selectionsort algorithm
9.1. DIREKTE METHODEN 209

* @param vec the array to be sorted


* @exception NullPointerException if <code>vec</code>
* is not initialized
*/
public static void selectionSort(Item vec[])
throws NullPointerException {
if (vec == null) throw new NullPointerException();

int minIndx; // Index of smallest key in each pass


int bottom; // bottom for each pass
int i;
Item temp;
int n = vec.length;

for (bottom = 0; bottom < n-1; bottom++) {


// INVARIANT (prior to test):
// All vec[bottom+1..n-1] are >= vec[bottom]
// && vec[0..bottom] are in ascending order
// && bottom >= 0
minIndx = bottom;
for (i = bottom+1; i < n; i++) {
// INVARIANT (prior to test):
// vec[minIndx] <= all
// vec[0..i-1]
// && i >= bottom+1
if (vec[i].key < vec[minIndx].key) {
minIndx = i;
}
}
temp = vec[bottom];
vec[bottom] = vec[minIndx];
vec[minIndx] = temp;
}
}

Für das Beispiel aus Abbildung 9.1 ergeben sich die in Abbildung 9.2 dargestellten Zustände nach
den einzelnen Phasen
Für die Anzahl C(n) der Vergleiche ergibt sich analog zu Bubblesort:
n−1
n(n − 1)
C(n) = ∑ (n − i) = 2
,
i=1

da die sequentielle Suche in Phase i (Bestimmung des Minimum in n − i + 1 Komponenten) gerade


n − i Vergleiche erfordert.
210 KAPITEL 9. SORTIEREN IN ARRAYS

Phase Array vec


Input 63 24 12 53 72 18 44 35
1 12 24 63 53 72 18 44 35
2 12 18 63 53 72 24 44 35
3 12 18 24 53 72 63 44 35
4 12 18 24 35 72 63 44 53
5 12 18 24 35 44 63 72 53
6 12 18 24 35 44 53 72 63
7 12 18 24 35 44 53 63 72

Abbildung 9.2: Phasen bei Selection Sort.

Die Anzahl A(n) der Zuweisungen von Arraykomponenten ist jedoch deutlich geringer, da pro Phase
maximal ein Austausch erfolgt. Also gilt

A(n) ≤ 3(n − 1) = O(n) .

Beispiele zeigen wieder, dass C(n) ∈ Θ(n2 ) und A(n) ∈ Θ(n) gilt.

9.1.3 Sortieren durch direktes Einfügen: Insertion Sort

Diese Methode wird oft beim Kartenspiel genutzt. Die Arraykomponenten (Karten) werden gedank-
lich in eine Zielsequenz vec[0]. . .vec[i-1] (die Karten, die man bereits in der Hand hat) und die
Quellsequenz vec[i]. . .vec[n-1] (die Karten für die eigene Hand, die noch verdeckt auf dem Tisch
liegen) aufgeteilt.
Dann lässt sich der Algorithmus folgendermaßen beschreiben:

1. Gegeben ist vec[] mit n Komponenten. Anfangs besteht die Zielsequenz aus vec[0] und die
Quellsequenz aus vec[1]. . .vec[n-1].

2. Es finden n − 1 Phasen i = 1, . . . , n − 1 statt.

3. In Phase i wird die nächste “Karte” vec[i] der Quellsequenz genommen und an der richtigen
Stelle (bzgl. vec[i].key) in die Zielsequenz vec[0]. . .vec[i-1] eingefügt.

Die Korrektheit dieses Algorithmus folgt aus der folgenden Invariante:


Nach jeder Phase i gilt

vec[0].key ≤ vec[1].key ≤ . . . ≤ vec[i].key . (9.2)

Im Standardbeispiel ergeben sich die in Abbildung 9.3 dargestellten Zustände nach jeder Phase:
9.1. DIREKTE METHODEN 211

Phase Array vec


Input 63 24 12 53 72 18 44 35
1 24 63 12 53 72 18 44 35
2 12 24 63 53 72 18 44 35
3 12 24 53 63 72 18 44 35
4 12 24 53 63 72 18 44 35
5 12 18 24 53 63 72 44 35
6 12 18 24 44 53 63 72 35
7 12 18 24 35 44 53 63 72

Abbildung 9.3: Phasen bei Insertion Sort.

Die Anzahl der Vergleiche hängt davon ab, wie das Einfügen in die Zielsequenz durchgeführt wird.
Bei sequentieller Suche der Stelle (von links nach rechts) ergeben sich im Worst Case folgende Zahlen:
Phase 1 : 1 Vergleich
Phase 2 : 2 Vergleiche
...
Phase n − 1 : n − 1 Vergleiche
n(n−1)
In diesem Fall ist wiederum C(n) = 2 ∈ O(n2 ).
Da die Zielsequenz bereits aufsteigend sortiert ist, lässt sich statt der sequentiellen Suche die binäre
Suche verwenden. Phase i erfordert dann (Zieldatei enthält i Elemente) gemäß Satz 5.1 höchstens
blog ic + 1 Vergleiche. Also gilt dann:
n−1
C(n) ≤ ∑ (blog ic + 1)
i=1
n−1
≤ ∑ (log(n − 1) + 1) = (n − 1)(log(n − 1) + 1)
i=1
= (n − 1) log(n − 1) + (n − 1) = O(n log n)

Bezüglich der Zahl A(n) der Zuweisungen von Arraykomponenten ist in beiden Varianten (sequentiel-
le oder binäre Suche) eine Verschiebung der Komponenten der Quelldatei rechts von der Einfügestelle
k um jeweils eine Stelle erforderlich, im schlimmsten Fall (k = 0) also i Verschiebungen in Phase i.
Dies lässt sich mit i + 1 Zuweisungen realisieren:

temp = vec[i];
for (j = i-1; j >= 0; j--) vec[j+1] = vec[j];
vec[0] = temp;

Also ist
n−1 n n
A(n) ≤ ∑ (i + 1) = ∑ i = ( ∑ i) − 1
i=1 i=2 i=1
n(n+1)
= 2 − 1 ∈ O(n2 ) .
212 KAPITEL 9. SORTIEREN IN ARRAYS

Das Beispiel des absteigend sortierten Arrays zeigt, dass dieser Fall auch eintritt, also A(n) ∈ Ω(n2 )
gilt.
Insertion Sort (mit binärer Suche) ist also bezüglich der Anzahl der Vergleiche sehr gut (O(n log n)),
aber bezüglich der Anzahl der Zuweisungen schlecht (Ω(n2 )).
Die hier vorgestellten direkten Methoden sind mit ihrer Worst Case Laufzeit von Θ(n2 ) als sehr auf-
wendig einzustufen. Wir werden im Rest des Kapitels drei “intelligentere” Sortiermethoden kennen
lernen, die im Mittel, und teilweise auch im Worst Case, mit O(n log n) Vergleichen und Zuweisungen
auskommen.

9.2 Mergesort

Mergesort teilt das zu sortierende Array in zwei gleichgroße Teilfolgen (Unterschied höchstens eine
Komponente), sortiert diese (durch rekursive Anwendung von Mergesort auf die beiden Teile) und
mischt die dann sortierten Teile zusammen.

9.2.1 Mischen sortierter Arrays

Wir betrachten daher zunächst das Mischen von zwei bereits sortierten Arrays. Seien dazu vec1[]
und vec2[] bereits sortierte Arrays der Länge m bzw. n mit Komponenten vom Typ Item. Diese sind
in das Array vec[] der Länge m + n zu verschmelzen.
Dazu durchlaufen wir vec1 und vec2 von links nach rechts mit zwei Indexzeigern i und j wie folgt:

1. Initialisierung: i = 0; j = 0; k= 0;

2. Wiederhole Schritt 3 bis i = m oder j = n.

3. Falls vec1[i].key < vec2[j].key, so kopiere vec1[i] an die Position k von vec und
erhöhe i und k um 1.
Andernfalls kopiere vec2[j] an die Position k von vec und erhöhe j und k um 1.

4. Ist i = m und j < n, so übertrage die restlichen Komponenten von vec2 nach vec.

5. Ist j = n und i < m, so übertrage die restlichen Komponenten von vec1 nach vec.

Bei jedem Wiedereintritt in die Schleife 3 gilt die Invariante

vec[0].key ≤ . . . ≤ vec[k-1].key (9.3)


vec[k-1].key ≤ vec1[i].key ≤ . . . ≤ vec1[m-1].key
vec[k-1].key ≤ vec2[j].key ≤ . . . ≤ vec2[n-1].key

Hieraus folgt sofort, dass vec am Ende aufsteigend sortiert ist.


Als Beispiel betrachten wir die Arrays:
9.2. MERGESORT 213

vec1: 12 24 53 63 vec2: 18 35 44 72

Die dazugehörige Folge der Werte von i,j,k und vec bei jedem Wiedereintritt in die Schleife 3 ist
in Abbildung 9.4 angegeben. Am Ende dieser Schleife ist i = 4 und Schritt 4 des Algorithmus wird
ausgeführt, d. h. der “Rest” von vec2, also die 72, wird nach vec übertragen.

i j k vec
1 0 1 12 - - - - - - -
1 1 2 12 18 - - - - - -
2 1 3 12 18 24 - - - - -
2 2 4 12 18 24 35 - - - -
2 3 5 12 18 24 35 44 - - -
3 3 6 12 18 24 35 44 53 - -
4 3 7 12 18 24 35 44 53 63 -

Abbildung 9.4: Phasen bei Merge.

Sei C(m, n) die maximale Anzahl von Schlüsselvergleichen und A(m, n) die maximale Anzahl von
Zuweisungen von Komponenten beim Mischen.
Vergleiche treten nur in der Schleife 3 auf, und zwar genau einer pro Durchlauf. Da die Schleife
maximal m + n − 1 mal durchlaufen wird, gilt

C(m, n) ≤ m + n − 1 .

Zuweisungen treten genau n + m mal auf, da jede Komponente von vec einen Wert bekommt. Also ist

A(m, n) = m + n .

Wir geben nun eine Java Methode für dieses Verfahren an, und zwar in der (später benötigten) Version,
dass zwei benachbarte, bereits sortierte Teilbereiche eines Arrays vec gemischt werden und dann in
demselben Bereich von vec aufsteigend sortiert gemischt stehen.
Wir verlangen also folgendes Input/Output Verhalten:
Input: vec

left middle right


mit den sortierten Bereichen vec[left]. . .vec[middle] und vec[middle+1]. . .vec[right].
Output: vec

left right
mit dem aufsteigend sortierten Bereich vec[left]. . .vec[right].
Dazu werden zunächst die beiden sortierten Bereiche auf lokale Arrays vec1 und vec2 kopiert, die
dann in den entsprechenden Bereich von vec zurückgemischt werden.
214 KAPITEL 9. SORTIEREN IN ARRAYS

Programm 9.1 merge


/**
* merges two sorted adjacent ranges of an array
* @param vec the array in which this happens
* @param left start of the first range
* @param middle end of the first range
* @param right end of the second range
*/
private static void merge(Item[] vec, int left, int middle, int right) {
int i, j;
int m = middle - left + 1; // length of first array region
int n = right - middle; // length of second array region

// make copies of array regions to be merged


// (only the references to the items)
Item[] copy1 = new Item[m];
Item[] copy2 = new Item[n];
for (i = 0; i < m; i++) copy1[i] = vec[left + i];
for (j = 0; j < n; j++) copy2[j] = vec[middle + 1 + j];

i = 0; j = 0;
// merge copy1 and copy2 into vec[left...right]
while (i < m && j < n) {
if (copy1[i].key < copy2[j].key) {
vec[left+i+j] = copy1[i];
i++;
} else {
vec[left+i+j] = copy2[j];
j++;
}//endif
}//endwhile
if (j == n) { // second array region is completely handled,
// so copy rest of first region
while (i < m) {
vec[left+i+j] = copy1[i];
i++;
}
}
// if (i == m) do nothing,
// rest of second region is already in place
}
9.2. MERGESORT 215

9.2.2 Sortieren durch rekursives Mischen: Mergesort

Mit dieser Methode merge() ergibt sich dann sehr einfach folgende rekursive Variante von Mergesort.

Programm 9.2 mergeSort


/**
* Sorts with mergesort algorithm
* @param vec the array to be sorted
* @exception NullPointerException if <code>vec</code>
* is not initialized
*/
public static void mergeSort(Item vec[])
throws NullPointerException {
if (vec == null) throw new NullPointerException();
mergeSort(vec, 0, vec.length - 1);
}

/**
* sorts array by mergesort in a certain range
* @param <code>vec</code> the array in which this happens
* @param <code>first</code> start of the range
* @param <code>last</code> end of the range
*/
private static void mergeSort(Item[] vec, int first, int last) {
if (first == last) return;
// devide vec into 2 equal parts
int middle = (first + last) / 2;
mergeSort(vec, first, middle); // sort the first part
mergeSort(vec, middle+1, last); // sort the second part
merge(vec, first, middle, last); // merge the 2 sorted parts
}

Die public Methode mergeSort(Item[]) bekommt nur das Array übergeben und ruft die private
Methode mergeSort(Item[], int, int) in den Grenzen 0 und vec.length-1 des Arrays vec
auf.
Die Korrektheit von mergeSort(Item[], int, int) ergibt sich sofort durch vollständige Indukti-
on nach der Anzahl n = last - first + 1 der zu sortierenden Komponenten.
Ist n = 1, also last=first (Induktionsanfang), so wird im Rumpf von mergeSort() nichts gemacht
und das Array vec ist nach Abarbeitung von mergeSort() trivialerweise im Bereich first. . .last
sortiert.
Ist n > 1, so sind first. . .middle und middle+1. . .last Bereiche mit weniger als n Elementen,
die also nach Induktionsvoraussetzung durch die Aufrufe mergeSort(vec, first, middle) und
216 KAPITEL 9. SORTIEREN IN ARRAYS

mergeSort(vec, middle+1, last) korrekt sortiert werden. Die Korrektheit von merge() ergibt
dann die Korrektheit von mergeSort().
Für das Standardbeispiel

a 63 24 12 53 72 18 44 35

ergibt der Aufruf mergeSort(a,0,7) dann den in Abbildung 9.5 dargestellten Ablauf. Dabei be-
schreiben die Einrücktiefe die Aufrufhierarchie (Rekursionsbaum), und die Kästen die bereits sortier-
ten Teile des Arrays.

mergeSort(a,0,7) 63 24 12 53 72 18 44 35
mergeSort(a,0,3) 63 24 12 53 72 18 44 35
mergeSort(a,0,1) 63 24 12 53 72 18 44 35
mergeSort(a,0,0) 63 24 12 53 72 18 44 35
mergeSort(a,1,1) 63 24 12 53 72 18 44 35
merge(a,0,0,1) 24 63 12 53 72 18 44 35
mergeSort(a,2,3) 24 63 12 53 72 18 44 35
mergeSort(a,2,2) 24 63 12 53 72 18 44 35
mergeSort(a,3,3) 24 63 12 53 72 18 44 35
merge(a,2,2,3) 24 63 12 53 72 18 44 35
merge(a,0,1,3) 12 24 53 63 72 18 44 35
mergeSort(a,4,7) 12 24 53 63 72 18 44 35
mergeSort(a,4,5) 12 24 53 63 72 18 44 35
mergeSort(a,4,4) 12 24 53 63 72 18 44 35
mergeSort(a,5,5) 12 24 53 63 72 18 44 35
merge(a,4,4,5) 12 24 53 63 18 72 44 35
mergeSort(a,6,7) 12 24 53 63 18 72 44 35
mergeSort(a,6,6) 12 24 53 63 18 72 44 35
mergeSort(a,7,7) 12 24 53 63 18 72 44 35
merge(a,6,6,7) 12 24 53 63 18 72 35 44
merge(a,4,5,7) 12 24 53 63 18 35 44 72
merge(a,0,3,7) 12 18 24 35 44 53 63 72

Abbildung 9.5: Die Rekursion bei mergeSort().

9.2.3 Die Analyse von Mergesort

Wir ermitteln nun den Worst Case Aufwand C(n) für die Anzahl der Vergleiche und A(n) für die
Anzahl der Zuweisungen von mergeSort() beim Sortieren eines Arrays mit n Komponenten.
9.2. MERGESORT 217

Aus dem rekursiven Aufbau des Algorithmus ergeben sich sofort die folgenden Rekursionsgleichun-
gen für C(n).
C(2) = 1 C(2n) = 2 ·C(n) +C(n, n) für n > 1

In Worten: Das Sortieren eines 2-elementigen Arrays erfordert einen Vergleich. Das Sortieren eines
Arrays der Länge 2n erfordert den Aufwand für das Sortieren von 2 Arrays der Länge n (rekursive
Aufrufe von mergeSort() für die beiden Teile), also 2 · C(n), plus den Aufwand C(n, n) für das
Mischen (Aufruf von merge()).
Da C(n, n) = 2n − 1, ist also

C(2) = 1 C(2n) = 2 ·C(n) + 2n − 1 für n > 1 . (9.4)

Um diese Rekursionsgleichung zu lösen, betrachten wir zunächst den Fall, dass n eine Zweierpotenz
ist, etwa n = 2q . Dann ist n genau q mal durch 2 ohne Rest teilbar und wir erhalten durch mehrfache
Anwendung der Rekursionsgleichung
n n
C(n) = 2 ·C( ) + 2 · − 1
2 2
n
= 2 ·C( ) + n − 1 1 mal angewendet
 2 n n 
= 2 2 ·C( ) + 2 · − 1 + n − 1
4 4
n
= 4 ·C( ) + 2n − 3 2 mal angewendet
 4 n n 
= 4 2 ·C( ) + 2 · − 1 + 2n − 3
8 8
n
= 8 ·C( ) + 3n − 7 3 mal angewendet
8
. . . (insgesamt q − 1 mal anwenden)
= 2q−1 ·C(2) + (q − 1)n − (2q−1 − 1)

Wegen n = 2q und C(2) = 1 folgt hieraus

C(2q ) = (q − 1)2q + 1 .

Wir verifizieren diese eher intuitive Vorgehensweise durch einen formalen Beweis mit vollständiger
Induktion.2

Lemma 9.1 Ist n = 2q , so hat die Rekursionsgleichung 9.4 die Lösung C(2q ) = (q − 1)2q + 1.

Beweis: Der Beweis erfolgt durch vollständige Induktion nach q. Ist q = 1, so ist C(21 ) = 1 und
(q − 1)2q + 1 = 1 (Induktionsanfang). Also sei die Behauptung richtig für 2r mit 1 ≤ r ≤ q. Wir
2 Eine Alternative ist die Verifikation durch Einsetzen der “vermuteten” Formel C(2q ) = (q − 1)2q + 1 in die Rekursions-

gleichung und Nachrechnen der Gleichung. Dies führt auf die gleichen Beweisschritte.
218 KAPITEL 9. SORTIEREN IN ARRAYS

schließen jetzt auf q + 1:

C(2q+1 ) = 2 ·C(2q ) + 2 · 2q − 1 Rekursionsgleichung


= 2 · [(q − 1)2q + 1] + 2 · 2q − 1 Induktionsvoraussetzung
= (q − 1)2q+1 + 2 + 2q+1 − 1
= q · 2q+1 + 1,

was zu zeigen war.

Bezüglich der Anzahl A(n) der Zuweisungen von Arraykomponenten ergibt sich ganz analog:

A(2n) = 2 · A(n) + Zuweisungen in merge()

In merge() werden zunächst die Teile von vec nach vec1 und vec2 kopiert. Dies erfordert 2n Zu-
weisungen. Für das Mergen sind dann wieder A(n, n) = 2n Zuweisungen erforderlich. Also ergibt sich
folgende Rekursionsgleichung:

A(2) = 4 A(2n) = 2 · A(n) + 4 · n für n > 1 (9.5)

Der gleiche Lösungsansatz liefert für n = 2q

A(n) = 2q2q .

Wir betrachten nun den Fall, dass n keine Zweierpotenz ist. Dann unterscheiden sich die jeweiligen
Teilarrays um maximal 1, vgl. Abbildung 9.6.

0 1 2 3 4 5

0 1 2 3 4 5

0 1 2 3 4 5

0 1 3 4

Abbildung 9.6: Rekursionsbaum von mergeSort() für n = 6.

Dies führt dazu, dass im zugehörigen Rekursionsbaum nicht alle Zweige bis auf die unterste Ebene
reichen. Vervollständigt man (gedanklich) den Rekursionsbaum bis auf die unterste Ebene, so würde
9.3. BESCHLEUNIGUNG DURCH AUFTEILUNG: DIVIDE AND CONQUER 219

dies dem Rekursionsbaum für ein Array der Länge n0 entsprechen, wobei n0 die nächstgrößere Zwei-
erpotenz nach n ist, also n0 = min{2r | 2r ≥ n, r = 1, 2, . . .}.
Sei n0 = 2q dieses Minimum. Dann ist 2q−1 < n ≤ 2q = n0 , also n0 = 2 · 2q−1 < 2n sowie q − 1 < log n ≤
q. Also ist (wegen der Vervollständigung):

C(n) ≤ C(n0 ) = C(2q ) = (q − 1)2q + 1


< (log n) · n0 + 1 < (log n) · 2n + 1
= 2n log n + 1 ∈ O(n log n) .

Entsprechend folgt

A(n) ≤ A(n0 ) = A(2q ) = 2q2q


= 2(q − 1 + 1)n0 < 2(1 + log n)n0
< 4n(1 + log n) = 4n + 4n log n ∈ O(n log n) .

Wir erhalten also

Satz 9.3 mergeSort() sortiert ein Array mit n Komponenten mit O(n log n) Vergleichen und Zuwei-
sungen.

Betrachten wir zum Abschluss noch den Rekursionsaufwand und die Rekursionstiefe. Für n = 2q
ist die Rekursionstiefe gerade q = log n, für beliebige n ergibt sich aus der soeben durchgeführten
Vervollständigungsüberlegung
log n0 < log(2n) = log n + 1
als Schranke für die Rekursionstiefe.
Die Anzahl der rekursiven Aufrufe ergibt sich als Summe entlang der Schichten des Rekursionsbaums
zu
q
∑ 2i = 2q+1 − 1 = 2n0 − 1 < 4n − 1 = O(n) .
i=0
Rekursionsaufwand und Rekursionstiefe halten sich also in vernünftigen Grenzen.

9.3 Beschleunigung durch Aufteilung: Divide and Conquer

Mergesort ist ein typisches Beispiel für die sogenannte Beschleunigung durch Aufteilung“. Dieses

Prinzip tritt oft bei der Konzeption von Algorithmen auf. Daher hat man Interesse an einer allgemeinen
Aussage über die Laufzeit in solchen Situationen.

9.3.1 Aufteilungs-Beschleunigungs-Sätze

Gegeben ist ein Problem der Größe a · n mit der Laufzeit f (a · n). Dieses zerlegt man in b Teilprobleme
der Größe n mit Laufzeit f (n) pro Teilproblem, also b · f (n) für alle Teilprobleme zusammen. Ist die
220 KAPITEL 9. SORTIEREN IN ARRAYS

Laufzeit für das Aufteilen und das Zusammenfügen der Teillösungen durch c · n beschränkt, so ergibt
sich die folgende Rekursionsgleichung und der

Satz 9.4 (Aufteilungs-Beschleunigungs-Satz) Seien a > 0, b, c ganze Zahlen und sei folgende Re-
kursionsgleichung gegeben:
c
f (1) ≤
a
f (a · n) ≤ b · f (n) + c · n für n > 1

Dann gilt:

 O(n) , falls a > b
f (n) ∈ O(n · log2 n) , falls a = b
O(nloga b ) , falls a < b

Beweis:: Für n = aq gilt


c q b i
 
f (n) ≤ n ∑
a i=0 a

Dies zeigt man durch Induktion über q. Für q = 0 ist die Summe 1 und daher f (1) ≤ ac .
Die Behauptung sei nun für q gezeigt. Dann ergibt sich im Induktionsschluss auf q + 1:

f (aq+1 ) = f (a · aq )
≤ b · f (aq ) + c · aq (Rekursionsformel)
c q q b i
 
= b· ·a ∑ + c · aq (Induktionsvoraussetzung)
a i=0 a
c q+1 b q b i c q+1
 
=
a
a ∑ a + a ·a
a i=0
c q+1 q b i+1 c q+1
 
= a ∑ + a
a i=0 a a
!
c q+1 q+1 b i
 
= a ∑ +1
a i=1 a

c q+1 q+1 b i
 
= a ∑ .
a i=0 a

Also ist
c loga n b i
 
f (n) ≤ n ∑ .
a i=0 a

Wir betrachten jetzt 3 Fälle:


9.3. BESCHLEUNIGUNG DURCH AUFTEILUNG: DIVIDE AND CONQUER 221

b
1. a > b: Dann ist a < 1 und daher
loga n  i  i

b b a
∑ <∑ = ,
i=0 a i=0 a a−b
b i b 1 a

da die geometrische Reihe ∑∞
i=0 a wegen a < 1 gegen k1 := 1− ba
= a−b konvergiert. Also ist

c · k1
f (n) < n ⇒ f (n) ∈ O(n) .
a
2. a = b: Dann ist
c loga n c c c
f (n) ≤ n ∑ 1 = n(loga n + 1) = n loga n + n .
a i=0 a a a
Für n ≥ a ist loga n ≥ 1 und daher
c c 2c
f (n) ≤ n loga n + n loga n = n loga n
a a a
2c
= ( · loga 2) · n log2 n ∈ O(n log2 n) .
a
3. a < b: Dann ist
c loga n b i
 
f (n) ≤ ·n ∑
a i=0 a
c q q b i
 
= a ∑ da n = aq
a i=0 a
c q i q−i c q q−i i
= ∑b a = a ∑b a
a i=0 i=0
q  i
c q a c ∞  a i
= b ∑ < bq ∑ .
a i=0 b a i=0 b
a i b

∑∞
i=0 b ist wie in Fall 1 eine geometrische Reihe mit Wert k2 = b−a . Also ist
ck2 q ck2 loga n c · k2 loga b
f (n) < b = b = n ∈ O(nloga b ) .
a a a
Hier wurde ausgenutzt, dass bloga n = (aloga b )loga n = (aloga n )loga b = nloga b .

Die Verallgemeinerung von n = aq auf beliebige n folgt analog zu der Verallgemeinerung auf beliebi-
ge n bei Mergesort.

Offenbar ist der Fall 2 gerade der auf Mergesort zutreffende Fall.
Der Aufteilungs-Beschleunigungssatz wurde hier nur in einer speziellen Form bewiesen, um den Be-
weis einfacher zu halten. Wir geben nachstehend eine allgemeinere Version an und verweisen für den
Beweis auf [CLRS01].
222 KAPITEL 9. SORTIEREN IN ARRAYS

Satz 9.5 (Aufteilungs-Beschleunigungs-Satz, Allgemeine Version) Seien a > 0 und b > 0 natürliche
Zahlen und sei die folgende Rekursionsgleichung gegeben:

f (a · n) = b · f (n) + g(n) .

Dann hat f (n) folgendes asymptotisches Wachstum:

1. Ist g(n) = O(nloga b−ε ) für eine Konstante ε > 0, so ist f (n) = Θ(nloga b ).

2. Ist g(n) = Θ(nloga b ), so ist f (n) = Θ(nloga b · log n)

3. Ist g(n) = Ω(nloga b+ε ) für eine Konstante ε > 0, und ist b · g( an ) ≤ c · g(n) für eine Konstante
c < 1 und alle n ≥ n0 (c, n0 geeignet gewählt), so ist f (n) = Θ(g(n)).

Die Unterscheidung erfolgt hier also nach dem Wachstum von g(n) im Verhältnis zu nloga b . Ist g(n)
deutlich kleiner als nloga b (Fall 1), so bestimmt nloga b das Wachstum von f (n). Hat g(n) dasselbe
Wachstum wie nloga b (Fall 2), so kommt im Wachstum von f (n) ein log n Faktor dazu. Ist schließlich
g(n) deutlich größer als nloga b und gilt die zusätzliche Regularitätsbedingung“ aus 3., so bestimmt

g(n) allein die Größenordnung von f (n).
Deutlich kleiner bzw. deutlich größer bedeutet dabei jeweils mindestens um einen polynomialen Fak-
tor nε .

Beispiel 9.1

1. f (3n) = 9 f (n) + n
Hier ergibt sich aus der einfachen Version des Satzes (Fall 3) f (n) = O(nlog3 9 ) = O(n2 ). In
der allgemeinen Version trifft Fall 1 zu, da g(n) ∈ O(nloga b−1 ) (also ε = 1), und man erhält
f (n) = Θ(nloga b ) = Θ(n2 ).

2. f ( 32 n) = f (n) + 1
Hier ist g(n) = 1 und nlog3/2 1 = n0 = 1. Also trifft Fall 2 der allgemeinen Version zu, und man
erhält f (n) = Θ(log n).

3. f (4n) = 3 f (n) + n log n


Hier ist g(n) = n log n und nloga b = nlog4 3 = O(n0,793 ). Also ist g(n) = Ω(nloga b+ε ) mit ε = 0, 2.
Es würde also Fall 3. zutreffen,
 falls wir die Regularitätsbedingung für g(n) zeigen können. Es
ist b · g( an ) = 3 · 4n · log n4 < 43 n log n = 43 g(n), so dass die Regularitätsbedingung mit c = 34
gilt. Also folgt f (n) = Θ(n log n).

4. f (2n) = 2 f (n) + n log n


Diese Rekursionsgleichung fällt nicht unter das allgemeine Schema, da g(n) = n log n zwar
asymptotisch größer als nloga b = n ist, aber nicht um einen Faktor nε . Dieser Fall fällt also in
die Lücke“ zwischen Fall 2. und 3.

9.3. BESCHLEUNIGUNG DURCH AUFTEILUNG: DIVIDE AND CONQUER 223

9.3.2 Multiplikation von Dualzahlen

Als weitere Anwendung betrachten wir die Multiplikation von zwei n-stelligen Dualzahlen. Die tra-
ditionelle Methode erfordert Θ(n2 ) Bit Operationen. Durch Aufteilung und Beschleunigung erreicht
man O(nlog 3 ) = O(n1,59 ) Operationen. Dies ist nützlich bei der Implementation der Multiplikation
beliebig langer Dualzahlen, z. B. in Programmpaketen, die mit den Standard long oder int Zahlen
nicht auskommen.
Seien x, y zwei n-stellige Dualzahlen, wobei n eine Zweierpotenz sei. Wir teilen x, y in zwei 2n -stellige
Zahlen wie folgt:

x = a b
y = c d

Dann ist
n n
xy = (a2 2 + b)(c2 2 + d)
n
= ac2n + (ad + bc)2 2 + bd.
Man hat die Multiplikation also auf 4 Multiplikationen von n2 -stelligen Zahlen und einige Additionen
n
und Shifts (Multiplikationen mit 2 2 bzw. 2n ), die nur linearen Aufwand erfordern) zurückgeführt.
Dies führt zur Rekursionsgleichung
n
T (n) = 4 · T ( ) + c0 · n ,
2
mit der Lösung T (n) = Θ(n2 ), also ohne Gewinn gegenüber der traditionellen Methode.
Die Anweisungen
u := (a + b)(c + d)
v := ac
w := bd
n
z := v2n + (u − v − w)2 2 + w
n
führen jedoch zur Berechnung von z = xy mit 3 Multiplikation von Zahlen der Länge 2 bzw. n2 + 1, da
n

bei a + b bzw. c + d ein Übertrag auf die 2 + 1 -te Position entstehen könnte.
Ignorieren wir diesen Übertrag, so erhält man
n
T (n) = 3T + c1 n
2
mit der gewünschten Lösung T (n) = Θ(nlog2 3 ).
Um den Übertrag zu berücksichtigen, schreiben wir a + b und c + d in der Form

a+b = α ā
c+d = γ c̄
224 KAPITEL 9. SORTIEREN IN ARRAYS

n n
also a + b = α2 2 + ā, c + d = γ2 2 + c̄ mit den führenden Bits α, γ und den 2n -stelligen Resten ā, c̄.
Dann ist
n
(a + b)(c + d) = αγ2n + (γ ā + α c̄)2 2 + āc̄ .
Hierin tritt nur ein Produkt von 2n -stelligen Zahlen auf (nämlich āc̄). Der Rest sind Shifts bzw. lineare
Operationen auf 2n -stelligen Zahlen (z. B. α ā).
Daher erhält man insgesamt die Rekursionsgleichung
n
T (n) = 3T + c2 n ,
2
wobei c2 n folgenden Aufwand enthält:

– Additionen a + b, c + d : 2 · n2
– Produkt αγ : 1
– Shift von αγ auf αγ2n : n
– Produkte α ā, γ c̄: 2 · 2n
n
– Addition α ā + γ c̄: 2 +1
– Shift von α ā, γ c̄ um n2 Stellen: n
2
– Shift von v auf v2n : n
– Addition u − v − w: 2( n2 + 1)
– Shift von u − v − w um n2 Stellen: n
2
– Addition zu z: 2n

Dieser Aufwand addiert sich zu 8,5 n + 2 ≤ 9n für n ≥ 2. Also kann c2 als 9 angenommen werden.
Als Lösung erhält man nach dem Aufteilung-Beschleunigungs-Satz

T (n) = Θ(nlog 3 ) = Θ(n1,59 ) .

Der Trick“ bestand also darin, auf Kosten zusätzlicher Additionen und Shifts, eine teure“ Multipli-
” ”
kation von 2n -stelligen Zahlen einzusparen. Die rekursive Anwendung dieses Tricks ergibt dann die
Beschleunigung von Θ(n2 ) auf Θ(n1,59 ). Für die normale Computerarithmetik (n = 32) zahlt sich die-
ser Trick nicht aus, jedoch bedeutet er für Computerarithmetiken mit beliebigstelligen Dualzahlen,
die meist softwaremäßig realisiert werden, eine wichtige Beschleunigung.
Das Verfahren lässt sich natürlich auch im Dezimalsystem anwenden. Wir geben ein Beispiel für
n = 4:

x = 4217 y = 5236

Dann ist

a = 42, b = 17 und a + b = 59 ;
c = 52, d = 36 und c + d = 88 .
9.4. QUICKSORT 225

Es folgt

u = (a + b)(c + d) = 59 · 88 = 5192
v = ac = 42 · 52 = 2184
w = bd = 17 · 36 = 612
xy = v · 104 + (u − v − w) · 102 + w
= 2184 · 104 + 2396 · 102 + 612
= 21.840.000 + 239.600 + 612
= 22.080.212

Auf ähnliche Weise wie die Multiplikation von Zahlen lässt sich auch die Multiplikation (großer) n×n
Matrizen beschleunigen, vgl. [CLRS01] Hier erhält man die Rekursionsgleichung

T (2n) = 7 · T (n) + 14n2

mit der Lösung (gemäß Satz 9.5) Θ(nlog 7 ) = Θ(n2,81 ), also eine Beschleunigung gegenüber der nor-
malen Methode mit dem Aufwand Θ(n3 ). Hier lassen sich noch weitere Beschleunigungen erzielen.
Der momentane “Rekord” steht bei O(n2,39 ), vgl. [CW87].

9.4 Quicksort

Quicksort basiert (im Gegensatz zu Mergesort) auf variabler Aufteilung des Eingabearrays. Es wurde
1962 von Hoare entwickelt. Es benötigt zwar im Worst Case Ω(n2 ) Vergleiche, im Mittel jedoch nur
O(n log n) Vergleiche, und ist aufgrund empirischer Vergleiche allen anderen O(n log n) Sortierverfah-
ren überlegen.

9.4.1 Der Algorithmus

Wir geben zunächst eine Grobbeschreibung von Quicksort an.

1. Gegeben ist vec[] mit n Komponenten.

2. Wähle eine beliebige Komponente vec[pivot].

3. Zerlege das Array vec in zwei Teilbereiche vec[0]. . .vec[k-1] und vec[k+1]. . .vec[n-1]
mit

a) vec[i].key ≤ vec[pivot].key für i = 0, . . . , k − 1


b) vec[k].key = vec[pivot].key
c) vec[j].key > vec[pivot].key für j = k + 1, . . . , n − 1.

4. Sofern ein Teilbereich aus mehr als einer Komponente besteht, so wende Quicksort rekursiv auf
ihn an.
226 KAPITEL 9. SORTIEREN IN ARRAYS

Die Korrektheit des Algorithmus folgt leicht durch vollständige Induktion. Die Aufteilung erzeugt
Arrays kleinerer Länge, die nach Induktionsvoraussetzung durch die Aufrufe von Quicksort in Schritt
4. korrekt sortiert werden. Die Eigenschaften 3a)–3c) ergeben dann die Korrektheit für das gesamte
Array.
Im Standardbeispiel ergibt sich, falls man stets die mittlere Komponente wählt (gekennzeichnet durch
*) die in Abbildung 9.7 dargestellte Folge von Zuständen (jeweils nach der Aufteilung). Die umrahm-
ten Bereiche geben die aufzuteilenden Bereiche an.

Input 63 24 12 53* 72 18 44 35
1. Aufteilung 18 24 12* 35 44 53 72* 63
2. Aufteilung 12 24 18* 35 44 53 63 72
3. Aufteilung 12 18 24 35* 44 53 63 72
Output 12 18 24 35 44 53 63 72

Abbildung 9.7: Phasen bei Quicksort.

Wir betrachten nun die Durchführung der Aufteilung im Detail. Da sie rekursiv auf stets andere Teile
des Arrays vec angewendet wird, betrachten wir einen Bereich von loBound bis hiBound.
Grobbeschreibung der Aufteilung
1. Gegeben ist vec und der Bereich zwischen loBound und hiBound.

2. Wähle eine Komponente vec[pivot].

3. Tausche vec[pivot] mit vec[loBound].

4. Setze Indexzeiger loSwap auf loBound+1 und hiSwap auf hiBound.

5. Solange loSwap ≤ hiSwap wiederhole:

5.1 Inkrementiere ggf. loSwap solange, bis vec[loSwap].key > vec[loBound].key.


5.2 Dekrementiere ggf. hiSwap solange, bis vec[hiSwap].key ≤ vec[loBound].key.
5.3 Falls loSwap < hiSwap, so vertausche vec[loSwap] und vec[hiSwap], und inkremen-
tiere bzw. dekrementiere loSwap und hiSwap um jeweils 1.

6. Tausche vec[loBound] und vec[hiSwap].

Abbildung 9.8 illustriert diese Aufteilung. ` und h geben die jeweilige Position von loSwap und
hiSwap an, ∗ das gewählte vec[pivot] Element.
Bei jedem Eintritt in die Schleife 5 gilt die Invariante:
vec[i].key ≤ vec[pivot].key für i = loBound . . . loSwap − 1,
vec[j].key > vec[pivot].key für j = hiSwap + 1 . . .hiBound,
(9.6)
loSwap < hiSwap ⇒ vec[loSwap-1].key ≤ vec[pivot].key ≤
≤ vec[hiSwap+1].key.
9.4. QUICKSORT 227

Input 63 24 12 53 72 18 44 35
Schritt 2 63 24 12 53* 72 18 44 35
Schritt 3 53* 24 12 63 72 18 44 35
Schritt 4 53* 24` 12 63 72 18 44 35h
Am Ende von 5.1 53* 24 12 63` 72 18 44 35h
Am Ende von 5.2 53* 24 12 63` 72 18 44 35h
Schritt 5.3 53* 24 12 35 72` 18 44h 63
Am Ende von 5.1 53* 24 12 35 72` 18 44h 63
Am Ende von 5.2 53* 24 12 35 72` 18 44h 63
Schritt 5.3 53* 24 12 35 44 18`h 72 63
Am Ende von 5.1 53* 24 12 35 44 18` 72h 63
Am Ende von 5.2 53* 24 12 35 44 18` 72h 63
Schritt 5.3 53* 24 12 35 44 18` 72h 63
Schritt 6 18 24 12 35 44 53* 72 63

Abbildung 9.8: Die Aufteilung bei Quicksort im Detail.

Beim Austritt gilt zusätzlich

loSwap ≥ hiSwap, vec[hiSwap].key ≤ vec[pivot].key, (9.7)

so dass der Tausch in Schritt 6 die “Pivot”-Komponente genau an die richtige Stelle tauscht.

Lemma 9.2 Enthält der aufzuteilende Bereich m Komponenten, so ist die Anzahl der Vergleiche von
Komponenten für die Aufteilung m − 1.

Beweis: Da loSwap und hiSwap nach einem Austausch inkrementiert bzw. dekrementiert werden,
wird jedes anderen Elemente genau einmal mit dem Pivotelement verglichen. Dies ergibt m − 1 Ver-
gleiche.

Es folgt eine Java Methode dieses Algorithmus.

Programm 9.3 quickSort


/**
* Sorts with quicksort algorithm
* @param vec the array to be sorted
* @exception NullPointerException if <code>vec</code>
* is not initialized
*/
public static void quickSort(Item[] vec)
throws NullPointerException {
228 KAPITEL 9. SORTIEREN IN ARRAYS

if (vec == null) throw new NullPointerException();


quickSort(vec, 0, vec.length - 1);
}

/**
* sorts array by quicksort in a certain range
* @param vec the array in which this happens
* @param loBound start of the range
* @param hiBound end of the range
*/
private static void quickSort(Item[] vec, int loBound, int hiBound) {
int loSwap, hiSwap;
int pivotKey, pivotIndex;
Item temp, pivotItem;

if (hiBound - loBound == 1) { // Two items to sort


if (vec[loBound].key > vec[hiBound].key) {
temp = vec[loBound];
vec[loBound] = vec[hiBound];
vec[hiBound] = temp;
}
return;
}
pivotIndex = (loBound + hiBound) / 2; // 3 or more items to sort
pivotItem = vec[pivotIndex];
vec[pivotIndex] = vec[loBound];
vec[loBound] = pivotItem;
pivotKey = pivotItem.key;
loSwap = loBound + 1;
hiSwap = hiBound;
do {
while (loSwap <= hiSwap && vec[loSwap].key <= pivotKey)
// INVARIANT (prior to test):
// All vec[loBound+1..loSwap-1]
// are <= pivot && loSwap <= hiSwap
loSwap++;
while (vec[hiSwap].key > pivotKey)
// INVARIANT (prior to test):
// All vec[hiSwap+1..hiBound]
// are > pivot && hiSwap >= loSwap
hiSwap--;
if (loSwap < hiSwap) {
temp = vec[loSwap];
vec[loSwap] = vec[hiSwap];
9.4. QUICKSORT 229

vec[hiSwap] = temp;
loSwap++;
hiSwap--;
}
// INVARIANT: All vec[loBound..loSwap-1] are <= pivot
// && All vec[hiSwap+1..hiBound] are > pivot
// && (loSwap < hiSwap) -->
// vec[loSwap-1] <= pivot < vec[hiSwap-1]
// && (loSwap >= hiSwap) --> vec[hiSwap] <= pivot
// && loBound <= loSwap <= hiSwap+1 <= hiBound+1
} while (loSwap <= hiSwap);
vec[loBound] = vec[hiSwap];
vec[hiSwap] = pivotItem;

if (loBound < hiSwap-1) // 2 or more items in 1st subvec


quickSort(vec, loBound, hiSwap-1);

if (hiSwap+1 < hiBound) // 2 or more items in 2nd subvec


quickSort(vec, hiSwap+1, hiBound);
}

9.4.2 Der Rekursionsaufwand von Quicksort

Da die Aufteilung (im Unterschied zur Mergesort) variabel ist, hat der Rekursionsbaum bei Quicksort
i. a. Teilbäume unterschiedlicher Höhe. Im Standardbeispiel ergibt sich der in Abbildung 9.9 darge-
stellte Baum.

QuickSort(vec,0,7)

QuickSort(vec,0,4) QuickSort(vec,6,7)

QuickSort(vec,1,4)

QuickSort(vec,2,4)

Abbildung 9.9: Rekursionsbaum zu Quicksort.

Man sieht, dass der Baum am tiefsten wird, wenn als Vergleichselement jeweils das kleinste oder
größte Element des aufzuteilenden Bereiches gewählt wird. In diesem Fall entartet der Rekursions-
baum zu einer Liste (keine Verzweigungen). Die Rekursionstiefe kann also bis zu n − 1 betragen.
Wir zeigen durch Induktion, dass auch die Anzahl R(n) der rekursiven Aufrufe bei einem Arraybereich
230 KAPITEL 9. SORTIEREN IN ARRAYS

der Länge n höchstens n − 1 beträgt.


Für n = 1 erfolgt kein Aufruf, also gilt R(1) = 0 (Induktionsanfang). Für n > 1 bewirkt der erste
Aufruf eine Aufteilung in Bereiche mit n1 und n2 Komponenten, wobei n1 + n2 = n − 1 gilt, da die
Vergleichskomponente weg fällt. Also sind n1 , n2 < n und man erhält

R(n) = 1 + R(n1 ) + R(n2 )


≤ 1 + (n1 − 1) + (n2 − 1) (Induktionsvoraussetzung)
= n1 + n2 − 1
< n − 1.

Also ist R(n) ≤ n − 1.

9.4.3 Der Worst Case Aufwand von Quicksort

Vergleiche von Schlüsseln treten bei Quicksort nur bei den Aufteilungen auf. Dabei muss jeder andere
Schlüssel mit dem Vergleichsschlüssel verglichen werden, also erfolgen bei einem Bereich von n
Komponenten mindestens (und mit Lemma 9.2 sogar genau) n − 1 Vergleiche.
Wird nun jeweils der größte bzw. kleinste Schlüsselwert als Vergleichsschlüssel gewählt, so verklei-
nert sich der Bereich jeweils nur um ein Element und man erhält:

n(n − 1)
(n − 1) + (n − 2) + . . . + 2 + 1 = Vergleiche.
2
Also gilt für die Worst Case Anzahl C(n) von Vergleichen:

C(n) = Ω(n2 ) .

9.4.4 Der mittlere Aufwand von Quicksort

Quicksort ist also im Worst Case schlecht. Erfahrungsgemäß ist Quicksort aber sehr schnell im Ver-
gleich zu anderen Ω(n2 ) Sortierverfahren wie Bubblesort. Dies liegt daran, dass der Worst Case nur
bei wenigen Eingabefolgen auftritt.
Man wird daher Quicksort gerechter, wenn man nicht den Worst Case betrachtet, sondern den Auf-
wand über alle möglichen Eingabefolgen mittelt, also den mittleren Aufwand C(n) bei Gleichver-
teilung aller n! Reihenfolgen der Schlüssel 1, 2, . . . , n betrachtet. Gleichverteilung bedeutet hier, dass
jede Reihenfolge (Permutation) der Werte 1, . . . , n mit dem gleichen Gewicht (nämlich 1) in das Mittel
eingeht.3
Als Vergleichsbeispiel betrachten wir das Würfeln mit einem Würfel mit den Augenzahlen 1, 2, . . . , 6.
Dann ergibt sich die mittlere Augenzahl bei Gleichverteilung zu 1+2+3+...+6
6 = 3, 5. Die mittlere
3 In der Terminologie der Wahrscheinlichkeitstheorie sagt man, dass jede Reihenfolge mit der gleichen Wahrscheinlich-

keit 1/n! auftritt (daher der Name Gleichverteilung), und man nennt C(n) auch die erwartete Anzahl der Vergleiche.
9.4. QUICKSORT 231

Schrittlänge beim Mensch-ärgere-dich-nicht beträgt also 3, 5; der Worst Case (bzgl. Weiterkommen)
jedoch nur 1.
Für n = 3 ergeben sich bei Quicksort 3! = 6 Permutationen, nämlich 123, 132, 213, 231, 312, 321.
Die Anzahl der Vergleiche pro Permutation beträgt (gemäß der Implementation in Programm 9.3)
2, 4, 4, 4, 4, 2. Der mittlere Aufwand beträgt also 2 + 4 + 4 3!
+4+4+2
= 10
3 Vergleiche gegenüber
dem Worst Case von 4 Vergleichen.
Sei Π die Menge aller Permutationen von 1, . . . , n. Für π ∈ Π sei C(π) die Anzahl von Vergleichen,
die Quicksort benötigt, um π zu sortieren. Dann ist
1
C(n) = ∑ C(π) .
n! π∈Π

Wir werden jetzt C(n) nach oben abschätzen. Dafür teilen wir die Menge Π aller Permutationen in die
Mengen Π1 , Π2 , . . . , Πn , wobei
Πk = {π ∈ Π | das Vergleichselement hat den Wert k} .

Für n = 3 ergibt sich Π1 = {213, 312}, Π2 = {123, 321} und Π3 = {132, 231}.
In Πk ist das Vergleichselement fest vorgeschrieben, die anderen Komponenten können jedoch in jeder
Reihenfolge auftreten. Also ist
|Πk | = (n − 1)! für k = 1, . . . , n .
Für alle π ∈ Πk ergibt die erste Aufteilung in Quicksort die Teilarrays bestehend aus einer Permutation
π1 von 1, 2, . . . , k − 1 und einer Permutation π2 von k + 1, . . . , n (da ja das Vergleichselement gerade k
ist).
Z(π) sei die Anzahl der Vergleiche mit der π in die Teile π1 und π2 zerlegt wird. Dann ist für alle
π ∈ Πk
C(π) = Z(π) +C(π1 ) +C(π2 ) .
Dabei ist Z(π) ≤ n (vgl. Lemma 9.2). Summiert über alle π ∈ Πk , so ergibt sich wegen |Πk | = (n−1)!:

∑ C(π) = ∑ [Z(π) +C(π1 ) +C(π2 )] = ∑ Z(π) + ∑ C(π1 ) + ∑ C(π2 )


π∈Πk π∈Πk π∈Πk π∈Πk π∈Πk
| {z } | {z } | {z }
=:S1 =:S2 =:S3

Hierin ist wegen Lemma 9.2


S1 ≤ ∑ n = n(n − 1)! = n! .
π∈Πk
Wenn π alle Permutationen aus Πk durchläuft, entstehen bei π1 alle Permutationen von 1, . . . , k − 1,
und zwar jede (n − 1)!/(k − 1)! mal, da Πk ja insgesamt (n − 1)! Permutationen enthält, und wegen
der Arbeitsweise des Aufteilungsalgorithmus (und der Gleichverteilung der Permutationen in Πk ) jede
Permutation von 1, . . . , k − 1 gleich häufig entsteht. Also ist
(n − 1)!
S2 = ∑ C(π1 )
(k − 1)! π1 Permutation von 1,...,k−1

= (n − 1)! C(k − 1) .
232 KAPITEL 9. SORTIEREN IN ARRAYS

Entsprechend folgt
S3 = (n − 1)! C(n − k) .
Durch Zusammensetzen aller Gleichungen bzw. Ungleichungen ergibt sich
1
C(n) = ∑ C(π)
n! π∈Π
1 n 1 n
= ∑ ∑ C(π) = ∑ (S1 + S2 + S3 )
n! k=1 π∈Πk n! k=1
1 n
≤ ∑ (n! + (n − 1)! C(k − 1) + (n − 1)! C(n − k))
n! k=1
n! n (n − 1)! n (n − 1)! n
= ∑ 1 + ∑ C(k − 1) + ∑ C(n − k)
n! k=1 n! k=1 n! k=1
1 n−1 1 n−1
= n+ ∑ C(k) + ∑ C(k)
n k=0 n k=0
2 n−1
= n+ ∑ C(k) .
n k=0

Wir haben damit eine Rekursionsgleichung für C(n) gefunden. Beachtet man noch die Anfangswerte

C(0) = C(1) = 0, C(2) = 1

so ist
2 n−1
C(n) ≤ n + ∑ C(k) für n ≥ 2 .
n k=2

Lemma 9.3 Für die Lösung r(n) der Rekursionsgleichung

2 n−1
r(n) = n + ∑ r(k) für n ≥ 2
n k=2

mit den Anfangswerten r(0) = r(1) = 0, r(2) = 1 gilt für alle n ≥ 2

r(n) ≤ 2n ln n . 4

Beweis: Der Beweis erfolgt durch vollständige Induktion nach n mit dem Ansatz r(n) ≤ cn ln n, wobei
die Konstante c während des Beweises ermittelt wird.
Induktionsanfang: Für n = 2 ist r(2) = 1. Andererseits ist c · 2 ln 2 ≈ c · 1, 39. Also gilt der Induktions-
anfang für alle c ≥ 1.
Induktionsvoraussetzung: Die Behauptung gelte für 2, 3, . . . , n − 1 und ein festes c ≥ 1.
4 ln = log n (natürlicher Logarithmus).
e
9.4. QUICKSORT 233

Schluss auf n:

2 n−1
r(n) = n + ∑ r(k)
n k=2
2 n−1
≤ n+ ∑ c · k ln k (nach Induktionsvoraussetzung)
n k=2

Um diesen Ausdruck weiter nach oben abzuschätzen, betrachten wir die Funktion f (x) = x ln x. Dann
ist ∑n−1
k=2 k ln k gerade der Flächeninhalt der schattierten Fläche unter der Kurve f (x), siehe Abbil-
dung 9.10. Also gilt:

n−1 Zn
∑ k ln k ≤ x ln x dx
k=2
2
n Zn
x2 x
= ln x − dx (partielle Integration)
2 2 2
2
n2 n2
= ln n − 2 ln 2 − [ − 1]
2 4
n2 n2
≤ ln n −
2 4

266 KAPITEL 10. SORTIEREN IN ARRAYS

f (x) = x ln x

x
1 2 3 4 ··· n−1 n

Abbildung 10.10:
Abbildung 9.10:Verlauf
Verlaufvon
von ff (x) = xxlnlnx.x.
(x) =

2c
= c · n ln n + 2n − n
! "# 4 $
=:R(n)
≤ c · n ln n falls R(n) ≤ 0 .

Nun ist
c
R(n) = 2n − n
234 KAPITEL 9. SORTIEREN IN ARRAYS

Hieraus folgt

2 n−1 2c n−1
r(n) ≤ n + ∑ c · k · ln k = n + ∑ k ln k
n k=2 n k=2
2c n2 n2
 
≤ n+ ln n −
n 2 4
2c 2c
= n + n ln n − n
2 4
c
= c · n ln n + n − n
| {z2 }
=:R(n)
≤ c · n ln n falls R(n) ≤ 0 .

Nun ist
c
R(n) = n − n
2
≤ 0 für c ≥ 2 .

Aus dem Lemma folgt:

Satz 9.6 Für die mittlere Anzahl C(n) der Vergleiche zum Sortieren eines n-elementigen Arrays mit
Quicksort gilt
C(n) ∈ O(n log n) .

Beweis: Aus C(n) ≤ r(n) und Lemma 9.3 folgt


log n 2
C(n) ≤ 2 · n ln n = 2 · n · = n log n
log e log e
für alle n ≥ 2.
2
Also ist C(n) ∈ O(n log n) mit der O-Konstanten log e ≈ 1,386.

Entsprechend kann man für die mittlere Anzahl A(n) von Zuweisungen beweisen (Übung)

A(n) = O(n log n).

Quicksort arbeitet also im Mittel beweisbar sehr schnell, und dies wird auch in allen Laufzeituntersu-
chungen bestätigt. Quicksort ist der Sortieralgorithmus, den man verwenden sollte.
9.5. HEAPSORT 235

9.5 Heapsort

Heapsort basiert im Gegensatz zu Mergesort und Quicksort nicht auf dem Prinzip der Aufteilung,
sondern nutzt eine spezielle Datenstruktur (den Heap), mit der wiederholt auf das größte Element
eines Arrays zugegriffen wird. Es ähnelt damit eher einem verbesserten Selection Sort.

9.5.1 Die Grobstruktur von Heapsort

Ein Heap (auch Priority Queue genannt) ist eine abstrakte Datenstruktur mit folgenden Kennzeichen:
Wertebereich: Eine Menge von Werten des (homogenen) Komponententyps. Alle Komponenten be-
sitzen einen Wert (Schlüssel).
Operationen:

1. Einfügen einer Komponente.

2. Zugriff auf die Komponente mit dem größten Wert.

3. Entfernen der Komponente mit dem größten Wert.

4. Änderung des Wertes einer Komponente.

Statt des größten Wertes wird auch oft der kleinste Wert in 2. und 3. genommen.
Bei der Anwendung in Heapsort sind die Komponenten von vec die Elemente des Heaps, und die
Schlüssel vec[i].key sind die Werte. Dann arbeitet Heapsort nach folgender Idee:
Grobstruktur von Heapsort

1. Gegeben sei das Array vec[] mit n Komponenten.

2. Initialisiere den Heap mit den Komponenten von vec.

3. for i := n − 1 downto 0 do

3.1 Greife auf das größte Element des Heaps zu.


3.2 Weise diesen Wert der Arraykomponente vec[i] zu.
3.3 Entferne das größte Element aus dem Heap.

Die Korrektheit des Algorithmus ist offensichtlich, und auch die Ähnlichkeit zu Selection Sort. Um zu
einem schnellen Algorithmus zu kommen, muss man den Heap so implementieren, dass die benötigten
Operationen schnell ausgeführt werden können.
In Heapsort sind dies die Operationen 2 (in Schritt 3.1) und 3 (in Schritt 3.3), die jeweils n mal
nacheinander ausgeführt werden. Hinzu kommt das Initialisieren des Heaps in Schritt 2.
Wir brauchen offenbar nicht alle Heapoperationen (4 wird nicht benötigt) und die anderen Opera-
tionen nur in bestimmter Reihenfolge (Einfügen nur bei der Initialisierung, Zugriff und Entfernen
236 KAPITEL 9. SORTIEREN IN ARRAYS

stets nacheinander). Daher werden wir keinen allgemeinen Heap verwenden (vgl. dazu [CLRS01]),
sondern die benötigten Heapoperationen direkt im Array vec implementieren, und zwar so, dass gilt:
Worst Case-Aufwand der hier gewählten Heap Implementation

• Initialisierung in O(n).

• Zugriff auf das größte Element in O(1).

• Entfernen des größten Elements in O(log n).

Dabei werden sowohl Vergleiche als auch Zuweisungen von Arraykomponenten berücksichtigt. Hieraus
folgt sofort:

Satz 9.7 Der Worst Case Aufwand zum Sortieren eines n-elementigen Arrays mit Heapsort beträgt
O(n log n).

9.5.2 Die Implementation des Heaps

Die Grundidee besteht darin, sich das Array vec als binären Baum vorzustellen, wobei die Kompo-
nenten der Reihe nach in die Schichten 0, 1, 2 . . . von links nach rechts angeordnet werden.
Für das Standardbeispiel

vec = 63 24 12 53 72 18 44 35
0 1 2 3 4 5 6 7

ergibt sich so der in Abbildung 9.11 dargestellte Baum.

0 63

1 24 2 12

3 53 4 72 5 18 6 44

7 35

Abbildung 9.11: Array als voller Baum.

Für einen Knoten v in einem binären Baum bezeichnet man die von v aus über eine gerichtete Kante
erreichbaren Knoten als Söhne und nennt v den Vater dieser Söhne. Bei 2 Söhnen unterscheidet man
(bzgl. einer gegebenen Darstellung) zwischen linkem und rechtem Sohn.
Mit diesen Bezeichnungen gelten für den Baum zu einem Heap folgende Eigenschaften:
9.5. HEAPSORT 237

Lemma 9.4

a) vec[0] ist die Wurzel des Baumes.

b) Der linke Sohn von vec[i] (falls vorhanden) ist vec[2i+1].

c) Der rechte Sohn von vec[i] (falls vorhanden) ist vec[2i+2].

d) Nur Knoten vec[i] mit i ≤ 2n haben Söhne.


 

e) Der Baum ist voll.

Beweis: Übung.

Wir sagen, dass vec die Heapeigenschaft (heap ordering) erfüllt, wenn für i = 0, . . . , n − 1 gilt:

vec[i].key ≥ vec[2i+1].key falls 2i + 1 < n


vec[i].key ≥ vec[2i+2].key falls 2i + 2 < n

Für jeden Knoten vec[i] mit einem oder zwei Söhnen ist also der key-Wert der Söhne nicht größer
als der des Vaters.
Hieraus ergibt sich direkt:

Lemma 9.5 Erfüllt vec die Heapeigenschaft, so gilt:

a) vec[0].key ist der größte auftretende Schlüsselwert.

b) Entlang jeden Weges von einem Blatt zu der Wurzel sind die Schlüsselwerte aufsteigend sortiert.

Erfüllt vec die Heapeigenschaft, so kann also auf die größte Komponente vec[0] in O(1) Zeit zuge-
griffen werden. Wenn wir sie entfernen, so haben beide Teilbäume noch die Heapeigenschaft. Wenn
wir dann ein neues Element an die Wurzel stellen (einfügen), so müssen wir die Heapeigenschaft
wieder herstellen.
Dazu verwenden wir die Funktion heapify(). Sie setzt voraus, dass die Heapeigenschaft bereits im
Bereich vec[top+1]. . .vec[bottom] gilt, fügt vec[top] hinzu und ordnet die Komponenten so um,
dass hinterher die Heapordnung im Bereich vec[top]. . .vec[bottom] gilt. Dazu werden folgende
Schritte ausgeführt:

1. Ermittle den größeren der beiden Söhne (child) von top (falls keine Söhne existieren, so ist
die Heapeigenschaft trivialerweise erfüllt, falls nur ein Sohn existiert, so nehme diesen).

2. Vergleiche vec[child] mit vec[top]. Falls vec[child].key > vec[top].key so tausche


vec[top] und vec[child].
238 KAPITEL 9. SORTIEREN IN ARRAYS

3. Wende heapify() rekursiv auf child an.

Die Korrektheit von heapify() sieht man wie folgt:


Seien r und s die Söhne von top und seien b = vec[r], c = vec[s] und a = vec[top] die zu-
gehörigen key-Werte. In den dazugehörigen Teilbäumen (siehe Abbildung 9.12 gilt nach Vorausset-
zung die Heapeigenschaft. O. B. d. A. sei r der größere Sohn (also b ≥ c) und b > a. Dann werden
die Inhalte der Komponenten top und r getauscht. Nach dem Tausch ist b = vec[top].key > a, c
und somit die Heapeigenschaft in top erfüllt. Im rechten Teilbaum gilt sie unverändert. Im linken
Teilbaum könnte sie verletzt sein, weshalb der rekursive Aufruf von heapify() für r nötig wird (und
per Induktionsannahme die Herstellung der Heapeigenschaft in diesem Teilbaum sichert).

top a top b

r b s c r a s c

Abbildung 9.12: Zur Korrektheit von heapify()

Als Beispiel betrachten wir in Abbildung 9.13 das Einfügen von 27 als Wurzel in einen Baum, dessen
Teilbäume beide die Heapeigenschaft erfüllen. Durchgezogene Linien bedeuten dabei, dass die Hea-
peigenschaft erfüllt ist, gepunktete Linien den durch heapify() durchgeführten Vergleich mit den
Söhnen.
Es folgt eine Java Implementation von heapify():

Programm 9.4 heapify


/**
* establishes heap property in a certain range
* @param vec the array in which this happens
* @param top start of the range
* @param bottom end of the range
*/
private static void heapify(Item[] vec, int top, int bottom) {
Item temp;
int child;

if (2*top+1 > bottom) return; // nothing to do

if (2*top+2 > bottom) {


// vec[2*top+1] is only child of vec[top]
child = 2*top+1;
} else { // 2 sons, determine bigger one
9.5. HEAPSORT 239

0 27 0 63

1 63 2 44 1 27 2 44

3 53 4 24 5 18 6 12 3 53 4 24 5 18 6 12

7 35 7 35

0 63 0 63

1 53 2 44 1 53 2 44

3 27 4 24 5 18 6 12 3 35 4 24 5 18 6 12

7 35 7 27

Abbildung 9.13: Ein Beispiel für heapify().

if (vec[2*top+1].key > vec[2*top+2].key) {


child = 2*top+1;
} else {
child = 2*top+2;
}
}//endif

// check if exchange is necessary


if (vec[top].key < vec[child].key) {
temp = vec[top];
vec[top] = vec[child];
vec[child] = temp;
// recursive call for possible further exchanges
heapify(vec, child, bottom);
}//endif
}

heapify() lässt sich nun einfach zum Herstellen der Heapeigenschaft im gesamten Array vec nut-
zen, indem man das Array von hinten nach vorn durchläuft und in jeder Komponente i (die Söhne
hat) heapify(vec,i,n-1) aufruft. Vor dem Aufruf erfüllt vec[i+1]. . .[n-1] bereits die Heapei-
genschaft, und der Aufruf stellt die Heapeigenschaft für vec[i]. . .vec[n-1] her.
240 KAPITEL 9. SORTIEREN IN ARRAYS

Eine Java Implementation lautet:

Programm 9.5 createHeap


/**
* turns array into a heap
* @param vec the array to which this happens
*/
private static void createHeap(Item[] vec) {
for (int i = vec.length/2 - 1; i >= 0; i--) {
heapify(vec, i, vec.length - 1);
}
}

Im Standardbeispiel ergeben sich die in Abbildung 9.14 dargestellten Zustände nach jedem Durchlauf
der Schleife. Dabei deuten durchgezogene Linien bereits hergestellte Heapbedingungen an.

9.5.3 Die Implementation von Heapsort

Mit heapify() und createHeap() lässt sich Heapsort jetzt einfach implementieren:

Programm 9.6 heapSort


/**
* sorts array by heapsort in a certain range
* @param vec the array in which this happens
*/
public static void heapSort(Item[] vec)
throws NullPointerException {
if (vec == null) throw new NullPointerException();

Item temp;
int last;
int n = vec.length;

createHeap(vec);
for (last = n-1; last > 0; last--) {
// exchange top component with
// current last component of vec
temp = vec[0];
vec[0] = vec[last];
vec[last] = temp;
// call Heapify to to reestablish heap property
heapify(vec, 0, last-1);
}//endfor
9.5. HEAPSORT 241

i=3
0 63 0 63

1 24 2 12 1 24 2 12

3 53 4 72 5 18 6 44 3 53 4 72 5 18 6 44

7 35 7 35

i=2 i=1
0 63 0 63

1 24 2 44 1 72 2 44

3 53 4 72 5 18 6 12 3 53 4 24 5 18 6 12

7 35 7 35

i=0
0 72

1 63 2 44

3 53 4 24 5 18 6 12

7 35

Abbildung 9.14: Ein Beispiel für createHeap().


242 KAPITEL 9. SORTIEREN IN ARRAYS

Die Komponenten vec[0]. . .vec[last] bilden also den aktuellen Heap (mit jeweiligem größten
Element vec[0]), und die Komponenten vec[last+1]. . .vec[n-1] den bereits sortierten Teil des
Arrays.
Im Standardbeispiel ergeben sich jeweils beim Eintritt in die for-Schleife die in Abbildung 9.15
dargestellten Heaps. Die noch verbundenen Komponenten des Arrays stellen den jeweiligen Heap
vec[0]. . .vec[last] dar, die Aktionen von heapify() werden nicht mehr dargestellt.

9.5.4 Die Analyse von Heapsort

Zur Vorbereitung der Analyse betrachten wir die Interpretation des Arrays als Baum genauer:
Da die Komponenten des Arrays schichtweise im Baum angeordnet sind, hat er die Eigenschaft, dass
alle Schichten i bis auf eventuell die letzte voll sind, d. h. 2i Knoten enthalten. Ein solcher Baum heißt
voller (binärer) Baum.

Lemma 9.6 Sei T ein voller binärer Baum mit n Knoten. Sei h die Höhe von T . Dann gilt:

a) 2h ≤ n ≤ 2h+1 − 1

b) h ≤ blog nc

Beweis: In T sind alle Schichten voll bis eventuell auf die letzte. Also sind in Schicht i für i =
0, . . . , h − 1 genau 2i Knoten, und in Schicht h zwischen 1 und 2h Knoten (vergleiche Abbildung 9.16).
Also ist !
h−1 h
∑ 2i + 1 ≤ n ≤ ∑ 2i .
i=0 i=0

Wegen ∑ki=0 2i = 2k+1 − 1 ergibt sich

2h ≤ n ≤ 2h+1 − 1 ,

also a). Aus 2h ≤ n folgt h ≤ log n und somit h ≤ blog nc, da h eine ganze Zahl ist.

Betrachten wir jetzt den Aufwand für einen Aufruf von heapify() einschließlich der dadurch er-
zeugten weiteren rekursiven Aufrufe.
Bei jedem Aufruf, bei dem ein Austausch erfolgt, “sinkt” das betrachtete Element um eine Stufe nach
unten im Baum. Die Anzahl der Folgeaufrufe durch Rekursion ist also durch die Anzahl der Schichten
unterhalb der Ausgangsstufe des Elements beschränkt.
Diese Beobachtung ist der Kern der folgenden Abschätzung. Wir bezeichnen die Überprüfung und
ggf. Herstellung der Heapeigenschaft für einen Knoten mit seinen Söhnen als eine Prüfaktion.
9.5. HEAPSORT 243

0 72 last = 7 0 63

1 63 2 44 1 53 2 44

3 53 4 24 5 18 6 12 3 35 4 24 5 18 6 12

7 35 7 72

last = 6 0 53 last = 5 0 44

1 35 2 44 1 35 2 18

3 12 4 24 5 18 6 63 3 12 4 24 5 53 6 63

7 72 7 72

last = 4 0 35 last = 3 0 24

1 24 2 18 1 12 2 18

3 12 4 44 5 53 6 63 3 35 4 44 5 53 6 63

7 72 7 72

last = 2 0 18 last = 1 0 12

1 12 2 24 1 18 2 24

3 35 4 44 5 53 6 63 3 35 4 44 5 53 6 63

7 72 7 72

Abbildung 9.15: Ein Beispiel für HeapSort.


244 KAPITEL 9. SORTIEREN IN ARRAYS

Schicht 0 20 Knoten
Schicht 1 21 Knoten
Schicht 2 22 Knoten
Schicht h = 3

Abbildung 9.16: Ein voller Baum.

Lemma 9.7 Für die Anzahl P1 (n) der Prüfaktionen beim Herstellen der Heapeigenschaft in einem
n-elementigen Array gilt
P1 (n) ≤ 2n .

Beweis: Wir betrachten (wie in createHeap()) die Schichten von unten nach oben. Da die Anzahl
der Prüfaktionen nach der angestellten Vorüberlegung durch die Anzahl der Schichten unterhalb des
Knotens beschränkt ist, folgt:

Schicht h: keine Prüfaktionen


Schicht h − 1: höchstens 1 Prüfaktionen pro Knoten der Schicht
Schicht h − 2: höchstens 2 Prüfaktionen pro Knoten der Schicht
...
Schicht h − i: höchstens i Prüfaktionen pro Knoten der Schicht
...
Schicht 0: höchstens h Prüfaktionen.

Also ist

P1 (n) ≤ 2h−1 · 1 + 2h−2 · 2 + . . . + 2h−i · i + . . . + 20 · h


h h h
2h i
= ∑ 2h−i · i = ∑ i
i = 2 h
∑ i
i=1 i=1 2 i=1 2

i
≤ 2n, da 2h ≤ n und ∑ 2i = 2 .
i=1

Die Anzahl P2 (n) der Prüfaktionen in HeapSort (außerhalb von createHeap()) ergibt sich analog.
Im i-ten Durchlauf der for-Schleife wird heapify() für die Komponente mit Index 0 und den Heap
mit n − i Komponenten aufgerufen, dessen Höhe nach Lemma 9.6 höchstens log(n − i) ist. Also folgt

n−2 n−1
P2 (n) ≤ ∑ log(n − i) ≤ ∑ log n = (n − 1) log n .
i=1 i=1
9.6. LITERATURHINWEISE 245

Pro Prüfaktion erfolgen 2 Vergleiche und maximal 3 Zuweisungen (1 Austausch). Hinzu kommen
jeweils 3 Zuweisungen (1 Austausch) in der for-Schleife von HeapSort. Hieraus folgt:

Satz 9.8 Für die Anzahl C(n) der Vergleiche und die Anzahl A(n) der Zuweisungen bei Heapsort gilt:

C(n) = O(n log n), A(n) = O(n log n) .

Beweis: Nachrechnen ergibt:

C(n) ≤ 2 · P1 (n) + 2 · P2 (n)


≤ 4n + 2n log n
≤ 3n log n für n ≥ 16 ,

A(n) ≤ 3 · P1 (n) + 3 · (n − 1) + 3 · P2 (n)


≤ 9n + 3n log n
≤ 5n log n für n ≥ 23 .

9.6 Literaturhinweise
Sortieralgorithmen werden in allen Büchern über Algorithmen und Datenstrukturen behandelt, vom Klassiker
[Knu98b] bis zu [CLRS01]. Die Implementationen der Algorithmen lehnt sich zum Teil an [HR94] an.
Die allgemeine Version des Aufteilungs-Beschleunigungssatzes ist aus [CLRS01]. Dort finden sich auch wei-
tere Details zur Beschleunigung der Matrixmultiplikation. Die Anwendung auf die Multiplikation n-stelliger
Dualzahlen ist aus [AHU83].
246 KAPITEL 9. SORTIEREN IN ARRAYS
Kapitel 10

Untere Komplexitätsschranken für das


Sortieren

Die besten bisher kennen gelernten Sortierverfahren für Arrays haben einen Aufwand von O(n log n)
im Worst Case (Mergesort, Heapsort), bzw. im Average Case (Quicksort).
Es stellt sich nun die Frage, ob es noch bessere Sortierverfahren geben kann. Dies kann tatsächlich
der Fall sein, wenn man zusätzliche Informationen über die Schlüsselmenge hat, wie das Verfahren
Bucketsort in Coma II zeigt. Für eine große Klasse von Algorithmen, die alle in Kapitel 9 behandelten
Algorithmen umfasst, ist dies jedoch nicht der Fall.
Um dies zu zeigen, benötigen wir noch einen Begriff. Ein Algorithmus heißt deterministisch, wenn der
Ablauf des Algorithmus nur vom Input abhängt (und nicht etwa von im Algorithmus ausgeführten Zu-
fallsexperimenten wie bei sogenannten randomisierten Algorithmen). Dies bedeutet beim Sortieren,
dass bei gleicher Eingabe in verschiedenen Läufen des Algorithmus dieselbe Folge von Vergleichen
angestellt wird. Mit diesem Begriff gilt dann:

Satz 10.1 (Untere Schranken für das Sortieren mit Vergleichen) Jeder deterministische Sortieral-
gorithmus, der auf paarweisen Vergleichen von Schlüsseln basiert, braucht zum Sortieren eines n-
elementigen Arrays sowohl im Worst Case, als auch im Mittel (bei Gleichverteilung) Ω(n log n) Ver-
gleiche.

Dieser Satz zeigt also, dass Mergesort, Heapsort und Quicksort bzgl. der Größenordnung optimal sind,
und dass Laufzeitunterschiede höchstens der O-Konstanten zuzuschreiben sind.
Man beachte den Unterschied zu den bisher gemachten O(. . .) Abschätzungen für ein Problem. Diese
haben wir dadurch erhalten, dass ein konkreter Algorithmus, der das Problem löst, analysiert wurde.
Die im Satz formulierte Ω(. . .) Abschätzung bezieht sich jedoch auf alle möglichen Sortierverfahren
(bekannte und unbekannte). Sie macht also eine Aussage über eine Klasse von Algorithmen statt über
einen konkreten Algorithmus und ist damit von ganz anderer Natur.

Version vom 17. Juni 2010

247
248 KAPITEL 10. UNTERE KOMPLEXITÄTSSCHRANKEN FÜR DAS SORTIEREN

10.1 Das Entscheidungsbaum-Modell

Zum Beweis des Satzes werden wir die Tatsache nutzen, dass jeder deterministische Sortieralgorith-
mus, der nur auf paarweisen Vergleichen von Schlüsseln basiert, durch einen Entscheidungsbaum wie
folgt beschrieben werden kann.

• Innere Knoten des Entscheidungsbaums sind Vergleiche im Algorithmus.


• Ein Weg von der Wurzel bis zu einem Blatt entspricht im Algorithmus der Folge der angestellten
Vergleiche. Dabei wird vereinbart, dass beim Weitergehen nach links bzw. rechts der letzte
Vergleich richtig bzw. falsch ist.
• Blätter des Baumes stellen die Permutationen des Eingabe-Array dar, die zu der zu der angestell-
ten Folge von Vergleichen führen. Bei n zu sortierenden Elementen sind dies n! Permutationen.

Als Beispiel betrachten wir einen Ausschnitt des Entscheidungsbaums für das Sortieren von w1 , w2 ,
w3 , w4 mit Mergesort. Es erfolgt also zunächst die Aufteilung in die Teilfolgen w1 , w2 und w3 , w4 .
Diese werden dann sortiert und gemischt. Es entsteht der in Abbildung 10.1 dargestellte Baum.

w1 < w2 Vergleich für 1. Teilfolge

ja nein

w3 < w4 w3 < w4 Vergleich für 2. Teilfolge

ja nein ja nein

w1 < w3 w1 < w4 w2 < w3 w2 < w4 1. Vergleich beim Mischen

nein

w1 < w3 2. Vergleich beim Mischen

ja nein
..
w2 < w3 w4 w3 w1 w2 .

ja nein

w4 w1 w2 w3 w4 w1 w3 w2 Permutationen

Abbildung 10.1: Der Entscheidungsbaum für Mergesort.

Satz 10.2 Jeder deterministische Sortieralgorithmus, der auf paarweisen Vergleichen basiert, erzeugt
einen solchen Entscheidungsbaum T .
10.2. ANALYSE DES ENTSCHEIDUNGSBAUMS 249

Beweis: Da der Algorithmus deterministisch ist, hat er für jede Eingabefolge denselben ersten Ver-
gleich zwischen Arraykomponenten. Dieser bildet die Wurzel des Entscheidungsbaums. In Abhängig-
keit vom Ausgang des Vergleichs (“<” oder “>”) ist der nächste Vergleich wiederum eindeutig be-
stimmt. Die Fortsetzung dieser Argumentation liefert für jede Eingabefolge eine endliche Folge von
Vergleichen, die einem Weg von der Wurzel bis zu einem Blatt (Permutation, die diese Folge von
Vergleichen verursacht) entspricht.

Wir überlegen nun, wie wir den Worst Case bzw. Average Case Aufwand C(n) bzw. C(n) des Algo-
rithmus im Baum T ablesen können. Sei dazu h(v) die Höhe des Knoten v im Baum T , h(T ) die Höhe
von T , und H(T ) := ∑v Blatt h(v) die sogenannte Blätterhöhensumme von T .

Lemma 10.1 Sei T der Entscheidungsbaum für den Algorithmus A und C(n) bzw. C(n) die Worst
Case bzw. Average Case (bei Gleichverteilung) Anzahl von Vergleichen bei n zu sortierenden Kompo-
nenten. Dann gilt:

a) C(n) = max h(v) = h(T ),


v Blatt

1 1
b) C(n) = n! ∑ h(v) = H(T ).
v Blatt von T n!

Beweis: Die Anzahl der Vergleiche, um zu einer sortierten Ausgabe v zu kommen, ist gerade h(v).
Also folgt a) direkt. Da wir Gleichverteilung der n! verschiedenen Eingabereihenfolgen (und damit
Ausgabereihenfolgen) annehmen, folgt b).

10.2 Analyse des Entscheidungsbaums

Die Abschätzung von C(n) bzw. C(n) nach unten reduziert sich also auf die Abschätzung der Höhe
bzw. der Blätterhöhensumme eines binären Baumes mit n! Blättern nach unten. Dazu zeigen wir
folgendes Lemma.

Lemma 10.2 Sei T ein binärer Baum mit b Blättern. Dann gilt:

a) h(T ) ≥ log b

b) H(T ) ≥ b · log b

Beweis: Der Beweis wird in beiden Fällen durch vollständige Induktion nach der Höhe h(T ) von T
geführt.
250 KAPITEL 10. UNTERE KOMPLEXITÄTSSCHRANKEN FÜR DAS SORTIEREN

Ist h(T ) = 0, so besteht T nur aus der Wurzel, die zugleich ein Blatt ist. Also ist b = 1 und log b =
0 = h(T ). Entsprechend ist H(T ) = ∑v Blatt h(v) = 0 und b · log b = 0.
Es gelten nun a), b) für h(T ) = 0, 1, . . . , h − 1 (Induktionsvoraussetzung).
Zum Schluss auf h betrachte man die beiden Teilbäume T1 und T2 von T , wobei einer leer sein kann,
vgl. Abbildung 10.2
T

 

 

 




 h2

h1 T2 




 T1



 | {z }
b2
| {z }
b1

Abbildung 10.2: Teilbäume im Beweis von Lemma 10.2

Jeder der Teilbäume hat eine geringere Höhe als h, also trifft auf T1 und T2 die Induktionsvorausset-
zung zu. Sei bi die Anzahl der Blätter und hi die Höhe von Ti (i = 1, 2), und sei o. B. d. A. b1 ≥ b2 .
Dann gilt:

h(T ) = 1 + max{h1 , h2 } ≥ 1 + h1
≥ 1 + log b1 (Induktionsvoraussetzung)
= log 2 + log b1 = log(2 · b1 )
≥ log(b1 + b2 ) (da b1 ≥ b2 )
= log b.

Dies beweist a). im Fall b) gilt


H(T ) = b + H(T1 ) + H(T2 ) ,
da in T jedes Blatt gegenüber T1 und T2 eine um 1 größere Höhe hat. Auf T1 und T2 ist die Indukti-
onsvoraussetzung anwendbar und es folgt

H(T ) ≥ b + b1 log b1 + b2 log b2 (Induktionsvoraussetzung)


= b + b1 log b1 + (b − b1 ) log(b − b1 ) .

Da wir nicht genau wissen, wie groß b1 ist, fassen wir die rechte Seite als Funktion von x = b1 auf
und suchen ihr Minimum.
Also ist

H(T ) ≥ b + min (x log x + (b − x) log(b − x)) .


x∈[1,b]
10.2. ANALYSE DES ENTSCHEIDUNGSBAUMS 251
11.2. ANALYSE DES ENTSCHEIDUNGSBAUMS 283

Die Funktion f (x) := x log x + (b − x) log(b − x) hat auf dem Interval [1, b] in etwa den in Abbil-
Die Funktion f (x) := x log x + (b − x) log(b − x) hat auf dem Interval [1, b] in etwa den in
dung 10.3 dargestellten Verlauf.
Abbildung 11.3 dargestellten Verlauf.

1 2 3 b/2 b

Abbildung 11.3: Verlauf der Funktion f (x) := x log x + (b − x) log(b − x).


Abbildung 10.3: Verlauf der Funktion f (x) := x log x + (b − x) log(b − x).
Eine Kurvendiskussion zeigt, daß f (x) das Minimum auf [1, b] bei x = b/2 annimmt (Übung).
EineDamit
Kurvendiskussion
ist zeigt, dass f (x) das Minimum auf [1, b] bei x = b/2 annimmt (Übung). Damit
ist
b b b b
H(T ) ≥ b + b log b + (b − b) log(b − b)
2 2 2
H(T ) ≥ b + log + (b − ) log(b − ) 2
2 b2 2 2
= b + b log b = b + b(log b − log 2)
2
= b + b log = b + b(log b − log 2)
= b + b(log b2 − 1) = b log b .
= b + b(log b − 1) = b log b .

Für die endgültige Abschätzung benötigen wir noch eine Abschätzung von n! nach unten. Es
Für die 1
ist1 endgültige untere Schranke benötigen wir noch eine Abschätzung von n! nach unten. Es ist
n! = n(n − 1) · · · 2 · 1 ≥ n(n − 1) · · · #n/2$ ≥ #n/2$!n/2"+1 n/2
bn/2c+1 ≥ (n/2) n/2.
n! = n(n − 1) · · · 2 · 1 ≥ !n(n − 1)"#· · · dn/2e$ ≥ dn/2e ≥ (n/2) .
| !n/2"+1{zFaktoren }
bn/2c+1 Faktoren
Nach diesen Vorbereitungen kommen wir jetzt zum
Nach diesen Vorbereitungen kommen wir jetzt zum
Beweis von Satz
Beweis von 10.1:
SatzSei T der
11.1: SeiEntscheidungsbaum zum gegebenen
T der Entscheidungsbaum Sortieralgorithmus
zum gegebenen A. Bei einem
Sortieralgorithmus
Inputarray der Länge n hat T n! Blätter. Es folgt
A. Bei einem Inputarray der Länge n hat T n! Blätter. Es folgt

C(n) =
C(n) = h(T h(T)) ≥ ≥log
logn!n!
% & nn nn nn n
≥ log
≥ log (n/2) (n/2) n/2
n/2
== log log == log lognn−−n
2 2 2 2 2 2 22
nn n n
= log
lognn ++ log log n −
33 6 2
1
Eine genauere Abschätzung ergibtnsich aus der nStirlingschen n Formel n! = √2πn( ne )n (1 + Θ( n1 )), speziell
√ √ ≥ log n für log n ≥ , also n ≥ 8
2πn( ne )n ≤ n! ≤ 2πn( ne )n+(1/12n) . 3 6 2
= Ω(n log n).
√ √
1 Eine
genauere Abschätzung ergibt sich aus der Stirlingschen Formel n! = 2πn( ne )n (1 + Θ( 1n )), speziell 2πn( ne )n ≤

n! ≤ 2πn( ne )n+(1/12n) .
252 KAPITEL 10. UNTERE KOMPLEXITÄTSSCHRANKEN FÜR DAS SORTIEREN

Entsprechend ist
1 1
C(n) = H(T ) ≥ (n! log n!) = log n!
n! n!
= Ω(n log n) (wie oben).

Dieses Ergebnis, bzw. genauer die Ungleichungen

C(n) ≥ log n! , C(n) ≥ log n!

werden auch als informations-theoretische Schranke für das Sortieren bezeichnet. Sie lassen sich an-
schaulich folgendermaßen interpretieren. Jeder Sortieralgorithmus muss zwischen n! Möglichkeiten
(den sortierten Reihenfolgen) unterscheiden und muss daher log n! Bits an Information sammeln. Ein
Vergleich ergibt höchstens ein Bit Information. Oder, anders formuliert: Jeder Vergleich teilt die Men-
ge der in Frage kommenden Permutationen in zwei Teilmengen, die Menge Π+ der Permutationen,
die weiterhin als gesuchte Permutation in Frage kommen, und die Menge Π− der Permutationen, die
nicht zum gemachten Vergleich passen. Die Menge Π+ wird dann weiter durch Vergleiche unterteilt.
Man ist am Ziel, wenn nur noch eine Permutation (die gesuchte Sortierung) übrig bleibt. Dazu braucht
man (sukkzessive Halbierung, vgl. binäre Suche in Abschnitt 5.1.2) mindestens log n! Vergleiche.

10.3 Literaturhinweise
Untere Schranken für Sortieralgorithmen werden in [CLRS01, Meh88, OW02] behandelt.
[Meh88] geht auch ausführlich auf Erweiterungen solcher Schrankenresultate ein und zeigt unter anderem, dass
durch Randomisierung, also die zufallsgesteuerte Wahl der Vergleiche, keine Beschleunigung möglich ist. Die
erwartete Anzahl der Vergleiche beträgt immer noch Ω(n log n).
Literaturverzeichnis

[AHU83] Alfred V. Aho, John E. Hopcroft, and Jeffrey D. Ullman. Data Structures and Algorith-
mus. Addison-Wesley, Reading, NY, 1983. 9.6

[Aig04] Martin Aigner. Diskrete Mathematik. Vieweg, Wiesbaden/Braunschweig, 2004. 5. Aufla-


ge. 1.4

[ALSU06] Alfred V. Aho, Monica S. Lam, Ravi Sethi, and Jeffrey D. Ullman. Compilers: Principles,
Techniques, and Tools. Addison-Wesley, Reading, NY, 2nd edition, 2006. 3.5

[AU94] Alfred V. Aho and Jeffrey D. Ullman. Foundations of Computer Science. W. H. Freeman
and Company, New York, c edition edition, 1994. 1.4

[Bro06] J. Glenn Brookshear. Computer Science: An Overview. Prentice Hall, Englewood Cliffs,
NJ, 9th edition, 2006. 1.4

[CLRS01] Thomas H. Cormen, Charles E. Leiserson, Ronald R. Rivest, and Clifford Stein. Introduc-
tion to Algorithms. The MIT Press, Cambridge, MA, second edition, 2001. 1.4, 5.4, 8.4,
9.3.1, 9.3.2, 9.5.1, 9.6, 10.3

[CW87] D. Coppersmith and S. Winograd. Matrix multiplication via arithmetic progression. In


Proc. 19th Ann. Symp. on Comp., pages 1–6, 1987. 9.3.2

[CW98] Mary Campione and Cathy Walrath. The Java Tutorial: Object-Oriented Programming
for the Internet. Addison-Wesley, Reading, NY, second edition, 1998. 3.5

[DD04] Harvey M. Deitel and Paul J. Deitel. Java: How to Program. Prentice Hall, Englewood
Cliffs, NJ, 6th edition, 2004. 1.4, 2.4, 3.5

[Fla05] David Flanagan. Java in a Nutshell. O’Reilly & Associates, Sebastopol, CA, 5th edition,
2005. 1.4

[GL02] Les Goldschlager and Andrew Lister. Informatik – Eine moderne Einführung. Carl Hanser
Verlag, München, 3. auflage edition, 2002. 1.4

[GMW91] Philip E. Gill, Walter Murray, and Margaret H. Wright. Numerical Linear Algebra and
Optimization, volume 1. Addison-Wesley, Reading, NY, 1991. 5.4

253
254 LITERATURVERZEICHNIS

[GR03] Ralph P. Grimaldi and David J. Rothman. Discrete and Combinatorial Mathematics: An
Applied Introduction. Addison-Wesley, Reading, NY, 5th edition, 2003. 1.4

[GS06] Heinz-Peter Gumm and Manfred Sommer. Einführung in die Informatik. Oldenbourg
Verlag, München, 2006. 7. Auflage. 1.4

[GT05] Michael T. Goodrich and Roberto Tamassia. Data Structures and Algorithms in Java.
John Wiley & Sons, New York, 4th edition, 2005. 1.4, 4.10

[HMU06] John E. Hopcroft, Rajeev Motwani, and Jeffrey D. Ullman. Introduction to Automata
Theory, Languages, and Computation. Addison-Wesley, Reading, NY, 3rd edition, 2006.
3.5

[Hor95] Ellis Horowitz. Fundamentals of Programming Languages. W. H. Freeman and Company,


New York, 2nd edition, 1995. Second Edition. 3.5

[HR94] Mark R. Headington and David D. Riley. Data Abstraction and Structures Using C++.
D. C. Heath and Company, Lexington, MA, 1994. 4.10, 6.5, 9.6

[Job05] Fritz Jobst. Programmieren in Java. Carl Hanser Verlag, München, 2005. 5. Auflage. 1.4

[Jun94] Dieter Jungnickel. Graphen, Netzwerke und Algorithmen. BI Wissenschaftsverlag, Mann-


heim, 1994. 3. Auflage. 5.4

[Knu97] Donald. E. Knuth. The Art of Computer Programming, volume 1 Fundamental Algo-
rithms. Addison-Wesley, Reading, NY, third edition, 1997. 1.4

[Knu98a] Donald. E. Knuth. The Art of Computer Programming, volume 2 Seminumerical Algo-
rithms. Addison-Wesley, Reading, NY, third edition, 1998. 1.4, 6.5

[Knu98b] Donald. E. Knuth. The Art of Computer Programming, volume 3 Sorting and Searching.
Addison-Wesley, Reading, NY, second edition, 1998. 1.4, 5.4, 9.6

[Krü06] Guido Krüger. Handbuch der Java Programmierung. Addison-Wesley, Reading, NY, 4.
auflage edition, 2006. 1.4, 2.4

[Küh99] Ralf Kühnel. Die Java 1.2 Fibel. Addison-Wesley Longman, Amsterdam, 1999. 3.
Auflage. 3.5, 6.5

[LV92] G. T. Leavens and M. Vermeulen. The 3 + x problem. Computers Math. Appl., 24:79–99,
1992. 7.1.4, 7.3

[Meh88] Kurt Mehlhorn. Datenstrukturen und effiziente Algorithmen, Band I: Sortieren und Su-
chen. Teubner Verlag, Stuttgart, 1988. 1.4, 5.4, 10.3

[Mei98] Christoph Meinel. Effiziente Algorithmen. Carl Hanser Verlag, München, 1998. 1.4

[OSW02] Thomas Ottmann, Michael Schrapp, and Peter Widmayer. PASCAL in 100 Beispielen.
Teubner Verlag, Stuttgart, 2002. 2.4
LITERATURVERZEICHNIS 255

[OW02] Thomas Ottmann and Peter Widmayer. Algorithmen und Datenstrukturen. Spektrum
Akademischer Verlag, 2002. 4. Auflage. 1.4, 5.4, 10.3

[OW04] T. Ottmann and P. Widmayer. Programmierung mit PASCAL. Teubner Verlag, Stuttgart,
7. auflage edition, 2004. 7.3

[PM88] S. Park and K. Miller. Random number generators: Good ones are hard to find. Comm.
ACM, 31:1192–1201, 1988. 6.5

[Pre00] Bruno R. Preiss. Data Structures and Algorithms with Object-Oriented Design Patterns
in Java. John Wiley & Sons, New York, 2000. 1.4

[Sed03] Robert Sedgewick. Algorithms in Java, Parts 1–4 (Fundamental Algorithms, Data Struc-
tures, Sorting, Searching). Addison-Wesley, Reading, NY, 3rd edition, 2003. 1.4

[SS06] Gunter Saake and Kai-Uwe Sattler. Algorithmen und Datenstrukturen. Eine Einführung
mit Java. dpunkt.verlag, Heidelberg, 3. auflage edition, 2006. 1.4

[Tar83] Robert Endre Tarjan. Data Structures and Network Algorithms. SIAM, Philadelphia, PA,
1983. 5.4

[vL90a] Jan van Leeuwen, editor. Handbook of Theoretical Computer Science, A: Algorithms and
Complexity Theory. North-Holland, Amsterdam, 1990. 1.4

[vL90b] Jan van Leeuwen, editor. Handbook of Theoretical Computer Science, B: Formal Models
and Semantics. North-Holland, Amsterdam, 1990. 1.4

[Wii87] Stephen A. Wiitala. Discrete Mathematics: A Unified Approach. Mc Graw-Hill, New


York, 1987. 1.4, 3.5

[Wir00] Niklaus Wirth. Algorithmen und Datenstrukturen. Teubner Verlag, Stuttgart, 5. auflage
edition, 2000. 7.3
Index

abstract, 161 awt, 14


Ackermann Funktion, 190
Activation Record, 148 Bellman Gleichungen, 124
Adjazenzmatrix, 118 als Matrixmultiplikation, 131
aktueller Parameter, 138 Berechenbarkeit, 8
Algorithmus, 1, 8 Betriebssystem, 7
deterministischer, 247 bewerteter Graph, 118
randomisierter, 247 Bewertungsmatrix, 118
Analyse Bezeichner, 23
a posteriori, 198, 203 Binärer Baum, 181
a priori, 198 binare Suche, 106
von Algorithmen, 198 Blockscope, 143
Analyse von Algorithmen, 197 Bogen, 117
anonyme Klasse, 164 Bubblesort, 206
Applet, 14
Call by reference, 139
GGT, 50
Call by value, 139
Hypothek, 41
clone(), 75, 164
ListDemo, 88 Cloneable, 163
Primzahl, 25 Comparable, 163
RollDice, 176 Computer, 2
StackDemo, 99 Computerorientierte Mathematik, iii
Steuer, 20 Copy Konstruktor, 156
StringDemo, 67
StringExceptionHandlingDemo, 70 Datenfluss, 138
Temperatur, 12 Datenstruktur, 64
Array, 73 Datentyp, 17, 59
Assembler, 4 Fraction, 60
asymptotische Notation, 199 Default Konstruktor, 156
Aufrufbaum, 148 Definition, 37, 38
Aufteilungs-Beschleunigungs-Satz Deklaration, 37
allgemeiner, 221 Devisentausch, 119
Aufteilungs-Beschleunigungss-Satz Digraph, 117
spezieller, 220 direkter Zugriff, 65
Ausdruck, 17, 35 do while Anweisung, 27, 54

256
INDEX 257

einfache Anweisung, 36 Java-Compiler, 6


elementarer Weg, 119 javadoc, 79
Entscheidungsbaum, 248
Entwurf von Algorithmen, 8 Kante, 117
Erkennung korrekter Klammerausdrucke, 95 Klasse, 14, 17, 154
Ermittlung kurzester Wege, 133 Fraction, 168
Ermittlung negativer Zykel, 132 ListNode, 80
Exception-Handling, 69 Matrix, 112
RandomNumber, 173
Feldzugriff, 74 SimpleList, 83
Fibonacci Zahlen, 193 Stack, 91
final, 161 Klassenscope, 143
Flowlayout, 23 Klassentyp, 32
formaler Parameter, 138 Knoten, 117
Funktion, 18, 137 Kommentar, 14
Funktionsaufruf, 146 Kompilieren, 6
Komplexität
Garbage Collection, 168 mittlere, 198
gerichteter Baum, 134 worst case, 198
getClass(), 159 Komplexitat von Algorithmen, 8
ggT, 49 Konstante, 23
goto Anweisung, 55 Konstruktor, 32
Graph, 117 korrekter Klammerausdruck, 94
Größenordnung, 200 Korrektheit von Algorithmen, 8
kurzester Weg, 119
Hardware, 2 Kurzester-Wege-Baum, 134
Heap, 235
Heapeigenschaft, 237 L’Hopital, 201
Heapsort, 235 lineare Datenstruktur, 64
Analyse, 242 Liste, 79
hohere Programmiersprache, 4 lokale Klasse, 164
html-Datei, 18 lvalue, 35
Hypothekberechnung, 39
Maschinensprache, 4
if Anweisung, 23, 44 Matrix, 109
Implementation des ActionListener, 164 Matrixmultiplikation, 110
innere Klasse, 164 mehrdimensionales Array, 76
Insertion Sort, 210 Mergesort, 212
Interface, 162 Analyse, 216
Interpretieren, 6 Methode, 18
bellman(), 127
Java, 6 bellmanShort(), 129
Java Bytecode, 6 binare Suche, 107
Java Dokumentation, 79 bubbleSort(), 206
Java Virtual Machine, 6 createHeap(), 240
258 INDEX

heapify(), 238 Seiteneffekt, 36


merge(), 214 Selection Sort, 208
mergeSort(), 215 selektive Anweisung, 46
quickSort(), 227 sequentielle Suche, 105
selectionSort(), 208 sequentieller Zugriff, 65
Methode:heapSort(), 240 Software-Hardware-Hierarchie, 7
Modifizierer, 160 Sortieren
Multiplikation von Dualzahlen, 223 Algorithmen, 205
Bubblesort, 206
native, 161 direkte Methoden, 206
negativer Zykel, 126 Heapsort, 235
Insertion Sort, 210
Oberklasse, 157 Mergesort, 212
Quicksort, 225
Package, 160
Selection Sort, 208
Prinzip der optimalen Substruktur, 124
untere Schranken, 247
Priority Queue, 235
Stack, 91
private, 160
StackInterface, 162
Programmiersprache, 4
Standardtyp, 31
protected, 160
static Feld, 157
Prozess, 2
static Methode, 157
Pseudocode, 39
statische Scoperegeln, 143
public, 160
String, 65
Queue, 102 StringBuffer, 66
Quicksort, 225 StringBuilder, 66
mittlerer Aufwand, 230 strukturierter Datentyp, 64
Rekursionsaufwand, 229 super, 158
Worst Case Aufwand, 230 synchronized, 161

this, 112, 156


Rücksprungadresse, 148
transient, 161
Rechnermodell, 3, 197
Türme von Hanoi, 185
Record, 77
Typuberprufung, 63
Referenztyp, 32
Rekursion, 181 Überdeckung, 144
Rekursionstiefe, 153 Übersetzung, 4
Run-Time-Stack, 148 Ulams Funktion, 191
Runnable, 163 Unicode, 23
rvalue, 35 Unterklasse, 157

Schranken Variable, 16
obere, 199 Varuabler Parameter, 139
untere, 202 Vektor, 109
Scope, 143 Vererbung, 158
Scopeblock, 148 volatile, 161
INDEX 259

Warteschlange, 102
Weg, 119
Weglange, 119
Wertparameter, 139
while Anweisung, 27, 48
Wrapper-Klasse, 33

Zufallszahlen, 173
zusammengesetzte Anweisung, 39
Zuweisung, 16
Zykel, 119

Das könnte Ihnen auch gefallen