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`

    &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.
