題目:題目:
給定兩個大小為 m 和 n 的正序(從小到大)陣列 nums1 和 nums2。
請你找出這兩個正序陣列的中位數,並且要求演算法的時間複雜度為 O(log(m + n))。
你可以假設 nums1 和 nums2 不會同時為空。
示例 1:
nums1 = [1, 3]
nums2 = [2]
則中位數是 2.0
示例 2:
nums1 = [1, 2]
nums2 = [3, 4]
則中位數是 (2 + 3)/2 = 2.5
解析:
1,看到這個題的瞬間,先不考慮題目中的時間和空間的複雜度,我用PHP的 array_merge()一下這兩個陣列,然後在sort() 一下,在 count() 一下,計算出中位數的下標,就得到了。當然,這肯定不符合題意的。又回到題目,我要求時間和空間複查度都要要小於log級別,看到這三個單詞,我想到了 二分天下 中,這就是標準的時間空間的級別,那麼,現在的問題是怎麼二分?
首先:1,兩個有序陣列找中位數,必然中位數在其中一個有序陣列中,並且在這個陣列中存在這樣的關係:陣列中位數的左邊的所有值肯定都小於右邊的所有值。
其次:2,既然這個中位數是這兩個有序陣列的中位數,那如果合併兩個陣列後,比中位數小的或等於元素個數 加 比中位數大的元素個數必然等於兩個陣列的總數,反過來說,不合並也是存在這個等式的
再其次:3,根據第一個分析,不管兩個有序陣列怎麼樣,比中位數小的在左邊,其中一個陣列的比中位數小的所有元素中的最大元素 必定比另一個有序陣列比中位數大的元素小,反過來也必須成立。
最後:4,我們對其中一個陣列進行二分,那必然滿足 2 的規則。
注意:計算中位數時,要判斷兩個陣列的總長度,奇數是剛好中間的元素,偶數是中間兩個元素的和處以2
## 分析那麼多,不如看程式碼:
function findMedianSortedArrays($nums1, $nums2) {
// 統計兩個陣列長度
$len1 = count($nums1);
$len2 = count($nums2);
// 既然二分任何一個有序陣列都可以,那麼就選 個數小的那個進行二分, 如果傳參的第一個陣列大於第二個,遞迴一下
if ($len1 > $len2) {
$this->findMedianSortedArrays($nums2, $nums1);
}
// 再判斷 $nums1 陣列長度是否為 0 ,如果是零,則就是查詢 $nums2 的中位數,那麼就簡單了三
if ($len1 == 0) {
// 一個有序陣列 不管是奇數還是偶數,這個寫法都能準確找到中位數,這個就不解釋了,可以自己試試
return ($nums2[floor($len2 / 2)] + $nums2[floor(($len2 - 1) / 2)]) / 2;
}
// 開始分割查詢:
// 統計一下兩個陣列的總長度
$len = $len1 + $len2;
// 對 $nums1 進行切得 下標
$slice1 = 0;
// 根據解析 中第二條能推匯出 $nums2 得切點,但是還是要預存一下
$slice2 = 0;
// $nums1 的切割區間 左 和 右
$sliceL = 0;
$sliceR = $len1;
// 接下來就是在迴圈找找出 中位數了
// 只要切點 不大於 長度,那麼就是成立的
while ($slice1 <= $len1) {
// 剛進來時 先對 小的陣列進行二分 ($sliceL + $sliceR)/2 和 ($sliceR - $sliceL) / 2 + $sliceL 在數學上是一樣的,但是第二種可以防止記憶體溢位
$slice1 = floor($sliceR - $sliceL) / 2 + $sliceL;
// 得到小陣列的切點,那麼大陣列的二分切點也知道了,因為滿足上面分析的第二條
// 簡單證明一下,第一個切點等式:2*$slice1 = $sliceR - $sliceL
// $len = $len1 + $len2
// 根據二分 $len2 = 2 * $slice2
// 替換一下上面的等式 $len = 2 * $slice1 + 2 * $slice2
// 得到下面的等式
$slice2 = floor($len/2) - $slice1;
// 計算 $nums1 被分過後小的半段的最大元素,比如陣列 [1, 3, 4, 6, 8, 13] 中分後為 [[1, 3, 4] 和 [6, 8, 13]
// 根據不斷的移動切點 有可能切點在 開始位置 ,當為開始位置時,要找一個肯定小的標記,這裡用 PHP 的系統常量,否則就是切點的前一個
$l1 = ($slice1 == 0) ? PHP_INT_MIN :$nums1[$slice1 - 1];
$l2 = ($slice2 == 0) ? PHP_INT_MIN :$nums2[$slice2 - 1];
// 同理,計算陣列大的半段的最小元素 也存在一個當切點是陣列長度時
$r1 = ($slice1 == $len1) ? PHP_INT_MAX : $nums1[$slice1];
$r2 = ($slice2 == $len2) ? PHP_INT_MAX : $nums2[$slice2];
// 根據解析 第三條 其中一個陣列的比中位數小的所有元素中的最大元素 必定比另一個有序陣列比中位數大的元素小
if ($l1 > $r2) {
// 如果大了 則不是中位數 要向左移動切點,找前一個值,本體的二分就體現在這裡,大了,則後面的全部值都大了,直接不進入計算了
$cutR = $slice1 - 1;
// 同理 $nums2 切割後 所有小的元素中的最大值也 必須小於 $nums1 切割後 所有大元素的最小值。
} elseif ($l2 > $r1) {
// 直接拋棄切點的前半段,在二分
$cutL = $slice1 + 1;
// 當上面的 兩個都滿足,則找到了本題的中位數了,
} else {
// 偶數時,中位數時 小的所有元素的最大值和大的所有元素的最小值相加除2
if ($len % 2 ==0) {
$l1 = $l1 > $l2 ? $l1 : $l2;
$r1 = $r1 < $r2 ? $r1 : $r2;
return ($l1 + $r1) / 2;
}
// 奇數時,所有大元素的最小值
return min($r1, $r2);
}
}
本題確實有難,需要時間推理和證明…
本作品採用《CC 協議》,轉載必須註明作者和本文連結