Problem Statement
You’re given the root of a binary tree and two distinct nodes p and q that are guaranteed to exist in the tree. Your task is to return the node that is their lowest common ancestor: the deepest node in the tree that has both p and q somewhere in its subtree (and a node counts as being in its own subtree).
This can be trickier than it looks because the ancestor might be one of the nodes themselves, and p and q can lie on different sides of the tree at unknown depths. A naive “walk upward” idea isn’t directly available unless you build extra parent information, and brute searching from every node repeats the same subtree work many times.
Constraints:
- The number of nodes in the tree is in the range $[2, 10^5]$.
- $-10^9 \leq$ Node.val $\leq 10^9$
- All
Node.valare unique. p $!=$ qpandqwill exist in the tree.
Examples
The Problem With Checking Every Subtree
A straightforward idea is: for every node, check whether p and q are both somewhere in its subtree, and pick the deepest such node. The issue is that “is X in this subtree?” is itself a traversal, so repeating it for many nodes causes a lot of repeated work. On a large tree (up to $10^5$ nodes), doing subtree searches repeatedly can explode to quadratic time. The repeated work comes from re-walking the same branches over and over: many candidate ancestors share big overlapping portions of the tree, yet a naive method re-computes membership for those portions each time.
Optimized Approach Using Postorder DFS
The key is to traverse bottom-up and let each subtree report what it contains. When exploring a node, you only need to know whether the left subtree contains p or q, and whether the right subtree contains p or q. If both sides report a “hit,” then the current node is the lowest place where the two targets come together, so it must be the LCA. This approach feels inevitable once you notice that the LCA is precisely the first node (from the bottom) where paths to p and q merge. A postorder traversal naturally surfaces that merge point because it processes children before parents.
Why this works even when one node is an ancestor of the other
A common mistake is to assume the LCA must be strictly above both nodes. But the definition allows a node to be a descendant of itself. In the bottom-up reporting approach, if the current node isp(orq), it reports itself upward. If the other target is somewhere below it, that information will come back through one subtree, and the algorithm correctly returns the current node as the merge point.Solution Steps
Below are the step-by-step instructions to implement the greedy solution:
- If
rootisNone, returnNone.- If
rootisporq, returnroot.- Recursively compute
left = lowestCommonAncestor(root.left, p, q).- Recursively compute
right = lowestCommonAncestor(root.right, p, q).- If both
leftandrightare notNone, returnroot(this is wherepandqsplit across subtrees).- Otherwise, return
leftif it’s notNone, else returnright.Let’s look at the following illustration to get a better understanding of the solution.
Python Implementation
Let’s look at the code for the solution we just discussed.
Time Complexity
The time complexity is $O(n)$ because each node is visited at most once, and the work done per node is constant aside from the two recursive calls.
Space Complexity
The space complexity is $O(h)$ in the worst case due to the recursion stack, where $h$ is the height of the tree. In a skewed tree, $h$ can be $n$, so worst-case stack usage is linear in the number of nodes.
Edge Cases
Below are special cases you should verify your solution handles correctly.
- One node is the ancestor of the other
Input:p$= 5$,q$= 4$ inroot$= [3,5,1,6,2,0,8,null,null,7,4]$
Why it works: when recursion reachesp, it returnsp, and that value bubbles up, makingpthe merge point when the other node is found below it. - Targets are in different subtrees of the root
Input:p$= 5$,q$= 1$ inroot$=[3,5,1,…]$
Why it works: left recursion returns $5$, right recursion returns $1$, so the root returns itself as the first node combining both. - Very small tree
Input:root$= [1,2]$,p$= 1$,q$= 2$
Why it works: hittingpreturns it immediately, and the other target found in a child makespthe correct LCA. - Highly unbalanced tree (linked-list shaped)
Small example: $1−>2−>3−>4$,p$= 3$,q$= 4$
Why it works: recursion still bubbles up the first shared node; only the recursion depth grows.
Common Pitfalls
Below are common mistakes to avoid while implementing the solution.
- Comparing by value instead of node identity: in many interview setups,
pandqare node references, not just values. Useroot is p/root is q(or pointer equality) rather thanroot.val == p.val. - Forgetting the “node can be a descendant of itself” rule: leads to returning the parent of the correct LCA when one node is an ancestor of the other.
- Trying to build full root-to-node paths unnecessarily: it can work, but is easy to implement incorrectly and can add extra memory and bookkeeping.
- Not considering recursion depth for $10^5$ nodes: in Python, a skewed tree can exceed the recursion limit. In interviews, mentioning an iterative variant is often appreciated (even if you code the recursive one).