📌 题目链接:98. 验证二叉搜索树 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:树、深度优先搜索(DFS)、二叉搜索树(BST)、中序遍历
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(n)(递归栈或显式栈)
在面试中,验证二叉搜索树(Validate BST) 是一道高频经典题,不仅考察你对 BST 性质的理解,还考验你对递归设计边界条件和中序遍历特性的掌握。很多候选人会误以为“左子节点 < 根 < 右子节点”就足够,但忽略了整棵左子树必须小于根,整棵右子树必须大于根这一全局约束。
本题是 LeetCode Hot100 中关于树结构与搜索性质的关键一环,务必彻底吃透!
🧠 题目分析
题目要求判断一棵二叉树是否为有效的二叉搜索树(Valid BST) 。关键定义如下:
- 节点的左子树只包含严格小于当前节点的数;
- 节点的右子树只包含严格大于当前节点的数;
- 所有子树自身也必须是 BST。
⚠️ 注意: “严格”意味着不能相等! 即 [2,2,2] 不是有效 BST。
常见误区:
- 仅比较
root->left->val < root->val < root->right->val❌(无法保证子树整体满足) - 忽略整棵树的上下界传递(例如右子树中的左节点仍需 > 根节点)❌
因此,必须采用全局视角进行验证。
🔑 核心算法及代码讲解
本题有两种主流解法,均达到最优时间复杂度 O(n),但在思想上有本质区别:
✅ 方法一:递归 + 上下界约束(推荐!面试首选)
核心思想:
每个节点的值必须落在一个动态变化的开区间 (lower, upper) 内。
- 初始时,根节点的合法范围是
(-∞, +∞); - 进入左子树时,上界更新为当前节点值(因为左子树所有值必须 < 当前值);
- 进入右子树时,下界更新为当前节点值(因为右子树所有值必须 > 当前值)。
💡 为什么用开区间?因为题目要求“严格小于/大于”,不能等于。
📜 C++ 代码(带逐行注释)
// 辅助函数:判断以 root 为根的子树是否在 (lower, upper) 范围内
bool helper(TreeNode* root, long long lower, long long upper) {
// 空节点视为合法 BST
if (root == nullptr) {
return true;
}
// 当前节点值不在 (lower, upper) 开区间内 → 非法
if (root->val <= lower || root->val >= upper) {
return false;
}
// 递归检查左子树(上界变为 root->val)和右子树(下界变为 root->val)
return helper(root->left, lower, root->val) &&
helper(root->right, root->val, upper);
}
bool isValidBST(TreeNode* root) {
// 初始调用:整个树的值应在 (LONG_MIN, LONG_MAX) 范围内
return helper(root, LONG_MIN, LONG_MAX);
}
🎯 面试加分点:
- 使用
long long避免INT_MIN / INT_MAX边界溢出(题目允许 val = -2³¹ 或 2³¹-1);- 明确说明为何用
LONG_MIN / LONG_MAX而不是INT_MIN / INT_MAX;- 强调“开区间”设计是为了满足“严格不等”。
✅ 方法二:中序遍历 + 单调递增校验
核心思想:
BST 的中序遍历结果一定是严格递增序列。
因此,我们可以在中序遍历过程中,记录前一个访问的节点值,若当前值 ≤ 前一个值,则非法。
💡 中序遍历顺序:左 → 根 → 右,天然符合 BST 的升序特性。
📜 C++ 代码(迭代版,避免递归栈)
bool isValidBST(TreeNode* root) {
stack<TreeNode*> stk;
long long prev = (long long)INT_MIN - 1; // 初始化为比最小值还小
while (!stk.empty() || root != nullptr) {
// 一路向左到底
while (root != nullptr) {
stk.push(root);
root = root->left;
}
// 弹出栈顶(当前子树的最左节点)
root = stk.top();
stk.pop();
// 检查是否破坏递增性
if (root->val <= prev) {
return false;
}
prev = root->val; // 更新前驱值
root = root->right; // 转向右子树
}
return true;
}
🎯 面试对比分析:
- 递归法:逻辑清晰,体现对 BST 定义的直接应用,代码简洁;
- 中序法:利用 BST 的衍生性质,适合已知“中序=升序”的场景;
- 若面试官问“哪种更好?”,可答:“递归法更贴近定义,中序法空间局部性更好(迭代),但两者复杂度相同。”
🧩 解题思路(分步拆解)
递归法步骤:
-
定义递归函数:
helper(node, lower, upper)表示 node 子树所有值 ∈ (lower, upper); -
终止条件:node 为空 → 合法;
-
当前检查:若
node->val不在 (lower, upper) → 返回 false; -
递归左右:
- 左子树:新上界 =
node->val; - 右子树:新下界 =
node->val;
- 左子树:新上界 =
-
返回:左右子树都合法才合法。
中序遍历步骤:
- 初始化栈和
prev = 极小值; - 循环直到栈空且当前节点为空;
- 向左走到底,沿途入栈;
- 弹出栈顶,检查是否 ≤
prev→ 是则非法; - 更新 prev,转向右子树;
- 全程无违规 → 合法。
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 递归 + 边界 | O(n) | O(h) ≈ O(n) 最坏 | 逻辑直接,易理解 | 递归深度大时可能栈溢出 |
| 中序遍历(迭代) | O(n) | O(h) ≈ O(n) 最坏 | 避免递归,可控栈 | 需额外维护栈和前驱值 |
📌 h 为树高,最坏情况(链状树)h = n,平均情况(平衡树)h = log n。
💻 完整可运行代码
C++ 版本
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
bool helper(TreeNode* root, long long lower, long long upper) {
if (root == nullptr) {
return true;
}
if (root->val <= lower || root->val >= upper) {
return false;
}
return helper(root->left, lower, root->val) && helper(root->right, root->val, upper);
}
bool isValidBST(TreeNode* root) {
return helper(root, LONG_MIN, LONG_MAX);
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 示例 1: [2,1,3] → true
TreeNode* root1 = new TreeNode(2);
root1->left = new TreeNode(1);
root1->right = new TreeNode(3);
Solution sol;
cout << "Example 1: " << sol.isValidBST(root1) << "\n"; // 输出 1
// 示例 2: [5,1,4,null,null,3,6] → false
TreeNode* root2 = new TreeNode(5);
root2->left = new TreeNode(1);
root2->right = new TreeNode(4);
root2->right->left = new TreeNode(3);
root2->right->right = new TreeNode(6);
cout << "Example 2: " << sol.isValidBST(root2) << "\n"; // 输出 0
return 0;
}
JavaScript 版本
// Definition for a binary tree node.
function TreeNode(val, left, right) {
this.val = (val === undefined ? 0 : val);
this.left = (left === undefined ? null : left);
this.right = (right === undefined ? null : right);
}
var isValidBST = function(root) {
const helper = (node, lower, upper) => {
if (node === null) return true;
if (node.val <= lower || node.val >= upper) return false;
return helper(node.left, lower, node.val) &&
helper(node.right, node.val, upper);
};
return helper(root, -Infinity, Infinity);
};
// 测试
let root1 = new TreeNode(2);
root1.left = new TreeNode(1);
root1.right = new TreeNode(3);
console.log("Example 1:", isValidBST(root1)); // true
let root2 = new TreeNode(5);
root2.left = new TreeNode(1);
root2.right = new TreeNode(4);
root2.right.left = new TreeNode(3);
root2.right.right = new TreeNode(6);
console.log("Example 2:", isValidBST(root2)); // false
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!