B-Baum

Daten- und Indexstruktur in der Informatik

Ein B-Baum (englisch B-tree) ist in der Informatik eine Daten- oder Indexstruktur, die häufig in Datenbanken und Dateisystemen eingesetzt wird. Ein B-Baum ist ein immer vollständig balancierter Baum, der Daten nach Schlüsseln sortiert speichert. Er kann binär sein, ist aber im Allgemeinen kein Binärbaum. Das Einfügen, Suchen und Löschen von Daten in B-Bäumen ist in amortisiert logarithmischer Zeit möglich. B-Bäume wachsen und schrumpfen, anders als viele Suchbäume, von den Blättern hin zur Wurzel.

Geschichte und Namensgebung

Bearbeiten

Der B-Baum wurde 1972 von Rudolf Bayer und Edward M. McCreight entwickelt. Er erwies sich als ideale Datenstruktur zur Verwaltung von Indizes für das relationale Datenbankmodell, das 1970 von Edgar F. Codd entwickelt worden war. Diese Kombination führte zur Entwicklung des ersten SQL-Datenbanksystems System R bei IBM.

Die Erfinder lieferten keine Erklärung für die Herkunft des Namens B-Baum. Die häufigste Interpretation ist, dass B für balanciert steht. Weitere Interpretationen sind B für Bayer, Barbara (nach seiner Frau), Broad, Busch, Bushy, Boeing (da Rudolf Bayer für Boeing Scientific Research Labs gearbeitet hat), Banyanbaum (ein Baum, bei dem Äste und Wurzeln ein Netz erstellen) oder binär aufgrund der ausgeführten binären Suche innerhalb eines Knotens.

Eigenschaften

Bearbeiten

In einem B-Baum kann ein Knoten – im Unterschied zu Binärbäumen – mehr als 2 Kind-Knoten haben. Dies ermöglicht es, mit einer variablen Anzahl Schlüssel (oder Datenwerte) pro Knoten die Anzahl der bei einer Datensuche zu lesenden Knoten zu reduzieren. Die maximale erlaubte Anzahl der Schlüssel ist von einem Parameter   (in der Literatur manchmal auch als  ,   oder   definiert), dem Verzweigungsgrad (oder Ordnung) des B-Baumes, abhängig. Die Bedeutung von   ist je nach Definition unterschiedlich: Entweder bezeichnet   die maximale Anzahl von Kindknoten – in diesem Fall ist die maximal erlaubte Anzahl von Schlüsseln  , oder die minimal erlaubte Anzahl von Kindknoten[1] – in dem Fall wäre die maximal erlaubte Anzahl an Schlüsseln  .

Anwendung finden B-Bäume unter anderem bei Datenbanksystemen, die mit sehr großen Datenmengen umgehen müssen, von denen nur ein Bruchteil gleichzeitig in den Hauptspeicher eines Rechners passt. Die Daten sind daher persistent auf Hintergrundspeicher (z. B. Festplatten) abgelegt und können blockweise gelesen werden. Ein Knoten des B-Baumes kann dann als ein Block gelesen bzw. gespeichert werden. Durch den großen Verzweigungsgrad bei B-Bäumen wird die Baumhöhe und damit die Anzahl der (langsamen) Schreib-/Lesezugriffe reduziert. Die variable Schlüsselmenge pro Knoten vermeidet zusätzlich häufiges Balancieren des Baumes. Im Kontext von Datenbanksystemen werden abweichend der o. g. Definition von  , folgendes definiert. Wenn über den Verzweigungsgrad des B-Baumes gesprochen wird, definiert  , welches beim Verzweigungsgrad als   bezeichnet wird, die minimale Anzahl von Schlüsseln eines Knotens und   die maximale Belegung eines Knotens. Daraus ergibt sich die Anzahl der Kindknoten. Wenn   die Anzahl vorhandener Schlüsselwerte in einem Knoten ist, dann hat dieser Knoten   Kindknoten. Mit anderen Worten hat ein Knoten mindestens   und maximal   Kindknoten[2]. Die Ordnung eines B-Baumes   beschreibt im Gegensatz zum Verzweigungsgrad zunächst die Anzahl der Kindknoten. Ein Knoten eines Baumes mit der Ordnung   besitzt minimal   und maximal   Kindknoten. Wenn nun   die Anzahl vorhandener Kindknoten ist, dann hat der Vaterknoten   Schlüssel.

