Level Up Your Coding Skills & Crack Interviews — Save up to 50% or more on Educative.io Today! Claim Discount

Arrow
Table of contents

236. Lowest Common Ancestor of a Binary Tree

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.val are unique.
  • p $!=$ q
  • p and q will exist in the tree.

Examples

1 / 4

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 is p (or q), 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:

  1. If root is None, return None.
  2. If root is p or q, return root.
  3. Recursively compute left = lowestCommonAncestor(root.left, p, q).
  4. Recursively compute right = lowestCommonAncestor(root.right, p, q).
  5. If both left and right are not None, return root (this is where p and q split across subtrees).
  6. Otherwise, return left if it’s not None, else return right.

Let’s look at the following illustration to get a better understanding of the solution.

1 / 11

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$ in root $= [3,5,1,6,2,0,8,null,null,7,4]$
    Why it works: when recursion reaches p, it returns p, and that value bubbles up, making p the merge point when the other node is found below it.
  • Targets are in different subtrees of the root
    Input: p $= 5$, q $= 1$ in root $=[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: hitting p returns it immediately, and the other target found in a child makes p the 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, p and q are node references, not just values. Use root is p / root is q (or pointer equality) rather than root.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).

Frequently Asked Questions

Why not just find the depth of both nodes and move the deeper one up?

That only works if you have parent pointers (or can move upward efficiently). In a typical binary tree node definition, you only have children, so “moving up” requires extra preprocessing anyway.

How do I recognize the Postorder DFS pattern?

When the question asks for a “lowest” meeting point of two targets in a tree and you can determine it by combining information from left and right subtrees, a bottom-up traversal that returns “what I found” is a strong fit.

 

Does this approach rely on node values being unique?

The uniqueness helps avoid ambiguity, but this solution primarily relies on node identity (p and q as references). It works regardless of values as long as p and q are specific nodes in the tree.

Is there a faster-than-O(n) solution?

For a single query on an arbitrary binary tree, you generally must inspect enough of the tree to locate both nodes, which makes $ the standard optimal time. Faster queries are possible only with preprocessing for multiple LCA queries.

What changes if I need to answer many LCA queries on the same tree?

You’d typically preprocess the tree (e.g., store parent pointers and depths, or use more advanced preprocessing) so each query is faster, trading extra memory and preprocessing time for quicker lookups.

Share with others:

Leave a Reply

Your email address will not be published. Required fields are marked *