LC 98 · Medium · Pre-order · pass bounds down

Validate Binary Search Tree

A property that a purely local check would get wrong, because ancestors constrain descendants.

Recognise it

Before any code, what in the question tells you this is the pattern?

A binary search tree, plus a property that is about more than a node and its immediate children. The giveaway is that a purely local check would pass on a tree you can see is wrong, which means information has to travel down from ancestors.

The brute force

Always have this one ready. It is the honest starting point, and it is what the real solution improves on.

At each node, check that the left child is smaller and the right child is larger, then recurse into both.

O(n) time.

A tree where every parent-child pair passes but the tree is not a BST.54866 > 4, so the pair passesbut 6 sits left of 5 and 6 > 5Being a BST constrains every ancestor,not just the nearest one.
The violating node is deep in the left subtree and larger than the root.

This is not slow, it is wrong, and that makes it more instructive than a slow solution. Take the tree with root 5, left child 4, and a right child of that 4 equal to 6. Every parent-child pair passes: 4 is less than 5, and 6 is greater than 4. But 6 sits in the left subtree of 5 and is larger than 5, so the tree is not a BST. The check fails because being a BST is a statement about every ancestor, not just the nearest one.

The approach

The idea in plain language, before it becomes syntax.

Give each node a window of values it is allowed to hold. The root inherits an unbounded window. Whenever you move to a left child, the current node becomes a new ceiling, because everything to the left must be smaller than it. Moving right, the current node becomes a new floor.

A tree is a valid BST exactly when every node lies strictly inside the window it inherits. That single statement is provably equivalent to the definition, which is why this approach is correct where the local check is not: the window carries down the constraint from every ancestor, not just the parent.

There is a second correct approach worth knowing. An in-order traversal of a BST produces a strictly increasing sequence, so you can walk in order and check each value against the previous one. It uses less conceptual machinery but needs care with the very first node, and it does not generalise to pruning problems the way bounds do.

A BST with the allowed window narrowing on the way down.Every node inherits a window of allowed values.84122610148 in (-inf, +inf)4 in (-inf, 8)12 in (8, +inf)6 in (4, 8)Going left lowers the ceiling to this value. Going right raises the floor to it.Strict on both ends: duplicates make the tree invalid.
Validate Binary Search Tree: the idea in one picture.edit source
The allowed window narrowing as the walk descends.8(-inf, +inf)4(-inf, 8)12(8, +inf)left lowers the ceiling to this valueright raises the floor to this valueStrict on both ends: duplicates are invalid.
Going left lowers the ceiling; going right raises the floor.

The algorithm

The same idea again, as steps you could follow with a pencil.

Flowchart for the bounded validation walk.ok(node, low, high)node is null ?yestruenolow < val < high ?nofalseyesok(left, low, val) and ok(right, val, high)
Validate Binary Search Tree: the loop, step by step.edit source
Time O(n)Space O(h)

The solution

Now the code, and why each decision in it is the way it is.

validate_binary_search_tree
def isValidBST(root):
    def ok(node, low, high):
        if not node:
            return True
        if not (low < node.val < high):
            return False
        # Going left caps the max; going right raises the min.
        return ok(node.left, low, node.val) and ok(node.right, node.val, high)
    return ok(root, float('-inf'), float('inf'))

The comparison is strict on both ends. Under the usual definition duplicates make a tree invalid, so a single <code>&lt;=</code> flips the answer on a tree full of equal values.

Initial bounds must be genuinely unbounded. Using the smallest and largest machine integers breaks on a tree that actually contains those values, which is exactly the test case that gets written. Python uses infinities and the Java version uses nullable bounds, which is the cleanest way to say "no bound yet" in a language without them.

Bounds are passed down rather than results being passed up, which makes this a pre-order algorithm. That is the opposite direction of travel from the depth and path-sum problems, and noticing which direction information flows is a fast way to classify an unfamiliar tree question.

Where people slip

  • Comparing each node only against its own children is wrong. A node deep in the left subtree can be larger than an ancestor while still being larger than its parent.
  • The comparison is strict on both sides. Duplicate values make a tree invalid under the usual definition, so <= anywhere is a bug.
  • Using integer sentinels for the initial bounds breaks when the tree contains those exact values. Use infinities, or nullable bounds as in the Java version.

Your notes

What tripped you up here? Write it in your own words.

Where interviewers go next

Solve this one and the follow-up is usually a variation rather than a new idea. Knowing the connection is worth more than memorising each problem.

  • Kth Smallest in a BSTLC 230In-order traversal of a BST is sorted, so stop counting at k rather than collecting everything.
  • Recover Binary Search TreeLC 99Two nodes were swapped. In-order gives a nearly sorted sequence with one or two inversions.
  • Range Sum of BSTLC 938The same bounds idea used to prune whole subtrees instead of to reject them.