【问题标题】:In-place way to apply a permutation to a list? (inverse of sorting-by-key)将排列应用于列表的就地方式? (按键排序的逆)
【发布时间】:2011-08-31 04:38:03
【问题描述】:

这是我想做的一个例子

spam_list = ["We", "are", "the", "knights", "who", "say", "Ni"]
spam_order = [0,1,2,4,5,6,3]
spam_list.magical_sort(spam_order)
print(spam_list)

["We", "are", "the", "who", "say", "Ni", "knights"]

我可以用enumeratelist等来做,但我想直接影响spam_list,比如list.sort(),而不是像sorted()一样复制它

编辑:推送一个字符串示例以避免spam_list的索引和值之间的混淆

编辑:原来这是Python sort parallel arrays in place? 的副本。好吧,我不能删除这么多关于 SO 一致性参数的努力。

【问题讨论】:

  • 我假设您也不会接受复制spam_order的解决方案?
  • 嗯,这个我可以忍受。 spam_order 只是 ints 的一个列表,它并没有那么耗费内存。
  • 这是一个有趣的问题,因为根据上下文,真正的解决方案可能位于上游。我可以想到三种情况:1)排序索引不同并且独立于数据,2)排序索引是固定的并且独立于数据,3)排序索引来自上游某处的数据。这三者中的每一个都为实时代码提出了不同的解决方案集。
  • 我的问题是 2)。但是,是的,其他人也很有趣。
  • 这里的大多数解决方案都非常好,但解决了最普遍的情况,即#1。如果索引顺序在必须排序的大量变量列表上是固定的,则可能会有更有效的解决方案利用这一事实。

标签: python list sorting


【解决方案1】:

你可以使用 numpy。

import numpy as np
spam_list = list(np.array(spam_list)[spam_order])

