二分查找之謎題

1 前言

二分查找本身是個簡單的算法,但是正是因為其簡單,更容易寫錯。甚至于在二分查找算法剛出現(xiàn)的時候,也是存在bug的(溢出的bug),這個bug直到幾十年后才修復(見《編程珠璣》)。本文打算對二分查找算法進行總結,并對由二分查找引申出來的問題進行分析和匯總。若有錯誤,請指正。

2 二分查找是這樣的

相信大家都知道二分查找的基本算法,如下所示,這就是二分查找算法代碼:

int bisearch(int a[], int n, int t)  //數(shù)組a有序,長度為n, 待查找的值為t
{
    int l = 0, u = n - 1;
    while (l <= u) {
        int m = l + (u - l) / 2; //同(l+u)/ 2,這里是為了溢出
        if (t > a[m])
            l = m + 1;
        else if (t < a[m])
            u = m - 1;
        else
            return m;
    }
    return -(l+1);
}

算法的思想就是:從數(shù)組中間開始,每次排除一半的數(shù)據(jù),時間復雜度為O(lgN)。這依賴于數(shù)組有序這個性質。如果t存在數(shù)組中,則返回t在數(shù)組的位置;否則,不存在則返回-(l+1)。

這里需要解釋下為什么t不存在數(shù)組中時不是返回-1而要返回-(l+1)。首先我們可以觀察l的值,如果查找不成功,則l的值恰好是t應該在數(shù)組中插入的位置。

舉個例子,假定有序數(shù)組a={1, 3, 4, 7, 8}, 那么如果t=0,則顯然t不在數(shù)組中,則二分查找算法最終會使得l=0 > u=-1退出循環(huán);如果t=9,則t也不在數(shù)組中,則最后l=5 > u=4退出循環(huán)。如果t=5,則最后l=3 > u=2退出循環(huán)。因此在一些算法中,比如DHT(一致性哈希)中,就需要這個返回值來使得新加入的節(jié)點可以插入到合適的位置中,在求最長遞增子序列的NlgN算法中,也用到了這一點,參見博文最長遞增子序列算法。

還有一個小點就是之所以返回-(l+1)而不是直接返回-l是因為l可能為0,如果直接返回-l就無法判斷是正常返回位置0還是查找不成功返回的0。

3 二分查找數(shù)字第一次出現(xiàn)的位置

現(xiàn)在考慮一個稍微復雜點的問題,如果有序數(shù)組中有重復數(shù)字,比如數(shù)組a={1, 2, 3, 3, 5, 7, 8},需要在其中找出3第一次出現(xiàn)的位置。這里3第一次出現(xiàn)位置為2。這個問題在《編程珠璣》第九章有很好的分析,這里就直接用了。算法的精髓在于循環(huán)不變式的巧妙設計,代碼如下:

int bsearch_first(int a[], int n, int t)
{
    int l = -1, u = n;
    while (l + 1 != u) {
        /*循環(huán)不變式a[l]<t<=a[u] && l<u*/
        int m = l + (u - l) / 2; //同(l+u)/ 2
        if (t > a[m])
            l = m;
        else
            u = m;
    }
    /*assert: l+1=u && a[l]<t<=a[u]*/
    int p = u;
    if (p>=n || a[p]!=t)
        p = -1;
    return p;
}

算法分析:設定兩個不存在的元素a[-1]和a[n],使得a[-1] < t <= a[n],但是我們并不會去訪問者兩個元素,因為(l+u)/2 > l=-1, (l+u)/2 < u=n。循環(huán)不變式為l<u && t>a[l] && t<=a[u] 。循環(huán)退出時必然有l+1=u, 而且a[l] < t <= a[u]。循環(huán)退出后u的值為t可能出現(xiàn)的位置,其范圍為[0, n],如果t在數(shù)組中,則第一個出現(xiàn)的位置p=u,如果不在,則設置p=-1返回。該算法的效率雖然解決了更為復雜的問題,但是其效率比初始版本的二分查找還要高,因為它在每次循環(huán)中只需要比較一次,前一程序則通常需要比較兩次。

舉個例子:對于數(shù)組a={1, 2, 3, 3, 5, 7, 8},我們如果查找t=3,則可以得到p=u=2,如果查找t=4,a[3]<t<=a[4], 所以p=u=4,判斷a[4] != t,所以設置p=-1。 一種例外情況是u>=n, 比如t=9,則u=7,此時也是設置p=-1.特別注意的是,l=-1,u=n這兩個值不能寫成l=0,u=n-1。雖然這兩個值不會訪問到,但是如果改成后面的那樣,就會導致二分查找失敗,那樣就訪問不到第一個數(shù)字。如在a={1,2,3,4,5}中查找1,如果初始設置l=0,u=n-1,則會導致查找失敗。

擴展
如果要查找數(shù)字在數(shù)組中最后出現(xiàn)的位置呢?其實這跟上述算法是類似的,稍微改一下上面的算法就可以了,代碼如下:

int bsearch_last(int a[], int n, int t)
{
    int l = -1, u = n;
    while (l + 1 != u) {
        /*循環(huán)不變式, a[l] <= t < a[u]*/
        int m = l + (u - l) / 2;
        if (t >= a[m])
            l = m;
        else
            u = m;
    }
    /*assert: l+1 = u && a[l] <= t < a[u]*/
    int p = l;
    if (p<=-1 || a[p]!=t)
        p = -1;
    return p;
}

當然還有一種方法可以將查詢數(shù)字第一次出現(xiàn)和最后一次出現(xiàn)的代碼寫在一個程序中,只需要對原始的二分查找稍微修改即可,代碼如下:

int binary_search_first_last(int arr[], int p, int q, int value, bool firstflag = true)
{
    int begin = p;
    int end = q;
    while(begin <= end)
    {
        int mid = (begin + end)/2;
        if(arr[mid] == value) //找到了,判斷是第一次出現(xiàn)還是最后一次出現(xiàn)
        {
            if(firstflag) //查詢第一次出現(xiàn)的位置
            {
                if(mid != p && arr[mid-1] != value)
                    return mid;
                else if(mid == p)
                    return p;
                else
                    end = mid - 1;
            }
            else    //查詢最后一次出現(xiàn)的位置
            {
                if(mid != q && arr[mid+1] != value)
                    return mid;
                else if(mid == q)
                    return q;
                else
                    begin = mid + 1;
            }
        }
        else if(arr[mid] < value)
            begin = mid + 1;
        else  
            end = mid - 1;
  }  
    return -1;
}  

4 旋轉數(shù)組元素查找問題

題目

把一個有序數(shù)組最開始的若干個元素搬到數(shù)組的末尾,我們稱之為數(shù)組的旋轉。例如數(shù)組{3, 4, 5, 1, 2}為{1, 2, 3, 4, 5}的一個旋轉?,F(xiàn)在給出旋轉后的數(shù)組和一個數(shù),旋轉了多少位不知道,要求給出一個算法,算出給出的數(shù)在該數(shù)組中的下標,如果沒有找到這個數(shù),則返回-1。要求查找次數(shù)不能超過n。

分析

由題目可以知道,旋轉后的數(shù)組雖然整體無序了,但是其前后兩部分是部分有序的。由此還是可以使用二分查找來解決該問題的。

解法一::2次二分查找。
首先確定數(shù)組分割點,也就是說分割點兩邊的數(shù)組都有序。比如例子中的數(shù)組以位置2分割,前面部分{3,4,5}有序,后半部分{1,2}有序。然后對這兩部分分別使用二分查找即可。代碼如下:

int split(int a[], int n)
{
    for (int i=0; i<n-1; i++) {
        if (a[i+1] < a[i])
            return i;
    }
    return -1;
}

int bsearch_rotate(int a[], int n, int t)
{
    int p = split(a, n); //找到分割位置
    if (p == -1)
        return bsearch_first(a, n, t); //如果原數(shù)組有序,則直接二分查找即可
    else {
        int left = bsearch_first(a, p+1, t); //查找左半部分
        if (left == -1) {  //左半部分沒有找到,則查找右半部分
            int right = bsearch_first(a+p+1, n-p-1, t); //查找右半部分
            if (right != -1) return right+p+1;  //返回位置,注意要加上p+1
            return -1;
        }
        return left; //左半部分找到,則直接返回
    }
}

解法二:1次二分查找。
二分查找算法有兩個關鍵點:1)數(shù)組有序;2)根據(jù)當前區(qū)間的中間元素與x的大小關系,確定下次二分查找在前半段區(qū)間還是后半段區(qū)間進行。

仔細分析該問題,可以發(fā)現(xiàn),每次根據(jù)low和high求出mid后,mid左邊([low, mid])和右邊([mid, high])至少一個是有序的。a[mid]分別與a[left]和a[right]比較,確定哪一段是有序的。

  • 如果左邊是有序的,若x<a[mid]且x>a[left], 則right=mid-1;其他情況,left =mid+1;
  • 如果右邊是有序的,若x> a[mid] 且x<a[right] 則left=mid+1;其他情況,right =mid-1;
    代碼如下:
int bsearch_rotate(int a[], int n, int t)
{
    int low = 0, high = n-1;
    while (low <= high) {
        int mid = low + (high-low) / 2;
        if (t == a[mid])
            return mid;
        if (a[mid] >= a[low]) { //數(shù)組左半有序
            if (t >= a[low] && t < a[mid])
                high = mid - 1;
            else
                low = mid + 1;
        } else {       //數(shù)組右半段有序
            if (t > a[mid] && t <= a[high])
                low = mid + 1;
            else
                high = mid - 1;
        }   
    }   
    return -1; 
}

5 參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 1、用C語言實現(xiàn)一個revert函數(shù),它的功能是將輸入的字符串在原串上倒序后返回。 2、用C語言實現(xiàn)函數(shù)void ...
    希崽家的小哲閱讀 6,705評論 0 12
  • 原文出處:http://www.cnblogs.com/maybe2030/p/4715035.html引文出處:...
    明教de教主閱讀 9,323評論 0 7
  • 分治策略 本文包括分治的基本概念二分查找快速排序歸并排序找出偽幣棋盤覆蓋最大子數(shù)組 源碼鏈接:https://gi...
    廖少少閱讀 2,003評論 0 7
  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經(jīng)驗。 張土汪:刷leetcod...
    土汪閱讀 12,932評論 0 33
  • (打油詩) 兒女斷奶后, 媽媽更辛苦。 上午打麻將, 下午斗地主。 晚上聊微信, 家務誰做主。 天天方便面, 口口...
    噴泉閱讀 217評論 2 7

友情鏈接更多精彩內容