尋找兩個有序數組的中位數

給定兩個大小爲 m 和 n 的有序數組 nums1 和 nums2。請你找出這兩個有序數組的中位數,而且要求算法的時間複雜度爲 O(log(m + n))。你能夠假設 nums1 和 nums2 不會同時爲空。(LeetCode題目)

示例 1:

nums1 = [1, 3]
nums2 = [2]
則中位數是 2.0

示例 2:

nums1 = [1, 2]
nums2 = [3, 4]
則中位數是 (2 + 3)/2 = 2.5

方法一:子集劃分

爲了解決這個問題,咱們須要理解 「中位數的做用是什麼」。在統計中,中位數被用來:將一個集合劃分爲兩個長度相等的子集,其中一個子集中的元素老是大於另外一個子集中的元素。
若是理解了中位數的劃分做用,咱們就很接近答案了。算法

首先,讓咱們在任一位置 i 將 A劃分紅兩個部分:數組

left_A             |        right_A
A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]

因爲 A中有 m個元素, 因此咱們有 m+1種劃分的方法(i=0∼m)。3d

咱們知道:
len(left_A)=i,len(right_A)=m−i
注意:當 i=0 時,left_A爲空集, 而當 i=m時, right_A爲空集。code

採用一樣的方式,咱們在任一位置 j將 B劃分紅兩個部分:對象

left_B             |        right_B
B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]

將 left_A和 left_B放入一個集合,並將 right_A和 right_B放入另外一個集合。 再把這兩個新的集合分別命名爲 left_part和 right_part:blog

left_part          |        right_part
A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]
B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]

若是咱們能夠確認:遞歸

len(left_part)=len(right_part)
    max⁡(left_part)≤min⁡(right_part)

那麼,咱們已經將 {A,B}中的全部元素劃分爲相同長度的兩個部分,且其中一部分中的元素老是大於另外一部分中的元素。那麼:median=(max(left_part)+min(right_part)​)/2leetcode

要確保這兩個條件,咱們只須要保證:io

i+j=m−i+n−j(或:m−i+n−j+1,即m+n爲奇數時,把多的一個放left_part)
    若是 n≥m只須要使  i=0∼m, j=(m+n+1)/2−i(由於j是整型,因此m+n爲奇數或偶數時,j都是(m+n+1)/2,如m+n=3,j=2;m+n=4,j=2)
    B[j−1]≤A[i] 以及 A[i−1]≤B[j]

ps.1 爲了簡化分析,假設 A[i−1],B[j−1],A[i],B[j]老是存在,哪怕出現 i=0,i=m,j=0或是 j=n這樣的臨界條件。咱們將在最後討論如何處理這些臨界值。class

ps.2 爲何 n≥m?因爲0≤i≤m且 j=(m+n+1)/2−i,咱們必須確保 j不是負數。若是 n<m,那麼 j 將多是負數,而這會形成錯誤的答案。

因此,咱們須要作的是:

在 [0,m]中搜索並找到目標對象 i,以使:

B[j−1]≤A[i]且 A[i−1]≤B[j], 其中 j=(m+n+1)/2−i

接着,咱們能夠按照如下步驟來進行二叉樹搜索:

設 imin=0,imax=m, 而後開始在 [imin,imax]中進行搜索。

令 i=(imin+imax2)/2​, j=(m+n+1)/2−i

如今咱們有 len(left_part)=len(right_part)。 並且咱們只會遇到三種狀況:

    B[j−1]≤A[i]且 A[i−1]≤B[j]:
    這意味着咱們找到了目標對象 i,因此能夠中止搜索。

    B[j−1]>A[i]:
    這意味着 A[i]過小,咱們必須調整 i 以使 B[j−1]≤A[i]。
    咱們能夠增大 i 嗎?
          是的,由於當 i 被增大的時候,j 就會被減少。
          所以 B[j−1]會減少,而 A[i]會增大,那麼 B[j−1]≤A[i]就可能被知足。
    咱們能夠減少 i 嗎?
          不行,由於當 i 被減少的時候,j 就會被增大。
          所以 B[j−1]會增大,而 A[i] 會減少,那麼 B[j−1]≤A[i]就可能不知足。
    因此咱們必須增大 i。也就是說,咱們必須將搜索範圍調整爲 [i+1,imax]。

    A[i−1]>B[j]:
    這意味着 A[i−1] 太大,咱們必須減少 i以使 A[i−1]≤B[j]。
    也就是說,咱們必須將搜索範圍調整爲 [imin,i−1]。

當找到目標對象 i 時,中位數爲:

max⁡(A[i−1],B[j−1]), 當 m+n爲奇數時

max⁡(A[i−1],B[j−1])+min⁡(A[i],B[j])/2, 當 m+n爲偶數時

如今,讓咱們來考慮這些臨界值 i=0,i=m,j=0,j=n,此時 A[i−1],B[j−1],A[i],B[j]可能不存在。
其實這種狀況比你想象的要容易得多。

