生成包含重复元素的列表排列

30
在Python中,使用itertools模块生成列表的所有排列非常简单。我有一个情况,我正在使用的序列只有两个字符(即'1122')。我想生成所有唯一的排列。
对于字符串'1122',有6个唯一的排列(112212121221等),但是itertools.permutations将产生24个项目。只记录唯一的排列很简单,但收集它们所需的时间比必要的多得多,因为会考虑所有720个项目。
是否有一个函数或模块在生成排列时考虑了重复元素,以便我不必编写自己的函数?
6个回答

21

这个网页看起来很有前途。

def next_permutation(seq, pred=cmp):
    """Like C++ std::next_permutation() but implemented as
    generator. Yields copies of seq."""
    def reverse(seq, start, end):
        # seq = seq[:start] + reversed(seq[start:end]) + \
        #       seq[end:]
        end -= 1
        if end <= start:
            return
        while True:
            seq[start], seq[end] = seq[end], seq[start]
            if start == end or start+1 == end:
                return
            start += 1
            end -= 1
    if not seq:
        raise StopIteration
    try:
        seq[0]
    except TypeError:
        raise TypeError("seq must allow random access.")
    first = 0
    last = len(seq)
    seq = seq[:]
    # Yield input sequence as the STL version is often
    # used inside do {} while.
    yield seq[:]
    if last == 1:
        raise StopIteration
    while True:
        next = last - 1
        while True:
            # Step 1.
            next1 = next
            next -= 1
            if pred(seq[next], seq[next1]) < 0:
                # Step 2.
                mid = last - 1
                while not (pred(seq[next], seq[mid]) < 0):
                    mid -= 1
                seq[next], seq[mid] = seq[mid], seq[next]
                # Step 3.
                reverse(seq, next1, last)
                # Change to yield references to get rid of
                # (at worst) |seq|! copy operations.
                yield seq[:]
                break
            if next == first:
                raise StopIteration
    raise StopIteration

>>> for p in next_permutation([int(c) for c in "111222"]):
...     print p
... 
[1, 1, 1, 2, 2, 2]
[1, 1, 2, 1, 2, 2]
[1, 1, 2, 2, 1, 2]
[1, 1, 2, 2, 2, 1]
[1, 2, 1, 1, 2, 2]
[1, 2, 1, 2, 1, 2]
[1, 2, 1, 2, 2, 1]
[1, 2, 2, 1, 1, 2]
[1, 2, 2, 1, 2, 1]
[1, 2, 2, 2, 1, 1]
[2, 1, 1, 1, 2, 2]
[2, 1, 1, 2, 1, 2]
[2, 1, 1, 2, 2, 1]
[2, 1, 2, 1, 1, 2]
[2, 1, 2, 1, 2, 1]
[2, 1, 2, 2, 1, 1]
[2, 2, 1, 1, 1, 2]
[2, 2, 1, 1, 2, 1]
[2, 2, 1, 2, 1, 1]
[2, 2, 2, 1, 1, 1]
>>> 

2017-08-12

七年过去了,这里有一个更好的算法(更加清晰易懂):

from itertools import permutations

def unique_perms(series):
    return {"".join(p) for p in permutations(series)}

print(sorted(unique_perms('1122')))

每次迭代都使用 reverse 是否可以? 这具有 O(n) 复杂度,并且在每次迭代上运行的事实可能会使整个算法变得非常缓慢。 - ovgolovin
这个算法有问题,它还依赖于序列中的初始顺序。序列 [1,1,2,2,2] 和 [2,1,1,2,2] 导致不同数量的排列。 - freude
1
@freude:此函数迭代直到达到最大的词典排序排列,然后停止。因此,为了获得所有排列,请确保对输入进行排序(以便它是最低的词典排序排列)。 - nneonneo
使用list()将此迭代器括起来是不起作用的:list(next_permutation([1, 2]))会得到[[2, 1], [2, 1]]。有任何想法为什么会这样? - Nico Schlömer
修复了。 (第一个“yield”中缺少了一份副本。) - Nico Schlömer
显示剩余4条评论

15

More Itertools有一个函数可以完成这个任务:

more-itertools.distinct_permutations(iterable)

迭代地返回给定可迭代对象中元素的不同排列方式。

set(permutations(iterable))等价,但是会避免生成和丢弃重复的结果。对于较大的输入序列,这种方法更加高效。

from more_itertools import distinct_permutations

for p in distinct_permutations('1122'):
    print(''.join(p))

# 2211
# 2121
# 1221
# 2112
# 1212
# 1122

安装:

pip install more-itertools

3
毫无疑问,这是最简洁的答案。 - nivniv

2
使用set可以使解决方案更简单。输入的字符串中包含重复字符和不重复字符。
from itertools import permutations

def perm(s):
    return set(permutations(s))
    
    l = '1122'
    
    perm(l)
  
    {('1', '1', '2', '2'),
     ('1', '2', '1', '2'),
     ('1', '2', '2', '1'),
     ('2', '1', '1', '2'),
     ('2', '1', '2', '1'),
     ('2', '2', '1', '1')}
    
    
    l2 = '1234'
    
    perm(l2)

    {('1', '2', '3', '4'),
     ('1', '2', '4', '3'),
     ('1', '3', '2', '4'),
     ('1', '3', '4', '2'),
     ('1', '4', '2', '3'),
     ('1', '4', '3', '2'),
     ('2', '1', '3', '4'),
     ('2', '1', '4', '3'),
     ('2', '3', '1', '4'),
     ('2', '3', '4', '1'),
     ('2', '4', '1', '3'),
     ('2', '4', '3', '1'),
     ('3', '1', '2', '4'),
     ('3', '1', '4', '2'),
     ('3', '2', '1', '4'),
     ('3', '2', '4', '1'),
     ('3', '4', '1', '2'),
     ('3', '4', '2', '1'),
     ('4', '1', '2', '3'),
     ('4', '1', '3', '2'),
     ('4', '2', '1', '3'),
     ('4', '2', '3', '1'),
     ('4', '3', '1', '2'),
     ('4', '3', '2', '1')}

