814. Binary Tree Pruning
Problem Description
You are given the root
of a binary tree. Your task is to remove all subtrees that don't contain the value 1
.
A subtree consists of a node and all of its descendants. If a subtree doesn't have any node with value 1
, it should be completely removed from the tree.
The key points to understand:
- A subtree includes a node plus every node that descends from it
- Any subtree that doesn't contain at least one
1
should be pruned (removed) - After pruning, you should return the modified tree
For example, if you have a node with value 0
and all its descendants also have value 0
, then this entire subtree should be removed since it contains no 1
.
The solution works recursively from bottom to top:
- First, it processes the left and right subtrees by recursively pruning them
- After pruning children, it checks if the current node should be removed
- A node is removed if its value is
0
and it has no remaining children (both left and right arenull
) - The condition
root.left == root.right
cleverly checks if both children arenull
(sincenull == null
istrue
)
This bottom-up approach ensures that we first clean up the descendants before deciding whether to keep the current node, effectively removing all subtrees that don't contain any 1
.
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
- We arrive at DFS (Depth-First Search) as the recommended approach.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this binary tree pruning problem.
This makes perfect sense because:
- We need to traverse the entire tree to check every subtree
- We need to process children before deciding whether to keep or remove a parent node (post-order traversal)
- DFS naturally handles the recursive nature of checking each subtree from bottom to top
- The problem requires examining all descendants of a node before making a decision about that node, which aligns perfectly with DFS's recursive traversal pattern
The solution implements DFS through recursion, where we:
- Recursively process left and right subtrees first
- Make decisions about the current node based on its value and the results from its children
- Return
None
to prune a subtree or return the node to keep it
Intuition
The key insight is that we need to make decisions about parent nodes based on information from their children. This naturally suggests a bottom-up approach where we process leaves first and work our way up to the root.
Think about it this way: How can we know if a subtree contains a 1
? We need to check:
- Does the current node have value
1
? - Does any node in the left subtree have value
1
? - Does any node in the right subtree have value
1
?
If the answer to all three questions is "no", then we should remove this entire subtree.
This leads us to a recursive strategy where we first prune the children, then decide about the current node. By processing children first (post-order traversal), we ensure that when we examine a node, its children have already been cleaned up.
The clever part of the solution is recognizing that after pruning:
- If a child subtree doesn't contain any
1
, it becomesNone
- A node with value
0
and both children asNone
should also be removed - The condition
root.left == root.right
elegantly checks if both areNone
This way, the pruning propagates upward: if we remove all descendants of a node with value 0
, that node itself becomes a leaf with value 0
, which should also be removed. The recursion naturally handles this cascading effect, pruning entire branches that don't contain any 1
values.
Learn more about Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The solution implements a recursive DFS approach with post-order traversal. Let's walk through the implementation step by step:
Base Case:
if root is None: return root
If we encounter a None
node (empty tree or reached beyond a leaf), we simply return None
. This handles the edge case and serves as our recursion termination condition.
Recursive Pruning:
root.left = self.pruneTree(root.left) root.right = self.pruneTree(root.right)
We recursively prune both subtrees first. This is crucial because we need to know the final state of the children before deciding about the current node. The pruned subtrees are reassigned to the current node's left and right pointers.
Decision Logic:
if root.val == 0 and root.left == root.right: return None return root
After processing children, we check if the current node should be removed:
root.val == 0
: The node has value0
root.left == root.right
: Both children areNone
(sinceNone == None
evaluates toTrue
)
If both conditions are met, the node is a leaf with value 0
(or became a leaf after pruning its children), so we return None
to remove it. Otherwise, we return the node itself.
Why This Works:
The post-order traversal ensures that we process nodes from bottom to top. When a subtree doesn't contain any 1
:
- All its leaves with value
0
are removed first - This makes their parents become new leaves
- If these parents also have value
0
, they get removed too - This cascades upward until we either reach a node with value
1
or remove the entire subtree
The algorithm runs in O(n)
time where n
is the number of nodes, as we visit each node exactly once. The space complexity is 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 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the pruning algorithm works:
Initial Tree:
0 / \ 0 1 / \ 0 0
We'll trace through the recursive calls step by step:
Step 1: Start at root (value 0)
- Before deciding about the root, we need to prune its children
- Recursively call
pruneTree(root.left)
where left child has value 0
Step 2: Process left subtree (value 0)
- Before deciding about this node, prune its children
- Recursively call on its left child (value 0, leaf)
Step 3: Process leftmost leaf (value 0)
- Base case: no children to process
- Check:
val == 0
✓ andleft == right
(both None) ✓ - Return
None
(remove this node)
Step 4: Back to left subtree, process its right child (value 0, leaf)
- Base case: no children to process
- Check:
val == 0
✓ andleft == right
(both None) ✓ - Return
None
(remove this node)
Step 5: Back to left subtree node
- Both children have been pruned and returned
None
- Now
root.left = None
androot.right = None
- Check:
val == 0
✓ andleft == right
(both None) ✓ - Return
None
(remove entire left subtree)
Step 6: Back to root, process right child (value 1)
- This is a leaf with value 1
- Check:
val == 0
✗ (value is 1) - Return the node itself (keep it)
Step 7: Back to root
- Left child has been pruned to
None
- Right child remains (value 1)
- Check:
val == 0
✓ butleft == right
✗ (None ≠ node with value 1) - Return root (keep it because it has a valid right child)
Final Tree:
0 \ 1
The algorithm successfully removed the entire left subtree (which contained only 0s) while preserving the path to the node with value 1. The root with value 0 is kept because it's needed to maintain the connection to the right subtree containing 1.
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
10class Solution:
11 def pruneTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
12 """
13 Prunes a binary tree by removing all subtrees that contain only 0 values.
14
15 Args:
16 root: The root node of the binary tree
17
18 Returns:
19 The root of the pruned tree, or None if the entire tree is pruned
20 """
21 # Base case: if the node is None, return None
22 if root is None:
23 return root
24
25 # Recursively prune the left subtree
26 root.left = self.pruneTree(root.left)
27
28 # Recursively prune the right subtree
29 root.right = self.pruneTree(root.right)
30
31 # If current node has value 0 and both children are None (or pruned),
32 # then this node should be pruned as well
33 # Note: root.left == root.right checks if both are None
34 if root.val == 0 and root.left == root.right:
35 return None
36
37 # Otherwise, keep this node in the tree
38 return root
39
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 * Prunes a binary tree by removing all subtrees that contain only 0 values.
19 * A subtree is pruned if it doesn't contain any node with value 1.
20 *
21 * @param root The root of the binary tree to prune
22 * @return The root of the pruned tree, or null if entire tree is pruned
23 */
24 public TreeNode pruneTree(TreeNode root) {
25 // Base case: if node is null, return null
26 if (root == null) {
27 return null;
28 }
29
30 // Recursively prune left subtree
31 root.left = pruneTree(root.left);
32
33 // Recursively prune right subtree
34 root.right = pruneTree(root.right);
35
36 // After pruning children, check if current node should be removed:
37 // Remove node if it has value 0 and no children (leaf node with 0)
38 if (root.val == 0 && root.left == null && root.right == null) {
39 return null;
40 }
41
42 // Keep the node if it has value 1 or has at least one child
43 return root;
44 }
45}
46
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 * Prunes a binary tree by removing all subtrees that contain only 0 values.
16 * A subtree is removed if it doesn't contain any node with value 1.
17 *
18 * @param root The root of the binary tree to prune
19 * @return The root of the pruned tree, or nullptr if entire tree is pruned
20 */
21 TreeNode* pruneTree(TreeNode* root) {
22 // Base case: if the node is null, return null
23 if (root == nullptr) {
24 return nullptr;
25 }
26
27 // Recursively prune the left subtree
28 root->left = pruneTree(root->left);
29
30 // Recursively prune the right subtree
31 root->right = pruneTree(root->right);
32
33 // If current node has value 0 and both children are null (pruned),
34 // then this node should also be pruned
35 // Note: root->left == root->right checks if both are nullptr
36 if (root->val == 0 && root->left == nullptr && root->right == nullptr) {
37 return nullptr;
38 }
39
40 // Otherwise, keep this node in the tree
41 return root;
42 }
43};
44
1/**
2 * Definition for a binary tree node.
3 * Each node contains a value and references to left and right children
4 */
5class TreeNode {
6 val: number;
7 left: TreeNode | null;
8 right: TreeNode | null;
9
10 constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
11 this.val = (val === undefined ? 0 : val);
12 this.left = (left === undefined ? null : left);
13 this.right = (right === undefined ? null : right);
14 }
15}
16
17/**
18 * Prunes a binary tree by removing all subtrees containing only 0 values
19 *
20 * The function recursively traverses the tree in post-order fashion:
21 * 1. First prunes the left subtree
22 * 2. Then prunes the right subtree
23 * 3. Finally decides whether to prune the current node
24 *
25 * A node is pruned if:
26 * - Its value is 0
27 * - It has no left child (after pruning)
28 * - It has no right child (after pruning)
29 *
30 * @param root - The root node of the binary tree to prune
31 * @returns The root of the pruned tree, or null if entire tree is pruned
32 */
33function pruneTree(root: TreeNode | null): TreeNode | null {
34 // Base case: if root is null, return null
35 if (!root) {
36 return root;
37 }
38
39 // Recursively prune the left subtree
40 root.left = pruneTree(root.left);
41
42 // Recursively prune the right subtree
43 root.right = pruneTree(root.right);
44
45 // Check if current node should be pruned
46 // Node is pruned if value is 0 and both children are null
47 // Note: root.left === root.right checks if both are null
48 if (root.val === 0 && root.left === root.right) {
49 return null;
50 }
51
52 // Return the current node if it shouldn't be pruned
53 return root;
54}
55
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a post-order traversal of the binary tree, visiting each node exactly once. At each node, it performs constant time operations:
- Checking if the node is null
- Recursively processing left and right subtrees
- Checking if
root.val == 0
androot.left == root.right
(checking if both children are None) - Returning either None or the node itself
Since we visit all n
nodes exactly once and perform O(1)
work at each node, the total time complexity is O(n)
.
Space Complexity: O(n)
The space complexity comes from the recursive call stack. In the worst case, when the tree is completely skewed (essentially a linked list), the recursion depth can reach n
, requiring O(n)
space on the call stack.
For a balanced binary tree, the recursion depth would be O(log n)
, but since we must consider the worst-case scenario, the space complexity is O(n)
.
Note: The clever condition root.left == root.right
checks if both children are None simultaneously, since if both are None, they would be equal. This is equivalent to checking root.left is None and root.right is None
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Order of Operations - Checking Node Before Pruning Children
A common mistake is to check whether the current node should be removed before recursively pruning its children. This leads to incorrect results because the decision about keeping a node depends on the final state of its children after pruning.
Incorrect Implementation:
def pruneTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if root is None:
return None
# WRONG: Checking before pruning children
if root.val == 0 and not root.left and not root.right:
return None
# Children are pruned after the check
root.left = self.pruneTree(root.left)
root.right = self.pruneTree(root.right)
return root
Why This Fails: Consider a tree where a node with value 0 has children that will be pruned:
0 / \ 0 0
The incorrect approach would keep the root node because it initially has children, even though those children should be removed, making the root a leaf with value 0 that should also be removed.
Solution: Always prune children first (post-order traversal), then make the decision about the current node:
def pruneTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if root is None:
return None
# CORRECT: First prune the children
root.left = self.pruneTree(root.left)
root.right = self.pruneTree(root.right)
# Then check if current node should be removed
if root.val == 0 and root.left is None and root.right is None:
return None
return root
2. Forgetting to Reassign the Pruned Children
Another pitfall is calling the recursive function on children but not reassigning the results back to the parent's pointers.
Incorrect Implementation:
def pruneTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if root is None:
return None
# WRONG: Not reassigning the pruned results
self.pruneTree(root.left)
self.pruneTree(root.right)
if root.val == 0 and root.left is None and root.right is None:
return None
return root
Why This Fails:
The recursive calls might return None
(indicating the subtree should be removed), but without reassignment, the parent node still points to the original children. This leaves nodes that should be pruned still connected to the tree.
Solution: Always reassign the results of recursive calls:
root.left = self.pruneTree(root.left) root.right = self.pruneTree(root.right)
3. Misunderstanding the Pruning Condition
Some might think a node should be removed if it has value 0, regardless of its children:
Incorrect Logic:
if root.val == 0: return None
Why This Fails: A node with value 0 should be kept if it has at least one child remaining after pruning (meaning there's a 1 somewhere in its subtree). Only leaf nodes with value 0 should be removed.
Solution: Check both conditions: the node has value 0 AND it has no children (is a leaf):
if root.val == 0 and root.left is None and root.right is None: return None
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!