Skip to content

Commit 34a5b91

Browse files
committed
add 二分查找模板
1 parent 0da033c commit 34a5b91

File tree

2 files changed

+189
-0
lines changed

2 files changed

+189
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- [冒泡插入选择排序](https://github.com/wuqinqiang/Lettcode-php/blob/master/算法/冒泡插入选择.md)
2626
- [归并快速排序](https://github.com/wuqinqiang/Lettcode-php/blob/master/算法/归并快速排序.md)
2727
- [二分查找](https://github.com/wuqinqiang/Lettcode-php/blob/master/算法/二分查找.md)
28+
- [十分好用的二分查找模板](https://github.com/wuqinqiang/Lettcode-php/blob/master/算法/十分好用的二分查找模板.md)
2829
- [动态规划](https://github.com/wuqinqiang/Lettcode-php/blob/master/算法/动态规划.md)
2930
- [LRU](https://github.com/wuqinqiang/Lettcode-php/blob/master/算法/LRU.md)
3031
****
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
2+
3+
**最近发现一个二分查找很好用的模板,花了一点时间理解了下这个模板,然后这两天就会一直去找二分查找的题,利用那套模板来实现。这套模板和传统模板的不同在于传统的模板,可能我们会这样写:**
4+
5+
```
6+
7+
function binarySerach($data, $res)
8+
{
9+
$left = 0;
10+
$right = count($data) - 1;
11+
while ($left <= $right) {
12+
$middle = $left + (($right - $left) >> 1);
13+
if ($data[$middle] == $res) return $middle;
14+
else if ($data[$middle] > $res) $right = $middle - 1;
15+
else $left = $middle + 1;
16+
}
17+
return -1;
18+
}
19+
20+
```
21+
22+
23+
24+
**这样写的缺点在哪呢,有两个点,一个是while里面产生了三个分支,其实大部分情况下,我们应该只需要去区分一下是否需要排除掉中位数,至于结果的处理,应该留到最后具体的场景实现。第二点就是(这里看不出来,不过我可以举个场景),比如结果不存在,那么返回第一个大于或者第一个小于这类的场景。这时候因为你while的条件是小于或者等于,最后如果没有符合条件,需要格外返回别的数据以满足题目需求,这时候你必然要根据当前的语境去判断是返回left还是right,可能有些时候还会把自己绕晕,如果场景复杂的话。**
25+
26+
27+
28+
**下面我们来稍微改进一下当前模板来做下一道题目。**
29+
30+
31+
32+
33+
<a href="https://github.com/wuqinqiang/">
34+
​ <img src="https://github.com/wuqinqiang/Lettcode-php/blob/master/images/287.png">
35+
</a>
36+
37+
38+
39+
#### 题目描述
40+
41+
42+
43+
**题目让我们找出重复的数,给定的数组的值都在1-n之间,数组总数是n+1,那么必然有数字是重复的,让我们来找出这个重复的数。**
44+
45+
46+
47+
#### 题目分析
48+
49+
50+
**这道题可以有更好的解法(我这里强行把它当做二分的场景),二分的解题思路就是取n的中位数,遍历数组,统计不大于中位数的个数,如果不大于中位数的个数比中位数还大,说明重复的数在中位数的左边(注意包括中位数自己,理解一下这一句),否则的话重复的数肯定出现在中位数的右边,而且肯定不包括中位数。好了下面看代码再细讲。**
51+
52+
53+
54+
#### 代码实现
55+
56+
```
57+
/**
58+
* @param Integer[] $nums
59+
* @return Integer
60+
*/
61+
function findDuplicate($nums) {
62+
$left = 1;
63+
$right = count($nums) - 1;
64+
while ($left < $right) { // <
65+
$middle = ($left + $right) >> 1;
66+
$sum = 0;
67+
for ($j = 0; $j < count($nums); $j++) {
68+
if ($nums[$j] <= $middle) {
69+
$sum++;
70+
}
71+
}
72+
if ($sum <= $middle) { //排除掉中位数
73+
$left = $middle + 1;
74+
} else { //不能排除中位数
75+
$right = $middle;
76+
}
77+
}
78+
//相返回left还是right都可以 因为必然存在left==right
79+
return $left;
80+
}
81+
82+
```
83+
84+
85+
86+
**这个模板第一步就是解除歧义,所有的判断都是left<right,这样最终肯定存在left==right不用再去进一步根据题意取哪边,语句中只剩下两个分支(请忽略掉其他的逻辑来看),一个往左边收,一个往右边收,至于结果,如果不存在其他的题意 随便返回left或者right,有些题目是会变的,那么具体也在最后循环体外自行处理。接着中位数问题,每一次的判断往哪边靠,最大的争议点在于中位数是否要排除,必然会存在满足或者不满足,如果一方排除了中位数,那边另一方必然不能再排除,否则结果必错。这里还有一个地方很重要也值得你去思考的一个点。**
87+
88+
```
89+
// $middle=floor(($l+$r)/2);
90+
// $middle=$l+floor(($r-$l)/2);
91+
// $middle=$l+(($r-$l)>>1);
92+
// $middle = ($l + $r) >> 1;
93+
//上面的结果都是一样的,只是在数值大的情况下值会溢出
94+
95+
[1,3,6,9,12] 奇数的时候中位数没有争议直接是6 (0+4)/2 位置2=6
96+
[1,3,6,9] 如果是($l + $r) >> 1 最终取的是左中位3 也就是位置1
97+
[1,3,6,9] 想要右中位那么($l + $r + 1) >> 1 取6
98+
99+
```
100+
101+
**取左中位还是右中位重要吗?很重要!!!!稍不留神,你的程序就是死的.这里有个窍门就是如果你选择的是左中位数,那么在左边界语句缩边的同时一定要把中位数排除,为什么,道理很简单**
102+
103+
```
104+
[1,3,6,9]
105+
就像现在这样
106+
// $middle = ($l + $r) >> 1; 此时中位数是3
107+
if(排除中位数的语句){
108+
$right = $miidle+1
109+
}else{
110+
$left=$middle;
111+
}
112+
//这时候对于如果走进的是else,就是灾难,因为left并不收缩,
113+
//这时候就进入了死循
114+
115+
```
116+
117+
**所以你可以看到上面解题,我选择的是左中位数,我的左边界语句可以排除左中位数,就没问题。所以如果中位数选择的是左中位数,并且程序的左边界并不能排除中位数,那么这时候就应该选择右中位数,反之也是同样的道理。为了不吹牛,我拿出一个选右中位数的例子。为了省事直接拿的Leetcode china上的。**
118+
119+
120+
<a href="https://github.com/wuqinqiang/">
121+
​ <img src="https://github.com/wuqinqiang/Lettcode-php/blob/master/images/69.png">
122+
</a>
123+
124+
**这道题二分查找按照上面规则的解。**
125+
126+
```
127+
/**
128+
* @param Integer $x
129+
* @return Integer
130+
*/
131+
function mySqrt($x)
132+
{
133+
$left = 0;
134+
$right = $x;
135+
while ($left < $right) {
136+
$middle = ($left + $right + 1) >> 1;
137+
// $middle=($left+$right)>>1;
138+
if ($middle * $middle > $x) {
139+
$right = $middle - 1;
140+
} else {
141+
$left = $middle;
142+
}
143+
}
144+
return $left;
145+
}
146+
```
147+
148+
149+
**注意看这里我选择了右中位数。下面我们来解释一下如果选择左中位数会咋么样。道理解释起来也很简单,按照题目的意思,一个数的平方大于目标数,那么它一定不会是目标数的平方根(此时右边界语句一定能排除掉中位数!),反之,如果一个数的平方小于等于目标数,那么平方根可能就是中位数(并不能把左中位数排除),左边界语句不能排除中位数,那么此时程序必要进入死循环,所以这时候我们需要取右中位数。**
150+
151+
152+
**所以什么时候知道取哪边呢,也很简单,你可以先随便取左或者右,在收缩边界的时候,第一个语句直接写能排除中位数的,那么另一个边界一定不能排除中位数。然后再去判断取的那边中位数在边界的条件下是否能收缩空间,如若不能,反向获取中位数。当然这里的二分场景并不是很复杂,到了更复杂的二分场景,可能还需要做更多的操作。**
153+
154+
#### 最后再来一道
155+
156+
<a href="https://github.com/wuqinqiang/">
157+
​ <img src="https://github.com/wuqinqiang/Lettcode-php/blob/master/images/74.png">
158+
</a>
159+
**这道题稍微加了一点游戏难度,当然我可没说用暴力破解法,也不说解题思路了,直接贴按照上面说的模板解题代码**
160+
```
161+
/**
162+
* @param Integer[][] $matrix
163+
* @param Integer $target
164+
* @return Boolean
165+
*/
166+
function searchMatrix($matrix, $target)
167+
{
168+
$m = count($matrix);
169+
if ($m == 0) return false;
170+
$n = count($matrix[0]);
171+
if ($n == 0) return false;
172+
$left = 0;
173+
$right = $m * $n - 1;
174+
while ($left < $right) {
175+
$middle = ($left + $right) >> 1;
176+
$res = $matrix[$middle / $n][$middle % $n];
177+
if ($res < $target) {
178+
$left = $middle + 1;
179+
} else {
180+
$right = $middle;
181+
}
182+
183+
}
184+
return $matrix[$left / $n][$left % $n] == $target ? true : false;
185+
}
186+
187+
```
188+
**最后附上这个二分查找思路的地址值得一看:**[十分好用的二分查找模板](https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/)

0 commit comments

Comments
 (0)