Recommended resources
The freely available resources this lesson will direct you to.
Welcome back! In our previous lessons, we've explored two fundamental recursive patterns for binary trees: bubbling results up the tree (post-order, like calculating tree height) and passing constraints down the tree (pre-order, like validating a BST).
Today, we'll strengthen our understanding of the "bubble-up" pattern by applying it to another classic interview problem: finding the Lowest Common Ancestor (LCA) of two nodes. This problem requires a clever use of recursive return values to signal where the target nodes are found. Mastering this will not only add another essential algorithm to your toolkit but also deepen your intuition for how recursive functions can communicate information up the call stack.
What is the Lowest Common Ancestor?
The Lowest Common Ancestor (LCA) of two nodes, p and q, in a binary tree is the lowest (i.e., deepest) node that has both p and q as descendants. A key rule here, which often trips people up, is that a node is considered a descendant of itself.
This rule gives rise to two main scenarios, which are visualized perfectly in the image below.

- Standard Case:
pandqare in different subtrees of their LCA. The LCA is the "junction point" where the paths from the root topandqdiverge. - Edge Case: One node is an ancestor of the other. For instance, if
pis an ancestor ofq, thenpis their LCA.
With this definition in mind, our task is to design an algorithm that can efficiently find this node.
A Recursive Approach: Thinking from a Single Node's Perspective
A brute-force method might involve finding the path from the root to p, finding the path from the root to q, and then comparing these paths to find the last common node. This works, but it requires extra space to store the paths. As you've seen, recursion often provides a more elegant solution for tree problems by avoiding extra storage.
The key to the recursive solution is to define what our function should "report back" from any given subtree. Let's define a recursive function find(node, p, q) that traverses the tree. For any given node, what information can it provide to its parent? It can tell its parent one of three things about the subtree it's rooting:
- "I found
pin my subtree." - "I found
qin my subtree." - "I found neither
pnorqin my subtree."
How does this help? Imagine you're at a currentNode. You ask your left child to find(p, q) and your right child to find(p, q).
- If the left child reports finding
pand the right child reports findingq, thencurrentNodemust be the LCA! It's the first node to "see"pandqin separate subtrees. - If the left child reports finding
p(orq), but the right child reports finding nothing, then the LCA must be in the left subtree. ThecurrentNodeisn't the LCA, so it should just pass the report from its left child up to its own parent. - If the current node itself is
porq, it has found one of the targets. It can immediately report this back to its parent.
The following video from Back To Back SWE provides an excellent explanation of this "single node policy," which is the core intuition for solving this problem recursively.
Lowest Common Ancestor Between 2 Binary Tree Nodes (A Recursive Approach)
Watch the presenter explain the logic from the perspective of a single node. This is a powerful mental model for many recursive tree problems.
Focus on the two main examples from the beginning. Then, pay close attention to the section on defining a policy for a single node. This covers the critical cases: finding a target in both subtrees, in just one, or in neither.
The Algorithm in Detail
The intuition from the video translates into a concise, three-part recursive algorithm: base cases, recursive exploration, and decision logic. The following resource from AlgoMonster breaks this down very clearly.
236. Lowest Common Ancestor of a Binary Tree
This text formalizes the recursive strategy. It will solidify your understanding of the base cases and the logic for combining the results from the left and right subtrees.
Please read the sections Problem Description, Intuition, and Solution Approach. Focus on how the return values (p, q, or None/null) are used to signal what was found in a subtree.
To summarize the logic from the reading:
-
Base Cases:
- If the current node is
null, we've reached the end of a branch. Returnnull. - If the current node is
porq, we've found one of our targets. Return the current node.
- If the current node is
-
Recursive Step:
- Call the function recursively on the left child:
leftResult = lowestCommonAncestor(root.left, p, q). - Call the function recursively on the right child:
rightResult = lowestCommonAncestor(root.right, p, q).
- Call the function recursively on the left child:
-
Decision Logic:
- If
leftResultandrightResultare both non-null, the currentrootis the LCA. Returnroot. - Otherwise, the LCA (or one of the target nodes) is in one of the subtrees. Return whichever of
leftResultorrightResultis notnull. If both arenull, this will correctly returnnull.
- If
This is a post-order traversal pattern: the decision at a node is made after the recursive calls to its children have returned.
Visualizing the Recursive Calls
Seeing the algorithm in action helps connect all the pieces. The next video provides a step-by-step visual trace of the recursive calls and what they return.
Lowest Common Ancestor Binary Tree
This video by Tushar Roy is a classic for a reason. It masterfully visualizes the "bubbling up" of return values (null or a found node) from the recursive calls.
Watch the first two examples to see the logic in action. The first, finding the LCA of 2 and 5, runs from this timestamp. The second, for nodes 11 and 8, runs from here. Observe how a node becomes the LCA when it receives non-null values from both its left and right recursive calls.
TypeScript Implementation
Now, let's translate this logic into a TypeScript function. The code is a direct implementation of the three-step process we've discussed.
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val);
this.left = (left === undefined ? null : left);
this.right = (right === undefined ? null : right);
}
}
function lowestCommonAncestor(
root: TreeNode | null,
p: TreeNode | null,
q: TreeNode | null
): TreeNode | null {
// 1. Base Cases: If root is null, or we found p or q.
if (!root || root === p || root === q) {
return root;
}
// 2. Recursive Step: Search in left and right subtrees.
const leftResult = lowestCommonAncestor(root.left, p, q);
const rightResult = lowestCommonAncestor(root.right, p, q);
// 3. Decision Logic: Combine the results.
// If both children returned a non-null node, p and q are in different
// subtrees. This means the current root is their LCA.
if (leftResult && rightResult) {
return root;
}
// Otherwise, the LCA must be in whichever subtree returned a non-null
// result. We propagate that result upwards. If both were null, this
// correctly returns null.
return leftResult || rightResult;
};
This algorithm visits each node in the tree once, so its time complexity is , where is the number of nodes. The space complexity is determined by the maximum depth of the recursion stack, which is equal to the height of the tree, . For a balanced tree, this is , but for a skewed tree, it can be in the worst case.
Conclusion
In this lesson, we dissected the Lowest Common Ancestor problem and developed an elegant recursive solution. This algorithm is a prime example of a post-order traversal where information is "bubbled up" the call stack to solve a global problem.
Key Takeaways:
- LCA Definition: The LCA is the deepest node that is an ancestor to both target nodes. Remember that a node can be a descendant of itself.
- Recursive Strategy: The solution relies on a recursive function that reports what it finds in its subtree:
p,q, ornull. - The "Split Point" is the LCA: A node is the LCA if and only if the recursive calls on its left and right children both return non-null values. This means the targets
pandqare in different subtrees, and the current node is their first meeting point. - Propagating Results: If only one child returns a non-null value, it means both targets are in that one subtree. The current node simply passes that result up to its parent.
In our next lesson, we'll shift our focus from general tree traversal algorithms to a specialized tree-based data structure: the binary min-heap. Heaps are fundamental for implementing priority queues and solving a wide range of problems involving ordering and selection, such as finding the "top K" elements in a dataset.