【題目】Median of Two Sorted Arrays
【描述】There are two sorted arrays nums1 and nums2 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)).
【中文描述】兩個已排序數組(默認為從小到大排序)nums1 和 nums2, 大小分別為m,n。 要求找到這兩個數組合並后的中位數。總的運行時間復雜度不能超過O(log(m+n))。
——————————————————————————————————————————————————————————————————————————————————————
【初始思路】
實際上一開始並沒有認真看題目,心想這個很簡單嘛,先merge,然后找中位數,分分鍾kill掉。結果做了一半的時候才看見時間要求。顯然,merge數組的最優時間復雜度是O(K) (K為M和N中小者+兩者只差)。 遠遠大於題目要求O(log(m+n))。
【靈光一現!】
一看到log的復雜度要求,應該立馬想到二分法,這是一個CSer基本的素養。
【中位數 / Median】
一個奇數長度數組中最中間的那個數。如果數組長度為偶數,那就是最中間2個數的平均數。
【重整思路】
如何二分?既然是找中位數,必然與數組中各數的位置(index)有關,我們可以借助二分法不斷縮小index的范圍,最后在極小范圍內找到中位數。
想要更加通俗易懂的解釋?以下用奇數長度數組作解釋,偶數可類比:
(1) M+N個元素的中位數,位於兩數組merge后的(M+N)/2+1位置。我們把這個位置記為K。又由於merge后數組中找位置K的元素,等同於找第K小元素:find the Kth minimum element!
(2) 我們對兩個數組各自二分,取一半,棄一半,在剩下的元素中繼續找?但是注意,這里的一半不能取數組長度的一半。 因為我們現在要找Kth,所以二分的實際尺度應該取決於K的大小。在去掉K/2后,由於前K個元素中的K/2已經被舍棄,所以在剩余數組中查找:Jth = K - K/2。(倘若取了數組的一半,那么棄掉的元素個數和K無關,那就無法確定 J 的位置。)
(3) 同時,應當注意,由於已經去掉了某個數組的前k/2個元素,那么這個數組參與查找J的范圍起始點應當從當前start點往前移動k/2個位置,所以,start = start + k/2。
舉個籠統的例子,要求找第8小元素,我們先找第4小,確定了前面4小后,把前4小舍棄,然后從第5小位置起,找第8小其實變成了找當前的第4小(5->END)。繼續二分,找第2小,然后把前2小舍棄,實際變成從第3小位置找第2小(7->END)。最后找第1小,得到總的第7小,那么第8小也就變成了從8->END子數組的第1小,可輕松得到。
(4) 每次在兩個數組中找前K/2元素,有如下一些情況需要考慮:
(4.1) 如果在數組A中找K/2的時候,K/2 >= A.length,此時可以直接丟棄B中前K/2,為什么?
反證:當K/2 >= A.length時,而我們要找的K就在B的前K/2元素中。我們假設 k 所在的數組下標記為p,那么B中含有的屬於merge后數組前K個元素的元素有p+1個(請自行考慮為何)。顯然,A中必然含有另外 k-(p+1)個元素。由此,得到如下不等式:
· p <= k/2 - 1 (kth 實際所處位置為p,在B的前k/2個元素里。-1是因為現在算的是數組下標,從0開始)
· 所以,p + 1 <= k/2;
· 所以,k - (p+1) >= k - k/2。
顯然,A.length >= k - (p+1) >= k/2 ,這與上面的假設,k/2 >= A.length是矛盾的。
得證,且反之亦然。
(4.2) 如果4.1情況未出現,也即我們找到了在A/B中各自的前k/2個元素,我們記為其最后一個元素為A[mid]和B[mid],顯然mid=k/2 - 1(因為mid為位置,而k/2是個數,所以需-1)。那么剩下的問題就是確定下一步該如何舍棄元素縮小下一步檢索范圍。有如下結論:
如果A[mid]>=B[mid],那么B的前k/2個元素可以直接舍棄。如果A[mid]<B[mid],那么A的前k/2個元素可以直接舍棄。
反證:當A[mid] >= B[mid],並且第k元素在B的前 k/2 個元素里,假設其值為maxK, 位置在p, 說明B中包含前k個元素中的 p + 1個元素,且p <= k/2 - 1。那么A中必然包含前k個元素中的 k - ( p + 1)個元素。有如下不等式:
· p < = k/2 -1 可得 p + 1 <= k/2
· 所以, k - (P + 1) >= k/2
· 也即,在A中,實際屬於全部數組中前k個元素的元素個數為k - (P + 1),假設某元素數組下標為A[k-(p+1) - 1],那么它必然大於A[mid]。而顯然,由於A[k - (p+1) -1] < maxk,所以,得到 A[mid] < maxk。 而由於maxk在B的前k/2個元素里,所以B[mid] > maxk。 得到:A[mid] < B[mid],與題設矛盾。得證!
也可以看下面圖幫助理解這個證明過程:
(4.3) 此外,基准條件需要考慮。由於每一步查找第J小得時候,J = k - k/2,那么J=1的時候,程序如何處理。顯然,根據定義來看,J=1說明只需要在A/B兩個數組中找第1小即可。那實際就是直接返回當前位置元素即可。但是,A/B各存在一個起始當前元素,如何取舍?可以這么考慮,merge后的數組是從小到大的,從上面分析來看,我們實際上要找的是第k小。也就是說,如果在前面各步,我們已經丟棄了全部 (k-1) 個元素,剩下要選的元素就是第k小。很顯然,應該選兩者中小的那個,大的那個元素在merge后實際是第k+1小。所以:
return Math.min(A[starta], B[startb]);
【Show me the Code!!!】
有了上面的分析,我們可以實現主程序:

