Facebook Pixel

1325. Delete Leaves With a Given Value

Problem Description

You are given a binary tree and an integer target. Your task is to delete all leaf nodes that have a value equal to target.

The key challenge is that this deletion process should be recursive in nature. When you delete a leaf node with value target, its parent node might become a new leaf node. If this newly created leaf node also has value target, it should be deleted as well. This process continues until no more leaf nodes with value target can be deleted.

For example, consider a tree where a parent node has value target and two children that are both leaf nodes with value target. First, you would delete both children. After deleting them, the parent becomes a leaf node. Since the parent also has value target and is now a leaf, it should be deleted too.

The function should return the root of the modified tree after all possible deletions. If the entire tree gets deleted (including the root), the function should return None.

A leaf node is defined as a node that has no left child and no right child.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure.

DFS

  • Yes: We arrive at DFS (Depth-First Search) as our algorithm choice.

Conclusion: The flowchart suggests using a DFS approach for this problem. This makes perfect sense because:

  1. We need to traverse the entire tree to identify and remove leaf nodes with the target value
  2. The deletion process needs to work from the bottom up - we must first process children before determining if a parent becomes a leaf node
  3. DFS naturally processes nodes in a post-order fashion (left subtree → right subtree → current node), which is exactly what we need for this cascading deletion problem
  4. After processing both children of a node, we can determine if that node itself has become a leaf and should be deleted

The DFS pattern allows us to recursively visit each node, process its children first, and then make decisions about the current node based on the updated state of its children. This bottom-up approach ensures that we correctly handle the cascading nature of the deletions where removing leaves can create new leaves that also need to be removed.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight for solving this problem is recognizing that we need to work from the bottom of the tree upward. Why? Because deleting leaf nodes can cause their parents to become new leaf nodes, which might also need deletion.

Think about it this way: if we tried to delete nodes from top to bottom, we wouldn't know whether a parent node will become a leaf until after we've processed its children. This creates a dependency - we can't make a decision about a parent until we know what happens to its children.

This naturally leads us to a post-order traversal pattern where we:

  1. First process the left subtree completely
  2. Then process the right subtree completely
  3. Finally make a decision about the current node

By the time we reach a node, both its subtrees have been fully processed and cleaned up. At this point, we can check: "Is this node now a leaf (both children are None)? And does it have the target value?" If yes, we return None to effectively delete it. Otherwise, we keep the node.

The recursive nature of DFS perfectly captures this bottom-up processing. Each recursive call handles a subtree and returns either the modified subtree or None if the entire subtree should be deleted. The parent then uses these returned values to update its children pointers.

The beauty of this approach is that it handles the cascading deletions automatically. When a node's children are deleted (returning None), that node immediately knows it's now a leaf and can check if it should also be deleted. This ripple effect continues up the tree until we reach nodes that either don't have the target value or aren't leaves after cleanup.

Learn more about Tree, Depth-First Search and Binary Tree patterns.

Solution Approach

The implementation uses a recursive DFS approach with post-order traversal. Let's walk through how the solution works:

Base Case:

if root is None:
    return None

If we encounter a None node (empty subtree), we simply return None. This handles the case where we've reached beyond leaf nodes.

Recursive Processing:

root.left = self.removeLeafNodes(root.left, target)
root.right = self.removeLeafNodes(root.right, target)

These two lines are the heart of the post-order traversal. We first recursively process the left subtree, then the right subtree. The key insight here is that we're not just traversing - we're also updating the child pointers. If a subtree gets completely deleted (returns None), the parent's pointer to that child becomes None.

Decision Logic:

if root.left is None and root.right is None and root.val == target:
    return None
return root

After processing both children, we check if the current node should be deleted. A node is deleted if:

  • It has no left child (root.left is None)
  • It has no right child (root.right is None)
  • Its value equals the target (root.val == target)

If all three conditions are met, we return None to signal this node should be deleted. Otherwise, we return the node itself.

How Cascading Deletion Works:

Consider a small example where node A (value = target) has two children B and C (both leaves with value = target):

  1. When we process node A, we first recursively process B
  2. B is a leaf with target value, so it returns None
  3. We then process C, which also returns None
  4. Now root.left and root.right for A are both None
  5. A is now a leaf! If A's value equals target, it also returns None

The algorithm elegantly handles the cascading nature by updating parent-child relationships as it unwinds the recursion stack. Each level of recursion makes its deletion decision based on the already-processed results from deeper levels.

