如何利用Python实现二分查找(迭代和递归)
“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))