Python 二分查找與 bisect 模塊

Python 的列表(list)內(nèi)部實(shí)現(xiàn)是一個(gè)數(shù)組店诗,也就是一個(gè)線性表裹刮。在列表中查找元素可以使用 list.index() 方法,其時(shí)間復(fù)雜度為O(n)庞瘸。對(duì)于大數(shù)據(jù)量捧弃,則可以用二分查找進(jìn)行優(yōu)化。二分查找要求對(duì)象必須有序,其基本原理如下:

  • 1.從數(shù)組的中間元素開始违霞,如果中間元素正好是要查找的元素嘴办,則搜素過程結(jié)束;
  • 2.如果某一特定元素大于或者小于中間元素买鸽,則在數(shù)組大于或小于中間元素的那一半中查找涧郊,而且跟開始一樣從中間元素開始比較。
  • 3.如果在某一步驟數(shù)組為空眼五,則代表找不到妆艘。

二分查找也成為折半查找,算法每一次比較都使搜索范圍縮小一半看幼, 其時(shí)間復(fù)雜度為 O(logn)批旺。

我們分別用遞歸和循環(huán)來實(shí)現(xiàn)二分查找:

def binary_search_recursion(lst, value, low, high):
    if high < low:
        return None
    mid = (low + high) / 2
    if lst[mid] > value:
        return binary_search_recursion(lst, value, low, mid-1)
    elif lst[mid] < value:
        return binary_search_recursion(lst, value, mid+1, high)
    else:
        return mid

def binary_search_loop(lst,value):
    low, high = 0, len(lst)-1
    while low <= high:
        mid = (low + high) / 2
        if lst[mid] < value:
            low = mid + 1
        elif lst[mid] > value:
            high = mid - 1
        else:
            return mid
    return None

歡迎加入學(xué)習(xí)交流群:【923414804】,學(xué)習(xí)過程中遇到什么問題或者想獲取學(xué)習(xí)資源的話诵姜,我們可以相互幫助汽煮,快來一起學(xué)Python。 

接著對(duì)這兩種實(shí)現(xiàn)進(jìn)行一下性能測(cè)試:

if __name__ == "__main__":
    import random
    lst = [random.randint(0, 10000) for _ in xrange(100000)]
    lst.sort()

    def test_recursion():
        binary_search_recursion(lst, 999, 0, len(lst)-1)

    def test_loop():
        binary_search_loop(lst, 999)

    import timeit
    t1 = 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()

執(zhí)行結(jié)果如下:

Recursion: 3.12596702576
Loop: 2.08254289627

可以看出循環(huán)方式比遞歸效率高棚唆。

Python 有一個(gè) bisect 模塊暇赤,用于維護(hù)有序列表。bisect 模塊實(shí)現(xiàn)了一個(gè)算法用于插入元素到有序列表宵凌。在一些情況下翎卓,這比反復(fù)排序列表或構(gòu)造一個(gè)大的列表再排序的效率更高。Bisect 是二分法的意思摆寄,這里使用二分法來排序,它會(huì)將一個(gè)元素插入到一個(gè)有序列表的合適位置坯门,這使得不需要每次調(diào)用 sort 的方式維護(hù)有序列表微饥。

下面是一個(gè)簡(jiǎn)單的使用示例:

import bisect
import random

random.seed(1)

print'New  Pos Contents'
print'---  --- --------'

l = []
for i in range(1, 15):
    r = random.randint(1, 100)
    position = bisect.bisect(l, r)
    bisect.insort(l, r)
    print'%3d  %3d' % (r, position), l

輸出結(jié)果:

New  Pos Contents
---  --- --------
 14    0 [14]
 85    1 [14, 85]
 77    1 [14, 77, 85]
 26    1 [14, 26, 77, 85]
 50    2 [14, 26, 50, 77, 85]
 45    2 [14, 26, 45, 50, 77, 85]
 66    4 [14, 26, 45, 50, 66, 77, 85]
 79    6 [14, 26, 45, 50, 66, 77, 79, 85]
 10    0 [10, 14, 26, 45, 50, 66, 77, 79, 85]
  3    0 [3, 10, 14, 26, 45, 50, 66, 77, 79, 85]
 84    9 [3, 10, 14, 26, 45, 50, 66, 77, 79, 84, 85]
 44    4 [3, 10, 14, 26, 44, 45, 50, 66, 77, 79, 84, 85]
 77    9 [3, 10, 14, 26, 44, 45, 50, 66, 77, 77, 79, 84, 85]
  1    0 [1, 3, 10, 14, 26, 44, 45, 50, 66, 77, 77, 79, 84, 85]

Bisect模塊提供的函數(shù)有:

  • bisect.bisect_left(a,x, lo=0, hi=len(a)) :

查找在有序列表 a 中插入 x 的index。lo 和 hi 用于指定列表的區(qū)間古戴,默認(rèn)是使用整個(gè)列表欠橘。如果 x 已經(jīng)存在,在其左邊插入现恼。返回值為 index肃续。

  • bisect.bisect_right(a,x, lo=0, hi=len(a))
  • bisect.bisect(a, x,lo=0, hi=len(a)) :