咱們須要作的是確保 max(left_part)≤min(right_part)。 所以,若是 i和 j 不是臨界值(這意味着 A[i−1],B[j−1],A[i],B[j]所有存在), 那麼咱們必須同時檢查 B[j−1]≤A[i]以及 A[i−1]≤B[j]是否成立。
可是若是 A[i−1],B[j−1],A[i],B[j]中部分不存在,那麼咱們只須要檢查這兩個條件中的一個(或不須要檢查)。
舉個例子,若是 i=0那麼 A[i−1]不存在,咱們就不須要檢查 A[i−1]≤B[j]是否成立。
因此,咱們須要作的是:

在 [0,m]中搜索並找到目標對象 i,以使:

(j=0 or i=m or B[j−1]≤A[i])或是
(i=0 or j=n or A[i−1]≤B[j]), 其中 j=(m+n+1)/2−i

在循環搜索中,咱們只會遇到三種狀況:

(j=0 or i=m or B[j−1]≤A[i])或是 (i=0 or j=n or A[i−1]≤B[j]),這意味着 i 是完美的,咱們能夠中止搜索。
    j>0 and i<m and B[j−1]>A[i] 這意味着 i 過小,咱們必須增大它。
    i>0 and j<n and A[i−1]>B[j] 這意味着 i 太大,咱們必須減少它。
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        if (m > n) { //確保n>=m
            vector<int> temp = nums1;
            nums1 = nums2, nums2 = temp;
            m = nums1.size(), n = nums2.size();
        }
        int imin = 0, imax = m,half=(m+n+1)/2;
        int i = (imin + imax) / 2, j = half - i;
        while (imax>=imin)
        {
            i = (imin + imax) / 2, j = half - i;
            if (j > 0 && i<m && nums2[j - 1]>nums1[i])
                imin = i + 1;
            else if (i > 0 && j<n && nums1[i - 1]>nums2[j])
                imax = i - 1;
            else
            {
                int maxleft, minright;
                if (i == 0)maxleft = nums2[j - 1];
                else if (j == 0)maxleft = nums1[i - 1];
                else
                {
                    maxleft = nums2[j - 1] > nums1[i - 1] ? nums2[j - 1] : nums1[i - 1];
                }

                if ((m + n) % 2)
                    return maxleft * 1.0;

                if (i == m)minright = nums2[j];
                else if (j == n)minright = nums1[i];
                else 
                {
                    minright = nums2[j] < nums1[i] ? nums2[j] : nums1[i];
                }

                return (maxleft + minright) * 1.0 / 2;
            }
        }
        return -1;
            
    }
};

方法二:第K小值

題目是求中位數,其實就是求第 k 小數的一種特殊狀況。
因爲數列是有序的,其實咱們徹底能夠一半一半的排除。假設咱們要找第 k 小數,咱們能夠每次循環排除掉 k/2 個數。看下邊一個例子。
假設咱們要找第 7 小的數字。

咱們比較兩個數組的第 k/2 個數字,若是 k 是奇數,向下取整。也就是比較第 3 個數字,上邊數組中的 4 和下邊數組中的 3,若是哪一個小,就代表該數組的前 k/2 個數字都不是第 k 小數字,因此能夠排除。也就是 1,2,3這三個數字不多是第7 小的數字,咱們能夠把它排除掉。將 1,3,4,9和 4,5,6,7,8,9,10 兩個數組做爲新的數組進行比較。

更通常的狀況 A[1] ,A[2] ,A[3],A[k/2] ... ,B[1],B[2],B[3],B[k/2] ... ,若是 A[k/2]<B[k/2] ,那麼A[1],A[2],A[3],A[k/2]都不多是第 k 小的數字。

橙色的部分表示已經去掉的數字。

因爲咱們已經排除掉了 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 了。

class Solution {
public:
    int findK(vector<int>& A1, int s1, int e1, vector<int>& A2, int s2, int e2, int k) {
        int len1 = e1 - s1 + 1, len2 = e2 - s2 + 1;
        if (len1 == 0)return A2[s2 + k - 1]; //因爲數組下標從0開始,因此要減1
        if (len2 == 0)return A1[s1 + k - 1];
        if (k == 1)return A1[s1] < A2[s2] ? A1[s1] : A2[s2];
        int m = k / 2;
        int i = len1 < m ? s1 + len1 - 1 : s1 + m - 1;
        int j= len2 < m ? s2 + len2 - 1 : s2 + m - 1;
        if (A1[i] < A2[j])return findK(A1, i+1, e1, A2, s2, e2, k - (i-s1+1));
        else
            return findK(A1, s1, e1, A2, j+1, e2, k -(j-s2+1));
    }
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        int k1 = (m + n + 1) / 2, k2 = (m + n + 2) / 2; //如m+n爲3,則k1=2,k2=2;如m+n=4,則k1=2,k2=3。從而使得m+n爲奇數和偶數獲得的中位數公式統一,即(findk1+findk2)/2
        return (findK(nums1, 0, m - 1, nums2, 0, n - 1, k1) + findK(nums1, 0, m - 1, nums2, 0, n - 1, k2)) * 0.5;
    }
};
相關文章
相關標籤/搜索