Problem overview
The method isValidBST receives the root of a binary tree and returns true if 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 must be strictly larger. The rule covers the whole subtree, not only the children. The lesson provides TreeNode, with an int val and two TreeNode * fields, left and right.
The tree with root 2 and children 1 and 3 is valid. The tree with root 5, left child 1 and right child 4 (whose children are 3 and 6) is not: 4 is in the right subtree of 5 but is smaller than 5. The third example is the instructive one: root 5 with children 4 and 6, and 6 has children 3 and 7. Every node is in order with its own children, since 4 < 5 < 6 and 3 < 6 < 7, but the 3 sits in the right subtree of 5, where every value must be larger than 5. The answer is false.
Approach: every node lives inside a range
Comparing each node only with its children is not enough, as the third example shows. A node's value has to respect every ancestor. Going down from the root, each ancestor narrows the allowed range:
- Going into the left child of a node with value
vmeans every value there must be belowv, so the upper bound becomesv. - Going into the right child means every value there must be above
v, so the lower bound becomesv.
The other bound is inherited unchanged. A node is valid when its value lies strictly between the current lower and upper bounds, and both of its subtrees are valid with their own tightened ranges. The root starts with no limits at all.
In the third example, the 6 is reached with the range (5, no limit), so it fits. Its left child 3 is reached with the range (5, 6), and 3 is not above 5, so the answer is false.
"No limit" needs a value that no node can reach. Node values range over all 32-bit integers, so neither INT_MIN nor INT_MAX can stand for "unbounded": a node holding that very value would be rejected, because the check is strict. The lesson uses the 64-bit type long long for the bounds, with LLONG_MIN and LLONG_MAX as the starting limits. Every int is strictly between them.
Step by step
- Call a helper from
isValidBST:validate(root, LLONG_MIN, LLONG_MAX), and return its answer. The root has nothing above it, so nothing limits its value yet. - Write
validateaboveisValidBST. It returns abooland takes aTreeNode*namednodeand twolong longbounds,lowandhigh. It answers whether every value in the subtree undernodeis larger thanlowand smaller thanhigh. - Accept an empty subtree: if
!node, returntrue. An empty tree breaks no rule, and this test ends the recursion past the leaves. - Reject a value out of range: if
node->val <= low || node->val >= high, returnfalse. The comparison is strict, so a value equal to a bound is rejected too, which makes duplicates invalid. - Check both subtrees with
&&: the left one gets the range (low,node->val) and the right one gets (node->val,high).
The solution in C++
class Solution {
public:
bool validate(TreeNode* node, long long low, long long high) {
if (!node) {
return true;
}
if (node->val <= low || node->val >= high) {
return false;
}
return validate(node->left, low, node->val) && validate(node->right, node->val, high);
}
bool isValidBST(TreeNode* root) {
return validate(root, LLONG_MIN, LLONG_MAX);
}
};
C++ details worth a closer look:
- Why
long long. A tree holding the single valueINT_MAXis a valid BST. A version ofvalidatewithintbounds andINT_MINandINT_MAXas the starting limits returnsfalsefor it, becauseINT_MAX >= high. Withlong longbounds it returnstrue.LLONG_MINandLLONG_MAXare -9223372036854775808 and 9223372036854775807. - Mixed comparisons.
node->valis anintandlowis along long. C++ converts theinttolong longbefore comparing, so the comparison is exact. Passingnode->valas along longargument converts the same way. !node. A null pointer is false in a condition, so!nodeis true exactly whennodeisnullptr.- Short-circuit
&&. If the left subtree is invalid, the right subtree is not examined at all. The answer is alreadyfalse. - Constants from
<climits>.LLONG_MINandLLONG_MAXare defined there. In a file of your own, add#include <climits>or#include <bits/stdc++.h>.
Complexity
Time: O(n), where n is the number of nodes. Each node is visited once, and each visit does a constant amount of work: two comparisons and two calls.
Space: O(h), where h is the height of the tree, for the recursion stack. A balanced tree has h around log n, and a chain has h equal to n. The constraints keep the depth at 800 levels or less.
Edge cases and common mistakes
- Checking only the children. The third example passes a children-only check and is still invalid. Passing the bounds down is what catches it.
- Duplicates. The rule is strict, so a node with a left child holding the same value is invalid. Root 2 with left child 2 returns
false. Using<and>in the rejection test, instead of<=and>=, would accept it. - Extreme values. A single node with
INT_MAXorINT_MINis valid. So is rootINT_MINwith right childINT_MAX. Both work because the starting bounds lie beyond everyint. - Using the parent's value only. Replacing both bounds at each step with the parent's value forgets the earlier ancestors, which is the same error as comparing with the children alone.
- Dereferencing null. The
!nodetest has to come first. Readingnode->valon a null pointer is undefined behaviour. - A single node. A tree of one node is valid, whatever its value.
Testing
- Root 2 with children 1 and 3 gives
true. - Root 5 with children 1 and 4, where 4 has children 3 and 6, gives
false. - Root 5 with children 4 and 6, where 6 has children 3 and 7, gives
false. - Root 2 with a left child 2 gives
false. - Root 2 with children 2 and 2 gives
false. - A single node
INT_MAXgivestrue, and so does a single nodeINT_MIN. - Root
INT_MINwith right childINT_MAXgivestrue.
