236. Lowest Common Ancestor of a Binary Tree
Problem Description
You are given a binary tree and two nodes p
and q
that exist in the tree. Your task is to find the lowest common ancestor (LCA) of these two nodes.
The lowest common ancestor is defined as the lowest (deepest) node in the tree that has both p
and q
as descendants. An important note: a node can be a descendant of itself.
For example, if we have a tree where node p
is a parent of node q
, then p
itself would be the LCA of p
and q
since:
p
is an ancestor ofq
(obviously)p
is an ancestor of itself (by definition)- There's no lower node that satisfies both conditions
The solution uses a recursive approach that works as follows:
-
Base cases: If we reach a
null
node or find eitherp
orq
, we return that node immediately. -
Recursive search: We search both left and right subtrees for
p
andq
. -
Decision logic:
- If both left and right subtree searches return non-null values, it means
p
is in one subtree andq
is in the other. The current node must be their LCA. - If only one subtree returns a non-null value, that value is either the LCA (if both nodes were found in that subtree) or one of the target nodes moving up the recursion chain.
- If both left and right subtree searches return non-null values, it means
The algorithm efficiently finds the LCA in a single traversal with O(n)
time complexity where n
is the number of nodes in the tree.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure.
DFS
- We arrive at DFS: Since we're dealing with a tree structure, the flowchart leads us directly to using Depth-First Search (DFS).
Conclusion: The flowchart suggests using a DFS approach for finding the Lowest Common Ancestor in a binary tree.
This makes perfect sense for the LCA problem because:
- We need to traverse the entire tree potentially to find both nodes
p
andq
- DFS allows us to explore each branch fully before backtracking
- The recursive nature of DFS perfectly matches the recursive structure of trees
- We can use the return values from recursive calls to determine if we've found both nodes in different subtrees
The solution implements DFS through recursion, where we:
- Traverse down each branch (left and right subtrees)
- Return information about whether we found
p
orq
in each subtree - Use this information to identify the LCA as the node where both subtrees contain one of our target nodes
Intuition
When searching for the lowest common ancestor of two nodes in a binary tree, we need to think about what makes a node the LCA. A node is the LCA if:
- Both
p
andq
are in its subtree (including itself) - No descendant of this node has both
p
andq
in its subtree
The key insight is that as we traverse the tree from top to bottom, the first node we encounter that "sees" both p
and q
in its subtrees (or is itself one of them) must be the LCA.
Think of it like this: if we're at a node and we find p
in the left subtree and q
in the right subtree, then the current node must be their LCA. Why? Because any node lower than the current one can only contain either p
or q
, not both.
This naturally leads to a recursive DFS approach where each recursive call answers the question: "Do you contain p
or q
in your subtree?"
The beauty of the solution lies in how we handle the return values:
- If we find
p
orq
directly, we return that node immediately - If both left and right recursive calls return non-null values, we know we've found
p
andq
in different subtrees, making the current node the LCA - If only one side returns a non-null value, we propagate that value up (it could be either one of the nodes we're looking for, or it could already be the LCA found in a deeper recursion)
The return root if left and right else (left or right)
line elegantly captures this logic: if both sides found something, the current node is the answer; otherwise, pass up whatever was found (if anything).
Solution Approach
The solution implements a recursive DFS traversal as suggested by our flowchart analysis. Let's walk through the implementation step by step:
Base Cases:
if root in (None, p, q): return root
This handles three crucial scenarios:
- If
root
isNone
, we've reached a leaf's child, so returnNone
- If
root
equalsp
orq
, we've found one of our target nodes, so return it immediately
Recursive Exploration:
left = self.lowestCommonAncestor(root.left, p, q) right = self.lowestCommonAncestor(root.right, p, q)
We recursively search both the left and right subtrees. Each recursive call will return:
None
if neitherp
norq
was found in that subtree- The node
p
orq
if one of them was found - The LCA if both nodes were found in that subtree
Decision Logic:
return root if left and right else (left or right)
This single line handles all possible outcomes:
- If both
left
andright
are non-null: This meansp
was found in one subtree andq
in the other. The currentroot
node is their LCA. - If only
left
is non-null: Either both nodes were in the left subtree (andleft
contains their LCA), or only one node was found on the left side. - If only
right
is non-null: Similar to the left case but for the right subtree. - If both are null: Neither node was found in this subtree, return
None
(vialeft or right
which evaluates toNone
).
Time Complexity: O(n)
where n
is the number of nodes, as we potentially visit every node once.
Space Complexity: O(h)
where h
is the height of the tree, due to the recursion stack. In the worst case (skewed tree), this could be O(n)
.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through finding the LCA of nodes 5
and 1
in this binary tree:
3 / \ 5 1 / \ \ 6 2 8 / \ 7 4
Step-by-step execution:
-
Start at root (3):
- Check if root is
null
,5
, or1
? No, so continue. - Recursively search left subtree (node
5
)
- Check if root is
-
At node 5 (left child of 3):
- Check if root is
null
,5
, or1
? Yes! It's5
. - Immediately return node
5
(no need to search further down)
- Check if root is
-
Back at root (3), now search right:
- Recursively search right subtree (node
1
)
- Recursively search right subtree (node
-
At node 1 (right child of 3):
- Check if root is
null
,5
, or1
? Yes! It's1
. - Immediately return node
1
- Check if root is
-
Back at root (3) with results:
left
= node5
(found in left subtree)right
= node1
(found in right subtree)- Since both
left
andright
are non-null, the current node (3) is the LCA - Return node
3
Another example: Finding LCA of nodes 7
and 4
(both in the same subtree):
3 / \ 5 1 / \ \ 6 2 8 / \ 7 4
- Start at root (3): Not
7
or4
, search both subtrees - Left subtree (5): Not
7
or4
, search both its subtrees- Left of 5 (node 6): Not
7
or4
, search children → both returnnull
- Right of 5 (node 2): Not
7
or4
, search children- Left of 2 (node 7): Found! Return
7
- Right of 2 (node 4): Found! Return
4
- Left of 2 (node 7): Found! Return
- At node 2: Both children returned non-null → node
2
is LCA, return it
- Left of 5 (node 6): Not
- Back at node 5:
left
=null
,right
= node2
→ return node2
- Right subtree of 3: Returns
null
(neither node found) - At root (3):
left
= node2
,right
=null
→ return node2
The final answer is node 2
, which is indeed the lowest common ancestor of nodes 7
and 4
.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, x):
4# self.val = x
5# self.left = None
6# self.right = None
7
8
9class Solution:
10 def lowestCommonAncestor(
11 self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode'
12 ) -> 'TreeNode':
13 """
14 Find the lowest common ancestor (LCA) of two nodes in a binary tree.
15
16 Args:
17 root: The root node of the binary tree
18 p: First target node
19 q: Second target node
20
21 Returns:
22 The lowest common ancestor node of p and q
23 """
24 # Base case: if root is None or root is one of the target nodes
25 # then root itself is the answer
26 if root in (None, p, q):
27 return root
28
29 # Recursively search for p and q in the left subtree
30 left_result = self.lowestCommonAncestor(root.left, p, q)
31
32 # Recursively search for p and q in the right subtree
33 right_result = self.lowestCommonAncestor(root.right, p, q)
34
35 # If both left and right subtrees return non-None values,
36 # it means p and q are in different subtrees, so current root is the LCA
37 if left_result and right_result:
38 return root
39
40 # If only one subtree contains both nodes (or one node),
41 # return the non-None result
42 return left_result or right_result
43
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode(int x) { val = x; }
8 * }
9 */
10class Solution {
11 /**
12 * Finds the lowest common ancestor (LCA) of two nodes in a binary tree.
13 *
14 * @param root The root of the binary tree
15 * @param p The first target node
16 * @param q The second target node
17 * @return The lowest common ancestor of nodes p and q
18 */
19 public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
20 // Base case: if root is null or root is one of the target nodes
21 // then root itself is the answer
22 if (root == null || root == p || root == q) {
23 return root;
24 }
25
26 // Recursively search for LCA in the left subtree
27 TreeNode leftLCA = lowestCommonAncestor(root.left, p, q);
28
29 // Recursively search for LCA in the right subtree
30 TreeNode rightLCA = lowestCommonAncestor(root.right, p, q);
31
32 // If both left and right subtrees return non-null values,
33 // it means p and q are found in different subtrees,
34 // so the current root is the LCA
35 if (leftLCA != null && rightLCA != null) {
36 return root;
37 }
38
39 // If only one subtree returns a non-null value,
40 // return that value (it could be the LCA or one of the target nodes)
41 // If both are null, return null
42 return leftLCA == null ? rightLCA : leftLCA;
43 }
44}
45
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode(int x) : val(x), left(NULL), right(NULL) {}
8 * };
9 */
10class Solution {
11public:
12 /**
13 * Finds the lowest common ancestor (LCA) of two nodes in a binary tree.
14 *
15 * @param root The root of the binary tree
16 * @param p First target node
17 * @param q Second target node
18 * @return The lowest common ancestor of nodes p and q
19 */
20 TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
21 // Base case: if root is null or root is one of the target nodes
22 // Return root as it could be the LCA or indicate finding a target
23 if (root == nullptr || root == p || root == q) {
24 return root;
25 }
26
27 // Recursively search for p and q in the left subtree
28 TreeNode* leftResult = lowestCommonAncestor(root->left, p, q);
29
30 // Recursively search for p and q in the right subtree
31 TreeNode* rightResult = lowestCommonAncestor(root->right, p, q);
32
33 // If both left and right subtrees return non-null values,
34 // it means p and q are found in different subtrees,
35 // so current root is the LCA
36 if (leftResult != nullptr && rightResult != nullptr) {
37 return root;
38 }
39
40 // If only one subtree contains both nodes (or one node),
41 // return the non-null result which represents the LCA
42 return (leftResult != nullptr) ? leftResult : rightResult;
43 }
44};
45
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Finds the lowest common ancestor (LCA) of two nodes in a binary tree.
17 * The LCA is defined as the lowest node that has both p and q as descendants
18 * (where we allow a node to be a descendant of itself).
19 *
20 * @param root - The root node of the binary tree
21 * @param p - The first target node
22 * @param q - The second target node
23 * @returns The lowest common ancestor node, or null if not found
24 */
25function lowestCommonAncestor(
26 root: TreeNode | null,
27 p: TreeNode | null,
28 q: TreeNode | null
29): TreeNode | null {
30 // Base case: if root is null or root matches either p or q
31 // then root itself is the answer
32 if (!root || root === p || root === q) {
33 return root;
34 }
35
36 // Recursively search for p and q in the left subtree
37 const leftResult: TreeNode | null = lowestCommonAncestor(root.left, p, q);
38
39 // Recursively search for p and q in the right subtree
40 const rightResult: TreeNode | null = lowestCommonAncestor(root.right, p, q);
41
42 // If both left and right subtrees return non-null values,
43 // it means p and q are in different subtrees, so current root is the LCA
44 if (leftResult && rightResult) {
45 return root;
46 }
47
48 // Otherwise, return the non-null result (if any)
49 // This means both nodes are in the same subtree
50 return leftResult || rightResult;
51}
52
Time and Space Complexity
The time complexity is O(n)
, where n
is the number of nodes in the binary tree. This is because in the worst case, the algorithm needs to visit every node in the tree exactly once. The recursive function traverses the entire tree by visiting each node and making recursive calls to its left and right children.
The space complexity is O(n)
, where n
is the number of nodes in the binary tree. This space is used by the recursion call stack. In the worst case, when the tree is completely unbalanced (essentially a linked list), the recursion depth can reach n
levels deep, requiring O(n)
space on the call stack. In the best case of a balanced tree, the space complexity would be O(log n)
due to the height of the tree, but we consider the worst-case scenario for complexity analysis.
Common Pitfalls
1. Assuming Nodes Always Exist in the Tree
One of the most common pitfalls is not considering the case where p
or q
might not actually exist in the tree. The given solution assumes both nodes are present, which could lead to incorrect results if this assumption is violated.
Problem Example:
# Tree: [3, 5, 1, 6, 2, 0, 8] # If we search for LCA of node 5 and node 10 (which doesn't exist) # The algorithm would incorrectly return node 5
Solution: Add a verification step to ensure both nodes exist before finding LCA:
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
# First verify both nodes exist
def findNode(root, target):
if not root:
return False
if root == target:
return True
return findNode(root.left, target) or findNode(root.right, target)
# Only proceed if both nodes exist
if not findNode(root, p) or not findNode(root, q):
return None
# Original LCA logic
return self.findLCA(root, p, q)
def findLCA(self, root, p, q):
if root in (None, p, q):
return root
left = self.findLCA(root.left, p, q)
right = self.findLCA(root.right, p, q)
return root if left and right else (left or right)
2. Misunderstanding the "Descendant of Itself" Rule
Developers often miss that a node can be its own ancestor/descendant, leading to incorrect implementations that skip the current node when it matches p
or q
.
Incorrect Approach:
# WRONG: Continuing to search even after finding p or q if root == p or root == q: # Don't return immediately, keep searching children left = self.lowestCommonAncestor(root.left, p, q) right = self.lowestCommonAncestor(root.right, p, q) # This is incorrect!
Correct Understanding:
When we find p
or q
, we should return immediately because:
- If the other node is in this node's subtree, this node is the LCA
- If the other node is elsewhere, a higher ancestor will be the LCA
3. Confusing Return Values in Recursive Calls
The recursive function returns different types of information depending on the context, which can be confusing:
- Sometimes it returns one of the target nodes (
p
orq
) - Sometimes it returns the actual LCA
- Sometimes it returns
None
Mental Model to Avoid Confusion: Think of the return value as "evidence of finding nodes":
None
= "I found nothing"- A node = "I found something important (either a target node or the LCA)"
- The parent logic determines whether that "something" is the final answer or just partial evidence
Which technique can we use to find the middle of a linked list?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!