二分搜索算法在搜索以及ACM中是很常用的。二分搜索算法是分治算法的一个典型的应用。但是二分搜索算法的正确性确实我们必须注意的一个问题。下面就介绍下二分搜索算法:
二分搜索算法的主要思想:对于一个已排序的元素序列,我们每次选取包含问题解的区间,然后求解其区间的中点,然后将中点元素的值与对应的键值进行比较,找出其对应的包含问题解的子区间。这样逐步将包含问题解的区间缩小,直至找到问题的解。
例如:从一堆一排序的元素1,3,5 ,7 ,8,9中查找元素7,如果存在则返回其下标,否则返回-1。由于已排序,我们先找出区间中间元素的下标,为1 + (6-1)/2 = 3,故而中间元素为5,而5小于7,区间[1 ,3]内的元素都小于5,故而问题转化为从区间[4,6]中找元素。这样再次找中间位置的元素为8,区间[5,6]中比不包含键值7,故而包含问题解的区间变为[4,4],即4号位置元素,对比发现相同,然后返回下标4。
源代码1:
int binary_search(int array[],int key){
int begin = 0;
int end = array.size()-1;
while(begin < end)
{
int mid = begin + (end-begin)/2;
if(array[mid] < key)
begin = mid+1;
else if(array[mid] > key)
end = mid-1;
else return mid;
}
return -1;
}
这段代码是在网上看到的,初看时,感觉没问题,不过细细一看,就有问题了。这里可以是一组这样的数据:1 ,3 ,5 ,7, 9 ,然后查找元素7。这里结果会得到-1,但是7确实在这组元素中。不知读者有没发现,这段代码有漏判元素,其原因就在于:begin = mid +1 和end = mid -1 这两句上,虽然判断条件没错,但是这两句会使得begin和end的很容易漏判,上述情况就是这样,当循环到begin==end的时候,就停止循环,而程序却没判断begin==end的情况,这里就导致错误了。我们可以在return -1 之前加一句判断,这样就没错了。我写一个代码:
int b_search(int * A , int x , int y , int key)
{
int v ;
while(x < y)
{
v = x + (y-x) /2 ;
if(A[v] < key)
y = v - 1 ;
else if(A[v]>key)
x = v + 1 ;
else
return v ;
}
if(A[x] == key)
return x ;
return -1 ;
}
不过也可以换种方式写,下面的代码是刘汝佳那本白书上的代码:
int bsearch(int * A , int x , int y , int v)
{
int m ;
while(x < y)
{
m = x + (y - x)/2 ;
if(A[m] == v)
return m ;
else if(A[m]>v)
y = m ;
else
x = m + 1 ;
}
return -1 ;
}
在很多情况下,我们不仅要找出某个元素是不是存在,我们还得找出这个元素的区间,及找出下界,与上界。我们让然可以利用二分的思想来解决这些问题。这里我们先看代码:
int lower_case(int * A , int x , int y , int key)
{
int v ;
while(x < y)
{
v = x + (y - x) /2 ;
if(A[v] > key)
y = v - 1 ;
else if(A[v] < key)
x = v + 1 ;
else
y = v ;
}
if(A[x]==key)
return x ;
return -1 ;
}
分析如下:如果元素A[v] > key 说明区间[v , y]中一定没有与key相等的元素。所以此时区间变为[x ,v-1];如果元素A{v] < key 说明区间[x , v]中一定没有与key相等的元素,所以区间变为[v+1 , y];当A[v] = key的时候说明已经存在key元素,但是其左侧仍然可能存在相同的key值,所以区间变成[x,v]。到这里还没结束,我们仍然要继续验证这个正确性,假若区间[x , v-1]或者[x,v]或者[v+1 , y]与区间[x,y]重复就会出现死循环,但是我们可以利用v的值来验证区间不会重复。当然在最后还要多判断一次,如果最后循环结果得到的A{x]的值与key不相同,则说明不存在key元素。这里我将刘汝佳白书上的代码提供在这里,虽有不同但是本质都是一样的:
int lower_bound(int *A , int x , int y, int key)
{
int m ;
while(x < y)
{
m = x + (y-x)/2 ;
if(A[m]>= key)
y = m ;
else
x = m + 1 ;
}
return x ;
}
再看找区间的上界。这里同样,我直接将代码贴在这里,原理跟上面的差不多,
int upper_case(int * A , int x , int y , int key)
{
int v ;
while(x < y)
{
v = x + (y - x + 1)/2 ;//这里需要注意
if(A[v] > key)
y = v - 1 ;
else if(A[v] < key)
x = v + 1 ;
else
x = v ;
}
if(A[x]==key)
return x ;
return -1 ;
}
刘汝佳白书上的代码:这里注意,程序求出来的结果是元素区间的上界+1 。
int upper_bound(int * A , int x , int y , int key)
{
int m ;
while(x < y)
{
m = x + (y-x)/2 ;
if(A[m] > key)
y = m ;
else
x = m + 1 ;
}
return x ;
}
上述算法其实在STL库中都有实现,直接调用函数lower_bound()和upper_bound(),可以得到对应的上下界。头文件:#include<algorithm>。在使用二分搜索的时候我们必须注意先对元素进行排序,然后在使用二分搜索技术。利用二分搜索的思想我们还可以解决很多问题。比如高次方程的根的逼近求解,以及求单调函数的解等等,这些问题我们可以结合数学知识,利用函数的单调性,求解问题的解。这里有一个关于求解方程解的问题的一个大致模板,用于解决浮点数问题:
double bsearch()
{
double x = min ;
double y = max ;
double v ;
while(fabs(x - y) < 1e-9 )
{
v = (y - x)/2 + x ;
if(f(v) > 0)
x = v ;
else
y = v ;
}
return v ;
}
在写利用二分搜索思想的程序时,要注意以下几点:1 , 当前区间一定包含问题的解或者说是目标元素。2,每次待查询的元素区间不断的缩小。有一篇关于二分查找的不错的文章,可以看一下(不过,可能有些许错误,需要读者自己辨别):
http://duanple.blog.163.com/blog/static/709717672009049528185/