1650. Lowest Common Ancestor of a Binary Tree III 🔒
Problem Description
You are given two nodes p
and q
from a binary tree where each node has a reference to its parent node. Your task is to find their lowest common ancestor (LCA).
The node structure includes:
val
: the value of the nodeleft
: pointer to the left childright
: pointer to the right childparent
: pointer to the parent node
The lowest common ancestor is defined as the lowest node in the tree that has both p
and q
as descendants. Note that a node can be considered a descendant of itself.
The solution uses a hash table approach:
-
First, traverse from node
p
upward to the root, storing all visited nodes in a setvis
. This captures the complete path fromp
to the root. -
Then, traverse from node
q
upward toward the root. The first node encountered that exists in thevis
set is the lowest common ancestor, since it's the first shared node on both paths to the root. -
Return this node as the result.
The time complexity is O(h)
where h
is the height of the tree, as we might need to traverse from a leaf to the root in the worst case. The space complexity is also O(h)
for storing the visited nodes in the hash set.
Intuition
Since each node has a parent pointer, we can think of the problem as finding where two paths converge. If we trace the path from any node to the root, we get a unique sequence of nodes. The key insight is that the paths from p
to root and from q
to root must eventually meet at some common node - at the very least, they'll meet at the root itself.
The lowest common ancestor is simply the first point where these two paths intersect when viewed from the bottom up. This is similar to finding the intersection point of two linked lists.
We can visualize this: imagine climbing up from node p
to the root and marking every node we visit along the way. Then, if we climb up from node q
, the first marked node we encounter must be the lowest common ancestor. This is because:
- It's definitely a common ancestor (both paths pass through it)
- It's the lowest one (it's the first common node we find when climbing up from
q
)
This naturally leads to the hash table solution: we record all nodes on the path from p
to root in a set, then traverse from q
upward until we find a node that exists in our set. This intersection point is guaranteed to be the LCA.
The elegance of this approach is that we don't need to worry about the tree structure, left/right children, or complex recursion. We simply follow parent pointers upward, turning a tree problem into a path intersection problem.
Learn more about Tree, Two Pointers and Binary Tree patterns.
Solution Approach
The implementation uses a hash table (set) to efficiently track and find the intersection of two paths. Here's how the algorithm works step by step:
Step 1: Build the path from p
to root
vis = set()
node = p
while node:
vis.add(node)
node = node.parent
We initialize an empty set vis
to store visited nodes. Starting from node p
, we traverse upward by following parent pointers, adding each node to the set. This continues until we reach the root (when node
becomes None
).
Step 2: Find the intersection point from q
node = q while node not in vis: node = node.parent return node
Starting from node q
, we traverse upward toward the root. At each step, we check if the current node exists in our vis
set. The first node that exists in vis
is the lowest common ancestor, and we immediately return it.
Why this works:
- The set
vis
contains all ancestors ofp
(includingp
itself) - When traversing from
q
upward, we're guaranteed to eventually find a common node since all paths lead to the root - The first common node we encounter is the lowest (closest to
p
andq
) common ancestor
Time and Space Complexity:
- Time:
O(h)
whereh
is the height of the tree. In the worst case, we traverse from a leaf to the root twice. - Space:
O(h)
for storing up toh
nodes in the hash set when one node is deep in the tree.
The beauty of this solution is its simplicity - by leveraging the parent pointers and a hash table, we transform the tree problem into a straightforward path intersection problem.
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 a concrete example to illustrate the solution approach.
Consider this binary tree:
3 / \ 5 1 / \ \ 6 2 8 / \ 7 4
Where each node has a parent pointer (not shown for clarity). Let's find the LCA of nodes p = 7
and q = 4
.
Step 1: Build the path from p (node 7) to root
Starting from node 7, we traverse upward using parent pointers:
- Start at node 7, add to set:
vis = {7}
- Move to parent (node 2), add to set:
vis = {7, 2}
- Move to parent (node 5), add to set:
vis = {7, 2, 5}
- Move to parent (node 3), add to set:
vis = {7, 2, 5, 3}
- Move to parent (None - we've reached root), stop
Our set now contains the complete path from node 7 to the root: {7, 2, 5, 3}
Step 2: Traverse from q (node 4) upward until we find intersection
Starting from node 4, we check each node against our set:
- Start at node 4: Is 4 in
vis
? No, move up - Move to parent (node 2): Is 2 in
vis
? Yes!
Node 2 is the first node we encounter that exists in our set, so node 2 is the LCA.
Verification: Node 2 is indeed the lowest common ancestor of nodes 7 and 4 - it's their direct parent and the lowest node that has both as descendants.
Let's try another example with p = 6
and q = 8
:
Step 1: Build path from node 6 to root:
- Path: 6 → 5 → 3 → None
vis = {6, 5, 3}
Step 2: Traverse from node 8 upward:
- Node 8: Not in
vis
, move up - Node 1: Not in
vis
, move up - Node 3: Found in
vis
!
Node 3 is the LCA of nodes 6 and 8, which makes sense as it's the root and the only node that has both as descendants.
Solution Implementation
1"""
2# Definition for a Node.
3class Node:
4 def __init__(self, val):
5 self.val = val
6 self.left = None
7 self.right = None
8 self.parent = None
9"""
10
11
12class Solution:
13 def lowestCommonAncestor(self, p: "Node", q: "Node") -> "Node":
14 """
15 Find the lowest common ancestor of two nodes in a binary tree with parent pointers.
16
17 Args:
18 p: First node
19 q: Second node
20
21 Returns:
22 The lowest common ancestor node of p and q
23 """
24 # Create a set to store all ancestors of node p
25 visited_ancestors = set()
26
27 # Traverse from p to root, adding all ancestors to the set
28 current_node = p
29 while current_node:
30 visited_ancestors.add(current_node)
31 current_node = current_node.parent
32
33 # Traverse from q upward until we find a node that exists in p's ancestor set
34 current_node = q
35 while current_node not in visited_ancestors:
36 current_node = current_node.parent
37
38 # The first common ancestor found is the lowest common ancestor
39 return current_node
40
1/*
2// Definition for a Node.
3class Node {
4 public int val;
5 public Node left;
6 public Node right;
7 public Node parent;
8};
9*/
10
11class Solution {
12 public Node lowestCommonAncestor(Node p, Node q) {
13 // Create a set to store all ancestors of node p
14 Set<Node> visitedNodes = new HashSet<>();
15
16 // Traverse from node p to root, adding all ancestors to the set
17 for (Node currentNode = p; currentNode != null; currentNode = currentNode.parent) {
18 visitedNodes.add(currentNode);
19 }
20
21 // Traverse from node q towards root
22 // The first node that already exists in the set is the LCA
23 for (Node currentNode = q; ; currentNode = currentNode.parent) {
24 // If add() returns false, it means the node was already in the set
25 // This node is the lowest common ancestor
26 if (!visitedNodes.add(currentNode)) {
27 return currentNode;
28 }
29 }
30 }
31}
32
1/*
2// Definition for a Node.
3class Node {
4public:
5 int val;
6 Node* left;
7 Node* right;
8 Node* parent;
9};
10*/
11
12class Solution {
13public:
14 Node* lowestCommonAncestor(Node* p, Node* q) {
15 // Use a hash set to store all ancestors of node p
16 unordered_set<Node*> visitedAncestors;
17
18 // Traverse from node p to root, storing all ancestors
19 for (Node* currentNode = p; currentNode != nullptr; currentNode = currentNode->parent) {
20 visitedAncestors.insert(currentNode);
21 }
22
23 // Traverse from node q to root
24 // The first node that exists in p's ancestors is the LCA
25 for (Node* currentNode = q; currentNode != nullptr; currentNode = currentNode->parent) {
26 if (visitedAncestors.count(currentNode)) {
27 return currentNode;
28 }
29 }
30
31 // This line should never be reached if inputs are valid
32 return nullptr;
33 }
34};
35
1/**
2 * Definition for a binary tree node.
3 * class Node {
4 * val: number
5 * left: Node | null
6 * right: Node | null
7 * parent: Node | null
8 * constructor(val?: number, left?: Node | null, right?: Node | null, parent?: Node | null) {
9 * this.val = (val===undefined ? 0 : val)
10 * this.left = (left===undefined ? null : left)
11 * this.right = (right===undefined ? null : right)
12 * this.parent = (parent===undefined ? null : parent)
13 * }
14 * }
15 */
16
17/**
18 * Finds the lowest common ancestor of two nodes in a binary tree with parent pointers.
19 *
20 * The algorithm works by:
21 * 1. Traversing from node p to root, storing all ancestors in a set
22 * 2. Traversing from node q upward until finding a node that exists in p's ancestor set
23 *
24 * @param p - First node in the binary tree
25 * @param q - Second node in the binary tree
26 * @returns The lowest common ancestor node of p and q
27 */
28function lowestCommonAncestor(p: Node | null, q: Node | null): Node | null {
29 // Set to store all ancestors of node p
30 const visitedAncestors: Set<Node> = new Set<Node>();
31
32 // Traverse from p to root, adding all ancestors to the set
33 for (let currentNode = p; currentNode !== null; currentNode = currentNode.parent) {
34 visitedAncestors.add(currentNode);
35 }
36
37 // Traverse from q upward until we find a common ancestor
38 for (let currentNode = q; currentNode !== null; currentNode = currentNode.parent) {
39 // If current node exists in p's ancestor set, it's the LCA
40 if (visitedAncestors.has(currentNode)) {
41 return currentNode;
42 }
43 }
44
45 // This line should never be reached if p and q are in the same tree
46 return null;
47}
48
Time and Space Complexity
The time complexity is O(n)
, where n
is the number of nodes in the binary tree. In the worst case, we need to traverse from node p
to the root (up to n
nodes), storing each node in the set. Then we traverse from node q
upward until we find a node that exists in the set (up to n
nodes in the worst case). Therefore, the total time complexity is O(n) + O(n) = O(n)
.
The space complexity is O(n)
, where n
is the number of nodes in the binary tree. In the worst case, if node p
is a leaf node and the root is the lowest common ancestor, we need to store all nodes from p
to the root in the vis
set, which could be up to n
nodes in a skewed tree. Therefore, the space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling the Case Where a Node is Its Own Ancestor
A common mistake is assuming that the LCA must be a strict ancestor (excluding the nodes themselves). However, by definition, a node can be a descendant of itself, meaning if p
is an ancestor of q
(or vice versa), then p
(or q
) itself is the LCA.
Incorrect Approach:
# Wrong: Starting from parent instead of the node itself
vis = set()
node = p.parent # WRONG: Skips p itself
while node:
vis.add(node)
node = node.parent
Why it fails: If q
is a descendant of p
, this approach would miss p
as the LCA and incorrectly return p
's parent or a higher ancestor.
Correct Solution: Always include the starting node in the visited set:
vis = set()
node = p # Correct: Start with p itself
while node:
vis.add(node)
node = node.parent
2. Infinite Loop When Parent Pointers Form a Cycle
While rare in properly constructed trees, if there's a bug in tree construction that creates a cycle in parent pointers, the traversal could get stuck in an infinite loop.
Vulnerable Code:
while node: vis.add(node) node = node.parent # Could loop forever if there's a cycle
Defensive Solution: Add cycle detection or limit traversal depth:
vis = set()
node = p
while node and node not in vis: # Check for cycles
vis.add(node)
node = node.parent
3. Not Validating Input Nodes
The code assumes both p
and q
are valid nodes in the same tree. If they're from different trees or if either is None
, the algorithm could fail or produce incorrect results.
Problem Scenario:
- If
p
andq
are from different trees, the second traversal might never find a common ancestor - If either node is
None
, the code would crash with an AttributeError
Robust Solution: Add input validation:
def lowestCommonAncestor(self, p: "Node", q: "Node") -> "Node":
if not p or not q:
return None
# Rest of the algorithm...
visited_ancestors = set()
current_node = p
# ...
4. Memory Optimization Opportunity Missed
For very deep trees, storing all ancestors of p
might use significant memory. An alternative two-pointer approach can achieve O(1) space complexity.
Space-Efficient Alternative:
def lowestCommonAncestor(self, p: "Node", q: "Node") -> "Node":
# Get depths of both nodes
def get_depth(node):
depth = 0
while node:
depth += 1
node = node.parent
return depth
depth_p = get_depth(p)
depth_q = get_depth(q)
# Align both nodes to same depth
while depth_p > depth_q:
p = p.parent
depth_p -= 1
while depth_q > depth_p:
q = q.parent
depth_q -= 1
# Move both upward until they meet
while p != q:
p = p.parent
q = q.parent
return p
This approach uses O(1) extra space but requires two initial passes to calculate depths, maintaining the same O(h) time complexity.
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
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
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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!