二分查找算法及其STL库函数(基于有序数组)

以下内容整理和参考一些博客,还有和自己的理解,若有错误请不吝指出
题目

二分查找是针对有序数组所用的一种快速查找元素的方法。

二分查找的条件以及优缺点

条件:针对有序数组(元素从小到大或从大到小)
优点:查询速度较快,时间复杂度为O(log2n)
缺点:有硬性条件的限制,而且即使查到后,插入与删除困难。
在这里插入图片描述
二分查找一般有三个应用场景:①查找一个数②寻找左侧边界③寻找右侧边界

大致模板如下:

int Binary_Search(int num[], int key) {
    int left = 0, right = ...;
    while(...) {
        int mid = left + ((right - left) >> 1); //防止溢出,+的运算优先级高于>>,所以要记得把后面的加上括号
        if (num[mid] == key) {
            ...
        } 
        else if (num[mid] < key) {
            left = ...
        } 
        else if (num[mid] > key) {
            right = ...
        }
    }
    return ...;
}

搜索区间为 [left, right] 则 while (left <= right), left = mid + 1, right = mid - 1.
搜索区间为 [left, right) 则 while (left < right), left = mid + 1, right = mid.

①二分查找一个数

查找一个数,找到则返回其索引,否则返回-1。(该算法的局限性:当是在一组有序序列中查找存在重复的数时,返回的索引只是该数其中的一个,不一定是第一个,即该算法无法处理寻找这个数的左右边界)

int Binary_Search(int num[], int key) {
    int left = 0, right = num.length - 1;
    while(left <= right) {
        int mid = left + ((right - left) >> 1);
        if (num[mid] == key) return mid;
        else if (num[mid] < key) left = mid + 1;
        else if (num[mid] > key) right = mid - 1;
    }
    return -1;
}
/*折半查找,递归实现*/  
int Binary_Search2(int *a, int low, int high, int key)  
{  
     if(low > hign) return -1;
     int mid = (low + high) / 2;  
     if(a[mid] == key) return mid;  
     if(a[mid] > key) return Binary_Search2(a, low, mid-1, key); //有没有return都可以。  
     else return Binary_Search2(a, mid+1, high, key); //有没有return都可以。  
}  
②寻找一个数左边界的二分查找

可用于查找第一个大于或等于key的数的索引位置,相当于lower_bound函数。

当 num[mid] == key (即查找到key)时不要立即返回,而是缩小搜索区间的右界right,在[left,right)中继续查找,使得区间不断向左收缩,达到锁定左侧边界的目的

当循环终止时left == right,right == mid,此时mid为第一个key的索引(即左侧边界),所以key的左边界索引为left或right。

int left_bound(int num[], int key) {
	if (num.length == 0) return -1;
    int left = 0, right = num.length;
    while (left < right) {
        int mid = left + ((right - left) >> 1);
        if (num[mid] == key) right = mid;
        else if (num[mid] < key) left = mid + 1;
        else if (num[mid] > key) right = mid;
    }
    /*return left; 返回第一个大于或等于key的数的索引位置 */
    return num[left] == key ? left : -1; //key存在则返回左边界索引,不存在返回-1  
}
②寻找一个数右边界的二分查找

可用于查找第一个大于key的数的索引位置,相当于upper_bound函数。

当 num[mid] == key (即查找到key)时不要立即返回,而是增大搜索区间的左界 left,在[left,right)中继续查找,使得区间不断向右收缩,达到锁定右侧边界的目的。

注意,循环终止时left=mid+1,所以此时mid是最后一个key的位置(即右侧边界),故mid=left-1,所以key的右边界索引为left-1。

int right_bound(int num[], int key) {
	if (num.length == 0) return -1;
    int left = 0, right = num.length;
    while (left < right) {
        int mid = left + ((right - left) >> 1);
        if (num[mid] == key) left = mid + 1;
        else if (num[mid] < key) left = mid + 1;
        else if (num[mid] > key) right = mid;
    }
    /*return left; 返回第一个大于key的数的索引位置,这里left不用-1*/
    return num[left-1] == key ? left-1 : -1; //key存在则返回右边界索引,不存在返回-1
}

优化:
mid = (left+right)/2或middle= (left+right)>>1; 这样的话left与right的值比较大的时候,其和可能溢出。所以可以用int mid = left - (left - right) /2;middle=left + ((right-left)>>1);来防止越界,或用int mid = left & right + (left ^ right) >> 1;(进行计算机最喜欢的位运算,效率略高)来代替求两个数的平均值的操作。

