Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 49

Lowest Common Ancestor in a Binary Tree

Recommended resources

Hello! Welcome to the final lesson in our "Binary Trees and Traversals" module.

In our previous lesson, we saw how to use recursive DFS traversals to compute tree properties like height and diameter. You learned a powerful pattern where a recursive function returns information (like height) from subtrees, allowing the parent node to calculate a result.

Today, we'll apply a very similar recursive strategy to solve another classic tree problem. This lesson focuses on the learning outcome: Find the lowest common ancestor of two nodes in a binary tree. Mastering this problem is a great way to solidify your understanding of recursion on trees.

1. What is the Lowest Common Ancestor (LCA)?

First, let's define what we're looking for.

Definition: The Lowest Common Ancestor (LCA) between two nodes p and q in a binary tree is the lowest (deepest) node that has both p and q as descendants. A crucial part of this definition is that a node is considered a descendant of itself.

Let's look at a few cases to make this clear.

Lowest Common Ancestor (LCA) Example in a Binary Tree
This image shows a binary tree. If we want to find the LCA of nodes 5 and 1, we trace their parents upwards. Their first common parent is node 3. Therefore, 3 is the LCA.

Now, consider the case where one node is an ancestor of the other. For example, in the tree above, what is the LCA of nodes 5 and 4?

  • Node 5 is an ancestor of node 4.
  • Node 5 is an ancestor of itself (by our definition).
  • Therefore, the lowest node that is an ancestor to both is node 5 itself.

To see these cases explained visually, the first part of the following video is very helpful.

LOWEST COMMON ANCESTOR OF A BINARY TREE I | PYTHON | LEETCODE 236

This video, 'LOWEST COMMON ANCESTOR OF A BINARY TREE' from Cracking FAANG, begins by clearly defining the LCA and walking through the three possible scenarios you'll encounter.

Watch from the beginning to 01:46. Pay attention to the three examples given: nodes in different subtrees, and the two cases where one node is an ancestor of the other.


2. The Recursive Intuition

Just like with height and diameter, a recursive DFS approach is the most elegant way to solve the LCA problem. The core idea is to traverse the tree and have our recursive function report back what it finds in its subtrees.

For any given node, our recursive function will answer the question: "Have you found p or q in the subtree rooted at this node?"

There are three key possibilities for any node we are visiting:

  1. The current node is p or q. If so, we've found one of the nodes. There's no need to search deeper in this branch. This node itself is the "evidence" we need to pass back up to its parent.

  2. p is in one subtree, and q is in the other. If our recursive call on the left subtree finds one of the nodes, and the recursive call on the right subtree finds the other, then the current node must be the point where the paths to p and q diverge. This makes the current node the LCA.

  3. Both p and q are in the same subtree (either left or right). In this case, the recursive call on that subtree will eventually find the LCA at a deeper level and return it. Our current node should simply pass that result up to its parent.

This logic of defining a "policy" for a single node and trusting recursion to handle the rest is a fundamental skill for tree problems. The following video provides an excellent mental framework for this.

Lowest Common Ancestor Between 2 Binary Tree Nodes (A Recursive Approach)

The video 'Lowest Common Ancestor Between 2 Binary Tree Nodes' from Back To Back SWE explains the powerful idea of focusing only on the logic for a single node when designing a recursive solution.

Watch from 06:33 to 15:54. The key insight here is to define a 'policy' for a single node based on what its children (the recursive calls) return. This will help you understand the 'why' behind the code.


3. The Algorithm and Implementation

Now, let's translate this intuition into a concrete algorithm and Python code. Our function, lowestCommonAncestor(root, p, q), will perform a post-order traversal and return the following:

  • The node p or q if it's found.
  • The LCA if it's found.
  • None if neither is found in the current subtree.

Here's the step-by-step logic:

  1. Base Case: If the current root is None, or if it's equal to p or q, we return root. This stops the recursion for this path.
  2. Recursive Step: Recursively call the function on the left and right subtrees.
    • left_result = lowestCommonAncestor(root.left, p, q)
    • right_result = lowestCommonAncestor(root.right, p, q)
  3. Combine Results:
    • If left_result and right_result are both non-None, it means p was found on one side and q on the other. The current root is the LCA. Return root.
    • If only left_result is non-None, it means the LCA (or one of the target nodes) is in the left subtree. Return left_result.
    • If only right_result is non-None, do the same for the right side. Return right_result.
    • If both are None, neither node was found in this subtree. Return None.

The following article provides a clean implementation of this logic.

236. Lowest Common Ancestor of a Binary Tree

This article from Algo Monster, '236. Lowest Common Ancestor of a Binary Tree', provides the code and a detailed textual walkthrough of the algorithm.

First, review the Python code in the 'Solution Implementation' section. Then, read the 'Solution Approach' and the two excellent 'Example Walkthrough' sections. This will help you trace the recursive calls for both main scenarios.

Here is the Python implementation for your reference:

# 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':
        # Base case: If root is None, or we found p or q, return root.
        if root is None or root == p or root == q:
            return root

        # Recursively search in left and right subtrees.
        left_result = self.lowestCommonAncestor(root.left, p, q)
        right_result = self.lowestCommonAncestor(root.right, p, q)

        # Decision logic:
        # If both subtrees returned a non-None value, then the current root is the LCA.
        if left_result and right_result:
            return root
        
        # Otherwise, the result is in whichever subtree returned a non-None value.
        # If both returned None, this expression correctly evaluates to None.
        return left_result or right_result

Test your understanding!

You are looking for the LCA of nodes p and q. In the initial call to lowestCommonAncestor(root, p, q), the function returns node q. What does this imply about the relationship between p and q?

Show answer

This implies that node p is a descendant of node q. The recursive search found q first. As the recursion continued down from q, it eventually found p in one of q's subtrees. The call that found p returned p, but the call at node q saw that p was in one of its subtrees and simply returned itself (q) as the LCA.


4. Complexity Analysis

Understanding the performance of this algorithm is crucial for competitive programming.

  • Time Complexity: , where is the number of nodes in the tree. In the worst case, we have to visit every node once to find p and q.
  • Space Complexity: , where is the height of the tree. This space is used by the recursion call stack. For a balanced tree, this is . For a completely skewed tree (like a linked list), the height is , making the worst-case space complexity .

The final section of the "Cracking FAANG" video provides a concise summary of this analysis.

LOWEST COMMON ANCESTOR OF A BINARY TREE I | PYTHON | LEETCODE 236

Let's return to the 'Cracking FAANG' video to review the complexity analysis.

Watch from 11:06 to 12:23. The video explains the O(N) time complexity and discusses the space complexity related to the recursion stack.


Conclusion

Congratulations on completing the "Binary Trees and Traversals" module! You've built a solid foundation, progressing from basic traversals to solving complex problems like finding diameter and the lowest common ancestor.

Key Takeaways:

  • The LCA is the deepest node that is an ancestor to two given nodes, p and q. A node can be an ancestor of itself.
  • A recursive, post-order traversal provides an elegant solution.
  • The recursive function's logic is key: it returns the relevant node (p, q, or the LCA) from its subtree.
  • The LCA is identified at the node where the search results from the left and right subtrees first diverge.

The recursive patterns you've practiced in this module are fundamental and will reappear in many other data structures and algorithms.

Up Next:
We will now move on to the next module, "Binary Search Trees (BSTs)". BSTs are a special, ordered type of binary tree. This ordering property allows for highly efficient searching, insertion, and deletion operations, making them one of the most important data structures in computer science. Your understanding of tree traversal and recursion will be invaluable as we explore them.