长大后想做什么?做回小孩!

0%

LeetCode——寻找两个有序数组的中位数

NO.4 寻找两个有序数组的中位数 困难

1h9jjs.png

思路一:暴力法 直接合并两个有序数组,然后根据奇偶性找到中位数。但是这种笨办法不能满足时间复杂度的要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int[] num=new int[nums1.length+nums2.length];
int count=0,i=0,j=0;
//合并两个有序数组
while (count<(nums1.length+nums2.length)){
if (i<nums1.length&&j<nums2.length){
if (nums1[i]<nums2[j]){
num[count++]=nums1[i++];
}else {
num[count++]=nums2[j++];
}
}else if (i<nums1.length){
num[count++]=nums1[i++];
}else if (j<nums2.length){
num[count++]=nums2[j++];
}
}
//判断合并后的数组元素个数的奇偶性
if (count%2==0){
//注意这里是2.0,如果是2会导致结果为int类型丢失精度
return (num[count/2-1]+num[count/2])/2.0;
}else {
return num[count/2];
}
}

时间复杂度:O(n+m)

思路二:二分法 根据题目中要求的时间复杂度O(log(m+n))想到要使用二分法。因此我们就不能合并两个数组了。

其实根据上一题我们就不难发现是否合并两个数组并不重要,我们知道两个数组的长度总和是count,知道中位数是第count/2个或者(num[count/2-1]+num[count/2])/2.0就够了。我们困难的是怎样在不同的数组之间进行二分法。

我们换个思考方向:我们把“找中位数”看作是”找第k小的数“的特殊情况。可以充分利用数组是有序的这一特点去找第k小的数,每次排除掉k/2个元素。

看一个”寻找第k小的数“例子:

  1. 假设我们现在要从A和B两个有序数组中找第7小的数字,我们先比较两个数组的第k/2个元素的大小。3<4所以A数组[1,2,3]这三个元素必然不是第7小的数字,所以排除掉。1o5FLF.png
  2. 已经排除了3个,所以我们现在需要在两个数组剩余的部分寻找第4小的数。同样的,我们先比较两个数组剩余元素的第k/2个元素的大小,5>3所以B数组[1,3]这两个元素必然不是第4小的元素,所以排除。1o5EdJ.png
  3. 我们继续在两个数组剩余部分寻找第2小的数。我们比较两个数组剩余元素的第k/2个元素,4=4去掉哪个都行,我们统一处理即可,去掉B的4元素。1o4zin.png
  4. 此时k=1,只需要判断两个数组剩余部分的第一个元素哪个小即可,找到A数组的4就是第7小的数。1o5PMT.png

按照上述例子中的算法,会出现一个问题:每次循环都需要取两个数组剩余部分的第k/2个元素进行比较,如果此时某个数组剩余部分不足k/2个元素怎么办???

再看一个例子:

  1. 依然是找第7小的数,但是B数组不能取到第k/2个元素,此时取出B数组的最后一个元素和A数组的第k/2个元素作比较即可。1o7m7D.png
  2. 此时B数组已空,所以直接返回A数组的第5个元素即可。1o7e0O.png

回到本题“寻找中位数”!有了这个”寻找第k小的数“的算法,去寻找两个有序数组的中位数就容易多了。可以看到无论是找第奇数个还是找第偶数个对上述算法并无影响,最终都会因为k==1或一个数组空了,返回寻找结果。

最终,“寻找中位数”这个算法我们就以递归的方式进行,为了防止数组长度小于k/2,所以每次比较数组的第min(k/2,数组剩余len)个元素,将小的那部分排除之后,将两个新数组继续送入递归,并将k减去排除的元素个数。递归的出口就是k==1或其中一个数组剩余长度为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
//将奇数和偶数情况统一处理,如果是奇数情况就求两次。这部分也可以用判断分别处理
int Kth1=(len1+len2+1)/2,Kth2=(len1+len2+2)/2;
//注意最后结果是double,如果/2会丢失精度
return (findKth(nums1,0,len1-1,nums2,0,len2-1,Kth1)
+findKth(nums1,0,len1-1,nums2,0,len2-1,Kth2))/2.0;
}
public int findKth(int[] nums1,int start1,int end1,int[] nums2,int start2,int end2,int k){
//计算两个数组剩余部分长度
int len1=end1-start1+1;
int len2=end2-start2+1;
//很巧妙的一步,让len1总是剩余长度较小的那个,如果出现为空的情况一定是len1
if (len1>len2)return findKth(nums2,start2,end2,nums1,start1,end1,k);
//递归的出口,当某个数组剩余长度为0或者k==1的时候
if (len1==0)return nums2[start2+k-1];
if (k==1)return Math.min(nums1[start1],nums2[start2]);
//比较两个数组剩余部分的第k/2个元素大小,如果越界则取数组最后一个元素进行比较即可
int i=start1+Math.min(len1,k/2)-1,j=start2+ Math.min(len2,k/2)-1;
//排除较小的元素部分,k减去排除元素的个数
if (nums1[i]<nums2[j]){
return findKth(nums1,i+1,end1,nums2,start2,end2,k-(i-start1+1));
}else {
return findKth(nums1,start1,end1,nums2,j+1,end2,k-(j-start2+1));
}
}

时间复杂度:O(log(n+m))


本人菜鸟,有错误请告知,感激不尽!

更多题解和学习记录博客:博客github