Problem overview
The method IsValidBST receives root, the root of a binary tree, and returns true when the tree 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 must be strictly larger. The rule covers the whole subtree, not only the two children. The lesson provides the TreeNode class with the fields val, left and right.
[2, 1, 3]is valid: 1 is smaller than 2 and 3 is larger.[5, 1, 4, null, null, 3, 6]is not valid: the right child of 5 is 4, which is smaller than 5.[5, 4, 6, null, null, 3, 7]is not valid, although every node is in order with its own children (4 < 5 < 6 and 3 < 6 < 7). The 3 lies in the right subtree of 5, where every value must be larger than 5.
Approach: pass the allowed range down
The third example is the trap. Comparing each node only with its children accepts that tree, because the mistake is between a node and a grandchild. The fix is to remember what the ancestors demand. Each node must lie strictly inside a range (low, high) that comes from the nodes above it:
- The root can hold any value, so its range is unlimited.
- Going into a left child, the node's value becomes the new upper bound: everything on the left must be smaller.
- Going into a right child, the node's value becomes the new lower bound: everything on the right must be larger.
- The other bound is inherited unchanged, which is how the demands of higher ancestors stay in force.
In the third example, the 3 is reached through the 5's right side, so its range is (5, 6), and the 3 fails it.
The bounds start as "no limit yet". Node values can be as small as -2147483648 and as large as 2147483647, the smallest and largest int. If the unlimited bounds were int.MinValue and int.MaxValue, a node holding one of those values would equal a bound, fail the strict test, and a perfectly valid tree would be rejected. The bounds are therefore long, which holds every int with room to spare.
Each node is visited once, so the time is O(n).
Step by step
- Start in
IsValidBST. Call the helperValidateonrootwithlong.MinValueandlong.MaxValueas the bounds, and return its answer. The root has no ancestors, so nothing limits it yet. - Declare
Validateas apublic boolmethod taking aTreeNode nodeand twolongvalues,lowandhigh. It returnstruewhen every value undernodeis larger thanlowand smaller thanhigh. - Handle the empty tree. If
node == null, returntrue: an empty subtree breaks no rule, and this check also ends the recursion past a leaf. - Check the node's own value. If
node.val <= low || node.val >= high, returnfalse. The test is strict, so a value equal to a bound is also rejected. Anintcompared with alongworks directly because C# widens theint. - Check both subtrees. The left subtree gets
(low, node.val)and the right subtree gets(node.val, high). Join the two recursive calls with&&.
The solution in C#
class Solution {
public bool Validate(TreeNode node, long low, long high) {
if (node == null) {
return true;
}
if (node.val <= low || node.val >= high) {
return false;
}
return Validate(node.left, low, node.val) && Validate(node.right, node.val, high);
}
public bool IsValidBST(TreeNode root) {
return Validate(root, long.MinValue, long.MaxValue);
}
}
The C# details worth knowing:
- Implicit widening. Passing
node.val, anint, where alongparameter is expected needs no cast. Widening to a larger type never loses information, so C# does it automatically. The reverse,longtoint, needs an explicit cast. long.MinValueandlong.MaxValue. These are the constants of the 64-bit type,-9223372036854775808and9223372036854775807. Everyintis strictly between them.- Short-circuit
&&. When the left subtree is already invalid, the right call isn't made. The answer isfalseeither way. - Null checks.
node == nullis how C# tests for a missing child. Readingnode.valon a null node would throw aNullReferenceException, which is why the null test comes first.
Complexity
Time: O(n). Each node is checked once, with a constant amount of work, and the recursion visits each missing child once as well.
Space: O(h), where h is the height of the tree, for the call stack. The statement allows up to 800 levels, which the default stack handles. A chain of n nodes would use O(n) stack.
Edge cases and common mistakes
- Comparing only with the children. This accepts
[5, 4, 6, null, null, 3, 7], which is invalid. In a run, the child-only check returnstruefor that tree, while the bounds version returnsfalse. - Using
intbounds. A single node holding 2147483647 is a valid tree, but a version withint.MinValueandint.MaxValueas the starting bounds returnsfalsefor it, because the value equals the upper bound. - Duplicates. The rule is strict, so
[2, 2, 2]is not valid. Using<where the code uses<=would accept it. - A single node. Any single node is valid, including one holding
int.MinValueorint.MaxValue. - Negative values.
[0, -1]is valid. The bounds handle negatives like any other numbers. - Swapping the bounds. The left child must get the node's value as its upper bound, and the right child as its lower bound.
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, null, 2147483647]givestrue.[0, -1]givestrue.