Alle Blattknoten sind auf gleicher Höhe. Ein B-Baum wird durch den Mindestgrad   definiert. Der Wert von   hängt von der Größe der Speicherblöcke ab. Jeder Knoten außer dem Wurzelknoten muss mindestens   Schlüssel enthalten. Der Wurzelknoten darf mindestens 1 Schlüssel enthalten. Alle Knoten einschließlich dem Wurzelknoten dürfen höchstens   Schlüssel enthalten. Die Anzahl der Kindknoten eines Knotens ist gleich der Anzahl der darin enthaltenen Schlüssel plus 1. Alle Schlüssel eines Knotens sind in aufsteigender Reihenfolge sortiert. Der Kindknoten zwischen zwei Schlüsseln   und   enthält alle Schlüssel im Bereich von   und  . Der B-Baum wächst und schrumpft vom Wurzelknoten, was im Gegensatz zu binären Suchbäumen steht. Wie bei anderen balancierten binären Suchbäumen ist die Zeitkomplexität zum Suchen, Einfügen und Löschen gleich  . Das Einfügen eines Knotens in den B-Baum erfolgt nur am Blattknoten.[3]

Ein vollständig besetzter B-Baum, in dem   als die maximal erlaubte Anzahl von Kindknoten und h als die Höhe des Baums definiert ist, speichert gerade   Schlüssel. So können etwa bei einem entsprechend groß gewählten   (z. B.  ) bei einer Höhe von   bereits   Schlüssel gespeichert werden. Da eine Suchoperation höchstens   Knotenzugriffe benötigt, müssen für jede Suchanfrage in einem solchen Baum höchstens fünf Baumknoten inspiziert werden.

Für die Höhe   eines B-Baumes mit   gespeicherten Datenelementen gilt:

 

Damit sind im schlimmsten Fall immer noch Zugriffe auf   Baumknoten zum Auffinden eines Datenelements notwendig. Die Konstante dieser Abschätzung ist aber deutlich geringer als bei (balancierten) binären Suchbäumen mit Höhe  :

 

Bei einem minimalen Verzweigungsgrad von   benötigt ein B-Baum damit Zugriffe auf zehnmal weniger Knoten zum Auffinden eines Datenelements. Wenn der Zugriff auf einen Knoten die Dauer der gesamten Operation dominiert (wie das beim Zugriff auf Hintergrundspeicher der Fall ist), ergibt sich dadurch eine zehnfach erhöhte Ausführungsgeschwindigkeit.[4][5][6]

Definitionen

Bearbeiten
 
Abbildung 1: B-Baum
  1. Ein Knoten eines B-Baumes speichert
    • eine variable Anzahl   von Schlüsseln   (und optional ein pro Schlüssel zugeordnetes Datenelement),
    • eine Markierung isLeaf, die angibt, ob es sich bei dem Knoten um ein Blatt oder einen inneren Knoten handelt.
    • Falls es sich um einen inneren Knoten handelt, zusätzlich   Verweise auf Kindknoten.
  2. Für die Schlüssel in einem B-Baum gilt eine gegenüber binären Suchbäumen verallgemeinerte Sortierungsbedingung:
    • Alle Schlüssel eines Knotens sind aufsteigend sortiert.
    • Bei einem inneren Knoten   teilen seine Schlüssel   die Schlüsselbereiche seiner Unterbäume   in   Teilbereiche ein. Jeder Schlüssel   stellt demnach eine Grenze dar, dessen linksseitiger Verweis auf kleinere Werte und dessen rechtsseitig angeordneter Verweis auf größere Werte verweist. In einem Unterbaum   kommen folglich nur Schlüssel   vor, für die gilt:
      •  , falls  
      •  , falls  
      •  , falls  
  3. Alle Blattknoten des B-Baumes befinden sich in gleicher Tiefe. Die Tiefe der Blattknoten ist gleich der Höhe   des Baumes.
  4. Es gilt folgende Beschränkung für die erlaubte Anzahl von Kindverweisen bzw. Schlüsseln pro Knoten. Dazu wird eine Konstante   festgelegt, die den minimalen Verzweigungsgrad von Baumknoten angibt.
    • Alle Knoten außer der Wurzel haben
      • mindestens   und höchstens   Schlüssel und
      • mindestens   und höchstens   Kindverweise, wenn es sich um innere Knoten handelt.
    • Die Wurzel hat
      • mindestens   und höchstens   Schlüssel, wenn die Wurzel der einzige Knoten im B-Baum ist, und
      • mindestens   und höchstens   Kindverweise, wenn die Höhe des Baumes größer 0 ist.

Spezialfälle und Varianten

Bearbeiten

