2-3 Trees (Search and Insert) in C/C++?



What is 2-3 Trees?

A 2-3 tree is a tree data structure, where each internal node has either 2 or 3 children (also, we can say that 2-nodes and 3-nodes, respectively). It is a type of B-tree that ensures efficient search, insertion, and deletion operations with O(logn) time complexity.

2-3 tree

Properties of 2-3 tree

  • 2-node contains one data element and has two children (or none if it is a leaf).
  • 3-node contains two data elements and has three children (or none if it is a leaf).
  • Data is stored in the sorted order.
  • It is a balanced tree.
  • All the leaf node are at the same level.
  • Each node can either be leaf, 2 node, or 3 node.

2-3 Tree: Search Operation

Searching for an element in a 2-3 tree is same as searching for an item in a binary search tree. To search for a key K in the given 2-3 tree T, we follow the following steps:

Base Case

  1. If T is empty, return false (K can not be found in the tree).
  2. If the current node contains the data value that is equal to K, return true.
  3. If we reached to the leaf-node and it doesn't contain the required key value K, return false.

Recursive Call

  1. If K < current_node.left_val, then we explore the left subtree of the current node.
  2. Else if current_node.left_val < K < current_node.right_val, then we explore the middle subtree of the current node.
  3. Else if K > current_node.right_val, then we explore the right subtree of the current node.

Let's see the following example: Search 5 in following 2-3 tree.

2-3 tree search current node current middle node not found

Following is the code of the search operation:

bool search(Node* node, int key) {
   if (!node) return false;
   int i = 0;
   while (i < node->nKeys && key > node->key[i])
      ++i;
   if (i < node->nKeys && key == node->key[i])
      return true;
   return search(node->child[i], key);
}

2-3 Tree: Insertion Operation

To perform an insertion operation in a 2-3 tree, we need to find the proper location of the key 'k' and append it there.

  • Find the proper leaf node where the new key should go.
  • Insert the key:
  • If the node has 1 key, add the new key.
  • If it already has 2 keys, then:
  • Temporarily store 3 keys
  • Split the node into two nodes with 1 key each
  • Promote the middle key to the parent
  • If the root splits, create a new root with 1 key and two children.

There are three possible cases in insertion, which you can see below:

Case 1: Insert in a node with only one data element: Insert 4 in the following 2-3 Tree:

2-3 tree insertion

Case 2: Insert a node with two data element whose parent contains only one data element: Insert 10 in the following 2-3 Tree:

2-3 tree insertion 2-3 tree insertion

Case 3: Insert in a node with two data elements whose parent also contains two data elements: Insert 1 in the following 2-3 Tree:

2-3 tree 2-3 tree insertion

Following is the code of the insertion operation:

void insertInternal(Node*& node, int key, int& upKey, Node*& newChild) {
   int tempKeys[3], i;
   Node* tempChildren[4];

   // If the current node is a leaf
   if (node->isLeaf) {
      // Copy existing keys to a temporary array
      for (i = 0; i < node->nKeys; ++i)
         tempKeys[i] = node->key[i];

      // Insert the new key in sorted order
      i = node->nKeys - 1;
      while (i >= 0 && key < tempKeys[i]) {
         tempKeys[i + 1] = tempKeys[i];
         --i;
      }
      tempKeys[i + 1] = key;

      // If the leaf node has room (only 1 key), insert without splitting
      if (node->nKeys < 2) {
         for (int j = 0; j <= node->nKeys; ++j)
            node->key[j] = tempKeys[j];
         node->nKeys++;
         newChild = nullptr; // No split occurred
      } else {
         // Leaf node is full, needs to be split
         node->key[0] = tempKeys[0]; // Left node keeps first key
         node->nKeys = 1;

         newChild = new Node(); // Create right node
         newChild->key[0] = tempKeys[2]; // Right node gets third key
         newChild->nKeys = 1;

         upKey = tempKeys[1]; // Promote middle key to parent
      }
   } else {
      // Internal node: find child to recurse into
      i = node->nKeys - 1;
      while (i >= 0 && key < node->key[i])
         --i;
      int pos = i + 1;

      int tempUpKey;
      Node* tempNewChild = nullptr;

      // Recursively insert into appropriate child
      insertInternal(node->child[pos], key, tempUpKey, tempNewChild);

      // If no split happened in child, just return
      if (!tempNewChild) {
         newChild = nullptr;
         return;
      }

      // Child split occurred: insert promoted key into current node
      for (i = 0; i < node->nKeys; ++i)
         tempKeys[i] = node->key[i];
      for (i = 0; i <= node->nKeys; ++i)
         tempChildren[i] = node->child[i];

      // Shift keys and children to make space for new key and child
      i = node->nKeys - 1;
      while (i >= pos) {
         tempKeys[i + 1] = tempKeys[i];
         tempChildren[i + 2] = tempChildren[i + 1];
         --i;
      }

      // Insert the new promoted key and corresponding child
      tempKeys[pos] = tempUpKey;
      tempChildren[pos + 1] = tempNewChild;

      // If current node has room, insert without splitting
      if (node->nKeys < 2) {
         for (int j = 0; j <= node->nKeys + 1; ++j)
            node->child[j] = tempChildren[j];
         for (int j = 0; j <= node->nKeys; ++j)
            node->key[j] = tempKeys[j];
         node->nKeys++;
         newChild = nullptr;
      } else {
         // Current node is full, needs to be split
         node->key[0] = tempKeys[0];
         node->child[0] = tempChildren[0];
         node->child[1] = tempChildren[1];
         node->nKeys = 1;

         newChild = new Node();
         newChild->isLeaf = false;
         newChild->key[0] = tempKeys[2];
         newChild->child[0] = tempChildren[2];
         newChild->child[1] = tempChildren[3];
         newChild->nKeys = 1;

         upKey = tempKeys[1]; // Promote middle key to parent
      }
   }
}

// Entry point for inserting a key into the 2-3 tree
void insert(Node*& root, int key) {
   int upKey;
   Node* newChild = nullptr;

   // Start the recursive insertion process
   insertInternal(root, key, upKey, newChild);

   // If the root node split, create a new root
   if (newChild) {
      Node* newRoot = new Node();
      newRoot->key[0] = upKey;
      newRoot->child[0] = root;
      newRoot->child[1] = newChild;
      newRoot->nKeys = 1;
      newRoot->isLeaf = false;
      root = newRoot;
   }
}

Example: Search and Insert Operation in 2-3 Tree

In the following example, we implement search and insert operation in 2-3 tree using C/C++ Program:

C++ C
#include <iostream>
using namespace std;

// Node definition for 2-3 Tree
struct Node {
   int nKeys; // Number of keys (1 or 2)
   int key[3]; // Temporary array to hold 3 keys during split
   Node * child[4]; // Up to 4 children during split
   bool isLeaf;

   Node() {
      nKeys = 0;
      isLeaf = true;
      for (int i = 0; i < 4; ++i)
         child[i] = nullptr;
   }
};

// Inorder traversal (sorted output)
void traverse(Node * node) {
   if (!node) return;
   for (int i = 0; i < node -> nKeys; ++i) {
      traverse(node -> child[i]);
      cout << node -> key[i] << " ";
   }
   traverse(node -> child[node -> nKeys]);
}

// Search function
bool search(Node * node, int key) {
   if (!node) return false;
   int i = 0;
   while (i < node -> nKeys && key > node -> key[i])
      ++i;
   if (i < node -> nKeys && key == node -> key[i])
      return true;
   return search(node -> child[i], key);
}

// Internal insert function that handles recursion and splitting
void insertInternal(Node * & node, int key, int & upKey, Node * & newChild) {
   int tempKeys[3], i;
   Node * tempChildren[4];

   if (node -> isLeaf) {
      // Leaf case: insert into tempKeys
      for (i = 0; i < node -> nKeys; ++i)
         tempKeys[i] = node -> key[i];

      i = node -> nKeys - 1;
      while (i >= 0 && key < tempKeys[i]) {
         tempKeys[i + 1] = tempKeys[i];
         --i;
      }
      tempKeys[i + 1] = key;

      if (node -> nKeys < 2) {
         // No split needed
         for (int j = 0; j <= node -> nKeys; ++j)
            node -> key[j] = tempKeys[j];
         node -> nKeys++;
         newChild = nullptr;
      } else {
         // Split leaf node
         node -> key[0] = tempKeys[0];
         node -> nKeys = 1;

         newChild = new Node();
         newChild -> key[0] = tempKeys[2];
         newChild -> nKeys = 1;

         upKey = tempKeys[1];
      }
   } else {
      // Internal node case
      i = node -> nKeys - 1;
      while (i >= 0 && key < node -> key[i])
         --i;
      int pos = i + 1;

      int tempUpKey;
      Node * tempNewChild = nullptr;
      insertInternal(node -> child[pos], key, tempUpKey, tempNewChild);

      if (!tempNewChild) {
         newChild = nullptr;
         return;
      }

      // Copy keys and children to temp arrays
      for (i = 0; i < node -> nKeys; ++i)
         tempKeys[i] = node -> key[i];
      for (i = 0; i <= node -> nKeys; ++i)
         tempChildren[i] = node -> child[i];

      // Insert new key and child in order
      i = node -> nKeys - 1;
      while (i >= pos) {
         tempKeys[i + 1] = tempKeys[i];
         tempChildren[i + 2] = tempChildren[i + 1];
         --i;
      }
      tempKeys[pos] = tempUpKey;
      tempChildren[pos + 1] = tempNewChild;

      if (node -> nKeys < 2) {
         // No split needed
         for (int j = 0; j <= node -> nKeys + 1; ++j)
            node -> child[j] = tempChildren[j];
         for (int j = 0; j <= node -> nKeys; ++j)
            node -> key[j] = tempKeys[j];
         node -> nKeys++;
         newChild = nullptr;
      } else {
         // Split internal node
         node -> key[0] = tempKeys[0];
         node -> child[0] = tempChildren[0];
         node -> child[1] = tempChildren[1];
         node -> nKeys = 1;

         newChild = new Node();
         newChild -> isLeaf = false;
         newChild -> key[0] = tempKeys[2];
         newChild -> child[0] = tempChildren[2];
         newChild -> child[1] = tempChildren[3];
         newChild -> nKeys = 1;

         upKey = tempKeys[1];
      }
   }
}

// Public insert function
void insert(Node * & root, int key) {
   int upKey;
   Node * newChild = nullptr;
   insertInternal(root, key, upKey, newChild);

   if (newChild) {
      // Create new root if root is split
      Node * newRoot = new Node();
      newRoot -> key[0] = upKey;
      newRoot -> child[0] = root;
      newRoot -> child[1] = newChild;
      newRoot -> nKeys = 1;
      newRoot -> isLeaf = false;
      root = newRoot;
   }
}

int main() {
   Node * root = new Node();
   int keys[] = {2, 7, 1, 3, 6, 9, 11};
   for (int key: keys)
      insert(root, key);
   cout << "Inorder Traversal of 2-3 Tree:\n";
   traverse(root);
   cout << "\n";
   int target = 3;
   cout << "Search " << target << ": " << (search(root, target) ? "Found" : "Not Found") << "\n";
   target = 5;
   cout << "Search " << target << ": " << (search(root, target) ? "Found" : "Not Found") << "\n";
   return 0;
}

Following is the output:

Inorder Traversal of 2-3 Tree:
1 2 3 6 7 9 11 
Search 3: Found
Search 5: Not Found
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// Definition of the Node structure
typedef struct Node {
   int nKeys;
   int key[3]; // Temporarily hold 3 keys during split
   struct Node * child[4]; // Can have up to 4 children
   bool isLeaf;
}
Node;

// Function to create a new node
Node * createNode() {
   Node * newNode = (Node * ) malloc(sizeof(Node));
   newNode -> nKeys = 0;
   newNode -> isLeaf = true;
   for (int i = 0; i < 4; ++i)
      newNode -> child[i] = NULL;
   return newNode;
}

// Inorder traversal
void traverse(Node * node) {
   if (!node) return;
   for (int i = 0; i < node -> nKeys; ++i) {
      traverse(node -> child[i]);
      printf("%d ", node -> key[i]);
   }
   traverse(node -> child[node -> nKeys]);
}

// Search key in 2-3 Tree
bool search(Node * node, int key) {
   if (!node) return false;
   int i = 0;
   while (i < node -> nKeys && key > node -> key[i])
      ++i;
   if (i < node -> nKeys && key == node -> key[i])
      return true;
   return search(node -> child[i], key);
}

// Internal insert function
void insertInternal(Node * node, int key, int * upKey, Node ** newChild) {
   int tempKeys[3];
   Node * tempChildren[4];
   int i;
   if (node -> isLeaf) {
      // Insert key in sorted order into tempKeys
      for (i = 0; i < node -> nKeys; ++i)
         tempKeys[i] = node -> key[i];

      i = node -> nKeys - 1;
      while (i >= 0 && key < tempKeys[i]) {
         tempKeys[i + 1] = tempKeys[i];
         --i;
      }
      tempKeys[i + 1] = key;
      if (node -> nKeys < 2) {
         for (int j = 0; j <= node -> nKeys; ++j)
            node -> key[j] = tempKeys[j];
         node -> nKeys++;
         * newChild = NULL;
      } else {
         // Split leaf node
         node -> key[0] = tempKeys[0];
         node -> nKeys = 1;
         Node * right = createNode();
         right -> key[0] = tempKeys[2];
         right -> nKeys = 1;

         * upKey = tempKeys[1];
         * newChild = right;
      }
   } else {
      // Internal node
      i = node -> nKeys - 1;
      while (i >= 0 && key < node -> key[i])
         --i;
      int pos = i + 1;

      int tempUpKey;
      Node * tempNewChild = NULL;
      insertInternal(node -> child[pos], key, & tempUpKey, & tempNewChild);

      if (!tempNewChild) {
         * newChild = NULL;
         return;
      }

      for (i = 0; i < node -> nKeys; ++i)
         tempKeys[i] = node -> key[i];
      for (i = 0; i <= node -> nKeys; ++i)
         tempChildren[i] = node -> child[i];

      // Insert new key and child into temp arrays
      i = node -> nKeys - 1;
      while (i >= pos) {
         tempKeys[i + 1] = tempKeys[i];
         tempChildren[i + 2] = tempChildren[i + 1];
         --i;
      }
      tempKeys[pos] = tempUpKey;
      tempChildren[pos + 1] = tempNewChild;

      if (node -> nKeys < 2) {
         for (int j = 0; j <= node -> nKeys + 1; ++j)
            node -> child[j] = tempChildren[j];
         for (int j = 0; j <= node -> nKeys; ++j)
            node -> key[j] = tempKeys[j];
         node -> nKeys++;
         * newChild = NULL;
      } else {
         // Split internal node
         node -> key[0] = tempKeys[0];
         node -> child[0] = tempChildren[0];
         node -> child[1] = tempChildren[1];
         node -> nKeys = 1;

         Node * right = createNode();
         right -> isLeaf = false;
         right -> key[0] = tempKeys[2];
         right -> child[0] = tempChildren[2];
         right -> child[1] = tempChildren[3];
         right -> nKeys = 1;

         * upKey = tempKeys[1];
         * newChild = right;
      }
   }
}

// Public insert function
void insert(Node ** root, int key) {
   int upKey;
   Node * newChild = NULL;
   insertInternal( * root, key, & upKey, & newChild);

   if (newChild) {
      Node * newRoot = createNode();
      newRoot -> key[0] = upKey;
      newRoot -> child[0] = * root;
      newRoot -> child[1] = newChild;
      newRoot -> nKeys = 1;
      newRoot -> isLeaf = false;
      * root = newRoot;
   }
}

int main() {
   Node * root = createNode();
   int keys[] = { 2, 7, 1, 3, 6, 9, 11 };
   int n = sizeof(keys) / sizeof(keys[0]);
   for (int i = 0; i < n; ++i)
      insert( & root, keys[i]);
	  
   printf("Inorder Traversal of 2-3 Tree:\n");
   traverse(root);
   printf("\n");
   int target = 3;
   printf("Search %d: %s\n", target, search(root, target) ? "Found" : "Not Found");
   target = 5;
   printf("Search %d: %s\n", target, search(root, target) ? "Found" : "Not Found");
   return 0;
}

Following is the output:

1 2 3 6 7 9 11 
Search 3: Found
Search 5: Not Found

Conclusion

We learned that a 2-3 tree is a balanced search tree that ensures efficient search and insertion operations in O(logn) time. It maintains balance by splitting nodes and enabling keys during insertion. With the help of diagrams, we understand how insertion and searching operations can take place.

Updated on: 2025-07-25T17:12:28+05:30

844 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements