108. 将有序数组转换为二叉搜索树
文章目录
- 108. 将有序数组转换为二叉搜索树
- 思路
- 递归
- 总结
108. 将有序数组转换为二叉搜索树
108. 将有序数组转换为二叉搜索树
给你一个整数数组 nums
,其中元素已经按 升序
排列,请你将其转换为一棵
平衡二叉搜索树。
平衡二叉树
: 是指该树所有节点的左右子树的深度相差不超过 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 <= nums.length <= 10^4
-10^4 <= nums[i] <= 10^4 - nums 按 严格递增 顺序排列
思路
做这道题目之前大家可以了解一下这几道(知道如何划分左右子区间去递
,并用上层的Left
和Right
接住归
上来的节点):
106. 从中序与后序遍历序列构造二叉树
654.最大二叉树中其实已经讲过了,如果根据数组构造一棵二叉树。
701.二叉搜索树中的插入操作
450.删除二叉搜索树中的节点
进入正题:
题目中说要转换为一棵高度平衡二叉搜索树。为什么强调要平衡呢?
因为只要给我们一个有序数组,如果不强调平衡,都可以以线性结构来构造二叉搜索树。
例如 有序数组[-10,-3,0]
就可以构造成这样的二叉搜索树,如图。
上图中,是符合二叉搜索树的特性吧,如果要这么做的话,是不是本题意义就不大了,所以才强调是平衡二叉搜索树。
其实数组构造二叉树,构成平衡树是自然而然的事情,因为大家默认都是从数组中间位置取值作为节点元素,一般不会随机取。所以想构成不平衡的二叉树是自找麻烦。
在654.最大二叉树中其实已经讲过了,如果根据数组构造一棵二叉树。
本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。
本题其实要比654.最大二叉树简单一些,因为有序数组构造二叉搜索树,寻找分割点就比较容易了。
分割点就是数组中间位置的节点。
那么为问题来了,如果数组长度为偶数,中间节点有两个,取哪一个?
取哪一个都可以,只不过构成了不同的平衡二叉搜索树。
例如:输入:[-10,-3,0,5,9]
如下两棵树,都是这个数组的平衡二叉搜索树:
如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1
,取右边元素就是树2
。
这也是题目中强调答案不是唯一的原因。 理解这一点,这道题目算是理解到位了。
递归
递归三部曲:
1.确定递归函数返回值及其参数
删除二叉树节点,增加二叉树节点,都是用递归函数的返回值来完成,这样是比较方便的。
相信大家如果仔细看下面两篇文章后
701.二叉搜索树中的插入操作
450.删除二叉搜索树中的节点
一定会对递归函数返回值的作用深有感触。
那么本题要构造二叉树,依然用递归函数的返回值来构造中节点的左右孩子。
再来看参数,首先是传入数组,然后就是左下标left
和右下标right
,我们在106. 从中序与后序遍历序列构造二叉树中提过,在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。
所以代码如下:
// 左闭右闭区间[left, right]
func dfs(nums []int,left,right int)*TreeNode{}
这里注意,我这里定义的是左闭右闭区间,在不断分割的过程中,也会坚持左闭右闭的区间,这又涉及到我们讲过的循环不变量(即开始定好了左闭右闭,则在递归的过程中要全程坚持这个区间划分原则,否则区间会划乱,导致计算结果不对)。
2.确定递归终止条件
这里定义的是左闭右闭的区间,所以当区间 left > right
时,说明区间没有元素了,就可以返回空节点了。
代码如下:
// 当前区间已经没有元素,可以递归结束返回nil,让上层递归的Left或者Right接住它if left > right {return nil}
3.确定单层递归的逻辑
首先取数组中间元素的位置,不难写出mid := (left + right) / 2
;,这么写其实有一个问题,就是数值越界,例如left
和right
都是最大int
,这么操作就越界了,在二分法 中尤其需要注意!
所以可以这么写:mid := left + ((right - left) / 2
取了中间位置,就开始以中间位置的元素构造节点,代码: root := &TreeNode{Val:nums[mid]}
接着划分区间,root
的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。最后返回root
节点
单层递归整体代码如下:
mid := left + (right - left) / 2root := &TreeNode{Val:nums[mid]}root.Left = dfs(nums,left,mid - 1)root.Right = dfs(nums,mid+1,right)return root
这里mid := left + ((right - left) / 2)的写法相当于是如果数组长度为偶数,中间位置有两个元素,取靠左边的。
递归整体Go
代码如下:
/*** Definition for a binary tree node.* type TreeNode struct {* Val int* Left *TreeNode* Right *TreeNode* }*/
func sortedArrayToBST(nums []int) *TreeNode {// 由于是要构造平衡的二叉搜索树,所以两边的高度最多相差1,因此我们应该选数组的中间位置元素作为根节点// 这样左右子树的元素才接近,且满足根节点大于左子树所有节点,小于右子树所有节点if len(nums) == 0 {return nil}// 左闭右闭root := dfs(nums,0,len(nums) - 1)return root}func dfs(nums []int,left,right int)*TreeNode{// 当前区间已经没有元素,可以递归结束返回nil,让上层递归的Left或者Right接住它if left > right {return nil}mid := left + (right - left) / 2root := &TreeNode{Val:nums[mid]}root.Left = dfs(nums,left,mid - 1)root.Right = dfs(nums,mid+1,right)return root
}
注意:在调用dfs
的时候传入的left
和right
为什么是0
和len(nums) - 1
,因为定义的区间为左闭右闭
。
总结
在二叉树专题中,已经写了多道构造二叉树的题目了,其实思路也是一样的,不断中间分割,然后递归处理左区间,右区间,也可以说是分治。
此时相信大家应该对通过递归函数的返回值来增删二叉树很熟悉了,这也是常规操作。
在定义区间的过程中我们又一次强调了循环不变量的重要性。