1. 二分搜索
二分搜索的目的是在有序序列里log(n)的时间查找目标
注:本文指的升序数组其实是非降序数组, 指的降序数组是非升序数组, 也就是有重复值, 但是为了方便, 就直接用比较直观的称呼了
2. c++现成的实现
-
lower_bound
和upper_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
这里有几个美丽的性质
- 一定满足L <= mid < R
- R没有被赋值的情况就是整个数组都不满足的情况, 此时R为数组长度, 答案的范围自然处于[L, R]
- 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
这里也有几个美丽的性质
- 一定满足 L < mid <= R, 因为这里(L+R+1)>>1 是取上界
- 最后check一下是否要返回-1是我加的边界条件, 此时L一定为0, L没有被赋值过有两种可能性, 一种就是整个数组不满足条件, 一种就是只有L=0满足条件
- 与之前不同的是, 这次满足条件动的是L, 因为L被赋值的话100%是满足的点. 而之前满足条件动的是R, 因为R被赋值的话是100%满足的点, 两者LR都互相逼近, 从mid分别取下界和上界来看, 前者是L向R逼近, 而后者是R向L逼近.
- 但是思路相同的是, 每次循环依然保证答案除了-1之外, 一定在[L, R]之间
结语
写题的时候随便套了个模板跑过了, 结束之后才来琢磨一下到底为什么
还有一些细节方面的事情
比如为了减小越界的可能可以int mid = l + ((r - l) >> 1)
以及对应的 int mid = l + ((r - l + 1) >> 1)
之前贴的reference里面的讨论也很有价值, 需要进一步理解的可以去看一看