Pythonでのバイナリ検索(二分)


176

リスト/タプルでバイナリ検索を実行し、見つかった場合はアイテムの位置を返し、見つからない場合は「False」(-1、なしなど)を返すライブラリ関数はありますか?

bisectモジュールで関数bisect_left / rightを見つけましたが、アイテムがリストにない場合でも位置を返します。それは彼らの意図する使用法には完全に問題ありませんが、私はアイテムがリストにあるかどうかを知りたいだけです(何も挿入したくない)。

bisect_leftその位置にあるアイテムが私が検索しているものと等しいかどうかを使用して確認することを考えましたが、それは面倒に思われます(そして、番号がリスト内の最大の番号よりも大きくなる可能性があるかどうかも境界チェックを行う必要があります)。より良い方法がある場合、それについて知りたいのですが。

編集これが何のために必要なのかを明確にするために:辞書がこれに非常に適していることは承知していますが、メモリ消費量をできるだけ低く保つようにしています。私の使用目的は、一種の双方向ルックアップテーブルになります。テーブルに値のリストがあり、それらのインデックスに基づいて値にアクセスできるようにする必要があります。また、特定の値のインデックス、または値がリストにない場合はNoneを見つけられるようにしたいと思います。

これに辞書を使用するのが最も速い方法ですが、メモリ要件が(約)2倍になります。

私は、Pythonライブラリーで何かを見落とした可能性があると考えて、この質問をしていました。萌さんが示唆したように、自分でコードを書かなければならないようです。


1
あなたが達成しようとしていることは何ですか?値が一意である場合は、セットと「セット内の値:何か」の使用を検討してください。
カークストラウザー2008年

価値がある場合、「-1」は真と見なされます。「0」は偽です。
グリフ

3
配列で検索された項目のインデックスを返す関数はすでに0を返すことができるため、-1を述べました。そのため、項目が見つからない場合は-1が返されます(部分文字列検索と同様)。
rslite 2008年

3
numpyを使用するnp.searchsortedと便利です。docs.scipy.org/doc/numpy/reference/generated/…–
Roman

回答:


237
from bisect import bisect_left

def binary_search(a, x, lo=0, hi=None):  # can't use a to specify default for hi
    hi = hi if hi is not None else len(a)  # hi defaults to len(a)   
    pos = bisect_left(a, x, lo, hi)  # find insertion position
    return pos if pos != hi and a[pos] == x else -1  # don't walk off the end

10
@volcano binsearchも一般的です。
cubuspl42 14

4
@TomSwirlyないあなたのようにシンプルですが、正しいとまだ改善など:if hi is None: hi = len(a)
マーク身代金

降順はどうですか?
Parikshit Chalke

2
コードの外側に説明を追加できますか?ここの基準が変更されました。
SSアン

54

bisect_left / rightのコードを見て、目的に合わせて調整してみませんか。

このような:

def binary_search(a, x, lo=0, hi=None):
    if hi is None:
        hi = len(a)
    while lo < hi:
        mid = (lo+hi)//2
        midval = a[mid]
        if midval < x:
            lo = mid+1
        elif midval > x: 
            hi = mid
        else:
            return mid
    return -1

29
私はもともとこれを+1していましたが、今ではこれは良いことではないと結論に達しました。この答えに従うと、コードの重複が多く発生します。ご存じのように、バイナリ検索を行うのは非常に簡単です。
abyx 2009年

1
それはすべきではありませんhi = mid - 1elif
パヴェルPrażak

7
@Paweł:上限が含まれるか含まれないかに応じて、これらは2つの同等のバリアントです。あなたは変更することができますhi = midhi = mid-1してhi = len(a)までhi = len(a)-1while lo < hi:while lo <= hi、それが同等に正しいだろう
user102008

2
なぜ次のようなことをしないでください:def binary_search(a、x、lo = 0、hi = None):i = bisect(a、x、lo、hi)if if a [i] == x else -1 sorry for the書式設定-コメント領域でこれを適切に行う方法が不明
Vitali

1
bisect.bisect_left()これよりも使うべきです。
alastair

37

これは少し話題から外れていますが(OPの質問に対してMoeの回答は完全であると思われるため)、手順全体の複雑さを最初から最後まで検討する価値があるかもしれません。ソートされたリスト(バイナリ検索が役立つ場所)に格納していて、存在を確認しているだけの場合は、(特に指定がない限り、最悪の場合)発生します。