【讨论】:

    【解决方案2】:

    但我想直接影响 spam_list,比如 list.sort(),而不是像 sorted() 那样复制它

    只有 一个 解决方案,完全符合您的要求。 其他每一个解决方案都隐含地复制一个或两个列表(或将其转换为字典等)。您要求的是一种对两个列表进行就地排序的方法,使用O(1) 额外空间,使用一个列表作为另一个列表的键。我个人会接受额外的空间复杂度,但如果你真的愿意,你可以这样做:

    (编辑:可能是原始发布者并不真正关心.sort,因为它很有效,而是因为它修改了状态;总的来说,这是一件危险的事情,非低级语言试图避免这种情况甚至禁止它,但使用切片分配的解决方案将实现“就地”语义)

    • 创建一个自定义字典子类(实际上是 Zip 类),它由您正在排序的两个列表支持。
    • 索引myZip[i] -> 生成元组(list1[i],list2[i])
    • 分配myZip[i]=(x1,x2) -> 分派到list1[i]=x1, list2[i]=x2
    • 使用 that 来执行myZip(spam_list,spam_order).sort(),现在spam_listspam_order 都已就地排序

    例子:

    #!/usr/bin/python3
    
    class LiveZip(list):
        def __init__(self, list1, list2):
            self.list1 = list1
            self.list2 = list2
    
        def __len__(self):
            return len(self.list1)
    
        def __getitem__(self, i):
            return (self.list1[i], self.list2[i])
    
        def __setitem__(self, i, tuple):
            x1,x2 = tuple
            self.list1[i] = x1
            self.list2[i] = x2
    
    spam_list = ["We", "are", "the", "knights", "who", "say", "Ni"]
    spam_order = [0,1,2,4,5,6,3]
    
    #spam_list.magical_sort(spam_order)
    proxy = LiveZip(spam_order, spam_list)
    

    现在让我们看看它是否有效......

    #proxy.sort()
    #fail --> oops, the internal implementation is not meant to be subclassed! lame
    # It turns out that the python [].sort method does NOT work without passing in
    # a list to the constructor (i.e. the internal implementation does not use the
    # public interface), so you HAVE to implement your own sort if you want to not
    # use any extra space. This kind of dumb. But the approach above means you can 
    # just use any standard textbook in-place sorting algorithm:
    def myInPlaceSort(x):
        # [replace with in-place textbook sorting algorithm]
    

    现在可以了:

    myInPlaceSort(proxy)
    
    print(spam_list)
    

    不幸的是没有办法只对O(1)空间中的一个列表进行排序而不对另一个列表进行排序;如果您不想对两个列表都进行排序,则不妨采用构建虚拟列表的原始方法。

    但是,您可以执行以下操作:

    spam_list.sort(key=lambda x:x)

    但是如果 key 或 cmp 函数对任何集合进行了任何引用(例如,如果您传入一个必须构造的 dict 的 dict.__getitem__),这并不比您原来的 O(N)-space 方法好,除非您已经碰巧有这样一本字典了。

    原来这是 Python sort parallel arrays in place? 的重复问题,但除了 this one 之外,该问题也没有正确答案,这与我的相同,但没有示例代码。除非您对代码进行了难以置信的优化或专门化,否则我只会使用您的原始解决方案,它在空间复杂度上与其他解决方案相当。

    编辑2: 正如 senderle 指出的那样,OP 根本不需要排序,而是希望,我认为,应用排列。为了实现这一点,您可以并且应该使用简单的索引其他答案建议[spam_list[i] for i in spam_order],但是仍然必须制作显式或隐式副本,因为您仍然需要中间数据。 (无关的,为了记录,应用逆排列是我认为与身份并行排序的逆,你可以使用一个来获得另一个,尽管排序的时间效率较低。_,spam_order_inverse = parallelSort(spam_order, range(N)), then 按spam_order_inverse排序。我把上面关于排序的讨论留作记录。)

    编辑3:

    然而,在O(#cycles) 空间中实现就地排列是可能的,但时间效率很差。每个排列都可以分解为并行应用于子集的不相交排列。这些子集称为循环或轨道。周期等于它们的大小。因此,您信心大增,并执行以下操作:

    Create a temp variable.
    
    For index i=0...N:
        Put x_i into temp, assign NULL to x_i
        Swap temp with x_p(i)
        Swap temp with x_p(p(i))
        ...
        Swap temp with x_p(..p(i)..), which is x_i
        Put a "do not repeat" marker on the smallest element you visited larger than i
        Whenever you encounter a "do not repeat" marker, perform the loop again but
          without swapping, moving the marker to the smallest element larger than i    
        To avoid having to perform the loop again, use a bloom filter
    

    这将在 O(N^2) 时间和 O(#cycles) 地方运行,没有布隆过滤器,或者如果你使用它们,则在 ~O(N) 时间和 O(#cycle + bloomfilter_space) 空间

    【讨论】:

    • 好吧@Space_c0wb0y 上面的解决方案似乎没有复制任何列表。
    • @Evpok:是的,我相应地编辑了我的答案。感谢@ninjagecko 指出这一点。
    • @ninjagecko:您的解决方案对我来说非常不清楚。它在 O(1) 空间中是如何工作的?
    • @Evpok:是的,Python 使用引用。当您复制listdict 时,不会复制实际值,只会复制引用。
    • @ninjagecko,我认为这不是 OP 想要的。这进行了并行排序,但 OP 要求的是一个 - 我不知道确切地称呼它 - 一个“转换排序”。换句话说,OP 想要["We", "are", "the", "who", "say", "Ni", "knights"],但这给了['We', 'are', 'the', 'Ni', 'knights', 'who', 'say']
    【解决方案3】:

    如果您实际上根本不关心效率,而只想要就地语义(这有点奇怪,因为有整个编程语言都致力于避免就地语义),那么您可以这样做:

    def modifyList(toModify, newList):
        toModify[:] = newList
    
    def permuteAndUpdate(toPermute, permutation):
        modifyList(toPermute, [toPermute[i] for i in permutation])
    
    permuteAndUpdate(spam_list, spam_order)
    
    print(spam_list)
    # ['We', 'are', 'the', 'Ni', 'knights', 'who', 'say']
    

    感谢 senderle 认识到这可能是 OP 实际所追求的;他应该随时将这个答案复制到他自己的答案中。除非您真的更喜欢他的答案,否则不应接受此答案。

    【讨论】:

    • 懒得编辑我的帖子,但如果你这样做我不会生气。 :)
    • 顺便说一句,我以前从未想过将置换应用程序视为逆排序,但当然这正是它的本质——感谢您的洞察力。
    【解决方案4】:

    如果问题是 in-placeness 而不是内存使用本身——如果你希望这有副作用,换句话说——那么你可以使用切片分配。盗用Peter Collingridge

    other_spam_list = spam_list
    spam_list[:] = [spam_list[i] for i in spam_order]
    assert other_spam_list == spam_list
    

    看来您甚至可以使用生成器表达式来做到这一点!但我怀疑这仍然隐含地创建了某种新的序列——可能是一个元组。如果没有,我认为它会表现出错误的行为;但我测试了它,它的行为似乎是正确的。

    spam_list[:] = (spam_list[i] for i in spam_order)
    

    啊哈!通过无与伦比的Sven Marnach 查看这个出色的答案——生成器切片分配确实生成了一个隐式元组。这意味着它是安全的,但不像您想象的那么高效。尽管如此,元组比列表更节省内存,因此从这个角度来看,生成器表达式更可取。

    【讨论】:

    • 嗯,你可能是对的,OP 并不真正关心效率,实际上只需要就地语义。这有点奇怪,因为整个编程语言都致力于避免就地语义......但这可能确实是他想要的,如果是这样,使用切片的函数绝对是要走的路。
    • 随意将stackoverflow.com/questions/6098250/… 的任何部分复制到您的答案中;我澄清了一点,但不想在未经许可的情况下编辑您的帖子
    • 盗用和改进。感谢您指出切片分配。我以前没见过。
    • 其实我并不关心就地语义,只想要效率,但无论如何谢谢你=)
    【解决方案5】:

    你可以给排序函数一个特殊的key

    order = dict(zip(spam_list, spam_order))
    spam_list.sort(key=order.get)
    

    编辑:正如@ninjagecko 在his answer 中指出的那样,这并不是很有效,因为它会复制两个列表来创建查找字典。但是,对于OP给出的修改示例,这是唯一的方法,因为必须建立一些索引。好处是,至少对于字符串,值不会被复制,所以开销只是字典本身的开销。

    【讨论】:

    • 前者非常适合我,后者是另外一回事。不过两者都还不错。非常感谢。
    • +1 用于实际使用 sort() 函数。我不知道你可以指定这样的键。
    • 好吧,跳到@ninjagecko的解决方案。
    • @Evpok:我认为@ninjagecko 的解决方案在这里没有任何好处。您需要一些索引结构来查找项目的顺序,这需要空间。 dict 是一个非常有效的索引结构,所以你不妨使用它。
    • 经过一些测试和分析后,这个解决方案似乎使用了两倍于map(spam_list.__getitem__, spam_order) 的调用并且没有节省那么多内存,所以我最后一次更改了 cmy 选择(希望如此)。
    【解决方案6】:
    map(lambda x:spam_list[x], spam_order)
    

    【讨论】:

      【解决方案7】:

      你可以试试:

      spam_list = [spam_list[i] for i in spam_order]
      

      【讨论】:

      • 好一个。一行,减轻了我对zip 之类的混乱,但仍然不是我想要的。不过谢谢 =)
      • 我更喜欢这个解决方案,它可以通过切片分配更原地进行——尽管仍然会生成一个副本。
      • 我个人可能会spam_list = map(spam_list.__getitem__, spam_order),它的工作原理大致相同,但查找次数更少。
      • @Yann Vernier,有趣的一点——快速测试确认您的建议更快。 (但也许并不奇怪,lambda expression version 更慢。)
      • 这根本不存在,它会创建一个新列表。
      猜你喜欢
      • 1970-01-01
      • 2014-02-18
      • 2014-02-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-09
      • 2011-09-13
      相关资源
      最近更新 更多