ProblemsTrees
Validate Binary Search Tree
You'll pass each node a range of allowed values, so one DFS checks every ancestor's rule at once. You need binary trees. Related chapter: trees.
1. The problem
You get the root of a binary tree. Return whether it's a binary search tree (BST): at every node, the entire left subtree holds smaller values and the entire right subtree holds larger values. Equal values aren't allowed. Don't change the tree. Our function also treats an empty tree as valid. The inputs below list the tree level by level. This is LeetCode 98.
| Tree | Answer | Why |
|---|---|---|
[4,2,6] |
True | All left values are below 4; all right values above |
[5,3,8,null,null,4,9] |
False | 4 is in 5's right subtree |
[2,2,3] |
False | Equal keys violate strict order |
[] |
True | LeetCode doesn't pass this; we return True |
"Entire" is the word that matters. In the second example, 4 is fine as 8's left child, but it sits in 5's right subtree and is smaller than 5.
2. A correct baseline
For each node, collect every value below it and check them directly.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val, self.left, self.right = val, left, right
def values(root):
result, pending = [], [root]
while pending:
node = pending.pop()
if node is not None:
result.append(node.val)
pending.extend((node.left, node.right))
return result
def baseline(root):
if root is None:
return True
return (all(x < root.val for x in values(root.left))
and all(x > root.val for x in values(root.right))
and baseline(root.left) and baseline(root.right))
assert baseline(TreeNode(4, TreeNode(2), TreeNode(6)))
assert not baseline(TreeNode(5, TreeNode(3), TreeNode(8, TreeNode(4), TreeNode(9))))
assert not baseline(TreeNode(2, TreeNode(2), TreeNode(3)))
assert baseline(None)
On a chain of n nodes, each node collects everything below it, so this takes O(n²) time. On a 10,000-node chain, that's about 50 million values collected. It uses O(n) extra space.
The bottleneck. It checks each node against every ancestor separately, collecting the same values again and again.
3. The observation
Follow the second example down. At the root 5, any value is allowed. Going right, values must be above 5. Going left from 8, values must be below 8. So 4 must fall strictly between 5 and 8, and it doesn't.
Every ancestor adds a limit, but only the tightest two matter: the largest lower limit and the smallest upper limit. Going left, the current value becomes the new upper limit. Going right, it becomes the new lower limit. Check lower < value < upper, with strict <, which also rejects duplicates. Use None for "no limit". A made-up limit like -10**9 could wrongly reject a real value.
4. The solution and a trace
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val, self.left, self.right = val, left, right
class Solution:
def isValidBST(self, root) -> bool:
"""Acyclic binary tree with integer keys; strict order, empty is valid."""
pending = [(root, None, None)]
while pending:
node, lower, upper = pending.pop()
if node is None:
continue
# Bounds summarize all ancestor restrictions on this subtree.
if lower is not None and node.val <= lower:
return False
if upper is not None and node.val >= upper:
return False
pending.append((node.right, node.val, upper))
pending.append((node.left, lower, node.val))
return True
solve = Solution().isValidBST
assert solve(TreeNode(4, TreeNode(2), TreeNode(6)))
assert not solve(TreeNode(5, TreeNode(3), TreeNode(8, TreeNode(4), TreeNode(9))))
assert not solve(TreeNode(2, TreeNode(2), TreeNode(3)))
assert solve(None)
Trace the second example, skipping empty children:
| Node | Lower | Upper | Action |
|---|---|---|---|
| 5 | None | None | Push both children with their ranges |
| 3 | None | 5 | Fine; no children |
| 8 | 5 | None | Push both children with their ranges |
| 4 | 5 | 8 | Reject: 4 ≤ 5 |
5. Why it works
What stays true. Every node on the stack carries exactly the range its ancestors allow.
Why that stays true. A node that passes lies inside its own range. So the narrower ranges it hands its children still respect every ancestor above it.
A False answer is right. A rejected value breaks some ancestor's rule.
A True answer is right. If every node passes, every value respects every ancestor's left/right rule, so the tree is a BST. The strict < takes care of duplicates.
6. Cost, edge cases, and failure
Cost. Each node is pushed and checked once: O(n) time. The stack holds O(h) entries for a tree of height h, which is O(n) for a chain. There's no recursion, and the tree isn't changed.
Edge cases. Negative values work. So do the smallest and largest 32-bit integers, because a missing limit is None, not a number.
Where it fails. Checking only each node's own children misses the problem:
root = TreeNode(5, TreeNode(3), TreeNode(8, TreeNode(4), TreeNode(9)))
def local_only(node):
# Deliberately wrong: forgets ancestors above the parent.
if node is None:
return True
return ((node.left is None or node.left.val < node.val)
and (node.right is None or node.right.val > node.val)
and local_only(node.left) and local_only(node.right))
assert local_only(root)
assert not solve(root)
assert solve(TreeNode(-(2**31), None, TreeNode(2**31 - 1)))
Going further: random checks, related problems, and a reconstruction exercise
Check it against the baseline
import random
rng = random.Random(98)
def random_tree(depth):
if depth == 0 or rng.randrange(4) == 0:
return None
return TreeNode(rng.randrange(-5, 6), random_tree(depth - 1), random_tree(depth - 1))
def snapshot(root):
nodes, pending = [], [root]
while pending:
node = pending.pop()
if node is not None:
nodes.append((node, node.val, node.left, node.right))
pending.extend((node.left, node.right))
return nodes
for _ in range(500):
root = random_tree(6)
before = snapshot(root)
assert solve(root) == baseline(root)
assert all(node.val == val and node.left is left and node.right is right
for node, val, left, right in before)
# Assign increasing inorder values to also test genuinely valid shapes.
def assign(node, counter):
if node is not None:
assign(node.left, counter)
node.val = next(counter)
assign(node.right, counter)
assign(root, iter(range(1000)))
assert solve(root) and baseline(root)
chain = None
for value in range(9999, -1, -1):
chain = TreeNode(value, None, chain)
assert solve(chain)
Related problems
| Problem | What changes | What follows |
|---|---|---|
| Search in a BST | The tree is promised to be a BST; find a value | Compare and follow one side at each node |
| Balanced Binary Tree | Check heights instead of value order | A different question: a balanced tree can still fail the BST rule |
Reconstruct it
Write the range each child gets before you write the loop. Draw a node that obeys its parent but breaks its grandparent's rule. Then explain why the checks use strict <.