十分好用的二分查詢模板 手撕二分還怕嗎?

Remember發表於2019-10-31

2019-10-31 星期四 開始吧

十分好用的二分查詢模板 手撕二分還怕嗎?

最近發現一個二分查詢很好用的模板,花了一點時間理解了下這個模板,然後這兩天就會一直去找二分查詢的題,利用那套模板來實現。這套模板和傳統模板的不同在於傳統的模板,可能我們會這樣寫:


function binarySerach($data, $res)
{
        $left  = 0;
        $right = count($data) - 1;
        while ($left <= $right) {
            $middle = $left + (($right - $left) >> 1);
            if ($data[$middle] == $res) return $middle;
            else if ($data[$middle] > $res) $right = $middle - 1;
            else $left = $middle + 1;
        }
        return -1;
 }

這樣寫的缺點在哪呢,有兩個點,一個是while裡面產生了三個分支,其實大部分情況下,我們應該只需要去區分一下是否需要排除掉中位數,至於結果的處理,應該留到最後具體的場景實現。第二點就是(這裡看不出來,不過我可以舉個場景),比如結果不存在,那麼返回第一個大於或者第一個小於這類的場景。這時候因為你while的條件是小於或者等於,最後如果沒有符合條件,需要格外返回別的資料以滿足題目需求,這時候你必然要根據當前的語境去判斷是返回left還是right,可能有些時候還會把自己繞暈,如果場景複雜的話。

下面我們來稍微改進一下當前模板來做下一道題目。

十分好用的二分查詢模板 手撕二分還怕嗎?

題目描述

題目讓我們找出重複的數,給定的陣列的值都在1-n之間,陣列總數是n+1,那麼必然有數字是重複的,讓我們來找出這個重複的數。

題目分析

這道題可以有更好的解法(我這裡強行把它當做二分的場景),二分的解題思路就是取n的中位數,遍歷陣列,統計不大於中位數的個數,如果不大於中位數的個數比中位數還大,說明重複的數在中位數的左邊(注意包括中位數自己,理解一下這一句),否則的話重複的數肯定出現在中位數的右邊,而且肯定不包括中位數。好了下面看程式碼再細講。

程式碼實現

    /**
     * @param Integer[] $nums
     * @return Integer
     */
    function findDuplicate($nums) {
        $left = 1;
        $right = count($nums) - 1;
        while ($left < $right) {  // <
            $middle = ($left + $right) >> 1;
            $sum = 0;
            for ($j = 0; $j < count($nums); $j++) {
                if ($nums[$j] <= $middle) {
                    $sum++;
                }
            }
            if ($sum <= $middle) {  //排除掉中位數
                $left = $middle + 1;
            } else {   //不能排除中位數
                 $right = $middle;
            }
        }
        //相返回left還是right都可以  因為必然存在left==right
        return $left;
    }

這個模板第一步就是解除歧義,所有的判斷都是left<right,這樣最終肯定存在left==right不用再去進一步根據題意取哪邊,語句中只剩下兩個分支(請忽略掉其他的邏輯來看),一個往左邊收,一個往右邊收,至於結果,如果不存在其他的題意 隨便返回left或者right,有些題目是會變的,那麼具體也在最後迴圈體外自行處理。接著中位數問題,每一次的判斷往哪邊靠,最大的爭議點在於中位數是否要排除,必然會存在滿足或者不滿足,如果一方排除了中位數,那邊另一方必然不能再排除,否則結果必錯。這裡還有一個地方很重要也值得你去思考的一個點。

     // $middle=floor(($l+$r)/2);
    // $middle=$l+floor(($r-$l)/2);
   // $middle=$l+(($r-$l)>>1); 
   // $middle = ($l + $r) >> 1;
  //上面的結果都是一樣的,只是在數值大的情況下值會溢位

[1,3,6,9,12]  奇數的時候中位數沒有爭議直接是6 (0+4)/2 位置2=6
[1,3,6,9] 如果是($l + $r) >> 1  最終取的是左中位3  也就是位置1
[1,3,6,9] 想要右中位那麼($l + $r + 1) >> 1 取6

取左中位還是右中位重要嗎?很重要!!!!稍不留神,你的程式就是死的.這裡有個竅門就是如果你選擇的是左中位數,那麼在左邊界語句縮邊的同時一定要把中位數排除,為什麼,道理很簡單

  [1,3,6,9] 
  就像現在這樣
  // $middle = ($l + $r) >> 1; 此時中位數是3
if(排除中位數的語句){
   $right = $miidle+1
}else{
   $left=$middle;
}
 //這時候對於如果走進的是else,就是災難,因為left並不收縮,
 //這時候就進入了死循

所以你可以看到上面解題,我選擇的是左中位數,我的左邊界語句可以排除左中位數,就沒問題。所以如果中位數選擇的是左中位數,並且程式的左邊界並不能排除中位數,那麼這時候就應該選擇右中位數,反之也是同樣的道理。為了不吹牛,我拿出一個選右中位數的例子。為了省事直接拿的Leetcode china上的。

十分好用的二分查詢模板 手撕二分還怕嗎?

這道題二分查詢按照上面規則的解。

  /**
     * @param Integer $x
     * @return Integer
     */
    function mySqrt($x)
   {
        $left  = 0;
        $right = $x;
        while ($left < $right) {
            $middle = ($left + $right + 1) >> 1;
            //      $middle=($left+$right)>>1;
            if ($middle * $middle > $x) {
                $right = $middle - 1;
            } else {
                $left = $middle;
            }
        }
        return $left;
    }

注意看這裡我選擇了右中位數。下面我們來解釋一下如果選擇左中位數會咋麼樣。道理解釋起來也很簡單,按照題目的意思,一個數的平方大於目標數,那麼它一定不會是目標數的平方根(此時右邊界語句一定能排除掉中位數!),反之,如果一個數的平方小於等於目標數,那麼平方根可能就是中位數(並不能把左中位數排除),左邊界語句不能排除中位數,那麼此時程式必要進入死迴圈,所以這時候我們需要取右中位數。

所以什麼時候知道取哪邊呢,也很簡單,你可以先隨便取左或者右,在收縮邊界的時候,第一個語句直接寫能排除中位數的,那麼另一個邊界一定不能排除中位數。然後再去判斷取的那邊中位數在邊界的條件下是否能收縮空間,如若不能,反向獲取中位數。當然這裡的二分場景並不是很複雜,到了更復雜的二分場景,可能還需要做更多的操作。

最後再來一道

十分好用的二分查詢模板 手撕二分還怕嗎?

這道題稍微加了一點遊戲難度,當然我可沒說用暴力破解法,也不說解題思路了,直接貼按照上面說的模板解題程式碼

  /**
     * @param Integer[][] $matrix
     * @param Integer $target
     * @return Boolean
     */
    function searchMatrix($matrix, $target)
    {
        $m = count($matrix);
        if ($m == 0) return false;
        $n = count($matrix[0]);
        if ($n == 0) return false;
        $left = 0;
        $right = $m * $n - 1;
        while ($left < $right) {
            $middle = ($left + $right) >> 1;
            $res = $matrix[$middle / $n][$middle % $n];
            if ($res < $target) {
                $left = $middle + 1;
            } else {
                $right = $middle;
            }

        }
        return $matrix[$left / $n][$left % $n] == $target ? true : false;
    }

最後附上這個二分查詢思路的地址值得一看:十分好用的二分查詢模板

結語

本人使用php開刷Leetcode之路,歡迎PR:Leetcode-php

吳親庫裡

相關文章