Python实现二分查找与bisect模块详解
前言
其实Python的列表(list)内部实现是一个数组,也就是一个线性表。在列表中查找元素可以使用list.index()方法,其时间复杂度为O(n)。对于大数据量,则可以用二分查找进行优化。
二分查找要求对象必须有序,其基本原理如下:
1.从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;
2.如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。
3.如果在某一步骤数组为空,则代表找不到。
二分查找也成为折半查找,算法每一次比较都使搜索范围缩小一半,其时间复杂度为O(logn)。
我们分别用递归和循环来实现二分查找:
defbinary_search_recursion(lst,value,low,high): ifhigh<low: returnNone mid=(low+high)/2 iflst[mid]>value: returnbinary_search_recursion(lst,value,low,mid-1) eliflst[mid]<value: returnbinary_search_recursion(lst,value,mid+1,high) else: returnmid defbinary_search_loop(lst,value): low,high=0,len(lst)-1 whilelow<=high: mid=(low+high)/2 iflst[mid]<value: low=mid+1 eliflst[mid]>value: high=mid-1 else: returnmid returnNone
接着对这两种实现进行一下性能测试:
if__name__=="__main__": importrandom lst=[random.randint(0,10000)for_inxrange(100000)] lst.sort() deftest_recursion(): binary_search_recursion(lst,999,0,len(lst)-1) deftest_loop(): binary_search_loop(lst,999) importtimeit t1=timeit.Timer("test_recursion()",setup="from__main__importtest_recursion") t2=timeit.Timer("test_loop()",setup="from__main__importtest_loop") print"Recursion:",t1.timeit() print"Loop:",t2.timeit()
执行结果如下:
Recursion:3.12596702576 Loop:2.08254289627
可以看出循环方式比递归效率高。
bisect模块
Python有一个bisect模块,用于维护有序列表。bisect模块实现了一个算法用于插入元素到有序列表。在一些情况下,这比反复排序列表或构造一个大的列表再排序的效率更高。Bisect是二分法的意思,这里使用二分法来排序,它会将一个元素插入到一个有序列表的合适位置,这使得不需要每次调用sort的方式维护有序列表。
下面是一个简单的使用示例:
importbisect importrandom random.seed(1) print'NewPosContents' print'--------------' l=[] foriinrange(1,15): r=random.randint(1,100) position=bisect.bisect(l,r) bisect.insort(l,r) print'%3d%3d'%(r,position),l
输出结果:
NewPosContents -------------- 140[14] 851[14,85] 771[14,77,85] 261[14,26,77,85] 502[14,26,50,77,85] 452[14,26,45,50,77,85] 664[14,26,45,50,66,77,85] 796[14,26,45,50,66,77,79,85] 100[10,14,26,45,50,66,77,79,85] 30[3,10,14,26,45,50,66,77,79,85] 849[3,10,14,26,45,50,66,77,79,84,85] 444[3,10,14,26,44,45,50,66,77,79,84,85] 779[3,10,14,26,44,45,50,66,77,77,79,84,85] 10[1,3,10,14,26,44,45,50,66,77,77,79,84,85]
Bisect模块提供的函数有:
bisect.bisect_left(a,x,lo=0,hi=len(a)):
查找在有序列表a中插入x的index。lo和hi用于指定列表的区间,默认是使用整个列表。如果x已经存在,在其左边插入。返回值为index。
bisect.bisect_right(a,x,lo=0,hi=len(a))
bisect.bisect(a,x,lo=0,hi=len(a)):
这2个函数和bisect_left类似,但如果x已经存在,在其右边插入。
bisect.insort_left(a,x,lo=0,hi=len(a)):
在有序列表a中插入x。和a.insert(bisect.bisect_left(a,x,lo,hi),x)的效果相同。
bisect.insort_right(a,x,lo=0,hi=len(a))
bisect.insort(a,x,lo=0,hi=len(a)):
和insort_left类似,但如果x已经存在,在其右边插入。
Bisect模块提供的函数可以分两类:bisect*只用于查找index,不进行实际的插入;而insort*则用于实际插入。
该模块比较典型的应用是计算分数等级:
defgrade(score,breakpoints=[60,70,80,90],grades='FDCBA'): i=bisect.bisect(breakpoints,score) returngrades[i] print[grade(score)forscorein[33,99,77,70,89,90,100]]
执行结果:
['F','A','C','C','B','A','A']
同样,我们可以用bisect模块实现二分查找:
defbinary_search_bisect(lst,x): frombisectimportbisect_left i=bisect_left(lst,x) ifi!=len(lst)andlst[i]==x: returni returnNone
我们再来测试一下它与递归和循环实现的二分查找的性能:
Recursion:4.00940990448 Loop:2.6583480835 Bisect:1.74922895432
可以看到其比循环实现略快,比递归实现差不多要快一半。
Python著名的数据处理库numpy也有一个用于二分查找的函数numpy.searchsorted,用法与bisect基本相同,只不过如果要右边插入时,需要设置参数side='right',例如:
>>>importnumpyasnp >>>frombisectimportbisect_left,bisect_right >>>data=[2,4,7,9] >>>bisect_left(data,4) 1 >>>np.searchsorted(data,4) 1 >>>bisect_right(data,4) 2 >>>np.searchsorted(data,4,side='right') 2
那么,我们再来比较一下性能:
In[20]:%timeit-n100bisect_left(data,99999) 100loops,bestof3:670nsperloop In[21]:%timeit-n100np.searchsorted(data,99999) 100loops,bestof3:56.9msperloop In[22]:%timeit-n100bisect_left(data,8888) 100loops,bestof3:961nsperloop In[23]:%timeit-n100np.searchsorted(data,8888) 100loops,bestof3:57.6msperloop In[24]:%timeit-n100bisect_left(data,777777) 100loops,bestof3:670nsperloop In[25]:%timeit-n100np.searchsorted(data,777777) 100loops,bestof3:58.4msperloop
可以发现numpy.searchsorted效率是很低的,跟bisect根本不在一个数量级上。因此searchsorted不适合用于搜索普通的数组,但是它用来搜索numpy.ndarray是相当快的:
In[30]:data_ndarray=np.arange(0,1000000) In[31]:%timeitnp.searchsorted(data_ndarray,99999) Theslowestruntook16.04timeslongerthanthefastest.Thiscouldmeanthatanintermediateresultisbeingcached. 1000000loops,bestof3:996nsperloop In[32]:%timeitnp.searchsorted(data_ndarray,8888) Theslowestruntook18.22timeslongerthanthefastest.Thiscouldmeanthatanintermediateresultisbeingcached. 1000000loops,bestof3:994nsperloop In[33]:%timeitnp.searchsorted(data_ndarray,777777) Theslowestruntook31.32timeslongerthanthefastest.Thiscouldmeanthatanintermediateresultisbeingcached. 1000000loops,bestof3:990nsperloop
numpy.searchsorted可以同时搜索多个值:
>>>np.searchsorted([1,2,3,4,5],3) 2 >>>np.searchsorted([1,2,3,4,5],3,side='right') 3 >>>np.searchsorted([1,2,3,4,5],[-10,10,2,3]) array([0,5,1,2])
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家学习或者使用python能有一定的帮助,如果有疑问大家可以留言交流。