1. 程式人生 > 其它 >【Leetcode刷題篇】leetcode4 尋找兩個正序陣列的中位數

【Leetcode刷題篇】leetcode4 尋找兩個正序陣列的中位數

技術標籤:LeetCode刷題篇中位數正序陣列尋找中位數二分法

給定兩個大小為 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

示例 3:

輸入:nums1 = [0,0], nums2 = [0,0]
輸出:0.00000

示例 4:
輸入:nums1 = [], nums2 = [1]
輸出:1.00000

示例 5:
輸入:nums1 = [2], nums2 = []
輸出:2.00000

解法一、簡單粗暴,先將兩個數組合並,兩個有序陣列的合併也是歸併排序中的一部分。然後根據奇數,還是偶數,返回中位數。

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
	    	// 先合併兩個陣列
	    	ArrayList<Integer> res = new ArrayList
<>(); int i = 0,j=0; while(i<nums1.length&&j<nums2.length) { // 對其遍歷 if(nums1[i]<nums2[j]) { // 對其判斷 res.add(nums1[i]); i++; }else { res.add(nums2[j]); j++; } } while(i<nums1.length) { res.
add(nums1[i++]); } while(j<nums2.length) { res.add(nums2[j++]); } // 中位數 if(res.size()%2==1) { return res.get(res.size()>>1); }else { return (res.get((res.size()>>1)-1) + res.get(res.size()>>1))>>1; } }

解法二、解法二
其實,我們不需要將兩個陣列真的合併,我們只需要找到中位數在哪裡就可以了。

開始的思路是寫一個迴圈,然後裡邊判斷是否到了中位數的位置,到了就返回結果,但這裡對偶數和奇數的分類會很麻煩。當其中一個數組遍歷完後,出了 for 迴圈對邊界的判斷也會分幾種情況。總體來說,雖然複雜度不影響,但程式碼會看起來很亂。

首先是怎麼將奇數和偶數的情況合併一下。

用 len 表示合併後陣列的長度,如果是奇數,我們需要知道第 (len+1)/2 個數就可以了,如果遍歷的話需要遍歷 int(len/2 ) + 1 次。如果是偶數,我們需要知道第 len/2和 len/2+1 個數,也是需要遍歷 len/2+1 次。所以遍歷的話,奇數和偶數都是 len/2+1 次。

返回中位數的話,奇數需要最後一次遍歷的結果就可以了,偶數需要最後一次和上一次遍歷的結果。所以我們用兩個變數 left 和 right,right 儲存當前迴圈的結果,在每次迴圈前將 right 的值賦給 left。這樣在最後一次迴圈的時候,left 將得到 right 的值,也就是上一次迴圈的結果,接下來 right 更新為最後一次的結果。

迴圈中該怎麼寫,什麼時候 A 陣列後移,什麼時候 B 陣列後移。用 aStart 和 bStart 分別表示當前指向 A 陣列和 B 陣列的位置。如果 aStart 還沒有到最後並且此時 A 位置的數字小於 B 位置的陣列,那麼就可以後移了。也就是aStart<m&&A[aStart]< B[bStart]。

但如果 B 陣列此刻已經沒有數字了,繼續取數字 B[ bStart ],則會越界,所以判斷下 bStart 是否大於陣列長度了,這樣 || 後邊的就不會執行了,也就不會導致錯誤了,所以增加為 aStart<m&&(bStart) >= n||A[aStart]<B[bStart]) 。

public double findMedianSortedArrays(int[] A, int[] B) {
    int m = A.length;
    int n = B.length;
    int len = m + n;
    int left = -1, right = -1;
    int aStart = 0, bStart = 0;
    for (int i = 0; i <= len / 2; i++) {
        left = right;
        if (aStart < m && (bStart >= n || A[aStart] < B[bStart])) {
            right = A[aStart++];
        } else {
            right = B[bStart++];
        }
    }
    if ((len & 1) == 0)
        return (left + right) / 2.0;
    else
        return right;
}

