Facebook Pixel

236. Lowest Common Ancestor of a Binary Tree

Problem Description

You are given a binary tree and two nodes p and q that exist in the tree. Your task is to find the lowest common ancestor (LCA) of these two nodes.

The lowest common ancestor is defined as the lowest (deepest) node in the tree that has both p and q as descendants. An important note: a node can be a descendant of itself.

For example, if we have a tree where node p is a parent of node q, then p itself would be the LCA of p and q since:

  • p is an ancestor of q (obviously)
  • p is an ancestor of itself (by definition)
  • There's no lower node that satisfies both conditions

The solution uses a recursive approach that works as follows:

  1. Base cases: If we reach a null node or find either p or q, we return that node immediately.

  2. Recursive search: We search both left and right subtrees for p and q.

  3. Decision logic:

    • If both left and right subtree searches return non-null values, it means p is in one subtree and q is in the other. The current node must be their LCA.
    • If only one subtree returns a non-null value, that value is either the LCA (if both nodes were found in that subtree) or one of the target nodes moving up the recursion chain.

The algorithm efficiently finds the LCA in a single traversal with O(n) time complexity where n is the number of nodes in the tree.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure.

DFS

  • We arrive at DFS: Since we're dealing with a tree structure, the flowchart leads us directly to using Depth-First Search (DFS).

Conclusion: The flowchart suggests using a DFS approach for finding the Lowest Common Ancestor in a binary tree.

This makes perfect sense for the LCA problem because:

  1. We need to traverse the entire tree potentially to find both nodes p and q
  2. DFS allows us to explore each branch fully before backtracking
  3. The recursive nature of DFS perfectly matches the recursive structure of trees
  4. We can use the return values from recursive calls to determine if we've found both nodes in different subtrees

The solution implements DFS through recursion, where we:

  • Traverse down each branch (left and right subtrees)
  • Return information about whether we found p or q in each subtree
  • Use this information to identify the LCA as the node where both subtrees contain one of our target nodes
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When searching for the lowest common ancestor of two nodes in a binary tree, we need to think about what makes a node the LCA. A node is the LCA if:

  • Both p and q are in its subtree (including itself)
  • No descendant of this node has both p and q in its subtree

The key insight is that as we traverse the tree from top to bottom, the first node we encounter that "sees" both p and q in its subtrees (or is itself one of them) must be the LCA.

Think of it like this: if we're at a node and we find p in the left subtree and q in the right subtree, then the current node must be their LCA. Why? Because any node lower than the current one can only contain either p or q, not both.

This naturally leads to a recursive DFS approach where each recursive call answers the question: "Do you contain p or q in your subtree?"

The beauty of the solution lies in how we handle the return values:

  • If we find p or q directly, we return that node immediately
  • If both left and right recursive calls return non-null values, we know we've found p and q in different subtrees, making the current node the LCA
  • If only one side returns a non-null value, we propagate that value up (it could be either one of the nodes we're looking for, or it could already be the LCA found in a deeper recursion)

The return root if left and right else (left or right) line elegantly captures this logic: if both sides found something, the current node is the answer; otherwise, pass up whatever was found (if anything).

Solution Approach

The solution implements a recursive DFS traversal as suggested by our flowchart analysis. Let's walk through the implementation step by step:

Base Cases:

if root in (None, p, q):
    return root

This handles three crucial scenarios:

  • If root is None, we've reached a leaf's child, so return None
  • If root equals p or q, we've found one of our target nodes, so return it immediately

Recursive Exploration:

left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)

We recursively search both the left and right subtrees. Each recursive call will return:

  • None if neither p nor q was found in that subtree
  • The node p or q if one of them was found
  • The LCA if both nodes were found in that subtree

Decision Logic:

return root if left and right else (left or right)

This single line handles all possible outcomes:

  • If both left and right are non-null: This means p was found in one subtree and q in the other. The current root node is their LCA.
  • If only left is non-null: Either both nodes were in the left subtree (and left contains their LCA), or only one node was found on the left side.
  • If only right is non-null: Similar to the left case but for the right subtree.
  • If both are null: Neither node was found in this subtree, return None (via left or right which evaluates to None).

Time Complexity: O(n) where n is the number of nodes, as we potentially visit every node once.

Space Complexity: O(h) where h is the height of the tree, due to the recursion stack. In the worst case (skewed tree), this could be O(n).

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through finding the LCA of nodes 5 and 1 in this binary tree:

        3
       / \
      5   1
     / \   \
    6   2   8
       / \
      7   4

Step-by-step execution:

  1. Start at root (3):

    • Check if root is null, 5, or 1? No, so continue.
    • Recursively search left subtree (node 5)
  2. At node 5 (left child of 3):

    • Check if root is null, 5, or 1? Yes! It's 5.
    • Immediately return node 5 (no need to search further down)
  3. Back at root (3), now search right:

    • Recursively search right subtree (node 1)
  4. At node 1 (right child of 3):

    • Check if root is null, 5, or 1? Yes! It's 1.
    • Immediately return node 1
  5. Back at root (3) with results:

    • left = node 5 (found in left subtree)
    • right = node 1 (found in right subtree)
    • Since both left and right are non-null, the current node (3) is the LCA
    • Return node 3

Another example: Finding LCA of nodes 7 and 4 (both in the same subtree):

        3
       / \
      5   1
     / \   \
    6   2   8
       / \
      7   4
  1. Start at root (3): Not 7 or 4, search both subtrees
  2. Left subtree (5): Not 7 or 4, search both its subtrees
    • Left of 5 (node 6): Not 7 or 4, search children → both return null
    • Right of 5 (node 2): Not 7 or 4, search children
      • Left of 2 (node 7): Found! Return 7
      • Right of 2 (node 4): Found! Return 4
    • At node 2: Both children returned non-null → node 2 is LCA, return it
  3. Back at node 5: left = null, right = node 2 → return node 2
  4. Right subtree of 3: Returns null (neither node found)
  5. At root (3): left = node 2, right = null → return node 2

The final answer is node 2, which is indeed the lowest common ancestor of nodes 7 and 4.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, x):
4#         self.val = x
5#         self.left = None
6#         self.right = None
7
8
9class Solution:
10    def lowestCommonAncestor(
11        self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode'
12    ) -> 'TreeNode':
13        """
14        Find the lowest common ancestor (LCA) of two nodes in a binary tree.
15      
16        Args:
17            root: The root node of the binary tree
18            p: First target node
19            q: Second target node
20          
21        Returns:
22            The lowest common ancestor node of p and q
23        """
24        # Base case: if root is None or root is one of the target nodes
25        # then root itself is the answer
26        if root in (None, p, q):
27            return root
28      
29        # Recursively search for p and q in the left subtree
30        left_result = self.lowestCommonAncestor(root.left, p, q)
31      
32        # Recursively search for p and q in the right subtree
33        right_result = self.lowestCommonAncestor(root.right, p, q)
34      
35        # If both left and right subtrees return non-None values,
36        # it means p and q are in different subtrees, so current root is the LCA
37        if left_result and right_result:
38            return root
39      
40        # If only one subtree contains both nodes (or one node),
41        # return the non-None result
42        return left_result or right_result
43
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode(int x) { val = x; }
8 * }
9 */
10class Solution {
11    /**
12     * Finds the lowest common ancestor (LCA) of two nodes in a binary tree.
13     * 
14     * @param root The root of the binary tree
15     * @param p The first target node
16     * @param q The second target node
17     * @return The lowest common ancestor of nodes p and q
18     */
19    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
20        // Base case: if root is null or root is one of the target nodes
21        // then root itself is the answer
22        if (root == null || root == p || root == q) {
23            return root;
24        }
25      
26        // Recursively search for LCA in the left subtree
27        TreeNode leftLCA = lowestCommonAncestor(root.left, p, q);
28      
29        // Recursively search for LCA in the right subtree
30        TreeNode rightLCA = lowestCommonAncestor(root.right, p, q);
31      
32        // If both left and right subtrees return non-null values,
33        // it means p and q are found in different subtrees,
34        // so the current root is the LCA
35        if (leftLCA != null && rightLCA != null) {
36            return root;
37        }
38      
39        // If only one subtree returns a non-null value,
40        // return that value (it could be the LCA or one of the target nodes)
41        // If both are null, return null
42        return leftLCA == null ? rightLCA : leftLCA;
43    }
44}
45
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
8 * };
9 */
10class Solution {
11public:
12    /**
13     * Finds the lowest common ancestor (LCA) of two nodes in a binary tree.
14     * 
15     * @param root The root of the binary tree
16     * @param p First target node
17     * @param q Second target node
18     * @return The lowest common ancestor of nodes p and q
19     */
20    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
21        // Base case: if root is null or root is one of the target nodes
22        // Return root as it could be the LCA or indicate finding a target
23        if (root == nullptr || root == p || root == q) {
24            return root;
25        }
26      
27        // Recursively search for p and q in the left subtree
28        TreeNode* leftResult = lowestCommonAncestor(root->left, p, q);
29      
30        // Recursively search for p and q in the right subtree
31        TreeNode* rightResult = lowestCommonAncestor(root->right, p, q);
32      
33        // If both left and right subtrees return non-null values,
34        // it means p and q are found in different subtrees,
35        // so current root is the LCA
36        if (leftResult != nullptr && rightResult != nullptr) {
37            return root;
38        }
39      
40        // If only one subtree contains both nodes (or one node),
41        // return the non-null result which represents the LCA
42        return (leftResult != nullptr) ? leftResult : rightResult;
43    }
44};
45
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Finds the lowest common ancestor (LCA) of two nodes in a binary tree.
17 * The LCA is defined as the lowest node that has both p and q as descendants
18 * (where we allow a node to be a descendant of itself).
19 * 
20 * @param root - The root node of the binary tree
21 * @param p - The first target node
22 * @param q - The second target node
23 * @returns The lowest common ancestor node, or null if not found
24 */
25function lowestCommonAncestor(
26    root: TreeNode | null,
27    p: TreeNode | null,
28    q: TreeNode | null
29): TreeNode | null {
30    // Base case: if root is null or root matches either p or q
31    // then root itself is the answer
32    if (!root || root === p || root === q) {
33        return root;
34    }
35  
36    // Recursively search for p and q in the left subtree
37    const leftResult: TreeNode | null = lowestCommonAncestor(root.left, p, q);
38  
39    // Recursively search for p and q in the right subtree
40    const rightResult: TreeNode | null = lowestCommonAncestor(root.right, p, q);
41  
42    // If both left and right subtrees return non-null values,
43    // it means p and q are in different subtrees, so current root is the LCA
44    if (leftResult && rightResult) {
45        return root;
46    }
47  
48    // Otherwise, return the non-null result (if any)
49    // This means both nodes are in the same subtree
50    return leftResult || rightResult;
51}
52

Time and Space Complexity

The time complexity is O(n), where n is the number of nodes in the binary tree. This is because in the worst case, the algorithm needs to visit every node in the tree exactly once. The recursive function traverses the entire tree by visiting each node and making recursive calls to its left and right children.

The space complexity is O(n), where n is the number of nodes in the binary tree. This space is used by the recursion call stack. In the worst case, when the tree is completely unbalanced (essentially a linked list), the recursion depth can reach n levels deep, requiring O(n) space on the call stack. In the best case of a balanced tree, the space complexity would be O(log n) due to the height of the tree, but we consider the worst-case scenario for complexity analysis.

Common Pitfalls

1. Assuming Nodes Always Exist in the Tree

One of the most common pitfalls is not considering the case where p or q might not actually exist in the tree. The given solution assumes both nodes are present, which could lead to incorrect results if this assumption is violated.

Problem Example:

# Tree: [3, 5, 1, 6, 2, 0, 8]
# If we search for LCA of node 5 and node 10 (which doesn't exist)
# The algorithm would incorrectly return node 5

Solution: Add a verification step to ensure both nodes exist before finding LCA:

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        # First verify both nodes exist
        def findNode(root, target):
            if not root:
                return False
            if root == target:
                return True
            return findNode(root.left, target) or findNode(root.right, target)
      
        # Only proceed if both nodes exist
        if not findNode(root, p) or not findNode(root, q):
            return None
          
        # Original LCA logic
        return self.findLCA(root, p, q)
  
    def findLCA(self, root, p, q):
        if root in (None, p, q):
            return root
        left = self.findLCA(root.left, p, q)
        right = self.findLCA(root.right, p, q)
        return root if left and right else (left or right)

2. Misunderstanding the "Descendant of Itself" Rule

Developers often miss that a node can be its own ancestor/descendant, leading to incorrect implementations that skip the current node when it matches p or q.

Incorrect Approach:

# WRONG: Continuing to search even after finding p or q
if root == p or root == q:
    # Don't return immediately, keep searching children
    left = self.lowestCommonAncestor(root.left, p, q)
    right = self.lowestCommonAncestor(root.right, p, q)
    # This is incorrect!

Correct Understanding: When we find p or q, we should return immediately because:

  • If the other node is in this node's subtree, this node is the LCA
  • If the other node is elsewhere, a higher ancestor will be the LCA

3. Confusing Return Values in Recursive Calls

The recursive function returns different types of information depending on the context, which can be confusing:

  • Sometimes it returns one of the target nodes (p or q)
  • Sometimes it returns the actual LCA
  • Sometimes it returns None

Mental Model to Avoid Confusion: Think of the return value as "evidence of finding nodes":

  • None = "I found nothing"
  • A node = "I found something important (either a target node or the LCA)"
  • The parent logic determines whether that "something" is the final answer or just partial evidence
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More