Given the root
of a binary tree where each node has a unique value, and an array of n
node values nodes
, return the lowest common ancestor (LCA) of all nodes in nodes
. All the nodes will exist in the tree, and all node values will be unique.
Extending the definition of LCA on Wikipedia: “The lowest common ancestor of two nodes v
and w
in a tree T
is the lowest node that has both v
and w
as descendants (where we allow a node to be a descendant of itself).”
A descendant of a node x
is any node from the subtree rooted at x
.
Example 1:
Input: root = [3,5,1,6,2,0,8,null,null,7,4], nodes = [4,7] Output: 2 Explanation: The lowest common ancestor of nodes 4 and 7 is node 2.
Example 2:
Input: root = [3,5,1,6,2,0,8,null,null,7,4], nodes = [4,6,7] Output: 5 Explanation: The lowest common ancestor of nodes 4, 6, and 7 is node 5.
Example 3:
Input: root = [3,5,1,6,2,0,8,null,null,7,4], nodes = [5] Output: 5 Explanation: The lowest common ancestor of a single node is the node itself.
Constraints:
[1, 104]
.-109 <= Node.val <= 109
Node.val
are unique.1 <= nodes.length <= 104
nodes.length <= n
, where n
is the number of nodes in the tree.nodes
exist in the tree.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 problem asks us to find the shared parent node that is deepest in the tree for a given set of nodes. The brute force method essentially checks every node to see if it can be the lowest common ancestor, by verifying if all the target nodes are descendants of it.
Here's how the algorithm would work step-by-step:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def lowest_common_ancestor_brute_force(root, nodes):
potential_lcas = None
def is_descendant(node, target_node):
if not node:
return False
if node == target_node:
return True
return is_descendant(node.left, target_node) or is_descendant(node.right, target_node)
def all_nodes_are_descendants(current_node, target_nodes):
for target_node in target_nodes:
if not is_descendant(current_node, target_node):
return False
return True
def find_all_nodes(node):
if not node:
return []
return [node] + find_all_nodes(node.left) + find_all_nodes(node.right)
all_tree_nodes = find_all_nodes(root)
for current_node in all_tree_nodes:
# Check if the current node is a potential LCA.
if all_nodes_are_descendants(current_node, nodes):
# Update potential_lcas if current node is deeper.
if not potential_lcas:
potential_lcas = current_node
else:
def get_depth(node):
if not node:
return 0
return 1 + max(get_depth(node.left), get_depth(node.right))
current_depth = get_depth(current_node)
existing_depth = get_depth(potential_lcas)
if current_depth > existing_depth:
potential_lcas = current_node
# Return the deepest common ancestor found
return potential_lcas
The key idea is to traverse the tree recursively. As we explore the tree, we check if the current node is one of the target nodes we are looking for. If it is, we know that node is an ancestor.
Here's how the algorithm would work step-by-step:
def lowestCommonAncestor(root, nodes):
def recurse_tree(current_node):
if not current_node:
return None
# Recursively search in the left subtree
left_subtree = recurse_tree(current_node.left)
# Recursively search in the right subtree
right_subtree = recurse_tree(current_node.right)
# If both subtrees return a node, current node is LCA
if left_subtree and right_subtree:
return current_node
# If either subtree returns a node, return that node
if left_subtree:
return left_subtree
if right_subtree:
return right_subtree
# Check if the current node is one of the target nodes
if current_node in nodes:
# The node is one of the target nodes
return current_node
return None
return recurse_tree(root)
Case | How to Handle |
---|---|
root is null | Return null if the root is null, as there is no tree to traverse. |
nodes array is null or empty | Return null if the nodes array is empty because there's nothing to find the LCA for. |
nodes array contains null node(s) | Handle null nodes in the input array by skipping them during LCA calculation, or throw an exception, depending on requirements. |
All nodes in 'nodes' are the same node (e.g., the root) | The LCA should be that single node, so ensure the algorithm correctly identifies it. |
One of the nodes in 'nodes' is the root, and all other nodes are descendants of that root | The LCA should be the root node itself. |
None of the nodes in 'nodes' are present in the tree | Return null, indicating that no common ancestor exists for the given nodes in the tree. |
The tree is highly skewed (e.g., a linked list) | The recursive calls could potentially lead to stack overflow for very deep trees, so iterative solutions should be considered for better performance and memory usage. |
The tree is large and 'nodes' contains a significant number of nodes scattered throughout the tree | Ensure the solution scales efficiently, potentially using memoization or other optimization techniques to avoid redundant calculations. |