Files
codetutor/backend/data/questions/delete-leaves-with-a-given-value.yaml

234 lines
10 KiB
YAML

title: Delete Leaves With a Given Value
slug: delete-leaves-with-a-given-value
difficulty: medium
leetcode_id: 1325
leetcode_url: https://leetcode.com/problems/delete-leaves-with-a-given-value/
categories:
- trees
- recursion
patterns:
- dfs
- tree-traversal
function_signature: "def remove_leaf_nodes(root: TreeNode | None, target: int) -> TreeNode | None:"
test_cases:
visible:
- input: { root: [1, 2, 3, 2, null, 2, 4], target: 2 }
expected: [1, null, 3, null, 4]
- input: { root: [1, 3, 3, 3, 2], target: 3 }
expected: [1, 3, null, null, 2]
- input: { root: [1, 2, null, 2, null, 2], target: 2 }
expected: [1]
hidden:
- input: { root: [2], target: 2 }
expected: []
- input: { root: [1], target: 2 }
expected: [1]
- input: { root: [1, 1, 1], target: 1 }
expected: []
- input: { root: [5, 3, 7, 2, 4, 6, 8], target: 9 }
expected: [5, 3, 7, 2, 4, 6, 8]
- input: { root: [1, 2, 3, 4, 2, 2, 5], target: 2 }
expected: [1, 2, 3, 4, null, null, 5]
- input: { root: [3, 3, 3, 3, 3, 3, 3], target: 3 }
expected: []
description: |
Given a binary tree `root` and an integer `target`, delete all the **leaf nodes** with value `target`.
Note that once you delete a leaf node with value `target`, if its parent node becomes a leaf node and has the value `target`, it should also be deleted (you need to continue doing that until you cannot).
constraints: |
- The number of nodes in the tree is in the range `[1, 3000]`
- `1 <= Node.val, target <= 1000`
examples:
- input: "root = [1,2,3,2,null,2,4], target = 2"
output: "[1,null,3,null,4]"
explanation: "Leaf nodes with value 2 are removed. After removing, new nodes become leaf nodes with value 2 and are also removed."
- input: "root = [1,3,3,3,2], target = 3"
output: "[1,3,null,null,2]"
explanation: "The leaf node with value 3 on the left subtree is removed."
- input: "root = [1,2,null,2,null,2], target = 2"
output: "[1]"
explanation: "Leaf nodes with value 2 are removed at each step, cascading up the tree until only the root remains."
explanation:
intuition: |
Imagine you're pruning a plant by removing dead leaves. When you remove a leaf, you might expose a new leaf underneath that also needs to be removed. This cascading effect is the key insight.
The problem requires **post-order traversal** — we must process children before their parent. Why? Because a node can only become a leaf *after* its children have been removed. If we tried to process nodes top-down (pre-order), we wouldn't know which nodes would eventually become leaves.
Think of it like this: you're cleaning a tree from the bottom up. First, check the deepest leaves and remove any that match the target. Then move up one level — some nodes that had children might now be leaves themselves. Repeat until no more matching leaves exist.
The elegant recursive solution handles this naturally: by recursing to children first and then checking the current node, we automatically process in post-order. If both children return `None` (either they were removed or didn't exist), and the current node's value matches the target, we've found a new leaf to delete.
approach: |
We solve this using **Post-Order DFS (Depth-First Search)**:
**Step 1: Define the recursive function**
- The function takes a node and returns the modified subtree (or `None` if the node should be deleted)
- Base case: if the node is `None`, return `None`
&nbsp;
**Step 2: Recursively process children first**
- Call the function on `node.left` and assign the result back to `node.left`
- Call the function on `node.right` and assign the result back to `node.right`
- This ensures we process the deepest nodes first (post-order)
&nbsp;
**Step 3: Check if the current node should be deleted**
- After processing children, check if this node is now a leaf (`left` and `right` are both `None`)
- If it's a leaf AND its value equals `target`, return `None` to delete it
- Otherwise, return the node (keep it in the tree)
&nbsp;
**Step 4: Handle the root case**
- Apply the function to the root and return the result
- The root itself might be deleted if it becomes a matching leaf
&nbsp;
The post-order traversal ensures cascading deletions happen automatically — when we check a parent, its children have already been processed and potentially removed.
common_pitfalls:
- title: Using Pre-Order Instead of Post-Order
description: |
A common mistake is checking whether to delete a node *before* processing its children:
```python
# Wrong: checking before recursing
if is_leaf(node) and node.val == target:
return None
node.left = recurse(node.left)
node.right = recurse(node.right)
```
This fails because a node might not be a leaf initially, but becomes one after its children are deleted. By checking first, you'd miss these cascading deletions.
For example, with `[1,2,null,2]` and `target = 2`: the inner `2` would be deleted, but then the outer `2` would be missed because it wasn't a leaf when first visited.
wrong_approach: "Check node before recursing to children"
correct_approach: "Recurse to children first, then check node (post-order)"
- title: Not Updating Parent References
description: |
When deleting a node, you must update the parent's reference to that child:
```python
# Wrong: just returning None without updating
recurse(node.left) # Doesn't update the reference!
# Correct: assign the result back
node.left = recurse(node.left)
```
If you don't assign the result back to `node.left` or `node.right`, the deleted nodes remain connected to the tree.
wrong_approach: "Recursing without assignment"
correct_approach: "Assign recursive result back to node.left and node.right"
- title: Forgetting the Root Can Be Deleted
description: |
The root node itself can become a leaf and match the target. For example, `[2]` with `target = 2` should return an empty tree (`None`).
Make sure your function handles this by returning the result of calling the recursive function on the root, not just always returning the original root.
wrong_approach: "Always returning the original root"
correct_approach: "Return the result of the recursive call on root"
key_takeaways:
- "**Post-order for cascading effects**: When deletions can trigger more deletions, process children before parents"
- "**Recursive return value pattern**: Return `None` to signal deletion, return the node to keep it — let the parent handle the assignment"
- "**Tree modification in-place**: Update `node.left` and `node.right` with recursive results to properly disconnect deleted nodes"
- "**Similar problems**: This pattern applies to pruning BSTs, removing subtrees, and any problem where changes propagate upward"
time_complexity: "O(n). We visit each node exactly once during the DFS traversal."
space_complexity: "O(h) where `h` is the height of the tree. This accounts for the recursion stack, which in the worst case (skewed tree) is O(n), but for a balanced tree is O(log n)."
solutions:
- approach_name: Post-Order DFS
is_optimal: true
code: |
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def removeLeafNodes(root: TreeNode, target: int) -> TreeNode:
# Base case: empty node
if not root:
return None
# Process children first (post-order)
# Assign results back to update the tree structure
root.left = removeLeafNodes(root.left, target)
root.right = removeLeafNodes(root.right, target)
# Now check: is this node a leaf with the target value?
is_leaf = root.left is None and root.right is None
if is_leaf and root.val == target:
# Delete this node by returning None
return None
# Keep this node
return root
explanation: |
**Time Complexity:** O(n) — Each node is visited exactly once.
**Space Complexity:** O(h) — Recursion stack depth equals tree height.
The post-order traversal (left, right, then current) ensures we process children before deciding whether to delete the parent. This naturally handles cascading deletions without needing multiple passes.
- approach_name: Iterative with Parent Tracking
is_optimal: false
code: |
def removeLeafNodes(root: TreeNode, target: int) -> TreeNode:
# Use a dummy parent for uniform handling of root deletion
dummy = TreeNode(0)
dummy.left = root
# Stack stores (node, parent, is_left_child)
stack = [(root, dummy, True)]
# Track which nodes we've fully processed
visited = set()
while stack:
node, parent, is_left = stack[-1]
if node is None:
stack.pop()
continue
# If children haven't been visited, push them first
if node not in visited:
visited.add(node)
if node.right:
stack.append((node.right, node, False))
if node.left:
stack.append((node.left, node, True))
else:
# Children processed, now handle this node
stack.pop()
is_leaf = node.left is None and node.right is None
if is_leaf and node.val == target:
# Delete by updating parent reference
if is_left:
parent.left = None
else:
parent.right = None
return dummy.left
explanation: |
**Time Complexity:** O(n) — Each node visited twice (once to push children, once to process).
**Space Complexity:** O(n) — Stack and visited set can hold all nodes.
This iterative approach simulates post-order traversal using a stack and a visited set. It's more complex than the recursive solution and uses more space. The dummy node simplifies handling root deletion. Included to show how post-order can be done iteratively, but the recursive solution is cleaner.