动态根据范围和值拆分列表
我想把一个数值分成指定数量的部分。比如说,如果我有一个 value = 165340
,并且 split = 5
,那么我希望得到的列表应该是 ['0-33068', '33069-66137', '66138-99204', '99205-132272', '132273-165340']
这样的形式。
到目前为止,我想到的办法有点简单,不够灵活……我在想,怎样才能动态地生成一个这样的字符串列表,里面的数字是根据 val/split
的差值来分割的呢?
for i in range(split):
if i==0:
lst.append('%s-%s' % (i, val/split))
elif i==1:
lst.append('%s-%s' % (val/split+i, val/split*2+1))
elif i == 2:
lst.append('%s-%s' % (val/split*i+2, val/split*3))
elif i == 3:
lst.append('%s-%s' % (val/split*i+1, val/split*4))
elif i == 4:
lst.append('%s-%s' % (val/split*i+1, val/split*5))
else:
pass
3 个回答
这里有一个使用numpy的可能方法:
from numpy import arange
v = 165340
s = 5
splits = arange(s + 1) * (v / s)
lst = ['%d-%d' % (splits[idx], splits[idx+1]) for idx in range(s)]
print '\n'.join(lst)
输出结果是:
0-33068
33068-66136
66136-99204
99204-132272
132272-165340
要回答这个问题,首先得弄清楚我们该如何处理 0
—— 但看起来你并没有考虑这个问题。你例子中的区间不一致;你在第一个区间里从 0
开始,而前两个区间都有 33,069 个元素(包括 0
),但你最后一个区间却结束在 165340
。如果 0
和 165340
都算在元素数量里,那么 165340
是 不能 被平均分成五个区间的。
这里有几种不同的解决方案,可能会帮助你理解这个问题。
从零开始的均匀区间
我们先假设你确实想把 0
和“最大”值都算作元素,并在结果中显示出来。换句话说,值 11 实际上表示以下 12 个元素的范围:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
并且可以均匀分成以下非负区间:
['0-3', '4-7', '8-11']
如果我们只关心能被整除的情况,可以用一个相对简单的函数(注意:这些解决方案适用于 Python 3.x,或者在 Python 2.x 中使用 from __future__ import division
):
>>> def evenintervals(value, n):
... binsize = (value + 1) // n
... intervals = ((x * binsize, (x + 1) * binsize - 1) for x in range(n))
... return ['{}-{}'.format(x, y) for x, y in intervals]
...
>>> evenintervals(11, 3)
['0-3', '4-7', '8-11']
>>> evenintervals(17, 2)
['0-8', '9-17']
不过,这个函数处理 165340
(以及其他不能整除的情况)时,会把一些数字从末尾去掉:
>>> evenintervals(165340, 5)
['0-33067', '33068-66135', '66136-99203', '99204-132271', '132272-165339']
从纯数学的角度来看,这样做是行不通的。不过,如果你出于某种原因想要 显示 0
,但不想把它 算作 第一个区间的元素,我们可以稍微调整一下。
从一开始的均匀区间
这里有一个函数,它不把 0
作为列表的元素,但如果你真的想显示它,也可以选择显示:
>>> def evenintervals1(value, n, show_zero=False):
... binsize = value // n
... intervals = [[x * binsize + 1, (x + 1) * binsize] for x in range(n)]
... if show_zero:
... intervals[0][0] = 0
... return ['{}-{}'.format(x, y) for x, y in intervals]
...
>>> evenintervals1(20, 4)
['1-5', '6-10', '11-15', '16-20']
>>> evenintervals1(20, 5, show_zero=True)
['0-5', '6-10', '11-15', '16-20']
这个版本的函数可能是你在问题中 要求 的最接近的东西,尽管它没有显示你在例子输出中给出的确切值:
>>> evenintervals1(165340, 5, show_zero=True)
['0-33068', '33069-66136', '66137-99204', '99205-132272', '132273-165340']
但我们仍然面临着输入不能整除的问题。如果我们想要一个更通用的解决方案呢?
不均匀区间
让我们想想如何处理更广泛的输入。我们应该能够从任何正整数 n
生成从 1
到 n
的非重叠正整数范围。换句话说,如果我们的整数是 5
,我们希望能够生成最多五个范围的列表。但是,我们应该如何分配“多余”的元素,以使范围尽可能均匀呢?
我们可能不想随机分配它们。我们可以简单地延长或缩短列表中的最后一个范围,但这样可能会导致非常不平衡的情况:
# 40 split 7 times, adding remainder to last item
['1-5', '6-10', '11-15', '16-20', '21-25', '26-30', '31-40']
# 40 split 7 times, subtracting excess from last item
['1-6', '7-12', '13-18', '19-24', '25-30', '31-36', '37-40']
在前一种情况下,最后一个元素比其他元素大 100%,而在后一种情况下,它比其他元素小 33%。如果你把一个非常大的值分成更少的区间,这可能不是太大问题。
更可能的是,我们希望一个函数能生成尽可能均匀的范围。我将通过把除法的余数分配到列表的前几个元素来实现这一点,借助 itertools
的帮助:
>>> from itertools import zip_longest # izip_longest for Python 2.7
>>> def anyintervals(value, n):
... binsize, extras = value // n, value % n
... intervals = []
... lower = 0
... upper = 0
... for newbinsize in map(sum, zip_longest([binsize] * n, [1] * extras, fillvalue=0)):
... lower, upper = upper + 1, upper + newbinsize
... intervals.append((lower, upper))
... return ['{}-{}'.format(x, y) for x, y in intervals]
...
>>> anyintervals(11, 3)
['1-4', '5-8', '9-11']
>>> anyintervals(17, 2)
['1-9', 10-17']
最后,使用 OP 给出的示例输入:
>>> anyintervals(165340, 5)
['1-33068', '33069-66136', '66137-99204', '99205-132272', '132273-165340']
如果真的很重要要让第一个区间从零开始,我们可以在返回之前,应用在 evenintervals1
中使用的相同逻辑,修改 intervals
中的第一个整数,或者写一个类似的函数,从零开始计数。
我确实实现了另一个版本,它将“多余”的元素分配到最后的范围,而不是第一个,当然还有很多其他实现,你可能会感兴趣去尝试,但这些解决方案留给读者自己去探索。;)
总结:
我在这里尝试了很多方法,特别是使用 remainder = value % numsplits
,然后用 int(i * remainder // numsplits)
来尽量保持结果接近。不过,最后我还是放弃了这些方法,回到了浮点数计算,因为它似乎能给出最接近的结果。使用浮点数时,还是要注意一些常见的问题。
def segment(value, numsplits):
return ["{}-{}".format(
int(round(1 + i * value/(numsplits*1.0),0)),
int(round(1 + i * value/(numsplits*1.0) +
value/(numsplits*1.0)-1, 0))) for
i in range(numsplits)]
>>> segment(165340, 5)
['1-33068', '33069-66136', '66137-99204', '99205-132272', '132273-165340']
>>> segment(7, 4)
['1-2', '3-4', '4-5', '6-7']
我觉得这个方法没有太大问题。我是从1开始的,而不是0,但其实这并不是必要的(你可以把 int(round(1 + i * ...
改成 int(round(i * ...
来调整)。下面是之前的结果。
value = 165340
numsplits = 5
result = ["{}-{}".format(i + value//numsplits*i, i + value//numsplits*i + value//numsplits) for i in range(numsplits)]
可能值得加一个函数进去。
def segment(value,numsplits):
return ["{}-{}".format(value*i//numsplits, 1 + value//numsplits*i + value//numsplits) for i in range(numsplits)]
下面的代码会把结果限制在你的值范围内。
def segment(value, numsplits):
return ["{}-{}".format(max(0,i + value*i//numsplits), min(value,i + value*i//numsplits + value//numsplits)) for i in range(numsplits)]