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.

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.
- Find the path from the
rootto nodepand store it in a list. - Find the path from the
rootto nodeqand store it in another list. - 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), ifrootisnullor ifrootis one of the nodes we're looking for (porq). - The actual LCA, if
pandqare found in different subtrees of the currentroot. - A non-null node, if both
pandqare found in the same subtree of theroot. null, if neitherpnorqcan be found in the tree rooted atroot.
This translates into a simple, powerful algorithm:
- Base Case: If the current node (
root) isnull, or if it matchesporq, returnroot. Findingporqis a success condition, so we propagate that node up the call stack. - 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);
- Analyze Results: After the recursive calls return, analyze
leftLCAandrightLCA:- If
leftLCAandrightLCAare both not null, it meanspwas found in the left subtree andqwas found in the right subtree (or vice-versa). Therefore, the currentrootis the split point and the LCA. We returnroot. - If only
leftLCAis not null, it means bothpandqare in the left subtree. TheleftLCAthat was returned is the actual LCA. So, we returnleftLCA. - Similarly, if only
rightLCAis not null, we returnrightLCA. - If both are
null, neither node was found in this subtree, so we returnnull.
- If
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
-
lca(20, 10, 14):- Calls
lca(8, 10, 14)on its left. - Calls
lca(22, 10, 14)on its right.
- Calls
-
lca(8, 10, 14):- Calls
lca(5, 10, 14)-> returnsnull. - Calls
lca(3, 10, 14)on its right.
- Calls
-
lca(3, 10, 14):- Calls
lca(10, 10, 14)->root == 10, so it returnsnode(10). Let's call thisleftResult. - Calls
lca(14, 10, 14)->root == 14, so it returnsnode(14). Let's call thisrightResult. - Now,
lca(3, ...)has received non-nullleftResultandrightResult. It concludes it is the LCA and returnsnode(3)up to its caller (lca(8, ...)).
- Calls
-
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, ...)).
- Its left call returned
-
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 returnnullsince neither 10 nor 14 are in the right subtree.
- Its left call has now returned
-
Finally, at
lca(20, ...):- The left call returned
node(3). - The right call returned
null. - It returns the non-null value,
node(3).
- The left call returned
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.