演算法-兩個排序陣列的中位數

zeng沐白發表於2018-07-07

題目

兩個排序陣列的中位數

給定兩個大小為 m 和 n 的有序陣列 nums1 和 nums2 。

請找出這兩個有序陣列的中位數。要求演算法的時間複雜度為 O(log (m+n)) 。

示例 1:

nums1 = [1, 3]
nums2 = [2]

中位數是 2.0
示例 2:

nums1 = [1, 2]
nums2 = [3, 4]

中位數是 (2 + 3)/2 = 2.5
複製程式碼

思路:

  1. 中位數下標 (假設陣列為arr,長度為len,下標範圍[0 ,len - 1])
    1. 如果len % 2 = 1 (為奇數),那麼中位數的值為arr[mindIndex = len /2] 。
    2. 如果len % 2 - 0 (為偶數),那麼中位數的值為(arr[len/2]+arr[len/2 - 1])/2.0的值的和的平均值。
  2. 思路一:最簡單的思路把兩個有序陣列合併成一個有序的陣列,再通過規則1.獲取中位數的值。優化:結合題目要求,只需要獲取中位數的值,並不需要真的要把兩個陣列排序成一個有序陣列。那麼通過兩陣列間隔讀取,下標後移,直到累加到位置為為len/2即可獲取到結果。這裡如何更快地把找到len/2的位置是效率的關鍵。
  3. 思路二:假定陣列A、B、長度分別為Alen、Blen。那麼排序後的新陣列為C、Clen。Clen = (Alen + Blen + 1)/ 2。因為A和B都是有序的,如果C是由A和B的一部分元素組成的。那麼可以想象,C必然是A左邊一定長度為ALefPairLen和B的左邊一定長度為BLeftPairLen的兩部分組成.ALefPairLen和BLeftPairLen是聯動關係,ALefPairLen + BLeftPairLen = (Alen + Blen + 1)/ 2,一增一減,一減一增,總長度是固定的。然後為了獲得中位數,只需要獲取其中一個陣列的用於合併那部分的末尾Index,或者index + 1 (index + 1 = LefPairLen) ;這就變成了一個查詢指定下標的問題了。

繼續思路二(切換到程式中的變數,便於理解)

  1. 定義: 假定,A陣列的用於合併的長度為i,,那麼最後一個元素的下標為i-1,i為用於合併的最後一個元素的下一個元素的下標。B的陣列的長度j = (Alen + Blen + 1)/ 2 - i,那麼B陣列的最後一個元素的下標為j-1,j為用於合併的最後一個元素的下一個元素的下標。

  2. 結束條件: 要獲取合法的i值,需要滿足 A[i -1 ] <= B[j] && A[j] >= B[j -1]; 如果leftPairA的最後一個元素少於leftPairB在原陣列中最後一個元素的下一個元素,那麼leftPairA是符合要求的。反過來B陣列也一樣。要達到在leftPairA和leftPairB右邊的元素都比這兩部分大,不然就需要繼續調整。

  3. 特殊情況:程式碼註釋中記錄。

  4. 數的獲取: 因為取長度的時候為halfLen = (Alen + Blen + 1)/ 2。如果總長度為奇數,最後一位就是中位fmax(a[i-1],B[j-1])。如果總長度為偶數,需要取處於中間的兩個數,最後一位(fmax(a[i-1],B[j-1])為左邊的一個,右邊一個為halfLen之外的fmin(A[i],B[j]))。

double findMedianSortedArrays(int* nums1, int nums1Size, int* nums2, int nums2Size) {
    int* A = nums1;
    int* B = nums2;
    int m = nums1Size;
    int n = nums2Size;
    
    if (m > n ) {
        int* tmp = A;
        A = B;
        B = tmp;
        int k = m;
        m = n;
        n = k;
    }
    
    int iMin = 0;
    int iMax = m;
    //halfLen 偶數等於實際的一半,奇數等於向上取整
    int halfLen = (m + n + 1) / 2 ;
    
    while (iMin <= iMax) {
        //偶數index[i-1 , i ],奇數[i]
        int i = (iMin + iMax) /2;
        //長度之外的第一個元素
        int j = halfLen - i;
        if (i < iMax && A[i] < B[j - 1] )
        {
            //太小了,下限提高
            iMin = iMin + 1;
        }
        else if ( i > iMin && A[i- 1] > B[j]) {
            //太大了,上限降低
            iMax = iMax - 1;
        }
        else
        {
            
            //滿足需求 (A[i - 1] < B[j] && A[i] > B[j - 1]) || i == 0 || j == 0 || i == m || j == n
            int leftMax = 0;
            //m > n
            if (i == 0) {
                //在A被選中的長度為0
                leftMax = B[j - 1];
            }else if (j == 0)
            {
                //在B中被選中的長度為0
                leftMax = A[i - 1];
            }
            else
            {
                //i = m;代表A陣列都被選中
                //j = n;代表整個B陣列都被選中
                //包含 j == n || i == m 的情況
                leftMax = fmax(A[i - 1], B[j - 1]);
            }
            
            if ((m + n) % 2 == 1) {
                return leftMax;
            }
            
            int rightMin = 0;
            if (i == m)
            {
                //A全部被選中。那麼總長度為偶數時,中位數為中間兩數的平均值,選中的最後一個數為leftMax,rightMin為hanlflen之外的第一個數B[j];
                rightMin = B[j];
            }else if (j == n)
            {
                //B全部被選中。那麼總長度為偶數時,中位數為中間兩數的平均值,選中的最後一個數為leftMax,rightMin為hanlflen之外的第一個數A[j];
                rightMin = A[i];
            }
            else
            {
                //包含j == 0 || i == 0 的情況
                rightMin = fmin(A[i], B[j]);
            }
            
            return (leftMax + rightMin) / 2.0 ;
        }
    }
    
    return 0;
}

複製程式碼
//另外一種效率不錯的解法
double findMedianSortedArrays(int* nums1, int nums1Size, int* nums2, int nums2Size) {
    int* A = nums1;
    int* B = nums2;
    int m = nums1Size;
    int n = nums2Size;
    
    if (m < n ) {
        int* tmp = A;
        A = B;
        B = tmp;
        int k = m;
        m = n;
        n = k;
    }
    
    int iMin = 0;
    int iMax = m;
    int halfLen = (m + n + 1) / 2 ;
    
    while (iMin <= iMax) {
        //偶數index[i-1 , i ],奇數[i]
        int i = (iMin + iMax) /2;
        //長度之外的第一個元素
        int j = halfLen - i;
        if ( A[i- 1] > B[j] && i > iMin ) {
            //太大了,上限降低
            iMax = iMax - 1;
        }
        else if (A[i] < B[j - 1] && i < iMax)
        {
            //太小了,下限提高
            iMin = iMin + 1;
        }
        else
        {
            
            //滿足需求 A[i - 1] < B[j] && A[i] > B[j - 1]
            int leftMax = 0;
            //m > n
            if (i == 0) {
                //在A中無法找到符合的數值
                leftMax = B[j - 1];
            }else if (j == 0)
            {
                //在B中無法找到符合的數值
                leftMax = A[i - 1];
            }
            else
            {
                //包含 j == n || i == m 的情況
                leftMax = fmax(A[i - 1], B[j - 1]);
                if ((m + n) % 2 == 1) {
                    return leftMax;
                }
            }
            
            int rightMin = 0;
             if (i == m)
            {
                rightMin = B[j];
            }else if (j == n)
            {
                rightMin = A[i];
            }
            else
            {
                //包含j == 0 || i == 0 的情況
                rightMin = fmin(A[i], B[j]);
        
            
            return (leftMax + rightMin) / 2.0 ;
        }
    }
    return 0;
}

複製程式碼

小結:這道理題要處理的細節比較多。切割長度halfLen = (m + n + 1) / 2,最後一位是哪一位,是剛好是中位數呢,還是中位數左移一位,還是右移一位呢。這個會影響到後面的中位數下標取值。還要搞懂每個臨界條件代表的意義。總的來說如果每一句程式碼都是必須的,那麼必然要做到對程式碼的含義瞭然於心,才能保證程式的正確性。對的思路+對的編碼 = 對的答案。

待完善L('ω')┘三└('ω')」....

相關文章