Problem overview
The method isValidBST gets the root of a binary tree and returns true when it is a valid binary search tree. For every node, every value in its left subtree must be strictly smaller than the node's value, and every value in its right subtree strictly larger. The rule covers the whole subtree, not just the two children. The lesson provides the TreeNode class, with an Int field value and the fields left and right, which are null for a missing child.
The tree [2, 1, 3] is valid. The tree [5, 1, 4, null, null, 3, 6] is not, because the right child of 5 is 4, which is smaller. The tree [5, 4, 6, null, null, 3, 7] is not valid either, but it is the subtle one: each node is in order with its own children (4 < 5 < 6 and 3 < 6 < 7), yet 3 sits in the right subtree of 5, where everything must be larger than 5.
Approach: every node lives inside a range
Comparing each node with only its children misses the third example: a child can be fine against its parent and still break a rule set by a node higher up. The check that works describes what is allowed for each node as a range (low, high), with both ends excluded, that every ancestor contributes to:
- The root can hold any value, so its range is unlimited.
- When the search goes into the left child of a node, everything there must be smaller than that node. The upper bound becomes the node's value, and the lower bound stays as it was.
- When it goes into the right child, the lower bound becomes the node's value, and the upper bound stays.
In [5, 4, 6, null, null, 3, 7], the node 6 gets the range (5, no limit). Its left child, 3, gets the range (5, 6), and 3 is not larger than 5, so the tree is invalid. The rule "the value must be strictly inside the range" is one comparison per node, and each node is visited once, so the time is O(n).
"No limit yet" needs a value that no node can reach. Node values go from -2^31 to 2^31 - 1, the full range of an Int. If the bounds were Int.MinValue and Int.MaxValue, a root holding either of those values would fail the strict comparison and be rejected, although a single-node tree is valid. The bounds are therefore Long values, which are wider than any Int.
Step by step
isValidBST
- Call the helper
validateonrootwithLong.MinValueandLong.MaxValueas the bounds, and return its answer. The root has no ancestors, so nothing limits it yet.
validate
- Declare it above
isValidBSTwith aTreeNodenamednode, twoLongboundslowandhigh, and aBooleanresult. It answers whether every value in the tree undernodelies strictly betweenlowandhigh, and the tree itself is valid. - Empty tree: if
node == null, returntrue. An empty tree breaks no rule, and this check also ends the recursion past a leaf. - Out of range: if
node.value <= lowornode.value >= high, returnfalse. Both comparisons include equality, because duplicates aren't allowed. AnIntcompared with aLongis widened automatically. - Check both subtrees: the left one with the bounds
lowandnode.value, the right one withnode.valueandhigh, joined with&&.
The solution in Scala
class Solution {
def validate(node: TreeNode, low: Long, high: Long): Boolean = {
if (node == null) {
return true
}
if (node.value <= low || node.value >= high) {
return false
}
return validate(node.left, low, node.value) && validate(node.right, node.value, high)
}
def isValidBST(root: TreeNode): Boolean = {
return validate(root, Long.MinValue, Long.MaxValue)
}
}
The Scala details worth noticing:
- Widening from
InttoLongis automatic.node.value <= lowcompares anIntwith aLongwithout a cast, andvalidate(node.left, low, node.value)passes anIntwhere aLongis expected. The widening is always exact. - The bounds are exclusive. A node equal to a bound is rejected, so a tree like
[2, 2, 2]returnsfalse. &&short-circuits. If the left subtree is invalid, the right one isn't searched.- Root values at the edge of
Int.[2147483647]and[-2147483648]both returntrue, becauseLong.MaxValueandLong.MinValuelie beyond any node. nullas an empty tree. The checknode == nullis the base case of the recursion. Without it, the code would readnode.valueon a missing child.
Complexity
Time: O(n). Each node is visited once, and the work per node is two comparisons and two calls. A tree with an invalid node can stop earlier, but the worst case, a valid tree, visits everything.
Space: O(h) for the recursion, where h is the height of the tree. It is at most 800 here, and about log n for a balanced tree.
Edge cases and common mistakes
- Checking only children.
[5, 4, 6, null, null, 3, 7]passes a child-only check and returnstrue, although the right answer isfalse. The bounds from higher nodes are what catch the 3. - Duplicates.
[2, 2, 2]returnsfalse, and so does[2147483647, 2147483647], a root with an equal left child. Both comparisons must reject equality. - Extreme values with
Intbounds. A version usingInt.MinValueandInt.MaxValueas the starting bounds returnsfalsefor the valid one-node trees[2147483647]and[-2147483648]. UsingLongbounds avoids it. - Using the wrong bound on the way down. The left child tightens the upper bound, and the right child tightens the lower bound. Swapping them rejects valid trees.
- Single node. A tree with one node is always valid, whatever its value.
- Lopsided trees. A tree like
[-2147483648, null, 2147483647], with the smallest and the largest value, returnstrue.
Testing
[2, 1, 3]givestrue.[5, 1, 4, null, null, 3, 6]givesfalse.[5, 4, 6, null, null, 3, 7]givesfalse.[2, 2, 2]givesfalse.[2147483647]givestrue.[-2147483648]givestrue.[-2147483648, null, 2147483647]givestrue.[2147483647, 2147483647]givesfalse.
