https://leetcode.windliang.cc/ 第一時間發佈
已知兩個有序數組,找到兩個數組合並後的中位數。java
簡單粗暴,先將兩個數組合並,兩個有序數組的合併也是歸併排序中的一部分。而後根據奇數,仍是偶數,返回中位數。算法
public double findMedianSortedArrays(int[] nums1, int[] nums2) { int[] nums; int m = nums1.length; int n = nums2.length; nums = new int[m + n]; if (m == 0) { if (n % 2 == 0) { return (nums2[n / 2 - 1] + nums2[n / 2]) / 2.0; } else { return nums2[n / 2]; } } if (n == 0) { if (m % 2 == 0) { return (nums1[m / 2 - 1] + nums1[m / 2]) / 2.0; } else { return nums1[m / 2]; } } int count = 0; int i = 0, j = 0; while (count != (m + n)) { if (i == m) { while (j != n) { nums[count++] = nums2[j++]; } break; } if (j == n) { while (i != m) { nums[count++] = nums1[i++]; } break; } if (nums1[i] < nums2[j]) { nums[count++] = nums1[i++]; } else { nums[count++] = nums2[j++]; } } if (count % 2 == 0) { return (nums[count / 2 - 1] + nums[count / 2]) / 2.0; } else { return nums[count / 2]; } }
時間複雜度:遍歷所有數組,O(m + n)數組
空間複雜度:開闢了一個數組,保存合併後的兩個數組,O(m + n)spa
其實,咱們不須要將兩個數組真的合併,咱們只須要找到中位數在哪裏就能夠了。.net
開始的思路是寫一個循環,而後裏邊判斷是否到了中位數的位置,到了就返回結果,但這裏對偶數和奇數的分類會很麻煩。當其中一個數組遍歷完後,出了 for 循環對邊界的判斷也會分幾種狀況。整體來講,雖然複雜度不影響,但代碼會看起來很亂。而後在 這裏 找到了另外一種思路。code
首先是怎麼將奇數和偶數的狀況合併一下。blog
用 len 表示合併後數組的長度,若是是奇數,咱們須要知道第 (len + 1)/ 2 個數就能夠了,若是遍歷的話須要遍歷 int ( len / 2 ) + 1 次。若是是偶數,咱們須要知道第 len / 2 和 len / 2 + 1 個數,也是須要遍歷 len / 2 + 1 次。因此遍歷的話,奇數和偶數都是 len / 2 + 1 次。排序
返回中位數的話,奇數須要最後一次遍歷的結果就能夠了,偶數須要最後一次和上一次遍歷的結果。因此咱們用兩個變量 left 和 right ,right 保存當前循環的結果,在每次循環前將 right 的值賦給 left 。這樣在最後一次循環的時候,left 將獲得 right 的值,也就是上一次循環的結果,接下來 right 更新爲最後一次的結果。遞歸
循環中該怎麼寫,何時 A 數組後移,何時 B 數組後移。用 aStart 和 bStart 分別表示當前指向 A 數組和 B 數組的位置。若是 aStart 尚未到最後而且此時 A 位置的數字小於 B 位置的數組,那麼就能夠後移了。也就是aStart < m && A[aStart] < B[bStart]。leetcode
但若是 B 數組此刻已經沒有數字了,繼續取數字B [ bStart ],則會越界,因此判斷下 bStart 是否大於數組長度了,這樣 || 後邊的就不會執行了,也就不會致使錯誤了,因此增長爲 aStart < m && ( bStart >= n || A [ aStart ] < B [ bStart ] ) 。
public double findMedianSortedArrays(int[] A, int[] B) { int m = A.length; int n = B.length; int len = m + n; int left = -1, right = -1; int aStart = 0, bStart = 0; for (int i = 0; i <= len / 2; i++) { left = right; if (aStart < m && (bStart >= n || A[aStart] < B[bStart])) { right = A[aStart++]; } else { right = B[bStart++]; } } if ((len & 1) == 0) return (left + right) / 2.0; else return right; }
時間複雜度:遍歷 len/2 + 1 次,len = m + n ,因此時間複雜度依舊是 O(m + n)。
空間複雜度:咱們申請了常數個變量,也就是 m,n,len,left,right,aStart,bStart 以及 i 。
總共 8 個變量,因此空間複雜度是 O(1)。
上邊的兩種思路,時間複雜度都達不到題目的要求 O ( log ( m + n ) )。看到 log ,很明顯,咱們只有用到二分的方法才能達到。咱們不妨用另外一種思路,題目是求中位數,其實就是求第 k 小數的一種特殊狀況,而求第 k 小數有一種算法。
解法二中,咱們一次遍歷就至關於去掉不多是中位數的一個值,也就是一個一個排除。因爲數列是有序的,其實咱們徹底能夠一半兒一半兒的排除。假設咱們要找第 k 小數,咱們能夠每次循環排除掉 k / 2 個數。看下邊一個例子。
假設咱們要找第 7 小的數字。
咱們比較兩個數組的第 k / 2 個數字,若是 k 是奇數,向下取整。也就是比較第 3 個數字,上邊數組中的 8 和 下邊數組中的 3 ,若是哪一個小,就代表該數組的前 k / 2 個數字都不是第 k 小數字,因此能夠排除。也就是 1,2,3 這三個數字不多是第 7 小的數字,咱們能夠把它排除掉。將 1389 和 45678910 兩個數組做爲新的數組進行比較。
更通常的狀況 A [ 1 ],A [ 2 ],A [ 3 ],A [ k / 2] ... ,B[ 1 ],B [ 2 ],B [ 3 ],B[ k / 2] ... ,若是 A [ k / 2 ] < B [ k / 2 ] ,那麼 A [ 1 ],A [ 2 ],A [ 3 ],A [ k / 2] 都不多是第 k 小的數字。
A 數組中比 A [ k / 2 ] 小的數有 k / 2 - 1 個,B 數組中,B [ k / 2 ] 比 A [ k / 2 ] 小,假設 B [ k / 2 ] 前邊的數字都比 A [ k / 2 ] 小,也只有 k / 2 - 1 個,因此比 A [ k / 2 ] 小的數字最多有 k / 2 - 1 + k / 2 - 1 = k - 2 個,因此 A [ k / 2 ] 最可能是第 k - 1 小的數。而比 A [ k / 2 ] 小的數更不多是第 k 小的數了,因此能夠把它們排除。
橙色的部分表示已經去掉的數字。
因爲咱們已經排除掉了 3 個數字,就是這 3 個數字必定在最前邊,因此在兩個新數組中,咱們只須要找第 7 - 3 = 4 小的數字就能夠了,也就是 k = 4 。此時兩個數組,比較第 2 個數字,3 < 5,因此咱們能夠把小的那個數組中的 1 ,3 排除掉了。
咱們又排除掉 2 個數字,因此如今找第 4 - 2 = 2 小的數字就能夠了。此時比較兩個數組中的第 k / 2 = 1 個數,4 = 4 ,怎麼辦呢?因爲兩個數相等,因此咱們不管去掉哪一個數組中的都行,由於去掉 1 個總會保留 1 個的,因此沒有影響。爲了統一,咱們就假設 4 > 4 吧,因此此時將下邊的 4 去掉。
因爲又去掉 1 個數字,此時咱們要找第 1 小的數字,因此只需判斷兩個數組中第一個數字哪一個小就能夠了,也就是 4 。
因此第 7 小的數字是 4 。
咱們每次都是取 k / 2 的數進行比較,有時候可能會遇到數組長度小於 k / 2 的時候。
此時 k / 2 等於 3 ,而上邊的數組長度是 2 ,咱們此時將箭頭指向它的末尾就能夠了。這樣的話,因爲 2 < 3 ,因此就會致使上邊的數組 1,2 都被排除。形成下邊的狀況。
因爲 2 個元素被排除,因此此時 k = 5 ,又因爲上邊的數組已經空了,咱們只須要返回下邊的數組的第 5 個數字就能夠了。
從上邊能夠看到,不管是找第奇數個仍是第偶數個數字,對咱們的算法並無影響,並且在算法進行中,k 的值都有可能從奇數變爲偶數,最終都會變爲 1 或者因爲一個數組空了,直接返回結果。
因此咱們採用遞歸的思路,爲了防止數組長度小於 k / 2 ,因此每次比較 min ( k / 2,len ( 數組 ) ) 對應的數字,把小的那個對應的數組的數字排除,將兩個新數組進入遞歸,而且 k 要減去排除的數字的個數。遞歸出口就是當 k = 1 或者其中一個數字長度是 0 了。
public double findMedianSortedArrays(int[] nums1, int[] nums2) { int n = nums1.length; int m = nums2.length; int left = (n + m + 1) / 2; int right = (n + m + 2) / 2; //將偶數和奇數的狀況合併,若是是奇數,會求兩次一樣的 k 。 return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5; } private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) { int len1 = end1 - start1 + 1; int len2 = end2 - start2 + 1; //讓 len1 的長度小於 len2,這樣就能保證若是有數組空了,必定是 len1 if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k); if (len1 == 0) return nums2[start2 + k - 1]; if (k == 1) return Math.min(nums1[start1], nums2[start2]); int i = start1 + Math.min(len1, k / 2) - 1; int j = start2 + Math.min(len2, k / 2) - 1; if (nums1[i] > nums2[j]) { return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1)); } else { return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1)); } }
時間複雜度:每進行一次循環,咱們就減小 k / 2 個元素,因此時間複雜度是 O(log(k)),而 k = (m + n)/ 2 ,因此最終的複雜也就是 O(log(m + n))。
空間複雜度:雖然咱們用到了遞歸,可是能夠看到這個遞歸屬於尾遞歸,因此編譯器不須要不停地堆棧,因此空間複雜度爲 O(1)。
咱們首先理一下中位數的定義是什麼
中位數(又稱中值,英語:Median), 統計學中的專有名詞,表明一個樣本、種羣或 機率分佈中的一個數值,其可將數值集合劃分爲相等的上下兩部分。
因此咱們只須要將數組進行切。
一個長度爲 m 的數組,有 0 到 m 總共 m + 1 個位置能夠切。
咱們把數組 A 和數組 B 分別在 i 和 j 進行切割。
將 i 的左邊和 j 的左邊組合成「左半部分」,將 i 的右邊和 j 的右邊組合成「右半部分」。
當 A 數組和 B 數組的總長度是偶數時,若是咱們可以保證
i + j = m - i + n - j , 也就是 j = ( m + n ) / 2 - i
那麼,中位數就能夠表示以下
(左半部分最大值 + 右半部分最大值 )/ 2 。
(max ( A [ i - 1 ] , B [ j - 1 ])+ min ( A [ i ] , B [ j ])) / 2
當 A 數組和 B 數組的總長度是奇數時,若是咱們可以保證
i + j = m - i + n - j + 1也就是 j = ( m + n + 1) / 2 - i
那麼,中位數就是
左半部分最大值,也就是左半部比右半部分多出的那一個數。
max ( A [ i - 1 ] , B [ j - 1 ])
上邊的第一個條件咱們其實能夠合併爲 j = ( m + n + 1) / 2 - i,由於若是 m + n 是偶數,因爲咱們取的是 int 值,因此加 1 也不會影響結果。固然,因爲 0 <= i <= m ,爲了保證 0 <= j <= n ,咱們必須保證 m <= n 。
$$m\leq n,i<m,j=(m+n+1)/2-i\geq(m+m+1)/2-i>(m+m+1)/2-m=0$$
$$m\leq n,i>0,j=(m+n+1)/2-i\leq (n+n+1)/2-i<(n+n+1)/2=n$$
最後一步因爲是 int 間的運算,因此 1 / 2 = 0。
而對於第二個條件,奇數和偶數的狀況是同樣的,咱們進一步分析。爲了保證 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ])),由於 A 數組和 B 數組是有序的,因此 A [ i - 1 ] <= A [ i ],B [ i - 1 ] <= B [ i ] 這是自然的,因此咱們只須要保證 B [ j - 1 ] < = A [ i ] 和 A [ i - 1 ] <= B [ j ] 因此咱們分兩種狀況討論:
此時很明顯,咱們須要增長 i ,爲了數量的平衡還要減小 j ,幸運的是 j = ( m + n + 1) / 2 - i,i 增大,j 天然會減小。
此時和上邊的狀況相反,咱們要減小 i ,增大 j 。
上邊兩種狀況,咱們把邊界都排除了,須要單獨討論。
此時左半部分當 j = 0 時,最大的值就是 A [ i - 1 ] ;當 i = 0 時 最大的值就是 B [ j - 1] 。右半部分最小值和以前同樣。
此時左半部分最大值和以前同樣。右半部分當 j = n 時,最小值就是 A [ i ] ;當 i = m 時,最小值就是B [ j ] 。
全部的思路都理清了,最後一個問題,增長 i 的方式。固然用二分了。初始化 i 爲中間的值,而後減半找中間的,減半找中間的,減半找中間的直到答案。
class Solution { public double findMedianSortedArrays(int[] A, int[] B) { int m = A.length; int n = B.length; if (m > n) { return findMedianSortedArrays(B,A); // 保證 m <= n } int iMin = 0, iMax = m; while (iMin <= iMax) { int i = (iMin + iMax) / 2; int j = (m + n + 1) / 2 - i; if (j != 0 && i != m && B[j-1] > A[i]){ // i 須要增大 iMin = i + 1; } else if (i != 0 && j != n && A[i-1] > B[j]) { // i 須要減少 iMax = i - 1; } else { // 達到要求,而且將邊界條件列出來單獨考慮 int maxLeft = 0; if (i == 0) { maxLeft = B[j-1]; } else if (j == 0) { maxLeft = A[i-1]; } else { maxLeft = Math.max(A[i-1], B[j-1]); } if ( (m + n) % 2 == 1 ) { return maxLeft; } // 奇數的話不須要考慮右半部分 int minRight = 0; if (i == m) { minRight = B[j]; } else if (j == n) { minRight = A[i]; } else { minRight = Math.min(B[j], A[i]); } return (maxLeft + minRight) / 2.0; //若是是偶數的話返回結果 } } return 0.0; } }
時間複雜度:咱們對較短的數組進行了二分查找,因此時間複雜度是 O(log(min(m,n)))。
空間複雜度:只有一些固定的變量,和數組長度無關,因此空間複雜度是 O ( 1 ) 。
解法二中體會到了對狀況的轉換,有時候即便有了思路,代碼也不必定寫的優雅,須要多鍛鍊才能夠。解法三和解法四充分發揮了二分查找的優點,將時間複雜度降爲 log 級別。