ACM 选手图解 LeetCode 将有序数组转化为二叉搜索树

300 阅读5分钟

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


大家好呀,我是帅蛋。

在二叉搜索树前几篇实战文章中,我们应用过二叉搜索树的一个性质来解题:对二叉搜索树进行中序遍历时,得到的结果是一个有序的序列

ACM 选手图解 LeetCode 二叉搜索树中的众数

ACM 选手图解 LeetCode 二叉搜索树的最小绝对差

ACM 选手图解 LeetCode 验证二叉搜索树

一直把人二叉搜索树整成有序序列,今天我们就反过来,将有序的序列整成一棵二叉搜索树。

108-0

LeetCode 108:将有序数组转换为二叉搜索树

题意

给定一个升序数组 nums,将其转换为一棵高度平衡的二叉搜索树。

高度平衡二叉树是一棵满足【每个节点的左右两个子树的高度差的绝对值不超过1】的二叉树。

题意

输入:nums = [-10, -3, 0, 5, 9] 输出:[0, -3, 9, -10, null, 5] or [0, 10, 5, null, -3, null, 9]

108-1

108-2

提示

  • 1 <= nums.length <= 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 按严格递增顺序排列

题目解析

将有序数组转换为二叉搜索树,难度简单,说白了就是构造一棵二叉搜索树。

之前关于构造二叉树这种事,我们都做了好几次了,可以说都是老熟人儿:

ACM 选手图解 LeetCode 最大二叉树

ACM 选手图解 LeetCode 从前序与中序遍历构造二叉树

ACM 选手图解 LeetCode 从中序与后序遍历构造二叉树

构造二叉树这种事,说白了就是根据情况,找到一个点当根节点,然后左区间是一棵子树,右区间是一棵子树。

比如就拿中序遍历来说,它的遍历顺序为左子树、根、右子树,它就是下面那样:

108-3

我在开头的时候讲过:对二叉搜索树进行中序遍历时,得到的结果是一个有序的序列

那把一个有序数组构造成二叉搜索树,其实这个“有序数组”就是可以看成是二叉搜索树中序遍历出来的。

108-4

这样的话,那构造二叉搜索树就太简单了:

  • 有序数组中间节点为根节点。
  • 根节点左侧区间为左子树。
  • 根节点右侧区间为右子树。

这里有一点大家稍微注意一下,那就是找【中间节点】。

如果数组是奇数那没问题,中间那个就是根节点,如果数组是偶数个,那中间节点就是两个,可能你有疑问,这个时候该怎么取?

108-5

其实很简单,取左边那个或者右边那个都可以,比如对于 nums = [1,3],取 1 就是右边那棵,取 3 就是左边那棵。

108-6

递归法

实现【递归算法】,两步走起来:

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

(1) 找出重复的子问题。 我在题目解析中讲过了,有序序列构造二叉树:

  • 有序数组中间节点为根节点。
  • 根节点左侧区间为左子树。
  • 根节点右侧区间为右子树。

那重复的子问题就是,找到根节点,递归构造左子树,递归构造右子树。

mid = left + (right - left) // 2
# 根节点
midNode = TreeNode(nums[mid])
# 递归构造左子树
midNode.left  = self.process(nums, left, mid - 1)
# 递归构造右子树
midNode.right = self.process(nums, mid + 1, right)

(2) 确定终止条件。

对于终止条件来说,当 left > right 时,就终止,返回 None,因为这个时候就是空节点。

if left > right:
    return None

接下俩还是走个过场:这两点确定好了,代码基本也就写出来了。

108-7

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 process(self, nums, left, right):
        if left > right:
            return None
        # 找数组中间元素
        mid = left + (right - left) // 2
        # 根节点
        midNode = TreeNode(nums[mid])
        # 递归构造左子树
        midNode.left  = self.process(nums, left, mid - 1)
        # 递归构造右子树
        midNode.right = self.process(nums, mid + 1, right)
​
        return midNode
​
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        return self.process(nums, 0, len(nums) - 1)

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 process(int[] nums, int left, int right){
        if(left > right){
            return null;
        }
        // 找数组中间元素
        int mid = left + ((right - left) >> 1);
        // 根节点
        TreeNode midNode = new TreeNode(nums[mid]);
        // 递归构造左子树
        midNode.left = process(nums, left, mid - 1);
        // 递归构造右子树
        midNode.right = process(nums, mid + 1, right);
​
        return midNode;
    }
​
    public TreeNode sortedArrayToBST(int[] nums) {
        TreeNode root = process(nums, 0, nums.length - 1);
        return root;
    }
}

本题解对于每个数组元素都要访问到,所以时间复杂度为 O(n)

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

非递归法(迭代)

基于二叉搜索树的特性,对于二叉搜索树的非递归法,就是一个模拟的过程。

这里为了模拟构造二叉搜索树的过程,需要用到 3 个队列:

  • rootQue 存放遍历的节点。
  • leftQue 存放左区间的下标。
  • rightQue 存放右区间的下标。

之后就是不断的模拟寻找根节点,构造左子树和构造右子树。

