Python:有效地检查整数是否在* many *范围内

时间:2011-05-19 05:00:51

标签: python

我正在处理邮资申请,需要根据多个邮政编码范围检查整数邮政编码,并根据邮政编码匹配的范围返回不同的代码。

每个代码都有多个邮政编码范围。例如,如果邮政编码在1000-2429,2545-2575,2640-2686范围内或等于2890,则应返回 M 代码。

我可以写成:

if 1000 <= postcode <= 2429 or 2545 <= postcode <= 2575 or 2640 <= postcode <= 2686 or postcode == 2890:
    return 'M'

但这似乎是很多代码行,因为有27个可返回代码和77个总范围要检查。是否有一种更有效(最好更简洁)的方法是使用Python将整数与所有这些范围匹配?


编辑:有很多出色的解决方案,所以我已经实现了所有可能的解决方案,并对其性能进行了基准测试。

此程序的环境是一个Web服务(实际上是Django),它需要在运行中逐个检查邮政编码区域代码。那么,我的首选实现是可以快速用于每个请求的实现,并且不需要将任何进程保存在内存中,或者需要批量处理许多邮政编码。

我使用timeit.Timer测试了以下解决方案,每次使用随机生成的邮政编码默认重复1000000次。

IF解决方案(我的原创)

if 1000 <= postcode <= 2249 or 2555 <= postcode <= 2574 or ...:
    return 'M'
if 2250 <= postcode <= 2265 or ...:
    return 'N'
...

1米重复的时间:5.11秒。

元组中的范围(Jeff Mercado)

我的思绪更优雅,更容易进入并阅读范围。如果它们随时间变化特别好,这是可能的。但它的实施速度确实慢了四倍。

if any(lower <= postcode <= upper for (lower, upper) in [(1000, 2249), (2555, 2574), ...]):
    return 'M'
if any(lower <= postcode <= upper for (lower, upper) in [(2250, 2265), ...]):
    return 'N'
...

1m代表的时间:19.61秒。

设置成员资格(gnibbler)

正如作者所说,“只有在构建一次集合以检查循环中的许多邮政编码时才会更好”。但是我想我还是要测试一下。

if postcode in set(chain(*(xrange(start, end+1) for start, end in ((1000, 2249), (2555, 2574), ...)))):
    return 'M'
if postcode in set(chain(*(xrange(start, end+1) for start, end in ((2250, 2265), ...)))):
    return 'N'
...

1m reps的时间:339.35秒。

Bisect(罗伯特·金)

这个可能比我的智力水平高一点。我学到了很多关于bisect模块的内容,但是我无法确定哪些参数可以让find_ge()进行可运行的测试。我希望通过许多邮政编码循环会非常快,但如果每次都必须进行设置,那就不行了。因此,对于一个邮政区域代码(具有四个范围的 M 代码),只需重复填充numbersedgepairsedgeanswers等1米,但实际上并非如此运行fast_solver

1m代表的时间:105.61秒。

Dict(哨兵)

使用每个邮政区域代码预先生成一个dict,cPickled在源文件(106 KB)中,并为每次运行加载。我期待这种方法有更好的性能,但至少在我的系统上,IO真的破坏了它。该服务器是一款不那么令人眩目的快速顶级Mac Mini。

1m代表的时间:5895.18秒(从10,000次运行中推断)。

摘要

好吧,我希望有人能给出一个我没有考虑过的简单'呃'答案,但事实证明这更复杂(甚至有点争议)。

如果在这种情况下计算每纳秒的效率,我可能会保持一个单独的进程运行,它实现了二进制搜索或dict解决方案之一,并将结果保存在内存中以便进行极快的查找。但是,由于IF树只需要五秒钟才能运行一百万次,这对于我的小型企业而言足够快,这就是我最终将在我的应用程序中使用的。

感谢大家的贡献!

10 个答案:

答案 0 :(得分:14)

您可以将范围抛出到元组中,并将元组放在列表中。然后使用any()来帮助您查找您的值是否在这些范围内。

ranges = [(1000,2429), (2545,2575), (2640,2686), (2890, 2890)]
if any(lower <= postcode <= upper for (lower, upper) in ranges):
    print('M')

答案 1 :(得分:6)

可能最快的是检查一组

的成员资格
>>> from itertools import chain
>>> ranges = ((1000, 2429), (2545, 2575), (2640, 2686), (2890, 2890))
>>> postcodes = set(chain(*(xrange(start, end+1) for start, end in ranges)))
>>> 1000 in postcodes
True
>>> 2500 in postcodes
False

但是它确实以这种方式使用了更多的内存,并且构建集合需要时间,因此只有在构建集合一次以检查循环中的许多邮政编码时才会更好

