Advanced   Java   Services Binäre Bäume Back Next Up Home


Binäre Bäume

Ein binärer Baum ist eine Datenstruktur, bei der - im Gegensatz zu einer einfach verketteten Liste - ein Element zwei Nachfolger hat. Die Elemente nennt man Knoten. Sind beide Nachfolger eines Knotens NULL so spricht man von einem Blatt oder Blattknoten. Der Knoten der keinen Vorgänger hat wird Wurzel oder Root genannt. Die graphische Darstellung eines Baumes beginnt immer mit der Wurzel. In der folgenden Abbildung ist b die Wurzel des Baumes, n und a haben nur einen linken Nachfolger und r nur einen rechten Nachfolger. y, r und die beiden e sind Blätter. Der Baum hat maximal 6 Stufen und minimal 3 Stufen. Die sechste Stufe ist nur mit y besetzt, in der dritten Stufe findet man n, t, e, e .

c-binary-tree-01.jpg



Vollständig ausgeglichene Bäume

Ein binärer Baum heißt vollständig ausgeglichen, wenn sich minimale und maximale Stufenhöhe höchstens um eins unterscheiden.

Der folgende Baum ist vollständig ausgeglichen.

c-binary-tree-02.jpg


Geordnete Bäume

Ein binärer Baum heißt geordnet, wenn die Daten eine Ordnungsrelation erfüllen, also vergleichbar sind und für jeden Knoten gilt, daß der linke Nachfolger kleiner (oder kleiner gleich) dem rechten Nachfolger ist (oder umgekehrt).

Der folgende Baum ist durch "<=" geordnet und nicht ausgeglichen.

c-binary-tree-03.jpg


Geordnete Suchbäume (Binary Search Tree)

Ein binärer Baum heißt binärer Suchbaum (BST), wenn er die folgenden Bedingungen erfüllt.

Der folgende Baum ist ein durch "<" geordneter und ausgeglichener BST.

c-binary-tree-04.jpg

Binäre Suchbäume werden im nächsten Kapitel behandelt.


Traversieren von Bäumen

Man unterscheidet gewöhnlich vier verschiedene Arten, einen Baum zu durchschreiten:

Man sieht, daß die ersten drei Traversierungen rekursiv definiert sind.

Als Beispiel traversieren wir den Baum aus der ersten Abbildung nach allen vier Varianten:

c-binary-tree-01.jpg
Preorder :
 b i n a r y t r e e
Inorder :
 r y a n i t b e r e
Postorder :
 y r a n t i e e r b
Levelorder :
 b   i r   n t e e   a   r   y 
Anzahl der Stufen :
 6

Realisierung des Knotens

Wie zu erwarten realisieren wir den Knoten durch ein struct.

struct node
{
  int ch;
  struct node *left;
  struct node *right;
};

typedef struct node node;

Im Datenteil wählen wir den Typ int, so können wir Zeichen und Zahlen darstellen. Alle Beispiele und Operationen lassen sich aber auch mit komplexen Datentypen realisieren.


Einfache nicht rekursive Funktionen

Erstellen eines Knotens

Die Funktion holt Speicher für einen Knoten, initialisiert den Datenteil und setzt die beiden Zeiger auf NULL. Zurückggeben wird ein Zeiger auf den Knoten oder Null, falls kein Speicher angefordert werden konnte.

node* createNode(int ch)
{
   node *nod = malloc(sizeof(node));
   if (nod != NULL)
   {
      nod->ch = ch;
      nod->left = NULL;
      nod->right = NULL;
   }
   return nod;
}

Anhängen eines linken bzw. rechten Knotens

Hängt an nod links (bzw. rechts) einen neuen Knoten an. Falls NULL übergebn wird oder kein Speicher angefordert werden konnte wird NULL zurückgegeben, ansonsten der neue Knoten.

node* appendLeftNode(node* nod, int ch)
{
   if (nod != NULL)
   {
      node *leftnode = malloc(sizeof(node));
      if (leftnode != null)
      {
         leftnode->ch = ch;
         leftnode->left = NULL;
         leftnode->right = NULL;
         nod->left = leftnode;
      }
      return leftnode;
   }
   return nod;
}
node* appendRightNode(node* nod, int ch)
{
   if (nod != NULL)
   {
      node* rightnode = malloc(sizeof(node));
      if (rightnode != null)
      {
         rightnode->ch = ch;
         rightnode->left = NULL;
         rightnode->right = NULL;
         nod->right = rightnode;
      }
      return rightnode;
   }
   return nod;
}

Aufbau des Beispielbaums

Mit Hilfe dieser beiden Funktionen können wir den oben abgebildeten Baum leicht aufbauen.

