669. Trim a Binary Search Tree - Explanation
Description
Remove every node whose value is outside the inclusive interval [low, high], and return the root of the remaining binary search tree. The root may change, and the result may be empty.
Preserve the relative structure of retained nodes: a retained descendant must remain a descendant of its retained ancestors. This determines one unique trimmed tree. Both boundary values are allowed to remain.
The input is a valid binary search tree with distinct values. Each node's left subtree contains smaller values, and its right subtree contains larger values. Examples use level-order arrays; null marks a missing child, and [] represents an empty result.
Example 1:
Input: root = [8,3,10,1,6,null,14,null,null,4,7,13]
low = 4
high = 13
Output: [8,6,10,4,7,null,13]Explanation: Remove 1 and 3 below the lower bound, connecting node 6 as the left child of 8. Remove 14 above the upper bound, connecting node 13 as the right child of 10. Values 4 and 13 remain because the bounds are inclusive.
Example 2:
Input: root = [5,2,8,1,3,7,9]
low = 6
high = 9
Output: [8,7,9]Explanation: The original root 5 and its whole left subtree are too small. Node 8 becomes the root, and its children 7 and 9 both remain.
Example 3:
Input: root = [4,2,6]
low = 7
high = 8
Output: []Explanation: Every node is smaller than 7. All nodes are removed, so the result is an empty tree.
Constraints:
- The input tree contains between 1 and 10000 nodes.
0 <= Node.val <= 10000- Node values are unique, and the input is a valid binary search tree.
0 <= low <= high <= 10000
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Binary Search Tree (BST) properties - Understanding that left subtree values are less than root and right subtree values are greater, which enables pruning decisions
- Recursion and DFS - Traversing trees recursively and understanding how to rebuild tree structure during traversal
- Tree node manipulation - Modifying parent-child relationships by reassigning left/right pointers
1. Depth First Search
Intuition
The BST property gives us a powerful pruning strategy. If the current node's value is greater than high, then the node and its entire right subtree are too large, so we can discard them and only keep the trimmed left subtree. Similarly, if the value is less than low, the node and its left subtree are too small. When the node is within range, we recursively trim both children and attach the results.
Algorithm
- If the current node is
null, returnnull(base case). - If the node's value is greater than
high, return the result of trimming the left subtree (discard current node and right subtree). - If the node's value is less than
low, return the result of trimming the right subtree (discard current node and left subtree). - Otherwise, recursively trim both left and right children, attach them to the current node, and return the node.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
if not root:
return None
if root.val > high:
return self.trimBST(root.left, low, high)
if root.val < low:
return self.trimBST(root.right, low, high)
root.left = self.trimBST(root.left, low, high)
root.right = self.trimBST(root.right, low, high)
return rootTime & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
2. Iterative DFS
Intuition
We can avoid recursion by using a stack. The idea remains the same: nodes outside the range need to be replaced by their valid children. We first find a valid root, then process the tree using the stack, fixing any out-of-range children by replacing them with their appropriate grandchildren.
Algorithm
- Find the new root by moving to the right child if the current root is too small, or to the left child if too large, until the root is within
[low, high]. - Initialize a stack with the valid root.
- While the stack is not empty:
- Pop a
node. - If the left child exists and its value is less than
low, replace it with its right child. - If the right child exists and its value is greater than
high, replace it with its left child. - If any replacement was made, push the
nodeback for reprocessing. - Otherwise, push both children (if they exist) onto the stack.
- Pop a
- Return the valid root.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def trimBST(self, root, low, high):
while root and (root.val < low or root.val > high):
if root.val < low:
root = root.right
else:
root = root.left
stack = [root]
while stack:
node = stack.pop()
if not node:
continue
left_out = node.left and node.left.val < low
right_out = node.right and node.right.val > high
if left_out:
node.left = node.left.right
if right_out:
node.right = node.right.left
if left_out or right_out:
stack.append(node)
else:
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return rootTime & Space Complexity
- Time complexity:
- Space complexity:
3. Iterative DFS (Optimal)
Intuition
Instead of using a stack, we can trim the tree in two linear passes. After finding a valid root, we traverse down the left spine fixing any nodes that fall below low, then traverse down the right spine fixing any nodes that exceed high. This works because in a BST, once we fix a node on one side, we only need to continue checking in that direction.
Algorithm
- Find a valid root by skipping nodes outside the range.
- Save the valid root as
tmpRoot. - Traverse the left spine: while there is a left child with value less than
low, replace it with its right child. Move to the next left child. - Reset to
tmpRootand traverse the right spine: while there is a right child with value greater thanhigh, replace it with its left child. Move to the next right child. - Return
tmpRoot.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
while root and (root.val < low or root.val > high):
root = root.right if root.val < low else root.left
tmpRoot = root
while root:
while root.left and root.left.val < low:
root.left = root.left.right
root = root.left
root = tmpRoot
while root:
while root.right and root.right.val > high:
root.right = root.right.left
root = root.right
return tmpRootTime & Space Complexity
- Time complexity:
- Space complexity: extra space.
Common Pitfalls
Forgetting to Recursively Trim After Skipping a Node
When a node's value is outside the range and you skip to its child, you must continue trimming that child subtree. A common mistake is to simply return the child without recursively processing it. The child itself or its descendants may also be outside the valid range and need trimming.
Confusing Which Subtree to Keep When Node is Out of Range
When a node's value is greater than high, you should return the trimmed left subtree (not the right). When a node's value is less than low, you should return the trimmed right subtree (not the left). Mixing these up violates the BST property and produces incorrect results.
Not Handling the Case Where the Root Itself is Invalid
The root node may be outside the [low, high] range, meaning the entire original root gets discarded. A common oversight is assuming the root will always be valid. Your solution must handle finding a new valid root by traversing into the appropriate subtree before beginning the main trimming logic.
Sign in to join the discussion