1. 程式人生 > >兩個有序陣列尋找合併之後第k大的數

兩個有序陣列尋找合併之後第k大的數

http://m.blog.csdn.net/blog/fangkyo/8114784

經常有同學在面試或者筆試的時候遇到兩個有序陣列(未必等長)找第k個數的問題。歸併比較的方法固然可以完成,但是面試官總是期望O(logN)的解決方案。我參考了網上的所有方法都發現有特殊案例無法通過的情況,主要問題集中在邊界情況上,比如一個數組只有一個或兩個元素時,或者一個數組的長度不足k/2,總之很麻煩。現在我給出一個比較完美和簡單的解決方案,希望大家指正,如果可以的話希望廣大人民群眾不要再被這道題折磨了。

廢話少說,進入正題吧,我的方法是基於中位數的比較。為什麼使用中位數而不是在兩個陣列中各取k/2大的數呢,是因為只要陣列的長度不為0,那麼中位數總是存在的,而第K/2的數就不一定了,陣列的長度很有可能小於k/2哦。通過這樣的方法我們可以規避一大票邊界問題。

我們將兩個陣列按照3種情況來討論:


1、兩個陣列的長度均為奇數。
我們設a陣列的中位數下標為x,b陣列的中位數下標為y,則如下圖所示,a陣列的長度為2x+1,b陣列的長度為2y+1,總的元素的個數為2x+2y+2。

我們現在需要探討的就是k是大於x+y+1,還是小於x+y+1的情況,也就是我們想找的第k大的數是在a、b陣列歸併後的陣列中的前一半內,還是在後一半內。不失一般性,我們討論當a[x] <= b[y]的情況,


當k <= x + y + 1,也即k屬於歸併陣列的前一半時,從下圖可以看出黃色區域明顯不包含第k個數。原因很簡單,因為b[y]大於等於藍色部分的數,而藍色部分的數目即為x+y+1,恰好為全體數的前一半的個數,也就是說黃色區域的數不可能屬於歸併後的前一半陣列,可以排除在外,在剩餘的兩個陣列中尋找第k大數。

 

當k > x + y + 1,也即k屬於歸併後陣列的後一半,那麼從下圖可以看出黃色區域明顯不包含第k個數。原因是黃色區域的數必然小於藍色區域的數,而藍色區域的數的數目之和即為x+y+1,恰好為歸併後陣列的和的一半,因此可以將黃色區域排除在外,注意在剩餘的兩個 陣列中尋找第 k - x -1 大數。

a[x] > b[y]時,只是上述的情況反一下,讀者可以自己試著推一下。

2、兩個陣列的長度均為偶數

我們尋找a陣列的上中位下標,記為x,b陣列的下中位下標,記為y,如下圖所示。這樣錯開中位數的目的是為了在將來做排除時能夠保證像奇數情況下能將第k大數歸類到前一半或者後一半的完美性質。

這樣看來,a陣列長度為2x + 2,b陣列長度為2y,因此歸併後的陣列長度為2x + 2y + 2,與奇數情況一致。
不失一般性,我們討論a[x] <= b[y]的情況。

當k <= x + y + 1,也即k屬於歸併陣列的前一半時,如下圖所示。顯然黃色部分可以排除在外,因為黃色部分一定大於等於藍色部分,而藍色部分的長度之和為x+y+1,恰好為歸併陣列的一半。也就是說黃色部分必然位於歸併陣列的後一半,不可能包含第k個數。之後我們在排除了黃色區域的兩個陣列中尋找第k個數。



當k > x + y + 1,也即k屬於歸併陣列的後一半時,如下圖所示。顯然黃色部分可以排除在外,因為黃色部分一定小於藍色部分,而藍色部分的長度之和為x+y+1,恰好為歸併陣列的一半。也就是說黃色部分必然位於歸併陣列的前一半,不可能包含第k個數。之後我們在排除了黃色區域的兩個陣列中尋找第k - x - 1個數。


a[x] > b[y]時,只是上述的情況反一下,讀者可以自己試著推一下。

3、當a、b陣列的長度為一奇一偶的情況。
若 k = 1,那麼直接返回min(a[0], b[0])即可。
若 k > 1,不妨假設 a[0] < b[0],那麼a[0]必然是兩個陣列中最小的數,那麼排除a[0]後我們可以在a、b兩個陣列中尋找第k-1個數,並且a的長度減一,這樣就將原問題轉化為上述1、2討論的問題。

4、當a、b陣列的長度有一個為0時,直接返回另一個長度非0陣列的第k個數即可。

綜上所述,我們在每次遞迴的時候都去除了一個數組中包含一箇中位數的一半元素,直到一個數組被完全排除在外為止,直接去另一個數組的第k個數。上面的過程中由於每次總會去除一箇中位數,因此演算法總能夠收斂,不難證明演算法的時間複雜度為O(logN)。

我將我的程式碼貼出來作為參考:


#include <iostream>
using namespace std;

int getKth( int a[], int alen,
            int b[], int blen,
            int k)
{
     if( NULL == a || alen < 0 || NULL == b || blen < 0 || k <=0 || k > alen + blen )
         return -1;
         
     if( alen == 0 )
         return b[ k - 1 ];
     if( blen == 0 )
         return a[ k - 1 ];
     
     if( k == 1 )
         return min( a[0], b[0] );
         
     if( ( alen & 0x1 ) ^ ( blen &0x1 ) ) // 一奇一偶
     {
         if( a[0] < b[0] )
             return getKth( a+1, alen -1, b, blen, k-1);
         else
             return getKth( a, alen, b+1, blen - 1, k-1);
     } 
     
     int aMid, bMid;
     if( alen & 0x1 ) // 兩個陣列長度為奇數 
     {
         aMid = alen / 2;
         bMid = blen / 2;
     }
     else
     {
         aMid = alen / 2 - 1;
         bMid = blen / 2;
     }
     
     if( a[aMid] <= b[bMid] )
     {
         if( k <= aMid + bMid + 1 )
             return getKth( a, alen, b, bMid , k  );
         else
             return getKth( a+aMid + 1, alen - aMid - 1, b, blen, k - aMid - 1 );
     }
     else
     {
         if( k <= aMid + bMid + 1 )
             return getKth( a, aMid, b, blen, k );
         else
             return getKth( a, alen, b+bMid+1, blen - bMid - 1, k - bMid - 1 );
     }
     return 0;
}

int main()
{
    int a[] = {1,3,6,94};
    int b[] = {2,4,5,8,10,22};
    for( int i=1; i<=10; ++i )
         cout << getKth( a, 4, b, 6, i ) << endl;
    system("pause");
    return 0;
}