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.

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:
-
The current
nodeisporq. If so, we've found one of the nodes. There's no need to search deeper in this branch. Thisnodeitself is the "evidence" we need to pass back up to its parent. -
pis in one subtree, andqis 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 currentnodemust be the point where the paths topandqdiverge. This makes the currentnodethe LCA. -
Both
pandqare 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 currentnodeshould 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
porqif it's found. - The LCA if it's found.
Noneif neither is found in the current subtree.
Here's the step-by-step logic:
- Base Case: If the current root is
None, or if it's equal toporq, we returnroot. This stops the recursion for this path. - 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)
- Combine Results:
- If
left_resultandright_resultare both non-None, it meanspwas found on one side andqon the other. The currentrootis the LCA. Returnroot. - If only
left_resultis non-None, it means the LCA (or one of the target nodes) is in the left subtree. Returnleft_result. - If only
right_resultis non-None, do the same for the right side. Returnright_result. - If both are
None, neither node was found in this subtree. ReturnNone.
- If
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
pandq. - 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,
pandq. 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.