“Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky, and many good programmers have done it wrong the first few times they tried.”
— Donald Knuth
翻譯:雖然二分查找的基本思想相對簡單,但細節可能會非常棘手,許多優秀的程序員在最開始的嘗試中都做錯了。
二分查找 Binary Search
算法思想:二分查找用於在一個含有n個元素的有序序列中有效地定位目標值。
考慮一下三種情況:
- 如果目標值
value == [middle]
的數據,查找成功並且終止 - 如果目標值
value > [middle]
的數據,對後半部分序列重複這一過程,即索引的範圍從mid + 1
到right
- 如果目標值
value < [middle]
的數據,對前半部分序列重複這一過程,即索引的範圍從left
到middle - 1
迭代定義 - Iteratively
# binary_search.py
def binary_search_iterative(elements, value):
left, right = 0, len(elements)-1
while left <= right:
middle = (left + right) // 2
if elements[middle] == value:
return middle
elif elements[middle] < value:
left = middle + 1
else:
right = middle - 1
return None
if __name__ == '__main__':
nums = [1, 2, 3, 4, 5]
print(binary_search_iterative(nums, 2)) # Output: 1,值爲2的元素出現在索引1的位置(索引從0開始)
print(binary_search_iterative(nums, 10)) # Output: None,表示空,沒有找到指定的元素
遞歸定義 - Recursively
第一版
類似二分查找的迭代版本,使用切片運算符:
將列表切碎:
def binary_search_recursive(elements, value):
if len(elements) == 0:
return False
left, right = 0, len(elements) - 1
if left <= right:
middle = (left + right) // 2
if elements[middle] == value:
return True
elif elements[middle] < value:
return binary_search_recursive(elements[middle+1:], value)
else:
return binary_search_recursive(elements[:middle], value)
return False
if __name__ == '__main__':
nums = [1, 2, 3, 4, 5]
print(binary_search_recursive(nums, 2)) # True
print(binary_search_recursive(nums, 10)) # False
不用像迭代那樣使用while
循環,只需檢查一下條件。在對應的分片列表中調用相同的函數。
使用分片會有什麼問題?好吧,事實證明,切片會生成元素引用的副本,這些副本可能具有顯着的內存和計算開銷。
第2版
爲避免複製操作,您可以重複使用相同的列表,但在必要時將不同的邊界傳遞給函數:
def binary_search(elements, value, left, right):
if left <= right:
middle = (left + right) // 2
if elements[middle] == value:
return True
elif elements[middle] < value:
return binary_search(elements, value, middle+1, right)
else:
return binary_search(elements, value, left, middle-1)
return False
if __name__ == '__main__':
nums = [1, 2, 3, 4, 5]
print(binary_search(nums, 2, 0, len(nums)-1)) # True
print(binary_search(nums, 10, 0, len(nums)-1)) # False
缺點是每次您要調用該函數時,都必須傳遞初始邊界,以確保它們正確無誤:binary_search(nums, 10, 0, len(nums)-1)
第3版
更進一步,您可能希望將一個函數嵌套在另一個函數中以隱藏技術細節並利用外部作用域中的變量重用:
def contains(elements, value):
def recursive(left, right):
if left <= right:
midddle = (left + right) // 2
if elements[midddle] == value:
return True
elif elements[midddle] < value:
return recursive(midddle+1, right)
else:
return recursive(left, midddle-1)
return False
return recursive(0, len(elements) - 1)
if __name__ == '__main__':
nums = [1, 2, 3, 4, 5]
print(contains(nums, 2)) # True
print(contains(nums, 10)) # False
即使在封閉範圍內定義了recursive()
內部函數,也可以訪問元素和值參數。
迭代和遞歸實現之間的選擇通常是性能考慮,便利性以及個人喜好的最終結果。
總結
本文中介紹了首先二分查找的基本思想,然後用迭代和遞歸兩種方法實現了簡易版的二分查找,其實Python實現了功能更強大的二分查找的庫 bisect
,感興趣的同學,可以在本文的基礎上進行學習。
最後:二分查找的時間複雜度:O(log(n))