验证二叉搜索树
BST 的约束是全局的:每个节点要落在祖先划定的 (low, high) 开区间里,只比较父子必然漏判。
问题拆解
判断一棵二叉树是不是二叉搜索树:左子树所有节点都小于根,右子树所有节点都大于根,且左右子树自身也是 BST。
最经典的错误写法是只检查每个节点和它的直接孩子:left.val < node.val < right.val。这个条件太弱了。反例:根为 5,左孩子 3,左孩子的右孩子是 6——每对父子都满足大小关系,但 6 出现在了根的左子树里,比根还大,整棵树不是 BST。
问题出在“小于根”是对整棵左子树的要求,不是只对左孩子。换句话说,每个节点的合法取值范围由它的所有祖先共同决定:往左走一步,上界收紧为父节点的值;往右走一步,下界收紧。只要递归时把这对界传下去,约束就从“局部”变成了“全局”。
BST 的定义写的是“左子树所有节点”,而不是“左孩子”。把祖先的约束浓缩成一对 (low, high) 开区间传下去,是让全局约束可递归的关键。
上下界递归
界的初值是“无穷”。用 Long.MIN_VALUE 之类的哨兵会在节点值恰好取到整型边界时误判,更稳妥的做法是用可空类型表示“无界”。
public boolean isValidBST(TreeNode root) {
return valid(root, null, null); // null 表示无界
}
private boolean valid(TreeNode node, Integer low, Integer high) {
if (node == null) {
return true;
}
// 开区间:必须严格落在 (low, high) 内
if (low != null && node.val <= low) {
return false;
}
if (high != null && node.val >= high) {
return false;
}
return valid(node.left, low, node.val)
&& valid(node.right, node.val, high);
}
def isValidBST(root: Optional[TreeNode]) -> bool:
def valid(node, low, high): # None 表示无界
if node is None:
return True
# 开区间:必须严格落在 (low, high) 内
if low is not None and node.val <= low:
return False
if high is not None and node.val >= high:
return False
return valid(node.left, low, node.val) and valid(node.right, node.val, high)
return valid(root, None, None)
func isValidBST(root *TreeNode) bool {
var valid func(node *TreeNode, low, high *int) bool // nil 表示无界
valid = func(node *TreeNode, low, high *int) bool {
if node == nil {
return true
}
// 开区间:必须严格落在 (low, high) 内
if low != nil && node.Val <= *low {
return false
}
if high != nil && node.Val >= *high {
return false
}
return valid(node.Left, low, &node.Val) && valid(node.Right, &node.Val, high)
}
return valid(root, nil, nil)
}
pub fn is_valid_bst(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
// None 表示无界
fn valid(node: &Option<Rc<RefCell<TreeNode>>>, low: Option<i32>, high: Option<i32>) -> bool {
match node {
None => true,
Some(rc) => {
let n = rc.borrow();
// 开区间:必须严格落在 (low, high) 内
if low.is_some_and(|l| n.val <= l) || high.is_some_and(|h| n.val >= h) {
return false;
}
valid(&n.left, low, Some(n.val)) && valid(&n.right, Some(n.val), high)
}
}
}
valid(&root, None, None)
}
两处细节值得盯一眼。第一,比较必须带等号:BST 不允许相等值,node.val <= low 就要判假——用例里专门有 [2,2,2] 这种树。第二,往左递归时只收紧上界、下界原样传递,往右相反;写反一处,整棵树的判断就会静默出错。
另一条思路是中序遍历:BST 的中序序列严格递增,遍历时记住前驱、发现不增即返回假。两种写法复杂度相同,上下界版更直接地对应定义,中序版则在“找 BST 中第 k 小”这类题里更常用。
复杂度
| 指标 | 复杂度 | 原因 |
|---|---|---|
| 时间 | O(n) |
每个节点检查一次 |
| 空间 | O(h) |
递归栈深度为树高,最坏退化成 O(n) |
可以迁移的模式
- 递归验证全局性质时,把祖先施加的约束整理成参数(这里是一对上下界)随递归下传;
- “无穷”哨兵值有整型边界风险,可空类型(null / None / Option)是更干净的表达;
- 中序遍历严格递增是 BST 的等价刻画,很多 BST 题都从这条性质切入。
看到“验证某种递归定义的结构”,先问一句:约束是局部的还是全局的?只查父子这类局部检查,往往正是出题人埋的坑。