questions D-E
This commit is contained in:
209
backend/data/questions/delete-leaves-with-a-given-value.yaml
Normal file
209
backend/data/questions/delete-leaves-with-a-given-value.yaml
Normal file
@@ -0,0 +1,209 @@
|
||||
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
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user