八分钟内掌握二分搜索算法
你如何在英语词典中查到一个词?我知道你不会按照这种方法做:从第一页开始,翻阅每一个词,直到找到你要找的那个词——当然,除非你的词是 "土豚"(aardvark)。但如果你要找的词是 "动物园"(zoo),这种方法会花很长时间。
你会如何在英语词典中查找一个词呢?
一个更快的方法是在中间打开,然后决定是在字典的前半部分还是后半部分继续搜索。
这种方法是对二分搜索算法的一种宽泛描述,这种算法在一个排序的元素列表中寻找一个元素的位置。它被称为二分搜索(来自拉丁语bīnī:"二乘二,对"),因为它在每次迭代时将数组分成两半,以缩小搜索空间。
让我们来定义一下前面那句话中的专业术语。一个 "算法 "是解决一个问题的方法,就像我们在例子中用来查找一个单词的方法。一个 "元素 "就是我们要找的那个词,而 "元素的排序列表 "就是字典。之所以说是 "排序",是因为字典里的词是按字母顺序排列的。
本文讨论了二分搜索算法在直观层面上是如何工作的。然后我们将看看它在Python和C++中的实现以及它们的内置函数。最后,我们将讨论它与线性搜索算法的性能比较。
算法
本节将让你对二分搜索算法有一个更好的直观感受。首先,我们看一下问题陈述,然后了解一下算法本身,最后,通过一个例子来了解一下算法。
问题陈述
在Leetcode,一个练习编码面试问题的平台上,二分搜索问题被陈述如下[3]:
给出一个由n个元素组成的排序(升序)的整数数组nums和一个目标值target,写一个函数来搜索nums中的目标target。如果目标值存在,返回其索引;否则,返回-1。
二分搜索算法
二分搜索算法的工作原理如下:
1. 设置搜索空间等于排序后的数组。
3. 取搜索空间的中间元素,与目标值进行比较。
3. 如果数组中没有匹配的元素,返回-1
举例说明
让我们通过一个例子来了解二分搜索算法。在下面的图片中,你可以看到一个排序的数组nums = [3, 7, 8, 9, 14, 16, 17, 22, 24],n = 9个元素。我们想找到目标target=8的位置。
二分搜索算法问题陈述(图片由作者受Mike Buss启发[7])。
迭代1:
二分搜索算法迭代1(图片由作者受Mike Buss启发[7])。
我们通过称为low和high的起始和结束索引来定义搜索空间。我们设置搜索空间的方法是将low指定为数组中第一个元素的索引(0),high指定为数组中最后一个元素的索引(8)。
我们通过使用公式(low + high)/2得到数组中间元素mid的索引。这个操作执行了一个地板函数,以达到所需的中间元素:mid = (low + high) // 2 = (0 + 8) / 2 = 4
中间元素的值是nums[mid] = nums[4] = 14,因此大于target=8。 因此,中间元素右边的所有元素都可以被丢弃。我们将high更新为(mid - 1) = 4 - 1 = 3,使搜索空间减半。
迭代2:
二分搜索算法迭代2(图片由作者受Mike Buss启发[7])。
现在,我们重复第2步。
现在数组中间元素的索引是mid = (low + high) // 2 = (0 + 3) / 2 = 1。中间元素的值是nums[mid] = nums[1] = 7,因此小于target=8。 因此,中间元素左边的所有元素都可以丢弃。我们将low更新为(mid + 1)= 1 + 1 = 2,从而将搜索空间减半。
迭代3:
二分搜索算法迭代2(图片由作者受Mike Buss启发[7])。
再次,我们重复步骤2。
现在数组中间元素的索引是mid = (low + high) // 2 = (1 + 3) / 2 = 2。
中间元素的值是nums[mid] = nums[2] = 8,因此等于target = 8。 我们返回mid = 2作为目标的位置并终止该函数。
实现
在这一节中,你将看到Python和C++中二分搜索算法的最基本实现。我们还将看看 Python 和 C++ 中内置的二分搜索函数。
二分搜索算法有不同的实现方法 [4]。然而,本文将只讨论元素迭代实现,这也是最常见的实现。
Python
def binary_search(self, nums: List[int], target: int) -> int: # Set search space equal to sorted list low = 0 high = len(nums) - 1 while low <= high: # Get the middle element of the search space mid = (high + low) // 2 # Compare middle element to target if nums[mid] == target: return mid if target < nums[mid]: high = mid - 1 else: low = mid + 1 return -1
你可以使用Python中bisect模块的bisect_left()函数,而不是编写你自己的二分搜索函数,[5]。
from bisect import bisect_lefti = bisect_left(target, nums)
C++
C++的实现看起来如下所示:
int binary_search(vector<int>& nums, int target) {
// Set search space equal to sorted list
int low = 0;
int high = nums.size() -1;
while (low <= high){
// Get the middle element of the search space
int mid = (low + high) / 2;
// Compare middle element to target
if (nums[mid] == target){
return mid;
}
if (target < nums[mid]){
high = mid - 1;
}else{
low = mid + 1;
}
}
return -1;
}
用C++实现的二分搜索算法
在C++中,标准模板库(STL)提供了函数lower_bound(),可以像下面的例子[2]那样使用它。还有一个函数binary_search(),它返回一个布尔值,即target是否存在于排序后的数组中,但不包括其位置[1]。
#include <algorithm>
i = std::lower_bound(nums.begin(), nums.end(), target);
讨论
二分搜索算法的时间和空间复杂度为。
与线性搜索算法相比,二分搜索算法的主要优势在于其速度。因为线性搜索算法的概念是遍历数组直到找到目标元素--就像从英语词典的第一页开始查找一个特定的单词——线性搜索算法的时间复杂度是O(n)。
例如,如果我们想在前面的例子中找到长度为8的数组中的一个元素,在最坏的情况下将需要n=8次迭代。而使用二分搜索算法则只需要三次迭代。
然而,二分搜索算法的主要缺点是,它需要一个排序的数组,在每次迭代时丢弃一半的搜索空间。尽管在运行二分搜索算法之前可以对数组进行排序,但排序算法会增加整体的时间复杂度。一般来说,排序的时间复杂度为O(n log n),比线性搜索算法的线性时间复杂度更差。
下面,你可以看到二分搜索算法、线性搜索算法以及作为预处理步骤的额外排序的二分搜索算法的时间复杂度:
大O总结(图片灵感来自[8])。
结论
开发算法的最佳方法是将问题分解成你已经知道如何解决的算法,如搜索和排序。这就是为什么了解二分搜索算法可以帮助你写出更好的算法——无论你是软件工程师、数据科学家,还是其他开发算法的人。
了解二分搜索算法可以帮助你编写更好的算法--无论你是软件工程师、数据科学家,还是其他任何人。
这篇文章解释了二分搜索算法的工作原理。该算法在一个排序的列表中寻找一个元素。因为搜索空间是排序的,所以该算法在每次迭代后都会丢弃一半的搜索空间。因此,我们将搜索空间减半,直到找到目标元素。你可以看到下面的算法的视觉摘要。
如何在一个数组中二分搜索数字8(图片由作者受Mike Buss启发[7])。
二分搜索算法在排序列表上比线性搜索算法更有效。它有一个对数的时间复杂度和恒定的空间复杂度。
引用
[1] “C++ reference”, “std::binary_search.” cppreference.com. https://en.cppreference.com/w/cpp/algorithm/binary_search (accessed July 2, 2022)
[2] “C++ reference”, “std::lower_bound.” cppreference.com. https://en.cppreference.com/w/cpp/algorithm/lower_bound (accessed July 2, 2022)
[3] Leetcode, “704. Binary Search.” leetcode.com. https://leetcode.com/problems/binary-search/ (accessed July 2, 2022)
[4] Leetcode, “Learning about Binary Search”. leetcode.com. https://leetcode.com/explore/learn/card/binary-search/ (accessed July 2, 2022)
[5] “Python”, “bisect — Array bisection algorithm.” python.org. https://docs.python.org/3/library/string.html#formatspec (accessed July 2, 2022)
[6] S. Selkow, G. T. Heineman, G. Pollice, Algorithms in a Nutshell (2008), O’Reilly Media.
[7] M. Buss, “Divide and Conquer with Binary Search in Swift”, mikebuss.com. https://mikebuss.com/2016/04/21/binary-search/ (accessed July 2, 2022)
[8] “Big-O Cheat Sheet”, “Know Thy Complexities!”, bigocheatsheet.com. https://www.bigocheatsheet.com/ (accessed July 2, 2022)
原文标题:
Everything You Need to Know About the Binary Search Algorithm
原文链接:
https://towardsdatascience.com/everything-you-need-to-know-about-the-binary-search-algorithm-6bc4f9a3127d
编辑:黄继彦
译者简介
欧阳锦,一名在埃因霍温理工大学就读的硕士生。喜欢数据科学和人工智能相关方向。欢迎不同观点和想法的交流与碰撞,对未知充满好奇,对热爱充满坚持。