Problem overview
The method isValidBST(root) receives the root of a binary tree and returns true when it is a valid binary search tree. For every node, all values in its left subtree must be strictly smaller than the node's value, and all values in its right subtree strictly larger. The rule covers the whole subtree, not only the two children. The lesson provides the TreeNode constructor, with val, left and right.
The tree [2, 1, 3] is valid. The tree [5, 1, 4, null, null, 3, 6] isn't: the right child of 5 is 4, which is smaller than 5. The tree [5, 4, 6, null, null, 3, 7] is the subtle one. Every node is in order with its own children (4 < 5 < 6 and 3 < 6 < 7), but 3 sits in the right subtree of 5, where every value must be larger than 5, so the answer is false.
Approach: give every node a range
Comparing each node only with its children misses the third example. A node's value must also fit the limits set by all of its ancestors. The fix is to carry those limits down the recursion as a range (low, high), with both ends exclusive.
- The root has no limits yet, so it starts with no lower and no upper bound.
- Going into the left child, the upper bound becomes the current node's value: everything on the left must be smaller. The lower bound stays as it was.
- Going into the right child, the lower bound becomes the current node's value, and the upper bound stays.
Every node is checked once against the range it inherits, so the time is O(n). An alternative is to walk the tree in order and check that the values only increase, which also works in O(n). This lesson uses ranges.
The task's note warns that values can be as small as -2147483648 and as large as 2147483647. A bound that means "no limit yet" therefore can't be one of those numbers, or a valid tree holding that value would be rejected. The solution uses null for "no bound".
Step by step
- In
isValidBST, call the helperthis.validate(root, null, null)and return its answer. The root has no limits. - Declare
validate(node, low, high)aboveisValidBST. It returnstruewhen every value in the tree undernodeis larger thanlowand smaller thanhigh, and anullbound limits nothing. - Return
truefor an empty tree.!nodeis true fornull. This also ends the recursion past a leaf. - Check the lower bound. If
low !== nullandnode.val <= low, returnfalse. A value equal to the bound breaks the rule too, since the inequality is strict. - Check the upper bound. If
high !== nullandnode.val >= high, returnfalse. - Recurse. The left subtree gets
(low, node.val)and the right subtree gets(node.val, high). Join the two calls with&&, so both must be valid.
The solution in JavaScript
class Solution {
validate(node, low, high) {
if (!node) {
return true;
}
if (low !== null && node.val <= low) {
return false;
}
if (high !== null && node.val >= high) {
return false;
}
return this.validate(node.left, low, node.val) && this.validate(node.right, node.val, high);
}
isValidBST(root) {
return this.validate(root, null, null);
}
}
The JavaScript details worth knowing:
nullfor "no bound". The guardlow !== null && node.val <= lowuses&&to skip the comparison when there is no bound.!==is true when the two sides differ and converts neither one.- Why not
0or-Infinity. Using0would reject negative values.-Infinitywould also work, butnullwith an explicit check makes the "no limit" case clear. !node. The empty tree isnull, which is falsy, so!nodeistruefor it. A node object is always truthy, even when itsvalis0, so!nodenever fires for a real node.- Short-circuit
&&. If the left subtree is invalid, the right call is skipped, because the answer is alreadyfalse. - Numbers. JavaScript numbers are doubles, so
2147483647and-2147483648are exact and compare correctly.
Complexity
Time: O(n). Each node is visited at most once, and the work per node is two comparisons and two calls.
Space: O(h) for the recursion, where h is the tree's height. The constraints limit it to 800 levels.
Edge cases and common mistakes
- Equal values.
[2, 2, 2]is not valid, because the rule is strict. Using<instead of<=in the checks would accept it. The same applies to[1, null, 1]. - Extreme values.
[2147483647]and[-2147483648, null, 2147483647]are valid. A bound that used one of these numbers for "no limit" would reject them wrongly. - Comparing only with children. It accepts
[5, 4, 6, null, null, 3, 7]. With bounds, the node 3 is checked against the range(5, 6), and the answer isfalse. - A bigger valid tree.
[3, 1, 5, 0, 2, 4, 6]is valid.[10, 5, 15, null, null, 6, 20]is not, because 6 is in the right subtree of 10. - Swapping the bounds. The left child tightens the upper bound and the right child tightens the lower one. Mixing them up rejects valid trees.
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.[3, 1, 5, 0, 2, 4, 6]givestrue.[2147483647]givestrue.
