二分法在平時經常用到,除了查詢某個key的下標以外,還有很多變形的形式。比如 STL 裡的 lower_bound,upper_bound
。
總結一下注意點,有這麼幾個:
- 陣列是非遞增還是非遞減
- 結束條件,即while (condition) 應當是<還是<=
- 求mid應當是偏向左還是右,即 mid = (left + right) » 1, 還是 mid = (left + right + 1) » 1
- 如何得到迴圈不變式
- while結束後是否需要判斷一次條件
整理了下常見的幾個問題如下:
- 查詢值key的下標,如果不存在返回-1.
- 查詢值key第一次出現的下標x,如果不存在返回-1.
- 查詢值key最後一次出現的下標x,如果不存在返回-1.
- 查詢剛好小於key的元素下標x,如果不存在返回-1.
- 查詢剛好大於key的元素下標x,如果不存在返回-1,等價於std::upper_bound.
- 查詢第一個>=key的下標,如果不存在返回-1,等價於std::lower_bound.
leetcode上也有很多類似的題目。例如:Search a 2D Matrix
二分查詢,必須條件是有序陣列,然後不斷折半,幾乎每次迴圈都可以降低一半左右的資料量。因此是O(lgN)的方法,要注意的是二分查詢要能夠退出,不能陷入死迴圈。
二分查詢用到的一個重要定義就是迴圈不變式,顧名思義,就是在迴圈中不會改變這麼一個性質。舉個例子,插入排序,不斷的迴圈到新的索引,但保持前面的排序性質不變。其實就是數學歸納法,具體的定義不用管,我們在第一個例子裡看下。
1. 查詢值為key的下標,如果不存在返回-1.
先看一下虛擬碼:
1 2 3 4 5 6 7 8 |
while left <= right if array[mid] > key: right = left - 1 else if array[mid] < key: left = mid + 1 else return mid return -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。
參考程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int BS(const VecInt& vec, int key) { int left = 0, right = vec.size() - 1; while (left <= right) { if (vec[mid] > key) right = mid - 1; else if (vec[mid] < key) left = mid + 1; else return mid; } return -1; } |
接著考慮一個複雜一點的問題
2. 查詢值key第一次出現的下標x,如果不存在返回-1.
我們仍然考慮中間值與key的關係:
- 如果array[mid]<key,那麼x一定在[mid+1, right]區間裡。
- 如果array[mid]>key,那麼x一定在[left, mid-1]區間裡。
- 如果array[mid]≤key,那麼不能推斷任何關係。 比如對key=1,陣列{0,1,1,2,3},{0,0,0,1,2},array[mid] = array[2] ≤ 1,但一個在左半區間,一個在右半區間。
- 如果array[mid]≥key,那麼x一定在[left, mid]區間裡。
綜合上面的結果,我們可以採用1,4即<和≥的組合判斷來實現我們的迴圈不變式,即迴圈過程中一直滿足key在我們的區間裡。
這裡需要注意兩個問題:
- 迴圈能否退出,我們注意到4的區間改變裡是令
right = mid,
如果left=right=mid時,迴圈是無法退出的。 換句話說,第一個問題我們始終在減小著區間,而在這個問題裡,某種情況下區間是不會減小的! - 迴圈退出後的判斷問題,再看下我們的條件1,4組合,只是使得我們最後的區間滿足了≥key,是否=key,還需要再判斷一次。
參考程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int BS_First(const VecInt& vec, int key) { int left = 0, right = vec.size() - 1; while (left < right)//問題1,left < right時繼續,相等就break. { int mid = (left + right) >> 1; if (vec[mid] < key) left = mid + 1; else right = mid; } if (vec[left] == key)//問題2,再判斷一次。 return left; return -1; } |
接下來的這個問題還有一個小小的坑,需要注意下:
3. 查詢值key最後一次出現的下標x,如果不存在返回-1.
省去分析的過程,我們直接寫下想到的迴圈不變式:
- 如果array[mid]>key,那麼x一定在[left, mid-1]區間裡。
- 如果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 ,向右偏向就可以了。
參考程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
int BS_Last(const VecInt& vec, int key) { int left = 0, right = vec.size() - 1; while (left < right) { int mid = (left + right + 1) >> 1; if (vec[mid] > key) right = mid - 1; else left = mid; } if (vec[left] == key) return left; return -1; } |
接下來的題目都是類似的。
只貼下迴圈不變式和參考程式碼,如果你有別的心得,或者這篇文章有錯誤歡迎提出。
4. 查詢剛好小於key的元素下標x,如果不存在返回-1.
- 如果array[mid]<key,那麼x在區間[mid, right]
- 如果array[mid]≥key,那麼x在區間[left, mid-1]
參考程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int BS_Last_Less(const VecInt& vec, int key) { int left = 0, right = vec.size() - 1; while (left < right) { int mid = (left + right + 1) >> 1; if (vec[mid] < key) left = mid; else right = mid - 1; } if (vec[left] < key) return left; return -1; } |
5. 查詢剛好大於key的元素下標x,如果不存在返回-1,等價於std::upper_bound.
- 如果array[mid]>key,那麼x在區間[left, mid]
- 如果array[mid]≤key,那麼x在區間[mid + 1, right]
參考程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int BS_First_Greater(const VecInt& vec, int key) { int left = 0, right = vec.size() - 1; while (left < right) { int mid = (left + right) >> 1; if (vec[mid] > key) right = mid; else left = mid + 1; } if (vec[left] > key) return left; return -1; } |
6. 查詢第一個>=key的下標,如果不存在返回-1,等價於std::lower_bound.
- 如果array[mid]<key,那麼x在區間[mid + 1, right]
- 如果array[mid]≥key,那麼x在區間[left, mid]
參考程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int BS_First_Greater_Or_Equal(const VecInt& vec, int key) { int left = 0, right = vec.size() - 1; while (left < right) { int mid = (left + right) >> 1; if (vec[mid] < key) left = mid +1; else right = mid; } if (vec[left] >= key) return left; return -1; } |
如果有什麼疑問或者錯誤,歡迎指出。