1. 程式人生 > >Python 二分查詢與 bisect 模組

Python 二分查詢與 bisect 模組

Python 的列表(list)內部實現是一個數組,也就是一個線性表。在列表中查詢元素可以使用 list.index() 方法,其時間複雜度為O(n)。對於大資料量,則可以用二分查詢進行優化。二分查詢要求物件必須有序,其基本原理如下:

  • 1.從陣列的中間元素開始,如果中間元素正好是要查詢的元素,則搜素過程結束;
  • 2.如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中查詢,而且跟開始一樣從中間元素開始比較。
  • 3.如果在某一步驟陣列為空,則代表找不到。

二分查詢也成為折半查詢,演算法每一次比較都使搜尋範圍縮小一半, 其時間複雜度為 O(logn)。

我們分別用遞迴和迴圈來實現二分查詢:

Python
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+
1,high)else:returnmid  defbinary_search_loop(lst,value):low,high=0,len(lst)-1whilelow<=high:mid=(low+high)/2iflst[mid]<value:low=mid+1eliflst[mid]>value:high=mid-1else:returnmid  returnNone

接著對這兩種實現進行一下效能測試:

Python
1234567891011121314151617 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()

執行結果如下:

Python
12 Recursion:3.12596702576Loop:2.08254289627

可以看出迴圈方式比遞迴效率高。

Python 有一個 bisect 模組,用於維護有序列表。bisect 模組實現了一個演算法用於插入元素到有序列表。在一些情況下,這比反覆排序列表或構造一個大的列表再排序的效率更高。Bisect 是二分法的意思,這裡使用二分法來排序,它會將一個元素插入到一個有序列表的合適位置,這使得不需要每次呼叫 sort 的方式維護有序列表。

下面是一個簡單的使用示例:

Python
1234567891011121314 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

輸出結果:

Python
12345678910111213141516 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* 則用於實際插入。該模組比較典型的應用是計算分數等級:

Python
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]]

執行結果:

Python
1 ['F','A','C','C','B','A','A']

同樣,我們可以用 bisect 模組實現二分查詢:

Python
123456 defbinary_search_bisect(lst,x):frombisectimportbisect_lefti=bisect_left(lst,x)ifi!=len(lst)andlst[i]==x:returnireturnNone

我們再來測試一下它與遞迴和迴圈實現的二分查詢的效能:

Python
123 Recursion:4.00940990448Loop:2.6583480835Bisect:1.74922895432

可以看到其比迴圈實現略快,比遞迴實現差不多要快一半。

Python 著名的資料處理庫 numpy 也有一個用於二分查詢的函式 numpy.searchsorted, 用法與 bisect 基本相同,只不過如果要右邊插入時,需要設定引數 side='right',例如:

Python
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

那麼,我們再來比較一下效能:

Python
1234567891011121314151617 In[20]:%timeit-n100bisect_left(data,99999)100loops,