Topics › Binary Search Trees › Practice Problems
LCA in BST
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given the root of a binary search tree and the keys p and q of two nodes that are both present in it. Return their lowest common ancestor (LCA): the deepest node that has both of them in its subtree. A node counts as an ancestor of itself, so if p lies inside the subtree of q, the answer is q.
Keys in a BST are distinct, so a key identifies a node uniquely. LeetCode passes node references instead of keys; the code is the same with p.val and q.val. Constraints: up to 10^5 nodes, keys fit in a 32-bit signed integer.
All examples use this tree:
6
/ \
2 8
/ \ / \
0 4 7 9
/ \
3 5
Example 1
p = 2, q = 8
answer = 6 // 2 is on the left of 6, 8 on the right
Example 2
p = 2, q = 4
answer = 2 // 4 is inside the subtree of 2, and 2 is its own ancestor
Approach 1: Brute force, the general binary-tree LCA
Intuition. Forget the ordering for a moment; the LCA of a plain binary tree can be found with one recursion. Ask each subtree “do you contain p or q?”. If the current node is p or q, report it. If one key turns up on the left and the other on the right, the current node is where their paths meet, so it is the LCA. If only one side reports something, pass that report upward. Returning a node as soon as it equals p (without looking below it) is safe because both keys are guaranteed to exist: if the other key is not found elsewhere, it must be underneath, and then this node is the answer anyway.
Algorithm.
- If
rootisnullor holdsporq, returnroot. - Recurse into both children.
- If both calls return a node, return
root. Otherwise return whichever call returned a node (ornull).
static TreeNode bruteForce(TreeNode root, int p, int q) {
if (root == null || root.val == p || root.val == q) return root;
TreeNode left = bruteForce(root.left, p, q);
TreeNode right = bruteForce(root.right, p, q);
if (left != null && right != null) return root; // p and q were found on different sides
return left != null ? left : right;
}
Complexity. Time O(n): in the worst case it visits every node, because it never uses the ordering to skip a subtree. Space O(h) for the recursion.
Approach 2: Better, compare the two search paths
Intuition. In a BST the route from the root to a key is forced: compare with the current node and go left or right. The ancestors of p are exactly the nodes on that search path, and the same goes for q. Common ancestors are therefore the common prefix of the two paths, and the lowest common ancestor is the last node of that prefix. The search only touches one root-to-node path per key, so we stop wasting time on irrelevant subtrees.
Algorithm.
- Record the search path from the root to
pand from the root toq. - Walk both lists from the start while they hold the same node.
- The last shared node is the LCA.
static TreeNode better(TreeNode root, int p, int q) {
List<TreeNode> pathP = searchPath(root, p);
List<TreeNode> pathQ = searchPath(root, q);
TreeNode lca = null;
for (int i = 0; i < pathP.size() && i < pathQ.size(); i++) {
if (pathP.get(i) != pathQ.get(i)) break; // same node object, not just same key
lca = pathP.get(i);
}
return lca;
}
static List<TreeNode> searchPath(TreeNode root, int key) {
List<TreeNode> path = new ArrayList<>();
TreeNode cur = root;
while (cur != null) {
path.add(cur);
if (key == cur.val) break;
cur = key < cur.val ? cur.left : cur.right;
}
return path;
}
Comparing with != here is deliberate: we want the same node object, not merely the same key.
Complexity. Time O(h) for two searches and one prefix scan. Space O(h) for the two lists. On a balanced tree h ≈ log n; on a chain h = n.
Approach 3: Optimal, find the split point
Intuition. We do not need to store the paths. Walk both searches at the same time from the root. While p and q both want to go left (both smaller than the current key) or both want to go right (both larger), the two paths are still identical, so we move along. The first node where they disagree, meaning one key is on the left and the other on the right, or one of them is the current node, is where the common prefix ends. That node is the LCA.
Why no deeper node can work: below the split node, every node lies entirely in its left subtree or entirely in its right subtree, so it cannot have both p and q under it. And when the current node equals p, any deeper node is below p and cannot contain p itself.
Algorithm.
- Start with
cur = root. - If both keys are smaller than
cur.val, go left. If both are larger, go right. - Otherwise return
cur.
static TreeNode optimal(TreeNode root, int p, int q) {
TreeNode cur = root;
while (cur != null) {
if (p < cur.val && q < cur.val) cur = cur.left; // both keys live in the left subtree
else if (p > cur.val && q > cur.val) cur = cur.right; // both keys live in the right subtree
else return cur; // they split here, or one of them is cur
}
return null;
}
Dry run with p = 3, q = 5:
| cur | p vs cur | q vs cur | action |
|---|---|---|---|
| 6 | 3 < 6 | 5 < 6 | both left, go to 2 |
| 2 | 3 > 2 | 5 > 2 | both right, go to 4 |
| 4 | 3 < 4 | 5 > 4 | they split, return 4 |
Complexity. Time O(h): one step down per iteration. Space O(1): a single pointer, no recursion and no lists.
A recursive version of the same logic is just as short, but it costs O(h) stack space for no benefit, since each call makes at most one recursive call and nothing happens after it returns.
Edge cases and pitfalls
- One key is an ancestor of the other.
p = 2, q = 4returns2. Theelse return curbranch handles it becausep == cur.valfails both “strictly smaller” and “strictly larger” tests. p == q. The LCA of a node with itself is that node. All three methods return it.- Order of
pandq. None of the methods assumep < q, and the tests call(5, 0)as well as(0, 5). - Skewed trees. On a chain
1 → 2 → 3 → 4,h = nand the BST methods are linear. The tests include a right-skewed chain. - Overflow trick. Some solutions test
(cur.val - p) * (cur.val - q) > 0to see if both keys are on the same side. Both the subtraction and the product can overflowint. Two plain comparisons are clearer and always correct. - Keys that might be missing. The problem guarantees both exist. If that is not guaranteed, the split-point method can return a node even when one key is absent, so search for both keys first.
Related problems
- LCA in BT is the general version that Approach 1 solves; without ordering there is no split-point shortcut.
- Search in a Binary Search Tree is the single-key walk that Approaches 2 and 3 are built from.
- Inorder Successor/Predecessor in BST uses the same “walk down, keep a candidate” pattern.
- Floor and Ceil in a BST is another one-path walk that relies on the ordering.
- Check if a tree is a BST or not covers the property this solution depends on.