Lineare dynamische Datenstrukturen Steffen Reith [email protected] Fachhochschule Wiesbaden 21. Mai 2007 Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 1 / 28 Einleitung Bis jetzt: Untersuchung von (einigen) Algorithmen und deren Analyse Dazu wurden nur Variablen und Arrays von fester Größe benutzt Vorteil: Einfache Handhabung und Deklaration Nachteile: Unflexibel, da eine feste Grenze existiert und evtl. sehr viel Speicher unnötig verbraucht wird Um diese Nachteile zu beseitigen benötigen wir Datenstrukturen, die dynamisch wachsen (und) schrumpfen können. Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 2 / 28 Datenstrukturen und Wiederverwertbarkeit Ziel: Unsere Programme sollen wiederverwertbar sein (Verbesserung der Softwarequalität) Programmierer sollen unsere Datenstrukturen leicht benutzen können Die Implementierung soll jederzeit änderbar sein (ohne dass der Benutzer diese bemerkt) Abstraktion von unnötige Details (z.B. Speicherlayout) Dieses Verbergen von Details von Datenstrukturen ist als Geheimnisprinzip bekannt (Programming by contract). Solche Datenstrukturen sind auch als abstrakte Datentypen (ADT) bekannt. Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 3 / 28 Abstrakte Datentypen Konkrete Datentypen: Werden direkt aus den Basisdatentypen bzw. C++-Klassen konstruiert (wie schon bekannt). Abstrakte Datentypen: Bestehen aus einer Spezifikation der Schnittstelle nach außen. Verfügbare Operationen und deren Semantik wird beschrieben. Abstrakte Datentypen entsprechen Softwaremodulen (, dem Prinzip des Klassenkonzepts in OO-Sprachen). Folgende Prinzipien kommen zum Einsatz: Kapselung: Ein ADT darf nur über seine Schnittstellen benutzt werden Geheimnisprinzip: Die interne Realisierung eines ADT ist verborgen Das Geheimnisprinzip kommt schon bei den Basisdatentypen zur Anwendung. Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 4 / 28 Die Datenstruktur Stack Eine einfache Datenstruktur ist der Kellerspeicher (engl. Stack). Ein Stack ist die Verwirklichung des LIFO-Prinzips (LIFO Last-In-First-Out-Speicher): Beim Auslesen eines Elements kann nur auf das zuletzt gespeicherte Element zugegriffen werden Die nächste Ausleseoperation liefert das vorletzte Element, etc. Anderer Name für Stack: Stapel Die Schnittstelle eines Stapels besteht aus vier Operationen: void push(Obj data); (Daten auf den Stack legen) Obj pop(); (Oberstes Element entfernen) Obj top(); (Oberstes Element auslesen und nicht entfernen ) boolean is empty(); (Test ob Stack leer) Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 5 / 28 Ein Beispiel Gegeben sei ein leerer Stack. Nach push(7); push(12); push(3); push(17); sieht der Stack wie folgt aus: 7 12 3 17 Und drei pop-Operationen ergeben 17, 3 und 12 (in dieser Reihenfolge). Übrig bleibt: 7 Mit Feldern können wir sicherlich leicht einen Stapel fester Größe implementieren. Aber: Wie machen wir das dynamisch? Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 6 / 28 Interne Darstellung der Daten Wir benötigen eine Hilfsdatenstruktur: Nutz− daten struct Node { T data; // Nutzdaten struct Node *next; // Referenz } in C/C++ next Referenz (Zeiger) auf nächstes Objekt Objekte dieses Hilfsdatentyps können leicht mit new erzeugt werden. Deutet die Referenz next auf kein anderes Node-Objekt, so wird der Wert null verwendet. In C++ wird der struct evtl. durch eine Klasse incl. Zugriffsmethoden implementiert. Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 7 / 28 Ablauf der Push-Operation Schritt 1 NULL 17 3 12 7 17 3 12 7 head ptr Schritt 2 NULL head 5 ptr Schritt 3 NULL 5 ptr 17 3 12 7 head Schritt 4 NULL 5 17 3 12 7 head Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 8 / 28 Einige Hinweise zur C++-Implementierung template <class T> class Stack { private: // Node<T> ist eigene Klasse (noch implementieren) Node<T> *head; // Zeiger auf top-Objekt unsigned long numElements; // Anzahl der Elemente public: Stack(); // Konstruktor ∼Stack(); // Destruktor void push(T &elem);// ’elem’ auf Stack ablegen T pop();// ein Element vom Stack holen bool isEmpty(); // Test ob Stack leer ist }; Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 9 / 28 Generische Datentypen Die Programmlogik eines int-Stacks unterscheidet sich nicht von der Programmlogik eines Stacks von Kundendatensätzen. Ziel: Die Implementierung sollte unabhängig vom Nutzdatentyp sein. Dazu dienen in C so genannte templates“ (vgl. List<Integer> ” myIntList). In JAVA verwendet man Generics“. ” Solche parametrierbare abstrakte Datentypen sind als generische ” Datentypen“ bekannt. Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 10 / 28 Laufzeiten & Anwendungen Für die Zeitkomplexität von Operationen auf einen Stapel ergibt sich: Operation is empty top push pop Zeitkomplexität O(1) O(1) O(1) O(1) Grund: Für jede Operation sind nur endlich viele elementare Anweisungen auszuführen. Anwendungen von Stapelspeichern: Speicherung lokaler Variablen von Unterprogrammen Speichern von Rücksprungadressen Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 11 / 28 Warteschlangen Eine Warteschlange (engl. Queue) ist die Verwirklichung des FIFO-Prinzips (FIFO - First In First Out) Neue Elemente werden hinten an die Schlange angefügt Alte Elemente werden vorne aus der Warteschlange entfernt Die Schnittstelle einer Warteschlange besteht aus den Operationen: append(Obj data); (Hänge ein Element hinten an) Obj get(); (Entferne ein Element am Ende) boolean is empty(); (Test ob Queue leer) Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 12 / 28 Ein Beispiel Gegeben sei eine leere Warteschlange. Nach append(7); append(12); append(3); append(17); erhalten wir: 7 3 12 17 head tail Und dreimal get ergibt dann 7, 12 und 3. Zurück bleibt die Warteschlange: 17 head Steffen Reith tail Lineare dynamische Datenstrukturen 21. Mai 2007 13 / 28 Ablauf der Append-Operation (I) tail Schritt 1 NULL 7 12 3 head ptr tail Schritt 2 NULL 7 12 3 17 ptr head tail Schritt 3 NULL 7 12 3 head Steffen Reith 17 ptr Lineare dynamische Datenstrukturen NULL 21. Mai 2007 14 / 28 Ablauf der Append-Operation (II) tail Schritt 4 7 12 3 17 head NULL ptr tail Schritt 5 7 12 3 17 head Steffen Reith NULL Lineare dynamische Datenstrukturen 21. Mai 2007 15 / 28 Ablauf der Get-Operation Die Referenz head kopieren Dann die im Node enthaltenen Informationen kopieren Die Referenz head auf den Nachfolger des head-Knotens setzen Überflüssiges Element mit delete freigeben (in JAVA unnötig) Die Verkettungsrichtung ist wichtig! Neue Elemente werden am Schwanz“ eingekettet ” Alte Elemente werden am Kopf“ ausgekettet ” Annahme: Verkettungsrichtung wäre umgekehrt append ist einfach (Schwanz neu setzen) get wäre schwierig, da keine Referenz auf das Vorgängerelement existiert (⇒ komplette Queue durchlaufen) Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 16 / 28 Laufzeiten & Anwendungen Für die Zeitkomplexität von Operationen einer Warteschlange ergibt sich: Operation append get is empty Zeitkomplexität O(1) O(1) O(1) Grund: Für jede Operation sind nur endlich viele elementare Anweisungen auszuführen. Anwendungen von Warteschlangen: Warteschlangen von Prozessen in Betriebssystemen (vgl. kfifo.c im Linux Kern) Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 17 / 28 Lineare Listen Bis jetzt: Einfügen und Entfernen von Elementen nur am Anfang oder Ende der Datenstruktur. Ziel: Daten sollen geordnet (also sortiert) verwaltet werden und die Datenstruktur soll wieder dynamisch sein (⇒ Ein- und Ausfügen an beliebigen Stellen der Datenstruktur) Die Schnittstelle einer linearen Liste besteht aus den Operationen: insert(Obj data); (Element an der richtigen Stelle einfügen) Obj remove(Obj data); (Entferne das Element) boolean is empty(); (Test ob Liste leer) Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 18 / 28 Die Remove-Operation Angenommen wir haben die folgende sortierte Liste gegeben: 3 7 12 17 head NULL Es soll remove(12) simuliert werden: 1 Setze Vorgänger“ auf head. ” 2 Rücke den Vorgänger“ so lange vor, bis er auf ein Objekt deutet, ” dessen next-Referenz auf das Objekt deutet, das die zu entfernenden Daten enthält. 3 Sichere die Referenz auf den direkten Nachfolger des Vorgängerobjekts“ in die temporäre Referenz tmpRef. ” 4 Setze die next-Referenz des Vorgängersobjekts“ auf die ” next-Referenz von tmpRef. 5 Lösche das durch tmpPtr referenzierte Objekt mit delete (in JAVA nicht notwendig). Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 19 / 28 Ablauf der Remove-Operation (I) Schritt 1 3 7 12 17 head NULL vorgnger Schritt 2 3 7 12 17 head NULL vorgnger Schritt 3 3 7 12 head NULL vorgnger Steffen Reith 17 tmp Lineare dynamische Datenstrukturen 21. Mai 2007 20 / 28 Ablauf der Remove-Operation (II) Schritt 4 3 7 12 17 head NULL tmp Schritt 5 3 7 17 head NULL Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 21 / 28 Die Insert-Operation Es soll insert(9) simuliert werden: 1 Neuen Node erzeugen (mit new) und Daten in diesem speichern. 2 Vorgänger“ auf den Anfang der Liste setzen. ” Vorgänger“ solange vorrücken, bis er auf das Objekt deutet, nach ” dem das neue Objekt einsortiert werden soll. 3 4 next-Referenz von tmpRef auf den Nachfolger des Vorgängerobjekts zeigen lassen. 5 next-Referenz des Vorgängerobjekts auf das im ersten Schritt erzeugte Objekt deuten lassen vorgaenger.next = tmpRef;. Wir erhalten: 3 7 9 head Steffen Reith 17 NULL Lineare dynamische Datenstrukturen 21. Mai 2007 22 / 28 Ablauf der Insert-Operation (I) Schritt 1 3 7 17 9 head NULL tmpRef Schritt 2 3 7 17 9 head NULL vorgaenger tmpRef Schritt 3 3 7 17 9 head NULL tmpRef vorgaenger Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 23 / 28 21. Mai 2007 24 / 28 Ablauf der Insert-Operation (II) Schritt 4 3 7 17 head NULL vorgaenger tmpRef 9 3 7 Schritt 5 head 17 NULL vorgaenger tmpRef Steffen Reith 9 Lineare dynamische Datenstrukturen Einige Bemerkungen Die oben beschriebenen Operationen funktionieren nur auf dem inneren“ Teil der Liste. ” Sind Kopf oder Schwanz betroffen, so müssen diese als Sonderfälle berücksichtigt werden. (⇒ fehleranfällig & arbeitsintensiv) Idee: Füge ein kleinstes“ und ein größtes“ Dummyelement ein (mit ” ” ungültigen Daten). gültiger Datenbereich 1 .. 1000 −1 3 7 12 17 head 9999 NULL Sentinel Diese Dummyelemente nennt man auch Sentinels. (engl. Wächter) Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 25 / 28 Laufzeiten Für die Zeitkomplexität der Operationen auf Listen mit n Elementen ergibt sich: Operation insert remove is empty Zeitkomplexität O(n) O(n) O(1) Anwendungen von Listen: Verwaltung von sortierten Dateien Implementierungsgrundlage für Stacks und Queues Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 26 / 28 Weitere lineare Datenstrukturen Man kann eine lineare Liste auch in eine Ringliste umwandeln: 3 7 12 17 head Anwendungen: Zuteilung von Zeitscheiben in einem Betriebssystem Sende- und Empfangsbuffer bei der Datenkommunikation Nachteil linearer Listen: man kann sich nur in eine Richtung bewegen Lösung: Verkettungen in zwei Richtungen NULL NULL head Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 27 / 28 Mischen von statischen und dynamischen Ansätzen Aufgabe: Speichere und verwalte große Datensätze (z.B. vollständige Personaldatensätze) Lösung 1: Arbeiten mit (dynamischen) Arrays: Vorteil: schneller Zugriff auf die einzelnen Komponenten Nachteil: z.B. sind Sortieroperationen sehr aufwändig (kopieren) Lösung 2: Mischansatz: Den eigentlichen Datensatz in einem dynamisch erzeugten Objekt ablegen Einen Schlüssel (z.B. Personalnummer) mit einer Referenz auf das Objekt in einem Array ablegen. Vorteil: Schnelle Zugriffe auf die Daten und effizientes Sortieren Nachteil: Kompliziertere Implementierung Steffen Reith Lineare dynamische Datenstrukturen 21. Mai 2007 28 / 28