用 PHP 在 力扣 刷演算法 [尋找兩個正序陣列的中位數]{有空就更}

666666發表於2020-05-19

題目:題目:

給定兩個大小為 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 協議》,轉載必須註明作者和本文連結

join_jiang

相關文章