Problem overview
The method isValidBST(root) receives the root of a binary tree and returns true if it is a valid binary search tree. The tree is valid when, for every node, all values in its left subtree are strictly smaller than the node's value and all values in its right subtree are strictly larger. The rule covers the whole subtree, not just the two children. The lesson provides the TreeNode type: a node has a number val and two children, left and right, each a node or null.
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, and it shows the real difficulty: 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.
Approach: every node lives in a range
Comparing each node only with its children misses the third tree. What a node has to respect comes from all of its ancestors: each time the path goes left at a node, that node's value becomes an upper limit for everything below, and each time it goes right, that value becomes a lower limit.
So the check carries a range (low, high) down the recursion. The root has no limits. For a node with value v in the range (low, high):
- its value must be strictly above
lowand strictly belowhigh; - its left subtree gets the range
(low, v); - its right subtree gets the range
(v, high).
In [5,4,6,null,null,3,7], the node 3 is reached by going right at 5 and then left at 6, so its range is (5, 6), and 3 is outside it.
A bound that means "no limit yet" can't be a number, because node values can be as small and as large as the smallest and largest 32-bit integers. So the "no limit" is null.
Step by step
- Start the check.
isValidBSTcallsthis.validate(root, null, null)and returns its answer: nothing limits the root yet. - Declare the helper
validate(node: TreeNode | null, low: number | null, high: number | null): boolean. It tells whether every value undernodeis abovelowand belowhigh, where anullbound limits nothing. - Empty tree. If
!node, returntrue. An empty tree breaks no rule, and this also ends the recursion past a leaf. - Check the lower bound. If
low !== null && node.val <= low, returnfalse. The comparison is strict in the other direction: a value equal to the bound breaks the rule. - Check the upper bound. If
high !== null && node.val >= high, returnfalse. - Recurse. The left subtree keeps
lowand getsnode.valas its upper bound. The right subtree getsnode.valas its lower bound and keepshigh. Both must be valid, so the two calls are joined with&&.
The solution in TypeScript
class Solution {
validate(node: TreeNode | null, low: number | null, high: number | null): boolean {
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: TreeNode | null): boolean {
return this.validate(root, null, null);
}
}
Details worth knowing:
- Why the
nullchecks are needed. In JavaScript,nullbecomes 0 in a numeric comparison. Withconst low: number | null = nullandconst val = -3, the expressionval <= lowistrue, so an unguarded check would reject a valid negative value.low !== null && val <= lowisfalse, as it should be. - The union type.
number | nullsays that a bound is either a number ornull. It exists only for the compiler and is erased in the JavaScript output. At run time,!== nullis what tells the cases apart. !node. ATreeNodeobject is always truthy, even when itsvalis 0, so!nodeis true only fornull(orundefined). It checks the node, not the node's value.- Short-circuit
&&. When the left subtree is already invalid, the right one isn't searched, since the answer isfalseanyway. - Number precision. Values up to 2^31 - 1 are exactly representable in a TypeScript
number, so the bounds never lose precision.
Complexity
Time: O(n), where n is the number of nodes. Each node is visited once, plus one call for each missing child, with constant work per call.
Space: O(h) for the recursion, where h is the height of the tree: up to the 800 levels the constraints allow, or O(log n) for a balanced tree.
Edge cases and common mistakes
- Comparing only with the children. This accepts
[5,4,6,null,null,3,7]. Carrying the range is what rejects it. - Duplicates. The inequalities are strict, so
[2,2,2]is not valid, and neither is[1,null,1]. - Extreme values.
[2147483647]and[-2147483648]are valid single nodes. The tree[-2147483648,null,2147483647]is valid too. Using the smallest and largest integers as "no limit" would reject some of these, which is whynullis used. - Zero.
[0,-1]is valid. A check likeif (low && ...)would treat a bound of 0 as "no bound". Comparing with!== nullavoids that. - Single node or empty subtrees. A leaf, or a tree of one node, is always valid.
- Using
<where<=is needed. The rejection tests are<=and>=because equal values break the rule.
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.[-2147483648,null,2147483647]givestrue.[0,-1]givestrue.[3,1,5,0,2,4,6]givestrue.[5,1,6,null,null,5,7]givesfalse: the 5 in the right subtree equals the root.
