二分查找(Binary Search)


1.遞歸實現

int binarySearchRecursive(int a[],int low,int high,int key){
    if(low>high)
        return -(low+1);

    int mid=low+(high-low)/2;
    if(key<a[mid])
        return binarySearchRecursive(a,low,mid-1,key);
    else if(key > a[mid])
        return binarySearchRecursive(a,mid+1,high,key);
    else
        return mid;

}

int binarySearchRecursive(int a[],int n,int key){
    return binarySearchRecursive(a,0,n-1,key);
}

 

2.非遞歸實現

int binarySearch(int a[],int n,int key){
    int low=0,high=n-1;
    int mid;
    while(low<=high){
        mid=low+(high-low)/2;
        if(key<a[mid])
            high=mid-1;
        else if(key>a[mid])
            low=mid+1;
        else
            return mid;
    }
    return -(low+1);
}

 

3.二分查找的一種版本,現實需求中可能需要查找“在不破壞有序的狀態的原則下,可插入key的第一個位置”。

/*返回遞增數組中第一個不小於key的數的索引,即在不破壞排序狀態的原則下,可插入key的第一個位置。
算法思想:循環不變式為a[low]<key&&a[high]>=key,
所以當low+1==high && high<n時,high就應該是第一個大於等於key的數的索引;
但是當high==n,這時就可以判斷數組中沒有大於等於key的值,則插入位置為high,返回-(high+1);*/
int lowerBound(int a[],int n,int key){
    int low=-1,high=n;//假設n>=0, a[-1]<key&&a[n]>=key(但是程序並不訪問這兩個假想的元素)
    int mid;
    while(low+1!=high){
        mid=low+(high-low)/2;
        if(a[mid]<key)
            low=mid;//修正low,確保滿足循環不變式中a[low]<key
        else
            high=mid;//修正high,確保滿足循環不變式中a[high]>=key
    }
    int index=high;//第一個大於等於key的數的索引

    /*判斷第一個大於等於key的數a[index]是否存在數組中*/
    if(index>=n||a[index]!=key)//不存在
        index=-(high+1);//修正index為負的插入位置的后一個位置

    return index;
}

 

4.二分查找的一種版本,現實需求中可能需要查找“在不破壞有序的狀態的原則下,可插入key的最后一個位置”。

/*upperBound試圖在已排序數組中尋找可插入key的最后一個合適的位置。
算法思想:循環不變式為a[low]<=key&&a[high]>key,
所以當low+1==high && low>=0時,low就應該是第一個大於key的數的索引;
但是當low<0,這時就可以判斷數組中沒有小於等於key的值,則插入位置為low+1,返回-(low+1+1)*/
int upperBound(int a[],int n,int key){
    int low=-1,high=n;//假設n>=0, a[-1]<=key&&a[n]>key(但是程序並不訪問這兩個假想的元素)
    int mid;
    while(low+1!=high){
        mid=low+(high-low)/2;
        if(a[mid]<=key)
            low=mid;//修正low,確保滿足循環不變式中a[low]<=key
        else
            high=mid;//修正high,確保滿足循環不變式中a[high]>key
    }
    int index=low+1;//第一個大於key的數的索引

    /*判斷最后一個小於等於key的數a[low]是否存在數組中*/
    if(low<=-1||a[low]!=key)//不存在
        index=-(low+1+1);//修正index為負的插入位置的后一個位置

    return index;
}

 

幸運的是,STL在<algorithm>中實現了這些算法的泛型版本。對應的函數分別為:binary_search, lower_bound, upper_bound

其內部實現非常精妙,詳見侯捷的《STL 源碼剖析》。當然思想與上文實現大同小異,但是速度方面有待驗證。

此外,C庫函數也提供了void* bsearch(const void *key, const void *base, size_t n, size_t size, int (*com) (const void *first, const void *second) )。

下面給出以上函數的例子:

#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<ctime>
using namespace std;

//print a array with macro
#define printArray(arr,n) for(int i=0;i<n;i++){\
        cout<<a[i]<<' ';\
    }\
    cout<<endl<<endl;\


//compare function greater
int greater(const void *first,const void *second){
    int _first=*static_cast<const int*>(first);
    int _second=*static_cast<const int*>(second);
    if(_first>_second)
        return 1;
    else if(_first<_second)
        return -1;
    else
        return 0;
}


