1. 程式人生 > >LeetCode | Median of Two Sorted Arrays(兩個陣列的中位數)

LeetCode | Median of Two Sorted Arrays(兩個陣列的中位數)

題目:

There are two sorted arrays A and B of size m and n respectively. Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).

題目解析:

這道題是讓找出兩個有序陣列A和B中,處於兩個中間值的數字。但要求時間複雜度為O(log (m+n))。

方法一:

很常規的做法是:將A和B歸併成一個數組,然後找出C[(n+m)/2]的值就行了,時間複雜度為O(n+m)。(對一個無序陣列歸併排序O(nlogn))

但明顯不符合題意,這就迫使我們想其他辦法。

方法二:

我們可以發現,現在我們是不需要“排序”這麼複雜的操作的,因為我們僅僅需要第k大的元素。我們可以用一個計數器,記錄當前已經找到第m大的元素了。同時我們使用兩個指標pA和pB,分別指向A和B陣列的第一個元素。使用類似於merge sort的原理,如果陣列A當前元素小,那麼pA++,同時m++。如果陣列B當前元素小,那麼pB++,同時m++。最終當m等於k的時候,就得到了我們的答案——O(k)時間,O(1)空間。

方法三:(需要再判斷A和B為空的情況,A為空,直接返回B的中間值)

看到log(m+n),我們能想到的是二分查詢(logn),那麼我們就向二分查詢這個方向上靠攏。

但兩個陣列是分別有序的,但是我們要找的中間值的位置是固定的:(n+m)/2。那麼我們就假設,在a中的長度為len1,在b中的長度為len2。因為長度一定,對其中一個進行折半,另外一個就通過“前一個相對移動的距離”來改變同樣的距離。

1、但是m和n的值相對大小不知道,我們就假定讓n<=m,那麼在函式中我們就做了相應的對換,並且函式指標也相應改變,保持a指向短陣列。

2、因為咱們要找的是(n+m)/2位置的值,那麼這個值要麼在a[len1]出現,要麼在b[len2]出現。

3、假定len2來折半查詢,當b[len2]這個值是中間值的時候,必須滿足a[len1]<=b[len2]<=a[len1+1]。表明a[len1+1]在模擬陣列C[n+m]中不會出現在b[len2]前面;當a[len1]為中間值的時候,必須滿足a[len1-1]<=b[len2]<=a[len1]。正因為b[len2]和a[len1]都要在C中前半段出現,就要找其中大的一個,當滿足a[len1-1]<=b[len2]時就不用再進行折半查詢,已經能確定a[len1]為中間值。

4、當len1為0的時候的特殊比較:如果b[len2]<=a[len1]並且當b[len2+1]<=a[len1]的時候,C的前半段將不含a陣列資料,中間值為b[len2+1].

5、當len1為n-1的時候:a[len1]<=b[len2]就已經能保證b[len2]為中間值,就不需要也不能比較b[len2]<=a[len1+1]這個表示式了。

6、不用再判斷low和high的關係,因為我們已經保證了n<=m,所以把教研全部都放在了a陣列的下標驗證中。

程式碼如下:

#include <stdio.h>
#include <stdlib.h>

