如何提高涉及生成器和嵌套循环的代码的效率

时间:2019-05-02 08:15:12

标签: python python-3.x

我正在编写一个代码,该代码基于两个都按升序排列的数字列表(a和b)以渐进方式生成子列表的列表。每个包含两个元素的子列表都可以视为这两个列表中元素的组合。第二个元素(来自列表b)必须大于第一个元素(来自列表a)。特别是,对于第二个元素,该值可能并不总是数字。子列表可以是[elem,None],这意味着列表b中没有匹配列表a中的“ elem”。最终输出中不应有任何重复项。如果您将输出想象成是在表中,则每个子列表将是一行,并且在两列中的每列中,元素都将以升序排列,第二列中为“无”。

由于我最后一个问题的友好回答,我很受启发,并编写了可以实现目标的代码。 (How to generate combinations with none values in a progressive manner)代码显示在这里。

import itertools as it
import time

start=time.time()

a=[1,5,6,7,8,10,11,13,15,16,20,24,25,27]
b=[2,8,9,10,11,12,13,14,17,18,21,26]

def create_combos(lst1, lst2): #a is the base list; l is the adjacent detector list
    n = len(lst1)
    x_ref = [None,None]
    for i in range(1,n+1):
        choices_index = it.combinations(range(n),i)
        choices_value = list(it.combinations(lst2,i)) 
        for choice in choices_index:
            for values in choices_value:
                x = [[elem,None] for elem in lst1]
                for index,value in zip(choice,values): #Iterate over two lists in parallel  
                    if value <= x[index][0]:
                        x[index][0] = None
                        break
                    else:
                        x[index][1] = value #over-write in appropriate location
                if x_ref not in x:
                    yield x

count=0
combos=create_combos(a,b)
for combo in combos:
#    print(combo)
    count+=1
print('The number of combos is ',count)

end=time.time()
print('Run time is ',end-start)

这段代码是关于我在有限的python知识方面我所能获得的最好的代码。但是,由于列表a和b中的元素数量超过15个,运行仍然花费了很长时间。我知道这可能是因为组合的急剧增加。但是,我想知道是否可以进行任何改进以提高其效率,也许就组合的生成方式而言。而且,我正在生成所有可能的组合,然后将不适当的组合删除,我认为这可能还是无效的。

期望的结果是在合理的时间内处理每个列表中的大约30个元素。

编辑:由于一旦每个列表中的元素数量变大,连击数量也会急剧增加。因此,我想保留生成器,以便一次只允许一个组合占用内存。

如果我对以上任何陈述不清楚,请随时提问。谢谢:)

1 个答案:

答案 0 :(得分:2)

编辑2:

好的,如果您做的更聪明,则可以更快地执行此操作。我现在将使用NumPy和Numba来真正加快速度。如果您不想使用Numba,则只需注释使用的部件,它仍然可以工作,但速度较慢。如果您不希望使用NumPy,则可以将其替换为列表或嵌套列表,但又可能会有明显的性能差异。

所以让我们看看。要更改的两个关键事项是:

  • 为输出预分配空间(而不是使用生成器,我们立即生成整个输出)。
  • 重新使用计算的组合。

要进行预分配,我们需要首先计算总共有多少组合。该算法是相似的,但是只是计数,如果您有一个用于部分计数的缓存,它实际上是相当快的。 Numba在这里确实产生了很大的变化,但是我已经使用了。

import numba as nb

def count_combos(a, b):
    cache = np.zeros([len(a), len(b)], dtype=np.int32)
    total = count_combos_rec(a, b, 0, 0, cache)
    return total

@nb.njit
def count_combos_rec(a, b, i, j, cache):
    if i >= len(a) or j >= len(b):
        return 1
    if cache[i][j] > 0:
        return cache[i][j]
    while j < len(b) and a[i] >= b[j]:
        j += 1
    count = 0
    for j2 in range(j, len(b)):
        count += count_combos_rec(a, b, i + 1, j2 + 1, cache)
    count += count_combos_rec(a, b, i + 1, j, cache)
    cache[i][j] = count
    return count

