LeetCode Binary Search Summary 二分搜尋法小結

Grandyang發表於2017-05-15

 

二分查詢法作為一種常見的查詢方法,將原本是線性時間提升到了對數時間範圍,大大縮短了搜尋時間,具有很大的應用場景,而在LeetCode中,要運用二分搜尋法來解的題目也有很多,但是實際上二分查詢法的查詢目標有很多種,而且在細節寫法也有一些變化。之前有網友留言希望博主能針對二分查詢法的具體寫法做個總結,博主由於之前一直很忙,一直拖著沒寫,為了樹立博主言出必行的正面形象,不能再無限制的拖下去了,那麼今天就來做個了斷吧,總結寫起來~ (以下內容均為博主自己的總結,並不權威,權當參考,歡迎各位大神們留言討論指正)

根據查詢的目標不同,博主將二分查詢法主要分為以下五類:

 

第一類: 需查詢和目標值完全相等的數

這是最簡單的一類,也是我們最開始學二分查詢法需要解決的問題,比如我們有陣列[2, 4, 5, 6, 9],target = 6,那麼我們可以寫出二分查詢法的程式碼如下:

 

int find(vector<int>& nums, int target) {
    int left = 0, right = nums.size();
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) return mid;
        else if (nums[mid] < target) left = mid + 1;
        else right = mid;
    }
    return -1;
}

 

會返回3,也就是target的在陣列中的位置。注意二分查詢法的寫法並不唯一,主要可以變動地方有四處:

第一處是right的初始化,可以寫成 nums.size() 或者 nums.size() - 1

第二處是left和right的關係,可以寫成 left < right 或者 left <= right

第三處是更新right的賦值,可以寫成 right = mid 或者 right = mid - 1

第四處是最後返回值,可以返回left,right,或right - 1

但是這些不同的寫法並不能隨機的組合,像博主的那種寫法,若right初始化為了nums.size(),那麼就必須用left < right,而最後的right的賦值必須用 right = mid。但是如果我們right初始化為 nums.size() - 1,那麼就必須用 left <= right,並且right的賦值要寫成 right = mid - 1,不然就會出錯。所以博主的建議是選擇一套自己喜歡的寫法,並且記住,實在不行就帶簡單的例子來一步一步執行,確定正確的寫法也行。

第一類應用例項:

Intersection of Two Arrays

 

第二類: 查詢第一個不小於目標值的數,可變形為查詢最後一個小於目標值的數

這是比較常見的一類,因為我們要查詢的目標值不一定會在陣列中出現,也有可能是跟目標值相等的數在陣列中並不唯一,而是有多個,那麼這種情況下nums[mid] == target這條判斷語句就沒有必要存在。比如在陣列[2, 4, 5, 6, 9]中查詢數字3,就會返回數字4的位置;在陣列[0, 1, 1, 1, 1]中查詢數字1,就會返回第一個數字1的位置。我們可以使用如下程式碼:

 

int find(vector<int>& nums, int target) {
    int left = 0, right = nums.size();
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) left = mid + 1;
        else right = mid;
    }
    return right;
}

 

最後我們需要返回的位置就是right指標指向的地方。在C++的STL中有專門的查詢第一個不小於目標值的數的函式lower_bound,在博主的解法中也會時不時的用到這個函式。但是如果面試的時候人家不讓使用內建函式,那麼我們只能老老實實寫上面這段二分查詢的函式。

這一類可以輕鬆的變形為查詢最後一個小於目標值的數,怎麼變呢。我們已經找到了第一個不小於目標值的數,那麼再往前退一位,返回right - 1,就是最後一個小於目標值的數。

第二類應用例項:

 
第二類變形應用:Valid Triangle Number
 

第三類: 查詢第一個大於目標值的數,可變形為查詢最後一個不大於目標值的數

這一類也比較常見,尤其是查詢第一個大於目標值的數,在C++的STL也有專門的函式upper_bound,這裡跟上面的那種情況的寫法上很相似,只需要新增一個等號,將之前的 nums[mid] < target 變成 nums[mid] <= target,就這一個小小的變化,其實直接就改變了搜尋的方向,使得在陣列中有很多跟目標值相同的數字存在的情況下,返回最後一個相同的數字的下一個位置。比如在陣列[2, 4, 5, 6, 9]中查詢數字3,還是返回數字4的位置,這跟上面那查詢方式返回的結果相同,因為數字4在此陣列中既是第一個不小於目標值3的數,也是第一個大於目標值3的數,所以make sense;在陣列[0, 1, 1, 1, 1]中查詢數字1,就會返回座標5,通過對比返回的座標和陣列的長度,我們就知道是否存在這樣一個大於目標值的數。參見下面的程式碼:

 

int find(vector<int>& nums, int target) {
    int left = 0, right = nums.size();
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] <= target) left = mid + 1;
        else right = mid;
    }
    return right;
}

 

這一類可以輕鬆的變形為查詢最後一個不大於目標值的數,怎麼變呢。我們已經找到了第一個大於目標值的數,那麼再往前退一位,返回right - 1,就是最後一個不大於目標值的數。比如在陣列[0, 1, 1, 1, 1]中查詢數字1,就會返回最後一個數字1的位置4,這在有些情況下是需要這麼做的。

第三類應用例項:

Kth Smallest Element in a Sorted Matrix

第三類變形應用示例:

Sqrt(x)

 

第四類: 用子函式當作判斷關係

這是最令博主頭疼的一類,而且通常情況下都很難。因為這裡在二分查詢法重要的比較大小的地方使用到了子函式,並不是之前三類中簡單的數字大小的比較,比如Split Array Largest Sum那道題中的解法一,就是根據是否能分割陣列來確定下一步搜尋的範圍。類似的還有Guess Number Higher or Lower這道題,是根據給定函式guess的返回值情況來確定搜尋的範圍。對於這類題目,博主也很無奈,遇到了只能自求多福了。

第四類應用例項:

Split Array Largest SumGuess Number Higher or LowerFind K Closest ElementsFind K-th Smallest Pair DistanceKth Smallest Number in Multiplication TableMaximum Average Subarray IIMinimize Max Distance to Gas StationSwim in Rising Water

 

第五類: 其他

有些題目不屬於上述的四類,但是還是需要用到二分搜尋法,比如這道 Find Peak Element,求的是陣列的區域性峰值。由於是求的峰值,需要跟相鄰的數字比較,那麼target就不是一個固定的值,而且這道題的一定要注意的是right的初始化,一定要是nums.size() - 1,這是由於算出了mid後,nums[mid] 要和 nums[mid+1] 比較,如果right初始化為nums.size()的話,mid+1可能會越界,從而不能找到正確的值。

第五類應用例項:

Find Peak Element

 

綜上所述,博主大致將二分搜尋法的應用場景分成了主要這五類,其中第二類和第三類還有各自的擴充套件。根據目前博主的經驗來看,第二類和第三類的應用場景最多,也是最重要的兩類。第一類,第四類,和第五類較少,其中第一類最簡單,第四類最難,遇到這類,博主也沒啥好建議,多多練習吧~

 

如果有寫的有遺漏或者錯誤的地方,請大家踴躍留言啊,共同進步哈~

 

LeetCode All in One 題目講解彙總(持續更新中...)

相關文章