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