Problem overview
The function is_valid_bst receives the root of a binary tree, an Option<Box<TreeNode>>, and returns true if it is a valid binary search tree. The lesson provides TreeNode, with an i32 field val and the fields left and right, both Option<Box<TreeNode>>.
The rule is about whole subtrees: 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. Equal values are not allowed. Node values can be any 32-bit integer.
The tree 2 with children 1 and 3 is valid. The tree 5 with children 1 and 4, where 4 has children 3 and 6, is not, because 4 is in the right subtree of 5 and is smaller than 5.
Approach: every node lives in a range
The tempting check compares each node only with its children. It is wrong. Take the tree 5 with children 4 and 6, where 6 has children 3 and 7. Every node is in order with its own children: 4 < 5 < 6 and 3 < 6 < 7. Yet 3 sits in the right subtree of 5, where every value must be larger than 5, so the tree is invalid. The rule reaches past the parent.
The fix is to pass down the range of values a node may hold. The root may hold anything. When the search goes into the left child of a node with value v, everything there must be below v, so the upper bound becomes v and the lower bound stays what it was. For the right child, the lower bound becomes v and the upper bound stays. Each node then only has to be strictly between its two bounds, and all the ancestors' rules are carried by those two numbers.
On the invalid tree above, the helper sees:
node 5 must be in (-9223372036854775808, 9223372036854775807)
node 4 must be in (-9223372036854775808, 5)
node 6 must be in (5, 9223372036854775807)
node 3 must be in (5, 6)
outside
The node 3 must be larger than 5, which it isn't, so the function returns false there and stops.
Another way to validate is to read the values in order (left subtree, node, right subtree) and check that they increase strictly. The bounds approach needs no list, and both take O(n) time.
Step by step
- Start the range wide.
is_valid_bstends with a call toSelf::validateon&root, withi64::MINandi64::MAXas the bounds. The root has no ancestors, so nothing limits it yet. The borrow&rootlends the tree without moving it. - Write the helper's signature.
validatetakesnode: &Option<Box<TreeNode>>and twoi64bounds,lowandhigh, and returns abool: whether every value in the tree undernodeis larger thanlowand smaller thanhigh. - Make the body a
matchonnode. It has no semicolon after it, so its value is the helper's result. - Empty tree.
None => true. An empty tree breaks no rule, and this arm also ends the recursion past a leaf. - Node arm.
Some(node)binds the boxed node. Becausenodeis borrowed,node.valandnode.leftare read, not moved. - Widen the value.
let val = node.val as i64, so it can be compared with thei64bounds. - Reject values outside the range. If
val <= low || val >= high,return false. The comparison is strict on both sides, so a value equal to a bound is invalid. - Check the subtrees. The left one gets
lowandval, and the right one getsvalandhigh. The two calls are joined with&&, with no semicolon.
The solution in Rust
impl Solution {
fn validate(node: &Option<Box<TreeNode>>, low: i64, high: i64) -> bool {
match node {
None => true,
Some(node) => {
let val = node.val as i64;
if val <= low || val >= high {
return false;
}
Self::validate(&node.left, low, val) && Self::validate(&node.right, val, high)
}
}
}
fn is_valid_bst(root: Option<Box<TreeNode>>) -> bool {
Self::validate(&root, i64::MIN, i64::MAX)
}
}
Rust details worth knowing:
- Why
i64bounds. Node values can bei32::MINori32::MAX. If the starting bounds werei32::MINandi32::MAX, the strict test would reject a root holding either one. Withi64, eveni32::MAXis well belowi64::MAX, so a single node holding any 32-bit value is accepted. - Casting with
as i64. Widening ani32never loses information, so the cast is safe. The helper's own calls passval, already ani64. - Matching on a reference.
match nodewherenodeis a&Option<Box<TreeNode>>lets the patternSome(node)bind a reference to the box, thanks to match ergonomics. That is why&node.leftis passed on, to keep borrowing the subtree. - Early
returnin an arm. Thereturn falseexits this call ofvalidate. The last expression of the arm is the value when the check passes. &&short-circuits. If the left subtree is invalid, the right one is not examined.- The tree is only borrowed. Neither function consumes anything inside the tree, so the same tree can be checked again.
Complexity
Time: O(n), where n is the number of nodes. Each node is visited at most once, with a constant amount of work.
Space: O(h), where h is the height of the tree, for the recursion. The lesson limits the depth to 800.
Edge cases and common mistakes
- Duplicates. The tree 2 with two children both 2 is invalid, and so is 1 with a left child 1. A value equal to a bound is rejected, because the rule says strictly smaller and strictly larger.
- A deep violation. The tree 2 with children 1 and 3, where 3 has a right child 2, is invalid. The 2 is in the right subtree of 2, so it would have to be larger than 2.
- Extreme values. A single node holding
i32::MINori32::MAXis valid. So is a rooti32::MAXwith a left childi32::MIN, and a rooti32::MINwith a right childi32::MAX. Bounds ofi32::MINandi32::MAXwould wrongly reject these, which is the reason for the wider type. - Comparing only with children. It accepts the tree 5, 4, 6, 3, 7 from the overview, which is invalid.
- Passing the wrong bound. The left child gets
valas its upper bound, the right child as its lower bound. Swapping them rejects valid trees.
Testing
[2, 1, 3]givestrue.[5, 1, 4, None, None, 3, 6]givesfalse.[5, 4, 6, None, None, 3, 7]givesfalse.[1]givestrue.[2, 2, 2]givesfalse, and[1, 1]givesfalse.[2, 1, 3, None, None, None, 2]givesfalse.[2147483647, -2147483648]givestrue.[-2147483648, None, 2147483647]givestrue.
