HOWTO · Python

Python 中列表的組合

本教程演示瞭如何在 python 中查詢列表元素的組合

本頁內容

使用 itertools.combinations(),可在不重複使用輸入位置的情況下選取固定數量的項目。如果同一個位置可以重複選取,則使用 itertools.combinations_with_replacement()。這兩個函式都會產生組合:選取順序不同不會形成新的結果;排列則會將不同順序視為不同結果。

選擇合適的組合方式

需要固定的選取數量時,使用 combinations();允許重複選取同一個位置時,使用 combinations_with_replacement();需要產生所有子集合時,則使用 powerset() 函式。標準函式庫中的這些函式會回傳由 tuple 組成的迭代器。它們依輸入位置讀取資料,因此不同位置上的相同值可能產生看起來重複的 tuple。結果順序依照輸入位置排列;每個 tuple 內的值不會自動排序。若需要自訂規則,可以使用遞迴,明確控制選取流程。

使用 itertools.combinations() 進行不重複選取

itertools.combinations(iterable, r) 會產生長度為 r 的 tuple,而且不會重複使用同一個位置。它會依照 iterable 的輸入順序讀取資料。若要一次儲存或檢視所有結果,請將迭代器轉成 list;否則可以直接逐一迭代。

執行以下範例:
本節中的程式碼以及顯示的標籤或輸出仍為英文。

from itertools import combinations

items = ["red", "blue", "green"]
for pair in combinations(items, 2):
    print(pair)

這些雙元素結果最多只會使用每個位置一次。若要取得個別項目,將 r 設為 1;若要將整個輸入作為一個 tuple,則將它設為輸入位置的數量。如果 r 大於位置數量,迭代器就不會產生任何結果。

選取項目可能重複時使用 itertools.combinations_with_replacement()

若允許重複選取同一個輸入位置,請使用 itertools.combinations_with_replacement(iterable, r)。它同樣會依照位置順序產生長度為 r 的 tuple。例如,從 ["red", "blue"] 選取一對時,結果會包含 ("red", "red") 和 ("blue", "blue");combinations() 則不會產生這些配對。當重複選取有其意義時,這種方式就很實用,例如選兩球冰淇淋,而且兩球可以是同一種口味。引數 r 仍會決定每個 tuple 的長度。

使用遞迴或產生器自訂實作

遞迴回溯函式可以將結果逐步建立在 list 中。每一步先附加一個值,再從下一個位置遞迴呼叫,接著移除該值,讓迴圈嘗試其他選項。產生器遵循相同的選取邏輯,但會在找到結果時逐一產生。除非呼叫端明確將結果轉成 list,否則不必保留整個結果集合;不過,這並不代表它本身一定比較快。

以下兩個自訂函式都要求 r 是非負整數,否則會引發 ValueError。當 r = 0 時,各自都會回傳唯一的空 tuple;當 r 大於輸入位置數量且大於零時,兩者都不會產生結果。產生器函式本體會延遲執行,因此要等到開始迭代時才會檢查引數。
本節中的程式碼以及顯示的標籤或輸出仍為英文。

def get_combinations(items, r):
    if not isinstance(r, int) or isinstance(r, bool) or r < 0:
        raise ValueError("r must be a nonnegative integer")

    results = []

    def backtrack(start, chosen):
        if len(chosen) == r:
            results.append(tuple(chosen))
            return
        for index in range(start, len(items)):
            chosen.append(items[index])
            backtrack(index + 1, chosen)
            chosen.pop()

    backtrack(0, [])
    return results


def generate_combinations(items, r):
    if not isinstance(r, int) or isinstance(r, bool) or r < 0:
        raise ValueError("r must be a nonnegative integer")
    if r == 0:
        yield ()
        return
    for index in range(len(items)):
        for suffix in generate_combinations(items[index + 1 :], r - 1):
            yield (items[index],) + suffix


items = ["red", "blue", "green"]
print(get_combinations(items, 2))
print(list(generate_combinations(items, 2)))

若要撰寫延遲產生結果的自訂版本,請在完整選取結果形成時使用 yield。無論採用哪一種實作,下一次遞迴呼叫都要從 index + 1 開始,避免重複使用同一個位置。如果輸入中有重複值,即使它們來自不同位置,輸出看起來也可能相同。

使用 powerset() 函式產生所有子集合

冪集包含各種大小的子集合,包括空 tuple 和完整輸入。這種實作會合併每種可能大小的結果。chain.from_iterable() 會回傳迭代器;只有在需要一次建立所有子集合時,才將它包在 list() 中。
本節中的程式碼以及顯示的標籤或輸出仍為英文。

from itertools import chain, combinations


def powerset(items):
    values = list(items)
    return chain.from_iterable(
        combinations(values, size) for size in range(len(values) + 1)
    )


for subset in powerset(["red", "blue"]):
    print(subset)

若輸入有 n 個位置,冪集就有 2**n 個子集合。這個數量會快速增加,因此如果任務只需要固定大小的選取,建議使用固定大小的組合迭代器。只有在結果確實需要包含所有大小的子集合時,才使用冪集。