這是我作的第二個leetcode題目,一開始覺得和第一個同樣很簡單,可是作的過程當中才發現這個題目很是難,給人一種「剛上戰場就踩上地雷掛掉了」的感受。後來搜了一下leetcode的難度分佈表(leetcode難度及面試頻率)才發現,該問題是難度爲5的問題,真是小看了它!網上搜了不少答案,可是鮮見簡明正確的解答,惟有一種尋找第k小值的方法很是好,在此整理一下。面試
首先對leetcode的編譯運行吐槽一下:貌似沒有超時判斷,並且small和large的數據集相差很小。此題一開始我採用最笨的方法去實現,利用排序將兩個數組合併成一個數組,而後返回中位數:算法
該方法竟然也經過測試,可是其複雜度最壞狀況爲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小的數。此外咱們還須要考慮幾個邊界條件:翻譯
最終實現的代碼爲:
咱們能夠看出,代碼很是簡潔,並且效率也很高。在最好狀況下,每次都有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]
對稱,同樣的作法。
第一步:假設兩個有序數組(已經各自排序完成了)長度相等,試寫函數找出兩個數組合並後的中位數。 第二步:假設兩個有序數組長度不等,同樣的求出中位數 |
解析: 這個題目看起來很是簡單。第一題的話: 假設數組長度爲n, 那麼我就把數組1和數組2直接合並,而後再直接找到中間元素。對於這樣的方案,第一題和第一題就沒有什麼區別了。這樣的話時間複雜度就是O(n)。一般在這樣的狀況下,那些mentor類型的達人就會循循善誘道:「你還有更好的辦法嗎:)」 若是比線性更高效,直接能想到的就是對數了O(log(n)),這個時間複雜度在這裏可能嗎? 固然仍是可能的。來繼續看看下面的分析。
先找來了一個圖(本身畫的,簡陋了點)
咱們先來分析看看: 想到對數的效率,首先想到的就是二分查找,對於這個題目二分查找的意義在哪裏呢?
咱們找到了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 ); }