想系统提升编程能力、查看更完整的学习路线,欢迎访问 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或全局变量
- 边界条件清晰,利用(-∞,+∞)巧妙处理初始状态
面试建议:
- 先花30秒口述思路:"BST的定义是递归的,我可以用递归+上下界来验证"
- 重点讲解🏆最优解(递归上下界):边写边说"左子树的上界是父节点值,右子树的下界是父节点值"
- 如果有时间,可以提到"也可以用中序遍历,因为BST中序遍历递增"
- 强调陷阱用例:指出很多人只检查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
易错点 ⚠️
- 只检查父子关系:错误写法
node.left.val < node.val,这只检查了直接孩子,但BST要求整个左子树都小于根!正确做法是用上下界递归传递约束。 - 相等值判断错误:BST要求严格大于/小于,
<=或>=会导致错误。必须用<和>。 - 边界值处理:用
None表示无穷时,比较前要判断if prev is not None,否则None <= 值会报错。使用float('inf')更安全。
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
- 场景1:数据库索引验证 — 数据库的B树索引(BST的推广)在插入删除后需要验证结构完整性,用类似的递归+边界检查来自动化测试。
- 场景2:配置文件校验 — 某些配置系统用树形结构表达层级关系,需要验证配置值是否在允许范围内,可以用递归+约束传递。
- 场景3:游戏技能树验证 — 游戏中的技能树可能要求"前置技能等级必须更低",验证时用类似BST的递归+边界思路。
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 700. 二叉搜索树中的搜索 | Easy | BST查找 | 利用BST左<根<右的性质,O(h)时间找到目标 |
| LeetCode 701. 二叉搜索树中的插入 | Medium | BST插入 | 找到合适位置,保持BST性质不变 |
| LeetCode 530. 二叉搜索树的最小绝对差 | Easy | BST中序遍历 | 中序遍历得到递增序列,相邻差的最小值 |
| LeetCode 501. 二叉搜索树中的众数 | Easy | BST性质 | 中序遍历时统计相同值的连续出现次数 |
📝 课后小测
试试这道变体题,不要看答案,自己先想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 学习资料都在这里,后续复习和拓展会更省时间。