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.