int main(void)
{
   node* root = createNode('b');
   printf("root =  %c\n", root->ch); // b
   node* node_i  = appendLeftNode(root, 'i');
   node* node_n  = appendLeftNode(node_i, 'n');
   node* node_t  = appendRightNode(node_i, 't');
   node* node_a  = appendLeftNode(node_n, 'a');
   node* node_r1 = appendLeftNode(node_a, 'r');
   node* node_y  = appendRightNode(node_r1, 'y');
   node* node_r2 = appendRightNode(root, 'r');
   node* node_e1 = appendLeftNode(node_r2, 'e');
   node* node_e2 = appendRightNode(node_r2, 'e');

   return EXIT_SUCCESS;
}

Traversieren von binären Bäumen (Pre- In- und Postorder)

Zum Traversieren codieren wir die weiter oben angeführten rekursiven Algorithmen. Preorder, Inorder und Postorder verwenden im Grunde denselben Algorithmus und erledigen ihre Aufgabe nur an unterschiedlichen Stellen, wie das folgende Schem zeigt.

void traverse(node* root)
{
   if (root == null) return;

   // preorder task
   traverse(root->left);
   // inorder task
   traverse(root->right);
   // postorder task
}

Wenn wir den Baum ausgeben wollen brauchen wir lediglich printf() an den entsprechenden Stellen einfügen.

Preordertraversierung

void printTreeInPreorder(node* root)
{
   if (root == null) return;

   printf("%c ", root->ch);
   printTreeInPreorder(root->left);
   printTreeInPreorder(root->right);
}

Inordertraversierung

void printTreeInInorder(node* root)
{
   if (root == null) return;

   printTreeInInorder(root->left);
   printf("%c ", root->ch);
   printTreeInInorder(root->right);
}

Postordertraversierung

void printTreeInPostorder(node* root)
{
   if (root == null) return;

   printTreeInPostorder(root->left);
   printTreeInPostorder(root->right);
   printf("%c ", root->ch);
}

Anzahl der Knoten

Wir verwenden den gleichen Algorithmus um die Anzahl der Knoten zu ermitteln. Als Zähler wird eine statische Variable verwendet, die bekanntlich nur einmal initialisiert wird. In diesem Fall ist es egal, wo der Zähler inkrementiert wird.

int nodeCount(node* root)
{
   static int count = 0;
   if (root == NULL) return 0;
   count++;
   nodeCount(root->left);
   nodeCount(root->right);
   return count;
}

Die folgende Variante kommt ohne statische Variable aus.

int nodeCount(node* root)
{
   if (root==NULL) return 0;

   return nodeCount(root->left) + 1 + nodeCount(root->right) ;
}

Maximale Tiefe eines Baums

Eine weitere Abwandlung dieses Algorithmus' liefert uns die maximale Tiefe eines Baumes.

int maxTreeDepth(node* nod)
{
   //if (nod == NULL) return -1; // Erste Stufe erhält Index 0
   if (nod == NULL) return 0;  // Erste Stufe erhält Index 1

   int lheight = maxTreeDepth(nod->left);
   int rheight = maxTreeDepth(nod->right);

   return (lheight > rheight) ? lheight+1 : rheight+1;
}

Minimale Tiefe eines Baums

Leider läßt sich der obige Algorithmus nicht verwenden um die minimale Tiefe eines Baumes zu ermitteln. Hier gehen wir einen andseren Weg. Einer Hilfsfunktion übergibt man eine obere Grenze für das Minimum, etwa die Anzahl der Knoten, als Zeiger. Die Hilfsfunktion verbessert dann erkursiv diesen Startwert bis das Minimum erreicht ist.

Die Hilfsfunktion seekMin

/*
 * Hilfsfunktion zur Ermittlung der kürzesten Pfadlänge
 */
void seekMin(node* node, int* min, int pathLen)
{
   if (node==NULL) return ;

   pathLen++;
   if (node->left == NULL && node->right == NULL)
   {
      if ( pathLen != 0 && pathLen < *min)
      {
         *min = pathLen;
      }
   }
   else
   {
      seekMin(node->left, min, pathLen);
      seekMin(node->right, min, pathLen);
   }
}

Die Funktion minTreeDepth stützt sich auf die obige Funktion

/*
 * Ermittelt die minimale Pfadlänge mit Hilfe der Funktion seekMin
 */
int minTreeDepth(node* node)
{
   int min = nodeCount(node);
   seekMin(node, &min, 0);
   return min;
}

Alle Knoten einer Stufe ermitteln

Eine erneute Variation des Standardalgorithmus' ermöglicht die Ausgabe aller Knoten einer Stufe.

void printTreeLevel(node* root, int level)
{
   if(root == NULL || level <= 0)  return;

   if (level == 1)
      printf("%c ", root->ch);
   else
   {
      printTreeLevel(root->left, level - 1);
      printTreeLevel(root->right, level - 1);
   }
}

Traversierung in Levelorder

Da wir die Stufentiefe ermitteln können und alle Knoten einer Stufe ausgeben können ist die folgende Funktion offensichtlich.

/*
 * Verwendet printTreeLevel und maxTreeDepth
 */
