题目
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
解题思路
上一篇文章讲了个简单的二分查找的变形,查找元素的位置,变形为查找第一个大于等于指定元素的下标。再来看这个题,题目两个数组有序,要求时间复杂度log级别,因此也是一道考察二分查找的变形题目,关键在于如何二分。这里题目,要求找出并返回这两个正序数组的 中位数 。我们不妨用另一种思路,题目是求中位数,其实就是求第 k 小数的一种特殊情况,而求第 k 小数有一种算法。
由于数列是有序的,其实我们完全可以一半儿一半儿的排除。假设我们要找第 k 小数,我们可以每次循环排除掉 k/2 个数。这里需要注意,中位数当奇数时只需要找一个数字,偶数时找两个数字,为了统一形式,这里都找两个,只不过奇数时两个一样罢了。
无论是找第奇数个数字,还是第偶数个数字,对我们的算法并没有影响,而且在算法进行中,k 的值都有可能从奇数变为偶数,最终都会变为 1 或者由于一个数组空了,直接返回结果。
所以我们采用递归的思路,为了防止数组长度小于 k/2,所以每次比较 min(k/2,len(数组) 对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且 k 要减去排除的数字的个数。**递归出口就是当 k=1 或者其中一个数字长度是 0 了。**看到这可能还是有点迷,下面看代码,上面有注释。
代码
// 转化为 求第k小个数字
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n = nums1.length;
int m = nums2.length;
// 奇数K相同
int left = (m + n + 1) / 2;
int right = (m + n + 2) / 2;
// 递归调用函数,求两个数组的第K小的元素
return (getKth(nums1,0,n-1,nums2,0,m-1,left) + getKth(nums1,0,n-1,nums2,0,m-1,right)) / 2.0;
}
// 求第K小的元素
public int getKth(int[] nums1,int start1,int end1,int[] nums2,int start2,int end2,int k){
// 数组1的长度
int len1 = end1 - start1 + 1;
// 数组2的长度
int len2 = end2 - start2 + 1;
//为了方便计算,我们这里规定len1小于len2,这样就能保证如果有数组空了,一定是 len1
if(len1 > len2) return getKth(nums2,start2,end2,nums1,start1,end1,k);
if(len1 == 0) return nums2[start2 + k - 1]; // 索引从0开始,要减1
// 只剩一个元素了,选两个数组最小的即可
if(k == 1) return Math.min(nums1[start1],nums2[start2]);
// 为了防止数组不够用,因此如果超了直接赋值给最后一位
int i = start1 + Math.min(k /2, len1) - 1;
int j = start2 + Math.min(k / 2,len2) - 1;
// 比较当前元素,把小的那一部分截取掉
if(nums1[i] > nums2[j]){
// 截取nums2
return getKth(nums1,start1,end1,nums2,j + 1,end2,k - (j - start2 + 1));
}else{
// 截取nums1
return getKth(nums1,i + 1,end1,nums2,start2,end2,k - (i - start1 + 1));
}
}
总结
二分的题目比较灵活,因此要注意其变形,关键还在于如何确定找第几个,通过二分缩小范围,以及退出二分的条件。