這2個(gè)函數(shù)和 bisect_left 類似,但如果 x 已經(jīng)存在叉袍,在其右邊插入始锚。

  • 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 已經(jīng)存在,在其右邊插入。

Bisect 模塊提供的函數(shù)可以分兩類: bisect* 只用于查找 index姐呐, 不進(jìn)行實(shí)際的插入殿怜;而 insort* 則用于實(shí)際插入。該模塊比較典型的應(yīng)用是計(jì)算分?jǐn)?shù)等級(jí):

def grade(score,breakpoints=[60, 70, 80, 90], grades='FDCBA'):
    i = bisect.bisect(breakpoints, score)
    return grades[i]

print [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]

執(zhí)行結(jié)果:

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

同樣曙砂,我們可以用 bisect 模塊實(shí)現(xiàn)二分查找:

def binary_search_bisect(lst, x):
    from bisect import bisect_left
    i = bisect_left(lst, x)
    if i != len(lst) and lst[i] == x:
        return i
    return None

我們?cè)賮頊y(cè)試一下它與遞歸和循環(huán)實(shí)現(xiàn)的二分查找的性能:

Recursion: 4.00940990448
Loop: 2.6583480835
Bisect: 1.74922895432

可以看到其比循環(huán)實(shí)現(xiàn)略快头谜,比遞歸實(shí)現(xiàn)差不多要快一半。

Python 著名的數(shù)據(jù)處理庫 numpy 也有一個(gè)用于二分查找的函數(shù) numpy.searchsorted鸠澈, 用法與 bisect 基本相同柱告,只不過如果要右邊插入時(shí),需要設(shè)置參數(shù) side='right'款侵,例如:

>>> import numpy as np
>>> from bisect import bisect_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

那么末荐,我們?cè)賮肀容^一下性能:

In [20]: %timeit -n 100 bisect_left(data, 99999)
100 loops, best of 3: 670 ns per loop

In [21]: %timeit -n 100 np.searchsorted(data, 99999)
100 loops, best of 3: 56.9 ms per loop

In [22]: %timeit -n 100 bisect_left(data, 8888)
100 loops, best of 3: 961 ns per loop

In [23]: %timeit -n 100 np.searchsorted(data, 8888)
100 loops, best of 3: 57.6 ms per loop

In [24]: %timeit -n 100 bisect_left(data, 777777)
100 loops, best of 3: 670 ns per loop

In [25]: %timeit -n 100 np.searchsorted(data, 777777)
100 loops, best of 3: 58.4 ms per loop

可以發(fā)現(xiàn) numpy.searchsorted 效率是很低的,跟 bisect 根本不在一個(gè)數(shù)量級(jí)上新锈。因此 searchsorted 不適合用于搜索普通的數(shù)組甲脏,但是它用來搜索 numpy.ndarray 是相當(dāng)快的:

In [30]: data_ndarray = np.arange(0, 1000000)

In [31]: %timeit np.searchsorted(data_ndarray, 99999)
The slowest run took 16.04 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 996 ns per loop

In [32]: %timeit np.searchsorted(data_ndarray, 8888)
The slowest run took 18.22 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 994 ns per loop

In [33]: %timeit np.searchsorted(data_ndarray, 777777)
The slowest run took 31.32 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 990 ns per loop

numpy.searchsorted 可以同時(shí)搜索多個(gè)值:

>>> 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])
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市妹笆,隨后出現(xiàn)的幾起案子块请,更是在濱河造成了極大的恐慌,老刑警劉巖拳缠,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墩新,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡窟坐,警方通過查閱死者的電腦和手機(jī)海渊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哲鸳,“玉大人臣疑,你說我怎么就攤上這事♂悴ぃ” “怎么了讯沈?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)婿奔。 經(jīng)常有香客問我缺狠,道長(zhǎng),這世上最難降的妖魔是什么萍摊? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任挤茄,我火速辦了婚禮,結(jié)果婚禮上冰木,老公的妹妹穿的比我還像新娘驮樊。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布囚衔。 她就那樣靜靜地躺著挖腰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪练湿。 梳的紋絲不亂的頭發(fā)上猴仑,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音肥哎,去河邊找鬼辽俗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛篡诽,可吹牛的內(nèi)容都是我干的崖飘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼杈女,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼朱浴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起达椰,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤翰蠢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后啰劲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梁沧,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蝇裤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了廷支。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡栓辜,死狀恐怖恋拍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情啃憎,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布似炎,位于F島的核電站辛萍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏羡藐。R本人自食惡果不足惜贩毕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望仆嗦。 院中可真熱鬧辉阶,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至规辱,卻和暖如春谆棺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背罕袋。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國打工改淑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留表悬,地道東北人壁袄。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像辣吃,于是被迫代替她去往敵國和親榆纽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仰猖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容