1
以下是足够简洁的代码:set(permutations(s)) - Leland Hepworth
1
@LelandHepworth,是的,你说得对。没有必要使用re或list。 - LetzerWille

1

这也是一个常见的面试问题。如果无法使用标准库模块,可以考虑以下实现:

我们定义了一种排列的字典序。一旦这样做,我们就可以从最小排列开始,最小地递增它,直到达到最大排列。

def next_permutation_helper(perm):
    if not perm:
        return perm

    n = len(perm)

    """
    Find k such that p[k] < p[k + l] and entries after index k appear in
    decreasing order.
    """
    for i in range(n - 1, -1, -1):
        if not perm[i - 1] >= perm[i]:
            break

    # k refers to the inversion point
    k = i - 1

    # Permutation is already the max it can be
    if k == -1:
        return []

    """
    Find the smallest p[l] such that p[l] > p[k]
    (such an l must exist since p[k] < p[k + 1].
    Swap p[l] and p[k]
    """
    for i in range(n - 1, k, -1):
        if not perm[k] >= perm[i]:
            perm[i], perm[k] = perm[k], perm[i]
            break

    # Reverse the sequence after position k.
    perm[k + 1 :] = reversed(perm[k + 1 :])

    return perm


def multiset_permutation(A):
    """
    We sort array first and `next_permutation()` will ensure we generate
    permutations in lexicographic order
    """
    A = sorted(A)
    result = list()

    while True:
        result.append(A.copy())
        A = next_permutation_helper(A)
        if not A:
            break

    return result

输出:

>>> multiset_permutation([1, 1, 2, 2])
[[1, 1, 2, 2], [1, 2, 1, 2], [1, 2, 2, 1], [2, 1, 1, 2], [2, 1, 2, 1], [2, 2, 1, 1]]

您可以在此行上使用join将可变列表的输出转换为字符串:
result.append("".join(map(str, A.copy())))

获取:

['1122', '1212', '1221', '2112', '2121', '2211']

0
一个非常简单的解决方案,可能类似于 more_itertools 使用的方式,利用@Brayoni建议的排列字典序,可以通过构建可迭代项的索引来完成。
假设你有 L = '1122',你可以使用类似以下的代码构建一个非常简单的索引:
index = {x: i for i, x in enumerate(sorted(L))}

假设您有一个排列P,它是L的一种排列。无论P有多少个元素,词典序规定如果您将P映射到使用索引,则必须始终增加。像这样映射P:
mapped = tuple(index[e] for e in p)  # or tuple(map(index.__getitem__, p))

现在,您可以丢弃那些小于或等于迄今为止最大值的元素:
def perm_with_dupes(it, n=None):
    it = tuple(it)   # permutations will do this anyway
    if n is None:
        n = len(it)
    index = {x: i for i, x in enumerate(it)}
    maximum = (-1,) * (len(it) if n is None else n)
    for perm in permutations(it, n):
        key = tuple(index[e] for e in perm)
        if key <= maximum: continue
        maximum = key
        yield perm

请注意,除了保留最后一个最大项之外,没有额外的内存开销。如果您喜欢,可以使用''连接元组。

0
from more_itertools import distinct_permutations

x = [p for p in distinct_permutations(['M','I','S', 'S', 'I'])]

for item in x:
    
  print(item)

输出:

('I', 'S', 'S', 'I', 'M')
('S', 'I', 'S', 'I', 'M')
('S', 'S', 'I', 'I', 'M')
('I', 'S', 'I', 'S', 'M')
('S', 'I', 'I', 'S', 'M')
('I', 'I', 'S', 'S', 'M')
('I', 'S', 'I', 'M', 'S')
('S', 'I', 'I', 'M', 'S')
('I', 'I', 'S', 'M', 'S')
('I', 'I', 'M', 'S', 'S')
('I', 'S', 'S', 'M', 'I')
('S', 'I', 'S', 'M', 'I')
('S', 'S', 'I', 'M', 'I')
('S', 'S', 'M', 'I', 'I')
('I', 'S', 'M', 'S', 'I')
('S', 'I', 'M', 'S', 'I')
('S', 'M', 'I', 'S', 'I')
('S', 'M', 'S', 'I', 'I')
('I', 'M', 'S', 'S', 'I')
('M', 'I', 'S', 'S', 'I')
('M', 'S', 'I', 'S', 'I')
('M', 'S', 'S', 'I', 'I')
('I', 'S', 'M', 'I', 'S')
('S', 'I', 'M', 'I', 'S')
('S', 'M', 'I', 'I', 'S')
('I', 'M', 'S', 'I', 'S')
('M', 'I', 'S', 'I', 'S')
('M', 'S', 'I', 'I', 'S')
('I', 'M', 'I', 'S', 'S')
('M', 'I', 'I', 'S', 'S')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接