Skip to main content

Welcome to a Grasp lesson

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

Lesson 46

Finding the Lowest Common Ancestor (LCA)

Recommended resources

Hello! In our last lesson, we learned to see a binary tree from above and below by finding its top and bottom views. This involved a clever use of level-order traversal and the new concept of "horizontal distance."

Today, we shift from problems about a tree's "view" to problems about the relationships between its nodes. We will tackle one of the most fundamental tree problems asked in interviews: finding the Lowest Common Ancestor (LCA) of two nodes in a binary tree. This is a crucial concept that builds on your understanding of tree traversals and recursion.

By the end of this lesson, you will be able to define what an LCA is and implement an efficient, single-pass recursive algorithm to find it.

1. What is a Lowest Common Ancestor?

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 key part of this definition is that a node can be a descendant of itself.

Let's look at a simple example.

Lowest Common Ancestor in a Binary Tree
In this tree, the LCA of nodes 40 and 50 is 20. For nodes 30 and 40, the LCA is 10. Crucially, for nodes 40 and 20, the LCA is 20, because 20 is an ancestor of 40 (and a node is a descendant of itself).

This problem appears in various contexts, from calculating distances between nodes in a network to version control systems like Git finding a common base for merging branches.


2. Approach 1: Storing Root-to-Node Paths

An intuitive way to solve this is to think about the paths from the root to each of our target nodes.

  1. Find the path from the root to node p and store it in a list.
  2. Find the path from the root to node q and store it in another list.
  3. Compare these two paths. The last node they have in common is the LCA.

For a detailed explanation of this method, including how to implement the path-finding logic, please read the following resource.

LCA of a Binary Tree: Approach 1

This article from Scaler provides a clear walkthrough of the path-storing approach. It will show you how to implement a recursive function to find the paths and then compare them.

Read the section titled 'Approach 1: By Storing Root To n1 And Root To n2 Paths'. Pay attention to the algorithm for finding a path and the dry run, which illustrates how the paths are compared.

This method works correctly, but it has some drawbacks:

  • Time Complexity: You need to traverse the tree to find the first path and then again for the second path. This leads to a time complexity of , as in the worst case you might visit all nodes.
  • Space Complexity: You need to store the paths. In the worst case of a skewed tree, a path could be as long as the number of nodes, leading to a space complexity of (or where H is the height of the tree).

Given your goal of preparing for competitive programming and technical interviews, it's essential to know a more optimized solution that uses less space and only requires a single traversal of the tree.


3. Approach 2: The Efficient Single-Pass Recursion

The most elegant and efficient solution uses recursion to find the LCA in a single traversal. The core idea is to define a recursive function that searches for p and q in the subtrees and uses the return values to pinpoint the LCA.

To understand the logic, let's watch a short video that introduces the concept and the algorithm.

Lowest Common Ancestor Binary Tree

This video by Tushar Roy clearly explains the intuition behind the single-pass recursive solution. Focus on how a node determines if it's the LCA based on what its children find.

Watch from the beginning to 02:37. This segment defines the problem with great examples and then introduces the core logic of the recursive algorithm.

The Recursive Logic

The recursive function lowestCommonAncestor(root, p, q) has a clear "contract". It returns:

  • The node itself (root), if root is null or if root is one of the nodes we're looking for (p or q).
  • The actual LCA, if p and q are found in different subtrees of the current root.
  • A non-null node, if both p and q are found in the same subtree of the root.
  • null, if neither p nor q can be found in the tree rooted at root.

This translates into a simple, powerful algorithm:

  1. Base Case: If the current node (root) is null, or if it matches p or q, return root. Finding p or q is a success condition, so we propagate that node up the call stack.
  2. Recursive Step: Recursively call the function for the left and right children:
    • TreeNode leftLCA = lowestCommonAncestor(root.left, p, q);
    • TreeNode rightLCA = lowestCommonAncestor(root.right, p, q);
  3. Analyze Results: After the recursive calls return, analyze leftLCA and rightLCA:
    • If leftLCA and rightLCA are both not null, it means p was found in the left subtree and q was found in the right subtree (or vice-versa). Therefore, the current root is the split point and the LCA. We return root.
    • If only leftLCA is not null, it means both p and q are in the left subtree. The leftLCA that was returned is the actual LCA. So, we return leftLCA.
    • Similarly, if only rightLCA is not null, we return rightLCA.
    • If both are null, neither node was found in this subtree, so we return null.

To see this logic in action, let's watch a detailed walkthrough.

Lowest Common Ancestor Binary Tree

This segment animates the recursive calls and return values. Visualizing the call stack is key to understanding how this algorithm works, especially for the case where one node is an ancestor of the other.

Watch from 02:37 to 09:14. The video covers two crucial examples: one where the nodes are in different subtrees, and one where one node is an ancestor of the other. Observe what each recursive call returns (a node or null).

Java Implementation

Here is the Java implementation, which beautifully reflects the concise logic we just discussed.

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        // Base case: if root is null, or we've found p or q, return the root.
        if (root == null || root == p || root == q) {
            return root;
        }

        // Recur for left and right subtrees
        TreeNode leftLCA = lowestCommonAncestor(root.left, p, q);
        TreeNode rightLCA = lowestCommonAncestor(root.right, p, q);

        // If both subtrees returned a non-null node, then the current root is the LCA.
        if (leftLCA != null && rightLCA != null) {
            return root;
        }

        // Otherwise, the LCA must be in one of the subtrees.
        // We bubble up the non-null value. If both are null, this returns null.
        return (leftLCA != null) ? leftLCA : rightLCA;
    }
}

This code is taken from the takeuforward.org article "Lowest Common Ancestor for two given Nodes" (LINK), which is another great resource for this topic.

  • Time Complexity: , as we visit each node in the tree exactly once.
  • Space Complexity: , where H is the height of the tree. This space is used by the recursion call stack. In the worst case (a skewed tree), this can be . For a balanced tree, it's . This is a significant improvement over the path-storing approach.
Test your understanding!

Consider this tree from our previous lesson:

      20
     /  \
    8    22
   / \     \
  5   3     25
     / \
    10  14

What will lowestCommonAncestor(root, node(10), node(14)) return at each step? Trace the final return value.

Show answer
  1. lca(20, 10, 14):

    • Calls lca(8, 10, 14) on its left.
    • Calls lca(22, 10, 14) on its right.
  2. lca(8, 10, 14):

    • Calls lca(5, 10, 14) -> returns null.
    • Calls lca(3, 10, 14) on its right.
  3. lca(3, 10, 14):

    • Calls lca(10, 10, 14) -> root == 10, so it returns node(10). Let's call this leftResult.
    • Calls lca(14, 10, 14) -> root == 14, so it returns node(14). Let's call this rightResult.
    • Now, lca(3, ...) has received non-null leftResult and rightResult. It concludes it is the LCA and returns node(3) up to its caller (lca(8, ...)).
  4. Back at lca(8, ...):

    • Its left call returned null.
    • Its right call returned node(3).
    • It returns the non-null result, node(3), up to its caller (lca(20, ...)).
  5. Back at lca(20, ...):

    • Its left call has now returned node(3).
    • It now executes its right call, lca(22, 10, 14). This will eventually return null since neither 10 nor 14 are in the right subtree.
  6. Finally, at lca(20, ...):

    • The left call returned node(3).
    • The right call returned null.
    • It returns the non-null value, node(3).

The final answer is node(3).


Conclusion

Today we explored the important problem of finding the Lowest Common Ancestor. While the intuitive path-storing method is a good starting point, the single-pass recursive solution is far more efficient and demonstrates a powerful way to use recursion to solve complex tree problems.

Key Takeaways:

  • The LCA is the deepest node that is an ancestor to both target nodes. A node is considered a descendant of itself.
  • The path-storing method is easy to understand but uses extra space and multiple traversals.
  • The single-pass recursive algorithm is highly efficient, with time and space complexity. Its core logic relies on checking the results from left and right recursive calls to determine if the current node is the "split point."

Preview of the Next Lesson:
So far, we have been analyzing and traversing trees that are already given to us. In the next lesson, we will switch gears and learn how to construct a binary tree from its preorder and inorder traversals. This will give you deeper insight into the fundamental properties of tree traversals.