Builds on Bottom-Up DFS, which you haven’t finished yet.
Rewire Tree (Return the Node That Stays)
A binary tree is given by its root. Every node holds a different value. A list to_delete holds values. Delete every node whose value is in to_delete.
A node that is deleted takes its links with it. Its children are not deleted, but they have no parent now, so each of them is the top of a separate tree. After all deletions, the nodes that remain form several separate trees. This group of trees is called a forest. Return the root of every tree in the forest, in any order.
Rewire Tree
tree + delete rule ➔ each call returns the node that stays (or None) ➔ parent stores it as its child| 1 | from typing import List, Optional |
| 2 | |
| 3 | class TreeNode: |
| 4 | def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None): |
| 5 | self.val = val |
| 6 | self.left = left |
| 7 | self.right = right |
| 8 | |
| 9 | def delNodes(root: Optional[TreeNode], to_delete: List[int]) -> List[Optional[TreeNode]]: |
| 10 | """Delete Nodes And Return Forest: delete every node whose value is in to_delete and |
| 11 | return the roots of the trees that remain. |
| 12 | N = number of nodes. M = len(to_delete). H = height. |
| 13 | Time: O(N + M) | Space: O(H + M) recursion stack and the set of deleted values, output not counted |
| 14 | """ |
| 15 | doomed = set(to_delete) # A set makes each test "is this value deleted?" O(1) |
| 16 | forest: List[Optional[TreeNode]] = [] # The root of every tree that remains |
| 17 | |
| 18 | def rewire(node: Optional[TreeNode]) -> Optional[TreeNode]: |
| 19 | """Returns the node that stays in node's place after every deletion in its |
| 20 | subtree, or None if node itself is deleted.""" |
| 21 | if not node: |
| 22 | return None # An empty place stays empty |
| 23 | # 1. Rewire Both Subtrees First: |
| 24 | # Postorder: each child returns the node that stays in its place, and node |
| 25 | # stores it, so a deleted child stops being node's child. |
| 26 | node.left = rewire(node.left) |
| 27 | node.right = rewire(node.right) |
| 28 | |
| 29 | # 2. Keep The Node: |
| 30 | # A node that is not deleted stays where it is, with its children already |
| 31 | # rewired. |
| 32 | if node.val not in doomed: |
| 33 | return node |
| 34 | |
| 35 | # 3. Delete The Node: |
| 36 | # A deleted node loses its parent link, so each child that stays is the root of |
| 37 | # a new tree. Both children were rewired in step 1, so neither is deleted. |
| 38 | if node.left: |
| 39 | forest.append(node.left) |
| 40 | if node.right: |
| 41 | forest.append(node.right) |
| 42 | return None |
| 43 | |
| 44 | # 4. Keep The Root If It Stays: |
| 45 | # The root has no parent to store the return value, so this function keeps it when |
| 46 | # rewire(root) returns a node. |
| 47 | root = rewire(root) |
| 48 | if root: |
| 49 | forest.append(root) |
| 50 | return forest |
rewire(node) returns node when it stays and None when it is deleted, and every pointer below node already points only at nodes that stay.A deleted node must also disappear from its parent's pointer, so let rewire(node) return the node that stays in that place, or None, and let the parent store it: node.left = rewire(node.left). Both children are rewired before node is checked, so when node is deleted, node.left and node.right are already final, and each one that exists is the root of a new tree for forest. A node that is kept returns itself. The root has no parent to store the result, so delNodes appends it to forest when rewire(root) returns a node. The trap is calling rewire(node.left) without storing the result: the deleted child stays attached to its parent, and the tree still contains a value that should be gone.
See it run
Rewire Tree, step by step
call now 1returns -answer not yet
Each call returns the node that stays in its place, or None. The parent stores the return value as its child.
nowthe call that is runningwaitinga call that waits for its childrenstaysa node whose call returned the nodedeleteda node whose call returned None
Call rewire on the node 1. The call waits for its two children before it checks the node.
Gotcha 1 of 4
- The trap
Gotcha 1: Calling
rewire(node.left)andrewire(node.right)without storing the resultsMistakerewire(node.left) rewire(node.right)Fixnode.left = rewire(node.left) node.right = rewire(node.right)What happens:
rewire(node.left)returnsNonewhen the left child is deleted, but the call does not changenode.left. If the result is not stored,nodestill points at the deleted child. In[1,2,4,null,3]withto_delete = [3], node 2 still points at node 3, so the tree keeps the value 3 that should be gone.Example: root = [1,2,4,null,3], to_delete = [3] (each tree is shown as its level-order list)Mistake: returns [[1, 2, 4, None, 3]]Fix: returns [[1, 2, 4]]
- The tree is a binary search tree and the rule is a range of values, as in Trim a Binary Search Tree (LC 669): the order tells you which whole side to drop without visiting it. Switch to BST Bounds. See BST Bounds
- You build one tree from two input trees, as in Merge Two Binary Trees (LC 617): each call needs a pair of nodes, not one node. Switch to Two Tree Walk. See Two Tree Walk
Deep Dive
Why the parent must store what the call returns
A tree keeps each child as a pointer in its parent. To delete a node, the parent's pointer must stop pointing at it. A call that only looks at the tree cannot change that pointer. A call that returns a node can: rewire(node) returns the node that stays in that place, and it returns None when the node is deleted.
The parent stores the answer with node.left = rewire(node.left). Take the tree [1, 2, 3, 4, 5, 6, 7] with to_delete = [3, 5]. The call on the node 5 returns None, and the node 2 stores it, so the node 2 has no right child. The call on the node 3 returns None, and the node 1 stores it, so the node 1 has no right child.
Without the assignment, the node 2 would still point at the node 5 and the node 1 would still point at the node 3. The tree would keep the values that should be gone.
Why both children are rewired before the node is checked
A deleted node cannot take its children with it. Each child that stays has lost its parent, so it is the root of a new tree. The deleted node must know which of its children stay, and that is known only after the calls on the children have returned.
So the walk is postorder. In the example, the calls on the nodes 6 and 7 return both nodes. When the call on the node 3 sees that 3 is deleted, node.left is the node 6 and node.right is the node 7, and both go to forest.
A child of a deleted node can be deleted too. Because the children were rewired first, such a child already returned None, so node.left is None and it is not added. Adding the children before the calls run would add a deleted node to forest.
Delete Leaves With a Given Value: the rule reads the rewired children
Delete Leaves With a Given Value (LC 1325) uses the same walk with another rule. A node is deleted when it is a leaf that holds target. After the two assignments, a node whose children were both removed has no children, so it is a leaf now.
That is why the rule is checked after the children are rewired. On [1, 2, null, 2, null, 2] with target = 2, the deepest node is removed first. Its parent becomes a leaf with the value 2 and is removed next, and so on up the chain. The answer is [1].
Binary Tree Pruning (LC 814) works the same way: a node is removed when it holds 0 and has no children after its subtrees were pruned. Delete Node in a BST (LC 450) returns the node that replaces the deleted node, and the parent stores it.
The cost, and the root
Each node gets one call, and each missing child gets one call that returns at once. A call does a set lookup and at most two appends, so the time is O(N + M), where M is the length of to_delete. The recursion stack is as deep as the tree is high, so the space is O(H + M) with the set.
The root has no parent to store the return value. delNodes therefore writes root = rewire(root) and adds the root to forest when it is not None. Without that, a root that stays is missing from the answer.
60-Second Pitch
"To delete a node, its parent must stop pointing at it, so I write rewire(node) to return the node that stays in that place, or None. The parent stores the answer: node.left = rewire(node.left). I rewire both children first. Then a node that is not deleted returns itself, and a deleted node sends each child that stays to the forest and returns None. The root has no parent, so I add it myself when it stays. Each node is visited once, so the time is O(N + M) and the stack is as deep as the tree."
Common Questions
- How is Rewire Tree different from Bottom-Up DFS?
Bottom-Up DFS returns a number or a flag that describes a subtree. Rewire Tree returns the node itself, and the parent stores it as its child, so the call changes the shape of the tree.
- Why does the root need special handling?
Every other node has a parent that stores the return value. The root has none, so
delNodesstores the result inrootand adds it toforestif it is notNone.- What happens if a deleted node has a deleted child?
The child was rewired first and returned
None, so the deleted node seesNonein that place and does not add it toforest.- Why use a set for
to_delete? A set lookup is
O(1). A list lookup reads up to M values for every node and makes the walk O(N * M).
SWE Reasoning
Spot it
- The statement deletes, removes or cuts nodes of a tree by a rule.
- The answer is the changed tree, or the roots of the pieces that remain.
- Whether a node is removed can depend on what is left below it after its own children were changed.
- You can say in one sentence what
rewire(node)returns: the node that stays in that place, orNone.
The invariant, step by step
- Define
rewire(node)as the node that stays in node's place after every deletion in its subtree, orNone. - Rewire both children and store the results. After that, no pointer below
nodeleads to a deleted node. - If the rule keeps
node, return it. If the rule removes it, each child that stays lost its parent and goes toforest, and the call returnsNone. - The root has no parent, so
delNodesstores the result inrootand adds it toforestwhen it exists.
Traps
- Not storing the result.
rewire(node.left)alone leaves a deleted child attached to its parent. - Forgetting the root. A root that stays has no deleted parent to add it to
forest. - Adding a missing child. A deleted node can have one child. Test
if node.left:beforeforest.append(node.left). - A list for the deleted values. A set makes each test
O(1).
Practice
Builds on Bottom-Up DFS, which you haven’t finished yet.
Rewire Tree (Return the Node That Stays)
A binary tree is given by its root. Every node holds a different value. A list to_delete holds values. Delete every node whose value is in to_delete.
A node that is deleted takes its links with it. Its children are not deleted, but they have no parent now, so each of them is the top of a separate tree. After all deletions, the nodes that remain form several separate trees. This group of trees is called a forest. Return the root of every tree in the forest, in any order.
Rewire Tree
tree + delete rule ➔ each call returns the node that stays (or None) ➔ parent stores it as its child| 1 | from typing import List, Optional |
| 2 | |
| 3 | class TreeNode: |
| 4 | def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None): |
| 5 | self.val = val |
| 6 | self.left = left |
| 7 | self.right = right |
| 8 | |
| 9 | def delNodes(root: Optional[TreeNode], to_delete: List[int]) -> List[Optional[TreeNode]]: |
| 10 | """Delete Nodes And Return Forest: delete every node whose value is in to_delete and |
| 11 | return the roots of the trees that remain. |
| 12 | N = number of nodes. M = len(to_delete). H = height. |
| 13 | Time: O(N + M) | Space: O(H + M) recursion stack and the set of deleted values, output not counted |
| 14 | """ |
| 15 | doomed = set(to_delete) # A set makes each test "is this value deleted?" O(1) |
| 16 | forest: List[Optional[TreeNode]] = [] # The root of every tree that remains |
| 17 | |
| 18 | def rewire(node: Optional[TreeNode]) -> Optional[TreeNode]: |
| 19 | """Returns the node that stays in node's place after every deletion in its |
| 20 | subtree, or None if node itself is deleted.""" |
| 21 | if not node: |
| 22 | return None # An empty place stays empty |
| 23 | # 1. Rewire Both Subtrees First: |
| 24 | # Postorder: each child returns the node that stays in its place, and node |
| 25 | # stores it, so a deleted child stops being node's child. |
| 26 | node.left = rewire(node.left) |
| 27 | node.right = rewire(node.right) |
| 28 | |
| 29 | # 2. Keep The Node: |
| 30 | # A node that is not deleted stays where it is, with its children already |
| 31 | # rewired. |
| 32 | if node.val not in doomed: |
| 33 | return node |
| 34 | |
| 35 | # 3. Delete The Node: |
| 36 | # A deleted node loses its parent link, so each child that stays is the root of |
| 37 | # a new tree. Both children were rewired in step 1, so neither is deleted. |
| 38 | if node.left: |
| 39 | forest.append(node.left) |
| 40 | if node.right: |
| 41 | forest.append(node.right) |
| 42 | return None |
| 43 | |
| 44 | # 4. Keep The Root If It Stays: |
| 45 | # The root has no parent to store the return value, so this function keeps it when |
| 46 | # rewire(root) returns a node. |
| 47 | root = rewire(root) |
| 48 | if root: |
| 49 | forest.append(root) |
| 50 | return forest |
rewire(node) returns node when it stays and None when it is deleted, and every pointer below node already points only at nodes that stay.A deleted node must also disappear from its parent's pointer, so let rewire(node) return the node that stays in that place, or None, and let the parent store it: node.left = rewire(node.left). Both children are rewired before node is checked, so when node is deleted, node.left and node.right are already final, and each one that exists is the root of a new tree for forest. A node that is kept returns itself. The root has no parent to store the result, so delNodes appends it to forest when rewire(root) returns a node. The trap is calling rewire(node.left) without storing the result: the deleted child stays attached to its parent, and the tree still contains a value that should be gone.
See it run
Rewire Tree, step by step
call now 1returns -answer not yet
Each call returns the node that stays in its place, or None. The parent stores the return value as its child.
nowthe call that is runningwaitinga call that waits for its childrenstaysa node whose call returned the nodedeleteda node whose call returned None
Call rewire on the node 1. The call waits for its two children before it checks the node.
Gotcha 1 of 4
- The trap
Gotcha 1: Calling
rewire(node.left)andrewire(node.right)without storing the resultsMistakerewire(node.left) rewire(node.right)Fixnode.left = rewire(node.left) node.right = rewire(node.right)What happens:
rewire(node.left)returnsNonewhen the left child is deleted, but the call does not changenode.left. If the result is not stored,nodestill points at the deleted child. In[1,2,4,null,3]withto_delete = [3], node 2 still points at node 3, so the tree keeps the value 3 that should be gone.Example: root = [1,2,4,null,3], to_delete = [3] (each tree is shown as its level-order list)Mistake: returns [[1, 2, 4, None, 3]]Fix: returns [[1, 2, 4]]
- The tree is a binary search tree and the rule is a range of values, as in Trim a Binary Search Tree (LC 669): the order tells you which whole side to drop without visiting it. Switch to BST Bounds. See BST Bounds
- You build one tree from two input trees, as in Merge Two Binary Trees (LC 617): each call needs a pair of nodes, not one node. Switch to Two Tree Walk. See Two Tree Walk
Deep Dive
Why the parent must store what the call returns
A tree keeps each child as a pointer in its parent. To delete a node, the parent's pointer must stop pointing at it. A call that only looks at the tree cannot change that pointer. A call that returns a node can: rewire(node) returns the node that stays in that place, and it returns None when the node is deleted.
The parent stores the answer with node.left = rewire(node.left). Take the tree [1, 2, 3, 4, 5, 6, 7] with to_delete = [3, 5]. The call on the node 5 returns None, and the node 2 stores it, so the node 2 has no right child. The call on the node 3 returns None, and the node 1 stores it, so the node 1 has no right child.
Without the assignment, the node 2 would still point at the node 5 and the node 1 would still point at the node 3. The tree would keep the values that should be gone.
Why both children are rewired before the node is checked
A deleted node cannot take its children with it. Each child that stays has lost its parent, so it is the root of a new tree. The deleted node must know which of its children stay, and that is known only after the calls on the children have returned.
So the walk is postorder. In the example, the calls on the nodes 6 and 7 return both nodes. When the call on the node 3 sees that 3 is deleted, node.left is the node 6 and node.right is the node 7, and both go to forest.
A child of a deleted node can be deleted too. Because the children were rewired first, such a child already returned None, so node.left is None and it is not added. Adding the children before the calls run would add a deleted node to forest.
Delete Leaves With a Given Value: the rule reads the rewired children
Delete Leaves With a Given Value (LC 1325) uses the same walk with another rule. A node is deleted when it is a leaf that holds target. After the two assignments, a node whose children were both removed has no children, so it is a leaf now.
That is why the rule is checked after the children are rewired. On [1, 2, null, 2, null, 2] with target = 2, the deepest node is removed first. Its parent becomes a leaf with the value 2 and is removed next, and so on up the chain. The answer is [1].
Binary Tree Pruning (LC 814) works the same way: a node is removed when it holds 0 and has no children after its subtrees were pruned. Delete Node in a BST (LC 450) returns the node that replaces the deleted node, and the parent stores it.
The cost, and the root
Each node gets one call, and each missing child gets one call that returns at once. A call does a set lookup and at most two appends, so the time is O(N + M), where M is the length of to_delete. The recursion stack is as deep as the tree is high, so the space is O(H + M) with the set.
The root has no parent to store the return value. delNodes therefore writes root = rewire(root) and adds the root to forest when it is not None. Without that, a root that stays is missing from the answer.
60-Second Pitch
"To delete a node, its parent must stop pointing at it, so I write rewire(node) to return the node that stays in that place, or None. The parent stores the answer: node.left = rewire(node.left). I rewire both children first. Then a node that is not deleted returns itself, and a deleted node sends each child that stays to the forest and returns None. The root has no parent, so I add it myself when it stays. Each node is visited once, so the time is O(N + M) and the stack is as deep as the tree."
Common Questions
- How is Rewire Tree different from Bottom-Up DFS?
Bottom-Up DFS returns a number or a flag that describes a subtree. Rewire Tree returns the node itself, and the parent stores it as its child, so the call changes the shape of the tree.
- Why does the root need special handling?
Every other node has a parent that stores the return value. The root has none, so
delNodesstores the result inrootand adds it toforestif it is notNone.- What happens if a deleted node has a deleted child?
The child was rewired first and returned
None, so the deleted node seesNonein that place and does not add it toforest.- Why use a set for
to_delete? A set lookup is
O(1). A list lookup reads up to M values for every node and makes the walk O(N * M).
SWE Reasoning
Spot it
- The statement deletes, removes or cuts nodes of a tree by a rule.
- The answer is the changed tree, or the roots of the pieces that remain.
- Whether a node is removed can depend on what is left below it after its own children were changed.
- You can say in one sentence what
rewire(node)returns: the node that stays in that place, orNone.
The invariant, step by step
- Define
rewire(node)as the node that stays in node's place after every deletion in its subtree, orNone. - Rewire both children and store the results. After that, no pointer below
nodeleads to a deleted node. - If the rule keeps
node, return it. If the rule removes it, each child that stays lost its parent and goes toforest, and the call returnsNone. - The root has no parent, so
delNodesstores the result inrootand adds it toforestwhen it exists.
Traps
- Not storing the result.
rewire(node.left)alone leaves a deleted child attached to its parent. - Forgetting the root. A root that stays has no deleted parent to add it to
forest. - Adding a missing child. A deleted node can have one child. Test
if node.left:beforeforest.append(node.left). - A list for the deleted values. A set makes each test
O(1).