【力扣100题】33.验证二叉搜索树

一、题目描述

给你一个二叉树的根节点 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

相关推荐
To_OC8 小时前
LC 207 课程表:刚学图论那会儿,我连这是拓扑排序都没看出来
javascript·算法·leetcode
To_OC8 小时前
LC 208 实现 Trie 前缀树:曾被名字劝退,写完发现是送分题
javascript·算法·leetcode
BadBadBad__AK10 小时前
线段树维护区间 k 次方和
c++·数学·算法·stl
_清歌1 天前
DSpark 深度解读:DeepSeek-V4 如何用「半自回归」把推理速度提升 85%
算法
统计实现局1 天前
SVD 的三步走:双对角化、Givens 收敛、排序
算法
躬行见万象1 天前
《VLA 系列》UniLab 强化训练 | G1 机器人 |复现
算法
统计实现局1 天前
对称不定分解(Bunch-Kaufman):为什么 Cholesky 不够用
算法
统计实现局1 天前
dqrsl 拆解:拿着 QR 结果能算出哪 5 种东西
算法
统计实现局1 天前
为什么 Cholesky 求逆比 Gauss-Jordan 快一倍——行列式溢出防护详
算法