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, so a node's value must also fit every node above it. The lesson provides the TreeNode class, with val, left and right, as shown in a comment in the starter code.
The tree [2, 1, 3] is valid. The tree [5, 1, 4, None, None, 3, 6] is not, because the right child of 5 is 4, which is smaller. The tree [5, 4, 6, None, None, 3, 7] is not valid either, and it is the instructive one. Each 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 inside a range
Comparing each node only to its children, as the third example shows, misses violations that involve a grandparent or something further up. The fix is to carry the information down. Each node must lie strictly between a low and a high bound that its ancestors set.
- The root has no ancestors, so it has no bounds: both are
None. - Going into the left child of a node with value
v, the upper bound becomesvand the lower bound stays what it was. - Going into the right child, the lower bound becomes
vand the upper bound stays what it was.
The bounds are None and not a very small or large number because node values may be as small and as large as the 32-bit integer limits: any number picked to mean "no limit" could itself be a value in the tree. With None, "no bound on this side" can't be confused with a real value.
For [5, 4, 6, None, None, 3, 7] the helper visits the nodes with these bounds:
node 5, low None, high None
node 4, low None, high 5
node 6, low 5, high None
node 3, low 5, high 6
The node 3 has to be larger than 5 and smaller than 6, which no value can be, so the check fails there and the answer is False. Each node is visited at most once, so the time is O(n).
Step by step
- In
isValidBST, call the helper onrootwithNonefor both bounds and return its answer. - Declare the helper
validate(self, node, low, high). It returnsTruewhen every value in the tree undernodeis larger thanlowand smaller thanhigh, where aNonebound limits nothing. - Empty tree: if
not node, returnTrue. An empty tree breaks no rule, and this is also where the recursion ends below a leaf. - Lower bound: if
low is not Noneandnode.val <= low, returnFalse. The comparison is "less than or equal" because values must be strictly larger, so a value equal to the bound is a violation too. WhenlowisNone,andstops before the comparison. - Upper bound: likewise, if
high is not Noneandnode.val >= high, returnFalse. - Recurse: return the
andof two calls. The left subtree getslowandnode.val, the right subtree getsnode.valandhigh.
The solution in Python
class Solution:
def validate(self, node, low, high):
if not node:
return True
if low is not None and node.val <= low:
return False
if high is not None and node.val >= high:
return False
return self.validate(node.left, low, node.val) and self.validate(node.right, node.val, high)
def isValidBST(self, root: Optional[TreeNode]) -> bool:
return self.validate(root, None, None)
Python details:
- Use
is not Nonefor the bounds, not truthiness. A bound can be 0, which is falsy. Writingif low and node.val <= lowwould skip the check for a bound of 0 and accept invalid trees. The comparison withNoneasks only the question that matters. not nodeis fine for the node itself, because aTreeNodeobject is always truthy andNoneis falsy.- Short-circuit
and. If the left subtree is already invalid, the right one isn't searched. - No integer limits. Python integers have no fixed size, so values at the 32-bit extremes need no special care. The
Nonebounds matter for correctness of the idea, not because of overflow.
Complexity
The time is O(n): each node is checked once, with constant work. The space is the recursion stack, O(h) for a tree of height h, which is O(n) for a chain.
Edge cases and common mistakes
- Comparing only with children. This accepts
[5, 4, 6, None, None, 3, 7], which is invalid. - Allowing equal values.
[2, 2, 2]returnsFalse: a duplicate of the node on either side breaks the strict rule. Using<instead of<=in the checks would accept it. - Using a number for "no bound". The values
-2147483648and2147483647are allowed in a tree. A tree such as[-2147483648]is valid, and so is[2147483647, -2147483648], and a made-up limit can reject one of them. - A single node is always valid.
- A valid-looking subtree in a bad place.
[10, 5, 15, None, None, 6, 20]returnsFalsebecause 6 is in the right subtree of 10 but smaller than it.
Testing
| tree (level order) | result |
|---|---|
[2, 1, 3] | True |
[5, 1, 4, None, None, 3, 6] | False |
[5, 4, 6, None, None, 3, 7] | False |
[2, 2, 2] | False |
[1] | True |
[-2147483648] | True |
[2147483647, -2147483648] | True |
[10, 5, 15, None, None, 6, 20] | False |
