【问题标题】:Why does Python's itertools.permutations contain duplicates? (When the original list has duplicates)为什么 Python 的 itertools.permutations 包含重复项? (当原始列表有重复时)
【发布时间】:2011-09-25 22:33:21
【问题描述】:

普遍认为n个不同符号的列表有n!排列。然而,当符号不明确时,数学和其他领域中最常见的约定似乎是只计算不同的排列。因此列表[1, 1, 2] 的排列通常被认为是
[1, 1, 2], [1, 2, 1], [2, 1, 1]。事实上,下面的 C++ 代码正好打印出这三个:

int a[] = {1, 1, 2};
do {
    cout<<a[0]<<" "<<a[1]<<" "<<a[2]<<endl;
} while(next_permutation(a,a+3));

另一方面,Python 的 itertools.permutations 似乎打印了其他内容:

import itertools
for a in itertools.permutations([1, 1, 2]):
    print a

打印出来

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

正如用户Artsiom Rudzenka 在回答中指出的那样,Python documentation 是这么说的:

元素根据它们的位置而不是它们的值被视为唯一的。

我的问题:为什么做出这个设计决定?

似乎遵循通常的约定会产生更有用的结果(实际上它通常正是我想要的)......或者是否有一些我缺少的 Python 行为应用程序?

[或者是一些实现问题? next_permutation 中的算法——例如 StackOverflow here (by me)shown here to be O(1) amortised 中解释的算法——在 Python 中似乎是高效且可实现的,但 Python 是否做得更高效,因为它不能保证基于值的字典顺序?如果是这样,那么提高效率是否值得?]

【问题讨论】:

  • 根据documentation Python 确实保证字典顺序。
  • 上面的输出示例似乎没有排序(1,2,1 在 1,1,2 之前)。也许是因为元素不是唯一的?
  • @Macke:是的,这就是我的意思——字典顺序是基于位置,而不是价值。如果您将两个 1 视为“1”和“1+”,第二个更大,那么 (1,2,1+) 在 (1+,1,2) 之前就可以了。但是,当然,1 是 1。:-) 另外,如果您要求它提供 [3,2,1] 的排列(例如),那么结果实际上将按字典顺序倒序排列。如果你要求 [2, 1,3],它们都不会出现。关键是 Python 不看值,只看位置。
  • 我也想知道。特别是因为“元素根据其位置而不是其价值被视为唯一”似乎是多余的 - 一次只有一个元素可以占据特定位置,所以基本上他们说“我们假设所有元素都是不同的”或“我们不'不检查解决方案的唯一性"。

标签: python algorithm language-design permutation


【解决方案1】:

重温这个老问题,现在最简单的方法就是使用more_itertools.distinct_permutations