Für den Spezialfall   spricht man von 2-3-4-Bäumen, da Knoten in einem solchen Baum 2, 3 oder 4 Kinder haben können. Verbreitete Varianten des B-Baumes sind B+-Bäume, in denen die Daten nur in den Blättern gespeichert werden, und B*-Bäume, die durch eine modifizierte Überlaufbehandlung immer zu   gefüllt sind. Alle diese Varianten werden wie auch der reguläre B-Baum in der Praxis oft eingesetzt.

Auch ein R-Baum kann als balancierter Baum als Erweiterung des B-Baumes bezeichnet werden.

Operationen

Bearbeiten

Die Suche nach einem Schlüssel   liefert denjenigen Knoten  , der diesen Schlüssel speichert, und die Position   innerhalb dieses Knotens, für die gilt, dass  . Enthält der Baum den Schlüssel   nicht, liefert die Suche das Ergebnis nicht enthalten.

Die Suche läuft in folgenden Schritten ab:

  1. Die Suche beginnt mit dem Wurzelknoten   als aktuellem Knoten  .
  2. Ist   ein innerer Knoten,
    • wird die Position   des kleinsten Schlüssels bestimmt, der größer oder gleich   ist.
    • Existiert eine solche Position  ,
      • aber ist  , kann der gesuchte Schlüssel nur in dem Unterbaum mit Wurzel   enthalten sein. Die Suche wird daher mit Schritt 2 und dem Knoten   als aktuellem Knoten fortgesetzt.
      • ansonsten wurde der Schlüssel gefunden und   wird als Ergebnis zurückgeliefert.
    • Existiert keine solche Position, ist der Schlüssel größer als alle im aktuellen Knoten gespeicherten Schlüssel. In diesem Fall kann der gesuchte Schlüssel nur noch in dem Unterbaum enthalten sein, auf den der letzte Kindverweis   zeigt. In diesem Fall wird die Suche mit Schritt 2 und dem Knoten   als aktuellem Knoten fortgesetzt.
  3. Ist   ein Blattknoten,
    • Wird   in den Schlüsseln von   gesucht.
    • Wenn der Schlüssel an Position   gefunden wird, ist das Ergebnis  , ansonsten nicht enthalten.
 
Abbildung 2: Suche im B-Baum

In Abbildung 2 ist die Situation während der Suche nach dem Schlüssel   dargestellt. Im Schritt 2 aus obigem Algorithmus wird im aktuellen Knoten   die kleinste Position   gesucht, für die   gilt. Im konkreten Beispiel wird die Position   gefunden, da   gilt. Die Suche wird daher im rot markierten Unterbaum   fortgesetzt, weil sich aufgrund der B-Baum-Eigenschaft (2) der gesuchte Schlüssel   nur in diesem Unterbaum befinden kann.

Einfügen

Bearbeiten
 
Abbildung 3: Teilen eines vollen B-Baum-Knotens.

Das Einfügen eines Schlüssels   in einen B-Baum geschieht immer in einem Blattknoten.

  1. In einem vorbereitenden Schritt wird der Blattknoten   gesucht, in den eingefügt werden muss. Dabei werden Vorkehrungen getroffen, damit die Einfügeoperation nicht die B-Baum-Bedingungen verletzt und einen Knoten erzeugt, der mehr als   Schlüssel enthält.
  2. In einem abschließenden Schritt wird   unter Berücksichtigung der Sortierreihenfolge lokal in   eingefügt.

Die Suche von   läuft mit zwei Unterschieden so ab, wie unter Suchen beschrieben. Diese Unterschiede sind:

  • Das Einfügen eines neuen Schlüssels   geschieht immer in einem Blattknoten. Dem Einfügen muss daher immer ein vollständiger Suchlauf vorhergehen, der ergibt, dass der Schlüssel   noch nicht existiert, und der ermittelt, in welchen Knoten er einzutragen ist. Dies kann nur ein Blattknoten sein, denn diese Aussage ist erst nach dem Durchsuchen über die gesamte Höhe des Baumes zulässig. Die Suche bricht jedoch in einem inneren Knoten ab, wenn dort der Schlüssel   bereits gefunden wird und ein Einfügen deshalb nicht notwendig ist.
  • Bevor die Suche zu einem Kindknoten   absteigt, wird überprüft, ob   voll ist, d. h. bereits   Schlüssel enthält. In diesem Fall wird   vorsorglich geteilt. Dies garantiert, dass die Einfügeoperation mit einem einzigen Baumabstieg durchgeführt werden kann und keine anschließenden Reparaturmaßnahmen zur Wiederherstellung der B-Baum-Bedingungen durchgeführt werden müssen.

Das Teilen eines vollen Baumknotens geschieht, wie in Abbildung 3 gezeigt. Die Suche ist an Knoten   angekommen und würde zum Kindknoten   absteigen (roter Pfeil). Das heißt, die Suchposition ist  . Da dieser Kindknoten voll ist, muss er vor dem Abstieg geteilt werden, um zu garantieren, dass ein Einfügen möglich ist. Ein voller Knoten hat mit   immer eine ungerade Anzahl von Schlüsseln. Der mittlere davon (in der Abbildung ist das Schlüssel  ) wird im aktuellen Knoten an der Suchposition   eingefügt. Der Knoten   wird in zwei gleich große Knoten mit jeweils   Schlüsseln geteilt und diese über die beiden neuen Zeigerpositionen verlinkt (zwei rote Pfeile im Ergebnis). Die Suche steigt anschließend entweder in den Unterbaum   oder   ab, je nachdem, ob der einzufügende Schlüssel kleiner oder gleich dem mittleren Schlüssel des geteilten Knotens ist oder nicht.

Löschen

Bearbeiten

Das Löschen eines Schlüssels   ist eine komplexere Operation als das Einfügen, da hier auch der Fall betrachtet werden muss, dass ein Schlüssel aus einem inneren Knoten gelöscht wird. Der Ablauf ist dabei wie die Suche nach einem geeigneten Platz zum Einfügen eines Schlüssels, allerdings mit dem Unterschied, dass vor dem Abstieg in einen Unterbaum überprüft wird, ob dieser genügend Schlüssel ( ) enthält, um eine eventuelle Löschoperation ohne Verletzung der B-Baum-Bedingungen durchführen zu können. Dieses Vorgehen ist analog zum Einfügen und vermeidet anschließende Reparaturmaßnahmen.

Enthält der Unterbaum, den die Suche für den Abstieg ausgewählt hat, die minimale Anzahl von Schlüsseln  , wird entweder eine Verschiebung oder eine Verschmelzung durchgeführt. Wird der gesuchte Schlüssel in einem Blattknoten gefunden, kann er dort direkt gelöscht werden. Wird er dagegen in einem inneren Knoten gefunden, passiert die Löschung wie in Löschen aus inneren Knoten beschrieben.

Verschiebung

Bearbeiten
 
Abbildung 4: Verschieben eines Schlüssels im B-Baum.

Enthält der für den Abstieg ausgewählte Unterbaum nur die minimale Schlüsselanzahl  , aber ein vorausgehender oder nachfolgender Geschwisterknoten hat mindestens   Schlüssel, wird ein Schlüssel in den ausgewählten Knoten verschoben, wie in Abbildung 4 gezeigt. Die Suche hat hier   für den Abstieg ausgewählt (da  ), dieser Knoten enthält aber nur   Schlüssel (roter Pfeil). Da der nachfolgende Geschwisterknoten   ausreichend viele Schlüssel enthält, kann von dort der kleinste Schlüssel   in den Vaterknoten verschoben werden, um im Gegenzug den Schlüssel   als zusätzlichen Schlüssel in den für den Abstieg ausgewählten Knoten zu verschieben. Dazu wird der linke Unterbaum von   zum neuen rechten Unterbaum des verschobenen Schlüssels  . Man kann sich leicht davon überzeugen, dass diese Rotation die Sortierungsbedingungen erhält, da für alle Schlüssel   im verschobenen Unterbaum vor und nach der Verschiebung die Forderung   gilt. Eine symmetrische Operation kann zur Verschiebung eines Schlüssels aus einem vorausgehenden Geschwisterknoten durchgeführt werden.

Verschmelzung

Bearbeiten
 
Abbildung 5: Verschmelzen zweier B-Baum Kindknoten.

Enthalten sowohl der für den Abstieg ausgewählte Unterbaum   als auch sein unmittelbar vorausgehender und nachfolgender Geschwisterknoten genau die minimale Schlüsselanzahl, ist eine Verschiebung nicht möglich. In diesem Fall wird eine Verschmelzung des ausgewählten Unterbaumes mit dem vorausgehenden oder nachfolgenden Geschwisterknoten gemäß Abbildung 5 durchgeführt. Dazu wird der Schlüssel aus dem Vaterknoten  , welcher die Wertebereiche der Schlüssel in den beiden zu verschmelzenden Knoten trennt, als mittlerer Schlüssel in den verschmolzenen Knoten verschoben. Die beiden Verweise auf die jetzt verschmolzenen Kindknoten werden durch einen Verweis auf den neuen Knoten ersetzt.

