求兩個有序數組的中位數

這是我作的第二個leetcode題目,一開始覺得和第一個同樣很簡單,可是作的過程當中才發現這個題目很是難,給人一種「剛上戰場就踩上地雷掛掉了」的感受。後來搜了一下leetcode的難度分佈表(leetcode難度及面試頻率)才發現,該問題是難度爲5的問題,真是小看了它!網上搜了不少答案,可是鮮見簡明正確的解答,惟有一種尋找第k小值的方法很是好,在此整理一下。面試

       首先對leetcode的編譯運行吐槽一下:貌似沒有超時判斷,並且small和large的數據集相差很小。此題一開始我採用最笨的方法去實現,利用排序將兩個數組合併成一個數組,而後返回中位數:算法

 

[cpp]  view plain copy print ? 在CODE上查看代碼片 派生到個人代碼片
 
  1. class Solution {  
  2. public:  
  3.     double findMedianSortedArrays(int A[], int m, int B[], int n) {  
  4.         // Start typing your C/C++ solution below  
  5.         // DO NOT write int main() function  
  6.         int *a=new int[m+n];  
  7.           
  8.         memcpy(a,A,sizeof(int)*m);  
  9.         memcpy(a+m,B,sizeof(int)*n);  
  10.           
  11.         sort(a,a+n+m);  
  12.           
  13.         double median=(double) ((n+m)%2? a[(n+m)>>1]:(a[(n+m-1)>>1]+a[(n+m)>>1])/2.0);  
  14.           
  15.         delete a;  
  16.           
  17.         return median;  
  18.     }  
  19. };  

 

該方法竟然也經過測試,可是其複雜度最壞狀況爲O(nlogn),這說明leetcode只對算法的正確性有要求,時間要求其實不嚴格。數組

另外一種方法便是利用相似merge的操做找到中位數,利用兩個分別指向A和B數組頭的指針去遍歷數組,而後統計元素個數,直到找到中位數,此時算法複雜度爲O(n)。以後還嘗試了根據算法導論中的習題(9.3-8)擴展的方法,可是該方法會存在無窮多的邊界細節問題,並且擴展也不見得正確,這個可從各網頁的評論看出,很是不建議你們走這條路。ide

最後從medianof two sorted arrays中看到了一種很是好的方法。原文用英文進行解釋,在此咱們將其翻譯成漢語。該方法的核心是將原問題轉變成一個尋找第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-1]大於合併以後的第k小值,咱們不妨假定其爲第(k+1)小值。因爲A[k/2-1]小於B[k/2-1],因此B[k/2-1]至少是第(k+2)小值。但實際上,在A中至多存在k/2-1個元素小於A[k/2-1],B中也至多存在k/2-1個元素小於A[k/2-1],因此小於A[k/2-1]的元素個數至多有k/2+ k/2-2,小於k,這與A[k/2-1]是第(k+1)的數矛盾。ui

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

當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得到另外一個數。).net

經過上面的分析,咱們便可以採用遞歸的方式實現尋找第k小的數。此外咱們還須要考慮幾個邊界條件:翻譯

 

  • 若是A或者B爲空,則直接返回B[k-1]或者A[k-1];
  • 若是k爲1,咱們只須要返回A[0]和B[0]中的較小值;
  • 若是A[k/2-1]=B[k/2-1],返回其中一個;

 

最終實現的代碼爲:

[cpp]  view plain copy print ? 在CODE上查看代碼片 派生到個人代碼片
 
  1. double findKth(int a[], int m, int b[], int n, int k)  
  2. {  
  3.     //always assume that m is equal or smaller than n  
  4.     if (m > n)  
  5.         return findKth(b, n, a, m, k);  
  6.     if (m == 0)  
  7.         return b[k - 1];  
  8.     if (k == 1)  
  9.         return min(a[0], b[0]);  
  10.     //divide k into two parts  
  11.     int pa = min(k / 2, m), pb = k - pa;  
  12.     if (a[pa - 1] < b[pb - 1])  
  13.         return findKth(a + pa, m - pa, b, n, k - pa);  
  14.     else if (a[pa - 1] > b[pb - 1])  
  15.         return findKth(a, m, b + pb, n - pb, k - pb);  
  16.     else  
  17.         return a[pa - 1];  
  18. }  
  19.   
  20. class Solution  
  21. {  
  22. public:  
  23.     double findMedianSortedArrays(int A[], int m, int B[], int n)  
  24.     {  
  25.         int total = m + n;  
  26.         if (total & 0x1)  
  27.             return findKth(A, m, B, n, total / 2 + 1);  
  28.         else  
  29.             return (findKth(A, m, B, n, total / 2)  
  30.                     + findKth(A, m, B, n, total / 2 + 1)) / 2;  
  31.     }  
  32. };  

咱們能夠看出,代碼很是簡潔,並且效率也很高。在最好狀況下,每次都有k一半的元素被刪除,因此算法複雜度爲logk,因爲求中位數時k爲(m+n)/2,因此算法複雜度爲log(m+n)。

 

 

若是有兩個有序的數組,都是已經排好序的。那麼求它們的中位數應該怎樣求呢。若是採用對這兩個數組進行排序的方法,最快的時間複雜度也要o(nlogn)的時間。可是,若是採用中位數和順序統學的方法來尋找,則能夠在o(n)的時間內解決這個問題。

       咱們先尋找每一個數組的中位數,由於是排好順序的數組,所以,能夠在o(1)時間內找到。而後,比較這兩個數字的大小。若是A的中位數大於B的中位數,則在A的前半個數組和B的後半個數組中尋找,反之,在B的前半個數組和A的後半個數組尋找。根據遞歸方程,解得時間複雜度是o(n)。

       中位數:一組數據中間位置的數,若是是偶數個數,則取中間兩個位置數的平均值。

       此題兩個數組個數同樣,那麼兩個數組的中位數總個數是2*n也就是偶數,中位數必定是中間兩個位置的平均數。時間複雜度要求O(logn),則必定要充分利用數組有序的信息。

       網上看了不少版本,都是對奇偶數的考慮不全。本身試着編寫了一下,發現不少細節很容易忽略。測試考慮的是整數,能夠把數組直接所有設置爲double類型。若是是整型,要考慮結果類型的強制轉換。還有最後遞歸結束條件只考慮兩數組中只剩下一個元素,或者中位數相等的兩種狀況。你們能夠多找幾個例子試驗下。此代碼在VC++6.0上測試過。你們能夠本身再測試下,有問題的話歡迎提出。代碼以下:

//最後返回結果必定要是double,由於是兩個中位數的平均值不必定是整數
double MidNum(int *A,int l1,int r1,int *B,int l2,int r2)
{
	//根據奇偶決定中位數的位置,要保證兩個字數組元素個數相等
	int mid1,mid2;
	if( (r1-l1+1)%2==0 )	//偶數時
	{
		mid1=(l1+r1)/2+1;	//A取下中位數
		mid2=(l2+r2)/2;		//B取上中位數
	}
	else					//奇數時
	{
		mid1=(l1+r1)/2;
		mid2=(l2+r2)/2;
	}

	if(l1==r1 && l2==r2)			//最後兩個數組都剩下一個元素
		return (double)(A[l1]+B[l2])/2;		
	//最後兩個數組都剩下兩個元素,並且A[mid1]>B[mid2],底下的狀況處理不了,一直遞歸下去
	//如A[6]={1,3,5,6,8,10};B[6]={2,4,7,9,11,15};最後A{6,8},B{4,7}
	if( r1-l1==1 && r2-l2==1 )		
	{
		if(A[mid1]>B[mid2])			//得對剩下的4個數排序A[l1],A[r1](r1==mid1),B[l2](l2==mid2),B[r2]
		{
			if(B[r2]<=A[l1])		//B[mid2],B[r2],A[l1],A[mid1]
				return (double)(B[r2]+A[l1])/2;
			else if(A[l1]<=B[mid2] && A[mid1]>=B[r2])	//A[l1],B[mid2],A[mid1],B[r2]
				return (double)(B[mid2]+A[mid1])/2;
			else if(A[l1]<=B[mid2] && A[mid1]<B[r2])	//A[l1],B[mid2],B[r2],A[mid1]
				return (double)(B[mid2]+B[r2])/2;			
		}
	}
	if(A[mid1]==B[mid2])
		return A[mid1];
	else if( A[mid1] > B[mid2])
		return MidNum(A,l1,mid1,B,mid2,r2);
	else 
		return MidNum(A,mid1,r1,B,l2,mid2);
}


int main()
{
	int A[6]={1,3,5,6,8,10};
	int B[6]={2,4,6,9,11,15};

	double m2=MidNum(A,0,5,B,0,5);
	cout<<m2<<endl;

	int A2[10]={17,18,28,37,42,54,63,72,89,96};
	int B2[10]={3,51,71,72,91,111,121,131,141,1000};

	double m=MidNum(A2,0,9,B2,0,9);
	cout<<m<<endl;

	return 0;
}

  因爲複雜度是logn的,因此考慮要用到二分。

首先,假設兩個數組的長度都是奇數,並且大於1。令mid 爲 (1 + n) / 2,也就是中間的那個元素的下標。考慮一下X[mid]和Y[mid]的大小關係:

