Programmieren in C


von Prof. Jürgen Plate

7 Dynamische Datenstrukturen

7.1 Dynamische Speicherverwaltung

Bisher waren Datenobjekte (Variable), die in einem Programm definiert werden, sind statische Objekte. Das heißt, daß der C-Compiler für die Bereitstellung von Speicher für diese Objekte sorgt und daß ihre Anzahl und Größe (also der Speicherbedarf) zum Zeitpunkt der Übersetzung festliegen muß. Bei manchen Algorithmen ergibt sich das Problem, daß die Größe eines Objektes (z. B. Arrays) bzw. die Anzahl der Objekte erst zur Programmlaufzeit angegeben werden kann.

Mit den bisherbekannten Datenstrukturen der Sprache C bleibt nur die Notlösung: Definition statisch angelegter Objekte mit Maximalgröße. Zum einen wird hier oft Speicher verschwendet und zum anderen reich mitunter der verfügbare Speicher nicht für die Maximalwerte aus und das Programm wird nicht gestartet, obwohl in der aktuellen Situation wesentlich weniger Speicher gebraucht würde. Das Ganze ist also wenig flexibel.

Die Lösung liegt in einer dynamischen Speicherallokation bei Auftreten des Platzbedarfs, d. h. Definition der Objekte zur Laufzeit (--> dynamisch). Diese Lösung ist am Bedarf ausgerichtet und der belegte Platz kann sogar wieder dynamisch freigegeben werden. Dynamisch allokierte Objekte können nicht wie statische Objekte definiert werden. Sie werden auch nicht über einen Namen, sondern nur über ihre Adresse angesprochen. Diese Adresse entsteht bei der Speicherbelegung ("Allokation") und wird normalerweise einer Zeiger-Variablen zugewiesen.

Eine statische Variable, etwa ein Array, hat einfach die Form:

Bei dynamischer Allokation geht man folgendermaßen vor:

Es ist wichtig, sich den Unterschied zum äquivalenten statischen Array klarzumachen. Während dyn eine Variable ist, deren Speicherplatz sich aus &dyn ergibt, ist stat eine konstante Adresse. Interessant ist die Frage, was denn dann &stat ist. Intern und formal wird dazu folgender Trick verwendet: &stat entspricht stat, die Adressen sind also identisch.

Die dynamische Speicherbelegung erfolgt mittels spezieller, in der Standardbibliothek enthaltenen, Speicherallokationsfunktionen. size_t ist dabei ein maschinenabhängig definierter Datentyp unsigned int, definiert in <stdlib.h>, <stdio.h.h>, <stddef.h>, etc.).

Die Speicherallokations-Funktionen liefern die Anfangsadresse des allokierten Blocks als void-Pointer (void *) es ist daher kein Type-Cast bei Zuweisung an Pointer-Variable erforderlich. Um dem Programm mehr Klarheit zu geben, schadet es aber auch nicht, Type-Cast zu verwenden.

Die Funktionen malloc und calloc liefern bei Fehler (z. B. wenn der angeforderte Speicherplatz nicht zur Verfügung steht) den Nullpointer NULL zurück. Nach jedem Aufruf sollte deshalb deren Rückgabewert getestet werden! Dies kann mit void assert(int) geschehen. assert() ist ein Makro und benötigt das Headerfile <assert.h> und u. U. <stdio.h>. Ist das Argument NULL, wird das Programm abgebrochen, der Modulname und die Programmzeilennummer des assert-Aufrufs ausgegeben.

Für malloc, calloc und free wird das Headerfile <stdlib.h> oder <alloc.h> benötigt.

Der für die dynamische Speicherverwaltung zur Verfügung stehende Speicherbereich wird als Heap bezeichnet. Die Lebensdauer dynamisch allokierten Speichers ist nicht an die Ausführungszeit eines Blocks gebunden. Nicht mehr benötigter dynamisch allokierter Speicher ist explizit freizugeben (free()). Ein erstes Beispiel:

    
  int *ip;
  ip = (int *) malloc(n*sizeof(int));
ip zeigt hier auf n Speicherplätze vom Datentyp int. Mit
  free(ip);
kann der bereitgestellte Speicherplatz wieder freigegeben werden.

Im folgenden Beispielprogramm wird dynamisch Speicherplatz für Objekte bereitgestellt, die einfachen Variablen der Datentypen int, float und double entsprechen. Nach dem Ablegen von eingelesenen Zahlenwerten auf diesen Objekten und dem anschließenden Ausdrucken wird der zugeteilte Speicherplatz wieder freigegeben.

#include <stdio.h>
#include <stdlib.h>
main()
  {
  int *ip; float *fp; double *dp;

  /* Speicher anfordern */
  ip = (int *) malloc (sizeof(int));
  fp = (float *) malloc (sizeof(float));
  dp = (double *) malloc (sizeof(double)); 

  printf("Drei Zahlen eingeben:\n");
  scanf("%d %f %lf", ip, fp, dp);
  printf("%d, %f, %f\n", *ip, *fp, *dp);

  /* Speicher freigeben */
  free(ip);
  free(fp);
  free(dp);
  }

Das folgende Demonstrationsprogramm zur Verwendung der Bibliotheksfunktionen zur dynamischen Speicherverwaltung liest eine beliebige Anzahl von double-Werten vom Terminal und gibt sie dann wieder aus. Beachten Sie die Verwendung von sizeof() und die Fehlerprüfung.

#include <stdio.h>
#include <stdlib.h>

void main(void)
  {
  int i, anz;
  double *dfeld;

  printf("\nAnzahl der double-Werte ? ");
  scanf("%d", &anz);

  if (anz <= 0) 
    {
    printf("\nAnzahl muß >0 sein !\n");
    exit(1);
    }

  if ((dfeld = malloc(anz*sizeof(double))) == NULL)
    { 
    printf("\nSpeicherplatz reicht nicht aus !\n"); 
    exit(1);
    }

  /* Einlesen der double-Werte */

  for (i = 0; i < anz ; i++) 
    {
    printf (" Doublewert Nr. %d bitte:",i);
    scanf ("%f",dfeld+i);
    }

  printf ("\n");

  /* Ausgabe der double-Werte */

  for (i = 0; i < anz ; i++) 
    printf (" Doublewert Nr. %d  = %f:\n",i, dfeld[i]);

  free(dfeld);
  }

Ein weiteres Beispiel definiert einen struct-Typ für Angestellte. Die Komponente "chef" ermöglicht es, einen Pointer aufzunehmen, der auf eine entsprechende zweite Struktur verweist, welche die Informationen über den Vorgesetzten des Angestellten enthält. Da der Chef wiederum einen Vorgesetzten haben kann usw., lassen sich so Firmenhierarchien aufbauen.

Es wird im Programm Speicherplatz für die Daten des Angestellten und der beiden übergeordneten Vorgesetzten reserviert. Dabei erfolgt gleichzeitig die Verkettung der Strukturen über ihre chef-Pointer, so dass die gewuenschte Hierarchie entsteht.

#include <stdio.h>

main() 
  {
  struct mitarbeiter 
    {
    char  name[20];
    int   pers_nr;
    float gehalt;
    struct mitarbeiter *chef;
    };

struct mitarbeiter *ang_zeiger; /* Zeiger auf die Daten eines Mitarbeiters */

 ang_zeiger =                                    /* Mitarbeiter */
   (struct mitarbeiter *) malloc(sizeof(struct mitarbeiter));

 (*ang_zeiger).chef =                /* der direkte Vorgesetzte */
   (struct mitarbeiter *) malloc(sizeof(struct mitarbeiter));

 (*(*ang_zeiger).chef).chef =        /* und dessen Vorgesetzter */
   (struct mitarbeiter *) malloc(sizeof(struct mitarbeiter));


 (*ang_zeiger).pers_nr = 1234;              /* Entweder so ... */
 ang_zeiger->pers_nr   = 1234;              /* oder so!        */

 
 ang_zeiger->chef->chef->pers_nr = 0001;       /* Der Oberboss */

 /* Abschluss der Verkettung der Chef-Hierarchie
    durch den NULL-Zeiger - WICHTIG! */
 ang_zeiger->chef->chef->chef = NULL;
 }