ソートされたリスト

  • O(n log n)は、最初にリストを作成します(ソートされていないデータの場合。O(n)、ソートされている場合)
  • O(log n)ルックアップ(これはバイナリ検索部分です)
  • O(n)挿入/削除(パターンに応じて、O(1)またはO(log n)の平均的なケースになる場合があります)

に対してset()、あなたは負担しています

  • 作成するO(n)
  • O(1)ルックアップ
  • O(1)挿入/削除

ソートされたリストが実際にあなたに与えるものは、「次」、「前」、および「範囲」(範囲の挿入または削除を含む)であり、開始インデックスが指定されたO(1)またはO(| range |)です。これらの種類の操作を頻繁に使用しない場合は、セットとして保存し、表示用に並べ替える方が全体的に適切です。 set()Pythonでは追加のオーバーヘッドはほとんど発生しません。


7
ソートされたリストを使用すると、もう1つことがわかります。O(n)の順序付けられた走査。O(n log n)のセットでは、データへの参照をリストにコピーする必要があります。
10

1
本当に本当です!範囲検索の意味を拡張していただき、ありがとうございます。Fwiw、フルトラバーサルは、最小値と最大値の間の範囲クエリと同じです。つまり、O(k)です。k= n :)
Gregg Lind


11

最も簡単なのは、bisectを使用し、1つの位置をチェックして、アイテムが存在するかどうかを確認することです。

def binary_search(a,x,lo=0,hi=-1):
    i = bisect(a,x,lo,hi)
    if i == 0:
        return -1
    elif a[i-1] == x:
        return i-1
    else:
        return -1

2
いいですが、「hi」値を渡さないとコードbarfsになります。私は次のように書きます: "def binary_search(a、x、lo = 0、hi = None):from bisect import bisect i = bisect(a、x、lo、hi or len(a))return(i- 1 if a [i-1] == x else -1) "そして、次のようにテストします:" for i in range(1、20):a = list(range(i))for aa in a:j = binary_search (a、aa)if j!= aa:print i、aa、j "
hughdbrown 2009

8

これはマニュアルから正しいです:

http://docs.python.org/2/library/bisect.html

8.5.1。ソートされたリストの検索

上記のbisect()関数は挿入ポイントを見つけるのに役立ちますが、一般的な検索タスクに使用するのは扱いにくいか扱いにくい場合があります。次の5つの関数は、それらをソート済みリストの標準ルックアップに変換する方法を示しています。

def index(a, x):
    'Locate the leftmost value exactly equal to x'
    i = bisect_left(a, x)
    if i != len(a) and a[i] == x:
        return i
    raise ValueError

したがって、少し変更すると、コードは次のようになります。

def index(a, x):
    'Locate the leftmost value exactly equal to x'
    i = bisect_left(a, x)
    if i != len(a) and a[i] == x:
        return i
    return -1

6

bisectモジュールを使用した@DaveAbrahamsの答えが正しいアプローチであることに同意します。彼は彼の答えで1つの重要な詳細に言及しませんでした。

ドキュメントから bisect.bisect_left(a, x, lo=0, hi=len(a))

二分モジュールでは、検索配列を事前に事前計算する必要はありません。とbisect.bisect_leftのデフォルトを使用して、エンドポイントをの代わりに提示することができます。0len(a)

私の使用にとってさらに重要なのは、特定の関数のエラーが最小になるような値Xを探すことです。そのために、代わりにbisect_leftのアルゴリズムで計算を呼び出す方法が必要でした。これは本当に簡単です。

__getitem__として定義するオブジェクトを提供するだけですa

たとえば、bisectアルゴリズムを使用して、任意の精度の平方根を見つけることができます。

import bisect

class sqrt_array(object):
    def __init__(self, digits):
        self.precision = float(10**(digits))
    def __getitem__(self, key):
        return (key/self.precision)**2.0

sa = sqrt_array(4)

# "search" in the range of 0 to 10 with a "precision" of 0.0001
index = bisect.bisect_left(sa, 7, 0, 10*10**4)
print 7**0.5
print index/(10**4.0)

これはきれいではありません。これに使用scipy.optimizeします。
Neil G

4

それが存在するかどうかを確認したいだけの場合は、リストを辞書に変えてみてください。

# Generate a list
l = [n*n for n in range(1000)]

# Convert to dict - doesn't matter what you map values to
d = dict((x, 1) for x in l)

count = 0
for n in range(1000000):
    # Compare with "if n in l"
    if n in d:
        count += 1