1 public static double findMedianSortedArrays(int[] nums1, int[] nums2) { 2 int lengthA = nums1.length; 3 int lengthB = nums2.length; 4 int len = lengthA + lengthB; 5 if(len % 2 == 0) { 6 return (findKth(nums1, nums2, 0, 0, len/2) + findKth(nums1, nums2, 0, 0, len/2+1))/ 2.0; 7 } else { 8 return findKth(nums1, nums2, 0, 0, len/2 + 1); 9 } 10 }
主程序實際調用了方法 findKth(int[] nums1, int[]nums2, int starta, int startb, int k),它主要負責在兩個數組中找第k個元素,並且棄掉無用元素,並前移下一次查詢的起始位置。具體代碼如下:

1 public static double findKth(int[] nums1, int[]nums2, int starta, int startb, int k){ 2 int len1 = nums1.length; 3 int len2 = nums2.length; 4 5 if(starta >= len1){ 6 //If the start point is beyond the length of this array, then this array will be eliminated at once. 7 //Because, starta moves when we 'cut' elements off array nums1. When the starta is larger than the len1. 8 //That means it is not necessary need nums1 any more. 9 //Same with the nums2. 10 return nums2[startb + k - 1];//index starts from 0 11 } 12 if(startb >= len2){ 13 return nums1[starta + k - 1];//index starts from 0 14 } 15 if(k == 1){ 16 //This block means: 17 //When we need to find the 1th min element in two Array 18 //We just return the first element which is the element pointed by the 'start index' in each Array and compare them. 19 //The reason for we picking the smaller one is when the two arrays merged, the smaller one will stand in front of the bigger one; 20 //The smaller one will be the Kth, and the bigger one actually become the (k+1)th element in the merge Array. 21 return Math.min(nums1[starta], nums2[startb]); 22 23 } 24 25 int mid = k/2 - 1;//index starts from 0, so the mid of K is K/2 - 1 26 int keypoint1 = starta + mid >= len1? Integer.MAX_VALUE : nums1[starta + mid];//keypoint1 is the k/2 one of nums1 27 int keypoint2 = startb + mid >= len2? Integer.MAX_VALUE : nums2[startb + mid];//keypoint2 is the k/2 one of nums2 28 29 if(keypoint1 > keypoint2){ 30 //When we cut off some elements from one array, the 'start' index moves forward by [start + k/2] 31 //k-k2 means that we have eliminated K/2 elements, so k-k/2 elements left 32 return findKth(nums1, nums2, starta, startb + k/2, k - k/2); 33 } else { 34 return findKth(nums1, nums2, starta + k/2, startb, k - k/2); 35 } 36 }
【O分析】
空間復雜度很顯然是O(1),着重來看看時間復雜度:
由於 中位數 實際是兩個數組merge后的最中間那個數,所以k = ( m+n )/2
我們只需要考慮findKth方法總共跑了多少次,就可以知道O是多少了。
由於每一次跑findKth,我們都實際上把k縮小了一半。那么總執行次數為N,那么總處理元素個數為:2N = k。又由於 k = (m+n)/2。
顯然,N = log2 ((m+n)/2) = O(log2(m+n))。 滿足題目要求。