L47. LCA in Binary Search Tree
Finding the Lowest Common Ancestor (LCA) in a Binary Search Tree (BST) is a classic algorithmic problem. Because a Binary Search Tree possesses an inherent ordering property—where all elements in the left subtree are smaller than the node, and all elements in the right subtree are greater—we can leverage this structural invariant to find the LCA much more efficiently than in a standard binary tree.
1. Defining the Problem: What is an LCA?
The Lowest Common Ancestor (LCA) of two nodes and in a tree is defined as the lowest node in the tree that has both and as descendants (where we allow a node to be a descendant of itself).
- Path to Root visualization: If you trace the path from node up to the root, and trace the path from node up to the root, the two paths will overlap.
- Intersection Point: The LCA is the first intersection point when looking from the bottom up, or the last intersection point when looking from the top down.
2. Naive Approach vs. BST-Optimized Approach
The Naive Binary Tree Approach
If you treat the BST as a generic Binary Tree (ignoring the left-less, right-greater rule):
- You must traverse the entire tree to locate both nodes or use a recursive function visiting every node.
- Time Complexity: , where is the total number of nodes in the tree.
The BST-Optimized Approach
Because it is a Binary Search Tree, we do not need to traverse every node blindly. We can navigate based on value comparisons, drastically cutting down our search space to the height of the tree.
- Time Complexity: , where is the height of the tree (which is for balanced BSTs).
- Space Complexity: iterative auxiliary space (or recursive stack space).
3. Core Mental Model and Decision Possibilities
When standing at any current node during our traversal while searching for nodes and , there are four fundamental relative positioning possibilities:
- Both nodes lie to the right: Both and are strictly greater than the current node's value.
- Both nodes lie to the left: Both and are strictly smaller than the current node's value.
- Split Path (LCA Found): One node lies on the left subtree and the other lies on the right subtree (or one of the nodes is the current node itself).
- Current Node is or : The current node matches one of the target values.
Real-Life Analogy: Navigating a City Grid
Imagine looking for a meeting point between two friends coming from different neighborhoods. If both friends live strictly north of your current highway exit, you must drive further north. If both live south, you drive south. The exact moment their paths diverge—where one friend needs to go left (west) and the other right (east)—is your optimal meeting point (the LCA).
4. Step-by-Step Flow Walkthrough
Let us trace an example using a BST where we want to find the LCA of nodes 5 and 9.
10
/ \
4 12
/ \
2 8
/ \
5 9
- Step 1: Start at root 10. We are looking for 5 and 9.
- Are both values ? Yes ( and ).
- Action: Move to the left child (4).
- Step 2: At node 4.
- Are both values ? No ( and ).
- Are both values ? Yes ( and ).
- Action: Move to the right child (8).
- Step 3: At node 8.
- Are both values ? No (, but ).
- Are both values ? No (, but ).
- Path Split: The values have split across the current node. One is smaller (), one is greater ().
- Conclusion: Node 8 is our Lowest Common Ancestor.
5. Comparative Strategy Table
6. Implementation Code (Recursive Java Solution)
Below is the clean, production-grade recursive implementation in Java as demonstrated in the lecture. The C++ implementation is structurally identical, substituting arrow operators (->) for dot notation (.) where applicable.
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
public class BSTLowestCommonAncestor {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// Base case: if root is null, return null
if (root == null) {
return null;
}
// If both nodes are greater than the current node, LCA lies in the right subtree
if (p.val > root.val && q.val > root.val) {
return lowestCommonAncestor(root.right, p, q);
}
// If both nodes are smaller than the current node, LCA lies in the left subtree
if (p.val < root.val && q.val < root.val) {
return lowestCommonAncestor(root.left, p, q);
}
// If neither condition is met, we have hit the split point (LCA)
return root;
}
}
Note on Iterative Optimization: While the recursion above takes stack space, you can easily rewrite this logic using a simple
while (root != null)loop to achieve auxiliary space complexity by reassigningroot = root.leftorroot = root.rightdynamically.
7. Summary and Key Takeaways
- BST Invariant Utilization: Always look to exploit BST properties (left < node < right) to avoid unnecessary traversal.
- Definition of LCA: The lowest common ancestor is the exact divergence/split point where target nodes stop sharing the same subtree direction.
- Comparison Rules:
- If
p.val > root.valandq.val > root.valTraverse Right. - If
p.val < root.valandq.val < root.valTraverse Left. - Otherwise Current
rootis the LCA.
- If
- Time Complexity: Bounded by the height of the tree: , transforming worst-case linear time () into logarithmic time () for balanced trees.
- Space Complexity: Can be optimized from recursion stack down to space using a straightforward iterative
whileloop.