二分查找算法分析

二分查找算法的思想很简单,《编程珠玑》中的描述: 在一个包含t的数组内,二分查找通过对范围的跟综来解决问题。开始时,范围就是整个数组。通过将范围中间的元素与t比较并丢弃一半范围,范围就被缩小。这个过程一直持续,直到在t被发现,或者那个能够包含t的范围已成为空。

        Donald Knuth在他的《Sorting and Searching》一书中指出,尽管第一个二分查找算法早在1946年就被发表,但第一个没有bug的二分查找算法却是在12年后才被发表出来。其中常见的一个bug是对中间值下标的计算,如果写成(low+high)/2,当low+high很大时可能会溢出,从而导致数组访问出错。改进的方法是将计算方式写成如下形式:low+ ( (high-low) >>1)即可。下面给出修改后的算法代码:

  1. int binarysearch1(int a[],int n,int x)  
  2. {  
  3.     int l,u,m;  
  4.     l=0;u=n;  
  5.     while(l<u)  
  6.     {  
  7.         m=l+((u-l)>>1);  
  8.         if(x<a[m])  
  9.             u=m;  
  10.         else if(x==a[m])  
  11.             return m;  
  12.         else  
  13.             l=m+1;  
  14.     }  
  15.     return -1;  
  16. }  

       这里注意一点,由于使用的是不对称区间,所以下标的调整看上去有点不规整。一个是u=m,另一个是l=m+1。其实很好理解,调整前区间的形式应该是 [ )的形式,如果中间值比查找值小,那么调整的是左边界,也就是闭的部分,所以加1;否则,调整是右边界,是开的部分,所以不用减1。调整后仍是[ )的形式。当然也可以写成对称的形式。代码如下:

  1. int binarysearch1(int a[],int n,int x)  
  2. {  
  3.     int l,u,m;  
  4.     l=0;u=n-1;  
  5.     while(l<=u)  
  6.     {  
  7.         m=l+((u-l)>>1);  
  8.         if(x<a[m])  
  9.             u=m-1;  
  10.         else if(x==a[m])  
  11.             return m;  
  12.         else  
  13.             l=m+1;  
  14.     }  
  15.     return -1;  
  16. }  

       这样也看上去比较规整,但是有个不足。如果想把程序改成“纯指针”的形式,就会有麻烦。修改成纯指针的代码如下:

  1. int binarysearch2(int *a,int n,int x)  
  2. {  
  3.     int *l,*u,*m;  
  4.     l=a;u=a+n-1;  
  5.     while(l<=u)  
  6.     {  
  7.         m=l+((u-l)>>1);  
  8.         if(x<*m)  
  9.             u=m-1;  
  10.         else if(x==*m)  
  11.             return m-a;  
  12.         else  
  13.             l=m+1;  
  14.     }  
  15.     return -1;  
  16. }  

       当n为0时,会引用无效地址。而用非对称区间则不会有这个问题。代码如下:

  1. int binarysearch2(int *a,int n,int x)  
  2. {  
  3.     int *l,*u,*m;  
  4.     l=a;u=a+n;  
  5.     while(l<u)  
  6.     {  
  7.         m=l+((u-l)>>1);  
  8.         if(x<*m)  
  9.             u=m;  
  10.         else if(x==*m)  
  11.             return m-a;  
  12.         else  
  13.             l=m+1;  
  14.     }  
  15.     return -1;  
  16. }  

       上面给出的二分查找是迭代法实现,当然也可以用递归的方式实现。代码如下:

  1. int binarysearch3(int a[],int l,int u,int x)  
  2.   
  3. int m=l+((u-l)>>1);  
  4. if(l<=u)  
  5. {  
  6.     if(x<a[m])  
  7.         return binarysearch3(a,l,m-1,x);  
  8.     else if(x==a[m])  
  9.         return m;  
  10.     else   
  11.         return binarysearch3(a,m+1,u,x);  
  12. }  
  13. return -1;  
  14.     

       上述这些二分算法,若数组元素重复,返回的是重复元素的某一个元素。如果希望返回被查找元素第一次出现的位置,则需要修改代码。下面给出了一种解法:

  1. int binarysearch4(int a[],int n,int x)  
  2. {  
  3.     int l,u,m;  
  4.     int flag=-1;  
  5.     l=0;u=n;  
  6.     while(l<u)  
  7.     {  
  8.         m=l+((u-l)>>1);  
  9.         if(x<a[m])  
  10.             u=m;  
  11.         else if(x==a[m])  
  12.             flag=u=m;  
  13.         else  
  14.             l=m+1;  
  15.     }  
  16.     return flag;  
  17. }  

       下面是《编程珠玑》上的解法:

  1. int binarysearch4(int a[],int n,int x)  
  2. {  
  3.     int l,u,m;    
  4.     l=-1;u=n;  
  5.     while(l+1<u)  
  6.     {  
  7.         m=l+((u-l)>>1);  
  8.         if(a[m]<x)  
  9.             l=m;  
  10.         else  
  11.             u=m;  
  12.     }  
  13.     return (u>=n||a[u]!=x)?-1:u;  
  14. }  

        至此二分算法的代码讨论结束,下面讨论一下程序的测试问题。《代码之美》有一章专门介绍二分查找算法的测试,非常漂亮。这里班门弄斧,简单给出几个测试用例。针对binarysearch1。测试程序如下:

  1. #include <iostream>  
  2. #include <cassert>  
  3. #include <algorithm>  
  4. #include <ctime>  
  5. using namespace std;  
  6.   
  7. int calmid(int l,int u) {  return l+((u-l)>>1);  }  
  8. int binarysearch1(int a[],int n,int x);  
  9.   
  10. #define bs1 binarysearch1  
  11.   
  12. int main()  
  13. {  
  14.     long start,end;  
  15.     start=clock();    
  16.   
  17.     int a[9]={-2147483648,-13,-10,-5,-3,0,1,400,2147483647};  
  18.     //中值下标计算的测试  
  19.     assert(calmid(0,1)==0);  
  20.     assert(calmid(0,2)==1);  
  21.     assert(calmid(1000000,2000000)==1500000);  
  22.     assert(calmid(2147483646,2147483647)==2147483646);  
  23.     assert(calmid(2147483645,2147483647)==2147483646);  
  24.   
  25.     //冒烟测试  
  26.     assert(bs1(a,9,0)==5);  
  27.     assert(bs1(a,9,1)==6);  
  28.     assert(bs1(a,9,2)==-1);  
  29.   
  30.     //边界测试  
  31.     assert(bs1(a,0,1)==-1);                            //0个元素  
  32.     assert(bs1(a,1,-2147483648)==0);       //1个元素 成功  
  33.     assert(bs1(a,1,-2147483647)==-1);      //1个元素 失败  
  34.   
  35.     assert(bs1(a,9,-2147483648)==0);       //首个元素  
  36.     assert(bs1(a,9,-3)==4);                           //中间元素  
  37.     assert(bs1(a,9,2147483647)==8);        //末尾元素  
  38.   
  39.     //自动化测试  
  40.     int b[10000];  
  41.     int i,j;  
  42.     for(i=0;i<10000;i++)  
  43.     {  
  44.         b[i]=i*10;  
  45.         for(j=0;j<=i;j++)  
  46.         {  
  47.             assert(bs1(b,i+1,j*10)==j);  
  48.             assert(bs1(b,i+1,j*10-5)==-1);  
  49.         }  
  50.     }  
  51.   
  52.     //自动化测试 引入随机数  
  53.     srand(time(0));  
  54.     for(i=0;i<10000;i++)  
  55.     {  
  56.         b[i]=rand()%1000000;  
  57.         sort(&b[0],&b[i]);  
  58.         for(j=0;j<=i;j++)  
  59.         {  
  60.             int x=rand();  
  61.             int k=bs1(b,i+1,x);  
  62.             if(k!=-1)  
  63.                 assert(b[k]==x);  
  64.         }  
  65.     }  
  66.   
  67.     end=clock();  
  68.     cout<<(end-start)/1000.0<<'s'<<endl;  
  69.     return 0;  
  70. }  

       注意到数组的元素有正数,负数,零,最大值,最小值。通常会忘掉负数的测试,引入最大值和最小值,主要是为了边界测试。

       第一,测试了中值下标的计算。另外写了一个小函数,单独测试。考虑到内存可能放不下这么大的数组,因此只是模拟测试,并没有真正申请这么大的空间,但是对于中值下标的测试足够了。

       第二,冒烟测试。即做一些最基本的测试。测试通过后进行边界测试。

       第三,边界测试。这里有三种类型,一是针对数组元素个数,分别是0个,1个。二是针对元素位置,分别是首个元素,中间元素,末尾元素。三是针对元素值,有最大值,最小值,0等测试。

       第四,自动化测试。这里自动生成测试的数组,然后针对每个元素进行成功查找测试。

       第五,自动化测试,只不过数组的元素是随机值。

       第五,性能测试。这里相关代码没有列出。以上测试都通过时,可以修改查找算法,添加性能测试的代码。其实可以简单添加一个比较的计数器。返回值从原来的查找结果改为比较的计数器值即可。代码比较简单,就不列了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值