题目描述

✅ 98. 验证二叉搜索树

image-20260928194047175

image-20260928194047176

题意分析

判断一棵二叉树是否为有效的二叉搜索树,返回布尔值。对于每个节点,它左子树中的所有值都必须严格小于该节点,右子树中的所有值都必须严格大于该节点;这个要求要在整棵树的每个位置同时成立。

限制针对整棵子树,不能只检查节点和直接孩子。深层节点既受父节点约束,也受沿途所有祖先约束。因为大小关系是严格的,树中不能出现重复值。节点值可以等于 32 位整数的最小值或最大值,验证边界时不能把这些合法值排除。

解法:递归传递上下界

核心思路

[!blue]

沿着根到当前节点的路径,每次进入右子树都会产生一个“必须大于祖先”的下界,每次进入左子树都会产生一个“必须小于祖先”的上界。把这些条件合并后,当前节点的合法范围就是一个开区间 (lower, upper)。

递归先检查当前值是否落在区间内。若它小于等于下界,或大于等于上界,已经违反祖先约束,可以立即返回 false;若通过,则继续检查两棵子树。

向左递归时,所有左子树节点还必须小于当前值,所以把上界收紧为 node.val,原下界继续保留。向右递归时则把下界收紧为当前值,原上界继续保留。当前值已通过父区间检查,因此新边界一定比对应的旧边界更严格,不会丢掉更紧的祖先限制。

空子树不含任何节点,不会违反限制,返回 true。从根开始不断传递并收紧区间,就把所有祖先的条件带到了每个节点;只有当前节点和左右子树全部合法,整棵子树才合法。

根没有祖先,初始范围应覆盖所有合法节点值。Java 使用比 int 更宽的 long 极值作为边界;Go 用 nil 表示这一侧没有边界,有边界时传递对应节点值的地址。验证过程中只读取节点值,不修改它们。

解题步骤

  1. 从根节点调用验证函数,初始不限制节点的合法 int 取值。
  2. 当前节点为空时返回 true。
  3. 若当前值不在开区间内,立即返回 false。
  4. 用 (lower, node.val) 验证左子树,用 (node.val, upper) 验证右子树。
  5. 只有两次验证都通过才返回 true,遇到任一违规即可短路结束。

代码实现

class Solution {
    public boolean isValidBST(TreeNode root) {
        return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    private boolean validate(TreeNode node, long lower, long upper) {
        if (node == null) {
            return true;
        }

        if (node.val <= lower || node.val >= upper) {
            return false;
        }

        // 左右子树分别继承并收紧当前节点的取值范围。
        return validate(node.left, lower, node.val) && validate(node.right, node.val, upper);
    }
}
func isValidBST(root *TreeNode) bool {
    return validateBST(root, nil, nil)
}

func validateBST(node *TreeNode, lower *int, upper *int) bool {
    if node == nil {
        return true
    }
    if lower != nil && node.Val <= *lower {
        return false
    }
    if upper != nil && node.Val >= *upper {
        return false
    }

    // 递归向下传递当前子树允许的开区间边界。
    return validateBST(node.Left, lower, &node.Val) && validateBST(node.Right, &node.Val, upper)
}

复杂度分析

  • 时间复杂度:$O(n)$,每个节点最多检查一次,发现违规后可以提前结束。
  • 空间复杂度:$O(h)$,h 为树高,来自递归栈。平衡树为 $O(\log n)$,链状树最坏为 $O(n)$。

关键点总结

[!green]

  • 合法区间汇总的是所有祖先限制,当前值通过后才能继续向下收紧。
  • 左递归只更新上界,右递归只更新下界,另一侧限制继续继承。
  • 使用开区间排除重复值,并让初始边界覆盖节点的全部合法取值。

解法二:中序遍历检查严格递增

核心思路

[!blue]

二叉搜索树的中序遍历按“左子树、当前节点、右子树”访问。左子树所有值更小,右子树所有值更大,两棵子树内部也满足相同规则,因此完整的中序序列一定严格递增。

反过来,若整棵树的中序序列严格递增,那么任意节点左子树的值都出现在它之前、且都更小,右子树的值都出现在它之后、且都更大;这正好满足每个节点的搜索树条件。因此,验证 BST 等价于验证中序序列严格递增。

不需要保存完整序列,只记录上一次真正访问的节点 previous。每次访问新节点时,若它的值小于等于前一个值,就直接返回 false;否则更新 previous。所有相邻访问都严格递增,就保证整个序列严格递增。

用显式栈模拟中序:先沿左孩子不断入栈,直到没有左子树,再弹出节点进行比较;随后转到它的右子树,重复同样过程。previous 初始为空,表示还没有访问过节点,不能用某个合法整数值代替这个状态。

解题步骤

  1. 初始化空栈,令当前节点为根,previous 为空。
  2. 当前节点非空时不断入栈并转向左孩子,找到下一次中序访问的位置。
  3. 弹出节点;若已有 previous 且当前值不大于它,返回 false。
  4. 将 previous 更新为当前节点,再转向其右孩子。
  5. 当前节点为空且栈也为空时,所有节点均已通过比较,返回 true。

代码实现

class Solution {
    public boolean isValidBST(TreeNode root) {
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode node = root;
        TreeNode previous = null;

        while (node != null || !stack.isEmpty()) {
            while (node != null) {
                stack.push(node);
                node = node.left;
            }

            node = stack.pop();
            if (previous != null && node.val <= previous.val) {
                return false;
            }

            previous = node;
            node = node.right;
        }

        return true;
    }
}
func isValidBST(root *TreeNode) bool {
    stack := make([]*TreeNode, 0)
    node := root
    var previous *TreeNode

    for node != nil || len(stack) > 0 {
        for node != nil {
            stack = append(stack, node)
            node = node.Left
        }

        node = stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        if previous != nil && node.Val <= previous.Val {
            return false
        }

        previous = node
        node = node.Right
    }

    return true
}

复杂度分析

  • 时间复杂度:$O(n)$,每个节点至多入栈、出栈各一次,首次出现非递增关系时即可结束。
  • 空间复杂度:$O(h)$,栈最多保存一条向下路径上的节点;只额外保存一个前驱节点,无需存下整个中序序列。

关键点总结

[!green]

  • BST 条件与中序序列严格递增互为充要条件,重复值也会在比较中被拒绝。
  • 比较发生在中序弹出节点时,previous 表示上一次访问,不是父节点。
  • 用空引用表示还没有前驱,整数最小值和最大值都可以正常参与验证。

易错点总结

[!yellow]

  • 只比较父节点与直接孩子,会漏掉满足父子关系却违反更早祖先限制的深层节点。
  • 进入某棵子树时清空另一侧边界,会丢失从祖先继承的约束。
  • Java 用 Integer.MIN_VALUE、Integer.MAX_VALUE 作为开区间边界,会错误排除等于极值的合法节点。
  • 无论用区间还是中序法,都要拒绝相等值;只检查大小方向而放过相等,不符合本题要求。
  • 中序法要比较相邻两次真正访问的节点,不能把最近压栈的节点误当成前驱,也不能用一个合法整数值充当“尚无前驱”的哨兵。

相似题目

题目 难度 关联与区别
99. 恢复二叉搜索树 中等 原题BST有两个值被交换,需要定位中序逆序点,本题只检查结构和值是否满足严格有序。
700. 二叉搜索树中的搜索 简单 BST查找依赖全局有序约束,本题验证该约束不能只比较父子局部值。
补充题 204. 二叉搜索树与完全二叉树的判定 中等 都检查二叉搜索树的有序约束;补充题还要按层验证完全二叉树条件。
230. 二叉搜索树中第 K 小的元素 中等 通过中序访问利用二叉搜索树的升序性质;本题验证整个序列严格递增,该题在第 k 次访问时取值。
173. 二叉搜索树迭代器 中等 通过中序访问利用二叉搜索树的升序性质;本题验证整个序列严格递增,该题用栈保存尚未访问的后续节点。
530. 二叉搜索树的最小绝对差 简单 通过中序访问利用二叉搜索树的升序性质;本题验证整个序列严格递增,该题比较相邻中序值的差。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/83568790
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!