1. Objektorientierte Programmierung mit Daten− strukturen und

Werbung
Algorithmen und Datenstrukturen
1. Objektorientierte Programmierung mit Daten−
strukturen und Algorithmen
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 Daten−
verarbeitung (DV). So führte hier bspw. die Frage des Zugriffs auf ein Element einer
endlichen Menge zu einer großen Sammlung von Algorithmen , 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.
Die Datenorganisation oder Datenstruktur und die zugehörigen Algorithmen sind
demnach ein entscheidender Bestandteil eines leistungsfähigen Programms. Ein
einführendes Beispiel soll diesen Sachverhalt vertiefen.
1
1.1 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.
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 :
2
#ifndef BAUMKNOTEN
#define BAUMKNOTEN
#ifndef NULL
const int NULL = 0;
#endif // NULL
1
2
D. E. Knuth hat einen großen Teil dieses Wissens in "The Art of Computer Programming" zusammengefaßt
vgl. pr11_1, baumkno.h
1
Algorithmen und Datenstrukturen
// 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
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:
2
Algorithmen und Datenstrukturen
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;
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.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;
3
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;
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.
1.1.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:
Zeiger auf Baumknoten
Top−Element
Zeiger auf Baumknoten
Zeiger auf Baumknoten
nil
nil
Abb. 1.1−2: Aufbau eines Stapels
4
Algorithmen und Datenstrukturen
Der nicht rekursive Baumdurchlauf−Algorithmus läßt sich mit Hilfe der
Stapelprozeduren der Containerklasse Stack der Standard Template Library (STL)
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();
}
}
}
Dieser Algorithmus ist zu überprüfen mit Hilfe des folgenden binären Baumes
5
Algorithmen und Datenstrukturen
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 Durch−
laufen 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
1.1.3 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.:
−− das 1−dimensionale Feld F
6
Fritz
Algorithmen und Datenstrukturen
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)
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.
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
7
Algorithmen und Datenstrukturen
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 mit Hilfe von Daten−
verarbeitungsanlagen.
Bäume sind deshalb auch Bestandteil von Container−Klassen aktueller Compiler
(C++, Java). Die Java Foundation Classes (JFC) enthalten eine Klasse JTree aus
dem Paket javax.swing, die eine Baumstruktur darstellt .
3
1.2 Begriffe und Erläuterungen zu Datenstrukturen und Pro−
grammierverfahren
1.2.1 Algorithmus (Verarbeitung von Daten)
1.2.1.1 Datenstruktur und Programmierverfahren
Datenorganisation heißt: Daten zweckmäßig so einrichten, daß eine möglichst
effektive Verarbeitung erreicht werden kann. So wurde im einführenden Beispiel die
Bearbeitung rekursiver Strukturen (Bäume) mit Hilfe der Datenstruktur Stapel und
den dazugehörigen Programmierverfahren (PUSH, POP) ermöglicht. Das braucht
aber immer nicht "von Anfang an" untersucht bzw. implementiert zu werden. Bereits
vorhandenes
Wissen,
z.B.
über
Datenstrukturen
und
dazugehörige
Programmierverfahren, ist zu nutzen. Das Wissen über den Stapel und seine
Programmierung kann allgemein zur Bearbeitung der Rekursion auf einem
Digitalrechner benutzt werden und ist nicht nur auf die Bearbeitung von
Baumstrukturen beschänkt.
Datenstruktur und Programmierverfahren bilden (, wie das einführende Beispiel
zeigt,) eine Einheit. Bei der Formulierung des Lösungswegs ist man auf eine
bestimmte Darstellung der Daten festgelegt. Rein gefühlsmäßig könnte man sogar
sagen: Daten gehen den Algorithmen voran. Programmieren führt direkt zum
Denken in Datenstrukturen, um Datenelemente, die zueinander in bestimmten
Beziehungen stehen, zusammenzufassen. 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.
Die Programmierverfahren sind durch Algorithmen festgelegt.
1.2.1.2 Der intuitive Algorithmus−Begriff
Algorithmen spielen auch im täglichen Leben eine Rolle, z.B. in
− Bedienungsanleitungen
− Gebrauchsanleitungen
− Bauanleitungen
3
vgl. pr13229
8
Algorithmen und Datenstrukturen
Man kann deshalb zunächst einmal den Begriff Algorithmus intuitiv so festlegen:
Ein Algorithmus ist eine präzise (d.h. in einer festgelegten Sprache abgefasste) endliche
Beschreibung eines allgemeinen Verfahrens unter Angabe ausführbarer (Verarbeitungs−) Schritte.
Bei der Konzeption von Algorithmen spielen die Begriffe Terminierung und
Determinismus eine Rolle:
Ein Algorithmus heißt terminierend, wenn er (bei jeder erlaubten Eingabe von Parameterwerten) nach
endlich vielen Schritten abbricht.
bzw.
Ein Algorithmus terminiert, falls er für alle Eingaben nach endlich vielen Schritten ein Resultat liefert.
Ein deterministischer Ablauf ist bestimmt durch die eindeutige Vorgabe der
Schrittfolge. Ein determiniertes Ergebnis wird eindeutig erreicht nach vorgegebener
Eingabe. Nicht determiniert ist bspw. die zufällige Wahl einer Karte aus einem
Kartenstapel.
Ein Algorithmus heißt determiniert, falls er bei gleichen Eingaben und Startbedingungen stets
dasselbe Ergebnis liefert.
Ein Algorithmus heißt deterministisch, wenn zu jedem Zeitpunkt seiner Ausführung höchstens eine
Möglichkeit der Fortsetzung besteht; anderenfalls heißt er "nicht deterministisch".
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 x
Oder: Subtrahiere 4 von x und subtrahiere das Ergebnis von x
3. Schreibe das Ergebnis auf
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.1.3 Bausteine für Algorithmen
Gängige Bausteine zur Beschreibung 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:
Öffne Kaffeedose;
9
Algorithmen und Datenstrukturen
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
jeweils
den
Programmiersprachen−
repeat ... until ?
do ?
while ...
while ? do ...
while ( ... ) ...
for each ... do ?
for ... do ?
for ( ... ) ...
− Unterprogramm
− 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
10
Algorithmen und Datenstrukturen
einem rekursiven Aufruf. Typischerweise werden die aktuellen Parameter so
modifiziert, daß die Problemgröße schrumpft, damit nach mehrmaligem Wiederholen
dieses Prinzips keine weiterer Aufruf erforderlich ist und die Rekursion abbrechen
kann.
1.2.1.4 Formale Eigenschaften von Algorithmen
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 nachweisen .
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. Man könnte beides durch
Implementierung des Algorithmus in einer konkreten Programmiersprache auf einem
konkreten Rechner für eine Menge repräsentativer Eingaben messen. Solche
experimentell ermittelten Meßergebnisse lassen sich nicht oder nur schwer auf
andere Implementierungen und andere Rechner übertragen.
Aus dieser Schwierigkeit bieten sich 2 Auswege an:
4
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
Literatur5 zu diesem Zweck häufig benutztes Maschinenmodell ist das der RAM (Random−Access−
Maschine). 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 Parameter6.
Laufzeit und Speicherbedarf eines Algorithmus hängen in der Regel von der Größe der Eingabe ab7.
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 worst−
case 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.
Statt "für die Laufzeit T(N) eines Algorithmus gilt für alle N: T ( N ) ≤ c1 ⋅ N + c 2 mit 2 Konstanten c1
und c2" sagt man "T(N) ist von der Größenordnung N" oder "T(N) ist O(N)" oder "T(N) ist ein O(N)"
und schreibt
T ( N ) ∈ O( N ) .
Die weitaus häufigsten und wichtigsten Funktionen zur Messung der Effizienz von
Algorithmen in Abhängigkeit von der Problemgröße sind:
4 E. Dijkstra formulierte das so: Man kann durch Testen die Anwesenheit von Fehlern, aber nicht die
Abwesenheit von Fehlern nachweisen.
5 Vgl. Aho, Hopcroft, Ullman: The Design and Analysis of Computer Algorithms, Addison−Wesley
Publishing Company
6 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.
7 die im Einheitskostenmaß oder im logarithmischen Kostenmaß gemessen wird
11
Algorithmen und Datenstrukturen
Logarithmisches Wachstum
log N
N
N ⋅ log N
Lineares Wachstum
N ⋅ log N −Wachstum
Quadratisches, kubisches, ... Wachstum
3
N 2 , N , ...
N
2 N , 3 , ...
Exponentielles Wachstum
Abb.:
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.
1.2.1.5 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 f ( N ) → N
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ätz,
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 , a2 , a3 ,..., an ∈ Z
12
Algorithmen und Datenstrukturen
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
Wie oft wird die Anweisung "max = i" im Mittel ausgeführt (abhängig von n)?
Die gesuchte mittlere Anzahl sei Tn. Offenbar gilt:
ausgeführt, wenn ai das größte der Elemente
1 ≤ Tn ≤ n
. "max = i" wird genau dann
a1 , a2 , a3 ,..., ai ist.
a , a , a ,..., a
n
Angenommen wird Gleichverteilung: Für jedes i = 1, ... , n hat jedes der Elemente 1 2 3
die gleiche Chance das größte zu sein, d.h.: Bei N Durchläufen wird N/n−mal die Anweisung "max = i"
ausgeführt.
N ⋅ Tn (Aufwendungen bei N Durchläufen vom "max = i"):
N N
N
1 1
1
N ⋅ Tn = N + + + ... + = N (1 + + + ... + )
2 3
n
2 3
n
Daraus folgt für
Dies ist Hn, die n−te harmonische Zahl. Für Hn ist keine geschlossene Formel bekannt, jedoch eine
T = H ≈ ln n + γ
8
n
ungefähre Abschätzung: n
. 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. O−Notation läßt sich mathematisch exakt definieren:
f (n)
f ( n) = O( g (n )) :⇔ ∃ c, n0 ∀n ≥ n 0 : f ( n) ≤ c ⋅ g ( n) mit f , g : N → N , d.h. g ( n) ist für
genügend große 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 f : Ν → Ν durch Eingabe einer einfachen Vergleichsfunktion
g : Ν → Ν 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 (logn)
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
O (n k ) für k ≥ 0
O (2 n )
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!)
8
Eulersche Konstante
γ = 0.57721566
13
Algorithmen und Datenstrukturen
Abb.:
Zur Veranschaulichung des Wachstums konnen die folgende Tabellen betrachtet
werden:
f(N)
ldN
N
N ⋅ ldN
N=2
1
2
2
24=16
4
16
64
25=256
8
256
1808
210
10
1024
10240
N2
N3
2N
4
8
4
256
4096
65536
65536
16777200
≈ 1077
1048576
≈ 109
≈ 10308
220
20
1048576
20971520
≈ 1012
≈ 1018
≈ 10315653
−6
Unter der Annahme "1 Schritt dauert 1 µs = 10 s folgt für
N=
N
N2
3
N
2N
3N
N!
10
20
µs
10
µs
100
µs
20
µs
400
1 ms
1 ms
59 ms
3,62 s
8 ms
1s
58 min
771 Jahre
30
µs
30
µs
900
27 ms
18 min
6.5 Jahre
1016 Jahre
40
50
60
1.6 ms
2.5 ms
60
3.6 ms
64 ms
13 Tage
3855 Jahre
1032 Jahre
125 ms
36 Jahre
108 Jahre
1049 Jahre
216 ms
366 Jahre
1013 Jahre
1066 Jahre
µs
40
µs
50
µs
Abb.:
1.2.1.6 Laufzeitberechnungen ("Big−O"−)
N
Ein einfaches Beispiel: Gegeben ist die folgende Funktion zur Berechnung von
∑i
3
i =1
public static int sum(int n)
{
/* 1 */ int teilSumme;
/* 2 */ teilSumme = 0;
/* 3 */ for (int i = 1; i <= n; i++)
/* 4 */
teilSumme += i * i * i;
/* 5 */ returm teilSumme;
}
Analyse zur Effizienz:
Zeile 1 und Zeile 2 zählen je einmal.
Zeile 4 zählt viermal (2 Multiplikationen, Zuweisung und 1. Additition) und
wird N−mal ausgeführt. Das ergibt 4N.
Zeile 3 zeigt die Deklaration und Initialisierung von i (zählt zweimal), den
Test i <= N und das Inkrementieren i++ (zählt jeweils N−mal).
Insgesamt führt das zu 2N + 2. Ignoriert man Aufruf und
Rückkehranweisung der Funktion erhält man 6N + 4. Man sagt dazu: Die
Funktion besitzt ein Leistungsverhalten von O(N).
Einiges kann bei der Abschätzung offensichtlich beschleunigt werden. In Zeile 3
steht bspw. eine O(1)−Anweisung. Es ist egal (für die Abschätzung der
Laufzeitberechnung), ob bei der Ausführung diese Anweisung 2fach oder 3fach
gezählt wird. Auch bzgl. der Schleife ist der Faktor 2 und die Addition von 2
unerheblich.
14
Algorithmen und Datenstrukturen
Das führt zu folgenden Regeln zur Abschätzung des Leistungsverhaltens nach der
"Big−O"−Notation:
1. Regel (für Schleifen): Die Laufzeit einer Schleife ist im wesentlichen bestimmt
durch die Anzahl der Anweisungen innerhalb des Schleifenkörpers multipliziert mit
der Anzahl der Iterationen.
2. Regel (für verschachtelte Schleifen): Die Laufzeit einer Anweisung innerhalb einer
Gruppe verschachtelter Schleifen ist bestimmt durch die Laufzeit der Anweisung
multipliziert mit dem Produkt aller Schleifengrößen.
Bsp.:
for (i=1; i <= n; i++)
for (j = 1; j <= n; j++)
k++;
ist einzuordnen unter O(N2).
3. Regel (aufeinanderfolgende Anweisungen): Der größte Wert zählt für das
Leistungsverhalten.
Bsp.:
for (int i=1; i <= n; i++)
a[i] = 0;
// O(N)
for (int i=1; i <= n; i++)
for (int j=1; j <= n; j++)
a[i] += i + j;
// O(N2)
Insgesamt ergibt sich das Leistungsverhalten O(N2).
4. Regel: Die Laufzeit einer "if"−Anweisung ist niemals größer als die Laufzeit des
Tests plus der größeren Laufzeit vom "ja"− bzw. "nein"−Zweig.
Rekursionen können häufig auf einfache Schleifen mit dem Leistungsverhalten O(N)
zurückgeführt werden, z.B.:
public static long fakultaet(int n)
{
if (n <= 1) return 1;
else return n * fakultaet(n−1);
Liegen in einer Funktion mehrere rekursive Aufrufe vor, dann ist die Umsetzung in
eine einfache Schleifenstruktur nicht so einfach.
Bsp.:
public static long fib(int n)
{
/* 1 */ if (n <= 1)
/* 2 */ return 1;
else
/* 3 */ returm fib(n−1) + fib(n−2);
}
Die Analyse ergibt unter der Annahme, daß T(N) die Laufzeit nach einem Aufruf von
fib(n) ist: Für N = 0, N = 1 ist T(0) = T(1) = 1 (irgendein konstanter Wert.
In Zeile 3 wird fib(N−1) aufgerufen, was eine Laufzeit von T(N−1) bewirkt.
Anschließend wird fib(N−2) aufgerufen, was eine Laufzeit von T(N−2) bewirkt.
Zusammengezählt ergibt das: T(N) = T(N−1) + T(N−2) + 2. Da fib(N) =
fib(N−1) + fib(N−2) ist, kann leicht gezeigt werden, daß T(N) >= fib(N) ist.
5
fib( N ) < ( ) N
3 . Das bedeutet: Die Laufzeit dieses Programms
Man kann zeigen:
wächst exponentiell (schlechter geht es nicht mehr).
15
Algorithmen und Datenstrukturen
Man kann häufig dasselbe Problem mit verschieden Algorithmen lösen. Das Ziel ist
natürlich, den für das Problem besten Algorithmus zu finden bzw. zu implementieren:
16
Algorithmen und Datenstrukturen
Bsp.: Das Maximum−Subarray−Problem
Gegeben ist eine Folge X von N ganzen Zahlen in einem Array. Gesucht ist die maximale Summe
aller zusammenhängenden Teilfolgen. Sie wird als maximale Teilsumme bezeichnet.
So ist für die Eingabefolge
X[0]
31
X[1]
−41
X[2]
59
X[3]
26
X[4]
−53
X[5]
58
X[6]
97
X[7]
−93
X[8]
−23
X[9]
84
die Summe der Teilfolgen X[2] + X[3] + X[4] + X[5] + X[6] mit dem Wert (59 + 26 − 53 + 58 + 97) =
187 die Lösung des Problems. Lösungen zu diesem Problem können auf verschiedene Weise erreicht
werden:
1. Lösung
public
{
/* 1
/* 2
/* 3
/*
/*
/*
/*
/*
4
5
6
7
8
/* 9
}
static int
maxSubsum1(int a[])
*/ int maxSumme = 0;
*/ for (int i = 0; i < a.length;i++)
*/ for (int j = i; j < a.length; j++)
{
*/
int summe = 0;
*/
for (int k = i; k <= j; k++)
*/
summe += a[k];
*/
if (summe > maxSumme)
*/
maxSumme = summe;
}
*/ return maxSumme;
N
Die Analyse des Leistungsverhaltens wird bestimmt durch
berechnet, wieviele Male Zeile 6 ausgeführt wird.
N
j
∑∑∑1 = O( N
i =1 j =i k =i
3
)
. Diese Summe
2. Lösung
public static int maxSubsum2(int a[])
{
/* 1 */ int maxSumme = 0;
/* 2 */ for (int i = 0; i < a.length;i++)
{
/* 3 */ int summe = 0;
/* 4 */ for (int j = i; j < a.length; j++)
{
/* 5 */
summe += a[j];
/* 6 */
if (summe > maxSumme)
/* 7 */
maxSumme = summe;
}
}
/* 8 */ return maxSumme;
}
In dieser Lösung ist das Leistungsverhalten auf O(N2) reduziert.
3. Lösung
Diese Lösung folgt der "Divide−and−Conquer"−Strategie, die ein sehr allgemeines und mächtiges
Prinzip zur algorithmischen Lösung von Problemen darstellt. Das zugehörige Problemlösungsschema
kann allg. so formuliert werden:
17
Algorithmen und Datenstrukturen
1. Divide: Teile das Problem der Größe N in (wenigstens) 2 annähernd gleich große Teilprobleme,
wenn N > 1 ist, sonst löse das Problem der Größe 1 direkt.
2. Conquer: Löse die Teilprobleme auf dieselbe Art.
3. Merge: Füge die Teillösungen zur Gesamtlösung zusammen.
Abb.: Divide and Conquer−Verfahren zur Lösung eines Problems der Größe N
Bei der Anwendung dieses Algorithmus auf das vorliegende Problem bewirkt das Teilen der Folge in
Teilfolgen evtl. das Trennen der Teilfolge mit der größten Teilsumme, z.B.: Bei der Vorgabe
a[0]
4
a[1]
−3
a[2]
5
1. Häfte
a[3]
−2
a[4]
−1
a[5]
2
a[6]
6
2. Hälfte
a[7]
−2
ist die größte Teilsumme in der ersten Teilhälfte 6 (a[0] + a[1] + a[2]), die größte Teilsumme in der
zweiten Teilhälfte ist 8 (a[5] + a[6]). Die maximale Summe in der 1. Hälfte, die das letzte Element in
der 1. Hälfte mit einschließt (a[0] + a[1] + a[2] + a[3]) ist 4. Die maximale Summe in der 2. Hälfte, die
das erste Element in der 2. Hälfte einschließt ist 7. Die maximale Summe, die beide Hälften
überspannt ist 4 + 7 = 11. Der Algorithmus muß demnach Teilsummenbildungen über die jeweilige
Teilhälften berücksichtigen.
private static int maxSubsum(int a[], int links, int rechts)
{
/* 1 */
if (links == rechts)
/* 2 */
if (a[links] > 0)
// Falls dieses Element positiv ist
/* 3 */
return a[links];
// dann ist es die max.Teilsumme
/* 4 */
else return 0;
/* 5 */
int mitte = (links + rechts) / 2;
/* 6 */
int maxLinkeSumme = maxSubsum(a, links, mitte);
/* 7 */
int maxRechteSumme = maxSubsum(a, mitte + 1, rechts);
/* 8 */
int maxLinkeGrenzSumme = 0, linkeGrenzsumme = 0;
/* 9 */
/*10 */
/*11 */
/*12 */
/*13 */
/*14 */
/*15 */
/*16 */
/*17 */
/*18 */
for (int i = mitte; i >= links; i−−)
{
linkeGrenzsumme += a[i];
if (linkeGrenzsumme > maxLinkeGrenzSumme)
maxLinkeGrenzSumme = linkeGrenzsumme;
}
int maxRechteGrenzSumme = 0, rechteGrenzsumme = 0;
for (int i = mitte + 1; i <= rechts; i++)
{
rechteGrenzsumme += a[i];
if (rechteGrenzsumme > maxRechteGrenzSumme)
maxRechteGrenzSumme = rechteGrenzsumme;
}
if (maxLinkeSumme > maxRechteSumme)
if (maxLinkeSumme > (maxRechteGrenzSumme + maxLinkeGrenzSumme))
return maxLinkeSumme;
else return (maxRechteGrenzSumme + maxLinkeGrenzSumme);
else if (maxRechteSumme > (maxRechteGrenzSumme +
maxLinkeGrenzSumme) )
return maxRechteSumme;
else return (maxRechteGrenzSumme + maxLinkeGrenzSumme);
}
public static int maxSubsum3(int a[])
{
return maxSubsum(a, 0, a.length − 1);
}
Die Anwendung des Lösungsverfahrens auf das
Implementierung mit dem Leistungsverhalten O(NlogN).
Max−Subarray−Problem
führt
zu
einer
4. Lösung: Implementierung mit dem Leistungsverhalten O(N)
Die Positionen 0,..,N−1 der Eingabefolge bilden eine aufsteigend sortierte, lineare Folge von
Inspektionsstellen (oder: Ereignispunkten). Man durchläuft die Eingabe in der durch die
18
Algorithmen und Datenstrukturen
Inspektionsstelleb vorgegebenen Reihenfolge und führt zugleich eine vom jeweiligen Problem
abhängige, dynamisch veränderliche, d.h. an jeder Informationsstelle gegebenenfalls zu korrigierende
Information mit. Im vorliegenden Fall ist das die maximale Summe einer Teilfolge (maxSumme) im
gesamten bisher inspizierten Anfangsteil und das an der Inspektionstelle endende rechte
Randmaximum (summe) des bisher inspizierten Anfangsstücks.
public static int maxSubsum4(int a[])
{
/* 1 */ int maxSumme = 0, summe = 0;
/* 2 */ for (int j = 0; j < a.length; j++)
{
/* 3 */
summe += a[j];
/* 4 */
if (summe > maxSumme)
/* 5 */
maxSumme = summe;
/* 6 */
else if (summe < 0)
/* 7 */
summe = 0;
}
/* 8 */ return maxSumme;
}
Das ist ein Algorithmus, der in linearer Zeit ausführbar ist. Zur Bestimmung der maximalen Teilfolge
müssen alle Folgeelemente wenigstens einmal betrachtet werden. Das sind insgesamt N Schritte.
1.2.1.7 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.
19
Algorithmen und Datenstrukturen
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.
1.2.1.8 Effizienz
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.
Berechnungsgrundlagen für rechnerische Komplexität
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 DV−
Literatur 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 Handlungsreisenden9 (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
9 Vorbild für viele Optimierungsaufgaben, wie sie vor allem im Operations Research immer wieder
vorkommen.
20
Algorithmen und Datenstrukturen
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
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,
a x N +...+a x 2 + a x + a
2
1
0 ausgedrückt
wenn man die benötigte Rechenzeit durch ein Polynom N
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
das Durchsuchen proportional
Zeit"
N ⋅ ( N − 1) / 2 , d.h. O(N2), und damit polynomial "in quadratischer
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
21
Algorithmen und Datenstrukturen
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. "Bubble−
2
Sort") bestehen aus Algorithmen mit einer Komplexität von O ( N ) . 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
N
Bearbeitung von Graphen). Ein Algorithmus mit einer Komplexität von O ( 2 ) zeigt
exponentielle Komplexität. Ein derartiger Algorithmus ist nur für kleine N auf einem
Rechner lauffähig.
22
Algorithmen und Datenstrukturen
1.2.2 Daten und Datenstrukturen
1.2.2.1 Der Begriff Datenstruktur
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; wik 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
(Ordnungs−relation). Dafür steht hier (im vorliegenden Beispiel) der Name der
Studenten:
23
Algorithmen und Datenstrukturen
Josef
Juergen
Liesel
Abb. 1.2−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:
24
Algorithmen und Datenstrukturen
STUDENT
FACH
DATEN
DOZENT
DATEN
DATEN
DATEN
DATEN
DATEN
DATEN
DATEN
DATEN
DATEN
geordnet
geordnet
(z.B. nach Matrikelnummern)
geordnet
(z.B. nach Titel
im Vorlesungsverzeichnis)
(z.B. nach Namen)
Abb.: 1.2−2: 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.2−3: Darstellung der Zusammensetzung eines Geräts
25
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 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.2−4: 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:
26
Algorithmen und Datenstrukturen
Person
Person
"Juergen"
Person
"Josef"
Buch
Buch
"Stroustrup"
"Goldberg"
Buch
"Lippman"
Abb. 1.2−5: 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.2−6: 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.
27
Algorithmen und Datenstrukturen
1.2.2.2 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.2−7: 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
( x, y ) ∈ R }
( x, y ) ∈ R }
Bild(R) = { y |
Def(R) = { x |
28
Algorithmen und Datenstrukturen
2. Inverse Relation (Umkehrrelation)
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
x∈ M
(Für alle Elemente x aus M gilt, x steht in Relation zu x)
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.2−8: 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:
29
Algorithmen und Datenstrukturen
∀ ( x , y ) ∈ R → (( y , x ) ∉ R )
( x, y )∈R
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
( x, y ) ∈ R
( y, z ) ∈ R
Eine binäre Relation ist transitiv, wenn ((
) und ((
) ist, dann ist auch ((
). x hat also y zur Folge und y hat z zur Folge. Somit hat x auch z zur Folge.
( x, z ) ∈ R
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:
(1)
Mx ∩ M y = 0
M ∪ M ∪....∪ M = M
1
2
y
(2)
(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?
30
Algorithmen und Datenstrukturen
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
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 Eigen−
schaft 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.
31
Algorithmen und Datenstrukturen
(1)
(2)
a
a
b
e
e
b
d
c
d
c
Abb. 1.2−9: Totale und partielle Ordnungsrelationen
5. "Natürliche Ordnungsbeziehungen" in Java
Das Comparable Interface aus dem Paket java.lang dient zum Herstellen
"natürlicher Ordnungsbeziehungen":
/**************** Comparable.java *****************************
/** Das Interface deklariert eine Methode anhand der sich das
* das aufgerufene Objekt mit dem uebergebenen vergleicht.
* Fuer jeden vom Objekt abgeleiteten Datentyp muss eine solche
* Vergleichsklasse implementiert werden.
* Die Methode erzeugt eine Fehlermeldung, wenn "a" ein Objekt
* einer anderen Klasse als dieses Objekt ist.
*
* int compareTo(Comparable a)
*
liefert
0, wenn this == 0
*
liefert < 0, wenn this < a
*
liefert > 0, wenn this > a
*
*/
public interface Comparable
{
public int compareTo(Comparable a);
}
Seit dem JDK 1.2 wird das Comparable Interface bereits von vielen eingebauten
Klassen implementiert, etwa von String, Character, Double, usw. Die natürliche
Ordnung ergibt sich, indem man alle Elemente paarweise miteinander vergleicht und
dabei jeweils das kleinere vor der größere Element stellt.
Besitzt eine Klasse das Interface Comparable nicht, dann kann auch eine
Implementierung des Interface Comparator vorgesehen werden.
/**************** Comparator.java *****************************
/** Das Interface deklariert zwei Methoden zur Durchfuehrung von
* Vergleichen.
* Die Methode equals() führt auf den Rückgabewert true bzw. false,
* je nachdem ob this == o ist.
* compare() hat folgende Rückgabewerte:
* Falls das erste Element vor dem zweiten Element kommt,
* ist der Rückgabewert negativ.
* Falls das erste Element nach dem zweiten Element kommt,
32
Algorithmen und Datenstrukturen
* ist der Rückgabewert positiv
* Der Rückgabewert 0 signalisiert, dass die beiden Elemente an
* der gleichen Ordnungsposition eingeordnet werden.
*/
public interface Comparator
{
public int compare(Object element1, Object element2);
public boolean equals(Object o);
}
1.2.2.3 Klassifikation von Datenstrukturen
Eine Datenstruktur ist durch Anzahl und Eigenschaften der Relationen bestimmt.
Obwohl sehr viele Relationstypen denkbar sind, gibt es nur 4 fundamentale
Datenstrukturen , 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.
10
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.
2. Bäume
Sie 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.: 1.2−10: 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:
10
nach: Rembold, Ulrich (Hrsg.): "Einführung in die Informatik", München/Wien, 1987
33
Algorithmen und Datenstrukturen
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. ........
Die Definitionen eines Baums mit Hilfe von Äquivalenzrelationen regelt aus−
schließ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.
3. Graphen
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 . Die folgende Darstellung zeigt einen Netzplan zur Ermittlung
des kritischen Wegs:
11
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.2−11: Ein Graph der Netzplantechnik
4. Dateien
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 Studenten12 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.
11
12
vgl. 1.2.2.2, Abb. 1.2−7
vgl. 1.2.2.1
34
Algorithmen und Datenstrukturen
5. Datenbanken
Eine Datenbank ist die Sammlung von verschiedenen Datensatz−Typen. Die
Datensätze sind in einer Codasyl−Datenbank untereinander verbunden, z.B. alle
Studenten im Fachbereich "Informatik und Mathematik" der Fachhochschule
Regensburg, z.B.:
13
Fachbereich Informatik
Student_1
Student_2
....
Student_n
Abb.: 1.2−12: Erscheinungsbild der Datensätze "Fachbereich" und "Student"
Der letzte Studentensatz zeigt auf den Satz "Fachbereich Informatik und
Mathematik" zurück. Diese detaillierte Darstellung der physischen Struktur kann auf
folgende Beschreibung der logischen Datenstruktur zurückgeführt werden:
Fachbereich
betreut
Student
Abb.: 1.2−13: Logische Datenstruktur
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:
Fachbereich
1
betreut
M
Student
Abb. 1.2−14: "ER"−Diagramm zur Darstellung der Beziehung "Fachbereich−Student"
Die als "1" und "M" an den Kanten aufgeschriebenen Zahlen zeigen: Ein Fach−
bereich betreut mehrere (viele) Studenten. Solche Beziehungen können vom Typ
13
Datenbank der Data Base Task Group der Conference on Data Systems Languages (CODASYL)
35
Algorithmen und Datenstrukturen
1:M, 1:1, M:N sein. Es ist auch die Bezugnahme auf gleiche Entitätstypen möglich,
z.B.:
Person
1
1
Heirat
Abb.: 1.2−15: Bezugnahme auf den gleiche Entitätstyp "Person"
36
Algorithmen und Datenstrukturen
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. 1.2−16: 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 Datnbank für das Personalwesen:
37
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
JOB_ID
SY
PR
OP
IN
SY
PR
KA
PR
OP
TA
IN
SY
IN
KA
38
Algorithmen und Datenstrukturen
A12
A13
A14
SY
IN
KA
Abb. 1.2−17: Tabellen zur relationalen Datenbank
Die relationale Datenbank besteht, wie die vorliegende Darstellung zeigt aus einer
Datenstruktur, die Dateien (Tabellen) vernetzt. Die Verbindung besorgen
Referenzen (Fremdschlüssel).
Die vorliegende Einteilung der Datenstrukturen zeigt: Sammeln und Ordnen der
durch die reale Welt vorgegebenen Objekte ist eine der wichtigsten und häufigsten
Anwendungen in der Datenverarbeitung. Leider unterstützen herkömmliche
Programmiersprachen nicht umfassend genug diese Möglichkeiten. Erst die
objektorientierte Programmierung (vor allem Smalltalk) haben hier den Ansatz zu
einer umfassenden Implementierung (Collection, Container) gezeigt.
1.2.3 Definitionsmethoden für Datenstrukturen
1.2.3.1 Der abstrakte Datentyp
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). Der ADT ist eine Kapsel, der die gemeinsame Deklaration von
Daten und Algorithmen zu einem Typ zusammenfaßt. Datenkapseln sind
elementare Bausteine, die den konventionellen Programmierstil (Programm =
Algorithmus + Daten) auf ein anspruchvolleres Niveau anheben. Die Datenkapsel
betrachtet Daten und Rechenvorschrift als eine Einheit. Das bedeutet aber auch:
Für die Ausführung einer Aufgabe ist die Datenkapsel selbst verantwortlich. Der
Anwender hat damit nichts zu tun. Er teilt dem durch die Datenkapsel bestimmten
Objekt lediglich über eine Botschaft mit, daß er eine spezielle Funktion ausgeführt
haben möchte. Das Empfangsobjekt wählt daraufhin eine ihm bekannte Methode
aus und führt die dazugehörige Prozedur aus. Das Ergebnis der Methode wird vom
Objekt an den den Sender der Botschaft wieder zurückgeschickt. Die durch die
Datenkapsel realisierte Einheit hat einen speziellen Namen: Objekt. Den
zugehörigen Programmierstil nennt man: objektorientierte Programmierung.
Die objektorientierte Sichtweise faßt Daten, Prozeduren und Funktionen zu
möglichst realistischen Modellen (Objekten) der Wirklichkeit zusammen. Zugriff auf
objektorientierte Modelle ist nur den Methoden (Prozeduren und Funktionen) erlaubt.
Eine Methode gehört zu einem Objekt mit dem Zweck die Daten des Oblekts zu
bearbeiten. Nachrichen (Botschaften) sind neben den Objekten das 2. wesentliche
Element in objektorientierten Programmiersprachen. Objekte machen nur dann
etwas, wenn sie eine Nachricht empfangen und für diese Nachricht eine Methode
haben. Andernfalls geben sie die Nachricht an die Klasse weiter, der das Objekt
angehört. Klassen (Objekttypen) sind Realisierungen abstrakter Datentypen und
umfassen: Attribute (Eigenschaften) , Methoden, Axiome. Sie beschreiben Zustand
und Verhalten gleichartiger Objekte.
39
Algorithmen und Datenstrukturen
Generell gilt: Ein neues Objekt (Instanz, Exemplar) einer Klasse erbt alle
Eigenschaften der Klasse. Man kann aber diesem Erbe Eigenschaften hinzufügen
bzw. Methoden streichen bzw. modifizieren. Falls der Erbe selbst Nachkommen
erhält, dann geschieht folgendes:
1. Die Instanz wird zur Klasse
2. Die Nachkommen erben die Eigenschaftten der Vorfahren
Jede Klasse in einem objektorientierten Programmiersystem (OOP) hat einen
Vorfahren
Eine unmittelbare Implementierung der objektorientierten Programmierung (und
damit von ADT) gibt es erst seit 1981 (Smalltalk−80) . In der Praxis ist dieser
Programmierstil erst seit 1980 verbreitet. Aktuell ist die objektorientierte
Programmierung vor allem durch die inzwischen weit bekannte Programmier−
sprache C++ bzw. Java.
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 bereitgestellt. Damit sollte dem Programmierer
wenigstens durch die Spezifikation die Einheit von Daten und zugehörigen
Operationen vermittelt werden.
14
15
1.2.3.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.
Eine spezielle axiomatische Methode ist die algebraische Spezifikation von Daten−
strukturen. 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
14
15
→ Schlange<T>
vgl. BYTE, Heft August 1981
vgl. Guttag, John: "Abstract Data Types and the Development of Data Structures", CACM, June 1977
40
Algorithmen und Datenstrukturen
FuegeHinzu
Vorn
Entferne
Leer
:
:
:
:
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.
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
41
Algorithmen und Datenstrukturen
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
−> Binaerbaum<T>
bin
: Binaerbaum<T>, T, Binaerbaum<T> → Binaerbaum<T>
→ Binaerbaum<T>
links
: Binaerbaum<T>
→ Binaerbaum<T>
rechts : Binaerbaum<T>
wert
: Binaerbaum<T>
−> T
→ 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.
1.2.3.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.
42
Algorithmen und Datenstrukturen
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 DV−
Anlage verknüpft (Standard−Typen). Sie sind die "Atome" auf der niedrigsten
Betrachtungsebene. Neue "höherwertige" Datentypen werden aus bereits definierten
"niederstufigen" Datentypen definiert.
1.2.3.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",
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
43
sie auf den
Algorithmen und Datenstrukturen
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.
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
44
Algorithmen und Datenstrukturen
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)
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.:
45
Algorithmen und Datenstrukturen
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
− 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.
46
Algorithmen und Datenstrukturen
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.
Ein A ist immer Ein A ist immer Ein A ist mit
mit
einem
B mit einem oder keinem
oder
assoziiert
mehre−ren
B einem B asso−
ziiert
assoziiert
Ein A ist mit kei−
nem, einem oder
mehreren B asso−
ziiert
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 UML−
Notation ist dafür oft eine gestrichelte Linie mit einem Pfeil, z.B.:
47
Algorithmen und Datenstrukturen
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.:
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
Existenzab−
hä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
48
Algorithmen und Datenstrukturen
(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
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.
49
Algorithmen und Datenstrukturen
<<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
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
50
Algorithmen und Datenstrukturen
1.2.3.5 Die Implementierung abstrakter Datentypen in C++
Klassen (Objekttypen) sind Realisierungen abstrakter Datentypen und umfassen:
Daten und Methoden (Operationen auf den Daten). Das C++−Klassenkonzept
(definiert über struct, union, class) stellt ein universell einsetzbares Werkzeug für
die Erzeugung neuer Datentypen (, die so bequem wie eingebaute Typen eingesetzt
werden können) zur Verfügung. Zu einer Klasse gehören Daten− und
Verarbeitungselemente (d.h. Funktionen). Bestandteile einer Klasse können dem
allgemeinen Zugriff entzogen sein (information hiding). Der Programmentwickler
bestimmt die Sichtbarkeit eines jeden Elements. Einer Klasse ist ein Name (Typ−
Bezeichner) zugeordnet. Dieser Typ−Bezeichner kann zur Deklaration von Instanzen
oder Objekten des Klassentyps verwendet werden.
1.2.3.5.1. Das Konzept für benutzerdefinierte Datentypen: class bzw. struct
Definition einer Klasse
Sie besteht aus 2 Teilen
1. dem Kopf, der neben dem Schlüsselwort class (bzw. struct, union) den Bezeichner der Klasse
enthält
2. den Rumpf, der umschlossen von geschweiften Klammern, abgeschlossen durch ein Semikolon,
die Mitglieder (member, Komponenten, Elemente) der Klasse enthält.
Der Zugriff auf Elemente von Klassen wird durch 3 Schlüsselworte gesteuert:
private: Auf Elemente, die nach diesem Schlüsselwort stehen, können nur Elementfunktionen
zugreifen, die innerhalb derselben Klasse definiert sind. "class"−Komponenten sind
standardmäßig "private".
protected: Auf Elemente, die nach diesem Schlüsselwort stehen, können nur Elementfunktionen
zugreifen, die in derselben Klasse stehen und Elementfunktionen, die von derselben
Klasse abgeleitet sind
public: Auf Elemente, die nach diesem Schlüsselwort stehen, können alle Funktionen in demselben
Gültigkeitsbereich, den die Klassendefinition hat, zugreifen.
Der Geltungsbereich von Namen der Klassenkomponenten ist klassenlokal, d.h. die
Namen können innerhalb von Elementfunktionen und im folgenden Zusammenhang
benutzt werden:
klassenobjekt.komponentenname
zeiger_auf_klassenobjekt−>komponentenname
klassenname::komponentenname
− Der Punktoperator "." wird benutzt, falls der Programmierer Zugriff auf ein
Datenelement oder eine Elementfunktion eines speziellen Klassenobjekts wünscht.
− Der Pfeil−Operator "−>" wird benutzt, falls der Programmierer Zugriff auf ein
spezielles Klassenobjekt über einen Zeiger wünscht
Klassen besitzen einen öffentlichen (public) und einen versteckten (private) Bereich.
Versteckte Elemente (protected) sind nur den in der Klasse aufgeführten
Funktionen zugänglich. Programmteile außerhalb der Klasse dürfen nur auf den mit
public als öffentlich deklarierten Teil einer Klasse zugreifen.
51
Algorithmen und Datenstrukturen
Eine mit struct definierte Klasse ist eine Klasse, in der alle Elemente gemäß
Voreinstellung public sind. Die Elemente einer mit union definierten Klasse sind
public. Dies kann nicht geändert werden. Die Elemente einer mit class definierten
Klasse sind gemäß Voreinstellung private. Die Zugriffsebenen können verändert
werden.
Datenelemente und Elementfunktionen
Datenelemente einer Klasse werden genau wie die Elemente einer Struktur
angegeben. Eine Initialisierung der Datenelemente ist aber nicht erlaubt. Deshalb
dürfen Datenelemente auch nicht mit const deklariert sein.
Funktionen einer Klasse sind innerhalb der Klassendefinition deklariert. Wird die
Funktion auch innerhalb der Klassendefinition definiert, dann ist diese Funktion
inline. Elementfunktionen von Klassen unterscheiden sich von den gewöhnlichen
Funktionen:
− Der Gültigkeitsbereich einer Klassenfunktion ist auf die Klasse beschränkt (class scope).
Gewöhnliche Funktionen gelten in der ganzen Datei, in der sie definiert sind
− Eine Klassendefinition kann automatisch auf alle Datenelemente der Klasse zugreifen. Gewöhnliche
Funktionen können nur auf die als "public" definierten Elemente einer Klasse zugreifen.
Es
gibt
zwei
Möglichkeiten
(Schnittstellenfunktionen) in Klassen:
zur
Angabe
von
Elementfunktionen
− Definition der Funktion innerhalb von Klassen
− Deklaration der Funktion innerhalb, Definition der Funktion außerhalb der Klasse.
Mit dem Scope−Operator :: wird bei der Defintion der Funktion außerhalb der Klasse
dem Compiler mitgeteilt, wohin die Funktion gehört.
Beim Aufruf von Elementfunktionen muß der Name des Zielobjekts angegeben
werden: klassenobjektname.elementfunktionen(parameterliste)
Die Funktionen einer Klasse haben die Aufgabe, alle Manipulationen an den Daten
dieser Klasse vorzunehmen.
Der "this"−Zeiger
Jeder (Element−) Funktion wird ein Zeiger auf das Element, für das die Funktion
aufgerufen wurde, als zusätzlicher (verborgener) Parameter übergeben. Dieser
Zeiger heißt this, ist automatisch vom Compiler) deklariert und mit der Adresse der
jeweiligen Instanz (zum Zeitpunkt des Aufrufs der Elementfunktion) besetzt. "this" ist
als *const deklariert, sein Wert kann nicht verändert werden. Der Wert des Objekts
(*this), auf den this zeigt, kann allerdings verändert werden.
52
Algorithmen und Datenstrukturen
1.2.3.5.2. Generischer ADT
In den Erläuterungen zur axiomatischen Methode wurde der Typ Schlange<T> als
generischer abstrakter Datentyp bezeichnet, da er "übliche Schlangeneigenschaften"
bereitstellt. Ein Typ Stapel<T> stellt dann übliche Stapeleigenschaften bereit. Der
eigentliche benutzerdefinierte Datentyp bezeiht sich konkret auf einen speziellen
Typ, z.B. Schlange<int>, Stapel<int>. In C++ kann mit Hilfe von templates
(Schablonen) zur Übersetzungszeit eine neue Klasse bzw. auch eine neue Funktion
erzeugt werden. Schablonen (Templates) können als "Meta−Funktionen" aufgefaßt
werden, die zur Übersetzungszeit neue Klassen bzw. neue Funktionen erzeugen.
Klassenschablonen
Mit einem Klassen−Template (auch generische Klasse oder Klassengenerator) kann
ein Muster für Klassendefinitionen angelegt werden. In einer Klassenschablone steht
vor der eigentlichen Klassendefinition eine Parameterliste. Hier werden in
allgemeiner Form "Datentypen" bezeichnet, wie sie die Elemente einer Klasse
(Daten und Funktionen) benötigen.
Bsp.: Klassenschablone für einen Stapel
16
template <class T> class Stapel
{
private:
static const int stapelgroesse;
T *inhalt;
int nachf;
void init() { inhalt = new T[stapelgroesse]; nachf = 0; }
public:
Stapel()
{ init(); }
Stapel(T e) { init(); push(e); }
Stapel(const Stapel&);
~Stapel() { delete [] inhalt; }
// Destruktor
Stapel& push(T w)
{ inhalt[nachf++] = w; return *this; }
Stapel& pop()
{ nachf−−; return *this; }
T top()
{ return inhalt[nachf − 1]; }
int stapeltiefe()
{ return nachf; }
int istleer()
{ return nachf == 0; }
long groesseSt() const
{ return sizeof(Stapel<T>) + stapelgroesse * sizeof(T); }
Stapel& operator = (const Stapel<T>&);
int operator == (const Stapel<T>&);
friend ostream& operator << (ostream& o, const Stapel<T>& s);
};
template <class T> const int Stapel<T> :: stapelgroesse = 100;
// Kopierkonstruktor
template <class T> Stapel<T> :: Stapel(const Stapel& s)
{
init();
// Anlegen eines neuen Felds
nachf = s.nachf;
for (int i = 0; i < nachf; i++)
inhalt[i] = s.inhalt[i];
}
// Operatorfunktion (Stapel mit eigenem Zuweisungsoperator)
template <class T>
Stapel<T>& Stapel<T> :: operator = (const Stapel<T>& r)
{
nachf = r.nachf;
for (int i = 0; i < nachf; i++)
16
PR12351.CPP
53
Algorithmen und Datenstrukturen
inhalt[i] = r.inhalt[i];
return *this;
}
// Funktionscablone zum Aufruf einer Methode, die den Platzbedarf
// fuer Struktur und gestapelte Elemente ermitteln
template <class T> long groesse(const Stapel<T>& s)
{
return s.groesseSt();
}
// Operator <<()
template <class T> ostream& operator << (ostream& o, const Stapel<T>& s)
{
o << "<";
for (int i = 0; i < s.nachf; i++)
{
if ((i <= s.nachf − 1) && (i > 0)) o << ", ";
o << s.inhalt[i];
}
return o << ">";
}
54
Algorithmen und Datenstrukturen
1.2.3.6 Die Implementierung abstrakter Datentypen in Java
1.2.3.6.1 Modellierung von Zustand und Verhalten
Die Abbildung von Zustand bzw. Verhalten in Instanzvariable bzw. Instanzmethoden
Objekte sind die Schlüssel zum Verständnis der objektorientierten Technologie.
Objekte sind Gegenstände des täglichen Lebens: der Schreibtisch, das Skript, die
Vorlesung. All diese Objekte der realen Welt haben Zustand und Verhalten. Auch
Software−Objekte haben Zustand und Verhalten. Der Zustand wird in Variablen
festgehalten, das Verhalten der Objekte beschreiben Methoden. Die Variablen
bilden den Kern der Objekte. Methoden schirmen den Objektkern von anderen
Objekten des Programms ab (Kapselung). Software−Objekte kommunizieren und
verkehren über Nachrichen (Botschaften) miteinander. Das sendende Objekt
schickt dem Zielobjekt eine Aufforderung, eine bestimmte Methode auszuführen.
Das Zielobjekt versteht (hoffentlich) die Aufforderung und reagiert mit der
zugehörigen Methode. Die genaue formale Schreibweise solcher Botschaften in
objektorientierten Sprachen ist im Detail verschieden, jedoch wird meistens folgende
Form verwendet: Empfänger.Methodenname(Argument). "Argument" ist in dem
Botschaftsausdruck ein Übergabeparameter für die Methode.
In der realen Welt existieren häufig Objekte der gleichen Art. Sie werden über einen
Prototyp, eine Klasse, zusammengefaßt. Eine Klassendefinition in Java wird durch
das Schlüsselwort "class" eingeleitet. Anschließend folgt innerhalb von
geschweiften
Klammern
eine
beliebige
Anzahl
an
Variablen−
und
Methodendefinitionen. Zum Anlegen eines Objekts einer Klasse (Instanziierung )
muß eine Variable vom Typ der Klasse deklariert und mit Hilfe des new−Operators
ein neu erzeugtes Objekt zugewiesen werden. Das Speicher−Management in Java
erfolgt automatisch. Während das Erzeugen von Objekten immer einen expliziten
Aufruf des new−Operators erfordert , erfolgt die Rückgabe von nicht mehr
benötigtem Speicher automatisch .
Das Schreiben eines Programms besteht damit aus Entwurf und Zusammenstellung
von Klassen. Klassenbibliotheken (Sammlung von Klassen) stellen Lösungen für
grundlegende Programmieraufgaben bereit.
Zustand, Aussehen und andere Qualitäten eines Objekts (Attribute) werden durch
Variable definiert. Da jede Instanz einer Klasse verschiedene Werte für ihre
Variablen haben kann, spricht man von Instanzvariablen. Zusätzlich gibt es noch
Klassenvariable, die die Klasse selbst und alle ihre Instanzen betreffen. Werte von
Klassenvariablen werden direkt in der Klasse gespeichert. Der Zustand wird in
Variablen festgehalten und zeigt den momentanen Stand der Objektstruktur an, d.h.
die in den einzelnen Bestandteilen des Objekts enthaltenen Informationen und
Daten. Abhängig vom Detaillierungsgrad kann die Notation für eine Variable den
Namen, den Datentyp und den voreingestellten Wert zeigen:
17
18
19
Sichtbarkeit Typ Name = voreingestellter_Wert;
Sichtbarkeit: öffentlich (public), geschützt (protected) oder privat (private)
Typ: Datentyp
17
Eine Instanz einer Klasse ist ein (tatsächliches) Objekt (konkrete Darstellung)
Ausnahmen: String−, Array−Literale
19 Ein Garbage−Collector (niedrigpriorisierte Hintergrundprozeß) sucht in regelmäßigen Abständen nach nicht
mehr referenzierten Objekten und gibt den durch sie belegten Speicher an das Laufzeitsystem zurück
18
55
Algorithmen und Datenstrukturen
Name: eine nach bestimmten Regeln gebildete Zeichenkette
Nach der Initialisierung haben alle Variablen des Objekts zunächst Standardwerte
(voreingestellte Werte). Der Zugriff auf sie erfolgt mit Hilfe der Punktnotation:
Objekt.Variable.
Zur Bezugnahme auf das aktuelle Objekt dient das Schlüsselwort this. Es kann an
jeder beliebigen Stelle angegeben werden, an der das Objekt erscheinen kann, z.B.
in einer Punktnotation zum Verweis auf Instanzvariablen des Objekts oder als
Argument für eine Methode oder als Ausgabewert der aktuellen Methoden. In vielen
Fällen kann das Schlüsselwort this entfallen. Das hängt davon ab, ob es Variablen
mit gleichem Namen im lokalen Bereich gibt.
Zur Definition des Verhaltens von Objekten dienen Methoden. Methoden sind
Funktionen, die innerhalb von Klassen definiert werden und auf Klasseninstanzen
angewandt werden. Methoden wirken sich aber nicht nur auf ein Objekt aus. Objekte
kommunizieren auch miteinander durch Methoden. Eine Klasse oder ein Objekt kann
Methoden einer anderen Klasse oder eines anderen Objekts aufrufen, um
Änderungen in der Umgebung mitzuteilen oder ein Objekt aufzufordern, seinen
Zustand zu ändern. Instanzmethoden (Operationen, Services) werden auf eine
Instanz angewandt, Klassenmethoden beziehen sich auf eine Klasse.
Klassenmethoden können nur mit Klassenvariablen arbeiten.
Die Beschreibung der Operationen (Nachrichten, Methoden) erfolgt nach dem
folgenden Schema:
Sichbarkeit Rückgabetypausdruck Name(Parameterliste)
Sichtbarkeit: öffentlich (public), geschützt (protected), privat (private)
Rückgabetypausdruck: Jede Methode ist typisiert. Der Typ einer Methode bestimt den Typ des
Rückgabewerts. Dieser kann von einem beliebigen primitiven Typ, einem Objekttyp oder vom Typ
void sein. Methoden vom Typ void haben keinen Rückgabewert und dürfen nicht in Ausdrücken
verwendet werden. Hat eine Methode einen Rückgabewert, dann kann sie mit der "return"−
Anweisung einen Wert an den Aufrufer zurückgeben.
Parameterliste enthält optional Argumente und hat folgende Struktur: Datentyp
variablenname, ..... Die Anzahl der Parameter ist beliebig und kann Null sein.
In Java wird jede selbstdefinierte Klasse mit Hilfe des Operators new instanziert. Mit
Ausnahme von Zeichenketten (Strings) und Datenfeldern (Arrays), bei denen der
Compiler auch Literale zur Objekterzeugung bereitstellt, gilt dies für alle
vordefinierten Klassen der Java−Bibliothek.
Der Aufruf einer Methode erfolgt ähnlich der Verwendung einer Instanzvariablen in
"Punktnotation". Zur Unterscheidung von einem Variablenzugriff müssen zusätzlich
die Parameter in Klammern angegeben werden, selbst wenn die Parameter−Liste
leer ist.
Konstruktoren
Eine "Constructor"−Methode bestimmt, wie ein Objekt initialisiert wird. Konstruktoren
haben immer den gleichen Namen wie die Klasse und besitzen keine "return"−
Anweisung. Java erledigt beim Aufruf eines Konstruktors folgende Aufgaben:
− Speicherzuweisung für das Objekt
− Initialisieung der Instanzvariablen des Objekts auf ihre Anfangswerte oder einen Default−Wert (0 bei
Zahlen, "null" bei Objekten, "false" bei booleschen Operatoren.
− Aufruf der Konstruktor−Methode der Klasse
56
Algorithmen und Datenstrukturen
Gewöhnlich stellt man "explizit" einen "default"−Konstruktor zur Verfügung. Dieser
parameterlose Konstruktor überlagert den implizit bereitgestellten "default"−
Konstruktor. Er wird dann bei allen parameterlosen Instanzierungen verwendet.
Konstruktoren können aber auch − wie normale Dateien − Parameter übergeben
bekommen.
super() bestimmt einen Aufruf des "default"−Konstruktors der eindeutig
bestimmten "Superklasse". "super(...)" darf nur als erste Anweisung eines
Konstruktors auftreten. Generell bezeichnet das reservierte Wort "super", die nach
"extends" benannte Superklasse.
Häufig sind die Parameter Anfangswerte für Instanzvariablen und haben oft den
gleichen Namen wie die entsprechenden Instanzvariablen. In diesen Fällen löst die
Verwendung von bspw. this.ersterOperand
= ersterOperand; derartige
Namenskonflikte auf.
Bei "this" handelt es sich um einen Zeiger, der beim Anlegen eines Objekts
automatisch generiert wird. "this" ist eine Referenzvariable, die auf das aktuelle
Objekt zeigt und zum Ansprechen der eigenen Methoden und Instanzvariablen dient.
Der "this"−Zeiger ist auch explizit verfügbar und kann wie eine ganz normale
Objektvariable verwendet werden. Er wird als versteckter Parameter an jede nicht
statische Methode übergeben.
Konstruktoren können in Java verkettet aufgerufen werden, d.h. sie können sich
gegenseitig aufrufen. Der aufrufende Konstruktor wird dabei als normale Methode
angesehen, die über this einen weiteren Konstruktor der aktuellen Klasse aufrufen
kann.
"Freundliche" Klassen und "freundliche" Methoden
Der voreingestellte "Defaultstatus" einer Klasse ist immer "freundlich" und wird dann
verwendet, wenn keine Angaben über die Sichtbarkeit (Spezifizierer, Modifizierer)
am Beginn der Klassendefinition vorliegen.
Die freundliche Grundeinstellung aller Klassen bedeutet: Diese Klasse kann von
anderen Klassen nur innerhalb desselben Pakets benutzt werden. Das Paket−
Konzept von Java faßt mehrere Klassen zu einem Paket über die Anweisung
"package" zusammen. Durch die Anweisung "import" werden einzelne Pakete
dann in einem Programm verfügbar gemacht. Klassen, die ohne "package"−
Anweisung definiert werden, werden vom Compiler in ein Standardpaket gestellt. Die
".java"− und ".class"−Dateien dieses Pakets befinden sich im aktuellen
Verzeichnis oder im darunterliegenden Verzeichnis.
Mit dem Voranstellen von "public" vor die Klassendeklaration wird eine Klasse als
"öffentlich" deklariert.Dies bedeutet: Alle Objekte haben Zugriff auf "public"−
Klassen (nicht nur die des eigenen Pakets).
Der voreingestellte Defaultstaus einer Methode ist immer freundlich und wird immer
dann verwendet, wenn keine explizite Angabe zur Sichtbarkeit am Anfang der
Methodendeklaration vorliegt. Die freundliche Grundeinstellung aller Methoden
bedeutet: Die Methoden können sowohl innerhalb der Klasse als auch innerhalb des
zugehörigen Pakets benutzt werden.
Zugriffsrechte auf Klassen, Variable und Methoden
Es gibt in Java insgesamt 4 Zugriffsrechte:
57
Algorithmen und Datenstrukturen
private
Ohne Schlüsselwort
protected
public
Zugriff nur innerhalb einer Klasse
Zugriff innerhalb eines Pakets
Zugriff innerhalb eines Pakets oder von Subklassen in einem anderen
Paket
Zugriff von überall
Mit Voranstellen des Schlüsselworts "public" können alle Klassen, Variablen /
Konstanten und Methoden für einen beliebigen Zugriff eingerichtet werden. Eine
derartige Möglichkeit, die in etwa der Zugriffsmöglichkeit globaler Variablen in
konventionellen Programmiersprachen entspricht, ist insbesondere bei komplexen
Programm−Systemen gefährlich. Es ist nicht sichergestellt, daß zu jedem Zeitpunkt
die Werte der Instanzvariablen von Objekten bestimmte Bedingungen erfüllen.
Abhilfe verspricht hier das Prinzip der Datenkapselung (data hiding, data
encapsulation). Instanzvariable werden als "private" erklärt, d.h.: Zugriff auf
diese Instanzvariable nur innerhalb der Klasse. Von außerhalb kann nur indirekt
über das Aufrufen von Methoden, die als "public" erklärt sind, auf die
Instanzvariablen zugegriffen werden. Deshalb sollte auch prinzipiell für jede
Instanzvariable eine entsprechende "get"− und eine "set"−Methode ("hole" bzw.
"setze") zur Verfügung gestellt werden, die jeweils "public" erklärt werden.
Klassen
Instanzvariable
Instanzkonstanten
Instanzmethoden
public
private
public
public, falls ein Zugriff von außen erforderlich und sinnvoll ist.
private, falls es sich um klaseninterne Hilfsmethoden handelt.
Klassenvariable und Klassenmethoden
Das reservierte Wort static macht Variable und Methoden (wie bspw. main()) zu
Klassenvariablen bzw. Klassenmethoden.
Klassenvariable werden in der Klasse definiert und gespeichert. Deshalb wirken sich
ihre Werte auf die Klasse und all ihre Instanzen aus. Jede Instanz hat Zugang zu der
Klassenvariablen, jedoch gibt es für alle Instanzen dieser Variablen nur einen Wert.
Durch Änderung des Werts ändern sich die Werte aller Instanzen der betreffenden
Klasse.
Klassenmethoden wirken sich wie Klassenvariable auf die ganze Klasse, nicht auf
einzelne Instanzen aus. Klassenmethoden sind nützlich zum Zusammenfassen
allgemeiner Methoden an einer Stelle der Klasse. So umfaßt die Math−Klasse
zahlreiche mathematische Funktionen in der Form von Klassenmethoden. Es gibt
keine Instanzen der Klasse Math.
Auch rekurvive Programme benutzen Klassenmethoden, z.B.:
// Berechnung der Fakultaet
public static long fakultaet(int n)
{
if (n < 0)
{
return −1;
}
else if (n == 0)
{
return 1;
}
else
{
return n * fakultaet(n−1);
58
Algorithmen und Datenstrukturen
}
}
Der Aufruf einer Klassenmethode kann im Rahmen der Punktnotation aber auch
direkt erfolgen, z.B.:
long resultat = fakultaet(i);
bzw.
long resultat = Rechentafel.fakultaet(i);
unter der Voraussetzung: Die Klassenmethode "fakultaet" befindet sich in der Klasse
"Rechentafel".
Lokale Variable und Konstanten
Lokale Variable werden innerhalb von Methodendefinitionen deklariert und können
nur dort verwendet werden. Es gibt auch auf Blöcke beschränkte lokale Variablen.
Die Deklaration einer lokalen Variablen gilt in Java als ausführbare Anweisung. Sie
darf überall dort erfolgen, wo eine Anweisung verwendet werden darf. Die
Sichtbarkeit einer lokalen Variablen erstreckt sich von der Deklaration bis zum Ende
des umschließenden Blocks.
Lokale Variablen existieren nur solange im Speicher, wie die Methode oder der
Block existiert. Lokale Variable müssen unbedingt ein Wert zugewiesen bekommen,
bevor sie benutzt werden können. Instanz− und Klassenvariable haben einen
typspezifischen Defaultwert. Auf lokale Variable kann direkt und nicht über die
Punktnotation zugegriffen werden. Lokale Variable können auch nicht als
Konstanten gesetzt werden.
Beim Bezug einer Variablen in einer Methodendefinition sucht Java zuerst eine
Definition dieser Variablen im aktuellen Bereich, dann durchsucht es die äußeren
Bereiche bis zur Definition der aktuellen Methode. Ist die gesuchte Größe keine
lokale Variable, sucht Java nach einer Definition dieser Variablen als Instanzvariable
in der aktuellen Klasse und zum Schluß in der Superklasse.
Konstanten sind Speicherbereiche mit Werten, die sich nie ändern. In Java können
solche Konstanten ausschließlich für Instanz− und Klassenvariable erstellt werden.
Konstante bestehen aus einer Variablendeklaration, der das Schlüsselwort final
vorangestellt ist, und ein Anfangswert zugewiesen ist, z.B.: final int LINKS =
0;
20
Überladen von Methoden
In Java ist es erlaubt, Methoden zu überladen, d.h. innerhalb einer Klasse zwei
unterschiedliche Methoden mit denselben Namen zu definieren. Der Compiler
unterscheidet die verschiedenen Varianten anhand Anzahl und Typisierung der
Parameter. Es ist nicht erlaubt, zwei Methoden mit exakt demselben Namen und
identischer Parameterliste zu definieren. Es werden auch zwei Methoden, die sich
nur durch den Typ ihres Rückgabewerts unterscheiden als gleich angesehen.
Der Compiler kann die Namen in allen drei Fällen unterscheiden, denn er arbeitet
mit der Signatur der Methode. Darunter versteht man ihren internen Namen. Dieser
setzt sich aus dem nach außen sichtbaren Namen und zusätzlich kodierter
Information über Reihenfolge und die Typen der formalen Parameter zusammen.
Überladen kann bedeuten, daß bei Namensgleichheit von Methoden in
"Superklasse" und abgeleiteter Klasse die Methode der abgeleitetem Klasse, die der
20
Das bedeutet: das Schlüsselwort final ist bei lokalen Variablen nicht erlaubt
59
Algorithmen und Datenstrukturen
Superklasse überdeckt. Es wird aber vorausgesetzt, daß sich die Parameter
signifikant unterscheiden, sonst handelt es sich bei der Konstellation Superklasse −
Subklasse um den Vorgang des Überschreibens. Soll die Originalmethode
aufgerufen werden, dann wird das Schlüsselwort "super" benutzt. Damit wird der
Methodenaufruf in der Hierarchie nach oben weitergegeben.
Überschreiben einer Oberklassen−Methode: Zum Überschreiben einer Methode wird
eine Methode erstellt, die den gleichen Namen, Ausgabetyp und die gleiche
Parameterliste wie eine Methode der Superklasse besitzt. Da Java die erste
Methodendefinition ausführt, die es findet und die in Namen, Ausgabetyp und
Parameterliste übereinstimmt, wird die ursprüngliche Methodendefinition dadurch
verborgen.
Konstruktor−Methoden können "technisch" nicht überschrieben werden. Da sie den
gleichen Namen wie die aktuelle Klasse haben, werden Konstruktoren nicht vererbt,
sondern immer neu erstellt. Wird ein Konstruktor einer bestimmten Klasse
aufgerufen, wird gleichzeitig auch der Konstruktor aller Superklassen aktiviert, so
daß alle Teile einer Klasse initialisiert werden. Ein spezielles Überschreiben kann
über super(arg1, arg2, ...) ermöglicht werden.
Die Methode toString() ist eine in Java häufig verwendete Methode.
"toString()" wird als weitere Instanzmethode der Klasse Rechentafel definiert
und überschreibt die Methode gleichen Namens, die in der Oberklasse Object
definiert ist.
1.2.3.6.2 Referenzen, einfache Typen und Referenztypen
Referenzen: Referenzen sind Verweise, die auf Objekte eines bestimmten Typs
zeigen. Dieser Typ kann eine Klasse oder ein Interface sein. Referenzen treten als
lokale Variable, Instanz− oder Klassenvariable in Erscheinung.
Beim Zuweisen von Variablen für Objekte bzw. beim Weiterreichen von Objekten als
Argumente an Methoden werden Referenzen auf diese Objekte festgelegt.
Java verfügt über ein automatisches Speichermanagement. Deshalb braucht der
Java−Programmierer sich nicht um die Rückgabe von Speicher zu kümmern, der
von Referenzvariablen belegt ist. Ein mit niederer Priorität im Hintergrund
arbeitender "Garbage Collector" sucht ständig nach Objekten, die nicht mehr
referenziert werden, um den belegten Speicher freizugeben.
Finalizer−Methoden werden aufgerufen, kurz bevor das Objekt im Papierkob landet
und sein Speicher freigegeben wird. Zum Erstellen einer Finalizer−Methode dient
der folgende Eintrag in der Klassendefinition void finalize(){ ... }. Im
Körper dieser Methode können alle möglichen Reinigungsprozeduren stehen, die
das Objekt ausführen soll. Der Aufruf der Methode finalize() bewirkt nicht
unmittelbar die Ablage im Papierkorb. Nur durch das Entfernen aller Referenzen auf
das Objekt wird das Objekt zum Löschen markiert.
60
Algorithmen und Datenstrukturen
Einfache Typen: Jedes Java−Programm besteht aus einer Sammlung von Klassen.
Der vollständige Code von Java wird in Klassen eingeteilt. Es gibt davon nur eine
Ausnahme: Boolesche Operatoren, Zahlen und andere einfache Typen sind in Java
erst einmal keine Objekte. Java hat für alle einfachen Typen sog. Wrapper−Klassen
implementiert. Ein Wrapper−Klasse ist eine spezielle Klasse, die eine
Objektschnittstelle für die in Java verschiedenen primitiven Typen darstellt. Über
Wrapper−Objekte können alle einfachen Typen wie Klassen behandelt werden. Java
besitzt acht primitive Datentypen:
− vier Ganzzahltypen mit unterschiedlichen Wertebereichen
− zwei Gleitpunktzahlentypen mit unterschiedlichen Wertebereichen nach IEEE Standard of Binary Floating
Point Arithmetic.
− einen Zeichentyp
Primitiver Typ
boolean
char
byte
short
int
long
float
double
void
Größe
1−Bit
16−Bit
8−Bit
16−Bit
32−Bit
64−Bit
32−Bit
64−Bit
−
Minimum
−
Unicode 0
−128
−215
−231
−263
IEEE 754
IEEE 754
−
Maximum
−
Unicode 216−1
+128
+215−1
+231−1
+263−1
IEEE 754
IEEE 754
−
Wrapper−Klasse
Boolean
Character
Byte
Short
Integer
Long
Float
Double
Void
Den primitiven Typen sind "Wrapper"−Klassen zugeordnet, die ein nicht primitives
Objekt auf dem "Heap" zur Darstellung des primitiven Typs erzeugen, z.B.:
char zeichen = ’x’;
Character zeichenDarstellung = new Character(zeichen)
bzw.
Character zeichenDarstellung = new Character(’x’);
Referenztypen: Dazu gehören: Objekte der benutzerdefinierten und aus vom System
bereitgestellten Klassen, der Klassen String und Array (Datenfeld). Weiterhin gibt
es die vordefinierte Konstante "null", die eine leere Referenz bezeichnet.
"String" und "Array" weisen einige Besonderheiten aus:
− Für Strings und Arrays kennt der Compiler Literale, die einen expliziten Aufruf des Operator new
überflüssig machen
− Arrays sind klassenlose Objekte. Sie können ausschließlich vom Compiler erzeugt werden, besitzen
aber keine explizite Klassendefinition. Sie werden dennoch vom Laufzeitsystem wie normale
Objeklte behandelt.
− Die Klasse String ist zwar im JDK vorhanden. Der Compiler hat aber Kenntnis über den inneren
Aufbau von Strings und generiert bei Anzeigeoperationen Code, der auf Methoden der Klassen
String und StringBuffer zugreift.
61
Algorithmen und Datenstrukturen
1.2.3.6.3 Superklassen und Subklassen, Vererbung und Klassenhierarchie
Klassen können sich auf andere Klassen beziehen. Ausgangspunkt ist eine
Superklasse, von der Subklassen (Unter−) abgeleitet sein können. Jede Subklasse
erbt den Zustand (über die Variablen−Deklarationen) und das Verhalten der
Superklasse. Darüber hinaus können Subklassen eigene Variable und Methoden
hinzufügen. Superklassen, Subklassen bilden eine mehrstufige Hierarchie. In Java
sind alle Klassen Ableitungen einer einzigen obersten Klasse − der Klasse Object.
Sie stellt die allgemeinste Klasse in der Hierarchie dar und legt die Verhaltensweisen
und Attribute, die an alle Klassen in der Java−Klassenbibliothek vererbt werden, fest.
Vererbung bedeutet, daß alle Klassen in eine strikte Hierarchie eingeordnet sind und
etwas von übergeordneten Klassen erben. Was kann von übergeordneten Klassen
vererbt werden? Beim Erstellen einer neuen Instanz erhält man ein Exemplar jeder
Variablen, die in der aktuellen Klasse definiert ist. Zusätzlich wird ein Exemplar für
jede Variable bereitgestellt, die sich in den Superklassen der aktuellen Klasse
befindet. Bei der Methodenauswahl haben neue Objekte Zugang zu allen
Methodennamen ihrer Klasse und deren Superklasse. Methodennamen werden
dynamisch beim Aufruf einer Methode gewählt. Das bedeutet: Java prüft zuerst die
Klasse des Objekts auf Definition der betreffenden Methode. Ist sie nicht in der
Klasse des Objekts definiert, sucht Java in der Superklasse dieser Klasse usw.
aufwärts in der Hierarchie bis die Definition der Methode gefunden wird.
Ist in einer Subklasse eine Methode mit gleichem Namen, gleicher Anzahl und
gleichem Typ der Argumente wie in der Superklasse definiert, dann wird die
Methodendefinition, die zuerst (von unten nach oben in der Hierarchie) gefunden
wird, ausgeführt. Methoden der abgeleiteten Klasse überdecken die Methoden der
Superklasse (Overriding beim Überschreiben / Überdefinieren).
Zum Ableiten einer Klasse aus einer bestehenden Klasse ist im Kopf der Klasse mit
Hilfe des Schlüsselworts "extends" ein Verweis auf die Basisklasse anzugeben.
Dadurch wird bewirkt: Die abgeleitete Klasse erbt alle Eigenschaften der
Basisklasse, d.h. alle Variablen und alle Methoden. Durch Hinzufügen neuer
Elemente oder Überladen der vorhandenen kann die Funktionalität der abgeleiteten
Klasse erweitert werden.
Die Vererbung einer Klasse kann beliebig tief geschachtelt werden. Eine abgeleitete
Klasse erbt dabei jeweils die Eigenschaften der unmittelbaren "Superklasse", die
ihrerseits die Eigenschaften ihrer unmittelbaren "Superklasse" erbt.
Superklassen können abstrakte Klassen mit generischem Verhalten sein. Von einer
abstrakten Klasse wird nie eine direkte Instanz benötigt. Sie dient nur zu
Verweiszwecken. Abstrakte Klassen dürfen keine Implementierung einer Methode
enthalten und sind damit auch nicht instanziierbar. Java charakterisiert abstrakte
Klassen mit dem Schlüsselwort abstract. Abstrakte Klassen werden zum Aufbau
einer Klassenhierarchie verwendet, z.B. für die Zusammenfassung von zwei oder
mehr Klassen.
Auch eine abstrakte Methode ist zunächst einmal durch das reservierte Wort
"abstract" erklärt. Eine abstrakte Methode besteht nur aus dem Methodenkopf,
anstelle des Methodenrumpfs (der Methodendefinition) steht nur das "Semikolon",
z.B. public abstract String toString();.
Enthält eine Klasse mindestens eine abstrakte Methode, so wird automatisch die
gesamte Klasse zu einer abstrakten Klasse. Abstrakte Klassen enthalten keine
Konstruktoren. Sie können zwar Konstruktoren aufnehmen, allerdings führt jeder
explizite Versuch zur Erzeugung eines Objekts einer abstrakten Klasse zu einer
Fehlermeldung. Abstrakte Methoden stellen eine Schnittstellenbeschreibung dar, die
62
Algorithmen und Datenstrukturen
der Programmierer einer Subklasse zu definieren hat. Ein konkrete Subklasse einer
abstrakten Klasse muß alle abstrakten Methoden der Superklasse(n)
implementieren.
1.2.3.6.4 Schnittstellen und Pakete
Schnittstellen
Definition: Eine Schnittstelle ist in Java eine Sammlung von Methodennamen ohne
konkrete Definition. Klassen haben die Möglichkeit zur Definition eines Objekts,
Schnittstellen können lediglich ein paar abstrakte Methoden und Konstanten (finale
Daten) definieren .
Schnittstellen bilden in Java den Ersatz für Mehrfachvererbung. Eine Klasse kann
demnach nur eine Superklasse, jedoch dafür mehrere Schnittstellen haben.
Ein Schnittstelle ("Interface") ist eine besondere Form der Klasse, die ausschließlich
abstrakte Methoden und Konstanten enthält. Anstelle von class dient zur Definition
eines "Interface" das Schlüsselwort "interface".
"Interfaces" werden formal wie Klassen definiert, z.B.:
21
public interface meineSchnittstelle
{
// Abstrakte Methoden
}
bzw.
public interface meineSpezialschnittstelle extends meineSchnittstelle
{
// Abstrakte Methoden
}
"Interfaces" können benutzt werden, indem sie in einer Klasse implementiert werden,
z.B.:
public class MeineKlasse extend Object implements meineSchnittstelle
{
/* Normale Klassendefinition + Methoden aus meineSchnittstelle */
}
Eine Klasse kann mehrere Schnittstellen implementieren, z.B.
public class MeineKlasse extends Object
implements meineSchnittstelle1, meineSchnittstelle2
{
/* Normale Klassendefinition + Metoden aus meinerSchnittstelle1
und meinerSchnittstelle2 */
}
Verwendung: Bei der Vererbung von Klassen spricht man von Ableitung, bei
Interfaces nennt man es Implementierung. Durch Implementieren einer Schnittstelle
verpflichtet sich die Klasse, alle Methoden, die im Interface definiert sind, zu
implementieren. Die Implementierung eines Interface wird durch das Schlüsselwort
"implements" bei der Klassendefinition angezeigt.
21
Angaben zur Implementierung sind nicht möglich
63
Algorithmen und Datenstrukturen
Bsp.: Zahlreiche von Java bereitgestellte Referenztypen (Klassen) haben das
Interface Comparable implementiert. Deshalb sortiert die nachfolgenden
Sortierroutine (Sort) String− und Zahlen−Objekte, wie der Aufrufe aus SortTest
beweisen.
public class Sort
{
public void sort(Comparable x[])
{
bubbleSort(x);
}
public void bubbleSort(Comparable x[])
{
for (int i = 0; i < x.length; i++)
{
for (int j = i + 1; j < x.length; j++)
{
if (x[i].compareTo(x[j]) > 0)
{
Comparable temp = x[i];
x[i] = x[j];
x[j] = temp;
}
}
}
}
}
public class SortTest
{
public static void main(String args[])
{
String sa[] = {
"Juergen","Christian","Bernd","Werner","Uwe",
"Erich","Kurt","Karl","Emil","Ernst"
};
// Ausgabe des noch nicht sortierten x
System.out.println("Vor dem Sortieren:");
for (int i = 0; i < sa.length; i++)
{
System.out.println("sa["+i+"] = " + sa[i]);
}
// Aufruf der Sortieroutine
Sort s = new Sort();
s.sort(sa);
// Gib das sortierte Feld x aus
System.out.println();
System.out.println("Nach dem Sortieren:");
for (int i = 0; i < sa.length; i++)
{
System.out.println("sa["+i+"] = " + sa[i]);
}
System.out.println();
int za[] = { 50, 70, 80, 10, 20, 30, 1, 2, 3, 99, 12, 11, 13};
Integer o[] = new Integer[za.length];
for (int i = 0; i < za.length; i++)
{
o[i] = new Integer(za[i]);
}
// Ausgabe des noch nicht sortierten za
System.out.println("Vor dem Sortieren:");
for (int i = 0; i < za.length; i++)
{
System.out.println("za["+i+"] = " + za[i]);
}
// Aufruf der Sortieroutine
Sort zs = new Sort();
zs.sort(o);
// Gib das sortierte Feld x aus
for (int i = 0; i < za.length; i++)
{
za[i] = ((Integer) o[i]).intValue();;
64
Algorithmen und Datenstrukturen
}
System.out.println();
System.out.println("Nach dem Sortieren:");
for (int i = 0; i < za.length; i++)
{
System.out.println("za["+i+"] = " + za[i]);
}
}
}
Konstanten. Neben abstrakten Methoden können Interfaces auch Konstanten
(Variablen mit dem Attributen static und final) enthalten. Wenn eine Klasse ein
solches Interface implementiert, erbt es gleichzeitig auch alle seine Konstanten.
Ein Interface kann ausschließlich Konstanten enthalten.
Datentypen. Durch die Definition eines Interface wird auch der zugehörige
Referenztyp erzeugt, der wie andere Datentypen eingesetzt werden kann
Pakete
Definition: Pakete sind in Java Zusammenfassungen von Klassen und Schnittstellen.
Sie entsprechen in Java den Bibliotheken anderer Programmiersprachen. Pakete
ermöglichen, daß modulare Klassengruppen nur verfügbar sind, wenn sie gebraucht
werden. Potentielle Konflikte zwischen Klassenamen in unterschiedlichen
Klassengruppen können dadurch vermieden werden.
Für Methoden und Variablen innerhalb von Paketen besteht eine
Zugriffsschutzschablone. Jedes Paket ist gegenüber anderen, nicht zum Paket
zählenden Klassen abgeschirmt. Klassen, Methoden oder Variablen sind nur
sichtbar für Klassen im gleichen Paket. Klassen, die ohne "package" Anweisung
definiert sind, werden vom Compiler in ein "Standardpaket" gestellt. Voraussetzung
dafür ist: Die ".java"− und ".class"−Dateien dieses Pakets befinden sich im aktuellen
Verzeichnis (oder in einen darunter liegenden Klassenverzeichnis).
Die Java−Klassenbibliothek von Java 1.0 enthält folgende Pakete:
22
− java.lang: Klassen, die unmittelbar zur Sprache gehören. Das Paket umfaßt u.a.
die Klassen Object, String, System, außerdem die Sonderklassen für die
Primitivtypen (Integer, Character, Float, etc.)
Object
Class
String
StringBuffer
Thread
ThreadGroup
Throwable
22
Aus dieser Klasse leiten sich alle weiteren Klassen ab. Ohne explizite Angabe der
Klasse, die eine neue Klasse erweitern soll, erweitert die neue Klasse die Object−
Klasse. Die Klasse Object ist die Basis−Klasse jeder anderen Klasse in Java. Sie
definiert Methoden, die von allen Klasse in Java unterstützt werden.
Für jede in java definierte Klasse gibt es eine Instanz von Class, die diese Klasse
beschreibt
Enthält Methoden zur Manipulation von Java−Zeichenketten
Dient zum Erstellen von Java−Zeichenketten
Stellt einen Ausführungs−Thread in einem Java−Programm dar. Jedes Programm kann
mehrere Threads laufen lassen
Ermöglicht die Verknüpfung von Threads untereinander. Einige Thread−Operationen
können nur von Threads aus der gleichen ThreadGroup ausgeführt werden.
Ist die Basisklasse für Ausnahmen. Jedes Objekt, das mit der "catch"−Anweisung
gefangen oder mit der "throw"−Anweisung verworfen wird, muß eine Subklasse von
Throwable sein.
Die Klassenbibliothek des JDK befindet sich in einem Paket mit dem namen "java".
65
Algorithmen und Datenstrukturen
System
Runtime
Process
Math
Number
Character
Boolean
ClassLoader
SecurityManager
Compiler
Stellt spezielle Utilities auf Systemebene zur Verfügung
Enthält eine Vielzahl gleicher Funktionen wie System, behandelt aber auch das
Laufen externer Programme
Stellt ein externes Programm dar, das von einem Runtime−Objekt gestartet wurde.
Stellt eine Reihe mathematischer Funktionen zur verfügung
Ist die Basisklasse für Double,Float, Integer und Long (Objeckt−Wrapper)
Ist ein Objekt−Wrapper für den datentyp char und enthält eine Reihe nützlicher
zeichenorientierter Operationen
Ist ein Objekt−Wrapper für den Datentyp boolean
Ermöglicht der Laufzeit−Umgebung von Java neue Klassen hinzuzufügen
Legt die Sicherheits−Restriktionen der aktuellen Laufzeitumgebung fest. Viele Java−
Klassen benutzen den Security−Manager zur Sicherstellung, daß eine Operation auch
tatsächlich genehmigt ist.
Ermöglicht, falls vorhanden, den Zugriff auf den "just−in−time" Compiler
Abb.: Klassen des java.lang−Pakets
Zusätzlich enthält das java.lang−Paket noch zwei Schnittstellen:
Cloneable
Runnable
Muß von einem anderen Objekt implementiert werden, das dann geklont oder
kopiert werden kann
Wird zusammen mit der Thread−Klasse benutzt, um die aufgerufene
Methode zu definieren, wenn ein Thread gestartet wird.
Abb.: Schnittstellen im java.lang−Paket
− java.util
− java.io: Klassen zum Lesen und Schreiben von Datenströmen und zum
Handhaben von Dateien.
− java.net: Klassen zur Netzunterstützung, z.B. socket und URL (eine Klasse
zum Darstellen von Referenzen auf Dokumente im World Wide Web).
− java.awt (Abstract Windowing Toolkit): Klassen zum Implementieren einer
grafischen Benutzeroberfläche. Das Paket enthält auch eine Klasse für Grafik
(java.awt.Graphics) und Bildverarbeitung (java.awt.Image).
− java.applet: Klassen zum Implementieren von Applets, z.B. die Klasse
Applet.
Die Java Version 1.1 hat die Klassenbibliothek umfassend erweitert :
23
Paket
java.applet
java.awt
java.awt.datatranfer
java.awt.event
java.awt.image
java.beans
java.io
java.lang
23
Bedeutung
Applets
Abstract Window Toolkit
ClipBoard−Funktionalität (Copy / Paste)
AWT Event−handling
Bildanzeige
Java Beans
Ein− und Ausgabe, Streams
Elementare Sprachunterstützung
Zusätzlich 15 weitere Packages, etwa 500 Klassen und Schnittstellen
66
Algorithmen und Datenstrukturen
java.lang.reflect
java.math
java.net
java.rmi
java.rmi.dgc
java.rmi.registry
java.rmi.server
java.security
java.security.aci
java.security.interfaces
java.sql
java.util
java.util.zip
Introspektion, besserer Zugriff auf Klassen durch
Debugger und Inspektoren
Netzzugriffe
Remote Method Invocation, Zugriff auf Objekte in
anderen virtuellen Maschinen
RMI Distributed Garbage Collection
Verwaltet Datenbank, die RMI−Verbindungen
koordiniert
RMI−Server
Sicherheit durch digitale Signaturen, Schlüssel
Access Control Lists
Digital Signature Algorithm (DAS−Klassen)
Datenbankzugriff (JDBC)
Diverse Utilities, Datenstrukturen
JAR−Files, Kompression, Prüfsummen
Abb.: Klassenbibliothek der Java−Version 1.1
Verwendung: Jede Klasse ist Bestandteil eines Pakets. Der vollständige Name einer
Klasse besteht aus dem Namen des Pakets, danach kommt der ein Punkt, gefolgt
von dem eigentlichen Namen der Klasse.
Zur Verwendung einer Klasse muß angegeben werden, in welchem Paket sie liegt.
Hier gibt es zwei unterschiedliche Möglichkeiten:
− Die Klasse wird über ihren vollen (qualifizieren) Namen angesprochen, z.B.
java.util.Date d = new java.util.Date();
− Am Anfang des Klassenprogramms werden die gewünschten Klassen mit Hilfe der import−
Anweisung eingebunden, z.B.:
import java util.*;
......
Date d = new Date();
Die import−Anweisung gibt es in unterschiedlichen Ausprägungen:
−− Mit "import paket.Klasse" wird genau eine Klasse importiert, alle anderen Klassen des
Pakets bleiben verborgen
−−Mit "import paket.*"24 können alle Klassen des angegebenen Pakets auf einmal importiert
werden.
Standardmäßig haben Java−Klassen Zugang zu den in java.lang befindlichen
Klassen. Klassen aus anderen Paketen müssen explizit über den Paketnamen
einbezogen oder in die Quelldatei importiert werden.
24
type import on demand, d.h.: Die Klasse wird erst dann in dem angegebenen Paket gesucht, wenn das
Programm sie wirklich benötigt.
67
Algorithmen und Datenstrukturen
1.2.3.6.5 Polymorphismus und Binden
Polymorphismus
Definition: Polymorphismus ist die Eigenschaft von Objekten, mehreren Klassen
(und nicht nur genau einer Klasse) anzugehören, d.h. je nach Zusammenhang in
unterschiedlicher Gestalt (polymorph) aufzutreten. Java ist polymorph.
Binden: Das Schema einer Botschaft wird aus "Empfänger Methode Argument"
gebildet. Darüber soll eine Nachricht die physikalische Adresse eines Objekts im
Speicher finden. Trotz Polymorphismus und Vererbung ist der Methodenname (oft.
Selektor genannt) nur eine Verknüpfung zum Programmcode einer Methode. Vor der
Ausführung einer Methode muß daher eine genaue Zuordnung zwischen dem
Selektor und der physikalischen Adresse des tatsächlichen Programmcodes erfolgen
(Binden oder Linken).
In der objektorientierten Programmierung unterscheidet man: frühes und spätes
Binden:
Frühes Binden: Ein Compiler ordnet schon zum Zeitpunkt der Übersetzung des Programms die
tatsächliche, physikalische Adresse der Methode dem Methodenaufruf zu.
Spätes Binden: Hier wir erst zur Laufzeit des Programms die tatsächliche Verknüpfung zwischen Selektor und
Code hergestellt. Die richtige Verbidung übernimmt das Laufzeitprogramm der Programmiersprache.
Java unterstützt das Konzept des "Late Binding".
68
Algorithmen und Datenstrukturen
1.3 Sammeln (über Container) und Ordnen
1.3.1 Ausgangspunkt: Das Konzept zum Sammeln in Smalltalk
Gefordert ist eine allgemeines Konzept zum Sammeln und Ordnen von Objekten
(Behälter, Container, Collection). Der Entwurf solcher Konzepte bzw.
Containerklassen ist Gegenstand objektorientierter Programmiersprachen.
Ein einziger Containertyp, der allen Anforderungen gerecht wird, wäre sicherlich die
beste Lösung. Dem stehen verschiedene Schwierigkeiten entgegen:
− Gegensätzliche Ordnungskriterien (z.B. in Schlangen, Stapeln)
− Unterschiedliche Forderungen (z.B. bzgl. des Begriffs "Enthalten" in einer Menge (set) oder in einer
Sammlung (bag)
− Identifikation der Objekte über Wert oder Schlüssel oder Position (Index)
− Unterschiedliche Anforderungen der Zugriffs−Effizienz (z.B. wahlfrei−berechenbar, mengenmäßig
eingeschränkt, Zugriff über "keys" in einem "dictionary25")
Unter einem Container (bzw. Collection bzw. Ansammlung) versteht man eine
allgemeine Zusammenfassung von Objekten in irgendeiner, nicht näher
spezifizierten Organisationsstruktur. Ordnung kann in einer (An−)Sammlung viel
bedeuten, z.B.:
− in einem "array" die Ablage in irgendeiner Reihenfolge unter einem automatisch fortgeschriebenen
Index
− in einer hierarchischen Struktur (Baum−) ein Container (z.B. Liste), bei dem die enthaltenen
Elemente selbst (Listen) sein dürfen.
In Smalltalk/V realisiert die abstrakte Klasse "Collection" zusammen mit ihren
Unterklassen ein leistungsfähiges Konzept zum Sammeln und Ordnen von Objekten.
Die Klasse Collection beschreibt die gemeinsamen Eigenschaften aller Objekt−
Ansammlungen. Da es keine allgemeinen Ansammlungen gibt, kann man auch
keine Instanzen der Klasse Collection bilden (abstrakte Oberklasse).
Objekteigenschaften, die unabhängig von der Zugehörigkeit zu spezifischen
Unterklassen sind, können in der Oberklasse spezifiziert werden. Collection ist eine
direkte Unterklasse der allgemeinsten Klasse Object.
Object
Collection
Bag
IndexedCollection
FixedSizedCollection
Array
Bitmap
ByteArray
Interval
String
Symbol
OrderedCollection
Set
Dictionary
Identity Dictionary
System Dictionary
Abb. 1.3−1: Klassen zum Sammeln und Ordnen von Objekten in Smalltalk/V
25
Tabellen, vgl. 1.3.2
69
Algorithmen und Datenstrukturen
Viele bekannte Datenstrukturen stehen bereits in den Klassen zur Verfügung. Durch
Unterklassen und Kombination von Klassen lassen sich beliebig andere
Datenstrukturen erstellen.
"Collection" selbst nimmt keinen Bezug auf irgendein Ordnungsprinzip, nach dem
seine Elemente abgelegt sind. Die Subklassen von Collection werden so organisiert,
daß sie häufig auftretende Ordnungsprinzipien unterstützen. Das erste
Unterscheidungsmerkmal
betrifft
die
Indizierbarkeit
der
Sammlung
(IndexedCollection). Alle anderen (nicht indizierten) Sammlungen teilen sich sich in
Bag (mehrfache Einträge sind erlaubt) und Set (mehrfache Einträge sind nicht
erlaubt). IndexedCollection wird unterteilt in Sammlungen mit einer festen Anzahl
von Elementen (FixedCollection) oder mit variabler Anzahl von Elementen
(OrderedCollection, paßt die Größe automatisch dem Bedarf an und ermöglicht den
Aufbau üblicher dynamischer Datenstrukturen: Stacks, Fifos, etc.). In der Subklasse
SortedCollection kann die Reihenfolge der Elemente durch eine Sortiervorschrift
festgelegt werden.
Bei nicht indizierten Sammlungen wird nur die Klasse Set weiter spezialisiert. Ihre
Subklasse Dictionary" kann auf die Elemente einer Sammlung über Schlüsselwörter
zugreifen.
Allen Sammlungen ist gemeinsam: Sie enthalten Nachrichten zum Hinzufügen und
Entfernen von Objekten, zum Test auf das Vorhandensein von Elementen und zum
Aufzählen der Elemente. Beschreibungen von Reaktionen auf Nachrichten eines
Objekts heißen Methoden. Jede Methode ist mit einer Nachrichtenkennung
versehen und besteht aus Smalltalk Anweisungen:
Eigenschaft der Methode:
Hinzufügen
Smalltalk−Anweisung
add:anObject
addALL:aCollection
Entfernen
remove:anObject
removeALL
Testen
includes:anObject
isEmpty
occurencesOf:anObject
Aufzählen
do:aBlock
reject:aBlock
collect:aBlock
Bedeutung:
füge ein Objekt hinzu
füge alle Elemente von
aCollection hinzu
entferne ein Objekt
entferne alle Elemente von
aCollection
gib true zurück, falls die
Anwendung leer ist, sonst false
gib true zurück, falls die
Anwendung leer ist, sonst false
gib zurück, wie oft ein
Objekt vorkommt
gib eine Ansammlung mit
den Elementen zurück,
für die die Auswertung
von aBlock true ergibt
gib eine Ansammlung mit
den Elementen zurück, für
die Auswertung von aBlock
false ergibt
führe aBlock für jedes
Element aus und gib eine
Ansammlung mit den
Ergebnisobjekten als
Element zurück
Abb. 1.3−2: Ein Auszug der Instanzmethoden zu Smalltalk−Anweisungen
Die Zuordnung von Nachrichtenkennungen zu Methoden erfolgt dynamisch beim
Senden der Nachricht. Gibt es in der Klasse des Empfängers eine Methode mit der
70
Algorithmen und Datenstrukturen
Nachrichtenkennung, so wird diese Methode ausgeführt. Andernfalls wird die
Methodensuche in der Oberklasse fortgesetzt und solange in der Klassenhierarchie
nach oben gegangen, bis eine Methode mit der gewünschten Nachrichtenkennung
gefunden wird. Gibt es keine solche Methode, dann setzt eine
Ausnahmebehandlung an.
1.3.2 Behälter−Klassen
Kollektionen
Linear
Allgemein
indexiert
Nichtlinear
Direkt−
Zugriff
Sequentieller
Zugriff
Hierarchische
Sammlung
Baum
Dictionary
Hash−
Tabelle
Liste
"array"
Gruppen−
Kollektionen
Heap
Set
Stapel Schlange
Graph
prioritäts−
gest. Schl.
"record" "file"
Abb. 1.3−4: Hierarchischer Aufbau der Klasse Kollektion
Die Abbildung zeigt unterschiedliche, benutzerdefinierte Datentypen. Gemeinsam ist
diesen Klassen nur die Aufnahme und Berechnung der Daten durch ihre Instanzen.
Kollektionen können in lineare und nichlineare Kategorien eingeteilt werden. Eine
lineare Kollektion enthält eine Liste mit Elementen, die durch ihre Stellung (Position)
geordnet sind, Es gibt ein erstes, zweites, drittes Element etc.
71
Algorithmen und Datenstrukturen
1.3.2.1 Lineare Kollektionen
1. Sammlungen mit direktem Zugriff
Ein "array" 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) ist in der Regel eine Zusammenfassung von Datenbehältern
unterschiedlichen Typs:
"record"−Kollektion
Daten
72
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 Datenbe−
hälter vorgesehen.
Eine Datei (file) ist eine extern eingerichtete Sammlung, die mit einer Datenstruktur
("stream") 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 dient zum Zugriff auf Random−
Access−Dateien.
26
2. Sammlungen mit sequentiellem Zugriff
Darunter versteht man lineare Listen (linear list), 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) 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.
"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 Ende
der Liste entfernt.
26
Implementiert das Interface DataInput und DataOutput mit eigenen Methoden.
73
Algorithmen und Datenstrukturen
Eine Schlange 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). 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.2.2 Nichtlineare Kollektionen
1. Hierarchische Sammlung
Eine hierarchisch angeordnete Sammlung von Datenbehältern ist gewöhlich 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:
74
Algorithmen und Datenstrukturen
[1]
wk1
[2]
[3]
wk2
wk3
[4]
[5]
wk4
wk5
[8]
[9]
wk8
[0]
[6]
[7]
wk6
wk7
[10]
[11]
[12]
[13]
[14]
[15]
wk10
wk11
wk12
wk13
wk14
wk15
wk9
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
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
75
Algorithmen und Datenstrukturen
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:
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:
76
Algorithmen und Datenstrukturen
X[1]
10
X[2]
X[3]
15
30
X[4]
40
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 Java .
27
// Erzeugung mit optionaler Angabe zur Kapazitaet (Defaultwert: 100)
//
// ******************PUBLIC OPERATIONEN**********************************
// void insert( x )
−−> Einfuegen x
// Comparable loescheMin( )−−> Rueckgabe und entfernen des kleinsten
//
Elements
// Comparable findMin( ) −−> Rueckgabe des kleinsten Elements
// boolean isEmpty( )
−−> Rueckgabe: true, falls leer; anderenfalls
//
false
// boolean isFull( )
−−> Rueckgabe true, falls voll; anderenfalls
// false
// void makeEmpty( )
−−> Entfernen aller Elemente
Anwendung
Der Binary Heap kann zum Sortieren herangezogen werden.
Ein Heap kann aber auch in Simulationsprogrammen und vor allem zur
Implementierung von Priority Queues verwendet werden. Hier wird vielfach der
27
Vgl. pr13228
77
Algorithmen und Datenstrukturen
einfache Heap durch komplexere Datenstrukturen ( Binomial Heap, Fibonacci Heap)
ersetzt.
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.
"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.
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.
78
Herunterladen