Lowest Common Ancestor of a BST
Imagine you are navigating a family tree of employees in a company organized by hierarchy, and you want to find the closest common manager of two employees quickly.
Given a binary search tree (BST) and two nodes p and q in the BST, find the lowest common ancestor (LCA) of p and q. The LCA is defined as the lowest node in the BST that has both p and q as descendants (where we allow a node to be a descendant of itself). Input: root of BST, nodes p and q. Output: the LCA node.
The number of nodes in the tree is in the range [1, 10^5]All node values are uniquep and q are different nodes and both exist in the BST"root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8"6The LCA of nodes 2 and 8 is 6 because 6 is the lowest node that has both 2 and 8 as descendants.
"root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4"2The LCA of nodes 2 and 4 is 2 because a node can be a descendant of itself.
- p is ancestor of q → LCA is p
- q is ancestor of p → LCA is q
- p and q are on different subtrees → LCA is root or intermediate node
- Tree with only one node → LCA is that node
Solution becomes inefficient and may time out on large inputs
✅ Use BST ordering to prune search space
Incorrect LCA returned when one node is ancestor of the other
✅ Allow returning p or q if it matches current node during search
Code may crash or return incorrect results
✅ Add base case checks for null root
Stack overflow error on very deep BSTs
✅ Use iterative approach to avoid recursion stack
Incorrect traversal direction, wrong LCA found
✅ Use proper comparison operators to decide subtree traversal
Intuition
Find the path from root to p and root to q separately, then compare these paths to find the last common node, which is the LCA.
Algorithm
- Traverse the BST from root to find the path to node p and store it.
- Traverse the BST from root to find the path to node q and store it.
- Compare the two paths node by node until they differ.
- Return the last common node found in both paths as the LCA.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def find_path(root, target, path):
if not root:
return False
path.append(root)
if root.val == target.val:
return True
if (root.left and find_path(root.left, target, path)) or (root.right and find_path(root.right, target, path)):
return True
path.pop()
return False
def lowestCommonAncestor(root, p, q):
path_p = []
path_q = []
find_path(root, p, path_p)
find_path(root, q, path_q)
i = 0
while i < len(path_p) and i < len(path_q) and path_p[i] == path_q[i]:
i += 1
return path_p[i-1]
# Example usage:
# Construct BST nodes
# root = TreeNode(6)
# ... build tree ...
# print(lowestCommonAncestor(root, p, q).val)def find_path(root, target, path):Defines helper to find path from root to target nodeif not root:Base case: reached leaf without finding targetpath.append(root)Add current node to path as we go downif root.val == target.val:Check if current node is targetif (root.left and find_path(root.left, target, path)) or (root.right and find_path(root.right, target, path))Recursively search left or right subtreepath.pop()Backtrack if target not found in this pathfind_path(root, p, path_p)Find path to node pfind_path(root, q, path_q)Find path to node qwhile i < len(path_p) and i < len(path_q) and path_p[i] == path_q[i]:Compare paths to find last common nodereturn path_p[i-1]Return the last common ancestor nodeimport java.util.*;
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int x) { val = x; }
}
public class Solution {
public boolean findPath(TreeNode root, TreeNode target, List<TreeNode> path) {
if (root == null) return false;
path.add(root);
if (root.val == target.val) return true;
if ((root.left != null && findPath(root.left, target, path)) || (root.right != null && findPath(root.right, target, path)))
return true;
path.remove(path.size() - 1);
return false;
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
List<TreeNode> pathP = new ArrayList<>();
List<TreeNode> pathQ = new ArrayList<>();
findPath(root, p, pathP);
findPath(root, q, pathQ);
int i = 0;
while (i < pathP.size() && i < pathQ.size() && pathP.get(i) == pathQ.get(i)) {
i++;
}
return pathP.get(i - 1);
}
// main method and tree construction can be added for testing
}public boolean findPath(TreeNode root, TreeNode target, List<TreeNode> path)Helper to find path from root to targetif (root == null) return false;Base case: no node foundpath.add(root);Add current node to pathif (root.val == target.val) return true;Check if current node is targetif ((root.left != null && findPath(root.left, target, path)) || (root.right != null && findPath(root.right, target, path)))Search left or right subtree recursivelypath.remove(path.size() - 1);Backtrack if target not foundfindPath(root, p, pathP);Find path to pfindPath(root, q, pathQ);Find path to qwhile (i < pathP.size() && i < pathQ.size() && pathP.get(i) == pathQ.get(i))Compare paths to find last common nodereturn pathP.get(i - 1);Return LCA node#include <iostream>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left, *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
bool findPath(TreeNode* root, TreeNode* target, vector<TreeNode*>& path) {
if (!root) return false;
path.push_back(root);
if (root->val == target->val) return true;
if ((root->left && findPath(root->left, target, path)) || (root->right && findPath(root->right, target, path)))
return true;
path.pop_back();
return false;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
vector<TreeNode*> pathP, pathQ;
findPath(root, p, pathP);
findPath(root, q, pathQ);
int i = 0;
while (i < pathP.size() && i < pathQ.size() && pathP[i] == pathQ[i])
i++;
return pathP[i - 1];
}
// main function and tree construction can be added for testingbool findPath(TreeNode* root, TreeNode* target, vector<TreeNode*>& path)Helper to find path from root to target nodeif (!root) return false;Base case: reached null nodepath.push_back(root);Add current node to pathif (root->val == target->val) return true;Check if current node is targetif ((root->left && findPath(root->left, target, path)) || (root->right && findPath(root->right, target, path)))Recursively search left or right subtreepath.pop_back();Backtrack if target not foundfindPath(root, p, pathP);Find path to pfindPath(root, q, pathQ);Find path to qwhile (i < pathP.size() && i < pathQ.size() && pathP[i] == pathQ[i])Compare paths to find last common nodereturn pathP[i - 1];Return LCA nodefunction TreeNode(val, left=null, right=null) {
this.val = val;
this.left = left;
this.right = right;
}
function findPath(root, target, path) {
if (!root) return false;
path.push(root);
if (root.val === target.val) return true;
if ((root.left && findPath(root.left, target, path)) || (root.right && findPath(root.right, target, path)))
return true;
path.pop();
return false;
}
function lowestCommonAncestor(root, p, q) {
const pathP = [];
const pathQ = [];
findPath(root, p, pathP);
findPath(root, q, pathQ);
let i = 0;
while (i < pathP.length && i < pathQ.length && pathP[i] === pathQ[i]) {
i++;
}
return pathP[i - 1];
}
// Example usage:
// let root = new TreeNode(6);
// ... build tree ...
// console.log(lowestCommonAncestor(root, p, q).val);function findPath(root, target, path)Helper to find path from root to targetif (!root) return false;Base case: no node foundpath.push(root);Add current node to pathif (root.val === target.val) return true;Check if current node is targetif ((root.left && findPath(root.left, target, path)) || (root.right && findPath(root.right, target, path)))Recursively search left or right subtreepath.pop();Backtrack if target not foundfindPath(root, p, pathP);Find path to pfindPath(root, q, pathQ);Find path to qwhile (i < pathP.length && i < pathQ.length && pathP[i] === pathQ[i])Compare paths to find last common nodereturn pathP[i - 1];Return LCA nodeO(n) in worst case, where n is number of nodes, because we may traverse the tree twice to find pathsO(n) for storing paths and recursion stackFinding each path can take O(n) time in worst case, and storing paths requires O(n) space. Comparing paths is O(n) as well.
This approach is easy to implement but not optimal. It helps beginners understand the problem before moving to better solutions.
Intuition
Because BST nodes are ordered, if both p and q are smaller than current node, LCA lies in left subtree; if both are greater, LCA lies in right subtree; otherwise current node is LCA.
Algorithm
- Start at root node.
- If both p and q values are less than root's value, recurse into left subtree.
- If both p and q values are greater than root's value, recurse into right subtree.
- Otherwise, root is the LCA.
LCA(root) = if p.val < root.val and q.val < root.val then LCA(root.left) else if p.val > root.val and q.val > root.val then LCA(root.right) else rootdef lowestCommonAncestor(root, p, q):
if root is None:
return None
if p.val < root.val and q.val < root.val:
return lowestCommonAncestor(root.left, p, q)
elif p.val > root.val and q.val > root.val:
return lowestCommonAncestor(root.right, p, q)
else:
return root
# Example usage:
# print(lowestCommonAncestor(root, p, q).val)if root is None:Base case: empty subtreeif p.val < root.val and q.val < root.val:Both nodes smaller, go leftreturn lowestCommonAncestor(root.left, p, q)Recurse left subtreeelif p.val > root.val and q.val > root.val:Both nodes greater, go rightreturn lowestCommonAncestor(root.right, p, q)Recurse right subtreeelse:Nodes split, current root is LCAreturn rootReturn LCA nodepublic TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return null;
if (p.val < root.val && q.val < root.val) {
return lowestCommonAncestor(root.left, p, q);
} else if (p.val > root.val && q.val > root.val) {
return lowestCommonAncestor(root.right, p, q);
} else {
return root;
}
}
// main method and tree construction can be added for testingif (root == null) return null;Base case: empty subtreeif (p.val < root.val && q.val < root.val)Both nodes smaller, go leftreturn lowestCommonAncestor(root.left, p, q);Recurse left subtreeelse if (p.val > root.val && q.val > root.val)Both nodes greater, go rightreturn lowestCommonAncestor(root.right, p, q);Recurse right subtreeelseNodes split, current root is LCAreturn root;Return LCA nodeTreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root) return nullptr;
if (p->val < root->val && q->val < root->val) {
return lowestCommonAncestor(root->left, p, q);
} else if (p->val > root->val && q->val > root->val) {
return lowestCommonAncestor(root->right, p, q);
} else {
return root;
}
}
// main function and tree construction can be added for testingif (!root) return nullptr;Base case: empty subtreeif (p->val < root->val && q->val < root->val)Both nodes smaller, go leftreturn lowestCommonAncestor(root->left, p, q);Recurse left subtreeelse if (p->val > root->val && q->val > root->val)Both nodes greater, go rightreturn lowestCommonAncestor(root->right, p, q);Recurse right subtreeelseNodes split, current root is LCAreturn root;Return LCA nodefunction lowestCommonAncestor(root, p, q) {
if (!root) return null;
if (p.val < root.val && q.val < root.val) {
return lowestCommonAncestor(root.left, p, q);
} else if (p.val > root.val && q.val > root.val) {
return lowestCommonAncestor(root.right, p, q);
} else {
return root;
}
}
// Example usage:
// console.log(lowestCommonAncestor(root, p, q).val);if (!root) return null;Base case: empty subtreeif (p.val < root.val && q.val < root.val)Both nodes smaller, go leftreturn lowestCommonAncestor(root.left, p, q);Recurse left subtreeelse if (p.val > root.val && q.val > root.val)Both nodes greater, go rightreturn lowestCommonAncestor(root.right, p, q);Recurse right subtreeelseNodes split, current root is LCAreturn root;Return LCA nodeO(h), where h is the height of the BSTO(h) due to recursion stackEach recursive call moves down one level of the tree, so time and space depend on tree height, which is O(log n) for balanced BSTs and O(n) worst case.
This approach is optimal for BSTs and is the standard solution interviewers expect.
Intuition
Use a loop to traverse the BST from root, moving left or right based on p and q values until the split point (LCA) is found.
Algorithm
- Initialize current node as root.
- While current node is not null:
- If both p and q values are less than current node's value, move to left child.
- Else if both p and q values are greater than current node's value, move to right child.
- Else current node is LCA; break and return it.
def lowestCommonAncestor(root, p, q):
current = root
while current:
if p.val < current.val and q.val < current.val:
current = current.left
elif p.val > current.val and q.val > current.val:
current = current.right
else:
return current
# Example usage:
# print(lowestCommonAncestor(root, p, q).val)current = rootStart traversal from rootwhile current:Loop until LCA found or tree endsif p.val < current.val and q.val < current.val:Both nodes smaller, go leftcurrent = current.leftMove to left childelif p.val > current.val and q.val > current.val:Both nodes greater, go rightcurrent = current.rightMove to right childelse:Nodes split, current is LCAreturn currentReturn LCA nodepublic TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode current = root;
while (current != null) {
if (p.val < current.val && q.val < current.val) {
current = current.left;
} else if (p.val > current.val && q.val > current.val) {
current = current.right;
} else {
return current;
}
}
return null;
}
// main method and tree construction can be added for testingTreeNode current = root;Initialize traversal pointerwhile (current != null)Loop until LCA found or endif (p.val < current.val && q.val < current.val)Both nodes smaller, go leftcurrent = current.left;Move leftelse if (p.val > current.val && q.val > current.val)Both nodes greater, go rightcurrent = current.right;Move rightelseNodes split, current is LCAreturn current;Return LCA nodeTreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
TreeNode* current = root;
while (current) {
if (p->val < current->val && q->val < current->val) {
current = current->left;
} else if (p->val > current->val && q->val > current->val) {
current = current->right;
} else {
return current;
}
}
return nullptr;
}
// main function and tree construction can be added for testingTreeNode* current = root;Initialize traversal pointerwhile (current)Loop until LCA found or endif (p->val < current->val && q->val < current->val)Both nodes smaller, go leftcurrent = current->left;Move leftelse if (p->val > current->val && q->val > current->val)Both nodes greater, go rightcurrent = current->right;Move rightelseNodes split, current is LCAreturn current;Return LCA nodefunction lowestCommonAncestor(root, p, q) {
let current = root;
while (current) {
if (p.val < current.val && q.val < current.val) {
current = current.left;
} else if (p.val > current.val && q.val > current.val) {
current = current.right;
} else {
return current;
}
}
return null;
}
// Example usage:
// console.log(lowestCommonAncestor(root, p, q).val);let current = root;Initialize traversal pointerwhile (current)Loop until LCA found or endif (p.val < current.val && q.val < current.val)Both nodes smaller, go leftcurrent = current.left;Move leftelse if (p.val > current.val && q.val > current.val)Both nodes greater, go rightcurrent = current.right;Move rightelseNodes split, current is LCAreturn current;Return LCA nodeO(h), where h is the height of the BSTO(1) additional spaceIterative traversal visits nodes down the tree height only, no recursion stack used.
This is the best approach to code in interviews for this problem due to its efficiency and simplicity.
| Approach | Time | Space | Stack Risk | Reconstruct | Use In Interview |
|---|---|---|---|---|---|
| 1. Brute Force (Find Paths and Compare) | O(n) | O(n) | Yes (due to recursion in path finding) | Yes (paths explicitly stored) | Mention only - never code |
| 2. Recursive BST Property Exploitation | O(h) | O(h) | Yes (deep recursion possible) | N/A | Good to explain and code if comfortable with recursion |
| 3. Iterative BST Property Exploitation | O(h) | O(1) | No | N/A | Best approach to code in interviews |
How to Present
Step 1: Clarify the problem and confirm BST properties and input guarantees.Step 2: Describe the brute force approach of finding paths and comparing them.Step 3: Explain how BST ordering allows pruning and leads to a recursive solution.Step 4: Present the iterative solution as the optimal approach.Step 5: Code the iterative solution and test with examples.
Time Allocation
What the Interviewer Tests
Interviewer tests your understanding of BST properties, ability to optimize from brute force, and clean coding skills. They also check if you handle edge cases and explain your approach clearly.
Common Follow-ups
- What if the tree is not a BST? → Use general binary tree LCA algorithm.
- Can you find LCA without parent pointers? → Yes, using recursion or iteration as shown.
When to Use
1) Input is a BST, 2) Need to find common ancestor of two nodes, 3) Nodes guaranteed to exist, 4) Want efficient search using BST ordering
