二分查找由於思想簡單,在經典算法中最容易被初學者忽視。
看懂了書上的一種寫法後,就以爲自己會了,而實際上是一看就會,一寫就廢。
不信的話,先來看一個問題:
找出升序數組中小於等於目標值的最大值?(數組中可能不包含目標值)
如果感覺很棘手,彆着急,看完本文你能隨手擼出兩種不同的解法。如果你寫出來了,也彆着急,繼續看下去,你會發現你寫的不一定完美。好啦,不管怎樣,看完絕對不虧,哈哈。
一、開門見山
不繞彎彎,針對問題:找出升序數組中小於等於目標值的最大值?
直接給出兩種二分查找的典型寫法,
第一種:
int binarySearch(int[] a, int target){
int low = 0, high = a.length - 1;
while(low <= high){
int mid = (low + high) >>> 1;
if(a[mid] < target) low = mid + 1;
else if(a[mid] > target) high = mid - 1;
else return mid;
}
return high; // 必須返回 high
}
第二種:
int binarySearch(int[] a, int target){
int low = 0, high = a.length;
while(low < high){
int mid = (low + high + 1) >>> 1;
if(a[mid] > target) high = mid - 1;
else low = mid;
}
return low; // 返回 high 也可以
}
這兩種寫法最明顯的區別是,在 while 循環裏,第一種是三分支判斷,第二種是兩分支判斷。
二分查找版,找不同遊戲:
- high 的初始值,有 a.length 和 a.length-1 兩種
- while 循環條件,有 low < high 和 low <= high 兩種
- 取中位數,有 取左中位數 和 取右中位數 兩種
- 取中位數的寫法,以取左中位數爲例,有 low+(high-low)/2、low+((high-low)>>1)、(low+high)>>>1(推薦這種寫法)等。注意 low+high 可能溢出,最好不要寫成 (low+high)/2
- low 和 high 的更新,以 low 爲例,有 low = mid 和 low = mid+1 兩種
- 判斷分支的個數,有兩分支和三分支兩種。
可見二分查找的寫法是多麼靈活!
不過對於上面的找不同,有幾點需要說明一下。
high 的初值設置和 while 循環條件有關,當循環條件爲 low <= high 時,必須寫 a.length-1,否則兩種寫法都可以。
另外,只有兩分支寫法需要取右中位數和邊界更新爲中位數(e.g. low = mid),後面會說到。
簡單解釋一下,爲什麼 (low+high)>>>1 不用擔心溢出,溢出之所以出錯,是因爲有一部分數值進到了符號位,我們只要把符號位當成是數值位,結果就是正確的,而無符號右移,剛好就不會處理符號的問題。
二、第一種寫法:三分支二分查找(推薦寫法)
這種寫法,除了返回值以外基本上都可以閉着眼睛寫
int low = 0, high = a.length - 1;
while(low <= high){
int mid = (low + high) >>> 1;
if(a[mid] < target) low = mid + 1;
else if(a[mid] > target) high = mid - 1;
else return mid;
}
由於 while 循環條件是 low <= high,所以退出循環時,low != high,至於最終是返回 low 還是 high。記住下面這種圖就 OK 了。
這幅圖的含義是,如果二分查找的目標值 target 不在數組中,那麼最後退出循環時,high 指向數組中剛好小於 target 的值,low 指向數組中剛好大於 target 的值。
三、第二種寫法:兩分支二分查找
while 循環條件是 low < high,退出循環時,一定有 low == high,不用糾結返回 low 還是返回 high。
循環裏只寫兩個分支,一個分支排除中位數,另一個分支不排除中位數,不再單獨對中位數做判斷。
根據“排除中位數的邏輯”,會有以下兩種情況:
if(排除中位數的邏輯) low = mid + 1;
else high = mid;
if(排除中位數的邏輯) high = mid - 1;
else low = mid;
因爲有一個邊界更新爲中位數,爲了避免死循環,所以有 取左中位數 和 取右中位數 的區別。
例如下面的代碼就有陷入死循環的風險:
mid = (low + high) >>> 1; // 選擇左中位數
if(排除中位數的邏輯) high = mid - 1;
else low = mid;
當候選區間只剩兩個元素時,此時求得的左中位數就是左邊界,一旦進入左邊界更新爲中位數的邏輯分支,下一次循環,左邊界沒有變,求得的左中位數還是左邊界,左邊界還是不變,就會陷入死循環。
解決辦法是,對於左邊界更新爲中位數的情況,我們取右中位數。對於右邊界更新爲中位數的情況,我們取左中位數。
四、具體問題
4.1 找出升序數組中小於等於目標值的最大值
前面已經給出代碼,不再重複列出。
4.2 找出升序數組中大於等於目標值的最小值
第一種寫法:
int binarySearch(int[] a, int target){
int low = 0, high = a.length - 1;
while(low <= high){
int mid = (low + high) >>> 1;
if(a[mid] < target) low = mid + 1;
else if(a[mid] > target) high = mid - 1;
else return mid;
}
return low;
}
第二種寫法(小於目標值的肯定不要,作爲排除中位數的邏輯):
int binarySearch(int[] a, int target){
int low = 0, high = a.length;
while(low < high){
int mid = (low + high) >>> 1;
if(a[mid] < target) low = mid + 1;
else high = mid;
}
return low;
}
4.3 找出升序數組中小於目標值的最大值
第一種寫法:
int binarySearch(int[] a, int target){
int low = 0, high = a.length - 1;
while(low <= high){
int mid = (low + high) >>> 1;
if(a[mid] < target) low = mid + 1;
else if(a[mid] > target) high = mid - 1;
else return mid - 1;
}
return high;
}
第二種寫法(大於等於目標值的肯定不要,作爲排除中位數的邏輯):
int binarySearch(int[] a, int target){
int low = 0, high = a.length;
while(low < high){
int mid = (low + high + 1) >>> 1;
if(a[mid] >= target) high = mid - 1;
else low = mid;
}
return low;
}
4.4 找出升序數組中大於目標值的最小值
第一種寫法:
int binarySearch(int[] a, int target){
int low = 0, high = a.length - 1;
while(low <= high){
int mid = (low + high) >>> 1;
if(a[mid] < target) low = mid + 1;
else if(a[mid] > target) high = mid - 1;
else return mid + 1;
}
return low;
}
第二種寫法(小於等於目標值的肯定不要,作爲排除中位數的邏輯):
int binarySearch(int[] a, int target){
int low = 0, high = a.length;
while(low < high){
int mid = (low + high) >>> 1;
if(a[mid] <= target) low = mid + 1;
else high = mid;
}
return low;
}
五、最後
不是說第一種寫法 while 循環條件一定要是 low <= high,只是 low <= high 更好。不是說第二種寫法 while 循環條件一定要是 low < high,只是寫 low < high 更好。
我還是推薦大家使用第一種寫法,代碼對稱,寫法固定,好記,只需要記住那張圖就可以了。當然,第二種寫法也不是白看的,當你看到別人用第二種寫法時,能很快反應過來,他寫的是什麼,寫的正不正確。
參考:leetcode 題解