【LeetCode Hot100 刷题日记 (43/100)】98. 验证二叉搜索树 —— 递归边界约束 & 中序遍历单调性🌳

56 阅读6分钟

📌 题目链接: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 的衍生性质,适合已知“中序=升序”的场景;
  • 若面试官问“哪种更好?”,可答:“递归法更贴近定义,中序法空间局部性更好(迭代),但两者复杂度相同。”

🧩 解题思路(分步拆解)

递归法步骤:

  1. 定义递归函数helper(node, lower, upper) 表示 node 子树所有值 ∈ (lower, upper);

  2. 终止条件:node 为空 → 合法;

  3. 当前检查:若 node->val 不在 (lower, upper) → 返回 false;

  4. 递归左右

    • 左子树:新上界 = node->val
    • 右子树:新下界 = node->val
  5. 返回:左右子树都合法才合法。

中序遍历步骤:

  1. 初始化栈和 prev = 极小值
  2. 循环直到栈空且当前节点为空;
  3. 向左走到底,沿途入栈;
  4. 弹出栈顶,检查是否 ≤ prev → 是则非法;
  5. 更新 prev,转向右子树;
  6. 全程无违规 → 合法。

📊 算法分析

方法时间复杂度空间复杂度优点缺点
递归 + 边界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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!