先舉乙個簡單的例子來說明二分搜尋的做法:
問題:有乙個陣列a = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91],查詢元素23在陣列中的下標。
解法:初始條件: 陣列a, 搜尋下邊界l是0, 上邊界h是9
第一次搜尋: 計算中間點位置:m = (下邊界l + 上邊界) / 2 = (0 + 9) / 2 = 4,a[4]的值是16, 小於23,因此,數字23在陣列**現的下標位置應該大於4,於是搜尋的範圍應該是在陣列分割之後的右邊部分,於是下邊界l置為5
第二次搜尋:計算中間點位置:m = (下邊界l + 上邊界) / 2 = (5 + 9) / 2 = 7,a[7]的值是56, 大於23,因此,數字23在陣列**現的下標位置應該小於7,於是搜尋的範圍應該是在陣列分割之後的左邊部分,於是上邊界h置為6
第三次搜尋:計算中間點位置:m = (下邊界l + 上邊界) / 2 = (5 + 6) / 2 = 5,a[5]的值是23, 搜尋結束,返回5
整個搜尋過程如下圖所示:
從以上過程可以看出:二分法每次都將搜尋範圍減少一半,是一種非常高效的方法,事實上,其時間複雜度是o(log n)。
二分法看起來非常簡單,但是據說90%的程式設計師都寫不對這個演算法,這裡,我們從最簡單的情況開始,把這個演算法寫對。
最簡單的情況是陣列中元素嚴格按照公升序排列。
思路如下:
若陣列中間值小於查詢值,則說明查詢值的位置一定在右半部分(不包含中間值)
若陣列中間值等於查詢值,則說明查詢值的位置是中間位置
若陣列中間值大於查詢值,則說明查詢值的位置一定在左半部分(不包含中間值)
**如下:
def _binary_search(arr, key, lower, upper):
# 如果下邊界已經超過上邊界了,說明在arr中找不到key, 返回-1
if lower > upper:
return -1
# 計算中間點的index, 一種方法是middle = (upper + lower) // 2, 但這樣寫在
# c之類的語言中可能會產生整數溢位,在python中沒有這個問題, 但一般還是按以下寫法
middle = lower + (upper - lower) // 2
# 陣列的中間元素等於key,則返回中間元素的下標
if arr[middle] == key:
return middle
elif arr[middle] > key:
return _binary_search(arr, key, lower, upper-1)
else:
return _binary_search(arr, key, lower+1, upper)
def binary_search(arr, key):
return _binary_search(arr, key, 0, len(arr)-1)
現在問題加大一些難度,假設陣列中的元素不是嚴格公升序的, 即,陣列中存在重複元素。那麼如何查詢某個元素在陣列**現的初始位置以及結束位置呢?
例如:陣列[1, 3, 3, 4, 5]中3的初始位置是1, 結束位置是2。
思路還是與上面一致,還是依據與中間結果的比較來縮小問題規模,只是在邊界值的處理上有些細節需要注意。
對於求初始位置:
若陣列中間值小於查詢值,則說明查詢值的初始位置一定在右半部分(不包含中間值)
若陣列中間值等於查詢值,則說明查詢值的初始位置一定在左半部分(包含中間值)
若陣列中間值大於查詢值,則說明查詢值的初始位置一定在左半部分(不包含中間值)
對於求結束位置:
若陣列中間值小於查詢值,則說明查詢值的結束位置一定在右半部分(不包含中間值)
若陣列中間值等於查詢值,則說明查詢值的結束位置一定在右半部分(包含中間值)
若陣列中間值大於查詢值,則說明查詢值的結束位置一定在左半部分(不包含中間值)
**如下:
def _lower_bound_binary_search(arr, key, lower, upper):
# 若只剩下乙個元素或者不剩元素了,則退出程式
if lower >= upper:
# 若最後乙個元素等於所需查詢的元素, 則返回其下標
if arr[lower] == key:
return lower
else:
return -1
# 求中間點index
middle = lower + (upper - lower) // 2
# 若中點值小於key, 則在右半部分搜尋
if arr[middle] < key:
return _lower_bound_binary_search(arr, key, middle+1, upper)
# 若中點值大於等於key, 則在左半部分搜尋(包括中點)
else:
return _lower_bound_binary_search(arr, key, lower, middle)
def lower_bound_binary_search(arr, key):
return _lower_bound_binary_search(arr, key, 0, len(arr)-1)
def _upper_bound_binary_search(arr, key, lower, upper):
# 若只剩下乙個元素或者不剩元素了,則退出程式
if lower >= upper:
# 若最後乙個元素等於所需查詢的元素, 則返回其下標
if arr[upper] == key:
return upper
else:
return -1
# 求中間點index, 需向上取整
middle = lower + (upper - lower + 1) // 2
# 若中點值大於key, 則在左半部分搜尋
if arr[middle] > key:
return _upper_bound_binary_search(arr, key, lower, middle-1)
# 若中點值小於等於key,則在右半部分搜尋
else:
return _upper_bound_binary_search(arr, key, middle, upper)
def upper_bound_binary_search(arr, key):
return _upper_bound_binary_search(arr, key, 0, len(arr)-1)
第一步:計算陣列中間點的位置,取到中間點的值
第二步:根據中間點的值與查詢值的大小關係,確定往陣列的左半部分或者右半部分查詢
第三步:重複上面兩步,直到查詢到所需的值,或者查詢完整個陣列, 確認值不存在
計算中間點index有講究
計算中間點index最自然的寫法是:
middle = (lower + upper) // 2
對於python,這樣寫沒有問題, 因為python直譯器幫我們處理了整數溢位問題,試想,假如我們使用的c語言,lower, upper可能都是32位的整數, 再假設,lower, upper 都是大於2的16次方的整數,那麼二者的和就溢位了。所以,我們採用最安全的寫法:
middle = lower + (upper - lower) // 2
其實這個寫法也還可以優化下:可以用移位運算來代替除以2
middle = lower + ((upper - lower) >> 1)
向上向下取整有講究取兩個整數的平均值有兩種不同的取法,一種是向上取整, 一種是向下取整。
向下取整:
middle = lower + (upper - lower) // 2
向上取整:
middle = lower + (upper - lower + 1) // 2
例如:對3,5求均值,無論向上向下取整,結果都是4;但是對於3,4求均值,向上取整的話結果是4,向下取整的話結果是3。
其實對於嚴格公升序排序的陣列,進行二分搜尋的時候, 選取中間點,無論向上向下取整都是可以的。
但是,對於含有重複元素的陣列,進行二分搜尋的時候,若是求開始位置,則必須使用向下取整,若是求結束位置,則必須使用向上取整。為什麼會有這種區別呢?因為我們取的中間點的位置必須盡可能地靠近需要求取的位置。
遞迴退出條件有講究
對於上面**的遞迴退出條件,顯然:當lower > upper時是可以退出的,這代表了查詢失敗。
但是,對於求開始位置、或者結束位置的二分搜尋,其實是有乙個陷阱的。
例如:求陣列[2]中元素2出現的開始位置,若我們還是按照lower > upper的退出條件的話,則會陷入無限迴圈。詳細說明如下:
lower = 0, upper = 0, middle = (lower + upper) // 2 = 0, arr[middle] = arr[0] = 2
因此下一次還是在繼續重複這個迴圈。
也就是說若陣列只有乙個元素, 且該元素等於要查詢的元素, 則永遠跳不出迴圈了。
所以,對於求開始位置、結束位置的二分搜尋,退出的邊界時既要考慮陣列剩下乙個元素的情況,也要考慮陣列不剩任何元素的情況。
迭代二分查詢二分查詢
在寫這篇文章之前,已經寫過了幾篇關於改迭代二分查詢主題的文章,想要了解的朋友可以去翻一下之前的文章 bentley在他的著作 writing correct programs 中寫道,90 的計算機專家不能在2小時內寫出完整確正的二分搜尋演算法。難怪有人說,二分查詢道理單簡,甚至小學生都能明確。不過...
1128 二分 二分查詢
時間限制 10000ms 單點時限 1000ms 記憶體限制 256mb 描述nettle最近在玩 艦 因此nettle收集了很多很多的船 這裡我們假設nettle氪了很多金,開了無數個船位 去除掉重複的船之後,還剩下n 1 n 1,000,000 種不同的船。每一艘船有乙個稀有值,任意兩艘船的稀有...
尺取 二分查詢
尺取法 這是一種比較有趣的方法,想吃子一樣去解決問題。現在我只是知道了可以用陣列來模擬 尺子 加油學習!方法是 陣列模擬 二分查詢 說一 下資料的意思,10個數,從中找出和為15的最短子串。10 5 1 3 5 10 7 4 9 2 8 15 include using namespace std ...