Ответ 1
Что нужно попробовать:
- Предварительно обработайте диапазоны, чтобы они не перекрывались, и выражали их как полуоткрытые интервалы.
- Используйте
bisect
для выполнения поиска. Обратите внимание, что при предварительной обработке в 1 все, что вам нужно знать, - это то, является ли результат вызоваbisect
четным или нечетным. - Если пакетная обработка запросов является опцией, рассмотрите группировку ваших входов в массив и используя
numpy.searchsorted
.
Некоторые коды и тайминги. Сначала настройте (здесь используйте IPython 2.1 и Python 3.4):
In [1]: ranges = [(1, 5), (10, 20), (40, 50)]
In [2]: nums = list(range(1000000)) # force a list to remove generator overhead
Сроки для исходного метода на моей машине (но с выражением генератора вместо понимания списка):
In [3]: %timeit [n for n in nums if any(r[0] <= n <= r[1] for r in ranges)]
1 loops, best of 3: 922 ms per loop
Теперь мы перерабатываем диапазоны как список граничных точек; каждая граничная точка с четным индексом является точкой входа в один из диапазонов, а каждая граничная точка с нечетным индексом является точкой выхода. Обратите внимание на преобразование в полуоткрытые интервалы и что я поместил все числа в один список.
In [4]: boundaries = [1, 6, 10, 21, 40, 51]
С этим легко использовать bisect.bisect
, чтобы получить те же результаты, что и раньше, но быстрее.
In [5]: from bisect import bisect
In [6]: %timeit [n for n in nums if bisect(boundaries, n) % 2]
1 loops, best of 3: 298 ms per loop
Наконец, в зависимости от контекста вы можете использовать функцию searchsorted
из NumPy. Это похоже на bisect.bisect
, но работает сразу с целыми наборами значений. Например:
In [7]: import numpy
In [8]: numpy.where(numpy.searchsorted(boundaries, nums, side="right") % 2)[0]
Out[8]:
array([ 1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50])
На первый взгляд, результаты %timeit
весьма разочаровывают.
In [9]: %timeit numpy.where(numpy.searchsorted(boundaries, nums, side="right") % 2)[0]
10 loops, best of 3: 159 ms per loop
Однако оказывается, что большая часть затрат на производительность заключается в преобразовании входов в searchsorted
из списков Python в массивы NumPy. Пусть предварительно преобразует оба списка в массивы и повторите попытку:
In [10]: boundaries = numpy.array(boundaries)
In [11]: nums = numpy.array(nums)
In [12]: %timeit numpy.where(numpy.searchsorted(boundaries, nums, side="right") % 2)[0]
10 loops, best of 3: 24.6 ms per loop
Гораздо быстрее, чем что-либо еще. Однако это немного изменяет: мы можем, конечно, препроцессить boundaries
, чтобы превратить его в массив, но если значения, которые вы хотите протестировать, естественно не создаются в форме массива, тогда необходимо будет учитывать стоимость конверсии, С другой стороны, это показывает, что стоимость самого поиска может быть уменьшена до достаточно малого значения, которое больше не будет доминирующим фактором в течение времени выполнения.
Вот еще один вариант в этих строках. Он снова использует NumPy, но делает прямой нелинейный линейный поиск за значение. (Пожалуйста, простите приглашения IPython
не по порядку: я добавил это позже.: -)
In [29]: numpy.where(numpy.logical_xor.reduce(numpy.greater_equal.outer(boundaries, nums), axis=0))
Out[29]:
(array([ 2, 3, 4, 5, 6, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 41,
42, 43, 44, 45, 46, 47, 48, 49, 50, 51]),)
In [30]: %timeit numpy.where(numpy.logical_xor.reduce(numpy.greater_equal.outer(boundaries, nums), axis=0))
10 loops, best of 3: 16.7 ms per loop
Для этих конкретных тестовых данных это быстрее, чем searchsorted
, но время будет расти линейно в количестве диапазонов, тогда как для searchsorted
оно должно расти в соответствии с журналом количества диапазонов. Обратите внимание, что он также использует объем памяти, пропорциональный len(boundaries) * len(nums)
. Это не обязательно проблема: если вы обнаруживаете, что сталкиваетесь с ограничениями памяти, вы, вероятно, можете объединить массивы в меньшие размеры (например, 10000 элементов за раз), не теряя при этом слишком большой производительности.
Перемещение масштаба, если ни один из них не соответствует купюре, я бы попробовал Cython и NumPy, записывая функцию поиска (с входами, объявленными как массивы int), которые выполняют простой линейный поиск в массиве boundaries
. Я попробовал это, но не смог получить результаты лучше, чем те, которые основаны на bisect.bisect
. Для справки, здесь код Cython, который я пробовал; вы можете сделать лучше:
cimport cython
cimport numpy as np
@cython.boundscheck(False)
@cython.wraparound(False)
def search(np.ndarray[long, ndim=1] boundaries, long val):
cdef long j, k, n=len(boundaries)
for j in range(n):
if boundaries[j] > val:
return j & 1
return 0
И тайминги:
In [13]: from my_cython_extension import search
In [14]: %timeit [n for n in nums if search(boundaries, n)]
1 loops, best of 3: 793 ms per loop