Hot 100 --- 验证二叉搜索树
本文概览:本文以LeetCode题目"验证二叉搜索树"为例,讲解BST的核心性质和两种验证方法:中序遍历(判断递增)和递归上下限传递,重点详解递归中上下限的传递关系
一、题目

二、题目分析
二叉搜索树(BST)有一个核心性质:左子树的所有值必须小于根节点的值,右子树的所有值必须大于根节点的值。注意是"所有",不是"左孩子小于根"就够了
比如下面这棵树就不是合法的 BST:
1 | 5 |
节点 6 的左孩子是 3,3 < 6 没问题,但 3 < 5 吗?3 在根节点 5 的右子树中,右子树的所有值都必须大于 5,而 3 不满足,所以这不是 BST
难点就在于这个"全量判断"——不仅要保证当前节点和直接父节点的关系,还要保证它和更上层祖先节点的关系。这导致递归条件不好找
这题有两种解法:
- 中序遍历:BST 的中序遍历一定是严格递增的,遍历出来检查即可
- 递归上下限传递:每个节点维护一个取值范围(上限和下限),递归时向子树传递并缩小范围
思路概览
方法一:中序遍历
1 | class Solution { |
方法二:递归上下限传递
1 | class Solution { |
思路简要说明
- 方法一:利用 BST 中序遍历严格递增的性质,遍历时维护前一个值
prev,一旦出现当前值 <= prev就不是 BST - 方法二:每个节点都有一个取值范围
(lower, upper),递归时把范围往子树传递——左子树的上限缩小为当前节点值,右子树的下限缩小为当前节点值
三、思路详解
方法一:中序遍历
BST 有一个重要性质:中序遍历的结果一定是严格递增的
因为在 BST 中,中序遍历的顺序是"左→根→右",而左子树所有值 < 根 < 右子树所有值,所以遍历出来的序列天然是递增的。如果中途出现非递增,就说明不是 BST
代码就是在中序遍历的基础上加一个判断:用 prev 记录前一个遍历到的值,如果当前值 <= prev,直接返回 false
注意 prev 用 long 类型且初始值为 Long.MIN_VALUE,是为了处理节点值为 Integer.MIN_VALUE 的边界情况
方法二:递归上下限传递(重点)
为什么直接递归不好写?
如果按"左根右"整体来看,递归出口很难找——你需要同时保证左子树所有值 < 根,右子树所有值 > 根,而且这个约束还会往更深层传递。比如根的右子树中某个节点,它不仅要大于自己的父节点,还要大于根节点。这种"跨层约束"让人很难一次性想清楚
关键转变:不要看整体,看单个节点
二叉树的递归,核心就是看一个节点。如果我们给每个节点划定一个取值范围,只检查当前节点是否在这个范围内,问题就简单了
上下限的含义
每个节点都有一个取值范围 (lower, upper):
- lower(下限):当前节点必须大于这个值
- upper(上限):当前节点必须小于这个值
根节点没有约束,所以初始范围是 (Long.MIN_VALUE, Long.MAX_VALUE)
上下限怎么传递?
核心规则就两条:
- 往左子树递:上限更新为当前节点值,下限复用当前节点的下限
- 往右子树递:下限更新为当前节点值,上限复用当前节点的上限
为什么?先看第一轮:
1 | 5 |
这一轮很好理解。3 是 5 的左孩子,只需要小于 5;6 是 5 的右孩子,只需要大于 5
关键在下一轮——为什么要"复用"父节点的上下限?看这个例子:
1 | 5 |
节点 6 的范围是 (5, MAX)。现在看 6 的子树:
6 的左孩子 4:
- 4 必须 < 6(因为 4 是 6 的左孩子)→ 上限 = 6
- 4 必须 > 5(因为 4 在 5 的右子树中,右子树所有值 > 5)→ 下限 = 5(复用 6 的下限)
- 所以 4 的范围 =
(5, 6)
6 的右孩子 7:
- 7 必须 > 6(因为 7 是 6 的右孩子)→ 下限 = 6
- 7 没有更上的上限约束 → 上限复用 6 的上限 = MAX
- 所以 7 的范围 =
(6, MAX)
可以看到,4 的下限 5 不是来自 6,而是来自更上层的根节点 5。这就是"复用"的含义——6 把从祖先那里继承来的下限 5 继续传递给左子树
如果不复用,4 的范围会变成 (6, 6) 或者 (MIN, 6),那就无法保证 4 > 5,就会漏判
用图把完整传递过程画出来:
1 | 5 |
4 的范围是 (5, 6),意味着 4 必须满足 5 < 4 < 6,而 4 不满足,所以这不是 BST
再看一个更深层的例子,体会复用的作用:
1 | 10 |
9 的范围是 (10, 11),必须满足 10 < 9 < 11,而 9 < 10,不满足!所以这不是 BST
这就是复用的精髓:9 虽然是 11 的左孩子(9 < 11 没问题),但它同时在 10 的右子树中(必须 > 10),也在 12 的左子树中(必须 < 12),也在 15 的左子树中(必须 < 15)。这些来自祖先的约束,就是通过"复用"一层层传下来的
递归三步分析
回到我们之前讲的递归三步框架:
- 出口:
if (node == null) return true;— 空节点不违反任何约束 - 出口补充:
if (node.val <= lower || node.val >= upper) return false;— 当前节点超出范围 - 递归调用:
- 左子树:
isValidBST(node.left, lower, node.val)— 上限更新为当前节点值,下限复用 - 右子树:
isValidBST(node.right, node.val, upper)— 下限更新为当前节点值,上限复用
- 左子树:
这和之前二叉树递归题的模式完全一样:出口判断 → 递归调用。只不过这里多了一个"上下限传递"的机制
为什么用 long 不用 int?
因为节点值的范围是 Integer.MIN_VALUE 到 Integer.MAX_VALUE。如果用 int 类型的上下限,当节点值恰好等于 Integer.MIN_VALUE 或 Integer.MAX_VALUE 时,就无法区分"边界值"和"无约束"。用 long 并初始化为 Long.MIN_VALUE 和 Long.MAX_VALUE,就不会有这个问题
两种方法对比
| 中序遍历 | 递归上下限传递 | |
|---|---|---|
| 思路 | 利用BST中序递增性质 | 给每个节点维护取值范围 |
| 额外空间 | O(1)(只需prev变量) | O(1)(参数传递) |
| 递归结构 | 中序遍历框架 | 后序/先序都行 |
| 理解难度 | 较低 | 较高(上下限传递关系) |
两种方法时间复杂度都是 O(n),空间复杂度都是 O(h)(h 为树高,递归栈开销)
中序遍历更直观,但递归上下限传递的思想更通用——它体现了"把全局约束分解为局部约束"的思路,在很多涉及区间判断的题目中都会用到

