Algorithmen und Datenstrukturen 1. Objektorientierte Programmierung mit Datenstrukturen 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 Datenverarbeitung (DV). So führte hier bspw. die Frage des Zugriffs auf ein Element einer endlichen Menge zu einer großen Sammlung von Algorithmen 1, die grundlegende Aufgaben der DV lösen. Dabei ergab sich: Die Leistungsfähigkeit dieser Lösungen (Programme) ist wesentlich bestimmt durch geeignete Organisationsformen für die zu bearbeitenden Daten. 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 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 B L, 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 bereit2: #ifndef BAUMKNOTEN #define BAUMKNOTEN #ifndef NULL const int NULL = 0; #endif // NULL // Deklaration eines Binaerbaumknotens fuer einen binaeren Baum 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 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 werden 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() 3 Algorithmen und Datenstrukturen { int zahl; baumKnoten<int> *wurzel; baumKnoten<int> *lKind, *rKind, *z; lKind = new baumKnoten<int>(3); rKind = new baumKnoten<int>(4); z = new baumKnoten<int>(2,lKind,rKind); lKind = z; rKind = new baumKnoten<int>(5); z = new baumKnoten<int>(1,lKind,rKind); wurzel = z; cout << "Inorder: " << endl; ausgBaum(wurzel,0); wlr(wurzel); // Rekursive Problemlösung cout << endl; // Nichtrekursive Problemlösung wlrnr(wurzel); cout << endl; } Das Durchlaufen geht offensichtlich von der Wurzel aus, ignoriert zuerst die rechten Teilbäume, bis man auf leere Bäume stößt. Dann werden die Teiluntersuchungen abgeschlossen und beim Rückweg die rechten Bäume durchlaufen. Jeder Baumknoten enthält 2 Zeiger (Adressen von BL und BR). Die Zeiger, die auf leere Bäume hinweisen, werden auf „NULL“ gestellt. 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 Stapels 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 4 Algorithmen und Datenstrukturen Abb. 1.1-2: Aufbau eines Stapels Der nicht rekursive Baumdurchlauf-Algorithmus läßt sich mit Hilfe der Stapelprozeduren der Containerklasse Stack der Standard Template Library (STL) 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 Z3 2 5 Z4 Z5 3 4 Abb. 1.1-3: Zeiger im Binärbaum Welche Baumknoten (bzw. die Zeiger auf die Baumknoten) werden beim Durchlaufen des vorliegenden Baumes (vgl. Abb. 1.1-3) über die Funktionsschablone wlrnr aufgesucht? Welche Werte (Zeiger auf Baumknoten) nimmt der Stapel an? Besuchte Knoten ¦ Stapel -----------------+------------¦ Null Z1 ¦ Z5 Null Z2 ¦ Z4 Z5 Null Z3 ¦ Z4 Z5 Null Z4 ¦ Z5 Null Z5 ¦ Null 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 Datenverarbeitungsanlagen. 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 darstellt3. Die Klasse JTree. Die Baumdarstellung basiert auf einem hierarchischen Datenmodell. Die Modellklasse für JTree muß das Interface TreeModel implementieren. In Swing enthalten ist die Implementierungsklasse DefaultTreeModel. Die einzelnen Baumknoten müssen die Interfaces TreeNode oder MutableTreeNode implementieren. Die Klasse DefaultTreeModel enthält eine universelle Implementierung (inkl. Navigationsmethoden) für Baumstrukturen. 3 vgl. pr13229 8 Algorithmen und Datenstrukturen 1.2 Begriffe und Erläuterungen zu Datenstrukturen und Programmierverfahren 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 beschrä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 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 Verwendung 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. 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. 9 Algorithmen und Datenstrukturen Nicht deterministische Algorithmen mit determiniertem Ergebnis heißen determiniert. Bsp. für einen nicht determinierten, nicht deterministischen Algorithmus: 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 Kaffeepulver in Tasse (3) Fülle Wasser in Tasse (2) kann verfeinert werden zu: Öffne Kaffeedose; Entnehme Löffel von Kaffee; Kippe Löffel in Tasse; Schließe Kaffeedose; - parallele Ausführung - bedingte Ausführung Die Auswahl / Selektion kann allgemein so formuliert werden: falls Bedingung, dann Schritt bzw. falls Bedingung dann Schritt a sonst Schritt b „falls ... dann ... sonst ...“ entspricht in Programmiersprachen den Konstrukten: if Bedingung then ... else … fi if Bedingung then … else …endif if (Bedingung) … else … 10 Algorithmen und Datenstrukturen - 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 einen 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 einem rekursiven Aufruf. Typischerweise werden die aktuellen Parameter so modifiziert, daß die Problemgröße schrumpft, damit nach mehrmaligem Wiederholen dieses Prinzips kein weiterer Aufruf erforderlich ist und die Rekursion abbrechen kann. Bsp.: Türme von Hanoi 11 Algorithmen und Datenstrukturen 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 nachweisen4. Präzise oder sogar formalisierte Korrektheitsbeweise verlangen, das auch das durch den Algorithmus zu lösende Problem vollständig und präzise definiert ist. In der Regel wird aber auf umfangreiche, formale Korrektheitsbeweise verzichtet. 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: 1. Man benutzt einen idealisierenden 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-AccessMaschine). Eine solche Maschine verfügt über einige Register und eine (abzählbar unendliche) Menge einzeln adressierbarer Speicherzellen. Register und Speicherzellen können je eine (im Prinzip) unbeschränkt große (ganze oder reelle) Zahl aufnehmen. Das Befehlsrepertoire für eine RAM ähnelt einer einfachen, herkömmlichen Assemblersprache. Die Kostenmaße Speicherplatz und Laufzeit enthalten dann folgende Bedeutung: Der von einem Algorithmus benötigte Speicherplatz ist die Anzahl der zur Ausführung benötigten RAM-Speicherzellen. Die benötigte Zeit ist die Zahl der ausgeführten RAM-Befehle. 2. Bestimmung einiger für die Effizienz des Algorithmus besonders charakteristischer Parameter 6. 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 worstcase Analyse für die Ausführung eines Algorithmus der Problemgröß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 Ausdruck 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 in Abhängigkeit von der Problemgröße N gilt für alle N T ( N ) c1 N c2 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 12 Algorithmen und Datenstrukturen Logarithmisches Wachstum log N N N log N Lineares Wachstum N log N -Wachstum Quadratisches, kubisches, ... Wachstum N 2 , N 3 , ... Exponentielles Wachstum 2 N , 3 N , ... 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 Analyse der Komplexität von Algorithmen (bzw. Problemklassen) darum, als Maß für den Aufwand eine Funktion f : anzugeben, wobei f ( ) a bedeutet: „ Bei einem Problem der Größe N ist der Aufwand a“. Die Problemgröße „N“ bezeichnet dabei in der Regel ein grobes Maß für den Umfang einer Eingabe, z.B. die Anzahl der Elemente in der Eingabeliste oder die Größe eines bestimmten Eingabewertes. Der Aufwand „a“ ist in der Regel ein grobes Maß für die Rechenzeit. Die Rechenzeit wird häufig dadurch abgeschätzt, daß man zählt, wie häufig eine bestimmte Operation ausgeführt wird, z.B. Speicherzugriffe, Multiplikationen, Additionen, Vergleiche, etc. Bsp.: Wie oft wird die Wertzuweisung „x = x + 1“ in folgenden Anweisungen ausgeführt? 1. x = x + 1; ............ ..1-mal 2. for (i=1; i <= n; i++) x = x + 1;.. ..n-mal 3. for (i=1; i <= n; i++) for (j = 1; j <= n; j++) x = x + 1;................................... ......... n2-mal Die Aufwandsfunktion 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 Gesucht: Der Index i der (ersten) größten Zahl unter den a i (i=1,...,n) 13 Algorithmen und Datenstrukturen 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: wenn ai das größte der Elemente 1 Tn n . „max = i“ wird genau dann ausgeführt, a1 , a 2 , a3 ,..., ai ist. Angenommen wird Gleichverteilung: Für jedes i = 1, ... , n hat jedes der Elemente a1 , a2 , a3 ,..., an die gleiche Chance das größte zu sein, d.h.: Bei N Durchläufen wird N/n-mal die Anweisung „max = i“ ausgeführt. Daraus folgt für N Tn (Aufwendungen bei N Durchläufen vom „max = i“): N Tn N N N N 1 1 1 ... N (1 ... ) 2 3 n 2 3 n Dies ist Hn, die n-te harmonische Zahl. Für Hn ist keine geschlossene Formel bekannt, jedoch eine ungefähre Abschätzung: Tn H n ln n 8. 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) ist für g (n) 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: f (n) O( g (n)) : c, n0 n n0 : f (n) c g (n) mit f , g : N N , d.h. O-Notation O(1) Aufwand Konstanter Aufwand O (log n) Logarithmischer Aufwand O (n) Linearer Aufwand Problemklasse Einige Suchverfahren für Tabellen („Hashing“) Allgemeine Suchverfahren für Tabellen (Binäre Suche) Sequentielle Suche, Suche in Texten, syntaktische Analyse in Programmen „sclaues Sortieren“, z.B. Quicksort O(n log n) O(n 2 ) Quadratischer Aufwand Einige dynamische Optimierungsverfahren, z.B. optimale Suchbäume); „dummes Sortieren“, z.B. Bubble-Sort Multiplikationen Matrix mal Vektor Exponentieller Aufwand Viele Optimierungsprobleme, automatisches Beweisen (im Prädikatenkalkül 1. Stufe) Alle Permutationen O(n k ) für k 0 O(2 n ) O (n!) 8 Eulersche Konstante 0.57721566 14 Algorithmen und Datenstrukturen Zur Veranschaulichung des Wachstums können die folgenden Tabellen betrachtet werden: f(N) ldN N N ldN N2 N3 2N N=2 1 2 2 4 8 4 24=16 4 16 64 256 4096 65536 25=256 8 256 1808 65536 16777200 1077 210 10 1024 10240 1048576 109 10308 220 20 1048576 20971520 1012 1018 10315653 Unter der Annahme „1 Schritt dauert 1 s 10 6 s folgt für N= N N2 N3 2N 3N N! 10 10 s 100 s 1 ms 1 ms 59 ms 3,62 s 20 20 s 400 s 8 ms 1s 58 min 771 Jahre 30 30 s 900 s 27 ms 18 min 6.5 Jahre 1016 Jahre 40 40 s 1.6 ms 64 ms 13 Tage 3855 Jahre 1032 Jahre 50 50 s 2.5 ms 125 ms 36 Jahre 108 Jahre 1049 Jahre 60 60 s 3.6 ms 216 ms 366 Jahre 1013 Jahre 1066 Jahre Abb.: 1.2.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 4 zählen einmal. Zeile 3 zählt viermal (2 Multiplikationen und 1 Additionen) und wird N-mal ausgeführt. Das ergibt 4N. Zeile 2 zeigt die Initialisierung von i (zählt einmal), den Test i <= N (zählt Nmal). 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. Das führt zu folgenden Regeln zur Abschätzung des Leistungsverhaltens nach der „Big-O“-Notation: 15 Algorithmen und Datenstrukturen 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 meherere 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-1) 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 Man kann zeigen: fib( N ) ( ) N . Das bedeutet: Die Laufzeit dieses Programms 3 wächst exponentiell (schlechter geht es nicht mehr). 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: Bsp.: Das Maximum-Subarray-Problem 16 Algorithmen und Datenstrukturen 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 N j 1 O( N 2 ) . Diese Summe i 1 j i k i berechnet, wieviele Male Zeile 6 ausgeführt wird. 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älfte 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 jeweiligen 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 4. Lösung: Implementierung mit dem Leistungsverhalten O(N) 18 führt zu einer Algorithmen und Datenstrukturen 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 Inspektionsstelle 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 Inspektionsstelle 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 19 Algorithmen und Datenstrukturen Iteration: 64, 32, 16, 8, 4, 2, 1, 0, -1. Die Laufzeit läßt sich demnach in der Größenordnung O(logN) sehen. Die binäre Suche ist eine Implementierung eines Algorithmus für eine Datenstruktur (sequentiell gespeicherte Liste, Array). Zum Aufsuchen von Datenelementen wird eine Zeit von O(logN) verbraucht. Alle anderen Operationen (z.B. Einfügen) nehmen ein Leistungsverhalten von O(N) in Anspruch. 1.2.1.8 Effizienz System-Effizienz und rechnerische Effizienz Effiziente Algorithmen zeichnen sich aus durch - schnelle Bearbeitungsfolgen (Systemeffizienz) auf unterschiedlichen 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 DVLiteratur auftauchen, weil sie knapp darzustellen und im Prinzip einfach zu verstehen sind. Manche von ihnen sind nur von theoretischen Interesse, wie etwa die Türme von Hanoi. Ein anderes klassisches Problem ist dagegen das Problem des Handlungsreisenden 9 (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: 9 Vorbild für viele Optimierungsaufgaben, wie sie vor allem im Operations Research immer wieder vorkommen. 20 Algorithmen und Datenstrukturen 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 Beispiel, auch tatsächlich) ist sie exakte Lösung dieser Optimierungsaufgabe mit einem trivialen Suchalgorithmus zu erledigen. Man rechnet sich einfach die Route der Gesamtstrecke aus und wählt die kürzeste. Der benötigte Rechenaufwand steigt mit der Zahl N der zu besuchenden Städte sprunghaft an. Erhöht man bspw. N von 5 auf 10, so verlängert sich die Rechenzeit etwa auf das „dreißigtausendfache“. Dies nennt man kombinatorische Explosion, weil der Suchprozeß jede mögliche Kombination der für das Problem relevanten Objekte einzeln durchprobieren muß. Der Aufwand steigt proportional zur Fakultät (N!). - exponentielle Explosion Wie kann man die vollständige Prüfung aller möglichen Kombinationen und damit die kombinatorische Explosion umgehen? Naheliegend für das TSP ist, nicht alle möglichen Routen zu berechnen und erst dann die optimale zu suchen, sondern sich immer die bis jetzt beste zu merken und das Ausprobieren einer neuen Wegkombination sofort abzubrechen, wenn bereits eine Teilstrecke zu größeren Kilometerzahlen führt als das bisherige Optimum, z.B.: Route 1 München Karlsruhe Heidelberg Mannheim Wiesbaden Frankfurt München Streckensumme 0 287 341 362 454 486 881 Route 2 München Wiesbaden Karlsruhe Frankfurt Heidelberg Streckensumme 0 429 722 865 960 Route 2 kann abgebrochen werden, weil die Teilstrecke der Route 2 (960) bereits länger ist als die Gesamtstrecke der Route 1. Diese Verbesserung vermeidet die kombinatorische Explosion, ersetzt sie aber leider nur durch die etwas schwächere exponentielle Explosion. Die Rechenzeit nimmt exponentiell, d.h. mit aN für irgendeinen problemspezifischen Wert zu. Im vorliegenden Fall ist a etwa 1.26. - polynominales Zeitverhalten In der Regel ist polynominales Zeitverhalten das beste, auf das man hoffen kann. Hiervon redet man, wenn die benötigte Rechenzeit durch ein Polynom T a n N ... a 2 N a1 N a 0 ausgedrückt n 2 wird. „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 polynominale Zeitverhalten nach dieser höchsten Potenz. Man sagt, ein Verfahren zeigt polynominales 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 polynominaler 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 polynominaler 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 21 Algorithmen und Datenstrukturen 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 Zeitaufwand für das Durchsuchen proportional N ( N 1) / 2 , d.h. O(N2), und damit polynomial „in quadratischer Zeit“ Die meisten Algorithmen für Datenstrukturen bewegen sich in einem schmalen Band rechnerischer Komplexität. So ist ein Algorithmus von der Ordnung O(1) unabhängig von der Anzahl der Datenelemente in der Datensammlung. Der Algorithmus läuft unter konstanter Zeit ab, z.B.: Das Suchen des zuletzt in eine Schlange eingefügten Elements bzw. die Suche des Topelements in einem Stapel. Ein Algorithmus mit dem Verhalten O(N) ist linear. Zeitlich verhält er sich proportional zur Größe der Liste. Bsp.: Das Bestimmen des größten Elements in einer Liste. Es sind N Elemente zu überprüfen, bevor das Ende der Liste erkannt wird. Andere Algorithmen zeigen „logarithmisches Verhalten“. Ein solches Verhalten läßt sich beobachten, falls Teildaten die Größe einer Liste auf die Hälfte, ein Viertel, ein Achtel... reduzieren. Die Lösungssuche ist dann auf die Teilliste beschränkt. Derartiges Verhalten findet sich bei der Behandlung binärer Bäume bzw. tritt auf beim „binären Suchen“. Der Algorithmus für binäre Suche zeigt das Verhalten O(log 2 N ) , Sortieralgorithmen wie der Quicksort und Heapsort besitzen eine rechnerische Komplexität von O( N log 2 N ) . Einfache Sortierverfahren (z.B. „BubbleSort“) bestehen aus Algorithmen mit einer Komplexität von O( N 2 ) . Sie sind deshalb auch nur für kleine Datenmengen brauchbar. Algorithmen mit kubischem Verhalten O( N 3 ) sind bereits äußerst langsam (z.B. der Algorithmus von Warshall zur Bearbeitung von Graphen). Ein Algorithmus mit einer Komplexität von O( 2 N ) zeigt exponentielle Komplexität. Ein derartiger Algorithmus ist nur für kleine N auf einem Rechner lauffähig. 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; w i k bezeichnet die i-te Komponente des Knoten. Es gilt wk ( w1k , w2 k ,...., wn k ) Die Knotenwerte des vorstehenden Beispiels sind: wk1 = (Jürgen____,Regensburg,Bad Hersfeld,....__,Ulmenweg__) wk2 = (Josef_____,Straubing_,......______,....__,........__) wk3 = (Liesel____,....._____,......______,....__,........__) .......... wkn = (__________,__________,____________,______,__________) Welche Operationen sind mit dieser Datenstruktur möglich? Bei der vorliegenden Tabelle sind z.B. Zugriffsfunktionen zum Einfügen, Löschen und Ändern eines Tabelleneintrages mögliche Operationen. Generell bestimmen Datenstrukturen auch die Operationen, die mit diesen Strukturen ausgeführt werden dürfen. Zusammenhänge zwischen den Knoten eines Datenbestandes lassen sich mit Hilfe von Relationen bequem darstellen. Den vorliegenden Datenbestand wird man aus Verarbeitungsgründen bspw. nach einem bestimmten Merkmal anordnen (Ordnungsrelation). Dafür steht hier (im vorliegenden Beispiel) der Name der Studenten: 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 sind 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 (z.B. nach Matrikelnummern) geordnet (z.B. nach Titel im Vorlesungsverzeichnis) geordnet (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 reprä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ältige 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 Def(R) = { x | ( x , y ) R } Bild(R) = { y | ( x , y ) R } 2. Inverse Relation (Umkehrrelation) 28 Algorithmen und Datenstrukturen Relationen sind umkehrbar. Die Beziehungen zwischen 2 Größen x und y können auch als Beziehung zwischen y und x dargestellt werden, z.B.: Aus „x ist Vater von y“ wird durch Umkehrung „y ist Sohn von x“. Allgemein gilt: R-1 = { (y,x) | ( x , y ) R } 3. Reflexive Relation ( x, x ) R (Für alle Elemente x aus M gilt, x steht in Relation zu x) xM 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 asymmetrisch: ( 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. 29 Algorithmen und Datenstrukturen Gilt für x <> y die vorstehende Relation und für x = y ( x , x ) R , so wird diese binäre Relation "unstreng asymmetrisch" oder "antisymmetrisch" genannt. 6. Transitive Relation Eine binäre Relation ist transitiv, wenn (( ( x , y ) R ) und (( ( y , z ) R ) ist, dann ist auch (( ( x , z ) R ). x hat also y zur Folge und y hat z zur Folge. Somit hat x auch z zur Folge. 7. Äquivalenzrelation Eine binäre Relation ist eine Äquivalenzrelation, wenn sie folgenden Eigenschaften entspricht: - Reflexivität - Transitivität - Symmetrie Bsp.: Die Beziehung "... ist ebenso groß 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 groß ist wie x3, dann ist x1 ebenso groß wie x3. Die Relation ist transitiv. Klasseneinteilung - Ist eine Äquivalenzrelation R in einer Menge M erklärt, so ist M in Klassen eingeteilt - Jede Klasse enthält Elemente aus M, die untereinander äquivalent sind - Die Einteilung in Klassen beruht auf Mengen M1, M2, ... , Mx, ... , My Für die Teilmengen gilt: Mx M y 0 (2) M1 M 2 .... M y M (1) (3) Mx <> 0 (keine Teilmenge ist die leere Menge) Bsp.: Klasseneinteilungen können sein: - Die Menge der Studenten der FH Regensburg: Äquivalenzrelation "... ist im gleichen Semester wie ..." - Die Menge aller Einwohner einer Stadt in die Klassen der Einwohner, die in derselben 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 Äquivalenzrelationen? 1) 2) 3) 4) ... ... ... ... gehört dem gleichen Sportverein an ... hat denselben Geburtsort wie ... wohnt in derselben Stadt wie ... hat dieselbe Anzahl von Söhnen Ordnungen 30 Algorithmen und Datenstrukturen 1. Halbordnung Eine binäre Relation ist eine "Halbordnung", wenn sie folgende Eigenschaften besitzt: "Reflexivität, Transitivität" 2. Strenge Ordnungsrelation Eine binäre Relation ist eine "strenge Ordnungsrelation", wenn sie folgende Eigenschaft besitzt: "Transitivität, Asymmetrie" 3. Unstrenge Ordnungsrelation Eine binäre Relation ist eine "unstrenge Ordnungsrelation", wenn sie folgende Eigenschaften besitzt: Transitivität, unstrenge Asymmetrie 4. Totale Ordnungsrelation und partielle Ordnungsrelation Tritt in der Ordnungsrelation x vor y auf, so verwendet man das Symbol < (x < y). Vergleicht man die Abb. 1.2-9, so kann man für (1) schreiben: e < a < b < d und c < d Das Element c kann man weder mit den Elementen e, a noch mit b in eine gemeinsame Ordnung bringen. Daher bezeichnet man diese Ordnungsrelation als partielle Ordnung (teilweise Ordnung). Eine totale Ordnungsrelation enthält im Gegensatz dazu Abb. 1.2-9 in (2): e < a < b<c<d Kann also jedes Element hinsichtlich aller anderen Elemente geordnet werden, so ist die Ordnungsrelation eine totale, andernfalls heißt sie partiell. (1) (2) a a b e e b d c d 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“: 31 Algorithmen und Datenstrukturen /**************** 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 das 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, * 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 32 Algorithmen und Datenstrukturen Datenstrukturen10, die immer wieder verwendet werden und auf die andere Datenstrukturen zurückgeführt werden können. Den 4 Datenstrukturen ist gemeinsam, daß sie nur binäre Relationen verwenden. 1. 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 (sequentiell 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 2: Suchverfahren Kapitel 1: Datenverarbeitung und Datenorganisation Abschnitt 1: Ein einführendes Beispiel 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: 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 ausschließlich "Vorgänger/Nachfolger" - Beziehungen (in vertikaler Richtung) zwischen den Knoten eines Baums. Ein Baum ist auch hinsichtlich der Baumknoten in der horizontalen Ebene geordnet, wenn zur Definition des Baums neben der Äquivalenzrelation auch eine partielle Ordnung (Knoten K i 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 10 nach: Rembold, Ulrich (Hrsg.): "Einführung in die Informatik", München/Wien, 1987 33 Algorithmen und Datenstrukturen dieser Menge11. Die folgende Darstellung zeigt einen Netzplan zur Ermittlung des kritischen Wegs: Die einzelnen Knoten des Graphen sind Anfangs- und Endereignispunkte der Tätigkeiten, die an den Kanten angegeben sind. Die Kanten (Pfeile) beschreiben die Vorgangsdauer und sind Abbildungen binärer Relationen. Zwischen den Knoten liegt eine partielle Ordnungsrelation. Bestelle A 50 Tage 1 Baue B 20 Tage Korrigiere Fehler Teste B 4 3 2 25 Tage 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 mehreren binären 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. 5. Datenbanken Eine Datenbank ist die Sammlung von verschiedenen Datensatz-Typen. Die Datensätze sind in einer Codasyl-Datenbank13 untereinander verbunden, z.B. alle 11 vgl. 1.2.2.2, Abb. 1.2-7 vgl. 1.2.2.1 13 Datenbank der Data Base Task Group der Conference on Data Systems Languages (CODASYL) 12 34 Algorithmen und Datenstrukturen Studenten im Fachbereich „Informatik und Mathematik“ der Fachhochschule Regensburg, z.B.: Fachbereich Informatik Student_2 Student_1 ….. ….. 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 Fachbereich betreut mehrere (viele) Studenten. Solche Beziehungen können vom Typ 1:M, 1:1, M:N sein. Es ist auch die Bezugnahme auf gleiche Entitätstypen möglich, z.B.: Person 35 Algorithmen und Datenstrukturen 1 1 Heirat Abb.: 1.2-15: Bezugnahme auf den gleiche Entitätstyp „Person“ 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 Datenbank für das Personalwesen ABTEILUNG ABT_ID KO OD PA RZ VT BEZEICHNUNG Konstruktion Organisation und Datenverarbeitung Personalabteilung Rechenzentrum Vertrieb ANGESTELLTE 36 Algorithmen und Datenstrukturen 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 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 JOB_ID KA TA SY PR OP TITEL Kaufm. Angestellter Techn. Angestellter Systemplaner Programmierer Operateur 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 GEHALT 3000,00 DM 3000,00 DM 6000,00 DM 5000,00 DM 3500,00 DM QUALIFIKATION ANG_ID A1 A1 A1 A2 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14 JOB_ID SY PR OP IN SY PR KA PR OP TA IN SY IN KA SY IN KA Abb. 1.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) hat hier den Ansatz zu einer umfassenden Implementierung (Collection, Container) gezeigt. 37 Algorithmen und Datenstrukturen 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 Objekts zu bearbeiten. Nachrichten (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. 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 Eigenschaften 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)14. In der Praxis ist dieser Programmierstil erst seit 1980 verbreitet. Aktuell ist die objektorientierte Programmierung vor allem durch die inzwischen weit bekannte Programmiersprache 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 14 vgl. BYTE, Heft August 1981 38 Algorithmen und Datenstrukturen Methoden zur Deklaration von ADT15 bereitgestellt. Damit sollte dem Programmierer wenigstens durch die Spezifikation die Einheit von Daten und zugehörigen Operationen vermittelt werden. 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 Datenstrukturen. Sie soll hier stellvertretend für axiomatische Definitionsmethoden an einem Beispiel vorgestellt werden. 1. Bsp.: Die algebraische Spezifikation des (ADT) Schlange Konventionell würde die Datenstruktur Schlange so definiert werden: Eine Schlange ist ein lineares Feld, bei dem nur am Anfang Knoten entfernt und nur am Ende Knoten hinzugefügt werden können. Die Definition ist ungenau. Operationen sollten mathematisch exakt als Funktionen und Beziehungen der Operationen als Gleichungen angegeben sein. Erst dann ist die Prüfung auf Konsistenz und der Nachweis der korrekten Implementierung möglich. Die algebraische Spezifikation bestimmt den ADT Schlange deshalb folgendermaßen: ADT Schlange Typen Schlange<T>, boolean Funktionen (Protokoll) NeueSchlange FuegeHinzu : Vorn : Entferne : Leer : T,Schlange<T> Schlange<T> Schlange<T> Schlange<T> Schlange<T> Schlange<T> T Schlange<T> boolean Axiome Für alle t : T bzw. s : Schlange<T> gilt: 1. 2. 3. 4. 5. 6. 15 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)) vgl. Guttag, John: "Abstract Data Types and the Development of Data Structures", CACM, June 1977 39 Algorithmen und Datenstrukturen 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 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. 40 Algorithmen und Datenstrukturen 3. Bsp.: Die algebraische Spezifikation für einen binären Baum ADT Binaerbaum<T>, boolean 1. Funktionen (Operationen, Protokoll) Binaerbaum<T> NeuerBinaerbaum bin : Binaerbaum<T>, T, Binaerbaum<T> Binaerbaum<T> Binaerbaum<T> links : Binaerbaum<T> Binaerbaum<T> rechts : Binaerbaum<T> T wert : Binaerbaum<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. 2. Jeder Operator oder jede Funktion erwartet Argumente eines bestimmten Typs und liefert Resultate eines bestimmten Typs. Bei der konstruktiven Methode erfolgt die Definition von Datenstrukturen mit Hilfe bereits eingeführter Datentypen. Die niedrigste Stufe bilden die einfachen Datentypen. Basis-Datentypen werden in den meisten Programmiersprachen zur Verfügung gestellt und sind eng mit dem physikalischen Wertevorrat einer DVAnlage verknüpft (Standard-Typen). Sie sind die „Atome“ auf der niedrigsten Betrachtungsebene. Neue "höherwertige" Datentypen werden aus bereits definierten „niederstufigen“ Datentypen definiert. 1.2.3.4 Die objektorientierte Modellierung abstrakter Datentypen 41 Algorithmen und Datenstrukturen Die Spezifikation abstrakter Datentypen Im Mittelpunkt dieser Methode steht die Definition von Struktur und Wertebereich der Daten bzw. eine Sammlung von Operationen mit Zugriff auf die Daten. Jede Aufgabe aus der Datenverarbeitung läßt sich auf ein solches Schema (Datenabstraktion) zurückführen. Zur Beschreibung des ADT dient das folgende Format: ADT Name Daten Beschreibung der Datenstruktur Operationen Konstruktor Intialisierungswerte: Daten zur Initialisierung des Objekts Verarbeitung: Initialisierung des Objekts Operation1 Eingabe: Daten der Anwendung dieser Methode Vorbedingung: Notwendiger Zustand des Systems vor Ausführung einer Operation Verarbeitung: Aktionen, die an den Daten ausgeführt werden Ausgabe: Daten (Rückgabewerte) an die Anwendung dieser Methode Nachbedingung: Zustand des Systems nach Ausführung der Operation Operation2 ......... Operationn ......... Bsp.: Anwendung dieser Vorlage zur Beschreibung des ADT Stapel ADT Stapel Daten Eine Liste von Datenelementen mit einer Position „top“, sie auf den Anfang des Stapels verweist. Operationen Konstruktor: Initialisierungswerte: keine Verarbeitung: Initialisiere „top“. Push Eingabe: Ein Datenelement zur Aufnahme in den Stapel Vorbedingung: keine Verarbeitung: Speichere das Datenelement am Anfang („top“) des Stapels Ausgabe: keine Nachbedingung: Der Stapel hat ein neues Datenelement an der Spitze („top“). Pop Eingabe: keine Vorbedingung: Der Stapel ist nicht leer Verarbeitung: Das Element an der Spitze („top“) wird entfernt. Ausgabe: keine Peek bzw. Top Eingabe: keine Vorbedingung: Stapel ist nicht leer Verarbeitung: Bestimme den Wert des Datenelements an der Spitze („top“ des Stapel. Ausgabe: Rückgabe des Datenwerts, der an der Spitze („top“) des Stapels steht. Nachbedingung: Der Stapel bleibt unverändert. Leer 42 Algorithmen und Datenstrukturen 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 Datentypen 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. Eine strenge visuelle Unterscheidung zwischen Klassen und Objekten entfällt in der UML. Objekte werden von den Klassen dadurch unterschieden, daß ihre Bezeichnung unterstrichen ist. Häufig wird auch dem Bezeichner eines Objekts ein Doppelpunkt vorangestellt.. Auch können Klassen und Objekte zusammen im Klassendiagramm auftreten. Klasse Objekt Wenn man die Objekt-Klassen-Beziehung (Exemplarbeziehung, Instanzbeziehung) darstellen möchte, wird zwischen einem Objekt und seiner Klasse ein gestrichelter Pfeil in Richtung Klasse gezeichnet: Klasse Objekt Die Definition einer Klasse umfaßt die „bedeutsamen“ Eigenschaften. Das sind: - Attribute d.h.: die Struktur der Objekte: ihre Bestandteile und die in ihnen enthaltenen Informationen und Daten.. Abhängig von der Detaillierung im Diagramm kann die Notation für ein Attribut den Attributnamen, den Typ und den voreingestellten Wert zeigen: Sichtbarkeit Name: Typ = voreingestellter Wert - Operationen d.h.: das Verhalten der Objekte. Manchmal wird auch von Services oder Methoden gesprochen. Das Verhalten eines Objekts wird beschrieben durch die möglichen Nachrichten, die es verstehen kann. Zu jeder Nachricht benötigt das Objekt entsprechende Operationen. Die UML-Syntax für Operationen ist: Sichtbarkeit Name (Parameterliste) : Rückgabetypausdruck (Eigenschaften) Sichtbarkeit ist + (öffentlich), # (geschützt) oder – (privat) 43 Algorithmen und Datenstrukturen 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.: 44 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 - Qualifizierte 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. Multiplizität (Kardinalität) oder Rollennamen 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. 45 Algorithmen und Datenstrukturen Rolle1 Rolle2 K1 K2 1 0..* Abb.: Binäre Relation R = C1 x C2 Rolle1 K1 Rollen ... K2 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ängen 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 mit Ein A ist immer mit Ein A ist mit keinem Ein A ist mit keinem, einem B assoziiert einem oder mehreren oder einem B asso- einem oder mehreren B assoziiert ziiert B assoziiert Unified A 1 B A 1..* B A 0..1 B A * B 1:1 1..* 1:1..n 0..* 2..6 0..* * 17 4 n m 0..n:2..6 0..n:0..n 17:4 ? Abb.: Kardinalitäten für Beziehungen Pfeile in Klassendiagrammen zeigen Navigierbarkeit an. Wenn die Navigierbarkeit nur in einer Richtung existiert, nennt man die Assoziation eine gerichtete Assoziation (uni-directional association). Eine ungerichtete (bidirektionale) Assoziation enthält Navigierbarkeiten in beiden Richtungen. In UML bedeuten Assoziationen ohne Pfeile, daß die Navigierbarkeit unbekannt oder die Assoziation ungerichtet ist. Ungerichtete Assoziationen enthalten eine zusätzliche Bedingung: Die zu dieser Assoziation zugehörigen zwei Rollen sind zueinander invers. Abhängigkeit (dependency): Manchmal nutzt eine Klasse eine andere. Die UMLNotation ist dafür oft eine gestrichelte Linie mit einem Pfeil, z.B.: Applet 46 Algorithmen und Datenstrukturen 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 Existenzabhängiges Teil Abb.: Aggregation und Komposition Eine Aggregation wird durch eine Raute dargestellt. Die Komposition wird durch eine ausgefüllte Raute dargestellt und beschreibt ein „physikalisches Enthaltensein“. Die Vererbung (Spezialisierung bzw. Generalisierung) stellt eine Verallgemeinerung von Eigenschaften dar. Eine Generalisierung (generalization) ist eine Beziehung zwischen dem Allgemeinen und dem Speziellen, in der Objekte des speziellen Typs (der Subklasse) durch Elemente des allgemeinen Typs (der Oberklasse) ersetzt werden können. Grafisch wird eine Generalisierung als durchgezogene Linie mit einer unausgefüllten, auf die Oberklasse zeigenden Pfeilspitze wiedergegeben, z.B.: 47 Algorithmen und Datenstrukturen 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. 48 Algorithmen und Datenstrukturen <<interface>> InputStream OrderReader DataInput {abstract} Abhängigkeit Generalisierung Verfeinerung DataInputStrea m 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 49 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 (TypBezeichner) 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 50 Algorithmen und Datenstrukturen 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. 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 Definition 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. 51 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 bezieht 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 Stapel16 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) 16 PR12351.CPP 52 Algorithmen und Datenstrukturen template <class T> Stapel<T>& Stapel<T> :: operator = (const Stapel<T>& r) { nachf = r.nachf; for (int i = 0; i < nachf; i++) inhalt[i] = r.inhalt[i]; return *this; } // Funktionsschablone 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 << ">"; } 53 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 Nachrichten (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 Variablenund Methodendefinitionen. Zum Anlegen eines Objekts einer Klasse (Instanziierung 17) 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 erfordert18, erfolgt die Rückgabe von nicht mehr benötigtem Speicher automatisch19. 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: Sichtbarkeit Typ Name = voreingestellter_Wert; Sichtbarkeit: öffentlich (public), geschützt (protected) oder privat (private) 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 54 Algorithmen und Datenstrukturen Typ: Datentyp 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: Sichtbarkeit Rückgabetypausdruck Name(Parameterliste) Sichtbarkeit: öffentlich (public), geschützt (protected), privat (private) Rückgabetypausdruck: Jede Methode ist typisiert. Der Typ einer Methode bestimmt 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 instanziiert. 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 - Initialisierung 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 55 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 Instanziierungen 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 PaketKonzept 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. 56 Algorithmen und Datenstrukturen Zugriffsrechte auf Klassen, Variable und Methoden Es gibt in Java insgesamt 4 Zugriffsrechte: 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 klasseninterne 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 rekursive Programme benutzen Klassenmethoden, z.B.: // Berechnung der Fakultaet public static long fakultaet(int n) { 0) { return -1; } else if (n == 0) { 57 if (n < Algorithmen und Datenstrukturen return 1; } else { return n * fakultaet(n-1); } } 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 Konstanten20 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; Ü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. 20 Das bedeutet: das Schlüsselwort final ist bei lokalen Variablen nicht erlaubt 58 Algorithmen und Datenstrukturen 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 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 Papierkorb landet und sein Speicher freigegeben wird. Zum Erstellen einer Finalizer-Methode dient der 59 Algorithmen und Datenstrukturen 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. 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 Objekte behandelt. 60 Algorithmen und Datenstrukturen - 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 62 Algorithmen und Datenstrukturen 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 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) definieren21. 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.: 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 */ } 21 Angaben zur Implementierung sind nicht möglich 63 Algorithmen und Datenstrukturen Eine Klasse kann mehrere Schnittstellen implementieren, z.B. public class MeineKlasse extends Object implements meineSchnittstelle1, meineSchnittstelle2 { /* Normale Klassendefinition + Methoden 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. 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++) 64 Algorithmen und Datenstrukturen { 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();; } 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). 65 Algorithmen und Datenstrukturen Die Java-Klassenbibliothek22 von Java 1.0 enthält folgende Pakete: - 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 System Runtime Process Math Number Character Boolean ClassLoader SecurityManager Compiler 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. 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 JavaKlassen 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). 22 Die Klassenbibliothek des JDK befindet sich in einem Paket mit dem namen „java“. 66 Algorithmen und Datenstrukturen - 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 erweitert23: Paket java.applet java.awt java.awt.datatranfer java.awt.event java.awt.image java.beans java.io java.lang 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 Bedeutung Applets Abstract Window Toolkit ClipBoard-Funktionalität (Copy / Paste) AWT Event-handling Bildanzeige Java Beans Ein- und Ausgabe, Streams Elementare Sprachunterstützung 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. 23 Zusätzlich 15 weitere Packages, etwa 500 Klassen und Schnittstellen 67 Algorithmen und Datenstrukturen 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. 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 Verbindung übernimmt das Laufzeitprogramm der Programmiersprache. Java unterstützt das Konzept des „Late Binding“. 24 type import on demand, d.h.: Die Klasse wird erst dann in dem angegebenen Paket gesucht, wenn das Programm sie wirklich benötigt. 68 Algorithmen und Datenstrukturen 1.3 Sammlungen (Container) und Ordnungen 1.3.1 Ausgangspunkt: Das Konzept für Sammlungen 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 ObjektAnsammlungen. 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 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 DirektZugriff Nichtlinear Hierarchische Sammlung Sequentieller Zugriff Baum Dictionary HashTabeyl le Liste „array“ „record“ Heap GruppenKollektionen Set Stapel Schlange Graph prioritätsgest. Schl. „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 72 Algorithmen und Datenstrukturen Daten Ein Element mit einer Sammlung von Datenfeldern mit möglicherweise unterschiedlichen Typen. Operationen Der Operator . ist für den Direktzugriff auf den Datenbehälter vorgesehen. Eine Datei (file) ist eine extern eingerichtete Sammlung, die mit einer Datenstruktur („stream“) 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 Java26 dient zum Zugriff auf RandomAccess-Dateien. 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 26 Implementiert das Interface DataInput und DataOutput mit eigenen Methoden. 73 Algorithmen und Datenstrukturen 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. 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. 74 Algorithmen und Datenstrukturen 1.3.2.2 Nichtlineare Kollektionen 1. Hierarchische Sammlung Eine hierarchisch angeordnete Sammlung von Datenbehältern ist gewöhnlich ein Baum mit einem Ursprungs- bzw. Ausgangsknoten, der „Wurzel“ genannt wird. Von besonderer Bedeutung ist eine Baumstruktur, in der jeder Baumknoten zwei Zeiger auf nachfolgende Knoten aufnehmen kann. Diese Binärbaum-Struktur kann mit Hilfe einer speziellen angeordneten Folge der Baumknoten zu einem binären Suchbaum erweitert werden. Binäre Suchbäume bilden die Ausgangsbasis für das Speichern großer Datenmengen. „Baum“-Kollektion Daten Eine hierarchisch angeordnete Ansammlung von Knotenelementen, die von einem Wurzelknoten abgeleitet sind. Jeder Knoten hält Zeiger zu Nachfolgeknoten, die wiederum Wurzeln von Teilbäumen sein können. Operationen Die Baumstruktur erlaubt das Hinzufügen und Löschen von Knoten. Obwohl die Baumstruktur nichtlinear ist, ermöglichen Algorithmen zum Ansteuern der Baumknoten den Zugriff auf die in den Knoten gespeicherten Informationen. Ein „heap“ ist eine spezielle Version, in der das kleinste bzw. größte Element den Wurzelknoten besetzt. Operationen zum Löschen entfernen den Wurzelknoten, dabei wird, wie auch beim Einfügen, der Baum reorganisiert. Basis der Heap-Darstellung ist ein „array“ (Feldbaum), dessen Komponenten eine Binärbaumstruktur überlagert ist. In der folgenden Darstellung ist eine derartige Interpretation durch Markierung der Knotenelemente eines Binärbaums mit Indexpositionen sichtbar: [1] wk1 [2] [3] wk2 wk3 [4] [5] wk4 [8] [0] [10] wk9 [7] wk7 wk6 wk5 [9] wk8 [6] [13] [14] [11] [12] [15] wk1 wk1 wk1 wk1 wk1 wk1 0 1 2 3 4 5 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 75 Algorithmen und Datenstrukturen Liegen die Knoten auf den hier in Klammern angegebenen Positionen, so kann man von jeder Position I mit Pl = 2 * I Pr = 2 * I + 1 Pv = I div 2 auf die Position des linken (Pl) und des rechten (Pr) Nachfolgers und des Vorgängers (Pv) gelangen. Ein binärer Baum kann somit in einem eindimensionalen Feld (array) gespeichert werden, falls folgende Relationen eingehalten werden: X[I] <= X[2*I] X[I] <= X[2*I+1] X[1] = min(X[I] .. X[N]) Anstatt auf das kleinste kann auch auf das größte Element Bezug genommen werden Aufbau des Heap: Ausgangspunkt ist ein „array“ mit bspw. folgenden Daten: X[1] 40 X[2] 10 X[3] 30 X[4] ...... , der folgendermaßen interpretiert wird: X[1] 4 0 X[2] X[3] 10 30 Abb. 1.3-11: Interpretation von Feldinhalten im Rahmen eines binären Baums Durch eine neue Anordnung der Daten in den Feldkomponenten entsteht ein „heap“: X[1] 10 X[2] X[3] 40 30 Abb. 1.3-12: Falls ein neues Element eingefügt wird, dann wird nach dem Ordnen gemäß der „heap“-Bedingung erreicht: 76 Algorithmen und Datenstrukturen X[1] 10 X[2] X[3] 40 30 X[4] 15 X[1] 10 X[2] X[3] 15 30 X[4] 40 Abb.1.3-13: Das Einbringen eines Elements in einen Heap Beim Löschen wird das Wurzelelement an der 1. Position entfernt. Das letzte Element im „heap“ wird dazu benutzt, das Vakuum zu füllen. Anschließend wird reorganisiert: X[1] 10 X[2] X[3] 15 30 X[4] 40 77 Algorithmen und Datenstrukturen X[1] 40 X[2] X[3] 15 30 X[1] 15 X[2] X[3] 40 30 Abb. 1.3-14: Das Löschen eines Elements im Heap Implementierung des Heap in Java27. // 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 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 27 Vgl. pr13228 78 Algorithmen und Datenstrukturen 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. 79