题解 | #将升序数组转化为平衡二叉搜索树#
将升序数组转化为平衡二叉搜索树
http://www.nowcoder.com/practice/7e5b00f94b254da599a9472fe5ab283d
题目描述
给出一个升序排序的数组,将其转化为平衡二叉搜索树(BST)
方法一: 递归
解题思路
先了解一下平衡二叉搜索树的特点。
二叉搜索树,它的中序遍历后的结果就是一个升序的序列;
平衡的,说明任何一个节点的左子树和右子树的高度相差不超过1。
题目中所给的是一个升序排序的数组,所以一个大体流程就是根据一个中序遍历的序列,还原出一个它对应的二叉树。因为是中序遍历的结果,所以元素的左边都是以该元素为根节点的左子树上的元素,元素的右边都是以该元素为根节点的右子树上的元素。
又因为是平衡的,所以先找出序列的中点,以中点的值生成根节点,他的左边右边相差不差过一个元素,即它的左子树和右子树的元素个数相差不超过一个,每次都安一样的策略来找根节点,即左右子树的高度相差不会超过1。
以中点的左边序列为一个新的序列,按照一样的方法生成一个平衡二叉树,该树是第一步找的根节点的左子树。
以中点的右边序列为一个新的序列,按照一样的方法生成一个平衡二叉树,该树是第一步找的根节点的右子树。
也就是把问题的规模拆分成一个一个的小规模问题,解法都是一样的,只是问题的规模不一样,到规模足够小的时候我们可以很方便的找出答案,再一步一步往上推回去,即可得到真个大规模问题的答案。
在该题中,规模小到序列中只有一个元素时,即可很方便的找出答案,只有一个元素,直接以它为根节点返回即可。
以下以 [5,8,10,12,17] 数组为例,整个的递归的流程演示如下图所示:
复杂度分析
对于递归解决的问题,他的时间复杂度可以用 Master 公式来求解,虽然 Master 公式有它的适用条件,即大规模的问题每次拆分成的小问题规模都是一样的。该题符合这个条件,该题中每次拆分的小问题规模就是大问题规模的一半。
Master 公式:
说明:
T(n): 表示样本量为 n 时的时间复杂度。
T(n / b) : 拆分的小问题规模,拆分成 b 等份。
a : 小问题规模总共需要算几次。
O(n^d) : 除小问题规模的开销外,其他操作的时间复杂度。
由a、b、d的值,来决定对应的时间复杂度为多少:
- 1) log(b,a) > d , 复杂度为O(N^log(b,a))
- 2) log(b,a) = d , 复杂度为O(N^d * logN)
- 3) log(b,a) < d , 复杂度为O(N^d)
在该题中,就是:T(n) = 2 * T(n/2) + O(1),a=2,b=2,d=0;满足 log(2, 2) > 0,
所以时间复杂度为:O(N^log(2,2)) = O(N)
空间复杂度:O(N)
空间上的消耗有一部分是在栈的开辟上。在这个流程中,每次都将问题规模缩小了一半,所以栈上存储的数量也就是 logN 这么大。
另一部分是建立整个树需要 N 个节点,所以空间复杂度为O(N)。
示例代码
public class Solution { /** * * @param num int整型一维数组 * @return TreeNode类 */ public TreeNode sortedArrayToBST (int[] nums) { // 判断特殊情况, 数组为空,或数组上没有元素,直接返回 null if (nums == null || nums.length == 0) { return null; } return process(nums, 0, nums.length - 1); } /** * * @param nums 整个的有序数组 * @param left 数组的左边界, 闭区间 * @param right 数组的右边界, 闭区间 * @return nums[left ... right] 这个范围的数组,转成 BST 后的根节点 */ public TreeNode process(int[] nums, int left, int right) { // 左边界 比 右边界 大, 说明数组上没有元素,直接返回 null if (left > right) { return null; } // 如果只有一个元素,就把它当成根节点直接返回 if (left == right) { TreeNode root = new TreeNode(nums[left]); return root; } // nums[left ... right] 这个数组的长度 int len = right - left + 1; // nums[left ... right] 这个数组的中点下标,这个下标里的元素值就是 BST 的根节点的值 int mid = left + len / 2; TreeNode root = new TreeNode(nums[mid]); // 找出根节点的左子树: 继续递归用这个方法,找出左子树上这个局部范围的BST的根节点 root.left = process(nums, left, mid - 1); // 找出根节点的右子树: 继续递归用这个方法,找出右子树上这个局部范围的BST的根节点 root.right = process(nums, mid + 1, right); return root; } }
方法二: 用栈模拟递归
解题思路
之前说的是递归方法求解,但递归方法基本都可以改写成非递归方法,只是压栈出栈的行为我们自己操作了而已。
为了便于在栈中的操作,我们每次压栈的时候都需要记录保存一些信息,就该题来说,就要知道中点的值,以及这个中点对应范围的左边界和右边界。所以这里就自定义了一个 Node 类。
整个流程就是每次在栈里面压入根节点,然后找出这个根节点的左子树,当左边没有元素了,就再来生成右子树。生成右子树时的根节点的信息就从栈里面拿到(之前压入栈的作用就在这里)。
以下以 [5,8,10,12,17] 数组为例,整个的递归的流程演示如下图所示:
复杂度分析
时间复杂度: O(N^2)
看代码的书写有 2 个嵌套的 while 循环,所以时间复杂度即为 O(N^2)
空间复杂度:O(N)
空间的有一部分在 栈空间的存储上, 极端情况下把根节点的 “左节点” 全部存进去,个数就是这颗二叉树的高度,在生成右节点的时候会把之前存储的左节点弹出来在进行操作。
另一部分是建立整个树需要 N 个节点,所以空间复杂度为O(N)。
示例代码
public class Solution { // 自定义的 Node 类, 保存压栈出栈需要用的信息 class Node { TreeNode node; // 在 nums[left...right] 范围里,选出的根节点 int left; // 数组的左边界 int right; // 数组的右边界 public Node(TreeNode node, int left, int right) { this.node = node; this.left = left; this.right = right; } } /** * * @param nums int整型一维数组 * @return TreeNode类 */ public TreeNode sortedArrayToBST (int[] nums) { // 判断特殊情况, 数组为空,或数组上没有元素,直接返回 null if (nums == null || nums.length == 0) { return null; } // 左边界,闭区间 int left = 0; // 右边界,闭区间 int right = nums.length - 1; // 左右边界范围里的中点,就是根节点上的值 int mid = left + (right - left + 1) / 2; TreeNode root = new TreeNode(nums[mid]); Stack<Node> stack = new Stack<>(); stack.push(new Node(root, left, right)); TreeNode curRoot = root; // 还在范围里,或 栈里有元素,就继续操作 while (left < right || !stack.isEmpty()) { // 生成左子树 // left == right , 就只有一个元素,已经生成根节点了 while (left < right) { // 右边界: 在 根节点的下标 的左边 right = mid - 1; // 更新中点(寻找左子树上的根节点) mid = left + (right - left + 1) / 2; TreeNode leftNode = new TreeNode(nums[mid]); curRoot.left = leftNode; // 更新根节点(在子树的视角来看的根节点) curRoot = curRoot.left; // 压栈,之后弹出这个元素时再用它来生成右子树 stack.push(new Node(leftNode, left, right)); } // 生成右子树 // 拿出栈里之前保存的元素,生成它的右子树 Node node = stack.pop(); left = node.left; right = node.right; mid = left + (right - left + 1) / 2; // 左边界: 在 根节点的下标 的右边 left = mid + 1; // left <= right 说明右子树上还有元素,需要生成根节点 if (left <= right) { // 更新中点(寻找右子树上的根节点) mid = left + (right - left + 1) / 2; curRoot = node.node; TreeNode rightNode = new TreeNode(nums[mid]); curRoot.right = rightNode; // 更新根节点(在子树的视角来看的根节点) curRoot = curRoot.right; // 压栈, 这个节点也可能会有左节点或者右节点 stack.push(new Node(rightNode, left, right)); } } // 返回整颗树的根节点 return root; } }