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;
}
};