Funktion zum Aneinanderhängen zweier Strings in einem neu bereitzustellenden Speicherbereich. Im Gegensatz zur Bibliotheksfunktion strcat ist hier sichergestellt, daß genügend Speicherplatz bereitsteht.

#include <stdlib.h>
#include <string.h>

char *addstring(char *s1, char *s2)
  {
  char *as;
    if ((as=malloc(strlen(s1)+strlen(s2)+1)) != NULL)
      { 
      strcpy(as, s1);
      strcat(as, s2);
      }
  return as;
  }

Die folgende Funktion split teilt eine Zeichenkette an einer vorzugebenden Stelle in zwei Teilstrings. im Beispiel wird auch der Aufruf in main gezeigt.

#include <stdio.h> 
#include <string.h>

char *strchr(char *string_ptr, char find)
  /* Suche Position des Zeichens find im String, falls
     gefunden, Zeiger auf die Position von find, falls
     nicht return(NULL) */
  {
  while (*string_ptr != find)
    {
    if (*string_ptr == '\0')
      return (NULL);          /* nicht gefunden */
    string_ptr++;
    }
  return(string_ptr); 
  }

char * split(char *line, char find)
  {
  char *ptr1; /* Pointer auf den ersten Teil */
  char *ptr2; /* Pointer auf den zweiten Teil */

  ptr1 = line; /* Zeilenanfang */
  ptr2 = strchr(line,find); /* suche Zeichen */

  if (ptr2 == NULL) 
    return(NULL);
  ptr2++;        /* Erstes Zeichen von Teil 2 */
  return(ptr2);
  }
    
int main(void)
  {
  char line[80];  /* Eingabezeile */ 
  char *ptr;      /* Pointer auf den zweiten Teil */

  gets(line, sizeof(line), stdin);
  ptr = split(line,',');

  *(ptr-1) = '\0'; /* Komma auf Zeilenende setzen */
  if (ptr == NULL)
    {
    fprintf(stderr, "FEHLER: Kein Komma in %s\n", line);
    exit (8);
    }
 
  printf("Erster: %s Zweiter: %s\n", line, ptr);
  return (0);
  }

Unterprogramm zum Umwandeln deutscher Umlaute in ihre Ersatzdarstellung:

char* umlaut(char* eingabe)
  {
  int i = 0, j = 0;
  char *tmp1 = NULL;
  char *tmp2 = NULL;

  /* Wir sparen uns das Ermitteln der Laenge des     */
  /* Eingabestrings und nehmen mal den worst case an */
  tmp1= malloc(2*strlen(eingabe) + 1);
  if(tmp1 == NULL) return(NULL);

  /*Kopieren und Ersetzen */
  while(eingabe[i] != '\0')
    {
    switch (eingabe[i])
      {
      case 'ä': tmp1[j++] = 'a'; tmp1[j++]='e'; break;
      case 'ö': tmp1[j++] = 'o'; tmp1[j++]='e'; break;
      case 'ü': tmp1[j++] = 'u'; tmp1[j++]='e'; break;
      case 'Ä': tmp1[j++] = 'A'; tmp1[j++]='e'; break;
      case 'Ö': tmp1[j++] = 'O'; tmp1[j++]='e'; break;
      case 'Ü': tmp1[j++] = 'U'; tmp1[j++]='e'; break;
      case 'ß': tmp1[j++] = 's'; tmp1[j++]='s'; break;
      default: tmp1[j++] = eingabe[i];
      }
     i++;
    }
  tmp1[j] = '\0';
  /* Nun den String genau passend zurueckgeben */
  tmp2 = malloc(strlen(tmp1) + 1);
  if(tmp2 == NULL) return(NULL);
  strcpy(tmp2,tmp1);
  free(tmp1);
  return(tmp2);
  }

Einlesen einer unbekannten Anzahl von Zeilen (max. Anzahl 100). Nach Abschluss der Eingabe erfolgt Ausgabe der Zeilen in umgekehrter Reihenfolge (letzte Zeile zuerst).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXANZ  100
#define MAXLEN  136

int ZeilenEin(char *ZeilFeld[], int Max)
  {
  char *Next, Zeile[MAXLEN];
  int Laenge, ZeilZahl=0;

  while (gets(Zeile) != NULL)
    {
    Laenge = strlen(Zeile);
    if ((ZeilZahl < MAXANZ) && (Next=malloc(Laenge+1)) != NULL)
      { 
      strcpy(Next, Zeile);
      ZeilFeld[ZeilZahl++] = Next;
      }
    else
      return ZeilZahl;
    }
  return ZeilZahl;
  }

void ZeilenAus(char *ZeilFeld[], int Anz)
  { 
  int i;
  for (i = Anz; i > 0; )
    { 
    printf("%s\n",ZeilFeld[--i]);
    free(ZeilFeld[i]);
    }
  }

int main(void)
  { 
  char *Zeilen[MAXANZ];
  int Anz;
  
  Anz=ZeilenEin(Zeilen,MAXANZ);
  printf("\n\n%d Zeilen gelesen\n\n",Anz);
  ZeilenAus(Zeilen,Anz);
 
  return(0);
  }

Das folgende Programmfragment zeigt, wie man Zeichenketten speichersparend verwalten kann. Es wird zunächst nur ein Array von Zeigern definiert. Beim Einlesen der Strings wird jeweils genügend Speicher allokiert und die Zeichenkette an einem Array-Element "aufgehängt".

#include <stdio.h>
#include <string.h>

#define MAX 1000                       /* Maximalzahl Feldelemente */

char *string_feld[MAX];                /* Feld mit String-Zeigern */
int count;                             /* Anzahl der eingeg. Strings */

int  einlesen(char *feld[]);           /* Eingabefunktion */
void ausgabe(int anzahl,char *feld[]); /* Ausgabefunktion */

int main(void)
  {
  count = einlesen(string_feld);
  ausgabe(count,string_feld);
  }

int einlesen(char *feld[])
  /* Liest Strings auf die Variable feld ein, liefert Anzahl zurueck */
  {
  int anz;	                           /* Zaehler fuer Strings */
  char *ptr;                           /* Zeiger zum Pruefen auf EOF */
  char puffer[2000];                   /* Puffer fuer akt. String */
  anz = 0;
  do
    {
    ptr = gets(puffer);                /* String einlesen */
    if (ptr != NULL)                   /* EOF == (ptr == NULL) */
      {                                /* Platz reservieren */
      feld[anz] = (char *) malloc(strlen(puffer) + 1);
                                       /* einschl. '\0' am Ende */
      strcpy(feld[anz],puffer);          /* String in feld kopieren */
      anz++;                           /* Zaehler erhoehen */
      }
    }
  while (ptr != NULL);                 /* lesen bis EOF */
  return(anz);
  }

void ausgabe(int anzahl,char *feld[])
  /* Ausgabe der Strings in feld, der Parameter gbt die Anzahl an */
  {
  int i;
  puts("\n");                          /* Leerzeile */
  for (i=0; i<anzahl; i++)
    puts(feld[i]);                     /* haengt \n automatisch an */
  }

