一、题目描述
给你一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树。
有效二叉搜索树定义:
- 节点的左子树只包含 严格小于 当前节点的数
- 节点的右子树只包含 严格大于 当前节点的数
- 所有左子树和右子树自身必须也是二叉搜索树
示例
| 示例 | 输入 | 输出 |
|---|---|---|
| 示例1 | root = [2,1,3] |
true |
| 示例2 | root = [5,1,4,null,null,3,6] |
false |
示例1:有效BST 示例2:无效BST
2 5
/ \ / \
1 3 ← 符合BST 1 4 ← 右子树包含3,不大于5
/ \
3 6 ← 3不大于4,违反BST规则
提示
- 树中节点数目范围在
[1, 10^4]内 -2^31 <= Node.val <= 2^31 - 1
二、解题思路总览
| 方法 | 核心思想 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 递归(隐式栈) | 中序遍历 + 前驱节点比较 | O(n) | O(h),h为树高 |
核心思想:
- BST的中序遍历得到升序序列
- 遍历过程中记录前驱节点,与当前节点比较
- 如果前驱节点值 >= 当前节点值,说明不是升序,不是BST
为什么中序遍历?
- BST的左子树 < 根 < 右子树
- 中序遍历顺序:左-根-右,正好是升序
- 升序中断就说明不是BST
三、完整代码
cpp
class Solution {
public:
TreeNode* pre = NULL; // 1. 记录前驱节点
bool isValidBST(TreeNode* root) {
if (!root) return true; // 2. 空树是BST
// 3. 递归检查左子树
bool left = isValidBST(root->left);
// 4. 比较前驱节点与当前节点
// 中序遍历保证:左 < 根 < 右
// 如果前驱 >= 当前,说明不是升序,不是BST
if (pre != NULL && pre->val >= root->val) return false;
// 5. 更新前驱为当前节点
pre = root;
// 6. 递归检查右子树
bool right = isValidBST(root->right);
// 7. 左子树和右子树都必须为BST
return left && right;
}
};
四、算法流程图(ASCII)
中序遍历验证过程
示例1的BST:[2,1,3] 是有效的
2
/ \
1 3
中序遍历过程:
遍历顺序:1 → 2 → 3(升序)
pre的变化:NULL → 1 → 2 → 3
每次比较:pre.val < current.val ✓
示例2的树:[5,1,4,null,null,3,6] 不是有效的BST
5
/ \
1 4
/ \
3 6
中序遍历过程:
遍历顺序:1 → 5 → 3 → 4 → 6
pre的变化:NULL → 1 → 5 → 3
发现问题:pre(5).val >= current(3).val ✗ → 返回false
递归展开过程(以示例1为例)
isValidBST(2)
│
├── isValidBST(1) ← 左子树
│ │
│ ├── isValidBST(NULL) → true
│ │
│ ├── pre = NULL, root->val = 1
│ │ pre != NULL? No → 继续
│ │
│ ├── pre = 1
│ │
│ └── isValidBST(NULL) → true
│
├── pre = 1, root->val = 2
│ pre != NULL? Yes
│ pre.val(1) >= root.val(2)? No → 继续
│
├── pre = 2
│
└── isValidBST(3) ← 右子树
│
├── isValidBST(NULL) → true
│
├── pre = 2, root->val = 3
│ pre != NULL? Yes
│ pre.val(2) >= root.val(3)? No → 继续
│
└── pre = 3
最终返回:true && true && true = true
五、逐行解析
cpp
class Solution {
public:
// ─────────────────────────────────────────
// 全局变量 pre:记录中序遍历的前驱节点
// 初始化为 NULL
// ─────────────────────────────────────────
TreeNode* pre = NULL;
// ─────────────────────────────────────────
// 中序遍历验证BST
// 左-根-右 的顺序保证遍历结果是升序
// ─────────────────────────────────────────
bool isValidBST(TreeNode* root) {
// ─────────────────────────────────────────
// 第1步:递归终止条件
// 空树是有效的BST
// ─────────────────────────────────────────
if (!root) return true;
// ─────────────────────────────────────────
// 第2步:递归检查左子树
// 左子树必须是BST
// ─────────────────────────────────────────
bool left = isValidBST(root->left);
// ─────────────────────────────────────────
// 第3步:比较前驱节点与当前节点
//
// 中序遍历保证遍历结果是升序
// pre 是当前节点的前驱(中序遍历顺序)
// 如果 pre.val >= root.val,说明不是升序
// 不是升序就违反了BST的定义
// ─────────────────────────────────────────
if (pre != NULL && pre->val >= root->val) return false;
// ─────────────────────────────────────────
// 第4步:更新前驱节点
// 当前节点成为下一个节点的前驱
// ─────────────────────────────────────────
pre = root;
// ─────────────────────────────────────────
// 第5步:递归检查右子树
// 右子树必须是BST
// ─────────────────────────────────────────
bool right = isValidBST(root->right);
// ─────────────────────────────────────────
// 第6步:返回结果
// 左子树、右子树、当前节点都符合BST定义才行
// ─────────────────────────────────────────
return left && right;
}
};
六、复杂度分析
时间复杂度
| 分析 | 复杂度 |
|---|---|
| 每个节点访问一次 | O(n) |
推导:n 个节点,每个节点访问一次(比较、递归)。
空间复杂度
| 分析 | 复杂度 |
|---|---|
| 递归调用栈,最大深度为树高h | O(h) |
推导:
- 最坏情况(链表形状):h = n,复杂度 O(n)
- 平衡树情况:h = log n,复杂度 O(log n)
七、面试追问 FAQ
| 问题 | 回答 |
|---|---|
| 为什么用中序遍历? | BST中序遍历是升序,升序中断就说明不是BST |
pre 初始为什么是 NULL? |
第一个节点没有前驱,不需要比较 |
为什么要判断 pre != NULL? |
要先判断再访问 pre->val,避免空指针 |
| 可以用范围判断吗? | 可以,记录每个节点的有效范围 [min, max] |
| 如果节点值等于前驱值怎么办? | 应该返回 false,因为BST要求严格大于 |
八、相关题目
| 题目 | 难度 | 关键点 |
|---|---|---|
| 98. 验证二叉搜索树 | 中等 | 本题 |
| 94. 二叉树的中序遍历 | 简单 | 中序遍历基础 |
| 230. 二叉搜索树中第K小的元素 | 中等 | BST中序遍历 |
| 99. 恢复二叉搜索树 | 困难 | 中序遍历变形 |
九、总结
| 对比项 | 说明 |
|---|---|
| 代码行数 | 核心8行 |
| 时间复杂度 | O(n) |
| 空间复杂度 | O(h) |
| 递归顺序 | 中序遍历(左-根-右) |
| 核心技巧 | 前驱节点比较 |
核心公式:
BST的中序遍历 = 升序序列
验证方法:遍历过程中比较 pre.val < current.val
易错点:
- 不能只比较左右子节点,要确保整个左子树都小于根,整个右子树都大于根
- 使用前驱比较时,要先判断
pre != NULL