Двійковий пошук (бісекція) в Python


177

Чи є в бібліотеці функція бібліотеки, яка виконує двійковий пошук у списку / кортежі та повертає позицію елемента, якщо вона знайдена, та "Неправильно" (-1, немає тощо), якщо ні?

Я знайшов функції bisect_left / right в модулі bisect , але вони все одно повертають позицію, навіть якщо елемент не є в списку. Це цілком чудово для їх цільового використання, але я просто хочу знати, чи є елемент у списку чи ні (не хочу нічого вставляти).

Я подумав використати, bisect_leftа потім перевірити, чи предмет у цій позиції дорівнює тому, що я шукаю, але це здається громіздким (і мені також потрібно зробити межу перевірки, чи може число бути більшим за найбільшу кількість у моєму списку). Якщо є приємніший метод, я хотів би знати про це.

Редагування Щоб уточнити, для чого мені це потрібно: я знаю, що словник був би дуже підходящий для цього, але я намагаюся максимально знизити споживання пам'яті. Моє передбачуване використання буде своєрідною таблицею подвійного огляду. У таблиці є список значень, і мені потрібно мати можливість отримати доступ до значень на основі їх індексу. А також я хочу мати змогу знайти індекс певного значення або None, якщо значення немає в списку.

Використання словника для цього було б найшвидшим способом, але (приблизно) подвоїть потреби в пам'яті.

Я задавав це питання, думаючи, що, можливо, я щось не помітив у бібліотеках Python. Здається, мені доведеться написати власний код, як запропонував Мо.


1
Що ви намагаєтеся досягти? Якщо значення унікальні, розгляньте використання набору та "якщо значення у наборі: щось".
Кірк Штраузер

Для чого це варто, "-1" вважається правдою; "0" було б помилковим.
Гліф

3
Я згадав -1, тому що функція, яка повертає індекс шуканого елемента в масиві, може повернути 0, так що -1 повертається, якщо елемент не знайдено (подібно пошуку підрядків).
rslite

3
Якщо ви використовуєте numpy, np.searchsortedце корисно. docs.scipy.org/doc/numpy/reference/generated/…
Роман Шаповалов

Відповіді:


238
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

4
@TomSwirly не такий простий, як ваш, але правильний і все-таки вдосконалення:if hi is None: hi = len(a)
Позначте Ransom

Що щодо порядку зменшення?
Парикшит Чалке

2
Чи можете ви додати якесь пояснення поза кодом? Стандарти тут змінилися.
СС Енн

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 до цього, але тепер дійшов висновку, що це не дуже добре. Якщо ця відповідь буде дотримана, це спричинить багато дублювання коду, і, як ми всі знаємо, це дуже просто f * ck до двійкового пошуку.
abyx

1
не повинно бути hi = mid - 1в elif?
Paweł Prażak

7
@ Paweł: це два еквівалентні варіанти, залежно від того, включена чи виключна верхня межа. Ви можете змінити , hi = midщоб hi = mid-1і hi = len(a)до hi = len(a)-1і while lo < hi:до while lo <= hi, і це було б те ж саме правильне
user102008

2
чому б не зробити щось на кшталт: def binary_search (a, x, lo = 0, hi = None): i = bisect (a, x, lo, hi) повернути i якщо a [i] == x else -1 пробачте за форматування - не впевнений, як це правильно зробити в арсеналі коментарів
Віталій,

1
Вам слід використовувати, bisect.bisect_left()а не це.
аластер

37

Це трохи поза темою (оскільки відповідь Моу видається повною на питання ОП), але, можливо, варто переглянути складність всієї вашої процедури від кінця до кінця. Якщо ви зберігаєте річ у відсортованих списках (там, де допоможе бинарний пошук), а потім просто перевіряєте наявність, ви несете (найгірше, якщо не вказано):

Відсортовані списки

  • O (n журнал n) для початкового створення списку (якщо це несортовані дані. O (n), якщо він відсортований)
  • O (журнал n) пошуку (це частина двійкового пошуку)
  • O (n) вставити / видалити (може бути O (1) або O (log n) середній регістр, залежно від вашого шаблону)

