Spezielle Algorithmen - oth

Werbung
Algorithmen und Datenstrukturen
Prof. Jürgen Sauer
Spezielle Algorithmen
Skriptum zur Vorlesung im SS 2009
1
Algorithmen und Datenstrukturen
2
Algorithmen und Datenstrukturen
Inhaltsverzeichnis
Literaturverzeichnis.............................................................................................................................................. 5
1. GRUNDLEGENDE KONZEPTE ......................................................................................................... 7
1.1 Die zentralen Begriffe ..................................................................................................................................... 7
1.1.1 Datenstruktur und Algorithmus ................................................................................................................. 7
1.1.2 Ein einführendes Beispiel: Das Durchlaufen eines Binärbaums................................................................ 8
1.1.2.1 Rekursive Problemlösung ..................................................................................................................... 10
1.1.2.2 Nichtrekursive Problemlösung.............................................................................................................. 11
1.2 Algorithmische Grundkonzepte................................................................................................................... 15
1.2.1 Algorithmenbegriffe ................................................................................................................................ 15
1.2.2 Terminierung und Determinismus ........................................................................................................... 15
1.2.3 Algorithmenbausteine .............................................................................................................................. 16
1.2.4 Paradigmen der Algorithmenbeschreibung.............................................................................................. 18
1.2.4.1 Applikative Algorithmen .................................................................................................................. 19
1.2.4.2 Imperative Algorithmen .................................................................................................................... 21
1.2.4.3 Objektorientierte Algorithmen .......................................................................................................... 21
1.2.4.4 Paradigmen und Programmiersprachen ............................................................................................ 24
1.2.5 Beschreibung von Algorithmen ............................................................................................................... 24
1.2.6 Formale Eigenschaften von Algorithmen ................................................................................................ 28
1.2.6.1 Korrektheit, Terminierung, Hoare-Kalkül, Halteproblem................................................................. 28
1.2.6.1.1 Korrektheit, Terminierung ......................................................................................................... 28
1.2.6.1.2 Hoare-Kalkül.............................................................................................................................. 29
1.2.6.1.3 Halteproblem.............................................................................................................................. 29
1.2.6.2 Effizienz............................................................................................................................................ 31
1.2.7 Komplexität.............................................................................................................................................. 33
1.2.7.1 Laufzeitberechnungen....................................................................................................................... 35
1.2.7.1.1 Analyse der Laufzeit .................................................................................................................. 35
1.2.7.1.2 Asymptotische Analyse der Laufzeit („Big-O“) ........................................................................ 36
1.2.7.2 O(logN)-Algorithmen ....................................................................................................................... 40
1.2.7.3 Berechnungsgrundlagen für rechnerische Komplexität .................................................................... 41
1.2.7.3.1 System-Effizienz und rechnerische Effizienz ............................................................................ 41
1.2.7.3.2 P- bzw. NP-Probleme................................................................................................................. 41
1.2.7.3.3 Grenzen der Berechenbarkeit..................................................................................................... 42
1.3 Daten und Datenstrukturen ......................................................................................................................... 44
1.3.1 Datentyp................................................................................................................................................... 44
1.3.2 Datenstruktur............................................................................................................................................ 45
1.3.3 Relationen und Ordnungen ...................................................................................................................... 50
1.3.4 Klassifikation von Datenstrukturen ......................................................................................................... 54
1.3.4.1 Lineare Ordnungsgruppen ................................................................................................................ 54
1.3.4.2 Nichtlineare Kollektion..................................................................................................................... 57
1.3.4.2.1 Hierarchische angeordnete Sammlung (Bäume)........................................................................ 57
1.3.4.2.2 Gruppenkollektionen.................................................................................................................. 62
1.3.4.3 Dateien und Datenbanken ................................................................................................................. 63
1.3.5 Definitionsmethoden für Datenstrukturen................................................................................................ 66
1.3.5.1 Der abstrakte Datentyp...................................................................................................................... 66
1.3.5.2 Die axiomatische Methode................................................................................................................ 67
1.3.5.3 Die konstruktive Methode................................................................................................................. 69
1.3.5.4 Die objektorientierte Modellierung abstrakter Datentypen............................................................... 70
2. GRAPHEN UND GRAPHENALGORITHMEN.................................................................................. 78
2.1 Einführung .................................................................................................................................................... 78
3
Algorithmen und Datenstrukturen
2.1.1 Grundlagen............................................................................................................................................... 78
2.1.2 Definitionen ............................................................................................................................................. 82
2.1.3 Darstellung in Rechnerprogrammen ........................................................................................................ 88
2.2 Durchlaufen von Graphen ........................................................................................................................... 93
2.2.1 Tiefensuche (depth-first search)............................................................................................................... 93
2.2.1.1 Algorithmus ...................................................................................................................................... 93
2.2.1.2 Eigenschaften von DFS..................................................................................................................... 97
2.2.1.3 Kantenklassenfikation mit DFS ........................................................................................................ 98
2.2.1.4 Zusammenhangskomponenten........................................................................................................ 101
2.2.1.5 Topologisches Sortieren mittels Tiefensuche ................................................................................. 108
2.2.2 Breitensuche (breadth-first search) ........................................................................................................ 113
2.2.3 Implementierung .................................................................................................................................... 116
2.3 Topologischer Sort ...................................................................................................................................... 120
2.4 Transitive Hülle........................................................................................................................................... 123
2.4.1 Berechnung der Erreichbarkeit mittels Matrixmultiplikation ................................................................ 123
2.4.2 Warshalls Algorithmus zur Bestimmung der Wegematrix .................................................................... 125
2.4.3 Floyds Algorithmus zur Bestimmung der Abstandsmatrix.................................................................... 126
2.5 Kürzeste Wege............................................................................................................................................. 127
2.5.1 Die Datenstrukturen Graph, Vertex, Edge für die Berechnung kürzester Wege ................................... 127
2.5.2 Kürzeste Pfade in gerichteten, ungewichteten Graphen. ....................................................................... 128
2.5.3 Berechnung der kürzesten Pfadlängen in gewichteten Graphen (Algorithmus von Dijkstra) ............... 132
2.5.4 Berechnung der kürzesten Pfadlängen in gewichteten Graphen mit negativen Kosten......................... 137
2.5.5 Berechnung der kürzesten Pfadlängen in gewichteten, azyklischen Graphen ....................................... 138
2.5.6 All pairs shorted Path............................................................................................................................. 140
2.6 Minimale Spannbäume............................................................................................................................... 142
2.6.1 Der Algorithmus von Prim..................................................................................................................... 142
2.6.2 Der Algorithmus von Kruskal................................................................................................................ 145
2.7 Netzwerkflüsse............................................................................................................................................. 148
2.7.1 Maximale Flüsse .................................................................................................................................... 148
27.1.1 Netzwerk und maximaler Fluß......................................................................................................... 148
2.7.1.2 Optimieren und Finden augmentierender Pfade (Erweiterter Weg) ............................................... 150
2.7.1.2 Algorithmus für optimalen Fluss .................................................................................................... 152
2.7.1.4 Schnitte und das Max-Flow-Min-Cut Problem............................................................................... 157
2.7.2 Konsteminimale Flüsse .......................................................................................................................... 159
2.8 Matching ...................................................................................................................................................... 161
2.8.1 Ausgangspunkt, Motivierendes Beispiel, Definitionen, maximales Matching ...................................... 161
2.8.2 Bipartiter Graph ..................................................................................................................................... 165
2.8.3 Maximale Zuordnung im allgemeinen Fall............................................................................................ 170
4
Algorithmen und Datenstrukturen
Literaturverzeichnis
Sauer, Jürgen: Programmieren in Java, Skriptum zur Vorlesung im WS 2005/2007
http://fbim.fh-regensburg.de/~saj39122/pgj/index.html
Sauer, Jürgen: Programmieren in C++, Skriptum zur Vorlesung im SS 2006
http://fbim.fh-regensburg.de/~saj39122/pgc/index.html
Sauer, Jürgen: Datenbanken, Skriptum zur Vorlesung im SS 2007
http://fbim.fh-regensburg.de/~saj39122/dbnew/index.html
Sauer, Jürgen: Operations Research, Skriptum zur Vorlesung im SS 2005
Sedgewick, Robert: Algorithmen in Java, 3.überarbeitete Auflage, Pearson Studium,
München …. , 2003
Sedgewick, Robert: Algorithmen in C++, Teil 1 bis 4, 3.überarbeitete Auflage,
Pearson Studium, München …. , 2002
Wirth, Nicklaus: Algorithmen und Datenstrukturen, 2. duchgesehene Auflage,
Teubner, Stuttgart 1979
Ottmann, Thomas und Widmayer, Peter: Algorithmen und Datenstrukturen, BI
Wissenschaftsverlag, Mannheim /Wien /Zürich 1990
Weiss, Marc Allen: Data Structures and Algorithm Analysis in Java, Pearson, Boston
…., 2007
Saake, Gunter und Sattler, Kai Uwe: Algorithmen und Datenstrukturen,
dpunkt.verlag, 2. überarbeitete Auflage, Heidelberg, 2004
Maurer, H.: Datenstrukturen und Programmierverfahren, Teubner,Stuttgart 1974
Krüger, Guido und Stark, Thomas: Handbuch der Java-Programmierung, 5. Auflage,
HTML-Ausgabe 5.0.1, Addison-Wesley, 2007
Ullenboom, Christian: Java ist auch eine Insel, 7. aktualisierte Auflage, HTMLVersion
Ammeraal, Leendert: Programmdesign und Algorithmrn in C, Hanser Verlag
München Wien, 1989
5
Algorithmen und Datenstrukturen
6
Algorithmen und Datenstrukturen
1. Grundlegende Konzepte
1.1 Die zentralen Begriffe
1.1.1 Datenstruktur und Algorithmus
In den 50er Jahren bedeutete „Rechnen“ auf einem Computer weitgehend
„numerisches Lösen“ wissenschaftlich-technischer Probleme. Kontroll- und
Datenstrukturen waren sehr einfach und brauchten daher nicht weiter untersucht
werden. Ein bedeutender Anstoß kam hier aus der kommerziellen Datenverarbeitung (DV). So führte hier bspw. die Frage des Zugriffs auf ein Element einer
endlichen Menge zu einer großen Sammlung von Algorithmen 1, die grundlegende
Aufgaben der DV lösen. Dabei ergab sich: Die Leistungsfähigkeit dieser Lösungen
(Programme) ist wesentlich bestimmt durch geeignete Organisationsformen für die
zu bearbeitenden Daten.
Der Datentyp oder die Datenstruktur und die zugehörigen Algorithmen sind
demnach ein entscheidender
Bestandteil eines leistungsfähigen Programms.
Datenstrukturen und Programmierverfahren bilden eine Einheit. Bei der Formulierung
des Lösungswegs ist man auf eine bestimmte Darstellung der Daten festgelegt. Rein
gefühlsmäßig könnte man sagen: Daten gehen den Algorithmen voraus.
Programmieren führt direkt zum Denken in Datenstrukturen, um Datenelemente, die
zueinander in Beziehung stehen, zusammen zu fassen. Mit Hilfe solcher
Datenstrukturen ist es möglich, sich auf die relevanten Eigenschaften der Umwelt zu
konzentrieren und eigene Modelle zu bilden. Die Leistung des Rechners wird dabei
vom reinen Zahlenrechnen auf das weitaus höhere Niveau der „Verarbeitung von
Daten“ angehoben
Datenstrukturen und Algorithmen bilden die wesentlichen Bestandteile der
Programmierung. Ein erster Versuch soll diese zentralen Begriffe so festlegen (bzw.
abgrenzen):
Datenstruktur
Ein auf Daten anwendbares Ordnungsschema (z.B. ein Datensatz oder Array). Mit
der Hilfe von Datenstrukturen lassen sich die Daten interpretieren und spezifische
Operationen auf ihnen ausführen
Algorithmus
Verarbeitungsvorschrift, die angibt, wie Eingabe(daten) schrittweise mit Hilfe von
Anweisungen auf Rechnern in Ausgabe(daten) umgewandelt werden. Für die Lösung
eines Problems existieren meist mehrere Algorithmen, die sich in der Länge sowie
der für die Ausführung benötigte Zeit unterscheiden.
Programm und Programmiersprache
Ein Programm ist die Formulierung eines Algorithmus und seiner Datenbereiche in
einer Programmiersprache.
1
D. E. Knuth hat einen großen Teil dieses Wissens in "The Art of Computer Programming" zusammengefaßt
7
Algorithmen und Datenstrukturen
Eine Programmiersprache erlaubt, Algorithmen präzise zu beschreiben.
Insbesondere legen sie fest:
- die elementaren Operationen
- die Möglichkeiten zu ihrer Kombination
- die zulässigen Datenbereiche
1.1.2 Ein einführendes Beispiel: Das Durchlaufen eines Binärbaums
Das ist eine Grundaufgabe zur Behandlung von Datenstrukturen. Ein binärer Baum B
ist entweder leer, oder er besteht aus einem linken Baum BL, einem Knoten W und
einem rechten Teilbaum BR. Diese Definition ist rekursiv. Den Knoten W eines
nichtleeren Baumes nennt man seine Wurzel. Beim Durchlaufen des binären
Baumes sind alle Knoten aufzusuchen (, z. B. in einer vorgegebenen „von links nach
rechts"-Reihenfolge,) mit Hilfe eines systematischen Weges, der aus Kanten
aufgebaut ist 2.
Die Darstellung bzw. die Implementierung eines binären Baums benötigt einen
Binärbaum-Knoten:
Dateninformation
Knotenzeiger
Links
Rechts
Zeiger
Zeiger
zum linken
zum rechten
Nachfolgeknoten
Abb. 1.1-0: Knoten eines binären Suchbaums
Eine derartige Struktur stellt die Klassenschablone baumKnoten bereit 3:
#ifndef BAUMKNOTEN
#define BAUMKNOTEN
#ifndef NULL
const int NULL = 0;
#endif // NULL
// Deklaration eines Binaerbaumknotens fuer einen binaeren Baum
template <class T> class baumKnoten
{
protected:
// zeigt auf die linken und rechten Nachfolger des Knoten
baumKnoten<T> *links;
baumKnoten<T> *rechts;
public:
// Das oeffentlich zugaenglich Datenelement "daten"
T daten;
// Konstruktor
baumKnoten (const T& merkmal, baumKnoten<T> *lzgr = NULL,
baumKnoten<T> *rzgr = NULL);
// virtueller Destruktor
virtual ~baumKnoten(void);
// Zugriffsmethoden auf Zeigerfelder
2
3
vgl. Skriptum: Algorithmen und Datenstrukturen SS09, 4.2.3
vgl. pr11_1, baumkno.h
8
Algorithmen und Datenstrukturen
baumKnoten<T>* holeLinks(void) const;
baumKnoten<T>* holeRechts(void) const;
// Die Klasse binSBaum benoetigt den Zugriff auf
// "links" und "rechts"
};
// Schnittstellenfunktionen
// Konstruktor: Initialisiert "daten" und die Zeigerfelder
// Der Zeiger NULL verweist auf einen leeren Baum
template <class T>
baumKnoten<T>::baumKnoten (const T& merkmal, baumKnoten<T> *lzgr,
baumKnoten<T> *rzgr): daten(merkmal), links(lzgr), rechts(rzgr)
{}
// Die Methode holeLinks ermoeglicht den Zugriff auf den linken
// Nachfolger
template <class T>
baumKnoten<T>* baumKnoten<T>::holeLinks(void) const
{
// Rueckgabe des Werts vom privaten Datenelement links
return links;
}
// Die Methode "holeRechts" erlaubt dem Benutzer den Zugriff auf den
// rechten Nachfoger
template <class T>
baumKnoten<T>* baumKnoten<T>::holeRechts(void) const
{
// Rueckgabe des Werts vom privaten Datenelement rechts
return rechts;
}
// Destruktor: tut eigentlich nichts
template <class T>
baumKnoten<T>::~baumKnoten(void)
{}
#endif // BAUMKNOTEN
Mit der vorliegenden Implementierung zu einem Binärbaum-Knoten kann bspw. die
folgende Gestalt eines binären Baums erzeugt werden:
1
2
3
5
4
Abb1.1-1: Eine binäre Baumstruktur
Benötigt wird dazu die folgenden Anweisungen im Hauptprogrammabschnitt:
// Hauptprogramm
int main()
{
int zahl;
baumKnoten<int> *wurzel;
9
Algorithmen und Datenstrukturen
baumKnoten<int> *lKind, *rKind, *z;
lKind = new baumKnoten<int>(3);
rKind = new baumKnoten<int>(4);
z
= new baumKnoten<int>(2,lKind,rKind);
lKind = z;
rKind = new baumKnoten<int>(5);
z
= new baumKnoten<int>(1,lKind,rKind);
wurzel = z;
}
1.1.2.1 Rekursive Problemlösung
Rekursive Datenstrukturen (z.B. Bäume) werden zweckmäßigerweise mit Hilfe
rekursiv formulierter Zugriffsalgorithmen bearbeitet. Das zeigt die folgende Lösung in
C++:
#include<iostream.h>
#include<stdlib.h>
#include "baumkno.h"
// Funktionsschablone fuer Baumdurchlauf
template <class T> void wlr(baumKnoten<T>* b)
{
if (b != NULL)
{
cout << b->daten << ' ';
wlr(b->holeLinks());
// linker Abstieg
wlr(b->holeRechts());
// rechter Abstieg
}
}
// Hauptprogramm
int main()
{
int zahl;
baumKnoten<int> *wurzel;
baumKnoten<int> *lKind, *rKind, *z;
lKind = new baumKnoten<int>(3);
rKind = new baumKnoten<int>(4);
z
= new baumKnoten<int>(2,lKind,rKind);
lKind = z;
rKind = new baumKnoten<int>(5);
z
= new baumKnoten<int>(1,lKind,rKind);
wurzel = z;
cout << "Inorder: " << endl;
ausgBaum(wurzel,0);
wlr(wurzel);
// Rekursive Problemlösung
cout << endl;
// Nichtrekursive Problemlösung
wlrnr(wurzel);
cout << endl;
}
Das Durchlaufen geht offensichtlich von der Wurzel aus, ignoriert zuerst die rechten
Teilbäume, bis man auf leere Bäume stößt. Dann werden die Teiluntersuchungen
abgeschlossen und beim Rückweg die rechten Bäume durchlaufen.
Jeder Baumknoten enthält 2 Zeiger (Adressen von BL und BR). Die Zeiger, die auf
leere Bäume hinweisen, werden auf „NULL“ gestellt.
10
Algorithmen und Datenstrukturen
1.1.2.2 Nichtrekursive Problemlösung
Das vorliegende Beispiel ist in C++ notiert. C++ läßt rekursiv formulierte Prozeduren
zu. Was ist zu tun, wenn eine Programmiersprache rekursive Prozeduren nicht
zuläßt? Rekursive Lösungsangaben sind außerdem schwer verständlich, da ein
wesentlicher Teil des Lösungswegs dem Benutzer verborgen bleibt.
Die Ausführung rekursiver Prozeduren verlangt bekanntlich einen Stapel (stack). Ein
Stapel ist eine Datenstruktur, die auf eine Folge von Elementen 2 wesentliche
Operationen ermöglicht:
Die beiden wesentlichen Stackprozeduren sind PUSH und POP. PUSH fügt dem
Stapel ein neues Element an der Spitze (top of stack) hinzu. POP entfernt das
Spitzenelement. Die beiden Prozeduren sind mit der Typdefinition des Stapel
beschrieben. Der Stapel nimmt Zeiger auf die Baumknoten auf. Jedes Stapelelement
ist mit seinen Nachfolgern verkettet:
Top-Element
Zeiger auf Baumknoten
Zeiger auf Baumknoten
Zeiger auf Baumknoten
nil
nil
Abb. 1.1-2: Aufbau eines Stapels
Der nicht rekursive Baumdurchlauf-Algorithmus läßt sich mit Hilfe der
Stapelprozeduren der Containerklasse Stack der Standard Template Library (STL) 4
so formulieren:
template <class T> void wlrnr(baumKnoten<T>* z)
{
stack<baumKnoten<T>*, vector<baumKnoten<T>*> > s;
s.push(NULL);
while (z != NULL)
{
cout << z->daten << ' ';
if (z->holeRechts() != NULL)
s.push(z->holeRechts());
if (z->holeLinks() != NULL)
z = z->holeLinks();
else {
z = s.top();
s.pop();
}
}
}
4
vgl. Skriptum: Algorithmen und Datenstrukturen SS09, 2.2
11
Algorithmen und Datenstrukturen
Dieser Algorithmus ist zu überprüfen mit Hilfe des folgenden binären Baumes
Z1
1
Z2
Z5
2
5
Z3
Z4
3
4
Abb. 1.1-3: Zeiger im Binärbaum
Welche Baumknoten (bzw. die Zeiger auf die Baumknoten) werden beim Durchlaufen des vorliegenden Baumes (vgl. Abb. 1.1-3) über die Funktionsschablone
wlrnr aufgesucht? Welche Werte (Zeiger auf Baumknoten) nimmt der Stapel an?
Besuchte Knoten ¦
Stapel
-----------------+------------¦
Null
Z1
¦
Z5 Null
Z2
¦ Z4 Z5 Null
Z3
¦ Z4 Z5 Null
Z4
¦
Z5 Null
Z5
¦
Null
Verallgemeinerung
Bäume treten in vielen Situationen auf. Beispiele dafür sind:
- die Struktur einer Familie, z.B.:
Christian
Ludwig
Jürgen
Martin
Karl
Ernst
Abb. 1.1-4: Ein Familienstammbaum
- Bäume sind auch Verallgemeinerungen von Feldern (arrays), z.B.:
12
Fritz
Algorithmen und Datenstrukturen
-- das 1-dimensionale Feld F
F
F[1]
F[2]
F[3]
Abb. 1.1-5: Baumdarstellung eines eindimensionalen Felds
-- der 2-dimensionale Bereich
B
B[1,_]
B[2,_]
B[1,1]
B[1,2]
B[2,1]
B[2,2]
Abb. 1.1-6: Baumdarstellung eines zweidimensionalen Felds
-- Auch arithmetische Ausdrücke lassen sich als Bäume darstellen. So läßt sich
bspw. der Ausdruck ((3 * (5 - 2)) + 7) so darstellen:
+
*
7
3
-
5
2
Abb. 1.1-6: Baumdarstellung des arithmetischen Ausdrucks ((3 * (5 - 2)) + 7), sog. Kantorowitch-Baum
Durch das Aufnehmen des arithmetischen Ausdrucks in die Baumdarstellung können
Klammern zur Regelung der Abarbeitungsreihenfolge eingespart werden. Die
korrekte Auswertung des arithmetischen Ausdrucks ist auch ohne Klammern bei
geeignetem Durchlaufen und Verarbeiten der in den Baumknoten gespeicherten
Informationen gewährleistet.
13
Algorithmen und Datenstrukturen
Die Datenstruktur „Baum“ ist offensichtlich in vielen Anwendungsfällen die geeignete
Abbildung für Probleme, die mit Rechnerunterstützung gelöst werden sollen. Der zur
Lösung notwendige Verfahrensablauf ist durch das Aufsuchen der Baumknoten
festgelegt. Das einführende Beispiel zeigt das Zusammenwirken von Datenstruktur
und Programmierverfahren für Problemlösungen.
Bäume sind deshalb auch Bestandteil von Container-Klassen aktueller Compiler
(C++, Java).
14
Algorithmen und Datenstrukturen
1.2 Algorithmische Grundkonzepte
1.2.1 Algorithmenbegriffe
Algorithmen im Alltag
Gegeben ist ein Problem. Eine Handlungsvorschrift, deren mechanisches Befolgen
- ohne Verständnis des Problems
- mit sinnvollen Eingabedaten
- zur Lösung des Problems
führt, wird Algorithmus genannt. Ein Problem, für dessen Lösung ein Algorithmus
existiert, heißt berechenbar.
Bsp.:
- Zerlegung handwerklicher Arbeiten in einzelne Schritte
- Kochrezepte
- Verfahren zur schriftlichen Multiplikation
- Algorithmen zur Bestimmung des größten gemeinsamen Teiles zweier natürlichen Zahlen
- Bestimmung eines Schaltjahres
- Spielregeln
Der intuitive Algorithmenbegriff
Ein Algorithmus ist eine präzise (d.h. in einer festgelegten Sprache abgefasste)
endliche Verarbeitungsvorschrift, die genau festlegt, wie die Instanzen einer Klasse
von Problemen gelöst werden. Ein Algorithmus liefert eine Funktion (Abbildung), die
festlegt, wie aus einer zulässigen Eingabe die Ausgabe ermittelt werden kann.
Ein Algorithmus (in der EDV) ist
- ein Lösungsschritt für eine Problemklasse (konkretes Problem wird durch
Eingabeparameter identifiziert)
- geeignet für die Implementierung als Rechnerpogramm
- endliche Folge von elementaren, ausführbaren Instruktionen Verarbeitungsschritten
1.2.2 Terminierung und Determinismus
Abgeleitet vom intuitiven Algorithmenbegriff spielen bei der Konzeption von
Algorithmen die Begriffe Terminierung, Determinismus und Vollständigkeit eine
Rolle:
Terminierung
Ein Algorithmus heißt terminierend, wenn er (bei jeder erlaubten Eingabe von
Parametern) nach endlich vielen Schritten abbricht.
Determinismus
Ein Algorithmus hat einen deterministischen Ablauf, wenn er eine eindeutige
Schrittfolge besitzt. Der Algorithmus läuft bei jedem Ablauf mit den gleichen
15
Algorithmen und Datenstrukturen
Eingaben durch dieselbe Berechnung. Ein Algorithmus liefert ein determiniertes
Ergebnis, wenn bei vorgegebener Eingabe (auch bei mehrfacher Durchführung) stets
ein eindeutiges Ergebnis erreicht wird. Nicht deterministische Algorithmen mit
determiniertem Ergebnis heißen determinierter Algorithmus.Nicht deterministische
Algorithmen können zu einem determiniertem Ergebnis führen, z.B.:
1. Nimm eine Zahl x ungleich Null
2. Entweder: Addiere das Dreifache von x zu x und teile das Ergebnis durch den Anfangswert von x
Oder: Subtrahiere 4 von x und subtrahiere das Ergebnis von x
3. Schreibe das Ergebnis auf
Vollständigkeit
Alle Fälle, die bei korrekten Eingabedaten auftreten können, werden berücksichtigt.
Bsp.:
Nichtvollständige Algorithmen
(1) Wähle zufällig eine Zahl x
(2) Wähle zufällig eine Zahl y
(3) Das Ergebnis ist x/y
Was ist, wenn y == 0 sein sollte
Nicht terminierender Algorithmus
(1) Wähle zufällig eine Zahl x
(2) Ist die Zahl gerade, wiederhole ab (1)
(3) Ist die Zahl ungerade, wiederhole ab (1)
Nicht determinierter Algorithmus
60
64
(1) Wähle zufällig eine natürliche Zahl zwischen 2 und 2
(2) Prüfe, ob die Zahl eine Primzahl ist.
(3) Falls nicht, wiederhole ab 1.
Das Ergenis ist immer eine Primzahl, aber nicht die gleiche, daher ist der Algorithmus nicht
determiniert.
Deterministische,
terminierende
Algorithmen
definieren
jeweils
eine
Ein/Ausgabefunktion: f : Eingabewerte -> Ausgabewerte
Algorithmen geben eine konstruktiv ausführbare Beschreibung dieser Funktion, die
Funktion heißt Bedeutung (Semantik) des Algorithmus. Es kann mehrere
verschiedene Algorithmen mit der gleichen Bedeutung geben.
1.2.3 Algorithmenbausteine
Gängige Bausteine zur Beschreibung bzw. Ausführung von Algorithmen sind:
- elementare Operationen
- sequentielle Ausführung (ein Prozessor)
Der Sequenzoperator ist „;“. Sequenzen ohne Sequenzoperator sind häufig
durchnummeriert und können schrittweise verfeinert werden, z.B:
(1) Koche Wasser
(2) Gib Kaffepulver in Tasse
(3) Fülle Wasser in Tasse
(2) kann verfeinert werden zu:
16
Algorithmen und Datenstrukturen
Öffne Kaffeedose;
Entnehme Löffel von Kaffee;
Kippe Löffel in Tasse;
Schließe Kaffeedose;
- parallele Ausführung
- bedingte Ausführung
Die Auswahl / Selektion kann allgemein so formuliert werden:
falls Bedingung, dann Schritt
bzw.
falls Bedingung
dann Schritt a
sonst Schritt b
„falls ... dann ... sonst ...“ entspricht in Programmiersprachen den Konstrukten:
if Bedingung then ... else … fi
if Bedingung then … else …endif
if (Bedingung) … else …
- Schleife (Iteration)
Dafür schreibt man allgemein
wiederhole Schritte
bis Abbruchkriterium
Häufig findet man auch die Variante
solange Bedingung
führe aus Schritte
bzw. die Iteration über festen Bereich
wiederhole für Bereichsangabe
Schleifenrumpf
Diese Schleifenkonstrukte
Konstrukten:
wiederhole ... bis ...
solange … führe aus
wiederhole für
entsprechen
repeat ... until …
do …
while ...
while … do ...
while ( ... ) ...
for each ... do …
for ... do …
for ( ... ) ...
- Unterprogramm (Teilalgoritmus)
17
jeweils
den
Programmiersprachen-
Algorithmen und Datenstrukturen
- Rekursion
Eine Funktion (mit oder ohne Rückgabewert, mit oder ohne Parameter) darf in der
Deklaration ihres Rumpfes den eigenen Namen verwenden. Hierdurch kommt es zu
einem rekursiven Aufruf. Typischerweise werden die aktuellen Parameter so
modifiziert, daß die Problemgröße schrumpft, damit nach mehrmaligem Wiederholen
dieses Prinzips kein weiterer Aufruf erforderlich ist und die Rekursion abbrechen
kann.
1.2.4 Paradigmen der Algorithmenbeschreibung
Ein Algorithmenparadigma legt Denkmuster fest, die einer Beschreibung eines
Algorithmus zugrunde liegen. Faßt man einen Algorithmus als Beschreibung eines
allgemeinen Verfahrens unter Verwendung ausführbarer elementarer Schritte auf,
dann gibt es 2 grundlegende Arten, Schritte von Algorithmen zu notieren:
- Applikative Algorithmen sind eine Verallgemeinerung der Funktionsauswertung
mathematisch notierter Funktionen. In ihnen spielt die Rekursion 5 eine wesentliche
Rolle.
- Imperative Algorithmen basieren auf einem einfachen Maschinenmodell mit
gespeicherten und änderbaren Werten. Hier werden primär Schleifen und
Alternativen als Kontrollbausteine eingesetzt.
In der Informatik sind darüber hinaus noch folgende Paradigmen wichtig:
- Objektorientiete Algorithmen. In einem objektorientierten Algorithmus werden
Datenstrukturen und Methoden zu einer Klasse zusammengefasst. Von jeder
Klasse können Objekte gemäß der Datenstruktur erstellt und über die Methode
manipuliert werden.
Das objektorientierte Paradigma ist kein Algorithmenparadigma im engeren Sinne,
da es sich um ein Paradigma zur Strukturierung von Algorithmen handelt, das
sowohl mit applikativen, imperativen und logischen Konzepten zusammen
eingesetzt werden kann.
- logische (deduktive) Algorithmen. Ein logischer Algorithmus führt Berechnungen
durch, indem er aus Fakten und Regeln durch Ableitungen in einem logischen
Kalkül weitere Fakten ausweist.
5
vgl. Skriptum: Algorithmen und Datenstrukturen SS09, 3.3
18
Algorithmen und Datenstrukturen
1.2.4.1 Applikative Algorithmen
Applikative Algorithmen sind die Grundlage eine Reihe von universellen
Programmiersprachen wie APL, Lisp, Scheme etc. Diese Programmiersprachen
werden als funktionale Programmiersprachen bezeichnet.
Idee: Defintion zusammengesetzter Funktionen durch Ausdrücke / Terme, z.B.
f ( x) = 5 x + 1 .
Definitionen 6
Ein applikativer Algorithmus ist eine Liste von Funktionsdefinitionen
f1 (v1,1 ,..., v1,n1 ) = t1 (v1,1 ,..., v1,n1 )
.
.
f m (v m1,1 ,..., v m1,nm ) = t m (v m1,1 ,..., v m ,nm )
v1 ,..., v n : Unbestimmte vom Typ τ 1 ,...,τ n , formale Parameter. τ ist dabei der Typ des Terms
t (v1 ,..., v n )
t (v1 ,..., v n ) : ein Term (/Ausdruck), heißt Funktionsausdruck
Die erste Funktion wird ausgewertet und bestimmt die Bedeutung (Semantik) des
Algorithmus.
Bsp.: 7
1. f ( x, y ) = if g ( x, y ) then h( x + y ) else h( x − y ) fi
g ( x, y ) = ( x = y ) or odd ( y )
h( x) = j ( x + 1) ∗ j ( x − 1)
j ( x) = 2 x − 3
8 f (1,2) a if g (1,2) then h(1 + 2) else h(1 − 2) fi
a if 1 = 2 or odd (2) then h(1 + 2) else h(1 − 2) fi
a if 1 = 2 or false then h(1 + 2) else h(1 − 2) fi
a if false or false then h(1 + 2) else h(1 − 2) fi
a if false then h(1 + 2) else h(1 − 2) fi
a h(1 − 2)
a h(−1)
a j (−1 + 1) ∗ j (−1 − 1)
a j (0) ∗ j (−1 − 1)
a j (0) ∗ j (−1 − 1)
a j (0) ∗ j (−2)
a j (2 ∗ 0 − 3) ∗ j (−2)
a (−3) ∗ (−7)
6
Hier erfolgt eine Beschränkung der Definitionen auf Fünktionen über int und bool, obwohl die Konzepte
natürlich für beliebige Datentypen gelten.
7 x, y : ganze Zahlen
8 a : konsekutive Ausführung mehrerer elementarer Termauswertungsgebiete
19
Algorithmen und Datenstrukturen
a 21
2. f ( x, y ) = if x = 0 then y else (
if x > 0 then f ( x − 1, y ) + 1else − f (− x,− y ) fi ) fi
f (0, y ) a y für alle y
f (1, y ) a f (0, y ) + 1a y + 1
f (2, y ) a f (1, y ) + 1a y + 1 + 1 a y + 2
….
f (n, y ) a y + n
f (−1, y ) a − f (1,− y ) a −(1 − y ) a y − 1
…
f ( x, y ) = x + y
Eine Funktionsdefinition definiert eine Funktion mit folgender Signatur:
f : τ 1 × τ 2 × ...τ n → τ n
Sind a1 ,..., a n Werte vom Typ τ 1 ,...,τ n , so ersetzt man bei der Auswertung von
f (a1 ,..., a n ) im definierten Vorkommen v1 durch a1 und wertet t (a1 ,..., a n ) aus.
a1 ,..., a n : aktuelle Parameter
f (a1 ,..., a n ) : Funktionsaufruf
Aufrufe definierter Funktionen dürfen als Terme verwendet werden.
Bsp. für applikative Algorithmen
1. Fakultätsberechnung
x!= x ∗ ( x − 1) ∗ ( x − 2) ∗ ... ∗ 2 ∗ 1
für x > 0
fak ( x) = if x ≤ 0 then1else x ∗ fak ( x − 1)
mathematische Funktion
applikativer Algorithmus
2. Größter gemeinsamer Teiler 9(ggT)
ggT ( x, x) = x
mathematische
ggT ( x, y ) = ggT ( y, x)
Gesetzmäßigkeiten
ggT ( x, y ) = ggT ( x, y − x)
für x < y
applikativer Algorithmus
ggT ( x, y ) = if ( x ≤ 0) or ( y ≤ 0) then ggT ( x, y )
else if x = y then x
else if x > y then ggT ( y, x)
else ggT ( x, y − x)
fi fi fi;
ggT ist korrekt für positive Eingaben, bei negativen Eingaben ergeben sich nicht
abbrechbare Berechnungen (undefinierte Funktionen) 10.
ggT (39,15) a ggT (15,39) + ggT (15,24) a ggT (15,9) a ggT (9,15) a ggT (9,6) a ggT (6,9)
ggT (6,3) a ggT (3,6) + ggT (3,3) a 3
3. Fibonacci-Zahlen: f 0 = f 1 = 1, f i = f i −1 + f i − 2 für i > 0
fib( x) = if ( x = 0) or ( x = 1) then1 else fib( x − 2) + fib( x − 1) fi
⎧ x _ te Fibonacci − Zahl falls x ≥ 0
Bedeutung: fib( x) = ⎨
sonst
⎩ 1
9
vgl. 1.2.5
Das Berechnungsschema stützt sich auf eine Formularisierung des Originalverfahrens von Euklid ab.
10
20
Algorithmen und Datenstrukturen
1.2.4.2 Imperative Algorithmen
In einem imperativen Algorithmus gibt es Variable, die verschiedene Werte
annehmen können. Die Menge aller Variablen und ihrer Werte (sowie der
Programmzähler) beschreiben den Zustand zu einem bestimmten Zeitpunkt. Ein
Algorithmus bewirkt eine Zustandstransformation.
Imperative Konzepte
- Anweisungen
-- primitive Anweisungen: Zuweisung, Block, Prozeduraufruf
-- zusammengesetzte Anweisungen: Sequenz, Auswahl, Iteration
- Ausdrücke
-- primitive Ausdrücke: Konstante, Variable, Funktionsaufruf
-- zusammengesetzte Ausdrücke: Operanden / Operatoren
- Datentypen
-- primitive Datentypen: Wahrheitswerte, Zeichen, Zahlen, Aufzählung
-- zusammengesetzte Datentypen: Felder, Verbund, Vereinigung, Zeiger
- Abstraktion
-- Anweisung
-- Ausdruck: Funktionsdeklaration
-- Datentyp: Typdeklaration
- Weitere Konzepte
-- Ein- und Ausgabe
-- Ausnahmenbehandlung
-- Bibliotheken
-- Parallele und verteilte Berechnungen
Wertzuweisungen sind die einzigen elementaren Anweisungen imperativer
Algorithmen. Aus ihnen werden zusammengesetzte Anweisungen gebildet, aus
denen imperative Algorithmen bestehen.
Elementare Anweisungen können auf unterschiedliche Art zu komplexen
Anweisungen zusammengestzt werden:
(1) sequentielle Ausführung
(2) bedingte Ausführung
(3) wiederholte Ausführung
(4) Ausführung als Unterprogramm
(5) rekursive Ausführung eines Unterprogramms
Diese Möglichkeiten werden als Kontrollstrukturen bezeichnet.
1.2.4.3 Objektorientierte Algorithmen
Das objektorientierte Paradigma der Algorithmenentwicklung hat verschiedene
Wurzeln:
-
11
Realisierung abstrakter Datentypen 11
Rechnergeeignete Modellierung der realen Welt (objektorientierte Analyse)
Problemnaher Entwurf von Softwaresystemen (objektorientiertes Design)
Problemnahe Implementierung (objektorientierte Programmierung
Vgl. 1.3.5.1
21
Algorithmen und Datenstrukturen
Ein Objekt ist die Repräsentation eines Gegenstands und Sachverhalts der realen
Welt oder eines gedanklichen Konzepts.
Es ist gekennzeichnet durch
-
-
eine eindeutige Identität, durch die es sich von anderen Objekten unterscheidet
Wertbasierte Objektmodelle: In diesem Modell besitzen Objekte keine eigene Identität im
eigentlichen Sinn. Zwei Objekte werden schon als identisch angesehen, wenn ihr Zustand
gleich ist.
Identitätsbasierte Objektmodelle: Jedem Objekt innerhalb des Systems wird eine vom Wert
unabhängige Identität zugeordnet,
statische Eigenschaften zur Darstellung des Zustands des Objekts in Form von Attributen
dynamische Eigenschaften in Form von Methoden, die das Verhalten des Objekts
beschreiben
Der Zustand eines Objekts zu einem Zeitpunkt entspricht der Belegung der Attribute
des Objekts zu diesem Zeitpunkt.
Der Zustand kann mit Hilfe von Methoden erfragt und geändert werden.
Methoden sind in der programmiesprachlichen Umsetzung Prozeduren und
Funktionen, denen Parameter übergeben werden können. Der Zustand eines eine
Methode ausführenden Objekts (und nur dieses Objekts) ist der Methode im Sinne
einer Menge globaler Variablen direkt zugänglich. Es kann daher sowohl gelesen als
auch geändert werden.
Objekte verwenden das Geheimnisprinzip und das Prinzip der Kapselung. Sie
verbergen ihre Interna:
-
Zustand (Belegung der Attribute)
Implementierung ihres Zustands
Implementierung ihres Verhaltens
Objekte sind nur über ihre Schnittstelle, also über die Menge der vom Objekt der
Außenwelt zur Verfügung gestellten Methoden zugänglich. Man spricht von den
Diensten des Objekts.
Objekte interagieren über Nachrichten:
-
Ein Objekt x sendet eine Nachricht an Objekt y. y empfängt die Nachricht von x
Innerhalb der Programmiersprache wird dieser Vorgang meistens durch einen
Methodenaufruf implementiert
Nachrichten (Methodenaufrufe) können den Zustand eines Objekts verändern
Ein Objekt kann sich selbst Nachrichten schicken.
Objekte können in Beziehung zueinander stehen.
-
-
-
Die Beteiligten an eine Beziehung nehmen Rollen ein, z.B.:
Rolle des Arztes: „behandelnder Arzt“,
Rolle des Patienten: „Patient“
Ein Objekt kann mit mehreren Objekten in Beziehung stehen
Rolle vom Arzt: „behandelnder Arzt“
Rolle von Patient 1: „Patient“, Rolle von Patient 2: „Patient“
Nachrichen können nur ausgetauscht werden, wenn eine Beziehung besteht
Beziehungen können sich während der lebenszeit eines Objekts verändern
Es gibt in der Regel Objekte, die sich bezüglich der Attribute, Methoden und
Beziehungen ähnlich sind. Daher bieten es sich an, diese Objekt zu einer Klasse
zusammenzufassen. Die Klasse beinhaltet dann auch Angaben darüber, wie Objekte
dieser Klasse verwaltet (z.B. erzeugt oder gelöscht) werden können.
22
Algorithmen und Datenstrukturen
-
Klassendefinitionen sind eng verwandt mit abstrakten Datentypen. Sie legen Attribute und
Methoden der zugehörigen Objekte fest
Objekte dieser Klasse nennt man auch Instanzen dieser Klasse
Beziehungen (Assoziationen) zwischen Objekten werden auf Klassenebene beschrieben
Ein Konstruktor ist eine Methode zur Erzeugung von Objekten 12.
-
Es gibt Attribute von Klassen, die nicht an konkrete Instanzen gebunden sind. Diese heißen
Klassenvariable oder statische Vartiable.
Klassenvariable existieren für die gesamte Lebensdauer einer Klasse genau einmal –
unabhängig davon, wie viele Objekte erzeugt wurden
Neben Klassenvariablen gibt es auch Klassenmethoden, d.h. Methoden, deren Existenz
nicht an konkrete Objekte gebunden ist. Klassenmethoden werden auch statische Methoden
genannt.
Zu ähnlichen Klassen versucht man eine gemeinsame Oberklasse (Basisklasse) zu
finden, die die Ähnlichkeiten aufnimmt. Unterklassen (Subklassen) werden nur um
individuelle Eigenschaften ergänzt, denn eine Unterklasse erbt die Attribute und
Methoden der Oberklasse.
Eine Veraible vom Typ einer Basisklasse kann während ihrer Lebensdauer sowohl
Objekte ihres eigenen Typs als auch soche von abgeleiteten Klassen aufnehmen.
Dieses wird als Polymorphismus 13 bezeichner.
-
-
-
Eine Unterklasse erbt von ihrere Oberklasse alle Attribute und Methoden und kann diese um
weitere Methoden ergänzen
Erben heißt: Die Attribute und Methoden können in der Unterklasse verwendet werden, als
wären sie in der Klasse selbst definiert.
Vererbungen können mehrstufig sein. Es entstehen Vererbungshierarchien.
Eine Unterklasse kann eine Variable deklarieren, die denselben Namen trägt, wie eine der
Oberklasse. Hierdurch wird eine weiter oben liegende Variable verdeckt. Dies wird häufig
dazu benutzt, um den Typ einert Variablen der Oberklasse zu überschreiben. In manchen
Programmiersprachen gibt es Konstrukte, die den Zugriff auf verdeckte Variable ermöglichen
Metoden, die aus der Basisklasse geerbt wurden, dürfen in der abgeleiteten Klasse
überlagert, d.h. neu definiert werden.
Da eine Variable einer Basisklasse Werte von verschiedenen Typen annehmen kann,
entscheidet sichj bei überlagerten Mathoden erst zur Laufzeit, welche Methode zu verwenden
ist: Dynamische Methodensuche
Wird eine Methode in einer abgeleiteten Klasse überlagert, wird die ursprüngliche Methode
verdeckt. Aufrufe der Methode beziehen sich auf die überlagerte Variante
In amnchen Programmiersprachen gibt esw Konstrukte, die den Zugriff auf überlagerte
Methoden ermöglichen 14.
Mit Hilfe von Modifikatoren 15 können Sichtbarkeit und Eigenschaften von Klassen,
Variablen und Methoden beeinflusst werden.
-
Die Sichbarkeit bestimmt, ob eine Klasse, Variable oder Methode in anderen Klassen genutzt
werden kann.
Eigenschaften, die über Modifikatoren gesteuert werden können, sind z.B. die Lebensdauer
und die Veränderbarkeit
Abstrakte Methoden: Eine Methode heißt abstrakt, wenn ihre Deklaration nur die
Schnittstelle, nicht aber die Implementierung enthält. Im Gegensatz dazu stehen die
12
Vgl. Skriptum zur Vorlesung im WS 2005 / 2006: Programmieren in Java, 1.4.1.1.3 bzw.
Skriptum zur Vorlesung im SS 2006: Programmieren in C++, 3., 3.1, 3.2
13 Vgl. Skriptum zur Vorlesung im WS 2005 / 2006: Programmieren in Java, 1.4.1.8
14 in Java: Verwendung des Präfixes: super
15 Bsp. für Modifikatoren in Java sind: public, private, static, final, …
23
Algorithmen und Datenstrukturen
konkreten Methoden, deren Deklaration auch Implementierungen besitzen 16.
Abstrakte Methoden können nicht aufgerufen werden, sie definieren nur eine
Schnittstelle. Erst durch Überlagerung in einer abgeleiteten Klasse und durch
Angabe der fehlenden Implementierung wird eine abstrakte Klasse konkret.
Abstraklte Klassen: Eine Klasse, die nicht instanziiert werden kann, heißt abstrakte
Klasse. Klassen, von denen Objekte erzeugt werden können, sind konkrete Klassen.
Jede Klasse, die mindestens eine abstrakte Methode besitzt, ist abstrakt 17.
Schnittstellen: Eine Schnittstelle (interface) ist in Java eine Klasse, die
ausschließlich Konstanten und abstrakte Methoden enthält. Zur Definition einer
Schnittstelle wird das Schlüsselwort class durch das Schlüsselwort interface
ersetzt. Mitt „interfaces“ kann in Java das Konzept der Mehrfachvererbung
implementiert werden, das in C++ direkt realisierber ist.
Generizität: Unter Generizität versteht man die Parametrisierung von Klassen,
Datentypen, Prozeduren, Moduln, Funktionen, etc. Als Parameter werden in der
Regel Datentypen (manchmal auch Algorithmen in Form von Prozeduren) verwendet.
1.2.4.4 Paradigmen und Programmiersprachen
Zu den Paradigmen korrespondieren jeweils Programmiersprachen, die diesen
Ansatz realisieren. Moderne Programmiersprachen vereinen oft Ansätze mehrerer
Paradigmen. So ist bspw. Java bzw. C++ objektorientiert 18, umfasst aber auch
imperative und applikative Elemente.
1.2.5 Beschreibung von Algorithmen
Verbreitetes Grundschema von Algorithmen
Name des Algorithmus und Parameterliste
Spezifikation des Ein- und Ausgabeverhaltes
1. Schritt
Einführung von Hilfsgrößen
Vorbereitung
Initialisierungen
2. Schritt
Prüfe, ob ein einfacher Fall vorliegt
Trivialfall
Falls ja: Ergebnis ausgeben und enden
3. Schritt
Reduziere Problemstellung A auf einfachere Form B
Problemreduktion, (z.B. Aufteilen in Teilprobleme)
Ergebnisaufbau
4. Schritt
entweder Rekursion:
oder Iteration:
Rekursion bzw.
Rufe Algorithmus mit
Fahre mit B anstelle a bei
Iteration
reduziertem B auf
Schritt 2 fort
16
Java: Die Deklaration einer abstrakten Methode erfolgt durch den Modifikator abstract.
Java: Es ist erforderlich, abstrakte Klassen abzuleiten und in der abgeleiteten Klasse eine oder mehrere
abstrakte Methoden zu implementieren. Die Konkretisierung kann über mehrere Stufen erfolgen.
18 Vgl. Skriptum zur Vorlesung im WS 2005 / 2006: Programmieren in Java, 1. bzw.
Skriptum zur Vorlesung im SS 2006: Programmieren in C++
17
24
Algorithmen und Datenstrukturen
Verbale Umschreibung von Algorithmen
Eine derartige Handlungsanweisung könnte bspw. die „Berechnung des größten
gemeinsamen Teilers von a und b“ in folgender Weise sein:
1. Weise x den Wert von a zu
2. Weise y den Wert von b zu
3. Falls x gleich y ist: gehe zu 9
4. Falls x kleiner als y ist: gehe zu 7
5. Weise x den Wert von (x-y) zu
6. Gehe zu 3
7. Weise y den Wert von (y-x) zu
8. gehe zu 3
9. Weise ggTden Wert von x zu
Pseudo-Code
- Abstrakte Beschreibung eines Algorithmus
- Strukturierter als Beschreibung mit normalen Sprachvokabular
- weniger detailliert als ein Programm
- Bevorzugete Notation zur Beschreibung eines Algorithmus
- versteckt Programmimplementierungsprobleme
Bsp.: Finden des größten Elements in einem Array
Algorithmus arrayMax(a,n)
Input array a mit n Ganzzahlen
Output größtes Element von a
currentMax = a[0]
for i= 1 to n-1 do
if (a[i] > curentMax then currentMax = a[i]
return currentMax
Pseudocode-Details:
- Kontrollfluss
-- if … then … [else …]
-- while … do …
-- repeat … until …
-- for … do
- Einrücken ersetzt Klammern
- Deklaration von Methoden
Algorithmus methode(arg [,arg …])
Input …
Output …
- Rückgabewert
return Ausdruck
- Ausdrücke
=
Zuweisung
==
Gleiheitstest
2
Subscripts und andere mathematische Formulierungen sind erlaubt
n
Pseudo-Code Elemente:
Sequenz
Verzeigung
{
Anweisung_1
Anweisung_2
…
if Bedingung
{
Anweisung_1
Anweisung_2
25
Algorithmen und Datenstrukturen
Anweisung_n
}
…
Anweissung_n
}
else
{
Anweisung_m
…
Anweisung_k
}
Iteration
While Bedingung
{
Anweisung_1
Anweisung_2
…
Anweisung_n
}
Graphische Darstellung von Flußdiagrammen
Normierte Methode (DIN 66001) zur Darstellung von Programmen
Kontrollstrukturen und Struktogramme
Strukturblock
Anweisung_1
Anweisung_2
….
Java-Struktur 19
Kommentar
Block in geschweiften
Klammern
{
Anweisung_1;
Anweisung_2;
…..
Eine Folge von Anweisungen,
die alle der Reihe nach
abgearbeitet werden,
bezeichnet man als Sequenz.
Anweisung_n;
}
Anweisung_n
Sequenz
if-Anweisung
if (Bedingung)
{
anweisung1;
}
else {
Anweisung2;
}
Fallunterscheidung
(bedingte Anweisung)
19
1
2
3
A1
A2
A3
Fall
….
sonst
An
Mit einer Anweisung der Form
Wenn Bedingung erfüllt
dann führe Anweisung1 aus
sonst führe Anweisung 2 aus
führt man eine
Fallunterscheidung durch
switch-Anweisung
Mehrfachauswahl
switch (Ausdruck) {
case Wert1 :
Anweisung1;
break;
case Wert2 :
Anweisung2;
Der Ausdruck muß ganzzahlig
sein. Das Programm wird an
der case-Anweisung
fortgesetzt., deren Wert dem
Ausdruck entspricht. Falls
Ausdruck keinem der Werte
Vgl. Skriptum zur Vorlesung im WS 2005 / 2006: Programmieren in Java, 2.4
26
Algorithmen und Datenstrukturen
Mehrfachauswahl
break;
default:
Anweisungn
}
entspricht, geht es mit der
default-Anweisung weiter
for-Schleife
Eine Anweisung der Form
for (int i=1;i <=n; i++)
{
Anweisung;
}
Für Zähler = Anfang bis Ende
Anweisung
while-Schleife
Eine Anweisung der Form
while (Bedingung)
{
Anweisung;
}
Solange bedingung erfüllt
führe Anweisung aus
do-while-(repeat)Schleife
Eine Anweisung der Form
für i=1 bis n
Anweisung
Gezählte Schleife
for-Schleife
heißt gezählte Schleife.
Gezählte Schleifen werden
dann benutzt, wenn man weiß,
wie oft eine Schleife
durchlaufen werden muß
solange Bedingung
Anweisung
while-Schleife
Anweisung
bis Bedingung
repeat-Schleife
do
{
Anweisung;
} while (Bedingung);
Prozeduraufruf
Prozedurname(Arg1,
Arg2, … , Argn);
Prozeduraufruf
27
heißt Schleife mit Eingangsbedingung. Trifft die
Bedingung anfangs nicht zu,
so wird die Wiederholungsanweisung nicht ausgeführt
Wiedrhole Anweisung
Solange Bedingung erfüllt
heißt Schleife mit Ausgangsbedingung. Im Unterschied
zur while-Schleife wird die zu
wiederholende Anweisung
mindestens einmal ausgeführt.
Prozeduren werden über ihren
Namen aufgerufen. In
Klammern kann man
Argumente übergeben.
Algorithmen und Datenstrukturen
1.2.6 Formale Eigenschaften von Algorithmen
1.2.6.1 Korrektheit, Terminierung, Hoare-Kalkül, Halteproblem
1.2.6.1.1 Korrektheit, Terminierung
Die wichtigste formale Eigenschaft eines Algorithmus ist die Korrektheit. Dazu muß
gezeigt werden, daß der Algorithmus die jeweils gestellte Aufgabe richtig löst. Man
kann die Korrektheit eines Algorithmus im Allg. nicht durch Testen an ausgewählten
Beispielen nachweisen20:
Durch Testen kann lediglich nachgewiesen werden, dass sich ein Programm für endlich viele
Eingaben korrekt verhält.
Durch eine Verifikation kann nachgewiesen werden, dass sich das Programm für alle Eingaben
korrekt verhält.
Bei der Zusicherungsmethode sind zwischen den Statements sogenannte Zusicherungen eingesetzt,
die eine Aussage darstellen über die momentane Beziehung zwischen den Variablen. Typischerweise
gibt man Zusicherungen als Kommentare vor.
Programmverifikation ist der Nachweis, dass die Zusicherungen für ein Programm tatsächlich gelten.
Sie entspricht der Durchführung eines mathematischen Beweises (einer Ableitung). Gezeigt wird
damit: Das entsprechende Programm ist korrekt bzgl. seiner Spezifikation.
/* P */
while (b)
{
/* P && b */
…
/* P */
}
/* P && !b */
Die Schleifeninvariante P muß eine Aussage über das in der Schleife errechnete Resultat R enthalten:
P ∧ ¬B ⇒ R
Zusicherungen enthalten boolsche Ausdrücke, von denen der Programmierer annimmt, dass sie an
entsprechender Stelle gelten.
Beginnend mit der ersten, offensichtlich richtigen Zusicherung lässt sich als letzte
Zusicherung eine Aussage über das berechnete Ergebnis durch Anwendung der
Korrektheitsformel 21 ableiten:
{ P } A { Q }
P und Q sind Zusicherungen
P ist die pre-condition (Vorbedingung), beschreibt die Bedingungen (constraints).
Q ist die post-condition (Nachbedingung), beschreibt den Zustand nach Ausführung der Methode
Die Korrektheitsformel bedeutet: Jede Ausführung von A, bei der zu Beginn P erfüllt
ist, terminiert in einem Zustand, in dem Q erfüllt ist.
20
E. Dijkstra formulierte das so: Man kann durch Testen die Anwesenheit von Fehlern, aber nicht die
Abwesenheit von Fehlern nachweisen.
21 Robert Floyd hatte 1967 die Idee den Kanten von Flussdiagrammen Prädikate zuzuordnen, um
Korrektheitsbeweise zu führen. C.A.R. Hoare entwickelte die Idee weiter, indem er Programme mit
"Zusicherungen" anreicherte. Er entwickelte das nach ihm benannte "Hoare Tripel"
28
Algorithmen und Datenstrukturen
Die Korrektheitsformel bestimmt partielle Korrektheit : "Wenn P beim Start von A
erfüllt ist, und A terminiert, dann wird am Ende Q gelten".
Für die Terminierung gilt folgende Formel: { P} A. Sie bedeutet: "Wenn P beim
Start von A erfüllt ist, wird A terminieren.
Partielle Korrektheit und Terminierung führen zur totale Korrektheit. Totale
Korrektheit ist eine stärkere Anforderung an das Programm.
Bsp.:
1. Partielle Korrektheit nicht aber totale Korrektheit zeigt {true} while (x!=0) x = x-1;
{x==0}, da keine Terminierung bzgl. x < 0.
2. Die Hoare-Formel {x>0} while (x > 0) x = x+1; {false} terminiert nie. Sie ist partiell
korrekt, aber nicht total korrekt.
Generell drückt die Gültigkeit von {P} A {false} Nichtterminierung aus, d.h. {P}
A {false} ist partiell korrekt, A terminmiert aber nicht, für alle Anfangszustände,
die P erfüllen.
1.2.6.1.2 Hoare-Kalkül
Das Hoare Kalkül umfasst eine Menge von Regeln, die sich aus Prämissen und
Schlussfolgerung zusammensetzen:
Prämisse1
Prämisse2
…
Prämissen
--------------Konklusion
Mit dem Hoare Kalkül kann partielle (und evtl. totale) Korrektheit eines Programms
nachgewiesen werden:
- Zerlege den Algorithmus in seine einzelnen Anweisungen und füge vor (und nach) jeder Ausführung
geeignete Vor- und Nachbedingungen ein.
- Zeige, dass die einzelnen Anweisungen korrekt sind
- Beweise die Korrektheit des gesamten Algorithmus aus der Korrektheit der einzelnen Aussagen.
Die grundlegende Idee von Hoare zum konstruktiven Beweis partieller und totaler
Korrektheit ist:
Leite (rückwärts schreitend) ausgehend von der (gewünschten) Nachbedingung die Vorbedingung ab.
1.2.6.1.3 Halteproblem
Das Halteproblem kann durch die folgende Fragestellung beschrieben werden: „Gibt
es ein Programm, das für ein beliebiges anderes Programm entscheidet, ob es für
eine bestimmte Eingabe in eine Endlosschleife gerät oder nicht?“
Das allgemeine Halteproblem drückt offenbar folgende Frage aus: „Hält Algorithmus
x bei der Eingabe von y?“
29
Algorithmen und Datenstrukturen
Anschaulicher Beweis der Unentscheidbarkeit des Halteproblems
Annahme. Es gibt eine Maschine (Algorithmus) STOP mit 2 Eingaben:
„Algorithmentext x und eine Eingabe y“ und 2 Ausgaben:
- JA: x stoppt bei der Eingabe von y
- NEIN: x stoppt nicht bei der Eingabe von y
x
JA
STOP
y
NEIN
Mit dieser Maschine STOP kann man eine Maschine SELTSAM konstruieren:
SELTSAM
JA
x
x
x
OK
NEIN
Die Eingabe von x wird getestet, ob x bei der Eingabe von x stoppt. Im JA-Fall wird
in eine Endlosschleife gegangen, die nie anhält. Im NEIN-Fall hält SELTSAM mit der
Anzeige OK an.
Es folgt nun die Eingabe von SELTSAM (für sich selbst) mit der Frage: „Hält SELTSAM bei
der Eingabe von SELTSAM?“
1. Wenn JA, wird die JA-Anweisung von STOP angelaufen und SELTSAM gerät in eine
Endlosschleife, hält also nicht (Widerspruch!)
2. Wenn NEIN, so wird der NEIN-Ausgang von STOP angelaufen, und SELTSAM stoppt mit OK
(Widerspruch!)
Der Widerspruch folgt aus der Annahme, dass eine STOP-Maschine existiert, was
verneint werden muß.
Nicht entscheidbare (berechenbare) Probleme
Das Halteproblem ist ein Bsp. für ein „semantisches“ Problem von Algorithmen,
nämlich ein Problem der folgenden Art:
Kann man anhand eines Programmtextes entscheiden, ob die berechnete Funktion (Semantik) eine
bestimmte Eigenschaft hat.
Die Algorithmentheorie (Satz von Rice) hat dazu folgende Aussage gegeben:
Jede nicht triviale semantische Eigenschaft von Algorithmen ist nicht entscheidbar.
Nicht entscheidbar sind u.a. folgende Probleme:
1. Ist die Funktion überall definiert?
2. Berechnen 2 gegebene Algorithmen dieselbe Funktion?
3. Ist ein gegebener Algorithmus korrekt, d.h. berechnet er die gegebene (gewünschte) Funktion?
Das bedeutet nicht, dass man solche Fragen nicht im Einzelfall entscheiden könnte.
Es ist jedoch prinzipell unmöglich, eine allgemeine Methode hierfür zu finden, also
30
Algorithmen und Datenstrukturen
z.B. eine Algorithmus, der die Korrektheit aller Algorithmen nachweist (und damit
auch seine eigene).
1.2.6.2 Effizienz
Die zweite wichtige Eigenschaft eines Algorithmus ist seine Effizienz. Die wichtigsten
Maße für die Effizienz sind der zur Ausführung des Algorithmus benötigte
Speicherplatz und die benötigte Rechenzeit (Laufzeit):
1. Man kann die Laufzeit durch Implementierung des Algorithmus in einer
Programmiersprache (z.B. C++) auf einem konkreten Rechner für eine Menge
repräsentativer Eingaben messen.
Bsp.: Implementierung eines einfachen Sortieralgorithmus in C++ mit Messen der
CPU-Zeit 22.
#include <time.h>
// …
clock_t start, finish;
start = clock();
sort(…);
finish = clock();
cout << "sort hat " << double (finish – start) / CLOCKS_PER_SEC
<< " Sek. benoetigt\n";
// …
Solche experimentell ermittelten Meßergebnisse lassen sich nicht oder nur schwer
auf andere Implementierungen und andere Rechner übertragen.
2. Aus dieser Schwierigkeit bieten sich 2 Auswege an:
1. Man benutzt einen idealiserenden Modellrechner als Referenzmaschine und mißt die auf diesem
Rechner zur Ausführung des Algorithmus benötigte Zeit und benötigten Speicherplatz. Ein in der
Literatur 23 zu diesem Zweck häufig benutztes Maschinenmodell ist das der RAM (Random-AccessMaschine). Eine solche Maschine verfügt über einige Register und eine (abzählbar unendliche)
Menge einzeln addressierbarer Speicherzellen. Register und Speicherzellen können je eine (im
Prinzip) unbeschränkt große (ganze oder reelle) Zahl aufnehmen. Das Befehlsrepertoire für eine RAM
ähnelt einer einfachen, herkömmlichen Assemblersprache. Die Kostenmaße Speicherplatz und
Laufzeit enthalten dann folgende Bedeutung: Der von einem Algorithmus benötigte Speicherplatz ist
die Anzahl der zur Ausführung benötigten RAM-Speicherzellen. Die benötigte Zeit ist die Zahl der
ausgeführten RAM-Befehle.
2. Bestimmung einiger für die Effizienz des Algorithmus besonders charakteristischer Parameter 24.
Laufzeit und Speicherbedarf eines Algorithmus hängen in der Regel von der Größe der Eingabe ab 25.
Man unterscheidet zwischen dem Verhalten im besten Fall, dem Verhalten im Mittel (average case)
und dem Verhalten im schlechtesten Fall (worst case). In den meisten Fällen führt man eine worstcase Analyse für die Ausführung eines Algorithmus der Problengröße N durch. Dabei kommt es auf
den Speicherplatz nicht an, lediglich die Größenordnung der Laufzeit- und Speicherplatzfunktionen in
Abhängigkeit von der Größe der Eingabe N wird bestimmt. Zum Ausdruch dieser Größenordnung hat
sich eine besondere Notation eingebürgert: die O-Notation bzw. Big-O-Notation.
22
In Java steht zur Zeitmessung die Methode Methode currentTimeMillis() aus System zur Verfügung.
currentTimeMillis bestimmt die Anzahl der Millisekunden, die seit Mitternacht des 1.1.1970 vergangen sind.
23 Vgl. Aho, Hopcroft, Ullman: The Design and Analysis of Computer Algorithms, Addison-Wesley Publishing
Company
24 So ist es bspw. üblich, die Laufzeit eines Verfahrens zum Sortieren einer Folge von Schlüsseln durch die
Anzahl der dabei ausgeführten Vergleichsoperationen zwischen Schlüsseln und die Anzahl der ausgeführten
Bewegungen von den jeweiligen betroffenen Datensätzen zu messen.
25 die im Einheitskostenmaß oder im logarithmischen Kostenmaß gemessen wird
31
Algorithmen und Datenstrukturen
Laufzeit T(N): Die Laufzeit gibt exakt an, wieviel Schritte ein Algorithmus bei einer
Eingabelänge N benötigt. T(N) kann man im Rahmen sog. assymptotischer
Kostenmaße abschätzen. Für diese Abschätzung existieren die sog. Big-O-Notation
(bzw. Ω - und Θ -Notation):
Big-O-Notation: Ein Funktion f (N ) heißt von der Ordnung O ( g ( N )) , wenn 2 Konstante c0 und n0
existieren, so dass f ( N ) ≤ c o ⋅ g ( N ) für alle N > n0 .
Die Big-O-Notation liefert eine Obergrenze für die Wachstumsrate von Funktionen: f ∈ O(g ) , wenn f
höchstens so schnell wie g wächst. Man sagt dann: die Laufzeit eines Algorithmus "T(N) ist O(N)"
oder "T(N) ist ein O(N)".
Big- Ω -Notation: Ein Funktion f (N ) heißt von der Ordnung Ω( g ( N )) , wenn 2 Konstante c0 und n0
existieren, so dass f ( N ) ≥ c o ⋅ g ( N ) für alle N > n0 .
Die Big- Ω -Notation liefert eine Untergrenze für die Wachstumsrate von Funktionen: f ∈ Ω(g ) ,
wenn f mindestens so schnell wie g wächst.
θ -Notation: Das Laufzeitverhalten eines Algorithmus ist θ (N ) , falls O( N ) = Ω( N ) . Über θ (N )
kann das Laufzeitverhalten exakt beschrieben werden.
Damit lässt sich der Zeitbedarf eines Algorithmus darstellen als eine Zeitfunktion T (N ) 26 aus dem
Bereich der positiven reellen Zahlen: Ein Algorithmus hat die Komplexität O (g ) , wenn T ( N ) ∈ O ( g )
gilt.
Meistens erfolgt die Abschätzung hinsichtlich der oberen Schranken (Worst Case):
Groß-O-Notation.
T (N )
c1 g (n)
f ∈ Θ(g )
c 2 g ( n)
n0
N
Abb. 1.2-71: Assymptotische Kostenmaße
Zeitbedarf eines Algorithmus: Ist N die Problemgröße, A ein Algorithmus, dann hat
ein Algorithmus die Komplexität O(g ) , wenn für den Zeitbedarf von A T A (n) ∈ O ( g )
gilt. Wenn nicht explizit anders beschrieben, ist T A ( n) maximale Laufzeit für die
gegebene Faustregel in der O-Notation
26
falls nicht explizit anders beschrieben, ist T (N ) die maximale Laufzeit für die gegebene Problemgröße
N
32
Algorithmen und Datenstrukturen
Rechenregeln zur O-Notation.
⎧O( f ), falls g ∈ O( f )
Addition: f + g ∈ O(max( f , g )) = ⎨
⎩O( g ), falls f ∈ O( g )
Die Additionsregel dient zur Bestimmung der Komplexität bei Hintereinanderausführung der Programme
Multiplikation: f ⋅ g ∈ O( f ⋅ g )
Die Multiplikationsregel dient zur Bestimmung der Komplexität von ineinandergeschachtelten Schleifen
Linearität: f (n) = a ⋅ g (n) + b ∧ Ω(1) ⇒ f ∈ O( g )
1.2.7 Komplexität
Für die algorithmische Lösung eines gegebenen Problems ist es unerläßlich, daß der
gefundene Algorithmus das Problem korrekt löst. Darüber hinaus ist es natürlich
wünschenswert, daß er dies mit möglichst geringem Aufwand tut. Die Theorie der
Komplexität von Algorithmen beschäftigt sich damit, gegebene Algorithmen
hinsichtlich ihres Aufwands abzuschätzen und – darüber hinaus – für gegebene
Problemklassen anzugeben, mit welchem Mindestaufwand Probleme dieser Klasse
gelöst werden können.
Meistens geht es bei der Ananlyse der Komplexität von Algorithmen (bzw.
Problemklassen) darum, als Maß für den Aufwand eine Funktion anzugeben, wobei
f ( Ν) = a bedeutet: „ Bei einem Problem der Größe N ist der Aufwand a“. Die
Problemgröße „N“ bezeichnet dabei in der Regel ein grobes Maß für den Umfang
einer Eingabe, z.B. die Anzahl der Elemente in der Eingabeliste oder die Größe eines
bestimmten Eingabewertes. Der Aufwand „a“ ist in der Regel ein grobes Maß für die
Rechenzeit. Die Rechenzeit wird häufig dadurch abgeschätzt, daß man zählt, wie
häufig eine bestimmte Operation ausgeführt wird, z.B. Speicherzugriffe,
Multiplikationen, Additionen, Vergleiche, etc.
Bsp.: Wie oft wird die Wertzuweisung „x = x + 1“ in folgenden Anweisungen
ausgeführt?
1. x = x + 1; ............
..1-mal
2. for (i=1; i <= n; i++) x = x + 1;..
..n-mal
3. for (i=1; i <= n; i++)
for (j = 1; j <= n; j++)
x = x + 1;................................... ......... n2-mal
Die Aufwandfunktion läßt sich in den wenigsten Fällen exakt bestimmen.
Vorherrschende Analysemethoden sind:
- Abschätzungen des Aufwands im schlechtesten Fall
- Abschätzungen des Aufwands im Mittel
Selbst hierfür lassen sich im Allg. keine exakten Angaben machen. Man beschränkt
sich dann auf „ungefähres Rechnen in Größenordnungen“.
Bsp.: Gegeben: n ≥ 0 a1 , a 2 , a3 ,..., a n ∈ Z
Gesucht: Der Index i der (ersten) größten Zahl unter den ai (i=1,...,n)
Lösung:
max = 1;
for (i=2;i<=n;i++)
if (amax < ai) max = i
33
Algorithmen und Datenstrukturen
Wie oft wird die Anweisung „max = i“ im Mittel ausgeführt (abhängig von n)?
Die gesuchte mittlere Anzahl sei Tn. Offenbar gilt: 1 ≤ Tn ≤ n . „max = i“ wird genau dann
ausgeführt, wenn ai das größte der Elemente a1 , a 2 , a3 ,..., ai ist.
Angenommen wird Gleichverteilung: Für jedes i = 1, ... , n hat jedes der Elemente a1 , a 2 , a3 ,..., a n die
gleiche Chance das größte zu sein, d.h.: Bei N Durchläufen wird N/n-mal die Anweisung „max = i“
ausgeführt.
Daraus folgt für N ⋅ Tn (Aufwendungen bei N Durchläufen vom „max = i“):
N ⋅ Tn = N +
N N
N
1 1
1
+ + ... + = N (1 + + + ... + )
2 3
n
2 3
n
Dies ist Hn, die n-te harmonische Zahl. Für Hn ist keine geschlossene Formel bekannt, jedoch eine
ungefähre Abschätzung: Tn = H n ≈ ln n + γ 27. Interessant ist nur, daß Tn logarithmisch von n
abhängt. Man schreibt Tn ist „von der Ordnung logn“, die multiplikative und additive Konstante sowie
die Basis des Logarithmus bleiben unspezifiziert.
Diese sog. (Landau'sche) Big-O-Notation läßt sich mathematisch exakt definieren:
f ( n)
f (n) = O( g (n)) :⇔ ∃c, n0 ∀n ≥ n0 : f (n) ≤ c ⋅ g (n) , d.h.
ist für genügend große n
g ( n)
durch eine Konstante c beschränkt. „f“ wächst nicht stärker als „g“.
Diese Begriffsbildung wendet man bei der Analyse von Algorithmen an, um
Aufwandsfunktionen
durch
Eingabe
einer
einfachen
Vergleichsfunktion
abzuschätzen, so daß f (n) = O( g (n)) gilt, also das Wachstum von f durch das von g
beschränkt ist.
Gebräuchliche Vergleichsfunktionen sind:
O-Notation
O(1)
Aufwand
Konstanter Aufwand
O(log n)
Logarithmischer Aufwand
O(n)
Linearer Aufwand
Problemklasse
Einige Suchverfahren für Tabellen
(„Hashing“)
Allgemeine Suchverfahren für Tabellen
(Binäre Suche)
Sequentielle Suche, Suche in Texten,
syntaktische Analyse in Programmen
„schlaues Sortieren“, z.B. Quicksort
O(n ⋅ log n)
O(n 2 )
Quadratischer Aufwand
Einige dynamische Optimierungsverfahren,
z.B. optimale Suchbäume); „dummes
Sortieren“, z.B. Bubble-Sort
Multiplikationen Matrix mal Vektor
Exponentieller Aufwand
Viele Optimierungsprobleme, automatisches
Beweisen (im Prädikatenkalkül 1. Stufe)
Alle Permutationen
O(n k ) für k ≥ 0
O(2 n )
O(n!)
Zur Veranschaulichung des Wachstums konnen die folgende Tabellen betrachtet
werden:
f(N)
ldN
N
N ⋅ ldN
N2
27
N=2
1
2
2
4
Eulersche Konstante
24=16
4
16
64
256
25=256
8
256
1808
65536
γ = 0.57721566
34
210
10
1024
10240
1048576
220
20
1048576
20971520
≈ 1012
Algorithmen und Datenstrukturen
N3
2N
8
4
4096
65536
16777200
≈ 1077
≈ 109
≈ 10308
≈ 1018
≈ 10315653
Unter der Annahme „1 Schritt dauert 1 μs = 10 −6 s folgt für
N=
N
N2
N3
2N
3N
N!
10
10 μs
100 μs
1 ms
1 ms
59 ms
3,62 s
20
20 μs
400 μs
8 ms
1s
58 min
771 Jahre
30
30 μs
900 μs
27 ms
18 min
6.5 Jahre
1016 Jahre
40
40 μs
1.6 ms
64 ms
13 Tage
3855 Jahre
1032 Jahre
50
50 μs
2.5 ms
125 ms
36 Jahre
108 Jahre
1049 Jahre
60
60 μs
3.6 ms
216 ms
366 Jahre
1013 Jahre
1066 Jahre
Abb. 1.2-2: Polynomial- und Exponentialzeit
1.2.7.1 Laufzeitberechnungen
1.2.7.1.1 Analyse der Laufzeit
Die Laufzeit ist bestimmt durch die Anzahl der durchgeführten elementaren
Operationen (Grundrechenarten, Vergleiche, Feldzugriffe, Zugriffe auf die
Komponenten einer Struktur, etc.) Die Angabe der Laufzeit in Abhängigkeit von
konkreten Eingabewerten ist im Allg. nicht möglich oder sehr aufwendig. Daher
betrachtet man die Laufzeit häufig in Abhängigkeit von der Größe (dem Umfang) der
Eingabe.
Definition: T(n) = Anzahl der elementaren Operationen, die zur Bearbeitung einer
Eingabe der Größe n bearbeitet werden.
Eine Analyse der Laufzeit bezieht sich auf den besten, den schlechtesten und den
mittleren Fall:
-Tmin(n) = minimale Anzahl der Operationen, die durchgeführt werden, um eine Eingabe der Größe n
zu bearbeiten.
- Tmax(n) = maximale Anzahl der elementaren Operationen, die durchgeführt werden, um eine Eingabe
der Größe n zu bearbeiten.
Ist eine Wahrscheinlichkeitsverteilung der Eingabedaten gegeben, kann auch eine
mittlere Laufzeit Tmit(n) ermittelt werden.
Bsp.: Sequentielle Suche in Folgen
Gegeben ist eine Zahl n ≥ 0 , n Zahlen a1, a2, …, an (alle verschieden), eine Zahl b.
Gesucht ist der Index i = 1,2,…,n, so dass b == ai, falls ein Index existiert. Andernfalls ist i =
n+1.
Lösung: i = 1; while (i <= n && b != ai) i = i + 1;
Ergebnis hängt von der Eingabe ab, d.h. von n, a1, …, an und b
Aufwand der Suche:
1. erfolgreiche Suche (wenn b == ai): S = i Schritte
2. erfolglose Suche S = n+1 Schritte
Ziel: globalere Aussagen, die nur von einer einfachen Größe abhängen, z.B. von der Länge n der
Folge.
1) Wie groß ist S für gegebenes n im schlechtesten Fall?
- im schlechtesten Fall: b wird erst im letzten Schritt gefunden: b = an , S = n im schlechtesten Fall
2) Wie groß ist S für gegebenes n im Mittel
- im Mittel
- Wiederholte Anwendung mit verschiedenen Eingaben
- Annahme über Häufigkeit: Wie oft wird b an erster, zweiter, … letzter Stelle gefunden?
- Insgesamt für N-Suchvorgänge
35
Algorithmen und Datenstrukturen
N
N
N
N
N n(n + 1)
n +1
⋅ 1 + ⋅ 2 + ... + ⋅ n = (1 + 2 + ... + n ) = ⋅
=N
n
n
n
n
n
2
2
M
n +1
Schritte, also S =
im Mittel bei Gleichverteilung
- für eine Suche S =
N
2
M =
1.2.7.1.2 Asymptotische Analyse der Laufzeit („Big-O“)
(Analyse der Komplexität durch Angabe einer Funktion f : N → N als Maß für den Aufwand)
Definition: f (n) ist in der Größenordnung von g (n) „ f (n) = O( g (n)) “, falls Konstante
c und n0 existieren 28, so dass f (n) ≤ c ⋅ g (n) für n ≥ n0 .
f ( n)
ist für genügend große n durch eine Konstante c beschränkt, d.h. f wächst
g ( n)
nicht schneller als g.
Ziel der Charakterisierung T (n) = O( g (n)) ist es, eine möglichst einfache Funktion
g (n) zu finden. Bspw. ist T (n) = O(n) besser als T (n) = O(5n + 10) . Wünschenswert
ist auch die Charakterisierung der Laufzeit mit einer möglichst kleinen
Größenordnung.
Die O-Natation besteht in der Angabe einer asymptotischen oberen Schranke für die
Aufwandsfunktion (Wachstumsgeschwindigkeit bzw. Größenordnung)
Vorgehensweise bei der Analyse für Kontrollstrukturen
Die Algorithmen werden gemäß ihrer Kontrollstruktur von innen nach außen
analysiert. In der Laufzeit, die sich dann ergibt, werden anschließend die Konstanten
durch den Übergang zur O-Notation beseitigt.
Anweisungen: Anweisungen, die aus einer konstanten Anzahl von elementaren
Operationen bestehen, erhalten eine konstante Laufzeit.
Sequenz A1,A2,…,An. Werden für die einzelnen Anweisungen, die Laufzeiten T1,
T2,…,Tn ermittelt, dann ergibt sich für die Sequenz die Laufzeit T=T1+T2+…+Tn
Schleife, die genau n-mal durchlaufen wird, z.B. for-Schleife ohne break: for
(i=1;i<=n;i++) A; Wird für A die Laufzeit Ti ermittelt, dann ergibt sich als
n
Laufzeit für die for-Schleife T = ∑ Ti . Eigentlich müsste zu Ti noch eine Konstante C1
i =1
für i <=n und i++ und C2 für i = 1 hinzugezählt werden. Beim späteren Übergang
zur O-Notation würde die Konstanten jedoch wegfallen 29.
Fallunterscheidung (mit else-Teil): if (B) A1, else A2; Hier muß zwischen der Laufzeit
im besten und schlechtesten Fall unterschieden werden: Tmin=min(T1,T2),
Tmax=max(T1,T2), wobei T1 die Laufzeit für A1 und T2 die Laufzeit für A2 ist. Man geht
davon aus, dass die Bedingung B konstante Zeit benötigt und wegen des späteren
Übergangs zur O-Notation einfachheitshalber nicht mitgezählt werden muß.
Schleife mit k-maligen Durchläufen, wobei n1<=k<=n2. Diese tritt typischerweise bei
while-Schleifen auf. Es muß dann eine Analyse für den besten Fall (k=n1) und den
schlechtesten Fall (k=n2) durchgeführt werden.
Rekursion mit n → n − 1 : Es ergeben sich rekursive Gleichungen für die Laufzeiten
28
geeignetes n0 und c müssen angegeben werden, um zu zeigen, dass
29
vgl. Skriptum, 1.2.7
36
f (n) = O( g (n)) gilt
Algorithmen und Datenstrukturen
Bsp.: rekursive Fakultätsberechnung
int fak(int n)
{
if (n == 0) return 1;
else return n*fak(n-1);
}
Man erhält folgende Laufzeit: Tn = C 0
für n = 0
Tn = C1 + T (n − 1) ) für n > 0
Durch wiederholtes Einsetzen: T ( n) = C1 + C1 + ... + C1 + C 0 = O ( n)
1442443
n − mal
Rekursion mit Teile und Herrsche.
f ( x, n )
{ if (n == 1)
(1)
{ // Basisfall
/* löse Pr oblem direkt , Ergebnis sei loes * /
return loes;
}
else { // Teileschritt
/* teile x in 2Teilprobleme x1und x 2 jeweils derGröße n / 2 * / (2)
loe1 = f ( x1, n / 2);
loe2 = f ( x 2, n / 2);
// Herrscheschritt
/* Setze Loesung loes für x aus loe1und loe2 zusammen * /
(3)
return loes;
}
}
Für den Basisfall (1) wird eine konstante Anzahl C0 Operationen angesetzt. (2) und (3) benötigen
linearen Aufwand und damit C1 ⋅ n Operationen.
T ( n) = C 0
falls n = 1
T (n) = C1 ⋅ n + 2 ⋅ T (n / 2 ) 3031
Durch Einsetzen ergibt sich: T ( n) = C1 n + 2(C1 ⋅ n / 2 + 2T ( n / 4) ) = 2 ⋅ C1 ⋅ n + 4 ⋅ T (n / 4) )
Durch nochmaliges Einsetzen ergibt sich: T ( n) = 3 ⋅ C1 ⋅ n + 8 ⋅ T (n / 8) )
T (n) = log 2 (n) ⋅ C1 ⋅ n + 2 log n T (1)
log n
Mit 2
= n und T (1) = C 0 erhält man: T (n) = C1 ⋅ n ⋅ log 2 (n) + C 0 ⋅ n
30
n lässt sich ⎣log 2 n ⎦ -mal halbieren. Falls n eine Zweierpotenz ist (d.h. n = 2k), lässt sich n sogar exakt
log 2 n = k oft halbieren. n soll der Einfachheit halber hier eine Zweierpotenz sein.
31
Rekurrenzgleichung: Die Analyse rekursiver Algorithmen führt meistens auf eine sog. Rekurrenzgleichung
37
Algorithmen und Datenstrukturen
Lösung von Rekurrenzgleichungen
Eine Rekurrenzrelation (kurz Rekurrenz) ist eine Methode, eine Funktion durch einen
Ausdruck zu definieren, der die zu definierende Funktion selbst enthält, z.B.
Fibonacci-Zahlen 32.
Wie löst man Rekurrenzgleichungen? Es gibt 2 Verfahren: Substitutionsmethode
bzw. Mastertheorem.
Zur Lösung von Rekurrenzgleichungen haben sind 2 Verfahrenstechniken bekannt:
Substitutionsmethode bzw. Mastertheorem.
Lösung mit der Substitutionsmethode:
„Rate eine Lösung“ (z.B. über den Rekursionsbaum)
Beweise die Korrektheit der Lösung per Induktion
Lösung mit dem Mastertheorem:
Mit dem Mastertheorem kann man sehr einfach
Rekurrenzen
der
Form
⎛N⎞
T (n) = 2 ⋅ T ⎜ ⎟ + Θ(n) berechnen
⎝2⎠
Vollständige Induktion
Das Beweisverfahren der vollständigen Induktion ist ein Verfahren, mit dem
Aussagen über natürliche Zahlen bewiesen werden können. Neben Aussagen über
natürliche Zahlen können auch damit gut Aussagen bewiesen werden die
- rekursiv definierte Strukturen und
- abzählbare Strukturen
betreffen.
Grundidee: Eine Aussage ist gültig für alle natürlichen Zahlen n ∈ N , wenn man
nachweisen kann:
Die Aussage gilt für die erste natürliche Zahl n = 1 (Induktionsanfang)
Wenn die Aussage für eine natürliche Zahl n gilt, dann gilt sie auch für ihren Nachfolger n+1
(Induktionsschritt)
n
Einf. Bsp.: S (n) = ∑ i = 1 + 2 + 3 + ... + n =
i =1
1
⋅ n ⋅ (n + 1)
2
Beweis:
Induktionsanfang:
1
⋅ 1 ⋅ (1 + 1) = 1
2
Induktionsschritt:
Induktionsvoraussetzung:
1
⋅ k ⋅ (k + 1)
2
1
⋅ (k + 1) ⋅ (k +)2
2
k +1
k
1
1
1
i
=
i + (k + 1) ) = ⋅ k ⋅ (k + 1) + k + 1 = ⋅ (k 2 + k ) + (2k + 2)
∑
∑
2
2
2
i =1
i =1
1
1
= k 2 + k + 2k + 2 = (k + 2) ⋅ (k + 1)
2
2
Zu zeigen, dass gilt:
(
32
)
vgl. Skriptum: Algorithmen und Datenstrukturen SS09, 3.2.3
38
Algorithmen und Datenstrukturen
Asymptotische Abschätzung mit dem Master-Theorem
Das Mastertheorem hilft bei der Abschätzung der Rekurrenzen der Form
T (n) = a ⋅ T (n / b) + f (n) 33
Master-Theorem
- a ≥ 1 und b > 1 sind Konstanten. f (n) ist eine Finktion und T (n) ist über den nichtnegativen ganzen Zahlen durch folgende Rekurrenzgleichung definiert:
T (n) = a ⋅ T (n / b ) + f (n) . Interpretiere n / b so, dass entweder ⎣n / b⎦ oder ⎡n / b⎤
- Dann kann T (n) folgendermaßen asymptotisch abgeschätzt werden:
⎧ Θ(n logb a )
falls gilt : ∃ε > 0 mit f (n) = O(n logb a −ε )
⎪
T (n) = ⎨ Θ n logb a ⋅ log n
falls gilt : f (n) = Θ(n logb a )
⎪Θ( f (n))
falls : ∃ε > 0 mit f (n) = Ω(n logb a +ε ) ∧ ∃c < 1 : ∀n > n0 : a ⋅ f (n / b) ≤ c ⋅ f (n)
⎩
(
)
Anwendung des Theorems an einigen Beispielen
1. T (n) = 9 ⋅ T (n / 3) + n
a = 9, b = 3, f (n) = n
Da f (n) = O(n log3 9−ε ) mit ε = 1 gilt, kann Fall 1 des Master-Theorems angewendet
werden.
Somit gilt: T (n) = Θ n log3 9 = Θ n 2
2. T (n) = T (2n / 3) + 1
a = 1, b = 3 / 2
Da n logb a = n log3 / 2 1 = n 0 = 1 ist, gilt f (n) = Θ(n logb a ) = Θ(1) , und es kommt Fall 2 des
Master-Theorems zur Anwendung. Somit gilt: T (n) = Θ(n log3 / 2 1 log n) = Θ(log n)
3. T (n) = 3 ⋅ T (n / 4) + n ⋅ log n
a = 3, b = 4, f (n) = n log n
Es ist n logb a = n log 4 3 = O(n 0.379 ). Somit ist f (n) = Ω(n log 4 3+ε ) mit ε ≈ 0.2 . Weiterhin gilt
für hinreichend große n: a ⋅ f (n) = 3 ⋅ (n / 4) ⋅ log(n / 4) ≤ (3 / 4 )n log n . Fall 3 des MasterTheorems kann damit angewandt werden: T (n) = O( f (n)) = Θ(n ⋅ log n)
(
)
( )
Achtung! Es gibt Fälle, in denen die Struktur der Gleichung zu passen „scheint“, aber
kein Fall des Master-Theorems existiert, für den alle Bedingungen erfüllt sind.
33
Solche Rekurrenzen treten oft bei der Analyse sogenannter Divide-and-Conquer-Algorithmen auf.
39
Algorithmen und Datenstrukturen
1.2.7.2 O(logN)-Algorithmen
Gelingt es die Problemgröße in konstanter Zeit (O(1)) zu halbieren, dann zeigt der
zugehörige Algorithmus das Leistungsverhalten O(logN)). Nur spezielle Probleme
können dieses Leistungsverhalten erreichen.
Binäre Suche
Aufgabe: Gegeben ist eine Zahl X und eine sortiert vorliegenden Folge von
Ganzzahlen A0, A1, A2, ... , AN-1 im Arbeitsspeicher. Finde die Position i so, daß Ai=X
bzw. gib i=-1 zurück, wenn X nicht gefunden wurde.
Implementierung
public static int binaereSuche(Comparable a[], Comparable x)
{
/* 1 */ int links = 0, rechts = a.length - 1;
/* 2 */ while (links < rechts)
{
/* 3 */ int mitte = (links + rechts) / 2;
/* 4 */ if (a[mitte].compareTo(x) < 0)
/* 5 */
links = mitte + 1;
/* 6 */ else if (a[mitte].compareTo(x) > 0)
/* 7 */
rechts = mitte - 1;
else
/* 8 */
return mitte;
// Gefunden
}
/* 9 */ return -1;
// Nicht gefunden
}
Leistungsanalyse: Entscheidend für das Leistungsverhalten ist die Schleife (/* 2 */.
Sie beginnt mit (rechts – links) = N-1 und endet mit (rechts – links) =
-1. Bei jedem Schleifendurchgang muß (rechts – links) halbiert werden. Ist
bspw. (rechts – links) = 128, dann sind die maximalen Werte nach jeder
Iteration: 64, 32, 16, 8, 4, 2, 1, 0, -1. Die Laufzeit läßt sich demnach in der
Größenordnung O(logN) sehen.
Die binäre Suche ist eine Implementierung eines Algorithmus für eine Datenstruktur
(sequentiell gespeicherte Liste, Array). Zum Aufsuchen von Datenelementen wird
eine Zeit von O(logN) verbraucht. Alle anderen Operationen (z.B. Einfügen) nehmen
ein Leistungsverhalten von O(N) in Anspruch.
40
Algorithmen und Datenstrukturen
1.2.7.3 Berechnungsgrundlagen für rechnerische Komplexität
1.2.7.3.1 System-Effizienz und rechnerische Effizienz
Effiziente Algorithmen zeichnen sich aus durch
- schnelle Bearbeitungsfolgen (Systemeffizienz) auf unterschiedliche Rechnersystemen. Hier wird die
Laufzeit der diversen Suchalgorithmen auf dem Rechner (bzw. verschiedene Rechnersysteme)
ermittelt und miteinander verglichen. Die zeitliche Beanspruchung wird über die interne Systemuhr
gemessen und ist abhängig vom Rechnertyp
- Inanspruchnahme von möglichst wenig (Arbeits-) Speicher
- Optimierung wichtiger Leistungsmerkmale, z.B. die Anzahl der Vergleichsbedingungen, die Anzahl
der Iterationen, die Anzahl der Anweisungen (, die der Algorithmus benutzt). Die
Berechnungskriterien bestimmen die sog. rechnerische Komplexität in einer Datensammlung. Man
spricht auch von der rechnerischen Effizienz.
1.2.7.3.2 P- bzw. NP-Probleme
Von besonderem Interesse für die Praxis ist der Unterschied zwischen Problemen
mit polynomialer Laufzeit (d.h. T ( N ) = O( p ( N )) , p = Polynom in N) und solchen mit
nicht polynomialer Laufzeit. Probleme mit polynomialer Laufzeit nennt man leicht, alle
übrigen Probleme heißen hart (oder unzugänglich). Harte Probleme sind praktisch
nicht mehr (wohl aber theoretisch) algorithmisch lösbar, denn selbst für kleine
Eingaben benötigt ein derartiger Algorithmus Rechenzeit, die nicht mehr zumutbar ist
und leicht ein Menschenalter überschreitet 34.
Viele wichtige Problemlösungsverfahren liegen in dem Bereich zwischen leichten und
harten Problemen.. Man kann nicht zeigen, dass diese Probleme leicht sind, denn es
gibt für sie keinen Polynomialzeit-Algorithmus. Umgekehrt kann man auch nicht
sagen, dass es sich um harte Probleme handelt. Der Fakt, dass kein PolynomialzeitAlgorithmus gefunden wurde, schließt die Existenz eines solchem Algorithmus nicht
aus. Möglicherweise hat man sich bei der Suche danach bisher noch nicht klug
genug angestellt. Es wird dann nach seit Jahrzenten erfogloser Forschung
angenommen, dass es für diese Probleme keine polynomiellen Algorithmen gibt.
Man spricht in diesem Fall von der Klasse der sog. NP-vollständigen Probleme. 35
Es ist heute allgemeine Überzeugung, daß höchstens solche Algorithmen praktikabel
sind, deren Laufzeit durch ein Polynom in der Problemgröße beschränkt bleibt.
Algorithmen, die exponentielle Schrittzahl erfordern, sind schon für relativ kleine
Problemgrößen nicht mehr ausführbar.
34
35
vgl. Abb. 1.2-2
nichtdeterministisch polynomial
41
Algorithmen und Datenstrukturen
1.2.7.3.3 Grenzen der Berechenbarkeit
Generell kann man für Algorithmen folgende Grenzfälle bzgl. der Rechenbarkeit
beobachten:
- kombinatorische Explosion
Es gibt eine Reihe von klassischen Problemen, die immer wieder in der Mathematik oder der DVLiteratur auftauchen, weil sie knapp darzustellen und im Prinzip einfach zu verstehen sind. Manche
von ihnen sind nur von theoretischen Interesse, wie etwa die Türme von Hanoi.
Ein anderes klassisches Problem ist dagegen das Problem des Handlungsreisenden 36 (Travelling
Salesman Problem, TSP). Es besteht darin, daß ein Handlungsreisender eine Rundreise zwischen
einer Reihe von Städten machen soll, wobei er am Ende wieder am Abfahrtort ankommt. Dabei will er
den Aufwand (gefahrene Kilometer, gesamte Reisezeit, Eisenbahn- oder Flugkosten, je nach dem
jeweiligen Optimierungswunsch) minimieren. So zeigt bspw. die folgende Entfernungstabelle die zu
besuchenden Städte und die Kilometer zwischen ihnen:
München
Frankfurt
Heidelberg
Karlsruhe
Mannheim
Frankfurt
395
-
Heidelberg
333
95
-
Karlsruhe
287
143
54
-
Mannheim
347
88
21
68
-
Wiesbaden
427
32
103
150
92
Grundsätzlich (und bei wenigen Städten, wie in diesem Bsp., auch tatsächlich) ist die exakte Lösung
dieser Optimierungsaufgabe mit einem trivialen Suchalgorithmus zu erledigen. Man rechnet sich
einfach die Route der Gesamtstrecke aus und wählt die kürzeste.
Der benötigte Rechenaufwand steigt mit der Zahl N der zu besuchenden Städte sprunghaft an. Erhöht
man bspw. N von 5 auf 10, so verlängert sich die Rechenzeit etwa auf das „dreißigtausendfache“.
Dies nennt man kombinatorische Explosion, weil der Suchprozeß jede mögliche Kombination der
für das Problem relevanten Objekte einzeln durchprobieren muß. Der Aufwand steigt proportional zur
Fakultät (N!).
- exponentielle Explosion
Wie kann man die vollständige Prüfung aller möglichen Kombinationen und damit die kombinatorische
Explosion umgehen? Naheliegend für das TSP ist, nicht alle möglichen Routen zu berechnen und erst
dann die optimale zu suchen, sondern sich immer die bis jetzt beste zu merken und das Ausprobieren
einer neuen Wegkombination sofort abzubrechen, wenn bereits eine Teilstrecke zu größeren
Kilometerzahlen führt als das bisherige Optimum, z.B.:
Route 1
München
Karlsruhe
Heidelberg
Mannheim
Wiesbaden
Frankfurt
München
Streckensumme
0
287
341
362
454
486
881
Route 2
München
Wiesbaden
Karlsruhe
Frankfurt
Heidelberg
Streckensumme
0
429
722
865
960
Route 2 kann abgebrochen werden, weil die Teilstrecke der Route 2 (960) bereits länger ist als die
Gesamtstrecke der Route 1. Diese Verbesserung vermeidet die kombinatorische Explosion, ersetzt
36 Vorbild für viele Optimierungsaufgaben, wie sie vor allem im Operations Research immer wieder
vorkommen.
42
Algorithmen und Datenstrukturen
sie aber leider nur durch die etwas schwächere exponentielle Explosion. Die Rechenzeit nimmt
exponetiell, d.h. mit aN für irgendeinen problemspezifischen Wert zu. Im vorliegenden Fall ist a etwa
1.26.
- polynomiales Zeitverhalten
In der Regel ist polynomiales Zeitverhalten das beste, auf das man hoffen kann. Hiervon redet man,
wenn man die benötigte Rechenzeit durch ein Polynom T = a n N + ... + a 2 N + a1 N + a 0
n
2
ausgedrückt werden. „N“ ist bestimmt durch die zu suchenden problemspezifischen Werte, n
beschreibt den Exponenten. Da gegen das erste Glied mit der höchsten Potenz bei größeren
Objektzahlen alle anderen Terme des Ausdrucks vernachlässigt werden können, klassifiziert man das
polynomiale Zeitverhalten nach dieser höchsten Potenz. Man sagt, ein Verfahren zeigt polynomiales
Zeitverhalten O(Nn), falls die benötigte Rechenzeit mit der nten Potenz der Zahl der zu bearbeitenden
Objekte anwächst.
Die einzigen bekannten Lösungen des TSP, die in polynomialer Zeit ablaufen, verzichten darauf, unter
allen Umständen die beste Lösung zu finden, sondern geben sich mit einer recht guten Lösung
zufrieden. In der Fachsprache wird das so ausgedrückt, daß das TSP NP-vollständig sei. Das
bedeutet: In polynomialer Zeit kann nur eine nichtdeterministische Lösung berechnet werden, also
eine, die nicht immer deterministisch ein und dasselbe (optimale) Ergebnis findet.
Ein Verfahren, für das nicht garantiert werden kann, daß es in allen Fällen ein exaktes Resultat liefert,
wird heuristisch genannt. Eine naheliegende heuristische Lösung für das TSP ist der „Nächste
Nachbarn-Algorithmus“. Er beginnt die Route mit der Stadt, die am nächsten zum Ausgangsort liegt
und setzt sie immer mit derjenigen noch nicht besuchten Stadt fort, die wiederum die nächste zum
jeweiligen Aufenthaltsort ist. Da in jeder der N Städte alle (d.h. im Durchschnitt (N-1)/2) noch nicht
besuchte Orte nach dem nächsten benachbarten durchsucht werden müssen, ist der Teitaufwand für
2
das Durchsuchen proportional N ⋅ ( N − 1) / 2 , d.h. O(N ), und damit polynomial „in quadratischer
Zeit“
Die meisten Algorithmen für Datenstrukturen bewegen sich in einem schmalen Band
rechnerischer Komplexität. So ist ein Algorithmus von der Ordnung O(1) unabhängig
von der Anzahl der Datenelemente in der Datensammlung. Der Algorithmus läuft
unter konstanter Zeit ab, z.B.: Das Suchen des zuletzt in eine Schlange eingefügten
Elements bzw. die Suche des Topelements in einem Stapel.
Ein Algorithmus mit dem Verhalten O(N) ist linear. Zeitlich verhält er sich proportional
zur Größe der Liste.
Bsp.: Das Bestimmen des größten Elements in einer Liste. Es sind N Elemente zu
überprüfen, bevor das Ende der Liste erkannt wird.
Andere Algorithmen zeigen „logarithmisches Verhalten“. Ein solches Verhalten läßt
sich beobachten, falls Teildaten die Größe einer Liste auf die Hälfte, ein Viertel, ein
Achtel... reduzieren. Die Lösungssuche ist dann auf die Teilliste beschränkt.
Derartiges Verhalten findet sich bei der Behandlung binärer Bäume bzw. tritt auf
beim „binären Suchen“. Der Algorithmus für binäre Suche zeigt das Verhalten
O(log 2 N ) , Sortieralgorithmen wie der Quicksort und Heapsort besitzen eine
rechnerische Komplexität von O( N log 2 N ) . Einfache Sortierverfahren (z.B. „BubbleSort“) bestehen aus Algorithmen mit einer Komplexität von O( N 2 ) . Sie sind deshalb
auch nur für kleine Datenmengen brauchbar. Algorithmen mit kubischem Verhalten
O( N 3 ) sind bereits äußerst langsam (z.B. der Algorithmus von Warshall zur
Bearbeitung von Graphen). Ein Algorithmus mit einer Komplexität von O( 2 N ) zeigt
exponentielle Komplexität. Ein derartiger Algorithmus ist nur für kleine N auf einem
Rechner lauffähig.
43
Algorithmen und Datenstrukturen
1.3 Daten und Datenstrukturen
1.3.1 Datentyp
Ein Algorithmus verarbeitet Daten. Ein Datentyp soll gleichartige
zusammenfassen und die nötigen Basisoperationen zur Verfügung stellen.
Ein Datentyp ist durch 2 Angaben festgelegt:
Daten
1. Eine Menge von Daten (Werte)
2. Eine Menge von Operationen auf diesen Daten
Ein Datentyp 37ist demnach eine Zusammenfassung von Wertebereichen und
Operationen zu einer Einheit.
Eine passende Abstraktion für Datentypen sind Algebren. Eine Algebra ist eine
Wertemenge plus Operation auf diesen Werten. Ein typisches Beispiel für dieses
Konzept sind die natürlichen Zahlen mit den Operationen +, -, * ,%, etc.
Wertemengen eines Datentyps werden in der Informatik als Sorten bezeichnet. Die
Operationen eines Datentyps entsprechen Funktionen und werden durch
Algorithmen realisiert, In der Regel liegt eine mehrwertige Algebra vor, also eine
Algebra mit mehreren Sorten als Wertebereiche.
Bsp.:Natürliche Zahlen plus Wahrheitswerte mit den Operationen +, -, *, % auf
Zahlen, ¬, ∧, ∨ ,... auf Wahrheitswerten und =, <, > , ≤,... als Verbindung zwischen den
Sorten.
Signatur von Datentypen. Darunter versteht man eine Formularisierung der
Schnittstellenbeschreibung eines Datentyps. Sie besteht aus Angaben der Namen
der Sorten und der Operationen. Die Operationen werden neben dem Bezeichner der
Operation auch die Stelligkeit der Operanden und der Sorten der einzelnen
Parameter angegeben. Die Konstanten werden als 0-stellige Operationen realisert,
z.B 38.:
typ nat
sorts nat, bool
functions
0 -> nat
succ : nat -> nat
+ : nat x nat -> nat
<= : nat x nat -> bool
…..
37
38
Vgl. Skriptum Programmieren in Java WS 2005 / 2006: 1.3.4, 1.4.1.3, 2.3
Das Beispiel ist angelehnt an die algebraische Spezifikation von Datenstrukturen
44
Algorithmen und Datenstrukturen
1.3.2 Datenstruktur
Komplexe Datentypen, sog. Datenstrukturen, werden durch Kombination primitiver
Datentypen gebildet. Sie besitzen selbst spezifische Operationen.
Eine Datenstruktur ist ein Datentyp und dient zur Organisation von Daten zur
effizienten Unterstützung bestimmter Operationen.
Betrachtet wird ein Ausschnitt aus der realen Welt, z.B. die Hörer dieser Vorlesung
an einem bestimmten Tag:
Juergen
Josef
Liesel
Maria
........
Regensburg
.........
.........
.........
.........
Bad Hersfeld
.........
.........
.........
.........
13.11.70
........
........
........
........
Friedrich-. Ebertstr. 14
..........
..........
..........
..........
Diese Daten können sich zeitlich ändern, z.B. eine Woche später kann eine
veränderte Zusammensetzung der Zuhörerschaft vorliegen. Es ist aber deutlich
erkennbar: Die Modelldaten entsprechen einem zeitinvarianten Schema:
NAME
WOHNORT
GEBURTSORT
GEB.-DATUM
STRASSE
Diese Feststellung entspricht einem Abstraktionsprozeß und führt zur Datenstruktur.
Sie bestimmt den Rahmen (Schema) für die Beschreibung eines Datenbestandes.
Der Datenbestand ist dann eine Ansammlung von Datenelementen (Knoten), der
Knotentyp ist durch das Schema festgelegt.
Der Wert eines Knoten k ∈ K wird mit wk bezeichnet und ist ein n ≥ 0 -Tupel von
Zeichenfolgen; w i k bezeichnet die i-te Komponente des Knoten. Es gilt
wk = ( w1k , w2 k ,...., wn k )
Die Knotenwerte des vorstehenden Beispiels sind:
wk1 = (Jürgen____,Regensburg,Bad Hersfeld,....__,Ulmenweg__)
wk2 = (Josef_____,Straubing_,......______,....__,........__)
wk3 = (Liesel____,....._____,......______,....__,........__)
..........
wkn = (__________,__________,____________,______,__________)
Welche Operationen sind mit dieser Datenstruktur möglich?
Bei der vorliegenden Tabelle sind z.B. Zugriffsfunktionen zum Einfügen, Löschen
und Ändern eines Tabelleneintrages mögliche Operationen. Generell bestimmen
Datenstrukturen auch die Operationen, die mit diesen Strukturen ausgeführt werden
dürfen.
Zusammenhänge zwischen den Knoten eines Datenbestandes lassen sich mit Hilfe
von Relationen bequem darstellen. Den vorliegenden Datenbestand wird man aus
Verarbeitungsgründen bspw. nach einem bestimmten Merkmal anordnen (Ordnungsrelation). Dafür steht hier (im vorliegenden Beispiel) der Name der Studenten:
45
Algorithmen und Datenstrukturen
Josef
Juergen
Liesel
Abb. 1.3-1: Einfacher Zusammenhang zwischen Knoten eines Datenbestandes
Datenstrukturen bestehen also aus Knoten(den einzelnen Datenobjekten) und
Relationen (Verbindungen). Die Verbindungen bestimmen die Struktur des
Datenbestandes.
Bsp.:
1. An Bayerischen Fachhochschulen sind im Hauptstudium mindestens 2
allgemeinwissenschaftliche Wahlfächer zu absolvieren. Zwischen den einzelnen
Fächern, den Dozenten, die diese Fächer betreuen, und den Studenten bestehen
Verbindungen. Die Objektmengen der Studenten und die der Dozenten ist nach den
Namen sortiert (geordnet). Die Datenstruktur, aus der hervorgeht, welche
Vorlesungen die Studenten bei welchen Dozenten hören, ist:
46
Algorithmen und Datenstrukturen
STUDENT
FACH
DATEN
DOZENT
DATEN
DATEN
DATEN
DATEN
DATEN
DATEN
DATEN
DATEN
DATEN
geordnet
(z.B. nach Matrikelnummern)
geordnet
(z.B. nach Titel
im Vorlesungsverzeichnis)
geordnet
(z.B. nach Namen)
Abb. 1.3-3: Komplexer Zusammenhang zwischen den Knoten eines Datenbestands
2. Ein Gerät soll sich in folgender Form aus verschiedenen Teilen zusammensetzen:
Anfangszeiger Analyse
Anfangszeiger Vorrat
G1, 5
B2, 4
B1, 3
B3, 2
B4, 1
Abb. 1.3-4: Darstellung der Zusammensetzung eines Geräts
47
Algorithmen und Datenstrukturen
2 Relationen können hier unterschieden werden:
1) Beziehungsverhältnisse eines Knoten zu seinen unmittelbaren Nachfolgeknoten. Die Relation
Analyse beschreibt den Aufbau eines Gerätes
2) Die Relation Vorrat gibt die Knoten mit w2k <= 3 an.
Die Beschreibung eines Geräts erfordert in der Praxis eine weit komplexere
Datenstruktur (größere Knotenzahl, zusätzliche Relationen).
3. Eine Bibliotheksverwaltung 39 soll angeben, welches Buch welcher Student
entliehen hat. Es ist ausreichend, Bücher mit dem Namen des Verfassers (z.B.
„Stroustrup“) und die Entleiher mit ihrem Vornamen (z.B. „Juergen“, „Josef“)
anzugeben. Damit kann die Bibliotheksverwaltung Aussagen, z.B. „Josef hat
Stroustrup ausgeliehen“ oder „Juergen hat Goldberg zurückgegeben“ bzw. Fragen,
z.B. „welche Bücher hat Juergen ausgeliehen?“, realisieren. In die Bibliothek sind
Objekte aufzunehmen, die Bücher repäsentieren, z.B.:
Buch
„Stroustrup“
Weiterhin muß es Objekte geben, die Personen repräsentieren, z.B.:
Person
„Juergen“
Falls „Juergen“ Stroustrup“ ausleiht, ergibt sich folgende Darstellung:
Person
„Juergen“
Buch
„Stroustrup“
Abb. 1.3-5: Objekte und ihre Beziehung in der Bibliotheksverwaltung
Der Pfeil von „Stroustrup“ nach „Juergen“ zeigt: „Juergen“ ist der Entleiher von „Stroustrup“, der Pfeil
von „Juergen“ nach „Stroustrup“ besagt: „Stroustrup“ ist eines der von „Juergen“ entliehenen Bücher.
Für mehrere Personen kann sich folgende Darstellung ergeben:
39
pr13_3
48
Algorithmen und Datenstrukturen
Person
Person
„Juergen“
Person
„Josef“
Buch
Buch
„Stroustrup“
„Goldberg“
Buch
„Lippman“
Abb. 1.3-6: Objektverknüpfungen in der Bibliotheksverwaltung
Zur Verbindung der Klasse „Person“ bzw. „Buch“ wird eine Verbindungsstruktur benötigt:
Person
buecher =
Verbindungsstruktur
„Juergen“
Buch
„Stroustrup“
Abb. 1.3-7: Verbindungsstruktur zwischen den Objekttypen „Person“ und „Buch“
Ein bestimmtes Problem kann auf vielfätige Art in Rechnersystemen abgebildet
werden. So kann das vorliegende Problem über verkettete Listen im Arbeitsspeicher
oder auf Externspeicher (Dateien) realisiert werden.
Die vorliegenden Beispiele können folgendermaßen zusammengefaßt werden:
Die Verkörperung einer Datenstruktur wird durch das Paar D = (K,R) definiert.
K ist die Knotenmenge (Objektmenge) und R ist eine endliche Menge von binären
Relationen über K.
49
Algorithmen und Datenstrukturen
1.3.3 Relationen und Ordnungen
Relationen
Zusammenhänge zwischen den Knoten eines Datenbestandes lassen sich mit Hilfe
von Relationen bequem darstellen.
Eine Relation ist bspw. in folgender Form gegeben:
R = {(1,2),(1,3),(2,4),(2,5),(2,6),(3,5),(3,7),(5,7),(6,7)}
Diese Relation bezeichnet eine Menge geordneter Paare oder eine Produktmenge
M × N . Sind M und N also Mengen, dann nennt man jede Teilmenge M × N eine
zweistellige oder binäre Relation über M × N (oder nur über M , wenn M = N ist).
Jede binäre Relation auf einer Produktmenge kann durch einen Graphen dargestellt
werden, z.B.:
1
3
2
5
6
4
7
Abb. 1.3-8: Ein Graph zur Darstellung einer binären Relation
Bsp.: Gegeben ist S (eine Menge der Studenten) und V (eine Menge von
Vorlesungen). Die Beziehung ist: x ∈ S hört y ∈V . Diese Beziehung kann man durch
die Angabe aller Paare ( x , y ) beschreiben, für die gilt: Student x hört Vorlesung y .
Jedes dieser Paare ist Element des kartesischen Produkts S × V der Mengen S und
V.
Für Relationen sind aus der Mathematik folgende Erklärungen bekannt:
1. Vorgänger und Nachfolger
R ist eine Relation über der Menge M.
Gilt ( a , b ) ∈ R , dann sagt man: „a ist Vorgänger von b, b ist Nachfolger von a“.
Zweckmäßigerweise unterscheidet man in diesem Zusammenhang auch den
Definitions- und Bildbereich
Def(R) = { x | ( x , y ) ∈ R }
Bild(R) = { y | ( x , y ) ∈ R }
2. Inverse Relation (Umkehrrelation)
50
Algorithmen und Datenstrukturen
Relationen sind umkehrbar. Die Beziehungen zwischen 2 Grössen x und y können auch als
Beziehung zwischen y und x dargestellt werden, z.B.: Aus „x ist Vater von y“ wird durch Umkehrung „y
ist Sohn von x“.
Allgemein gilt:
R-1 = { (y,x) | ( x , y ) ∈ R }
3. Reflexive Relation
∀ ( x , x ) ∈ R (Für alle Elemente x aus M gilt, x steht in Relation zu x)
x ∈M
Beschreibt man bspw. die Relation "... ist Teiler von ..." für die Menge M = {2,4,6,12} in einem Grafen,
so erhält man:
12
4
6
2
Abb. 1.3-9: Die binäre Relation „... ist Teiler von ... “
Alle Pfeile, die von einer bestimmten Zahl ausgehen und wieder auf diese Zahl verweisen, sind
Kennzeichen einer reflexiven Relation ( in der Darstellung sind das Schleifen).
Eine Relation, die nicht reflexiv ist, ist antireflexiv oder irreflexiv.
4. Symmetrische Relation
Aus (( ( x , y ) ∈ R ) folgt auch (( ( y , x ) ∈ R ).
Das läßt sich auch so schreiben: Wenn ein geordnetes Paar (x,y) der Relation R angehört, dann
gehört auch das umgekehrte Paar (y,x) ebenfalls dieser Relation an.
Bsp.:
a) g ist parallel zu h
h ist parallel zu g
b) g ist senkrecht zu h
h ist senkrecht zu g
5. Asymmetrische Relation
Solche Relationen sind auch aus dem täglichen Leben bekannt. Es gilt bspw. „x ist Vater von y“ aber
nicht gleichzeitig „y ist Vater von x“.
Eine binäre Relation ist unter folgenden Bedingungen streng asymetrisch:
∀ ( x , y ) ∈ R → (( y , x ) ∉ R )
( x , y )∈R
51
Algorithmen und Datenstrukturen
Das läßt sich auch so ausdrücken: Gehört das geordnete Paar (x,y) zur Relation, so gehört das
entgegengesetzte Paar (y,x) nicht zur Relation.
Gilt für x <> y die vorstehende Relation und für x = y ∀( x , x ) ∈ R , so wird diese binäre Relation
"unstreng asymmetrisch" oder "antisymmetrisch" genannt.
6. Transitive Relation
Eine binäre Relation ist transitiv, wenn (( ( x , y ) ∈ R ) und (( ( y , z ) ∈ R ) ist, dann ist auch
(( ( x , z ) ∈ R ). x hat also y zur Folge und y hat z zur Folge. Somit hat x auch z zur Folge.
7. Äquivalenzrelation
Eine binäre Relation ist eine Äquivalenzrelation, wenn sie folgenden Eigenschaften entspricht:
- Reflexivität
- Transitivität
- Symmetrie
Bsp.: Die Beziehung "... ist ebenso gross wie ..." ist eine Äquivalenzrelation.
1. Wenn x1 ebenso groß ist wie x2, dann ist x2 ebenso groß wie x1. Die Relation ist symmetrisch.
2. x1 ist ebenso groß wie x1. Die Relation ist reflexiv.
3. Wenn x1 ebenso groß wie x2 und x2 ebenso gross ist wie x3, dann ist x1 ebenso groß wie x3. Die
Relation ist transitiv.
Klasseneinteilung
- Ist eine Äquivalenzrelation R in einer Menge M erklärt, so ist M in Klassen eingeteilt
- Jede Klasse enthält Elemente aus M, die untereinander äquivalent sind
- Die Einteilung in Klassen beruht auf Mengen M1, M2, ... , Mx, ... , My
Für die Teilmengen gilt:
Mx ∩ M y = 0
(2) M1 ∪ M 2 ∪....∪ M y = M
(1)
(3) Mx <> 0 (keine Teilmenge ist die leere Menge)
Bsp.: Klasseneinteilungen können sein:
- Die Menge der Studenten der FH Regensburg: Äquivalenzrelation "... ist im gleichen Semester
wie ..."
- Die Menge aller Einwohner einer Stadt in die Klassen der Einwohner, die in der-selben
Straße wohnen: Äquivalenzrelation ".. wohnt in der gleichen Strasse wie .."
Aufgabe
1. Welche der folgenden Relationen sind transitiv bzw. nicht transitiv?
1)
2)
3)
4)
5)
...
...
...
...
...
ist
ist
ist
ist
ist
der Teiler von ....
der Kamerad von ...
Bruder von ...
deckungsgleich mit ...
senkrecht zu ...
(transitiv)
(transitiv)
(transitiv)
(transitiv)
(nicht transitiv)
2. Welche der folgenden Relationen sind Aequivalenzrelationen?
1)
2)
3)
4)
...
...
...
...
gehört dem gleichen Sportverein an ...
hat denselben Geburtsort wie ...
wohnt in derselben Stadt wie ...
hat diesselbe Anzahl von Söhnen
52
Algorithmen und Datenstrukturen
Ordnungen
1. Halbordnung
Eine binäre Relation ist eine "Halbordnung", wenn sie folgende Eigenschaften
besitzt: "Reflexivität, Transitivität"
2. Strenge Ordnungsrelation
Eine binäre Relation ist eine "strenge Ordnungsrelation", wenn sie folgende Eigenschaft besitzt: "Transitivität, Asymmetrie"
3. Unstrenge Ordnungsrelation
Eine binäre Relation ist eine "unstrenge Ordnungsrelation", wenn sie folgende
Eigenschaften besitzt: Transitivität, unstrenge Asymmetrie
4. Totale Ordnungsrelation und partielle Ordnungsrelation
Tritt in der Ordnungsrelation x vor y auf, so verwendet man das Symbol < (x < y).
Vergleicht man die Abb. 1.2-9, so kann man für (1) schreiben: e < a < b < d und c < d
Das Element c kann man weder mit den Elementen e, a noch mit b in eine
gemeinsame Ordnung bringen. Daher bezeichnet man diese Ordnungsrelation als
partielle Ordnung (teilweise Ordnung).
Eine totale Ordnungsrelation enthält im Gegensatz dazu Abb. 1.2-9 in (2): e < a <
b<c<d
Kann also jedes Element hinsichtlich aller anderen Elemente geordnet werden, so ist
die Ordnungsrelation eine totale, andernfalls heißt sie partiell.
(1)
(2)
a
a
b
e
e
b
d
c
d
Abb.1.3-10: Totale und partielle Ordnungsrelationen
53
c
Algorithmen und Datenstrukturen
1.3.4 Klassifikation von Datenstrukturen
Eine Datenstruktur ist ein Datentyp mit folgenden Eigenschaften
1. Sie besteht aus mehreren Datenelementen. Diese können
-
atomare Datentypen oder
selbst Datenstrukturen sein
2. Sie setzt die Elemente durch eine Menge von Regeln (eine Struktur) in eine Beziehung (Relation).
Elementare Strukturrelationen
Menge
lineare Struktur (gerichtete 1:1-Relation)
Baum (hierarchisch)
(gerichtete 1 : n – Relation)
Graph (Netzwerk)
( n : m Relation)
Abb. 1.3.-11: Elementare Datenstrukturen
Eine Datenstruktur ist durch Anzahl und Eigenschaften der Relationen bestimmt.
Obwohl sehr viele Relationstypen denkbar sind, gibt es nur 4 fundamentale
Datenstrukturen 40, die immer wieder verwendet werden und auf die andere
Datenstrukturen zurückgeführt werden können. Den 4 Datenstrukturen ist
gemeinsam, daß sie nur binäre Relationen verwenden.
1.3.4.1 Lineare Ordnungsgruppen
Sie sind über eine (oder mehrere) totale Ordnung(en) definiert. Die bekanntesten
Verkörperungen der linearen Ordnung sind:
- (ein- oder mehrdimensionale) Felder (lineare Felder)
- Stapel
- Schlangen
- lineare Listen
Lineare Ordnungsgruppen können sequentiell (seqentiell gespeichert) bzw. verkettet
(verkettet gespeichert) angeordnet werden.
40
nach: Rembold, Ulrich (Hrsg.): "Einführung in die Informatik", München/Wien, 1987
54
Algorithmen und Datenstrukturen
1. Sammlungen mit direktem Zugriff
Ein „array“ (Reihung) ist eine Sammlung von Komponenten desselben Typs, auf den
direkt zugegriffen werden kann.
„array“-Kollektion
Daten
Eine Kollektion von Objekten desselben (einheitlichen) Typs
Operationen
Die Daten an jeder Stelle des „array“ können über einen
ganzzahligen Index erreicht werden.
Ein statisches Feld („array“) enthält eine feste Anzahl von Elementen und ist zur
Übersetzungszeit festgelegt. Ein dynamisches Feld benutzt Techniken zur
Speicherbeschaffung und kann während der Laufzeit an die Gegebenheiten
angepaßt werden. Ein „array“ kann zur Speicherung einer Liste herangezogen
werden. Allerdings können Elemente der Liste nur effizient am Ende des „array“
eingefügt
werden.
Anderenfalls
sind
für
spezielle
Einfügeoperationen
Verschiebungen der bereits vorliegenden Elemente (ab Einfügeposition) nötig.
Eine „array“-Klasse sollte Bereichsgrenzenüberwachung für Indexe und dynamische
Erweiterungsmöglichkeiten
erhalten.
Implementierungen
aktueller
Programmiersprachen
umfassen
Array-Klassen
mit
nützlichen
Bearbeitungsmethoden bzw. mit dynamischer Anpassung der Bereichsgrenzen zur
Laufzeit.
Eine Zeichenkette („character string“) ist ein spezialisierter „array“, dessen
Elemente aus Zeichen bestehen:
„character string“-Kollektion
Daten
Eine Zusammenstellung von Zeichen in bekannter Länge
Operationen
Sie umfassen Bestimmen der Länge der Zeichenkette, Kopieren
bzw. Verketten einer Zeichenkette auf eine bzw. mit einer
anderen Zeichenkette, Vergleich zweier Zeichenketten (für die
Musterverarbeitung), Ein-, Ausgabe von Zeichenketten
In einem „array“ kann ein Element über einen Index direkt angesprochen werden. In
vielen Anwendungen ist ein spezifisches Datenelement, der Schlüssel (key) für den
Zugriff auf einen Datensatz vorgesehen. Behälter, die Schlüssel und übrige
Datenelemente zusammen aufnehmen, sind Tabellen.
Ein Dictionary ist eine Menge von Elementen, die über einen Schlüssel identifiziert
werden. Das Paar aus Schlüsseln und zugeordnetem Wert heißt Assoziation, man
spricht auch von „assoziativen Arrays“. Derartige Tabellen ermöglichen den
Direktzugriff über Schlüssel so, wie in einem Array der Direktzugriff über den Index
erreicht wird, z.B.: Die Klasse Hashtable in Java
Der Verbund (record in Pascal, struct in C) ist in der Regel eine Zusammenfassung
von Datenbehältern unterschiedlichen Typs:
„record“-Kollektion
Daten
55
Algorithmen und Datenstrukturen
Ein Element mit einer Sammlung von Datenfeldern mit
möglicherweise unterschiedlichen Typen.
Operationen
Der Operator . ist für den Direktzugriff auf den Datenbehälter vorgesehen.
Eine Datei (file) ist eine extern eingerichtete Sammlung, die mit einer Datenstruktur
(„stream“) 41 genannt verknüpft wird.
„file“-Kollektion
Daten
Eine Folge von Bytes, die auf einem externen Gerät abgelegt
ist. Die Daten fließen wie ein Strom von und zum Gerät.
Operationen
Öffnen (open) der Datei, einlesen der Daten aus der Datei,
schreiben der Daten in die Datei, aufsuchen (seek) eines
bestimmten Punkts in der Datei (Direktzugriff) und schließen
(close) der Datei.
Bsp.: Die RandomAccessFile-Klasse in Java 42 dient zum Zugriff auf RandomAccess-Dateien.
2. Sammlungen mit sequentiellem Zugriff
Darunter versteht man lineare Listen (linear list) 43, die Daten in sequentieller
Anordnung aufnehmen:
„list“-Kollektion
Daten
Ein meist größere Objektsammlung von Daten gleichen Typs.
Operationen
Das Durchlaufen der Liste mit Zugriff auf die einzelnen
Elemente beginnt an einem Anfangspunkt, schreitet danach von
Element zu Element fort bis der gewünschte Ort erreicht ist.
Operationen zum Einfügen und Löschen verändern die Größe der
Liste.
Stapel (stack) und Schlangen (queue) 44 sind spezielle Versionen linearer Listen,
deren Zugriffsmöglichkeiten eingeschränkt sind.
„Stapel“-Kollektion
Daten
Eine Liste mit Elementen, auf die man nur über die Spitze
(„top“) zugreifen kann.
Operationen
Unterstützt werden „push“ und „pop“. „push“ fügt ein neues
Element an der Spitze der Liste hinzu, „pop“ entfernt ein
Element von der Spitze („top“) der Liste.
41
vgl. Programmieren in C++, Skriptum zur Vorlesung im SS 2006, 3.4.1, 3.4.2, 3.4.6
Implementiert das Interface DataInput und DataOutput mit eigenen Methoden.
43 Vgl. Programmieren in C++, Skriptum zur Vorlesung im SS 2006, 5.2
44 http://www.galileocomputing.de/openbook/javainsel7/javainsel_12_007.htm
Stand: März 2008 bzw. Programmieren in C++, Skriptum zur Vorlesung im SS 2006, 5.2
42
56
Algorithmen und Datenstrukturen
„Schlange“-Kollektion
Daten
Eine Sammlung von Elementen mit Zugriff am Anfang und Ende
der Liste.
Operationen
Ein Element wird am Ende der Liste hinzugefügt und am Anfang
der Liste entfernt.
Eine Schlange 45 ist besonders geeignet zur Verwaltung von „Wartelisten“ und kann
zur Simulation von Wartesystemen eingesetzt werden. Eine Schlange kann ihre
Elemente nach Prioritäten bzgl. der Verarbeitung geordnet haben (priority
queue) 46. Entfernt wird dann zuerst das Element, das die höchste Priorität besitzt.
„prioritätsgesteuerte Schlange“-Kollektion
Daten
Eine Sammlung von Elementen, von denen jedes Element eine
Priorität besitzt.
Operationen
Hinzufügen von Elementen zur Liste. Entfernt wird immer das
Element, das die höchste (oder niedrigste) Priorität besitzt.
1.3.4.2 Nichtlineare Kollektion
1.3.4.2.1 Hierarchische angeordnete Sammlung (Bäume)
Bäume sind im wesentlichen durch die Äquivalenzrelation bestimmt.
Bsp.: Gliederung zur Vorlesung Algorithmen und Datenstrukturen
Algorithmen und Datenstrukturen
Kapitel 1: Datenverarbeitung und
Datenorganisation
Abschnitt 1:
Ein einführendes Beispiel
Kapitel 2:
Suchverfahren
Abschnitt 2:
Begriffe
Abb.: Gliederung zur Vorlesung Datenorganisation
Die Verkörperung dieser Vorlesung ist das vorliegende Skriptum. Diese Skriptum {Seite 1, Seite 2, .....
, Seite n} teilt sich in einzelne Kapitel, diese wiederum gliedern sich in Abschnitte. Die folgenden
Äquivalenzrelationen definieren diesen Baum:
1. Seite i gehört zum gleichen Kapitel wie Seite j
2. Seite i gehört zum gleichen Abschnitt von Kapitel 1 wie Seite j
3. ........
45
Vgl. Programmieren in C++, Skriptum zur Vorlesung im SS 2006, 5.2
http://java.sun.com/j2se/1.5.0/docs/api/java/util/PriorityQueue.html
Stand März 2008
46
57
Algorithmen und Datenstrukturen
Die Definitionen eines Baums mit Hilfe von Äquivalenzrelationen regelt ausschließlich "Vorgänger/Nachfolger" - Beziehungen (in vertikaler Richtung) zwischen
den Knoten eines Baums. Ein Baum ist auch hinsichtlich der Baumknoten in der
horizontalen Ebene geordnet, wenn zur Definition des Baums neben der
Äquivalenzrelation auch eine partielle Ordnung (Knoten Ki kommt vor Knoten Kj, z.B.
Kapitel1 kommt vor Kapitel 2) eingeführt wird.
Eine hierarchisch angeordnete Sammlung von Datenbehältern ist gewöhnlich ein
Baum mit einem Ursprungs- bzw. Ausgangsknoten, der „Wurzel“ genannt wird. Von
besonderer Bedeutung ist eine Baumstruktur, in der jeder Baumknoten zwei Zeiger
auf nachfolgende Knoten aufnehmen kann. Diese Binärbaum-Struktur kann mit Hilfe
einer speziellen angeordneten Folge der Baumknoten zu einem binären Suchbaum
erweitert werden. Binäre Suchbäume bilden die Ausgangsbasis für das Speichern
großer Datenmengen.
„Baum“-Kollektion
Daten
Eine hierarchisch angeordnete Ansammlung von Knotenelementen,
die von einem Wurzelknoten abgeleitet sind. Jeder Knoten hält
Zeiger
zu
Nachfolgeknoten,
die
wiederum
Wurzeln
von
Teilbäumen sein können.
Operationen
Die Baumstruktur erlaubt das Hinzufügen und Löschen von
Knoten. Obwohl die Baumstruktur nichtlinear ist, ermöglichen
Algorithmen zum Ansteuern der Baumknoten den Zugriff auf die
in den Knoten gespeicherten Informationen.
Ein „heap“ ist eine spezielle Version, in der das kleinste bzw. größte Element den
Wurzelknoten besetzt. Operationen zum Löschen entfernen den Wurzelknoten, dabei
wird, wie auch beim Einfügen, der Baum reorganisiert.
Basis der Heap-Darstellung ist ein „array“ (Feldbaum), dessen Komponenten eine
Binärbaumstruktur überlagert ist. In der folgenden Darstellung ist eine derartige
Interpretation durch Markierung der Knotenelemente eines Binärbaums mit
Indexpositionen sichtbar:
[1]
wk1
[2]
[3]
wk2
wk3
[4]
[5]
wk4
[8]
[0]
wk5
[9]
[10]
wk9
wk8
[6]
wk6
[11]
wk10
[7]
[12]
wk11
wk7
[13]
wk12
wk13
[14]
wk14
[15]
wk15
wk1
wk2
wk3
wk4
wk5
wk6
wk7
wk8
wk9
wk10 wk11 wk12 wk13 wk14 wk15
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10] [11] [12] [13] [14] [15]
Abb. 1.3-10: Darstellung eines Feldbaums
58
Algorithmen und Datenstrukturen
Liegen die Knoten auf den hier in Klammern angegebenen Positionen, so kann man
von jeder Position I mit
Pl = 2 * I
Pr = 2 * I + 1
Pv = I div 2
auf die Position des linken (Pl) und des rechten (Pr) Nachfolgers und des Vorgängers
(Pv) gelangen. Ein binärer Baum kann somit in einem eindimensionalen Feld (array)
gespeichert werden, falls folgende Relationen eingehalten werden:
X[I] <= X[2*I]
X[I] <= X[2*I+1]
X[1] = min(X[I] .. X[N])
Anstatt auf das kleinste kann auch auf das größte Element Bezug genommen
werden
Aufbau des Heap: Ausgangspunkt ist ein „array“ mit bspw. folgenden Daten:
X[1]
40
X[2]
10
X[3]
30
X[4]
......
, der folgendermaßen interpretiert wird:
X[1]
40
X[2]
X[3]
10
30
Abb. 1.3-11: Interpretation von Feldinhalten im Rahmen eines binären Baums
Durch eine neue Anordnung der Daten in den Feldkomponenten entsteht ein „heap“:
X[1]
10
X[2]
X[3]
40
30
Abb. 1.3-12:
59
Algorithmen und Datenstrukturen
Falls ein neues Element eingefügt wird, dann wird nach dem Ordnen gemäß der
„heap“-Bedingung erreicht:
X[1]
10
X[2]
X[3]
40
30
X[4]
15
X[1]
10
X[2]
X[3]
15
30
X[4]
40
Abb.1.3-13: Das Einbringen eines Elements in einen Heap
Beim Löschen wird das Wurzelelement an der 1. Position entfernt. Das letzte
Element im „heap“ wird dazu benutzt, das Vakuum zu füllen. Anschließend wird
reorganisiert:
X[1]
10
X[2]
X[3]
15
30
X[4]
40
60
Algorithmen und Datenstrukturen
X[1]
40
X[2]
X[3]
15
30
X[1]
15
X[2]
X[3]
40
30
Abb. 1.3-14: Das Löschen eines Elements im Heap
Implementierung des Heap in C++ 47.
//
//
//
//
//
//
//
//
//
//
//
//
//
BinaryHeap class
Erzeugung mit optionaler Angabe zur Kapazitaet (Defaultwert: 100)
******************PUBLIC OPERATIONS*********************
void insert( x )
--> Insert x
deleteMin( minItem )
--> Remove (and optionally return) smallest item
Comparable findMin( ) --> Return smallest item
bool isEmpty( )
--> Return true if empty; else false
void makeEmpty( )
--> Remove all items
******************ERRORS********************************
Throws UnderflowException as warranted
Anwendung
Der Binary Heap kann zum Sortieren 48 herangezogen werden.
Ein Heap kann aber auch in Simulationsprogrammen und vor allem zur
Implementierung von Priority Queues verwendet werden. Hier wird vielfach der
einfache Heap durch komplexere Datenstrukturen ( Binomial Heap, Fibonacci Heap)
ersetzt.
47
48
Vgl. pr13_421
http://fbim.fh-regensburg.de/~saj39122/AD/projekte/heapsort/html/index.html
61
Algorithmen und Datenstrukturen
1.3.4.2.2 Gruppenkollektionen
Menge (Set)
Eine Gruppe umfaßt nichtlineare Kollektionen ohne jegliche Ordnungsbeziehung.
Eine Menge (set) einheitlicher Elemente ist. bspw. eine Gruppe. Operationen auf die
Kollektion „Set“ umfassen Vereinigung, Differenz und Schnittmengenbildung.
„Set“-Kollektion
Daten
Eine ungeordnete Ansammlung von Objekten ohne Ordnung
Operationen
Die binäre Operationen über Mitgliedschaft, Vereinigung,
Schnittmenge und Differenz bearbeiten die Strukturart „Set“.
Weiter Operationen testen auf Teilmengenbeziehungen.
Graph
Ein Graph (graph) ist eine Datenstruktur, die durch eine Menge Knoten und eine
Menge Kanten, die die Knoten verbinden, definiert ist.
In seiner einfachsten Form besteht eine Verkörperung dieser Datenstruktur aus einer
Knotenmenge K (Objektmenge) und einer festen aber beliebigen Relation R über
dieser Menge 49. Die folgende Darstellung zeigt einen Netzplan zur Ermittlung des
kritischen Wegs:
Die einzelnen Knoten des Graphen sind Anfangs- und Endereignispunkte der Tätigkeiten, die an den
Kanten angegeben sind. Die Kanten (Pfeile) beschreiben die Vorgangsdauer und sind Abbildungen
binärer Relationen. Zwischen den Knoten liegt eine partielle Ordnungsrelation.
Bestelle A
50 Tage
Baue B
1
Teste B
4
20 Tage
Korrigiere Fehler
2
25 Tage
3
15 Tage
Handbucherstellung
60 Tage
Abb. 1.3-11: Ein Graph der Netzplantechnik
„graph“-Kollektion
Daten
Eine Menge von Knoten und eine Menge verbindender Kanten.
Operationen
Der Graph kann Knoten hinzufügen bzw. löschen. Bestimmte
Algorithmen starten an einem gegebenen Knoten und finden alle
von diesem Knoten aus erreichbaren Knoten. Andere Algorithmen
erreichen jeden Knoten im Graphen über „Tiefen“ bzw.
„Breiten“ - Suche.
49
vgl. 1.2.2.2, Abb. 1.2-7
62
Algorithmen und Datenstrukturen
Ein Netzwerk ist spezielle Form eines Graphen, in dem jede Kante ein bestimmtes
Gewicht trägt. Den Gewichten können Kosten, Entfernungen etc. zugeordnet sein.
1.3.4.3 Dateien und Datenbanken
Datei
Damit ist eine Datenstruktur bestimmt, bei der Verbindungen zwischen den
Datenobjekten durch beliebige, binäre Relationen beschrieben werden. Die Anzahl
der Relationen ist somit im Gegensatz zur Datenstruktur Graph nicht auf eine
beschränkt. Verkörperungen solcher assoziativer Datenstrukturen sind vor allem
Dateien. In der Praxis wird statt mehrere binärer Relationen eine n-stellige Relation
(Datensatz) gespeichert. Eine Datei ist dann eine Sammlung von Datensätzen
gleichen Typs.
Bsp.: Studenten-Datei
Sie faßt die relevanten Daten der Studenten 50 nach einem ganz bestimmten Schema zusammen. Ein
solches Schema beschreibt einen Datensatz. Alle Datensätze, die nach diesem Schema aufgestellt
werden, ergeben die Studenten-Datei. Es sind binäre Relationen (Student - Wohnort, Student Geburtsort, ...), die aus Speicheraufwandsgründen zu einer n-stelligen Relation (bezogen auf eine
Datei) zusammengefaßt werden.
Datenbanken
Eine Datenbank ist die Sammlung von verschiedenen Datensatz-Typen.
Fachbereich
1
betreut
M
Student
Abb. 1.3-14: „ER“-Diagramm zur Darstellung der Beziehung „Fachbereich-Student“
Auch hier zeigt sich: Knoten bzw. Knotentypen und ihre Beziehungen bzw.
Beziehungstypen stehen im Mittelpunkt der Datenbank-Beschreibung. Statt Knoten
spricht man hier von Entitäten (bzw. Entitätstypen) und Beziehungen werden
Relationen genannt. Dies ist dann Basis für den Entity-Relationship (ER) -Ansatz
von P.S. Chen. Zur Beschreibung der Entitätstypen und ihrer Beziehungen benutzt
der ER-Ansatz in einem ER-Diagramm rechteckige Kanten bzw. Rauten:
Die als „1“ und „M“ an den Kanten aufgeschriebenen Zahlen zeigen: Ein Fachbereich betreut mehrere (viele) Studenten. Solche Beziehungen können vom Typ
1:M, 1:1, M:N sein. Es ist auch die Bezugnahme auf gleiche Entitätstypen möglich,
z.B.:
50
vgl. 1.2.2.1
63
Algorithmen und Datenstrukturen
Person
1
1
Heirat
Abb.: 1.3-15: Bezugnahme auf den gleiche Entitätstyp „Person“
Zur Verwaltung großer Datenbestände nutzen Datenbanken
-
die Speicherung von Daten in Tabellenform
mit effizienten Zugriffsalgorithmen nach wahlfreien Kriterien
und SQL als Datenzugriffssprache
Datenbanken zeigen den praktischen Einsatz vieler vorgestellten Methoden,
Algorithmen und Datenstrukturen, z.B. die Speicherung in Tabellen hinsichtlich
-
-
der Dateiorganisationsform
o sortiert nach Schlüssel
o B-Baum nach Schlüssel
o Hash-Tabelle nach Schlüssel
Zugriffsunterstützung nach Index
o Primärindexe verweisen auf Hauptdatei
o B –Bäume für Nichtschlüsselattribute zur Bescheunigung des Zugriffs
(Sekundärindex)
Die folgende Darstellung einer Datenbank in einem ER-Diagramm
Abt_ID
Bezeichnung
Job_ID
Titel
Abteilung
Gehalt
Job
Abt-Ang
Job-Ang
Qualifikation
Angestellte
Ang_ID
Name
GebJahr
Abb.: ER-Diagramm zur Datenbank Personalverwaltung
führt zum folgenden Schemaentwurf einer relationalen Datenbank
- Abteilung(Abt_ID,Bezeichnung)
- Angestellte(Ang_ID,Name,Gebjahr,Abt_ID,Job_ID)
- Job(Job_ID,Titel,Gehalt)
- Qualifikation(Ang_ID,Job_ID)
und resultiert in folgender relationalen Datenbank für das Personalwesen:
64
Algorithmen und Datenstrukturen
ABTEILUNG
ABT_ID
KO
OD
PA
RZ
VT
BEZEICHNUNG
Konstruktion
Organisation und Datenverarbeitung
Personalabteilung
Rechenzentrum
Vertrieb
ANG_ID
A1
A2
A3
A4
A5
A6
A7
A8
A9
A10
A11
A12
A13
A14
NAME
Fritz
Tom
Werner
Gerd
Emil
Uwe
Eva
Rita
Ute
Willi
Erna
Anton
Josef
Maria
ANGESTELLTE
GEBJAHR
2.1.1950
2.3.1951
23.4.1948
3.11.1950
2.3.1960
3.4.1952
17.11.1955
02.12.1957
08.09.1962
7.7.1956
13.10.1966
5.7.1948
2.8.1952
17.09.1964
ABT_ID
OD
KO
OD
VT
PA
RZ
KO
KO
OD
KO
OD
OD
KO
PA
JOB_ID
SY
IN
PR
KA
PR
OP
TA
TA
SY
IN
KA
SY
SY
KA
JOB
JOB_ID
KA
TA
SY
PR
OP
TITEL
Kaufm. Angestellter
Techn. Angestellter
Systemplaner
Programmierer
Operateur
GEHALT
3000,00 DM
3000,00 DM
6000,00 DM
5000,00 DM
3500,00 DM
QUALIFIKATION
ANG_ID
A1
A1
A1
A2
A2
A3
A4
A5
A6
A7
A8
A9
A10
A11
A12
A13
A14
JOB_ID
SY
PR
OP
IN
SY
PR
KA
PR
OP
TA
IN
SY
IN
KA
SY
IN
KA
Abb. 1.3-17: Tabellen zur relationalen Datenbank
65
Algorithmen und Datenstrukturen
Die relationale Datenbank besteht, wie die vorliegende Darstellung zeigt aus einer
Datenstruktur, die Dateien (Tabellen) vernetzt. Die Verbindung besorgen Referenzen
(Fremdschlüssel).
1.3.5 Definitionsmethoden für Datenstrukturen
1.3.5.1 Der abstrakte Datentyp
Anforderungen an die Definition eines Datentyps 51
-
Die Spezifikation eines Datentyps sollte unabhängig von seiner Implementierung sein. Daurch
kann die Spezifikation für unterschiedliche Implementierungen verwendet werden.
Reduzierung der von außen sichtbaren (zugänglichen) Aspekte auf der Schnittstelle des
Datentyps. Dadurch kann die Implementierung später verwendet werden, ohne dass
Programmteile, die den Datentyp benutzen, angepasst werden.
Aus diesen Anforderungen ergeben sich zwei Prinzipien:
-
Kapselung (ecucapsultaion): Alle Zugriffe gehen immer nur über die Schnittstelle des
Datentyps
Geheimnisprinzip (programming by contract). Die interne Realisierung des Datentyps bleibt
dem Benutzer verborgen
Ein Datentyp, dem nur Spezifikation und Eigenschaften (bspw. in Form von Regeln
oder Gesetzmäßigkeiten bekannt sind, heißt abstrakt. Man abstrahiert hier von der
konkreten Implementierung.
Ein abstrakter Datentyp wird als ADT bezeichnet.
Die Definition einer Datenstruktur ist bestimmt durch Daten (Datenfelder, Aufbau,
Wertebereiche) und die für die Daten gültigen Rechenvorschriften (Algorithmen,
Operationen). Datenfelder und Algorithmen bilden einen Typ, den abstrakten
Datentyp (ADT).
Eine Spezifikation eines ADT besteht aus:
-
Angabe der Signatur. Sie legt die Namen der Typen sowie die Funktionstypen fest und bildet
die Schnittstelle eines ADT.
Mengen und Funktionen, die zur Signatur passen, heißen Algebren
Gleichungen dienen als Axiome zur Einschränkung möglicher Algebren als Modell.
Zusätzlich erfolgt evtl. ein Import anderer Spezifikationen.
Daten und Algorithmen als Einheit zu sehen, war bereits schon vor 1980 bekannt.
Da damals noch keine allgemein einsetzbare Implementierung vorlag, hat man
Methoden zur Deklaration von ADT 52 bereitgestellt. Damit sollte dem Programmierer
wenigstens durch die Spezifikation die Einheit von Daten und zugehörigen
Operationen vermittelt werden.
51
52
vgl. 1.3.1
vgl. Guttag, John: "Abstract Data Types and the Development of Data Structures", CACM, June 1977
66
Algorithmen und Datenstrukturen
1.3.5.2 Die axiomatische Methode
Die axiomatische Methode beschreibt abstrakte (Daten-)Typen über die Angabe
einer Menge von Operationen und deren Eigenschaften, die in der Form von
Axiomen präzisiert werden. Problematisch ist jedoch: Die Axiomenmenge ist so
anzugeben, daß Widerspruchsfreiheit, Vollständigkeit und möglichst Eindeutigkeit
erzielt wird.
Algebraische Spezifikation
Eine spezielle axiomatische Methode ist die algebraische Spezifikation von Datenstrukturen. Sie soll hier stellvertretend für axiomatische Definitionsmethoden an
einem Beispiel vorgestellt werden.
1. Bsp.: Die algebraische Spezifikation des (ADT) Schlange
Konventionell würde die Datenstruktur Schlange so definiert werden: Eine Schlange
ist ein lineares Feld, bei dem nur am Anfang Knoten entfernt und nur am Ende
Knoten hinzugefügt werden können. Die Definition ist ungenau. Operationen sollten
mathematisch exakt als Funktionen und Beziehungen der Operationen als
Gleichungen angegeben sein. Erst dann ist die Prüfung auf Konsistenz und der
Nachweis der korrekten Implementierung möglich. Die algebraische Spezifikation
bestimmt den ADT Schlange deshalb folgendermaßen:
ADT Schlange
Typen
Schlange<T>, boolean
Funktionen (Protokoll)
NeueSchlange
FuegeHinzu :
Vorn
:
Entferne
:
Leer
:
→ Schlange<T>
T,Schlange<T> → Schlange<T>
→ T
Schlange<T>
Schlange<T> → Schlange<T>
→ boolean
Schlange<T>
Axiome
Für alle t : T bzw. s : Schlange<T> gilt:
1.
2.
3.
4.
5.
6.
Leer(NeueSchlange) = wahr
Leer(FuegeHinzu(t,s)) = falsch
Vorn(NeueSchlange) = Fehler
Vorn(FuegeHinzu(t,s)) = Wenn Leer(s), dann t; andernfalls Vorn(s)
Entferne(NeueSchlange) = Fehler
Entferne(FuegeHinzu(t,s)) = Wenn Leer(s), dann NeueSchlange; andernfalls
FuegeHinzu(t,Entferne(s))
Der Abschnitt Typen zeigt die Datentypen der Spezifikation. Der 1. Typ ist der
spezifizierte ADT. Von anderen Typen wird angenommen, daß sie an anderer Stelle
definiert sind. Der Typ „Schlange<T>“ wird als generischer ADT bezeichnet, da er
"übliche Schlangeneigenschaften" bereitstellt. Eigentliche Schlangentypen erhält
man durch die Vereinbarung eines Exemplars des ADT, z.B.: Schlange<integer>
Der Abschnitt Funktionen zeigt die auf Exemplare des Typs anwendbaren
Funktionen: f : D1 , D2 ,...., Dn → D . Einer der Datentypen D1 , D2 ,...., Dn oder D muß
der spezifizierte ADT sein.
67
Algorithmen und Datenstrukturen
Die Funktionen können eingeteilt werden in:
- Konstruktoren (constructor functions)
(Der ADT erscheint nur auf der rechten Seite des Pfeils.) Sie liefern neue Elemente (Instanzen) des
ADT.
- Zugriffsfunktionen (accessor functions)
(Der ADT erscheint nur auf der linken Seite des Pfeils.) Sie liefern Eigenschaften von existierenden
Elementen des Typs (vgl. Die Funktion: Leer)
- Umsetzungsfunktionen (transformer functions)
(Der ADT erscheint links und rechts vom Pfeil.) Sie bilden neue Elemente des ADT aus
bestehenden Elementen und (möglicherweise) anderen Argumenten (vgl. FuegeHinzu,
Entferne).
Der Abschnitt Axiome beschreibt die dynamischen Eigenschaften des ADT.
2. Bsp.: Die „algebraische Spezifikation“ des ADT Stapel
ADT Stapel<T>, integer, boolean
1. Funktionen (Operationen, Protokoll)
NeuerStapel
PUSH
POP
Top
Stapeltiefe
Leer
:
:
:
:
:
T,Stapel<T>
Stapel<T>
Stapel<T>
Stapel<T>
Stapel<T>
→
→
→
→
→
→
Stapel<T>
Stapel<T>
Stapel<T>
T
integer
boolean
2. Axiome
Für alle t:T und s:Stapel<T> gilt:
(POP(PUSH(t,s)) = s
Top(PUSH(t,s)) = t
Stapeltiefe(NeuerStapel) = 0
Stapeltiefe(PUSH(i,s)) = Stapeltiefe + 1
Leer(NeuerStapel) = wahr
¬ Leer(PUSH(t,s) = wahr
3. Restriktionen (conditional axioms)
Wenn Stapeltiefe(s) = 0, dann führt POP(s) auf einen Fehler
Wenn Stapeltiefe(s) = 0, dann ist Top(s) undefiniert
Wenn Leer(s) = wahr, dann ist Stapeltiefe(s) Null.
Wenn Stapeltiefe(s) = 0, dann ist Leer(s) wahr.
Für viele Programmierer ist eine solche Spezifikationsmethode zu abstrakt. Die
Angabe von Axiomen, die widerspruchsfrei und vollständig ist, ist nicht möglich bzw.
nicht nachvollziehbar.
3. Bsp.: Die algebraische Spezifikation für einen binären Baum
ADT Binaerbaum<T>, boolean
1. Funktionen (Operationen, Protokoll)
NeuerBinaerbaum
->
bin
: Binaerbaum<T>, T, Binaerbaum<T> →
→
links
: Binaerbaum<T>
→
rechts : Binaerbaum<T>
wert
: Binaerbaum<T>
->
68
Binaerbaum<T>
Binaerbaum<T>
Binaerbaum<T>
Binaerbaum<T>
T
Algorithmen und Datenstrukturen
→ boolea
istLeer : Binaerbaum<T>
2. Axiome
Für alle t:T und x:Binaerbaum<T>, y:Binaerbaum<T> gilt:
links(bin(x,t,y)) = x
rechts(bin(x,t,y)) = y
wert(bin(x,t,y)) = t
istLeer(NeuerBinaerbaum) = true
istLeer(bin(x,t,y)) = false
Der direkte Weg zur Deklaration von ADT im Rahmen der objektorientierten
Programmierung ist noch nicht weit verbreitet. Der konventionelle Programmierstil,
Daten und Algorithmen getrennt zu behandeln, bevorzugt den konstruktiven Aufbau
der Daten aus elementaren Datentypen.
Realisierung von abstrakten Datentypen
Die folgenden Elemente müssen im Programm abgebildet werden:
-
Name des ADT wird üblicherweise der Klassenname
Importierte ADTs werden sowohl zur Definition mit dem entsprechenden importierten Typ, als
auch zu Importanweisungen innerhalb des Programms benutzt
Objekterzeugende Operatoren: sog. Konstruktoren werden in den (meist speziellen)
Klassenmethoden abgebildet, die ein neues Objekt des gewünschten Typ zurückliefern
Lesende Operatoren: sog. Selektoren werden zu Methoden, die auf die Attribute nur lesend
zugreifen.
Scheibende Operatoren: sog. Manipulatoren werden zu Methoden, die den Zustand des
Objekts verändern
Axiome müssen sichergestellt sein.
1.3.5.3 Die konstruktive Methode
Die Basis bilden hier die Datentypen. Jedem Objekt ist eine Typvereinbarung in der
folgenden Form zugeordnet: X : T;
X ... Bezeichner (Identifizierer) für ein Objekt
T ... Bezeichner (Identifizierer) für einen Datentyp
Einem Datentyp sind folgende Eigenschaften zugeordnet:
1. Ein Datentyp bestimmt die Wertmenge, zu der eine Konstante gehört oder die durch eine Variable
oder durch einen Ausdruck angenommen werden kann oder die durch einen Operator oder durch
eine Funktion berechnet werden kann.
2. Jeder Operator oder jede Funktion erwartet Argumente eines bestimmten Typs und liefert Resultate
eines bestimmten Typs.
Bei der konstruktiven Methode erfolgt die Definition von Datenstrukturen mit Hilfe
bereits eingeführter Datentypen. Die niedrigste Stufe bilden die einfachen
Datentypen. Basis-Datentypen werden in den meisten Programmiersprachen zur
Verfügung gestellt und sind eng mit dem physikalischen Wertevorrat einer DVAnlage verknüpft (Standard-Typen). Sie sind die „Atome“ auf der niedrigsten
Betrachtungsebene. Neue "höherwertige" Datentypen werden aus bereits definierten
„niederstufigen“ Datentypen definiert.
69
Algorithmen und Datenstrukturen
1.3.5.4 Die objektorientierte Modellierung abstrakter Datentypen
Die Spezifikation abstrakter Datentypen
Im Mittelpunkt dieser Methode steht die Definition von Struktur und Wertebereich der
Daten bzw. eine Sammlung von Operationen mit Zugriff auf die Daten. Jede Aufgabe
aus der Datenverarbeitung läßt sich auf ein solches Schema (Datenabstraktion)
zurückführen.
Zur Beschreibung des ADT dient das folgende Format:
ADT Name
Daten
Beschreibung der Datenstruktur
Operationen
Konstruktor
Intialisierungswerte: Daten zur Initialisierung des
Objekts
Verarbeitung: Initialisierung des Objekts
Operation1
Eingabe: Daten der Anwendung dieser Methode
Vorbedingung: Notwendiger Zustand des Systems vor
Ausführung einer Operation
Verarbeitung: Aktionen, die an den Daten ausgeführt
werden
Ausgabe: Daten (Rückgabewerte) an die Anwendung dieser
Methode
Nachbedingung: Zustand des Systems nach Ausführung der
Operation
Operation2
.........
Operationn
.........
Bsp.: Anwendung dieser Vorlage zur Beschreibung des ADT Stapel
ADT Stapel
Daten
Eine Liste von Datenelementen mit einer Position „top“, sie auf den
Anfang des Stapels verweist.
Operationen
Konstruktor:
Initialisierungswerte: keine
Verarbeitung: Initialisiere „top“.
Push
Eingabe: Ein Datenelement zur Aufnahme in den Stapel
Vorbedingung: keine
Verarbeitung: Speichere das Datenelement am Anfang
(„top“) des Stapel
Ausgabe: keine
Nachbedingung: Der Stapel hat ein neues Datenelement
an der Spitze („top“).
Pop
Eingabe: keine
Vorbedingung: Der Stapel ist nicht leer
Verarbeitung: Das Element an der Spitze („top“) wird entfernt.
Ausgabe: keine
Peek bzw. Top
Eingabe: keine
Vorbedingung: Stapel ist nicht leer
Verarbeitung: Bestimme den Wert des Datenelements an der Spitze („top“
des Stapel.
Ausgabe: Rückgabe des Datenwerts, der an der Spitze
(„top“) des Stapel steht.
70
Algorithmen und Datenstrukturen
Nachbedingung: Der Stapel bleibt unverändert.
Leer
Eingabe: keine
Vorbedingung: keine
Verarbeitung: Prüfe, ob der Stapel leer ist.
Ausgabe: Gib TRUE zurueck, falls der Stapel leer ist; andernfalls FALSE.
Nachbedingung: keine
bereinigeStapel
Eingabe: keine
Vorbedingung: keine
Verarbeitung: Löscht alle Elemente im Stapel und setzt die Spitze
(„top“) des Stapels zurück.
Ausgabe: keine
Klassendiagramme der Unified Modelling Language
Visualisierung und Spezifizierung objektorientierter Softwaresysteme erfolgt mit der
Unified Modelling Language (UML). Zur Beschreibung abstrakter Dytentypen dient
das wichtigste Diagramm der UML: Das Klassendiagramm.
Das Klassendiagramm beschreibt die statische Struktur der Objekte in einem System
sowie ihre Beziehungen untereinander. Die Klasse ist das zentrale Element. Klassen
werden durch Rechtecke dargestellt, die entweder den Namen der Klasse tragen
oder zusätzlich auch Attribute und Operationen. Klassenname, Attribute, Operationen
(Methoden) sind jeweils durch eine horizontale Linie getrennt. Klassennamen
beginnen mit Großbuchstaben und sind Substantive im Singular.
Ein strenge visuelle Unterscheidung zwischen Klassen und Objekten entfällt in der
UML. Objekte werden von den Klassen dadurch unterschieden, daß ihre
Bezeichnung unterstrichen ist. Haufig wird auch dem Bezeichner eines Objekts ein
Doppelpunkt vorangestellt.. Auch können Klassen und Objekte zusammen im
Klassendiagramm auftreten.
Klasse
Objekt
Wenn man die Objekt-Klassen-Beziehung (Exemplarbeziehung, Instanzbeziehung)
darstellen möchte, wird zwischen einem Objekt und seiner Klasse ein gestrichelter
Pfeil in Richtung Klasse gezeichnet:
Klasse
Objekt
Die Definition einer Klasse umfaßt die „bedeutsamen“ Eigenschaften. Das sind:
- Attribute
d.h.: die Struktur der Objekte: ihre Bestandteile und die in ihnen enthaltenen Informationen und
Daten.. Abhängig von der Detaillierung im Diagramm kann die Notation für ein Attribut den
Attributnamen, den Typ und den voreingestellten Wert zeigen:
Sichtbarkeit Name: Typ = voreingestellter Wert
- Operationen
d.h.: das Verhalten der Objekte. Manchmal wird auch von Services oder Methoden gesprochen. Das
Verhalten eines Objekts wird beschrieben durch die möglichen Nachrichten, die es verstehen kann.
Zu jeder Nachricht benötigt das Objekt entsprechende Operationen. Die UML-Syntax für
Operationen ist:
Sichtbarkeit Name (Parameterliste) : Rückgabetypausdruck (Eigenschaften)
71
Algorithmen und Datenstrukturen
Sichtbarkeit ist + (öffentlich), # (geschützt) oder – (privat)
Name ist eine Zeichenkette
Parameterliste enthält optional Argumente, deren Syntax dieselbe wie für Attribute ist
Rückgabetypausdruck ist eine optionale, sprachabhängige Spezifikation
Eigenschaften zeigt Eigenschaftswerte (über String) an, die für die Operation Anwendung finden
- Zusicherungen
Die Bedingungen, Voraussetzungen und Regeln, die die Objekte erfüllen müssen, werden
Zusicherungen genannt. UML definiert keine strikte Syntax für die Beschreibung von Bedingungen.
Sie müssen nur in geschweifte Klammern ({}) gesetzt werden.
Idealerweise sollten Regeln als Zusicherungen (engl. assertions) in der Programmiersprache
implementiert werden können.
Attribute werden mindestens mit ihrem Namen aufgeführt und können zusätzliche
Angaben zu ihrem Typ (d.h. ihrer Klasse), einen Initialwert und evtl.
Eigenschaftswerte und Zusicherungen enthalten. Attribute bilden den Datenbestand
einer Klasse.
Operationen (Methoden) werden mindestens mit ihrem Namen, zusätzlich durch ihre
möglichen Parameter, deren Klasse und Initialwerte sowie evtl. Eigenschaftswerte
und Zusicherungen notiert. Methoden sind die aus anderen Sprachen bekannten
Funktionen.
Klassenname
attribut:Typ=initialerWert
operation(argumentenliste):rückgabetyp
Abb.:
Bsp.: Die Klasse Object aus dem Paket java.lang
Object
+equals(obj :Object)
#finalize()
+toString()
+getClass()
#clone()
+wait()
+notify()
........
Sämtliche Java-Klassen bilden eine Hierarchie mit java.lang.Object als
gemeinsame Basisklasse.
Assoziationen repräsentieren Beziehungen zwischen Instanzen von Klassen.
Mögliche Assoziationen sind:
- einfache (benannte) Assoziationen
72
Algorithmen und Datenstrukturen
- Assoziation mit angefügten Attributen oder Klassen
- Qualifzierte Assoziationen
- Aggregationen
- Assoziationen zwischen drei oder mehr Elementen
- Navigationsassoziationen
- Vererbung
Attribute werden von Assoziationen unterschieden:
Assoziation: Beschreibt Beziehungen, bei denen beteiligte Klassen und Objekte von anderen Klassen
und Objekten benutzt werden können.
Attribut: Beschreibt einen privaten Bestandteil einer Klasse oder eines Objekts, welcher von außen
nicht sichtbar bzw. modifizierbar ist.
Grafisch wird eine Assoziation als durchgezogene Line wiedergegeben, die gerichtet
sein kann, manchmal eine Beschriftung besitzt und oft noch weitere Details wie z.B.
Muliplizität (Kardinalität) oder Rollenanmen enthält, z.B.:
Arbeitet für
0..1
Arbeitgeber
Arbeitnehmer
Eine Assoziation kann einen Namen zur Beschreibung der Natur der Beziehung
(„Arbeitet für“) besitzen. Damit die Bedeutung unzweideutig ist, kann man dem
Namen eine Richtung zuweisen: Ein Dreieck zeigt in die Richtung, in der der Name
gelesen werden soll.
Rollen („Arbeitgeber, Arbeitnehmer) sind Namen für Klassen in einer Relation. Eine
Rolle ist die Seite, die die Klasse an einem Ende der Assoziation der Klasse am
anderen Ende der Assoziation zukehrt. Die Navigierbarkeit kann durch einen Pfeil in
Richtung einer Rolle angezeigt werden.
Rolle1
Rolle2
K1
K2
1
0..*
Abb.: Binäre Relation R = C1 x C2
Rolle1
K1
K2
Rollen
...
Kn
Abb.: n-äre Relation K1 x K2 x ... x Kn
In vielen Situationen ist es wichtig anzugeben, wie viele Objekte in der Instanz einer
Assoziation miteinander zusammenhänen können. Die Frage „Wie viele?“ bezeichnet
man als Multiplizität der Rolle einer Assoziation. Gibt man an einem Ende der
Assoziation eine Multiplizität an, dann spezifiziert man dadurch: Für jedes Objekt am
entgegengesetzten Ende der Assoziation muß die angegebene Anzahl von Objekten
vorhanden sein.
73
Algorithmen und Datenstrukturen
Ein A ist immer mit Ein A ist immer mit Ein A ist mit
einem B assoziiert einem oder mehre- keinem
oder
ren B assoziiert
einem B assoziiert
Ein A ist mit keinem, einem oder
mehreren B assoziiert
Unified
A
1 B
A
1..*
B
A
0..1 B
A
*
B
1:1
1..*
1:1..n
0..*
2..6
0..*
*
17
4
n
m
0..n:2..6
0..n:0..n
17:4
?
Abb.: Kardinalitäten für Beziehungen
Pfeile in Klassendiagrammen zeigen Navigierbarkeit an. Wenn die Navigierbarkeit
nur in einer Richtung existiert, nennt man die Assoziation eine gerichtete Assoziation
(uni-directional association). Eine ungerichtete (bidirektionale) Assoziation enthält
Navigierbarkeiten in beiden Richtungen. In UML bedeuten Assoziationen ohne Pfeile,
daß die Navigierbarbeit unbekannt oder die Assoziation ungerichtet ist.
Ungerichtete Assoziationen enthalten eine zusätzliche Bedingung: Die zu dieser
Assoziation zugehörigen zwei Rollen sind zueinander invers.
Abhängigkeit (dependency): Manchmal nutzt eine Klasse eine andere. Die UMLNotation ist dafür oft eine gestrichelte Linie mit einem Pfeil, z.B.:
Applet
WillkommenApplet
paint()
Graphics
Abb. : WillkommenApplet nutzt die Klasse Graphics über paint()
Reflexive Assoziation: Manchmal ist auch eine Klasse mit sich selbst assoziiert. Das
kann bspw. der fall sein, wenn eine Klasse Objekte hat, die mehrere Rollen spielen,
z.B.:
74
Algorithmen und Datenstrukturen
Fahrzeuginsasse
1
fahrer
fährt
0..4
beifahrer
Ein Fahrzeuginsasse kann entweder ein Fahrer oder ein Beifahrer sein. In der Rolle des Fahrers fährt
ein Fahrzeuginsasse null oder mehr Fahrzeuginsassen, die die Rolle von Beifahrern spielen.
Abb.:
Bei einer reflexiven Assoziation zieht man eine Linie von der Klasse aus zu dieser
zurück. Man kann die Rollen sowie die Namen, die Richtung und die Multiplizität der
Assoziation angeben.
Eine Aggregation ist eine Sonderform der Assoziation. Sie repräsentiert eine
(strukturelle) Ganzes/Teil-Beziehung. Zusätzlich zu einfacher Aggregation bietet UML
eine stärkere Art der Aggregation, die Komposition genannt wird. Bei der
Komposition darf ein Teil-Objekt nur zu genau einem Ganzen gehören.
Teil
Ganzes
Existenzabhängiges Teil
Abb.: Aggregation und Komposition
Eine Aggregation wird durch eine Raute dargestellt. Die Komposition wird durch eine
ausgefüllte Raute dargestellt und beschreibt ein „physikalisches Enthaltensein“.
Die Vererbung (Spezialisierung bzw. Generalisierung) stellt eine Verallgemeinerung
von Eigenschaften dar. Eine Generalisierung (generalization) ist eine Beziehung
zwischen dem Allgemeinen und dem Speziellen, in der Objekte des speziellen Typs
(der Subklasse) durch Elemente des allgemeinen Typs (der Oberklassse) ersetzt
werden können. Grafisch wird eine Generalisierung als durchgezogene Linle mit
einer unausgefüllten, auf die Oberklasse zeigenden Pfeilspitze wiedergegeben, z.B.:
Supertyp
Subtyp 1
Subtyp 2
75
Algorithmen und Datenstrukturen
Bsp.: Vererbungshierarchie und wichtige Methoden der Klasse Applet
Panel
Applet
+init()
+start()
+paint(g:Graphics) {geerbt}
+update(g:Graphics) {geerbt}
+repaint()
+stop()
+destroy()
+getParameter(name:String);
+getParameterInfo()
+getAppletInfo()
Abb.:
Schnittstellen und abstrakte Klassen: Eine Schnittstelle (Interface) ist eine
Ansammlung von Operationen, die eine Klasse ausführt. Programmiersprachen (z..
B. Java) benutzen ein einzelnes Konstrukt, die Klasse, die sowohl Schnittstelle als
auch deren Implementierung enthält. Bei der Bildung einer Unterklasse wird beides
vererbt. Eine reine Schnittstelle (wie bspw. in Java) ist eine Klasse ohne
Implementierung und besitzt daher nur Operationsdeklarationen. Schnittstellen
werden oft mit Hilfe abstrakter Klassen deklariert.
Bei abstrakten Klassen oder Methoden wird der Name des abstrakten Gegenstands
in der UML kursiv geschrieben. Ebenso kann man die Bedingung {abstract}
benutzen.
<<interface>>
InputStream
DataInput
OrderReader
{abstract}
Abhängigkeit
Generalisierung
Verfeinerung
DataInputStream
Irgendeine Klasse, z.B. „OrderReader“ benötigt die DataInput-Funktionalität. Die Klasse
DataInputStream implementiert DataInput und InputStream. Die Verbindung zwischen
DataInputStream und DataInput ist eine „Verfeinerung (refinement)“. Eine Verfeinerung ist in UML ein
allgemeiner Begriff zur Anzeige eines höheren Detaillierungsniveaus. Die Objektbeziehung zwischen
OrderReader und DataInput ist eine Abhängigkeit. Sie zeigt, daß „OrderReader“ die Schnittstelle
„DataInput für einige Zwecke benutzt.
Abb.: Schnittstellen und abstrakte Klassen: Ein Beispiel aus Java
Abstrakte Klassen und Schnittstellen ermöglichen die Definition einer Schnittstelle
und das Verschieben ihrer Implementierung auf später. Jedoch kann die abstrakte
76
Algorithmen und Datenstrukturen
Klasse schon die Implementierung einiger Methoden enthalten, während die
Schnittstelle die Verschiebung der Definition aller Methoden erzwingt.
Eine Schnittstelle modelliert man in Form einer gestrichelten Linie mit einem großen,
unausgefüllten Dreieck, das neben der Schnittstelle steht und auf diese zeigt. Eine
andere verkürzte Darstellung einer Klasse und einer Schnittstelle besteht aus einem
(kleinen Kreis), der mit der Klasse durch eine Linie verbunden ist, z.B.:
Object
Component
ImageObserver
Container
Panel
Applet
Abb.: Vererbungshierarchie von Applet
77
Algorithmen und Datenstrukturen
2. Graphen und Graphenalgorithmen
2.1 Einführung
2.1.1 Grundlagen
Viele Objekte und Vorgänge in verschiedenen Bereichen besitzen den Charakter
eines Systems, d.h.: Sie setzen sich aus einer Anzahl von Bestandteilen, Elementen
zusammen, die in gewisser Weise in Beziehung stehen. Sollen an einem solchen
System Untersuchungen durchgeführt werden, dann ist es oft zweckmäßig, den
Gegenstand der Betrachtungen durch ein graphisches Schema (Modell) zu
veranschaulichen. Dabei stehen grundsätzlich immer 2 Elemente untereinander in
Beziehung, d.h.: Die Theorie des graphischen Modells ist ein Teil der Mengenlehre,
die binäre Relationen einer abzählbaren Menge mit sich selbst behandelt.
Bsp.: Es ist K = {A, B, C, D} eine endliche Menge. Es ist leicht die Menge aller
geordneten Paare von K zu bilden:
K × K = {(A,A),(A,B),(A,C),(A,D),(B,A),(B,B),(B,C),(B,D),(C,A),(C,B),(C,C),(C,D),(D,A),(D,B),D,C),(D,D)}
Gegenüber der Mengenlehre ist die Graphentheorie nicht autonom.
Die Graphentheorie besitzt ein eigenes, sehr weites und spezifisches Vokabular. Sie
umfaßt viele Anwendungsungsmöglichkeiten in der Physik, aus dem
Fernmeldewesen und dem Operations Research (OR). Im OR sind es vor allem
Organisations- bzw. Verkehrs- und Transportprobleme, die mit Hilfe von
Graphenalgorithmen untersucht und gelöst werden.
Generell dienen Graphenalgorithmen in der Praxis zum Lösen von kombinatorischen
Problemen. Dabei geht man folgendermaßen vor:
1. Modelliere das Problem als Graph
2. Formuliere die Zielfunktion als Eigenschaft des Graphen
3. Löse das Problem mit Hilfe eines Graphenalgorithmus
Bsp.: Es ist K = {A,B,C,D} ein endliche Menge. Es ist leicht die Menge aller
geordneten Paare von K zu bilden:
K × K = {(A,A),(A,B),(A,C),(A,D),(B,A),(B,B),(B,C),(B,D),(C,A),(C,B),(C,C),(C,D),(D,A),(D,B),D,C),(D,D)}
Die Menge dieser Paare kann auf verschiedene Arten dargestellt werden:
78
Algorithmen und Datenstrukturen
1. Koordinatendarstellung
A
B
C
D
A
B
C
D
Abb.:
2. Darstellung durch Punkte (Kreise) und Kanten (ungerichteter Graph)
A
B
C
D
Abb.: ungerichteter Graph
Eine Kante (A,A) nennt man Schlinge. Ein schlingenfreier Graph heißt schlicht.
Ein ungerichteter Graph G = (V , E ) besteht aus
- einer endlichen Knotenmenge (vertices) V und
- einer endlichen Kantenmenge (edges) E
3. Darstellung durch Punkte (Kreise) und Pfeile (gerichteter Graph)
A
B
D
C
Abb.:
79
Algorithmen und Datenstrukturen
Einen Pfeil (A,A) nennt man eine Schlinge. Zwei Pfeile mit identischem Anfangsund Endknoten nennt man parallel. Analog lassen sich parallele Kanten definieren.
Ein Graphen ohne parallele Kanten bzw. Pfeile und ohne Schlingen bezeichnet man
als schlichte Graphen.
(
)
4. G = (V , E , φ ), φ : E → ℜ meist φ : E → ℜ + heißt bewerteter (weighted) Graph mit
Bewertung φ (Bewertungen geben z.B. Abstände, Kosten, Kapazitäten oder
Wahrscheinlichkeiten an.
5. Darstellung durch paarweise geordnete Paare
A
A
B
B
C
C
D
D
Abb
Ein bipartiter Graph ist ein Graph, dessen Knoten so in zwei Mengen zerteilt werden
können, dass jede Kante je einen Knoten aus beiden Mengen verbindet.
Probleme:
1. Herausfinden, ob ein Graph bipartit ist
2. Welches sind die Partitionen
80
Algorithmen und Datenstrukturen
Bipartites Matching: Bipartite Graphen dienen häufig zur Lösung
Zuordnungsproblemen, z.B. für Männer und Frauen in einem Tanzkurs.
Heini
von
Eva
Martin
Klaus
Maria
Pia
gematcht
Lilo
Uwe
Abb.: Jeder Teilnehmer im Tanzkurs ist ein Knoten im Graphen zugeordnet, jede Kante beschreibt
mögliche Tanzpartner. Drei Paare sind gefunden, aber nicht jeder Knoten hat einen Partner, und es
sind keine weiteren Paarungen möglich.
6. Darstellung mit Hilfe einer Matrix
A
1
1
1
1
A
B
C
D
B
1
1
1
1
C
1
1
1
1
D
1
1
1
1
Einige der geordneten Paare aus der Produktmenge K × K sollen eine bestimmte
Eigenschaft haben, während die anderen sie nicht besitzen.
Eine solche Untermenge von K × K ist :
G = {(A,B),(A,D),(B,B),(B,C),(B,D),(C,C),(D,A),(D,B),(D,C),(D,D)}
Üblicherweise wird diese Untermenge (Teilgraph) so dargestellt:
A
B
D
C
Abb.:
Betrachtet man hier die Paare z.B. (A,B) bzw. (A,D), so kann man feststellen: Von A
erreicht man, den Pfeilen folgend, direkt B oder D. B und D heißt auch die
"Inzidenzabbildung" von A und {B,D} das volle Bild von A.
Verwendet man das Symbol Γ zur Darstellung des vollen Bilds, dann kann man das
vorliegende Beispiel (vgl. Abb.:) so beschreiben:
Γ ( A) = ( B, D )
Γ ( B ) = ( B, C, D )
Γ( C ) = C
Γ ( D ) = ( A, B, C, D )
81
Algorithmen und Datenstrukturen
Zwei Kanten (Pfeile) werden benachbart oder adjazent genannt, wenn es einen
Knoten gibt, der Endknoten einen Kante und Anfangsknoten der anderen Kante ist.
Zwei Knoten heißen benachbart oder adjazent, wenn sie durch einen Kante (Pfeil)
unmittelbar verbunden sind.
Kanten (Pfeile), die denselben Anfangs- und Endknoten haben, heißen parallel.
2.1.2 Definitionen
Gegeben ist eine endliche (nicht leere) Menge K 53. Ist G eine Untermenge der
Produktmenge K × K , so nennt man ein Element der Menge K einen Knoten von G.
Die Elemente der Knotenmenge K können auf dem Papier durch Punkte (Kreise)
markiert werden.
A
B
D
C
Abb.:
Ein Element von G selbst ist eine (gerichtete) Kante. Im vorstehenden Bsp 54. sind
(A,B), (A,D), (B,B), (B,C), (B,D), (C,C), (D,A), (D,B), (D,C), (D,D) (gerichtete) Kanten.
Ein Graph wird durch die Menge seiner Knoten K und die seiner Inzidenzabbildungen
beschrieben: G=(K, Γ )
Ein Graph kann aber auch folgendermaßen beschrieben werden: G = (K,E) bzw.
G = (V , E ) . E ist die Menge der Kanten (gerichtet, ungerichtet, gewichtet). In
gewichteten Graphen werden jeder Kante ganze Zahlen (Gewichte, z.B. zur
Darstellung von Entfernungen oder Kosten) zugewiesen. Gewichtete gerichtete
Graphen werden auch Netzwerke genannt.
Falls die Anzahl der Knoten in einem Graphen "n" ist, dann liegt die Anzahl der
n ⋅ ( n − 1)
Kanten zwischen 0 und
im ungerichten Graphen. Ein gerichteter Graph
2
kann bis zu n ⋅ ( n − 1) Pfeile besitzen.
In einem vollständigen Graphen existiert zwischen jedem Knotenpaar eine Kante.
53
54
Anstatt K schreibt man häufig auch V (vom englischen Wort Vertex abgeleitet)
vgl. 2.1.1
82
Algorithmen und Datenstrukturen
Abb. Vollständiger Graph
Ein Graph G = (V , E ) heißt bipartit, wenn 2 disjunkte Knotenmengen V1 , V2 ⊆ V gibt,
so dass E ⊆ {{v1 , v 2 }v1 ∈ V1 , v 2 ∈ V2 } gilt.
Abb. Ein bipartiter Graph
Der Grad eines Knoten bezeichnet die Zahl der Kanten, die in Knoten enden.
Eingangsgrad: Zahl der ankommenden Kanten.
1
3
2
1
2
1
1
2
0
Abb.: Eingangsgrad
Ausgangsgrad: Zahl der abgehenden Kanten
2
1
0
1
2
0
1
2
3
Abb.: Ausgangsgrad
83
Algorithmen und Datenstrukturen
Bei ungerichteten Graphen ist der Ausgangsgrad gleich dem Eingangsgrad. Man
spricht dann nur von Grad.
Ein Pfad vom Knoten k1 zum Knoten kk ist eine Folge von Knoten k1, k2, ... , kk, wobei
(k1,k2), ... ,(kk-1,kk) Kanten sind. Die Länge des Pfads ist die Anzahl der Kanten im
Pfad. Auch Pfade können gerichtet oder ungerichtet sein.
kk
k1
Abb.:
Ein Graph ist zusammenhängend, wenn von jedem Knoten zu jedem anderen
Knoten im Graph ein Weg (Pfad) existiert.
X1
X5
X2
X4
X3
Abb.:
Dieser Graph ist streng zusammenhängend. Man kann sehen, daß es zwischen je 2
Knoten mindestens einen Weg gibt. Dies trifft auf den folgenden Grafen nicht zu:
X1
X6
X2
X3
X5
X4
Abb.:
Hier gibt es bspw. keinen Weg von X4 nach X1.
Ein Graph, der nicht zusammenhängend ist, setzt sich aus zusammenhängenden
Komponenten zusammen.
Ein Knoten in einem zusammenhängenden Netzwerk heißt Artikulationspunkt,
wenn durch sein Entfernen der Graph zerfällt, z.B.
84
Algorithmen und Datenstrukturen
Artikulationspunkte sind dunkel eingefärbt.
Abb.:
Erreichbarkeit: Der Knoten B ist in dem folgenden Graphen erreichbar vom Knoten
G, wenn es einen Pfad von G nach B gibt.
C
E
B
D
F
I
G
A
H
Abb.: Knoten B ist erreichbar von Knoten G
Ein Graph heißt Zyklus, wenn sein erster und letzter Knoten derselbe ist.
Abb.: Zyklus (manchmal auch geschlossener Pfad genannt)
Ein Zyklus ist ein einfacher Zyklus, wenn jeder Knoten (außer dem ersten und dem
letzten) nur einmal vorkommt.
85
Algorithmen und Datenstrukturen
Abb.: Einfacher Zyklus (manchmal auch geschlossener Pfad genannt)
Ein gerichteter Graph heißt azyklisch, wenn er keine Zyklen enthält. Ein azyklischer
Graph kann in Schichten eingeteilt werden (Stratifikation).
Bäume sind Graphen, die keine Zyklen enthalten. Graphen, die keine Zyklen
enthalten heißen Wald. Zusammenhängende Graphen, die keine Zyklen enthalten,
heißen Bäume. Wenn ein gerichteter Graph ein Baum ist und genau einen Knoten
mit Eingangsgrad 0 hat, heißt der Baum Wald.
Ein spannender Baum (Spannbaum) eines ungerichteten Graphen ist ein Teilgraph
des Graphen, und ist ein Baum der alle seine Knoten enthält.
Abb.
Einen spannenden Baum mit minimaler Summe der Kantenbewegungen bezeichnet
man als minimalen spannenden Baum. Zu dem folgenden Graphen
3
4
2
5
4
3
4
4
5
6
gehört der folgende minimale spannende Baum
Abb.: Minimaler spannender Baum
86
Algorithmen und Datenstrukturen
Ein Pfad wird als Circuit (Rundgang) bezeichnet, wenn der erste und letzte Knoten
des Pfads identisch sind. Ein Circuit wird als einfach (oder als Kreis) bezeichnet, falls
alle Knoten außer dem ersten und letzten genau einmal auftreten
Eulersche Pfade bzw. Eulerscher Kreis: Ausgangspunkt dieses Problems ist das
sog. Königsberger Brückenproblem, das Leonard Euler 1736 gelöst hat. Euler
interpretierte dabei die Brücken über den Fluß Pregel in Königsberg als Kanten und
Ufer bzw. Inseln als Knoten.
neuer Pregel
Pregel
alter Pregel
Abb.: Königsberger Brückenproblem mit Darstellung als Graph
Königsberger Brückenproblem: Existiert ein Eulerscher Pfad?
Lösung: Da man, wenn man in einen Knoten hineinkommt, auf anderem Weg wieder
herauskommen muß, gilt als Bedingung: Der Grad jedes Knoten muß durch 2 teilbar
sein.
Neuformulierung des Problems: Gibt es einen Zyklus im Graphen, der alle Kanten
genau einmal enthält (Eulerkreis) 55.
Bedingung für die Existenz eines Eulerkreises: Der Grad jedes Knoten muß durch 2
teilbar und zusammenhängend sein. Das Königsberger Brückenproblem stellt
offenbar keinen Eulerkreis dar.
Bekanntes Bsp.: Kann das Häuschen der folgenden Abbildung in einem Strich
gezeichnet werden?
Abb. Haus des Nikolaus
55
falls JA, wird der Graph eulersch genannt.
87
Algorithmen und Datenstrukturen
Hamiltonsche Pfade: Gegeben ist eine Landkarte mit Orten und Verbindungen.
Gesucht ist ein Rundgang einmal durch jeden Ort.
Abb.: Hamiltonscher Kreis
Verschärfung: jede Verbindung ist mit Kosten gewichtet. Gesucht ist der billigste
Rundgang.
Ein Hamiltonscher Pfad ist ein einfacher Zyklus, der jeden Knoten eines Graphen
enthält. Ein Algorithmus für das Finden eines Hamiltonschen Graphen ist relativ
einfach (modifizierte Tiefensuche) aber sehr aufwendig. Bis heute ist kein
Algorithmus bekann, der eine Lösung in polynomialer Zeit findet.
2.1.3 Darstellung in Rechnerprogrammen
1. Der abstrakte Datentyp (ADT) für gewichtete Graphen
Ein gewichteter Graph besteht aus Knoten und gewichteten Kanten. Der ADT
beschreibt die Operationen, die einem solchen gewichteten Graphen Datenwerte
hinzufügen oder löschen. Für jeden Knoten Ki definiert der ADT alle benachbarten
Knoten, die mit Ki durch eine Kante E(Ki,Kj) verbunden sind.
ADT Graph
Daten
Sie umfassen eine Menge von Knoten {Ki} und Kanten {Ei}. Eine Kante ist ein
Paar (Ki, Kj), das anzeigt: Es gibt eine Verbindung vom Knoten Ki zum Knoten
Kj. Verbunden ist mit jeder Kante die Angabe eines Gewichts. Es bestimmt
den Aufwand, um entlang der Kante vom Knoten Ki nach dem Knoten Kj zu
kommen.
Operationen
Konstruktor
Eingabe: keine
Verarbeitung: Erzeugt den Graphen als Menge von Knoten und Kanten
Einfuegen_Knoten
Eingabe: Ein neuer Knoten
Vorbedingung: keine
Verarbeitung: Füge den Knoten in die Menge der Knoten ein
Ausgabe: keine
Nachbedingung: Die Knotenliste nimmt zu
Einfügen_Kante
Eingabe: Ein Knotenpaar Ki und Kj und ein Gewicht
Vorbedingung: Ki und Kj sind Teil der Knotenmenge
Verarbeitung: Füge die Kante (Ki,Kj) mit dem gewicht in die Menge der
Kanten ein.
Ausgabe: keine
88
Algorithmen und Datenstrukturen
Nachbedingung: Die Kantenliste nimmt zu
Loesche_Knoten
Eingabe: Eine Referenz für den Knoten Kl
Vorbedingung: Der Eingabewert muß in der Knotenmenge vorliegen
Verarbeitung: Lösche den Knoten aus der Knotenliste und lösche alle
Kanten der Form (K,Kl) bzw. (Kl,K), die eine Verbindung mit
Knoten Kl besitzen
Loesche_Kante
Eingabe: Ein Knotenpaar Ki und Kj
Vorbedingung: Der Eingabewert muß in der Kantenliste vorliegen
Verarbeitung: Falls (Ki,Kj) existiert, loesche diese Kante aus der
Kantenliste
Ausgabe: keine
Nachbedingung: Die Kantenmenge wird modifiziert
Hole_Nachbarn:
Eingabe: Ein Knoten K
Vorbedingung: keine
Verarbeitung: Bestimme alle Knoten Kn, so daß (K,Kn) eine Kante ist
Ausgabe: Liste mit solchen Kanten
Nachbedingung: keine
Hole_Gewichte
Eingabe: Ein Knotenpaar Ki und Kj
Vorbedingung: Der Eingabe wert muß zur Knotenmenge gehören
Verarbeitung: Beschaffe das Gewicht der Kante (Ki, Kj), falls es
existiert
Ausgabe: Gib das Gewicht dieser Kante aus (bzw. Null, falls die Kante
nicht existiert
Nachbedingung: keine
2. Abbildung der Graphen
Es gibt zahlreiche Möglichkeiten zur Abbildung von Knoten und Graphen in einem
Rechnerprogramm. Eine einfache Abbildung speichert die Knoten in einer
sequentiellen Liste. Die Kanten werden in einer Matrix beschrieben
(Adjazenzmatrix), in der Zeile i bzw. Spalte j den Knoten Ki und Kj zugeordnet sind.
Jeder Eintrag in der Matrix gibt das Gewicht der Kante Eij = (Ki,Kj) oder den Wert 0
an, falls die Kante nicht existiert. In ungewichteten, gerichteten Graphen hat der
Eintrag der (booleschen) Wert 0 oder 1, je nachdem, ob die Kante zwischen den
Knoten existiert oder nicht, z.B.:
89
Algorithmen und Datenstrukturen
2
A
B
3
5
1
4
E
C
7
D
0 2 1 0 0
0 0 5 0 0
0 4 0 0 0
0 0 7 0 0
0 3 0 0 0
B
A
C
D
E
0 1 1 1 0
1 0 1 0 0
1 0 0 0 0
0 0 0 0 1
0 0 1 0 0
Abb.:
Besonders einfach kann einer Adjazenzmatrix A[i,j] geprüft werden, ob es eine Kante
von i nach j gibt. Die Laufzeit für diese Operation ist O(1). Will man alle Nachbarn
eines Knoten i in einem ungerichteten Graphen ermitteln, muß man hingegen alle
Einträge der i-ten Zeile oder der i-ten Spalte überprüfen (n Schritte). Bei gerichteten
Graphen findet man in der i-ten Zeile die Knoten, die von i aus erreichbar sind
(Nachfolger), in der j-ten Spalte hingegen die Knoten, von denen aus eine Kante
nach i führt (Vorgänger).
Der Speicherplatzbedarf für eine Adgazenzmatrix ist –unabhängig von der Anzahl
der Kanten – immer n2. 56 Gibt es wenige Kanten im Graphen, enthält die zugehörige
Adjazenzmatrix hauptsächlich Nullen. Ungerichtete Graphen können etwas effizienter
gespeichert werden, da ihre Matrix symmetrisch ist, somi müssen die Einträge nur
n ⋅ ( n − 1)
oberhalb der Diagonalen gespeichert werden. Dafür werden
Speicherplätze
2
56
kompakt bei dichten Graphen, also Graphen mit vielen Kanten
90
Algorithmen und Datenstrukturen
benötigt, wenn es keine Schlingen gibt.
n ⋅ (n + 1)
Speicherplätze benötigt man. Wenn
2
es Schlingen gibt 57.
In der Darstellungsform „Adjazenzliste“ werden für jeden Knoten alle mit ihm
verbundenen Knoten in eine Adjazenzliste für diese Knoten aufgelistet. Das läßt sich
leicht über verkettete Listen realisieren. In einem gewichteten Graph kann zu jedem
Listenelement ein Feld für das Gewicht hinzugefügt werden, z.B.:
2
A
B
3
5
1
4
E
C
7
Knoten:
57
D
Liste der Nachbarn:
A
B
2
B
C
5
C
B
4
D
C
7
E
B
3
C
1
in diesem Fall muß die Diagonale mitgespeichert werden.
91
Algorithmen und Datenstrukturen
B
A
C
D
E
A
B
C
B
A
C
C
A
D
E
E
C
D
Abb.:
Adjazenzlisten verbrauchen nur linear viel Speicherplatz, was insbesonders bei
dünnen Grafen (also Graphen mit wenig Kanten) von Vorteil ist. Viele
graphentheoretischen Probleme lassen sich mit Adjazenzlisten in linearer Zeit lösen.
Für einen gerichteten Graphen benötigt eine Adjazenzliste n+m Speicherplätze, für
einen ungerichteten Gaphen n+2m mit n als Knotenanzahl und m als als
Kantenanzahl.
3. Lösungsstrategien
Für die Lösung der Graphenprobleme stattet man die Algorithmen mit verschiedenen
Strategien aus:
- Greedy (sukzessive bestimmung der Lösungsvariablen)
- Divide and Conquer (Aufteilen, Lösen, Lösungen vereinigen)
- Dynamic Programming (Berechne Folgen von Teillösungen)
- Enumeration (Erzeuge alle Permutationen und überprüfe sie)
- Backtracking (Teillösungen werden systematisch erweitert)
- Branch and Bound (Erweitere Teillösungen an der vielversprechenden Stelle)
92
Algorithmen und Datenstrukturen
2.2 Durchlaufen von Graphen
Für manche Probleme ist es wichtig, alle Knoten in einem Graphen zu betrachten. So
kann man etwa einer in einem Labyrinth eingeschlossenen Person nachfühlen, dass
sie sämtliche Kreuzungen von Gängen in Augenschein nehmen will. Die Gänge des
Labyrinths sind hier die Kanten des Graphen, und Kreuzungen sind die Knoten.
Es gibt zwei Suchstrategien für Graphen: Tiefensuche und Breitensuche. Diese
Verfahren bilden die Grundlage für graphentheoretische Algorithmen, in denen alle
Ecken oder Kanten eines Graphen systematisch durchlaufen werden müssen.
Für die meisten Suchverfahren gilt folgendes algorithmisches Grundgerüst
(Markierungsalgorithmus):
1. Markiere den Startknoten
2. Solange es noch Kanten von markierten zu unmarkierten Knoten gibt, wähle eine solche
Kante und markiere deren Endknoten.
Die beiden hier angegebenen Verfahren (Tiefensuche
unterscheiden sich in der Auswahl der Kanten in Schritt 2.
und
Breitensuche)
2.2.1 Tiefensuche (depth-first search)
Bei der Tiefensuche (DFS) bewegt man sich möglichst weit vom Startknoten weg,
bevor man die restlichen Knoten besucht. Trifft man auf einen Knoten, der keine
unbesuchten Nachbarn hat, so erfolgt "backtracking", d.h. die Suche wird beim
Vorgänger fortgesetzt. Dadurch werden alle vom Startknoten erreichbaren Knoten
gefunden.
2.2.1.1 Algorithmus
Als Eingabe benötigt der Algorithmus einen Graphen und einen Startknoten.
- color[v]: repräsentiert den aktuellen Bearbeitungsstatus
weiß = unbesucht/unbearbeitet
schwarz = abgearbeitet (v und alle Nachbarn von v wurden besucht).
grau = in Bearbeitung (v wurde besucht, kann aber noch unbesuchte Nachbarn haben)
- p[v]: Vorgänger (predecessor) von v
- b[v]: Beginn der Suche (Einfügen des Knotens in den Stack bzw. Zeitpunkt des rekursiven Aufrufs)
- f[v]: Ende der Suche (Löschen des Knotens aus dem Stack bzw. Ende des rekursiven Aufrufs)
Die Knoten, die in Bearbeitung sind, werden in einem Stack K (LIFO) verwaltet.
for each vertex u ∈ V [G ] − {s}
color[u ] ← WHITE
b[u ] ← ∞
f [u ] ← ∞
p[u ] ← NIL
time ← 1
color[ s ] ← GRAY
PUSH ( K , s )
b[ s] ← time
do
93
Algorithmen und Datenstrukturen
p[ s] ← NIL
while K ≠ 0
do u ← TOP( K )
if ∃v ∈ Adj[u ] : color[v] = WHITE
then color[v ] ← GRAY
PUSH ( K , v)
b[v] ← time ← time + 1
else POP( K )
color[u ] ← BLACK
f [u ] ← time ← time + 1
Komplexität: Das Initialisieren des Graphen dauert O( | V | ) Zugriffe auf den Stack und
die "Arrays" brauchen konstante Zeit (insgesamt O( | V | )), die Adjazenzliste wird
genau einmal durchlaufen (O( | E | ). Damit ergibt sich eine Gesamtlaufzeit von
O( | V | + | E | ).
Bsp.: Tiefensuche in ungerichteten Graphen
Anfangsschritt: für alle v ∈ V :
color[v] ← WHITE , b[v] ← ∞ , f [v] ← ∞ , p[v] ← NIL
Stack
1.Schritt:
u
v
w
x
y
z
b[u ] = 1
Stack
u
v
w
x
y
z
u
2. Schritt: b[v ] = 2
Stack
u
v
w
x
y
z
v
u
94
Algorithmen und Datenstrukturen
3. Schritt:
b[ w] = 3
Stack
u
v
w
w
x
y
v
u
z
4. Schritt: b[ y ] = 4
Stack
u
v
w
y
w
x
y
v
u
z
5. Schritt: b[ x] = 5
Stack
x
u
v
w
y
w
x
y
v
u
z
6. Schritt: f [ x] = 6 , back edge zu u und v
Stack
u
v
w
y
w
x
7. Schritt: backtracking zu y,
y
v
u
z
f [ y] = 7
Stack
u
v
w
w
x
y
v
u
z
95
Algorithmen und Datenstrukturen
8. Schritt: backtracking zu w,
f [ z] = 8
Stack
u
v
w
z
w
x
y
v
u
z
9. Schritt: f [ z ] = 9
Stack
u
v
w
w
x
y
v
u
z
10. Schritt: backtracking zu w, f [ w] = 10
Stack
u
v
w
x
y
z
v
u
11. Schritt: backtracking zu v, f [ w] = 10
Stack
u
v
w
x
y
z
12. Schritt: backtracking zu w,
u
f [u ] = 10
Stack
u
v
w
x
y
z
96
Algorithmen und Datenstrukturen
2.2.1.2 Eigenschaften von DFS
Laufzeit
Die Laufzeit von DFS für einen Graphen G = (V , E ) mit n Knoten und m Kanten ist
O ( n + m) = O ( V + O E ) .
Predecessor-Graph
Gegeben ist G = (V , E ) . Der Predecessor-Graph von G wird definiert zu G p = (V , E p )
mit E p = {( parent[ s ], s ) s ∈ V ∧ parent[ s ] ≠ nil }.
Der Predecessor-Graph von DFS bildet einen „Depth-First Forest“, der sich aus
mehreren „Depth-First Trees“ zusammensetzen kann. Die Kanten von E p nennt
man die Baumkanten. Im Algorithmus zur Tiefensuche 58 ist dafür eine Zeitmessung
eingeführt:
- b[v]: Beginn der Suche
- f[v]: Ende der Suche
Nach Anwendung von DFS auf einen Graphen G gilt für 2 beliebige Knoten u und v
eine der 3 Bedingungen.
1. Die Zeitintervalle b[u] … f[u] und b[v] … f[v] sind disjunkt und weder v noch u sind Nachfahren des
jeweils anderen Knoten im DF Forest
2. Ein Intervall b[u] … f[u] ist vollständig im Intervall b[v] … f[v] enthalten, und u ist ein Nachfahre von v
in einem Baum des DF Forest.
3. Ein Intervall b[v] … f[v] ist vollständig im Intervall b[u] … f[u] enthalten, und v ist ein Nachfahre von v
in einem Baum des DF Forest.
Darau folgt direkt
Knoten v ist genau dann ein Nachfahre von Knoten u im DF Forest, wenn gilt: f[u] < f[v] < b[v] < b[u]
Im DF Forest eines gerichteten oder ungerichteten Graphen G = (V , E ) ist ein Knoten
v genau dann ein Nachfahre von Knoten u, falls zu der Zeit, wenn er entdeckt wird
(b[u]), ein Pfad von u nach v existiert, der ausschließlich unentdeckte Knoten
enthält.
Falls der Graph nicht zusammenhängend ist, dann erfordert die Verarbeitung aller
Knoten (und Kanten) einige Aufrufe von DFS, jeder Aufruf erzeugt einen Baum. Die
ganze Sammlung ist ein depth first spanning forest.
58
vgl. 2.2.1.1
97
Algorithmen und Datenstrukturen
2.2.1.3 Kantenklassenfikation mit DFS
Die Tiefensuche kann für eine Kantenklassifikation eines Graphen G = (V , E )
verwendet werden, mit der wichtige Informationen über G gesammelt werden
können.
Defintion von 4 Kantentypen (, die bei einem DFS-Durchlauf für G produziert
werden):
1. Tree Edges (Baumkanten) sind Kanten des DF Forest G p . Die Kante (u,v) ist eine
Baumkante, falls v über die Kante (u,v) entdeckt wurde.
2. Back Edges (Rückwärtskanten) sind Kanten (u,v), die einen Knoten u mit einem Vorfahren v
in einem DF Forest verbinden. Rückwärtskanten verbinden mit den Vorfahren
3. Forward Edges (Vorwärtskanten) sind Kanten (u,v), die nicht zum DF Forest gehören und
einen Knoten u mit einem Nachfolger v verbinden. Vorwärtskanten verbinden mit den
Nachkommen.
4. Cross Edges (Querkanten) sind die anderen (nicht direkt verwandten) Kanten.
Der angegebene DFS Algorithmus kann so modifiziert werden, dass die Kanten
entsprechend der vorstehenden Aufzählung klassifiziert werden.
Bsp.
1. Gegeben ist folgender ungerichteter Graph
A
B
D
E
C
Der Graph wird mit folgendem Algorithmus zur Tiefensuche untersucht:
void dfs( Vertex v)
{
v.visited = true;
for each Vertex w adjacent to v
if (!w.visited) dfs(w);
}
Abb.: Schablone zu depth-first-search in Pseudocode
Für jeden Knoten ist das Feld visited mit false initialisiert. Bei den rekursiven
Aufrufen werden nur nicht besuchte Knoten aufgesucht.
Start: Knoten A, der als besucht markiert wird.
Rekursiver Aufruf dfs(B), B wird als „besucht“ markiert.
Rekursiver Aufruf dfs(C), C wird als „besucht“ markiert.
Rekursiver Aufruf dfs(D), A und B sind markiert, C ist benachbart aber markiert, Rückkehr zu dfs(C), B
ist nun Nachbar aber markiert, nicht besucht (von C aus) ist der Nachbar E.
98
Algorithmen und Datenstrukturen
Rekursiver Aufruf dfs(E), E wird als „besucht“ markiert. A und C werden ignoriert, Rückkehr zu dfs(C),
Rückkehr zu dfs(B), A und B werden ignoriert, dfs(A) ignoriert D und E und kehrt zurück.
Diese Schritte kann man graphisch mit einem „depth-first spanning treee“
dokumentieren.
A
B
C
D
E
Jede Kante (v,w) im Graph ist im Baum present. Falls ((v,w) bearbeitet wird und w nicht markiert bzw.
(w,v) bearbeitet wird und v ist nicht markiert, dann wird das mit einer Baumkante markiert
Falls (v,w) bearbeitet wird und w ist schon markiert bzw. (w,v) wird bearbeitet und v ist schon markiert,
dann wird eine gestrichelte Linie aufgezeichnet (Rückwärtskante 59). Der Baum simuliert die Präorder
Traverse.
Die DFS-Klassifizierung eines ungerichteten Graphen G = (V , E ) ordnet jeder Kante
zu G entweder in die Klasse Baumkante oder in die Klasse Rückwärtskante ein.
2. Gegeben ist der folgende gerichtete Graph
A
B
D
C
E
G
F
J
I
Start: Knoten B
Von B aus Besuch der Knoten B, C, A, D, E und F
Restart aus einem noch nicht besuchten Knoten, z.B. H
Rückwärtskanten: (A,B) (I,H)
forward edges:
(C,D) (C,E) 60
cross edges:
(F,C) (G,F)
59
60
H
kein Bestandteil des Baums
führen von einem Baumknoten zu einem Nachfolger
99
Algorithmen und Datenstrukturen
B
H
C
G
F
A
J
I
D
E
Abb. Tiefensuche
Ein Nutzen der Tiefensuche ist die Überprüfungsmöglichkeit auf Zyklen. Gerichtete
Graphen sind dann und nur dann azyklisch, wenn sie keine Rückwärtskanten
besitzen. Der vorstehende Graph besitzt Rückwärtskanten und ist deshalb azyklisch.
100
Algorithmen und Datenstrukturen
2.2.1.4 Zusammenhangskomponenten
1. Connected
Ein ungerichteter Graph G = (V , E ) heißt genau dann zusammenhängend
(connected), wenn es für ein Knotenpaar (v, v' ) ∈ V einen Weg von v nach v' gibt.
Ein gerichteter Graph G = (V , E ) heißt stark zusammenhängend, wenn es einen Weg
von jedem Knoten zu jedem anderen Knoten im Graphen gibt.
Eine Komponente eines ungerichteten Graphen ist ein maximaler Teilgraph in dem
jeder Knoten von jedem anderen Knoten aus, erreichbar ist. Die Komponenten
können beim Traversieren mittels Tiefen- oder Breitensuche ermittelt werden.
Ein ungerichteter Graph
G = (V , E )
heißt zweifach zusammenhängend
(biconnected), wenn nach dem Entfernen eines beliebigen Knoten v aus G der
verbliebene Graph G − v zusammenhängend ist.
Eine zweifache Zusammenhangskomponente (biconnected component) eines
ungerichteten Graphen ist ein maximaler, zweifach zusammenhängender
Untergraph. In einem zweifach zusammenhängenden Graphen kann man einen
beliebigen Knoten samt allen inzidenten Kanten entfernen, ohne dass der Graph
zerfällt.
2. Artikulationspunkte
Falls ein Graph nicht zweifach zusammenhängend ist, werden die Knoten, deren
Entfernung den Graphen trennen würden, Artikulationspunkte genannt.
B
A
C
D
F
G
E
Die Entfernung vom Knoten C trennt den Knoten G vom Graphen
Die Entfernung vom Knoten D trennt die Knoten E und F vom Graphen
Abb.: Ein Graph mit den Artikulationspunkten C und D
Kritische Knoten und kritische Kanten
Kanten und Knoten eines ungerichteten Graphen sind dann kritisch, wenn sich bei
ihrer Entfernung die Anzahl der Komponenten des Graphen erhöht.
101
Algorithmen und Datenstrukturen
a
b
a
c
b
d
c
d
kritische Kante
kritische Knoten
e
f
e
g
f
g
Abb.:
Zur Bestimmung der Artikulationspunkte werden die Knoten des Graphen während
der Tiefensuche in „preorder“ Reihenfolge durchnummeriert und bei der Rückkehr
aus der Tiefensuche jeder Kante die kleinste Nummer aller über die Kante
erreichbaren Knoten zugewiesen.
Bsp.
a
b
c
d
e
f
g
Eine Kante ist genau dann kritisch, wenn die kleinste über sie erreichbare
Knotennummer größer ist als die Nummer des Knotens von dem aus si während der
Tiefensuche traversiert wird.
Ein Knoten (mit Ausnahme des Startknoten) ist genau dann kritisch, wenn bei der
Tiefensuche für mindestens eine der von ihm ausgehenden Kanten die kleinste über
diese während der Tiefensuche erreichbare Knotennummer größer oder gleich der
Nummer dieses Knoten ist,
Der Startknoten der Tiefensuche ist genau dann kritisch, wenn von der Wurzel des
bei der Tiefensuche generierten spannenden Baums mehr als eine Kante ausgeht.
102
Algorithmen und Datenstrukturen
Algorithmus
Mit Hilfe der DFS kann ein Algorithmus (mit linearer Laufzeit) zur Bestimmung aller
Artikulationspunkte in einem zusammenhängenden Graphen gefunden werden.
1. Start mit irgendeinem Knoten und Ausführen der Tiefensuche, Durchnummerieren der Knoten, wie
sie bei der Suche anfallen (preorder-number Num(v).
2. Für jeden Knoten im „depth-first search spanning tree“, der von v mit 0 oder mehr Baumkanten und
dann möglicherweise über eine Rückwärtskante erreichbar ist, Berechnung des am niedrigsten
nummerierten Knoten ( Low(v) ).
3. Low(v ) ist das Minimum von
1.
Num(v)
2. das niedrigste Num(w) unter allen Rückwärtskanten (v,w)
3. das niedrigste
Low( w) unter allen Baumkanten (v,w).
Low kann nur bewertet werden, wenn alle Kinder von v bei der Berechnung von Low(v)
berücksichtigt sind, d.h. eine Postorder-Traverse ist nötig. Für jede Kante (v,w) kann bestimmt
werden, ob eine Baumkante oder eine Rückwärtskante vorliegt ( Num(v ) bzw. Num(w) ). Low(v )
kann also leicht über das Durchlaufen der Adjazenzliste von v über das Feststellen des Minimums
errechnet werden. Die Laufzeit liegt bei O ( E + V ).
A 1/1
B 2/1
C 3/1
D 4/1
G 7/7
E 5/4
F 6/4
Abb.: DFS-Baum mit Num und Low
Bestimmen der Artikulationspunkte
- die Wurzel ist dann und nur dann ein Artikulationspunkt, wenn sie mehr als ein Kind besitzt. Das
Entfernen der Wurzel im Bsp. kettet lediglich die Wurzel aus.
- irgendein anderer Knoten v ist dann und nur dann Artikulationspunkt, wenn v ein Kind w hat, so dass
Low( w) ≥ Num(v) . Im Bsp. bestimmt der Algorithmus zu Artikulationspunkten C und D.
Low( E ) ≥ Num( D) . Es gibt nur einen Weg von E aus, das ist der Weg durch D
- C ist ein Artikulationspunkt, weil Low(G ) ≥ Num(C ) ist.
- D hat Kind E,
103
Algorithmen und Datenstrukturen
C 1/1
D 2/1
E 3/2
G 7/7
A 5/1
F 4/2
B 6/1
Abb.: depth-first Baum, falls Start der DFS im Punkt C
Der Algorithmus kann implementiert werden durch
1. Ausführen einer Preorder-Traverse zur Berechnung von Num
2. Ausführen einer Postorder-Traverse zur Berechnung von Low
3. Überprüfen, welche Knoten Artikulationspunkte sind.
Der erste Durchgang wird beschrieben durch folgenden Algorithmus:
void assignNum(Vertex v)
{
v.num = counter++; v.visited = true;
for each Vertex w adjacent to v
if (!w.visited)
{ w.parent = v;
assignNum(w);
}
}
Abb.: Pseudocode für Zuweisen Num 61
Der zweite und dritte Durchgang mit Postorder-Traversen nimmt dann folgende
Gestalt an:
void assignLow( Vertex v)
{
v.low = v.num; // Regel 1
for each Vertex w adjacent to v
{
if (w.num > v.num) // Vorwärts-Kante
{ assignLow(w);
if (w.low >= v.num )
System.out.println(v + “ ist ein Artikulationspunkt”);
v.low = min(v.low,w.low);
// Regel 3
}
else if (v.parent != w)
// Rückwärtskante
v.low = min(v.low, w.num);
// Regel 2
}
}
Abb.: Pseudocode für Berechnung von Low und Test auf Artikulationspunkte 62
61
62
vgl. Weiss, Mark Allen: Data Structures and Algorithms Analysis in Java, Second Edition, Seite 361
vgl. Weiss, Mark Allen: Data Structures and Algorithms Analysis in Java, Second Edition, Seite 362
104
Algorithmen und Datenstrukturen
3. Starke Zusammenhangskomponenten
1. Zwei Knoten v, w eines gerichteten Graphen G = (V , E ) heißen stark verbunden, falls es einen
Weg von v nach w und von w nach v gibt.
2. Eine starke Zusammenhangskomponente ist ein Untergraph von G mit maximaler Knotenzahl,
in der alle Paare von Knoten stark verbunden sind
3. Eine starke Zusammenhangskomponente (strongly connected components) eines gerichteten
Graphen G = (V , E ) ist eine maximale Knotenmenge , so dass für jedes Paar gilt: Der Knoten u
kann von v aus über einen Pfad, der vollständig zu C gehört, erreicht werden.
4. Ein gerichteter Graph wird als stark zusammenhängend bezeichnet, wenn er aus einer einzigen
starken
Zusammenhangskomponenten
besteht.
Besitzt
G
genau
eine
starke
Zusammenhangskomponente, so ist G stark verbunden.
Algorithmus für Strongly-Connected Components (SCC(G))
1. Berechne die “finishing time” f für jeden Knoten mit DFS(G).
(
Berechne
den
T
transponierten
Graphen
GT = V , ET
)
G = (V , E ) , wobei
E = {(v, u ) | (u , v ) ∈ E}. E besteht also aus den umgedrehten Kanten von G . G und G T haben
2.
von
T
die gleichen starken Zusammenhangskomponenten.
T
3. Berechne dfs (G ) , wobei die Knoten in der Reihenfolge ihrer „finishing-time“-Einträge aus der
dfs(G ) -Berechnung in Schritt 1 (fallend) in der Hauptschleife von dfs (G T ) abgearbeitet wurden.
Bsp.: Gegeben ist
A
B
D
C
E
G
F
J
H
I
Abb.: Ein gerichteter Graph G
105
Algorithmen und Datenstrukturen
A, 3
B, 6
D, 2
C, 4
G, 10
F, 5
E, 1
Abb.: G
T
H, 9
J, 8
I, 7
I
durchnummeriert in Postorder-Traversal von G
Eine Tiefensuche zu G T wird mit dem Knoten begonnen, der die größte Nummer
besitzt (Knoten G). Das führt aber nicht weiter, die nächste Suche wird bei H gestartet
und I bzw. J aufgesucht. Der nächste Aufruf startet bei B, besucht A, C und F.
Danach werden dfs (D) und schließlich dfs (E ) aufgerufen. Es ergibt sich folgender
depth-first spanning forest.
G
H
B
D
E
A
I
C
J
F
Abb.: Tiefensuche mit G - starke Komponenten sind {G}, {H , I , J }, {B, A, C , F }, {D}, {E}
T
Jeder dieser Bäume im depth-first
zusammenhängende Komponente
spanning
tree
bildet
eine
stark
Komponenten-Graph
Der Komponenten-Graph G SCC = (V SCC , E SCC ) wird folgendermassen aufgebaut:
4. Schwach zusammenhängende Komponenten eines gerichteten Graphen (weakly
connected components)
Die schwach zusammenhängenden Komponenten eines gerichteten Graphen
entsprechen den Komponenten jenes Graphen der entsteht, wenn sämtliche
gerichtete Kanten durch ungerichtete Kanten ersetzt werden.
106
Algorithmen und Datenstrukturen
5. Erreichbarkeit (reachability)
Für jedes Paar Knoten in einem Graph, z.B. (vi , v j ) , ist v j erreichbar von vi , wenn
ein direkter Pfad von vi nach v j besteht. Dies definiert die Erreichbarkeitsrelation R .
Für jeden Knoten vi bestimmt die Tiefensuche die Liste aller Knoten, die von vi aus
erreichbar sind. Wendet man die Tiefensuche auf jeden Knoten des Graphen an, gibt
es eine Reihe von Erreichbarkeitslisten, die die Relation R ausmachen. Diese
Relation kann mit Hilfe einer (n × n ) -Erreichbarkeitsmatrix beschrieben werden, die
eine 1 an der Stelle (i, j ) hat (vorgesehen für vi Rv j ).
Bsp.: Für den Graphen
A
B
C
D
sind Erreichbarkeitsliste bzw. Erreichbarkeitsmatrix
A:
B:
C:
D:
A B C B
B D
C B D
D
1
0
0
0
1
1
1
0
1
0
1
0
1
1
1
1
Die Erreichbarkeitsmatrix dient zur Bestimmung, ob es einen Pfad zwischen 2
Knoten gibt.
Tiefensuche-Algorithmus für das Verbindungsproblem
Der folgende Algorithmus 63 bestimmt alle Knoten, die mit einem gegebenen
verbunden sind:
typedef vertex<string> node;
typedef node::vertex_list nodeList; // Knotenliste
void findReachable(node& quelle, nodeList& reachable)
{
// finde alle Knoten, die von quelle aus erreichbar sind
// mit Hilfe der Tiefensuche
reachable.insert(&quelle);
nodeList::iterator itr = quelle.neighbors().begin(),
stop = quelle.neighbors().end();
for( ; itr != stop; ++itr)
if (reachable.count(*itr) == 0) findReachable(**itr, reachable);
}
63
/pgc/pr52_144/erreichbar.cpp
107
Algorithmen und Datenstrukturen
2.2.1.5 Topologisches Sortieren mittels Tiefensuche
Gegeben: Ein gerichteter, azyklischer Graph (directed acyclic graph, DAG)).
Eine topologische Sortierung eines DAG ist eine (lineare) Ordnung aller Knoten, so
dass für alle Kanten (u , v) des Graphen gilt: Der knoten (u ) erscheint in der Ordnung
vor v .
Eine topologische Sortierung eines gerichteten azyklischen Graphen kann man sich
als Aufreihung aller seiner Knoten entlang einer horizontalen Linie vorstellen, wobei
alle gerichteten Kanten von links nach rechts führen.
Bsp.: Gegeben ist der folgende, gerichtete und azyklische Graph:
2
10
1
4
9
6
8
3
7
5
Im vorliegenden Fall zeigt die Ausgabe
7
9
1
2
4
6
3
5
8
10
an, daß eine lineare Ordnung erreicht wurde.
Abb.:
Man kann feststellen, daß das vorliegende Ergebnis dem Eintragen der
vorgängerlosen Elemente in einen Stapel entspricht. Allerdings muß der Stapel in
umgekehrter Reihenfolge für den Erhalt der linearen Ordnung interpretiert werden.
Der Algorithmus, der zum topologischen Sortieren führt, ist offensichtlich rekursiv.
Der folgende Pseudo-Code berechnet eine topologische Sortierung für einen DAG:
1. Starte DFS und berechne die „finishing time“ (f) für alle Knoten
2. Wenn ein Knoten abgearbeitet ist, dann füge ihn am Anfang der (Ordnungs-) Liste ein.
3. Gib die Liste zurück
108
Algorithmen und Datenstrukturen
Bsp.: Gegeben ist der folgende gerichtete Graph
0
1
2
3
4
5
6
7
Gib die topologische Sortierung mit Hilfe der Tiefensuche für diesen Graphen an.
Jeder Knoten erhält eine Farbe 64.
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
0
1
2
3
5
6
7
8
4
64
- weiße Knoten wurden noch nicht behandelt
- schwarze Knoten wurden vollständig abgearbeitet
- Graue Knoten sind noch in Bearbeitung
109
Algorithmen und Datenstrukturen
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
0
1
2
3
7
8
7
8
7
8
7
8
7
6
8
4
5
6
110
7
Algorithmen und Datenstrukturen
0
1
2
7
3
6
8
4
5
6
7
0
1
2
3
7
6
8
4
5
6
7
0
1
2
3
7
6
8
4
5
6
7
0
1
2
3
6
5
7
8
4
5
6
7
0
1
2
3
6
5
7
8
4
4
4
5
6
111
7
Algorithmen und Datenstrukturen
0
1
2
3
7
6
5
8
3
4
4
5
6
7
0
1
2
3
6
5
7
8
2
3
4
4
5
6
7
0
1
2
3
7
1
6
5
8
2
3
4
4
5
6
7
Eine umgekehrte postorder-Beziehung entspricht einer topologischen Sortierung.
Bsp.:
a
b
c
d
e
f
g
h
i
Eine in a beginnende Tiefensuche liefert die „postorder“-Reihenfolge: i h e f d a
g b c . Die umgekehrte Reihenfolge c b g a d f c h i ist topologisch sortiert.
Eine bei c brginnrnde Tiefensuche liefert die „postorder“-Reihenfolge i g c h e f
d b a. Die umgekehrte Reihenfolge a b d f e h c g a ist ebenfalls topologisch
sortiert.
112
Algorithmen und Datenstrukturen
2.2.2 Breitensuche (breadth-first search)
Die Suche beginnt beim Startknoten, danach werden die Nachbarn der Startknoten
besucht, danach die Nachbarn der Nachbarn usw. Dadurch kann die Knotenmenge –
entsprechend ihrer minimalen Anzahl von Kanten zum Startknoten – in Level
unterteilt werden. In Level 0 befindet sich nur der Startknoten, Level 1 besteht aus
allen Nachbarn des Startknoten, usw.
Algorithmus
Eingaben sind ein Graph G=(V,E) und ein Startknoten s. Zu jedem Knoten speichert
man einige Daten:
color[v]: repräsentiert den aktuellen Bearbeitungsstatus
weiß = unbearbeitet / unbesucht
schwarz = abgearbeitet (v und alle Nachbarn von v wurden besucht)
grau = in Bearbeitung (v wurde besucht, kann aber noch unbesuchte Nachbarn haben
p[v]: Vorgänger (predecessor) von v
d[v]: Distanz zum Startknoten bzgl. der minimalen Kantenzahl
Zum Speichern der Knoten, die in Bearbeitung sind, wird eine Warteschlange Q (FIFO) verwendet.
u ∈ V [G ] − {s}
do color[u ] ← WHITE
d [u ] ← ∞
p[u ] ← NIL
color[ s ] ← GRAY
d [ s] ← 0
p[ s ] ← NIL
Q←s
while Q ≠ 0
// u ist erstes Element in Q
do u ← first[Q ]
// Nachbarn von u
for v ∈ Adj[u ]
do if color[v] ← WHITE
then color[v ] ← GRAY
d [v] ← d [u ] + 1
p[v] ← u
for each vertex
ENQUEUE(Q,v)
DEQUE(Q)
color[v] ← BLACK
// füge in Q ein
// lösche erstes Element aus Q
Komplexität
Das Initialisieren der Arrays dauert insgesamt O( | V | ). Die Operationen auf der Liste
(Einfügen und Löschen) und den Arrays brauchen konstante Zeit, insgesamt O( | V | ).
Das Durchsuchen der Adjazensliste dauert O( | E | ). Damit ergibt sich eine
Gesamtlaufzeit von O( | V | + | E | ).
113
Algorithmen und Datenstrukturen
BFS-Baum
Die Breitensuche konstruiert einen Baum, der die Zusammenhangskomponente des
Startknotens aufspannt. Der Weg im Baum vom Startknoten (Wurzel) zu den
Nachfolgern entspricht dem kürzesten Weg bzgl. der Kantenzahl im Graphen. Die
Levelnummer des Knotens entspricht der Höhe im Baum.
Bsp.: Breitensuche im ungerichteten Graphen
Anfangsschritt: für alle v ∈ V : color[v] ← WHITE , d [v ] = ∞ , p[v] ← NIL
r
s
t
u
v
w
x
y
1. Schritt: Startknoten wird grau markiert, Q = (s), d[s] = 0
r
s
t
u
v
w
x
y
2. Schritt: Q = (w,r), Level 0 abgearbeitet
r
s
t
u
v
w
x
y
r
s
t
u
v
w
x
y
t
u
3. Schritt: Q = (r,t,x)
4. Schritt: Q = (t,x,v), Level 1 abgearbeitet
r
s
v
w
x
y
114
Algorithmen und Datenstrukturen
5. Schritt: Q = (x,v,u)
r
s
v
w
t
u
x
y
6. Schritt: Q = (v,u,y)
r
s
v
w
t
u
x
y
7. Schritt: Q = (u,y)
r
s
v
w
r
s
v
w
t
u
x
y
8. Schritt: Q = (y)
t
u
x
y
9. Schritt: Q = (), Level 3 abgearbeitet
r
s
v
w
t
u
x
y
115
Algorithmen und Datenstrukturen
10. Schritt:
1
0
2
3
r
s
t
u
v
w
x
y
2
1
2
3
2.2.3 Implementierung
In der Klasse Graph sind Tiefensuche (Methode traverseDFS) und Breitensuche
(traverseBFS) implementiert 65.
import java.util.*;
/** Graphrepräsentation. */
/** Repräsentiert einen Knoten im Graphen. */
class Vertex
{
Object key = null; // Knotenbezeichner
LinkedList edges = null; // Liste ausgehender Kanten
/** Konstruktor */
public Vertex(Object key)
{ this.key = key; edges = new LinkedList(); }
/** Ueberschreibe Object.equals-Methode */
public boolean equals(Object obj)
{
if (obj == null) return false;
if (obj instanceof Vertex) return key.equals(((Vertex) obj).key);
else return key.equals(obj);
}
/** Ueberschreibe Object.hashCode-Methode */
public int hashCode()
{ return key.hashCode(); }
}
/** Repraesentiert eine Kante im Graphen. */
class Edge
{
Vertex dest = null; // Kantenzielknoten
int weight = 0; // Kantengewicht
/** Konstruktor */
public Edge(Vertex dest, int weight)
{
this.dest = dest; this.weight=weight;
}
}
class GraphException extends RuntimeException
{
public GraphException( String name )
{
super( name );
}
}
public class Graph
65
vgl. pr52220
116
Algorithmen und Datenstrukturen
{
protected Hashtable vertices = null; // enthaelt alle Knoten des Graphen
/** Konstruktor */
public Graph() { vertices = new Hashtable(); }
/** Fuegt einen Knoten in den Graphen ein. */
public void addVertex(Object key)
{
if (! vertices.containsKey(key))
// throw new GraphException("Knoten existiert bereits!");
vertices.put(key, new Vertex(key));
}
/** Fuegt eine Kante in den Graphen ein. */
public void addEdge(Object src, Object dest, int weight)
{
Vertex vsrc = (Vertex) vertices.get(src);
Vertex vdest = (Vertex) vertices.get(dest);
if (vsrc == null)
throw new GraphException("Ausgangsknoten existiert nicht!");
if (vdest == null)
throw new GraphException("Zielknoten existiert nicht!");
vsrc.edges.add(new Edge(vdest, weight));
}
/** Liefert einen Iterator ueber alle Knoten. */
public Iterator getVertices()
{ return vertices.values().iterator(); }
/** Liefert den zum Knotenbezeichner gehoerigen Knoten. */
public Vertex getVertex(Object key)
{
return (Vertex) vertices.get(key);
}
/** Liefert die Liste aller erreichbaren Knoten in Breitendurchlauf. */
public List traverseBFS(Object root)
{
LinkedList list = new LinkedList();
Hashtable d
= new Hashtable();
Hashtable pred = new Hashtable();
Hashtable color = new Hashtable();
Integer gray = new Integer(1);
Integer black = new Integer(2);
Queue q = new Queue();
Vertex v, u = null;
Iterator eIter = null;
//v = (Vertex)vertices.get(root);
color.put(root, gray);
d.put(root, new Integer(0));
q.enqueue(root);
while (! q.isEmpty())
{
v = (Vertex) vertices.get(((Vertex)q.firstEl()).key);
eIter = v.edges.iterator();
while(eIter.hasNext())
{
u = ((Edge)eIter.next()).dest;
// System.out.println(u.key.toString());
if (color.get(u) == null)
{
color.put(u, gray);
d.put(u, new Integer(((Integer)d.get(v)).intValue() + 1));
pred.put(u, v);
q.enqueue(u);
}
}
q.dequeue();
list.add(v);
color.put(v, black);
}
117
Algorithmen und Datenstrukturen
return list;
}
/** Liefert die Liste aller erreichbaren Knoten im Tiefendurchlauf. */
public List traverseDFS(Object root)
{
// Loesungsvorschlag: H. Auer
LinkedList list = new LinkedList();
// Hashtable d
= new Hashtable();
// Hashtable pred = new Hashtable();
Hashtable color = new Hashtable();
Integer gray = new Integer(1);
Integer black = new Integer(2);
Stack s = new Stack();
Vertex v, u = null;
Iterator eIter = null;
//v = (Vertex)vertices.get(root);
color.put(root, gray);
// d.put(root, new Integer(0));
s.push(root);
while (! s.empty())
{
v = (Vertex) vertices.get(((Vertex)s.peek()).key);
eIter = v.edges.iterator(); u = null; Vertex w;
while(eIter.hasNext())
{
w = ((Edge)eIter.next()).dest;
// System.out.println(u.key.toString());
if (color.get(w) == null) { u = w; break; }
}
if (u != null) { color.put(u, gray); s.push(u); }
else {
v = (Vertex) s.pop();
list.add(v);
color.put(v, black);
}
}
return list;
}
}
Anstatt einen Stapel explizit in die Tiefensuche einzubeziehen, kann man
Tiefensuche rekursiv so formulieren:
LinkedList liste = new LinkedList();
Hashtable color = new Hashtable();
Integer gray = new Integer(1);
Integer black = new Integer(2);
// Iterator eIter = null;
public List traverseDFSrek(Object root)
{
// LinkedList list = new LinkedList();
// Hashtable d
= new Hashtable();
// Hashtable pred = new Hashtable();
// Hashtable color = new Hashtable();
// Integer gray = new Integer(1);
// Integer black = new Integer(2);
// Stack s = new Stack();
Vertex v = (Vertex) root; Vertex u = null;
Iterator eIter = null;
//v = (Vertex)vertices.get(root);
color.put(root, gray);
// d.put(root, new Integer(0));
// s.push(root);
// while (! s.empty())
// {
118
Algorithmen und Datenstrukturen
// v = (Vertex) vertices.get(((Vertex)s.pop()).key);
// liste.add(v); // color.put(v,black);
eIter = v.edges.iterator();
while(eIter.hasNext())
{
u = ((Edge)eIter.next()).dest;
// System.out.println(u.key.toString());
if (color.get(u) == null)
{
color.put(u, gray);
traverseDFSrek(u);
}
}
liste.add(v);
// s.pop();
// list.add(v);
color.put(v, black);
//}
return liste;
}
Abb.: Durchläufe zur Breiten- bzw. Tiefensuche
119
Algorithmen und Datenstrukturen
2.3 Topologischer Sort
Sortieren bedeutet Herstellung einer totalen (vollständigen) Ordnung. Es gibt auch
Prozesse zur Herstellung von teilweisen Ordnungen 66, d.h.: Es gibt eine Ordnung für
einige Paare dieser Elemente, aber nicht für alle.
Die Kanten eines gerichteten Graphen bilden eine Halbordnung (die
Ordnungsrelation ist nur für solche Knoten definiert, die auf dem gleichen Pfad
liegen).
y
x< y
y < z y<z
x
z
Eine strenge Halbordnung ist irreflexiv und transitiv ( x < y ∧ y < z ⇒ x < z ).
Topologisches Sortieren bringt die Kanten eines gerichteten, zyklenfreien Graphen in
eine Reihenfolge, die mit der Halbordnung verträglich ist.
In Graphen für die Netzplantechnik ist die Feststellung partieller Ordnungen zur
Berechnung der kürzesten (und längsten) Wege erforderlich.
Bsp.: Die folgende Darstellung zeigt einen Netzplan zur Ermittlung des kritischen
Wegs. Die einzelnen Knoten des Graphen sind Anfangs- und Endereignispunkte der
Tätigkeiten, die an den Kanten angegeben sind. Die Kanten (Pfeile) beschreiben die
Vorgangsdauer und sind Abbildungen binärer Relationen. Zwischen den Knoten liegt
eine partielle Ordnungsrelation.
Bestelle A
50 Tage
Baue B
1
Teste B
4
20 Tage
Korrigiere Fehler
2
25 Tage
15 Tage
3
Handbucherstellung
60 Tage
Abb. : Ein Graph der Netzplantechnik
Zur Berechnung des kürzesten Wegs sind folgende Teilfolgen, die partiell geordnet sind, nötig:
1 -> 3:
50 Tage
1->4->2->3: 60 Tage
1->4->3:
80 Tage (kürzester Weg)
66
vgl. 1.2.2.2
120
Algorithmen und Datenstrukturen
Eindeutig ist das Bestimmen der topologischen Folgen nicht. Zu dem folgenden Graphen
2
1
4
3
kann es mehrere topologische Folgen geben.Zwei dieser topologischen Folgen sind
1
2
1
3
3
4
2
4
Abb.:
Bezugspunkt zur Ableitung eines Algorithmus für den topologischen Sort ist ein
gerichteter, azyklischer Graph, z.B.
0
1
1
2
2
3
1
3
4
5
3
2
6
7
Über der Knotenidentifikationen ist zusätzlich die Anzahl der Vorgänger vermerkt.
Dieser Zähler wird in die Knotenbeschreibung aufgenommen. Der Zähler soll
festhalten, wie viele unmittelbare Vorgänger der Knoten hat. Hat ein Knoten keine
Vorgänger, dann wird der Zähler auf 0 gesetzt.
Alle Knoten, die den Eingangsgrad 0 aufweisen, werden in einem Stapel oder in
einer Schlange abgelegt.
Ist z.B. die Schlange nicht leer, wird der Knoten, z.b. v, entfernt. Die Eingangsgrade
aller Knoten, die zu v benachbart sind, werden um eine Einheit erniedrigt.. Sobald
Eingangsgrade zu 0 werden, werden sie in die Schlange aufgenommen. Die
topologische Sortierung ist dann so gestaltet, wie die Knoten aus der Schlange
entfernt werden.
Damit kann der Algorithmus 67 durch folgende Pseudocode-Darstellung beschrieben
werden.
67 Der hier angegebene Algorithmus setzt voraus, dass der Graph in einer Adjazenzliste abgebildet ist,
Eingangsgrade berechnet wurdem und zusammen mit den Knoten abgespeichert wurden.
121
Algorithmen und Datenstrukturen
void topsort()
{
Queue<Vertex> q = new Queue<Vertex>();
int zaehler = 0;
Vertex v, w;
for each v
if (v.indegree 68 == 0)
q.enqueue(v);
while (!q.isEmpty())
{
v = q.dequeue();
zaehler++;
for each w adjacent to v
if (--w.indegree == 0)
q.enqueue(w);
}
if (zaehler >= anzahlKnoten)
throw new CycleFoundException;
}
Abb.: Pseudocode zur Durchführung einer topologischen Sortierung
Zur Bestimmung der gewünschten topologischen Folge wird mit den
Knotenpunktnummern begonnen, deren Zähler den Wert 0 enthalten. Sie verfügen
über keinen Vorgänger und erscheinen in der topologischen Folge an erster Stelle.
Schreibtischtest. Die folgende Tabelle soll anhand des folgenden Graphen
1
2
3
4
5
6
7
die Veränderung des Zählers für unmittelbare Vorgänger zeigen und über die
Knotenidentifikationen das Ein- bzw. Ausgliedern aus der Schlange (Queue) q.
Vertex
1
2
3
4
5
6
7
Enqueue
Dequeue
1
0
1
2
3
1
3
2
1
1
2
0
0
1
2
1
3
2
2
2
3
0
0
1
1
0
3
2
5
5
4
0
0
1
0
0
3
1
4
4
5
0
0
0
0
0
2
0
3,7
3
Abschätzung der Laufzeit (Komplexität): O( E + V
6
0
0
0
0
0
1
0
7
)
7
0
0
0
0
0
0
0
6
6
(, falls Adjazenzlisten benutzt
werden. Das ist einleuchtend, da man davon ausgehen kann
68
indegree (Eingangsgrad) ist der Zähler für die jeweilige Anzahl von Vorgängerknoten
122
Algorithmen und Datenstrukturen
- der Schleifenkörper wird einmal je Kante ausgeführt
- die Schlangenoperationen werden meistens einmal je Knoten ausgeführt
- der Zeitbedarf für die Initialisierung ist proportional zur Größe des Graphen
2.4 Transitive Hülle
Welche Knoten sind von einem gegeben Knoten aus erreichbar?
Gibt es Knoten, von denen aus alle anderen Knoten erreicht werden können?
Die Bestimmung der transitiven Hülle ermöglicht die Beantwortung solcher Fragen.
S. Warshall hat 1962 einen Algorithmus entwickelt, der die Berechnung der
transitiven Hülle über seine Adjazenzmatrix ermöglicht und nach folgenden Regeln
arbeitet:
Falls ein Weg existiert, um von einem Knoten x nach einem Knoten y zu gelangen, und ein Weg, um vom
Knoten y nach z zu gelangen, dann existiert auch ein Weg, um vom Knoten x nach dem Knoten z zu gelangen.
Bsp.: Der folgende Graph enthält gestrichelte Kanten, die die Erreichbarkeit
markieren
A
B
C
D
E
Die zu diesem Graphen errechnete transitive Hülle beschreibt die folgende
Erreichbarkeitsmatrix (Wegematrix):
1
0
0
1
0
1
1
0
1
0
1
1
1
1
1
0
0
0
1
0
1
1
0
1
1
2.4.1 Berechnung der Erreichbarkeit mittels Matrixmultiplikation
Eine häufig vorkommende Frage ist die nach dem Zusammenhang zweier Knoten.
Das kann man aus der Wegematrix 69 sofort ablesen. Die Wegematrix kann aus
Adjazenzmatrix und Kantenfolgen mit Matrixoperationen leicht bestimmt werden:
- In einem unbewerteten Graphen mit Adjazenzmatrix
aij
(r )
: Anzahl der Kantenfolgen von xi nach x j der Länge
nach Definition des Matrixprodukts)
69
A beschreibt A = A ⋅ A ⋅ A ⋅ ... ⋅ A = (aij )
vgl. 2.2.1.4
123
(r )
r (Beweis über vollständige Induktion r
Algorithmen und Datenstrukturen
- Ein gerichteter Graph ist genau dann azyklisch, wenn
A r = 0 für ein geeignetes r ≤ n = V , denn
in einem zyklischen Graphen gibt es Kantenfolgen beliebiger Länge, also A ≠ 0 und in einem
r
ayklischen Graphen hat eine Kantenfolge höchstens die Länge n − 1 also
- Die Wegematrix W ergibt sich aus der Adjazenzmatrix
n −1
∑A
r
A r = 0 für r ≥ n .
A , indem man
in
= A 0 + A1 + ... + A n −1 alle von 0 verschiedene Elemente setzt. A 0 ist die Einheitsmatrix.
r =0
Folgerung: Anstatt der Tiefensuche zur Ermittlung der Erreichbarkeitsmatrix kann
man den bekannten Algorithmus von Stephan Warshall benutzen.
Bsp.: Gegeben ist
A
B
C
D
Bestimme W
⎛0
⎜
⎜0
A=⎜
0
⎜
⎜0
⎝
1
0
1
0
1
0
0
0
0⎞
⎟
1⎟
0⎟
⎟
0 ⎟⎠
⎛0
⎜
⎜0
2
A =⎜
0
⎜
⎜0
⎝
1
0
1
0
1
0
0
0
0⎞ ⎛0
⎟ ⎜
1⎟ ⎜0
⋅
0⎟ ⎜0
⎟ ⎜
0 ⎟⎠ ⎜⎝ 0
1
0
1
0
1
0
0
0
0⎞ ⎛0
⎟ ⎜
1⎟ ⎜0
=
0⎟ ⎜0
⎟ ⎜
0 ⎟⎠ ⎜⎝ 0
0
0
0
0
0
0
0
0
1⎞
⎟
0⎟
1⎟
⎟
0 ⎟⎠
⎛0
⎜
⎜0
3
A =⎜
0
⎜
⎜0
⎝
1
0
1
0
1
0
0
0
0⎞ ⎛0
⎟ ⎜
1⎟ ⎜0
⋅
0⎟ ⎜0
⎟ ⎜
0 ⎟⎠ ⎜⎝ 0
0
0
0
0
0
0
0
0
1⎞ ⎛0
⎟ ⎜
0⎟ ⎜0
=
1⎟ ⎜0
⎟ ⎜
0 ⎟⎠ ⎜⎝ 0
0
0
0
0
0
0
0
0
1⎞
⎟
0⎟
0⎟
⎟
0 ⎟⎠
⎛0
⎜
⎜0
4
A =⎜
0
⎜
⎜0
⎝
1
0
1
0
1
0
0
0
0⎞ ⎛0
⎟ ⎜
1⎟ ⎜0
⋅
0⎟ ⎜0
⎟ ⎜
0 ⎟⎠ ⎜⎝ 0
0
0
0
0
0
0
0
0
1⎞ ⎛0
⎟ ⎜
0⎟ ⎜0
=
0⎟ ⎜0
⎟ ⎜
0 ⎟⎠ ⎜⎝ 0
0
0
0
0
0
0
0
0
0⎞
⎟
0⎟
0⎟
⎟
0 ⎟⎠
124
Algorithmen und Datenstrukturen
⎛1
⎜
⎜0
W = A 0 + A1 + A 2 + A 3 = ⎜
0
⎜
⎜0
⎝
1
1
1
0
1
0
1
0
1⎞
⎟
1⎟
1⎟
⎟
1⎟⎠
2.4.2 Warshalls Algorithmus zur Bestimmung der Wegematrix
Ist man an der Wegematrix interessiert, so kann man die Zahlen ungleich 0 durch 1
zusammenfassen, indem man die Adjazenzmatrix als logische Matrix auffasst und
die Addition und Multiplikation als logische Operatoren ∨ und ∧ 70.
void warshall :: transitive()
{
int i, j, k, n;
n = get_n();
for (k = 1; k <= n; k++)
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
adj[i][j] = (adj[i][j] || (adj[i][k] && adj[k][j]));
}
Durch Umordnung der Schleifenreihenfolge, Initialisierung der Diagonale von der
Ergebnismatrix C mit 1 (entspricht der Einheitsmatrix A 0 ) und der Überlagerung von
n −1
Ein- und Ausgabematrizen wird direkt
∑A
r
= A 0 + A1 + ... + A n −1 , die Potenzierung
r =0
und die Addition der Potenzen also vermieden.
void warshall (Bitmat a)
{
for (int k = 0; k < n; k++)
{
// Für alle I und j a[i][j] = 1, falls ein Pfad von I nach j
// existiert, der nicht durch
// irgendeinen Knoten >= k geht.
// beachte, ob es einen Pfad vom Knoten i nach j durch k gibt
a[k][k] = 1;
// ein Knoten ist von sich selbst erreichbar
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
a[i][j] |=a[i][k] & a[k][j];
// a[i][j] = 1, es gibt einen Pfad von i nach j, der nicht
// durch irgendeinen Knoten > k geht
}
}
Der Warshall-Algorithmus sucht nach allen möglichen Tripeln durch Erzeugen von 3
verschachtelten Schleifen mit den Laufvariablen i, j und k. Für jedes Paar (i, j ) wird
eine Kante (vi , v j ) hinzugefügt, falls es einen Knoten v k gibt, so dass (vi , v k ) und
(v
k
, v j ) in dem erweiterten Graphen sind.
Bsp.: Nimm an die Knoten v und w sind erreichbar über einen direkten Weg eines
gerichteten Pfads von 5 Knoten: v = x1 , x 2 , x3 , x 4 , x5 = w . Mit dreifach verschachtelten
Schleifen werden alle möglichen Knoten-Tripel betrachtet. Falls die Knoten x1 ...x5 in
70
in C++: | und &, vgl.pr54010.cpp in pr54_1
125
Algorithmen und Datenstrukturen
der angegebenen Reihenfolge erscheinen, dann ist x 2 identifiziert als der Knoten x1 ,
der x1 und x3 verbindet. Das führt zu der neuen Kante (x1 , x3 ) . x1 und x 4 haben x3
als verbundene Knoten, da der Verbindungsweg x1 und x3 in einem früheren
Stadium der Iteration gefunden wurde. So wird ( x1 , x 4 ) hinzugefügt, danach x1 und
x5 über x 4 mit ( x1 , x5 ) ergänzt.
2.4.3 Floyds Algorithmus zur Bestimmung der Abstandsmatrix
Falls in Warhalls Algorithmus die Diagonale mit 0 (Abstand eines Knoten zu sich
selbst) initialisiert wird, bei der Verkettung zweier Pfade & durch + (Summe der
Pfadlängen) und beim Finden eines neuen Pfads | durch min (Minimum vom neuen
und alten Abstand) ersetzen, erhält man Floyds Algorithmus zur Bestimmung der
Abstandsmatrix.
void floyd :: floydAl()
{
int i, j, k, n, dist = 0;
n = get_n();
for (k = 0; k < n; k++)
{
adj[k][k] = 0;
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
// min(adj[i][j],adj[i][k] + adj[k][j]);
if ((adj[i][k] != INFINITY) && (adj[k][j] != INFINITY))
if (adj[i][j] > (adj[i][k] + adj[k][j]))
{ adj[i][j] = (adj[i][k] + adj[k][j]); }
else;
}
}
126
Algorithmen und Datenstrukturen
2.5 Kürzeste Wege
2.5.1 Die Datenstrukturen Graph, Vertex, Edge für die Berechnung
kürzester Wege
Repräsentation von Knoten 71
// Basic info for each vertex.
struct Vertex
{
string
name;
// Vertex name
vector<Edge> adj;
// Adjacent vertices (and costs)
double
dist;
// Cost
Vertex
*prev;
// Previous vertex on shortest path
int
scratch; // Extra variable used in algorithm
// Konstruktor
Vertex( const string & nm ) : name( nm )
{ reset( ); }
void reset( )
{ dist = INFINITY; prev = NULL; scratch = 0; }
};
Über die Instanzvariable adj wird die Liste der benachbarten Knoten geführt, dist
enthält die Kosten, path den Vorgängerknoten vom kürzsten Pfad. Identifiziert wird
der Knoten durch einen Namen (Typ: string).
Repräsentation von Kanten
Die Kanten eines Graphen können Distanzen, Entfernungen, Gewichte, Kosten
aufnehmen. Jede Kante eines Graphen wird beschrieben über den Zielknoten und
das der Kante zugeordnete Gewicht.
struct Edge
{
// First vertex in edge is implicit
Vertex *dest;
// Second vertex in edge
double
cost;
// Edge cost
// Konstruktor
Edge( Vertex *d = 0, double c = 0.0 )
: dest( d ), cost( c ) { }
};
Die Klasse Graph zur Aufnahme von Algorithmen zur Berechnung kürzester Pfade
class Graph
{
private:
Vertex * getVertex( const string & vertexName );
void printPath( const Vertex & dest ) const;
void clearAll( );
typedef map<string,Vertex *,less<string> > vmap;
Graph( const Graph & rhs ) { }
const Graph & operator= ( const Graph & rhs )
{ return *this; }
vmap vertexMap;
public:
71
vgl. pr22859, pr55_1
127
Algorithmen und Datenstrukturen
Graph( ) { }
~Graph( );
void addEdge( const string & sourceName, const string & destName,
double cost );
void printPath( const string & destName ) const;
void unweighted( const string & startName );
void dijkstra( const string & startName );
void negative( const string & startName );
void acyclic( const string & startName );
};
2.5.2 Kürzeste Pfade in gerichteten, ungewichteten Graphen.
Lösungsbeschreibung. Die
ungewichteten Graphen G:
folgende
Abbildung
einen
gerichteten,
k2
k1
k4
k3
zeigt
k5
k7
k6
Abb.:
Ausgangspunkt ist ein Startknoten s (Eingabeparameter). Von diesem Knoten aus
soll der kürzeste Pfad zu allen anderen Knoten gefunden werden. Es interessiert nur
die Anzahl der Kanten, die in dem Pfad enthalten sind.
Falls für s der Knoten k3 gewählt wurde, kann zunächst am Knoten k3 der Wert 0
eingetragen werden. Die „0“ wird am Knoten k3 vermerkt.
k2
k1
k4
k3
k5
0
k6
k7
Abb.: Der Graph nach Markierung des Startknoten als erreichbar
Danach werden alle Knoten aufgesucht, die „eine Einheit“ von s entfernt sind. Im
vorliegenden Fall sind das k1 und k6. Dann werden die Knoten aufgesucht, die von s
zwei Einheiten entfernt sind. Das geschieht über alle Nachfolger von k1 und k6. Im
vorliegenden Fall sind es die Knoten k2 und k4. Aus den benachbarten Knoten von k2
und k4 erkennt man, dass k5 und k7 die kürzesten Pfadlängen von drei Knoten
besitzen. Da alle Knoten nun bewertet sind ergibt sich folgenden Bild:
128
Algorithmen und Datenstrukturen
k2
k1
1
2
k4
k3
k5
0
2
1
k7
k6
Abb.: Graph nach Ermitteln aller Knoten mit der kürzeszen Pfadlänge 2
Die hier verwendete Strategie ist unter dem Namen „breadth-first search“ 72 bekannt.
Die „Breitensuche zuerst“ berücksichtigt zunächst alle Knoten vom Startknoten aus,
die am weitesten entfernt liegenden Knoten werden zuerst ausgerechnet.
Übertragen der Lösungsbeschreibung in Quellcode. Zu Beginn sollte eine Tabelle mit
folgenden Einträgen vorliegen:
k
k1
k2
k3
k4
k5
k6
k7
bekannt
false
false
false
false
false
false
false
dk
∞
∞
0
∞
∞
∞
∞
pk
0
0
0
0
0
0
0
Die Tabelle überwacht den Fortschritt beim Ablauf des Algorithmus und führt Buch
über gewonnene Pfade. Für jeden Knoten werden 3 Angaben in der Tabelle
verwaltet:
- die Distanz dk des jeweiligen Knoten zu dem Startknoten s. Zu Beginn sind alle Knoten von s aus
unerreichbar ( ∞ ). Ausgenommen ist natürlich s, dessen Pfadlänge ist 0 (k3).
- Der Eintrag pk ist eine Variable für die Buchführung (und gibt den Vorgänger im Pfad an).
- Der Eintrag unter „bekannt“ wird auf „true“ gesetzt, nachdem der zugehörige Knoten erreicht wurde.
Zu Beginn wurden noch keine Knoten erreicht.
Das führt zu der folgenden Knotenbeschreibung:
struct Vertex
{
string
name;
//
vector<Edge> adj;
//
int
dist;
//
Vertex
*prev;
//
Vertex( const string & nm
{ reset( ); }
void reset( )
{ dist = INFINITY; prev =
Vertex name
Adjacent vertices (and costs)
Cost
Previous vertex on shortest path
) : name( nm )
NULL;}
};
Die Grundlage des Algorithmus kann folgendermaßen (in Pseudocode) beschrieben
werden:
void ungewichtet(Vertex s)
72
vgl. 2.2.2
129
Algorithmen und Datenstrukturen
/*
/*
/*
/*
1
2
3
4
*/
*/
*/
*/
/* 5 */
/* 6 */
/* 7 */
/* 8 */
/* 9 */
{
Vertex v, w;
s.dist = 0;
for (int aktDist = 0; aktDist < ANZAHL_KNOTEN; aktDist++)
for each v
if (!v.bekannt && v.dist == aktDist)
{
v.bekannt = true;
for each w benachbart_zu v
if (w.dist == INFINITY)
{
w.dist = aktDist + 1;
w.path = v;
}
}
}
Der Algorithmus deklariert schrittweise je nach Distanz (d = 0, d = 1, d= 2 ) die
Knoten als bekannt und setzt alle benachbarten Knoten von d w = ∞ auf die Distanz
d w = d + 1.
2
Die Laufzeit des Algorithmus liegt bei O( V ) . 73 Die Ineffizienz kann beseitigt werden:
Es gibt nur zwei unbekannte Knotentypen mit d v ≠ ∞ . Einigen Knoten wurde dv = aktDist
zugeordnet, der Rest zeigt dv = aktDist + 1. Man braucht daher nicht die ganze Tabelle, wie es
in Zeile 3 und Zeile 4 beschrieben ist, nach geeigneten Knoten zu durchsuchen. Am einfachsten ist
es, die Knoten in zwei Schachteln einzuordnen. In die erste Schachtel kommen Knoten, für die gilt: dv
= aktDist. In die zweite Schachtel kommen Knoten, für die gilt: dv = aktDist + 1. In Zeile 3 und
Zeile 4 kann nun irgendein Knoten aus der ersten Schachtel herausgegriffen werden. In Zeile 9 kann
w der zweiten Schachtel hinzugefügt werden. Wenn die äußere for-Schleife terminiert ist die erste
Schachtel leer, und die zweite Schachtel kann nach der ersten Schachtel für den nächsten Durchgang
übertragen werden.
Durch Anwendung einer Schlange (Queue) kann das Verfahren verbessert werden.
Am Anfang enthält diese Schlange nur Knoten mit Distanz aktDist. Benachbarte
Knoten haben die Distanz aktDist + 1 und werden „hinten“ an die Schlange
angefügt. Damit wird garantiert, daß zuerst alle Knoten mit Distanz aktDist
bearbeitet werden. Der verbesserte Algorithmus kann in Pseudocode so formuliert
werden:
/* 1 */
/* 2 */
/* 3 */
/*
/*
/*
/*
4
5
6
7
*/
*/
*/
*/
/* 8 */
/* 9 */
/*10 */
73
void ungewichtet(Vertex s)
{
Queue q; Vertex v, w;
q = new Queue();
q.enqueue(s); s.dist = 0;
while (!q.isEmpty())
{
v = q.dequeue();
v.bekannt = true; // Wird eigentlich nicht mehr benoetigt
for each w benachbart_zu v
if (w.dist == INFINITY)
{
w.dist = v.dist + 1;
w.path = v;
q.enqueue(w);
}
}
}
wegen der beiden verschachtelten for-Schleifen
130
Algorithmen und Datenstrukturen
Die folgende Tabelle zeigt, wie sich die Daten der Tabelle während der Ausführung
des Algorithmus ändern:
Anfangszustand
k
bekannt dk
k1
false
∞
k2
false
∞
k3
false
0
k4
false
∞
k5
false
∞
k6
false
∞
k7
false
∞
Q: k3
pk
0
0
0
0
0
0
0
k3 aus der Schlange
bekannt dk
pk
false
1
k3
false
0
∞
true
0
0
false
0
∞
false
0
∞
false
1
k3
false
0
∞
Q: k1, k6
k1 aus der Schlange
bekannt dk
pk
true
1
k3
false
2
k1
true
0
0
false
2
k1
false
0
∞
false
1
k3
false
0
∞
Q: k6, k2, k4
k6 aus der Schlange
bekannt dk
pk
true
1
k3
false
2
k1
true
0
0
false
2
k1
false
0
∞
true
1
k3
false
0
∞
Q: k2, k4
k2 aus der Schlange
k
bekannt dk
k1
true
1
k2
true
2
k3
true
0
k4
false
2
k5
false
3
k6
true
1
k7
false
∞
Q: k4, k5
pk
k3
k1
0
k1
k2
k3
0
k4 aus der Schlange
bekannt dk
pk
true
1
k3
true
2
k1
true
0
0
true
2
k1
false
3
k2
true
1
k3
false
3
k4
Q: k5, k7
k5 aus der Schlange
bekannt dk
pk
true
1
k3
true
2
k1
true
0
0
true
2
k1
true
3
k2
true
1
k3
false
3
k4
Q: k7
K7 aus der Schlange
bekannt dk
pk
true
1
k3
true
2
k1
true
0
0
true
2
k1
true
3
k2
true
1
k3
true
3
k4
Q: leer
Abb.:Veränderung der Daten während der Ausführung des Algorithmus zum kürzesten Pfad
Implementierung 74. Die Klasse Graph implementiert die Methode unweighted().
Die Schlange in dieser Liste wird über eine LinkedList mit den Methoden
removeFirst() und addLast() simuliert.
// Single-source unweighted shortest-path algorithm.
void Graph::unweighted( const string & startName )
{
vmap::iterator itr = vertexMap.find( startName );
if( itr == vertexMap.end( ) )
throw GraphException( startName + " is not a vertex in this graph" );
clearAll( );
Vertex *start = (*itr).second;
list<Vertex *> q; // Schlange
q.push_back( start ); start->dist = 0;
while( !q.empty( ) )
{
Vertex *v = q.front( ); q.pop_front( );
for( int i = 0; i < v->adj.size( ); i++ )
{
Edge e = v->adj[ i ];
Vertex *w = e.dest;
if( w->dist == INFINITY )
{
w->dist = v->dist + 1;
w->prev = v;
q.push_back( w );
}
}
}
}
74
vgl.: pr22859
131
Algorithmen und Datenstrukturen
2.5.3 Berechnung der kürzesten Pfadlängen in gewichteten Graphen
(Algorithmus von Dijkstra)
Gegeben ist ein gerichteter Graph G mit Knotenmenge V und Kantenmenge E. Jede
Kante e hat eine nichtnegative Länge, Außerdem ist ein Knoten s (Standort)
gegeben.
Gesucht ist der kürzeste Weg von s nach v für jeden Knoten v ∈ V des Graphen.
Vorausgesetzt ist, dass jeder Knoten v ∈ V durch wenigstens einen Weg von s aus
erreichbar ist. Für den kürzesten Weg soll die Länge ermittelt werden.
Lösungsbeschreibung. Die Lösung stützt sich auf die Berechnung der kürzesten
Pfadlängen in ungewichteten Graphen 75 ab. Im Algorithmus von Dijkstra werden
auch die Daten über „bekannt“, dv (kürzeste Pfadlänge) und pv (letzter Knoten, der
eine Veränderung von dv verursacht hat) verwaltet.
Es wird eine Menge S von Knoten betrachtet und schrittweise vergrößert, für die der
kürzeste Weg von s aus bereits bekannt ist. Jedem Knoten v ∈ V wird ein Distanz
d(v) zugeordnet. Anfangs ist d(s) = 0 und für alle von s verschiedenen Knoten v ∈ V
ist d v = ∞ , und S ist leer. Dann wird S nach dem Prinzip "Knoten mit kürzester
Distanz von s zuerst" schrittweise folgendermaßen vergrößert, bis S alle Knoten V
des Graphen enthält:
1. Wähle Knoten v ∈ V S mit minimaler Distanz
2. Nimm v zu S hinzu
3. Für jede Kante vw von einem Knoten v zu einem Knoten w ∉ S , ersetze d(w) durch
min({d ( w), d (v) + c( w, v)})
Der folgende Graph
2
k2
k1
4
1
3
10
2
2
k4
k3
5
8
k5
4
6
k7
k6
1
Abb.: Graph nach Ermitteln aller Knoten mit der kürzeszen Pfadlänge 2
mit der Knotenbeschreibung
struct Vertex
{
string
name;
vector<Edge> adj;
double
dist;
Vertex
*prev;
int
scratch;
// Konstruktor
Vertex( const string &
{ reset( ); }
75
//
//
//
//
//
Vertex name
Adjacent vertices (and costs)
Cost
Previous vertex on shortest path
Extra variable used in algorithm
nm ) : name( nm )
vgl. 2.5.2
132
Algorithmen und Datenstrukturen
void reset( )
{ dist = INFINITY; prev = NULL;/* pos = NULL;*/ scratch = 0; }
};
führt zu der folgende Initialisierung:
k
k1
k2
k3
k4
k5
k6
k7
bekannt
false
false
false
false
false
false
false
dk
0
∞
∞
∞
∞
∞
∞
pk
null
null
null
null
null
null
null
Abb.: Anfangszustand der Tabelle mit den Daten für den Algorithmus von Dijkstra
Der erste Knoten (Start) ist der Knoten k1 mit Pfadlänge 0. Nachdem k1 bekannt ist,
ergibt sich folgendes Bild:
k
k1
k2
k3
k4
k5
k6
k7
bekannt
true
false
false
false
false
false
false
dk
0
2
∞
1
∞
∞
∞
pk
null
k1
null
k1
null
null
null
Abb.: Zustand der Tabelle nach „k1 ist bekannt“
„k1“ besitzt die Nachbarknoten: k2 und k4. „k4“ wird gewählt und als bekannt markiert.
Die Knoten k3, k5, k6 und k7 sind jetzt die benachbarten Knoten.
k
k1
k2
k3
k4
k5
k6
k7
bekannt
true
false
false
true
false
false
false
dk
0
2
3
1
3
9
5
pk
null
k1
k4
k1
k4
k4
k4
Abb.: Zustand der Tabelle nach „k4 ist bekannt“
„k2“ wird gewählt. „k4“ ist benachbart, aber schon bekannt. „k5“ ist ebenfalls
benachbart, wir aber nicht ausgerichtet, da die Kosten von „k2“ aus 2 +10 = 12 sind
und ein Pfad der Länge 3 schon bekannt ist
k
k1
k2
k3
k4
k5
k6
k7
bekannt
true
true
false
true
false
false
false
dk
0
2
3
1
3
9
5
pk
null
k1
k4
k1
k4
k4
k4
Abb.: Zustand der Tabelle nach „k2 ist bekannt“
133
Algorithmen und Datenstrukturen
Der nächste ausgewählte Knoten ist „k5“ (ohne Ausrichtungen), danach wird k3
gewählt. Die Wahl von „k3“ bewirkt die Ausrichtung von „k6“
k
k1
k2
k3
k4
k5
k6
k7
bekannt
true
true
true
true
true
false
false
dk
0
2
3
1
3
8
5
pk
null
k1
k4
k1
k4
k3
k4
Abb.: Zustand der Tabelle „k5 ist bekannt“ und (anschließend) „k3 ist bekannt“.
„k7“ wird gewählt. Daraus resultiert folgende Tabelle:
k
k1
k2
k3
k4
k5
k6
k7
bekannt
true
true
true
true
true
false
true
dk
0
2
3
1
3
6
5
pk
null
k1
k4
k1
k4
k7
k4
Abb.: Zustand der Tabelle „k7 ist bekannt“.
Schließlich bleibt nur noch k6 übrig. Das ergibt dann die folgende Abschlußtabelle:
k
k1
k2
k3
k4
k5
k6
k7
bekannt
true
true
true
true
true
true
true
dk
0
2
3
1
3
6
5
pk
null
k1
k4
k1
k4
k7
k4
Abb.: Zustand der Tabelle nach „k6 ist bekannt“.
Der Algorithmus, der diese Tabellen
folgendermaßen beschrieben werden:
berechnet,
kann
void dijkstra(Vertex s)
{
Vertex v, w;
/* 1 */
s.dist = 0;
/* 2 */
for(; ;)
{
/* 3 */
v = kleinster_unbekannter_Distanzknoten;
/* 4 */
if (v == null)
/* 5 */
break;
/* 6 */
v.bekannt = true;
/* 7 */
for each w benachbart_zu v
/* 8 */
if (!w.bekannt)
/* 9 */
if (v.dist + cvw < w.dist)
{
/* 10 */
w.dist = v.dist + cvw;
/* 11 */
w.pfad = v;
134
(in
Pseudocode)
Algorithmen und Datenstrukturen
}
}
}
Die Laufzeit des Algorithmus resultiert aus dem Aufsuchen aller Knoten (in den
beiden for-Schleifen) und im Aufsuchen der Kanten (c(vw)) (in der inneren forSchleife): O(|E| + |V|2) = O(|V|2).
Ein Problem des vorstehenden Agorithmus ist das Durchsuchen der Knotenmenge
nach der kleinsten Distanz 76. Man kann das wiederholte Bestimmen der kleinsten
Distanz
einer
prioritätsgesteuerten
Warteschlange
übertragen.
Der
Leistungsaufwand beträgt dann O(|E| log(|V)+|V| log(|V|)). Der Algorithmus (in
Pseudocode) könnte so aussehen:
/* 1 */ for_all v ∈ V do d (v ) = ∞
/* 2 */ d ( s ) = 0 ; S = 0
/* 3 */ pq = new PriorityQueue(); // Vorrangwarteschlange für Knoten in V
/* 4 */ while pq ≠ 0 do /* pq = V S */
/* 5 */ pq.delete _ min()
/* 6 */ S = S ∪ {v}
/* 7 */ for _ all (v, w) ∈ E do
/* 8 */ if d (v) + c (v, w) < d ( w) pq.decrease _ key ( w, d (v) + c(v, w))
/* 9 */ Entferne (v, w) aus E
/*10*/ end while
Der “update” wird hier durch eine Operation für eine Priority Queue “decrease_key,
Herabsetzen eines Schlüssels um einen vorgegebenen Wert” vollzogen.
„decrease_key“ schränkt die Zeitbestimmung für das Minimum auf O(log V ) ein.
Damit ergibt sich eine Laufzeit von O( V log V + E log V ) = O( E log V )
prioritätsgesteuerte Warteschlange: Binary-Heaps sind eine mögliche
Implementation für Priority Queues. Ein Heap mit N Schlüsseln erlaubt das Einfügen
eines neuen Elements und das Entfernen des Minimums in O(log N ) Schritten. Da
das Minimum stets am Anfang des Heap steht, kann der Zugriff auf das kleinste
Element stets in konstanter Zeit ausgeführt werden. In Java (seit der Version 1.5)
und C++ gibt es die Klasse PriorityQueue, die sich auf einen Binary Heap abstützt.
In der Priority-Queue werden für den Algorithmus von Dijkstra Datensätze von
folgendem Typ abgelegt.
// Structure stored in priority queue for Dijkstra's algorithm.
struct Path
{
Vertex *dest;
// w
double cost;
// d(w)
Path( Vertex *d = 0, double c = 0.0 )
: dest( d ), cost( c ) { }
bool operator> ( const Path & rhs ) const
{ return cost > rhs.cost; }
bool operator< ( const Path & rhs ) const
{ return cost < rhs.cost; }
};
76
v = kleinster_unbekannter_Distanzknoten
135
Algorithmen und Datenstrukturen
Implementierung. 77
void Graph::dijkstra( const string & startName )
{
priority_queue<Path, vector<Path>, greater<Path> > pq;
Path vrec;
// Stores the result of a deleteMin
vmap::iterator itr = vertexMap.find( startName );
clearAll( );
if( itr == vertexMap.end( ) )
throw GraphException( startName + " is not a vertex in this graph" );
Vertex *start = (*itr).second;
pq.push( Path( start, 0 ) ); start->dist = 0;
for( int nodesSeen = 0; nodesSeen < vertexMap.size( ); nodesSeen++ )
{
do
// Find an unvisited vertex
{
if( pq.empty( ) ) return;
vrec = pq.top( ); pq.pop( );
} while( vrec.dest->scratch != 0 );
Vertex *v = vrec.dest;
v->scratch = 1;
for( int i = 0; i < v->adj.size( ); i++ )
{
Edge e = v->adj[ i ];
Vertex *w = e.dest;
double cvw = e.cost;
if( cvw < 0 )
throw GraphException( "Graph has negative edges" );
if( w->dist > v->dist + cvw )
{
w->dist = v->dist + cvw;
w->prev = v;
pq.push( Path( w, w->dist ) );
}
}
}
}
Der Dijkstra-Algorithmus kann mit unterschiedlichen Datenstrukturen eingesetzt
warden, z.B. auch mit Fibonacci-Heaps. Die Laufzeit lässt sich dadurch auf
O(V log V ) einschränken. Da Fibonacci-Heaps einen gewissen Betrag an
„Overhead“ ergeben, ist erreichte Vorteil zweifellhaft.
Nachteile. Der Algorithmus von Dijkstra hat zwei Nachteile:
Es wurden nur die kürzesten Verbindungen von einem ausgezeichneten Startknoten zu einem
anderen Knoten bestimmt.
Die Gewichte aller Kanten müssen positiv sein
77
vgl. pr22859, pr55_1
136
Algorithmen und Datenstrukturen
2.5.4 Berechnung der kürzesten Pfadlängen in gewichteten Graphen mit
negativen Kosten
Falls ein Graph Kanten mit negativen Kosten enthält, arbeitet der DijkstraAlgorithmus nicht korrekt. Das Problem ist, falls ein Knoten u als bekannt dekariert
ist, die Möglichkeit besteht, dass es einen Weg zurück nach u von einem Knoten v
mit negativem Resultat gibt.
Bsp.: Der folgende Graph besitzt einen negativen Zyklus:
2
k2
k1
4
1
3
-10
1
k4
k3
k5
2
2
6
6
k7
k6
1
Bei der Berechnung der Kosten von k5 nach k4 besitzen die direkt angegebenen Kosten den Wert 1.
Es existiert aber noch ein kürzerer Pfad k5, k4, k2, k5, k4 mit Kostenwert -5. Offensichtlich gelangt man
hier in einen (negativen Kosten-) Zyklus, der sogar mehrfach durchlaufen werden kann. So entstehen
dann immer noch weitere kürzere Pfade.
Ein kürzester Pfad zwischen k4 und k5 ist nicht definiert.
Abb.: Graph mit negativem Zyklus
Eine mögliche, aber umständliche Lösung ist: Addition einer Konstanten Δ zu jedem
Kantengewicht. Die Konstante wird so groß gewählt, dass keine negativen Kanten
nach der Addition vorliegen.
Besser ist der folgende Algorithmus (in Pseudocode):
void negativ_gewichtet(Vertex s)
{
Queue q;
Vertex v, w;
/* 1*/ q = new Queue();
//
for each v v.dist = INFINITY;
s.dist = 0;
/* 2*/ q.enqueue(s);
// Einreihen des Startknoten s
/* 3*/ while (!q.isEmpty())
{
/* 4 */ v = q.dequeue();
/* 5 */ for each w adjazent to v
/* 6 */
if (v.dist + cvw < w.dist)
{
// Update w
/* 7 */
w.dist = v.dist + cvw;
/* 8 */
w.path = v;
/* 9 */
if (w ist nicht in q)
/* 10*/
q.enqueue(w);
}
}
}
137
Algorithmen und Datenstrukturen
Komplexität. Jeder Knoten kann etwa |V| mal aus der Warteschlange entnommen
werden, die Laufzeit ist somit O( | E | ⋅ | V | ) (Anstieg gegenüber dem Djikstra
Algorithmus), falls Adjazenslisten benutzt werden. Falls negative "Kosten-Zyklen"
vorliegen, dann gelangt der Algorithmus in eine Endlosschleife.
Implementierung 78
void Graph::negative( const string & startName )
{
vmap::iterator itr = vertexMap.find( startName );
if( itr == vertexMap.end( ) )
throw GraphException( startName + " is not a vertex in this graph" );
clearAll( );
Vertex *start = (*itr).second;
list<Vertex *> q;
q.push_back( start ); start->dist = 0; start->scratch++;
while( !q.empty( ) )
{
Vertex *v = q.front( ); q.pop_front( );
if( v->scratch++ > 2 * vertexMap.size( ) )
throw GraphException( "Negative cycle detected" );
for( int i = 0; i < v->adj.size( ); i++ )
{
Edge e = v->adj[ i ];
Vertex *w = e.dest;
double cvw = e.cost;
if( w->dist > v->dist + cvw )
{
w->dist = v->dist + cvw;
w->prev = v;
// Enqueue only if not already on the queue
if( w->scratch++ % 2 == 0 )
q.push_back( w );
else
w->scratch--;
// undo the push
}
}
}
}
2.5.5 Berechnung der kürzesten Pfadlängen in gewichteten, azyklischen
Graphen
Falls bekannt ist, dass der Graph azyklisch ist, kann der Dijkstra-Algorithmus
verbessert werden: Die Knoten des Graphen werden in topologischer Reihenfolge
(partielle Ordnung 79) ausgewählt. Die Auswahl der Knoten in topologischer Folge
garantiert: die Distanz dv kann nicht weiter erniedrigt werden.
void Graph::acyclic( const string & startName )
{
vmap::iterator itr = vertexMap.find( startName );
if( itr == vertexMap.end( ) )
throw GraphException( startName + " is not a vertex in this graph" );
clearAll( );
Vertex *start = (*itr).second;
list<Vertex *> q;
start->dist = 0;
78
79
vgl. pr22859, pr55_1
vgl. 1.3.3
138
Algorithmen und Datenstrukturen
// Compute the indegrees
for( itr = vertexMap.begin( ); itr != vertexMap.end( ); ++itr )
{
Vertex *v = (*itr).second;
for( int i = 0; i < v->adj.size( ); i++ )
v->adj[ i ].dest->scratch++;
}
// Enqueue vertices of indegree zero
for( itr = vertexMap.begin( ); itr != vertexMap.end( ); ++itr )
{
Vertex *v = (*itr).second;
if( v->scratch == 0 )
q.push_back( v );
}
int iterations;
for( iterations = 0; !q.empty( ); iterations++ )
{
Vertex *v = q.front( );
q.pop_front( );
for( int i = 0; i < v->adj.size( ); i++ )
{
Edge e = v->adj[ i ];
Vertex *w = e.dest;
double cvw = e.cost;
if( --w->scratch == 0 )
q.push_back( w );
if( v->dist == INFINITY )
continue;
if( w->dist > v->dist + cvw )
{
w->dist = v->dist + cvw;
w->prev = v;
}
}
}
if( iterations != vertexMap.size( ) )
throw GraphException( "Graph has a cycle!" );
}
Eie bedeutende Anwendung azyklischer Graphen ist die „critical path analysis“.
Benutzt werden solche Graphen für die Zeitplanung bzw. Kapazitätsülanung in
Projekten. Man verwendet Aktivitätsgraphen (activity node graph, Kosten sind den
Knoten zugeordnet, Kanten zeigen Anordnungsbeziehungen) und ereignisorientierte
Graphen.
139
Algorithmen und Datenstrukturen
2.5.6 All pairs shorted Path
Der Algorithmus von Floyd berechnet die kürzesten Verbindungen von allen Knoten
zu allen anderen Knoten
Zugrundeliegende Idee: Es werden alle direkten Verbindungen zweier Knoten als die
"billigste" Veränderung der beiden Knoten verwendet. Die billigste Verbindung ist
entweder die direkte Verbindung oder aber zwei Wege über einen Mittelknoten.
Die Komplexität des Verfahrens von Floyd beträgt O( | V 3 | ).
Implementierung 80:
public static final int NO_EDGE = 0;
public int [][] floyd(int [][] a, int start)
{
if (a == null || a.length == 0) return null; // Matrix ist leer
int i, j, x, n = a.length;
// i = Start, j = Ende, x = Zwischenknoten
// Anlegen einer neuen Adjazenzmatrix
int [][] c = new int[n][n];
// Kopiere alle Werte aus der Matrix: Am Anfang ist die
// direkte Verbindung die einzige und daher auch die
// billigste
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
{
c[i][j] = a[i][j]; // direkte Kanten kopieren
}
// Suche fuer Knoten x nach Wegen ueber x, d.h. i -> x, x -> j
for (x = 0; x < n; x++)
for (i = 0; i < n; i++)
if (c[i][x] != NO_EDGE) // gibt es einen Weg i -> x
for (j = 0; j < n; j++)
if (c[x][j] != NO_EDGE)
if (c[i][j] == NO_EDGE // noch kein Weg i -> j
|| (c[i][x] + c[x][j] < c[i][j])) // i->x->j billiger
{
if (i == j) continue;
c[i][j] = c[i][x] + c[x][j];
}
return c;
}
Der Test dieses Algorithmus führt zu folgendem Resultat:
80
pr52221 bzw. pr54_1, pr54020.cpp
140
Algorithmen und Datenstrukturen
141
Algorithmen und Datenstrukturen
2.6 Minimale Spannbäume
Anwendung. Minimale spannende Bäume sind z.B. für folgende Fragestellung
interessant: "Finde die billigste Möglichkeit alle Punkte zu verbinden". Diese Frage
stellt sich bspw. für elektrische Schaltungen, Flugrouten und Autostrecken.
Problemstellung. Zu einem zusammenhängenden Graphen soll ein Spannbaum
(aufspannender Baum) mit minimalem Kantengewicht (minimale Gesamtlänge)
bestimmt werden.
Der minimale Spannbaum muß nicht eindeutig sein, zu jedem gewichteten Graphen
gibt es aber mindestens einen minimalen spannenden Baum.
2.6.1 Der Algorithmus von Prim
Das einfachste Verfahren zur Erzeugung eines minimale spannenden Baums stammt
von Prim aus dem Jahre 1952. In diesem Verfahren wird zu dem bereits
vorhandenen Teilgraph immer die billigste Kante hinzugefügt, die den Teilgraph mit
einem bisher noch nicht besuchten Knoten verbindet.
Aufgabe. Berechne einen spannenden Baum mit minimalen Kosten (minimum
spanning tree).
Lösungsbeschreibung. Der folgende Graph
2
k2
k1
4
1
3
10
2
7
k4
k3
5
8
k5
4
6
k7
k6
1
besitzt folgenden minimale Spannbaum:
2
k2
k1
1
2
k4
k3
k5
4
6
k7
k6
1
Abb.:
Die Anzahl der Kanten in einem minimal spannenden Baum ist |V| - 1 (Anzahl der
Knoten – 1). Der minimal spannende Baum ist
142
Algorithmen und Datenstrukturen
- ein Baum, der keine Zyklen besitzt.
- spannend, da er jeden Knoten abdeckt.
- ein Minimum.
Der Algorithmus von Prim arbeitet stufenweise. Auf jeder Stufe wird ein Knoten
ausgewählt. Die Kanten auf seine nachfolgenden Knoten werden dann untersucht.
Die Untersuchung folgt nach den Vorschriften des Dijkstra-Algorithmus. Es gibt nur
eine Ausnahme hinsichtlich der Ermittlung der Distanz: d w = min(d v , c vw )
Die Ausgangssituation zeigt folgende Tabelle:
k
k1
k2
k3
k4
k5
k6
k7
bekannt
false
false
false
false
false
false
false
dv
0
∞
∞
∞
∞
∞
∞
pv
null
null
null
null
null
null
null
Abb.: Ausgangssituation
„k1“ wird ausgewählt, „k2, k3, k4 sind zu k1 benachbart“. Das führt zur folgenden
Tabelle:
k
k1
k2
k3
k4
k5
k6
k7
bekannt
true
false
false
false
false
false
false
dv
0
2
4
1
∞
∞
∞
pv
null
k1
k1
k1
null
null
null
Abb.: Die Tabelle im Zustand „k1 ist bekannt“
Der nächste Knoten, der ausgewählt wird ist k4. Jeder Knoten ist zu k4 benachbart.
Ausgenommen ist k1, da dieser Knoten „bekannt“ ist. k2 bleibt unverändert, denn die
„Kosten“ von k4 nach k2 sind 3, bei k2 ist 2 eingetragen. Der Rest wird, wie die
folgende Tabelle zeigt, verändert:
k
k1
k2
k3
k4
k5
k6
k7
bekannt
true
false
false
true
false
false
false
dv
0
2
2
1
7
8
4
pv
null
k1
k4
k1
k4
k4
k4
Abb.: Die Tabelle im Zustand „k4 ist bekannt“
Der nächste Knoten, der ausgewählt wird, ist k2. Das zeigt keine Auswirkungen.
Dann wird k3 gewählt. Das bewirkt eine Veränderung der Distanz zu k6.
k
k1
k2
bekannt
true
true
dv
0
2
pv
null
k1
143
Algorithmen und Datenstrukturen
k3
k4
k5
k6
k7
true
true
false
false
false
2
1
7
5
4
k4
k1
k4
k3
k4
Abb.: Tabelle mit Zustand „k2 ist bekannt“ und (anschließend) mit dem Zustand „k3 ist bekannt“
Es folgt die Wahl des Knoten k7, was die Ausrichtung von k6 und k5 bewirkt:
k
k1
k2
k3
k4
k5
k6
k7
Bekannt
true
true
true
true
false
false
true
dv
0
2
2
1
6
1
4
pv
null
k1
k4
k1
k7
k7
k4
Abb.: Tabelle mit Zustand „k7 ist bekannt“
Jetzt werden noch k6 und dann k5 bestimmt. Die Tabelle nimmt danach folgende
Gestalt an:
k
k1
k2
k3
k4
k5
k6
k7
Bekannt
true
true
true
true
true
true
true
dv
0
2
2
1
6
1
4
pv
null
k1
k4
k1
k7
k7
k4
Abb.: Tabelle mit Zustand „k6 ist bekannt“ und (anschließend) „k5 ist bekannt“
Die Tabelle zeigt, daß folgende Kanten den minimal spannenden Baum bilden:
(k2,k1),k3,k4)(k4,k1),(k5,k7),(k6,k7),(k7,k4)
Der Algorithmus von Prim zeigt weitgehende Übereinstimmung mit dem Algorithmus
von Dijkstra 81.
Komplexität: Die Laufzeit ist O(|V|2)
Implementierung. MinimalSpanningTree.java 82
81
82
vgl. pr53330
144
Algorithmen und Datenstrukturen
Abb.
2.6.2 Der Algorithmus von Kruskal
Beschreibung des Algorithmus.
1. Markiere alle Knoten als nicht besucht.
2. Erstelle eine neue Adjazenzmatrix, in der die tatsächlich verwendeten Kanten eingetragen werden.
Zu Beginn sind alle Elemente 0.
3. Bestimme die billigste Kante von einem Knote i zu einem Knoten j, die entweder zwei bisher nicht
erreichte Knoten verbindet, einen nicht nicht erreichten mit einem erreichten oder zwei bisher
unverknüpfte Teilgraphen verbindet.
Falls beide Knoten bereits erreicht wurden, kann diese Kante ignoriert werden, da durch sie ein Zyklus
entstehen würde.
4. Markiere i und j als erreicht und setze minimalTree[i][j]= g, wobei g das Gewicht der Kante
(i,j) ist.
5. Fahre mit Schritt 3 fort, bis alle Knoten erreicht sind
Bsp.: Gegeben ist
2
k2
k1
4
1
3
10
2
7
k4
k3
5
8
k5
4
6
k7
k6
1
Bestimme den minimale spannenden Baum nach dem Algorithmus von Kruskal:
145
Algorithmen und Datenstrukturen
1. Schritt
k2
k1
1
k4
k3
k5
k7
k6
2. Schritt
k2
k1
1
k4
k3
k5
k7
k6
1
3. Schritt
2
k2
k1
1
k4
k3
k5
k7
k6
1
4. Schritt
2
k2
k1
1
2
k4
k3
k5
k7
k6
1
146
Algorithmen und Datenstrukturen
5. Schritt
2
k2
k1
1
2
k4
k3
k5
4
k7
k6
1
6. Schritt
2
k2
k1
1
2
k4
k3
k5
4
6
k7
k6
1
Abb. Lösungsschritte zum Demonstrationsbeispiel
Prinzip. Auswahl der Kanten in der Reihenfolge kleinster Gewichte mit Aufnahme
einer Kante, falls sie nicht einen Zyklus verursacht.
Implementierung. MinimalSpanningTree.java 83
83
vgl. pr53331
147
Algorithmen und Datenstrukturen
2.7 Netzwerkflüsse
2.7.1 Maximale Flüsse
27.1.1 Netzwerk und maximaler Fluß
Ein Netzwerk N = (V , E , s, t , c ) ist
-
-
ein gerichteter Graph G = (V , E ) ohne Mehrfachkanten
mit zwei ausgezeichneten Knoten (Quelle und Senke)
o Quelle s aus V . Die Quelle ist ein Knoten mit Eingangsgrad 0.
o Senke t aus V . Die Senke ist ein Knoten mit Ausgangsgrad 0.
Mit einer Kapazitätsfunktion c , die jeder Kante e aus E eine nicht-negative reelwertige
Kapazität c(e) zuweist
Ein Restnetzwerk (Residualnetzwerk) von N ist ein Netzwerk N f = (V , E , s, t , c f ) in
dem die Kapazitäten um den Fluß durch diese Kanten vermindert werden. Jedes
Restnetzwerk ist ein Teilgraph G f des Netzwerks.
c=1
c=1
c=2
c=3
s
t
c=4
c=0
c=3
c=2
Ein s-t-Fluß ist eine Funktion f , die jeder Kante e im Netzwerk einen nichtnegativen, reellen Flusswert f (e) zuweist, der einer Reihe von Bedingungen genügt.
c=1 f=0
c=1 f=0
c=2 f=0
c=3 f=1
s
t
c=4
c=0
f=1
c=3
f=2
c=2 f=1
0 ≤ f (e) ≤ c(e), ∀e ∈ E
f (e) − ∑ f (e)
-
Kapazitätsbeschränkung
-
Flusserhaltung
∑
e inc v
e aus v
Wert vom Fluß im Netzwerk: val ( f ) =
∑ f (s, y) − ∑ f ( y, s) . Der Wert eines Flusses
y∈s +
y∈s −
ist die Summe der lokalen Flüsse aus der Quelle (= Summe aller lokalen Flüsse in
die Senke)
148
Algorithmen und Datenstrukturen
Der Fluß mit maximalem Wert heißt maximaler Fluß:
Gegeben ist ein gerichteter, gewichteter Graph. Ein Fluß ist in diesem Graphen eine Funktion
f : E → ℜ mit
0 ≤ f (i, j ) ≤ c(i, j )
2. Für alle i ∈ V {s, t} gilt
1.
∑ f (a, i) = ∑ f (i, b)
a∈V ( i )
b∈N ( i )
V (i ) : alle Vorgänger von i
N (i ) : alle Nachfolger von i
Gesucht ist der maximale Gesamtfluß: F =
∑ f ( s, b) = ∑ f ( a, t )
Der maximale Fluß im Netzwerk hat genau einen Wert: den minimalen Schnitt. Die
folgenden Aussagen sind äquivalent:
f ist der maximale Fluß in N bzw. G
- das Restnetzwerk N f bzw. G f enthält keinen Verbesserungspfad
-
- f = c (S , T ) gilt für irgendeinen Schnitt (S , T )
149
Algorithmen und Datenstrukturen
2.7.1.2 Optimieren und Finden augmentierender Pfade (Erweiterter Weg)
Ein augmentierender Pfad bzgl. eines Flusses f ist ein Pfad von der Quelle zur
Senke ohne Berücksichtigung der Kantenrichtung wobei für jede Kante (v,w) gilt:
entweder Vorwärtskante: (v, w) ∈ E und f (v, w) < c(v, w)
oder
Rückwärtskante: ( w, v ) ∈ E und f ( w, v) > 0
Längs eines so erweiternden (augmentierenden) Wegs kann der Fluß vergrößert
werden, indem man durch Vorwärtskanten zusätzliche Einheiten fließen lässt oder
den Fluß durch Rückwärtskanten verringert. Beides ist nach der Definition des
erweiternden Wegs möglich.
Bsp. für Flussoptimierung mit einem augmentierenden Pfad
4/3
5/5
3/2
3/3
u
7/3
4/2
6/5
1/1
7/7
4/3
6/3
5/5
w
Auf dem augmentierenden Pfad wäre bis zum Knoten u noch Platz für 2 Einheiten (Restkapazität).
Wird der „Hahn“ von w nach u etwas zugedreht, fließen insgesamt 2 Einheiten mehr
4/3
5/5
3/2
3/3
u
7/5
4/4
6/5
1/1
7/7
4/1
6/5
5/5
w
Optimierung am augmentierenden Pfad
ci −1 / f i −1
ci / f i
ci +1 / f i +1
nur Vorwärtskanten: Erhöhe Fluß um minimale Restkapazität (slack) min(c k − f k )
über alle Pfadkanten k.
150
Algorithmen und Datenstrukturen
Wenn Rückwärtskanten vorkommen, dann:
Vorher
cd / f d
ca / f a
cb / f b
Nachher
cd / f d
ca + x / f a
cb − x / f b
cc / f c
ce / f e
cc + x / f c
cb / f b
Hier gilt: x = min (c v − f v ) über alle Vorwärtskanten v
v
oder x = min c r über alle Rückwärtskanten r (das kleinere)
r
Finden eines augmentierenden Pfads
Markiere Quelle s
wiederhole
wenn (v, w) existiert mit v markiert und
f (v, w) < c(v, w) markiere w
wenn (w, v ) existiert mit v markiert und f ( w, v) > 0 markiere w
solange in der Schleife neue Knoten markiert werden.
Der Algorithmus markiert genau die Knoten, die von der Quelle aus mit
augmentierenden Pfaden erreichbar sind. Ist am Ende die Senke markiert, dann ist
ein augmentierender Pfad durch das Netzwerk gefunden.
Längs eines so erweiterten Wegs kann der Fluß vergrößert werden, indem man
durch Vorwärtskanten zusätzliche Einheiten fließen lässt oder man den Fluß in den
Rückwärtskanten verringert. Beides ist nach der Definition des
151
Algorithmen und Datenstrukturen
2.7.1.2 Algorithmus für optimalen Fluss
Ford-Fulkerson-Algorithmus
Gegeben: ein Netzwerk mit den Kapazitäten c : E → ℜ ≥0 und 2 Knoten s, t
1. Initialisiere
f mit 0
2. solange ein augmentierender Weg P von s nach t im Restnetzwerk G f existiert
2a. Konstruktion bzw. Aktualisierung des Restnetzes G f
2b. Finden eines augmentierenden Wegs
3. für jede Kante e auf P erhöhe den Fluß um c f (P )
Bsp. zum Ford-Fulkerson-Algorithmus:
(a) Gegeben
12
16
20
s
t
10
4
7
13
9
4
14
(b) augmentierender Weg mit Kapazität 7
12/7
16/7
20/7
s
t
10/7
4
7/7
13
9/7
4
14/7
(c) Restnetz nach Schritt 1, augmentierender Weg mit Kapazität 4
12
9
7
7
13
s
t
3
13
11
7 0
9
4
7
7
152
Algorithmen und Datenstrukturen
(d) Restnetz nach Schritt 2, augmentierender Weg mit Kapazität 5
8
4
5
7
11
13
s
4
3
t
11
13
7
5
4
0
3
11
(e) Restnetz nach Schritt 3, augmentierender Weg mit Kapazität 4
3
9
12
16
8
s
4
3
t
11
13
7
5
3
4
11
(f) Restnetz nach Schritt 4, augmentierender Weg mit Kapazität 3
3
9
16
16
4
s
t
3
9
11
4
7
9
4
3
11
(g) Restnetz nach Schritt 5, kein augmentierender Weg
12
12
19
16
1
s
t
6
6
7
8
7
9
4
3
11
153
Algorithmen und Datenstrukturen
(h) Maximaler Fluß und minimaler Schnitt, beide mit Wert 23
12/12
16/16
20/19
s
t
10/4
4/0
13/7
7/7
9/0
4/4
14/11
Analyse der Laufzeit
Schritt 1: O( E )
Schritt 2a: O( E ) je Durchlauf
Schritt 3: O( E ) je Durchlauf, wenn z.B. Tiefen- oder Breitensuche benutzt wird
Wieviel Durchläufe gibt es? Falls die Kapazitäten ganze Zahlen sind, erhöht sich jeder Durchlauf den
Fluß um mindestens eins, also gibt es bis zu f
(
Laufzeit ist dann insgesamt O f
(
*
)
*
Durchläufe, wobei f
*
der maximale Fluß ist. Die
⋅E .
)
Die Laufzeit O f * ⋅ E ist nicht befriedigend, da f * evtl. exponentiell in der Größe
der Eingabe ist. Das folgende Bsp. zeigt die Möglichkeit, dass tatsächlich einmal f *
Durchläufe ausgeführt werden:
Bsp.:
1000
1000
999
1000
1
1
1
1000
1
1000
1000
(a) Netz
999
(b) 1. Schritt
Bei ungeschickter Wahl des 1. erweiternden Wegs, nämlich s, 1, 2, t ist in der 1. Iteration (1. Schritt)
lediglich eine Erhöhung um eine Einheit möglich. Saturiert wird nur die Kante (1,2). QAls nächstes
kann s, 2, 1, t gewählt werden, wobei wieder nur eine Erhöhung um eine Einheit zu erreichen ist.
154
Algorithmen und Datenstrukturen
999
999
1
1
1
1
1
999
999
(c) 2. Schritt
Abb.: Bestimmen des augmentierenden Weges mit Tiefensuche
Hier werden die augmentierenden Pfade mit Tiefensuche gefunden. In jedem Schritt wird der Fluß um
1 erhöht und in den (b) und (c) dargestellten Schritte werden
f * = 2000 mal wiederholt bis der
maximale Fluß erreicht ist
Wenn die augmentierenden Wege mit der Breitensuche bestimmt werden, dann werden 2 Durchläufe
benötigt.
1000
0
0
1000
1000
1000
1
1
1000
1000
1000
1000
1000
1000
0
0
1000
1000
Abb.: Bestimmen augmentierender Wege mit Breitensuche
Edmonds-Karp-Algorithmus
Es ist ersichtlich aus den vorstehenden Beispielen, dass die Breitensuche Vorteile
hat gegenüber der Tiefensuche. Genutzt wird die Breitensuche durch den
Algorithmus von Edmonds und Karp
1. Initialisiere
f mit 0
2. solange ein augmentierender Weg P von s nach t im Restnetzwerk G f existiert
2a. Konstruktion bzw. Aktualisierung des Restnetzes G f
155
Algorithmen und Datenstrukturen
2b. Finden eines augmentierenden Wegs mit Breitensuche
3. für jede Kante e auf P erhöhe den Fluß um c f (P )
Bei diesem Algorithmus wird der kürzesze augmentierende Weg bzgl. der
Kantenzahl ausgewählt. Falls δ f (u, v) ) der Abstand zwischen u und v im Restnetz
ist, also die Anzahl der Kanten auf dem kürzesten Weg von u nach v , gilt:
Beim Edmonds-Karp-Algorithmus gilt für alle Knoten v (ausgenommen s , t ): Während des Ablaufs
des Algorithmus ist δ f (u , v) ) monoton wachsend
156
Algorithmen und Datenstrukturen
2.7.1.4 Schnitte und das Max-Flow-Min-Cut Problem
Eine Unterteilung eines Netzwerks in eine Knotenmenge A und eine Knotenmenge B
heißt Schnitt.
A
B
w
Die Kapazität c( A, B) eines Schnitts A / B ist die Summe der Kapazitäten aller
Kanten von A nach B.
Der Wert eines Flusses (der Gesamtfluß) ist nie größer als die Kapazität eines
beliebigen Schnitts (irgendwie muß es ja durch).
D.h. w( f ) ≤ min c( A, B)
Schnitt AB
Max-Flow-Min-Cut-Theorem
w( f ) , der Wert von f ist maximal
⇔ es gibt keinen augmentierenden Pfad von Quelle zur Senke
Dann enden alle von s ausgehenden erweiternden Wege (- genauer gesagt deren
Anfangsstücke -)entweder bei einer saturierten Vorwärtskante oder bei einer
Rüpckwärtskante
mit Fluß 0. Durch diese Kanten wird ein Cut impliziert, dessen Kapazität gleich dem
momentanen Fluß ist.
⇔ w( f ) ≤ min c( A, B)
Schnitt AB
Beweis „ ⇒ “: durch Angeben der Optimierungsregel
Beweis „ ⇐ “: Definiere Schnitt A / B , so dass A = alle Knoten, die von der Quelle aus mit
augmentierenden Pfad erreichbar sind.
Für alle v ∈ A , w ∈ B gilt f (v, w) = c(v, w) , da sonst w auch mit augmentierendem Pad erreichbar.
Also ist w( f ) = c( A, B )
1
2
3
max flow
123
min cut
157
Algorithmen und Datenstrukturen
Bsp.:
Flussgraph:
12/12
a
b
16/11
20/15
s
t
10
4/1
13/7
7/7
9/4
c
4/4
d
14/11
Schnitt:
- cut ({s, a, c}, {b, d , t})
- Netto-Fluss. f (a, b ) + f (c, d ) + f (b, c ) = 19 . Netto-Fluss ist in allen Schnitten gleich
Eine interessante Eigenschaft des Netzwerks mit gannzahliger Kapazitäz ist, dass
auch die maximalen Flüsse in solchen Netzwerken immer ganzzahlig sind, da der
vorstehende Algorithmus nur ganzzahlige Erhöhungen durchführt.
Das Integral-Flow-Theorem: Wenn in einem Netzwerk alle Kapazitäten ganzzahlige
Werte sind, dann ist der maximale Fluß auch ganzzahlig.
Beweis:
Verwende den vorstehenden Algorithmus. Am Anfang ist der Fluß 0.
In jedem Schritt wird er um die Restkapazität eines augmentierenden Pfads erhöht
Da alter Fluß ganzzahlig und Kapazität ganzzahlig, ist auch die Restkapazität
ganzahlig und neuer Fluß auch
158
Algorithmen und Datenstrukturen
2.7.2 Konsteminimale Flüsse
Durch ein Netzwerk wird häufig nicht ein maximaler Fluß gesendet, sondern ein Fluß
mit vorgegebenem Wert, der bzgl. eines Kostenkriteriums minimale Kosten
verursacht.
Hier bestimmt man zunächst den maximalen Fluß ohne Rücksicht auf die Kosten und
steuert anschließend die einfachen Flüsse so um, bis das Kostenminmum erreicht
ist.
Bsp.: Gegeben ist das Verkehrsnetz
2
7(6)
4(3)
4(4)
1
4
2(2)
3(8)
3
Abb.:
Jede Strecke des Netzes (Kante des Graphen) hat eine begrenzte Kapazität
(bezeichnet durch die 1. Zahl an den Kanten). Die Zahl in den Klammern an den
Kanten gibt die Kosten des Transports (je Einheit) an. Gesucht ist der maximale Fluß
durch das Netz vom Knoten 1 zum Knoten 4, wobei die Kosten möglichst niedrig sein
sollen.
1. Berechnung des maximalen Flusses ohne Berücksichtigung der Kosten
2
7
7(6)
1
4(4)
4(3)
1
4
1
2(2)
2
3(8)
3
3
Abb.:
Der berechnete maximale Fluß besteht aus den Einzelflüssen:
7 (Einheiten) von 1 nach 2
3 (Einheiten) von 2 nach 3
4 (Einheiten) von 2 nach 4
3 (Einheiten) von 3 nach 4
Die Kosten betragen 91 [Kosteneinheiten]. Die Lösung ist nicht kostenminimal.
2. Kostenoptimale Lösung
159
Algorithmen und Datenstrukturen
Zwischen Knoten 1 und 3 bestehen 2 Alternativwege (1 - 2- 3 - 2) und (1 - 3). (1 - 3)
wird nicht benutzt. Dort betragen die Kosten nur 2 [Kosteneinheiten]. Eine
Umverteilung von 2 [Mengeneinheiten] führt hier zur Verbesserung. Man erhält die
Optimallösung mit 77 [Kosteneinheiten].
160
Algorithmen und Datenstrukturen
2.8 Matching
2.8.1 Ausgangspunkt, Motivierendes Beispiel, Definitionen, maximales
Matching
Ausgangspunkt
Zuordnungsprobleme (verschiedene Dinge einander zuordnen)
-
Männer / Frauen im Tanzkurs
Arbeiten / Arbeitskräfte
Koffer / Schließfächer
Gegeben: Ein ungerichteter G = (V , E ) . Die Kanten symbolisieren hier mögliche
Zuordnungen.
Gesucht: Eine Zuordnung M (Matching), d.h. eine unabhängige Kantenmenge M .
Unabhängig bedeutet, es gilt: (i, j ), (i ' , j ' ) ∈ M ⇒ i ≠ i ' , j ≠ j ' , i ≠ j ' , j ≠ i '
Keine der zwei Kanten in M haben die gleiche Zuordnung.
M ist die Anzahl der Kanten in M .
Motivierendes Beispiel
Gegeben Tanzkurs: Jeder Teilnehmer (Knoten) weiß, mit wen ergerne tanzt. (Kante).
Gesucht: Mögliche Paarungen (vgl. rot gefärbte Kanten).
Eva
Heino
Martin
Klaus
Maria
Pia
Uwe
Lilo
3 Paare sind gefunden, aber nicht jeder Knoten hat einen Partner. Es sind keine weiteren Paarungen
möglich.
Frage: Wie kriegt man eine optimale Paarbildung zustande?
Es sind ja noch ein Herr und eine Dame übrig geblieben!
Definitionen
-
-
Zwei Kanten (u, v ) und ( x, y ) (x,y) heißen unabhängig, wenn u , v, x, y vier
verschiedene Knoten sind. Wenn u = x oder u = y oder v = x oder v = y ,
dann heißen die Kanten benachbart (oder verbunden oder adjazent)
Die Kantenmenge M heißt unabhängig, wenn alle ihre Elemente paarweise
unabhägig sind. Solche Untermengen heißen auch Matching (Zuordnung).
161
Algorithmen und Datenstrukturen
-
Ein Knoten heißt frei bzgl. eines Matchings, wenn er keine Kante des
Matchings hat, sonst heißt er gematcht.
-
Ein Matching M heißt perfekt, wenn es alle Knoten des Graphen überdeckt.
-
Ein Matching M heißt maximal (nicht erweiterbar), wenn es um keine Kante
erweitert werden kann
-
Ein Matching M heißt Maximum, wenn es kein Matching mit mehr Kanten gibt,
d.h. |M| ist maximale Größe.
162
Algorithmen und Datenstrukturen
-
Ein Matching M bei dem nur ein Knoten frei bleibt, heißt fast perfekt.
gematcht
frei
Bsp.: Ein gerader Kreis hat 2 perfekte Matchings
Ein gieriger Algorithmus
- Gegeben: Graph G
- Gesucht Matching M
Solange eine unmarkierte Kante (u,v) in G existiert
Markiere (u,v)
Markiere alle benachbarten Kanten
Übertnehme (u,v) nach M
163
Algorithmen und Datenstrukturen
- Algorithmus liefert
- ein maximales Matching
- aber kein (fast) perfektes Matching
Beobachtung
Im folgenden Graphen sind
Eva
Heino
Martin
Klaus
Maria
Pia
Uwe
Lilo
Knoten in zwei Gruppen aufteilbar, Kanten jeweils zwischen diesen Gruppen
Klaus
Heino
Uwe
Martin
Maria
Lilo
Pia
Eva
164
Algorithmen und Datenstrukturen
2.8.2 Bipartiter Graph
Am häufigsten werden Matching-Probleme in bipartiten Graphen betrachtet.
Ungerichteter Graph G = ( X ∪ Y , E ) mit X ∩ Y = ∅ und nur Kanten (xi , y j ) ∈ E mit
xi ∈ X , y j ∈ Y oder umgekehrt.
Gegeben ist der folgende bipartite Graph G = ( X ∪ Y , E ) mit X = {x1 , x 2 ,..., x 6 } und
Y = {y1 , y 2 ,..., y 6 }
x1
x2
x3
x4
y1
y2
y3
y4
x5
y5
x6
y6
Zuordnung nicht maximal: Mehr Kanten bspw, wenn (x1 , y1 ) und (x3 , y 2 ) (x3,y2)
verwendet werden, statt (x1 , y 2 )
x1
x2
x3
x4
y1
y2
y3
y4
x5
y5
x6
y6
Formulierung als Flußproblem
Vorgehen
- Gegeben Graph G = ( X ∪ Y , E )
- Hinzufügen von zwei weiteren Knoten Quelle s und Senke t
- Jede Kante (xi , y j ) von G = ( X ∪ Y , E ) wird in einem Graphen G ' = ( X ∪ Y ∪ {s, t}, E ')
zu einem Pfeil von xi nach y j
- In E ' existiert ein Pfeil von s zu jedem Knoten xi ∈ X und von jedem y j ∈ Y
existiert ein Pfeil zu t
- Es ist also E ' = E ∪ {(s, x ) : x ∈ X } ∪ {( y, t ) : y ∈ Y }
- Jede Kante erhält die Kapazität 1
Maximale Zuordnung in G entspricht einem maximalen Fluß in G’
165
Algorithmen und Datenstrukturen
Bsp.:
s
x1
x2
x3
x4
y1
y2
y3
y4
x5
y5
x6
y6
t
Ablösung von (x1,y2) durch (x1,y1) und (x3,y2) in G entspricht Erweiterungspfad
e=[s,x3,y2,x1,y1,t] in G '
Jeder Fluß f in G ' entspricht einem Matching M = {( xi , y i ) | f ( xi , y i ) = 1} in G . Der
maximale Fluß ordnet genau den Kanten des „Maximum Matchings“ (und denen von
Quelle und Senke) 1.0 zu, sonst 0.0.
Erweiternder Weg
Erweiterungspfad in G’
- Vorwärtspfeil e mit Fluss f = 0
- Rückwärtspfeil e’ mit Fluss f(e’)=1
- Vorwärts- und Rückwärtspfeile wechseln sich ab
- Pfad beginnt und endet mit einem Vorwärtspfeil
Entsprechnug in G
- Pfad, dessen Kanten abwechselnd zur Zuordnung gehören bzw. nicht zur
Zuordnung gehören wird als alternierender Pfad bezeichnet, z.B. x2, y4,x5,y6
Vergrößerung der Zuordnung
- Vergrößerung alternierender Pfad: Alternierender Pfad mit freien Endknoten, z.B.:
y3,x2,y4,x4,y5,x6
- Freie Kante wird zu gebundener und umgekehrt.
Für ein gegebenes Matching M nennt man jede für die Zuordnung verwendete
Kante e ∈ M gebunden, jede Kante e'∈ E − M ist frei. Jeder Knoten, der eine
gebundene Kante inzidiert, ist ein gebundener Knoten, jeder andere Knoten ist frei.
Ein Weg in G dessen Kanten abwechselnd gebunden und frei sind, heißt
alternierender Weg. Die Länge eines alternierenden wegs, ist die Anzahl der Kanten
auf diesem Weg. Natürlich kann nicht jeder alternierende Weg zur Verrgrößerung der
Zuordnung benutzt werden. Das geht nur dann, wenn die beiden Knoten an den
166
Algorithmen und Datenstrukturen
Enden eines Wegs frei sind. Ein alternierender Weg mit zwei freien Knoten an beiden
Enden heißt deshalb vergrößernd.
Bsp.:
Erweiternder Weg
M ' ist das aus der Vergrößerung entstehende Matching. Falls es ein aus der
Vergrößerung entstehendes Matching gibt, ist der Pfad M - M ' alternierend:
M
M
M’
⇒
M
M’
M
M’
Beachte: Ein Zyklus kann kein vergrößernd alternierender Pfad sein
Es ist einleuchtend, dass sich ein Matching entlang eines solchen erweiternden
Wegs um eine Kante erweitern lässt, indem man jede Matchingkante zu einer
Nichtmatchingkante macht und umgekehrt.
Maximum Matching
Dabei gilt der folgende Satz: M ist Maximum Matching ⇔ Es gibt keinen
erweiternden Weg bzgl. M .
Beweis:
⇒ : trivial!
⇐: Es gibt keinen erweiternden Weg bzgl. M
Annahme: Es gibt M ' mit M ' > M
Betrachte nun M und M ' in G und entferne alle Kanten aus dem Rest von G, die
abwechselnd in M ' und M liegen, wobei diese Folge mit einer Kante aus M '
beginnt und aufhört.
Die Endpunkte dieser Folge von Kanten sind frei bzgl. M . Somit ist diese Folge
von Kanten ein erweiternder Weg bzgl. M . Dies ist ein Widerspruch zur
Voraussetzung.
Falls M < M ' , M ist nicht Maximum.
167
Algorithmen und Datenstrukturen
Bsp.:
v
v
w
v
Kein Maximum Matching da vergrößernd alternierend Pfad zwischen nicht gematchten Knoten v, w
v
v
w
w
Kein maximum Matching da vergrößert alternierender Pfad zwischen nicht gematchten Knoten
Daraus folgt ein Ansatz zur Lösung des Problems, ein Maximum Matching zu
bestimmen
Benutze irgend einfachen Algorithmus um ein maximales Matching M zu finden, solange ein
vergrößernd M-alternierender Pfad vorhanden ist
Vertausche die M-Zugehörigkeit der Kanten auf diesem Pfad
-
Der Algorithmus 84 fügt in jedem Schritt eine Kante zu M hinzu.
Da es nur endlich viele Kanten gibt, terminiert er.
Wenn er terminiert, hat er ein Maximum Matching gefunden
Noch offen: Wie findet man M-alternierende Pfade?
(Erweiternder) alternierender Baum
Bei bipartiten Graphen kann man für ein gegebenes Matching einen vergrößernden
Weg finden, indem man mit der Suche bei einem freien Knoten beginnt und entlang
eines bzg. M alternierenden Wegs fortschreitet. Sobald man bei einem freien Knoten
angekommen ist, ist ein vergrößernder Weg gefunden. Zu einem freien Startknoten
kann man einen entsprechenden Baum mit Hilfe der Breitensuche ermitteln.
84
abgeleitet aus dem Satz von Berge
168
Algorithmen und Datenstrukturen
x1
x2
x3
x4
y1
y2
y3
y4
x5
x6
y5
y3
y6
freier Knoten
freie Kante
gebundene Kante
y4
freie Kante
x5
x4
gebundene Kante
y5
y6
freie Kante
x6
Abb.: Breitensuchbaum für die in der vorstehenden Abbildung gezeigte Zuordnung und den
Startknoten y3
Maximal gewichtete Zuordnung (maximum weigtht matching)
Für einen ungerichteten, bewerteten Graph G = (V , E ) mit Kantenbewertung
w : E → ℜ ist das Gewicht einer Zuordnung M die Summe der Gewichte der Kanten
von M. Von Interesse ist eine maximale gewichtete Zuordnung (maximum weight
matching). Falls bspw. in einer Firma mit k Mitarbeitern m1,…,mk die k Tätigkeiten
t1,…,tk auszuführen sind und eine Maßzahl w(mi,tj) für die Eignung eines Mitarbeiters
mi für die Tätigkeit tj bekannt ist, sofern Mitarbeiter mi die Tätigkeit tj überhaupt
ausführen kann, so kann eine maximale gewichtete Zuordnung von Mitarbeitern und
Tätigkeit erwünscht sein.
169
Algorithmen und Datenstrukturen
m1
m2
m3
m4
m5
m6
6
1
2
2
t1
2
t2
1
5
t3
6
6
t4
5
6
5
t5
7
t6
2.8.3 Maximale Zuordnung im allgemeinen Fall
In allgemeinen Graphen kann man mit einer einfachen Breitensuche vergrößernde
Wege nicht unbedingt finden finden.
Bsp.: Gegeben ist
4
9
12
3
5
1
8
2
10
11
6
7
mit der Zuordnung M = {(6,7 ), (8,10 )}
Finde den vergrößernden Weg vom freien Knoten 2 aus mit Hilfe eines
alternierenden Baums.
2
freier Knoten
freie Kante
6
gebundene Kante
7
freie Kante
8
10
gebundene Kante
?
Abb.: Alternierender Baum (auf einen Teilgraph beschränkt)
170
Algorithmen und Datenstrukturen
Die Breitensuche sorgt dafür, dass Knoten 10 besucht wird, bevor die Nachfolger von
Knoten 8 im alternierenden Baum in Betracht gezogen werden. Wenn jeder Knoten,
wie bei der Breitensuche üblich, nur einmal besucht werden darf, so verhindert das
Finden des alternierenden Wegs 2, 6, 7, 10 der nicht mit einem freien Knoten endet,
dass der alternierende Weg 2, 6, 7, 8, 10, 11 gefunden wird (, obwohl der mit einem
freien Knoten enden würde. Die reine Breitensuche ist also hier nicht in der Lage,
vergrößernde Wege auch wirklich zu finden.
Ursache: Ein und derselbe Knoten kann auf mehreren, alternierenden Wegen in
gerader und ungerader Entfernung vom Startknoten auftreten. Knoten 10 tritt auf
dem alternierenden Weg 2, 6, 7, 10 in ungerader Entfernung vom Startknoten 2 auf,
währen er bei dem alternierenden Weg 2, 6, 7, 8, 10 in gerader Entfernung vom
Startknoten auftritt. Man kann aber nicht in eine Abänderung der reinen Breitensuche
das 2malige Besuchen eines jeden Knoten erlauben, nämlich je einmal für die
gerade und ungerade Entfernung vom Startknoten, dann können auch Knotenfolgen
gefunden werden, die keinen vergrößernden Weg beschreiben, z.B. das Matching
M = {(6,7 ), (8,10 )} kann für Startknoten 2 die Knotenfolge 2, 6, 7, 8, 10, 7, 6, 5 liefern.
Überlegung: Das Finden eines vergrößernden Wegs von einem freien Knoten v aus
ist nur dann schwierig, wenn es einen alternierednen Weg p von v zu einem Knoten
v’ in jeder Entfernung von v gibt und wenn eine Kante v’ mit einer anderen v’’
verbindet, der auf dem Weg ebenfalls in gerader Entfernung von v liegt.
v’
v
v’’
j
i
Der Teil des Wegs p von v’’ nach v’ heißt zusammen mit der Kante (v’,v’’) Blüte. Eine
Blüte ist also ein Zyklus ungerader Länge. Der Teil des Wegs p von v nach v’’ heißt
Stiel der Blüte.
In der vorstehenden Abb. gibt es sowohl einen alternierenden Weg von v nach i als
auch einen von v nach j. Den Weg von v nach i erhält man, wenn man im Zaklus
ungerader Länge im Uhrzeigersinn fortschreitet. Den Weg von v nach j erhält man
durch Besuchen einiger Knoten des Zyklus entgegen dem Uhrzeigersinn. Diese
beiden Wege kann man finden, wenn man die Blüte auf einen Knoten schrumpfen
lässt, also den Zyklus ungerader Länge in einen Knoten kollabiert. Jede Kante, die
vor dem Schrumpfen mit einem Knoten des Zyklus inzident war, ist nach dem
Schrumpfen mit dem die Blüte repräsentierten Knoten inzident.
v
i
j
Abb.: Effekt des Schrumpfens der Blüte zur vorstehenden Abb.
171
Algorithmen und Datenstrukturen
Wenn ein Graph G’ aus einem Graph G durch Schrumpfen einer Blüte entsteht, so
gibt es in G’ genau dann einen vergrößernden Weg, wenn es einen solchen in G gibt.
Blüte
⇒
frei
Blüte in G = Knoten in G’
außen
außen
frei
G
Blüte (blossom): Kreis ungerader Länge
Kante von „außen“ nach „außen ergibt Blüte.
G’
Es gilt folgender Satz: G’ hat erweiternden Weg ⇔ G hat erweiternden Weg
172
Herunterladen