Problem
Given the root of a binary tree, determine whether it is a valid Binary Search Tree (BST).
A BST follows three important rules:
- Every node in the left subtree must have a value less than the current node.
- Every node in the right subtree must have a value greater than the current node.
- Both the left and right subtrees must also be valid BSTs.
Example 1
2
/ \
1 3Input: [2,1,3]
Output: TrueThis satisfies all BST rules.
Example 2
1
/ \
2 3Input: [1,2,3]
Output: FalseThe left child (2) is greater than its parent (1), violating the BST property.
My Initial Thought Process
At first, I thought validating a BST simply meant checking whether:
- the left child is smaller than the parent
- the right child is larger than the parent
Unfortunately, this isn't enough.
Consider this tree:
5
/ \
3 7
/
4If we only compare each node with its parent:
- 7 > 5 ✅
- 4 < 7 ✅
Everything appears correct.
However, 4 is inside the right subtree of 5, so it should also be greater than 5.
Since 4 < 5, this tree is not a valid BST.
This is the key insight of the problem:
Every node must satisfy constraints imposed by all of its ancestors, not just its parent.
The Better Idea: Carry the Valid Range
Instead of comparing each node only with its parent, we keep track of the range of values each node is allowed to have.
Initially, the root can be any value:
(-∞, +∞)Whenever we go left:
upper = current node valueWhenever we go right:
lower = current node valueFor example:
8
/ \
3 10
/ \
1 6The ranges become:
8 -> (-∞, +∞)
3 -> (-∞, 8)
10 -> (8, +∞)
1 -> (-∞, 3)
6 -> (3, 8)Notice how node 6 is constrained by both its parent (3) and the root (8).
DFS Solution
We perform a depth-first traversal while carrying two values:
lower→ smallest allowed valueupper→ largest allowed value
At every node we simply verify:
lower < node.val < upperIf this fails, the tree is not a BST.
Otherwise we continue searching both children with updated bounds.
Python Solution
from typing import Optional
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
def dfs(node, lower, upper):
if not node:
return True
valid = lower < node.val < upper
return (
valid
and dfs(node.left, lower, node.val)
and dfs(node.right, node.val, upper)
)
return dfs(root, float("-inf"), float("inf"))Dry Run
Consider:
5
/ \
3 7
/
6Start with
dfs(5, -∞, +∞)Root (5)
-∞ < 5 < +∞ ✅Left subtree:
dfs(3, -∞, 5)Right subtree:
dfs(7, 5, +∞)Node 3
Allowed range:
(-∞, 5)-∞ < 3 < 5 ✅Both children are None.
Node 7
Allowed range:
(5, +∞)5 < 7 < +∞ ✅Left child:
dfs(6, 5, 7)Node 6
Allowed range:
(5, 7)5 < 6 < 7 ✅Everything passes.
Return:
TrueNow consider this invalid tree:
5
/ \
3 7
/
4When we visit node 4, the valid range is:
(5, 7)Checking:
5 < 4 < 7 ❌The recursion immediately returns False.
Why This Works
Every recursive call narrows the valid interval.
Going left updates the upper bound.
Going right updates the lower bound.
This guarantees that every node satisfies the BST property with respect to every ancestor, not just its immediate parent.
Time Complexity
Each node is visited exactly once.
Time Complexity: O(n)
where n is the number of nodes.
Space Complexity
The recursion stack depends on the height of the tree.
- Balanced BST:
O(log n) - Skewed BST:
O(n)
Key Takeaways
- Comparing a node with only its parent is not sufficient.
- Every node must satisfy constraints inherited from all its ancestors.
- Passing a valid
(lower, upper)range during DFS elegantly enforces the BST rules. - This is the standard interview solution and is both simple and optimal.
Once you recognize that every node has an allowable range rather than just a parent relationship, the entire problem becomes much easier to reason about.