关于二分搜索的一点思考

【转1】

今晚看到算法引论关于二分搜索的相关问题,想起了当年看编程珠玑的“无处不在的二分搜索”那章,记得作者说过能完全写对二分搜索的程序员寥寥无几,当时自己也写了下,确实不容易写,主要的难点在于写对,大致的框架可能大家都非常熟悉,但是里面的下标怎么确定是正确的呢?不对的下标很有可能造成死循环。不过,算法引论所推崇的数学归纳法的思想还是很普适的,反应在程序上就是先写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】

错误代码类似于下面的样子:

#include 

int  search( int  array[],  int  n,  int  v)
{
        int  left,  right,  middle;

        left  =  0,  right  =  n;

        while  (left  <  right)
        {
                middle  =  (left  +  right)  /  2;
                if  (array[middle]  >  v)
                {
                        right  =  middle  -  1;
                }
                else  if  (array[middle]  <  v)
                {
                        left  =  middle  +  1;
                }
                else
                {
                        return  middle;
                }
        }

        return  -1;
}

int  main()
{
        int  array[]  =  {0,  1,  2,  3,  4,  5,  6,  7,  13,  19};

        int  m  =  search(array,  sizeof(array)/ sizeof(array[0]),  1);

        printf("m  =  %d\n",  m);

        return  0;
}

实际上,如果使用测试用例来测试,这个算法并不是在所有情况下都会出错的,还是有时可以得到正确的结果的.但是,你能看出来它错在哪儿吗?

循环的开始处,把循环写成如下所示,则遍历的序列区间是一个左闭右开的区间:[0,n)

 

left  =0,  right  =  n;
while  (left  <  right)
{
        //  关于二分搜索的一点思考循环体
}
若写成如下所示, 则遍历的序列区间是 一个左闭右开的区间:[0,n]  

 

left  = 0 , right  =  n;
while  (left  <=  right)
{
     //  关于二分搜索的一点思考循环体
}
但是,在循环内部, 却不是这样操作的:
                middle  =  (left  +  right)  /  2;

                if  (array[middle]  >  v)
                {
                        right  =  middle  -  1;
                }
                else  if  (array[middle]  <  v)
                {
                        left  =  middle  +  1;
                }
                else
                {
                        return  middle;
                }
当array[middle] > v条件满足时, 此时v如果存在的话必然在左闭右开区间[left, middle)中, 因此,当这个条件满足时, right应该为middle, 而在这里, right赋值为middle - 1了, 那么, 就有可能遗漏array[middle - 1] = v的情况.

因此,这种错误的写法并不是在所有的情况下都会出错,有时还是可以找到正确的结果的.

这是一种典型的二分查找算法写错的情况,循环体是左闭右开区间,而循环体内部却是采用左闭右闭区间的算法进行操作.
下面给出的两种正确的算法,算法search是左闭右闭区间算法,而算法search2是左闭右开区间算法,可以对比一下差异.
int  search( int  array[],  int  n,  int  v)
{
        int  left,  right,  middle;

        left  =  0,  right  =  n  -  1;

        while  (left  <=  right)
        {
                middle  =  (left  +  right)  /  2;
                if  (array[middle]  >  v)
                {
                        right  =  middle  -  1;
                }
                else  if  (array[middle]  <  v)
                {
                        left  =  middle  +  1;
                }
                else
                {
                        return  middle;
                }
        }

        return  -1;
}

int  search2( int  array[],  int  n,  int  v)
{
        int  left,  right,  middle;

        left  =  0,  right  =  n;

        while  (left  <  right)
        {
                middle  =  (left  +  right)  /  2;

                if  (array[middle]  >  v)
                {
                        right  =  middle;
                }
                else  if  (array[middle]  <  v)
                {
                        left  =  middle  +  1;
                }
                else
                {
                        return  middle;
                }
        }

        return  -1;
}

下面再给出另一种典型的错误的二分查找算法,当查找的元素不在序列内时,它可能造成程序的死循环.
int  search( int  array[],  int  n,  int  v)
{
        int  left,  right,  middle;

        left  =  0,  right  =  n  -  1;

        while  (left  <=  right)
        {
                middle  =  (left  +  right)  /  2;
                if  (array[middle]  >  v)
                {
                        right  =  middle;
                }
                else  if  (array[middle]  <  v)
                {
                        left  =  middle;
                }
                else
                {
                        return  middle;
                }
        }

        return  -1;
}
为什么会造成死循环?

从循环条件来看,这个算法的操作区间是左闭右闭区间的,因此当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;

}

你可能感兴趣的:(关于二分搜索的一点思考)