Given the root of a binary tree, each node in the tree has a distinct value.
After deleting all nodes with a value in to_delete, we are left with a forest (a disjoint union of trees).
Return the roots of the trees in the remaining forest. You may return the result in any order.
Example 1:
Input: root = [1,2,3,4,5,6,7], to_delete = [3,5] Output: [[1,2,null,4],[6],[7]]
Example 2:
Input: root = [1,2,4,null,3], to_delete = [3] Output: [[1,2,4]]
Constraints:
1000.1 and 1000.to_delete.length <= 1000to_delete contains distinct values between 1 and 1000.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 approach to deleting nodes and returning a forest involves checking every single node to see if it should be removed. For each possible removal, we reconstruct the forest and see what we're left with.
Here's how the algorithm would work step-by-step:
def delete_nodes_and_return_forest_brute_force(root, to_delete):
all_possible_forests = []
def get_forest_roots(node, current_deleted_nodes):
if node is None:
return []
forest_roots = []
if node.val not in current_deleted_nodes:
is_root = True
#Check if the node is a root by iterating through every node in the tree
#to see if current node is a child of a deleted node.
def check_if_child(potential_parent, node_val):
if potential_parent is None:
return False
if potential_parent.left and potential_parent.left.val == node_val:
return True
if potential_parent.right and potential_parent.right.val == node_val:
return True
return check_if_child(potential_parent.left, node_val) or check_if_child(potential_parent.right, node_val)
#If any of the nodes in the tree has current node as a child node then it's not a root
def is_actually_a_root(root_node):
#Iterate through every node in the tree
def helper(node):
if node is None:
return False
if node.val in current_deleted_nodes and check_if_child(node, root_node.val):
return True
return helper(node.left) or helper(node.right)
# Call helper from the tree root
return not helper(root)
if is_actually_a_root(node):
forest_roots.append(node)
forest_roots.extend(get_forest_roots(node.left, current_deleted_nodes))
forest_roots.extend(get_forest_roots(node.right, current_deleted_nodes))
return forest_roots
# Generate all possible combinations of nodes to delete
number_of_nodes = 0
def count_nodes(node):
nonlocal number_of_nodes
if not node: return
number_of_nodes += 1
count_nodes(node.left)
count_nodes(node.right)
count_nodes(root)
for i in range(1 << number_of_nodes):
current_deleted_nodes = []
all_nodes_list = []
# Extract all nodes from the tree into a list to iterate
def get_all_nodes(node):
if node is None:
return
all_nodes_list.append(node.val)
get_all_nodes(node.left)
get_all_nodes(node.right)
get_all_nodes(root)
# Find the nodes that are going to be deleted in current iteration
for j in range(number_of_nodes):
if (i >> j) & 1:
current_deleted_nodes.append(all_nodes_list[j])
# Now we extract the forest by passing the current nodes to delete
forest = get_forest_roots(root, current_deleted_nodes)
all_possible_forests.append((current_deleted_nodes, forest))
# Find the forest that corresponds to exactly deleting the nodes in 'to_delete'
for deleted_nodes, forest in all_possible_forests:
if sorted(deleted_nodes) == sorted(to_delete):
#The combination of nodes to delete matches to_delete, returning the forest
return forest
return []The goal is to remove specified nodes from a tree, resulting in a forest of smaller trees. We'll traverse the tree, identifying nodes to be deleted and adjusting the tree structure accordingly. This ensures that after deletion, all the remaining connected components are returned as the resulting forest.
Here's how the algorithm would work step-by-step:
class TreeNode:
def __init__(self, value=0, left=None, right=None):
self.value = value
self.left = left
self.right = right
def delete_nodes_and_return_forest(root, to_delete):
nodes_to_delete_set = set(to_delete)
forest = []
def helper(node, is_root):
if not node:
return None
# Traverse subtrees first
node_should_be_deleted = node.value in nodes_to_delete_set
node.left = helper(node.left, node_should_be_deleted)
node.right = helper(node.right, node_should_be_deleted)
# If the current node needs to be deleted
if node_should_be_deleted:
# Add children to the forest
if node.left:
forest.append(node.left)
if node.right:
forest.append(node.right)
return None
# If the current node is a root and not deleted, add to forest
if is_root:
forest.append(node)
return node
helper(root, True)
return forest| Case | How to Handle |
|---|---|
| Null root | If the root is null, return an empty list because there's no tree to process. |
| Empty 'to_delete' list | If 'to_delete' is empty, return a list containing only the root node. |
| All nodes need to be deleted | If all nodes are in 'to_delete', return an empty list indicating no forest remains. |
| Large tree and large 'to_delete' list causing recursion depth issues | The solution should be iterative or use a tree traversal approach to avoid stack overflow errors. |
| Tree is skewed heavily to one side (left or right) | Ensure the algorithm doesn't exhibit worst-case time complexity in skewed tree scenarios; an iterative solution helps. |
| Duplicate values in 'to_delete' list | Treat the 'to_delete' list as a set to avoid unnecessary repeated lookups during node deletion. |
| Deleting the root node | Handle the root node deletion specifically, adding its children to the result if they are not also being deleted. |
| 'to_delete' contains values not present in the tree | The algorithm should handle gracefully by simply ignoring these values in the to_delete array during the traversal process. |