将一个序列按照给定的最大/最小条件进行最优分割,同时保持元素的顺序,这个问题可以使用贪心算法来解决。因此,你只需要迭代一次输入序列并维护元素缓冲区即可。
在Python中,可以使用生成器来优雅地编写代码,并且不需要创建结果数组。
下面是该问题的主要算法:
def split_by_size(items, max_size, get_size=len):
buffer = []
buffer_size = 0
for item in items:
item_size = get_size(item)
if buffer_size + item_size <= max_size:
buffer.append(item)
buffer_size += item_size
else:
yield buffer
buffer = [item]
buffer_size = item_size
if buffer_size > 0:
yield buffer
其中最后一个参数将决定给定项目的大小的问题委托给指定的可调用对象。
我不会详细解释这个参数,但是我假设一个简单的len()
函数就可以了。此外,这也假设每个元素都满足条件,否则还需要处理这种情况。
测试以上代码:
import random
k = 10
n = 15
max_size = 10
random.seed(0)
items = [b'x' * random.randint(1, 2 * k // 3) for _ in range(n)]
print(items)
print(list(split_by_size(items, k)))
此外,如果您仍愿意将拆分结果存储在一个
list
中,那么以上方法的代码可以稍微更紧凑一些:
def chunks_by_size(items, max_size, get_size=len):
result = []
size = max_size + 1
for item in items:
item_size = get_size(item)
size += item_size
if size > max_size:
result.append([])
size = item_size
result[-1].append(item)
return result
但速度稍慢(请见下面的基准测试结果)。
您还可以考虑使用 functools.reduce()
(与@NizamMohamed答案基本相同),这样代码会更短,但也许不太易读:
def chunks_by_size_reduce(items, size, get_size=len):
return functools.reduce(
lambda a, b, size=size:
a[-1].append(b) or a
if a and sum(get_size(x) for x in a[-1]) + get_size(b) <= size
else a.append([b]) or a, items, [])
由于对于每个被考虑的元素都要调用"candidate"内部列表的每个元素,因此效率显然会更低,这使得复杂度为O(n k!)
,其中k
是每个子序列中平均元素的数量。请参见下面的基准测试结果。
如果使用itertools.accumulate()
来解决问题,我不会感到惊讶,但这也肯定会相当慢。
加速的最简单方法是使用Cython或Numba,在这里,它们被应用于split_by_size()
函数。对于两者而言,代码都不需要改变。
对所有这些进行基准测试,我们获得以下结果(_cy
代表Cython编译版本,而_nb
代表Numba编译版本):
%timeit list(split_by_size(items * 100000, k + 1))
# 10 loops, best of 3: 281 ms per loop
%timeit list(split_by_size_cy(items * 100000, k + 1))
# 10 loops, best of 3: 181 ms per loop
%timeit list(split_by_size_nb(items * 100000, k + 1))
# 100 loops, best of 3: 5.17 ms per loop
%timeit chunks_by_size(items * 100000, k + 1)
# 10 loops, best of 3: 318 ms per loop
%timeit chunks_by_size_reduce(items * 100000, k + 1)
# 1 loop, best of 3: 1.18 s per loop
需要注意的是,尽管Numba编译版本比其他替代方案快得多,但它也是最脆弱的,因为它需要设置forceobj
标志为True
,这可能导致执行不稳定。
无论如何,如果最终目标是通过某些I/O操作发送分组项目,我几乎不认为这会成为瓶颈。
请注意,该算法与其他答案基本相同,只是我认为这里的代码更加简洁。
sys.getsizeof
对于所有这些返回240
:{'a': []}
,{'b': [1]}
,{'c': [1, 2] * 999}
- DeepSpace