What is a Crit-bit Tree?
A Crit-bit Tree (a variation of the PATRICIA Trie / Radix Tree) is a space-efficient binary prefix tree that stores strings or binary keys. Unlike a standard Trie which allocates nodes for every character, a Crit-bit Tree only stores internal nodes at critical bits (the bit indexes where keys in the subtrees diverge), reducing lookup times and eliminating space overhead for single-child paths.
Explanation
- In a Crit-bit Tree, internal nodes represent a bit index (the critical bit) and have exactly two children. Leaves store the actual keys.
- During search, we inspect the key’s bit at the node’s
crit_bitposition to decide whether to branch left (0) or right (1).
Real-World Analogy
- Think of a customs passport checkpoint.
- Instead of reading your entire passport details line by line at each gate, the system has checkpoints that ask only binary questions:
- “Is the 3rd letter of your passport ID ‘A’ or ‘B’?” → Go left or right.
- “Is the 7th letter ‘X’ or ‘Y’?” → Go left or right.
- Only when you reach the final booth does the officer do a full comparison check of your passport to verify it matches.
Why Crit-bit Trees?
- A standard Trie Prefix Tree consumes substantial memory because each character requires a node.
- Crit-bit trees require only internal nodes for keys, regardless of key lengths.
- Search requires only a single full string comparison at the leaf node, while intermediate node visits are simple bitwise checks.
- They maintain alphabetical ordering, allowing fast prefix matching and range queries.
How It Works
Structural Design
- Leaf Node: Contains the actual key string.
- Internal Node: Contains a
crit_bit(absolute bit index from start of string) and two child pointers (child[0],child[1]).
(crit_bit: 13) <-- index of first bit difference
/ \
(crit_bit: 21) "hero"
/ \
"hello" "helium"
Search Operation
-
- Traverse down from the root.
-
- At each internal node, check the bit of the search key at index
crit_bit.
- At each internal node, check the bit of the search key at index
-
- If the bit is
0, branch tochild[0]. If1, branch tochild[1].
- If the bit is
-
- Repeat until a leaf node is reached.
-
- Compare the leaf’s key with the search key. If equal, return
true, else returnfalse.
- Compare the leaf’s key with the search key. If equal, return
Insertion Operation
-
- Traverse down the tree to find the closest leaf matching the target key.
-
- Find the first bit position
diff_bitwhere the target key differs from .
- Find the first bit position
-
- If they are identical, the key is already in the tree (duplicate).
-
- Otherwise, create a new leaf node for the target key and a new internal node with
crit_bit = diff_bit.
- Otherwise, create a new leaf node for the target key and a new internal node with
-
- Determine the parent/child branch direction by traversing from root down to
diff_bitposition. Insert the new internal node, setting the target key and the old subtree as its left/right children based on target key’s bit value atdiff_bit.
- Determine the parent/child branch direction by traversing from root down to
Time & Space Complexity
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Search | (where is key length) | auxiliary |
| Insert | auxiliary | |
| Delete | auxiliary |
- Note: The number of bit checks is bounded by , and only one full string comparison is performed at the leaf.
Implementation
-
Crit-bit Tree Implementation
Below are complete, functional implementations of a Crit-bit Tree in Python and C++, supporting search, insertion, and deletion.
def get_bit(s, i):
"""Return the bit value (0 or 1) at absolute bit index i of string s."""
byte_idx = i // 8
if byte_idx >= len(s):
return 0
bit_idx = 7 - (i % 8) # MSB to LSB
return (ord(s[byte_idx]) >> bit_idx) & 1
def find_diff_bit(s1, s2):
"""Return the first bit position where s1 and s2 differ. Return None if equal."""
limit = min(len(s1), len(s2))
diff_byte = -1
for idx in range(limit):
if s1[idx] != s2[idx]:
diff_byte = idx
break
if diff_byte == -1:
if len(s1) == len(s2):
return None
diff_byte = limit
val1 = ord(s1[limit]) if limit < len(s1) else 0
val2 = ord(s2[limit]) if limit < len(s2) else 0
diff_val = val1 ^ val2
else:
diff_val = ord(s1[diff_byte]) ^ ord(s2[diff_byte])
bit_pos = 0
while (diff_val & 128) == 0:
diff_val <<= 1
bit_pos += 1
return diff_byte * 8 + bit_pos
class CritBitNode:
def __init__(self, key=None, is_leaf=False):
self.key = key
self.is_leaf = is_leaf
self.crit_bit = -1
self.child = [None, None]
class CritBitTree:
def __init__(self):
self.root = None
def search(self, key):
"""Return True if the key is in the tree, False otherwise."""
if not self.root:
return False
curr = self.root
while not curr.is_leaf:
bit = get_bit(key, curr.crit_bit)
curr = curr.child[bit]
return curr.key == key
def insert(self, key):
"""Insert key into the tree. Return True if successful, False if duplicate."""
if not self.root:
self.root = CritBitNode(key, is_leaf=True)
return True
# 1. Search for closest leaf
curr = self.root
while not curr.is_leaf:
bit = get_bit(key, curr.crit_bit)
curr = curr.child[bit]
# 2. Find first differing bit
diff_bit = find_diff_bit(key, curr.key)
if diff_bit is None:
return False # Duplicate
# 3. Create new internal node and new leaf
new_internal = CritBitNode(is_leaf=False)
new_internal.crit_bit = diff_bit
new_leaf = CritBitNode(key, is_leaf=True)
key_bit = get_bit(key, diff_bit)
new_internal.child[key_bit] = new_leaf
# 4. Insert new internal node in the path
parent = None
curr = self.root
direction = 0
while not curr.is_leaf and curr.crit_bit < diff_bit:
parent = curr
direction = get_bit(key, curr.crit_bit)
curr = curr.child[direction]
new_internal.child[1 - key_bit] = curr
if not parent:
self.root = new_internal
else:
parent.child[direction] = new_internal
return True
def delete(self, key):
"""Delete key from the tree. Return True if successful, False if not found."""
if not self.root:
return False
parent = None
curr = self.root
direction = 0
while not curr.is_leaf:
parent = curr
direction = get_bit(key, curr.crit_bit)
curr = curr.child[direction]
if curr.key != key:
return False # Not found
if not parent:
self.root = None
return True
sibling = parent.child[1 - direction]
# Find grandparent
grandparent = None
gp_curr = self.root
gp_dir = 0
while gp_curr != parent:
grandparent = gp_curr
gp_dir = get_bit(key, gp_curr.crit_bit)
gp_curr = gp_curr.child[gp_dir]
if not grandparent:
self.root = sibling
else:
grandparent.child[gp_dir] = sibling
return True#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
struct CritBitNode {
std::string key;
bool is_leaf;
int crit_bit;
CritBitNode* child[2];
CritBitNode(std::string k, bool leaf) {
key = k;
is_leaf = leaf;
crit_bit = -1;
child[0] = nullptr;
child[1] = nullptr;
}
};
class CritBitTree {
private:
CritBitNode* root;
int get_bit(const std::string& s, int i) const {
int byte_idx = i / 8;
if (byte_idx >= s.length()) {
return 0;
}
int bit_idx = 7 - (i % 8);
return (s[byte_idx] >> bit_idx) & 1;
}
int find_diff_bit(const std::string& s1, const std::string& s2) const {
int limit = std::min(s1.length(), s2.length());
int diff_byte = -1;
for (int idx = 0; idx < limit; ++idx) {
if (s1[idx] != s2[idx]) {
diff_byte = idx;
break;
}
}
int diff_val = 0;
if (diff_byte == -1) {
if (s1.length() == s2.length()) {
return -1; // Identical
}
diff_byte = limit;
int val1 = (limit < s1.length()) ? (unsigned char)s1[limit] : 0;
int val2 = (limit < s2.length()) ? (unsigned char)s2[limit] : 0;
diff_val = val1 ^ val2;
} else {
diff_val = (unsigned char)s1[diff_byte] ^ (unsigned char)s2[diff_byte];
}
int bit_pos = 0;
while ((diff_val & 128) == 0) {
diff_val <<= 1;
bit_pos++;
}
return diff_byte * 8 + bit_pos;
}
void destroy_tree(CritBitNode* node) {
if (!node) return;
if (!node->is_leaf) {
destroy_tree(node->child[0]);
destroy_tree(node->child[1]);
}
delete node;
}
public:
CritBitTree() : root(nullptr) {}
~CritBitTree() {
destroy_tree(root);
}
bool search(const std::string& key) const {
if (!root) return false;
CritBitNode* curr = root;
while (!curr->is_leaf) {
int bit = get_bit(key, curr->crit_bit);
curr = curr->child[bit];
}
return curr->key == key;
}
bool insert(const std::string& key) {
if (!root) {
root = new CritBitNode(key, true);
return true;
}
CritBitNode* curr = root;
while (!curr->is_leaf) {
int bit = get_bit(key, curr->crit_bit);
curr = curr->child[bit];
}
int diff_bit = find_diff_bit(key, curr->key);
if (diff_bit == -1) {
return false; // Duplicate
}
CritBitNode* new_internal = new CritBitNode("", false);
new_internal->crit_bit = diff_bit;
CritBitNode* new_leaf = new CritBitNode(key, true);
int key_bit = get_bit(key, diff_bit);
new_internal->child[key_bit] = new_leaf;
CritBitNode* parent = nullptr;
curr = root;
int direction = 0;
while (!curr->is_leaf && curr->crit_bit < diff_bit) {
parent = curr;
direction = get_bit(key, curr->crit_bit);
curr = curr->child[direction];
}
new_internal->child[1 - key_bit] = curr;
if (!parent) {
root = new_internal;
} else {
parent->child[direction] = new_internal;
}
return true;
}
bool remove(const std::string& key) {
if (!root) return false;
CritBitNode* parent = nullptr;
CritBitNode* curr = root;
int direction = 0;
while (!curr->is_leaf) {
parent = curr;
direction = get_bit(key, curr->crit_bit);
curr = curr->child[direction];
}
if (curr->key != key) {
return false; // Not found
}
if (!parent) {
delete root;
root = nullptr;
return true;
}
CritBitNode* sibling = parent->child[1 - direction];
CritBitNode* grandparent = nullptr;
CritBitNode* gp_curr = root;
int gp_dir = 0;
while (gp_curr != parent) {
grandparent = gp_curr;
gp_dir = get_bit(key, gp_curr->crit_bit);
gp_curr = gp_curr->child[gp_dir];
}
if (!grandparent) {
root = sibling;
} else {
grandparent->child[gp_dir] = sibling;
}
delete curr;
delete parent;
return true;
}
};
When to Use
Use Crit-bit Trees When:
- ✅ You want a compact, fast prefix-search structure without hash collisions.
- ✅ You are dealing with variable-length string keys or binary byte arrays.
- ✅ Memory footprint is a critical priority (no pointers wasted on empty char branches).
- ✅ You require sorted iteration (alphabetical prefix traversal) of string keys.
Avoid When:
- ❌ Keys are primitive fixed-width integers (standard array indices or binary search is faster).
- ❌ You require simple lookup for random keys and ordering does not matter (standard Hash Tables are faster).
Variations & Related Concepts
- PATRICIA Trie: Practical Algorithm to Retrieve Information Coded in Alphanumeric. Crit-bit trees are a binary implementation of PATRICIA tries.
- Radix Tree: A space-optimized trie where each node that is the only child is merged with its parent.
- Trie (Prefix Tree): The base character-branching prefix tree.
Key Takeaways
- A Crit-bit Tree is a binary prefix trie that stores internal nodes only at bit positions where subtrees diverge.
- It requires only internal nodes to store keys, making it extremely memory efficient compared to traditional tries.
- Searching only does bit-testing during traversal, postponing the actual string comparison until the final leaf node.
- Insertion and deletion are dynamically balanced without complex rotations, executing in time where is key length.