7. Datenstrukturen und Speicherstrukturen Inhalt: Datenstrukturen – Definition und Arten Dynamische Speichertechnik Dynamische lineare Datenstrukturen: Umsetzung mit Techniken der dynamischen Speicherallokation und Zeigertechnik Nichtlineare dynamische Datenstrukturen Die Umsetzung einfacher Datenstrukturen im Speicher des Rechners erfolgt mit bislang bekannten Mitteln: Strukturen, Felder und mehrdimensionale Felder (Matrizen) Peter Sobe 1 Datenstrukturen - Definition Eine Datenstruktur beschreibt die Art der Organisation der Daten im Speicher des Rechners während der Verarbeitung auf Speichermedien (Festplatte, Band, CD, DVD, FlashRAM u.ä.) Die Daten werden dabei in Elemente aufgeteilt. Die einzelnen Elemente werden in regelmäßige Relationen gesetzt, z.B. eine Vorgänger- und Nachfolger-Relation. Peter Sobe 2 Datenstrukturen - Definition Verschiedene Quellen: www.computerlexikon.com Als Datenstruktur bezeichnet man das Konstrukt in einem Programm (bzw. im Speicher), das Daten auf eine gewisse Weise speichert. Durch den speziellen Aufbau einer Datenstruktur versucht man gewünschte Funktionen besonders effizient zu implementieren, wobei man zumeist entweder auf geringen Speicherbedarf oder hohe Geschwindigkeit hin optimiert. Durch eine falsche Datenstruktur für ein Problem, kann ein Programm um ein Vielfaches länger für die Lösung benötigen als eines, das auf eine für das Problem besser geeignete Datenstruktur zurückgreift. Peter Sobe 3 Datenstrukturen - Definition Verschiedene Quellen: wikipedia In der Informatik ist eine Datenstruktur ein mathematisches Objekt zur Speicherung von Daten. Es handelt sich um eine Struktur, weil die Daten in einer bestimmten Art und Weise angeordnet und verknüpft werden, um den Zugriff auf sie und ihre Verwaltung geeignet zu ermöglichen. Datenstrukturen sind daher nicht nur durch die enthaltenen Daten charakterisiert, sondern vor allem durch die Operationen auf diesen Daten, die Zugriff und Verwaltung realisieren. Die Definition von Datenstrukturen erfolgt durch die Angabe einer konkreten Spezifikation zur Datenhaltung und der dazu nötigen Operationen. Diese konkrete Spezifikation legt das allgemeine Verhalten der Operationen fest und abstrahiert damit von der konkreten Implementierung der Datenstruktur. Peter Sobe 4 Datenstrukturen - Definition Verschiedene Quellen: Ralf Hartmut Güting und Stefan Dieker: Datenstrukturen und Algorithmen, 3. Auflage, Teubner-Verlag, Stuttgart 2004, Reihe Leitfäden der Informatik Unter einer Datenstruktur verstehen wir die Implementierung eines Datentyps auf algorithmischer Ebene. Das heißt, für die Objekte der Trägermengen der Algebra wird eine Repräsentation festgelegt und die Operationen werden durch Algorithmen realisiert. Die Beschreibung einer Datenstruktur kann andere Datentypen benutzen, für die zugehörige Datenstrukturen bereits existieren oder noch zu entwerfen sind (“schrittweise Verfeinerung”). So entsteht eine Hierarchie von Datentypen bzw. Datenstrukturen. Ein Datentyp ist vollständig implementiert, wenn alle benutzten Datentypen implementiert sind. Letztlich müssen sich alle Implementierungen auf die elementaren Typen (Basisdatentypen, Felder, Strukturen/Records) und Mechanismen (Zeiger, Adressarithmetik, Referenzen) einer Programmiersprache abstützen. Peter Sobe 5 Datenstrukturen - Definition Begriffe und Zusammenhänge … aus Ralf Hartmut Güting und Stefan Dieker: Datenstrukturen und Algorithmen, 3. Auflage, Teubner-Verlag, Stuttgart 2004, Reihe Leitfäden der Informatik Gegenstand Ebene Mathematik Algorithmierung Funktion Spezifikation Implementierung Implementierung Algorithmus Spezifikation Implementierung Programmierung Peter Sobe Algebra Spezifikation Programm, Unterprogramm u.ä. Datenstruktur Spezifikation Implementierung Typ, Struktur, Modul mit eigenen Daten und Zugriffsfunktionen 6 Datenstrukturen Für viele Anwendungen ist die Wahl einer geeigneten Datenstruktur eine wesentliche Entscheidung Frage: Wie organisiert man die Daten im Speicher, damit sie günstig verarbeitet werden können? Günstig kann heißen: Programmcode zur Verarbeitung einfach und kurz wenig Anweisungen (schneller, weniger Prozessorbelastung) inhaltlich verwandte Elemente stehen nah beieinander (schneller) Geringer Speicherbedarf Beispiele: • Lineares Feld mit sortierten Elementen, z.B. für Telefonbucheinträge • Baumstruktur mit Verweisen auf Vater- und Mutterelemente zur Nachbildung von Stammbäumen • Netzstruktur, z.B. mit Struktur einer elektrischen Schaltung Peter Sobe 7 Datenstrukturen - Arten Lineare Strukturen Felder – eindimensional indizierte Strukturen Listen, Warteschlangen, Stapel Zirkulare Strukturen einfache Ringe vermaschte Ringe Mehrdimensional indizierte Strukturen Bäume Allgemeiner Baum Binärbaum ( Darstellung als Feld möglich) Netze (allgemeine Graphen) Hash-Strukturen Peter Sobe 8 Dynamische Speichertechnik (1) Unter dynamischer Speichertechnik versteht man die Bildung neuer Speicherplätze auf der Basis von Standarddatentypen und deklarierten Strukturen zur Laufzeit. Es wird Speicherplatz zur Laufzeit bereitgestellt und darauf ein neues Element einer Datenstruktur initialisiert. Dieses neue Element wird mit bereits vorhandenen Elementen in der Datenstruktur in Beziehung gesetzt. Ebenso können auch zur Laufzeit Elemente aus der Datenstruktur herausgelöst werden und deren Speicherplatz wieder freigegeben werden. Im Gegensatz werden die bisher benutzen Variablen und Felder als statische Datenstrukturen angesehen. Die Sichtbarkeit und der belegte Speicherplatz wird durch ihre Deklaration im globalen Kontext des Programms oder innerhalb Funktionen bestimmt. Peter Sobe 9 Dynamische Speichertechnik (2) Zur Realisierung solcher dynamischer Strukturen werden zwei Technologien benötigt: 1. Die dynamische Speicherallokation und –freigabe 2. Die Verwendung der Zeigertechnik zum dynamischen Herstellen und Lösen von Verbindungen von Elementen innerhalb der Datenstruktur Die vom Programmierer geschaffene Organisation der Daten (also wo welches Element im Speicher steht und wie es mit den anderen Elementen in Beziehung gesetzt wird) wird durch Datenstrukturen beschrieben. Peter Sobe 10 Dynamische Speichertechnik (3) Die dynamische Speicherallokation und –freigabe wird in C mit den Funktionen void *malloc(size_t size); // gibt die Anfangsadr. des // allokierten Bereichs zurück void free(void *addresse); durchgeführt, die in <alloc.h> zu finden sind. Es werden size Bytes allokiert und die Adresse dieses Speicherbereiches zurückgegeben. Ist der Rückkehrwert NULL, ist nicht genügend Speicher vorhanden. Ist die Größe in Bytes bekannt, kann diese direkt als Argument übergeben werden, sonst empfiehlt sich die Verwendung von sizeof(x), welche die notwendige Größe in Bytes für das Element x ermittelt. Bei der Speicherfreigabe mit free ist nur die Adresse des Speicherbereichs anzugeben. Peter Sobe 11 Dynamische lineare Datenstrukturen Dynamisches Feld: eine eindimensionale Datenstruktur Elemente werden durch Zahlenindex angesprochen Feld kann wachsen und schrumpfen – durch dynamische Speichertechnik Lineare Liste: eine eindimensionale Datenstruktur Liste kann wachsen und schrumpfen Effizentes Einfügen neuer Elemente und Löschen (kein Zahlenindex zum Zugriff) Weitere lineare Datenstrukturen: Warteschlange, Stapel (hier nicht ausgeführt) Peter Sobe 12 Dynamisches Feld (1) Bei manchen Anwendungen ist die Anzahl der zu verarbeitenden Elemente nicht bekannt und kann je nach Eingabewerten stark variieren. Felder mit einer maximalen Anzahl von Elementen sind dann ungünstig. Lösung: Ein dynamisches Feld, das sich mit Mitteln der dynamischen Speicherverwaltung (malloc, realloc, free) der benötigten Anzahl von Elementen anpasst. Zustand, nach Einfügen von 3 weiteren Elementen Startzustand, mit 3 Elementen als Reserve Elemente Zustand, nach Einfügen von 2 Elementen Elemente Elemente Zustand, nach Austragen von 4 Elementen Elemente Peter Sobe 13 Dynamisches Feld (2) Konstanten und Variablen zur Verwaltung: #define N_START 3 #define N_DELTA 5 unsigned int n_allocated=0, n_elements=0; Anfang: Allokieren von N_START Elementen Setzen von n_allocated = N_START feld = (elem_typ*) malloc(sizeof(elem_typ)* N_START); if (feld==NULL) { /* Fehlerbehandlung */} n_allocated = N_START; Peter Sobe 14 Dynamisches Feld (3) Einfügen eines neuen Elements am Ende: Prüfen, ob ausreichend Speicher allokiert ist ggf. dynamisches Vergrößern des Feldes und Anpassen der Variable n_allocated Einfügen des Elements in feld[n_elements] Erhöhen von n_elements um 1 if (n_elements+1 > n_allocated) { feld = realloc(feld, sizeof(element_typ) * (n_allocated + N_DELTA)); if (feld==NULL) { /* Fehlerbehandlung */ } n_allocated = n_allocated + N_DELTA; } feld[n_elements] = neues_element; n_elements = n_elements+1; Peter Sobe 15 Dynamisches Feld (4) Ausgliedern eines Elements an Position p (0≤p<n_elements): Verschieben der Feldelemente ab Position p+1 um eine Stelle nach links (kleinerer Index) Verringern von n_elements um 1 Prüfen, ob n_elements < n_acclocated – N_DELTA, wenn ja, dann Verkleinern des Feldes mittels realloc() for (i=p+1;i<n_elements; i++) feld[i-1] = feld[i]; n_elements = n_elements – 1; if (n_elements < n_allocated – N_DELTA) { feld = realloc(feld, sizeof(element_typ) * (n_allocated - N_DELTA)); n_allocated = n_allocated – N_DELTA; } Peter Sobe 16 Zeigertechnik bei linearen Listen Die Verwendung der Zeigertechnik zum dynamischen Herstellen und Lösen von Verbindungen von Objekten und der Datenstruktur erfordert in C den Einsatz des Verbundtyps struct{...}, da die Zeiger in das zu verbindende Objekt einbezogen werden müssen. Da das Objekt selbst eine Struktur ist, muss demzufolge der Zeiger auf die nächste Struktur auch vom gleichen Strukturtyp sein, z.B. struct list_element { float f; // weitere Elemente struct list_element *next; }; // Zeiger auf Nachfolger-Element Peter Sobe 17 Lineare Listen Beispiel: Objekt 1 Objekt 3 Objekt 2 Listenelement Durch einen Verbund (struct) müssen eigentliches Objekt und als Zeiger vereint werden. struct listelem { float t; char position[STRLEN]; Objekt struct listelem *next; }; typedef struct listelem list_elem_t; … list_elem_t *anker = NULL; Peter Sobe 18 Lineare Liste - Definition Eine Liste ist eine verkettete Folge von Elementen (Objekten), die aus Standarddatentypen zusammengesetzt sind und für die gilt: 1. Es gibt genau ein Listenelement, das keinen Vorgänger hat und Listenanfang heißt. Auf dieses Element zeigt der Listenanker. 2. Es gibt genau ein Listenelement, das keinen Nachfolger hat und Listenende heißt. 3. Die übrigen Listenelemente haben genau einen Vorgänger und genau einen Nachfolger. 4. Alle Listenelemente sind vom Listenanker aus durch Nachfolgerbildung in endlich vielen Schritten erreichbar. Peter Sobe 19 Listen Objekt 1 Objekt 3 Objekt 2 NULLZeiger Objekt 1 ist der Listenanfang und Objekt 3 das Listenende. Die Objekte enthalten die Nutzdaten, die wiederum aus mehreren Elementen bestehen können. Als zusätzliches charakteristisches Element tritt ein Zeiger hinzu, der die Nachfolgerelation darstellt. Das Listenende enthält den NULL-Zeiger. Die Anzahl der Objekte wird als Länge der Liste bezeichnet. Da die Nachfolgerelation eine Ordnungsrelation induziert, spricht man von linearen dynamischen Datenstrukturen. Hier: einfach verkettete Liste – es gibt auch doppelt verkettete Listen (vorwärts und rückwärts verkettet) Peter Sobe 20 Operationen mit Listen Als typische Operationen mit Listen gelten: 1. Das Erzeugen eines Listenelementes (create) 2. Das Löschen eines Listenelementes (obfree) 3. Das Einketten eines Listenelementes an eine festzulegende Position (insert) 4. Das Anhängen eines Listenelementes als neues letztes Element (append) 5. Das Ausketten eines bestimmten Listenelementes (dequeue) 6. Das Suchen eines Listenelementes (find) 7. Das Traversieren einer Liste (traverse) Peter Sobe 21 Erzeugen eines Listenelementes (create) list_elem_t *create(list_elem_t x) { list_elem_t *neu; neu= (list_elem_t*) malloc(sizeof x); //Reservieren von Speicherplatz *neu=x; //Belegung des Speichers return neu; } Der Funktion wird ein Objekt x übergeben, das die Werte für das neu zu erzeugende Heap-Objekt enthält. Der Rückkehrwert von create() ist die Heapadresse neu. Peter Sobe 22 Einketten eines Listenelementes (insert) void insert(list_elem_t *pos, list_elem_t *neu) { // pos zeigt auf das Listenelement, hinter dem das // Listenelement neu eingekettet werden soll neu->next=pos->next; pos->next = neu; } pos->n pos o o neu o neu->n = pos->n Voraussetzung: Die Liste hat bereits Elemente. 23 Peter Sobe Ausketten eines Listenelementes (dequeue) void dequeue(list_elem_t *pos) { // pos zeigt auf Element vor dem auszukettenden Element list_elem_t *h; h=pos->next; pos->next=(pos->next)->next; free(h); } pos->n = (pos->n)->n pos o o o pos->n free(h) Gesonderte Behandlung erforderlich, wenn erstes Element auszuketten ist. Peter Sobe 24 Suchen eines Listenelementes (find) list_elem_t *find(int krit, list_elem_t *anker) { list_elem_t *a; a=anker; while (a!=NULL) { if ((a->i)==krit) return a; //Objekt a gefunden a=a->next; } return NULL; //nichts gefunden } o a krit ? o a=a->n krit ? Peter Sobe 25 Nichtlineare dynamische Datenstrukturen Eine nichtlinear verkettete Menge von Elementen (Objekten), die aus Standarddatentypen zusammengesetzt sind, nennt man nichtlineare dynamische Datenstruktur. Solche Strukturen sind vor allem: Bäume (Binärbäume, allg. Bäume) typisch für diese Strukturen ist die Anordnung als Hierarchie, d.h. zwischen den Elementen besteht noch eine Halbordnungsrelation Graphen (kreisfreie Graphen, allgemeine Graphen) bei kreisfreien Graphen liegt noch eine Heterarchie als Struktur vor, während bei allgemeinen Graphen nur netzartige Strukturen als allgemeinster Fall auftreten. Peter Sobe 26 Nichtlineare dynamische Datenstrukturen Viele Anwendungsalgorithmen basieren auf solchen nichtlinearen Strukturen. Bei netzartigen Graphstrukturen müssen diese Algorithmen mit erschöpfendem Durchsuchen aller Möglichkeiten arbeiten. Das erfordert programmseitig spezielle Hilfen (globale stacks). Bei Hierarchien (Bäume) reicht die einfache Rekursivität aus. Bei vielen Sonderfällen in Hierarchien nutzt man die s.g. Teile-undHerrsche-Algorithmen. Diese Algorithmen teilen den aktuellen Bearbeitungsraum in Teilbearbeitungsräume und wenden dann den gleichen Algorithmus rekursiv auf die Teile an (herrschen), solange bis eine weitere Teilung nicht mehr sinnvoll ist. Beispiel: Binärbäume 27 Peter Sobe Binärer Baum und Rekursion (1) Wurzel B0 Linker Teilbaum B0L Rechter Teilbaum B0R Binärbaum über Knotenmenge V B0= (W0, B0L ,B0R) (W0, , ) Bx= (Wx, BxL ,BxR) (Wx, , ) mit W0,B0 , Wx,Bx , ε V und als leeres Element Indizes werden durch Aneinandereihung gebildet, z.B. x=0L → xL = 0LL, x=0R → xL = 0RL usw. Ein Baum ist entweder ein einzelner Knoten oder ein als Wurzel dienender Knoten, der mit einer Menge von Bäumen verbunden ist. (beim Binärbaum mit zwei Teilbäumen verbunden) Peter Sobe 28 Binärer Baum und Rekursion (2) Verschiedene Strategien zum Traversieren des Baums Preorder: 1. Besuche die Wurzel des Baumes 2. Besuche den linken Teilbaum 3. Besuche den rechten Teilbaum 1 2 5 3 4 6 7 29 Peter Sobe Binärer Baum und Rekursion (3) Verschiedene Strategien zum Traversieren des Baums Inorder (Symmetrische Strategie): 1. Besuche den linken Teilbaum 2. Besuche die Wurzel 3. Besuche den rechten Teilbaum 4 2 1 Peter Sobe 6 3 5 7 30 Binärer Baum und Rekursion (4) Verschiedene Strategien zum Traversieren des Baums Postorder: 1. Besuche den linken Teilbaum 2. Besuche den rechten Teilbaum 3. Besuche die Wurzel 7 6 3 1 2 4 5 31 Peter Sobe Binärer Baum und Rekursion (5) Strategien zum Traversieren des Baums (Fortsetzung) Alle bisherigen Verfahren besuchen entweder tiefe Knoten oder links stehende Knoten zuerst. Bei Suchbäumen werden Lösungen u.U. erst spät gefunden. Level-Order-Traversierung: Besuche die Knoten “von links nach rechts“ innerhalb einer Ebene, danach die jeweils tiefere Ebene. Diese Reihenfolge wird nicht durch Zeiger in innerhalb der Baumstruktur unterstützt Diese Reihenfolge wird auch nicht durch Rekursion unterstützt Peter Sobe 32 Binärer Baum mit sortierten Daten (1) Baum-Elemente: ID Daten Zeigerlinks Zeigerrechts Zeiger auf Baum-Wurzel: 15 8 4 … … … 12 20 … 17 … … 23 … NULL-Zeiger, wenn Nachfolge-Elemente nicht vorhanden: Peter Sobe 33 Binärer Baum mit sortierten Daten (2) Suchen eines Elements mit ID=x im sortierten Binärbaum: Knoten = Wurzel Aufsuchen Knoten: falls ID==x dann gefunden, Ende falls x<ID: Verfolge Zeiger-links falls x>ID: Verfolge Zeiger-rechts Nach „Verfolge“ wird der jeweilige Knoten nach o.g. Regel besucht, solange bis Knoten mit ID gefunden oder ein Verfolgen auf den NULL-Zeiger trifft. Dann ist das gesuchte Element im Baum nicht vorhanden. Peter Sobe 34 Bewertung Binärbaum Wurzel Beispiel: 15 8 20 4 2 17 12 6 10 16 14 23 19 21 26 im Bild allgemein Berechnung Anzahl Ebenen 4 e Konstruktionsparameter Anzahl Elemente 15 n = 2e-1 Schritte zum Finden 4 (inkl. Zugriff eines Elements auf Wurzel) Peter Sobe s=e s =┌ log2 n ┐ 35