今晚看到算法引论关于二分搜索的相关问题,想起了当年看编程珠玑的“无处不在的二分搜索”那章,记得作者说过能完全写对二分搜索的程序员寥寥无几,当时自己也写了下,确实不容易写,主要的难点在于写对,大致的框架可能大家都非常熟悉,但是里面的下标怎么确定是正确的呢?不对的下标很有可能造成死循环。不过,算法引论所推崇的数学归纳法的思想还是很普适的,反应在程序上就是先写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的时候退出。其实在下取整和上取整发生不同的地方就是临界的位置,这也是容易造成死循环的时候。
通过以上分析,我们总结下如何快速判断一个二分搜索程序是否会出现死循环:
其实,编程珠玑中也介绍了程序验证学的方法,即assert,这个方法也是很好的一种方法,特别的是写短小程序的时候。
用这个方法,我发现图6.3中,二叉搜索的特殊下标问题中的程序是错误的,即middle的取法应该是下取整,书上是上取整,我用程序跑书的例子,果然华丽的堆栈溢出了,看来这种方法还是挺有作用的,不知道有人和我有同样的疑问吗,勘误上并没有说程序的问题。 (完)
【转2】
错误代码类似于下面的样子:
循环的开始处,把循环写成如下所示,则遍历的序列区间是一个左闭右开的区间:[0,n);
[原创]
#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;
}