Validate Binary Search Tree
Check that a tree is a valid BST, and understand why comparing each node to its parent is not enough
Problem Statement
Given the root of a binary tree, decide whether it is a valid binary search tree (BST).
A valid BST requires:
- Every node in the left subtree is strictly less than the node.
- Every node in the right subtree is strictly greater than the node.
- Both subtrees are themselves valid BSTs.
Input: root = [2, 1, 3]
Output: true
Input: root = [5, 1, 4, null, null, 3, 6]
Output: false
Why: 4 is in 5's right subtree, but 4 < 5Intuition
The obvious first attempt is to check each node against its parent:
if node.left: assert node.left.val < node.val
if node.right: assert node.right.val > node.valThis is wrong, and the counterexample is the whole lesson of the problem.
The tree that breaks the naive check
5
/ \
1 7
/ \
3 9 ← 3 is less than 5, but sits in 5's RIGHT subtreeCheck every parent–child pair and each one passes: 1 < 5, 7 > 5, 3 < 7, 9 > 7.
But this is not a BST. Searching for 3 starts at 5, sees 3 < 5, and goes left — into a subtree that does not contain it. The tree lies about where its values are.
The naive check fails because the BST rule is not about parents. Read it again: every node in the left subtree, not the left child.
The key insight
A node's legal range is decided by every ancestor above it, not just its parent.
Going left tightens the ceiling. Going right raises the floor. So carry a (low, high) window down the tree, and check each node fits inside it.
How the window narrows
Start at the root with no constraints at all: (-∞, +∞).
| Move | What it means | New window |
|---|---|---|
Go left from node v | Everything here must be < v | (low, v) |
Go right from node v | Everything here must be > v | (v, high) |
Trace the broken tree above:
5 window (-inf, +inf) ok
├─ left 1 window (-inf, 5) ok 1 < 5
└─ right 7 window (5, +inf) ok 7 > 5
├─ left 3 window (5, 7) FAIL 3 is not > 5
└─ right 9 window (7, +inf) okNode 3 is caught. Its parent said "less than 7", but its grandparent had already said "greater than 5", and 3 cannot satisfy both.
Approach
Start with an unbounded window
The root may hold any value, so begin with low = -∞ and high = +∞.
Reject a node outside its window
If node.val <= low or node.val >= high, the tree is invalid. Both comparisons are strict — duplicates are not allowed.
Tighten and recurse left
The left subtree keeps the same floor but takes this node as its new ceiling: (low, node.val).
Tighten and recurse right
The right subtree takes this node as its new floor and keeps the ceiling: (node.val, high).
Valid only if both sides are valid
An empty subtree is trivially valid, which gives the base case for free.
Watch it run
Solution
class TreeNode:
def __init__(self, val: int = 0, left: "TreeNode | None" = None,
right: "TreeNode | None" = None):
self.val = val
self.left = left
self.right = right
def is_valid_bst(root: TreeNode | None) -> bool:
"""Check the BST property by carrying an allowed range down the tree."""
def check(node: TreeNode | None, low: float, high: float) -> bool:
# An empty subtree breaks no rules.
if node is None:
return True
# Strict comparisons: equal values are not allowed in a BST.
if not (low < node.val < high):
return False
# Going left lowers the ceiling; going right raises the floor.
return check(node.left, low, node.val) and check(node.right, node.val, high)
return check(root, float("-inf"), float("inf"))
def is_valid_bst_inorder(root: TreeNode | None) -> bool:
"""Alternative: an inorder walk of a valid BST is strictly increasing."""
previous: float = float("-inf")
stack: list[TreeNode] = []
node = root
while stack or node:
while node:
stack.append(node)
node = node.left
node = stack.pop()
# Must be strictly greater — equality means a duplicate.
if node.val <= previous:
return False
previous = node.val
node = node.right
return TrueThe inorder alternative
An inorder walk (left, node, right) of a valid BST produces values in strictly increasing order. So a second correct solution is: walk inorder, and check each value is bigger than the last.
It needs only one variable instead of two bounds, but the comparison must be strict — <= catches duplicates, < would let them through.
Complexity Analysis
Time Complexity
O(n)
Space Complexity
O(h)
- Time — every node is visited once and compared against two bounds. The bounds version can exit early: it stops at the first violation, so a tree broken near the root costs far less than
n. - Space — the recursion stack, one frame per level.
O(log n)balanced,O(n)for a chain.
Edge Cases
Related Problems
- Lowest Common Ancestor of a BST — exploits the same ordering, to navigate rather than to verify
- Maximum Depth of Binary Tree — the recursive skeleton without the bounds
- Binary Search — the same halving idea, on an array