徒手挖地球十五週目

徒手挖地球十五週目

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

最初寫了一套很多三元表達式判斷的冗雜版實現,參考了大佬的實現之後發現這句確實巧妙。

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