二分法是搜尋演演算法中極其典型的方法,其要求輸入序列有序並可隨機存取。演演算法思想為
輸入:有序陣列nums,目的數值target
要求輸出:如果target存在在陣列中,則輸出其index,否則輸出-1
形如下圖:
傳統的二分法程式碼如下:
func binarySearch(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
middle := (left + right) / 2
if nums[middle] == target {
return middle
} else if nums[middle] > target {
right = middle - 1
} else {
left = middle + 1
}
}
return -1
}
這裡要注意兩個問題:
=
的判斷,即for left <= right
還是for left < right
。首先對於第一個問題,=
是否應該存在,取決於對於二分查詢的初始化定義,例如:
[left,right](數學中的雙閉區間)
的形式,考慮left==right
即=
成立的情況,則表示區間內只有單個運算元
,這種情況還是需要處理,否則無法通過其餘方式表示這種情況,所以此時=
是必須的。[left,right)
的形式,考慮left==right
即=
成立的情況,事實證明,這種情況並不應該存在,我們無法用[i,i)
表示任何一個區間,所以,這種情況下,=
就不是必須的。然後考察對於第二個問題,判斷條件以及下一次查詢區間應該如何設定
?
注意:二分查詢是一個經典的查詢演演算法
,其目的是查詢到指定的位置或者值
,並不僅限於查詢到等於target的index
這一種情況。
但無論怎樣,二分查詢本身有一個固定模式,即二分
,就是從middle處將區間[left,right]分成兩份,然後根據middle的情況查詢(或者更新新的區間),因此,我們只需要考慮清楚如下三種條件時要怎麼處理即可:
討論完上述兩個問題,其實二分法就有了一個固定的框架:
func binarySearch(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
middle := (left + right) / 2
if nums[middle] == target {
// 當前值等於目標值時,如何處理(新的查詢區間是什麼)
} else if nums[middle] > target {
// 當前值大於目標值時,如何處理(新的查詢區間是什麼)
} else {
// 當前值小於目標值時,如何處理(新的查詢區間是什麼)
}
}
// 考慮返回值的意義
return
}
最後我們討論返回值的含義
這一話題。在傳統的二分查詢
中,只有在兩種情況下會返回:
這裡返回值的含義表示target在nums中的index
,該值只會出現在nums[middle]==target
這一條件下。然而,剛才提到了二分查詢不總是處理等式條件
,因此我們總要思考兩種返回值的含義:
這裡我們舉一個稍稍複雜一點的例子對二分查詢進行分析。
題目要求如下:
這個問題要求返回兩種返回值:
其中對於情況1,傳統的二分查詢演演算法就可以解決,而情況2,則需要藉助於本部分要講解的返回值的含義
。
對於傳統的二分法:
func binarySearch(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
middle := (left + right) / 2
if nums[middle] == target {
return middle
} else if nums[middle] > target {
right = middle - 1
} else {
left = middle + 1
}
}
return -1
}
如果target能在nums陣列中查詢到,必定最終查詢到一個[i,i]型別的區間,即區間中只有一個數位,否則區間就要再次進行二分。例如:如果要在下列陣列中查詢4所在的位置,查詢過程如下,第三步時,查詢區間為[2,3],有兩個值,無法確定答案,則需要再次進行一次查詢:
target == 4
nums 1 2 3 4
index 0 1 2 3
1 l r
2 l r
3 l r
4 lr
那麼最終我們處理的情況必定是對於區間[left,right]中,其中left == right,因此middle == left == right,此時nums[middle]和target的關係。
所以此時,left指向的永遠是大的那個值,right是小的那個值(因為left <= right時,迴圈不會終止,迴圈終止條件為left > right,根據陣列的有序性,nums[left] > nums[right])。
最後,我們考察該題,對於陣列nums,如果目標值不在其中,那麼其最終查詢到的值只有兩種情況:
大於middle的index
,即left
等於middle的index
,用target替換middle位置的值,即left
因此,該題的結果,只需要修改傳統二分查詢的最後一行:
func binarySearch(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
middle := (left + right) / 2
if nums[middle] == target {
return middle
} else if nums[middle] > target {
right = middle - 1
} else {
left = middle + 1
}
}
return left
}
題目要求如下:
注意這裡查詢的是元素第一次和最後一次出現的位置
,這裡我們以查詢第一次出現的位置舉例
,後者同理。
考察我們在判斷條件以及下一次查詢區間的設定中強調的,考察二分查詢的三種情況:
情況 | 分析 | 操作 |
---|---|---|
nums[middle] == target時,即當前值等於目標值 | 第一次出現的位置可能在當前值前面 | right = middle - 1 |
nums[middle] > target時,即當前值大於目標值 | 第一次出現的位置在當前值前面 | right = middle - 1 |
nums[middle] < target時,即當前值小於目標值 | 第一次出現的位置在當前值後面 | left = middle + 1 |
與之前不同的是當nums[middle] == target
時,不再有返回值了,那麼考慮最後返回值的含義,最終left > right
時情況有如下3種:
情況 | 分析 | 操作 |
---|---|---|
nums[middle] == target | 此時,middle前的值必定<middle,而不是等於(只要等於,考慮上表的情況1,會使right = middle - 1) | return left |
nums[middle] > target | 此情況不存在,因為如果有這種情況會繼續使right=middle-1 | 不進行操作 |
nums[middle] < target | 此時middle必定是target前的第一個元素 | return left |
經過上面的分析後,可以清晰的寫出程式碼:
l, r := 0, len(nums)-1
for l <= r {
m := (l + r) / 2
if nums[m] >= target {
r = m - 1
} else {
l = m + 1
}
}
result := l
而查詢元素出現的最後一個位置,只需要反過來,最後return right即可。程式碼如下:
l, r: = 0, len(nums)-1
for l <= r {
m := (l + r) / 2
if nums[m] <= target {
l = m + 1
} else {
r = m - 1
}
}
result := r
本文詳細分析了二分查詢的所有細節,對於二分查詢處理的問題,我們常常需要更加關注本文討論的後兩個問題:
最後填充模版即可。
func binarySearch(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
middle := (left + right) / 2
if nums[middle] == target {
// 當前值等於目標值時,如何處理(新的查詢區間是什麼)
} else if nums[middle] > target {
// 當前值大於目標值時,如何處理(新的查詢區間是什麼)
} else {
// 當前值小於目標值時,如何處理(新的查詢區間是什麼)
}
}
// 考慮返回值的意義
return
}