二分搜索漫谈

1. 二分搜索

二分搜索的目的是在有序序列里log(n)的时间查找目标

注:本文指的升序数组其实是非降序数组, 指的降序数组是非升序数组, 也就是有重复值, 但是为了方便, 就直接用比较直观的称呼了

2. c++现成的实现

  • lower_boundupper_bound
    默认的情况下, 他们都是处理升序数组
    lower_bound返回的是有序容器中第一个不小于(>=)给定值的迭代器, 如果所有元素都不满足, 则返回指向容器末尾的元素迭代器
    upper_bound返回的是有序容器中第一个大于(>)给定值元素的迭代器, 如果所有元素都不满足, 则返回指向末尾的元素迭代器
    那为什么升序数组不查找数组中第一个小于(<)或小于等于(<=)给定值元素的迭代器呢? 因为第一个值就是最小的, 若是第一个值满足, 那就是0, 如果第一个值不满足, 那么就无解

  • 升序数组
    对于数组{1, 3, 3, 5, 5, 7}
    lower_bound查询3为1, 而upper_bound查询3为3
    这就是区别

    vector<int> v = {1, 3, 3, 5, 5, 7};
    cout << std::lower_bound(v.begin(), v.end(), 3) - v.begin() << '\n';
    cout << std::upper_bound(v.begin(), v.end(), 3) - v.begin() << '\n';
    cout << std::upper_bound(v.begin(), v.end(), 8) - v.begin() << '\n';
1
3
6
  • 降序数组
    而如果要处理降序数组
    需要自定义比较函数
std::lower_bound(v.begin(), v.end(), value, std::greater<int>());

降序数组+自定义比较函数, lower_bound查询的是第一个不大于(<=)给定值的元素迭代器
upper_bound查询的是第一个小于(<)给定值的元素迭代器
和升序数组同理, 降序数组也不会去查找第一个>或是>=的数字, 否则O(1)即可得出答案

    vector<int> v = {7, 5, 5, 3, 3, 1};
    cout << std::lower_bound(v.begin(), v.end(), 3, std::greater<int>()) - v.begin() << '\n';
    cout << std::upper_bound(v.begin(), v.end(), 3, std::greater<int>()) - v.begin() << '\n';
    cout << std::upper_bound(v.begin(), v.end(), 0, std::greater<int>()) - v.begin() << '\n';
3
5
6

3. 手写模板

然而实际上我看到过几种模板
几种二分搜索模板reference

第一种模板

这里的模板其实没写清楚, 为了考虑所有元素均不满足的情况, 我们需要把r设置的比边界大1
我修改之后的模板如下:
可以用来求升序数组第一个>>=, 降序数组第一个<或者<=的元素下标

int bsearch1(int L, int R, const int *arr, const int target) // 升序数组arr, 目标值target
{
    while (L < R)
    {
        int mid = (L + R) >> 1;
        if (arr[mid] >= target) // lower_bound, 修改为>则为求upper_bound, 修改为< 或 <= 则为求降序(准确说是非升序)序列中相应的bound
            R = mid;
        else
            L = mid + 1;
    }
    return L;
}

int arr[] = {1, 3, 3, 5, 5, 7};
cout << bsearch1(0, sizeof(arr) / sizeof(int), arr, 3) << '\n'; // 输出 1

这里有几个美丽的性质

  1. 一定满足L <= mid < R
  2. R没有被赋值的情况就是整个数组都不满足的情况, 此时R为数组长度, 答案的范围自然处于[L, R]
  3. mid满足时设为右边界, 因为这之后不可能是答案. mid不满足时左边界设为mid+1. 这里的原则就是R被赋值过之后, 答案的范围也要被锁定在[L, R]中

第二种模板

也是我魔改过的版本(增加了对于整个数组不满足时候的判断)
则是用来求升序数组最后一个<=或者<目标target 的数字下标, 降序数组最后一个>=或者>目标target的数字下标
举个例子
{7, 5, 5, 3, 3, 1}中, >=3的最后一个数字下标是4
简单来说, 假如数组条件满足情况是
满足, 满足, 满足, 满足, 满足, 不满足
我们的目的是找最后一个满足的点, 而模板1的数组则是反过来, 不满足, 不满足,不满足,不满足,满足,满足,满足,满足, 找第一个满足的点
很神奇吧? 对于这种case, 其实也可以用模板1或lowerbound或者upper_bound求到逆条件之后回退1个pos来解决, 要注意越界问题

int bsearch2(int L, int R, const int *arr, const int target)
{
    while (L < R)
    {
        int mid = (L + R + 1) >> 1;
        if (arr[mid] >= target)
        {
            L = mid;
        }
        else
            R = mid - 1;
    }
    if (arr[L] < target)
    {
        return -1;
    }
    return L;
}

int arr[] = {7, 5, 5, 3, 3, 1};
cout << bsearch2(0, sizeof(arr) / sizeof(int) - 1, arr, 3) << '\n'; // 4
cout << bsearch2(0, sizeof(arr) / sizeof(int) - 1, arr, 8) << '\n'; // -1

这里也有几个美丽的性质

  1. 一定满足 L < mid <= R, 因为这里(L+R+1)>>1 是取上界
  2. 最后check一下是否要返回-1是我加的边界条件, 此时L一定为0, L没有被赋值过有两种可能性, 一种就是整个数组不满足条件, 一种就是只有L=0满足条件
  3. 与之前不同的是, 这次满足条件动的是L, 因为L被赋值的话100%是满足的点. 而之前满足条件动的是R, 因为R被赋值的话是100%满足的点, 两者LR都互相逼近, 从mid分别取下界和上界来看, 前者是L向R逼近, 而后者是R向L逼近.
  4. 但是思路相同的是, 每次循环依然保证答案除了-1之外, 一定在[L, R]之间

结语

写题的时候随便套了个模板跑过了, 结束之后才来琢磨一下到底为什么
还有一些细节方面的事情
比如为了减小越界的可能可以int mid = l + ((r - l) >> 1) 以及对应的 int mid = l + ((r - l + 1) >> 1)
之前贴的reference里面的讨论也很有价值, 需要进一步理解的可以去看一看

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值