现在我们可以为所有组合预分配一个大数组。而不是直接将组合存储在其中,我将使用一个整数数组来表示b中元素的位置(a中的元素被该位置隐含,而None匹配由-1表示。

为了重用组合,我们执行以下操作。每次我们需要查找某对i / j的组合时,如果以前没有计算过,就进行计算,然后将位置保存在输出数组中这些组合具有的位置首次存储。下次当我们遇到相同的i / j对时,我们只需要复制之前制作的相应零件即可。

总而言之,该算法以如下方式结束(本例中的结果是一个NumPy对象数组,第一列是a中的元素,第二列是b中的元素,但是您可以使用.tolist()将其转换为常规的Python列表。

import numpy as np
import numba as nb

def generate_combos(a, b):
    a = np.asarray(a)
    b = np.asarray(b)
    # Count combos
    total = count_combos(a, b)
    count_table = np.zeros([len(a), len(b)], np.int32)
    # Table telling first position of a i/j match
    ref_table = -np.ones([len(a), len(b)], dtype=np.int32)
    # Preallocate result
    result_idx = np.empty([total, len(a)], dtype=np.int32)
    # Make combos
    generate_combos_rec(a, b, 0, 0, result_idx, 0, count_table, ref_table)
    # Produce matchings array
    seconds = np.where(result_idx >= 0, b[result_idx], None)
    firsts = np.tile(a[np.newaxis], [len(seconds), 1])
    return np.stack([firsts, seconds], axis=-1)

@nb.njit
def generate_combos_rec(a, b, i, j, result, idx, count_table, ref_table):
    if i >= len(a):
        return idx + 1
    if j >= len(b):
        result[idx, i:] = -1
        return idx + 1
    elif ref_table[i, j] >= 0:
        r = ref_table[i, j]
        c = count_table[i, j]
        result[idx:idx + c, i:] = result[r:r + c, i:]
        return idx + c
    else:
        idx_ref = idx
        j_ref = j
        while j < len(b) and a[i] >= b[j]:
            j += 1
        for j2 in range(j, len(b)):
            idx_next = generate_combos_rec(a, b, i + 1, j2 + 1, result, idx, count_table, ref_table)
            result[idx:idx_next, i] = j2
            idx = idx_next
        idx_next = generate_combos_rec(a, b, i + 1, j, result, idx, count_table, ref_table)
        result[idx:idx_next, i] = -1
        idx = idx_next
        ref_table[i, j_ref] = idx_ref
        count_table[i, j_ref] = idx - idx_ref
        return idx

让我们检查结果是否正确:

a = [1, 5, 6, 7, 8, 10, 11, 13, 15, 16, 20, 24, 25, 27]
b = [2, 8, 9, 10, 11, 12, 13, 14, 17, 18, 21, 26]
# generate_combos_prev is the previous recursive method
combos1 = list(generate_combos_prev(a, b))
# Note we do not need list(...) here because it is not a generator
combos = generate_combos(a, b)
print((combos1 == combos).all())
# True

好的,现在让我们来看看性能。

%timeit list(generate_combos_prev(a, b))
# 3.7 s ± 17.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit generate_combos(a, b)
# 593 ms ± 2.66 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

好!那快6倍!除了附加的依赖项之外,唯一可能的缺点是,我们一次制作所有组合,而不是迭代地制作(因此您将在内存中一次拥有所有组合),并且我们需要一个表格来存储大小为O({ {1}}。


这是做您正在做的事的更快方法:

len(a) * len(b)

此算法的唯一区别在于,它生成的组合比您的组合多(在您的代码中,最终计数为1262169),即def generate_combos(a, b): # Assumes a and b are already sorted yield from generate_combos_rec(a, b, 0, 0, []) def generate_combos_rec(a, b, i, j, current): # i and j are the current indices for a and b respectively # current is the current combo if i >= len(a): # Here a copy of current combo is yielded # If you are going to use only one combo at a time you may skip the copy yield list(current) else: # Advance j until we get to a value bigger than a[i] while j < len(b) and a[i] >= b[j]: j += 1 # Match a[i] with every possible value from b for j2 in range(j, len(b)): current.append((a[i], b[j2])) yield from generate_combos_rec(a, b, i + 1, j2 + 1, current) current.pop() # Match a[i] with None current.append((a[i], None)) yield from generate_combos_rec(a, b, i + 1, j, current) current.pop() a = [1, 5, 6, 7, 8, 10, 11, 13, 15, 16, 20, 24, 25, 27] b = [2, 8, 9, 10, 11, 12, 13, 14, 17, 18, 21, 26] count = 0 combos = generate_combos(a, b) for combo in combos: count += 1 print('The number of combos is', count) # 1262170 中的每个元素都与a匹配的组合。这始终是最后一个生成的组合,因此,您可以根据需要忽略该组合。

编辑:如果愿意,可以将None中的# Match a[i] with None块移到generate_combos_rec循环和while循环之间,然后与for中与a匹配的每个值将是第一个而不是最后一个。这样可以使其更容易跳过。或者,您可以将None替换为:

yield list(current)

为避免生成额外的组合(以对每个生成的组合进行额外检查为代价)。

编辑2:

这里是经过稍微修改的版本,通过在递归中仅携带一个布尔值指示符来避免多余的组合。

if any(m is not None for _, m in current):
    yield list(current)