Da der Algorithmus vor dem Abstieg in einen Knoten sicherstellt, dass dieser mindestens   anstelle der von den B-Baum-Bedingungen geforderten   Schlüssel enthält, ist gewährleistet, dass der Vaterknoten   eine ausreichende Schlüsselanzahl enthält, um einen Schlüssel für die Verschmelzung zur Verfügung zu stellen. Nur im Fall, dass zwei Kinder des Wurzelknotens verschmolzen werden, kann diese Bedingung verletzt sein, da die Suche bei diesem Knoten beginnt. Die B-Baum-Bedingungen fordern für den Wurzelknoten mindestens einen Schlüssel, wenn der Baum nicht leer ist. Bei Verschmelzung der letzten zwei Kinder des Wurzelknotens, wird aber sein letzter Schlüssel in das neu entstehende einzige Kind verschoben, was zu einem leeren Wurzelknoten in einem nicht leeren Baum führt. In diesem Fall wird der leere Wurzelknoten gelöscht und durch sein einziges Kind ersetzt.

Löschen aus inneren Knoten

Bearbeiten
 
Abbildung 6: Löschen eines Schlüssels aus einem inneren Knoten.

Wird der zu löschende Schlüssel   bereits in einem inneren Knoten gefunden (  in Abbildung 6), kann dieser nicht direkt gelöscht werden, weil er für die Trennung der Wertebereiche seiner beiden Unterbäume   und   benötigt wird. In diesem Fall wird sein symmetrischer Vorgänger (oder sein symmetrischer Nachfolger) gelöscht und an seine Stelle kopiert. Der symmetrische Vorgänger ist der größte Blattknoten im linken Unterbaum  , befindet sich also dort ganz rechts außen. Der symmetrische Nachfolger ist entsprechend der kleinste Blattknoten im rechten Unterbaum   und befindet sich dort ganz links außen. Die Entscheidung, in welchen Unterbaum der Abstieg für die Löschung stattfindet, wird davon abhängig gemacht, welcher genügend Schlüssel enthält. Haben beide nur die minimale Schlüsselanzahl, werden die Unterbäume verschmolzen. Damit wird keine Trennung der Wertebereiche mehr benötigt und der Schlüssel kann direkt gelöscht werden.

Beispiel

Bearbeiten
 
Abbildung 7: Evolution eines B-Baumes

Abbildung 7 zeigt die Entwicklung eines B-Baumes mit minimalem Verzweigungsgrad  . Knoten in einem solchen Baum können minimal einen und maximal drei Schlüssel speichern und haben zwischen zwei und vier Verweise auf Kindknoten. Man spricht daher auch von einem 2-3-4-Baum. In einer praktischen Anwendung würde man dagegen einen B-Baum mit wesentlich größerem Verzweigungsgrad verwenden.

Folgende Operationen wurden auf einem 2-4 Baum (siehe Abbildung 7) durchgeführt:

  • a–c) Einfügen von 5, 13 und 27 in einen anfangs leeren Baum.
  • d–e) Einfügen von 9 führt zum Teilen des Wurzelknotens.
  • f) Einfügen von 7 in einen Blattknoten.
  • g–h) Einfügen von 3 führt zum Teilen eines Knotens.
  • i–j) Um 9 löschen zu können, wird ein Schlüssel aus einem Geschwisterknoten verschoben.
  • k–l) Das Löschen von 7 führt zum Verschmelzen von zwei Knoten.
  • m) Löschen von 5 aus einem Blatt.
  • n–q) Löschen von 3 führt zur Verschmelzung der letzten zwei Kinder des Wurzelknotens. Der entstehende leere Wurzelknoten wird durch sein einziges Kind ersetzt.

Programmierung

Bearbeiten

Das folgende Beispiel in der Programmiersprache C++ zeigt die Implementierung eines B-Baums mit den Funktionen search (suchen), insert (einfügen) und traverse (durchlaufen). Der B-Baum wird als Klasse BTree und seine Knoten als Klasse BTreeNode deklariert. Bei der Ausführung des Programms wird die Funktion main verwendet.[7]

Code-Schnipsel  
#include <iostream>
using namespace std;

// Deklariert die Klasse für die Knoten des B-Baums
class BTreeNode
{
    int* keys; // Zeiger auf das Array mit den Schlüsseln
    BTreeNode** childNodes; // Array von Zeigern auf das Array mit den Kindknoten
    BTreeNode* root; // Zeiger auf den Wurzelknoten des B-Baums
    int t; // Mindestgrad des Knotens (Mindestanzahl der Kindknoten, Mindestanzahl der Schlüssel plus 1)
    int n; // Aktuelle Zahl der Schlüssel
    bool isLeafNode; // Gibt an, ob der Schlüssel ein Blattknoten ist
public:
    // Deklariert die Klasse BTree als friend class von BTreeNode, damit BTreeNode auf private Attribute von BTree zugreifen kann
    friend class BTree;

    // Konstruktor
    BTreeNode(int _t, bool _isLeafNode)
    {
        t = _t;
        keys = new int[2 * t - 1]; // Deklariert ein Array für die maximale Anzahl der Schlüssel
        childNodes = new BTreeNode * [2 * t]; // Deklariert ein Array für die maximale Anzahl der Kindknoten
        // Initialisiert die Attribute
        root = NULL;
        n = 0;
        isLeafNode = _isLeafNode;
    }

    // Diese rekursive Funktion durchläuft alle Knoten des Teilbaums mit dem aktuellen Knoten als Wurzelknoten und gibt sie auf der Konsole aus
    void traverse()
    {
        int i; // Index für den aktuellen Kindknoten und aktuellen Schlüssel
        // Diese for-Schleife durchläuft alle Kindknoten und Schlüssel des aktuellen Knotens
        for (i = 0; i < n; i++)
        {
            // Wenn der aktuelle Knoten kein Blattknoten ist
            if (!isLeafNode)
            {
                childNodes[i]->traverse();// Rekursiver Aufruf für den aktuellen Kindknoten
            }
            cout << " " << keys[i]; // Ausgabe auf der Konsole
        }
        // Wenn der aktuelle Knoten kein Blattknoten ist
        if (!isLeafNode)
        {
            childNodes[i]->traverse(); // Rekursiver Aufruf für den Kindknoten
        }
    }

    // Diese rekursive Funktion durchläuft die Knoten des Teilbaums mit dem aktuellen Knoten als Wurzelknoten und sucht einen Schlüssel mit dem gegebenen Wert
    // Diese Funktion gibt einen Zeiger auf den Knoten mit dem angegebenen Wert zurück, wenn vorhanden, sonst einen Nullzeiger
    BTreeNode* search(int value)
    {
        int i = 0; // Initialisiert den Index für den aktuellen Schlüssel
        // Diese while-Schleife durchläuft das Array mit den Schlüsseln, bis ein Schlüssel größer oder gleich dem gegebenen Wert gefunden oder das Ende des Arrays erreicht ist
        while (i < n && keys[i] < value)
        {
            i++;
        }
        // Wenn der gefundene Schlüssel gleich dem gegebenen Wert ist, wird ein Zeiger auf den Knoten zurückgegeben
        if (keys[i] == value)
        {
            return this;
        }
        // Wenn kein Schlüssel gefunden wurde und der aktuelle Knoten ein Blattknoten ist, wird ein Nullzeiger zurückgegeben
        if (isLeafNode)
        {
            return NULL;
        }
        return childNodes[i]->search(value); // Rekursiver Aufruf der Funktion für den Kindknoten vor dem Schlüssel, der größer oder gleich dem gegebenen Wert ist
    }

    // Diese Funktion fügt den gegebenen Schlüssel in den Teilbaums mit dem aktuellen Knoten als Wurzelknoten ein
    void insert(int key)
    {
        // Wenn der Baum leer ist, wird ein Wurzelknoten mit dem gegebenen Schlüssel eingefügt
        if (root == NULL)
        {
            root = new BTreeNode(t, true); // Erzeugt den Wurzelknoten
            root->keys[0] = key; // Initialisiert den Schlüssel
            root->n = 1; // Aktualisiert das Attribut für die Anzahl der Schlüssel
        }
        else // Wenn der Baum nicht leer ist
        {
            // Wenn der Wurzelknoten voll ist
            if (root->n == 2 * t - 1)
            {
                BTreeNode* newRoot = new BTreeNode(t, false); // Erzeugt einen neuen Wurzelknoten
                newRoot->childNodes[0] = root; // Macht den alten Wurzelknoten zum Kindknoten des neuen Wurzelknotens
                newRoot->splitChild(0, root); // Aufruf der Funktion, teilt den alten Wurzelknotens
                int i = 0; // Index für den Kindknoten
                if (newRoot->keys[0] < key) // Wenn der Schlüssel kleiner als der gegebene Schlüssel ist, Index um 1 erhöhen
                {
                    i++;
                }
                newRoot->childNodes[i]->insertNonFull(key); // Fügt den Schlüssel in den Teilbaum mit dem Kindknoten als Wurzelknoten ein
                root = newRoot; // Setzt den neuen Wurzelknoten
            }
            else // Wenn der Wurzelknoten nicht voll ist
            {
                root->insertNonFull(key); // Aufruf der Funktion insertNonFull für den Wurzelknoten
            }
        }
    }

    // Diese rekursive Funktion fügt den gegebenen Schlüssel in einen Knoten ein, der nicht voll sein darf
    void insertNonFull(int key)
    {
        int i = n - 1; // Initialisiert den Index des Schlüssels mit dem Maximum
        if (isLeafNode) // Wenn der Knoten ein Blattknoten ist
        {
            // Diese while-Schleife bestimmt den Index, wo der Schlüssel eingefügt wird und schiebt alle größeren Schlüssel um einen Index weiter
            while (i >= 0 && keys[i] > key)
            {
                keys[i + 1] = keys[i];
                i--;
            }
            keys[i + 1] = key; // Fügt den gegebenen Schlüssel für diesen Index ein
            n++; // Aktualisiert das Attribut für die Anzahl der Schlüssel
        }
        else // Wenn der Knoten kein Blattknoten ist
        {
            // Diese while-Schleife bestimmt den Index des Kindknotens, in den der Schlüssel eingefügt wird
            while (i >= 0 && keys[i] > key)
            {
                i--;
            }
            if (childNodes[i + 1]->n == 2 * t - 1) // Wenn der Kindknoten voll ist
            {
                splitChild(i + 1, childNodes[i + 1]); // Teilt den Kindknoten
                // Bestimmt, ob der gegebene Schlüssel, links oder rechts vom mittleren Schlüssel eingefügt wird
                if (keys[i + 1] < key) // Wenn der Schlüssel kleiner als der gegebene Schlüssel ist, wird der Schlüssel rechts vom mittleren Schlüssel eingefügt, sonst links
                {
                    i++;
                }
            }
            childNodes[i + 1]->insertNonFull(key); // Rekursiver Aufruf für den Kindknoten, fügt den Schlüssel in den zugehörigen Teilbaum ein
        }
    }

    // Diese Funktion teilt den gegebenen Kindknoten des aktuellen Knotens, der voll sein muss
    void splitChild(int index, BTreeNode* _node)
    {
        BTreeNode* node = new BTreeNode(_node->t, _node->isLeafNode); // Erzeugt einen neuen Knoten
        node->n = t - 1; // Initialisiert das Attribut für die Anzahl der Schlüssel des neuen Knotens
        // Diese for-Schleife kopiert die letzten t - 1 Schlüssel vom gegebenen Kindknoten in den neuen Knoten
        for (int i = 0; i < t - 1; i++)
        {
            node->keys[i] = _node->keys[i + t];
        }
        // Wenn der Knoten kein Kindknoten ist
        if (!node->isLeafNode)
        {
            // Diese for-Schleife kopiert die letzten t Kindknoten in den neuen Knoten
            for (int i = 0; i < t; i++)
            {
                node->childNodes[i] = _node->childNodes[i + t];
            }
        }
        node->n = t - 1; // Aktualisiert das Attribut für die Anzahl der Schlüssel des neuen Knotens
        // Diese for-Schleife bestimmt den Index des Kindknotens, in den der Schlüssel eingefügt wird
        // Der Kindknoten rechts vom gegebenen Kindknoten um einen Index weiter geschoben
        for (int i = n; i >= index + 1; i--)
        {
            childNodes[i + 1] = childNodes[i];
        }
        childNodes[index + 1] = node; // Setzt den neuen Knoten als Kindknoten mit diesem Index
        // Die Schlüssel rechts vom gegebenen Kindknoten werden um einen Index weiter geschoben
        for (int i = n - 1; i >= index; i--)
        {
            keys[i + 1] = keys[i];
        }
        keys[index] = _node->keys[t - 1]; // Kopiert den mittleren Schlüssel in den Knoten
        n++; // Aktualisiert das Attribut für die Anzahl der Schlüssel des aktuellen Knotens
    }
};

// Deklariert die Klasse für den B-Baum
class BTree
{
    BTreeNode* root; // Zeiger auf den Wurzelknoten
    int t; // Mindestgrad der Knoten (Mindestanzahl der Kindknoten, Mindestanzahl der Schlüssel plus 1)
public:
    // Konstruktor
    BTree(int _t)
    {
        root = NULL;
        t = _t;
    }

    // Diese Funktion durchläuft alle Knoten des Baums und gibt sie auf der Konsole auf
    void traverse()
    {
        if (root != NULL)
        {
            root->traverse(); // Aufruf der Funktion für den Wurzelknoten
        }
    }