Im letzten Beispiel wird dynamisch Speicherplatz für Objekte bereitgestellt, die den Datentyp double besitzen und die Form von ein- und zweidimensionalen Feldern haben. Die Größe der Felder, d. h. Zeilenzahl und Spaltenzahl der Matrix und Anzahl der Komponenten des Arrays werden erst zur Laufzeit des Programms eingelesen. Anschließend werden die eingelesenen Zahlenwerte als Matrixelemente, bzw. Arraykomponenten abgelegt und wieder ausgedruckt. Am Ende wird der bereitgestellte Speicherplatz wieder freigegeben.

   
#include <stdio.h>
#include <stdlib.h> 
main()
  {
  double **a,    /* Eingabematrix */
           *u;    /* Eingabevektor */
  int i,j,n,m;

  printf("Zeilenzahl m und Spaltenzahl n eingeben: ");
  scanf("%d %d", &m, &n);
 
  /*   Speicherplatz bereitstellen   */
  a = (double **) malloc (m*sizeof(double *));
  for (i=0; i<m; i++)
    a[i] = (double *) malloc (n*sizeof(double)); 
  u = (double *) malloc (n*sizeof(double));

  printf("%dx%d Matrix A eingeben\n", m, n);
  for(i=0; i<m; i++) 
    for(j=0; j<n; j++)
      scanf("%lf", &a[i][j]);

  printf("%d Vektor u eingeben\n", n);
  for(j=0; j<n; j++)
    scanf("%lf", &u[j]);

  printf("Eingabedaten: Matrix A\n");
  for(i=0; i<m; i++) 
    {
    for(j=0; j<n; j++)   
      printf("%f ", a[i][j]);
    printf("\n");
    }

  printf("Eingabedaten: Vektor u\n");
  for(j=0; j<n; j++)
    printf("%f ", u[j]);
  printf("\n");
 
  /*   Speicherplatz freigeben   */
  for (i=0; i<m; i++)
    free (a[i]);
  free (a);
  free (u);
  }
Hier zeigt a auf m Speicherplätze vom Datentyp (double *). Jeder dieser Speicherplätze a[i] ist selbst Zeiger auf n Speicherplätze vom Datentyp double. Ebenso zeigt u auf n Speicherplätze vom Datentyp double.

Zeiger und Strukturen

Im folgenden Beispiel wird eine Struktur für eine Adresse verwendet:
struct kunde
  {
  int kundennr;
  char vorname[15];
  char nachname[20];
  char strasse [25];
  char plz[5];
  char ort[25];
  };
Im folgenden Programmbeispiel geht es darum, eine beliebige Zahl von Adressen einzulesen. Zur Speicherung wird kein Array verwendet, sondern ein zusammenhängender Speicherbereich, der dynamisch allokiert wird und über einen Pointer angesprochen werden kann. Dazu wird zuerst gefragt, wieviele Adressen eingegeben werden sollen und dementsprechnde Speicherplatz mit malloc() belegt.

Bei der Verwendung von Zeigern im Zusammenhang mit Strukturen wird häufig folgender Ausdruck benötigt, um auf eine einzelne Komponente zuzugreifen:

(*zeiger).strukturkomponente
Dafür gibt es folgende abgekürzte Schreibweise:
zeiger -> strukturkomponente
Die Zeichen - und > bilden einen Pfeil und bringen so sehr anschaulich das Zeigerkonzept zum Ausdruck.

Da wir nicht nur eine Strukturvariable haben, sondern (implizit) ein ganzes Feld, benötigen wir Schleifen zum Einlesen und Ausdrucken. Der Schleifenindex i beginnt bei 0 und wird so lange erhöht, bis die Anzahl der Kunden erreicht ist. Dieser Index muß zum Wert des Zeigers addiert werden, so daß Ausdrücke der Form

(zeiger + i) -> strukturkomponente
entstehen. Hat i beispielsweise den Wert 3, dann wird der Wert 3 * sizeof(struct kunde) zum Zeiger addiert. Der Zeiger zeigt dadurch auf das drittnächste Element.
int main(void)
  {
  struct kunde
    {
    int kundennr;
    char vorname[15];
    char nachname[20];
    char strasse [25];
    char plz[5];
    char ort[25];
    };

  char antwort,hilf[5];
  int i, anzahl;
  struct kunde *person;

  printf("Wie viele Kunden wollen Sie eintragen? ");
  scanf("%d",&anzahl); 
  gets(hilf); /* Liest RETURN-Zeichen, alternativ fflush(stdin) */

  /* Platz fuer anzahl kunden reservieren */
  person = (struct kunde *) malloc(anzahl * (sizeof(struct kunde)));

  printf("Geben Sie die Kundendaten ein:\n");
  for (i = 0; i < anzahl; i++)
    {
    printf("\nKundennummer: "); gets(hilf);
    (person+i)->kundennr = atoi(hilf);
    printf("\nVorname.....: "); gets((person+i)->vorname);
    printf("\nNachname....: "); gets((person+i)->nachname);
    printf("\nStraße: "); gets((person+i)->strasse);
    printf("\nPostleitzahl: "); gets((person+i)->plz);
    printf("\nWohnort.....: "); gets((person+i)->ort);
    }

  printf("\n\nSie haben folgende Daten eingegeben:\n");
  for (i = O; i < anzahl; i++)
    {
    printf("\nKundennummer: %d\n",(person+i)->kundennr);
    printf("%s %s\n",(person+i)->vorname,(person+i)->nachname);
    printf("%s\n",(person+i)->strasse);
    printf("%s %s\n",(person+i)->plz,(person+i)->ort);
    }

  return(0);
  }

Beispiel: Anwendung von Bibliotheksfunktionen. In time.h sind Funktionen und Strukturen zur Ermittlung von Datum und Uhrzeit enthalten. Für die Ausgabe braucht man einn Zeiger tp, der auf eine in time.h definierte Struktur tm zeigt.

#include <stdio.h>
#include <time.h>

time_t zeitpunkt;  /* time_t definiert in time.h */
struct tm *tp;     /* tm definiert in time.h */

int main(void)
  {
  time(&zeitpunkt);           /* Uhrzeit und Datum im internen Format:
                              /* Sekunden seit 1.1.1970 GMT */
  printf("%ld\n",zeitpunkt);  /* soviele sind es */

  tp=localtime(&zeitpunkt);   /* umwandeln im etwas Lesbareres */

  printf("Uhrzeit: %02d:%02d:%02d\n",
          tp->tm_hour, tp->tm_min, tp->tm_sec);

  printf("Tag: %02d\n",tp->tm_mday);
  printf("Monat: %02d\n",tp->tm_mon+1);
  printf("Jahr: %04d\n",tp->tm_year+1900);

  return(0);
  }

Gefahren bei der Rückgabe von Zeigern

Bei Zeigern, die als Rückgabewert einer Funktion dienen, muß darauf geachtet werden, daß sie nicht alf lokale Objekte verweisen, die nach dem Funktionsaufruf verschwunden sind.

Negativ-Beispiel 1: Die lokale Variable existiert nur während des Funtionsaufrufs. Die Ausgabe ist unbestimmt, sie könnte zufällig auch 1234 lauten.

int * murks(void)
  {
  int x = 1234;

  return(&x);
  }

int main(void)
  {
  int * ip;

  ip = murks();
  printf("%d\n",*ip);
  }

Negativ-Beispiel 2: Die Funktion soll ein Duplikat des als Parameter übergebenen Strings erzeugen und einen Zeiger darauf zurückgeben. Der Speicherplatz für das lokale Feld neu wird jedoch beim Verlassen der Funktion wieder freigegeben und die Kopie zerstört.

char * murks(char * txt)
  {
  char neu[100];
  char *n;
  
  n = neu;
  while ((*n++ = *txt++) != '\0'); /* kopieren */
  return(n);
  }

int main(void)
  {
  char * strp;

  strp = murks("Autsch!");
  printf("%ds\n",strp);
  }

Besser wäre es, dynamisch Speicher zu allocieren und den Pointer zurückzugeben, dann entspricht die Funktion der Bibliotheksroutine strdup:

char * kein_murks(char * txt)
  {
  char *n;
  
  n = (char *) malloc(strlen(txt) + 1);
  while ((*n++ = *txt++) != '\0'); /* kopieren */
  return(n);
  }

int main(void)
  {
  char * strp;

  strp = kein_murks("So geht es!");
  printf("%ds\n",strp);
  }

7.2 Dynamische Datenstrukturen

