LeetCode 98. 验证二叉搜索树:两种解法详解

0 阅读7分钟

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;
}

逐行解析

  1. 边界处理:如果根节点为 null,直接返回 true(题目默认空树是有效 BST);

  2. 栈初始化:用栈来暂存节点,因为中序遍历需要“先左后根再右”,栈的后进先出特性刚好适配;

  3. 外层循环:current 不为 null(还有左孩子要遍历)或栈不为空(还有节点未处理)时,继续循环;

  4. 内层循环:将 current 及其所有左孩子依次入栈,直到 current 为 null(找到最左侧节点);

  5. 弹出栈顶节点:此时弹出的是当前子树的“根”(最左侧节点的父节点,或最左侧节点本身);

  6. 验证逻辑:如果当前节点值 ≤ 前一个节点值,违反 BST 特性,直接返回 false;

  7. 更新 prev:将 prev 设为当前节点值,为下一次验证做准备;

  8. 遍历右子树:处理完当前“根”后,转向右子树,重复上述流程。

复杂度分析

  • 时间复杂度: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);
};

逐行解析

  1. 边界处理:空树返回 true,和解法一一致;

  2. 辅助函数 helper:接收三个参数——当前节点 node、节点的最小合法值 min、最大合法值 max;

  3. 递归终止条件:如果 node 为 null(叶子节点的左/右子树),返回 true(空子树有效);

  4. 验证逻辑:如果 node.val ≤ min 或 node.val ≥ max,说明超出合法范围,返回 false;

  5. 递归调用:

    • 左子树:min 不变(继承父节点的 min),max 设为当前节点值(左子树必须小于当前节点);

    • 右子树:max 不变(继承父节点的 max),min 设为当前节点值(右子树必须大于当前节点);

  6. 根节点调用:初始 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,误判无效)。

五、总结

这道题的核心是吃透二叉搜索树的定义和特性:要么利用“中序遍历严格递增”的特性做迭代验证,要么用递归给每个节点设定约束范围。两种解法各有优势,新手可以先从迭代法入手,理解中序遍历的流程,再尝试递归法简化代码。

建议大家动手敲一遍代码,测试几个边界案例(比如空树、单节点树、左斜树、右斜树、包含相同值的树),加深对两种解法的理解。