ACM 选手图解 LeetCode 修剪二叉搜索树(递归 + 非递归)

125 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情


大家好呀,我是帅蛋。

今天我要来 cos 一下“园丁”,来修剪修剪这棵二叉搜索树,这可是个体力活。

669-0

话不多说,掏出小剪刀,我们修起来~

669-1

LeetCode 669:修剪二叉搜索树

题意

给定二叉搜索树的根节点 root,以及最小边界 low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在 [low,high] 中,结果返回修剪好的二叉搜索树的新的根节点。

注意:修剪树不应该改变保留在树中的元素的相对结构(即,如果没有被移除,原有的父代子代关系都应当保留)。

示例

输入:root = [3,0,4,null,2,null,null,1],low = 1,high = 3

输出:[3,2,null,1]

669-2

提示

  • 树中节点数在范围 [1, 10^4] 内
  • 0 <= Node.val <= 10^4
  • 树中的每个节点的值都是唯一的
  • 题目数据保证输入是一棵有效的二叉搜索树
  • 0 <= low <= high <= 10^4

题目解析

修剪二叉搜索树,难度中等。

整个题目描述看起来很长,其实意思就是将二叉搜索树在 [low, high] 之外的节点值删掉。

如果你看过我之前【删除二叉搜索树中的节点】这篇文章,你会发现,其实类似批量删除二叉搜索树中的节点。

仅仅是删除不难,还是我之前说过的,难就难在删除节点之后仍然要保持剩下的二叉树依然是二叉搜索树。

669-3

递归法

根据【递归算法】文章中讲的,实现递归,需要两步:

  • 找出重复的子问题(递推公式)。
  • 终止条件。

(1) 找出重复的子问题。

给定了区间 [low, high],修剪完之后的二叉搜索树的每个节点值都要在这个区间内。

那对于原始二叉搜索树的每个节点来说,存在 3 种情况:

  • 当前节点值 < 左边界 low。

这种情况下了,根据二叉搜索树的特性,只需要考虑当前节点的右子树,左子树肯定是都去掉的。

# 如果当前节点值 < 左边界,根据二叉搜索树的特性,只需要考虑右子树,抛弃左子树
if root.val < low:
    return self.trimBST(root.right, low, high)
  • 当前节点值 > 右边界 high。

这种情况下,根据二叉搜索树的特性,只需要考虑当前节点的左子树,右子树则肯定都是去掉的。

# 如果当前节点值 > 右边界,根据二叉搜索树特性,只需要考虑左子树,抛弃右子树
if root.val > high:
    return self.trimBST(root.left, low, high)
  • 当前节点值在 [low, high] 之间。

在这个时候,根节点不需要修剪,那就是继续递归修剪左子树和右子树,左子树的结果返回给 root.left,右子树的结果返回给 root.right。

# root 在 [low, high] 区间内
if low <= root.val <= high:
   root.left = self.trimBST(root.left, low, high)
   root.right = self.trimBST(root.right, low, high)
   return root

(2) 确定终止条件。

对于终止条件就没啥好说的,遍历到空节点,直接返回就 ok 了。

if root == None:
    return None

这两点确定好了,下面我们直接来看代码。

669-4

Python 代码实现

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
        if root == None:
            return None
        # 如果当前节点值 < 左边界,根据二叉搜索树的特性,只需要考虑右子树,抛弃左子树
        if root.val < low:
            return self.trimBST(root.right, low, high)
        # 如果当前节点值 > 右边界,根据二叉搜索树特性,只需要考虑左子树,抛弃右子树
        if root.val > high:
            return self.trimBST(root.left, low, high)
        # root 在 [low, high] 区间内
        if low <= root.val <= high:
            root.left = self.trimBST(root.left, low, high)
            root.right = self.trimBST(root.right, low, high)
            return root

Java 代码实现

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode trimBST(TreeNode root, int low, int high) {
        if(root == null){
            return null;
        }
        // 如果当前节点值 < 左边界,根据二叉搜索树的特性,只需要考虑右子树,抛弃左子树
        if(root.val < low){
            return trimBST(root.right, low, high);
        }
        // 如果当前节点值 > 右边界,根据二叉搜索树特性,只需要考虑左子树,抛弃右子树
        if(root.val > high){
            return trimBST(root.left, low, high);
        }
        // root 在 [low, high] 区间内
        if(root.val >= low && root.val <= high){
            root.left = trimBST(root.left, low, high);
            root.right = trimBST(root.right, low, high);
        }
        return  root;
    }
}

本题解,每个节点都要访问一次,所以时间复杂度为 O(n)

在递归过程中调用了额外的栈空间,栈的大小取决于二叉搜索树的高度,同样二叉搜索树最坏情况下的高度为 n,所以空间复杂度为 O(n)

非递归法(迭代)

一般我们在用非递归法的时候,都是用栈来模拟递归,但是因为二叉搜索树是有序的特性,其实直接按照下面的方式直接模拟即可:

  • 找到第 1 个在 [low, high] 之间的节点(这个节点就是新的二叉搜索树的根节点),对这个节点的左右子树做修剪。
  • 先遍历左子树,对于左子树的节点,如果当前节点 cur 的左子树存在 且 cur.left.val < 左边界 low,直接把 cur.left 指向 cur.left.right 节点。
  • 再遍历右子树,对于右子树的节点,如果当前节点 cur 的右子树存在且 cur.right.val > 右边界 high,直接把 cur.right 指向 cur.right.left 节点。

上面 3 步一定要仔细揣摩,草稿纸走起来!

669-5

我们以下图为例:

669-6

对于它的修剪其实就是如下图:

669-7

去掉过程,我们最终得到的就是下图的二叉搜索树:

669-8

Python 代码实现

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
        if(root == None):
            return None
        # 找到修剪之后的二叉搜索树的头节点 root
        while root and (root.val > high or root.val < low):
            if root.val > high:
                root = root.left
            else:
                root = root.right
        # 修剪 root 的左子树,将 < low 的节点删除
        cur = root
        while cur:
            while cur.left and cur.left.val < low:
                cur.left = cur.left.right
            cur = cur.left
        # 修剪 root 的右子树,将 > high 的节点删除
        cur = root
        while cur:
            while cur.right and cur.right.val > high:
                cur.right = cur.right.left
            cur = cur.right
        
        return root

Java 代码实现

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode trimBST(TreeNode root, int low, int high) {
        // 找到修剪之后的二叉搜索树的头节点 root
        if(root == null){
            return null;
        }
        while(root != null && (root.val > high || root.val < low)){
            if(root.val > high){
                root = root.left;
            }
            else{
                root = root.right;
            }
        }
        TreeNode cur = root;
        // 修剪 root 的左子树,将 < low 的节点删除
        while(cur != null){
            while(cur.left != null && cur.left.val < low){
                cur.left = cur.left.right;
            }
            cur = cur.left;
        }
        // 修剪 root 的右子树,将 > high 的节点删除
        cur = root;
        while(cur != null){
            while(cur.right != null && cur.right.val > high){
                cur.right = cur.right.left;
            }
            cur = cur.right;
        }
        return root;
    }
}

同样,非递归法的时间复杂度为 O(n) ,对于空间复杂度,非递归法只额外开辟了常数级的空间,所以空间复杂度为 O(1)


图解修剪二叉搜索树到这就结束辣,大家学废了嘛?

相信二叉搜索树的实战题目做到现在,大家已经有点感觉了,感谢二叉搜索树的性质,其实很多时候在做题的时候我感觉比二叉树简单多了。

如果觉得还不错的话,记得帮我点赞 呀,让我看到你呀!

我是蛋蛋,我们下次见啦!