Algorithmen & Datenstrukturen
1. Grundlagen
1.1 Elementare Datenstrukturen
Datenstrukturen
Gleichartige Daten (ob vom elementaren Datentyp oder strukturiert) werden meist bei absch¨atzbarer Datenmenge in einem Array gehalten, ansonsten wird die Gr¨oße der Datenstruktur dynamisch verwaltet.
Statische Datenobjekte Dynamische Datenobjekte
- Speicherbedarf muss zum Zeit- - Gr¨oße des Speicherbedarfs passt punkt der Programmerstellung sich zur Laufzeit an den tats¨ach-
bekannt sein lichen Bedarf an
- automatische Speicherverwal- - programmlaufabh¨angige Speicher-
tung verwaltung
- fortlaufender Speicherbereich - verketteter Speicherbereich
Listen (1)
• Lineare Liste, einfach verkettet
p_head
L i s t e n NIL
• Lineare Liste, doppelt verkettet
p_head p_tail
NIL
NIL
L i s
t e n
Listen (2)
• Ringliste, einfach verkettet
p_tail
p_head L i
s
t e
n
• Einige Anwendungen:
* geordnete Ablage von Daten,
* Speicherverwaltung: Verkettung der freien Speicherbereiche,
* schwach besetzte Matrizen.
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¨alt neben den Nutzdaten einen Verweis (Zeiger) auf seinen unmittelbaren Nach- folger.
• Doppelt verkettete LL: Jedes Listenelement enth¨alt zwei Verweise, einen auf seinen unmittelbaren Nachfolger und einen auf seinen unmittelbaren Vorg¨anger.
• Ein Zeiger auf N U LL zeigt das Ende der Liste an.
• Zirkulare Liste oder Ringliste: Jedes Listenelement hat einen Nach- folger, der Zeiger des letzten Elements zeigt wieder auf das erste Element.
Listen (4)
Aufbau einer einfach verketteten linearen Liste
NIL p_head
p_next
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.
Listen (5)
Elementare Operationen
Create: Liste erzeugen: Head setzen und das erste Listenelement eintragen.
Destroy: alle Elemente entfernen.
Search: Element suchen Insert: Element einf¨ugen Delete: Element l¨oschen Update: Element ersetzen
Size: Anzahl der Elemente
Listen (6)
Beispiel f¨ur 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
Listen (7)
Beispiel: ein Element in eine Liste mit sortierten Elementen einf¨ugen
NIL p_head p_search
p_new
Listen (8)
void insert_element(liste_t * liste, eintrag_t * p_new) {
// falls Liste leer ...
// Einf¨ugestelle suchen ...
// falls vor dem ersten Element eingef¨ugt wird ...
// ansonsten bei p_search einf¨ugen
p_new->p_next = liste->p_search->p_next;
liste->p_search->p_next = p_new;
}
Listen (9)
Beispiel: ein Element aus einer Liste l¨oschen
NIL
p_head p_search p_del
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¨oschen // falls p_del=p_head
...
// ansonsten, sei p_search der Vorg¨anger 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.
Listen (11)
Implementation einer Liste mit fester maximaler Gr¨oße (bounded implementation)
Liegt die maximale Gr¨oße der Liste fest, kann eine statische Allokie- rung vorteilhaft sein. Anstatt Zeiger auf das n¨achste Datenelement werden die belegten und freien Elemente in einer Indexliste gef¨uhrt.
2 4 3 10 5 7 -1 8 11 6 9 -1
L i s n e t
0 1 2 3 4 5 6 7 8 9 10 11
head freeHead
list[12]
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¨ur NIL,
head zeigt auf das erste Listenelement
freeHead zeigt auf das erste freie Listenelement.
Listen (13)
Vergleich
• Bounded-Singly-Linked-List mit Arrays:
* Mehraufwand durch Verweise
* einfache Sortierung m¨oglich
• Bounded- mit Unbounded-Singly-Linked-List:
* Begrenzte Anzahl der Elemente
* vermeidet Speicherfragmentierung
Listen (14)
Beispiel: Einf¨ugen eines Elements am Anfang der Liste.
2 4 3 10 5 7 -1 8 11 6 9 -1
L i s n e t
0 1 2 3 4 5 6 7 8 9 10 11
head freeHead
2 0 3 10 5 7 -1 8 11 6 9 -1
L i s n e t
0 1 2 3 4 5 6 7 8 9 10 11
head freeHead
5
if (freeHead != NIL) { new_el = freeHead;
freeHead = list[freeHead].next;
list[new_el].data = daten;
list[new_el].next = head;
head = new_el;
}
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
NIL
NIL
L i s
t e n
Datenstruktur in C
typedef struct element { data_t data;
struct element * prev;
struct element * next;
} element_t;
Alle Funktionen sind analog zur einfach verketteten Liste aufgebaut.
Listen (16)
Doppelt verkettete Liste mit fester Gr¨oße
2 4 3 10 5 7 -1 8 11 6 9 -1
L i s n e t
0 1 2 3 4 5 6 7 8 9 10 11
Head FreeHead
0 2 9 10 3
-1
Tail
Auch hier werden alle Funktionen analog zur einfach verketteten Liste mit fester maximaler Gr¨oße aufgebaut werden.
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¨onnen.
• 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¨ucke f¨ur die Speicherung von Zwischenergebnissen.
• Auf diese Art werden die Daten bei Funktionsaufrufen auf den Stack-Speicher gelegt.
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: (liefert und) entfernt das zuletzt auf den Stack s gelegte Element
isEmpty: gibt an, ob der Stack leer ist size: gibt die Gr¨oße des Stacks
Ein Stack kann als Array oder lineare Liste implementiert werden.
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;
}
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¨oglicht das Auslesen der Daten in der Reihenfolge ihrer Speicherung.
Head Tail
Length
10 4 9 6 1 2 5
Head
8 10 4 9 6 1 2 5Tail
Queue (2)
Speicherung
Eine Queue kann als lineare Liste, Ringliste oder Array implementiert werden.
Struktur
Es m¨ussen 2 Indizes mitgef¨uhrt werden:
head, die Position zum Eintragen von Elementen, tail, die Position zum Entnehmen der Elemente.
4 9 6 1 2 5
head tail
next element
Queue (3)
Elementare Operationen
Create: Queue erzeugen
Destroy: Queue l¨oschen Insert(AtHead): Element einf¨ugen Remove(FromTail): Element l¨oschen IsEmpty: Test auf Eintr¨age IsFull: Test auf F¨ullung LengthOf: L¨ange der Queue Anwendung:
• Pufferung zwischen unterschiedlich leistungsf¨ahigen Datenquellen wie z.B. Druckerwarteschlangen
• pipes in Unix
• Ereignisverarbeitung
Queue (4)
Ringbuffer
Ist die maximale L¨ange bekannt, kann eine Queue auch als zirkula- res Array (Ringpuffer) implementiert werden.
Head
4 Tail
7
121 6
33
43 77 9
1
2
3
4
5
6 7
8
9 10 11
12
next
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.
B¨ aume (1)
B¨aume in der Informatik sind verzweigte Datenstrukturen.
Wurzel
Blätter
• Sie bestehen aus Knoten, die hierarchisch angeordnet sind und
¨
uber Kanten verbunden sind.
B¨ aume (2)
• Sie enthalten keine geschlossenen Maschen, d.h. sie sind zyklenfrei.
• Es gibt genau einen Knoten, der keinen Vorg¨anger (Eltern-Knoten, Parent-node) hat, die Wurzel (root) oder der oberste Knoten.
• Die untersten oder ¨außeren Knoten, an denen keine weiteren Kno- ten h¨angen, heißen Bl¨atter (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.
B¨ aume (3)
Die Knoten eines Baumes sind meistens nach einer bestimmten Ord- nungsregel angeordnet.
Wurzel 44
67 39
20
03 25
42 60 75
59 65 72 89
44
Blätter Vorfahren von 03 und 25 67 Parent von 60 und 75
60 75 Childern von 67 Nachfahren von 67 Geschwister
B¨ aume (4)
Jeder Knoten eines Baums besteht aus einem Datenteil und zwei oder mehr Zeigern auf andere Knoten.
Bedeutung f¨ur die Informatik
• Hervorrangendes Hilfsmittel, sortierte Daten so abzuspeichern, dass jedes Datenelement m¨oglichst schnell auffindbar ist.
• Hierarchische Strukturen wie z.B. Verzeichnisstrukturen bei Be- triebssystemen.
• Repr¨asentation mathematischer Formeln, z.B. arithmetische Aus- dr¨ucke
• ...
B¨ aume (5)
Bin¨are B¨aume
• Jeder Knoten hat h¨ochstens zwei Teilbaumzeiger, d.h. er ist vom Grad 2.
• Er ist entweder leer oder besteht aus der Wurzel und zwei bin¨aren B¨aumen (rekursive Definition).
• Jeder Knoten enth¨alt Daten und 2 Verweise auf die Unterb¨aume.
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¨arer Baum.
B¨ aume (6)
B¨ aume (7)
Geordnete B¨aume
• Jedes Datenelement enth¨alt einen Ordnungsbegriff, den Schl¨ussel, nach dessen Wert sortiert wird.
• Jeder linke Teilbaum eines Knoten enth¨alt nur Knoten mit kleine- ren Schl¨usselw¨ortern, jeder rechte Teilbaum nur Knoten mit gr¨oße- ren Schl¨usselw¨ortern (siehe Suchb¨aume).
B¨aume ausgleichen
• Ein ausgeglichener Baum ist ein Baum, der so flach wie m¨oglich ist, d.h. alle Bl¨atter liegen auf einer Ebene bzw. die vorletzte Ebene ist voll belegt.
• Ausgeglichene B¨aume werden oft verwendet, wenn Elemente ge- sucht werden sollen (schw¨achere Definitionen siehe AVL-B¨aumen).
Durchlaufen von B¨ aumen (1)
Methoden zum Durchlaufen (Traversing) von B¨aumen
09 53
05
11
15 79
20
Im Gegensatz zu den bisherigen Datenstrukturen ist das Durchlaufen von B¨aumen nicht eindeutig.
Durchlaufen von B¨ aumen (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
09 53
05
11
15 79
20
Durchlaufen von B¨ aumen (3)
• Postorder Traversing (siehe Ausdrucksb¨aume) 1. Postorder Traversing des linken Teilbaums 2. Postorder Traversing des rechten Teilbaums 3. Besuchen des Wurzelknotens
09 53
05
11
15 79
20
Durchlaufen von B¨ aumen (4)
• Inorder Traversing
1. Inorder Traversing des linken Teilbaums 2. Besuchen des Wurzelknotens
3. Inorder Traversing des rechten Teilbaums
09 53
05
11
15 79
20
Die Daten in einem bin¨aren Suchbaum sind in Inorder-Reihenfolge korrekt sortiert.
Durchlaufen von B¨ aumen (5)
• Level-Order Traversing
1. Besuchen des Wurzelknotens
2. Durchlaufen jeder weitere Ebene von links nach rechts
09 53
05
11
15 79
20
Suchb¨ aume (1)
• Jeder Knoten eines Baumes enth¨alt ein Datenelement. Dieses un- terteilt sich normalerweise in ein Schl¨usselelement (Key), nachdem die Knoten sortiert sind, und eine Datenstruktur.
• Beispiel f¨ur eine Struktur eines Knotens in einem bin¨aren Such- baum 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¨ugt, wie z.B. ein Zeiger auf den Elternknoten oder die Tiefe der Teilb¨aume (siehe sp¨ater AVL- B¨aume).
Suchb¨ aume (2)
67 39
20
03 25
42 60 75
59 65 72 89
44
Suchb¨ aume (3)
• Inorder Traversing (rekursiv: linker Teilbaum, ausgeben, rechter Teilbaum).
67 39
20
03 25
42 60 75
59 65 72 89
44
67 39
03 20 25 42 44 59 60 65 72 75 89
Ausdrucksbaum (1)
Aufbau eines Ausdrucksbaums zur Verarbeitung von Ausdr¨ucken:
• Die als Child-Elemente jedes Knotens vorhandenen Teilb¨aume bil- den die Operanden des im Parent-Knoten gespeicherten Opera- tors.
• Die Operanden k¨onnen Terminale oder selbst Ausdr¨ucke sein.
• Ausdr¨ucke werden zu Teilb¨aumen expandiert
• Terminale stehen in den Blatt-Knoten.
Ausdrucksbaum (2)
Beispiel: ((74-10)/32) x (23+17)
A B
-
10
32 17
x
23
74
A x B
/ +
C
A=C/32 B=23+17
C=74-10
Ausdrucksbaum (3)
/ +
-
10
32 17
x
23
74
Postfix-Ausdruck: 74 10 - 32 / 23 17 + x
Ausdrucksbaum (4)
Postfix-Ausdruck: 74 10 - 32 / 23 17 + x
Dieses ist genau die Notation, die wir zur Abarbeitung von Ausdr¨ucken beim Thema Stack behandelt haben.
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
74 10
64
64 32
2
23 17
40
80
80
2 2
Achtung, bei den Operationen - und / muss auf die Reihenfolge der Operanden geachtet werden.
Operationen f¨ ur B¨ aume
• Traversierung eines Baums.
• Suchen eines Elements.
• Ver¨andern eines Knotens.
• Einf¨ugen eines neuen Knotens.
• L¨oschen eines Elements.
• Ausgleichen eines Baums durch “Rotation”.
Einzelne Algorithmen f¨ur diese Operationen wie ein Suchalgorithmus k¨onnen iterativ (Wiederhol-Schleife) oder rekursiv (sich selbst aufru- fende Funktion) durchgef¨uhrt werden.
Weiteres zu B¨aumen im Kapitel “Suchen”.