Algorithmen & Datenstrukturen 1. Grundlagen 1.1 Elementare Datenstrukturen 1
Datenstrukturen Gleichartige Daten (ob vom elementaren Datentyp oder strukturiert) werden meist bei abschätzbarer Datenmenge in einem Array gehalten, ansonsten wird die Größe der Datenstruktur dynamisch verwaltet. Statische Datenobjekte Dynamische Datenobjekte - Speicherbedarf muss zum Zeit- - Größe des Speicherbedarfs passt punkt der Programmerstellung sich zur Laufzeit an den tatsächbekannt sein lichen Bedarf an - automatische Speicherverwal- - programmlaufabhängige Speichertung verwaltung - fortlaufender Speicherbereich - verketteter Speicherbereich 2
Listen (1) Lineare Liste, einfach verkettet p_head L i s t e n NIL Lineare Liste, doppelt verkettet p_head p_tail NIL L i s t e n NIL 3
Listen (2) Ringliste, einfach verkettet p_head L i p_tail n s e t Einige Anwendungen: * geordnete Ablage von Daten, * Speicherverwaltung: Verkettung der freien Speicherbereiche, * schwach besetzte Matrizen. 4
Listen (3) Listen sind bekannt aus EPR! Hier Wiederholung Eine Lineare Liste (LL) ist eine verkettete Folge von Elementen eines gegebenen Typs T. Einfach verkettete LL: Jedes Listenelement enthält neben den Nutzdaten einen Verweis (Zeiger) auf seinen unmittelbaren Nachfolger. Doppelt verkettete LL: Jedes Listenelement enthält zwei Verweise, einen auf seinen unmittelbaren Nachfolger und einen auf seinen unmittelbaren Vorgänger. Ein Zeiger auf NULL zeigt das Ende der Liste an. Zirkulare Liste oder Ringliste: Jedes Listenelement hat einen Nachfolger, der Zeiger des letzten Elements zeigt wieder auf das erste Element. 5
Listen (4) Aufbau einer einfach verketteten linearen Liste p_head p_next NIL Length p head: Listenanfang als Verweis auf das erste Listenelement p next: Verweis auf den Nachfolger Length: Anzahl der Elemente einer Liste NIL: Ende der Liste, in C Verweis auf NULL. 6
Listen (5) Elementare Operationen Create: Liste erzeugen: Head setzen und das erste Listenelement eintragen. Destroy: alle Elemente entfernen. Search: Element suchen Insert: Element einfügen Delete: Element löschen Update: Element ersetzen Size: Anzahl der Elemente 7
Listen (6) Beispiel für eine Liste in C typedef struct data {... } data_t typedef struct element { data_t data; struct element * p_next; } element_t; typedef struct list { int length; element_t * p_head;... element_t * p_search; } list_t 8
Listen (7) Beispiel: ein Element in eine Liste mit sortierten Elementen einfügen p_new p_head p_search NIL 9
Listen (8) void insert_element(liste_t * liste, eintrag_t * p_new) { // falls Liste leer... // Einfügestelle suchen... // falls vor dem ersten Element eingefügt wird... } // ansonsten bei p_search einfügen p_new->p_next = liste->p_search->p_next; liste->p_search->p_next = p_new; 10
Listen (9) Beispiel: ein Element aus einer Liste löschen p_head p_search p_del NIL 11
Listen (10) int delete_element(liste_t * liste, eintrag_t * p_del) { // falls Liste leer - Fehler... // Position von p_del suchen // falls nicht vorhanden - Fehler... // ansonsten p_del löschen // falls p_del=p_head... // ansonsten, sei p_search der Vorgänger von p_del p_search->p_next = p_del->p_next; free(p_del); liste->length--; return 0; } Die weiteren Funktion sind analog zu den obigen aufgebaut. 12
Listen (11) Implementation einer Liste mit fester maximaler Größe (bounded implementation) Liegt die maximale Größe der Liste fest, kann eine statische Allokierung vorteilhaft sein. Anstatt Zeiger auf das nächste Datenelement werden die belegten und freien Elemente in einer Indexliste geführt. list[12] 0 1 2 3 4 5 6 7 8 9 10 11 L i s n e t 2 4 3 10 5 7-1 8 11 6 9-1 head freehead 13
Listen (12) In C typedef struct element { data_t data; int next; } element_t; typedef struct list { element_t list_el[12]; /* Array aus 12 Elementen */ int head = -1 /* Verweis auf Listenanfang */ int freehead = 0 /* Verweis auf erstes freies Element */... } list_t -1 steht für NIL, head zeigt auf das erste Listenelement freehead zeigt auf das erste freie Listenelement. 14
Listen (13) Vergleich Bounded-Singly-Linked-List mit Arrays: * Mehraufwand durch Verweise * einfache Sortierung möglich Bounded- mit Unbounded-Singly-Linked-List: * Begrenzte Anzahl der Elemente * vermeidet Speicherfragmentierung 15
Listen (14) Beispiel: Einfügen eines Elements am Anfang der Liste. 0 1 2 3 4 5 6 7 8 9 10 11 L i s n e t 2 4 3 10 5 7-1 8 11 6 9-1 head freehead 0 1 2 3 4 5 6 7 8 9 10 11 L 5 i s n e t 2 0 3 10 5 7-1 8 11 6 9-1 if (freehead!= NIL) { new_el = freehead; freehead = list[freehead].next; list[new_el].data = daten; list[new_el].next = head; head = new_el; } head freehead 16
Listen (15) Doppelt-verkettete Liste Die doppelt-verkettete Liste ist eine Erweiterung der einfach-verketteten Liste, in der Verweise in beide Laufrichtungen gespeichert sind. p_head p_tail Datenstruktur in C L NIL i s NIL t e n typedef struct element { data_t data; struct element * prev; struct element * next; } element_t; Alle Funktionen sind analog zur einfach verketteten Liste aufgebaut. 17
Listen (16) Doppelt verkettete Liste mit fester Größe 0 1 2 3 4 5 6 7 8 9 10 11 Tail -1 0 2 9 10 3 L i s n e t 2 4 3 10 5 7-1 8 11 6 9-1 Head FreeHead Auch hier werden alle Funktionen analog zur einfach verketteten Liste mit fester maximaler Größe aufgebaut werden. 18
Stack (1) Ein Stack (Stapel, Kellerspeicher) ist ein abstrakter Datentyp, bei dem Elemente in der sogenannten LIFO = Last-In-First- Out-Reihenfolge abgelegt werden und wieder eingelesen werden können. D.h. die Daten werden in der umgekehrten Reihenfolge gelesen wie geschrieben. Anwendung: geschachtelte Strukturen unbekannter Tiefe wie z.b. bei der Auswertung arithmetischer Ausdrücke für die Speicherung von Zwischenergebnissen. Auf diese Art werden die Daten bei Funktionsaufrufen auf den Stack-Speicher gelegt. 19
Stack (2) Typische Stackoperationen: push: legt das Element x auf den Stack s peek/top: liefert das zuletzt auf den Stack s gelegte Element pop: isempty: size: (liefert und) entfernt das zuletzt auf den Stack s gelegte Element gibt an, ob der Stack leer ist gibt die Größe des Stacks Ein Stack kann als Array oder lineare Liste implementiert werden. 20
Stack (3) Beispiel: Berechnung von 5.1*(((91+28)*(4.3+6))+777) in umgekehrter polnischer Notation: 5.1 91 28 + 4.3 6 + * 777 + * #include <stdio.h> int main(void) push(pop()+pop()); { push(pop()*pop()); stackinit(); push(777); push(5.1); push(pop()+pop()); push(91); push(pop()*pop()); push(28); printf("%d\n",pop()); push(pop()+pop()); if (stackempty()) push(4.3); printf(" Stack is empty now\n"); push(6); return 0; } 21
Queue (1) Eine Queue (Warteschlange, Puffer) ist ein Datentyp, bei dem auf die Elemente nach dem FIFO = First-In-First-Out-Prinzip zugegriffen wird. Dies ermöglicht das Auslesen der Daten in der Reihenfolge ihrer Speicherung. Length Head 10 4 9 6 1 2 5 Tail Head 8 10 4 9 6 1 2 5 Tail 22
Queue (2) Speicherung Eine Queue kann als lineare Liste, Ringliste oder Array implementiert werden. Struktur Es müssen 2 Indizes mitgeführt werden: head, die Position zum Eintragen von Elementen, tail, die Position zum Entnehmen der Elemente. 4 9 6 1 2 5 head next element tail 23
Queue (3) Elementare Operationen Create: Queue erzeugen Destroy: Queue löschen Insert(AtHead): Element einfügen Remove(FromTail): Element löschen IsEmpty: Test auf Einträge IsFull: Test auf Füllung LengthOf: Länge der Queue Anwendung: Pufferung zwischen unterschiedlich leistungsfähigen Datenquellen wie z.b. Druckerwarteschlangen pipes in Unix Ereignisverarbeitung 24
Queue (4) 1 12 2 11 Ringbuffer Ist die maximale Länge bekannt, kann eine Queue auch als zirkulares Array (Ringpuffer) implementiert werden. 3 4 4 7 Tail next Head 9 33 77 43 10 9 5 121 6 8 6 7 25
Queue (5) Problem: Head und Tail wandern entgegen des Uhrzeigersinns. Die Queue ist voll oder leer bei (Head+1)%12=Tail Ausweg: Setze Head auf -1, wenn die Queue voll ist und Tail auf -1, wenn die Queue leer ist. 26
Bäume (1) Bäume in der Informatik sind verzweigte Datenstrukturen. Wurzel Blätter Sie bestehen aus Knoten, die hierarchisch angeordnet sind und über Kanten verbunden sind. 27
Bäume (2) Sie enthalten keine geschlossenen Maschen, d.h. sie sind zyklenfrei. Es gibt genau einen Knoten, der keinen Vorgänger (Eltern-Knoten, Parent-node) hat, die Wurzel (root) oder der oberste Knoten. Die untersten oder äußeren Knoten, an denen keine weiteren Knoten hängen, heißen Blätter (leaves). Die direkten Nachfolger eines Knotens heißen Kinder (Children). Jeder Knoten, der kein Blatt ist, hat einen Teilbaum. Die Entfernung von der Wurzel ist die Stufe eines Knotens. Die maximale Anzahl der Teilbaumzeiger im Knoten ist der Grad (degree) eines Baums. Jeder Knoten ist von der Wurzel aus auf genau einem Weg zu erreichen. 28
Bäume (3) Die Knoten eines Baumes sind meistens nach einer bestimmten Ordnungsregel angeordnet. Wurzel 44 39 67 20 42 60 75 03 25 59 65 72 89 Blätter Vorfahren von 03 und 25 67 Parent von 60 und 75 60 75 Childern von 67 Nachfahren von 67 Geschwister 29
Bäume (4) Jeder Knoten eines Baums besteht aus einem Datenteil und zwei oder mehr Zeigern auf andere Knoten. Bedeutung für die Informatik Hervorrangendes Hilfsmittel, sortierte Daten so abzuspeichern, dass jedes Datenelement möglichst schnell auffindbar ist. Hierarchische Strukturen wie z.b. Verzeichnisstrukturen bei Betriebssystemen. Repräsentation mathematischer Formeln, z.b. arithmetische Ausdrücke... 30
Bäume (5) Binäre Bäume Jeder Knoten hat höchstens zwei Teilbaumzeiger, d.h. er ist vom Grad 2. Er ist entweder leer oder besteht aus der Wurzel und zwei binären Bäumen (rekursive Definition). Jeder Knoten enthält Daten und 2 Verweise auf die Unterbäume. Besitzt ein Knoten links oder/und rechts kein Child-Element, wird der entsprechende Zeiger auf NULL gesetzt. Eine einfach verkettete lineare Liste ist ein nicht-ausgeglichener binärer Baum. 31
Bäume (6) 32
Bäume (7) Geordnete Bäume Jedes Datenelement enthält einen Ordnungsbegriff, den Schlüssel, nach dessen Wert sortiert wird. Jeder linke Teilbaum eines Knoten enthält nur Knoten mit kleineren Schlüsselwörtern, jeder rechte Teilbaum nur Knoten mit größeren Schlüsselwörtern (siehe Suchbäume). Bäume ausgleichen Ein ausgeglichener Baum ist ein Baum, der so flach wie möglich ist, d.h. alle Blätter liegen auf einer Ebene bzw. die vorletzte Ebene ist voll belegt. Ausgeglichene Bäume werden oft verwendet, wenn Elemente gesucht werden sollen (schwächere Definitionen siehe AVL-Bäumen). 33
Durchlaufen von Bäumen (1) Methoden zum Durchlaufen (Traversing) von Bäumen 20 09 53 05 15 79 11 Im Gegensatz zu den bisherigen Datenstrukturen ist das Durchlaufen von Bäumen nicht eindeutig. 34
Durchlaufen von Bäumen (2) Meistens werden 4 Typen der Bewegung verwendet: Preorder Traversing 1. Besuchen des Wurzelknotens 2. Preorder Traversing des linken Teilbaums 3. Preorder Traversing des rechten Teilbaums 20 09 53 05 15 79 11 35
Durchlaufen von Bäumen (3) Postorder Traversing (siehe Ausdrucksbäume) 1. Postorder Traversing des linken Teilbaums 2. Postorder Traversing des rechten Teilbaums 3. Besuchen des Wurzelknotens 20 09 53 05 15 79 11 36
Durchlaufen von Bäumen (4) Inorder Traversing 1. Inorder Traversing des linken Teilbaums 2. Besuchen des Wurzelknotens 3. Inorder Traversing des rechten Teilbaums 20 09 53 05 15 79 11 Die Daten in einem binären Suchbaum sind in Inorder-Reihenfolge korrekt sortiert. 37
Durchlaufen von Bäumen (5) Level-Order Traversing 1. Besuchen des Wurzelknotens 2. Durchlaufen jeder weitere Ebene von links nach rechts 20 09 53 05 15 79 11 38
Suchbäume (1) Jeder Knoten eines Baumes enthält ein Datenelement. Dieses unterteilt sich normalerweise in ein Schlüsselelement (Key), nachdem die Knoten sortiert sind, und eine Datenstruktur. Beispiel für eine Struktur eines Knotens in einem binären Suchbaum in C typedef struct node_b { struct node_b * p_left; struct node_b * p_right; int key; data_t data; } node_t; Oft werden weitere Daten hinzugefügt, wie z.b. ein Zeiger auf den Elternknoten oder die Tiefe der Teilbäume (siehe später AVL- Bäume). 39
Suchbäume (2) 44 39 67 20 42 60 75 03 25 59 65 72 89 40
Suchbäume (3) Inorder Traversing (rekursiv: linker Teilbaum, ausgeben, rechter Teilbaum). 44 39 67 20 42 60 75 03 25 59 65 72 89 03 20 25 39 42 44 59 60 65 67 72 75 89 41
Ausdrucksbaum (1) Aufbau eines Ausdrucksbaums zur Verarbeitung von Ausdrücken: Die als Child-Elemente jedes Knotens vorhandenen Teilbäume bilden die Operanden des im Parent-Knoten gespeicherten Operators. Die Operanden können Terminale oder selbst Ausdrücke sein. Ausdrücke werden zu Teilbäumen expandiert Terminale stehen in den Blatt-Knoten. 42
Ausdrucksbaum (2) Beispiel: ((74-10)/32) x (23+17) A x B x A B A=C/32 / B=23+17 + C 32 23 17 C=74-10 - 74 10 43
Ausdrucksbaum (3) x / + - 32 23 17 74 10 Postfix-Ausdruck: 74 10-32 / 23 17 + x 44
Ausdrucksbaum (4) Postfix-Ausdruck: 74 10-32 / 23 17 + x Dieses ist genau die Notation, die wir zur Abarbeitung von Ausdrücken beim Thema Stack behandelt haben. 17 10 32 23 40 74 64 2 80 64 2 2 80 auf Stack 74 auf Stack 10 vom Stack 10 vom Stack 74 - anwenden auf Stack 64 auf Stack 32 vom Stack 32 vom Stack 64 / anwenden auf Stack 2 auf Stack 23 auf Stack 17 vom Stack 17 vom Stack 23 + anwenden auf Stack 40 vom Stack 40 vom Stack 2 x anwenden auf Stack 80 Achtung, bei den Operationen - und / muss auf die Reihenfolge der Operanden geachtet werden. 45
Operationen für Bäume Traversierung eines Baums. Suchen eines Elements. Verändern eines Knotens. Einfügen eines neuen Knotens. Löschen eines Elements. Ausgleichen eines Baums durch Rotation. Einzelne Algorithmen für diese Operationen wie ein Suchalgorithmus können iterativ (Wiederhol-Schleife) oder rekursiv (sich selbst aufrufende Funktion) durchgeführt werden. Weiteres zu Bäumen im Kapitel Suchen. 46