基礎算法-二分查找

基礎算法-二分查找

二分查找算法是在實踐中用的最多的算法之一。因爲它簡單易懂,效率很高,成爲很多程序員的首選。之前我們也看到過很多關於二分查找的文章,例如你真的會二分查找嗎?這個看似簡單的算法,卻有很多需要我們注意的地方,這裏我們要思考:

  1. 什麼時候使用二分 ?
  2. 怎麼使用二分 ?
  3. 二分爲什麼效率高?

二分查找

二分查找又名折辦查找,是一種簡單而且較有效的查找方法。要滿足兩點:循序結構存儲關鍵字有序排序。二分查找有很多版本,我一般使用兩端閉區間的代碼格式,(注意這很重要,建議使用自己喜歡的唯一一種方法,防止出錯)。
給出我自己喜歡的格式:

// A 關鍵字有序,而且順序存儲
function binary_search(A, n, target, cmp):
    left = 0
    right = n - 1
    while left <= right:
        mid = (left + right) / 2
        if cmp(A[mid], target):
            left = mid + 1;
        else if not cmp(A[mid], target):
            right = mid - 1;
        else:
            return answer = A[mid]
    return unsucessful

什麼時候使用

1. 是否是查找問題
2. 是否能夠使用順序查找
3. (結果區間)是否有序
4. 是否是順序存儲(即通過下標可以直接定位到對應的值)     
5. 怎麼進行二分,這個看似簡單,其實二分的關鍵

我們直接來看幾個問題,分析是否滿足這幾個條件,當然有些簡單的問題我們可以直接看出來是二分查找,還是希望從頭分析,一起往能滿足我們給出的四個規則。

  1. 給定一個有序的整數數組A和一個值target,判斷target是否在A中,如果存在返回對應的任意一個下標,否則返回-1.

有序,數組,判斷是否存在(可以使用順序查找),結果區間是整數數組(有序),這裏我們只需要根據下標二分就行了。這就是我們最常用的二分查找的原型。

timg

  1. 有兩個有序數組A(長度n)和B(長度m),求這兩個數組合並之後的中位數,中位數我們認爲是有序數組的中間的數,median = S[(n+m) / 2], S是排序後的數組。
  1. 可以直接看出來是查找問題
  2. 能使用順序查找,順序掃描兩個數組,直到k = (n+m)/2,就得到我們要的中位數
  3. 結果區間是否有序,有序,我們可以假設有一個大數組是A+B排序後的結果
  4. A和B都是順序存儲
  5. 怎麼進行二分,這裏我們兩個數組A和B同時二分,每次也是縮小一般查詢區間。
A:[0, n-1], B: [0, m-1] 
mid_a = (left_a + right_a) / 2
mid_b = (left_b + right_b) / 2
if judge(A[mid_a], B[mid_b]): // A[mid_a] < B[mid_b]
    A: [mid_a+1, right_a]
    B: [left_b, mid_b-1]
else:
    A: [left_a, mid_a-1]
    B: [mid_b+1, right_b]
util left_a == right_a 
    return max(A[left_a], B[left_b])
  1. 實現 int sqrt(int x) 函數,計算並返回 x 的平方根。
  1. 查找問題,對應x = a * a, a屬於[0, x]之間,即從[0, x]中查找a,是的a*a = x
  2. 能否使用順序查找,可以遍歷for a in [0, x]: 直到滿足條件
  3. 結果區間是[0, x]有序
  4. 結果區間有序存儲,關鍵字就是結果
  5. 根據結果區間[0, x]進行二分即可。
int sqrt(int x) {
    long long a=0,b=x;
    while(a < b){
        long long m = (a+b)/2;
        if(m*m == x) break;
        else if(m*m > x) b = m-1; //第一個judge(x)
        else a = m+1;
    }
    if(b*b > x) b-=1;
    return b;
}
  1. 原木切割

題目:有一些原木,現在想把這些木頭切割成一些長度相同的小段木頭,需要得到的小段的數目至少爲 k。當然,我們希望得到的小段越長越好,你需要計算能夠得到的小段木頭的最大長度。
從這個問題中,我們很難想到二分查找問題,那就抽象這個問題,一些小木棒vector<int>, 小段木頭的長度length, 一定滿足0 < length <= max(vector<int>)

  1. 是查找問題,從0 < length <= max(vector<int>)找到最合適的長度滿足條件,
  2. 能夠使用順序查找,遍歷所有的長度,判斷是否滿足條件
  3. 結果區間[0, max(vector< int>)] 有序,而且順序
  4. 之後就可以堆結果區間進行二分查找即可,

下面的代碼是典型的轉換問題之後的二分查找的問題。

int cmp(vector<int> L, int k, int length){
    if(length == 0) return -1;
    int cnt = 0;
    for(int i = 0; i < L.size(); i ++){
        cnt += L[i] / length;
    }
    if(cnt < k) return 0;
    if(cnt >= k) return 1; //長度短
}

int woodCut(vector<int> L, int k) {
    long long maxlen = 0, minlen = 0, midlen = 0;
    for(int i = 0; i < L.size(); i ++){
        if(maxlen < L[i]) maxlen = L[i];
    }

    while(minlen <= maxlen){
        midlen = (maxlen + minlen) / 2;
        if(cmp(L, k, midlen) == 1){
            minlen = midlen + 1;
        }
        else if(cmp(L, k, midlen) == 0){
            maxlen = midlen - 1;
        }
        else{
            break;
        }
    }
    return maxlen;
}

二分查找很簡單,對於一些顯而易見的問題,我們都能想到是二分查找的問題.但是有一些問題是需要我們進行抽象之後的才能看處理其本質。我們需要指出這裏的有序和順序是指結果區間有序,關鍵字一般都是我們需要問題的結果,不是去看這個題目給出的數組,例如題目4中我們沒有關注給定的一些小木段,而是關注的結果區間的長度。

爲什麼有效

二分查找時間複雜度是O(logn),幾乎每一個人都知道,這裏我們就來公式證明一下這個顯而易見的結果。
根據二分的性質,

T(n) = T(n/2) + O(1)  
T(n/2) = T(n/4) + O(1)    
    .
    .
    .   
T(2) = T(1) + O(1)     
T(1) = O(1)

這裏個高度是logn(底數是2),因此 T(n) = O(1) * logn = O(logn)

Screenshot-from-2018-11-29-15-14-15

總結:

淺談二分到這裏就結束了,正所謂紙上得來終覺淺,絕知此事要躬行。多想多練必不可少。

生活如此,問題不大。喵~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章