Loading learning content...
Given a binary tree, how do you determine whether it is a valid Binary Search Tree? This seemingly simple question reveals one of the most instructive patterns in tree algorithms—and exposes a subtle trap that catches even experienced engineers.
BST validation is not merely an academic exercise. It appears in production code when:
This problem forces us to reason precisely about what the BST property actually means, and why our first intuition is often subtly wrong.
By the end of this page, you will understand the precise definition of a valid BST, recognize why naive approaches fail, master multiple validation strategies (recursive range-tracking and inorder traversal), and develop the analytical skills to choose the right approach for different contexts.
Before we can validate a BST, we must state precisely what we're validating. The Binary Search Tree property is deceptively simple:
For every node N in the tree:
- All values in N's left subtree are less than N's value
- All values in N's right subtree are greater than N's value
Pay careful attention to the words all values and subtree. This is not saying 'the left child is less than the root'—it's saying every descendant on the left must be less than the root. This distinction is the crux of BST validation.
The BST property is a global constraint on subtrees, not just a local constraint between adjacent nodes. Checking only parent-child relationships is insufficient and leads to incorrect validation.
Handling Equal Values:
Different implementations handle equal values differently:
For this discussion, we'll use strict inequality (no duplicates), but the patterns generalize. When solving interview problems, always clarify how duplicates should be handled.
The first approach most developers consider is straightforward: for each node, check if the left child is less than the node and the right child is greater. If this holds for every node, the tree is a BST.
This approach is incorrect.
1234567891011121314151617181920
# INCORRECT APPROACH - DO NOT USEdef is_valid_bst_naive(node): """ This approach checks only immediate children. It will incorrectly return True for trees that violate the BST property through deeper descendants. """ if node is None: return True # Check immediate left child if node.left and node.left.val >= node.val: return False # Check immediate right child if node.right and node.right.val <= node.val: return False # Recursively check subtrees return is_valid_bst_naive(node.left) and is_valid_bst_naive(node.right)Why does this fail?
Consider this counterexample:
In this tree:
The naive algorithm returns true because every local parent-child relationship satisfies the BST constraint.
But this tree is NOT a valid BST!
The node with value 6 is in the right subtree of the root 10, but 6 < 10. The BST property requires that all values in the right subtree be greater than the root. The value 6 violates this—it's less than the root 10, yet it appears in the right subtree.
Local correctness does not imply global correctness. A node must satisfy constraints imposed not just by its parent, but by all its ancestors. The node 6 must be greater than both its parent 15 AND the root 10—but the naive approach only checks the parent.
The correct approach tracks the valid range for each node as we recurse. Every node must fall within a range determined by its ancestors:
This elegantly enforces the global BST property by propagating constraints downward through the tree.
The Algorithm:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
def is_valid_bst(root): """ Validate BST using range-based recursion. Time Complexity: O(n) - visit each node exactly once Space Complexity: O(h) - recursion stack, where h is tree height O(log n) for balanced, O(n) for skewed """ def validate(node, min_val, max_val): # Base case: empty tree is valid if node is None: return True # Check if current node violates the range constraint if node.val <= min_val or node.val >= max_val: return False # Recursively validate subtrees with updated ranges # Left subtree: all values must be less than current node # Right subtree: all values must be greater than current node return (validate(node.left, min_val, node.val) and validate(node.right, node.val, max_val)) # Start with infinite range return validate(root, float('-inf'), float('inf')) # For trees with integer values, we can use sentinel valuesdef is_valid_bst_with_sentinels(root): """ Alternative using None as sentinel for unbounded ranges. Useful when dealing with extreme integer values. """ def validate(node, min_val, max_val): if node is None: return True # Check lower bound (if it exists) if min_val is not None and node.val <= min_val: return False # Check upper bound (if it exists) if max_val is not None and node.val >= max_val: return False return (validate(node.left, min_val, node.val) and validate(node.right, node.val, max_val)) return validate(root, None, None)Tracing Through the Counterexample:
Let's trace the range-based algorithm on our earlier counterexample:
| Node | Valid Range | Check | Result |
|---|---|---|---|
| 10 (root) | (-∞, +∞) | 10 in (-∞, +∞)? | ✓ Valid, recurse |
| 5 (left of 10) | (-∞, 10) | 5 in (-∞, 10)? | ✓ Valid, recurse |
| 15 (right of 10) | (10, +∞) | 15 in (10, +∞)? | ✓ Valid, recurse |
| 6 (left of 15) | (10, 15) | 6 in (10, 15)? | ✗ INVALID - 6 ≤ 10 |
The key insight: when we descend into the left subtree of 15, we inherit the constraint that all values must still be greater than 10 (from the ancestor root). The range becomes (10, 15), and 6 fails this check.
This is how range propagation captures the global BST constraint, not just local relationships.
Think of each node as inheriting a "passport" that must satisfy all border controls from root to leaf. Going left adds an upper bound; going right adds a lower bound. A valid BST node must clear ALL checkpoints.
A fundamentally different approach exploits a beautiful property of BSTs:
An inorder traversal of a valid BST produces a strictly increasing sequence.
This means we can validate a BST by performing an inorder traversal and verifying that each value is greater than the previous. If any value is less than or equal to its predecessor in the inorder sequence, the tree is not a valid BST.
Why does inorder traversal yield sorted output?
Inorder traversal visits nodes in the order: left subtree → root → right subtree. Due to the BST property:
By the time we visit the root, we've already visited all smaller values (left subtree). After the root, we visit all larger values (right subtree). This recursive property guarantees sorted output.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
def is_valid_bst_inorder(root): """ Validate BST using inorder traversal property. Key insight: Inorder traversal of a valid BST produces a strictly increasing sequence. Time Complexity: O(n) - visit each node exactly once Space Complexity: O(h) - recursion stack """ prev = [float('-inf')] # Use list for Python closure semantics def inorder(node): if node is None: return True # Visit left subtree if not inorder(node.left): return False # Check current node against previous value if node.val <= prev[0]: return False # Update previous value prev[0] = node.val # Visit right subtree return inorder(node.right) return inorder(root) # Alternative: Iterative inorder with explicit stackdef is_valid_bst_iterative(root): """ Iterative inorder traversal validation. Avoids recursion stack overflow for very deep trees. Uses explicit stack simulation of inorder traversal. """ stack = [] prev = float('-inf') current = root while stack or current: # Go as far left as possible while current: stack.append(current) current = current.left # Process current node current = stack.pop() # Validate against previous value if current.val <= prev: return False prev = current.val # Move to right subtree current = current.right return TrueIn the recursive Python solution, we use prev = [float('-inf')] instead of a simple variable. This is because Python's closure semantics require mutable objects (like lists) to modify outer variables from nested functions. Using prev[0] allows us to update the value across recursive calls.
Both the range-based and inorder approaches solve BST validation correctly with identical asymptotic complexity. However, they differ in important ways:
| Aspect | Range-Based | Inorder Traversal |
|---|---|---|
| Time Complexity | O(n) | O(n) |
| Space Complexity | O(h) recursion stack | O(h) recursion stack |
| Early Termination | Can terminate immediately on invalid node | Must complete traversal path to detect violations |
| Conceptual Basis | Directly encodes BST constraint | Exploits sorted output property |
| State Tracking | Two values (min, max) per call | One value (prev) shared across calls |
| Edge Cases | Requires careful handling of int overflow | Same overflow concerns |
| Generalization | Easily adapts to variants (≤ vs <) | Requires adjustment for duplicates |
When to Choose Each Approach:
Range-based is often preferred when:
Inorder-based is often preferred when:
Robust BST validation must handle several edge cases that appear in production code and interviews:
INT_MIN or INT_MAX can break naive implementations using infinity. Use nullable bounds or wider types (long in Java).1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
def is_valid_bst_production(root): """ Production-ready BST validation with comprehensive edge case handling. Features: - Handles None/empty trees correctly - Uses None bounds to avoid integer overflow issues - Supports customizable duplicate handling - Includes iterative option for deep trees """ def validate(node, min_val, max_val): if node is None: return True # Check bounds using None for unbounded (no overflow risk) if min_val is not None and node.val <= min_val: return False if max_val is not None and node.val >= max_val: return False return (validate(node.left, min_val, node.val) and validate(node.right, node.val, max_val)) return validate(root, None, None) def is_valid_bst_with_duplicates(root, duplicates_allowed_left=True): """ BST validation that handles duplicate values. Args: root: Root of the tree duplicates_allowed_left: If True, duplicates go to left subtree If False, duplicates go to right subtree """ def validate(node, min_val, max_val): if node is None: return True if min_val is not None: # Duplicates on left: left <= root, so right > root if duplicates_allowed_left and node.val <= min_val: return False # Duplicates on right: left < root, so right >= root if not duplicates_allowed_left and node.val < min_val: return False if max_val is not None: # Duplicates on left: left <= root if duplicates_allowed_left and node.val > max_val: return False # Duplicates on right: left < root (strict) if not duplicates_allowed_left and node.val >= max_val: return False return (validate(node.left, min_val, node.val) and validate(node.right, node.val, max_val)) return validate(root, None, None)In languages like Java or C++, using Integer.MIN_VALUE and Integer.MAX_VALUE as initial bounds can fail if the tree contains nodes with those exact values. The safer approach is using nullable bounds (Integer wrapper in Java) or wider types (long) to avoid this edge case.
BST validation is a common interview question precisely because it exposes several subtle errors. Being aware of these helps you avoid them:
< instead of <= (or vice versa) changes whether duplicates are allowed. Be consistent and intentional.min_val is the exclusive lower bound (value must be > min_val), and max_val is the exclusive upper bound (value must be < max_val).Debugging Strategy:
When your BST validation produces wrong results:
If you're unsure during an interview, explicitly state 'I'll assume no duplicates are allowed, so I'll use strict inequality.' This shows awareness of the ambiguity and prevents silent errors in your solution.
Understanding the complexity of BST validation reinforces fundamental reasoning about tree algorithms:
Time Complexity: O(n)
Both approaches visit each node exactly once:
At each node, we perform O(1) work (comparisons, bound updates). With n nodes, total work is O(n).
No approach can do better than O(n) because we must examine every node to guarantee correctness. A single invalid node anywhere in the tree invalidates the BST—we can't skip any nodes.
Space Complexity: O(h)
The space complexity is dominated by the recursion stack (or explicit stack for iterative versions):
For interview purposes, stating O(h) is precise. Stating O(n) worst case is also acceptable.
| Tree Type | Height (h) | Space Complexity | Example |
|---|---|---|---|
| Perfect binary tree | log₂(n) | O(log n) | 1 million nodes → ~20 stack frames |
| Complete binary tree | log₂(n) | O(log n) | Same as perfect, slight variation |
| Left-skewed tree | n | O(n) | 1 million nodes → 1 million stack frames |
| Right-skewed tree | n | O(n) | Same as left-skewed |
| Random BST (expected) | ~2 ln(n) | O(log n) | Expected balanced on average |
The O(h) space complexity is characteristic of DFS-based tree algorithms. It's also why binary search trees with guaranteed O(log n) height (like AVL and Red-Black trees) are preferred in production—they guarantee logarithmic stack depth for all recursive operations.
While BST validation might seem like purely algorithmic practice, it has genuine real-world applications:
Example: Database Index Corruption Detection
Consider a database using a B-tree index (a generalization of BST). After a power failure:
1. System restarts
2. Database runs integrity checks
3. Index validation discovers a subtree violates ordering
4. Database triggers index rebuild from source data
5. Operations resume with corrected index
Without validation, the corrupted index would cause wrong query results or crashes. The validation algorithm—though running in O(n)—is essential for system reliability.
BST validation is a foundational problem that teaches critical lessons about tree algorithms and the BST property. Let's consolidate what we've learned:
Looking Ahead:
BST validation is the first in a series of common BST patterns. Having mastered the fundamental property check, we're now equipped to explore more complex patterns:
Each of these patterns builds on the deep understanding of BST structure that validation requires.
You now understand how to correctly validate a Binary Search Tree using both range-based and inorder approaches. You know why naive approaches fail, how to handle edge cases, and where this pattern appears in production systems. Next, we'll explore finding floor and ceiling values in a BST.