236 lines
10 KiB
YAML
236 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:
|
|
- slug: dfs
|
|
is_optimal: true
|
|
- slug: tree-traversal
|
|
is_optimal: false
|
|
|
|
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`
|
|
|
|
|
|
|
|
**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)
|
|
|
|
|
|
|
|
**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)
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
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.
|