98. 验证二叉搜索树(Validate Binary Search Tree)¶
频次 ★★★★ · 难度 🟡 · 高频:字节/美团
题目¶
判断一棵树是否是二叉搜索树(BST):左子树所有节点 < 根,右子树所有节点 > 根,且递归成立。
示例:
思路¶
两种主流解法:
- 递归传范围:DFS 时给每个节点传入允许的
(min, max)区间,节点值必须在区间内,然后递归收紧区间 - 中序遍历:BST 的中序遍历是严格升序的,递归中序时检查当前值是否大于上一个值
推荐解法 1,因为更直接、不依赖全局变量。
代码¶
解法一:上下界传参(推荐,无共享状态)¶
public boolean isValidBST(TreeNode root) {
return validate(root, null, null);
}
private boolean validate(TreeNode node, Integer low, Integer high) {
if (node == null) return true;
if (low != null && node.val <= low) return false; // 越下界
if (high != null && node.val >= high) return false; // 越上界
return validate(node.left, low, node.val) // 往左:只收窄上界
&& validate(node.right, node.val, high); // 往右:只收窄下界
}
low/high 是开区间(不含端点),null 代表 ±∞。信息只向下流,纯参数传递。
解法二:中序遍历(利用「中序 = 升序」)¶
private Integer prev = null;
public boolean isValidBST(TreeNode root) {
prev = null; // 每次调用必须重置
return dfs(root);
}
private boolean dfs(TreeNode node) {
if (node == null) return true;
if (!dfs(node.left)) return false; // ① 走左
if (prev != null && node.val <= prev) return false; // ② 访问根:检查
prev = node.val; // ③ 访问根:更新
return dfs(node.right); // ④ 走右
}
必须拆成 isValidBST + dfs 两段:重置只能发生在最外层调用一次;如果写成自递归并在方法开头 prev = null,每一层都会被重置,检查直接失效。
两版对比¶
| 上下界传参 | 中序遍历 | |
|---|---|---|
| 信息流 | 只往下传(参数) | prev 需跨层共享(子→父、父→子) |
| 共享状态 | 无 | 成员变量 / 数组容器 |
| 重置问题 | 无 | 有(每次调用要重置) |
| 额外参数 | low、high |
无 |
| 纯函数性 | ✅ | ❌ |
| 代码量 | 略多(两个参数) | 略少 |
| 适合讲 | 面试首选,不依赖全局状态 | 展示「BST ↔ 中序升序」的双向理解 |
复杂度¶
- 时间:O(n)
- 空间:O(height)
边界条件¶
- 空树:true(空树被视为 BST)
- 单节点:true
- 值取 Integer.MAX_VALUE / MIN_VALUE:用 Integer 包装类(可为 null)避免 int 边界歧义
- 含相等值的树:BST 定义不等(≥或≤即非法)
变式¶
- 501. BST 中的众数:中序遍历统计,利用 BST 顺序性质避免哈希表
- 验证平衡二叉树 + BST(1373):综合两道题
- 将有序数组/链表转换为 BST(108/109):反过程——用单调性建树
易错点¶
<和<=的区别:BST 要求严格小于/大于,相等即非法- 不能只检查
node.left.val < node.val < node.right.val:必须保证整棵子树都在范围内——反例:[10,5,15,null,null,6,20],6 在 10 的右子树但 < 10,只检查父节点会漏掉 - 用
Integer而非int做参数:null表示没有边界,int初始值 0 会误判含Integer.MIN_VALUE的树。也不能用Integer.MIN_VALUE/MAX_VALUE当 ±∞——[2147483647]这种单节点极值树会被误杀 - 上下界版的方向:往左只收窄上界
high = node.val,往右只收窄下界low = node.val,另一个边界原样继承;合并用&&(左右都要合法),不是||
中序版的 prev 为什么不能当参数传¶
prev 的需求是跨层共享:左子树访问完的最后一个值,要能被父节点读到(子→父);父节点的值又要传给右子树(父→子)。而 Java 参数是值传递,Integer 不可变,prev = node.val 只改本层栈帧里的副本:
private boolean dfs(TreeNode node, Integer prev) { // ❌ 错
...
prev = node.val; // 只改本层
return dfs(node.right, prev); // 能往下传,但父层看不到
}
[5,6,null](左孩子 6 > 5)和 [1,1](相等)都会返回 true——因为"当前节点与左子树最大值"的比较丢失了。
三种可行写法:
| 写法 | 代码 | 特点 |
|---|---|---|
| 成员变量 | private Integer prev; + 主方法里 prev = null 重置 |
最简洁,但必须分离 isValidBST/dfs,否则自递归每层都会重置 |
| 数组当容器 | dfs(node, new long[]{Long.MIN_VALUE}),内部 prev[0] = node.val |
引用传递天然共享,每次调用新建、无残留 |
| 上下界传参 | 见上 | 信息只往下传,无需共享状态,纯函数,最优雅 |
方向铁律:prev 是"中序前一个",BST 要求升序,所以失败条件是 node.val <= prev(不是 prev < node.val,写反会把合法树判非法)。注意"漏写 prev = node.val"和"方向写反"这两个 bug 会互相掩盖——都没暴露时,测试全绿其实是假象。
面试追问¶
- 中序遍历解法怎么写? 递归中序维护
prev,每次node.val <= prev即返回 false。展示第二种解法说明"BST ↔ 中序升序"的双向理解
关联题¶
- 同套路:230. BST 中第 K 小的元素 —— 利用中序遍历模版
- 进阶:236. 二叉树的最近公共祖先 —— BST 版 LCA 可利用值范围直接判断方向,比普通二叉树更简单
- 知识点:BST 的递归区间性质见二叉树;中序有序、以及不平衡时退化成链表,见树