[LeetCode] 4. 兩個排序數組的中位數
該題的難度分級是Hard,那麽難在哪裏呢?我們先來看題目。
給定兩個大小為 m 和 n 的有序數組 nums1 和 nums2 。
請找出這兩個有序數組的中位數。要求算法的時間復雜度為 O(log (m+n)) 。
示例 1:
nums1 = [1, 3] nums2 = [2] 中位數是 2.0
示例 2:
nums1 = [1, 2] nums2 = [3, 4] 中位數是 (2 + 3)/2 = 2.5
當你看到要求的時間復雜度為O(log (m+n)),你想到了什麽?沒錯,二分法。
從示例上觀察來看,如果兩個數組的長度和是奇數,中位數就是大小在正中間的那個數;如果兩個數組的長度和是偶數,那麽中位數就是大小在正中間的兩個數的和的平均數。
似乎我們需要分類討論,並且考慮一些corner case才能完整的把這題解出來,那麽有沒有更加巧妙的方法呢?沒錯,我都這麽說了,肯定是有的。
下面的解法是官網社區討論組裏面一個大神的解法,我來搬運過來翻譯一下,幫助我自己理解,也幫助有緣人解惑。
原post:https://leetcode.com/problems/median-of-two-sorted-arrays/discuss/2471/Very-concise-O(log(min(MN)))-iterative-solution-with-detailed-explanation
話不多說,讓我們開始看大神如何裝逼。
這個解法需要一種思維轉換,我們能不能把長度和是奇數和長度和是偶數這兩種情況看作是同一種情況呢?Yes!
首先,我們來看一下中位數的概念:
如果我們將一個有序數組切一刀,分成2個等長的部分,那麽中位數就是前半部分的最大值和後半部分的最小值,這兩者和的平均數。
沒明白?沒關系,舉個例子:對於[2 3 5 7] 這個數組, 我們在3和5中間切一刀(對沒錯,就是這個紅色的斜杠)
[2 3 / 5 7]
那麽中位數median = (3+5)/2。後面我們都將用 ‘/‘ 代表一個切分,(數字 / 數字) 代表把在一個數字中間切了一刀(嗯,結果就是一個我變成兩個我)
對於數組 [2 3 4 5 6], 我們可以像這樣切分,把4一切二:
[2 3 (4/4) 5 7]
由於我們把4切成了2份,前半部分數組和後半部分數組都包含了4。那麽這時候中位數是多少?median = (4 + 4) / 2 = 4; 答案依舊正確。
好,現在為了方便,我們用L來表示切的那一刀左邊第一個元素,R來表示切的那一刀右邊的第一個元素。
也就是說,對於被切了一刀的數組 [2 3 / 5 7], 這個情況下,L = 3, R = 5。
於是我們可以觀察到,對於一個長度為N的有序數組,L和R的數組下標有如下規律:
N(數組長度) L / R對應的數組下標
1 0 / 0
2 0 / 1
3 1 / 1
4 1 / 2
5 2 / 2
6 2 / 3
7 3 / 3
8 3 / 4
不難發現,INDEXL = (N-1)/2 而 INDEXR = N/2. 那麽根據前面的分析,對於任意一個數組A, 其中位數median = (L + R)/2 = (A[(N-1)/2] + A[N/2])/2 。
往下看之前,請先確保以上內容你已經完全ok了解no破布。
好,我們繼續。現在我們來討論兩個數組情況,我們需要在數組中假想一些#(井號)出來,這些#把每一個數組中的數字都包裹了起來,並且,無論是“#”還是數字我們都稱作是一個position。
[6 9 13 18] -> [# 6 # 9 # 13 # 18 #] (N = 4)
position index 0 1 2 3 4 5 6 7 8 (N_Position = 9) 共有9個position
[6 9 11 13 18]-> [# 6 # 9 # 11 # 13 # 18 #] (N = 5)
position index 0 1 2 3 4 5 6 7 8 9 10 (N_Position = 11) 共有11個position
可以看到,無論N是多少,一定會有 2*N+1 個 ‘positions‘ . 因此,無論N是奇數還是偶數,從position的角度來看,假設第一個position下標為0,那麽要在正中間切一刀,一定是在第N個position(這裏稱為CutPosition)。
由於我們已經得出在一個數組中 index(L) = (N-1)/2, index(R) = N/2 , 所以我們進一步得出 index(L) = (CutPosition-1)/2, index(R) = (CutPosition)/2.
同樣的,往下看之前,請先確保以上內容你已經完全ok了解no破布。
好,我們繼續,對於2個數組的情況,
A1: [# 1 # 2 # 3 # 4 # 5 #] (N1 = 5, N1_positions = 11)
A2: [# 1 # 1 # 1 # 1 #] (N2 = 4, N2_positions = 9)
與單個數組的問題類似,我們要找到一個切法(就是在兩個數組上各切一刀),可以將兩個數組分別分成兩個部分,使得
“兩個左半部分中包含的任意數字” <= “兩個右半部分中包含的任意數字”
我們可以觀察得出,對於這兩個數組(長度分別為N1和N2):
-
總共有 (2N1+1)+(2N2+1) = 2N1 + 2N2 + 2 個position. 因此切完之後,左半邊和右半邊應該各包含 N1 + N2 個 positions,每一刀的左右各有一個position
-
在必須滿足第1條的原則下,假設我們在數組A2的position下標 C2 的地方切一刀, 那麽A1的切分position下標就必須是 C1 = N1 + N2 - C2. 來舉個例子, 如果 C2 = 2, 那麽就一定有 C1 = 4 + 5 - C2 = 7.
A1 [# 1 # 2 # 3 # (4/4) # 5 #] N1 = 5 (切在數字4上,所以用(4/4)表示)
position index 0 1 2 3 4 5 6 7 8 9 10 N_Positions = 11
A2 [# 1 / 1 # 1 # 1 #] N2 = 4 (由於切在#處,就用/代替#)
position index 0 1 2 3 4 5 6 7 8
N_Positions = 9
-
切完之後我們會得到2個L和2個R,分別是:
L1 = A1[(C1-1)/2]; R1 = A1[C1/2]; L2 = A2[(C2-1)/2]; R2 = A2[C2/2];
代入到上面的例子裏,L1和L2就是,
L1 = A1[(7-1)/2] = A1[3] = 4; R1 = A1[7/2] = A1[3] = 4;
L2 = A2[(2-1)/2] = A2[0] = 1; R2 = A1[2/2] = A1[1] = 1;
那麽現在問題來了,我們怎麽知道當前的切法是我們想要的切法?回顧一下我們想要滿足的條件是:所有左半部分的數都比右半部分要小。因為兩個數組是有序遞增的,所以L1, L2 是左半部分最大的兩個數,而R1, R2是右半部分最小的兩個數, 所以其實只需要滿足下列條件:
L1 <= R1 && L1 <= R2 && L2 <= R1 && L2 <= R2
由於數組是遞增排好序的,L1 <= R1 and L2 <= R2 是一定可以滿足的,我們只需要確保:
L1 <= R2 && L2 <= R1.
現在我們終於可以使用簡單的二分搜索來找到合適的切法了:(這裏邏輯很關鍵,暫時保留原文)
If (L1 > R2){
// it means there are too many large numbers on the left half of A1, then we must move C1 to the left (i.e. move C2 to the right);
// 意味著A1的左半邊的大數字太多了,需要將切分position C1左移
}
If (L2 > R1){
// then there are too many large numbers on the left half of A2, and we must move C2 to the left.
// 意味著A2的左半邊的大數字太多了,需要將切分position C2左移
}
Otherwise, this cut is the right one.
否則就滿足了條件,是正確的切分,通過計算 (max(L1,L2) + min(R1,R2)) / 2 得出中位數
After we find the cut, the medium can be computed as (max(L1, L2) + min(R1, R2)) / 2;
兩個要註意的地方:
A. 由於C1和C2存在互相依賴的關系(也就是,確定了C1就確定了C2,反之亦然), 我們可以先移動其中的一個,另一個隨之移動。(沒電了。。先這樣,明天繼續完成)we can just move one of them first, then calculate the other accordingly. However, it is much more practical to move C2 (the one on the shorter array) first. The reason is that on the shorter array, all positions are possible cut locations for median, but on the longer array, the positions that are too far left or right are simply impossible for a legitimate cut. For instance, [1], [2 3 4 5 6 7 8]. Clearly the cut between 2 and 3 is impossible, because the shorter array does not have that many elements to balance out the [3 4 5 6 7 8] part if you make the cut this way. Therefore, for the longer array to be used as the basis for the first cut, a range check must be performed. It would be just easier to do it on the shorter array, which requires no checks whatsoever. Also, moving only on the shorter array gives a run-time complexity of O(log(min(N1, N2))) (edited as suggested by @baselRus)
B. The only edge case is when a cut falls on the 0th(first) or the 2Nth(last) position. For instance, if C2 = 2N2, then R2 = A2[2*N2/2] = A2[N2], which exceeds the boundary of the array. To solve this problem, we can imagine that both A1 and A2 actually have two extra elements, INT_MAX at A[-1] and INT_MAX at A[N]. These additions don‘t change the result, but make the implementation easier: If any L falls out of the left boundary of the array, then L = INT_MIN, and if any R falls out of the right boundary, then R = INT_MAX.
ok,到此結束,源代碼就不貼了,看明白之後用自己熟悉的語言寫一下試試唄,實在不行再去原文鏈接看看,大神是怎麽寫的。
Good Luck!
[LeetCode] 4. 兩個排序數組的中位數