Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p
and q
as the lowest node in T
that has both p
and q
as descendants (where we allow a node to be a descendant of itself).”
Example 1:
Consider the following binary tree:
3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
If p = 5
and q = 1
, the LCA is 3
.
Example 2:
Using the same binary tree:
3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
If p = 5
and q = 4
, the LCA is 5
because 5 is an ancestor of 4.
Example 3:
Consider a simpler tree:
1
/ \
2 3
If p = 2
and q = 3
, the LCA is 1
.
Constraints:
[2, 10^5]
. Given these constraints, what are the edge cases, time complexity, and space complexity of your solution?Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”
A brute-force approach would involve traversing the tree to find the paths from the root to both nodes p
and q
. Once we have these paths, we can compare them from the beginning until they diverge. The last common node in both paths is the LCA.
Algorithm:
p
. This can be done using Depth-First Search (DFS).q
using DFS.Code (Python):
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def find_path(root, target, path):
if root is None:
return False
path.append(root)
if root == target:
return True
if (root.left and find_path(root.left, target, path)) or \
(root.right and find_path(root.right, target, path)):
return True
path.pop()
return False
def lowest_common_ancestor_naive(root, p, q):
path_p = []
path_q = []
find_path(root, p, path_p)
find_path(root, q, path_q)
lca = None
for i in range(min(len(path_p), len(path_q))):
if path_p[i] == path_q[i]:
lca = path_p[i]
else:
break
return lca
Time Complexity: O(N) in the worst case, where N is the number of nodes in the tree, because we might have to traverse the entire tree to find the paths to p
and q
.
Space Complexity: O(N) in the worst case, because the paths path_p
and path_q
can be as long as the height of the tree, which can be N in a skewed tree.
A more efficient approach uses recursion. The basic idea is as follows:
None
, return None
.p
or q
, return the current node.p
and q
in the left and right subtrees.None
node, then the current node is the LCA.None
node, then that node is the LCA.Code (Python):
def lowest_common_ancestor(root, p, q):
if not root:
return None
if root == p or root == q:
return root
left_lca = lowest_common_ancestor(root.left, p, q)
right_lca = lowest_common_ancestor(root.right, p, q)
if left_lca and right_lca:
return root
elif left_lca:
return left_lca
else:
return right_lca
Time Complexity: O(N), where N is the number of nodes in the tree. In the worst case, we visit each node once.
Space Complexity: O(H), where H is the height of the tree, due to the recursive call stack. In the worst case, H can be N (for a skewed tree), and in the best case, H can be log N (for a balanced tree).
None
), the function should return None
.p
or q
is the root, the root is the LCA.p
and q
will exist in the tree, so we don't need to explicitly handle this edge case.