二分法之整数

算法简介

二分法查找,也称为折半法,是一种在有序数组中查找特定元素的搜索算法。二分法分为整数二分和实数二分。整数二分涉及边界问题,实数二分不涉及边界问题。该算法讨论的是在整数上的二分。

算法思想

二分法的本质并不是具有单调性,它和单调性的关系是:如果具有单调性,则一定可以用二分法;如果没有单调性,也可以用二分法。那么二分的本质到底是什么呢?二分的本质是边界。假设我们给定了一个区间,我们在区间上定义了某种性质。在右半边区间是满足的,在左半边区间是不满足的。如下图:

如图所示,右边绿色的部分是满足该性质的,左边红色的部分是不能满足性质的。通过该性质可以将区间一分为二,一半满足该性质,一般不满足该性质。那么二分法可以寻找这个性质的边界,既可以寻找绿颜色的边界如b点,也可以寻找红颜色的边界如a点。具体步骤如下:

  • 如果二分的是红色的点a

  1. 首先找到中间值mid=l+r+1 >> 1。

  2. 判断下该中间值是否满足某种性质,比如是否满足红色这部分性质。有两种情况:如果满足的话,说明mid一定在红色区间,那么答案是在区间[mid,r]中,mid这一点是可以取到边界点的,所以说mid可能是答案,包含mid。区间的更新方式是l=mid。当mid不满足红色性质的时候,那么mid一定满足绿色性质,mid一定在绿色的区间[l,mid-1]。区间的更新方式是r=mid-1。 此时区间被划分成[l,mid-1]和[mid,r],mid在右区间。

  • 如果二分的是绿色的点b

  1. 首先找到中点mid=l+r>>1。

  2. 判断下该中间值是否满足某种性质,比如是否绿色这部分性质。有两种情况:如果满足的话,说明mid一定在绿色区间,那么答案是在区间[l,mid]中,mid这一点是可以取到边界点的,所以说mid可能是答案,包含mid。区间的更新方式是r=mid。当mid不满足绿色性质的时候,那么mid一定满足红色性质,mid一定在红颜色的区间[mid+1,r],不能取到mid。区间的更新方式是l=mid+1。 此时区间被划分成[l,mid]和[mid+1,r],mid在左区间。

模板

//区间[l,r]被划分成[l,mid]和[mid+1,r]时使用
int search_1(int l, int r){
     while(l < r){
        int mid = l + r >> 1;
        if(check(mid)) r = mid;  //check()判断mid是否满足性质
        else l = mid + 1;
     }
     return l;
}
​
//区间[l,r]被划分成[l,mid-1]和[mid,r]
int search_2(int l, int r){
     while(l < r){
         int mid = l + r + 1 >> 1;
         if(check(mid)) l = mid; //check()mid判断是否满足性质
         else r = mid - 1;
     }
     return l;
}

选择模板

当一个二分问题出现的时候如何去考虑?

遇到问题的时候不需要考虑上面的图,即二分的到底是红色的点还是绿色的点。我们考虑的是每次先写一个mid,之后,写一个check()函数,判断下check()函数是true或者false的时候该如何更新区间。如果区间更新的是l=mid和r=mid-1时,mid要加一,反之则不需要不上加一。核心的地方就是每次更新区间的时候,看下是l=mid还是r=mid。如果l=mid,则mid需要加一,如果r=mid,mid不需要加一。然后根据对应的模板解题。

注意

当mid属于右边区间的时候,mid=l + r + 1 >>1进行向下取整(相加的整数做除法的时候时向下取整mid=l+r>>1),那么mid为什么要加上一呢?

假设l和r之间只差1,即l=r-1时。如果mid=l + r >> 1进行向下取整得mid=l。当check(mid)=true时,l=mid(mid在右半边时),l更新的时候,l还是等于mid,更新的区间没有改变,那么下次循环的时候区间也不会改变,就会发生死循环。因此在更新区间的时候mid需要加上1。先while循环一遍,mid=r,区间从[mid,r]变成[r,r],循环就会结束。

算法案例模拟

给定一个按照升序排列的长度为n的数组,以及q个查询。对于每个查询,返回一个元素k的起始位置和终止位置(位数从0开始计数)。如果数组中不存在该元素,则返回"-1 -1 "。

#include<iostream>
​
using namespace std;
​
const int N = 100010;
​
int n,m;
int q[N];
int main(){
    //首先读入数组的长度和需要查询的数的个数
    scanf_s("%d%d", &n, &m);
    //循环读入
    for (int i = 0; i < n; i++) scanf_s("%d", &q[i]);
​
    //while控制查询的个数
    while(m--){
        int x;
        scanf_s("%d", &x);
​
        int l = 0, r = n - 1;
        while (l < r){
            int mid = l + r >> 1;
            //开始二分左边的点
            if (q[mid] >= x) r = mid;
            else l = mid + 1;
        }
        //当我们数列中不存在x的话 二分出来的值就是从左往右看第一个大于x的值
        if (q[l] != x) cout << "-1 -1" << endl;
        else{
            //输出l和输出r是一样的,因为当while循环结束的时候,l和r是相等的的
            cout << l << ' ';
            
        //开始二分右边的点
            int l = 0, r = n - 1;
            while (l < r){
                int mid = l + r + 1 >> 1;
                if (q[mid] <= x) l = mid;
                else r = mid - 1;
            }
            cout << l << endl;
        }
    }
    system("pause");
    return 0;
}

上述的情况如下(q[mid]>=x和q[mid]<=x),如下图:

运行截图

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值