注:本文如涉及到代码,均经过Python 3.7实际运行检验,保证其严谨性。
本文阅读时间约为5分钟。
上一节我们介绍过顺序查找算法。顺序查找算法对于有序表能节省一些比对次数,但并不改变其数量级。
那么问题来了,对于有序表而言,有没有办法利用其有序的特性,用一些更好的算法来完成有序表的查找?
答案是肯定的:有办法。我们今天要介绍的二分查找法就是其中之一。
二分查找算法
二分查找,我们拿待查找项与列表中间的数据项进行比对,比对的结果有2种可能:
- 待查找项与列表中间的数据项匹配,则完成查找。
- 待查找项与列表中间的数据项不匹配。此时又有2种情况:
- 列表中间数据项比待查找项大,那么待查找项只可能出现在列表的前半部分。
- 列表中间数据项比待查找项小,那么待查找项只可能出现在列表的后半部分。
可以看到,最坏的结果是我们将比对范围缩小到原来的1/2。
继续采用上述方法查找,每次下来,比对范围都会被缩小到原来的1/2。
根据上面的算法分析,很容易得到二分查找的代码,参考如下:
# 有序表的二分查找算法。
def binarySearch(alist, item):
first = 0
last = len(alist) - 1
found = False
while first <= last and not found:
midpoint = (first + last) // 2
if alist[midpoint] == item: # 待查找项与列表中间的数据项比对。
found = True
else: # 以下是待查找项与列表中间的数据项不匹配的2种情况。
if item < alist[midpoint]:
last = midpoint - 1
else:
first = midpoint + 1
return found
testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42]
print(binarySearch(testlist, 3))
print(binarySearch(testlist, 13))
<<<
False
True
<<<
二分查找算法实际上体现了解决问题的典型策略:分而治之。
正如我们“递归”部分说到的那样,分而治之是将问题分为若干个更小规模的部分,并通过解决每一个小规模部分问题和将结果汇总,来得到原问题的解。
可以看出,二分查找和递归都是基于分而治之的思想。两者之间显然有某种内在联系。
二分查找算法也适合用递归算法来实现:
# 二分查找算法的递归版本。
def binarySearch(alist, item):
if len(alist) == 0:
return False
else:
midpoint = len(alist) // 2
if alist[midpoint] == item:
return True
else:
if item < alist[midpoint]:
return binarySearch(alist[:midpoint], item)
else:
return binarySearch(alist[midpoint+1:], item)
testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42]
print(binarySearch(testlist, 3))
print(binarySearch(testlist, 13))
<<<
False
True
<<<
二分查找的算法分析
二分查找算法的每次比对都将下一步的比对范围缩小至此前的1/2。
当比对次数足够多之后,比对范围就会只剩下1个数据项。无论剩下的这个数据项是否能匹配待查找项,比对过程都将结束。比对的次数i和列表数据项的个数n有以下关系:
i = log2(n),也就是2^i = n。
也就是说,二分查找的算法复杂度是O(logn)。它显然优于上一节介绍的顺序查找算法,后者的算法复杂度是O(n)。
进一步思考二分查找的算法复杂度,递归版本有一个因素要引起注意:
binarySearch(alist[:midpoint], item)这个递归调用使用了列表切片,而切片操作的复杂度是O(k)——k的大小取决于切片多长——这样会使整个算法的时间复杂度稍有增加;
当然,我们采用切片是为了让程序的可读性更好;实际上切片操作不必一定出现在代码中,如在前面一个非递归版本的代码中。
算法的选择问题
既然二分查找的算法复杂度明显优于顺序查找,那么,是不是遇到查找问题一定要摒弃顺序查找而选择二分查找呢?
答案是:不一定。
这是因为,二分查找是有隐性成本的,它建立在一个前提下:有序。既然是有序,表明列表已经排好序了。然而,排序并非毫无代价,它也有时间开销。
因此,综合权衡利弊才能做出更好的选择:
- 如果一次排序后可以进行多次查找,那么排序的代价开销就能得到摊薄,此时选择二分查找是十分合算的。
- 但是如果数据集经常变动,查找次数相对较少,那么使用顺序查找显然更合算。
所以,在算法选择的问题上,光看明面上时间复杂度的优劣可能还不够,要根据实际情况,既要考虑显性成本,也要看到隐性成本,综合衡量做出取舍。
To be continued.