(1) X[mid] > Y[mid]
這種狀況下,咱們能夠想,當咱們把兩個數組合並排序後,X[mid]的排名(排名從1開始)確定是大於n的,由於咱們能夠肯定這些元素必定小於等於X[mid]:X[1...mid - 1],Y[1...mid - 1] ,Y[mid]。
同理,能夠分析出來,Y[mid]的排名確定是小於n + 1的。引入一個定理,若是咱們同時殺掉X[mid]後面的任意k個元素和Y[mid]前面的任意k個元素(k > 0),那麼,獲得的新的兩個數組的中位數,與原數組,仍然是同樣的。這個定理畫個圖不難證實。因此,原問題就被轉化爲一個更小的子問題了。

(2)X[mid] == Y[mid]
卻是這種狀況,我想了好久,到底應該怎麼處理。後來發現,本身犯傻了。若是X[mid]等於Y[mid]的話,考慮一下咱們排序的過程。首先,咱們能夠將X[1...mid-1]和Y[1...mid-1]合併排序獲得一個長度爲2*(mid-1)的新數組P,而後咱們把X[mid + 1...n]和Y[mid + 1...n]合併排序,獲得一個長度也是2*(mid-1)的新數組R,最後,咱們把X[mid] 和 Y[mid]插在中間,就獲得最後的有序數組了:P,X[mid],Y[mid],R
也就是說,當X[mid] == Y[mid]時,你能夠立刻肯定X[mid]和Y[mid]就是你要找的兩個中位數!

(3)X[mid] < Y[mid]
這種狀況和狀況(1)對稱,不累贅了。


而後,假設兩個數組的長度都是偶數,並且大於1。令mid爲(1+n)/ 2,也就是中間的兩個元素裏左邊的那個元素的下標。考慮一下X[mid] 和 Y[mid + 1]的大小關係
(1)X[mid] > Y[mid]
使用與上面奇數長度的狀況的相似的思路,咱們能夠知道,X[mid]的排名在一半之後,而Y[mid]的排名在一半之後,因此,咱們也能夠用一樣的思路來縮小問題的規模。
(2) X[mid] == Y[mid]
一樣的思路,因此一樣的結論。當它們相等的時候,你是能夠立刻肯定它們就是你要找的兩個中位數。
(3)X[mid] < Y[mid]
對稱,同樣的作法。

http://blog.csdn.net/hhygcy/article/details/4584064

2個有序數組求合併後的中位數

 

第一步:假設兩個有序數組(已經各自排序完成了)長度相等,試寫函數找出兩個數組合並後的中位數。 第二步:假設兩個有序數組長度不等,同樣的求出中位數

 


解析: 這個題目看起來很是簡單。第一題的話: 假設數組長度爲n, 那麼我就把數組1和數組2直接合並,而後再直接找到中間元素。對於這樣的方案,第一題和第一題就沒有什麼區別了。這樣的話時間複雜度就是O(n)。一般在這樣的狀況下,那些mentor類型的達人就會循循善誘道:「你還有更好的辦法嗎:)」 若是比線性更高效,直接能想到的就是對數了O(log(n)),這個時間複雜度在這裏可能嗎? 固然仍是可能的。來繼續看看下面的分析。

先找來了一個圖(本身畫的,簡陋了點)

sample

咱們先來分析看看: 想到對數的效率,首先想到的就是二分查找,對於這個題目二分查找的意義在哪裏呢?

咱們找到了A[n/2] 和 B[n/2]來比較,

若是他們相等,那樣的話,咱們的搜索結束了,由於答案已經找到了A[n/2]就確定是排序後的中位數了。

若是咱們發現B[n/2]>A[n/2],說明什麼,這個數字應該在 A[n/2]->A[n]這個序列裏面, 或者在 B[1]-B[n/4]這裏面。 或者,這裏的或者是很重要的, 咱們能夠說,咱們已經成功的把問題變成了在排序完成的數組A[n/2]-A[n]和B[0]-B[n/2]裏面找到合併之後的中位數, 顯然遞歸是個不錯的選擇了。

相似的, 若是B[n/2]<A[n/2]呢?顯然就是在A[0]-A[n/2]和B[n/2]-B[n]裏面尋找了。

在繼續想, 這個遞歸何時收斂呢?固然一個case就是相等的值出現, 若是不出現等到這個n==1的時候也就結束了。

照着這樣的思路, 咱們比較容易寫出以下的代碼, 固然邊界的值須要本身思量一下, 前面的想法只是想法而已。

立刻有人說那不定長的怎麼辦呢?同樣的,咱們仍是來畫個圖看看:(個人畫圖水平確定提升了)

int find_median_equal_length( int a[], int b[], int length)
{
	if (length == 1) 
		return a[0];	
		
	int i = length/2;
	if (a[i] == b[i])
		return a[i];
	else if (a[i]<b[i])
		return find_median_equal_length( &a[i], &b[0], length-i );
	else 
		return find_median_equal_length( &a[0], &b[i], length-i );
}
相關文章
相關標籤/搜索