徒手挖地球十五周目

徒手挖地球十五周目

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

1h9jjs.png

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

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小的数字,所以排除掉。在这里插入图片描述

  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小的数。在这里插入图片描述
    按照上述例子中的算法,会出现一个问题:每次循环都需要取两个数组剩余部分的第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。

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))


这个算法逻辑上并不难,但是细节还是需要注意的。

//很巧妙的一步,让len1总是剩余长度较小的那个,如果出现为空的情况一定是len1
if (len1>len2)return findKth(nums2,start2,end2,nums1,start1,end1,k);

最初写了一套很多三元表达式判断的冗杂版实现,参考了大佬的实现之后发现这句确实巧妙。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章