Skip to content

Coding patternsBlind 75

Validate Binary Search Tree

Every node must fit inside a range set by all its ancestors, not just its parent. Solved two ways, scanning every subtree in O(n²) and passing range limits down in O(n), with a step-through, code in five languages and the Big O worked out from the code.

You are given the root of a binary tree. Return true if it is a valid binary search tree (BST), and false if not. A tree is a valid BST when, at every node:

  • every value in its left subtree is smaller than the node’s value,
  • every value in its right subtree is larger than the node’s value,
  • and both subtrees are valid BSTs too.

Trees in the examples are written level by level, from left to right. null marks a missing child.

Example 1

Input: root = [6, 3, 9, 1, 7]

Output: false

Each child looks fine next to its parent: 3 < 6 and 7 > 3. But 7 is in the left subtree of 6, and 7 is larger than 6.

63179

Example 2

Input: root = [5, 2, 8, 1, 4, 7, 9]

Output: true

Everything left of 5 (2, 1, 4) is smaller, everything right (8, 7, 9) is larger, and the same holds under 2 and under 8.

Example 3

Input: root = [2, 2]

Output: false

The left child equals its parent. Values must be strictly smaller on the left, so equal is not allowed.

  • The tree has n nodes, where 1 ≤ n ≤ 10,000.
  • -2³¹ ≤ Node.val ≤ 2³¹ − 1
Are equal values allowed?
No. Left must be strictly smaller and right strictly larger, so a repeated value makes the tree invalid.
Is it enough to compare each node with its children?
No. Example 1 passes every parent-child check and is still invalid. The rule covers whole subtrees.
Can a value be the smallest or largest 32-bit integer?
Yes. So the starting limits must sit outside that range, or be "no limit".
Can the tree be empty?
No. It has at least one node, and a single node is always valid.

The rule at one node depends on the values in its whole subtree. Turned around, each node must fit a range that its ancestors set: every time the path goes left, the parent becomes an upper limit, and every time it goes right, a lower limit. Passing that range down is a tree traversal where each call hands information down to its children.

This problem has two approaches. There is no useful step between them.

Optimal O(n)

Turn the rule around. Instead of asking each node about everything below it, let each node tell its children what they are allowed to be.

The root may hold any value: its range is (−∞, ∞). When we go left from a node, every value there must be smaller than that node, so the node becomes the new upper limit. When we go right, it becomes the new lower limit. The other limit is passed down unchanged. So a node’s range always holds the tightest limits from all its ancestors.

Now each node needs one check: is its value strictly inside its range?

Why it is correct: a value fits its range exactly when it is on the correct side of every ancestor. That is the BST rule, checked once per node instead of once per pair.

63179
  1. def check(node, low=-inf, high=inf):
  2. if node is None:
  3. return True
  4. if not (low < node.val < high):
  5. return False
  6. return check(node.left, low, node.val) and check(node.right, node.val, high)

Each node gets an open range (low, high) that it must fit in. Every ancestor tightens the range on the way down. Start with no limits. Under each node is its range: <6 means below 6, 3–6 means between 3 and 6.

1 / 8
Solution.javasolution.pySolution.cssolution.gosolution.ts
class Solution {
// Range limits: pass down the open range (low, high) each node must
// fit in. One check per node, so n steps.
public boolean isValidBST(TreeNode root) {
// long limits sit outside the int range, so any value fits at first.
return check(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean check(TreeNode node, long low, long high) {
if (node == null) return true;
// Every ancestor left a limit. low and high are the tightest ones.
if (node.val <= low || node.val >= high) return false;
// Going left, this node becomes the new upper limit.
// Going right, it becomes the new lower limit.
return check(node.left, low, node.val) && check(node.right, node.val, high);
}
}
Time O(n)Space O(h)Here is how we get these from the code, in four steps.
  1. 1 Find the work that repeats

    There is no loop. The work repeats through recursion, so we count the calls. Each call does a fixed job: one range check and at most two more calls. So each call counts as 1 step.

    1. def check(node, low=-inf, high=inf):
    2. if node is None:
    3. return True
    4. if not (low < node.val < high):
    5. return False
    6. return check(node.left, low, node.val) and check(node.right, node.val, high)
  2. 2 Count the runs on Example 1

    The table lists the calls in order. 6, 3 and 1 fit their ranges. The two empty children of 1 return True at once. Then 7 fails its range (3, 6). The and stops the rest, so 9 is never called.

    6 calls.

    call123456
    node631emptyempty7
    range(−∞, ∞)(−∞, 6)(−∞, 3)(3, 6)
    resultfitsfitsfitsTrueTrueFalse
  3. 3 Write the count for any n

    In the worst case the tree is valid, so nothing stops early. Then each of the n nodes gets one call, and so does each of the n + 1 empty child spots:

    n + (n + 1) = 2n + 1 calls

    Check it with Example 2, which is valid: 7 nodes, so 2 × 7 + 1 = 15 calls.

  4. 4 Keep only the part that grows fastest

    • Drop the + 1. One extra call does not change the growth.
    • Drop the 2. When n doubles, 2n doubles too.

    What is left is n.

    So the time is O(n).

  5. S Space: count the extra memory the same way

    Each call keeps two numbers, low and high. The calls that wait on the call stack form one path from the root down, so there are at most h of them, where h is the height of the tree. A balanced tree has h about log n, and a chain has h = n. So the space is O(h).

    lowhighcall stack (one frame per level)

2,000 random trees of 1 to 9 nodes: real search trees, search trees with one value changed, and random trees with repeated values. The smallest and largest allowed values show up too. The brute force and the optimal solution gave the same answer every time.

Largest inputsResult
10,000 nodes in one long line (the most allowed), a valid search tree, so every node is checked. Optimal.0.002 s, and about 1 MB of extra memory: 10,000 calls wait at once, one per level, which is the O(h) space.
10,000 nodes, balanced and valid. Optimal.0.001 s, and under 0.01 MB of extra memory: only 14 calls wait at once.
2,000 nodes in one long line. Brute force.0.35 s. It scans each subtree again for every node above it, so at 10,000 nodes it would make about 50 million calls.