void Swap(int *a,int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

int MedianOfArray(int arr1[],int arr2[],int n,int m)
{
    printf("%d %d\n",n,m);
    int *a,*b;
    if(n > m){  //交換兩個引數,使得n <= m 並且a指向短陣列,b指向長陣列
        Swap(&n,&m);
        a = arr2;
        b = arr1;
    }else{
        a = arr1;
        b = arr2;
    }

    int low = 0;            //以下三個引數用於陣列b中
    int high;   //(n+m)/2表示要在b中找到多少個元素,轉化成陣列下標
    //對於{1,2,3,4,5},{6}來說,high=(n+m)/2的時候,high=3,mid=1指向了2,但是len=1卻超出了範圍
    high = (n+m)/2 - 1;     //減一是為了轉化成陣列下標;
        //high = (n+m)/2 - 1 -1;再減一是為了表示a陣列先給一個元素指標,剩下的high為在b陣列中的個數,
                        //但如果n+m=7時,high為1,也就是2個,算上a中的一共才3個,與中間值4不相符
    int mid = high;         //
    int len = 0;   //用於陣列a中,high-mid為個數,減一是轉化成下標
    int temp,increase,decrease;

    //n和m一樣長也要考慮
    while(1){
        printf("len = %d,mid = %d,a[len] = %d,b[mid] = %d\n",len,mid,a[len],b[mid]);
        if(a[len] < b[mid]){
            if(len+1 >= n && n == m)  //處理右邊界情況,當n==m的時候。由於前面保證了n<=m所以,要麼在a最右邊界,要麼在b中
                return a[len];
            if( (len+1)>=n || a[len+1] >= b[mid])    //當len為邊界的時候,或者b[mid]介於a[len]和a[len+1]之間
                return b[mid];                                      //或者當low和high已經不能再移動的時候

            high = mid;                 //調整high的值,重新界定mid和len的位置
            temp = (low + high)/2;      //但必須要注意防止len的值超過n,所以就需要if語句判斷

            if(high - temp > n - len - 1)
                increase = n - len - 1;
            else
                increase = high - temp;
            mid = high - increase;
            len = len + increase;

        }else{
            if(len == 0){   //處理左邊界情況。
                if(a[len] >= b[mid+1])  //如果a[len]大於等於b[mid+1]證明,中值已經中值一下的元素全在b中
                    return b[mid+1];
                return a[len];  //如果不是上述那樣,則a[len]就是中值!
            }
            if(a[len-1] <= b[mid])  //如果b[mid]介於a[len]和a[len-1]之間,取較大值a[len]
                return a[len];
            if(low == high)
                return b[mid];
            low = mid;
            temp = (low+high)/2;

            if(temp - low > len)
                decrease = len;
            else
                decrease = temp - low;
            mid = low + decrease;
            len = len - decrease;  //必須為mid-low,b陣列mid增加減少多少,a陣列len相應改變多少!
        }
    }
}

int main(void)
{
    int arr1[] = {1,2,3,4,5,6,7};
    int arr2[] = {1,2,3,4,5,6,7};

    printf("the median is :%d",MedianOfArray(arr1,arr2,sizeof(arr1)/sizeof(int),sizeof(arr2)/sizeof(int)));
    return 0;
}


方法四:

其實和思路二差不多,但是利用遞迴的方法實現的:尋找第k個數據。

該方法的核心是將原問題轉變成一個尋找第k小數的問題(假設兩個原序列升序排列),這樣中位數實際上是第(m+n)/2小的數。所以只要解決了第k小數的問題,原問題也得以解決。

首先假設陣列A和B的元素個數都大於k/2,我們比較A[k/2-1]和B[k/2-1]兩個元素,這兩個元素分別表示A的第k/2小的元素和B的第k/2小的元素。這兩個元素比較共有三種情況:>、<和=。如果A[k/2-1]<B[k/2-1],這表示A[0]到A[k/2-1]的元素都在A和B合併之後的前k小的元素中。換句話說,A[k/2-1]不可能大於兩數組合並之後的第k小值,所以我們可以將其拋棄。下一次我們再A[k/2...n]和B[0...m]之間找k-k/2小的資料,直到尋找第k小的資料。

當A[k/2-1]>B[k/2-1]時存在類似的結論。

當A[k/2-1]=B[k/2-1]時,我們已經找到了第k小的數,也即這個相等的元素,我們將其記為m。由於在A和B中分別有k/2-1個元素小於m,所以m即是第k小的數。
(這裡可能有人會有疑問,如果k為奇數,則m不是中位數。這裡是進行了理想化考慮,在實際程式碼中略有不同,是先求k/2,然後利用k-k/2獲得另一個數。)

通過上面的分析,我們即可以採用遞迴的方式實現尋找第k小的數。此外我們還需要考慮幾個邊界條件
如果A或者B為空,則直接返回B[k-1]或者A[k-1];
如果k為1,我們只需要返回A[0]和B[0]中的較小值;
如果A[k/2-1]=B[k/2-1],返回其中一個;

最終實現的程式碼為:

double findKth(int a[], int m, int b[], int n, int k)
{
	//always assume that m is equal or smaller than n
	if (m > n)
		return findKth(b, n, a, m, k);
	if (m == 0)
		return b[k - 1];
	if (k == 1)
		return min(a[0], b[0]);
	//divide k into two parts
	int pa = min(k / 2, m), pb = k - pa;
	if (a[pa - 1] < b[pb - 1])
		return findKth(a + pa, m - pa, b, n, k - pa);
	else if (a[pa - 1] > b[pb - 1])
		return findKth(a, m, b + pb, n - pb, k - pb);
	else
		return a[pa - 1];
}

class Solution
{
public:
	double findMedianSortedArrays(int A[], int m, int B[], int n)
	{
		int total = m + n;
		if (total & 0x1)
			return findKth(A, m, B, n, total / 2 + 1);
		else
			return (findKth(A, m, B, n, total / 2)
					+ findKth(A, m, B, n, total / 2 + 1)) / 2;
	}
};