编辑:似乎不同的范围需要映射到不同的字母

>>> from itertools import chain
>>> ranges = {'M':((1000,2429), (2545,2575), (2640,2686), (2890, 2890)),
              # more ranges
              }
>>> postcodemap = dict((k,v) for v in ranges for k in chain(*imap(xrange, *zip(*ranges[v]))))    
>>> print postcodemap.get(1000)
M
>>> print postcodemap.get(2500)
None

答案 2 :(得分:5)

在进行不等式时,你只需要解决边缘情况和边缘情况之间的一个数字。

e.g。如果您在 TEN

上进行以下测试

10&lt; 20,10&lt; 15,10&gt; 8,10> 12

它会给出True True True False

但请注意最接近10的数字是8和12

这意味着 9,10,11 将给出10个答案。如果你没有太多的初始范围数并且它们很稀疏那么这很有帮助。否则,您需要查看您的不等式是否是可传递的并使用范围树或其他东西。

所以你能做的就是将你所有的界限分成几个区间。 例如如果你的不等式有数字12,50,192,999

你会得到以下间隔,所有人都有相同的答案: 小于12,12,13-49,50,51-191,192,193-998,999,999 +

从这些区间可以看出,我们只需要解决9个案例,然后我们就可以快速解决任何问题。

以下是我如何使用这些预先计算的结果来解决新数字x的示例:

a)是x边界? (它在集合中) 如果是,则返回您之前为该边界找到的答案。 否则使用案例b)

b)找到小于x的最大边界数,称之为 maxS 找到大于x的最小边界数称为 minL 。 现在只返回之前找到的maxS和minL之间的解决方案。

Python binary search-like function to find first number in sorted list greater than a specific value 找到最接近的数字。 bisect模块将帮助(在您的代码中导入它) 这有助于找到maxS和minL

你可以使用bisect和我在示例代码中包含的函数:

def find_ge(a, key):
    '''Find smallest item greater-than or equal to key.
    Raise ValueError if no such item exists.
    If multiple keys are equal, return the leftmost.

    '''
    i = bisect_left(a, key)
    if i == len(a):
        raise ValueError('No item found with key at or above: %r' % (key,))
    return a[i]




ranges=[(1000,2429), (2545,2575), (2640,2686), (2890, 2890)]
numbers=[]
for pair in ranges:
        numbers+=list(pair)

numbers+=[-999999,999999] #ensure nothing goes outside the range
numbers.sort()
edges=set(numbers)

edgepairs={}

for i in range(len(numbers)-1):
        edgepairs[(numbers[i],numbers[i+1])]=(numbers[i+1]-numbers[i])//2



def slow_solver(x):
        return #your answer for postcode x


listedges=list(edges)
edgeanswers=dict(zip(listedges,map(solver,listedges)))
edgepairsanswers=dict(zip(edgepairs.keys(),map(solver,edgepairs.values())))

#now we are ready for fast solving:
def fast_solver(x):
        if x in edges:
                return edgeanswers[x]
        else:
                #find minL and maxS using find_ge and your own similar find_le
                return edgepairsanswers[(minL,maxS)]

答案 3 :(得分:4)

您的基准测试似乎包括为每个调用从头开始设置数据结构。为什么?您是否考虑过从邮政编码到区域代码的映射,在模块导入时从文件ONCE加载?这些看起来很像澳大利亚的邮政编码。如果是这样,他们就不是很多。

答案 4 :(得分:3)

这是一个快速而简短的解决方案,使用numpy:

import numpy as np
lows = np.array([1, 10, 100]) # the lower bounds
ups = np.array([3, 15, 130]) # the upper bounds

def in_range(x):
    return np.any((lows <= x) & (x <= ups))

现在例如

in_range(2) # True
in_range(23) # False

答案 5 :(得分:3)

最近我有类似的要求,我使用位操作来测试整数是否属于所述范围。它肯定更快,但我想如果您的范围涉及大量数字则不合适。我自由地复制了here

中的示例方法

首先,我们创建一个二进制数,该范围内的所有位都设置为1。

#Sets the bits to one between lower and upper range 
def setRange(permitRange, lower, upper):
  # the range is inclusive of left & right edge. So add 1 upper limit
  bUpper = 1 << (upper + 1)
  bLower = 1 << lower
  mask = bUpper - bLower
  return (permitRange | mask)

#For my case the ranges also include single integers. So added method to set single bits
#Set individual bits  to 1
def setBit(permitRange, number):
  mask = 1 << vlan
  return (permitRange| mask)

现在是时候解析范围并填充二进制掩码了。如果范围中的最大数字是n,我们将在二进制

