Python 二分查詢與 bisect 模組
Python 的列表(list)內部實現是一個數組,也就是一個線性表。在列表中查詢元素可以使用 list.index() 方法,其時間複雜度為O(n)。對於大資料量,則可以用二分查詢進行優化。二分查詢要求物件必須有序,其基本原理如下:
- 1.從陣列的中間元素開始,如果中間元素正好是要查詢的元素,則搜素過程結束;
- 2.如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中查詢,而且跟開始一樣從中間元素開始比較。
- 3.如果在某一步驟陣列為空,則代表找不到。
二分查詢也成為折半查詢,演算法每一次比較都使搜尋範圍縮小一半, 其時間複雜度為 O(logn)。
我們分別用遞迴和迴圈來實現二分查詢:
12345678910111213141516171819202122 | defbinary_search_recursion(lst,value,low,high):ifhigh<low:returnNonemid=(low+high)/2iflst[mid]>value:returnbinary_search_recursion(lst,value,low,mid-1)eliflst[mid]<value:returnbinary_search_recursion(lst,value,mid+ |
接著對這兩種實現進行一下效能測試:
Python1234567891011121314151617 | if__name__=="__main__":importrandomlst=[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)importtimeitt1=timeit.Timer("test_recursion()",setup="from __main__ import test_recursion")t2=timeit.Timer("test_loop()",setup="from __main__ import test_loop")print"Recursion:",t1.timeit()print"Loop:",t2.timeit() |
執行結果如下:
Python12 | Recursion:3.12596702576Loop:2.08254289627 |
可以看出迴圈方式比遞迴效率高。
Python 有一個 bisect
模組,用於維護有序列表。bisect
模組實現了一個演算法用於插入元素到有序列表。在一些情況下,這比反覆排序列表或構造一個大的列表再排序的效率更高。Bisect 是二分法的意思,這裡使用二分法來排序,它會將一個元素插入到一個有序列表的合適位置,這使得不需要每次呼叫 sort 的方式維護有序列表。
下面是一個簡單的使用示例:
Python1234567891011121314 | importbisectimportrandomrandom.seed(1)print'New Pos Contents'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 |
輸出結果:
Python12345678910111213141516 | NewPos Contents--------------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*
則用於實際插入。該模組比較典型的應用是計算分數等級:
12345 | defgrade(score,breakpoints=[60,70,80,90],grades='FDCBA'):i=bisect.bisect(breakpoints,score)returngrades[i]print[grade(score)forscore in[33,99,77,70,89,90,100]] |
執行結果:
Python1 | ['F','A','C','C','B','A','A'] |
同樣,我們可以用 bisect 模組實現二分查詢:
Python123456 | defbinary_search_bisect(lst,x):frombisectimportbisect_lefti=bisect_left(lst,x)ifi!=len(lst)andlst[i]==x:returnireturnNone |
我們再來測試一下它與遞迴和迴圈實現的二分查詢的效能:
Python123 | Recursion:4.00940990448Loop:2.6583480835Bisect:1.74922895432 |
可以看到其比迴圈實現略快,比遞迴實現差不多要快一半。
Python 著名的資料處理庫 numpy 也有一個用於二分查詢的函式 numpy.searchsorted, 用法與 bisect 基本相同,只不過如果要右邊插入時,需要設定引數 side='right'
,例如:
1234567891011 | >>>importnumpy asnp>>>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 |
那麼,我們再來比較一下效能:
Python1234567891011121314151617 | In[20]:%timeit-n100bisect_left(data,99999)100loops, |