Time Complexity: O(n) where n is the number of nodes, as we visit each node exactly once.

Space Complexity: O(h) where h is the height of the tree, due to the recursion call stack.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to see how the algorithm works. Consider this binary tree where we want to delete all leaf nodes with target = 2:

Initial Tree:
      1
     / \
    2   2
   /   
  2    

Target: 2

Step-by-step execution:

1. Start at root (value=1)

  • First, recursively process left child (value=2)

2. Process left subtree rooted at node (value=2)

  • Recursively process its left child (value=2, leaf)
  • Since this is a leaf with value=2, return None
  • Recursively process its right child (doesn't exist, returns None)
  • After processing: left=None, right=None
  • This node is now a leaf with value=2, so return None

3. Back at root (value=1)

  • Left subtree processing complete, root.left becomes None
  • Now recursively process right child (value=2)

4. Process right subtree rooted at node (value=2)

  • Left child doesn't exist (returns None)
  • Right child doesn't exist (returns None)
  • This is a leaf with value=2, so return None

5. Back at root (value=1) again

  • Right subtree processing complete, root.right becomes None
  • Check if root should be deleted:
    • Is it a leaf? Yes (both children are None)
    • Does it have target value? No (1 ≠ 2)
  • Return the root node

Final Tree:

  1

The algorithm successfully deleted all nodes with value 2. Notice how the deletion of the leftmost leaf (2) caused its parent (also 2) to become a leaf, which was then deleted in the cascading manner. The root survived because although it became a leaf, its value (1) didn't match the target.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8from typing import Optional
9
10
11class Solution:
12    def removeLeafNodes(
13        self, root: Optional[TreeNode], target: int
14    ) -> Optional[TreeNode]:
15        """
16        Remove all leaf nodes with value equal to target from the binary tree.
17        Continue removing until no more leaf nodes with target value exist.
18      
19        Args:
20            root: The root node of the binary tree
21            target: The target value to remove from leaf nodes
22          
23        Returns:
24            The root of the modified tree, or None if entire tree is removed
25        """
26        # Base case: if the current node is None, return None
27        if root is None:
28            return None
29      
30        # Recursively process left subtree first
31        # This ensures we process children before parents (post-order traversal)
32        root.left = self.removeLeafNodes(root.left, target)
33      
34        # Recursively process right subtree
35        root.right = self.removeLeafNodes(root.right, target)
36      
37        # After processing both subtrees, check if current node becomes a leaf
38        # If it's a leaf node with target value, remove it by returning None
39        if root.left is None and root.right is None and root.val == target:
40            return None
41      
42        # Otherwise, keep the current node and return it
43        return root
44
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    /**
18     * Removes all leaf nodes with the specified target value from the binary tree.
19     * Uses post-order traversal to process children before parents, ensuring
20     * newly created leaf nodes are also evaluated for removal.
21     * 
22     * @param root   The root of the binary tree
23     * @param target The target value to match for leaf node removal
24     * @return The root of the modified tree, or null if the entire tree is removed
25     */
26    public TreeNode removeLeafNodes(TreeNode root, int target) {
27        // Base case: if node is null, return null
28        if (root == null) {
29            return null;
30        }
31      
32        // Recursively process left subtree first
33        // This may return null if the entire left subtree is removed
34        root.left = removeLeafNodes(root.left, target);
35      
36        // Recursively process right subtree
37        // This may return null if the entire right subtree is removed
38        root.right = removeLeafNodes(root.right, target);
39      
40        // After processing both children, check if current node has become a leaf
41        // If it's a leaf node with target value, remove it by returning null
42        if (root.left == null && root.right == null && root.val == target) {
43            return null;
44        }
45      
46        // Otherwise, keep the current node and return it
47        return root;
48    }
49}
50
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    /**
15     * Removes all leaf nodes with the specified target value from the binary tree.
16     * Uses post-order traversal to ensure children are processed before parents.
17     * 
18     * @param root Pointer to the root of the binary tree
19     * @param target The target value to match for leaf node removal
20     * @return Pointer to the root of the modified tree (may be nullptr if entire tree is removed)
21     */
22    TreeNode* removeLeafNodes(TreeNode* root, int target) {
23        // Base case: if the node is null, return null
24        if (root == nullptr) {
25            return nullptr;
26        }
27      
28        // Recursively process the left subtree first
29        root->left = removeLeafNodes(root->left, target);
30      
31        // Recursively process the right subtree
32        root->right = removeLeafNodes(root->right, target);
33      
34        // After processing children, check if current node has become a leaf
35        // If it's a leaf node with the target value, remove it by returning null
36        if (root->left == nullptr && root->right == nullptr && root->val == target) {
37            return nullptr;
38        }
39      
40        // Otherwise, return the current node
41        return root;
42    }
43};
44
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Removes all leaf nodes with the target value from a binary tree.
17 * Uses post-order traversal to process children before their parent.
18 * 
19 * @param root - The root node of the binary tree
20 * @param target - The target value to remove from leaf nodes
21 * @returns The root of the modified tree, or null if the entire tree is removed
22 */
23function removeLeafNodes(root: TreeNode | null, target: number): TreeNode | null {
24    // Base case: if the node is null, return null
25    if (!root) {
26        return null;
27    }
28  
29    // Recursively process the left subtree first
30    root.left = removeLeafNodes(root.left, target);
31  
32    // Recursively process the right subtree
33    root.right = removeLeafNodes(root.right, target);
34  
35    // After processing children, check if current node is a leaf with target value
36    // If both children are null and the value equals target, remove this node
37    if (!root.left && !root.right && root.val === target) {
38        return null;
39    }
40  
41    // Otherwise, return the current node
42    return root;
43}
44