int main()
{
    srand(time(0));
    const int n=10;
    int a[n];
    for(int i=0;i<n;i++){
        a[i]=rand()%20;
    }
    printArray(a,n);

    sort(a,a+n);
    printArray(a,n);

    int b;
    b=rand()%20;
     //b=a[0];//test 1
    // b=a[n-1];//2
    printf("searching %d ...\n",b);
    bool found=binary_search(a,a+n,b);
    if(found){//found
        cout<<"found"<<endl;
    }
    else{
        cout<<"no found"<<endl;
    }

    cout<<"\nbsearch"<<endl;
    void* p=bsearch(&b,a,n,sizeof(int),::greater);
    if(p!=NULL){//found
        cout<<"found "<<*static_cast<int*>(p)<<endl;
    }
    else{
        cout<<"no found"<<endl;
    }

    cout<<"\nbinarySearchRecursive"<<endl;
    int index=binarySearchRecursive(a,n,b);
    if(index>=0){
        cout<<"found! And index="<<index<<endl;
    }
    else{
        cout<<"no found! But "<<b<<" should be insert at "<<-index-1<<endl;
    }


    cout<<"\nbinarySearch"<<endl;
    index=binarySearch(a,n,b);
    if(index>=0){
        cout<<"found! index="<<index<<endl;
    }
    else{
        cout<<"no found! But "<<b<<" should be insert at "<<-index-1<<endl;
    }

    int* bound=NULL;
    bound=lower_bound(a,a+n,b);
    cout<<"lower bound: ";
    if(bound!=NULL){//exist
        cout<<*bound;
    }
    else{
        cout<<"no exist";
    }
    cout<<endl;

    bound=upper_bound(a,a+n,b);
    cout<<"upper bound: ";

    if(bound!=NULL&&bound<a+n){//exist. 
        /*這里需要檢查bound是否在數組中,因為最大值的upper_bound返回其下一個位置,即a+n。
        而lower_bound一定指向數組外面。*/

        cout<<*bound;
    }
    else{
        cout<<"no exist";
    }
    cout<<endl;

    cout<<"\nlowerBound"<<endl;
    int lIndex=lowerBound(a,n,b);
    if(lIndex>=0){
        cout<<"found! The index of lower bound is "<<lIndex<<endl;
    }
    else{
        cout<<"no found! The index of lower bound is "<<-lIndex-1<<endl;
    }


    cout<<"\nupperBound"<<endl;
    int uIndex=upperBound(a,n,b);
    if(uIndex>=0){
        cout<<"found! The index of upper bound is "<<uIndex<<endl;
    }
    else{
        cout<<"no found! The index of upper bound is "<<-uIndex-1<<endl;
    }
}

 

那么這些算法的執行時間又是咋樣的呢?

我分別針對數組長度和選中概率(即,數組中數據被選中的概率)做了如下實驗:

//生成超過RAND_MAX的大隨機數
long bigRand(){
    return rand()*RAND_MAX+rand();
}

int main()
{
    srand(time(0));
    //生成測試數組
    const int n=10000000;
    long* a=new long[n];
    for(int i=0;i<n;i++){
        a[i]=bigRand()%(n*4);//設定選中概率
    }
    
    //生成查找的數值
    const int times=10000;//測試次數
    long b[times];
    for(int i=0;i<times;i++){
        b[i]=bigRand()%(n*4);
    }

    clock_t start,end;

    //start=clock();
    sort(a,a+n);
    //end=clock();
    //printf("sort eclipse time: %.2f ms\n",double(end-start)*1000/CLOCKS_PER_SEC);
    

    start=clock();
    for(int i=0;i<times;i++){
        binarySearchRecursive(a,n,b[i]);
    }
    end=clock();
    printf("%-30s: %.2f ms\n","binarySearchRecursive",double(end-start)*1000/CLOCKS_PER_SEC);


    start=clock();
    for(int i=0;i<times;i++){
        binarySearch(a,n,b[i]);
    }
    end=clock();
    printf("%-30s: %.2f ms\n","binarySearch",double(end-start)*1000/CLOCKS_PER_SEC);

    //vector<int> vec(a,a+n);

    start=clock();
    for(int i=0;i<times;i++){
        //binary_search(vec.begin(),vec.end(),b[i]);
        lower_bound(a,a+n,b[i]);
    }
    end=clock();
    printf("%-30s: %.2f ms\n","binary_search",double(end-start)*1000/CLOCKS_PER_SEC);


    start=clock();
    for(int i=0;i<times;i++){
        bsearch(&b[i],a,n,sizeof(int),::greater);
    }
    end=clock();
    printf("%-30s: %.2f ms\n","bsearch",double(end-start)*1000/CLOCKS_PER_SEC);


    delete []a;
}

Debug模式

 選中概率為0.5

選中概率0.5
  執行1000次的總時間(ms)
數組長度 binarySearchRecursive binarySearch lower_bound bsearch
10000000 13 5 28 12
1000000 10 3 22 9
100000 6 2 18 6
10000 6 2 15 5

 

 

 

 

 

  

 

選中概率為0.25

選中概率0.25
  執行1000次的總時間(ms)
數組長度 binarySearchRecursive binarySearch lower_bound bsearch
10000000 15 5 28 13
1000000 10 3 21 8
100000 7 3 18 7
10000 5 2 15 5

 

 

 

 

 

 

 選中概率為0.75的執行時間相似,這里就省略了。

對比以上實驗結果,可知Debug模式下binarySearch的執行效率最高,STL中以lower_bound為代表三算法效率最低。可能STL的算法沒有被優化調用。

由於實驗的時候忘記調成release模式,差點得出了相反的結果。

 

Release模式下binarySearchRecursive,binarySearch,lower_bound全為0,bsearch花費最多時間,可見STL三算法得到了優化調用,而因為正如《編程珠璣》中所述,C庫函數的通用接口開銷很大。

綜上,沒有特殊需求,二分搜索可以直接使用STL中lower_bound,upper_bound,binary_search函數。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM