📖 第46课:验证二叉搜索树

0 阅读15分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第46课:验证二叉搜索树

模块:二叉树 | 难度:Medium ⭐⭐⭐ LeetCode 链接:leetcode.cn/problems/va… 前置知识:第39课(二叉树中序遍历) 预计学习时间:25分钟


🎯 题目描述

给定一个二叉树的根节点,判断它是否是一个有效的二叉搜索树(BST)。

有效的BST定义:

  • 节点的左子树只包含小于当前节点的数
  • 节点的右子树只包含大于当前节点的数
  • 所有左子树和右子树自身也必须是二叉搜索树

示例:

输入: root = [2,1,3]
      2
     / \
    1   3
输出: true
解释: 符合BST定义

输入: root = [5,1,4,null,null,3,6]
      5
     / \
    1   4
       / \
      3   6
输出: false
解释: 根节点的右子树包含3,小于根节点5,违反BST定义

约束条件:

  • 树中节点数范围 [1, 10^4]
  • -2^31 ≤ Node.val ≤ 2^31 - 1

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
单节点root=[1]true基本功能
陷阱样例root=[5,1,4,null,null,3,6]false子树范围检查
重复值root=[2,2,2]false严格大于/小于
最小整数root=[-2147483648]true边界值处理
左子树违规root=[5,4,6,null,null,3,7]false深度范围检查

💡 思路引导

生活化比喻

想象你是图书馆管理员,正在检查书架上的书是否按编号正确排列。

🐌 笨办法:对于每本书,逐一检查它左边的所有书是否都比它小,右边的所有书是否都比它大。这太慢了!每本书要检查很多遍。

🚀 聪明办法1:你知道一个规律——如果按从左到右的顺序扫一遍,正确排列的书架应该呈现"严格递增"的编号。只需一遍扫描就能发现问题!

🎯 聪明办法2:或者为每个区域设定"允许范围"。比如1号货架的书编号应该在[0,100],如果发现150号书在这里,立刻就知道放错了!

关键洞察

BST的中序遍历结果一定是严格递增序列!反过来,如果中序遍历不是严格递增,就不是有效BST。


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:二叉树根节点(可能为空)
  • 输出:布尔值,true表示是有效BST,false表示不是
  • 限制:必须满足BST的三个条件(左子树小于根、右子树大于根、递归成立)

Step 2:先想笨办法(暴力法)

对于每个节点,遍历它的整个左子树确保所有值都小于当前节点,遍历整个右子树确保所有值都大于当前节点。

  • 时间复杂度:O(n²) — 每个节点都要遍历它的所有子孙节点
  • 瓶颈在哪:重复遍历!同一个节点被祖先们检查了多次

Step 3:瓶颈分析 → 优化方向

暴力法的核心问题是"没有利用BST的性质"。

  • 核心问题:我们多次遍历子树来检查值的范围
  • 优化思路1:能否利用"BST中序遍历有序"的性质?只遍历一次!
  • 优化思路2:能否在递归时传递"允许的值范围"?避免重复检查!

Step 4:选择武器

  • 选用1:中序遍历 + 递增性检查(解法一)
  • 理由:BST中序遍历必然递增,一次遍历O(n)即可判断
  • 选用2:递归 + 上下界(解法二,最优解)
  • 理由:递归时携带每个节点的合法范围,避免重复,更直观

🔑 模式识别提示:当题目出现"验证BST"、"检查树的性质",优先考虑"中序遍历"或"递归+边界约束"


🔑 解法一:中序遍历检查递增性

思路

利用BST的核心性质:中序遍历结果必须严格递增。我们进行中序遍历,同时记录上一个访问的节点值,如果当前值≤上一个值,立即返回false。

图解过程

示例: root = [5,1,4,null,null,3,6]
      5
     / \
    1   4
       / \
      3   6

中序遍历顺序: 左 -> 根 -> 右

Step 1: 访问节点1
  prev = None
  当前值 = 1
  ✅ 1 > None(初始), 更新prev=1

Step 2: 访问节点5
  prev = 1
  当前值 = 5
  ✅ 5 > 1, 更新prev=5

Step 3: 访问节点3
  prev = 5
  当前值 = 3
  ❌ 3 < 5, 不满足递增! 返回False

结论: 不是有效BST

边界示例: root = [2,1,3]

      2
     / \
    1   3

中序遍历: 1 -> 2 -> 3
  1: prev=None, 1 > None ✅, prev=1
  2: prev=1, 2 > 1 ✅, prev=2
  3: prev=2, 3 > 2 ✅

全部递增,返回True

Python代码

from typing import Optional


class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right


def isValidBST(root: Optional[TreeNode]) -> bool:
    """
    解法一:中序遍历检查递增性
    思路:BST的中序遍历必然严格递增
    """
    prev = None  # 记录上一个访问的节点值

    def inorder(node):
        nonlocal prev
        if not node:
            return True

        # 递归检查左子树
        if not inorder(node.left):
            return False

        # 检查当前节点:必须大于前一个值
        if prev is not None and node.val <= prev:
            return False
        prev = node.val  # 更新prev为当前值

        # 递归检查右子树
        return inorder(node.right)

    return inorder(root)


# ✅ 测试
root1 = TreeNode(2, TreeNode(1), TreeNode(3))
print(isValidBST(root1))  # 期望输出:True

root2 = TreeNode(5, TreeNode(1), TreeNode(4, TreeNode(3), TreeNode(6)))
print(isValidBST(root2))  # 期望输出:False

root3 = TreeNode(1)
print(isValidBST(root3))  # 期望输出:True (单节点)

复杂度分析

  • 时间复杂度:O(n) — 每个节点访问恰好一次进行中序遍历
    • 具体地说:如果输入规模 n=10000,大约需要 10000 次节点访问
  • 空间复杂度:O(h) — 递归栈深度,h为树高。最坏情况(链状树)O(n),平衡树O(log n)

优缺点

  • ✅ 利用BST核心性质,思路清晰
  • ✅ 只需一次遍历,时间O(n)最优
  • ❌ 需要使用nonlocal或全局变量来追踪prev
  • ❌ 不太直观,需要理解"中序遍历 → 递增"这个性质

🏆 解法二:递归 + 上下界检查(最优解)

优化思路

直接在递归过程中传递每个节点的合法值范围(下界low,上界high)。对于根节点,范围是(-∞, +∞);对于左子树,范围是(low, 父节点值);对于右子树,范围是(父节点值, high)。

💡 关键想法:不需要遍历后再判断,在递归的同时就能携带约束,违反约束立即返回False!

图解过程

示例: root = [5,1,4,null,null,3,6]
      5
     / \
    1   4
       / \
      3   6

Step 1: 检查根节点5
  范围: (-∞, +∞)
  ✅ -∞ < 5 < +∞

  递归左子树(节点1), 范围: (-∞, 5)
  递归右子树(节点4), 范围: (5, +∞)

Step 2: 检查节点1 (左子树)
  范围: (-∞, 5)
  ✅ -∞ < 1 < 5

Step 3: 检查节点4 (右子树)
  范围: (5, +∞)
  ❌ 4 < 5, 违反下界!

返回False

正确BST示例: root = [2,1,3]

      2
     / \
    1   3

检查节点2: 范围(-∞,+∞) ✅
  └─ 检查节点1: 范围(-∞,2) ✅ 1<2
  └─ 检查节点3: 范围(2,+∞) ✅ 3>2

全部通过,返回True

Python代码

def isValidBST_v2(root: Optional[TreeNode]) -> bool:
    """
    解法二:递归 + 上下界检查(最优解)
    思路:递归传递每个节点的合法值范围
    """
    def validate(node, low, high):
        # 空节点认为是有效的
        if not node:
            return True

        # 当前节点值必须在(low, high)开区间内
        if node.val <= low or node.val >= high:
            return False

        # 递归检查左右子树,同时更新上下界
        # 左子树: 上界变为当前节点值
        # 右子树: 下界变为当前节点值
        return (validate(node.left, low, node.val) and
                validate(node.right, node.val, high))

    # 初始范围:负无穷到正无穷
    return validate(root, float('-inf'), float('inf'))


# ✅ 测试
root1 = TreeNode(2, TreeNode(1), TreeNode(3))
print(isValidBST_v2(root1))  # 期望输出:True

root2 = TreeNode(5, TreeNode(1), TreeNode(4, TreeNode(3), TreeNode(6)))
print(isValidBST_v2(root2))  # 期望输出:False

# 陷阱用例:右子树的左孩子违规
root3 = TreeNode(5, TreeNode(4), TreeNode(6, TreeNode(3), TreeNode(7)))
print(isValidBST_v2(root3))  # 期望输出:False (3应该>5)

复杂度分析

  • 时间复杂度:O(n) — 每个节点访问恰好一次
    • 具体地说:n=10000时,恰好10000次递归调用
  • 空间复杂度:O(h) — 递归栈深度,平衡树O(log n),最坏链状O(n)

为什么这是最优解

  • 时间最优:O(n)已经是理论下限(至少要看一遍所有节点)
  • 代码最直观:边界约束一目了然,符合BST定义的直觉
  • 无需额外变量:不需要prev或全局变量,参数传递即可
  • 提前剪枝:一旦发现违规立即返回,无需继续遍历

🐍 Pythonic 写法

利用迭代 + 栈实现中序遍历,避免递归:

def isValidBST_iterative(root: Optional[TreeNode]) -> bool:
    """
    迭代版中序遍历 — 用栈模拟递归
    """
    stack = []
    prev = None
    current = root

    while stack or current:
        # 一路向左,所有左子节点入栈
        while current:
            stack.append(current)
            current = current.left

        # 弹出栈顶(当前最小未访问节点)
        current = stack.pop()

        # 检查递增性
        if prev is not None and current.val <= prev:
            return False
        prev = current.val

        # 转向右子树
        current = current.right

    return True

特点:显式栈替代递归,空间复杂度仍为O(h),但避免了函数调用开销。

⚠️ 面试建议:先写递归版(清晰),如果面试官问"能否用迭代",再展示此版本。面试官更看重你的思考过程,而非代码行数。


📊 解法对比

维度解法一:中序遍历🏆 解法二:递归上下界(最优)
时间复杂度O(n)O(n) ← 同样最优
空间复杂度O(h)O(h) ← 同样最优
代码难度中等(需理解中序性质)简单 ← 直观易懂
面试推荐⭐⭐⭐⭐⭐ ← 首选
适用场景适合熟悉遍历的候选人通用,符合BST定义直觉

为什么解法二是最优解:

  • 虽然两种解法时间空间复杂度相同,但解法二代码更简洁,逻辑更贴合BST定义(左子树<根<右子树的递归约束)
  • 面试时更容易写对,不需要nonlocal或全局变量
  • 边界条件清晰,利用(-∞,+∞)巧妙处理初始状态

面试建议:

  1. 先花30秒口述思路:"BST的定义是递归的,我可以用递归+上下界来验证"
  2. 重点讲解🏆最优解(递归上下界):边写边说"左子树的上界是父节点值,右子树的下界是父节点值"
  3. 如果有时间,可以提到"也可以用中序遍历,因为BST中序遍历递增"
  4. 强调陷阱用例:指出很多人只检查node.left.val < node.val,但忽略了"左子树的所有节点"都要小于根节点

🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你判断一个二叉树是否是有效的二叉搜索树。

:(审题30秒)好的,这道题要求验证BST。我先确认一下:BST的定义是左子树所有节点小于根,右子树所有节点大于根,且左右子树也是BST,对吗?

面试官:没错,还要注意是"严格小于"和"严格大于",不能有相等的情况。

:明白了。我的思路是用递归,同时为每个节点维护一个合法值范围。对于根节点,范围是负无穷到正无穷;对于左子树,上界变成父节点值;对于右子树,下界变成父节点值。如果任何节点的值超出范围,就返回false。时间复杂度O(n),空间复杂度O(h)。

面试官:很好,请写代码。

:(边写边说)我定义一个辅助函数validate,接收节点和上下界。对于空节点返回true。然后检查当前节点值是否在(low, high)开区间内,不在就返回false。最后递归检查左右子树,左子树的上界更新为当前值,右子树的下界更新为当前值。

面试官:测试一下这个用例:[5,1,4,null,null,3,6]

:(手动模拟)根节点5,范围(-∞,+∞)满足。左子树节点1,范围(-∞,5),1<5满足。右子树节点4,范围(5,+∞),但4<5,不满足!返回false。结果正确。

高频追问

追问应答策略
"还有其他方法吗?""可以用中序遍历,因为BST中序遍历结果必然递增。遍历时检查前后值的大小关系即可,时间空间复杂度相同,但需要额外变量记录前一个值。"
"如果允许节点值相等呢?""需要在题目中明确定义:是左边≤根还是右边≥根?然后调整边界检查为≤或≥。标准BST不允许相等。"
"空间能否优化到O(1)?""递归必然用O(h)栈空间。如果强行要求O(1),只能用Morris中序遍历(修改树结构),但会破坏原树且代码复杂,实际中不推荐。"
"如何处理整数溢出?""Python的int类型没有溢出问题。其他语言可以用long long或者用None表示无穷,比较时特殊处理。"

🎓 知识点总结

Python技巧卡片 🐍

# 技巧1:使用float('inf')和float('-inf')表示无穷
low = float('-inf')  # 负无穷
high = float('inf')  # 正无穷
# 好处:避免用None判断,直接比较即可

# 技巧2:nonlocal关键字修改外层变量
def outer():
    count = 0
    def inner():
        nonlocal count  # 声明要修改外层的count
        count += 1
    inner()
    print(count)  # 输出1

# 技巧3:Python的三元表达式用于简洁返回
return validate(left) and validate(right)  # 短路求值

💡 底层原理(选读)

为什么BST中序遍历是递增的?

中序遍历的顺序是:左 → 根 → 右

对于BST,根据定义:左子树所有节点 < 根 < 右子树所有节点。

所以访问顺序是:所有较小的值(左子树) → 根值 → 所有较大的值(右子树),自然呈现递增!

递归的空间复杂度为什么是O(h)?

每次递归调用会在调用栈上保存一个栈帧(包含局部变量和返回地址)。递归深度等于树的高度h,因此最多同时存在h个栈帧,空间复杂度O(h)。平衡树h=O(log n),链状树h=O(n)。

算法模式卡片 📐

  • 模式名称:递归 + 边界约束
  • 适用条件:需要验证树的某种递归性质,且性质可以用"上下界"或"约束条件"表达
  • 识别关键词:"验证BST"、"检查树的性质"、"递归定义"
  • 模板代码:
def validate_tree(node, constraint_param):
    if not node:
        return True  # 空节点满足性质

    # 检查当前节点是否满足约束
    if not satisfies_constraint(node, constraint_param):
        return False

    # 递归检查子树,传递更新后的约束
    left_ok = validate_tree(node.left, update_constraint_for_left(constraint_param))
    right_ok = validate_tree(node.right, update_constraint_for_right(constraint_param))

    return left_ok and right_ok

易错点 ⚠️

  1. 只检查父子关系:错误写法node.left.val < node.val,这只检查了直接孩子,但BST要求整个左子树都小于根!正确做法是用上下界递归传递约束。
  2. 相等值判断错误:BST要求严格大于/小于,<=>=会导致错误。必须用<>
  3. 边界值处理:用None表示无穷时,比较前要判断if prev is not None,否则None <= 值会报错。使用float('inf')更安全。

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:数据库索引验证 — 数据库的B树索引(BST的推广)在插入删除后需要验证结构完整性,用类似的递归+边界检查来自动化测试。
  • 场景2:配置文件校验 — 某些配置系统用树形结构表达层级关系,需要验证配置值是否在允许范围内,可以用递归+约束传递。
  • 场景3:游戏技能树验证 — 游戏中的技能树可能要求"前置技能等级必须更低",验证时用类似BST的递归+边界思路。

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 700. 二叉搜索树中的搜索EasyBST查找利用BST左<根<右的性质,O(h)时间找到目标
LeetCode 701. 二叉搜索树中的插入MediumBST插入找到合适位置,保持BST性质不变
LeetCode 530. 二叉搜索树的最小绝对差EasyBST中序遍历中序遍历得到递增序列,相邻差的最小值
LeetCode 501. 二叉搜索树中的众数EasyBST性质中序遍历时统计相同值的连续出现次数

📝 课后小测

试试这道变体题,不要看答案,自己先想5分钟!

题目:给定一个二叉搜索树的根节点root和一个整数k,请判断BST中是否存在两个节点的值之和等于k。

💡 提示(实在想不出来再点开)

利用BST中序遍历得到递增数组,然后用双指针找两数之和。

✅ 参考答案
def findTarget(root: Optional[TreeNode], k: int) -> bool:
    """
    BST两数之和
    思路:中序遍历 + 双指针
    """
    # Step 1: 中序遍历得到递增数组
    arr = []
    def inorder(node):
        if not node:
            return
        inorder(node.left)
        arr.append(node.val)
        inorder(node.right)
    inorder(root)

    # Step 2: 双指针找两数之和
    left, right = 0, len(arr) - 1
    while left < right:
        total = arr[left] + arr[right]
        if total == k:
            return True
        elif total < k:
            left += 1
        else:
            right -= 1
    return False

结合了BST中序遍历和双指针两个技巧,时间O(n),空间O(n)。


如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。