void printTreeInLevelorder(node* root)
{
   int size = maxTreeDepth(root) +1;
   for(int i=1; i < size; i++)
   {
      printTreeLevel(root, i);
      printf("  ");
   }
}

Löschen des ganzen Baumes

Der Standardalgorithmus hilft uns auch in diesem Fall. Es gibt allerdings nur eine richtige Reihenfolge und diese ist Postorder weil root immer am Schluß besucht werden muß. Einfügen von printf zeigt das.

/*
 * Löschen des ganzen Baumes
 */
void deleteTree (node *root )
{
  if ( root == NULL ) return;

  deleteTree(root->left);
  deleteTree(root->right);
  //printf("-%c ", root->ch);
  free(root);
}

Erstellen eines ausgeglichenen Baumes aus einem Array

Wir verwenden den von Niklaus Wirth beschriebenen rekursiven Algorithmus um einen Baum ausgeglichen aufzubauen.

/*
 * Baut einen ausgeglichenen binären Baum mit Buchstaben
 * Dazu wird ein Array von Buchstaben übergeben
 * Um den jeweils nächsten Buchstaben zu verwenden wird die
 * Arrayposition in einer statischen Variablen gespeichert.
 */
node* buildTreeFromArray(char* arr, int size)
{
   static int pos = 0;
   if (arr == null || size == 0)
      return null;

   int sizeleft = size / 2, sizeright = size - sizeleft - 1;
   node* newnode = createNode(arr[pos++]);
   // beim ersten mal ist das root
   if (newnode != null)
   {
      newnode->left = buildTreeFromArray(arr, sizeleft);
      newnode->right = buildTreeFromArray(arr, sizeright);
   }
   return newnode;
}

Wir übergeben das folgende Array

char arr[] = {'t', 'h', 'i', 's', 'i', 's', 'a', 'b', 'i', 'n', 'a', 'r', 'y', 't', 'r', 'e', 'e'};

und erhalten damit diesen Baum:

c-binary-tree-05.jpg
Preorder :
 t h i s i s a b i n a r y t r e e
Inorder :
 i s i s h b a i t y r a t n e r e 
Postorder :
 i s s i b i a h y r t a e e r n t
Levelorder :
 t   h n   i a a r   s s b i r t e e   i y 
Anzahl der Stufen :
 5

Mit etwas Mühe kann man die Anordnung des Baumes aus der Preorder-Reihenfolge eindeutig erschließen. Ebenso geht es mit Inorder und Postorder.Da die Stufen eins bis vier voll besetzt sind kann man die Abfolge auch aus der Levelorder-Reihenfolge schließen, lediglich bei der sechsten Stufe ist nicht klar, wo die beiden Knoten hängen. Man kann jedoch auch alle Pfade von der Spitze zu den Blättern einzeln ablaufen. Nick Parlante hat diesen Algorithmus vorgestellt, den wir hier in einer kleinen Variante zeigen.


Alle Pfade von Root zu einem Blatt durchlaufen

Um die Idee nachvollziehn zu können gehen wir von der Preorderanordnung aus und fügen nach jedem Blatt einen Zeilenvorschub ein. Überdies schreiben wir Buchstaben auf ein und demselben Level in einer Spalte untereinander

t h i s i
      s
    a b
      i
  n a r y
      t
    r e
      e

Wenn es nun gelingt die Pfadanfänge der vorigen Zeile von der ersten Zeile an zu übernehmen, dann haben wir alle Pfade ausgehend von Root aus gefunden.

t h i s i
" " " s
" " a b
" " " i
" n a r y
" " " t
" " r e
" " " e

Am einfachsten ist es die Elemente in ein Array zu schreiben, sich die Position zu merken und dann die alten Werte zu überschreiben. Das Array muß dabei mindestens so groß sein, wie der Baum Stufen hat.

Zum Ausgeben des Arrays wird eine kleine Hilfsfunktion bereitgestellt

void printArr(int arr[], int len)
{
   for (int i=0; i <len; i++)
   {
      printf("%c ", arr[i]);
   }
   printf("\n");
}

Die Hauptarbeit erledigt die folgende Funktion

void printPaths(node* node, int path[], int pathLen)
{
   if (node==NULL) return;

   // append this node to the path array
   path[pathLen] = node->ch;
   pathLen++;

   // it's a leaf, so print the path that led to here
   if (node->left==NULL && node->right==NULL)
   {
      printArr(path, pathLen);
   }
   else
   {
      // otherwise try both subtrees
      printPaths(node->left, path, pathLen);
      printPaths(node->right, path, pathLen);
   }
}

Nun müssen wir nur noch ein Array anlegen und die obige Funktion aufrufen

void printAllPaths(struct node* node)
{
   int len = maxTreeDepth(node);
   int path[len];

   printPaths(node, path, 0);
}

Die Ausgabe

t h i s i
t h i s
t h a b
t h a i
t n a r y
t n a t
t n r e
t n r e








Valid XHTML 1.0 Strict top Back Next Up Home