Dynamische Speicherstrukturen

Werbung
7. Dynamischer Speicher und Datenstrukturen
Inhalt:
7.1 Dynamisch angeforderter Speicher
7.2 Datenstrukturen
Die Folien basieren zum Teil auf einen Foliensatz von R. Großmann
und T. Wiedemann
Peter Sobe
1
Dynamische Speicherstrukuren (1)
Unter dynamischen Speicherstrukturen versteht man die
Bildung neuer Speicherstrukturen auf der Basis von
Standarddatentypen und deklarierten Strukturen zur Laufzeit. Es
wird Speicherplatz zur Laufzeit der Halde (Heap) entnommen,
damit ein neues Objekt initialisiert und mit bereits vorhandenen
Objekten in der Speicherstruktur verbunden. Ebenso können
auch zur Laufzeit Objekte aus der Speicherstruktur herausgelöst
werden und deren Speicherplatz wieder freigegeben werden.
Im Gegensatz werden die bisher benutzen Variablen und Felder
als statische Speicherstrukturen angesehen. Die Sichtbarkeit
und der belegte Speicherplatz wird durch ihre Deklaration im
globalen Kontext oder innerhalb Funktionen bestimmt.
Peter Sobe
2
Dynamische Speicherstrukuren (2)
Zur Realisierung solcher dynamischer Strukturen werden zwei
Technologien benötigt:
1. Die dynamische Speicherallokation und –freigabe im
Heap und
2. Die Verwendung der Zeigertechnik zum dynamischen
Herstellen und Lösen von Verbindungen von Objekten
und der Datenstruktur
Die interne 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
3
Dynamische Speicherstrukturen (3)
Die dynamische Speicherallokation und –freigabe im
Heap 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 auf dem Heap allokiert und die Adresse dieses
Speicherbereiches zurückgegeben. Ist der Rückkehrwert NULL, ist
nicht genügend Speicher vorhanden. Ist die genaue Bytezahl bekannt,
kann diese direkt als Argument übergeben werden, sonst empfiehlt sich
die Verwendung von sizeof(x), die die notwendige Bytezahl für das
Objekt x ermittelt.
Bei der Speicherfreigabe mit free ist nur die Heapadresse des
Speicherbereichs anzugeben.
Peter Sobe
4
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
5
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
6
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
7
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
8
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
Beispiele:
•
Lineares Feld mit sortierten Elementen, z.B. für Telefonbucheinträge
•
Baumstruktur mit Verweisen auf Vater- und Mutter-elemente zur
Nachbildung von Stammbäumen
•
Netzstruktur, z.B. mit Struktur einer elektrischen Schaltung
Peter Sobe
9
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;
};
Peter Sobe
// Zeiger auf Nachfolger-Element
10
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
11
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
12
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
13
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
14
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.
15
Peter Sobe
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.
Peter Sobe
16
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
o
pos
o
o
pos->n
free(h)
Gesonderte Behandlung erforderlich, wenn erstes Element auszuketten
ist.
17
Peter Sobe
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
Peter Sobe
krit ?
o
a=a->n
krit ?
18
Nichtlineare dynamische Datenstrukturen
Eine nichtlinear verkettete Folge 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 allg. Graphen nur netzartige
Strukturen als allgemeinster Fall auftreten.
Peter Sobe
19
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
Peter Sobe
20
Binärer Baum und Rekursion (1)
Wurzel B0
Rechter Teilbaum
B0R
Linker Teilbaum
B0L
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)
21
Peter Sobe
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
3
Peter Sobe
5
4
6
7
22
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
6
5
3
7
23
Peter Sobe
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
Peter Sobe
2
4
5
24
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
25
Peter Sobe
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
26
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.
27
Peter Sobe
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 ┐
28
Herunterladen