Dynamische Datenstrukturen ändern ihre Struktur und den von ihnen belegten Speicherplatz während der Programmausführung. Sie sind aus einzelnen Elementen, den sogenannten 'Knoten', aufgebaut, zwischen denen üblicherweise eine bestimmte Nachbarschafts-Beziehung (Verweise) besteht. Die Dynamik liegt im Einfügen neuer Knoten, Entfernen vorhandener Knoten und Änderung der Nachbarschaftsbeziehung. Der Speicherplatz für einen Knoten wird erst bei Bedarf zur Programmlaufzeit allokiert. Es handelt sich hierbei um Strukturen, die als 'listen' oder 'Bäume' bezeichnet werden.

Weiterhin ist der Speicherort der einzelnen Knoten im voraus nicht bekannt (und interessiert auch nicht. Benachbarte" Knoten, d. h. Knoten, die logisch nebeneinander angeordnet sind, liegen in der Regel weit auseinander. Die Beziehung der einzelnen Knoten untereinander wird sinnvollerweise über Zeiger hergestellt, die jeweils auf den Speicherort des " logischen Nachbarn" zeigen. Jeder Knoten wird daher neben den jeweils zu speichernden Nutzdaten mindestens einen Zeiger auf die jeweiligen "Nachbar"-Knoten enthalten. Man nenn solche Strukturen daher auch "verkettete Datenstrukturen" oder auch "selbstbezügliche (rekursive) Datenstrukturen". Die wichtigsten dieser Datenstrukturen sind :

Die wichtigsten Operationen mit dynamischen Datenstrukturen sind :

In C lassen sich die einzelnen Elemente (Knoten) durch structures darstellen. Zur Realisierung verketteter Datenstrukturen müssen diese structures Pointer auf structures ihres eigenen Typs enthalten. Beispiel:

struct datum 
  {
  int tag;
  int monat;
  int jahr;
  int jahrestag;
  char mon_name[4];
  struct datum heute;         /* FALSCH */
  };

struct datum 
  {
  int tag;
  int monat;
  int jahr;
  int jahrestag;
  char mon_name[4];
  struct datum *heute;        /* RICHTIG */
  };

Aufbau verketteter Datenstrukturen mittels rekursiver Strukturen

Einfach verkettete Liste

struct listelement
  { 
  int inhalt;
  struct listelement *next;
  };

Doppelt verkettete Liste

struct listelement
  { 
  int inhalt;
  struct listelement *next;
  struct listelement *back;
  };

Binärer Baum

struct baumelement
  { 
  int inhalt;
  struct baumelement *rechts;
  struct baumelement *links;
  };

7.3 Lineare Listen

Die Operationen auf einer linearen Liste lassen sich am besten anhand eines Beispiels erarbeiten. Wir betrachten Elemente, in denen jeweils die relevanten Informationen in Form einer Zeichenkette enthalten sind. Die Listenelemente können wir uns dann anschaulich folgendermaßen vorstellen:

Information des Listenelements (hier: Zeichenkette, z. B. ein Name) Zeiger auf nachfolgendes Element

Die Deklaration der entsprechenden Struktur sieht folgendermaßen aus:

struct LinListe 
  { 
  char inhalt[80]; 
  struct LinListe *next;
  };
Beim Aufbau der verketteten Liste können die einzelnen Listenelemente gleich nach einem bestimmten Ordnungskriterium (hier: Stringvergleich) hintereinander gehängt werden:

Das Symbol für elektrische Masse am Ende des letzten Elements deutet dabei den NULL-Zeiger an, der das Ende der Liste anzeigt.

Da jedes Element auf seinen Nachfolger zeigt, kann die ganze Liste an einem einzigen Zeiger hängen. Dieser Zeiger ist der Ursprung der Liste, oder auf englisch "root". root ist in den folgenden Beispielen eine globale Variable. Zeiger auf Listenelemente sind Zeiger auf Strukturvariable.

Wenn die Liste aufgebaut ist, so können in einer Schleife alle Elemente durchgegangen und der Inhalt ausgegeben oder geändert werden, wie das folgende Programmfragment, die Ausgabe der Liste, zeigt:

void DruckeListe(struct LinListe *root)
  { 
  struct LinListe *Tail = root;
  while (Tail != NULL)
    {
    printf("%s\n", Tail->inhalt);
    Tail = Tail->next;
    }
  }
Der Zeiger Tail (Schwanz) wird benötigt, um an der Liste entlang zu gehen. Tail kann so lange auf das jeweils nächste Element gesetzt werden, bis das Listenende erreicht ist. Den NULL-Zeiger zu dereferenzieren, würde zu einem Laufzeitfehler führen.

Man kann die Ausgabe auch rekursiv formulieren:

void DruckeListeRecursiv(struct LinListe *root)
  {
  if (root != NULL) 
    {
    printf("%s\n", root->inhalt);
    DruckeListeRecursiv(root->next);
    }
  } 
Als nächstes wollen wir ansehen, wie ein neuer Auftrag in die Liste eingefügt wird. Wir nehmen jetzt an, daß die Liste so aufgebaut wird, daß die Elemente der lexikalischen Ordnung nach geordnet sind. Man nennt diesen Vorgang auch "Sortieren durch Einfügen".
Für ein neues Listenlelement wird zuerst eine neue Datenstruktur angelegt. Wenn kein Speicherbereich ausreichender Größe zur Verfügung steht, dann ist der Rückgabewert der NULL-Zeiger.
struct LinListe *Erzeuge(char *String)
  /* erzeugt und initialisiert neues Element in der Liste */
  { 
  struct LinListe *Neu;
  Neu = (struct LinListe *) malloc(sizeof(struct LinListe));
  if (Neu == NULL)
    {  
    printf("Speicher voll, Abbruch...\n");
    exit(1);
    }
  Neu->next = NULL;
  strcpy(Neu->inhalt,String);
  return(Neu);
  }
Um das neue Element einzufügen, wird ein weiterer Zeiger verwendet, der so lange von einem Element zum Nächsten bewegt wird, bis die Stelle gefunden ist, an der das neue Element einzufügen ist. Der Zeiger heißt im Beispiel "Tail", als Abkürzung für Rest der Liste ("Schwanz").
Ist die passende Stelle gefunden, so wird das neue Element durch Ändern von zwei Zeigern eingefügt. Dabei sind drei Fälle zu unterscheiden.

Die Liste ist noch leer:
root zeigt auf den Nullpointer. In diesem Fall muß root auf das erste neu erzeugte Element zeigen:

Es wird am Anfang der nichtleeren Liste eingefügt:
In diesem Fall ändert sich der Wert von root.

Einfügen innerhalb der Liste:
Hier ist es egal, ob irgendwo zwischen zwei Elementen eingefügt wird oder ob das am Listenende geschieht.

Für alle drei Einfügevarianten schreiben wir eine einzige Funktion:

struct LinListe *ElementEinfuegen(struct LinListe *root, struct LinListe *Neu)
  { 
  struct LinListe *Tail;
  /* Wenn Neu das erste Listenelement werden muss, */
  /*dann ist die globale Variable root zu aendern */

  if (root == NULL)
    /*Liste noch leer, erstes Element, root wird neues Element */
    root = Neu;
  else if (strcmp(Neu->inhalt, root->inhalt) < 0)
    /* Einfuegen vor dem ersten Element, root aendert sich */
    {
    Tail = root;
    root = Neu;
    Neu->next = Tail;
    }
  else
    {
    Tail = root;
    /* Element suchen, nach dem Neu einzuhaengen ist */
    while ((Tail->next != NULL) && (strcmp(Tail->next->inhalt, Neu->inhalt) < 0)) 
      /*Reihenfolge beachten: erst auf NULL testen */
      Tail = Tail->next;
    /* Neu nach Tail einhaengen */
    Neu->next = Tail->next;
    Tail->next = Neu;
    }
  return(root);
  }
Wird kein Sortierkriterium benötigt, wird immer am Ende ein neues Listenelement angehängt. Die Funktion vereinfacht sich dann entsprechend:
struct LinListe *ElementAnhaengen(struct LinListe *root, struct LinListe *Neu)
  { 
  struct LinListe *Tail;
  /* Wenn Neu das erste Listenelement werden muss, */
  /*dann ist die globale Variable root zu aendern */

  if (root == NULL)
    /*Liste noch leer, erstes Element, root wird neues Element */
    root = Neu;
  else
    {
    Tail = root;
    /* Element suchen, nach dem Neu einzuhaengen ist */
    while (Tail->next != NULL) 
      Tail = Tail->next;
    /* Neu nach Tail einhaengen */
    Neu->next = Tail->next;
    Tail->next = Neu;
    }
  return(root);
  }

Zum Schluß noch das Testprogramm (ohne Funktionsdefinitionen):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct LinListe 
  { 
  char inhalt[80];
  struct LinListe *next;
  };

struct LinListe *root = NULL;

/* Hier Funktionen einfuegen, siehe oben */

int main(void)
  { 
  char str[80];
  struct LinListe *Neu;

  while(1)
    {
    if (gets(str) != NULL)
      {
      Neu = Erzeuge(str);
      root = ElementEinfuegen(root, Neu);
      }
   else break;
    }

  printf("\n");
  DruckeListe(root);

  return(0);
  }

Bei linearen Listen ist das Löschen von Einträgen auch noch recht einfach. Es muß lediglich der Zeiger des Vorgängers auf den Nachfolger gesetzt werden.

Da jedoch bei einer einfach verketteten Liste kein Verweis auf den Vorgänger existiert (da bräuchte man doppelt verkettete Listen), wir ein Trick verwendet. Beim durch die Liste laufende Zeiger Tailwird nicht der Inhalt des durch den Zeiger referierten Objektes untersucht, sondern der Inhalt des Nachfolgers. Dadurch zeigt Tail immer auf den Vorgänger des zu löschenden Elements und das "Ausklinken" ist ganz einfach.

Da nun erst ab dem zweiten Element der Liste nach dem zu l&öuml;schenden Element gesucht wird, muß das erste Element getrennt untersucht werden. Das ist aber sowieso notwendig, da sich dann der Wert von root ändert. Die Funktion sieht dann so aus:

struct LinListe *ElementLoeschen(struct LinListe *root, char *key)
  { 
  struct LinListe *Tail, *Hilf;
  
  if (strcmp(key, root->inhalt) == 0)
    /* Erstes Element loeschen, root aendert sich */
    {
    Hilf = root;
    root = root->next;
    free(Hilf);
    }
  else
    {
    Tail = root;
    while ((Tail->next != NULL) && (strcmp(Tail->next->inhalt, key) != 0)) 
      /*Reihenfolge beachten: erst auf NULL testen */
      Tail = Tail->next;
    if (Tail->next != NULL)
      {
      Hilf = Tail->next;
      Tail->next = Tail->next->next;
      free(Hilf);
      }
    else
      {
      printf("%s not found!\n",key);
      }
    }
  return(root);
  }

Beispiel: Keller, realisiert mit linearer Liste

Ein Keller, auch Stapel oder Stack genannt, ist eine besondere lineare Liste, bei der die Daten eine Folge bilden, die nur durch die Ein- und Ausfügeoperationen bestimmt wird. Das Prinzip eines Kellers besteht darin, daß stets das zuletzt eingefügte Element eines Kellers als erstes wieder entfernt werden muß. Die Kellerverwaltung arbeitet nach dem LIFO-Prinzip: Last In First Out. Was zuletzt in den Keller kam, kommt zuerst aus dem Keller auch wieder raus.

Man kann sich einen Keller als eine Bücherkiste vorstellen, in die man einzeln Bücher legen und wieder herausnehmen kann. Üblicherweise stehen folgende fünf Kelleroperationen zur Verfügung:

Init
Push
Pop
Top
Empty
Initialisiert einen neuen Keller.
Legt ein Element auf dem Keller ab.
Holt ein Element aus dem Keller.
Liest das oberste Element im Keller.
Prüft, ob ein Keller leer ist.

Datenstruktur für einen Keller

Man nutzt auch hier dynamisch verkettete lineare Listen. Die Elemente einer solchen Liste müssen außer der Nutzinformation noch einen Zeiger auf das nachfolgende Kellerelement speichern.
struct TStack
  {
  char Daten[100];
  struct TStack *Next;
  };

Implementierung der Kelleroperationen

Die Implementierung der Kelleroperationen beginnen wir mit den einfachen Funktionen Init und Empty.

Init erledigt sich durch die Definition der Kellervariablen:

struct TStack *Keller = NULL;
Empty degeneriert zur Abfrage Keller == NULL. Interessanter sind Push und Pop:

Etwas schwieriger ist die Implementierung der Push-Operation. Bevor die Daten im Keller abgelegt werden können, müssen Sie in einen Element-Verbund verpackt werden. Dazu erzeugen wir mittels New ein neues Element und legen in ihm die Daten ab. Im Bild liegen die beiden Werte 'X' und 'Y' auf dem Keller. Das 'Z' soll als nächstes auf den Keller gelegt werden. Das neue Element muß das oberste Kellerelement werden. Dies gelingt durch zwei Zeigeroperationen:

  1. Durch Setzen des bislang undefinierten Zeigers auf das bislang oberste Kellerelement werden die alten Kellerelemente Nachfolger des neuen Elements.
  2. Durch Umsetzen des Kellerzeigers vom alten Kelleranfang auf den neuen Kelleranfang wird das neue Element in den Keller aufgenommen.

void Push(char *Daten)
  {
  struct TStack *Element;

  Element = (struct TStack *) malloc(sizeof(struct TStack));
  strcpy(Element->Daten, Daten);
  Element->Next = Keller;
  Keller = Element;
  }

Pop muß das oberste Kellerelement vom Keller wegnehmen und dessen Daten zurückliefern. Wenn der Keller leer ist, geben wir eine Fehlermeldung aus. Bei Bedarf kann der Programmierer mit Empty Auskunft über den Keller erhalten. Wenn er dennoch durch falsche Programmierung eine Pop-Operation auf einem leeren Keller ausführt, wird er auf seinen Programmierfehler durch eine Fehlermeldung hingewiesen.

Das Abhängen des obersten Kellerelements erfolgt durch drei Zeigeroperationen:

  1. Kellerelement an Hilfsvariable Element binden.
  2. Keller-Zeiger auf nächstes Kellerelement verbiegen.
  3. Speicher des alten Kellerelements freigeben.

void Pop(char *Resultat)
  {
  struct TStack *Element;

  if (Keller == NULL)
    printf("Der Keller ist leer!\n");
  else
    {
    strcpy(Resultat, Keller->Daten);
    Element = Keller;
    Keller = Keller->Next;
    free(Element);
    }
  }

Ein Testprogramm sieht dann z. B. so aus:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct TStack
  {
  char Daten[100];
  struct TStack *Next;
  };

struct TStack *Keller = NULL;

void Push(char *Daten);

void Pop(char *Resultat);

int main(void)
  {
  char str[100];

  Push("Hallo");
  Push("Sie da!");
  Push("Ja, Sie!");

  while (Keller != NULL)
    {
    Pop(str);
    printf("%s\n",str);
    }

  return(0);
  }
Das Ergebnis ist dann:
Ja, Sie!
Sie da!
Hallo

Ähnlich aufgebaut ist eine Queue, bei der die Elemente in der Reihenfolge ausgegeben werden, in der Sie eingegeben wurden. Das folgende Programm zeigt die Realisierung einer Queue, deren Elemente aus einem String und einem Longintwert bestehen, der eine Zeitangabe repräsentiert (Sekunden seit 1.1.1970).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Queue 
  { 
  char Nachricht[80];
  long Datum;
  struct Queue *Next;
  };

struct Queue *root = NULL; /* Global! */

struct Queue *New(char Nachricht[], long Datum)
  /* erzeugt und initialisiert neues Element in der Liste */
  { 
  struct Queue *Neu;
  Neu = (struct Queue *) malloc(sizeof(struct Queue));
  if (Neu == NULL)
    {  
    printf("Speicher voll, Abbruch...\n");
    exit(1);
    }
  Neu->Next = NULL;
  strcpy(Neu->Nachricht,Nachricht);
  Neu->Datum = Datum;
  return(Neu);
  }

void Append(struct Queue *Neu)
  { 
  struct Queue *Tail;
  /* Wenn Neu das erste Listenelement werden muss, */
  /*dann ist die globale Variable root zu aendern */
  if (root == NULL)
    /*Liste noch leer, erstes Element, root wird neues Element */
    root = Neu;
  else 
    /* Einfuegen vor dem ersten Element */
    {
    Tail = root;
    root = Neu;
    Neu->Next = Tail;
    }
  }

struct Queue *Remove(void)
  { 
  struct Queue *Tail, *Hilf;
  
  if (root->Next == NULL)
    /* Erstes Element loeschen */
    {
    Hilf = root;
    root = NULL;
    return(Hilf);
    }
  else
    {
    Tail = root;
    while (Tail->Next != NULL) 
      { Hilf = Tail; Tail = Tail->Next; }
    Hilf->Next = NULL;
    return(Tail);
    }
  }

void Print(void)
  { 
  struct Queue *Tail = root;
  while (Tail != NULL)
    {
    printf("   %s - %ld\n", Tail->Nachricht, Tail->Datum);
    Tail = Tail->Next;
    }
  }

int IsEmpty(void)
  {
  if (root == NULL) return(1);
  else return(0);
  }

int main(void)
  { 
   struct Queue * X;

  Append(New("eins",1));
  printf("\n");
  Print();

  Append(New("zwei",2));
  printf("\n");
  Print();

  Append(New("drei",3));
  printf("\n");
  Print();

  Append(New("vier",4));
  printf("\n");
  Print();

  while (! IsEmpty())
    {
    X = Remove();
    printf("\n--- %s - %ld\n",X->Nachricht,X->Datum);
    Print();
    free(X);
    }

  return(0);
  }
Das nächste Beispiel beschreibt einen Ringpuffer. Ein Ringpuffer ist ein nach dem FIFO-Prinzip (first-in-first-out) organisierter Speicher zur Pufferung von Daten, die z. B. von einem Meßdatenerfassungsprogramm "angeliefert" werden, und zeitversetzt von einem Auswertungsprogramm weiterverarbeitet werden. Auch in der Betriebssystemsoftware für Netzwerkinterfaces und für die Datenfernübertragung finden Ringpuffer Verwendung.

Der Puffer muß groß genug sein, damit er auch bei maximalem Zeitversatz nicht "überläuft". Durch Realisierung als lineare Liste ist dieser Fall immer gegeben, da man zu der Liste immer neue Elemente hinzufügen kann (jedenfalls, solange noch Arbeitsspeicher vorhanden ist).

Es muß dafür gesorgt werden, daß aus einem leeren Puffer nicht mehr gelesen wird -> Meldung "Puffer leer".

Die Abbildung des Rings in einem linear adressierten Speicher geschieht dadurch, dass ein am Pufferende angekommener Zeiger wieder an den Anfang gesetzt wird. Das Letze Element der Lister verweist also wieder auf das erste.

Der Ringpuffer soll für die Verwaltung einer ständig wechselnden Anzahl von Nachrichten verwendet werden, wie es z. B. bei E-Mails der Fall ist. Die Nachrichten treffen als Texte unterschiedlicher Länge ein. Um sie effektiv zu verwalten und zu speichern, liegt die Verwendung von verketteten Listen nahe. Um aus diesen Listen einen Ringpuffer zu machen, muß folgendermaßen vorgegangen werden:

Es wird bei der ältesten Nachricht begonnen. Im Verlauf des Programms wird die jeweils aktuelle Nachricht angezeigt. Zur Interaktion mit dem Benutzer bietet das Hauptprogramm folgendes Menü an:

Nur zum Zweck der Simulation des Nachrichtenempfangs wird noch ein weiterer Menüpunkt benötigt:

Der Pufferzustand soll ständig erkennbar sein, d. h. in einer sog. Statuszeile wird die Zahl alter/neuer (= gelesener/ungelesener) Nachrichten angezeigt.


#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LEN 80

#define NEUE_NACHRICHT 0
#define GELESEN 1

struct Message
  {
  char nachricht[MAX_LEN];
  int status;
  struct Message *next;
  };



struct Message *Nachricht_Einfuegen(struct Message *root)
  /* Neue Nachricht einfuegen */
  {
  struct Message *p_tmp;

  p_tmp = (struct Message *) malloc(sizeof(struct Message));
  if (p_tmp == NULL)
    {
    printf("Speicher voll!\n");
    return(NULL);
    }

  printf("Text eingeben: ");
  fgets(p_tmp->nachricht, MAX_LEN-1, stdin);
  /* Newline weg */
  p_tmp->nachricht[strlen(p_tmp->nachricht)-1] = '\0';
  p_tmp->status = NEUE_NACHRICHT;

  if (root == NULL)
    {
    p_tmp->next = p_tmp;
    }
  else
    {
    p_tmp->next = root->next;
    root->next = p_tmp;
    }
  return(p_tmp);
  }

/* Display a Message */
struct Message *Zeige_Nachricht(struct Message *aktmsg)
  {
  if (aktmsg != NULL)
    {
    printf(">>>: %s\n",aktmsg->next->nachricht);
    aktmsg->next->status = GELESEN;
    return(aktmsg->next);
    }
  else
    {
    printf("### Keine Nachrichten gespeichert!\n");
    return(NULL);
    }
  }


struct Message *Loesche_Nachricht(struct Message *aktmsg)
  /* Nachricht entfernen */
  {
  struct Message *ptmp1, *ptmp2;

  if (aktmsg == NULL)
    {
    printf("### Keine Nachrichten gespeichert!\n");
    return(NULL);
    }
  ptmp1 = aktmsg->next; /* Nachfolger von aktmsg */
  ptmp2 = aktmsg;
  while (ptmp2->next != aktmsg)
    ptmp2 = ptmp2->next; /* einmal rum - Vorgaenger von aktmsg */
  ptmp2->next = ptmp1;   /* Aushaengen */
  if (ptmp1 == aktmsg)
    ptmp1 = NULL; /*Letzte Nachricht */
  free(aktmsg);
  return(ptmp1);
  }

struct Message *Loesche_Alles(struct Message *aktmsg)
  {
  while (aktmsg != NULL)
    aktmsg = Loesche_Nachricht(aktmsg);
  return(aktmsg);
  }

void Print_Status(struct Message *start)
  {
  struct Message *ptmp;
  int msg_count = 0;  /* Nachrichten gesamt */
  int read_count = 0; /* Nachrichten gelesen */

  if (start != NULL)
    {
    ptmp = start;
    do
      {
      msg_count++;
      if (ptmp->status == GELESEN) read_count++;
      ptmp = ptmp->next;
      }
    while (ptmp != start);
    }
  printf("*** %d Nachrichten, davon %d gelesen und %d ungelesen.\n",
         msg_count, read_count, msg_count-read_count);
  }


int main(void)
  {
  struct Message *rptr = NULL;
  int befehl = '.';

  while (befehl!='Q')
    {
    printf("\n");
    Print_Status(rptr);
    printf("Befehl: ");
    befehl=toupper(getchar());
    while (getchar()!='\n');  /* bis Zeilenende ueberlesen */

    switch(befehl)
      {
      case 'Q': break;
      case 'D': rptr = Loesche_Nachricht(rptr); break;
      case ' ': rptr = Zeige_Nachricht(rptr); break;
      case 'X': rptr = Loesche_Alles(rptr); break;
      case 'E': rptr = Nachricht_Einfuegen(rptr); break;
      default:  printf("### Falsche Eingabe!\n");
      }
    }
  return (0);
  }

7.4 Baumstrukturen

Baumstrukturen spielen in der Datenorganisation eine wichtige Rolle. Auch im Alltag sind diese Strukturen allgegenwärtig:

Ein Baum ist folgendermaßen definiert:

Ein Baum vom Grad 2 ist der binäre Baum. Er stellt einen wichtigen Sonderfall dar. Gegenüber dem allgemeinen Baum zeichnet er sich folgendermaßen aus:

Offensichtlich kann man in einem binären Baum der Tiefe n höchstens 2n-1 Knoten unterbringen. Bei einem unvollständigen Baum sind es entsprechend weniger. Im ungünstigsten Fall lassen sich nur n Elemente unterbringen, genau dann, wenn jeder Knoten nur einen Nachfolger besitzt. In diesem Fall degeneriert der Baum zu einer linearen Liste.

Die Baumstruktur ist rekursiver Natur, da die Nachfolger eines Knotens jeweils wieder als Wurzeln von Bäumen aufgefasst werden können. Diese Baum-Teilbaum-Beziehung lässt sich rekursiv wie folgt definieren:

Ein Binärbaum ist entweder leer oder er besteht aus einer Wurzel, die über bis zu zwei Kanten mit disjunkten nicht-leeren Binärbäumen verbunden ist.

Wenn die Knoteninhalte untereinander keine Ordnung besitzen ist der Baum ungeordnet. Bei geordneten Bäumen ist eine vollständige Ordnungsrelation zwischen den Knoten Voraussetzung. Sie wird oft über einen Schlüssel realisiert. Beispiele für Ordnungsschlüssel sind:

Lege nun "<" eine vollständige Ordnungsrelation zwischen den Knoten eines Binärbaums fest, dann ist jeder Knoten des Baums kleiner als alle Knoten seines rechten Teilbaums und zugleich größer oder gleich als alle Knoten seines linken Teilbaums.

Offensichtlich gilt für jeden Knoten, daß ein kleinerer Nochfolgeknoten links und ein größerer rechts unterhalb anzuordnen ist. Nach der verwendeten Ordnungsrelation ist im Fall der Gleichheit ein Nachfolgeknoten ebenfalls links unterhalb anzuordnen.

Der Suchbaum

Der geordnete Baum erlaubt eine schnelle Suche von Informationen, ähnlich wie das binäre Suchverfahren in linearen Strukturen. Der geordnete Baum heißt deshalb auch Suchbaum. Dazu ein Beispiel. Gegeben sei folgende lineare Liste:

Der direkte Zugriff auf die Elemente der Liste sei möglich. Bei der binären Suche wird auf das mittlere Element zugegriffen. Das Anfangselement ist somit nicht mehr A sondern E, das die Liste in zwei Teile teilt:

Ist E nicht das gesuchte Element, wird in der linken oder rechten Teilliste weitergesucht.

Teilt man die Liste solange nach diesem Verfahren, bis diese nur noch aus je einem Element bestehen, so liegt ein geordneter Baum in der bekannten Darstellung vor.

Dieser Baum ist so geordnet, dass alle Elemente im rechten Teilbaum einen lexikalisch größeren Wert besitzen als die Wurzel, und alle Elemente im linken Teilbaum einen kleineren. Dies gilt auch für alle Teilbäume. Einen derart georneten Baum bezeichnet man als binären Suchbaum. Damit wurde die linear verkettete Struktur, auf der im Prinzip nur sequentiell gesucht werden kann, durch Umorganisation in eine binäre Baumstruktur überführt, auf der der Suchvorgang aufgrund der Ordnungsrelation binär verläuft.

Auch bei der Datenstruktur Baum besteht die Notwendigkeit, Elementinhalte dauerhaft zu speichern und bei Bedarf zu laden. Dies führt zu dem grundsätzlichen Problem, die binär verzweigte Struktur so in eine lineare Struktur zu überführen, dass die Daten so als Datei gespeichert werden können, dass später daraus der ursprüngliche Binärbaum wieder rekonstruiert werden kann. Gegeben sei folgender Binärbaum:

Beispielimplementierung: Einfügen in einen Baum und Ausgabe des Baums:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXLEN 80

struct tnode              /* Binaerbaum */
  {
  char word[MAXLEN];      /* Text */
  int count;              /* Zaehler */
  struct tnode *left;     /* linker Nachfolger */
  struct tnode *right;    /* rechter Nachfolger */
  };

struct tnode *talloc(void);
  /* Platz fuer einen tnode besorgen */

struct tnode *insert_tree(struct tnode *p,char *w);
  /* w bei oder nach p einfuegen */

void treeprint(struct tnode *p);
  /* Baum p rekursiv ausgeben */


int main(void)  
  {
  struct tnode *root;
  char word[MAXLEN];

  root = NULL;
  while ((gets(word)) != NULL)  /* Baum einlesen */
    root = insert_tree(root,word);

  printf("\n");
  treeprint(root);              /* Baum ausgeben */

  return(0);  
  }


struct tnode *talloc(void)
  /* Platz fuer einen tnode besorgen */
  {
  return((struct tnode *) malloc(sizeof(struct tnode)));
  }


struct tnode *insert_tree(struct tnode *p,char *w)
  /* w bei oder nach p einfuegen */
  {
  struct tnode *talloc();
  int cond;
  if (p == NULL) 
    {                           /* ein neues Wort */
    p = talloc();   /* einen neuen Knoten anlegen */
    strcpy(p->word, w);
    p->count = 1;
    p->left = NULL;
    p->right = NULL;
    } 
  else
    { 
    cond = strcmp(w, p->word);   /* Vergleich w mit Knoten */
    if (cond == 0)
      p->count++;               /* Wort w schon vorhanden */
    else if (cond < 0)         /* w ist kleiner, links darunter */
      p->left = insert_tree(p->left,w);
    else                        /* w ist groesser, rechts darunter */
      p->right = insert_tree(p->right,w);
    }
  return(p);
  }

void treeprint(struct tnode *p)
  /* Baum p rekursiv ausgeben */
  {
  if (p != NULL) 
    {
    treeprint(p->left);
    printf("%s (%4d)\n", p->word, p->count);
    treeprint(p->right);
    }
  }

Ein weiteres Beispiel: Das folgende Programm liest aus der Quellatei die Wörter und gibt sie lexikographisch sortiert mit Häufigkeitsangabe in die Zieldatei aus.


#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#define MAX_WORT 200

struct tnode 
  {
  char *wort;
  int zahl;
  struct tnode *links;
  struct tnode *rechts;
  };

typedef struct tnode knoten;

FILE *quelle, *ziel;

FILE *eopen(char *file,char *mode);
int lieswort(char *w, int lim);
knoten *suche(knoten *p, char *w);
inorder(knoten *p);
char *merke_wort(char *s);


int main(int argc, char *argv[]) 
  {   
  knoten *unterbaum, *suche();
  char progname[500];
  char wort [MAX_WORT];
  int c;
  progname = argv[0];
  if (argc != 3) 
    {
    fprintf(stderr,"Syntax: %s Quelldatei Zieldatei\n",progname);
    exit(1);
    }
  quelle = eopen(argv[1],"r");       
  unterbaum = NULL;
  while ((c = lieswort(wort, MAX_WORT)) != -1)
    if (c == 1)
      unterbaum = suche(unterbaum, wort);
  fclose(quelle);
  ziel = eopen(argv[2],"w");
  inorder(unterbaum);
  fclose(ziel);
  return 0;
  }

int lieswort(char *w, int lim) 
  /* lieswort  liest aus einer Datei ein Wort der Laenge lim in den
  ** Speicher, auf den w zeigt, und gibt 1 zurueck, falls es
  ** wirklich Alphazeichen gelesen hat, jedoch -1 im Fall zu
  ** grosser Wortlaenge. Falls Nicht-Alpha-Zeichen gelesen werden,
  ** wird 0 zurueckgegeben.
  */
  {   
  int c;
    
  while (!isalpha(c = getc(quelle))) 
    if (c == EOF) return -1;
  *w++ = c; 
  lim--;    
    
  while (isalpha(c = getc(quelle))  && (lim > 0)) 
    {    
    *w++ = c; lim--;   
    }
  if (lim == 0) 
    {     
    printf("%s: Error: Wort zu lang ", progname); 
    return -1; 
    }
  else 
    *w = '\0';
  if (lim < MAX_WORT) return 1; 
  else return 0;
  }

FILE *eopen(char *file,char *mode) 
  {
  FILE *fp, *fopen();

  if ( (fp = fopen(file, mode)) != NULL )
     return fp;
  fprintf(stderr, 
          "%s: Datei %s kann im Modus %s nicht geoeffnet werden\n",
          progname, file, mode);
  exit(1);
  }

knoten *suche(knoten *p, char *w)
  /* suche  durchsucht den Baum auf das Vorkommen des Wortes w,
  ** traegt es gegebenenfalls lexikographisch richtig ein bzw. erhoeht
  ** dessen Zahl und gibt den Pointer auf seinen Knoten zurueck.
  */
  {    
  int  cond;
  
  if (p == NULL) 
    {
    p = (knoten *) malloc(sizeof(knoten));
    p->wort = merke_wort(w);
    p->zahl = 1;
    p->links = p->rechts = NULL;
    }
  else if ((cond = strcmp(w, p->wort)) == 0)
    p->zahl++;
  else if (cond < 0)
    p->links = suche(p->links, w);
  else p->rechts = suche(p->rechts, w);
  return p ;
  }

inorder(knoten *p) 
  /* inorder  gibt den Unterbaum, auf den p zeigt, in Inorder aus.
  */
  {    
  if (p != NULL)
    {  
    inorder(p->links);
    fprintf( ziel, "%4d %s\n", p->zahl, p->wort);
    inorder(p->rechts);
    }
  }

char *merke_wort(char *s) 
  /*  merke_wort  speichert das Wort, auf dessen ersten Charakter s zeigt,
  **  und gibt einen Zeiger auf dessen neue Lokation an.
  */
  {   
  char *p;
  if ((p = (char *)malloc( strlen(s)+1 )) != NULL)
    strcpy(p, s);
  return p ;
  }
Beispiel: Vokalbelliste mit binärem Baum. Die Vokabeln (deutsch,englisch) werden in einer Zeichenkette gespeichert. Ein Komma trennt beide Worte. Zusätzlich zur Einfüge- und Suchfunktion gibt es auch Funktionen zum Drucken, Speichen und Laden der Vokabelliste. Die Übergabe der Zeiger auf einzelne Teilbäme erfolgt diesmal über die Parameterliste.
/***************************************************************/
/* Das Programm ermoeglicht Operationen auf einem Suchbaum:    */
/* - Suchen von Elementen                                      */
/* - Einfuegen von Elementen                                   */
/*                                                             */
/* Ausserdem kann der Suchbaum auf den Bildschirm ausgegeben   */
/* werden.                                                     */
/***************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct bbaum
  {
  char wert[1000];       /* Eintrag des Baumknotens */
  struct bbaum  *left;   /* Zeiger auf li. Sohn */
  struct bbaum  *right;  /* Zeiger auf re. Sohn */
  };


/***************************************/
/* Suche eines Werts in einem Suchbaum */
/***************************************/

struct bbaum *suche(struct bbaum *baum, char *suchwert)
  {
  int i;
  char wort[1000];
  if (baum == NULL)
    /* Baum leer: Suchwert nicht darin enthalten */
    return NULL;

  strcpy(wort,baum->wert);
  i = 0; while(wort[i] != ',') i++;
  wort[i] = '\0';
  if (strcmp(wort,suchwert) > 0)
    /* Suchwert im linken Unterbaum (wenn ueberhaupt) */
    return suche(baum->left,suchwert);
  else if (strcmp(wort,suchwert) < 0)
    /* Suchwert im rechten Unterbaum (wenn ueberhaupt) */
    return suche(baum->right,suchwert);
  else
    /* Suchwert gefunden */
    return baum;
  }


/*******************************************/
/* Einfuegen eines Werts in einem Suchbaum */
/*******************************************/

int einfuegen(struct bbaum **baum, char *einwert)
  {
  if ((*baum) == NULL)
    {
    /* Blatt erreicht -> Erzeugen und Einfuegen
       eines neuen Elements */
   *baum = (struct bbaum *) malloc(sizeof(struct bbaum));
   strcpy((*baum)->wert,einwert);
   (*baum)->left = (*baum)->right = NULL;
   return 0;
   }

  if (!strcmp((*baum)->wert,einwert))
    /* Wert ist schon im Baum vorhanden */
    return -1;
  else if (strcmp((*baum)->wert,einwert) > 0)
    /* Einfuegen in den linken Unterbaum */
    return einfuegen(&((*baum)->left),einwert);

   else
    /* Einfuegen in den rechten Unterbaum */
    return einfuegen(&((*baum)->right),einwert);
  }


/****************************/
/* Ausgabe der Baumstruktur */
/****************************/

void drucke(struct bbaum *baum, int level)
  {
  int i;

  if (baum == NULL)
    /* leerer Baum: nichts zu tun */
    return;
  drucke(baum->left,level+1);
  for(i = 0; i <= level; i++) putchar(' ');
  printf("%s\n",baum->wert);
  drucke(baum->right,level+1);
  }

/***********************/
/* Speichern des Baums */
/***********************/

void save(struct bbaum *baum, FILE *fp)
  {
  if (baum == NULL)
    /* leerer Baum: nichts zu tun */
    return;
  fprintf(fp,"%s\n",baum->wert);
  save(baum->left,fp);
  save(baum->right,fp);
  }


int main(void)
  {
  struct bbaum *root = NULL; /* Wurzel des Baums */
  struct bbaum *eintrag;     /* Hilfszeiger auf Baumeintrag */
  char wert[1000], hilf[500];
  FILE *fp;
  int err;
  char choice;

  do
    {
    printf("Bitte waehlen:\n");
    printf(" e: Einfuegen eines Werts\n");
    printf(" f: Finden eines Werts\n");
    printf(" a: Ausgabe des Suchbaums\n");
    printf(" s: Speichern des Suchbaums\n");
    printf(" l: Laden des Suchbaums\n");
    printf(" q: Beenden des Programms\n");

    choice = getchar(); while(getchar() != '\n');
    printf("\n");

    switch(choice)
      {
      case 'e': printf("Bitte deutsches Wort eingeben: ");
                fgets(wert,499,stdin);
                wert[strlen(wert)-1] = '\0';
                printf("Bitte englisches Wort eingeben: ");
                fgets(hilf,499,stdin);
                hilf[strlen(hilf)-1] = '\0';
                strcat(wert,",");
                strcat(wert,hilf);
                err = einfuegen(&root,wert);
                if (err == 0)
                  printf("Ok\n\n");
                else printf("Wort im Baum schon vorhanden\n\n");
                break;

      case 'f': printf("Bitte zu suchendes Wort eingeben: ");
                fgets(wert,499,stdin);
                wert[strlen(wert)-1] = '\0';
                eintrag = suche(root,wert);
                if (eintrag != NULL)
                  printf("Gefunden: %s\n\n",eintrag->wert);
                else printf("Eintrag nicht gefunden!\n\n");
                break;

      case 'a': drucke(root,0);
                break;

      case 'l':  fp = fopen("DATEN","r");
                if (fp == NULL)
                  printf("Fehler beim Oeffnen von DATEN\n");
                else
                  {
                  while(fgets(wert,1000,fp) != NULL)
                    {
                    wert[strlen(wert)-1] = '\0';
                    err = einfuegen(&root,wert);
                    }
                  fclose(fp);
                  }
                break;

      case 's': fp = fopen("DATEN","w");
                if (fp == NULL)
                  printf("Fehler beim Oeffnen von DATEN\n");
                else
                  { save(root,fp); fclose(fp); }
                break;
      }

   } while (choice != 'q');

  return(0);
  }

Zum Inhaltsverzeichnis Zum nächsten Abschnitt


Copyright © FH München, FB 04, Prof. Jürgen Plate
Letzte Aktualisierung: 15. Jun 2006