二分查找

由一道題目引出的:

題目描述程序員

給定一個有序的數組,查找某個數是否在數組中,請編程實現。算法

分析與解法編程

一看到數組自己已經有序,我想你可能反應出了要用二分查找,畢竟二分查找的適用條件就是有序的。那什麼是二分查找呢?小程序

二分查找能夠解決(預排序數組的查找)問題:只要數組中包含T(即要查找的值),那麼經過不斷縮小包含T的範圍,最終就能夠找到它。其算法流程以下:數組

一開始,範圍覆蓋整個數組。
將數組的中間項與T進行比較,若是T比數組的中間項要小,則到數組的前半部分繼續查找,反之,則到數組的後半部分繼續查找。
如此,每次查找能夠排除一半元素,範圍縮小一半。就這樣反覆比較,反覆縮小範圍,最終就會在數組中找到T,或者肯定原覺得T所在的範圍實際爲空。
對於包含N個元素的表,整個查找過程大約要通過log(2)N次比較。編輯器

此時,可能有很多讀者內心嘀咕,不就二分查找麼,太簡單了。ide

然《編程珠璣》的做者Jon Bentley曾在貝爾實驗室作過一個實驗,即給一些專業的程序員幾個小時的時間,用任何一種語言編寫二分查找程序(寫出高級僞代碼也能夠),結果參與編寫的一百多人中:90%的程序員寫的程序中有bug(我並不認爲沒有bug的代碼就正確)。設計

也就是說:在足夠的時間內,只有大約10%的專業程序員能夠把這個小程序寫對。但寫不對這個小程序的還不止這些人:並且高德納在《計算機程序設計的藝術 第3卷 排序和查找》第6.2.1節的「歷史與參考文獻」部分指出,雖然早在1946年就有人將二分查找的方法公諸於世,但直到1962年纔有人寫出沒有bug的二分查找程序。code

你能正確無誤的寫出二分查找代碼麼?不妨一試,關閉全部網頁,窗口,打開記事本,或者編輯器,或者直接在本文評論下,不參考上面我寫的或其餘任何人的程序,給本身十分鐘到N個小時不等的時間,當即編寫一個二分查找程序。排序

正文:二分查找

關於二分查找法

二分查找法主要是解決在「一堆數中找出指定的數」這類問題。

而想要應用二分查找法,這「一堆數」必須有一下特徵:(1)存儲在數組中 (2) 有序排列
因此若是是用鏈表存儲的,就沒法在其上應用二分查找法了。

至因而順序遞增排列仍是遞減排列,數組中是否存在相同的元素都沒關係。不過通常狀況,咱們仍是但願並假設數組是遞增排列,數組中的元素互不相同。

二分查找法的基本實現

二分查找法在算法家族大類中屬於「分治法」,分治法基本均可以用遞歸來實現的,二分查找法的遞歸JS實現以下:

function bsearch(array,low,high,target)
{
    if (low > high) return -1;
    var mid = Math.floor((low + high)/2);
    if (array[mid]> target){
        return  bsearch(array, low, mid -1, target);
    } else if (array[mid]< target){
        return  bsearch(array, mid+1, high, target);
    }ese{return mid;}
        
}

不過全部的遞歸均可以自行定義stack來解遞歸,因此二分查找法也能夠不用遞歸實現,並且它的非遞歸實現甚至能夠不用棧,由於二分的遞歸實際上是尾遞歸,它不關心遞歸前的全部信息。

function bsearchWithoutRecursion(array,low,high,target)
{
    while(low <= high)
    {
        var mid = Math.floor((low + high)/2);
        if (array[mid] > target){
            high = mid - 1;
        }else if (array[mid] < target){
            low = mid + 1;
        }else{
            return mid;
        }
    }
    return -1;
}

用二分查找法找尋邊界值

以前的都是在數組中找到一個數要與目標相等,若是不存在則返回-1。咱們也能夠用二分查找法找尋邊界值,也就是說在有序數組中找到「正好大於(小於)目標數」的那個數。
用數學的表述方式就是:
在集合中找到一個大於(小於)目標數t的數x,使得集合中的任意數要麼大於(小於)等於x,要麼小於(大於)等於t。

舉例來講:
給予數組和目標數
var array = {2, 3, 5, 7, 11, 13, 17};
var target = 7;
那麼上界值應該是11,由於它「剛恰好」大於7;下屆值則是5,由於它「剛恰好」小於7。

用二分查找法找尋上界

function BSearchUpperBound(array,low,high,target)
{
    if(low > high || target >= array[high]) return -1;
    
    var mid = (low + high) / 2;
    while (high > low)
    {
        if (array[mid] > target){
            high = mid;
       } else{
            low = mid + 1; 
       }
        mid = (low + high) / 2;
    }

    return mid;
}

與精確查找不一樣之處在於,精確查找分紅三類:大於,小於,等於(目標數)。而界限查找則分紅了兩類:大於和不大於。
若是當前找到的數大於目標數時,它可能就是咱們要找的數,因此須要保留這個索引,也所以if (array[mid] > target)時 high=mid; 而沒有減1。

用二分查找法找尋下界

function BSearchLowerBound(array,low,high,target)
{
    if(high < low  || target <= array[low]) return -1;
    
    var mid = (low + high + 1) / 2; //make mid lean to large side
    while (low < high)
    {
        if (array[mid] < target){
            low = mid;
        }else{
            high = mid - 1;
        }
        mid = (low + high + 1) / 2;
    }

    return mid;
}

下屆尋找基本與上屆相同,須要注意的是在取中間索引時,使用了向上取整。若同以前同樣使用向下取整,那麼當low == high-1,而array[low] 又小於 target時就會造成死循環。由於low沒法往上爬超過high。
這兩個實現都是找嚴格界限,也就是要大於或者小於。若是要找鬆散界限,也就是找到大於等於或者小於等於的值(即包含自身),只要對代碼稍做修改就行了:

去掉判斷數組邊界的等號:
target >= array[high]改成 target > array[high]
在與中間值的比較中加上等號:
array[mid] > target改成array[mid] >= target

用二分查找法找尋區域

以前咱們使用二分查找法時,都是基於數組中的元素各不相同。假如存在重複數據,而數組依然有序,那麼咱們仍是能夠用二分查找法判別目標數是否存在。不過,返回的index就只能是隨機的重複數據中的某一個。

此時,咱們會但願知道有多少個目標數存在。或者說咱們但願數組的區域。
結合前面的界限查找,咱們只要找到目標數的嚴格上屆和嚴格下屆,那麼界限之間(不包括界限)的數據就是目標數的區域了。

//return type: pair<int, int>
//the fisrt value indicate the begining of range,
//the second value indicate the end of range.
//If target is not find, (-1,-1) will be returned
pair<int, int> SearchRange(int A[], int n, int target) 
{
    pair<int, int> r(-1, -1);
    if (n <= 0) return r;
    
    int lower = BSearchLowerBound(A, 0, n-1, target);
    lower = lower + 1; //move to next element
    
    if(A[lower] == target)
        r.first = lower;
    else //target is not in the array
        return r;
    
    int upper = BSearchUpperBound(A, 0, n-1, target);
    upper = upper < 0? (n-1):(upper - 1); //move to previous element
    
    //since in previous search we had check whether the target is
    //in the array or not, we do not need to check it here again
    r.second = upper;
    
    return r;
}

它的時間複雜度是兩次二分查找所用時間的和,也就是O(log n) + O(log n),最後仍是O(log n)。

相關文章
相關標籤/搜索