目录
算法通关村 —— 透彻理解二叉树中序遍历的应用
前面我们学习了很多有关二叉树中序遍历的经典算法,今天,让我们一起加大难度,挑战一下与其相关的应用题目,使得对二叉树中序遍历的了解更进一步。
一、 有序数组转为二叉搜索树
给定一个升序整数数组nums,将其转换为一棵高度平衡二叉树。
高度平衡二叉树:一棵满足每个节点的左右两个子树的高度差绝对值不超过1的二叉树。
如下所例:
输入:nums = [-10, -3, 0, 5, 9]
输出:[0,-3,9,-10,null,5]/[0,-10,5,null,-3,null,9]
如果只要求构造二叉搜索树,我们可以以升序序列中的任意一个元素作为根节点,以元素左边的升序序列构建左子树,右边构建右子树,这样便得到了一棵二叉搜索树。但由于本题要求高度平衡,所以我们就要尽量让两边子树节点数相同,所以我们可以选择升序序列的中间元素作为根节点,然后后面的构建子树其实就是二分查找的过程了,比根节点小便成为其左节点,否则成为其右节点。
具体实现代码如下:
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
// 递归将数组转换为二叉搜索树
return helper(nums, 0, nums.length - 1);
}
public TreeNode helper(int[] nums, int left, int right){
if(left > right) return null;
// 每次都选择中间位置左边的数字作为根节点
int mid = left + ((right - left) >> 1);
TreeNode root = new TreeNode(nums[mid]);
// 递归建立它的左右节点
root.left = helper(nums,left,mid-1);
root.right = helper(nums,mid+1,right);
return root;
}
}
二、二叉搜索树中的插入操作
给定二叉搜索树的根节点和要插入树种的值,将值插入二叉搜索树,返回插入后的二叉搜索树根节点,要求仍保持为二叉搜索树。输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。
那其实思路和上一道题也是一样的嘛,同样是中序遍历,如果比当前节点值小,就递归对该节点的左节点再次执行插入函数,如果比当前节点值大,就递归对该节点的右节点再次执行插入操作就可以了完成了。思路非常简单,具体实现代码如下:
class Solution {
public static TreeNode insertIntoBST(TreeNode root, int val) {
// 若根节点为空,则插入节点为树的根节点
if(root == null) return new TreeNode(val);
// 若该值与某节点值相等,无法插入,直接返回根节点
if(val == root.val) return root;
// 如果比根节点值大,则递归对其右节点再次执行插入函数
else if(val > root.val){
if(root.right == null) root.right = new TreeNode(val);
else insertIntoBST(root.right, val);
}
// 否则,对其左节点再次执行插入函数
else{
if(root.left == null) root.left = new TreeNode(val);
else insertIntoBST(root.left, val);
}
return root;
}
}
三、 寻找两个正序数组的中位数
这道题难度就稍微有点大了,做好心理准备哦。
给定两个大小分别为m和n的正序数组nums1和nums2,找出并返回两个数组的中位数,要求算法的时间复杂度应该为O(log(m+n))。如下所例:
输入:nums1 = [1,2] nums2 = [3, 4]
输出:2.50000
解释:合并数组 = [1,2,3,4], 中位数 (2 + 3) / 2 = 2.5
这道题如果单单是解决问题,其实是很容易的,难的是如何把时间复杂度降到O(log(m+n))?时间复杂度里面要求有log的,通常都要考虑二分、快排或者堆三个方面。对于有序序列,那我们就考虑能否使用二分来解决吧。
那决定使用二分啦,核心问题是我们要基于什么规则每次判断将数据砍掉一半呢?而且本题为两个序列,所以我们要解决的核心问题是如何从两个序列中分别砍半,图示如下 k=(m+n)/2:
根据中位数的定义我们可知,当m+n为奇数,中位数是有序数组的第(m+n)/2个元素,为偶数,中位数是第(m+n)/2个元素和第(m+n)/2 + 1个元素的平均值。因此,这道题就可以转化成寻找两个有序数组的第k小的数,k为(m+n)/2或者(m+n)/2 + 1。
那下面就是我们要解决的重点了,如何砍掉一半的数据呢?
假如有两个有序数组nums1和nums2,要找到第k个元素,我们先比较nums1[k/2-1]和nums2[k/2-1]。比较完大小后有以下几种情况:
⚪ 如果nums1[k/2-1] < nums2[k/2-1],则比nums1[k/2-1]小的数最多只有nums1的前k/2 - 1个数和nums2的前k/2 - 1个,因此比其小的数最多只有k-2个,因此nums1[k/2 - 1]不可能是第k个数,那么便可以排除掉nums1[0]到nums[k/2 - 1]的数,也就是一次砍掉一半。
⚪ 如果nums1[k/2-1] > nums2[k/2-1],便可以排除掉nums2[0]到nums2[k/2 - 1]的数。
⚪ 如果nums1[k/2-1] = nums2[k/2-1], 则归入第一种情况处理。
所以我们每次都能缩小一半范围,那最后我们就可以以log的时间复杂度找到我们要的中位数啦!
在此之前,我们还需要处理以下几个特殊情况:
⚪ 如果nums1[k/2 - 1] 或者 nums2[k/2 - 1]越界,那我们我们可以选取其对应数组的最后一个元素。在这种情况下,我们必须根据排除的个数减少k的值,而不能直接将k减去k/2。
⚪ 如果k=1,我们只需要返回两个数组首元素的最小值就可以了。
具体实现代码如下:
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len1 = nums1.length, len2 = nums2.length;
int len = len1 + len2;
// 如果长度为奇数,则中位数为最中间的树,否则为中间两数的平均值
if (len % 2 == 1){
int mid = len/2; // 取到最中间数的索引
double median = getmedian(nums1, nums2, mid + 1); // 递归排除不存在中位数的区间,直到找到中位数
return median;
}else {
int mid1 = len / 2 - 1, mid2 = len / 2; // 取到中间两数的索引
double median = (getmedian(nums1,nums2,mid1+1) + getmedian(nums1,nums2,mid2+1))/2.0;
return median;
}
}
// 找到两数组中第k小的数,不断排除不存在中位数的区间直到找到中位数
public int getmedian(int[] nums1, int[] nums2, int k){
int len1 = nums1.length, len2 = nums2.length;
int index1 = 0, index2 = 0;
int kthval = 0;
while(true){
// 处理边界情况:若数组索引越界,则选取对应数组中的最后一个元素
if (index1 == len1) return nums2[index2 + k - 1];
if (index2 == len2) return nums1[index1 + k - 1];
// 若k为1,返回两数组首元素的最小值
if (k == 1) return Math.min(nums1[index1],nums2[index2]);
int mid = k/2;
int newindex1 = Math.min(index1 + mid, len1) - 1;
int newindex2 = Math.min(index2 + mid, len2) - 1;
int tmp1 = nums1[newindex1], tmp2 = nums2[newindex2];
// 若nums1[k/2 - 1]<=nums2[k/2 - 1],则nums1[0]~nums1[k/2-1]不可能有第k个数,全部排除
if(tmp1 <= tmp2){
k -= (newindex1 - index1 + 1);
index1 = newindex1 + 1;
} else {
// 若nums1[k/2 - 1]>nums2[k/2 - 1],则nums2[0]~nums2[k/2-1]不可能有第k个数,全部排除
k -= (newindex2 - index2 + 1);
index2 = newindex2 + 1;
}
}
}
}