【讨论】:

    【解决方案2】:

    我还发现itertools 没有用于更直观的独特排列概念的函数也令人惊讶。对于任何严肃的应用程序来说,只生成重复排列以选择其中唯一的排列是不可能的。

    我编写了自己的迭代生成器函数,其行为类似于itertools.permutations,但不返回重复项。仅考虑原始列表的排列,可以使用标准 itertools 库创建子列表。

    def unique_permutations(t):
        lt = list(t)
        lnt = len(lt)
        if lnt == 1:
            yield lt
        st = set(t)
        for d in st:
            lt.remove(d)
            for perm in unique_permutations(lt):
                yield [d]+perm
            lt.append(d)
    

    【讨论】:

    • 谢谢。在我上面的回答中,我有一个link to code,它有 3 种方法,以及一些时间比较——你能测试一下你的unique_permutationsm_itertoolspm_nextperm_bm_nextperm_s 相比有多快吗?跨度>
    • 我按照你的建议测试了速度,而且——不出所料——我的代码比你建议的两个选项慢 5 到 10 倍。递归和列表修改是有代价的。尽管如此,它还是轻松地击败了 itertools 解决方法数百倍。我只是建议将其作为一种替代方案,如果它恰好更适合不同的目的,那么有人可能会找到改进的方法。
    【解决方案3】:

    我接受 Gareth Rees 的回答作为最吸引人的解释(缺少 Python 库设计者的回答),即 Python 的 itertools.permutations 不比较元素的值。想一想,这就是问题所要问的,但我现在看到它如何被视为一种优势,这取决于人们通常使用itertools.permutations 的目的。

    为了完整起见,我比较了三种生成所有不同排列的方法。方法 1 在内存和时间方面效率非常低,但需要的新代码最少,它是包装 Python 的 itertools.permutations,如 zeekay 的回答。方法 2 是 C++ 的 next_permutation 的基于生成器的版本,来自 this blog post。方法3是我写的更接近C++'s next_permutation algorithm的东西;它会就地修改列表(我没有把它说得太笼统)。

    def next_permutationS(l):
        n = len(l)
        #Step 1: Find tail
        last = n-1 #tail is from `last` to end
        while last>0:
            if l[last-1] < l[last]: break
            last -= 1
        #Step 2: Increase the number just before tail
        if last>0:
            small = l[last-1]
            big = n-1
            while l[big] <= small: big -= 1
            l[last-1], l[big] = l[big], small
        #Step 3: Reverse tail
        i = last
        j = n-1
        while i < j:
            l[i], l[j] = l[j], l[i]
            i += 1
            j -= 1
        return last>0
    

    以下是一些结果。我现在更加尊重 Python 的内置函数:当元素全部(或几乎全部)不同时,它的速度大约是其他方法的三到四倍。当然,当有很多重复元素时,使用它是一个糟糕的想法。

    Some results ("us" means microseconds):
    
    l                                       m_itertoolsp  m_nextperm_b  m_nextperm_s
    [1, 1, 2]                               5.98 us       12.3 us       7.54 us
    [1, 2, 3, 4, 5, 6]                      0.63 ms       2.69 ms       1.77 ms
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]         6.93 s        13.68 s       8.75 s
    
    [1, 2, 3, 4, 6, 6, 6]                   3.12 ms       3.34 ms       2.19 ms
    [1, 2, 2, 2, 2, 3, 3, 3, 3, 3]          2400 ms       5.87 ms       3.63 ms
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 2]          2320000 us    89.9 us       51.5 us
    [1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 4, 4]    429000 ms     361 ms        228 ms
    

    如果有人想探索,代码是here

    【讨论】:

    • 结果表中的方法m_itertoolspm_nextperm_bm_nextperm_s分别是指方法1、2、3吗?
    • 你可以通过以下方式反转尾部:l[last:n] = p[n-1:last-1:-1]
    • @IsaacTurner 我似乎错过了您发布的评论。是的,他们在答案中引用了方法 1、2 和 3。而且我还没有尝试过反转尾部的其他方式……它可以缩短代码,但我没有想过它的性能会如何。
    【解决方案4】:

    我不能代表itertools.permutations (Raymond Hettinger) 的设计师,但在我看来,有几点支持该设计:

    首先,如果您使用next_permutation 风格的方法,那么您将被限制为传入支持线性排序的对象。而itertools.permutations 提供any 类对象的排列。想象一下这会有多烦人:

    >>> list(itertools.permutations([1+2j, 1-2j, 2+j, 2-j]))
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: no ordering relation is defined for complex numbers
    

    其次,通过不对对象进行相等性测试,itertools.permutations 避免了在通常情况下不需要支付调用 __eq__ 方法的成本。

    基本上,itertools.permutations 可靠且廉价地解决了常见情况。当然有一个论点是itertools 应该提供一个避免重复排列的函数,但是这样的函数应该是itertools.permutations 的补充,而不是代替它。为什么不写一个这样的函数并提交补丁呢?

    【讨论】:

    • 谢谢,这是一个很好的观点,有时人们想要排列不可比较的元素——为这种情况编写代码,而不是查看值,确实使itertools.permutations 非常快。当然,这实际上是“通常情况”还是“常见情况”取决于用户。 :-) 顺便说一句,向 Python 库提交补丁并执行到最后的整个过程有多容易?
    • 关于效率的好答案和好观点。但是,我不相信这是 itertools.permutations 保留重复项的一个很好的理由。排列要求元素具有可比性是完全合理的。如果明确想要位置排列,可以明确写:([it[index] for index in indexes] for indexes in itertools.permutations(range(len(it))))
    • 我很困惑,为什么你需要对 unique_permutation 进行线性排序?你不只需要平等测试吗?
    • @EhsanKia:看看 OP 建议 Python 应该使用的 implementation of next_permutation。它对正在排列的对象使用&lt; 运算符来查找当前排列之后的最小排列。 (显然有多种方法可以解决这个问题,但它们会降低建议的方法的吸引力。)
    • @NeilG 您的观点是,通过遵循 OP 所需功能的实现来获得索引的排列是微不足道的,这是一个强大的观点。 OP 的设计似乎解决了当前实现解决的所有用例以及许多其他常见用例。而当前的实现并没有直接解决额外的用例。
    【解决方案5】:

    通过包装itertools.permutations 很容易获得您喜欢的行为,这可能会影响决策。如文档中所述,itertools 被设计为构建块/工具的集合,用于构建您自己的迭代器。

    def unique(iterable):
        seen = set()
        for x in iterable:
            if x in seen:
                continue
            seen.add(x)
            yield x
    
    for a in unique(permutations([1, 1, 2])):
        print a
    
    (1, 1, 2)
    (1, 2, 1)
    (2, 1, 1)
    

    但是,正如 cmets 中所指出的,这可能没有您希望的那么有效:

    >>> %timeit iterate(permutations([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]))
    1 loops, best of 3: 4.27 s per loop
    
    >>> %timeit iterate(unique(permutations([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2])))
    1 loops, best of 3: 13.2 s per loop
    

    也许如果有足够的兴趣,可以将新函数或itertools.permutations 的可选参数添加到itertools,以更有效地生成没有重复的排列。

    【讨论】:

    • +1。如果您想要独特的排列,这就是您必须做的。非唯一排列也很有用(也很有趣),但计算成本更高。
    • 这有 Ω(n!) 复杂性来生成所有排列——实际上我认为它是 Ω(nn!) 因为你需要 Ω(n) 时间来比较排列——这非常当列表有重复时,相对于next_permutation 非常糟糕(因此 *actual 排列的数量远小于 n!)。参见例如this post.
    • 而不是 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2],尝试另外几个 1,例如 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]——至少需要一百倍的时间。 :-)
    • 确实!出于好奇,请考虑:[1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],permutations 的效率会高于或低于新的next_permutations执行?主要优点是避免为已经看到的对象生成额外的排列,是吗?
    • 此解决方案的另一个严重问题是内存:由于您将所有可见的排列保存在 set 中,因此您需要的内存与所有排列的总大小一样多……哪种失败使用itertools 的重点。 (例如,对于 [1,2,3,4,5,6,7,8,9,10],这需要在内存中保存所有 10!≈ 300 万个排列,即几兆字节。)
    【解决方案6】:

    也许我错了,但似乎原因在于'Elements are treated as unique based on their position, not on their value. So if the input elements are unique, there will be no repeat values in each permutation.' 您已指定 (1,1,2) 并且从您的角度来看,0 索引处的 1 和 1 索引处的 1 是相同的 - 但事实并非如此,因为排列 python 实现使用索引而不是值。

    因此,如果我们看一下默认的 python 排列实现,我们会发现它使用索引:

    def permutations(iterable, r=None):
        pool = tuple(iterable)
        n = len(pool)
        r = n if r is None else r
        for indices in product(range(n), repeat=r):
            if len(set(indices)) == r:
                yield tuple(pool[i] for i in indices)
    

    例如,如果您将输入更改为 [1,2,3],您将得到正确的排列([(1, 2, 3), (1, 3, 2), (2, 1, 3), ( 2, 3, 1), (3, 1, 2), (3, 2, 1)]) 因为这些值是唯一的。

    【讨论】:

    • 问题是,为什么在我们通常期望其他东西的时候以这种方式实现?
    • @Space_C0wb0y - 哦,对不起 - 但是这个问题应该问已经实现 python 的人。他们为我们提供了教程和 api 参考,因此如果我们不接受它们,我们可以使用它的基本功能或不使用它们。但从教程的角度来看,这种方法可以正常工作
    • 是的,Space_C0wb0y 说得对:我的问题正是为什么会这样。 (一种可能的解释是,它的设计根本没有考虑到包含重复项的列表,如果找到了对此的参考,那将是一个答案。但可能还有其他一些解释。)而且我不认为关于语言背后的设计决策完全超出了本网站的范围:参与语言设计的一组人,或者有权参与讨论,或者对问题有一定的洞察力,可能与用户有重要的交集本网站的。
    猜你喜欢
    • 2022-01-21
    • 2019-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多