私のマシンでは、「if n in l」は37秒かかりましたが、「if n in d」は0.4秒かかりました。


2
これは、いくつかの理由で必ずしも良いオプションではありません。1)辞書/セットがより多くのメモリを消費します。2)リストに多くない場合は、バイナリ検索の方が高速です。3)リストを辞書に変換するのはO(n)操作ですが、バイナリ検索はO(log n)です。
Jason Baker、

3
参考までに、Pythonリストと比較してPythonの「設定」オーバーヘッドは非常に低いです。そして、それらは検索に対して非常に高速です。バイナリ検索が本当に優れているのは、範囲の検索です。
グレッグリンド

リストの変換はO(n)の場合がありますが、リスト内のデータの並べ替えは、バイナリ検索を行う前に行う必要があります。どこからデータを取得するかは、移動するときにおそらく辞書に挿入できます。メモリが問題である可能性があることに同意します。
Mark Ba​​ker、

4

これは:

  • 再帰的ではない(これにより、ほとんどの再帰的アプローチよりもメモリ効率が向上します
  • 実際に働いている
  • ifと条件が不必要なく実行されるため、高速
  • 数学的主張に基づくこと(高、低+)の床/ 2は常によりも小さい低いが下限となる高いが上限です。

def binsearch(t, key, low = 0, high = len(t) - 1):
    # bisecting the range
    while low < high:
        mid = (low + high)//2
        if t[mid] < key:
            low = mid + 1
        else:
            high = mid
    # at this point 'low' should point at the place
    # where the value of 'key' is possibly stored.
    return low if t[low] == key else -1

テストケースを共有できますか?
ライフバランス2017

2

Dave Abrahamsのソリューションは優れています。私はそれを最小限にしたでしょうが:

def binary_search(L, x):
    i = bisect.bisect_left(L, x)
    if i == len(L) or L[i] != x:
        return -1
    return i

2

Pythonには明示的なバイナリ検索アルゴリズムはありbisectませんが、バイナリ検索を使用して、ソートされたリスト内の要素の挿入ポイントを見つけるように設計されたモジュールがあります。これは、二分探索を実行するために「だまされ」ます。これの最大の利点は、ほとんどのライブラリコードが持つ利点と同じです-高性能で十分にテストされており、機能します(特に、エッジケースを慎重に検討しないと、バイナリ検索を正常に実装するのは非常に難しい場合があります)。

基本的なタイプ

Stringsやintsのような基本的なタイプの場合、それは非常に簡単です。必要なのはbisectモジュールとソートされたリストだけです。

>>> import bisect
>>> names = ['bender', 'fry', 'leela', 'nibbler', 'zoidberg']
>>> bisect.bisect_left(names, 'fry')
1
>>> keyword = 'fry'
>>> x = bisect.bisect_left(names, keyword)
>>> names[x] == keyword
True
>>> keyword = 'arnie'
>>> x = bisect.bisect_left(names, keyword)
>>> names[x] == keyword
False

これを使用して重複を見つけることもできます。

...
>>> names = ['bender', 'fry', 'fry', 'fry', 'leela', 'nibbler', 'zoidberg']
>>> keyword = 'fry'
>>> leftIndex = bisect.bisect_left(names, keyword)
>>> rightIndex = bisect.bisect_right(names, keyword)
>>> names[leftIndex:rightIndex]
['fry', 'fry', 'fry']

もちろん、必要に応じて、インデックスの値ではなくインデックスを返すこともできます。

オブジェクト

カスタム型またはカスタムオブジェクトの場合、少し注意が必要です。bisectを正しく比較するには、豊富な比較メソッドを実装する必要があります。

>>> import bisect
>>> class Tag(object):  # a simple wrapper around strings
...     def __init__(self, tag):
...         self.tag = tag
...     def __lt__(self, other):
...         return self.tag < other.tag
...     def __gt__(self, other):
...         return self.tag > other.tag
...
>>> tags = [Tag('bender'), Tag('fry'), Tag('leela'), Tag('nibbler'), Tag('zoidbe
rg')]
>>> key = Tag('fry')
>>> leftIndex = bisect.bisect_left(tags, key)
>>> rightIndex = bisect.bisect_right(tags, key)
>>> print([tag.tag for tag in tags[leftIndex:rightIndex]])
['fry']

これは少なくともPython 2.7-> 3.3で動作するはずです


1

値は実際のオブジェクトへのポインタにすぎないため、dictを使用しても、格納しているオブジェクトが本当に小さい場合を除いて、メモリ使用量が2倍になることは望ましくありません。

>>> a = 'foo'
>>> b = [a]
>>> c = [a]
>>> b[0] is c[0]
True

その例では、「foo」は一度だけ格納されます。違いはありますか?とにかく、正確にいくつのアイテムについて話しているのですか?


それは数とそれらの多くについてです:)私はコンピュータのメモリと同じくらい大きい配列を使いたいです。私の問題の根本が間違っている可能性があることは知っていますが、二分探索法の欠如に興味がありました。
rslite 2008年

1
ここでは、「本当に小さい」とみなすのに十分なほど小さいキーオブジェクトは使用できません。オブジェクトの最小コストは3ワード(type、refcount、payload)ですが、リストは1ワードを追加し、セットは1ワードを追加し、dictは2ワードを追加します。3つすべて(list / set / dict)も何らかの方法で領域を事前割り当てします。これは別の乗数ですが、問題になるほど十分ではありません。
ランフォリンクス

1

このコードは、整数リストを再帰的に処理します。最も単純なケースシナリオを探します。つまり、リストの長さが2未満です。これは、回答がすでに存在し、テストが実行されて正しい回答を確認することを意味します。そうでない場合、中間値が設定され、正しいかどうかがテストされます。そうでない場合、関数を再度呼び出して二分されますが、中央値を上限または下限として設定し、左または右にシフトします。

def binary_search(intList、intValue、lowValue、highValue):
    if(highValue-lowValue)<2:
        return intList [lowValue] == intValueまたはintList [highValue] == intValue
    middleValue = lowValue +((highValue-lowValue)/ 2)
    if intList [middleValue] == intValue:
        Trueを返す
    if intList [middleValue]> intValue:
        binary_search(intList、intValue、lowValue、middleValue-1)を返します
   binary_search(intList、intValue、middleValue + 1、highValue)を返します

1

ウィキペディアの例を確認してくださいhttp://en.wikipedia.org/wiki/Binary_search_algorithm

def binary_search(a, key, imin=0, imax=None):
    if imax is None:
        # if max amount not set, get the total
        imax = len(a) - 1

    while imin <= imax:
        # calculate the midpoint
        mid = (imin + imax)//2
        midval = a[mid]

        # determine which subarray to search
        if midval < key:
            # change min index to search upper subarray
            imin = mid + 1
        elif midval > key:
            # change max index to search lower subarray
            imax = mid - 1
        else:
            # return index number 
            return mid
    raise ValueError

0
'''
Only used if set your position as global
'''
position #set global 

def bst(array,taget): # just pass the array and target
        global position
        low = 0
        high = len(array)
    while low <= high:
        mid = (lo+hi)//2
        if a[mid] == target:
            position = mid
            return -1
        elif a[mid] < target: 
            high = mid+1
        else:
            low = mid-1
    return -1

これははるかに良く、効果的だと思います。私を訂正してください:)。ありがとうございました


0
  • s リストです。
  • binary(s, 0, len(s) - 1, find) 最初の呼び出しです。
  • 関数は、照会されたアイテムのインデックスを返します。そのようなアイテムがない場合、それは戻ります-1

    def binary(s,p,q,find):
        if find==s[(p+q)/2]:
            return (p+q)/2
        elif p==q-1 or p==q:
            if find==s[q]:
                return q
            else:
                return -1
        elif find < s[(p+q)/2]:
            return binary(s,p,(p+q)/2,find)
        elif find > s[(p+q)/2]:
            return binary(s,(p+q)/2+1,q,find)

0
def binary_search_length_of_a_list(single_method_list):
    index = 0
    first = 0
    last = 1

    while True:
        mid = ((first + last) // 2)
        if not single_method_list.get(index):
            break
        index = mid + 1
        first = index
        last = index + 1
    return mid

0

バイナリ検索:

// List - values inside list
// searchItem - Item to search
// size - Size of list
// upperBound - higher index of list
// lowerBound - lower index of list
def binarySearch(list, searchItem, size, upperBound, lowerBound):
        print(list)
        print(upperBound)
        print(lowerBound)
        mid = ((upperBound + lowerBound)) // 2
        print(mid)
        if int(list[int(mid)]) == value:
               return "value exist"
        elif int(list[int(mid)]) < value:
             return searchItem(list, value, size, upperBound, mid + 1)
        elif int(list[int(mid)]) > value:
               return searchItem(list, value, size, mid - 1, lowerBound)

//上記の関数を呼び出すには:

list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
searchItem = 1        
print(searchItem(list[0], item, len(list[0]) -1, len(list[0]) - 1, 0))

0

Pythonでのバイナリ検索とDjangoモデルのジェネリックが必要でした。Djangoモデルでは、1つのモデルが別のモデルへの外部キーを持つことがあり、取得したモデルオブジェクトで検索を実行したいと考えていました。これを使える関数を以下に書きました。

def binary_search(values, key, lo=0, hi=None, length=None, cmp=None):
    """
    This is a binary search function which search for given key in values.
    This is very generic since values and key can be of different type.
    If they are of different type then caller must specify `cmp` function to
    perform a comparison between key and values' item.
    :param values:  List of items in which key has to be search
    :param key: search key
    :param lo: start index to begin search
    :param hi: end index where search will be performed
    :param length: length of values
    :param cmp: a comparator function which can be used to compare key and values
    :return: -1 if key is not found else index
    """
    assert type(values[0]) == type(key) or cmp, "can't be compared"
    assert not (hi and length), "`hi`, `length` both can't be specified at the same time"

    lo = lo
    if not lo:
        lo = 0
    if hi:
        hi = hi
    elif length:
        hi = length - 1
    else:
        hi = len(values) - 1

    while lo <= hi:
        mid = lo + (hi - lo) // 2
        if not cmp:
            if values[mid] == key:
                return mid
            if values[mid] < key:
                lo = mid + 1
            else:
                hi = mid - 1
        else:
            val = cmp(values[mid], key)
            # 0 -> a == b
            # > 0 -> a > b
            # < 0 -> a < b
            if val == 0:
                return mid
            if val < 0:
                lo = mid + 1
            else:
                hi = mid - 1
    return -1

0

上記の多くの優れた解決策はありますが、単純な(KISSは単純な(私が原因で)バイナリー検索を実行するためのPython組み込み/ジェネリックbisect関数の愚かな使用を維持しています)を見たことはありません。名前の小さな文字列配列のすべてのケースをテストした以下の例があると思います。上記の解決策のいくつかはこれを暗示していますが、以下の簡単なコードが私と同じように混乱している人に役立つことを願っています。

Python bisectは、新しい値/検索項目をソートされたリストに挿入する場所を示すために使用されます。リスト/配列内の検索項目が見つかった場合にヒットのインデックスを返すbisect_leftを使用する以下のコード(注意:bisectおよびbisect_rightは、ヒットまたは一致後に挿入ポイントとして要素のインデックスを返します)見つからない場合、bisect_leftは、ソートされたリストの次の項目へのインデックスを返しますが、==検索値ではありません。他の唯一のケースは、検索項目がリストの最後に移動し、返されたインデックスがリスト/配列の最後を超え、Pythonによる初期の終了の下のコードで「and」ロジックハン​​ドルを使用する場合です。(最初の条件False Pythonは後続の条件をチェックしません)

#Code
from bisect import bisect_left
names=["Adam","Donny","Jalan","Zach","Zayed"]
search=""
lenNames = len(names)
while search !="none":
    search =input("Enter name to search for or 'none' to terminate program:")
    if search == "none":
        break
    i = bisect_left(names,search)
    print(i) # show index returned by Python bisect_left
    if i < (lenNames) and names[i] == search:
        print(names[i],"found") #return True - if function
    else:
        print(search,"not found") #return False – if function
##Exhaustive test cases:
##Enter name to search for or 'none' to terminate program:Zayed
##4
##Zayed found
##Enter name to search for or 'none' to terminate program:Zach
##3
##Zach found
##Enter name to search for or 'none' to terminate program:Jalan
##2
##Jalan found
##Enter name to search for or 'none' to terminate program:Donny
##1
##Donny found
##Enter name to search for or 'none' to terminate program:Adam
##0
##Adam found
##Enter name to search for or 'none' to terminate program:Abie
##0
##Abie not found
##Enter name to search for or 'none' to terminate program:Carla
##1
##Carla not found
##Enter name to search for or 'none' to terminate program:Ed
##2
##Ed not found
##Enter name to search for or 'none' to terminate program:Roger
##3
##Roger not found
##Enter name to search for or 'none' to terminate program:Zap
##4
##Zap not found
##Enter name to search for or 'none' to terminate program:Zyss
##5
##Zyss not found
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.