    // Diese sucht einen Schlüssel mit dem gegebenen Wert
    // Diese Funktion gibt einen Zeiger auf den Knoten mit dem angegebenen Wert zurück, wenn vorhanden, sonst einen Nullzeiger
    BTreeNode* search(int k)
    {
        return root == NULL ? NULL : root->search(k); // Aufruf der Funktion für den Wurzelknoten
    }

    // Diese Funktion fügt den gegebenen Schlüssel ein
    void insert(int key)
    {
        if (root == NULL)
        {
            root = new BTreeNode(t, true);
            root->keys[0] = key;
            root->n = 1;
        }
        else
        {
            if (root->n == 2 * t - 1)
            {
                BTreeNode* newRoot = new BTreeNode(t, false);
                newRoot->childNodes[0] = root;
                newRoot->splitChild(0, root);
                int i = 0;
                if (newRoot->keys[0] < key)
                {
                    i++;
                }
                newRoot->childNodes[i]->insertNonFull(key);
                root = newRoot;
            }
            else
            {
                root->insertNonFull(key);
            }
        }
    }
};

// Hauptfunktion die das Programm ausführt
int main()
{
    // Erzeugt einen B-Baum mit Mindestgrad 3 und fügt 8 Knoten ein
    BTree bTree(3);
    bTree.insert(10);
    bTree.insert(20);
    bTree.insert(5);
    bTree.insert(6);
    bTree.insert(12);
    bTree.insert(30);
    bTree.insert(7);
    bTree.insert(17);

    cout << "Durchlauf des erzeugten B-Baums:" << endl; // Ausgabe auf der Konsole
    bTree.traverse(); // Aufruf der Funktion, die die Knoten durchläuft und die Schlüssel auf der Konsole ausgibt
    cout << endl;
    // Ruft die Funktion auf, die einen Knoten mit dem Schlüssel 6 sucht und gibt das Ergebnis auf der Konsole aus
    int k = 6;
    bTree.search(k) != NULL ? cout << "Es ist ein Knoten mit dem Schlüssel " << k << " vorhanden" << endl : cout << "Es ist kein Knoten mit dem Schlüssel " << k << " vorhanden" << endl;
    // Ruft die Funktion auf, die einen Knoten mit dem Schlüssel 15 sucht und gibt das Ergebnis auf der Konsole aus
    k = 15;
    bTree.search(k) != NULL ? cout << "Es ist ein Knoten mit dem Schlüssel " << k << " vorhanden" << endl : cout << "Es ist kein Knoten mit dem Schlüssel " << k << " vorhanden" << endl;
}

Ähnliche Baumstrukturen

Bearbeiten

Literatur

Bearbeiten

Deutsch

Englisch

  • R. Bayer, E. McCreight: Organization and Maintenance of Large Ordered Indexes. In: Proceedings of the 1970 ACM SIGFIDET (Now SIGMOD) Workshop on Data Description, Access and Control. 1970, S. 107–141.[8]
  • R. Bayer, E. McCreight: Organization and Maintenance of Large Ordered Indexes. In: Acta Informatica, Band 1, 1972, S. 173–189.
  • R. Bayer, E. McCreight: Symmetric binary B-Trees: data structure and maintenance algorithms. In: Acta Informatica, Band 1, 1972, S. 290–306.
Bearbeiten

Tools zum Ausprobieren von B-Bäumen:

Einzelnachweise

Bearbeiten
  1. Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein: Introduction to Algorithms. 2. Auflage. MIT Press, Cambridge MA 2001, ISBN 0-262-03293-7, S. 439.
  2. Alfons Kemper, André Eickler: Datenbanksysteme eine Einführung. 8. aktualisierte und erw. Auflage. Oldenbourg Verlag, München 2011, ISBN 978-3-486-59834-6, S. 217.
  3. GeeksforGeeks: Introduction of B-Tree
  4. Ludwig-Maximilians-Universität München, Institut für Informatik: B-Bäume I
  5. T. Härder, Universität Kaiserslautern: Mehrwegbäume, B-Bäume, Höhe des B-Baumes
  6. Prof. Dr. E. Rahm, Universität Leipzig, Institut für Informatik: Algorithmen und Datenstrukturen 2
  7. GeeksforGeeks: Insert Operation in B-Tree
  8. R. Bayer, E. McCreight: Organization and Maintenance of Large Ordered Indices. In: Proceedings of the 1970 ACM SIGFIDET (Now SIGMOD) Workshop on Data Description, Access and Control (= SIGFIDET ’70). ACM, New York NY 1970, S. 107–141, doi:10.1145/1734663.1734671 (acm.org [abgerufen am 20. Februar 2019]).