Тоді як з a set(), ви несете відповідальність

  • O (n) творити
  • O (1) пошук
  • O (1) вставити / видалити

Те, що відсортований список дійсно отримує, ви "наступний", "попередній" та "діапазон" (включаючи вставлення або видалення діапазонів), які є O (1) або O (| діапазон |), з урахуванням початкового індексу. Якщо ви не використовуєте подібні операції часто, зберігання у вигляді наборів та сортування для відображення може бути найкращою угодою. set()припадає дуже мало додаткових накладних витрат у python.


7
Є ще одна річ, яку вам відсортовано. O (n) впорядкований обхід. З набором, який є O (n log n), і вам доведеться копіювати посилання на дані в список.
всезначний

1
Правда досить! Дякую, що розширили те, що я мав на увазі під діапазоном пошуку. Fwiw, повний обхід - це той самий запит діапазону між хв, макс, який є O (k), де k = n :)
Грегг Лінд

14

Можливо, варто згадати, що документи бісектіси тепер надають приклади пошуку: http://docs.python.org/library/bisect.html#searching-sorted-lists

(Підвищення ValueError замість повернення -1 або None є більш пітонічним - наприклад, list.index () робить це. Але, звичайно, ви можете адаптувати приклади до своїх потреб.)


11

Найпростіше - скористатися бісектією і перевірити одну позицію назад, щоб побачити, чи є предмет:

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
Добре, але код barfs, якщо ви не переходите у значення 'hi'. Я б написав так: "def binary_search (a, x, lo = 0, hi = None): from bisect import bisect i = bisect (a, x, lo, hi або len (a)) return (i- 1, якщо a [i-1] == x else -1) "і перевірити його так:" для i в діапазоні (1, 20): a = список (діапазон (i)) для aa в a: j = binary_search (a, aa) якщо j! = aa: print i, aa, j "
hughdbrown

8

Це правильно з посібника:

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

8.5.1. Пошук відсортованих списків

Вищеописані функції бісект () корисні для пошуку точок вставки, але їх можна складно або незручно використовувати для поширених завдань пошуку. Наступні п'ять функцій показують, як перетворити їх у стандартні підходи для відсортованих списків:

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

Я погоджуюся, що відповідь @ DaveAbrahams за допомогою модуля бісект - правильний підхід. У своїй відповіді він не згадав жодної важливої ​​деталі.

Від док bisect.bisect_left(a, x, lo=0, hi=len(a))

Модуль розділення не вимагає попереднього обчислення масиву пошуку заздалегідь. Ви можете просто представити кінцеві точки bisect.bisect_leftзамість нього за допомогою значень за замовчуванням 0та len(a).

Ще важливіше для мого використання, шукаючи значення X таке, що помилка даної функції зводиться до мінімуму. Для цього мені знадобився спосіб, щоб алгоритм bisect_left викликав замість мене обчислення. Це дійсно просто.

Просто надайте об'єкт, який визначає __getitem__ якa

Наприклад, ми могли б використовувати алгоритм бісектриси, щоб знайти квадратний корінь з довільною точністю!

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для цього.
Ніл 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

На моїй машині "якщо n в l" зайняло 37 секунд, тоді як "якщо n в d" - 0,4 секунди.


2
Це не завжди хороший варіант з кількох причин: 1) дикти / набори займають більше пам’яті. 2) якщо його в списку не так багато, двійковий пошук може бути швидшим. 3) перетворення списку в дікт - це операція O (n), а двійковий пошук - O (log n).
Джейсон Бейкер

3
Як FYI, "набір" накладних витрат у python порівняно зі списками python, дуже дуже низький. І вони надзвичайно швидкі для пошуку. Там, де двійковий пошук справді перевершує, - це шукати діапазони.
Грегг Лінд

