如何使用生成器在Python中生成一个列表的排列,但是要避免“反向重复”的情况。

14

这与问题“如何在Python中生成列表的所有排列”有关。

如何生成符合以下条件的所有排列:如果两个排列是彼此的反转(即[1,2,3,4]和[4,3,2,1]),则它们被视为相等,最终结果中只应包含其中一个。

示例:

permutations_without_duplicates ([1,2,3])
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]

我正在对包含唯一整数的列表进行排列。

由于结果排列数量很多,因此我希望尽可能使用Python的生成器。

编辑:如果可能的话,我不想将所有排列的列表存储到内存中。

10个回答

15

在SilentGhost的建议基础上,我有一个了不起的跟进 - 发布独立答案,因为评论的边界太窄而无法包含代码 :-)

itertools.permutations是内置的(自2.6以来)并且很快。我们只需要一个过滤条件,对于每个(perm,perm [:: -1]),只接受其中一个。由于OP说项目始终不同,因此我们只需比较任意两个元素:

for p in itertools.permutations(range(3)):
    if p[0] <= p[-1]:
        print(p)

输出结果为:

(0, 1, 2)
(0, 2, 1)
(1, 0, 2)

这个方法有效是因为反转排列总是会翻转第一个和最后一个元素之间的关系!

对于四个或更多元素,其他在中心对称的元素对(例如从两侧数过来的第二个:p [1] <= p [:: -1] [1])也可以使用。
(此前的回答声称p [0]&lt; p [1]可以工作,但事实并非如此——在p被反转后,它会选择不同的元素。)

您还可以对整个排列及其反向进行直接词典序比较:

for p in itertools.permutations(range(3)):
    if p <= p[::-1]:
        print(p)

我不确定是否有更有效的过滤方法。 itertools.permutations 保证按字典顺序排列,但字典位置 pp[::-1] 之间存在一种相当复杂的关系。特别是仅在中间停止并不起作用。

但我怀疑(没有检查)具有2:1过滤器的内置迭代器将优于任何自定义实现。 当然,它在简单性方面胜出!


1
很棒的评论,为一个出色的答案开了个好头 :) :) - viksit
2
这个简单的测试只有在你的集合中不包含重复项时才能工作。否则,你将不得不像Steve Jessop建议的那样进行完整的词典比较。 - Thomas Ahle
2
比较应该使用 <= 而不是 <,这样才能适用于 n=1 的特殊情况。 - Sven Marnach
哪一个更好?我应该使用 p[0] <= p[-1] 还是 p <= p[::-1] - Wei Shan Lee
你有没有想过如何在初始列表是非可比较对象的情况下实现这个功能(即Python不接受它们之间的<关系)? - Shoval Sadde

12
如果您按照字典序生成排列,那么您不需要存储任何内容来确定是否已经看到了给定排列的反转。你只需要将其与其反转进行字典序比较 - 如果它更小则返回它,如果更大则跳过它。
可能有一种更有效的方法,但这种方法简单并具有所需的属性(可实现为生成器,使用 O(n) 工作内存)。

这真是太棒了。非常简单而优雅。谢谢。 - Eric Hanson

4

这是ChristopheD接受的答案的更简洁、更快速的版本,我非常喜欢。递归很好用。我通过删除重复项来强制执行传入列表的唯一性,不过也许应该引发异常。

def fac(x): 
    return (1 if x==0 else x * fac(x-1))

def permz(plist):
    plist = sorted(set(plist))
    plen = len(plist)
    limit = fac(plen) / 2
    counter = 0
    if plen==1:
        yield plist
    else:
        for perm in permz(plist[1:]):
            for i in xrange(plen):
                if counter == limit:
                     raise StopIteration
                counter += 1
                yield perm[:i] + plist[0:1] + perm[i:]

# ---- testing ----
plists = [
    list('love'),
    range(5),
    [1,4,2,3,9],
    ['a',2,2.1],
    range(8)]               

for plist in plists:
    perms = list(permz(plist))
    print plist, True in [(list(reversed(i)) in foo) for i in perms]

3

编辑:完全更改以保持所有内容作为生成器(从不在内存中保存整个列表)。应该满足要求(仅计算可能排列的一半(而非反向排列)。 编辑2:从这里添加了更短(更简单)的阶乘函数。

编辑3:(见评论)-可以在bwopah的版本中找到改进版。

def fac(x): 
    return (1 if x==0 else x * fac(x-1))

def all_permutations(plist):
    global counter

    if len(plist) <=1:
        yield plist
    else:
        for perm in all_permutations(plist[1:]):
            for i in xrange(len(perm)+1):
                if len(perm[:i] + plist[0:1] + perm[i:]) == lenplist:
                        if counter == limit:
                             raise StopIteration
                        else:
                             counter = counter + 1
                yield perm[:i] + plist[0:1] + perm[i:]

counter = 0
plist = ['a','b','c']
lenplist = len(plist)
limit = fac(lenplist) / 2

all_permutations_gen = all_permutations(plist)
print all_permutations_gen
print list(all_permutations_gen)

计数器在这里不应该是全局的,作为本地变量同样有效。您也可以使用counter += 1而不是counter = counter + 1。 - Kiv
限制和lenplist最好也是本地的。 - Paul
1
将限制局部递归使其更快并使以下内容变得不必要: 如果len(perm [:i] + plist [0:1] + perm [i:])== lenplist,则不需要。 - Paul
请看这里以获取更优化的版本:https://dev59.com/XnNA5IYBdhLWcg3wcNbF#962134 - Paul
@Kiv,bpowah:好观点(这是一个快速版本;-)我本来会调整我的版本,但由于bpowah发布了更优化的版本,我将在帖子顶部链接到那个版本。谢谢! - ChristopheD

2
这个怎么样:
from itertools import permutations

def rev_generator(plist):
    reversed_elements = set()
    for i in permutations(plist):
        if i not in reversed_elements:
            reversed_i = tuple(reversed(i))
            reversed_elements.add(reversed_i)
            yield i

>>> list(rev_generator([1,2,3]))
[(1, 2, 3), (1, 3, 2), (2, 1, 3)]

此外,如果返回值必须是一个列表,您可以将yield i更改为yield list(i),但为了迭代目的,元组也能正常工作。


1
这个程序在内存中保存排列的列表(reversed_elements),但我想避免这种情况。 - Juha Syrjälä
1
你为什么要使用zip(*reversed(zip(i)))[0]而不是list(reversed(i))? - Nadia Alramli
我稍微整理了一下代码,它可以在Python2.6和3.0中运行。 - dbr
Nadia:不知道元组构造函数,决定聪明一点而不是查找它。 :P更直接的答案:它需要是一个元组,而不是列表。 - Patrick

2
这里是能够解决问题的代码。为了去除重复项,我注意到,如果列表的第一个位置的值大于最后一个位置的值,则它将成为重复项。我创建了一个映射来跟踪每个项目在列表中的位置,并使用该映射进行测试。此代码还不使用递归,因此它保持其内存占用量较小。此外,该列表可以是任何类型的项目,而不仅仅是数字,请参见最后两个测试用例。
class Permutation:
    def __init__(self, justalist):
        self._data = justalist[:]
        self._len=len(self._data)
        self._s=[]
        self._nfact=1
        self._map ={}
        i=0
        for elem in self._data:
            self._s.append(elem)
            self._map[str(elem)]=i
            i+=1
            self._nfact*=i
        if i != 0:
            self._nfact2=self._nfact//i

    def __iter__(self):
        for k in range(self._nfact):
            for i in range(self._len):
                self._s[i]=self._data[i]
            s=self._s
            factorial=self._nfact2
            for i in range(self._len-1):
                tempi = (k // factorial) % (self._len - i)
                temp = s[i + tempi]
                for j in range(i + tempi,i,-1):
                    s[j] = s[j-1]
                s[i] = temp
                factorial //= (self._len - (i + 1))

            if self._map[str(s[0])] < self._map[str(s[-1])]:
                yield s




s=[1,2]
print("_"*25)
print("input list:",s)
for sx in Permutation(s):
    print(sx)

s=[1,2,3]
print("_"*25)
print("input list:",s)
for sx in Permutation(s):
    print(sx)

s=[1,2,3,4]
print("_"*25)
print("input list:",s)
for sx in Permutation(s):
    print(sx)

s=[3,2,1]
print("_"*25)
print("input list:",s)
for sx in Permutation(s):
    print(sx)

s=["Apple","Pear","Orange"]
print("_"*25)
print("input list:",s)
for sx in Permutation(s):
    print(sx)

s=[[1,4,5],"Pear",(1,6,9),Permutation([])]
print("_"*25)
print("input list:",s)
for sx in Permutation(s):
    print(sx)

这是我的测试案例的输出结果。

_________________________
input list: [1, 2]
[1, 2]
_________________________
input list: [1, 2, 3]
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
_________________________
input list: [1, 2, 3, 4]
[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, 4, 1, 3]
[3, 1, 2, 4]
[3, 2, 1, 4]
_________________________
input list: [3, 2, 1]
[3, 2, 1]
[3, 1, 2]
[2, 3, 1]
_________________________
input list: ['Apple', 'Pear', 'Orange']
['Apple', 'Pear', 'Orange']
['Apple', 'Orange', 'Pear']
['Pear', 'Apple', 'Orange']
_________________________
input list: [[1, 4, 5], 'Pear', (1, 6, 9), <__main__.Permutation object at 0x0142DEF0>]
[[1, 4, 5], 'Pear', (1, 6, 9), <__main__.Permutation object at 0x0142DEF0>]
[[1, 4, 5], 'Pear', <__main__.Permutation object at 0x0142DEF0>, (1, 6, 9)]
[[1, 4, 5], (1, 6, 9), 'Pear', <__main__.Permutation object at 0x0142DEF0>]
[[1, 4, 5], (1, 6, 9), <__main__.Permutation object at 0x0142DEF0>, 'Pear']
[[1, 4, 5], <__main__.Permutation object at 0x0142DEF0>, 'Pear', (1, 6, 9)]
[[1, 4, 5], <__main__.Permutation object at 0x0142DEF0>, (1, 6, 9), 'Pear']
['Pear', [1, 4, 5], (1, 6, 9), <__main__.Permutation object at 0x0142DEF0>]
['Pear', [1, 4, 5], <__main__.Permutation object at 0x0142DEF0>, (1, 6, 9)]
['Pear', (1, 6, 9), [1, 4, 5], <__main__.Permutation object at 0x0142DEF0>]
['Pear', <__main__.Permutation object at 0x0142DEF0>, [1, 4, 5], (1, 6, 9)]
[(1, 6, 9), [1, 4, 5], 'Pear', <__main__.Permutation object at 0x0142DEF0>]
[(1, 6, 9), 'Pear', [1, 4, 5], <__main__.Permutation object at 0x0142DEF0>]

2

这是我的实现:

a = [1,2,3,4]

def p(l):
  if len(l) <= 1:
    yield l
  else:
    for i in range(len(l)):
      for q in p([l[j] for j in range(len(l)) if j != i]):
        yield [l[i]] + q

out = (i for i in p(a) if i < i[::-1])

P函数是一个常规的排列函数,可以得出所有可能性。当迭代结果时,可以使用过滤器。简单来说,它有两个可能的结果:所有排列的较小一半和较大一半。在这个例子中,输出包含列表的较小一半。


1

这是一个按照 onebyone 提议实现的程序。

http://en.wikipedia.org/wiki/Permutation#Lexicographical_order_generation 中可以得到以下算法,用于在给定排列后逐字典生成下一个排列。它直接改变所给出的排列。

  1. 找到最高的下标 i,使得 s[i] < s[i+1]。如果不存在这样的下标,则该排列是最后一个排列。
  2. 找到最高的下标 j,使得 s[j] > s[i] 并且 j > i。这样一个 j 必须存在,因为 i+1 就是这样一个下标。
  3. 交换 s[i] 和 s[j]。
  4. 反转下标大于 i 的所有元素的顺序

函数如下:

def perms(items):
    items.sort()
    yield items[:]
    m = [len(items)-2]  # step 1
    while m:
        i = m[-1]
        j = [ j for j in range(i+1,len(items)) if items[j]>items[i] ][-1] # step 2
        items[i], items[j] = items[j], items[i] # step 3
        items[i+1:] = list(reversed(items[i+1:])) # step 4
        if items<list(reversed(items)):
            yield items[:]
        m = [ i for i in range(len(items)-1) if items[i]<items[i+1] ]  # step 1

检查我们的工作:

>>> foo=list(perms([1,3,2,4,5]))
>>> True in [(list(reversed(i)) in foo) for i in foo]
False

1

首先是一些设置代码:

try:
    from itertools import permutations
except ImportError:
    # straight from http://docs.python.org/library/itertools.html#itertools.permutations
    def permutations(iterable, r=None):
        # permutations('ABCD', 2) --> AB AC AD BA BC BD CA CB CD DA DB DC
        # permutations(range(3)) --> 012 021 102 120 201 210
        pool = tuple(iterable)
        n = len(pool)
        r = n if r is None else r
        if r > n:
            return
        indices = range(n)
        cycles = range(n, n-r, -1)
        yield tuple(pool[i] for i in indices[:r])
        while n:
            for i in reversed(range(r)):
                cycles[i] -= 1
                if cycles[i] == 0:
                    indices[i:] = indices[i+1:] + indices[i:i+1]
                    cycles[i] = n - i
                else:
                    j = cycles[i]
                    indices[i], indices[-j] = indices[-j], indices[i]
                    yield tuple(pool[i] for i in indices[:r])
                    break
            else:
                return

def non_reversed_permutations(iterable):
    "Return non-reversed permutations for an iterable with unique items"
    for permutation in permutations(iterable):
        if permutation[0] < permutation[-1]:
            yield permutation

2
根据具体版本来做似乎有些不太正规。为什么不尝试导入模块,如果失败了再定义函数呢? - Ryan Ginstrom

-2

这将给我所有的排列组合,我只需要其中的一半。itertools.permutations([1,2,3]) 将包含 [1,2,3] 和 [3,2,1],而我只需要其中的一个。 - Juha Syrjälä
那么,问题是什么?找到排列的反向版本并删除它。检查最终列表的大小是否为itertools.permutations结果大小的一半。 - SilentGhost
1
@SilentGhost,这种方法需要我将所有排列都存储在内存中,我想避免这种情况。 @CristopheD,2.6没有问题。 - Juha Syrjälä
正好一半?列表长度为n的排列数量为2^n。然而,如果列表中所有元素都相同,则您希望迭代器仅返回一个元素,我猜想是这样? - Stephan202
@Christophe:当然,你是对的。我把排列集和幂集搞混了。我应该去睡觉了 ;) - Stephan202
显示剩余2条评论

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