Time and Space Complexity

Time Complexity: O(n) where n is the number of nodes in the binary tree. The algorithm performs a post-order traversal of the tree, visiting each node exactly once. At each node, it performs constant time operations (checking if it's a leaf node and comparing its value with the target), so the overall time complexity is linear with respect to the number of nodes.

Space Complexity: O(h) where h is the height of the binary tree. The space complexity is determined by the recursive call stack. In the worst case of a skewed tree (essentially a linked list), the height would be n, making the space complexity O(n). In the best case of a perfectly balanced tree, the height would be log(n), making the space complexity O(log n). The algorithm doesn't use any additional data structures besides the implicit call stack for recursion.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Attempting Pre-order or In-order Traversal

The Problem: A common mistake is trying to solve this problem using pre-order traversal (processing the current node before its children). This approach fails because you might delete a leaf node before realizing its parent should also be deleted.

Incorrect Approach:

def removeLeafNodes(self, root, target):
    if root is None:
        return None
  
    # WRONG: Checking current node first
    if root.left is None and root.right is None and root.val == target:
        return None
  
    # Processing children after parent - won't catch cascading deletions
    root.left = self.removeLeafNodes(root.left, target)
    root.right = self.removeLeafNodes(root.right, target)
  
    return root

This fails because when you check if a node is a leaf, its children haven't been processed yet. A node that appears to have children initially might become a leaf after those children are deleted.

Solution: Always use post-order traversal - process children first, then make decisions about the current node based on the updated state.

Pitfall 2: Not Updating Parent Pointers

The Problem: Some implementations correctly identify which nodes should be deleted but forget to update the parent's pointers to the deleted children.

Incorrect Approach:

def removeLeafNodes(self, root, target):
    if root is None:
        return None
  
    # WRONG: Just calling recursively without updating pointers
    self.removeLeafNodes(root.left, target)
    self.removeLeafNodes(root.right, target)
  
    if root.left is None and root.right is None and root.val == target:
        return None
  
    return root

Solution: Always assign the return value of recursive calls back to the child pointers:

root.left = self.removeLeafNodes(root.left, target)
root.right = self.removeLeafNodes(root.right, target)

Pitfall 3: Checking Original Children State Instead of Updated State

The Problem: Storing references to children before recursion and using those stored references for the leaf check.

Incorrect Approach:

def removeLeafNodes(self, root, target):
    if root is None:
        return None
  
    # WRONG: Storing original children
    left_child = root.left
    right_child = root.right
  
    root.left = self.removeLeafNodes(root.left, target)
    root.right = self.removeLeafNodes(root.right, target)
  
    # WRONG: Using original children for leaf check
    if left_child is None and right_child is None and root.val == target:
        return None
  
    return root

This incorrectly uses the original state of children to determine if the node is a leaf, missing cases where the node becomes a leaf after children are deleted.

Solution: Always check the current state of root.left and root.right after the recursive calls have updated them.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement priority queue?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More