簡體   English   中英

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

[英]How to improve the efficiency for a code involving generators and nested for loops

我正在編寫一個代碼,該代碼基於兩個都以升序排列的數字列表(a和b)以漸進方式生成子列表的列表。 每個包含兩個元素的子列表都可以視為這兩個列表中元素的組合。 第二個元素(來自列表b)必須大於第一個元素(來自列表a)。 特別是,對於第二個元素,該值可能並不總是數字。 子列表可以是[elem,None],這意味着列表b中沒有匹配列表a中的“ elem”。 最終輸出中不應有任何重復項。 如果您將輸出想象成是在一個表中,則每個子列表將是一行,並且在兩列的每一列中,這些元素都按升序排列,第二列中的值為“無”。

受到我的最后一個問題的友好回答,我受到啟發,並編寫了可以實現目標的代碼。 如何以漸進方式生成無值的組合 )此處顯示代碼。

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個元素。

編輯:由於一旦每個列表中的元素數量變大,連擊的數量也會急劇增加。 因此,我想保留生成器,以便一次只允許一個組合占用內存。

如果我對以上任何陳述不清楚,請隨時提問。 謝謝:)

編輯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( len(a) * len(b)的部分計數) len(a) * len(b) )。


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

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

這種算法的唯一區別是,它會產生一個比你的組合(在你的代碼最終計為1262169),即一個地方中的每個元素a與匹配None 這始終是要生成的最后一個組合,因此您可以根據需要忽略該組合。

編輯:如果你願意,你可以移動# Match a[i] with Nonegenerate_combos_rec只之間while環和for循環,然后將多余的結合在每個值a相匹配的None將是第一個而不是最后一個。 這樣可以使其更容易跳過。 或者,您可以將yield list(current)替換為:

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

為了避免生成額外的組合(以對每個生成的組合進行額外檢查為代價)。

編輯2:

這是一個經過稍微修改的版本,通過在遞歸中僅攜帶一個布爾值指示符來避免額外的組合。

def generate_combos(a, b):
    yield from generate_combos_rec(a, b, 0, 0, [], True)

def generate_combos_rec(a, b, i, j, current, all_none):
    if i >= len(a):
        if not all_none:
            yield list(current)
    else:
        while j < len(b) and a[i] >= b[j]:
            j += 1
        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, False)
            current.pop()
        current.append((a[i], None))
        yield from generate_combos_rec(a, b, i + 1, j, current, all_none)
        current.pop()

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM