Data Structure
Networking
RDBMS
Operating System
Java
MS Excel
iOS
HTML
CSS
Android
Python
C Programming
C++
C#
MongoDB
MySQL
Javascript
PHP
- Selected Reading
- UPSC IAS Exams Notes
- Developer's Best Practices
- Questions and Answers
- Effective Resume Writing
- HR Interview Questions
- Computer Glossary
- Who is Who
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.
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
- If T is empty, return false (K can not be found in the tree).
- If the current node contains the data value that is equal to K, return true.
- If we reached to the leaf-node and it doesn't contain the required key value K, return false.
Recursive Call
- If K < current_node.left_val, then we explore the left subtree of the current node.
- Else if current_node.left_val < K < current_node.right_val, then we explore the middle subtree of the current node.
- 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.
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:
Case 2: Insert a node with two data element whose parent contains only one data element: Insert 10 in the following 2-3 Tree:
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:
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:
#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.