Improve search in BST with sentinel
Problem
In this problem, we aim to improve the search operation in a Binary Search Tree (BST). Specifically, we introduce the concept of a sentinel node. A sentinel node ensures that searches do not reach end-of-tree null values during traversal. Instead, each null child pointer is replaced with a reference to a sentinel node. This approach simplifies handling cases like searching for non-existing elements and reduces complexity in edge-case processing.
Examples
8
/ \
3 10
/ \
1 6
/ \
4 7
Example 1
Input: Tree: [8, 3, 10, 1, 6, null, null, null, null, 4, 7]
SentinelValue: -1
SearchKey: 4
Output: Node found with key: 4
Example 2
Input:
Tree: [8, 3, 10, 1, 6, null, null, null, null, 4, 7]
SentinelValue: -1
SearchKey: 9
Output:
Node not found. Sentinel reached with value: -1
Explanation:
The BST structure is the same as Example 1.
The search operation does not find the node with value 9 and stops at the sentinel node with a value of -1.
Solution
Method 1 - Using the sentinel
The traditional method of searching in a Binary Search Tree (BST) involves starting at the root and traversing the tree based on comparison with the target value. If the value is smaller, you move left; if it's larger, you move right. The process stops either when the target is found or when a null node (representing a leaf) is reached.
Search without Sentinel
Code
Java
public class SearchWithoutSentinel {
public static TreeNode search(TreeNode root, int key) {
while (root != null) {
if (root.val == key) {
return root; // Node found
}
root = key < root.val ? root.left : root.right;
}
return null; // Not found
}
public static void main(String[] args) {
TreeNode root = new TreeNode(8);
root.left = new TreeNode(3);
root.right = new TreeNode(10);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(6);
TreeNode result = search(root, 6);
System.out.println(result != null ? "Node found with key: " + result.val : "Node not found");
}
}
- You traverse the tree until you find the target node or hit a
nullpointer. - Conditional Statements:
- One
whilecondition:root != null. - One check inside the loop:
key < root.val. - Combined average of 2.5 checks per iteration.
- One
Cost: On a tree with 1000 nodes, this results in roughly 2500 conditional checks during traversal.
Using the sentinel
We can optimise this process by replacing null child pointers in the BST with a sentinel node — a special placeholder object with a unique value. During the search, the sentinel node guarantees a "hit" every time, either on an actual node or on the sentinel itself. This eliminates the need to explicitly handle null.
Optimised Sentinel Node Implementation
- Sentinel Node Introduction:
- Use a static placeholder node (
sentinel) for all leaf child pointers. - Before initiating a search, set
sentinel.valto the target value.
- Use a static placeholder node (
- Benefits:
- Reduces the number of conditional checks per iteration from an average of 2.5 to 2.
- Saves 0.5 checks per iteration, which accumulates into significant performance benefits for large trees or frequent searches.
- Efficiency Gains:
- On a tree with 1000 nodes, this method saves 500 conditional checks (from 2500 to 2000).
Advantages
- Performance Gains: Saving 500 conditional checks over 1000 nodes is significant for large and frequently accessed trees.
- Simplified Logic: No need to explicitly handle
null(leaf) cases — always guaranteed a hit. - Use Cases:
- Optimised traversal for trees with large sizes.
- Scenarios with frequent searches, such as hot sections in code.
This improvement not only optimises conditional evaluations but also enhances clarity and reliability in tree traversal operations. For example, in high-performance applications requiring thousands of traversals, this method consistently outperforms traditional search mechanisms.
Code
Java
public class SearchWithSentinel {
static TreeNode sentinel = new TreeNode(-1); // A static sentinel node
public static TreeNode search(TreeNode root, int key) {
sentinel.val = key; // Set the sentinel to the target value
while (root != sentinel) {
if (root.val == key) {
return root; // Node found
}
root = key < root.val ? root.left : root.right;
}
return null; // Not found (guaranteed hit on sentinel, need additional check)
}
public static TreeNode insert(TreeNode root, int key) {
if (root == null) {
TreeNode newNode = new TreeNode(key);
newNode.left = newNode.right = sentinel; // Leaf children point to sentinel
return newNode;
}
if (key < root.val) {
root.left = insert(root.left, key);
} else if (key > root.val) {
root.right = insert(root.right, key);
}
return root;
}
public static void main(String[] args) {
TreeNode root = null;
root = insert(root, 8);
root = insert(root, 3);
root = insert(root, 10);
root = insert(root, 1);
root = insert(root, 6);
TreeNode result = search(root, 6);
System.out.println(result != null ? "Node found with key: " + result.val : "Node not found");
TreeNode notFoundResult = search(root, 15);
System.out.println(notFoundResult != null ? "Node found with key: " + notFoundResult.val : "Node not found (sentinel reached)");
}
}
Python
class SentinelNode(TreeNode):
def __init__(self, val):
super().__init__(val)
class Solution:
def __init__(self, sentinel_value):
self.sentinel = SentinelNode(sentinel_value)
def insert(self, root, key):
if root is None:
new_node = TreeNode(key)
new_node.left = self.sentinel
new_node.right = self.sentinel
return new_node
if key < root.val:
root.left = self.insert(root.left, key)
elif key > root.val:
root.right = self.insert(root.right, key)
return root
def search(self, root, key):
while root != self.sentinel:
if root.val == key:
return f"Node found with key: {key}"
elif key < root.val:
root = root.left
else:
root = root.right
return f"Node not found. Sentinel reached with value: {self.sentinel.val}"
# Example usage
solution = Solution(sentinel_value=-1)
root = None
root = solution.insert(root, 8)
root = solution.insert(root, 3)
root = solution.insert(root, 10)
root = solution.insert(root, 1)
root = solution.insert(root, 6)
root = solution.insert(root, 4)
root = solution.insert(root, 7)
# Example searches
print(solution.search(root, 4)) # Node found with key: 4
print(solution.search(root, 9)) # Node not found. Sentinel reached with value: -1
Complexity
- ⏰ Time complexity:
O(h). The search operation involves traversing the height of the BST. For a balanced BST, the time complexity isO(log n). For a degenerate (unbalanced) tree, the complexity deteriorates toO(n). - 🧺 Space complexity:
O(1). The space complexity isO(1)for iterative search since we use a constant amount of space. For recursive search, the space complexity isO(h)due to the recursion stack, wherehis the height of the BST.