1644. Lowest Common Ancestor of a Binary Tree II 🔒
Problem Description
This problem asks you to find the Lowest Common Ancestor (LCA) of two nodes p
and q
in a binary tree, with an important twist: either p
or q
might not exist in the tree.
Key requirements:
- Given a binary tree with root node and two target nodes
p
andq
- Find the lowest node that has both
p
andq
as descendants - If either
p
orq
doesn't exist in the tree, returnnull
- All node values in the tree are unique
- A node can be a descendant of itself
Understanding LCA: The lowest common ancestor is the deepest node in the tree that has both target nodes in its subtree. For example, if we're looking for the LCA of nodes with values 5 and 1 in this tree:
3 / \ 5 1 / \ 6 2
The LCA would be node 3, as it's the lowest node that contains both 5 and 1 as descendants.
The twist in this problem:
Unlike the standard LCA problem where both nodes are guaranteed to exist, here we must verify that both p
and q
actually exist in the tree before returning an ancestor. If either node is missing, we should return null
.
Solution approach: The solution uses a depth-first search (DFS) that:
- Traverses the entire tree to check if nodes exist
- Returns
True
if a subtree contains eitherp
orq
- When a node has both target nodes in its left and right subtrees (or one in a subtree and is itself one of the targets), it becomes the LCA
- Uses a nonlocal variable
ans
to store the LCA once found - Returns the LCA if both nodes exist, otherwise returns
null
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 with a root node and parent-child relationships.
DFS
- We arrive at DFS: For tree problems, DFS is often the natural choice, especially when we need to explore paths from root to leaves or find relationships between nodes at different depths.
Conclusion: The flowchart suggests using a Depth-First Search (DFS) approach for finding the Lowest Common Ancestor.
Why DFS is Perfect for This Problem
The DFS pattern fits naturally because:
- Tree Traversal: We need to traverse the entire tree to verify both nodes exist
- Bottom-up Information: DFS allows us to collect information from children before processing the parent (post-order traversal)
- Path Tracking: We need to track which subtrees contain our target nodes
- Ancestor Relationship: The LCA is found when a node has both targets in its subtrees (left/right) or is itself one of the targets while having the other in a subtree
The solution uses DFS to:
- Recursively search left and right subtrees
- Return boolean values indicating whether
p
orq
was found - Identify the LCA when both targets are found in different subtrees of a node
- Handle the edge case where one or both nodes don't exist by returning
null
Intuition
The key insight is that we need to solve two problems simultaneously: verify both nodes exist in the tree AND find their lowest common ancestor if they do exist.
Think about how we'd manually find the LCA of two nodes. We'd start from the root and ask: "Are both nodes in my left subtree? Are both in my right subtree? Or are they split between my subtrees?" The node where they "split" (one in left, one in right, or I'm one of them with the other below me) is the LCA.
But here's the catch - we can't assume both nodes exist. So we need to traverse the entire tree to confirm their existence while simultaneously tracking potential ancestors.
The natural approach is to use DFS to bubble up information from the bottom. Each recursive call answers: "Did I find p
or q
in this subtree?" By returning True
or False
from each subtree, we can identify the exact moment when:
- Both left and right subtrees contain one target each (current node is LCA)
- One subtree contains a target and the current node is the other target (current node is LCA)
Why can't we stop early when we find a potential LCA? Because we must verify both nodes exist. If we stopped at the first node that looks like an LCA but q
doesn't exist in the tree, we'd incorrectly return that node instead of null
.
The trick is using a nonlocal variable ans
to store our potential LCA. We only update ans
when we've confirmed we have both nodes in the current node's domain. After traversing the entire tree, if both nodes existed, ans
will hold the LCA; otherwise, it remains null
.
This approach elegantly handles all cases:
- Both nodes exist → return their LCA
- One or both nodes missing → return
null
- One node is an ancestor of the other → return that ancestor node
Learn more about Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The implementation uses a recursive DFS helper function that traverses the tree and collects information from the bottom up.
Core Algorithm Structure:
-
Base Case: If we reach a
null
node, returnFalse
(neither target found in this path) -
Recursive Calls:
l = dfs(root.left, p, q)
- Check if either target exists in left subtreer = dfs(root.right, p, q)
- Check if either target exists in right subtree
-
LCA Detection Logic:
- Case 1: If both
l
andr
areTrue
, the current node is the LCA (targets are in different subtrees)
if l and r: ans = root
- Case 2: If one subtree contains a target AND current node is the other target, current node is the LCA
if (l or r) and (root.val == p.val or root.val == q.val): ans = root
- Case 1: If both
-
Return Value: The function returns
True
if the current subtree contains at least one target:return l or r or root.val == p.val or root.val == q.val
This ensures parent nodes know if targets exist below them.
Key Implementation Details:
-
Nonlocal Variable:
ans
is declared as nonlocal to persist the LCA across recursive calls. It starts asNone
and only gets updated when we confirm both nodes are present. -
Full Tree Traversal: Unlike standard LCA where we can return early, we must traverse the entire tree to verify both nodes exist. Even after finding a potential LCA, we continue to ensure both nodes are actually in the tree.
-
Value Comparison: Since all node values are unique, we compare
root.val
withp.val
andq.val
rather than comparing node references.
Time Complexity: O(n)
where n
is the number of nodes, as we visit each node exactly once.
Space Complexity: O(h)
where h
is the height of the tree, due to the recursive call stack.
The beauty of this solution is that it handles both requirements (existence check and LCA finding) in a single pass through the tree, making it efficient despite the additional constraint.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through finding the LCA of nodes with values 5 and 4 in this tree:
3 / \ 5 1 / \ \ 6 2 8 / \ 7 4
Step-by-step DFS traversal:
-
Start at root (3): Call
dfs(3, 5, 4)
- Recursively explore left subtree first
-
Visit node 5: Call
dfs(5, 5, 4)
- Explore left:
dfs(6, 5, 4)
→ returnsFalse
(6 ≠5 and 6 ≠4, no children) - Explore right:
dfs(2, 5, 4)
- Left:
dfs(7, 5, 4)
→ returnsFalse
- Right:
dfs(4, 5, 4)
→ returnsTrue
(4 = 4) - Node 2:
l=False
,r=True
, returnsTrue
(found 4 in right subtree)
- Left:
- Node 5:
l=False
,r=True
, androot.val=5
(matches p)- Since
r=True
AND node 5 matches p, node 5 is the LCA - Set
ans = node 5
- Returns
True
(both conditions met)
- Since
- Explore left:
-
Back at root (3): Left subtree returned
True
- Continue with right subtree:
dfs(1, 5, 4)
- Node 1 and its subtree don't contain 5 or 4, returns
False
- Continue with right subtree:
-
Final result:
ans = node 5
is returned as the LCA
Why node 5 is the LCA:
- Node 5 itself is one of our targets (p=5)
- Node 4 exists in node 5's right subtree
- This makes node 5 the lowest node containing both targets
What if node 4 didn't exist? If we replaced node 4 with any other value (say 9), the algorithm would:
- Still traverse the entire tree
- Never find both targets in any node's domain
- Keep
ans = None
throughout - Return
None
since not both nodes exist
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 of nodes p and q in a binary tree.
15
16 Args:
17 root: The root 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
25 # Variable to store the LCA result
26 self.lca_node = None
27
28 def find_nodes(current_node: 'TreeNode', target_p: 'TreeNode', target_q: 'TreeNode') -> bool:
29 """
30 Recursively search for target nodes and identify the LCA.
31
32 Returns:
33 True if current subtree contains at least one target node
34 """
35 # Base case: reached a null node
36 if current_node is None:
37 return False
38
39 # Recursively check left subtree for target nodes
40 left_contains_target = find_nodes(current_node.left, target_p, target_q)
41
42 # Recursively check right subtree for target nodes
43 right_contains_target = find_nodes(current_node.right, target_p, target_q)
44
45 # Check if current node is one of the target nodes
46 is_current_target = (current_node.val == target_p.val or
47 current_node.val == target_q.val)
48
49 # LCA Case 1: Both left and right subtrees contain target nodes
50 if left_contains_target and right_contains_target:
51 self.lca_node = current_node
52
53 # LCA Case 2: One subtree contains a target and current node is the other target
54 if (left_contains_target or right_contains_target) and is_current_target:
55 self.lca_node = current_node
56
57 # Return True if current subtree contains any target node
58 return left_contains_target or right_contains_target or is_current_target
59
60 # Start the DFS traversal from root
61 find_nodes(root, p, q)
62
63 return self.lca_node
64
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 private TreeNode lowestCommonAncestor;
12
13 /**
14 * Finds the lowest common ancestor of two nodes in a binary tree.
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 public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
21 findLCA(root, p, q);
22 return lowestCommonAncestor;
23 }
24
25 /**
26 * Performs depth-first search to find if the current subtree contains p or q.
27 * Updates the LCA when found.
28 * @param currentNode The current node being processed
29 * @param p First target node
30 * @param q Second target node
31 * @return true if the current subtree contains at least one of p or q
32 */
33 private boolean findLCA(TreeNode currentNode, TreeNode p, TreeNode q) {
34 // Base case: reached null node
35 if (currentNode == null) {
36 return false;
37 }
38
39 // Recursively search left subtree
40 boolean leftContainsTarget = findLCA(currentNode.left, p, q);
41
42 // Recursively search right subtree
43 boolean rightContainsTarget = findLCA(currentNode.right, p, q);
44
45 // Case 1: Both left and right subtrees contain one target each
46 // Current node is the LCA
47 if (leftContainsTarget && rightContainsTarget) {
48 lowestCommonAncestor = currentNode;
49 }
50
51 // Case 2: One subtree contains a target and current node is the other target
52 // Current node is the LCA
53 if ((leftContainsTarget || rightContainsTarget) &&
54 (currentNode.val == p.val || currentNode.val == q.val)) {
55 lowestCommonAncestor = currentNode;
56 }
57
58 // Return true if current subtree contains at least one target node
59 return leftContainsTarget || rightContainsTarget ||
60 currentNode.val == p.val || currentNode.val == q.val;
61 }
62}
63
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 of two nodes in a binary tree
14 * @param root The root of the binary tree
15 * @param p First target node
16 * @param q Second target node
17 * @return The lowest common ancestor of nodes p and q
18 */
19 TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
20 findLCA(root, p, q);
21 return lca_node;
22 }
23
24private:
25 TreeNode* lca_node = nullptr; // Stores the lowest common ancestor
26
27 /**
28 * Performs DFS to find if current subtree contains p or q
29 * @param current Current node being processed
30 * @param p First target node
31 * @param q Second target node
32 * @return true if current subtree contains at least one of p or q
33 */
34 bool findLCA(TreeNode* current, TreeNode* p, TreeNode* q) {
35 // Base case: reached null node
36 if (!current) {
37 return false;
38 }
39
40 // Recursively search left and right subtrees
41 bool found_in_left = findLCA(current->left, p, q);
42 bool found_in_right = findLCA(current->right, p, q);
43
44 // Case 1: Both p and q found in different subtrees
45 // Current node is the LCA
46 if (found_in_left && found_in_right) {
47 lca_node = current;
48 }
49
50 // Case 2: One target found in subtree and current node is the other target
51 // Current node is the LCA
52 if ((found_in_left || found_in_right) &&
53 (current->val == p->val || current->val == q->val)) {
54 lca_node = current;
55 }
56
57 // Return true if current subtree contains at least one target node
58 return found_in_left || found_in_right ||
59 current->val == p->val || current->val == q->val;
60 }
61};
62
1/**
2 * Definition for a binary tree node.
3 */
4interface TreeNode {
5 val: number;
6 left: TreeNode | null;
7 right: TreeNode | null;
8}
9
10/**
11 * Finds the lowest common ancestor of two nodes in a binary tree.
12 * @param root - The root of the binary tree
13 * @param p - The first target node
14 * @param q - The second target node
15 * @returns The lowest common ancestor node of p and q
16 */
17const lowestCommonAncestor = function(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null {
18 // Variable to store the result (lowest common ancestor)
19 let answer: TreeNode | null = null;
20
21 /**
22 * Depth-first search to find the lowest common ancestor.
23 * @param node - Current node being processed
24 * @returns True if the current subtree contains p or q, false otherwise
25 */
26 const dfs = (node: TreeNode | null): boolean => {
27 // Base case: if node is null, return false
28 if (!node) {
29 return false;
30 }
31
32 // Recursively search in the left subtree
33 const leftContainsTarget: boolean = dfs(node.left);
34
35 // Recursively search in the right subtree
36 const rightContainsTarget: boolean = dfs(node.right);
37
38 // If both left and right subtrees contain targets, current node is LCA
39 if (leftContainsTarget && rightContainsTarget) {
40 answer = node;
41 }
42
43 // If one subtree contains a target and current node is the other target, current node is LCA
44 if ((leftContainsTarget || rightContainsTarget) && (node.val === p.val || node.val === q.val)) {
45 answer = node;
46 }
47
48 // Return true if current subtree contains at least one target node
49 return leftContainsTarget || rightContainsTarget || node.val === p.val || node.val === q.val;
50 };
51
52 // Start DFS from root
53 dfs(root);
54
55 return answer;
56};
57
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of nodes in the binary tree. The DFS traversal visits each node exactly once. At each node, we perform constant time operations (checking conditions, comparing values, and updating the answer variable), so the overall time complexity is linear with respect to the number of nodes.
Space Complexity: O(h)
where h
is the height of the binary tree. The space complexity is determined by the recursive call stack. In the worst case (a skewed tree), the height could be O(n)
, making the space complexity O(n)
. In the best case (a balanced tree), the height would be O(log n)
, making the space complexity O(log n)
. The additional space used for the ans
variable is O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Early Termination When Finding First Target
The Pitfall: A common mistake is trying to optimize by returning early when finding the first target node, similar to the standard LCA problem. For example:
def find_nodes(current_node, target_p, target_q):
if current_node is None:
return False
# WRONG: Early return when finding a target
if current_node.val == target_p.val or current_node.val == target_q.val:
return True # This prevents checking if the other node exists!
left = find_nodes(current_node.left, target_p, target_q)
right = find_nodes(current_node.right, target_p, target_q)
# ...
Why This Fails:
If p
is an ancestor of q
(or vice versa), returning early at p
prevents us from verifying that q
actually exists in the tree. For example, if we're looking for nodes 3 and 7 in a tree where 7 doesn't exist, but 3 is the root, we'd incorrectly return 3 as the LCA.
The Solution: Always complete the full traversal of both subtrees before checking the current node:
def find_nodes(current_node, target_p, target_q):
if current_node is None:
return False
# CORRECT: Check subtrees first
left_contains_target = find_nodes(current_node.left, target_p, target_q)
right_contains_target = find_nodes(current_node.right, target_p, target_q)
# Then check current node
is_current_target = (current_node.val == target_p.val or
current_node.val == target_q.val)
# Process LCA logic...
return left_contains_target or right_contains_target or is_current_target
2. Not Verifying Both Nodes Exist
The Pitfall: Setting the LCA without confirming both nodes are actually present in the tree:
def lowestCommonAncestor(self, root, p, q):
self.lca_node = None
def find_nodes(current_node, target_p, target_q):
# ... traversal logic ...
# WRONG: Setting LCA without verification
if left_contains_target and right_contains_target:
self.lca_node = current_node
return True # But what if only one node actually exists?
find_nodes(root, p, q)
return self.lca_node # Might return incorrect LCA
Why This Fails: Consider searching for nodes 5 and 99 where 99 doesn't exist. If node 5 has children, the algorithm might incorrectly identify 5 as the LCA when it finds 5 in one subtree and nothing in the other.
The Solution: The correct approach implicitly verifies both nodes exist by only setting the LCA when we have concrete evidence of both nodes:
# CORRECT: LCA is only set when we have proof of both nodes if left_contains_target and right_contains_target: # Both nodes must exist, one in each subtree self.lca_node = current_node if (left_contains_target or right_contains_target) and is_current_target: # Current node is one target, and the other exists in a subtree self.lca_node = current_node
3. Using Node References Instead of Values
The Pitfall: Comparing node objects directly instead of their values:
# WRONG: Direct node comparison is_current_target = (current_node == target_p or current_node == target_q)
Why This Fails:
The nodes p
and q
passed to the function might be different object instances than the actual nodes in the tree, even if they have the same values. Direct reference comparison would fail to identify matching nodes.
The Solution: Always compare node values since they're guaranteed to be unique:
# CORRECT: Compare values is_current_target = (current_node.val == target_p.val or current_node.val == target_q.val)
What data structure does Breadth-first search typically uses to store intermediate states?
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!