669. Trim a Binary Search Tree - Explanation

Problem Link

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:

Trim a Binary Search Tree, example 1: illustration of the example

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:

Trim a Binary Search Tree, example 2: illustration of the example

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:

Trim a Binary Search Tree, example 3: illustration of the example

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

Company Tags

Please upgrade to NeetCode Pro to view company tags.



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

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

  1. If the current node is null, return null (base case).
  2. If the node's value is greater than high, return the result of trimming the left subtree (discard current node and right subtree).
  3. If the node's value is less than low, return the result of trimming the right subtree (discard current node and left subtree).
  4. 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 root

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n) 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

  1. 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].
  2. Initialize a stack with the valid root.
  3. 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 node back for reprocessing.
    • Otherwise, push both children (if they exist) onto the stack.
  4. 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 root

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. Find a valid root by skipping nodes outside the range.
  2. Save the valid root as tmpRoot.
  3. 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.
  4. Reset to tmpRoot and traverse the right spine: while there is a right child with value greater than high, replace it with its left child. Move to the next right child.
  5. 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 tmpRoot

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(1)O(1) 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.