TL;DR
Walk down from the root until p and q split β O(h) time, O(1) space iteratively.
Approach 1 β Brute force: record both root-to-node paths
Ignore the BST property and treat it as a generic tree: 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
class Solution:
def lowestCommonAncestor(
self, 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 but O(h) extra space for two stored paths. Nothing here
fails the constraints β the waste is conceptual: we materialize two full
paths only to compare their prefixes, when the comparison can be done on the
fly with zero storage.
Approach 2 β Recursive split search
The insight: at any node, the BST ordering answers βwhich side is p on?
which side is q?β in O(1). If both values are below the node, recurse left;
both above, recurse right; otherwise the two targets are on different sides
(or one equals the node) β and that node is, by definition, 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
class Solution:
def lowestCommonAncestor(
self, root: TreeNode, p: TreeNode, q: TreeNode
) -> TreeNode:
if p.val < root.val and q.val < root.val:
return self.lowestCommonAncestor(root.left, p, q)
if p.val > root.val and q.val > root.val:
return self.lowestCommonAncestor(root.right, p, q)
return root
Walkthrough on root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4:
at 6, both 2 < 6 and 4 < 6 β go left. At 2, the first condition fails
(2 < 2 is false), the second fails (2 > 2 is false) β return 2. Two node
visits, and note how βa node is its own ancestorβ falls out for free.
Complexity: O(h) time, O(h) recursion stack.
Approach 3 β Iterative walk (O(1) space)
The insight: 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 stack entirely.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def lowestCommonAncestor(
self, 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 in the same tree: at 6, 2 < 6 but
8 > 6 β the targets straddle the node, so neither branch is taken and 6 is
returned immediately. One iteration.
Complexity: O(h) time, O(1) space β the ideal 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 (explore both children) β
correct, but O(n) instead of O(h), and it signals you missed the BST hint.
- Assuming h = log n: a skewed BST makes the recursive version O(n) deep,
which is exactly why the iterative form is worth showing.
Pattern takeaway
In a BST, comparisons replace search. Whenever a BST problem asks about the
relationship between values (predecessor, insertion point, LCA), you can steer
a single root-to-leaf walk with O(1) comparisons per step instead of exploring
subtrees β and any single-path recursion should convert to an O(1)-space loop.