Эффективность Python в ключевом слове для отсортированного списка

Если у меня есть список, который уже отсортирован и я использую ключевое слово in, например:

a = [1,2,5,6,8,9,10]
print 8 in a

Я думаю, что это должно выполнять последовательный поиск, но могу ли я ускорить его, выполнив двоичный поиск? Есть ли питонический способ поиска в отсортированном списке?


person off99555    schedule 14.04.2016    source источник
comment
«Я думаю, что это должен сделать последовательный поиск». Как вы думаете, почему это происходит?   -  person    schedule 14.04.2016
comment
преобразовать его в набор, а затем использовать в   -  person Benjamin    schedule 14.04.2016
comment
@Lutz Потому что интерпретатор не может волшебным образом понять, что список отсортирован?   -  person Voo    schedule 14.04.2016
comment
@Voo Это вопрос или утверждение?   -  person    schedule 14.04.2016
comment
@Lutz def is_in(some_arr, val): return val in some_arr - как, по вашему мнению, интерпретатор должен выяснить, отсортировано ли some_arr или нет. Ясно, что это невозможно, поэтому он не может этого сделать. Что ж, у него может быть дополнительная проверка, чтобы выяснить, отсортирован ли список, а затем использовать двоичный поиск, но, поскольку для этого требуется пройти весь список, это скорее противоречит цели.   -  person Voo    schedule 14.04.2016
comment
@Benjamin: преобразование в набор полезно, только если вы хотите выполнить несколько тестов in. Если список отсортирован, деление пополам (O(logN)) превзойдет преобразование в набор (O(N)).   -  person Martijn Pieters    schedule 14.04.2016
comment
@AnttiHaapala: bisect имеет ускорение C.   -  person Martijn Pieters    schedule 14.04.2016
comment
@MartijnPieters Я исправлен: d. Кажется, он был там с Python 2.4.   -  person Antti Haapala    schedule 14.04.2016
comment
Я ожидал, что python может иметь инвариант класса, такой как SortedList, поэтому при использовании вместе с оператором in он будет использовать оптимизированный двоичный поиск вместо последовательного поиска. Вот почему возник этот вопрос. И я думаю, что это довольно общий вопрос, но я не смог найти никого, кто бы его задавал, поэтому я задал его сам.   -  person off99555    schedule 14.04.2016


Ответы (2)


В стандартной библиотеке есть бинарный поиск для Python, в модуле bisect. Он не поддерживает in/contains как есть, но вы можете написать небольшую функцию для его обработки:

from bisect import bisect_left
def contains(a, x):
    """returns true if sorted sequence `a` contains `x`"""
    i = bisect_left(a, x)
    return i != len(a) and a[i] == x

потом

>>> contains([1,2,3], 3)
True
>>> contains([1,2,3], 4)
False

Однако это не будет очень быстрым, так как bisect написан на Python, а не на C, поэтому во многих случаях вы, вероятно, обнаружите, что последовательный in быстрее. bisect имеет необязательный C ускорение в CPython начиная с Python 2.4.

Трудно определить точную точку безубыточности в CPython. Это потому, что код написан на C; если вы проверите значение, которое больше или меньше любого значения в последовательности, то предсказание ветвления ЦП сыграет с вами злую шутку, и вы получите:

In [2]: a = list(range(100))
In [3]: %timeit contains(a, 101)
The slowest run took 8.09 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 370 ns per loop

Здесь лучшее из 3 не отражает истинное время работы алгоритма.

Но, настроив тесты, я пришел к выводу, что деление пополам может быть быстрее, чем in для списков, содержащих всего 30 элементов.


Однако, если вы выполняете действительно много in операций, вам следует использовать set; вы можете преобразовать список один раз в набор (он даже не будет отсортирован), и операция in будет асимптотически быстрее, чем любой двоичный поиск:

>>> a = [10, 6, 8, 1, 2, 5, 9]
>>> a_set = set(a)
>>> 10 in a_set
True

С другой стороны, сортировка списка требует большей временной сложности, чем создание набора, поэтому в большинстве случаев лучше всего использовать набор.

person Antti Haapala    schedule 14.04.2016

В стандартной библиотеке есть модуль bisect, поддерживающий поиск в отсортированных последовательностях.

Однако для небольших списков я готов поспорить, что реализация оператора in на C превзойдет bisect. Вам придется провести измерения с кучей распространенных случаев, чтобы определить реальную точку безубыточности на вашем целевом оборудовании...


Стоит отметить, что если вы можете обойтись без неупорядоченной итерации (например, set), то вы можете выполнить поиск в среднем за O(1) времени (используя оператор in), по сравнению с делением пополам последовательности, которая является O(logN) и оператором in в последовательности, которая O(N). И с набором вы также избегаете затрат на его сортировку в первую очередь :-).

person mgilson    schedule 14.04.2016
comment
Я провел несколько тестов, точка безубыточности на самом деле довольно мала; около 30 целых чисел в диапазоне 0-60, если половина поисков будет пропущена. - person Antti Haapala; 14.04.2016
comment
@AnttiHaapala - звучит довольно разумно. Спасибо за это :-). Очень интересно проводить такие тесты на скомпилированных языках, таких как C или Fortran. Тогда местоположение кеша и предсказание ветвления могут начать реально влиять на вашу среду выполнения. - person mgilson; 14.04.2016