Перетворення списку може бути O (n), але сортування даних у списку, що вам доведеться зробити перед двійковим пошуком, є гіршим. Звідки беруться дані, ви, ймовірно, можете вставити їх у словник під час проходження. Я згоден, що пам'ять може бути проблемою.
Марк Бейкер

4

Це:

  • не рекурсивна (що робить її більш ефективною пам'яттю) ніж більшість рекурсивних підходів)
  • насправді працює
  • швидко , так як вона працює без будь - яких непотрібних якщо х і умов
  • виходячи з математичного твердження, що підлога (низький + високий) / 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

Чи можете ви поділитися тестовими випадками?
життєвий баланс

2

Рішення Дейва Абрахамса добре. Хоча я б зробив це мінімалістично:

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

Очевидно, що ви можете просто повернути індекс, а не значення за цим індексом за бажанням.

Об'єкти

Для користувацьких типів або об'єктів речі трохи складніше: вам потрібно переконатися в застосуванні багатих методів порівняння, щоб отримати бісект для правильного порівняння.

>>> 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 не сподобається подвоїти використання пам'яті, якщо об'єкти, які ви зберігаєте, дійсно крихітні, оскільки значення є лише вказівниками на фактичні об'єкти:

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

У цьому прикладі "foo" зберігається лише один раз. Чи має це значення для вас? І саме про скільки предметів ми говоримо все-таки?


Йдеться про числа і їх багато :) Я хотів би використовувати масив майже такий же великий, як пам'ять комп'ютера. Я знаю, що основа моєї проблеми може бути неправильною, але мені було цікаво відсутність двійкового методу пошуку.
rslite

1
У вас не може бути ключового об'єкта, достатньо малого, щоб визнати його "справді крихітним". Об'єкт матиме мінімальну вартість у 3 слова (тип, знижка, корисне навантаження), тоді як список додає 1 слово, набір додає 1 слово, а дікта додає 2 слова. Усі три (list / set / dict) також певним чином виділяють простір, що є ще одним множником, але все ще недостатньо для значення.
Rhamphoryncus

1

Цей код працює з цілими списками рекурсивно. Шукає найпростіший сценарій випадку: довжина списку менше 2. Це означає, що відповідь вже є, і тест перевіряється на правильність відповіді. Якщо ні, середнє значення встановлюється і перевіряється на правильне, якщо не розбиття виконується шляхом повторного виклику функції, а встановлення середнього значення як верхнього або нижнього межі, зміщення його вліво або вправо

Def binary_search (intList, intValue, lowValue, highValue):
    якщо (highValue - низькийValue) <2:
        повернути intList [lowValue] == intValue або intList [highValue] == intValue
    middleValue = низькаValue + ((highValue - низькаValue) / 2)
    якщо intList [middleValue] == intValue:
        повернути True
    якщо 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 та generic для моделей Django. У моделях Django одна модель може мати зовнішній ключ до іншої моделі, і я хотів виконати деякий пошук на знайдених об'єктах моделей. Я написав таку функцію, яку ви можете використовувати.

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 / generic функції бісектії для двійкового пошуку. З трохи коду навколо функції бісектії, Я думаю, що у мене є приклад нижче, де я перевірив усі випадки на малий рядковий ряд імен. Деякі з вищезазначених рішень натякають на / говорять це, але, сподіваємось, простий код нижче допоможе кому-небудь плутати, як я.

Бісект Python використовується для вказівки, куди потрібно вставити нове значення / елемент пошуку у відсортований список. Нижче наведений код, який використовує bisect_left, який повертає індекс звернення, якщо елемент пошуку в списку / масиві знайдений (Примітка бісектії та bisect_right поверне індекс елемента після потрапляння чи збігу як точку вставки) Якщо не знайдено , bisect_left поверне індекс до наступного елемента в відсортованому списку, який не буде == пошуковим значенням. Єдиний інший випадок, коли елемент пошуку переходитиме в кінці списку, де індекс, що повертається, буде поза кінцем списку / масиву, і який у коді нижче раннього виходу Python з логічними ручками "та". (перша умова 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.