你真的熟練掌握二分查找了嗎?

二分查找由於思想簡單,在經典算法中最容易被初學者忽視。
看懂了書上的一種寫法後,就以爲自己會了,而實際上是一看就會,一寫就廢。

不信的話,先來看一個問題:
找出升序數組中小於等於目標值的最大值?(數組中可能不包含目標值)

如果感覺很棘手,彆着急,看完本文你能隨手擼出兩種不同的解法。如果你寫出來了,也彆着急,繼續看下去,你會發現你寫的不一定完美。好啦,不管怎樣,看完絕對不虧,哈哈。

一、開門見山

不繞彎彎,針對問題:找出升序數組中小於等於目標值的最大值?
直接給出兩種二分查找的典型寫法,
第一種:

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 循環裏,第一種是三分支判斷,第二種是兩分支判斷。

二分查找版,找不同遊戲:

  1. high 的初始值,有 a.length 和 a.length-1 兩種
  2. while 循環條件,有 low < high 和 low <= high 兩種
  3. 取中位數,有 取左中位數 和 取右中位數 兩種
  4. 取中位數的寫法,以取左中位數爲例,有 low+(high-low)/2、low+((high-low)>>1)、(low+high)>>>1(推薦這種寫法)等。注意 low+high 可能溢出,最好不要寫成 (low+high)/2
  5. low 和 high 的更新,以 low 爲例,有 low = mid 和 low = mid+1 兩種
  6. 判斷分支的個數,有兩分支和三分支兩種。

可見二分查找的寫法是多麼靈活!

不過對於上面的找不同,有幾點需要說明一下。
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 題解

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