补充:
二分的难点在于最后的边界问题,处理不好就会陷入死循环。有一种很稳妥的方法能很好的规避这些边界问题:我们可以二分到区间小到一定程度,比如5个以下,然后去顺序查找。这样处理对于二分的时间复杂度影响也极小。

如下代码所示。

int r=1000000000,l=0,ans;
while(r-l>4){
    int mid=l+((r-l)>>1);
    if(check(mid))l=mid;
    else r=mid-1;
}
for(int i=l;i<=r;i++){
    if(check(i))ans=i;
}

C++STL中的二分查找函数:

STL中与二分查找相关的函数有4个,分别是lower_bound, upper_bound, equal_rangebinary_search

其中每个函数实现的功能如下:

  • binary_search:查找某个元素是否出现,返回bool型。
  • lower_bound:查找第一个大于或等于某个元素的位置。
  • upper_bound:查找第一个大于某个元素的位置。
  • equal_range:查找某个元素出现的起止位置。注意,终止位置为最后一次出现的位置加一。(返回一个pari对象,pair::first是指向子范围左边界的迭代器. pair::second是指向子范围右边界的迭代器.它们的值和lower_bound,upper_bound分别返回的值相同.)
关于lower_bound和upper_bound:

在这里插入图片描述
这两个函数的返回值都是地址或迭代器(也就是标准库中的容器iterator)。
我们也可以用其返回值减去数组首地址得到我们习惯的数组下标。

当查找的元素一次都未出现时,二者返回的结果都是第一个大于该元素的位置。

int b[7]={1,2,3,3,3,4,6};
int k1=*upper_bound(b,b+7,3);//输出k1为值4
int *k2=upper_bound(b,b+7,3);//输出k2为地址0x6dfee0,输出*k2为值4
auto k3=upper_bound(b,b+7,3);//输出k3为地址0x6dfee0,输出*k3为值4
int k4=upper_bound(b,b+7,3)-b;//输出k4为下标5

/*lower_bound用法也一样*/

若要查找一个有序数组b中某个值key的个数,可以用以下代码求出:
int t=upper_bound(b,b+n,key)-lower_bound(b,b+n,key);

上述的二分查找函数的使用情况是默认从小到大的有序(即升序),如果要在从大到小的有序序列(即降序)使用时,我们需要使用函数的第4个参数去重载,第4个参数类似sort的第3个参数。

bool cmp(int x,int y){return x>y;}

int a[7]={6,5,4,4,3,2,1};
int k1=lower_bound(a,a+7,4,greater<int>())-a;//输出为下标2
int k2=upper_bound(a,a+7,4,greater<int>())-a;//输出为下标4
int k3=upper_bound(a,a+7,4,cmp)-a;//输出为下标4

STL库二分函数的代码应用如下:(题目文章开头已贴出)

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int M,a[200005],n,x;
    scanf("%d",&M);
    for(int i=0;i<M;i++)scanf("%d",&a[i]);
    scanf("%d",&n);
    for(int j=0;j<n;j++){
        scanf("%d",&x);
        int t=binary_search(a,a+M,x);///该二分查找函数为bool型,判断查找的数是否存在
        int *k=lower_bound(a,a+M,x);///找到需要查找的数的第一个位置(为地址)
        int *q=upper_bound(a,a+M,x);///找到比需要查找的数大的第一个数的第一个位置(为地址)
        int p=q-a-(k-a);///q-a和k-a分别得出两者的数组下标
        if(t){
            if(p==1) ///数组中无重复数字x的情况
                printf("%d",k-a);
            else ///数组中有重复数字x的情况
                for(int w=k-a;w<q-a;w++)printf("%d ",w);
        }
        else printf("Not Found");
        puts("");
    }
    return 0}

除了上述整数的二分外,还有针对浮点数的二分。

浮点数二分算法

浮点数二分相比于整数的二分好写很多,不用考虑边界问题。

浮点数二分有两种写法 :

  1. 以循环次数为循环终止条件
  2. 以精度为循环终止条件

第二种写法如果eps太小, 会可能由于浮点数的精度问题陷入死循环, 所以推荐第一种写法

for(int i=0; i<60; ++i){ //i为循环次数, 60次循环可以让精度达到初始区间长度的1/1e18
    double mid=(l+r)/2.0;
    if(check(mid))l=mid;
   	else r=mid;
}

const double eps=1e-8;
while(r-l<eps){  //其中eps是所需要的精度
    double mid=(l+r)/2.0;
    if(check(mid))l=mid;
   	else r=mid;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值