TL;DR
Walk down from the root until p and q split. O(h) time, O(1) space iteratively.
The examples below use this tree, root = [6,2,8,0,4,7,9,null,null,3,5]:
graph TD
6 --> 2
6 --> 8
2 --> 0
2 --> 4
4 --> 3
4 --> 5
8 --> 7
8 --> 9
Approach 1 — Brute force: record both root-to-node paths
Ignore the BST property and treat the tree as generic: find the path from the
root to p, the path to q, then walk the two paths in lockstep. The last
node they share is the LCA.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
def lowestCommonAncestor(
root: TreeNode, p: TreeNode, q: TreeNode
) -> TreeNode:
def path_to(target: TreeNode) -> List[TreeNode]:
path: List[TreeNode] = []
node = root
while node:
path.append(node)
if node is target:
return path
# still generic-looking, but we must pick a side somehow;
# a true generic-tree version would DFS both sides
node = node.left if target.val < node.val else node.right
return path
pa, pb = path_to(p), path_to(q)
lca = root
for x, y in zip(pa, pb):
if x is y:
lca = x
else:
break
return lca
Complexity: O(h) time and O(h) extra space for the two stored paths. This
passes the constraints, but it is wasteful: it materializes two full paths
only to compare their prefixes, when the comparison can be done on the fly
with no storage.
Approach 2 — Recursive split search
At any node, the BST ordering tells you in O(1) which side p and q each
fall on. If both values are below the node, recurse left; if both are above,
recurse right; otherwise the two targets are on different sides (or one equals
the node), and that node is the deepest node containing both. The recursion
follows exactly one root-to-LCA path.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
def lowestCommonAncestor(
root: TreeNode, p: TreeNode, q: TreeNode
) -> TreeNode:
if p.val < root.val and q.val < root.val:
return lowestCommonAncestor(root.left, p, q)
if p.val > root.val and q.val > root.val:
return lowestCommonAncestor(root.right, p, q)
return root
Walkthrough on p = 2, q = 4: at 6, both 2 < 6 and 4 < 6, so go
left. At 2, the first condition fails (2 < 2 is false) and the second fails
(2 > 2 is false), so return 2. Two node visits, and the “a node is its own
ancestor” case is handled without a special check.
Complexity: O(h) time, O(h) recursion stack.
Approach 3 — Iterative walk (O(1) space)
The recursion above is tail recursion: each call makes at most one recursive
call and returns its result unchanged. Any tail recursion converts to a loop,
dropping the call stack entirely.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
def lowestCommonAncestor(
root: TreeNode, p: TreeNode, q: TreeNode
) -> TreeNode:
node = root
while node:
if p.val < node.val and q.val < node.val:
node = node.left
elif p.val > node.val and q.val > node.val:
node = node.right
else:
return node
return None
Walkthrough on p = 2, q = 8: at 6, 2 < 6 but 8 > 6, so the
targets straddle the node. Neither branch is taken and 6 is returned
immediately. One iteration.
Complexity: O(h) time, O(1) space, which is optimal for this problem.
Common pitfalls
- Testing equality with
== on values when the problem hands you node
references — fine here since values are distinct, but compare node is p
when in doubt.
- Forgetting that the “split” case includes landing exactly on
p or q;
writing a separate equality check is redundant but writing the split check
as strict inequalities in both directions and looping forever is a bug.
- Reaching for the generic binary-tree LCA recursion that explores both
children: it is correct, but O(n) instead of O(h), and it ignores the BST
property.
- Assuming h = log n. A skewed BST makes the recursive version O(n) deep, which
is why the iterative form matters.
Pattern takeaway
In a BST, comparisons replace search. When a BST problem asks about the
relationship between values (predecessor, insertion point, LCA), you can drive
a single root-to-leaf walk with O(1) comparisons per step instead of exploring
subtrees, and any single-path recursion converts to an O(1)-space loop.