二分查找是指在已排序的序列中,通过不断折半缩小查找范围来查找数据的方法,序列越长,其优越性越明显,它的思想非常简单,但是实际写的时候很容易忽略各种边界值而写出死循环或者判断出错。
示例序列1,2,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,22,25,26,27,31,34,在其中寻找3。
先找这个序列中位数,13,3比13小,缩小查找范围:1,2,2,3,4,5,6,7,8,9,10,11,12,(因为刚才已经与13比过了,这里不再与其相比,最大位为mid_key - 1)
新的中位数6,对比,缩小查找范围:1,2,2,3,4,5
新的中位数2,对比,缩小查找范围:3,4,5
新的中位数4,对比,缩小查找范围:3。
新的中位数3,对比,找到了。
function digui($array,$find,$left=0,$right=0){
if($right == 0){
$right = count($array) - 1;
}
if($left > $right || $find < $array[$left] || $find > $array[$right]){//边界条件
echo $find." is not in </br>";
}else{
$mid_key = intval(($left + $right)/2);
$mid = $array[$mid_key];
//echo 'mid key is '.$mid_key.' and value is '.$mid."</br>";
if($find == $mid){
echo 'I find it :'.$find."</br>";
}elseif($find > $mid){//比中位数大
digui($array,$find,$mid_key+1,$right);
}else{//比中位数小
digui($array,$find,$left,$mid_key-1);
}
}
}
$array=[1,2,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,22,25,26,27,31,34];
//负数、小数也适用
//$array=[-11,-5.5,-3.9,-3.6,-3.2,-3.1,1.1,1.2,1.3,1.4,1.5,1.6,2,2,3,4,5,6,13,14,15,16,17,18,19,20,22,25,26,27,31,34];
for($i=0;$i<=35;$i++){
digui($array,$i);
}
foreach ($array as $v) {
digui($array,$v);
}
但是事实上,这段代码还有很严重的问题,比如$array=[1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,19,20,30,31,34],我需要找到2。
按我们的代码,前几步都是没问题的:
mid=12,对比,缩小范围:1,3,4,5,6,7,8,9,10,11
mid=7,对比,缩小范围:1,3,4,5,6
mid=4,对比,缩小范围:1,3
mid=3,对比,find比mid小,需要缩小范围为$left,$mid_key-1,即:1,1 ???
mid=1,对比,find比mid大,需要缩小范围为$mid_key+1,$right,即:1,1 ???
死循环了。。。 。。。
问题在哪呢?原来,当mid=4对比后,范围只剩下1,3两位时就不能再递归啦,直接对比左右边界,find=左或者find=右都可以说找到了,要是两边都没有,直接返回没有就行了。
以上代码可以优化为:
function digui($array,$find,$left=0,$right=0){
// echo $array[$left].'------'.$array[$right]."</br>";
if($right == 0){
$right = count($array) - 1;
}
if($left >= $right || $find < $array[$left] || $find > $array[$right]){//边界条件
echo $find." is not in </br>";
}else{
if($right - $left <= 1){//只剩下最后两个数,直接对比左右边界
if($find == $array[$left] || $find == $array[$right]){
//为了测试直观,存在输出和不存在输出可以分别进行.当然也可以直接全输出
// echo 'wow ! I find it :'.$find."</br>";
}else{
echo $find." is not in </br>";
}
}else{
$mid_key = intval(($left + $right)/2);
$mid = $array[$mid_key];
// echo 'mid key is '.$mid_key.' and value is '.$mid."</br>";
if($find == $mid){
//为了测试直观,存在输出和不存在输出可以分别进行.当然也可以直接全输出
// echo 'I find it :'.$find."</br>";
}elseif($find > $mid){//比中位数大
digui($array,$find,$mid_key,$right);
}else{//比中位数小
digui($array,$find,$left,$mid_key);
}
}
}
}
//$array=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,22,25,26,27,31,34];
$array=[1,3,3.1,3.2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,19,20,30,31,34];
//负数、小数也适用
//$array=[-11,-5.5,-3.9,-3.6,-3.2,-3.1,1.1,1.2,1.3,1.4,1.5,1.6,2,2,3,4,5,6,13,14,15,16,17,18,19,20,22,25,26,27,31,34];
for($i=0;$i<=35;$i++){
digui($array,$i);
}
foreach ($array as $v) {
digui($array,$v);
}
后来我努力学习算法(听起来好励志),终于写出一种更好理解更清爽的二分查找来,再也不用费尽心力考虑各种边界值了,啊!
function mySelect($arr,$find){
$left = 0;
$right = count($arr)-1;
//老规矩,边界排除掉可以极大提升查找效率(当然不排除边界也可以拿到正确结果)
if($find < $arr[$left] || $find > $arr[$right]){
return false;
}
return myFind($arr,$find,$left,$right);
}
function myFind($arr,$find,$left,$right){
while($left <= $right){
$mid = intval(($left+$right)/2);
if($find > $arr[$mid]){
return myFind($arr,$find,$mid+1,$right);
}elseif($find == $arr[$mid]){
return true;
}else{
return myFind($arr,$find,$left,$mid-1);
}
}
return false;
}
哭了 前边也不删了 菜鸡成长史
后续我在力扣刷又刷到一些可以二分查找来解的题目
69 x平方根
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842...,
由于返回类型是整数,小数部分将被舍去。
一开始我的解法是:
从0起,一个一个试
class Solution {
function mySqrt($x) {
$i=0;
while(true){
if($i*$i < $x){
$i++;
}elseif($i*$i == $x){
return $i;
}else{
return $i-1;
}
}
}
}
时间复杂度O(n),空间复杂度O(1),没超时,说明官方是认这种解法的,但是官方有测试用例:$x=2147395599,最后结果是46339,也就是我一个一个尝试了46339次。
迅速调整思路,二分查找来了
(据我观察,二分查找在本题是相当主流的解法)
class Solution {
/**
* @param Integer $x
* @return Integer
*/
function mySqrt($x) {
return $this->mySelect($x,0,$x);
}
function mySelect($x,$left,$right){
while($left<=$right){
$find = intval(($right+$left)/2);
if($find*$find <= $x && ($find+1)*($find+1) > $x){
return $find;
}elseif($find*$find > $x){
return $this->mySelect($x,$left,$find-1);
}else{
return $this->mySelect($x,$find+1,$right);
}
}
return 0;
}
}
时间复杂度并没有降低,还是O(n),空间复杂度甚至还提高了,也是O(n),但是在面对这么大的数据时,二分查找的确减少了不少次判断。