今晚看到算法引论关于二分搜索的相关问题,想起了当年看编程珠玑的“无处不在的二分搜索”那章,记得作者说过能完全写对二分搜索的程序员寥寥无几,当时自己也写了下,确实不容易写,主要的难点在于写对,大致的框架可能大家都非常熟悉,但是里面的下标怎么确定是正确的呢?不对的下标很有可能造成死循环。不过,算法引论所推崇的数学归纳法的思想还是很普适的,反应在程序上就是先写n=1的情况,再写归纳阶段的代码,这样的方法用在写二分搜索感觉很有效,例如书中最普通的二分搜索代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
int
BinarySearch(
int
* A,
int
l,
int
r,
int
z)
{
if
(l == r)
{
if
(A[l] == z)
return
l;
else
return
-1;
}
else
{
int
middle = (l + r + 1)/2;
if
(z < A[middle])
{
BinarySearch(A,l, middle-1,z);
}
else
{
BinarySearch(A,middle,r,z);
}
}
}
|
这里对程序稍微做了点修改,但是大意是一样的,可以看到,程序书写的逻辑上,先处理n=1的情况,即l==r,这时候只有1个元素,对于1个元素的判断是trivial的,然后在else里面,算出middle并递归判断,这里是上取整,那么为什么要上取整?
要明白这个问题就是要理解,如果下取整会出现什么情况?死循环!让我们来验证下,假如某个时刻l=r-1,且A[l]<=z,这个时候就是死循环的时候,因为每次middle都会等于l(下取整)而A[l]<=z所以会走else那个分支,又继续递归l(此时middle是等于l的)和r,从而一直死循环下去。
仔细分析上面的情况不难发现,关键的地方在middle是下取整的情况,如果下取整会出现一个关键的问题就是l有可能等于middle,我们如果抓住这个问题去分析,就会很容易发现在else分支会出现死循环。下面用同样的方法分析下上取整,如果上取整的话,middle则可能会等于r,如果A[middle]>z,会直接导致r等于l,即在下次进入n=1的判断;如果A[middle]<=z,则同样会有middle等于r进入n=1的分支,这也证明了这个程序一定会在n=1的时候退出。其实在下取整和上取整发生不同的地方就是临界的位置,这也是容易造成死循环的时候。
通过以上分析,我们总结下如何快速判断一个二分搜索程序是否会出现死循环:
- 查看middle的选取是那种取整类型,是上取整还是下取整
- 根据类型,假定临界条件的发生,对上取整就是middle等于r了,对下取整就是middle等于l了
- 用临界的情况去检验if的两个分支,看看会不会出现循环调用,如果会则一定会有死循环,否则可大致判断程序是正确的
其实,编程珠玑中也介绍了程序验证学的方法,即assert,这个方法也是很好的一种方法,特别的是写短小程序的时候。
用这个方法,我发现图6.3中,二叉搜索的特殊下标问题中的程序是错误的,即middle的取法应该是下取整,书上是上取整,我用程序跑书的例子,果然华丽的堆栈溢出了,看来这种方法还是挺有作用的,不知道有人和我有同样的疑问吗,勘误上并没有说程序的问题。 (完)
【转2】
错误代码类似于下面的样子:
int
{
}
int
{
}
循环的开始处,把循环写成如下所示,则遍历的序列区间是一个左闭右开的区间:[0,n);
while
{
}
while
{
}
因此,这种错误的写法并不是在所有的情况下都会出错,有时还是可以找到正确的结果的.
这是一种典型的二分查找算法写错的情况,循环体是左闭右开区间,而循环体内部却是采用左闭右闭区间的算法进行操作.
下面给出的两种正确的算法,算法search是左闭右闭区间算法,而算法search2是左闭右开区间算法,可以对比一下差异.
{
}
int
{
}
下面再给出另一种典型的错误的二分查找算法,当查找的元素不在序列内时,它可能造成程序的死循环.
{
}
从循环条件来看,这个算法的操作区间是左闭右闭区间的,因此当array[middle] > v时,v如果存在的话应该在[left, middle- 1]中,因此此时right应该是middle - 1,而不是middle;类似的,当array[middle] < v时,下一次操作的区间应该是[middle + 1, right]中.而当元素不存在这个序列中时,算法在一个错误的区间中循环,但是又不能终止循环,于是就造成了死循环.
因此,要将二分查找算法写对,其实很多人都大概知道思想,具体到编码的时候,就会被这些看似微小的地方搞糊涂.因此,需要注意这一点:
算法所操作的区间,是左闭右开区间,还是左闭右闭区间,这个区间,需要在循环初始化,循环体是否终止的判断中,以及每次修改left,right区间值这三个地方保持一致,否则就可能出错.
[原创]
#include
using namespace std;
#define ARRYLENGTH 10
int main(void)
{
int arr[ARRYLENGTH],i,j;
for(i=0;i
arr[i] = i+5;
int searchnum=6;
int l=0,m,h=ARRYLENGTH-1,p=-1;
while(l<=h && h
m=(l+h)/2;
if(arr[m]==searchnum){
p = m;
break;
}else if(arr[m]
l=m+1;
else
h=m-1;
}
cout<<"p="<<p<<endl;
return 0;
}