二分查找的确是个很不错的算法,虽然简单,但是边界容易出错。
书中最初的二分查找l和u(我的代码中是r)对应数组中最小下标和最大下标,即l=0,u=n-1,数组为arr[l..u]。而后面优化的代码中l和u就变成了最小下标-1和最大下标+1了,即l=-1,u=n,数组为arr[l+1...u-1]。不太明白其中深刻含义,我试了一下l=0,u=n(Python中范围都是这么表示),发现第二段代码(即找出t的第一处位置)实现不出来,用原来的代码在数组只剩下1个或2个元素的时候会出现死循环。
所以,我索性用最原始的表示方法实现了后面几个优化代码,并做了个时间测试程序(脚手架)来看运行时间。
原二分查找:
1 int bsearch(int *arr,int t,int l,int r) 2 { 3 int m; 4 while(l<=r)//要探测的部分至少有1个元素,l==r的时候仅有1个元素 5 { 6 m=(l+r)/2; 7 if(t>arr[m]) 8 l=m+1; 9 else if(t<arr[m]) 10 r=m-1; 11 else 12 return m;//发现t后马上返回结果 13 } 14 return -1; 15 }
改进的二分查找(找出t的在数组中的最小下标)
1 int bsearch1(int *arr,int t,int l,int r) 2 { 3 int m; 4 while(l<r)//要探测的部分至少有2个元素,必须要l<r而不是l<=r,否则会无限循环 5 { 6 m=(l+r)/2; 7 if(t>arr[m]) 8 l=m+1; 9 else 10 r=m; 11 } 12 if(arr[l]==t)//检查最后剩余的1个元素 13 return l; 14 else 15 return -1; 16 }
用l和i表示范围的二分查找(同样是找出t的在数组中的最小下标)
1 int bsearch2(int *arr,int t,int l,int r)//已知数组有1000个元素 2 { 3 int i=512; 4 //根据下面的判断,将数组范围变成arr[0..511]或arr[488..999] 5 //长度均为512(2的整数次幂) 6 if(t>=arr[512]) 7 l=1000-512; 8 while(i>1)//探测部分仅剩1个元素的时候停止,想想前一步:探测部分仅有2个元素 9 { 10 i/=2; 11 if(t>arr[l+i-1])//l+i-1相当于m(下取整的中点) 12 l+=i; 13 } 14 if(arr[l]==t)//检查最后剩余的1个元素 15 return l; 16 else 17 return -1; 18 }
上一个函数循环展开(同样是找出t的在数组中的最小下标)
1 int bsearch3(int *arr,int t,int l,int r) 2 { 3 if(t>=arr[512]) 4 l=1000-512; 5 if(t>arr[l+255]) l+=256; 6 if(t>arr[l+127]) l+=128; 7 if(t>arr[l+63]) l+=64; 8 if(t>arr[l+31]) l+=32; 9 if(t>arr[l+15]) l+=16; 10 if(t>arr[l+7]) l+=8; 11 if(t>arr[l+3]) l+=4; 12 if(t>arr[l+1]) l+=2; 13 if(t>arr[l]) l+=1; 14 if(t==arr[l]) 15 return l; 16 else 17 return -1; 18 }
时间测试程序
1 int main() 2 { 3 int arr[1000]; 4 for(int i=0;i<1000;++i) 5 arr[i]=i; 6 int (*func[])(int *,int,int,int)={bsearch,bsearch1,bsearch2,bsearch3};//函数数组 7 for(int i=0;i<4;++i) 8 { 9 clock_t begin=clock(); 10 int count=5000; 11 while(count--) 12 for(int t=-1;t<=1000;++t) 13 { 14 int res=func[i](arr,t,0,999); 15 if(t==1000&&res!=-1||t!=1000&&res!=t) 16 cout<<i<<" "<<t<<endl; 17 } 18 clock_t use=clock()-begin; 19 cout<<"bsearch"<<i<<" total clock:"<<use<<endl; 20 } 21 return 0; 22 }
测试结果显示速度上:bsearch3>bsearch2>bsearch>bsearch1。
但是这个测试结果并不具有普适性,因为速度较快的bsearch3和bsearch2都已知数组长度为1000,而最初的bsearch没有这个限制。
于是,我想通过修改程序让bsearch2也具有普适性。最初修改的版本
1 int bsearch2(int *arr,int t,int l,int r) 2 { 3 int i;//下面的循环和之后的移位,目的是找到不大于数组长度的最大的2的次幂 4 for(i=1;r-l+1>=i;i<<=1); 5 i>>=1; 6 if(t>=arr[i]) 7 l=r-l+1-i; 8 while(i>1)//探测部分仅剩1个元素的时候停止,想想前一步:探测部分仅有2个元素 9 { 10 i/=2; 11 if(t>arr[l+i-1])//l+i-1相当于m(下取整的中点) 12 l+=i; 13 } 14 if(arr[l]==t)//检查最后剩余的1个元素 15 return l; 16 else 17 return -1; 18 }
修改之后测试速度:bsearch3>bsearch>bsearch1>bsearch2。
尼玛,变成最慢的了。。。
问题的症结应该是如何更高效的找到不大于n(数组长度)的2的最大次幂。网上找到一个不错的方法http://www.cnblogs.com/chaosz/archive/2012/09/02.html
它是找不小于a的2的最小次幂。我在它的基础上稍作修改
1 int bsearch2(int *arr,int t,int l,int r) 2 { 3 int i=r-l+1; 4 i|=i>>1; 5 i|=i>>2; 6 i|=i>>4; 7 i|=i>>8; 8 i|=i>>16; 9 i=(i>>1)+1; 10 if(t>=arr[i]) 11 l=r+1-i; 12 while(i>1)//探测部分仅剩1个元素的时候停止,想想前一步:探测部分仅有2个元素 13 { 14 i/=2; 15 if(t>arr[l+i-1])//l+i-1相当于m(下取整的中点) 16 l+=i; 17 } 18 if(arr[l]==t)//检查最后剩余的1个元素 19 return l; 20 else 21 return -1; 22 }
修改之后测试速度:bsearch3>bsearch>bsearch2>bsearch1。
看来普适之后,bsearch2对阵不过bsearch。
综上,在[1]数组大小不确定,[2]不要求找到t的下标最小的位置,的条件下,还是原bsearch速度最快、最好理解。