验证二叉搜索树:只比左右孩子还不够
左子树里所有值都要小于根,右子树里所有值都要大于根。孩子合法不等于子孙合法,必须把祖先的上下界传下去。
递归携带可空的边界 (low, high):当前节点值必须满足 low < val < high(BST 不允许重复)。去左子树时 high 收紧为当前值,去右子树时 low 收紧为当前值;另一侧原样继承。这样每个节点都满足整条祖先路径的约束。时间 O(n),空间 O(h)。
给定一棵二叉树的根,判断它是不是有效的二叉搜索树。有效不是「每个节点的左孩子比它小、右孩子比它大」就够了:左子树里所有值都要小于根,右子树里所有值都要大于根,左右自己也必须是 BST。相等也不合法。
主反例层序 [5, 4, 6, null, null, 3, 7]:根是 5,左孩子 4,右孩子 6,6 下面再挂 3 和 7。只看父子:4<5、6>5、3<6、7>6,全部成立。但 3 落在 5 的右子树里,却比 5 小。
本文要回答:为什么局部「左小右大」会漏掉祖先约束,以及区间 (low, high) 每往下一层怎样只收紧一侧,让节点 3 收到 (5, 6) 并当场失败。
只比父子:局部全部通过,整棵仍非法
没有「整棵子树都受祖先约束」这个想法,验证函数会写成:左孩子小于自己、右孩子大于自己,再递归左右。主反例里这条路会全部放行。4 是 5 的左孩子,4<5;6 是右孩子,6>5;3 是 6 的左孩子,3<6;7 是 6 的右孩子,7>6。每一对父子都像一棵合法的小 BST。
BST 的查找希望「比当前值小就只看左边」。要让这句话永远正确,左边不能藏任何更大的数,右边不能藏任何更小的数。这不是对孩子的约束,是对整棵子树的约束。3 挂在 6 下面,对父亲 6 来说它是左孩子,看起来没问题;对祖先 5 来说,它落在右子树里却比 5 小。查找 3 若从 5 出发会走左边,永远找不到这棵右子树里的 3——结构已经自相矛盾。
中序遍历先左后根再右。若整棵是 BST,中序必须严格递增。这棵树的中序是 4, 5, 3, 6, 7。5 后面跟 3,递增断了,同样一票否决。中序断点和「3 必须大于 5」打的是同一个点:祖先的约束比父亲更长。只比孩子,就是把祖先扔掉了。
全局排序中序遍历有序 ⇔ 有效 BST。任何右子树的节点都必须大于所有祖先,不只是大于父亲。主反例的 3 对 6 合法、对 5 非法。
把祖先约束压缩成 (low, high) 往下传
根一开始落在开区间 (−∞, +∞) 里,没有祖先。走进左子树,最大值不能再达到根,上界变成根的值,下界原样继承。走进右子树,最小值必须超过根,下界变成根的值,上界原样继承。每往下走一层,区间只收紧一侧。当前节点的值必须严格落在 (low, high) 里:等于边界也不行,BST 不允许重复。
用手走主反例。根 5 在 (−∞, +∞) 里,通过。左孩子 4 收到 (−∞, 5),4<5,通过。右孩子 6 收到 (5, +∞),6>5,通过。再走进 6 的左孩子 3:从 6 往左只收紧 high,low 仍是 5 传下来的,于是 3 收到 (5, 6)。3≤5,不在开区间里,失败。7 若还要走,会收到 (6, +∞),它自己能过,但整棵已经在 3 上被否决。
区间不是父节点的标签,而是整条祖先路径的压缩结果。3 的父亲是 6,所以 high=6;3 的祖先还有 5,而且 3 一直走在 5 的右边,所以 low=5。只比父亲只会看见 3<6;带上祖先才会看见还必须 3>5。演示场景里最后一帧把失败钉在节点 3,区间正是 (5, 6)。
合法的小树 [2,1,3] 用同一套规则会全部通过:2 在 (−∞, +∞),1 在 (−∞, 2),3 在 (2, +∞)。中序 1,2,3 严格递增。两种检查是同一件事的两种写法:带界递归更贴「为什么 3 错」,中序更贴「哪里断了」。
区间语义每层只收紧一侧:向左改 high,向右改 low。另一侧必须原样继承,否则祖先约束会在下一层丢失。节点 3 的 low=5 不是 6 给的,是 5 给右子树划的。
Go:区间递归
func isValidBST(root *TreeNode) bool {var check func(*TreeNode, int, int) boolcheck = func(n *TreeNode, low, high int) bool {if n == nil { return true }if n.Val <= low || n.Val >= high { return false }return check(n.Left, low, n.Val) &&check(n.Right, n.Val, high)}return check(root, math.MinInt, math.MaxInt)}
1根用整数最小、最大值当 ±∞ 兜底。空树合法,直接 true。
2开区间:n.Val <= low 或 >= high 即失败。主反例的 3 走到 check(3, 5, 6),3<=5,走这条 false。
3左子树继承 low、把 high 收成当前值;右子树继承 high、把 low 收成当前值。3 的 low=5 是祖先留下的,不是父亲 6 新给的。
4左右同时满足才算有效。一边已经失败,另一边不必再装成整棵合法。
总结
验证 BST 必须带 (low, high)。[5,4,6,null,null,3,7] 里 3<6 过了父子,过不了 (5, 6)。
- 只比左右孩子会漏掉祖先。3 对父亲 6 合法,对祖先 5 非法,因为它落在 5 的右子树。
- 向左只收紧 high,向右只收紧 low,另一侧原样继承。区间是整条路径的压缩,不是父节点的标签。
- 开区间处理重复:等于边界即失败。中序必须严格递增,和带界是同一件事。