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 Algorithmen1, 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 BL, einem Knoten W und einem rechten Teilbaum BR. Diese Definition ist rekursiv. Den Knoten W eines nichtleeren Baumes nennt man seine Wurzel. Beim Durchlaufen des binären Baumes sind alle Knoten aufzusuchen (, z. B. in einer vorgegebenen „von links nach rechts"-Reihenfolge,) mit Hilfe eines systematischen Weges, der aus Kanten aufgebaut ist. Die Darstellung bzw. die Implementierung eines binären Baums benötigt einen Binärbaum-Knoten: Dateninformation Knotenzeiger Links Rechts Zeiger Zeiger zum linken zum rechten Nachfolgeknoten Abb. 1.1-0: Knoten eines binären Suchbaums Eine derartige Struktur stellt die Klassenschablone baumKnoten bereit2: #ifndef BAUMKNOTEN #define BAUMKNOTEN #ifndef NULL const int NULL = 0; #endif // NULL 1 2 D. E. Knuth hat einen großen Teil dieses Wissens in "The Art of Computer Programming" zusammengefaßt vgl. pr11_1, baumkno.h 1 Algorithmen und Datenstrukturen // Deklaration eines Binaerbaumknotens fuer einen binaeren Baum template <class T> class baumKnoten { protected: // zeigt auf die linken und rechten Nachfolger des Knoten baumKnoten<T> *links; baumKnoten<T> *rechts; public: // Das oeffentlich zugaenglich Datenelement "daten" T daten; // Konstruktor baumKnoten (const T& merkmal, baumKnoten<T> *lzgr = NULL, baumKnoten<T> *rzgr = NULL); // virtueller Destruktor virtual ~baumKnoten(void); // Zugriffsmethoden auf Zeigerfelder baumKnoten<T>* holeLinks(void) const; baumKnoten<T>* holeRechts(void) const; // Die Klasse binSBaum benoetigt den Zugriff auf // "links" und "rechts" }; // Schnittstellenfunktionen // Konstruktor: Initialisiert "daten" und die Zeigerfelder // Der Zeiger NULL verweist auf einen leeren Baum template <class T> baumKnoten<T>::baumKnoten (const T& merkmal, baumKnoten<T> *lzgr, baumKnoten<T> *rzgr): daten(merkmal), links(lzgr), rechts(rzgr) {} // Die Methode holeLinks ermoeglicht den Zugriff auf den linken // Nachfolger template <class T> baumKnoten<T>* baumKnoten<T>::holeLinks(void) const { // Rueckgabe des Werts vom privaten Datenelement links return links; } // Die Methode "holeRechts" erlaubt dem Benutzer den Zugriff auf den // rechten Nachfoger template <class T> baumKnoten<T>* baumKnoten<T>::holeRechts(void) const { // Rueckgabe des Werts vom privaten Datenelement rechts return rechts; } // Destruktor: tut eigentlich nichts template <class T> baumKnoten<T>::~baumKnoten(void) {} #endif // BAUMKNOTEN Mit der vorliegenden Implementierung zu einem Binärbaum-Knoten kann bspw. die folgende Gestalt eines binären Baums erzeugt werden: 2 Algorithmen und Datenstrukturen 1 2 3 5 4 Abb1.1-1: Eine binäre Baumstruktur Benötigt wird dazu die folgenden Anweisungen im Hauptprogrammabschnitt: // Hauptprogramm int main() { int zahl; baumKnoten<int> *wurzel; baumKnoten<int> *lKind, *rKind, *z; lKind = new baumKnoten<int>(3); rKind = new baumKnoten<int>(4); z = new baumKnoten<int>(2,lKind,rKind); lKind = z; rKind = new baumKnoten<int>(5); z = new baumKnoten<int>(1,lKind,rKind); wurzel = z; } 1.1.1 Rekursive Problemlösung Rekursive Datenstrukturen (z.B. Bäume) werden zweckmäßigerweise mit Hilfe rekursiv formulierter Zugriffsalgorithmen bearbeitet. Das zeigt die folgende Lösung in C++: #include<iostream.h> #include<stdlib.h> #include "baumkno.h" // Funktionsschablone fuer Baumdurchlauf template <class T> void wlr(baumKnoten<T>* b) { if (b != NULL) { cout << b->daten << ' '; wlr(b->holeLinks()); // linker Abstieg wlr(b->holeRechts()); // rechter Abstieg } } // Hauptprogramm 3 Algorithmen und Datenstrukturen int main() { int zahl; baumKnoten<int> *wurzel; baumKnoten<int> *lKind, *rKind, *z; lKind = new baumKnoten<int>(3); rKind = new baumKnoten<int>(4); z = new baumKnoten<int>(2,lKind,rKind); lKind = z; rKind = new baumKnoten<int>(5); z = new baumKnoten<int>(1,lKind,rKind); wurzel = z; cout << "Inorder: " << endl; ausgBaum(wurzel,0); wlr(wurzel); // Rekursive Problemlösung cout << endl; // Nichtrekursive Problemlösung wlrnr(wurzel); cout << endl; } Das Durchlaufen geht offensichtlich von der Wurzel aus, ignoriert zuerst die rechten Teilbäume, bis man auf leere Bäume stößt. Dann werden die Teiluntersuchungen abgeschlossen und beim Rückweg die rechten Bäume durchlaufen. Jeder Baumknoten enthält 2 Zeiger (Adressen von BL und BR). Die Zeiger, die auf leere Bäume hinweisen, werden auf „NULL“ gestellt. 1.1.2 Nichtrekursive Problemlösung Das vorliegende Beispiel ist in C++ notiert. C++ läßt rekursiv formulierte Prozeduren zu. Was ist zu tun, wenn eine Programmiersprache rekursive Prozeduren nicht zuläßt? Rekursive Lösungsangaben sind außerdem schwer verständlich, da ein wesentlicher Teil des Lösungswegs dem Benutzer verborgen bleibt. Die Ausführung rekursiver Prozeduren verlangt bekanntlich einen Stapel (stack). Ein Stapel ist eine Datenstruktur, die auf eine Folge von Elementen 2 wesentliche Operationen ermöglicht: Die beiden wesentlichen Stackprozeduren sind PUSH und POP. PUSH fügt dem Stapel ein neues Element an der Spitze (top of stack) hinzu. POP entfernt das Spitzenelement. Die beiden Prozeduren sind mit der Typdefinition des Stapel beschrieben. Der Stapel nimmt Zeiger auf die Baumknoten auf. Jedes Stapelelement ist mit seinen Nachfolgern verkettet: 4 Algorithmen und Datenstrukturen Zeiger auf Baumknoten Top-Element Zeiger auf Baumknoten Zeiger auf Baumknoten nil nil Abb. 1.1-2: Aufbau eines Stapels Der nicht rekursive Baumdurchlauf-Algorithmus läßt sich mit Hilfe der Stapelprozeduren der Containerklasse Stack der Standard Template Library (STL) 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. 7 Algorithmen und Datenstrukturen Die Datenstruktur „Baum“ ist offensichtlich in vielen Anwendungsfällen die geeignete Abbildung für Probleme, die mit Rechnerunterstützung gelöst werden sollen. Der zur Lösung notwendige Verfahrensablauf ist durch das Aufsuchen der Baumknoten festgelegt. Das einführende Beispiel zeigt das Zusammenwirken von Datenstruktur und Programmierverfahren für Problemlösungen 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 implemen-tieren. 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 Definition Datenorganisation heißt: Daten zweckmäßig so einrichten, daß eine möglichst effektive Verarbeitung erreicht werden kann. So wurde im einführenden Beispiel die Bearbeitung rekursiver Strukturen (Bäume) mit Hilfe der Datenstruktur Stapel und den dazugehörigen Programmierverfahren (PUSH, POP) ermöglicht. Das braucht aber immer nicht „von Anfang an“ untersucht bzw. implementiert zu werden. Bereits vorhandenes Wissen, z.B. über Datenstrukturen und dazugehörige Programmierverfahren, ist zu nutzen. Das Wissen über den Stapel und seine Programmierung kann allgemein zur Bearbeitung der Rekursion auf einem Digitalrechner benutzt werden und ist nicht nur auf die Bearbeitung von Baumstrukturen beschänkt. Die Programmierverfahren sind durch Algorithmen festgelegt. In der Regel ist das eine Folge von Anweisungen, die den Lösungsweg einer Aufgabe in endlich vielen Schritten exakt und vollständig beschreiben. 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. 1.2.1.2 Effizienz System-Effizienz und rechnerische Effizienz Effiziente Algorithmen zeichnen sich aus durch - schnelle Bearbeitungsfolgen (Systemeffizienz) auf unterschiedliche Rechnersystemen. Hier wird die Laufzeit der diversen Suchalgorithmen auf dem Rechner (bzw. verschiedene Rechnersysteme) ermittelt und miteinander verglichen. Die zeitliche Beanspruchung wird über die interne Systemuhr gemessen und ist abhängig vom Rechnertyp - Inanspruchnahme von möglichst wenig (Arbeits-) Speicher - Optimierung wichtiger Leistungsmerkmale, z.B. die Anzahl der Vergleichsbedingungen, die Anzahl der Iterationen, die Anzahl der Anweisungen (, die der Algorithmus benutzt). Die Berechnungskriterien bestimmen die sog. rechnerische Komplexität in einer Datensammlung. Man spricht auch von der rechnerischen Effizienz. 9 Algorithmen und Datenstrukturen Berechnungsgrundlagen für rechnerische Komplexität Generell kann man für Algorithmen folgende Grenzfälle bzgl. der Rechenbarkeit beobachten: - kombinatorische Explosion Der Suchprozeß probiert jede mögliche Kombination der für das Problem relevanten Objekte aus. Suchalgorithmen, die ein derartiges Verhalten zeigen, übersteigen schnell die Grenze der Rechenbarkeit. - exponentielle Explosion Die Rechenbarkeit für solche Suchprozesse nimmt exponentiell, z.B. mit xN für irgendeinen problemspezifischen Wert x zu. - polynomiales Verhalten N 2 Die benötigte Rechenzeit kann durch ein Polynom a N x +...+ a 2 x + a1 x + a0 ausgedrückt werden. „x“ ist bestimmt durch die zu suchenden problemspezifischen Werte, N beschreibt die Anzahl der Objekte. Da gegen das erste Glied mit der höchsten Potenz bei größeren Objektzahlen alle anderen Terme des Ausdrucks vernachlässigt werden können, klassifiziert man das polynomiale Zeitverhalten nach dieser höchsten Potenz. Man sagt, ein Verfahren zeigt polynomiales Zeitverhalten O(xN), falls die benötigte Rechenzeit mit der Nten Potenz der Zahl der zu bearbeitenden Objekte anwächst. 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 O( N log 2 N ) . Einfache Sortierverfahren (z.B. „Bubble-Sort“) 2 bestehen aus Algorithmen mit einer Komplexität von O( N ) . Sie sind deshalb auch nur für kleine 3 Datenmengen brauchbar. Algorithmen mit kubischem Verhalten O( N ) sind bereits äußerst rechnerische Komplexität von langsam (z.B. der Algorithmus von Warshall zur Bearbeitung von Graphen). Ein Algorithmus mit N einer Komplexität von O( 2 ) zeigt exponentielle Komplexität. Ein derartiger Algorithmus ist nur für kleine N auf einem Rechner lauffähig. N log 2 N N log 2 N N2 N3 2N 2 4 8 16 32 128 1 2 3 4 5 7 2 8 24 64 160 896 4 16 64 256 1024 16384 8 64 512 4096 32768 2097152 4 16 256 65536 4294967296 1024 10 10240 1048576 1073741824 3.4 ⋅ 1038 18 . ⋅ 10 308 65536 16 1048576 4294967296 2.8 ⋅ 1014 -4 4 übertrifft den Darstellungsbereich 10 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: 11 Algorithmen und Datenstrukturen Josef Juergen Liesel Abb. 1.2-1: Einfacher Zusammenhang zwischen Knoten eines Datenbestandes Datenstrukturen bestehen also aus Knoten(den einzelnen Datenobjekten) und Relationen (Verbindungen). Die Verbindungen bestimmen die Struktur des Datenbestandes. Bsp.: 1. An Bayerischen Fachhochschulen sind im Hauptstudium mindestens 2 allgemeinwissenschaftliche Wahlfächer zu absolvieren. Zwischen den einzelnen Fächern, den Dozenten, die diese Fächer betreuen, und den Studenten bestehen Verbindungen. Die Objektmengen der Studenten und die der Dozenten ist nach den Namen sortiert (geordnet). Die Datenstruktur, aus der hervorgeht, welche Vorlesungen die Studenten bei welchen Dozenten hören, ist: 12 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 13 Algorithmen und Datenstrukturen 2 Relationen können hier unterschieden werden: 1) Beziehungsverhältnisse eines Knoten zu seinen unmittelbaren Nachfolgeknoten. Die Relation Analyse beschreibt den Aufbau eines Gerätes 2) Die Relation Vorrat gibt die Knoten mit w2k <= 3 an. Die Beschreibung eines Geräts erfordert in der Praxis eine weit komplexere Datenstruktur (größere Knotenzahl, zusätzliche Relationen). 3. Eine Bibliotheksverwaltung soll angeben, welches Buch welcher Student entliehen hat. Es ist ausreichend, Bücher mit dem Namen des Verfassers (z.B. „Stroustrup“) und die Entleiher mit ihrem Vornamen (z.B. „Juergen“, „Josef“) anzugeben. Damit kann die Bibliotheksverwaltung Aussagen, z.B. „Josef hat Stroustrup ausgeliehen“ oder „Juergen hat Goldberg zurückgegeben“ bzw. Fragen, z.B. „welche Bücher hat Juergen ausgeliehen?“, realisieren. In die Bibliothek sind Objekte aufzunehmen, die Bücher repäsentieren, z.B.: Buch „Stroustrup“ Weiterhin muß es Objekte geben, die Personen repräsentieren, z.B.: Person „Juergen“ Falls „Juergen“ Stroustrup“ ausleiht, ergibt sich folgende Darstellung: Person „Juergen“ Buch „Stroustrup“ Abb. 1.2-4: Objekte und ihre Beziehung in der Bibliotheksverwaltung Der Pfeil von „Stroustrup“ nach „Juergen“ zeigt: „Juergen“ ist der Entleiher von „Stroustrup“, der Pfeil von „Juergen“ nach „Stroustrup“ besagt: „Stroustrup“ ist eines der von „Juergen“ entliehenen Bücher. Für mehrere Personen kann sich folgende Darstellung ergeben: 14 Algorithmen und Datenstrukturen Person Person „Juergen“ Person „Josef“ Buch „Stroustrup“ Buch „Goldberg“ Buch „Lippman“ Abb. 1.2-5: Objektverknüpfungen in der Bibliotheksverwaltung Zur Verbindung der Klasse „Person“ bzw. „Buch“ wird eine Verbindungsstruktur benötigt: Person buecher = Verbindungsstruktur „Juergen“ Buch „Stroustrup“ Abb.1.2-6: Verbindungsstruktur zwischen den Objekttypen „Person“ und „Buch“ Ein bestimmtes Problem kann auf vielfätige Art in Rechnersystemen abgebildet werden. So kann das vorliegende Problem über verkettete Listen im Arbeitsspeicher oder auf Externspeicher (Dateien) realisiert werden. Die vorliegenden Beispiele können folgendermaßen zusammengefaßt werden: Die Verkörperung einer Datenstruktur wird durch das Paar D = (K,R) definiert. K ist die Knotenmenge (Objektmenge) und R ist eine endliche Menge von binären Relationen über K. 15 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 } 16 Algorithmen und Datenstrukturen 2. Inverse Relation (Umkehrrelation) Relationen sind umkehrbar. Die Beziehungen zwischen 2 Grössen x und y können auch als Beziehung zwischen y und x dargestellt werden, z.B.: Aus „x ist Vater von y“ wird durch Umkehrung „y ist Sohn von x“. Allgemein gilt: R-1 = { (y,x) | ( x , y ) ∈ R } 3. Reflexive Relation ∀ ( x , x ) ∈ R (Für alle Elemente x aus M gilt, x steht in Relation zu x) x ∈M Beschreibt man bspw. die Relation "... ist Teiler von ..." für die Menge M = {2,4,6,12} in einem Grafen, so erhält man: 12 4 6 2 Abb.1.2-8: Die binäre Relation „... ist Teiler von ... “ Alle Pfeile, die von einer bestimmten Zahl ausgehen und wieder auf diese Zahl verweisen, sind Kennzeichen einer reflexiven Relation ( in der Darstellung sind das Schleifen). Eine Relation, die nicht reflexiv ist, ist antireflexiv oder irreflexiv. 4. Symmetrische Relation Aus (( ( x , y ) ∈ R ) folgt auch (( ( y , x ) ∈ R ). Das läßt sich auch so schreiben: Wenn ein geordnetes Paar (x,y) der Relation R angehört, dann gehört auch das umgekehrte Paar (y,x) ebenfalls dieser Relation an. Bsp.: a) g ist parallel zu h h ist parallel zu g b) g ist senkrecht zu h h ist senkrecht zu g 5. Asymmetrische Relation Solche Relationen sind auch aus dem täglichen Leben bekannt. Es gilt bspw. „x ist Vater von y“ aber nicht gleichzeitig „y ist Vater von x“. Eine binäre Relation ist unter folgenden Bedingungen streng asymetrisch: 17 Algorithmen und Datenstrukturen ∀ ( x , y ) ∈ R → (( y , x ) ∉ R) ( x , y )∈R Das läßt sich auch so ausdrücken: Gehört das geordnete Paar (x,y) zur Relation, so gehört das entgegengesetzte Paar (y,x) nicht zur Relation. Gilt für x <> y die vorstehende Relation und für x = y ∀( x , x ) ∈ R , so wird diese binäre Relation "unstreng asymmetrisch" oder "antisymmetrisch" genannt. 6. Transitive Relation Eine binäre Relation ist transitiv, wenn (( ( x , y ) ∈ R ) und (( ( y , z ) ∈ R ) ist, dann ist auch (( ( x , z ) ∈ R ). x hat also y zur Folge und y hat z zur Folge. Somit hat x auch z zur Folge. 7. Äquivalenzrelation Eine binäre Relation ist eine Äquivalenzrelation, wenn sie folgenden Eigenschaften entspricht: - Reflexivität - Transitivität - Symmetrie Bsp.: Die Beziehung "... ist ebenso gross wie ..." ist eine Äquivalenzrelation. 1. Wenn x1 ebenso groß ist wie x2, dann ist x2 ebenso groß wie x1. Die Relation ist symmetrisch. 2. x1 ist ebenso groß wie x1. Die Relation ist reflexiv. 3. Wenn x1 ebenso groß wie x2 und x2 ebenso gross ist wie x3, dann ist x1 ebenso groß wie x3. Die Relation ist transitiv. Klasseneinteilung - Ist eine Äquivalenzrelation R in einer Menge M erklärt, so ist M in Klassen eingeteilt - Jede Klasse enthält Elemente aus M, die untereinander äquivalent sind - Die Einteilung in Klassen beruht auf Mengen M1, M2, ... , Mx, ... , My Für die Teilmengen gilt: (1) M x ∩ M y = 0 (2) M1 ∪ M 2 ∪....∪ M y = M (3) Mx <> 0 (keine Teilmenge ist die leere Menge) Bsp.: Klasseneinteilungen können sein: - Die Menge der Studenten der FH Regensburg: Äquivalenzrelation "... ist im gleichen Semester wie ..." - Die Menge aller Einwohner einer Stadt in die Klassen der Einwohner, die in der-selben Straße wohnen: Äquivalenzrelation ".. wohnt in der gleichen Strasse wie .." Aufgabe 1. Welche der folgenden Relationen sind transitiv bzw. nicht transitiv? 1) 2) 3) 4) 5) ... ... ... ... ... ist ist ist ist ist der Teiler von .... der Kamerad von ... Bruder von ... deckungsgleich mit ... senkrecht zu ... (transitiv) (transitiv) (transitiv) (transitiv) (nicht transitiv) 2. Welche der folgenden Relationen sind Aequivalenzrelationen? 18 Algorithmen und Datenstrukturen 1) 2) 3) 4) ... ... ... ... gehört dem gleichen Sportverein an ... hat denselben Geburtsort wie ... wohnt in derselben Stadt wie ... hat diesselbe Anzahl von Söhnen Ordnungen 1. Halbordnung Eine binäre Relation ist eine "Halbordnung", wenn sie folgende Eigenschaften besitzt: "Reflexivität, Transitivität" 2. Strenge Ordnungsrelation Eine binäre Relation ist eine "strenge Ordnungsrelation", wenn sie folgende 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 19 c Algorithmen und Datenstrukturen Abb. 1.2-9: Totale und partielle Ordnungsrelationen Natürliche Ordnung im Java Development Kit (JDK) Im JDK gibt es die Möglichkeit, Elemente z.B. in einem Set, in einer Map5, etc. zu sortieren. Dabei kann entweder über die natürliche Ordnung, oder die Elemente können mit Hilfe eines expliziten Vergleichsobjekts sortiert werden. Für eine natürliche Ordnung muß sichergestellt werden, daß alle Elemente der Datensammlung (Collection) eine compareTo-Methode besitzen und je zwei beliebige Element miteinander verglichen werden können, ohne dass es zu einem Typfehler kommt. Dazu müssen die Elemente das Interface Comparable aus dem Paket java.lang implementieren. Das Interface Comparable besitzt lediglich eine einzige Methode zum Vergleich des aktuellen Elements mit einem anderen: public int compareTo(Object o) „compareTo“ muß einen Wert kleiner als Null zurückgeben, falls das aktuelle Element vor dem zu vergleichenden liegt. „compareTo“ muß den Wert Null zurückgen, falls das aktuelle Element und das zu vergleichende gleich sind. „compareTo“ muß einen Wert größer als Null zurückgeben, falls das aktuelle Element hinter dem zu vergleichenden liegt. Das Comparable-Interface ist seit dem JDK 1.2 bereits in vielen eingebauten Klassen implementiert (z.B. String, Character, Double, etc.). Die natürliche Ordnung ergibt sich durch paarweisen Vergleich aller Elemente. Dabei wird das kleinere vor das größere Element gestellt. Die zweite Möglichkeit zum Sortieren von Elementen im JDK besteht darin, an den Konstruktor der Collection-Klasse ein Objekt des Typs Comparator zu übergeben. Comparator ist ein Interface, das lediglich eine Einzige Methode dekariert: public int compare(Object o1, Object o2) Das übergebene Comparator-Objekt übernimmt die Aufgabe einer Vergleichsfunktion, deren Methode compare immer dann aufgerufen wird, wenn bestimmt werden muß, in welcher Reihenfolge zwei beliebige Elemente zueinander stehen. 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 Datenstrukturen6, 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 5 6 Derartige Datensammlungen werden Collections genannt, vgl. 1.3 nach: Rembold, Ulrich (Hrsg.): "Einführung in die Informatik", München/Wien, 1987 20 Algorithmen und Datenstrukturen Sie sind über eine (oder mehrere) totale Ordnung(en) definiert. Die bekanntesten Verkörperungen der linearen Ordnung sind: - (ein- oder mehrdimensionale) Felder (lineare Felder) - Stapel - Schlangen - lineare Listen Lineare Ordnungsgruppen können sequentiell (seqentiell gespeichert)7 bzw. verkettet (verkettet gespeichert)8 angeordnet werden. 2. Bäume Sie sind im wesentlichen durch die Äquivalenzrelation bestimmt. Bsp.: Gliederung zur Vorlesung Algorithmen und Datenstrukturen Algorithmen und Datenstrukturen Kapitel 1: Datenverarbeitung und Datenorganisation Abschnitt 1: Ein einführendes Beispiel Kapitel 2: Suchverfahren Abschnitt 2: Begriffe Abb.: 1.2-10: Gliederung zur Vorlesung Datenorganisation Die Verkörperung dieser Vorlesung ist das vorliegende Skriptum. Diese Skriptum {Seite 1, Seite 2, ..... , Seite n} teilt sich in einzelne Kapitel, diese wiederum gliedern sich in Abschnitte. Die folgenden Äquivalenzrelationen definieren diesen Baum: 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 Ki kommt vor Knoten Kj, z.B. Kapitel1 kommt vor Kapitel 2) eingeführt wird. 3. Graphen In seiner einfachsten Form besteht eine Verkörperung dieser Datenstruktur aus einer Knotenmenge K (Objektmenge) und einer festen aber beliebigen Relation R 7 8 vgl. 2.2.1 vgl. 2.2.2 21 Algorithmen und Datenstrukturen über dieser Menge9. 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 Ordnungsrelation10. Bestelle A 50 Tage Baue B Teste B 4 25 Tage 1 20 Tage Korrigiere Fehler 2 15 Tage 3 Handbucherstellung 60 Tage Abb. 1.2-11: Ein Graph der Netzplantechnik 4. Dateien Damit ist eine Datenstruktur bestimmt, bei der Verbindungen zwischen den Datenobjekten durch beliebige, binäre Relationen beschrieben werden. Die Anzahl der Relationen ist somit im Gegensatz zur Datenstruktur Graph nicht auf eine beschränkt. Verkörperungen solcher assoziativer Datenstrukturen sind vor allem Dateien. In der Praxis wird statt mehrere binärer Relationen eine n-stellige Relation (Datensatz) gespeichert. Eine Datei ist dann eine Sammlung von Datensätzen gleichen Typs. Bsp.: Studenten-Datei Sie faßt die relevanten Daten der Studenten11 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-Datenbank12 untereinander verbunden, z.B. alle Studenten im Fachbereich „Informatik und Mathematik“ der Fachhochschule Regensburg, z.B.: Fachbereich Informatik 9 vgl. 1.2.2.2, Abb. 1.2-7 vgl. 3.4.2 11 vgl. 1.2.2.1 12 Datenbank der Data Base Task Group der Conference on Data Systems Languages (CODASYL) 10 22 Algorithmen und Datenstrukturen Student_1 Student_2 .... Student_n Abb.: 1.2-12: Erscheinungsbild der Datensätze „Fachbereich“ und „Student“ Der letzte Studentensatz zeigt auf den Satz „Fachbereich Informatik und Mathematik“ zurück. Diese detaillierte Darstellung der physischen Struktur kann auf folgende Beschreibung der logischen Datenstruktur zurückgeführt werden: Fachbereich betreut Student Abb.: 1.2-13: Logische Datenstruktur Auch hier zeigt sich: Knoten bzw. Knotentypen und ihre Beziehungen bzw. Beziehungstypen stehen im Mittelpunkt der Datenbank-Beschreibung. Statt Knoten spricht man hier von Entitäten (bzw. Entitätstypen) und Beziehungen werden Relationen genannt. Dies ist dann Basis für den Entity-Relationship (ER) -Ansatz von P.S. Chen. Zur Beschreibung der Entitätstypen und ihrer Beziehungen benutzt der ER-Ansatz in einem ER-Diagramm rechteckige Kanten bzw. Rauten: 23 Algorithmen und Datenstrukturen 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 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) 24 Algorithmen und Datenstrukturen - Qualifikation(Ang_ID,Job_ID) und resultiert in folgender relationalen Datnbank für das Personalwesen ABTEILUNG ABT_ID KO OD PA RZ VT BEZEICHNUNG Konstruktion Organisation und Datenverarbeitung Personalabteilung Rechenzentrum Vertrieb ANGESTELLTE 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 25 Algorithmen und Datenstrukturen Abb. 1.2-17: Tabellen zur relationalen Datenbank Die relationale Datenbank besteht, wie die vorliegende Darstellung zeigt aus einer Datenstruktur, die Dateien (Tabellen) vernetzt. Die Verbindung besorgen Referenzen (Fremdschlüssel). Die vorliegende Einteilung der Datenstrukturen zeigt: Sammeln und Ordnen der durch die reale Welt vorgegebenen Objekte ist eine der wichtigsten und häufigsten Anwendungen in der Datenverarbeitung. Leider unterstützen herkömmliche Programmiersprachen nicht umfassend genug diese Möglichkeiten. Erst die objektorientierte Programmierung (vor allem Smalltalk) haben hier den Ansatz zu einer umfassenden Implementierung (Collection, Container) gezeigt. 1.2.3 Definitionsmethoden für Datenstrukturen 1.2.3.1 Der abstrakte Datentyp Die Definition einer Datenstruktur ist bestimmt durch Daten (Datenfelder, Aufbau, Wertebereiche) und die für die Daten gültigen Rechenvorschriften (Algorithmen, Operationen). Datenfelder und Algorithmen bilden einen Typ, den abstrakten Datentyp (ADT). Der ADT ist eine Kapsel, der die gemeinsame Deklaration von Daten und Algorithmen zu einem Typ zusammenfaßt. Datenkapseln sind elementare Bausteine, die den konventionellen Programmierstil (Programm = Algorithmus + Daten) auf ein anspruchvolleres Niveau anheben. Die Datenkapsel betrachtet Daten und Rechenvorschrift als eine Einheit. Das bedeutet aber auch: Für die Ausführung einer Aufgabe ist die Datenkapsel selbst verantwortlich. Der Anwender hat damit nichts zu tun. Er teilt dem durch die Datenkapsel bestimmten Objekt lediglich über eine Botschaft mit, daß er eine spezielle Funktion ausgeführt haben möchte. Das Empfangsobjekt wählt daraufhin eine ihm bekannte Methode aus und führt die dazugehörige Prozedur aus. Das Ergebnis der Methode wird vom Objekt an den den Sender der Botschaft wieder zurückgeschickt. Die durch die Datenkapsel realisierte Einheit hat einen speziellen Namen: Objekt. Den zugehörigen Programmierstil nennt man: objektorientierte Programmierung. Die objektorientierte Sichtweise faßt Daten, Prozeduren und Funktionen zu möglichst realistischen Modellen (Objekten) der Wirklichkeit zusammen. Zugriff auf objektorientierte Modelle ist nur den Methoden (Prozeduren und Funktionen) erlaubt. Eine Methode gehört zu einem Objekt mit dem Zweck die Daten des Oblekts zu bearbeiten. Nachrichen (Botschaften) sind neben den Objekten das 2. wesentliche Element in objektorientierten Programmiersprachen. Objekte machen nur dann etwas, wenn sie eine Nachricht empfangen und für diese Nachricht eine Methode haben. Andernfalls geben sie die Nachricht an die Klasse weiter, der das Objekt angehört. Klassen (Objekttypen) sind Realisierungen abstrakter Datentypen und umfassen: Attribute (Eigenschaften) , Methoden, Axiome. Sie beschreiben Zustand und Verhalten gleichartiger Objekte. Generell gilt: Ein neues Objekt (Instanz, Exemplar) einer Klasse erbt alle Eigenschaften der Klasse. Man kann aber diesem Erbe Eigenschaften hinzufügen 26 Algorithmen und Datenstrukturen bzw. Methoden streichen bzw. modifizieren. Falls der Erbe selbst Nachkommen erhält, dann geschieht folgendes: 1. Die Instanz wird zur Klasse 2. Die Nachkommen erben die Eigenschaftten der Vorfahren Jede Klasse in einem objektorientierten Programmiersystem (OOP) hat einen Vorfahren Eine unmittelbare Implementierung der objektorientierten Programmierung (und damit von ADT) gibt es erst seit 1981 (Smalltalk-80)13. 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 Methoden zur Deklaration von ADT14 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. Bsp.: Der abstrakte Datentyp (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: 13 14 vgl. BYTE, Heft August 1981 vgl. Guttag, John: "Abstract Data Types and the Development of Data Structures", CACM, June 1977 27 Algorithmen und Datenstrukturen ADT Schlange Typen Schlange<T>, boolean Funktionen (Protokoll) NeueSchlange FuegeHinzu : Vorn : Entferne : Leer : → Schlange<T> T,Schlange<T> → Schlange<T> Schlange<T> → T Schlange<T> → Schlange<T> Schlange<T> → boolean Axiome Für alle t : T bzw. s : Schlange<T> gilt: 1. 2. 3. 4. 5. 6. Leer(NeueSchlange) = wahr Leer(FuegeHinzu(t,s)) = falsch Vorn(NeueSchlange) = Fehler Vorn(FuegeHinzu(t,s)) = Wenn Leer(s), dann t; andernfalls Vorn(s) Entferne(NeueSchlange) = Fehler Entferne(FuegeHinzu(t,s)) = Wenn Leer(s), dann NeueSchlange; andernfalls FuegeHinzu(t,Entferne(s)) Der Abschnitt Typen zeigt die Datentypen der Spezifikation. Der 1. Typ ist der spezifizierte ADT. Von anderen Typen wird angenommen, daß sie an anderer Stelle definiert sind. Der Typ „Schlange<T>“ wird als generischer ADT bezeichnet, da er "übliche Schlangeneigenschaften" bereitstellt. Eigentliche Schlangentypen erhält man durch die Vereinbarung eines Exemplars des ADT, z.B.: Schlange<integer> Der Abschnitt Funktionen zeigt die auf Exemplare des Typs anwendbaren Funktionen: f : D1 , D2 ,...., Dn → D . Einer der Datentypen D1 , D2 ,...., Dn oder D muß der spezifizierte ADT sein. Die Funktionen können eingeteilt werden in: - Konstruktoren (constructor functions) (Der ADT erscheint nur auf der rechten Seite des Pfeils.) Sie liefern neue Elemente (Instanzen) des ADT. - Zugriffsfunktionen (accessor functions) (Der ADT erscheint nur auf der linken Seite des Pfeils.) Sie liefern Eigenschaften von existierenden Elementen des Typs (vgl. Die Funktion: Leer) - Umsetzungsfunktionen (transformer functions) (Der ADT erscheint links und rechts vom Pfeil.) Sie bilden neue Elemente des ADT aus bestehenden Elementen und (möglicherweise) anderen Argumenten (vgl. FuegeHinzu, Entferne). Der Abschnitt Axiome beschreibt die dynamischen Eigenschaften des ADT. Aufgabe: Entwickle die „algebraische Spezifikation“ des ADT Stapel ADT Stapel<T>, integer, boolean 1. Funktionen (Operationen, Protokoll) NeuerStapel PUSH POP → Stapel<T> : T,Stapel<T> → Stapel<T> : Stapel<T> → Stapel<T> 28 Algorithmen und Datenstrukturen Top Stapeltiefe Leer : 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. Der direkte Weg zur Deklaration von ADT im Rahmen der objektorientierten Programmierung ist noch nicht weit berbreitet. 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. 29 Algorithmen und Datenstrukturen 1.2.3.4 Die objektorientierte Modellierung abstrakter Datentypen Die Spezifikation abstrakter Datentypen Im Mittelpunkt dieser Methode steht die Definition von Struktur und Wertebereich der Daten bzw. eine Sammlung von Operationen mit Zugriff auf die Daten. Jede Aufgabe aus der Datenverarbeitung läßt sich auf ein solches Schema (Datenabstraktion) zurückführen. Zur Beschreibung des ADT dient das folgende Format: ADT Name Daten Beschreibung der Datenstruktur Operationen Konstruktor Intialisierungswerte: Daten zur Initialisierung des Objekts Verarbeitung: Initialisierung des Objekts Operation1 Eingabe: Daten der Anwendung dieser Methode Vorbedingung: Notwendiger Zustand des Systems vor Ausführung einer Operation Verarbeitung: Aktionen, die an den Daten ausgeführt werden Ausgabe: Daten (Rückgabewerte) an die Anwendung dieser Methode Nachbedingung: Zustand des Systems nach Ausführung der Operation Operation2 ......... Operationn ......... Bsp.: Anwendung dieser Vorlage zur Beschreibung des ADT Stapel ADT Stapel Daten Eine Liste von Datenelementen mit einer Position „top“, sie auf den Anfang des Stapels verweist. Operationen Konstruktor: Initialisierungswerte: keine Verarbeitung: Initialisiere „top“. Push Eingabe: Ein Datenelement zur Aufnahme in den Stapel Vorbedingung: keine Verarbeitung: Speichere das Datenelement am Anfang („top“) des Stapel Ausgabe: keine Nachbedingung: Der Stapel hat ein neues Datenelement an der Spitze („top“). Pop Eingabe: keine Vorbedingung: Der Stapel ist nicht leer Verarbeitung: Das Element an der Spitze („top“) wird entfernt. Ausgabe: keine Peek bzw. Top Eingabe: keine 30 Algorithmen und Datenstrukturen Vorbedingung: Stapel ist nicht leer Verarbeitung: Bestimme den Wert des Datenelements an der Spitze („top“ des Stapel. Ausgabe: Rückgabe des Datenwerts, der an der Spitze („top“) des Stapel steht. Nachbedingung: Der Stapel bleibt unverändert. Leer Eingabe: keine Vorbedingung: keine Verarbeitung: Prüfe, ob der Stapel leer ist. Ausgabe: Gib TRUE zurueck, falls der Stapel leer ist; andernfalls FALSE. Nachbedingung: keine bereinigeStapel Eingabe: keine Vorbedingung: keine Verarbeitung: Löscht alle Elemente im Stapel und setzt die Spitze („top“) des Stapels zurück. Ausgabe: keine Klassendiagramme der Unified Modelling Language Visualisierung und Spezifizierung objektorientierter Softwaresysteme erfolgt mit der Unified Modelling Language (UML). Zur Beschreibung abstrakter Dytentypen dient das wichtigste Diagramm der UML: Das Klassendiagramm. Elemente und Darstellung des Klassendiagramms Das Klassendiagramm beschreibt die statische Struktur der Objekte in einem System sowie ihre Beziehungen untereinander. Die Klasse ist das zentrale Element. Klassen werden durch Rechtecke dargestellt, die entweder den Namen der Klasse tragen oder zusätzlich auch Attribute und Operationen. Klassenname, Attribute, Operationen (Methoden) sind jeweils durch eine horizontale Linie getrennt. Klassennamen beginnen mit Großbuchstaben und sind Substantive im Singular. Ein strenge visuelle Unterscheidung zwischen Klassen und Objekten entfällt in der UML. Objekte werden von den Klassen dadurch unterschieden, daß ihre Bezeichnung unterstrichen ist. Haufig wird auch dem Bezeichner eines Objekts ein Doppelpunkt vorangestellt.. Auch können Klassen und Objekte zusammen im Klassendiagramm auftreten. Klasse Objekt Wenn man die Objekt-Klassen-Beziehung (Exemplarbeziehung, Instanzbeziehung) darstellen möchte, wird zwischen einem Objekt und seiner Klasse ein gestrichelter Pfeil in Richtung Klasse gezeichnet: Klasse Objekt Defintion einer Klasse Die Definition einer Klasse umfaßt die „bedeutsamen“ Eigenschaften. Das sind: 31 Algorithmen und Datenstrukturen - 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) 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 32 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 Assoziationen repräsentieren Beziehungen zwischen Instanzen von Klassen. Mögliche Assoziationen sind: - einfache (benannte) Assoziationen - Assoziation mit angefügten Attributen oder Klassen - Qualifzierte Assoziationen - Aggregationen - Assoziationen zwischen drei oder mehr Elementen - Navigationsassoziationen - Vererbung Attribute werden von Assoziationen unterschieden: Assoziation: Beschreibt Beziehungen, bei denen beteiligte Klassen und Objekte von anderen Klassen und Objekten benutzt werden können. Attribut: Beschreibt einen privaten Bestandteil einer Klasse oder eines Objekts, welcher von außen nicht sichtbar bzw. modifizierbar ist. Grafisch wird eine Assoziation als durchgezogene Line wiedergegeben, die gerichtet sein kann, manchmal eine Beschriftung besitzt und oft noch weitere Details wie z.B. Muliplizität (Kardinalität) oder Rollenanmen enthält, z.B.: Arbeitet für 0..1 Arbeitgeber Arbeitnehmer Eine Assoziation kann einen Namen zur Beschreibung der Natur der Beziehung („Arbeitet für“) besitzen. Damit die Bedeutung unzweideutig ist, kann man dem Namen eine Richtung zuweisen: Ein Dreieck zeigt in die Richtung, in der der Name gelesen werden soll. Rollen („Arbeitgeber, Arbeitnehmer) sind Namen für Klassen in einer Relation. Eine Rolle ist die Seite, die die Klasse an einem Ende der Assoziation der Klasse am 33 Algorithmen und Datenstrukturen anderen Ende der Assoziation zukehrt. Die Navigierbarkeit kann durch einen Pfeil in Richtung einer Rolle angezeigt werden. Rolle1 Rolle2 K1 K2 1 0..* Abb.: Binäre Relation R = C1 x C2 Rolle1 K1 K2 Rollen ... Kn Abb.: n-äre Relation K1 x K2 x ... x Kn In vielen Situationen ist es wichtig anzugeben, wie viele Objekte in der Instanz einer Assoziation miteinander zusammenhänen können. Die Frage „Wie viele?“ bezeichnet man als Multiplizität der Rolle einer Assoziation. Gibt man an einem Ende der Assoziation eine Multiplizität an, dann spezifiziert man dadurch: Für jedes Objekt am entgegengesetzten Ende der Assoziation muß die angegebene Anzahl von Objekten vorhanden sein. Ein A ist immer Ein A ist immer Ein A ist mit oder mit einem B mit einem oder keinem mehre-ren B einem B assoassoziiert ziiert assoziiert Ein A ist mit keinem, einem oder mehreren B assoziiert Unified A 1 B A 1..* B A 0..1 B A * B 1:1 1..* 1:1..n 0..* 2..6 0..* * 17 4 n m 0..n:2..6 0..n:0..n 17:4 ? Abb.: Kardinalitäten für Beziehungen Pfeile in Klassendiagrammen zeigen Navigierbarkeit an. So kann in der folgenden Darstellung 34 Algorithmen und Datenstrukturen Wenn die Navigierbarkeit nur in einer Richtung existiert, nennt man die Assoziation eine gerichtete Assoziation (uni-directional association). Eine ungerichtete (bidirektionale) Assoziation enthält Navigierbarkeiten in beiden Richtungen. In UML bedeuten Assoziationen ohne Pfeile, daß die Navigierbarbeit unbekannt oder die Assoziation ungerichtet ist. Ungerichtete Assoziationen enthalten eine zusätzliche Bedingung: Die zu dieser Assoziation zugehörigen zwei Rollen sind zueinander invers. Eine Aggregation ist eine Sonderform der Assoziation. Sie repräsentiert eine (strukturelle) Ganzes/Teil-Beziehung. Zusätzlich zu einfacher Aggregation bietet UML eine stärkere Art der Aggregation, die Komposition genannt wird. Bei der Komposition darf ein Teil-Objekt nur zu genau einem Ganzen gehören. Teil Ganzes Existenzabhängiges Teil Abb.: Aggregation und Komposition Eine Aggregation wird durch eine Raute dargestellt. Die Komposition wird durch eine ausgefüllte Raute dargestellt und beschreibt ein „physikalisches Enthaltensein“. Die Vererbung (Spezialisierung bzw. Generalisierung) stellt eine Verallgemeinerung von Eigenschaften dar. Eine Generalisierung (generalization) ist eine Beziehung zwischen dem Allgemeinen und dem Speziellen, in der Objekte des speziellen Typs (der Subklasse) durch Elemente des allgemeinen Typs (der Oberklassse) ersetzt werden können. Grafisch wird eine Generalisierung als durchgezogene Linle mit einer unausgefüllten, auf die Oberklasse zeigenden Pfeilspitze wiedergegeben, z.B.: Supertyp Subtyp 1 Subtyp 2 35 Algorithmen und Datenstrukturen Bsp.: Vererbungshierarchie und wichtige Methoden der Klasse Applet Panel Applet +init() +start() +paint(g:Graphics) {geerbt} +update(g:Graphics) {geerbt} +repaint() +stop() +destroy() +getParameter(name:String); +getParameterInfo() +getAppletInfo() Schnittstellen und abstrakte Klassen Programmiersprachen benutzen ein einzelnes Konstrukt, die Klasse, die sowohl Schnittstelle als die Implementierung enthält. Bei der Bildung einer Unterklasse wird beides vererbt. Eine reine Schnittstelle (wie bspw. in Java) ist eine Klasse ohne Implementierung und besitzt daher nur Operationsdeklarationen. Schnittstellen werden oft mit Hilfe abstrakter Klassen deklariert. Bei abstrakten Klassen oder Methoden wird der Name des abstrakten Gegenstands in der UML kursiv geschrieben. Ebenso kann man die Bedingung {abstract} benutzen. <<interface>> InputStream DataInput OrderReader {abstract} Abhängigkeit Generalisierung Verfeinerung DataInputStream Abb.: Schnittstellen und abstrakte Klassen: Ein Beispiel aus Java 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. 36 Algorithmen und Datenstrukturen 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. 1.2.3.5 Die Implementierung abstrakter Datentypen C++-Implementierung 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. Java-Implementierung In Java gibt es nur Klassen und Interfaces. 37 Algorithmen und Datenstrukturen 1.3 Sammlungen mit bzw. ohne Ordnungen 1.3.1 Ausgangspunkt: Das Konzept für Sammlungen in Smalltalk Ein Rückblick auf Datentypen prozeduraler Programmiersprachen zeigt: Was hier angeboten wird, reicht bei weitem nicht aus. Komplexe Ordnungsgruppen15 bzw. Sammlungen werden überhaupt nicht angeboten. Vorliegende Implementierungen zeigen nicht die gewünschte Flexibilität. Selbst im Fall des varianten "record"16 muß bereits dem Compiler bekannt sein, welche Möglichkeiten (unterschiedliche Satzteile) es überhaupt gibt. Gefragt ist aber, wie man verschiedenartige, verwandte Definitionen in einer Datenstruktur unterbringt, die auch neue bisher noch nicht verwendete Strukturen berücksichtigen kann. 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 "dictionary17") 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-80 18 ist zu Beginn eine Entscheidung zu treffen, ob die gespeicherten Elemente geordnet sein sollen. Sollen Elemente geordnet sein, dann bedient man sich einer der zahlreichen Unterklassen einer "sequenceableCollection". Im anderen Fall ist zu überlegen, ob der Zugriff über einen Schlüssel erfolgen soll. Das führt auf ein Wörterbuch (Tabelle) bzw. (kein Schlüsselzugriff) auf eine Menge (Set). Die Frage, ob Zugriff auf 15 vgl. 1.2.2.3: lineare Listen, Bäume, Graphen vgl. 1.2.4.2 c) 17 Tabellen, vgl. 1.3.2 18 Smalltalk-80 ist das Ergebnis von Forschungen eines 1970 am Xerox PARC (Palo Alto Research Center) begonnenen Projekts und umfaßt: Multitasking-BS, grafische Benutzeroberfläche, komplexe Entwicklungsumgebung mit Editor, Debugger, Interpreter und Compiler, Fenstertechnik, Dialogboxen, DropDown-Menüs. Aktueller Smalltalk-Dialekt ist VisualWorks, das von der von Xerox gegründeten Firma ParcPlace Systems vertrieben wird und auf allen wichtigen UNIX-Workstations portiert werden kann. Smalltalk/V von Digitalk. Wesentliche Unterschiede zu Smalltalk von ParcPlace Systems ergeben sich bei der grafischen Benutzeroberfläche und höheren Systemklassen. 16 38 Algorithmen und Datenstrukturen die Elemente über einen Schlüssel erfolgen kann, stellt sich auch bei geordneten Sammlungen. Die abstrakte Klasse Collection in Smalltalk 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 Viele bekannte Datenstrukturen stehen bereits in den Klassen zur Verfügung. Durch Unterklassen und Kombination von Klassen lassen sich beliebig andere Datenstrukturen erstellen. Ordnungsprinzipien „Collection“ selbst nimmt keinen Bezug auf irgendein Ordnungsprinzip, nach dem seine Elemente abgelegt sind. Die Subklassen von Collection werden so organisiert, daß sie häufig auftretende Ordnungsprinzipien unterstützen. Das erste Unterscheidungsmerkmal betrifft die Indizierbarkeit der Sammlung (IndexedCollection). Alle anderen (nicht indizierten) Sammlungen teilen sich sich in Bag (mehrfache Einträge sind erlaubt) und Set (mehrfache Einträge sind nicht erlaubt). IndexedCollection wird unterteilt in Sammlungen mit einer festen Anzahl von Elementen (FixedCollection) oder mit variabler Anzahl von Elementen (OrderedCollection, paßt die Größe automatisch dem Bedarf an und ermöglicht den Aufbau üblicher dynamischer Datenstrukturen: Stacks, Fifos, etc.). In der Subklasse SortedCollection kann die Reihenfolge der Elemente durch eine Sortiervorschrift festgelegt werden. Bei nicht indizierten Sammlungen wird nur die Klasse Set weiter spezialisiert. Ihre Subklasse Dictionary" kann auf die Elemente einer Sammlung über Schlüsselwörter zugreifen. Methoden zur Beschreibung des Objektverhalten 39 Algorithmen und Datenstrukturen 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 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. 40 Algorithmen und Datenstrukturen 1.3.2 Kollektion-Klassen Kollektionen Linear Allgemein indexiert DirektZugriff Nichtlinear Sequentieller Zugriff Hierarchische Sammlung Baum GruppenKollektionen Heap Set Graph Dictionary prioritäts- Hash- Liste Stapel Schlange Tabelle gest. Schl. „array“ „record“ „file“ Abb. 1.3-4: Hierarchischer Aufbau der Klasse Kollektion Die Abbildung zeigt unterschiedliche, benutzerdefinierte Datentypen. Gemeinsam ist diesen Klassen nur die Aufnahme und Berechnung der Daten durch ihre Instanzen. Kollektionen können in lineare und nichlineare Kategorien eingeteilt werden. Eine lineare Kollektion enthält eine Liste mit Elementen, die durch ihre Stellung (Position) geordnet sind, Es gibt ein erstes, zweites, drittes Element etc. 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. 41 Algorithmen und Datenstrukturen „array“-Kollektion Daten Eine Kollektion von Objekten desselben (einheitlichen) Typs Operationen Die Daten an jeder Stelle des „array“ können über einen ganzzahligen Index erreicht werden. Ein statisches Feld („array“) enthält eine feste Anzahl von Elementen und ist zur Übersetzungszeit festgelegt. Ein dynamisches Feld benutzt Techniken zur Speicherbeschaffung und kann während der Laufzeit an die Gegebenheiten angepaßt werden. Ein „array“ kann zur Speicherung einer Liste herangezogen werden. Allerdings können Elemente der Liste nur effizient am Ende des „array“ eingefügt werden. Anderenfalls sind für spezielle Einfügeoperationen Verschiebungen der bereits vorliegenden Elemente (ab Einfügeposition) nötig. Eine „array“-Klasse sollte Bereichsgrenzenüberwachung für Indexe und dynamische Erweiterungsmöglichkeiten erhalten. Implementierungen aktueller Programmiersprachen umfassen Array-Klassen mit nützlichen Bearbeitungsmethoden bzw. mit dynamischer Anpassung der Bereichsgrenzen zur Laufzeit. Eine Zeichenkette („character string“) ist ein spezialisierter „array“, dessen Elemente aus Zeichen bestehen: „character string“-Kollektion Daten Eine Zusammenstellung von Zeichen in bekannter Länge Operationen Sie umfassen Bestimmen der Länge der Zeichenkette, Kopieren bzw. Verketten einer Zeichenkette auf eine bzw. mit einer anderen Zeichenkette, Vergleich zweier Zeichenketten (für die Musterverarbeitung), Ein-, Ausgabe von Zeichenketten In einem „array“ kann ein Element über einen Index direkt angesprochen werden. In vielen Anwendungen ist ein spezifisches Datenelement, der Schlüssel (key) für den Zugriff auf einen Datensatz vorgesehen. Behälter, die Schlüssel und übrige Datenelemente zusammen aufnehmen, sind Tabellen. Ein Dictionary ist eine Menge von Elementen, die über einen Schlüssel identifiziert werden. Das Paar aus Schlüsseln und zugeordnetem Wert heißt Assoziation, man spricht auch von „assoziativen Arrays“. Derartige Tabellen ermöglichen den Direktzugriff über Schlüssel so, wie in einem Array der Direktzugriff über den Index erreicht wird, z.B.: Die Klasse Hashtable in Java Der Verbund (record) ist in der Regel eine Zusammenfassung von Datenbehältern unterschiedlichen Typs: „record“-Kollektion Daten 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. 42 Algorithmen und Datenstrukturen 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. 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. Bsp.: Die Klasse Stack in Java „Schlange“-Kollektion Daten Eine Sammlung von Elementen mit Zugriff am Anfang und Ende der Liste. Operationen Ein Element wird am Ende der Liste hinzugefügt und am Ende der Liste entfernt. 43 Algorithmen und Datenstrukturen Eine Schlange ist besonders geeignet zur Verwaltung von „Wartelisten“ und kann zur Simulation von Wartesystemen eingesetzt werden. Eine Schlange kann ihre Elemente nach Prioritäten bzgl. der Verarbeitung geordnet haben (priority queue). Entfernt wird dann zuerst das Element, das die höchste Priorität besitzt. „prioritätsgesteuerte Schlange“-Kollektion Daten Eine Sammlung von Elementen, von denen jedes Element eine Priorität besitzt. Operationen Hinzufügen von Elementen zur Liste. Entfernt wird immer das Element, das die höchste (oder niedrigste) Priorität besitzt. 1.3.2.2 Nichtlineare Kollektionen 1. Hierarchische Sammlung Eine hierarchisch angeordnete Sammlung von Datenbehältern ist gewöhlich ein Baum mit einem Ursprungs- bzw. Ausgangsknoten, der „Wurzel“ genannt wird. Von besonderer Bedeutung ist eine Baumstruktur, in der jeder Baumknoten zwei Zeiger auf nachfolgende Knoten aufnehmen kann. Diese Binärbaum-Struktur kann mit Hilfe einer speziellen angeordneten Folge der Baumknoten zu einem binären Suchbaum erweitert werden. Binäre Suchbäume bilden die Ausgangsbasis für das Speichern großer Datenmengen. „Baum“-Kollektion Daten Eine hierarchisch angeordnete Ansammlung von Knotenelementen, die von einem Wurzelknoten abgeleitet sind. Jeder Knoten hält Zeiger zu Nachfolgeknoten, die wiederum Wurzeln von Teilbäumen sein können. Operationen Die Baumstruktur erlaubt das Hinzufügen und Löschen von Knoten. Obwohl die Baumstruktur nichtlinear ist, ermöglichen Algorithmen zum Ansteuern der Baumknoten den Zugriff auf die in den Knoten gespeicherten Informationen. Ein „heap“ ist eine spezielle Version, in der das kleinste bzw. größte Element den Wurzelknoten besetzt. Operationen zum Löschen entfernen den Wurzelknoten, dabei wird, wie auch beim Einfügen, der Baum reorganisiert. Basis der Heap-Darstellung19 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 19 Vgl. 3.2.2.3 44 Algorithmen und Datenstrukturen [2] [3] wk2 wk3 [4] [5] wk4 [8] [15] wk8 wk5 [9] [10] [11] [12] wk9 wk10 wk11 wk12 [6] [7] wk6 wk7 [13] wk13 [14] wk14 wk15 Abb. 1.3-10: Darstellung eines Feldbaums Liegen die Knoten auf den hier in Klammern angegebenen Positionen, so kann man von jeder Position I mit Pl = 2 * I Pr = 2 * I + 1 Pv = I div 2 auf die Position des linken (Pl) und des rechten (Pr) Nachfolgers und des Vorgängers (Pv) gelangen. Ein binärer Baum kann somit in einem eindimensionalen Feld (array) gespeichert werden, falls folgende Relationen eingehalten werden: X[I] <= X[2*I] X[I] <= X[2*I+1] X[1] = min(X[I] .. X[N]) Anstatt auf das kleinste kann auch auf das größte Element Bezug genommen werden Aufbau des Heap: Ausgangspunkt ist ein „array“ mit bspw. folgenden Daten: X[1] 40 X[2] 10 X[3] 30 X[4] ...... , der folgendermaßen interpretiert wird: X[1] 40 X[2] X[3] 10 30 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“: 45 Algorithmen und Datenstrukturen X[1] 10 X[2] X[3] 40 30 Abb. 1.3-12: Falls ein neues Element eingefügt wird, dann wird nach dem Ordnen gemäß der „heap“-Bedingung erreicht: X[1] 10 X[2] X[3] 40 30 X[4] 15 X[1] 10 X[2] X[3] 15 30 X[4] 40 Abb.1.3-13: Das Einbringen eines Elements in einen Heap Beim Löschen wird das Wurzelelement an der 1. Position entfernt. Das letzte Element im „heap“ wird dazu benutzt, das Vakuum zu füllen. Anschließend wird reorganisiert: 46 Algorithmen und Datenstrukturen X[1] 10 X[2] X[3] 15 30 X[4] 40 X[1] 40 X[2] X[3] 15 30 X[1] 15 X[2] X[3] 40 30 Abb. 1.3-14: Das Löschen eines Elements im Heap Die Klasse BinaryHeap in Java20 // BinaryHeap class // // 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 20 vgl. pr13228 47 Algorithmen und Datenstrukturen 2. Gruppenkollektionen Menge (Set) Eine Gruppe umfaßt nichtlineare Kollektionen ohne jegliche Ordnungsbeziehung. Eine Menge (set) einheitlicher Elemente ist. bspw. eine Gruppe. Operationen auf die Kollektion „Set“ umfassen Vereinigung, Differenz und Schnittmengenbildung. „Set“-Kollektion Daten Eine ungeordnete Ansammlung von Objekten ohne Ordnung Operationen Die binäre Operationen über Mitgliedschaft, Vereinigung, Schnittmenge und Differenz bearbeiten die Strukturart „Set“. Weiter Operationen testen auf Teilmengenbeziehungen. Graphen 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. 48