1.题目:
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
【解析】需要注意的是:每个节点的左右两个子树的高度差的绝对值不超过1,那么高度差可以是0,也可以是1。
1.1 示例:
示例1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
1.2 提示:
- 1 <= nums.length <= 104
- -104 <= nums[i] <= 104
- nums 按 严格递增 顺序排列
1.3 先导知识:
说明:先导知识一定要看!要仔细阅读!
1.二叉搜索树的定义: 二叉搜索树的定义
【至少要知道】
① 二叉查找树,二叉排序树,二叉搜索树都是一个东西,且有个英文名叫:BST。
② 二叉搜索树可能是一颗空树。
③ 任意节点的左子树值一定小于根节点值,右子树值一定大于根节点值。
2.二叉树的遍历:二叉树的遍历
【至少要知道】
① 先序遍历是:根-左-右
② 中序遍历是:左-根-右
③ 后序遍历是:左-右-根
总结:“先”,“中”,“后”对应的是“根”的位置。
2. 思路:
-
二叉搜索树的中序遍历是升序序列,题目给定的数组是按照升序排序的有序数组,因此可以确保数组是二叉搜索树的中序遍历序列。
【解析】阅读了1.3中的先导知识后,官方提供的这段文字就非常好理解了。条件1:通过二叉搜索树的定义可知二叉搜索树任意节点左子树值小于根节点的值,右子树值大于根节点的值。条件2:中序遍历的顺序是左-根-右。条件1和条件2同时满足的情况下,且题目又给了一个升序的数组,所以结论就非常显然了:数组是二叉搜索树的中序遍历序列。 -
给定二叉搜索树的中序遍历,是否可以唯一地确定二叉搜索树?答案是否定的。如果没有要求二叉搜索树的高度平衡,则任何一个数字都可以作为二叉搜索树的根节点,因此可能的二叉搜索树有多个。
【解析】上图以数组形式给出了一组数,并要求中序遍历,情况显然是不唯一的,因为根节点的选择是任意的。这里强烈建议根据中序遍历的定义手推一遍上图的4种情况。 -
如果增加一个限制条件,即要求二叉搜索树的高度平衡,是否可以唯一地确定二叉搜索树?答案仍然是否定的。
-
直观地看,我们可以选择中间数字作为二叉搜索树的根节点,这样分给左右子树的数字个数相同或只相差 1,可以使得树保持平衡。如果数组长度是奇数,则根节点的选择是唯一的,如果数组长度是偶数,则可以选择中间位置左边的数字作为根节点或者选择中间位置右边的数字作为根节点,选择不同的数字作为根节点则创建的平衡二叉搜索树也是不同的。
【解析】
① 先看上图左边的蓝框:当选择0为根节点时,此时左子树共有2个元素“-10,-3”,此时选择任意一个作为根节点都是可行的。如果选择“-10”作为根节点,因为 -3 > -10,所以直接把 -3 插入到 根节点(-10)的右子树即可;如果选择“-3”作为根节点,因为-10 < -3 ,所以直接把 -10 插入到根节点(-3)的左子树即可。
② 上图右边的蓝框同理。 -
确定平衡二叉搜索树的根节点之后,其余的数字分别位于平衡二叉搜索树的左子树和右子树中,左子树和右子树分别也是平衡二叉搜索树,因此可以通过递归的方式创建平衡二叉搜索树。
【解析】这里需要注意的是,平衡二叉搜索树的定义: 每个节点 的左右两个子树的高度差的绝对值不超过 1 。所以官方这段文字中说“左子树和右子树分别也是平衡二叉搜索树”。综上就十分显然了,可以通过递归的方式创建平衡二叉搜索树。 -
当然,这只是我们直观的想法,为什么这么建树一定能保证是「平衡」的呢?这里可以参考链接: 1382. 将二叉搜索树变平衡,这两道题的构造方法完全相同,这种方法是正确的,1382 题解中给出了这个方法的正确性证明:1382 官方题解,感兴趣的同学可以戳进去参考。
【解析】
① 注意官方的这句描述:“这么建树” “一定”能保证是平衡。“这么建树”指的是每次选择“数组中间位置的数字”作为根节点。其实,也可以选择非中间位置的元素作为根节点,且同样能确保建立出平衡二叉搜索树。如下图所示。这样的可能性就比较多了。故官方给出了这么话:“这么建树"一定能保证是平衡的。
② 1382题解从数学公式层面推导了 "每次选择数组中间位置的值作为根节点来建树"这种方法的正确性,数学功底一般的同学可以直接略过,稍微有点烧脑,哈哈。
-
递归的基准情形是平衡二叉搜索树不包含任何数字,此时平衡二叉搜索树为空。
【解析】通过阅读先导知识我们就可以知道:二叉搜索树可以是空树,且空树同样满足"高度平衡",故空树也是平衡二叉搜索树。这个很容易忽略。 -
在给定中序遍历序列数组的情况下,每一个子树中的数字在数组中一定是连续的,因此可以通过数组下标范围确定子树包含的数字,下标范围记为 [left,right]。对于整个中序遍历序列,下标范围从 left=0到 right=nums.length−1。当 left>right时,平衡二叉搜索树为空。
-
以下三种方法中,方法一总是选择中间位置左边的数字作为根节点,方法二总是选择中间位置右边的数字作为根节点,方法三是方法一和方法二的结合,选择任意一个中间位置数字作为根节点。
【解析】这段话恰好就验证了6.中的解析,所以官方给出了3种不同的根节点选择方法。
3. 3种不同的解法:
3.1 方法1
【算法】选择中间位置左边的数字作为根节点,则根节点的下标为 mid=(left+right)/2,此处的除法为整数除法。
【Python3代码】
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]) -> TreeNode:
def helper(left, right):
if left > right:
return None
# 总是选择中间位置左边的数字作为根节点
mid = (left + right) // 2
root = TreeNode(nums[mid])
root.left = helper(left, mid - 1)
root.right = helper(mid + 1, right)
return root
return helper(0, len(nums) - 1)
【解析】
① TreeNode 是官方在答案编辑器里提示的,我拉下来方便解读。
② 注意sortedArrayToBST()是一个嵌套函数,嵌套函数的定义和使用需要了解一下: Python函数嵌套定义代码是什么?Python函数嵌套如何执行
③ 下面使用一个case来画一下该代码的运行示意图:
case: nums = [-10,-3,0,5,9]
【复杂度分析】
-
时间复杂度:O(n),其中 n 是数组的长度。每个数字只访问一次。
-
空间复杂度:O(logn),其中 n是数组的长度。空间复杂度不考虑返回值,因此空间复杂度主要取决于递归栈的深度,递归栈的深度是 O(logn)。
3.2 方法2:
【算法】中序遍历,总是选择中间位置右边的数字作为根节点
选择中间位置右边的数字作为根节点,则根节点的下标为 mid=(left+right+1)/2,此处的除法为整数除法。
【Python3代码】
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> TreeNode:
def helper(left, right):
if left > right:
return None
# 总是选择中间位置右边的数字作为根节点
mid = (left + right + 1) // 2
root = TreeNode(nums[mid])
root.left = helper(left, mid - 1)
root.right = helper(mid + 1, right)
return root
return helper(0, len(nums) - 1)
【复杂度分析】
同法1。
3.3 方法3:
【算法】中序遍历,选择任意一个中间位置数字作为根节点
选择任意一个中间位置数字作为根节点,则根节点的下标为 mid=(left+right)/2和 mid=(left+right+1)/2 两者中随机选择一个,此处的除法为整数除法。
【Python3代码】
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> TreeNode:
def helper(left, right):
if left > right:
return None
# 选择任意一个中间位置数字作为根节点
mid = (left + right + randint(0, 1)) // 2
root = TreeNode(nums[mid])
root.left = helper(left, mid - 1)
root.right = helper(mid + 1, right)
return root
return helper(0, len(nums) - 1)
【解析】
需要注意"随机" 是如何实现的,官方提供的代码中使用 randint(0,1)函数来实现。 Python 中的randint()函数
【复杂度分析】
同上。