记录 1 道算法题
合法二叉搜索树
二叉搜索树的特点是 左边的子节点以及后代节点都会比父节点小, 右边的子节点以及后代节点都会比父节点大。
- 利用一个闭合区间,递归传入,约束当前节点 root 的 val。
a
b c
d e f g
假设是上面的二叉树
b < a, d < b < a, 当同时一条线都是左节点的时候,只要小于父节点就一定小于祖先节点。
b < a, e > b, e < a,当出现了转折的时候,我们会发现 e 节点需要大于 b,但是同时他需要小于 a, 因为如果 e 大于 a 的话,那么在插入二叉树的时候就不会插入到 a 的左边,而应该会插入 a 的右边。
这时候约束区间就出现了,根节点左边的节点,如果是一条线到左边的,自然是 [-Infinity, 父节点的值]。 如果出现了转折,将会是 [父节点的值,祖先节点的值]。
每次发生转折,都会更新一个新的祖先节点。
a
b
c
d
像上面这种情况,d 节点的区间是 [父节点的值,a 的值]。
a
b
c
d
e
像上面这种情况,e 节点的区间是 [父节点的值,c 的值]。
另外,还是像上面这种情况, d 节点的区间是 [b 的值, c 的值]。因为如果他小于 b,那他应该在 b 的左边。
所以当出现转折的时候,区间是会发生变化的。
-
左节点的右子节点的区间是是 [父节点的值,父节点的父节点的值]
-
右节点的左子节点的区间是 [父节点的父节点的值,父节点的值]。
-
左节点的左子节点的区间是 [-Infinity, 父节点的值]
-
右节点的右子节点的区间是 [父节点的值,Infinity]
我们在递归的时候传入左边界的值和右边界的值。
// 根节点是特殊的节点,因为没有上一层的递归区间,我们默认给他的区间是无限制。
// 这样就有了最开始的父节点的父节点的值了,就是我们的默认值。
function isValidBST(root, lower = -Infinity, upper = Infinity) {
if (!root) return true
return root.val > lower &&
root.val < upper &&
isValidBST(root.left, lower, root.val) &&
isValidBST(root.right, root.val, upper)
}
看着像是只规定了左节点的右区间和右节点的左区间, 这就是递归神奇的地方。他可以很简洁。 其实里面是每次只手动修改其中一边的区间,保留另外一边的区间,这另外一边的区间就是父节点的父节点的值。
因为只有在转折的时候,这个父节点的父节点的值才会发生改变,上一轮是右节点,修改了 lower,如果下一轮递归是左节点,就会保留上一轮的 lower,修改 upper,这样一个约束区间就出现了。
- 使用二叉搜索树的另一个特点,使用循环解决
二叉搜索树的另一个特点是假如进行中序遍历,得到的是一个升序的数组。我们可以利用这一点,只要他不是升序的,就是不合法的。
function isValidBST(root) {
const stack = []
// 前一个值
let prev = -Infinity
while (stack.length > 0 || root) {
// 每次优先收集左子节点
while (root) {
stack.push(root)
root = root.left
}
// 然后开始从收集的最后(最深处)一个节点,
// 慢慢往上返回
// 相对的视角来看,这个节点如果是叶子节点,那他是左节点,
// 如果他有子节点,那么他是根节点
/**
* 像是这种遍历的感觉,一刀刀切
* /
* //
* ///
* ////
*/
root = stack.pop()
// 判断当前的节点是不是升序的
if (root.val <= prev) return false
// 更新前一个节点
prev = root.val
// 接下来处理右节点(下一轮会被视作根节点),如果是叶子节点会在下一轮循环被忽略,
// 然后继续从数组中弹出他的父节点
root = root.right
}
// 如果没有中途退出就是合法的
return true
}
结束