验证二叉搜索树 图解题解
光比父子三人组不够——深处的节点也可能悄悄越界。
验证二叉搜索树别只盯着父子大小——得给每个节点带一个「允许的取值区间」往下传:往左子树走,上界收紧成当前值;往右走,下界抬高成当前值。任何节点越界就判否。这样祖先的约束被一路「继承」到每个后代,深处藏着的不合法节点也逃不掉。
这道题到底在问什么
- 输入
- 是 BST
- 输出
- true
- 输入
- 某处违反
- 输出
- false
最优解:为什么这么做
一句话答案: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 即可。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3核心:中序遍历 BST = 把值从小到大排出来。一旦不递增,立刻判否。
- 4主角登场:一棵合法 BST,13 个节点。我们从最左下角开始,按「左→根→右」中序访问,每访问一个就把它的值追加到右边序列。
- 5中序第 1 个:值 1(整棵树最左下的节点)。序列起点,无需比较。
- 6访问值 2:和上一个 1 比 —— 2 > 1 ✓ 仍在递增,BST 没破。
- 7访问值 3:和上一个 2 比 —— 3 > 2 ✓ 仍在递增,BST 没破。
- 8访问值 4:和上一个 3 比 —— 4 > 3 ✓ 仍在递增,BST 没破。
- 9访问值 5:和上一个 4 比 —— 5 > 4 ✓ 仍在递增,BST 没破。
- 10访问值 6:和上一个 5 比 —— 6 > 5 ✓ 仍在递增,BST 没破。
- 11访问值 7:和上一个 6 比 —— 7 > 6 ✓ 仍在递增,BST 没破。
- 12访问值 8:和上一个 7 比 —— 8 > 7 ✓ 仍在递增,BST 没破。
- 13访问值 9:和上一个 8 比 —— 9 > 8 ✓ 仍在递增,BST 没破。
- 14访问值 10:和上一个 9 比 —— 10 > 9 ✓ 仍在递增,BST 没破。
- 15访问值 11:和上一个 10 比 —— 11 > 10 ✓ 仍在递增,BST 没破。
- 16访问值 12:和上一个 11 比 —— 12 > 11 ✓ 仍在递增,BST 没破。
- 17访问值 13:和上一个 12 比 —— 13 > 12 ✓ 仍在递增,BST 没破。
- 18全部访问完:序列 1,2,3,…,13 一路严格递增,没有任何一处下降 → 判定 ✅ 是 BST。
- 19换个反例:只把紫色这个节点的值改成 99(原本该是个小值)。光看它的直接父子可能没毛病,但中序遍历会戳穿它。
- 20反例同样从最左下开始中序遍历,值 1 作为序列起点。
- 21访问值 2:和上一个 1 比,2 > 1 ✓ 暂时还在递增,继续往后走。
- 22访问值 3:和上一个 2 比,3 > 2 ✓ 暂时还在递增,继续往后走。
- 23访问值 4:和上一个 3 比,4 > 3 ✓ 暂时还在递增,继续往后走。
- 24访问值 99:和上一个 4 比,99 > 4 ✓ 暂时还在递增,继续往后走。
- 25访问值 6:上一个是 99,而 6 ≤ 99 —— 序列「不增」了!中序一旦出现非递增,立刻判定 ❌ 不是 BST。
⚠️ 容易写错的地方
✗ 错:只比父子三个点
✓ 对:左/右整棵子树都要满足
5 在 7 的左下,但若 5 > 根 7 的祖先约束就违规;中序遍历天然管跨层
✗ 错:用 < 而不是 <=
✓ 对:相等也算不是 BST
BST 要求严格大于,出现相等值即非递增,必须判 false
✗ 错:边界用 int 比较溢出
✓ 对:节点值可能是 INT_MIN/MAX
初始 prev 用 long 或可空类型,别让极值卡死比较
完整代码(Python / Java / C++)
Python
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 TrueJava
public boolean isValidBST(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode node = root;
Integer prev = null; // 上一个中序值
while (!stack.isEmpty() || node != null) {
while (node != null) { // 一路向左
stack.push(node); node = node.left;
}
node = stack.pop(); // 中序访问
if (prev != null && node.val <= prev) return false;
prev = node.val;
node = node.right;
}
return true;
}C++
bool isValidBST(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* node = root;
long prev = LONG_MIN; // 上一个中序值
bool started = false;
while (!st.empty() || node) {
while (node) { st.push(node); node = node->left; }
node = st.top(); st.pop(); // 中序访问
if (started && node->val <= prev) return false;
prev = node->val; started = true;
node = node->right;
}
return true;
}复杂度
时间
O(n)
每个节点中序访问一次
空间
O(h)
栈深 = 树高 h,最坏 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 验证二叉搜索树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
除了中序遍历,还有别的判法吗?+
递归带上下界:isValid(node, lo, hi) 要求 lo < node.val < hi,往左子树收紧上界 hi=node.val,往右子树收紧下界 lo=node.val。两种写法等价,中序版更直观、上下界版更省一个 prev 变量。
为什么不能只检查每个节点和它左右孩子?+
那样只保证局部,管不住跨层。例如根=5、右孩子=6、但 6 的左孩子=3,3<5 违反了「右子树所有值 > 根」,只看父子三点发现不了。中序遍历或上下界递归才覆盖整棵子树约束。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 验证二叉搜索树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。