解法三、上邊的兩種思路,時間複雜度都達不到題目的要求 O(log(m+n)O(log(m+n)。看到 log,很明顯,我們只有用到二分的方法才能達到。我們不妨用另一種思路,題目是求中位數,其實就是求第 k 小數的一種特殊情況,而求第 k 小數有一種演算法。

解法二中,我們一次遍歷就相當於去掉不可能是中位數的一個值,也就是一個一個排除。由於數列是有序的,其實我們完全可以一半兒一半兒的排除。假設我們要找第 k 小數,我們可以每次迴圈排除掉 k/2 個數。看下邊一個例子。

假設我們要找第 7 小的數字。
在這裡插入圖片描述
我們比較兩個陣列的第 k/2 個數字,如果 k 是奇數,向下取整。也就是比較第 33 個數字,上邊陣列中的 44 和下邊陣列中的 33,如果哪個小,就表明該陣列的前 k/2 個數字都不是第 k 小數字,所以可以排除。也就是 11,22,33 這三個數字不可能是第 77 小的數字,我們可以把它排除掉。將 13491349 和 4567891045678910 兩個陣列作為新的陣列進行比較。

橙色的部分表示已經去掉的數字。
在這裡插入圖片描述
由於我們已經排除掉了 3 個數字,就是這 3 個數字一定在最前邊,所以在兩個新陣列中,我們只需要找第 7 - 3 = 4 小的數字就可以了,也就是 k = 4。此時兩個陣列,比較第 2 個數字,3 < 5,所以我們可以把小的那個陣列中的 1 ,3 排除掉了。
在這裡插入圖片描述
我們又排除掉 2 個數字,所以現在找第 4 - 2 = 2 小的數字就可以了。此時比較兩個陣列中的第 k / 2 = 1 個數,4 == 4,怎麼辦呢?由於兩個數相等,所以我們無論去掉哪個陣列中的都行,因為去掉 1 個總會保留 1 個的,所以沒有影響。為了統一,我們就假設 4 > 4 吧,所以此時將下邊的 4 去掉。
在這裡插入圖片描述
由於又去掉 1 個數字,此時我們要找第 1 小的數字,所以只需判斷兩個陣列中第一個數字哪個小就可以了,也就是 4。

所以第 7 小的數字是 4。

我們每次都是取 k/2 的數進行比較,有時候可能會遇到陣列長度小於 k/2的時候。
在這裡插入圖片描述
此時 k / 2 等於 3,而上邊的陣列長度是 2,我們此時將箭頭指向它的末尾就可以了。這樣的話,由於 2 < 3,所以就會導致上邊的陣列 1,2 都被排除。造成下邊的情況。
在這裡插入圖片描述
由於 2 個元素被排除,所以此時 k = 5,又由於上邊的陣列已經空了,我們只需要返回下邊的陣列的第 5 個數字就可以了。

從上邊可以看到,無論是找第奇數個還是第偶數個數字,對我們的演算法並沒有影響,而且在演算法進行中,k 的值都有可能從奇數變為偶數,最終都會變為 1 或者由於一個數組空了,直接返回結果。

所以我們採用遞迴的思路,為了防止陣列長度小於 k/2,所以每次比較 min(k/2,len(陣列) 對應的數字,把小的那個對應的陣列的數字排除,將兩個新陣列進入遞迴,並且 k 要減去排除的數字的個數。遞迴出口就是當 k=1 或者其中一個數字長度是 0 了。

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int n = nums1.length;
    int m = nums2.length;
    int left = (n + m + 1) / 2;
    int right = (n + m + 2) / 2;
    //將偶數和奇數的情況合併,如果是奇數,會求兩次同樣的 k 。
    return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;  
}
    
    private int getKth(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 的長度小於 len2,這樣就能保證如果有陣列空了,一定是 len1 
        if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
        if (len1 == 0) return nums2[start2 + k - 1];

        if (k == 1) return Math.min(nums1[start1], nums2[start2]);

        int i = start1 + Math.min(len1, k / 2) - 1;
        int j = start2 + Math.min(len2, k / 2) - 1;

        if (nums1[i] > nums2[j]) {
            return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
        }
        else {
            return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
        }
    }