1110. Delete Nodes And Return Forest
Problem Description
You are given the root of a binary tree where each node has a distinct value. You also have a list of values called to_delete
that specifies which nodes should be removed from the tree.
When you delete a node from the tree, the tree may split into multiple smaller trees (a forest). Specifically, when a node is deleted:
- Its parent (if it exists) will no longer point to it
- Its children (if they exist) become roots of new separate trees
Your task is to delete all nodes whose values appear in to_delete
and return a list containing the root nodes of all the remaining trees in the forest.
For example, if you have a tree and delete a node in the middle, you might end up with:
- The original root (if it wasn't deleted) leading a smaller tree
- The deleted node's left child (if it exists) becoming the root of a new tree
- The deleted node's right child (if it exists) becoming the root of another new tree
The key points to understand:
- Each node value in the tree is unique
- After deletion, you need to collect all the "new" root nodes
- A node becomes a new root if its parent was deleted but the node itself wasn't deleted
- The original root remains a root (and should be included) only if it wasn't deleted
- You can return the roots in any order
The solution uses a depth-first search (DFS) approach that processes nodes from bottom to top, handling deletions and collecting new roots as it traverses back up the tree.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves a binary tree, which is a special type of graph with nodes (tree nodes) and edges (parent-child relationships).
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure where each node has at most two children (left and right).
DFS
- Yes: We arrive at DFS as the solution pattern.
The flowchart leads us directly to DFS because:
- We're dealing with a tree data structure
- We need to traverse the entire tree to identify nodes to delete
- We need to process nodes and their relationships (parent-child connections)
- When deleting a node, we need to handle its subtrees, which requires visiting all nodes in a systematic way
Why DFS is the right choice for this problem:
- Bottom-up processing: DFS allows us to process children before their parents (post-order traversal), which is crucial here because we need to decide what to do with children before deleting their parent
- Tree modification: We need to modify the tree structure by removing nodes and potentially creating new trees, which is naturally handled through recursive DFS
- Collecting roots: As we traverse and delete nodes, DFS helps us identify and collect new root nodes when their parents are deleted
Conclusion: The flowchart correctly identifies DFS as the optimal approach for this tree modification problem where we need to delete specific nodes and return the resulting forest of trees.
Intuition
When we delete a node from a tree, we're essentially "cutting" the tree at that point. The key insight is that this cutting action creates new independent trees from the deleted node's children (if they exist and aren't also being deleted).
Think about it this way: imagine cutting a branch from a real tree. The part above the cut (toward the root) remains with the main tree, while the part below the cut falls off. But in our problem, when we "cut" (delete) a node, both its children become new independent trees.
The challenge is determining which nodes become new roots. A node becomes a new root in exactly one scenario: when its parent is deleted but the node itself survives. This means we need to track:
- Whether a node should be deleted
- Whether its children should become new roots
The natural way to handle this is to process the tree from bottom to top using post-order traversal (process children before parents). Why? Because when we reach a parent node, we need to already know what happened to its children:
- If a child was deleted, the parent should point to
null
instead - If a child survived, we need to decide if it becomes a new root based on whether the parent will be deleted
Here's the clever part: we can use the return value of our recursive function to signal whether a node survives or not:
- Return
null
if the node is deleted (telling the parent to disconnect) - Return the node itself if it survives (telling the parent to keep the connection)
When we delete a node, before returning null
, we check its children. If they exist (aren't null
), they must be surviving nodes that now become new roots, so we add them to our answer list.
The original root is special - if it survives the deletion process, it remains a root and should be added to our answer. This is why we check the final return value of our DFS call on the root.
Using a set for to_delete
values gives us O(1) lookup time, making the deletion check efficient as we traverse through the tree.
Learn more about Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The solution uses a depth-first search (DFS) with post-order traversal to process the tree from bottom to top. Here's how the implementation works:
Data Structure Setup:
- Convert the
to_delete
list into a sets
for O(1) lookup time when checking if a node should be deleted - Initialize an empty list
ans
to collect all the root nodes of the resulting forest
The DFS Function:
The core logic is in the recursive dfs(root)
function that returns either the node itself (if it survives) or None
(if it's deleted):
-
Base Case: If
root
isNone
, returnNone
immediately -
Recursive Processing:
- First recursively call
dfs(root.left)
anddfs(root.right)
- Assign the return values back to
root.left
androot.right
- This ensures children are processed before their parent (post-order)
- This also automatically updates the tree structure - deleted children become
None
- First recursively call
-
Decision Logic:
- Check if
root.val
is in the deletion sets
- If the node should NOT be deleted: Simply return
root
(the node survives with its updated children) - If the node should be deleted:
- Check if
root.left
exists (notNone
) - if yes, it becomes a new root, so add it toans
- Check if
root.right
exists (notNone
) - if yes, it becomes a new root, so add it toans
- Return
None
to signal to the parent that this node is deleted
- Check if
- Check if
Main Function Logic: After defining the DFS function:
- Call
dfs(root)
on the original root - If the return value is not
None
(meaning the original root survived), add it toans
- Return the
ans
list containing all roots of the forest
Why This Works:
- By processing bottom-up, when we delete a node, its children have already been processed and we know exactly which ones survived
- The return value cleverly serves dual purposes: updating parent pointers and signaling node survival
- Children of deleted nodes automatically become roots because they're added to
ans
right before their parent returnsNone
- The tree structure is modified in-place as we traverse, with deleted nodes being disconnected by setting references to
None
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach.
Given:
- Binary tree:
1 / \ 2 3 / \ 4 5
to_delete = [2]
Step-by-step DFS traversal (post-order):
-
Start DFS from root (1)
- First, recursively process left child (2)
-
DFS on node 2
- Recursively process its left child (4)
-
DFS on node 4
- Node 4 has no children, so
dfs(None)
returnsNone
for both - Node 4's value (4) is NOT in
to_delete
- Return node 4 itself
- Node 4 has no children, so
-
Back to node 2, process right child (5)
- Node 5 has no children
- Node 5's value (5) is NOT in
to_delete
- Return node 5 itself
-
Back to node 2 - decision time
root.left = node 4
(from step 3)root.right = node 5
(from step 4)- Node 2's value (2) IS in
to_delete = [2]
- Since node 2 will be deleted:
- Check left child: node 4 exists → add to
ans
(it becomes a new root) - Check right child: node 5 exists → add to
ans
(it becomes a new root)
- Check left child: node 4 exists → add to
- Return
None
(node 2 is deleted)
-
Back to root (1), process right child (3)
- Node 3 has no children
- Node 3's value (3) is NOT in
to_delete
- Return node 3 itself
-
Back to root (1) - final decision
root.left = None
(from step 5, since node 2 was deleted)root.right = node 3
(from step 6)- Node 1's value (1) is NOT in
to_delete
- Return node 1 itself
-
Main function finale
- DFS returned node 1 (not
None
) - Add node 1 to
ans
- Final
ans = [node 4, node 5, node 1]
- DFS returned node 1 (not
Resulting forest:
Tree 1: 1 Tree 2: 4 Tree 3: 5 \ 3
The deletion of node 2 split the original tree into three separate trees with roots 1, 4, and 5.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import Optional, List
9
10class Solution:
11 def delNodes(
12 self, root: Optional[TreeNode], to_delete: List[int]
13 ) -> List[TreeNode]:
14 """
15 Delete nodes from a binary tree and return the forest of remaining trees.
16
17 Args:
18 root: Root of the binary tree
19 to_delete: List of node values to delete
20
21 Returns:
22 List of root nodes of the remaining trees in the forest
23 """
24
25 def dfs(node: Optional[TreeNode]) -> Optional[TreeNode]:
26 """
27 Recursively process the tree, deleting nodes and collecting forest roots.
28
29 Args:
30 node: Current node being processed
31
32 Returns:
33 The node itself if not deleted, None if deleted
34 """
35 # Base case: empty node
36 if node is None:
37 return None
38
39 # Recursively process left and right subtrees first
40 # This ensures we process children before their parents (post-order)
41 node.left = dfs(node.left)
42 node.right = dfs(node.right)
43
44 # If current node should not be deleted, return it
45 if node.val not in to_delete_set:
46 return node
47
48 # Current node needs to be deleted
49 # Add its non-null children as new roots in the forest
50 if node.left:
51 forest_roots.append(node.left)
52 if node.right:
53 forest_roots.append(node.right)
54
55 # Return None to remove this node from its parent's reference
56 return None
57
58 # Convert to_delete list to set for O(1) lookup
59 to_delete_set = set(to_delete)
60
61 # List to store roots of trees in the resulting forest
62 forest_roots = []
63
64 # Process the entire tree and check if root survives
65 # If root is not deleted, it becomes a root in the forest
66 if dfs(root):
67 forest_roots.append(root)
68
69 return forest_roots
70
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 // Boolean array to mark nodes that need to be deleted (indexed by node value)
18 private boolean[] nodesToDelete = new boolean[1001];
19
20 // List to store the roots of remaining forest trees after deletion
21 private List<TreeNode> forestRoots = new ArrayList<>();
22
23 /**
24 * Deletes nodes with values in to_delete array and returns forest of remaining trees
25 * @param root The root of the binary tree
26 * @param to_delete Array containing values of nodes to be deleted
27 * @return List of root nodes of the remaining trees in the forest
28 */
29 public List<TreeNode> delNodes(TreeNode root, int[] to_delete) {
30 // Mark all nodes that need to be deleted
31 for (int nodeValue : to_delete) {
32 nodesToDelete[nodeValue] = true;
33 }
34
35 // Perform DFS traversal and deletion
36 // If root survives deletion, add it to the result
37 if (dfs(root) != null) {
38 forestRoots.add(root);
39 }
40
41 return forestRoots;
42 }
43
44 /**
45 * Performs post-order DFS to delete marked nodes and collect forest roots
46 * @param currentNode The current node being processed
47 * @return The node itself if not deleted, null if deleted
48 */
49 private TreeNode dfs(TreeNode currentNode) {
50 // Base case: null node
51 if (currentNode == null) {
52 return null;
53 }
54
55 // Recursively process left and right subtrees
56 // Update child references (will be null if child was deleted)
57 currentNode.left = dfs(currentNode.left);
58 currentNode.right = dfs(currentNode.right);
59
60 // If current node should not be deleted, return it
61 if (!nodesToDelete[currentNode.val]) {
62 return currentNode;
63 }
64
65 // Current node needs to be deleted
66 // Its non-null children become new roots in the forest
67 if (currentNode.left != null) {
68 forestRoots.add(currentNode.left);
69 }
70 if (currentNode.right != null) {
71 forestRoots.add(currentNode.right);
72 }
73
74 // Return null to indicate this node was deleted
75 return null;
76 }
77}
78
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 vector<TreeNode*> delNodes(TreeNode* root, vector<int>& to_delete) {
15 // Create a boolean array to mark nodes that need to be deleted
16 // Index represents node value (1-1000 based on problem constraints)
17 bool toDeleteSet[1001];
18 memset(toDeleteSet, false, sizeof(toDeleteSet));
19
20 // Mark all nodes that need to be deleted
21 for (int nodeValue : to_delete) {
22 toDeleteSet[nodeValue] = true;
23 }
24
25 // Result vector to store roots of remaining forest trees
26 vector<TreeNode*> forestRoots;
27
28 // DFS function to process the tree
29 // Returns the modified subtree root (nullptr if current node is deleted)
30 function<TreeNode*(TreeNode*)> processTree = [&](TreeNode* currentNode) -> TreeNode* {
31 // Base case: null node
32 if (!currentNode) {
33 return nullptr;
34 }
35
36 // Recursively process left and right subtrees
37 // This updates the children references after deletion
38 currentNode->left = processTree(currentNode->left);
39 currentNode->right = processTree(currentNode->right);
40
41 // If current node should not be deleted, return it as is
42 if (!toDeleteSet[currentNode->val]) {
43 return currentNode;
44 }
45
46 // Current node needs to be deleted
47 // Add non-null children to forest roots as they become new trees
48 if (currentNode->left) {
49 forestRoots.push_back(currentNode->left);
50 }
51 if (currentNode->right) {
52 forestRoots.push_back(currentNode->right);
53 }
54
55 // Return nullptr to indicate this node is deleted
56 return nullptr;
57 };
58
59 // Process the entire tree starting from root
60 TreeNode* processedRoot = processTree(root);
61
62 // If the original root wasn't deleted, add it to the result
63 if (processedRoot) {
64 forestRoots.push_back(processedRoot);
65 }
66
67 return forestRoots;
68 }
69};
70
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Deletes nodes from a binary tree and returns forest of remaining trees
17 * @param root - The root of the binary tree
18 * @param to_delete - Array of node values to delete
19 * @returns Array of root nodes representing the forest after deletion
20 */
21function delNodes(root: TreeNode | null, to_delete: number[]): Array<TreeNode | null> {
22 // Create a boolean array to mark nodes for deletion (assuming node values are in range [1, 1000])
23 const isNodeToDelete: boolean[] = Array(1001).fill(false);
24
25 // Mark all nodes that need to be deleted
26 for (const nodeValue of to_delete) {
27 isNodeToDelete[nodeValue] = true;
28 }
29
30 // Result array to store roots of the forest
31 const forest: Array<TreeNode | null> = [];
32
33 /**
34 * Performs post-order DFS traversal to delete nodes and collect forest roots
35 * @param currentNode - Current node being processed
36 * @returns The node itself if not deleted, null if deleted
37 */
38 const performDFS = (currentNode: TreeNode | null): TreeNode | null => {
39 // Base case: if node is null, return null
40 if (!currentNode) {
41 return null;
42 }
43
44 // Recursively process left and right subtrees
45 currentNode.left = performDFS(currentNode.left);
46 currentNode.right = performDFS(currentNode.right);
47
48 // If current node should not be deleted, return it
49 if (!isNodeToDelete[currentNode.val]) {
50 return currentNode;
51 }
52
53 // Current node needs to be deleted
54 // Add non-null children to forest as they become new roots
55 if (currentNode.left) {
56 forest.push(currentNode.left);
57 }
58 if (currentNode.right) {
59 forest.push(currentNode.right);
60 }
61
62 // Return null to indicate this node is deleted
63 return null;
64 };
65
66 // Process the entire tree starting from root
67 const remainingRoot: TreeNode | null = performDFS(root);
68
69 // If the original root was not deleted, add it to the forest
70 if (remainingRoot) {
71 forest.push(remainingRoot);
72 }
73
74 return forest;
75}
76
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node in the tree is visited exactly once during the traversal. At each node, the following operations are performed:
- Recursive calls to process left and right children
- Checking if the node's value exists in the set
s
(which isO(1)
average case for set lookup) - Potentially appending child nodes to the result list (which is
O(1)
for list append)
Since we visit all n
nodes exactly once and perform constant-time operations at each node, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of several components:
- The set
s
stores the values to delete, which takesO(m)
space wherem
is the size ofto_delete
. In the worst case,m
could beO(n)
. - The recursion call stack can go as deep as the height of the tree. In the worst case (skewed tree), this could be
O(n)
. In the best case (balanced tree), it would beO(log n)
. - The result list
ans
stores the roots of the remaining forest. In the worst case, if we delete all internal nodes and keep only leaf nodes, we could have up toO(n)
trees in the result.
Taking the worst-case scenario for all components, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Add the Original Root to the Result
One of the most common mistakes is forgetting to check if the original root survives the deletion process and should be added to the result list. Many developers focus on collecting the children of deleted nodes as new roots but overlook that the original root itself might still be a valid root.
Incorrect approach:
def delNodes(self, root, to_delete):
to_delete_set = set(to_delete)
forest_roots = []
def dfs(node):
if not node:
return None
node.left = dfs(node.left)
node.right = dfs(node.right)
if node.val in to_delete_set:
if node.left:
forest_roots.append(node.left)
if node.right:
forest_roots.append(node.right)
return None
return node
dfs(root) # ❌ Forgot to check if root survived!
return forest_roots
Solution:
Always check the return value of dfs(root)
. If it's not None
, the original root survived and should be added to the result:
if dfs(root): # ✅ Check if root survived forest_roots.append(root)
2. Adding Children to Result Before They're Fully Processed
Another pitfall is adding children nodes to the result before recursively processing them, which can lead to including nodes that should actually be deleted.
Incorrect approach:
def dfs(node):
if not node:
return None
if node.val in to_delete_set:
# ❌ Adding children before processing them
if node.left:
forest_roots.append(node.left)
if node.right:
forest_roots.append(node.right)
# Then processing children
dfs(node.left)
dfs(node.right)
return None
node.left = dfs(node.left)
node.right = dfs(node.right)
return node
Solution: Always process children first (post-order traversal) and use the return values to determine which children survived:
# ✅ Process children first node.left = dfs(node.left) node.right = dfs(node.right) # Then check if current node should be deleted if node.val in to_delete_set: # Now we know which children survived (non-None) if node.left: forest_roots.append(node.left) if node.right: forest_roots.append(node.right) return None
3. Not Using a Set for to_delete
Lookup
Using the list directly for membership checking leads to O(n) lookup time for each node, making the overall complexity unnecessarily high.
Inefficient approach:
# ❌ O(n) lookup for each node check if node.val in to_delete: # to_delete is a list # deletion logic
Solution: Convert the list to a set at the beginning for O(1) lookup:
# ✅ O(1) lookup
to_delete_set = set(to_delete)
if node.val in to_delete_set:
# deletion logic
What data structure does Breadth-first search typically uses to store intermediate states?
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
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
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!