中创建大于2 ^ n的整数
#Example range (10-20, 25, 30-50)
rangeList = "10-20, 25, 30-50"
maxRange = 100
permitRange = 1 << maxRange
for range in rangeList.split(","):
    if range.isdigit():
        permitRange = setBit(permitRange, int(range))
    else:
        lower, upper = range.split("-",1)
        permitRange = setRange(permitRange, int(lower), int(upper))
    return permitRange

要检查数字'n'是否属于该范围,只需测试第n位的位

#return a non-zero result, 2**offset, if the bit at 'offset' is one.
def testBit(permitRange, number):
    mask = 1 << number
    return (permitRange & mask)

if testBit(permitRange,10):
    do_something()

答案 6 :(得分:2)

完整数据不存在,但我假设范围不重叠,因此您可以将范围表示为单个排序的范围元组及其代码:

ranges = (
    (1000, 2249, 'M'), 
    (2250, 2265, 'N'), 
    (2555, 2574, 'M'),
    # ...
)

这意味着我们可以一次二进制搜索它们。这应该是 O(log(N))时间,这应该会导致非常大的集合具有相当不错的性能。

def code_lookup(value, ranges):
    left, right = 0, len(ranges)

    while left != right - 1:
        mid = left + (right - left) / 2

        if value <= ranges[mid - 1][1]:  # Check left split max
            right = mid
        elif value >= ranges[mid][0]:    # Check right split min
            left = mid
        else:                            # We are in a gap
            return None

    if ranges[left][0] <= value <= ranges[left][1]:
        # Return the code
        return ranges[left][2]

我没有您的确切值,但为了进行比较,我将其与某些生成的范围(包含各种代码的77个范围)进行比较,并将其与天真的方法进行比较:

def get_code_naive(value):
    if 1000 < value < 2249:
        return 'M'
    if 2250 < value < 2265:
        return 'N'
    # ...

1,000,000的结果是天真版本在大约5秒内运行,而二进制搜索版本在4秒内运行。所以它的速度要快一些(20%),代码维护起来要好得多,列表越长,随着时间的推移它就越能表现出天真的方法。

答案 7 :(得分:1)

警告 - 这可能是过早优化。对于大范围的列表,它可能是值得的,但可能不适用于您的情况。此外,虽然字典/集合解决方案将使用更多内存,但它们仍然可能是更好的选择。

您可以对范围终点进行二进制搜索。如果所有范围都不重叠,这将很容易,但仍可以(重复调整)重叠范围。

执行找到最高匹配比二分搜索。这与查找最低匹配 - 大于或等于(下限)二进制搜索相同,除了从结果中减去一个。

在结束点列表中使用半开项目 - 即如果您的范围是1000..2429(包括1000和2429),请使用值1000和2430.如果您获得具有相同值的终点和起点(两个范围接触,因此没有间隙)从列表中排除较低范围的终点。

如果找到范围的起点终点,则目标值在该范围内。如果您找到了终点范围的终点,则您的目标值不在任何范围内。

二进制搜索算法大致是(不要指望它在没有编辑的情况下运行)......

while upperbound > lowerbound :
  testpos = lowerbound + ((upperbound-lowerbound) // 2)

  if item [testpos] > goal :
    #  new best-so-far
    upperbound = testpos
  else :
    lowerbound = testpos + 1

注意 - 在Python 3中,整数除法需要“//”除法运算符。在Python 2中,正常的“/”将起作用,但最好为Python 3做好准备。

最后,上限和下限都指向找到的项目 - 但是对于“上限”搜索。减去一个以获得所需的搜索结果。如果给出-1,则没有匹配的范围。

库中可能有一个二进制搜索例程来执行上限搜索,所以如果是这样的话,请更喜欢这个。为了更好地理解二进制搜索的工作原理,请参阅How can I better understand the one-comparison-per-iteration binary search? - 不,我不是要求赞成投票; - )

答案 8 :(得分:0)

Python有一个范围(a,b)函数,表示从(和包括)a到(但不包括)b的范围。您可以列出这些范围,并检查其中是否有任何数字。使用具有相同含义但实际上并未在内存中创建列表的xrange(a,b)可能更有效。

list_of_ranges = []
list_of_ranges.append(xrange(1000, 2430))
list_of_ranges.append(xrange(2545, 2576))
for x in [999, 1000, 2429, 2430, 2544, 2545]:
    result = False
    for r in list_of_ranges:
        if x in r:
            result = True
            break
    print x, result

答案 9 :(得分:-3)

你真的做过基准吗?这段代码的性能是否会影响整个应用程序的性能?所以首先是基准!

但你也可以使用dict,例如用于存储“M”范围的所有键:

mhash = {1000: true, 1001: true,..., 2429: true,...}

if postcode in mhash:
   print 'M'

当然:哈希需要更多内存,但访问时间为O(1)。