~25 minutes · Day 38 · tree recursion · Python
Tonight's problem is Lowest Common Ancestor of a Binary Tree. Interviewers like it because the code is short, but the thinking is not: you must trust what each subtree reports back. The win is learning to stop searching the moment the two targets split across the current node.
If the two targets are found in different subtrees of a node, you are standing on the answer.
A lowest common ancestor is the deepest node that has both targets below it, counting a node as its own descendant. In this LeetCode problem, both target nodes are present in the tree; your job is to return the node where their paths first meet.
The LCA is the lowest node where the two target paths meet.
The brute force idea is: at every node, ask “does this subtree contain p?” and “does this subtree contain q?”, then keep trying to go lower. That can become O(n^2) in the worst case, because the same subtrees get searched again and again.
def contains(node, target):
if node is None:
return False
return (node == target or
contains(node.left, target) or
contains(node.right, target))
It works, but it wastes the most important thing recursion gives you: a return value. Each child can report whether it found nothing, one target, or the final answer.
Run one DFS. If a subtree finds p or q, it returns that node upward. If a node receives one result from the left child and one result from the right child, the split happened here, so this node is the LCA.
The recursive call does not need parent pointers; it bubbles findings upward until both sides meet.
def lowestCommonAncestor(self, root, p, q):
if root is None:
return None # this subtree found nothing
if root == p or root == q:
return root # this subtree found a target
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right:
return root # split: current node is LCA
return left or right # bubble up what was found
The line if left and right is the invariant becoming code. One target came from each side, so no lower node can contain both.
Variation: suppose one target might be an ancestor of the other. In the tree below, ask for p = 5 and q = 2. What should the call at node 5 return?
3
/ \
5 1
/ \
6 2
Before opening the reveal, predict it out loud: does node 5 wait for both children, or does it return itself immediately?
It returns itself immediately. If the current node is one target, and the other target is somewhere below it, then the current node is the lowest shared ancestor. That is why the base case checks root == p or root == q before asking the children.
| Approach | Time | Space |
|---|---|---|
| Brute force (re-check subtrees) | O(n^2) worst case | O(h) |
| Tree recursion (return findings upward) | O(n) | O(h) |
Here h is the tree height. The important upgrade is not clever syntax; it is asking every node for its answer once.
1. What makes the current node the LCA?
2. Spot the bug in this DFS order:
left = dfs(root.left)
right = dfs(root.right)
if left:
return left
if right:
return right
if root == p or root == q:
return root
if left and right: return root.3. Which problem is this pattern?
4. What is the one-pass DFS complexity?
Solve Lowest Common Ancestor of a Binary Tree, logging it in your Notion tracker with the cycle — predict → attempt → compare → encode.
Watch NeetCode — Lowest Common Ancestor of a Binary Tree after your attempt, as the compare step.