难易程度:★★★★
重 要 性: ★★★★★
该算法题在百度、猿辅导多家公司的面试中都出现过。
面试中经常要求手写代码求解此类问题!并且本文介绍的算法可以应用于其他类似问题的求解。
即:给定两个排好序的数组nums1和nums2,找出两个数组合并后的中位数。本文介绍一种O(log(Math.min(len1,len2)))复杂度的解法,这也是面试官期望的解法。本题思路如下:设nums1的长度为len1;nums2的长度为len2:
i将长度的len1的数组一份为二,i的取值可以为0~len1,共len1+1种取值;每个i取值都将nums1数组一分为二,划分后的两个子数组的长度为:i和len1-i。(下面有图解)。
j将长度为len2的数组一份为二,j的取值可以为0~len2,共len2+1种取值,每个j取值都将nums2数组一分为二,互粉后的两个子数组的长度为:j和len2-j。(下面有图解)
中位数将数组整体一份为二,并且具有如下特点:中位数将两个数组合并后的有序数组划分为两个等长或者长度相差为1的两部分(中位数左边和右边的子数组长度相等或者相差1):
如图:
i
nums1[i-1] / nums1[i]
nums2[j-1] / nums2[j]
j
上图的两个'/'分别代表i和j的切分位置,即i和j的取值;对于求解中位数而言,我们只需保证:
nums1[i-1]<=nums2[j]
nums2[j-1]<=nums1[i]
并且:当len1+len2为偶数时,左右两边数组长度相等:i+j = (len1-i)+(len2-j) ,即i+j = (len1+len2)/2;当len1+len2为奇数时,假设将多出的那个数划分到左侧,即左侧比右侧长度大一:i+j = (len1-i)+(len2-j)+1 ,即i+j=(len1+n+1)/2。所以有,不管len+len2是奇数还是偶数:
i+j = (len1+n+1)/2。
综上,解决这个问题只需要找到满足以下三个条件的i:
nums1[i-1]<=nums2[j]
nums2[j-1]<=nums1[i]
i+j = (len1+n+1)/2
前两个条件的含义是i和j左半部分的所有元素小于i和j右半部分的所有元素,即中位数的左边的最大元素必须小于等于中位数右边的最小元素。第三个条件含义是:右半部分长度与与半部分长度相等或者相差1(总长度为偶数时中位数是两个数的平均值,此时数组可以等分;总长度为奇数的时候,中位数只有一个值,此时数组不可以等分)。如果我们找到了满足上面三个条件的i和j:
len1+len2为偶数时(中位数为两个数平均值),中位数为:
(Math.max(nums1[i-1],nums2[j-1])
+Math.min(nums1[i],nums2[j])/2.0)
len1+len2为奇数时,中位数为:
Math.max(nums1[i-1],nums2[j-1])
因为左边比右边多一个元素,左边多出的那个元素就是中位数。
所以,我们只要搜索一个i的取值,同时有j = (len1+len2+1)/2 - i,使得满足下面两个条件:
nums1[i-1]<=nums2[j]
nums2[j-1]<=nums1[i]
找到满足条件的i和j之后,我们可以根据上面分析的结论,找出中位数。
对于i的取值,我们可以使用二分搜索,所以这个问题在O(log(Math.min(len1,len2)))复杂度就可以解决;另外需要注意i和j取值边界问题:
因为i的取值是限定在[0,len1]范围进行搜索,所以索引i不会有越界问题;但是对于j就有可能出现索引越界的可能,对此只需要保证:len1<=len2就可避免索引j的越界问题,证明如下:
条件:len1 <= len2 和j = (len1 + len2 + 1) / 2 - i(上面推导过整个表达式),i在[0,len1]区间搜索:
j >= (len1 + len2+ 1) / 2 - len1
= (len2-len1+1) / 2 >= 0
j<=(len2+len2+1) / 2 <=l en2
综上:当len1 <= len2时:0 <= j< = n,即当满足len1<=len2时,索引j的取值一定在[0,len2],得证。
分析总结:
我们的目标是找到一个i、 j=(len1+len2+1)/2-i,使得切分后两个数组左边子数组长度之和和右边子数组长度之和相等或者相差1,只要满足:
len1<=len2
j=(len1+len2+1)/2-i
nums1[i-1]<=nums2[j]
nums2[j-1]<=nums1[i]
注:文末有1.5T学习资料分享
class Solution {
public double findMedianSortedArrays
{
int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
/*
* 保证 len1<=len2
*/
if (len1 > len2) {
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
len1 = nums1.length;
len2 = nums2.length;
}
//只有一个数组时直接计算中位数
if (len2 == 0)
return 0.0;
if (len1 == 0) {
if (len2 % 2 != 0)
return nums2[len2/2]
* 1.0;
else {
return(nums2[len2/2-1]
+nums2[len2/2])/2.0;
}
}
int i_min = 0;
//i取值在[0,len1]
int i_max = len1;
int aux =(len1+len2+1)
/ 2;
// 二分搜索i
while (i_min <= i_max) {
int i=(i_min+i_max)/2;
//上面有推导,aux是常值
int j = aux - i;
/*i>0即有:
j<(len2+len2+1)/2
=len2,即nums2[j]不
会越界*/
if (i>0&&nums1[i-1]
> nums2[j])
/*想办法减小nums1[i-1],
因为nums1是升序,所以减小
nums1[i-1]就是减小i,即i
取值缩小在前半部分区间*/
i_max = i - 1;
/*i<len1,
j>(len1+n+1)/2-len1
=(len2-len1+1)/2>=0
所以nums2[j-1]不会越界*/
else if (i<len1&&nums1[i]
< nums2[j - 1])
/*想办法增大nums1[i],因为
nums1是升序,所以增大nums1[i]
就是增大i,即i取值只缩小在后半
部分区间*/
i_min = i + 1;
else {
// 找到了满足条件的i和j了
/* 由之前知道,如果总的数组
长度为奇数,左边部分比右边部分
长度多一个,当然最后的中位数
也就是左边最大值了*/
/*当length1+length2为奇数的时候,
中位数就是左半部分数组的最大值,
max_left;*/
/*若数组总长度是偶数,中位数为:
(max_left+max_right)/2
所以至少需要先找出max_left;
至于max_right与总长度是奇数
还是偶数有关*/
// 求解max_left
int max_left = 0;
if (i == 0)// 处理边界
max_left = nums2[j-1];
else if (j == 0)// 处理边界
max_left = nums1[i-1];
else
max_left = Math.max(
nums1[i-1],nums2[j-1]);
//总长度为奇数,中位数为max_left
if (((len1+len2)&1)!= 0){
return max_left*1.0;
}
/*总长度为偶数时,中位数为:
(max_left+max_right)/2
求解max_right*/
int min_right = 0;
if (i == len1)
//处理边界
min_right = nums2[j];
else if (j == len2)
//处理边界
min_right = nums1[i];
else
min_right = Math.min(
nums1[i],nums2[j]);
return (max_left
+min_right)/ 2.0;
}
}
return 1.0;// 只是为了通过编译
}
}
互联网求职、资料共享Q**:***990638,群内有1.5T学习资料,期待你的加入~
点击“阅读原文”获取更多面试技术分享