要尝试的事情:
- 预处理您的范围,使其不重叠,并将它们表示为半开区间。
- 使用
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
现在我们将范围重新设计为边界点列表; even 索引处的每个边界点都是某个范围的入口点,而 odd 索引处的每个边界点都是一个退出点。请注意转换为半开区间,并且我已将所有数字放入一个列表中。
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
最后,根据上下文,您可以使用 NumPy 中的searchsorted 函数。这类似于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
然而,事实证明,大部分性能成本是将输入从 Python 列表转换为 searchsorted 到 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,编写一个搜索函数(将输入声明为整数数组)对 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