Problem overview
The function isValidBST receives the root of a binary tree and returns true when it 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 whole subtrees, not just the two children. The lesson provides the TreeNode class, with an Int field value and nullable left and right children.
The tree with root 2 and children 1 and 3 is valid. The tree with root 5, left child 1 and right child 4, where 4 has children 3 and 6, is not: the 4 sits in the right subtree of 5 and is smaller. The third example is subtler: root 5 has children 4 and 6, and 6 has children 3 and 7. Every node is in order with its own children, but the 3 lies in the right subtree of 5, so the tree is invalid.
Approach: every node lives in a range
Comparing each node only with its children is not enough. Running exactly that check on the third example reports that the tree is fine, which is wrong. A node must fit between all of its ancestors: when you go left from a node, everything below must be smaller than it, and when you go right, everything below must be larger.
So pass a range down the recursion. The root may hold any value. Going to the left child keeps the lower bound and makes the parent's value the new upper bound. Going to the right child keeps the upper bound and makes the parent's value the new lower bound. A node is valid when its value lies strictly inside its range and both of its subtrees are valid with their narrower ranges.
The bounds are the tricky part. Node values range over every 32-bit integer, so no Int can stand for "no limit". A root holding Int.MIN_VALUE would fail a strict test against a lower bound of Int.MIN_VALUE. With Int bounds, a single node holding Int.MIN_VALUE and one holding Int.MAX_VALUE are both reported as invalid. Using Long for the bounds fixes this, since every Int is strictly between Long.MIN_VALUE and Long.MAX_VALUE.
Each node is visited once, so the time is O(n).
Step by step
- Start the check in
isValidBSTby callingvalidate(root, Long.MIN_VALUE, Long.MAX_VALUE)and returning the answer. The root has no ancestors, so nothing limits it. - Write the helper's signature:
validate(node: TreeNode?, low: Long, high: Long): Boolean. It tells whether every value in the tree undernodeis greater thanlowand smaller thanhigh. - Handle an empty tree. If
node == null, returntrue: nothing can break a rule. This also ends the recursion past a leaf. - Check the node's own value. If
node.value <= low || node.value >= high, returnfalse. The comparisons are strict, so a value equal to a bound is invalid too. - Check both subtrees and join them with
&&. The left subtree getslowandnode.value.toLong(), and the right subtree getsnode.value.toLong()andhigh.
The solution in Kotlin
class Solution {
fun validate(node: TreeNode?, low: Long, high: Long): Boolean {
if (node == null) {
return true
}
if (node.value <= low || node.value >= high) {
return false
}
return validate(node.left, low, node.value.toLong()) && validate(node.right, node.value.toLong(), high)
}
fun isValidBST(root: TreeNode?): Boolean {
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE)
}
}
The Kotlin details worth a closer look:
- No automatic widening. Kotlin never converts an
Intto aLongon its own when passing an argument. That is why the calls usenode.value.toLong(). Passing the bareIntwhere aLongparameter is expected does not compile. - Comparisons across the two types are allowed.
node.value <= lowcompares anIntwith aLongdirectly, because the number types have comparison operators for each other. - Smart cast. After the
node == nullcheck returns, Kotlin treatsnodeas non-null, sonode.valueandnode.leftneed no!!. - Short-circuit
&&. When the left subtree is invalid, the right one is never visited. - Int overflow.
Int.MAX_VALUE + 1wraps around to-2147483648without any error. The solution never adds to a value, only compares, so it can't overflow.
Complexity
Time: O(n). Each node is visited at most once, and each visit does two comparisons and two calls.
Space: O(h), where h is the height of the tree, for the recursion stack. The tree is at most 800 levels deep.
Edge cases and common mistakes
- Checking only children. The third example passes a children-only check and still is not a BST, because the 3 under the 6 is smaller than the 5 at the root.
- Equal values. The tree
[2, 2, 2]is invalid, since the rule is strict on both sides. Using<and>in the failing test, instead of<=and>=, would accept it. - Extreme values. A single node holding
Int.MAX_VALUE, orInt.MIN_VALUEas the root withInt.MAX_VALUEas its right child, is valid. Bounds of typeIntreject these,Longbounds accept them. - A single node.
[1]is valid. - Left child equal to the minimum.
[-2147483648, -2147483648]is invalid: the left child is not strictly smaller. - Passing the wrong bound. Giving the left subtree the parent's value as its lower bound, or the right subtree as its upper, flips the logic.
Testing
Trees are written in level order, with null for a missing child.
[2, 1, 3]givestrue.[5, 1, 4, null, null, 3, 6]givesfalse.[5, 4, 6, null, null, 3, 7]givesfalse.[1]givestrue.[2, 2, 2]givesfalse.[2147483647]givestrue.[-2147483648, null, 2147483647]givestrue.[-2147483648, -2147483648]givesfalse.[3, 1, 5, 0, 2, 4, 6]givestrue.
