动态根据范围和值拆分列表

2 投票
3 回答
2954 浏览
提问于 2025-04-18 11:13

我想把一个数值分成指定数量的部分。比如说,如果我有一个 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 个回答

0

这里有一个使用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
1

要回答这个问题,首先得弄清楚我们该如何处理 0 —— 但看起来你并没有考虑这个问题。你例子中的区间不一致;你在第一个区间里从 0 开始,而前两个区间都有 33,069 个元素(包括 0),但你最后一个区间却结束在 165340如果 0165340 都算在元素数量里,那么 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 生成从 1n 的非重叠正整数范围。换句话说,如果我们的整数是 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 中的第一个整数,或者写一个类似的函数,从零开始计数。

我确实实现了另一个版本,它将“多余”的元素分配到最后的范围,而不是第一个,当然还有很多其他实现,你可能会感兴趣去尝试,但这些解决方案留给读者自己去探索。;)

1

总结:

我在这里尝试了很多方法,特别是使用 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)]

撰写回答