×

Search anything:

B Tree in C++ with OOP and template

Internship at OpenGenus

Get this book -> Problems on Array: For Interviews and Competitive Programming

The B-tree data structure is a popular one in computer science that can effectively store and organize massive amounts of data. It is frequently used in file systems, databases, and other programmers that need quick access to large amounts of data.

B-trees are frequently implemented in C++ as templates, allowing for flexible customization for various data and application types. The balanced tree structure of the B-tree data structure makes it ideal for efficient data searching, insertion, and deletion. The order of the tree, also known as the maximum number of child nodes allowed, is maintained to keep the balance.B-trees are perfect for applications that need to store large amounts of data on disc because they are made to be effective for disc storage. They are designed to reduce memory usage and disc I/O, which can significantly boost performance in complicated applications.B-trees are also very flexible and can be tailored to meet various needs. For instance, the tree's order can be changed to optimize performance for a specific application, and it can be tuned to use less memory and disc I/O.

Overall, the storage and retrieval of large amounts of data can be optimized using the C++ data structure known as the B-tree. As a result of its quick access, effective storage, and concurrency support, it is a popular option for many different applications.

What is B Tree ?

In computer science, a B-tree is a type of self-balancing tree data structure that is frequently used to implement databases and file systems. It is a useful data structure for storing a lot of data on disc because it is made to minimize the number of disc accesses necessary to locate a specific piece of data.

While a binary search tree only has two children per node, a B-tree can have many more children per node. Each node in a B-tree can have a different number of keys and pointers to its offspring. A node typically has between B-1 and 2B-1 keys, where B is a tree parameter known as the "order" of the tree. All other nodes must have at least B/2 keys, and the root node of a B-tree must have at least one key.

A B-ability tree's to self-balance, or automatically adjust its structure to maintain a roughly uniform distribution of keys across its nodes, is one of its main advantages. This makes it possible to maintain the tree's effectiveness as the data it stores increases or changes over time. With the aid of object-oriented programming strategies and templates, a B-tree can be implemented in C++. Creating a BTree class with methods for adding, searching for, and removing nodes as well as splitting and merging nodes when necessary to maintain balance is the typical method for implementation.

Operations in B Tree

Insertion
The key should be inserted at the appropriate leaf node, which is reached by the insertion algorithm starting at the tree's root and recursively descending.

The general procedures for adding a new key to a B-tree are as follows:

1.Starting at the root node, move up the tree until you reach a leaf node by following the child pointer that points to the key value being inserted.

2.If the leaf node has space for the new key, place it there in the appropriate location to preserve the keys' order.

3.If the leaf node is completely full, divide it into two nodes and move the middle key up to the parent node. The smaller keys will be in the left node, and the larger keys will be in the right node. As you continue to insert the key, choose which of the two new nodes will house the new key that is currently being used.

4.Split the parent node as well if it fills up as a result of the split, and then elevate the middle key to become the parent. Recursively carry out this process until a node is identified that has room for the new key.

5.After the new key has been added, adjust the sizes of the nodes and subtrees as necessary.

Deletion
The deletion algorithm begins at the tree's base and recursively works its way down to the leaf node that contains the key that needs to be removed.

The general procedures for removing a key from a B-tree are as follows:

1.Follow the child pointer that points to the key value that is being deleted as you make your way up the tree from the root node until you reach a leaf node.

2.Remove the key from the leaf node if it is present there and needs to be deleted.

3.There are two scenarios that could happen if the deletion leaves the leaf node too empty:
a.If the current node's adjacent sibling node has more keys than the required minimum, a key should be transferred from the sibling node to the current node, and the keys and pointers of the two nodes should be adjusted accordingly.

b. Merge the current node with one of its siblings and remove the key from the parent node if both adjacent sibling nodes have the bare minimum number of keys. 2B-1 keys, where B is the tree's order, will be present in the resulting node.

Repeat the previous steps recursively until a node is found that has enough keys or until the root node is reached if the deletion leaves the parent node too empty.

Searching
The goal of B-tree searching is to locate a specific key within the tree. The search algorithm begins at the tree's base and moves recursively downward to the proper leaf node, which is where the key should be found.

The general procedures for looking for a key in a B-tree are as follows:

1.Compare the key being searched with the keys in the node starting at the root node.

2.Return the node and the key's index if the key was found in the node.

3.If the key is not present in the node, move on to the next node by following the child pointer that represents the key's potential range.

4.Return null to show that the key is not present in the tree if it cannot be found in any of the leaf nodes.

Implementing B Tree in C++

1.** BTree class template and Node class definition**

template <typename T, int M>
class BTree {
private:
    struct Node {
        std::vector<T> keys;
        std::vector<Node*> children;
        bool leaf;

        Node(bool is_leaf) : leaf(is_leaf) {
            keys.reserve(M - 1);
            children.reserve(M);
        }

        ~Node() {
            for (auto child : children) {
                delete child;
            }
        }
    };

    Node* root;

public:
    BTree() : root(nullptr) {}

    ~BTree() {
        delete root;
    }

    void insert(const T& key);
    bool search(const T& key) const;
    void print() const;

private:
    void split(Node* node, Node* parent);
    void print(Node* node) const;
};

The basic framework for the B-Tree implementation is provided by the Node class definition and the BTree class template. Two types are used as parameters for the BTree class template: T (the type of keys stored in the B-Tree) and M. (the maximum number of keys that can be stored in each node). The Node class, which represents a node in the B-Tree, includes pointers to the node's child nodes, a flag indicating whether or not the node is a leaf, the keys that are stored in the node, and the node's own keys. The public interface for the B-Tree is defined by the BTree class template, which also includes a pointer to the root node.

2.BTree insert() method

template <typename T, int M>
void BTree<T, M>::insert(const T& key) {
    if (root == nullptr) {
        root = new Node(true);
        root->keys.push_back(key);
        return;
    }

    Node* node = root;
    Node* parent = nullptr;

    while (!node->leaf) {
        parent = node;
        int i = 0;

        while (i < node->keys.size() && key > node->keys[i]) {
            ++i;
        }

        node = node->children[i];
    }

    // Insert the key into the leaf node
    int i = 0;
    while (i < node->keys.size() && key > node->keys[i]) {
        ++i;
    }

    node->keys.insert(node->keys.begin() + i, key);

    // Split the node if necessary
    if (node->keys.size() == M) {
        split(node, parent);
    }
}

By searching the tree for the right leaf node, inserting the key into it, and splitting the leaf node if necessary, the insert() method inserts a key into the B-Tree. A new leaf node is made and is set as the root if the root node is null. The while loop used by the insert() method traverses the tree from the root node to the proper leaf node. The split() method is used to split the leaf node if it fills to maintain the balance of the B-Tree.

3.BTree search() method

template <typename T, int M>
bool BTree<T, M>::search(const T& key) const {
    Node* node = root;

    while (node != nullptr) {
        int i = 0;

        while (i < node->keys.size() && key > node->keys[i]) {
            ++i;
        }

        if (i < node->keys.size() && node->keys[i] == key) {
            return true;
        }

        node = node->children[i];
    }

    return false;
}

By traversing the tree and conducting a binary search on the keys in each node, the search() method searches the B-Tree for a specified key. If the key is located, it returns true; otherwise, it returns false. In order to find the appropriate child node to traverse to next, the search() method uses a while loop to iterate through the tree from the root node to the appropriate leaf node.

4.BTree split() method

template <typename T, int M>
void BTree<T, M>::split(Node* node, Node* parent) {
    Node* new_node = new Node(node->leaf);

    int mid = node->keys.size() /

If a B-Tree node fills up, the split() method is used to split it (i.e., if it contains M keys). By splitting the keys and child pointers between the two nodes, this method creates a new node to hold the keys that will be transferred from the full node. If the full node serves as the root node, the middle key is stored in a new root node that is created. The split() method inserts the middle key into the parent node after dividing the keys and child pointers between the full node and the new node using a while loop. Recursively calling the split() method will split the parent node if it fills up.

How to use B Tree class

1.To use the BTree class, you first need to include the header file that contains the class definition. Assuming you've saved the code snippets I provided earlier in a file named "btree.h", you can include it in your code like this:

#include "btree.h"

2.Once you've included the header file, you can create an instance of the BTree class by specifying the types of the keys and the maximum number of keys per node. For example, to create a B-Tree that stores integers and has a maximum of 4 keys per node, you would write:

BTree<int, 4> myBTree;

This creates an empty B-Tree with no keys.

3.To insert a key into the B-Tree, you can call the insert() method on the BTree object, passing in the key as an argument. For example, to insert the integer 42 into the B-Tree, you would write:

myBTree.insert(42);

4.To search for a key in the B-Tree, you can call the search() method on the BTree object, passing in the key as an argument. For example, to search for the integer 42 in the B-Tree, you would write:

if (myBTree.search(42)) {
    cout << "Key found!" << endl;
} else {
    cout << "Key not found." << endl;
}

That's a brief introduction to using the BTree class. Naturally, you can modify the B-Tree to meet your needs by altering the types of the keys or the maximum number of keys per node. As additional functionality is required, you can also add new methods to the BTree class.

Advantages and disadvantages of B Tree

Advantages:

1.Fast access: B-trees are ideal for databases, file systems, and other applications that call for effective data search and retrieval because they are optimized for quick access to large amounts of data.

2.Balanced: The height of a B-tree is proportional to the logarithm of the number of keys in the tree, indicating that the structure is balanced. This makes it possible to perform searches, insertions, and deletions in logarithmic time, which is very effective even for very large data sets.

3.Flexible: Trees are extremely adaptable and can be tailored to meet various needs. The tree can be tuned to use less memory and disc I/O, and the order of the nodes can be changed to optimize performance for a specific application.

Disadvantages:

1.Complexity: B-trees are more complex than other data structures, such as binary search trees. This complexity can make them more difficult to implement and debug, and may require more advanced programming skills.

2.Overhead: B-trees require additional overhead to maintain the balance and order of the tree, which can increase memory usage and processing time. This overhead can be particularly significant for smaller data sets, where the benefits of B-trees may be outweighed by the additional overhead.

3.Additional disk I/O: Despite the fact that B-trees are intended to optimize disc I/O, they sometimes need more disc accesses than other data structures, especially for smaller data sets. B-trees may become less effective as a result, and further tuning and optimization may be needed to achieve their best performance.

Complete code of B Tree in C++

   #include <iostream>
#include <vector>
using namespace std;

template <typename T, int M>
class BTree {
private:
    struct Node {
        std::vector<T> keys;
        std::vector<Node*> children;
        bool leaf;

        Node(bool is_leaf) : leaf(is_leaf) {
            keys.reserve(M - 1);
            children.reserve(M);
        }

        ~Node() {
            for (auto child : children) {
                delete child;
            }
        }
    };

    Node* root;

public:
    BTree() : root(nullptr) {}

    ~BTree() {
        delete root;
    }

    void insert(const T& key) {
        if (root == nullptr) {
            root = new Node(true);
            root->keys.push_back(key);
            return;
        }

        Node* node = root;
        Node* parent = nullptr;

        while (!node->leaf) {
            parent = node;
            int i = 0;

            while (i < node->keys.size() && key > node->keys[i]) {
                ++i;
            }

            node = node->children[i];
        }

        // Insert the key into the leaf node
        int i = 0;
        while (i < node->keys.size() && key > node->keys[i]) {
            ++i;
        }

        node->keys.insert(node->keys.begin() + i, key);

        // Split the node if necessary
        if (node->keys.size() == M) {
            split(node, parent);
        }
    }

    bool search(const T& key) const {
        Node* node = root;
        while (node != nullptr) {
            int i = 0;
            while (i < node->keys.size() && key > node->keys[i]) {
                ++i;
            }

            if (i < node->keys.size() && key == node->keys[i]) {
                return true;
            }

            node = node->leaf ? nullptr : node->children[i];
        }

        return false;
    }

    void print() const {
        if (root != nullptr) {
            print(root);
        }
    }

private:
    void split(Node* node, Node* parent) {
        const int mid = node->keys.size() / 2;
        Node* left = new Node(node->leaf);
        Node* right = new Node(node->leaf);

        left->keys.assign(node->keys.begin(), node->keys.begin() + mid);
        right->keys.assign(node->keys.begin() + mid + 1, node->keys.end());

        if (!node->leaf) {
            left->children.assign(node->children.begin(), node->children.begin() + mid + 1);
            right->children.assign(node->children.begin() + mid + 1, node->children.end());
        }

        if (parent == nullptr) {
            root = new Node(false);
            root->keys.push_back(node->keys[mid]);
            root->children.push_back(left);
            root->children.push_back(right);
        } else {
            int i = 0;
            while (i < parent->keys.size() && parent->keys[i] < node->keys[mid]) {
                ++i;
            }

            parent->keys.insert(parent->keys.begin() + i, node->keys[mid]);
            parent->children.erase(parent->children.begin() + i);
            parent->children.insert(parent->children.begin() + i, left);
            parent->children.insert(parent->children.begin() + i + 1, right);
        }

        node->keys.erase(node->keys.begin() +   mid);
    node->children.erase(node->children.begin() + mid + 1);

    delete node;
}

void print(const Node* node, int depth = 0) const {
    if (node != nullptr) {
        for (int i = 0; i < depth; ++i) {
            std::cout << "  ";
        }

        for (const auto& key : node->keys) {
            std::cout << key << ' ';
        }
        std::cout << '\n';

        if (!node->leaf) {
            for (const auto& child : node->children) {
                print(child, depth + 1);
            }
        }
    }
}

};

int main() {
BTree<int, 2> tree;
tree.insert(1);
tree.insert(2);
tree.insert(3);
tree.insert(4);
tree.insert(5);
tree.insert(6);
tree.insert(7);
tree.insert(8);
tree.insert(9);
std::cout << "The B-Tree is:\n";
tree.print();

std::cout << "Is 3 in the tree? " << std::boolalpha << tree.search(3) << '\n';
std::cout << "Is 10 in the tree? " << std::boolalpha << tree.search(10) << '\n';

return 0;
}

Conclusion

With this article at OpenGenus, you must have the complete idea of implementing B-Tree in C++ Programming Language.

Overall, the B-tree data structure is a strong one that is frequently used in programmers that need quick access to large amounts of data. Its flexible design enables it to be tailored to meet various needs, and its balanced tree structure makes data searching, insertion, and deletion efficient.

B-trees have a lot of benefits, but they also have some drawbacks, like added complexity and overhead. The advantages of B-trees frequently outweigh the drawbacks in many applications, and these drawbacks can be minimized through careful tuning and optimization.

B-tree is an effective tool for speeding up the storage and retrieval of large amounts of data in C++, and using it can greatly increase the effectiveness and performance of applications that need quick access to large amounts of data.

B Tree in C++ with OOP and template
Share this