LeetCode 经典题目「98. 验证二叉搜索树」,这道题是二叉搜索树(BST)的核心基础题,考察对 BST 定义的理解和二叉树遍历的应用。题目难度中等,但容易踩坑,今天就带大家吃透两种主流解法,从迭代到递归,帮大家理清思路、避开误区。
一、题目解读:什么是“有效二叉搜索树”?
先明确题目核心要求,有效二叉搜索树(BST)必须满足 3 个条件,缺一不可:
-
节点的左子树 只包含严格小于 当前节点的数(不能等于);
-
节点的右子树 只包含严格大于 当前节点的数(不能等于);
-
所有左子树和右子树自身,也必须是有效的二叉搜索树(递归约束)。
这里有个常见误区:很多人会误以为“左孩子小于根、右孩子大于根”就够了,但其实不对——比如根节点是 5,左子树的右孩子是 6,虽然 6 大于左孩子,但小于根节点 5,这种情况就不是有效 BST。所以必须约束“整个左子树都小于根,整个右子树都大于根”,而不只是单个子节点。
题目给出的 TreeNode 定义如下(TypeScript 版本),大家可以先熟悉结构:
class TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val)
this.left = (left === undefined ? null : left)
this.right = (right === undefined ? null : right)
}
}
二、解法一:迭代式中序遍历(推荐,不易踩坑)
核心思路
二叉搜索树有一个关键特性:中序遍历(左 → 根 → 右)的结果是严格递增的序列。比如一个有效 BST,中序遍历后会得到一个从小到大的有序数组,只要遍历过程中发现当前节点值 ≤ 前一个节点值,就说明不是有效 BST。
基于这个特性,我们用“栈”实现迭代式中序遍历,记录前一个节点的值,逐个验证即可。
完整代码(TypeScript)
function isValidBST_1(root: TreeNode | null): boolean {
if (!root) return true; // 空树也是有效BST
const stack: TreeNode[] = []; // 用栈存储节点,实现中序遍历
let current: TreeNode | null = root; // 当前遍历的节点
let prev: number = -Infinity; // 记录前一个节点的值,初始为负无穷
// 迭代式中序遍历:左-根-右
while (current !== null || stack.length > 0) {
// 第一步:遍历到当前节点的最左侧,所有左孩子入栈
while (current !== null) {
stack.push(current);
current = current.left;
}
// 第二步:弹出栈顶节点(此时是最左侧节点,即当前“根”)
current = stack.pop()!;
// 验证:当前节点值必须严格大于前一个节点值
if (current.val <= prev) {
return false;
}
// 更新前一个节点值为当前节点值
prev = current.val;
// 第三步:遍历右子树
current = current.right;
}
// 所有节点验证通过,返回true
return true;
}
逐行解析
-
边界处理:如果根节点为 null,直接返回 true(题目默认空树是有效 BST);
-
栈初始化:用栈来暂存节点,因为中序遍历需要“先左后根再右”,栈的后进先出特性刚好适配;
-
外层循环:current 不为 null(还有左孩子要遍历)或栈不为空(还有节点未处理)时,继续循环;
-
内层循环:将 current 及其所有左孩子依次入栈,直到 current 为 null(找到最左侧节点);
-
弹出栈顶节点:此时弹出的是当前子树的“根”(最左侧节点的父节点,或最左侧节点本身);
-
验证逻辑:如果当前节点值 ≤ 前一个节点值,违反 BST 特性,直接返回 false;
-
更新 prev:将 prev 设为当前节点值,为下一次验证做准备;
-
遍历右子树:处理完当前“根”后,转向右子树,重复上述流程。
复杂度分析
-
时间复杂度:O(n),n 是二叉树的节点数,每个节点入栈、出栈各一次,遍历一次;
-
空间复杂度:O(n),最坏情况下(二叉树为左斜树或右斜树),栈需要存储所有节点。
三、解法二:递归法(更简洁,需理解边界约束)
核心思路
递归的核心是“给每个节点设定合法的取值范围”:
-
根节点的取值范围是(-∞,+∞),因为没有父节点约束;
-
左孩子的取值范围是(父节点的最小值,父节点值),必须严格小于父节点;
-
右孩子的取值范围是(父节点值,父节点的最大值),必须严格大于父节点;
通过递归,给每个节点传递其合法的 min 和 max,只要节点值超出范围,就返回 false;否则递归验证左、右子树。
完整代码(TypeScript)
function isValidBST_2(root: TreeNode | null): boolean {
if (!root) return true; // 空树有效
// 辅助递归函数:参数为当前节点、当前节点的最小值约束、最大值约束
const helper = (node: TreeNode | null, min: number, max: number): boolean => {
if (!node) return true; // 叶子节点的子节点为null,有效
// 节点值超出约束范围,无效
if (node.val <= min || node.val >= max) {
return false;
}
// 递归验证左子树和右子树:
// 左子树的最大值是当前节点值,最小值不变
// 右子树的最小值是当前节点值,最大值不变
return helper(node.left, min, node.val) && helper(node.right, node.val, max);
}
// 根节点的约束:min=-∞,max=+∞
return helper(root, -Infinity, Infinity);
};
逐行解析
-
边界处理:空树返回 true,和解法一一致;
-
辅助函数 helper:接收三个参数——当前节点 node、节点的最小合法值 min、最大合法值 max;
-
递归终止条件:如果 node 为 null(叶子节点的左/右子树),返回 true(空子树有效);
-
验证逻辑:如果 node.val ≤ min 或 node.val ≥ max,说明超出合法范围,返回 false;
-
递归调用:
-
左子树:min 不变(继承父节点的 min),max 设为当前节点值(左子树必须小于当前节点);
-
右子树:max 不变(继承父节点的 max),min 设为当前节点值(右子树必须大于当前节点);
-
-
根节点调用:初始 min 为 -∞,max 为 +∞,因为根节点没有父节点约束。
复杂度分析
-
时间复杂度:O(n),每个节点被递归访问一次;
-
空间复杂度:O(n),最坏情况下(二叉树为斜树),递归调用栈的深度等于节点数。
四、两种解法对比 & 避坑指南
解法对比
| 解法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 迭代中序遍历 | 直观,贴合 BST 特性,不易踩坑 | 代码稍长,需要手动维护栈 | 新手入门,避免递归栈溢出 |
| 递归法 | 代码简洁,逻辑清晰 | 容易忽略边界约束(如 min/max 传递) | 熟悉递归,追求代码简洁 |
常见坑点提醒
-
坑点 1:只判断“左孩子 < 根 && 右孩子 > 根”,忽略整个子树的约束(如前面提到的“根5,左子树右孩子6”的情况);
-
坑点 2:忘记“严格小于/大于”,用了 ≤ 或 ≥(题目明确要求严格,比如两个相同值的节点,即使在不同子树,也不是有效 BST);
-
坑点 3:递归时未正确传递 min/max,比如左子树的 max 未设为当前节点值,导致约束失效;
-
坑点 4:初始 prev 值设为 0(而非 -Infinity),当根节点值为负数时,会直接返回 false(比如根节点是 -5,prev=0,-5 ≤ 0,误判无效)。
五、总结
这道题的核心是吃透二叉搜索树的定义和特性:要么利用“中序遍历严格递增”的特性做迭代验证,要么用递归给每个节点设定约束范围。两种解法各有优势,新手可以先从迭代法入手,理解中序遍历的流程,再尝试递归法简化代码。
建议大家动手敲一遍代码,测试几个边界案例(比如空树、单节点树、左斜树、右斜树、包含相同值的树),加深对两种解法的理解。