如何寫出正確的二分法以及分析

yingshin發表於2016-05-20

二分法在平時經常用到,除了查詢某個key的下標以外,還有很多變形的形式。比如 STL 裡的 lower_bound,upper_bound

總結一下注意點,有這麼幾個:

  • 陣列是非遞增還是非遞減
  • 結束條件,即while (condition) 應當是<還是<=
  • 求mid應當是偏向左還是右,即 mid = (left + right) » 1, 還是 mid = (left + right + 1) » 1
  • 如何得到迴圈不變式
  • while結束後是否需要判斷一次條件

整理了下常見的幾個問題如下:

  1. 查詢值key的下標,如果不存在返回-1.
  2. 查詢值key第一次出現的下標x,如果不存在返回-1.
  3. 查詢值key最後一次出現的下標x,如果不存在返回-1.
  4. 查詢剛好小於key的元素下標x,如果不存在返回-1.
  5. 查詢剛好大於key的元素下標x,如果不存在返回-1,等價於std::upper_bound.
  6. 查詢第一個>=key的下標,如果不存在返回-1,等價於std::lower_bound.

leetcode上也有很多類似的題目。例如:Search a 2D Matrix

二分查詢,必須條件是有序陣列,然後不斷折半,幾乎每次迴圈都可以降低一半左右的資料量。因此是O(lgN)的方法,要注意的是二分查詢要能夠退出,不能陷入死迴圈。

二分查詢用到的一個重要定義就是迴圈不變式,顧名思義,就是在迴圈中不會改變這麼一個性質。舉個例子,插入排序,不斷的迴圈到新的索引,但保持前面的排序性質不變。其實就是數學歸納法,具體的定義不用管,我們在第一個例子裡看下。

1. 查詢值為key的下標,如果不存在返回-1.

先看一下虛擬碼:

這裡麵包含怎樣的迴圈不變式呢?

如果中間值比key大,那麼[mid, right]的值我們都可以忽略掉了,這些值都比key要大。

只要在[left, mid-1]裡查詢就是了。相反,如果中間值比key小,那麼[left, mid]的值可以 忽略掉,這些值都比key要小,只要在[mid+1, right]裡查詢就可以了。如果相等,表示找到了, 可以直接返回。因此,迴圈不變式就是在每次迴圈裡,我們都可以保證要找的index在我們新構造 的區間裡。如果最後這個區間沒有,那麼就確實是沒有

注意mid的求法,可能會int越界,但我們先不用考慮這個問題,要記住的是這點:

mid是偏向left的,即如果left=1,right=2,則mid=1。

參考程式碼:

接著考慮一個複雜一點的問題

2. 查詢值key第一次出現的下標x,如果不存在返回-1.

我們仍然考慮中間值與key的關係:

  1. 如果array[mid]<key,那麼x一定在[mid+1, right]區間裡。
  2. 如果array[mid]>key,那麼x一定在[left, mid-1]區間裡。
  3. 如果array[mid]≤key,那麼不能推斷任何關係。 比如對key=1,陣列{0,1,1,2,3},{0,0,0,1,2},array[mid] = array[2] ≤ 1,但一個在左半區間,一個在右半區間。
  4. 如果array[mid]≥key,那麼x一定在[left, mid]區間裡。

綜合上面的結果,我們可以採用1,4即<和≥的組合判斷來實現我們的迴圈不變式,即迴圈過程中一直滿足key在我們的區間裡。

這裡需要注意兩個問題:

  1. 迴圈能否退出,我們注意到4的區間改變裡是令right = mid,如果left=right=mid時,迴圈是無法退出的。 換句話說,第一個問題我們始終在減小著區間,而在這個問題裡,某種情況下區間是不會減小的!
  2. 迴圈退出後的判斷問題,再看下我們的條件1,4組合,只是使得我們最後的區間滿足了≥key,是否=key,還需要再判斷一次。

參考程式碼:

接下來的這個問題還有一個小小的坑,需要注意下:

3. 查詢值key最後一次出現的下標x,如果不存在返回-1.

省去分析的過程,我們直接寫下想到的迴圈不變式:

  1. 如果array[mid]>key,那麼x一定在[left, mid-1]區間裡。
  2. 如果array[mid]≤key, 那麼x一定在[mid, right]區間裡。

這裡需要注意個問題:

在條件2裡,實際上我們是令left=mid,但是如前面提到的,如果left=1,right=2,那麼mid=left=1, 同時又進入到條件2,left=mid=1,即使我們在while設定了left < right仍然無法退出迴圈,解決的辦法很簡單: mid = (left + right + 1) >> 1 ,向右偏向就可以了。

參考程式碼:

接下來的題目都是類似的。

只貼下迴圈不變式和參考程式碼,如果你有別的心得,或者這篇文章有錯誤歡迎提出。

4. 查詢剛好小於key的元素下標x,如果不存在返回-1.

  1. 如果array[mid]<key,那麼x在區間[mid, right]
  2. 如果array[mid]≥key,那麼x在區間[left, mid-1]

參考程式碼:

5. 查詢剛好大於key的元素下標x,如果不存在返回-1,等價於std::upper_bound.

  1. 如果array[mid]>key,那麼x在區間[left, mid]
  2. 如果array[mid]≤key,那麼x在區間[mid + 1, right]

參考程式碼:

6. 查詢第一個>=key的下標,如果不存在返回-1,等價於std::lower_bound.

  1. 如果array[mid]<key,那麼x在區間[mid + 1, right]
  2. 如果array[mid]≥key,那麼x在區間[left, mid]

參考程式碼:

如果有什麼疑問或者錯誤,歡迎指出。

相關文章