[路飞]合法的二叉搜索树

267 阅读1分钟

记录 1 道算法题

合法二叉搜索树

leetcode-cn.com/problems/le…


二叉搜索树的特点是 左边的子节点以及后代节点都会比父节点小, 右边的子节点以及后代节点都会比父节点大。

  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,这样一个约束区间就出现了。

  1. 使用二叉搜索树的另一个特点,使用循环解决

二叉搜索树的另一个特点是假如进行中序遍历,得到的是一个升序的数组。我们可以利用这一点,只要他不是升序的,就是不合法的。

    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
    }

结束