前景提要:很早以前,在知乎上,有人问了这么个问题:二分查找有几种写法?它们的区别是什么?
问题链接:https://www.zhihu.com/question/36132386
这个问题的回答者中赞最多的人也是小编昔日的ACM队友LightGHLi,那么小编先从队友LightGHLi的回答中开始讲起:
首先是说到二分的写法种类,LightGHLi的分析是这样的:
中值的取值方法有:向上取整,向下取整 (2种)
二分的区间有:闭区间,左闭右开区间,左开右闭区间,开区间 (4种)
问题类型有:
对于不下降序列a,求最小的i,使得a[i] = key
对于不下降序列a,求最大的i,使得a[i] = key
对于不下降序列a,求最小的i,使得a[i] > key
对于不下降序列a,求最大的i,使得a[i] < key
对于不上升序列a,求最小的i,使得a[i] = key
对于不上升序列a,求最大的i,使得a[i] = key
对于不上升序列a,求最小的i,使得a[i] < key
对于不上升序列a,求最大的i,使得a[i] > key (8种)
所以分析下来总共的写法有2 x 4 x 8 = 64种。
LightGHLi的分析很全面,但是略显复杂,这里小编一边对问题深度分析一下,一边简化一下我们的写法总数、
首先,我们所有的写法都是建立在整数查找的基础上,如果问题是浮点数查找,那么中值取整,边界处理等问题都不存在。
好,接下里我们先来说下区间,虽说区间有闭区间,左闭右开区间,左开右闭区间,开区间这四种,但是其实对于整数区间来说这四种都是可以互相转换的,举个例子,(1,4),(1,3],[2,4)其实都和[2,3]等价,只需要加减边界值1就能轻松转换,所以我们完全不必去纠结这个边界问题。
然后是关于向上取整和向下取整问题,最近有一个初中的小学弟问我向上取整和向下取整到底有什么区别,我仔细想了一下,还是回答它:没有区别,这真的只是写法上的区别。仔细思考一个问题,在进行二分的时候遇到偶数个个数时,必然会出现中值需要取整的情况,向上取整和向下取整从结果上来说,不同的写法也是能得到相同的结果的,我们一般的写法都是向下取整,大家这个问题也不必去纠结、
再来说下我们的8种问题类型啊,其实这8种问题类型互相之间也是有关系的,首先一个不上升序列倒过来就是一个不下降序列,这一点我们也不必去纠结,之后小编的分析都是以不下降序列来分析的,遇到不上升序列也不过就倒过来就完事了。再来说下我们的问题类型中看起来眼花缭乱的最大最小大于小于等于的问题,其实呢这些也是相互之间有关联的,我们可以将其分解为两大类,求出第一个大于key的数,和求出第一个小于等于key的数,相信很多同学也看出来,这两类就是C++的库函数upper_bound和lower_bound,此处需要上那张经常与各大教材的图:
那么我们的问题如何去转换呢,首先,第一个问题求最小的i,使得a[i] = key其实就是lower_bound的值,第二个问题求最大的i,使得a[i] = key就是upper_bound的前一个值,第三个问题求最小的i,使得a[i] > key就是upper_bound的值,第四个问题求最大的i,使得a[i] < key就是lower_bound的前一个值。
那么我们有怎么去写二分实现lower_bound和upper_bound,其实这两种的最大的区别就在于,如何处理当前中值与key相等的情况,在lower_bound中,当前中值和key相等的时候,我们向中值的左边继续搜索,而在upper_bound中,则向右边搜索,大家可以再思考一下这个变化带来的差异。
附上实现代码:
int lower_bound(int *array, int size, int key) {
int L = 0, R = size - 1, Mid;
while(L < R) {
Mid = L + ((R - L) >> 1); //向下取整
if(array[Mid] < key) {
L = Mid + 1;
}
else {
R = Mid;
}
}
return L;
}
int upper_bound(int *array, int size, int key) {
int L = 0, R = size - 1, Mid;
while(L < R) {
Mid = L + ((R - L) >> 1); //向下取整
if(array[Mid] <= key) {
L = Mid + 1;
}
else {
R = Mid;
}
}
return L;
}