题目描述
思路解析
一句话答案:LeetCode 98 验证二叉搜索树的经典判法是中序遍历:合法 BST 按「左、根、右」展开必然得到严格递增序列,遍历时记住上一个值 prev,一旦出现当前值小于等于 prev 就立刻返回 false。每个节点访问一次,时间 O(n),空间 O(h)。
BST 的定义比看起来更严格
二叉搜索树(BST)的约束是:每个节点的「左子树里所有值」都小于它、「右子树里所有值」都大于它——注意是整棵子树,不只是直接的左右孩子。这个「所有」正是本题的陷阱所在:一个值可能和它的父亲比完全正常,却违反了更上层祖先立下的范围约束。判断合法性必须能管住这种跨层的违规。
为什么只比较父子三个节点会漏判
最直觉的做法是对每个节点检查「左孩子 < 我 < 右孩子」。它只保证了局部有序,管不到跨层:比如根是 5、右孩子是 6,而 6 的左孩子是 3——这三个点里 3 < 6 没毛病,但 3 落在根 5 的右子树里,按定义右子树所有值必须大于 5,整棵树其实不合法。逐点的局部检查发现不了,因为约束是祖先传下来的「取值区间」,不是相邻两代的大小关系。
为什么中序遍历能一锤定音
关键观察:把 BST 按「左、根、右」的中序顺序展开,得到的值序列一定是严格递增的。道理可以递归地想——左子树的值全体小于根、又都排在根前面,右子树的值全体大于根、又都排在根后面,两棵子树内部再套用同样的论证。反过来,只要中序序列在某处不增,就说明某个「小于/大于整棵子树」的约束被打破了。于是复杂的树形区间约束被压平成一句话:中序序列是否严格递增。
实现上不用真把序列存出来:中序遍历(递归或用栈迭代)过程中维护一个变量 prev 记住上一个访问的值,每访问一个节点就和 prev 比一次,node.val <= prev 即刻返回 false,全程通过则返回 true。空间从存整个序列的 O(n) 省到 O(h)。
为什么相等也要判不合法
BST 定义要求严格大于、严格小于,值相等同样违规,所以比较必须写 node.val <= prev 而不是 <。这也解释了 prev 的初值为什么用 None(空值)而不是某个「足够小的数」:节点值可能取到整型的最小值,用具体数字兜底就可能在极值输入上误判;先判 prev is not None 再比较,第一个节点天然免比。
另一条路:带上下界的递归
等价的写法是把祖先的约束显式传下去:isValid(node, lo, hi) 要求 lo < node.val < hi,往左走把上界收紧为自己(hi = node.val),往右走把下界收紧为自己(lo = node.val)。这就是把「整棵子树的取值区间」写成了参数,正面解决跨层问题。两种写法都是 O(n) 时间、O(h) 空间;中序版更直观,上下界版则不需要 prev 变量,面试里任选其一都站得住。
复杂度与边界提醒
时间 O(n):每个节点被中序访问恰好一次,比较是常数时间。空间 O(h):显式栈或递归栈的深度等于树高,最坏(链状树)O(n)。
边界上除了前面说的相等值和极值初值,还要记得空树按约定是合法 BST,循环体一次都不进、直接返回 true 即可。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
核心:中序遍历 BST = 把值从小到大排出来。一旦不递增,立刻判否。
主角登场:一棵合法 BST,13 个节点。我们从最左下角开始,按「左→根→右」中序访问,每访问一个就把它的值追加到右边序列。
中序第 1 个:值 1(整棵树最左下的节点)。序列起点,无需比较。
访问值 2:和上一个 1 比 —— 2 > 1 ✓ 仍在递增,BST 没破。
访问值 3:和上一个 2 比 —— 3 > 2 ✓ 仍在递增,BST 没破。
访问值 4:和上一个 3 比 —— 4 > 3 ✓ 仍在递增,BST 没破。
访问值 5:和上一个 4 比 —— 5 > 4 ✓ 仍在递增,BST 没破。
访问值 6:和上一个 5 比 —— 6 > 5 ✓ 仍在递增,BST 没破。
访问值 7:和上一个 6 比 —— 7 > 6 ✓ 仍在递增,BST 没破。
访问值 8:和上一个 7 比 —— 8 > 7 ✓ 仍在递增,BST 没破。
访问值 9:和上一个 8 比 —— 9 > 8 ✓ 仍在递增,BST 没破。
访问值 10:和上一个 9 比 —— 10 > 9 ✓ 仍在递增,BST 没破。
访问值 11:和上一个 10 比 —— 11 > 10 ✓ 仍在递增,BST 没破。
访问值 12:和上一个 11 比 —— 12 > 11 ✓ 仍在递增,BST 没破。
访问值 13:和上一个 12 比 —— 13 > 12 ✓ 仍在递增,BST 没破。
全部访问完:序列 1,2,3,…,13 一路严格递增,没有任何一处下降 → 判定 ✅ 是 BST。
换个反例:只把紫色这个节点的值改成 99(原本该是个小值)。光看它的直接父子可能没毛病,但中序遍历会戳穿它。
反例同样从最左下开始中序遍历,值 1 作为序列起点。
访问值 2:和上一个 1 比,2 > 1 ✓ 暂时还在递增,继续往后走。
访问值 3:和上一个 2 比,3 > 2 ✓ 暂时还在递增,继续往后走。
访问值 4:和上一个 3 比,4 > 3 ✓ 暂时还在递增,继续往后走。
访问值 99:和上一个 4 比,99 > 4 ✓ 暂时还在递增,继续往后走。
访问值 6:上一个是 99,而 6 ≤ 99 —— 序列「不增」了!中序一旦出现非递增,立刻判定 ❌ 不是 BST。
参考代码
def isValidBST(root): prev = None # 上一个中序访问到的值 stack, node = [], root while stack or node: while node: # 一路向左压栈 stack.append(node); node = node.left node = stack.pop() # 中序访问 if prev is not None and node.val <= prev: return False # 出现非递增 → 不是 BST prev = node.val node = node.right return True复杂度
- 时间:O(n),每个节点中序访问一次
- 空间:O(h),栈深 = 树高 h,最坏 O(n)
易错点
面试追问把动画讲成自己的话
追问除了中序遍历,还有别的判法吗?
追问为什么不能只检查每个节点和它左右孩子?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
二叉搜索树中第 K 小的元素
LeetCode 230 · 中等 · 沿着 树 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题