我正在处理邮资申请,需要根据多个邮政编码范围检查整数邮政编码,并根据邮政编码匹配的范围返回不同的代码。
每个代码都有多个邮政编码范围。例如,如果邮政编码在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 1000 <= postcode <= 2249 or 2555 <= postcode <= 2574 or ...:
return 'M'
if 2250 <= postcode <= 2265 or ...:
return 'N'
...
1米重复的时间:5.11秒。
我的思绪更优雅,更容易进入并阅读范围。如果它们随时间变化特别好,这是可能的。但它的实施速度确实慢了四倍。
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秒。
正如作者所说,“只有在构建一次集合以检查循环中的许多邮政编码时才会更好”。但是我想我还是要测试一下。
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
模块的内容,但是我无法确定哪些参数可以让find_ge()
进行可运行的测试。我希望通过许多邮政编码循环会非常快,但如果每次都必须进行设置,那就不行了。因此,对于一个邮政区域代码(具有四个范围的 M 代码),只需重复填充numbers
,edgepairs
,edgeanswers
等1米,但实际上并非如此运行fast_solver
:
1m代表的时间:105.61秒。
使用每个邮政区域代码预先生成一个dict,cPickled在源文件(106 KB)中,并为每次运行加载。我期待这种方法有更好的性能,但至少在我的系统上,IO真的破坏了它。该服务器是一款不那么令人眩目的快速顶级Mac Mini。
1m代表的时间:5895.18秒(从10,000次运行中推断)。
好吧,我希望有人能给出一个我没有考虑过的简单'呃'答案,但事实证明这更复杂(甚至有点争议)。
如果在这种情况下计算每纳秒的效率,我可能会保持一个单独的进程运行,它实现了二进制搜索或dict解决方案之一,并将结果保存在内存中以便进行极快的查找。但是,由于IF树只需要五秒钟才能运行一百万次,这对于我的小型企业而言足够快,这就是我最终将在我的应用程序中使用的。
感谢大家的贡献!
答案 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)。