二分查找01.基本二本查找及其变种
基本的二分查找
我们假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找区间被缩小为空,才停止。
可以看出来,这是一个等比数列。其中 n/2k=1 时,k 的值就是总共缩小的次数。而每一次缩小操作只涉及两个数据的大小比较,所以,经过了 k 次区间缩小操作,时间复杂度就是 O(k)。通过 n/2k=1,我们可以求得 k=log2n,所以时间复杂度就是 O(logn)。
<?php
function bsearch($arr,$n){
$low=0;
$high=count($arr)-1;
//注意边界
while($low<=$high){
// $mid=intval(($low+$high)/2);
$mid=$low+(($high-$low)>>1);
if($arr[$mid]==$n){
return $mid;
}elseif($n<$arr[$mid]){ //n在 [low,mid-1]
$high=$mid-1;
}else{ //n 在[mid+1,high]
$low=$mid+1;
}
}
return -1;
}
易出错的三点
1. 循环退出条件
注意是 low<=high,而不是 low实际上,mid=(low+high)/2 这种写法是有问题的。
因为如果 low 和 high 比较大的话,两者之和就有可能会溢出。
改进的方法是将 mid 的计算方式写成 low+(high-low)/2。
更进一步,如果要将性能优化到极致的话,我们可以将这里的除以 2 操作转化成位运算 low+((high-low)>>1)。因为相比除法运算来说,计算机处理位运算要快得多。
3.low 和 high 的更新
low=mid+1,high=mid-1。 注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环 比如,当 high=3,low=3 时,如果 a[3] 不等于 value,就会导致一直循环不退出递归方式实现
//递归方式实现
function bsearch_recursion($arr,$n){
return bsearch_recursion_internally($arr,0,count($arr)-1,$n) ;
}
function bsearch_recursion_internally($arr,$low,$high,$n){
if($high<$low){
return -1;
}
$mid=$high-intval(($high-$low)>>1);
if($n==$arr[$mid]){
return $mid;
}
if($n<$arr[$mid]){
return bsearch_recursion_internally($arr,$low,$mid-1,$n);
}else{
return bsearch_recursion_internally($arr,$mid+1,$high,$n);
}
return -1;
}
二分查找应用场景的局限性
- 1.二分查找依赖的是顺序表结构,简单点说就是数组。
那二分查找能否依赖其他数据结构呢?比如链表。
答案是不可以的,主要原因是二分查找算法需要按照下标随机访问元,数组按照下标随机访问数据的时间复杂度是 O(1),而链表随机访问的时间复杂度是 O(n)。
所以,如果数据使用链表存储,二分查找的时间复杂就会变得很高。
二分查找只能用在数据是通过顺序表来存储的数据结构上。如果你的数据是通过其他数据结构存储的,则无法应用二分查找。
- 2.二分查找针对的是有序数据
- 3.数据量太小不适合二分查找
如果要处理的数据量很小,完全没有必要用二分查找,顺序遍历就足够了。
比如我们在一个大小为 10 的数组中查找一个元素,不管用二分查找还是顺序遍历,查找速度都差不多。只有数据量比较大的时候,二分查找的优势才会比较明显。
不过,这里有一个例外。如果数据之间的比较操作非常耗时,不管数据量大小,我都推荐使用二分查找。
比如,数组中存储的都是长度超过 300 的字符串,如此长的两个字符串之间比对大小,就会非常耗时。我们需要尽可能地减少比较次数,而比较次数的减少会大大提高性能,这个时候二分查找就比顺序遍历更有优势。
- 4.数据量太大也不适合二分查找
二分查找的底层需要依赖数组这种数据结构,而数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻。
比如,我们有 1GB 大小的数据,如果希望用数组来存储,那就需要 1GB 的连续内存空间
- 1.查找第一个值等于给定值的元素
- 2.查找最后一个值等于给定值的元素
- 3.查找第一个大于等于给定值的元素
- 4.查找最后1个小于等于给定值的元素
变形二分查找
1.查找第一个值等于给定值的元素
关键点在于在找到等于的值后,还继续怎么找
//第1个等于给定值
function bsearch_first_eq($arr,$value){
$low=0;
$high=count($arr)-1;
while($high>=$low){
$mid=$low+(($high-$low)>>1);
if($arr[$mid]==$value){
//因为是要找第一个,所以是从mid往前找,
if($mid==0 ||$arr[$mid-1]!=$value){
return $mid;
}
$high=$mid-1;
}else if($arr[$mid]>$value){
$high=$mid-1;
}else{
$low=$mid+1;
}
}
return -1;
}
2.查找最后一个值等于给定值的元素
function bsearch_last_eq($arr,$value){
$low=0;
$high=count($arr)-1;
while($high>=$low){
$mid=$low+(($high-$low)>>1);
if($arr[$mid] ==$value){
//因为要找最后1个,需要往后找
if($mid == $high || $arr[$mid+1] !=$value){
return $mid;
}
$low=$mid+1;
}elseif($arr[$mid]<$value){
$low=$mid+1;
}else{
$high=$mid-1;
}
}
return -1;
}
3.查找第一个大于等于给定值的元素
function bsearch_first_egt($arr,$value){
$low=0;
$high=count($arr)-1;
while($high>=$low){
$mid=$low+(($high-$low)>>1);
if($arr[$mid]>=$value){
if($mid==0 ||$arr[$mid-1]<$value){
return $mid;
}
$high=$mid-1;
}else{
$low=$mid+1;
}
}
return -1;
}
4.查找最后1个小于等于给定值的元素
function bsearch_last_elt($arr,$value){
$low=0;
$high=count($arr)-1;
while($high>=$low){
$mid=$low+(($high-$low)>>1);
if($arr[$mid]<=$value){
if($mid==$high||$arr[$mid+1]>$value){
return $mid;
}
$low=$mid+1;
}else{
$high=$mid-1;
}
}
return -1;
}