二分查找又称为折半查找,就是通多对有序数列不断二分,将复杂度降至O(logn),二分查找尽管可以用递归实现,但是一般把二分写成非递归的。
问题引入
有一个有序数组,要求针对的某个元素v,如果它存在于数组中,返回他的第一次出现的位置,如果不存在,返回一个下标i,在此处插入v,其他元素后移,数组仍然有序,说白点就是找到第一个>=v的元素的下标
比如 数组a[10] = {1,2,2,2,4,5,6,7,8,9},v = 2时返回下标[1],v = 3时返回下标[4]。
所以核心代码如下:
int lower_bound(int *a,int x,int y,int v)//返回第一个出现的位置,若不存在在此处插入v后仍然有序
//x表示开始查找的开始位置,y表示结束位置即返回值 ∈【x,y】
while(x < y)//结束循环的条件 左端点>=右端点
{
m = x+(y-x)/2;//根据分治思想要尽可能平均分开两部分查找范围
if(a[m] >= v) y = m;//出现相等元素继续向前找
else x = m + 1;//针対找过了的情况,像回退一个
}
return x;
进一步分析这段代码是如何放缩的:
因为我们要查找的是第一个>=v的元素,针对a[m]和v的关系可分为如下三种:
1.a[m] = v,我们要找>=v的第一个元素,所以下标可能出现在m左侧,也可能就是m(m-1下标的元素可能小于v了),所以查找区间变为[x,m],就是y = m。
2.a[m] > v,我们要找>=v的第一个元素,所以下标可能出现在m后面,但是可能是m,所以[x,m],即y = m。
3.a[m] < v,我们要找**>=v**的第一个元素,此时已将<了,只能往前面找,所以[m+1,y],即x = m+1。
#include <iostream>
using namespace std;
//找到第一个>=key的元素的下标
int lower_bound(int *a,int x,int y,int v)//返回第一个出现的位置,若不存在在此处插入v后仍然有序
{
int m;
while(x < y)
{
m = x+(y-x)/2;
if(a[m] >= v) y = m;//出现相等元素继续向前赵
else x = m + 1;//针対找过了的情况
}
return x;
}
int main()
{
int a[10] = {1,2,2,2,3,3,7,8,9,10};
int v = 2;
cout << lower_bound(a,0,9,v) << endl;
cout << upper_bound(a,0,9,v) << endl;
return 0;
}
运行结果如下
1
4
upper_bound
实际上这里upper_bound已经实现了STL中的同名函数,下面看一下STL中的源码:
与上面思想相同,不同的是采用first+half的方法来寻找合适位置,当half = 0时候就锁定了唯一的值,为了方便理解源码下面一段是我改编的
template <class _ForwardIter, class _Tp, class _Distance>
_ForwardIter __lower_bound(_ForwardIter __first, _ForwardIter __last,
const _Tp& __val, _Distance*)
{
_Distance __len = 0;
distance(__first, __last, __len);
_Distance __half;
_ForwardIter __middle;
while (__len > 0) {
__half = __len >> 1;
__middle = __first;
advance(__middle, __half);
if (*__middle < __val) {
__first = __middle;
++__first;
__len = __len - __half - 1;
}
else
__len = __half;
}
return __first;
}
int lower_bound_STL(int *a,int first,int last,int v)
{
int half; //这个half就是查找区间的一半,就是这个二分的核心
int len = last - first;//初始时查找区间是整个区间
int middle;//都是通过first + 区间长度来筛选的
while(len > 0)//当len = 0的时候就是我们要找的值了
{
half = len >> 1;
middle = first+half;
if(a[middle] < v)
{
first = middle + 1;
len = len - half -1;//与上面分析类似就是在[m+1,y]中查找
}
else len = half;// [x,m]中查找
}
return first;
}
至此,lower_bound已经结束了,upper_bound与它类似,查找的是第一个大于v的元素的下标(地址),只要做如下改变
if(a[m] <= v) x = m+1;
else y = m;
思想也是一样的,根据><=确定查找区间。
延伸
1.在STL中一般采用半开半闭区间,这样的好处是只要相减就可以得到v的个数。
eg: 数组a[10] = {1,2,2,2,4,5,6,7,8,9},v = 2时返回下标[1],v = 3时返回下标[4],那么2的个数就是4-1=3。
2.类似我们还可以写出返回最后一个>=v的元素的下标:
int bigandequal(int x,int y,int *a)
{
int m;
while(x < y)
{
m = x+(y-x+1)/2;
if(a[m] <= v) x = m;
else y = m-1;
}
return x;
}
例题(二分答案)
#include <iostream>
#define N 200100
using namespace std;
int t,n,k;
int judge(int m,char *light)
{
int cnt = 0;
for(int i = 0;i<n;)
{
if(light[i] == '0')
{
i++;
continue;
}
else
{
cnt++;
i += m;
}
}
return cnt;
}
int binsearch(char *light)
{
int x = 1,y = n;
int m;
while(x < y)
{
m = (x+y)/2;
if(judge(m,light) <= k) y = m;
else x = m+1;
}
return x;
}
int main()
{
cin >> t;
while(t--)
{
char light[N];
cin >> n >> k;
for(int i = 0;i<n;i++)
{
cin >> light[i] ;
}
cout << binsearch(light) << endl;
}
return 0;
}