我们以 nums = [-10, -3, 0, 5, 9] 为例。

首先初始化二叉搜索树的根节点以及 3 个队列:

108-8

# 初始化根节点
root = TreeNode(0)
# 队列存放遍历的节点
rootQue = [root]
# 队列存放左区间下标
leftQue = [0]
# 队列存放右区间下标
rightQue = [len(nums) - 1]

第 1 步,rootQue 不为空,cur 记录 rootQur 出队列节点 0,left 记录 leftQue 出队列下标 0,right 记录 rightQue 出队列下标 4。

cur = rootQue.pop(0)
left = leftQue.pop(0)
right = rightQue.pop(0)

此时的中间下标 mid = 2, nums[2] = 0,赋值给 cur,即此时 cur.val = nums[2] = 0。

108-9

# 找数组中间元素
mid = left + (right - left) // 2
# 将中间元素值赋值给节点
cur.val = nums[mid]

接下来处理左区间,此时 left = 0,mid = 2,left < mid,初始化 cur 节点的左孩子节点,将左孩子 cur.left、左区间的左下标 0 和左区间的右下标 mid - 1 = 1 分别入队列。

108-10

if left < mid:
    cur.left = TreeNode(0)
    rootQue.append(cur.left)
    leftQue.append(left)
    rightQue.append(mid - 1)

下面处理右区间,同理初始化 cur 节点的右孩子节点,将右孩子 cur.right、右区间的左下标 mid + 1 = 3 和右区间的右下标 4 分别入队列。

108-11

if right > mid:
    cur.right = TreeNode(0)
    rootQue.append(cur.right)
    leftQue.append(mid + 1)
    rightQue.append(right)

第 2 步,rootQue 不为空,cur 记录 rootQur 出队列节点 0,left 记录 leftQue 出队列下标 0,right 记录 rightQue 出队列下标 1。

此时的中间下标 mid = 0, nums[0] = -10,赋值给 cur,即此时 cur.val = nums[0] = -10。

108-12

因为篇幅原因,在这我就不把全部的都画完了。

108-13

之后的步骤都是和第 1 步的一样,不断的初始化节点、入队列出队列,直至 rootQue 为空。

大家可以自己动手把剩下的步骤画出来,检验一下自己是不是真的搞懂了。

108-14

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 sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        if len(nums) == 0:
            return None
        # 初始化根节点
        root = TreeNode(0)
        # 队列存放遍历的节点
        rootQue = [root]
        # 队列存放左区间下标
        leftQue = [0]
        # 队列存放右区间下标
        rightQue = [len(nums) - 1]
​
        while rootQue:
            cur = rootQue.pop(0)
            left = leftQue.pop(0)
            right = rightQue.pop(0)
            # 找数组中间元素
            mid = left + (right - left) // 2
            # 将中间元素值赋值给节点
            cur.val = nums[mid]
            # 处理左区间
            if left < mid:
                cur.left = TreeNode(0)
                rootQue.append(cur.left)
                leftQue.append(left)
                rightQue.append(mid - 1)
            # 处理右区间
            if right > mid:
                cur.right = TreeNode(0)
                rootQue.append(cur.right)
                leftQue.append(mid + 1)
                rightQue.append(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 sortedArrayToBST(int[] nums) {
        if(nums.length == 0){
            return null;
        }
        // 初始化根节点
        TreeNode root = new TreeNode(0);
        // 队列存放遍历的节点
        Queue<TreeNode> rootQue = new LinkedList<>();
        // 队列存放左区间下标
        Queue<Integer> leftQue = new LinkedList<>();
        // 队列存放右区间下标
        Queue<Integer> rightQue = new LinkedList<>();
        // 初始化 3 个队列
        rootQue.offer(root);
        leftQue.offer(0);
        rightQue.offer(nums.length - 1);
​
        while (!rootQue.isEmpty()){
            TreeNode cur = rootQue.poll();
        int left = leftQue.poll();
        int right = rightQue.poll();
            // 找数组中间元素
        int mid = left + ((right - left) >> 1);
            // 将中间元素值赋值给节点
        cur.val = nums[mid];
            // 处理左区间
        if (left < mid) {
          cur.left = new TreeNode(0);
          rootQue.offer(cur.left);
          leftQue.offer(left);
          rightQue.offer(mid - 1);
      }
            // 处理右区间
        if (right > mid) {
          cur.right = new TreeNode(0);
          rootQue.offer(cur.right);
          leftQue.offer(mid + 1);
          rightQue.offer(right);
      }
      }
        return root;
    }
}

同样对于非递归法,其时间复杂度为 O(n) ,但是额外申请了 3 个队列空间,所以非递归法空间复杂度为 O(n)


图解将有序数组转化为二叉搜索树到这就结束辣,是不是成就感满满!

108-15

做完这道题我们就厉害了,不管是将二叉搜索树搞成有序序列,还是将有序序列构造成二叉搜索树我们都玩了个遍!

还是要记得做好总结,别做完了就觉得是会了!

当然也不要忘了我的点赞,让我看到你呀!

108-16

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