第1关:二分查找算法
任务描述
本关任务:编写代码实现二分查找算法。
相关知识
为了完成本关任务,你需要掌握: 1.查找的基本概念; 2.如何实现二分查找。
查找的基本概念
如果数据项被保存在如列表这样的集合中,我们会称这些数据项具有线性或者顺序关系。在 Python 的列表 List 中,这些数据项的存储位置称为下标(index),这些下标都是有序的整数。通过下标,我们可以按照顺序来访问和查找数据项,这种技术称为“顺序查找”。图 1 显示了顺序查找的基本过程,要确定列表中是否存在需要查找的数据项,首先从列表的第 1 个数据项开始,按照下标增长的顺序逐个比对数据项,如果到最后一个都未发现要查找的项,那么查找失败。
图1 整数列表的顺序查找
要对查找算法进行分析,首先要确定其中的基本计算步骤。在查找算法中,这种基本计算步骤就是进行数据项的比对。比对当前数据项等于还是不等于要查找的数据项,比对的次数决定了算法的时间复杂度。在顺序查找算法中,为了保证讨论的是一般情形,需要假定列表中的数据项并没有按值排列顺序,在列表中各处出现的概率是相同的。对于具有 n 个数据项的列表,所查找的数据项是否在列表中,比对次数是不一样的。
-
如果数据项不在列表中,需要比对所有数据项才能得知,比对次数是 n。
-
如果数据项在列表中,其情况就较为复杂。最好的情况是第 1 次比对就找到,最坏的情况是要进行 n 次比对。
因为数据项在列表中各个位置出现的概率是相同的,平均状况下比对的次数是 n/2,所以顺序查找的时间复杂度是O(n)
。
以上我们是假定列表中的数据项是无序的,那么如果数据项排了序,顺序查找算法的效率又如何?在有序表中,当数据项存在时,比对过程与无序表完全相同。不同之处在于,如果数据项不存在,比对可以提前结束。图 2 展示了在有序表中顺序查找数据项 50 的过程,当看到 54 时,可知道后面不可能存在 50,可以提前退出查找。
图2 在有序表中进行顺序查找
实际上,对于有序表,顺序查找的算法复杂度仍然是O(n)
。只是在数据项不存在的时候,有序表的查找能节省一些比对次数,但并不改变其数量级。
二分查找
那么对于有序表,有没有更好更快的查找算法?在顺序查找中,如果第 1 个数据项不匹配查找项的话,那最多还有 n-1 个待比对的数据项。为了提升查找效率,我们可以引入二分查找算法,利用有序表的特性,迅速缩小待比对数据项的范围。
二分查找的基本思路为:
设 R[first,…,last] 是当前的查找区间,首先确定该区间的中间位置为 mid=(first+last)/2,然后将待查的数据项 k 与 R[mid]进行比较:
-
若相等,则查找成功;
-
若 R[mid]>k,则由表的有序性可知 R[mid,…,last] 均大于 k,因此若表中存在该数据项,则一定是在 mid 左边的子表 R[first,…,mid-1] 中,故将其作为新的查找区间;
-
若 R[mid]<k,则由表的有序性可知 R[first,…,mid] 均小于 k,因此若表中存在该数据项,则一定是在 mid 右边的子表 R[mid+1,…,last] 中,故将其作为新的查找区间。
无论如何,我们都会将比对范围缩小到原来的一半,然后在新的比对范围中重复这个过程。图 3 展示了在有序表中如何使用二分查找方法快速找到值为 54 的项。首先将有序表中间的项 44 与 54 进行比较,44 比 54 小,则 54 在列表的后半部分,新的比对范围缩小为从 54 到 93 的部分。再将中间项 65 与 54 进行比较,65 比 54 大,则 54 在前半部分,新的比对范围缩小为从 54 到 55 的部分。再将中间项 54 与 所查找的项 54 进行比较,匹配成功,查找结束,总共比较 3 次。
图3 在有序表中进行二分查找
编程要求
在右侧编辑器中的 Begin-End 区间补充代码,根据二分查找的算法思想完成binarySearch
函数,实现在有序表中查找数据项。若查找成功,返回 True;若查找失败,返回 False。
测试说明
平台会对你编写的代码进行测试,比对你输出的数值与实际正确的数值,只有所有数据全部计算正确才能通过测试:
测试输入:
0,1,2,8,13,17,19,32,42
输入说明:输入为需要在其中查找的有序表。
预期输出:
False
False
True
False
输出说明:输出为在有序表中查找数据项 3、7、13、22 的查找结果。若查找成功,返回 True;若查找失败,返回 False。
测试输入:
2,5,7,9,13,22,30,47,50
预期输出:
False
True
True
True
开始你的任务吧,祝你成功!
'''请在Begin-End之间补充代码, 完成binarySearch函数'''
def binarySearch(alist, item):
first = 0 # 查找范围第一项的下标
last = len(alist)-1 # 查找范围最后一项的下标
found = False
while first<=last and not found:
midpoint = (first + last)//2 # 中间项下标
# 若列表alist的中间项与查找项item相等,则查找成功,found置为True
# 否则就需要缩小比对范围,分为两种情况:
# 1.查找项比中间项小,说明查找项在前半部分,更改last
# 2.查找项比中间项大,说明查找项在后半部分,更改first
# ********** Begin ********** #
if alist[midpoint]==item:
found=True
else:
if item<alist[midpoint]:
last=midpoint-1
elif item>alist[midpoint]:
first=midpoint+1
# ********** End ********** #
return found
第2关:二分查找的递归实现
任务描述
本关任务:编写代码使用递归方法实现二分查找。
相关知识
为了完成本关任务,你需要掌握: 1.如何递归实现二分查找; 2.二分查找的算法分析。
递归实现二分查找
二分查找算法实际上体现了解决问题的典型策略:分而治之。该策略如图 1 所示,是将问题分为若干更小规模的部分,通过解决每一个小规模部分问题,并将结果汇总,从而得到原问题的解。
图1 分而治之的过程
显然,递归算法就是一种典型的分治策略算法,二分查找也适合用递归算法来实现。当我们对一个列表执行二分查找时,我们首先选择中间项。如果查找项比中间项小,我们可以在原来列表的前半部分执行二分查找;同样地,如果查找项更大,我们可以在后半部分执行二分查找。无论怎样,这都是一个对较小的列表实现二分查找的递归调用。
二分查找的算法分析
由于二分查找每次比对都将下一步的比对范围缩小一半,对于 n 个数据项的有序表,比对范围会逐渐减半到 n/2
、n/4
、n/8
……n/
。当比对次数足够多以后,比对范围内就会仅剩余 1 个数据项,且无论这个数据项是否匹配查找项,比对最终都会结束。根据等式n/
可求得 i 的值,所以二分法查找的时间复杂度是=1
O(logn)
。
另外,虽然二分查找在时间复杂度上优于顺序查找,但也要考虑到对数据项进行排序的开销。如果数据集经常变动,查找次数相对较少,那么可能直接对无序表进行顺序查找要更经济。所以,在算法的选择问题上,光看时间复杂度的优劣是不够的,还需要考虑到实际应用的情况。
编程要求
根据提示,在右侧编辑器中的 Begin-End 区间补充代码,完成binarySearch
函数,使用递归的方式来对有序表实现二分查找。若查找成功,返回 True;若查找失败,返回 False。
测试说明
平台会对你编写的代码进行测试,比对你输出的数值与实际正确的数值,只有所有数据全部计算正确才能通过测试:
测试输入:
0,1,2,8,13,17,19,32,42
输入说明:输入为需要在其中查找数据项的有序表。
预期输出:
True
True
True
False
输出说明:输出为在有序表中查找数据项 2、8、13、30 的查找结果。若查找成功,返回 True;若查找失败,返回 False。
测试输入:
2,5,7,9,13,22,30,47,50
预期输出:
True
False
True
True
提示:
list = [0, 1, 2, 3, 4, 5, 6]
print(len(list)//2)
print(list[:3])
print(list[4:])
输出:
3
[0, 1, 2]
[4, 5, 6]
开始你的任务吧,祝你成功!
'''请在Begin-End之间补充代码, 完成binarySearch函数'''
def binarySearch(alist, item):
if len(alist) == 0: # 递归的基本结束条件
return False
else:
midpoint = len(alist)//2
if alist[midpoint]==item:
return True
else: # 缩小规模
# 若查找项小于中间项,则需要递归调用自身来实现对前半部分(从最开始到中间)的查找,返回递归的结果
# 否则查找项大于中间项,则需要递归调用自身来实现对后半部分(从中间到最后)的查找,返回递归的结果
# ********** Begin ********** #
if item<alist[midpoint]:
return binarySearch(alist[:midpoint],item)
elif item>alist[midpoint]:
return binarySearch(alist[midpoint+1:],item)
# ********** End ********** #