You are given the root of a binary tree, along with two nodes, p
and q
. Your task is to write a function that finds the lowest common ancestor (LCA) of the nodes p
and q
in the tree. Unlike the standard LCA problem, if either p
or q
does not exist in the tree, the function should return null
. Assume all node values are unique.
Here's a more detailed explanation:
Lowest Common Ancestor: The LCA of two nodes p
and q
is defined as the lowest node in the tree that has both p
and q
as descendants (where we allow a node to be a descendant of itself).
Non-Existent Nodes: If either p
or q
is not present in the tree, the function must return null
.
Input: The function will receive the root of the binary tree (root
), and the two nodes to find the LCA for (p
and q
). The tree node structure is defined as follows:
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
Output: The function should return the TreeNode
that represents the LCA of p
and q
. If either p
or q
is not in the tree, return null
.
Example:
Consider the following binary tree:
3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
p
is node 5
and q
is node 1
, the LCA is node 3
.p
is node 5
and q
is node 4
, the LCA is node 5
.p
is node 5
and q
is node 10
(which is not in the tree), the function should return null
.How would you implement this function efficiently, considering the possibility of non-existent nodes?
When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The brute force way to find the lowest common ancestor of two people in a family tree is to check every possibility. We can do this by looking at all the ancestors of each person. The first ancestor they share in common as we go up the tree is the lowest common ancestor.
Here's how the algorithm would work step-by-step:
def lowest_common_ancestor_brute_force(root, node_one, node_two):
def find_ancestors(node):
ancestors = []
current_node = node
while current_node:
ancestors.append(current_node)
# Assuming each node has a 'parent' attribute.
if not hasattr(current_node, 'parent'):
return []
current_node = current_node.parent
return ancestors
node_one_ancestors = find_ancestors(node_one)
node_two_ancestors = find_ancestors(node_two)
# Reverse the lists to start from the immediate parents.
node_one_ancestors.reverse()
node_two_ancestors.reverse()
# Iterate to find the lowest common ancestor
lowest_common_ancestor = None
for i in range(min(len(node_one_ancestors), len(node_two_ancestors))):
# Stop when there's a divergence in ancestors.
if node_one_ancestors[i] == node_two_ancestors[i]:
lowest_common_ancestor = node_one_ancestors[i]
else:
break
# Return the found common ancestor.
return lowest_common_ancestor
The most efficient way to find the lowest common ancestor (LCA) in a binary tree when we know two nodes exist involves a single tree traversal. The idea is to recursively search for the nodes and track when they are found, returning relevant information up the tree. If both nodes are found in a subtree, the root of that subtree is the LCA.
Here's how the algorithm would work step-by-step:
def lowest_common_ancestor(root, first_node, second_node):
def recurse_tree(node):
if not node:
return None
# Recursively search the left subtree
left_result = recurse_tree(node.left)
# Recursively search the right subtree
right_result = recurse_tree(node.right)
# If the current node is one of the target nodes
node_is_target = node == first_node or node == second_node
# If the target nodes were found in left and right subtrees
if left_result and right_result:
return node
# If the current node is a target node and either target was found in a subtree
if node_is_target and (left_result or right_result):
return node
# If the current node is a target node
if node_is_target:
return node
# Propagate results up the tree.
return left_result or right_result
return recurse_tree(root)
Case | How to Handle |
---|---|
Root is null | Return null immediately since there is no tree to traverse. |
p or q is null | Return null if either p or q are null as LCA cannot be defined. |
p and q are the same node | Return p (or q) since the lowest common ancestor of a node with itself is the node itself. |
Either p or q, or both, do not exist in the tree. | Return null if either p or q are not in the tree, which can be determined during the recursive traversal. |
Large, unbalanced tree (e.g., skewed tree) | Recursive solution may lead to stack overflow; consider iterative solution using a parent pointer map. |
Tree with a single node that is either p or q. | If the root is equal to either p or q, check if the other node is present in the tree; if not, return null, otherwise return the root. |
p is an ancestor of q or q is an ancestor of p | The algorithm should correctly identify the ancestor as the LCA in these situations. |
All node values are the same, potentially including p and q. | The algorithm relies on object identity (pointer comparison), not value comparison, so identical values should not affect correctness. |