打乱列表,但保持某些元素不变

15

我有一个问题:

有一个元素列表,其中的元素属于 CAnswer 类(无需描述该类),我需要对它进行乱序操作。但是有一个限制,列表中有一些元素的 CAnswer.freeze 属性被设置为 True,这些元素不应该被打乱,而是保持在原来的位置上。例如,给定一个列表:

[a, b, c, d, e, f]

如果所有元素都是CAnswer的实例,但是c.freeze == True,而其他元素freeze == False,则可能的结果如下:

[e, a, c, f, b, d]

因此,索引为2的元素仍然位于其位置。

实现这个功能的最佳算法是什么?

先行致谢 :)


5
你尝试过什么? - David Robinson
5
我已尝试了两种方法-第一种是仅使用random.shuffle(),然后将某些元素恢复到它们的原始位置。另一种方法是使用random.choice随机选取元素进行排序,或选择某些元素作为“冻结”元素。但这两种方法似乎有点不太优雅,而且明显不符合Python语言的特点。 - Paweł Sopel
5个回答

14

另一种解决方案:

# memorize position of fixed elements
fixed = [(pos, item) for (pos,item) in enumerate(items) if item.freeze]
# shuffle list
random.shuffle(items)
# swap fixed elements back to their original position
for pos, item in fixed:
    index = items.index(item)
    items[pos], items[index] = items[index], items[pos]

这是我所做的,但不那么符合Python的风格——没有使用列表推导式,也没有同时迭代两个元素,谢谢! - Paweł Sopel
刚在我的脚本中实现了这个功能,完美运行,同时非常符合Python的优雅风格。再次感谢!StackOverflow的成员们统治着世界 ;) - Paweł Sopel
2
@Pawel:如果列表中有重复项,这个解决方案可能会出问题。此外(不太重要的是),items.index() 可以扫描整个列表以查找单个值。这使得算法的时间复杂度为二次方。 - jfs
@J.F. Sebastian,你说得对,感谢指出这一点!然而,对于手头的问题(将测验问题的答案洗牌),两者都不应该成为问题。 - tobias_k
它们并不是测验问题的确切答案,而是类似于调查系统中的答案。在列表中不应期望重复项,其长度很少超过10个,几乎从不超过30个,因此我认为这种方式完全符合我的需求——清晰、简单,也是对我来说最重要的——易于理解。我是Python新手(甚至不是程序员或IT人员),因此我需要一些我能理解和学到东西的代码。 - Paweł Sopel
简单而且可能足够好,但请注意如果重要的是只洗牌那些没有被冻结的项目,这个解决方案似乎很脆弱。它将随机化比预期更大的集合。随着固定元素数量在列表总元素数量中所占比例越来越大,这将变得越来越明显。 - FredrikHedman

12

一个解决方案:

def fixed_shuffle(lst):
    unfrozen_indices, unfrozen_subset = zip(*[(i, e) for i, e in enumerate(lst)
                                            if not e.freeze])
    unfrozen_indices = list(unfrozen_indices)
    random.shuffle(unfrozen_indices)
    for i, e in zip(unfrozen_indices, unfrozen_subset):
        lst[i] = e

注意:如果lst是一个 NumPy数组 而不是一个常规列表,则可以更简单一些:

def fixed_shuffle_numpy(lst):
    unfrozen_indices = [i for i, e in enumerate(lst) if not e.freeze]
    unfrozen_set = lst[unfrozen_indices]
    random.shuffle(unfrozen_set)
    lst[unfrozen_indices] = unfrozen_set

它的使用示例:

class CAnswer:
    def __init__(self, x, freeze=False):
        self.x = x
        self.freeze = freeze

    def __cmp__(self, other):
        return self.x.__cmp__(other.x)

    def __repr__(self):
        return "<CAnswer: %s>" % self.x


lst = [CAnswer(3), CAnswer(2), CAnswer(0, True), CAnswer(1), CAnswer(5),
       CAnswer(9, True), CAnswer(4)]

fixed_shuffle(lst)

看起来不错,应该能用,但作为一个新手,我需要几秒钟来理解。非常感谢! - Paweł Sopel
不错的代码。只洗牌未冻结的索引,没有其他操作。 - FredrikHedman

10

使用random.shuffle()源代码,在O(n)的时间复杂度和常数空间复杂度内完成:

from random import random

def shuffle_with_freeze(x):
    for i in reversed(xrange(1, len(x))):
        if x[i].freeze: continue # fixed
        # pick an element in x[:i+1] with which to exchange x[i]
        j = int(random() * (i+1))
        if x[j].freeze: continue #NOTE: it might make it less random
        x[i], x[j] = x[j], x[i] # swap

1

过度设计的解决方案:创建一个包含未冻结元素索引并模拟列表的包装类,并确保setter将写入原始列表:

class IndexedFilterList:
    def __init__(self, originalList, filterFunc):
        self.originalList = originalList
        self.indexes = [i for i, x in enumerate(originalList) if filterFunc(x)]

    def __len__(self):
        return len(self.indexes)

    def __getitem__(self, i):
        return self.originalList[self.indexes[i]]

    def __setitem__(self, i, value):
        self.originalList[self.indexes[i]] = value

请拨打电话:

random.shuffle(IndexedFilterList(mylist, lambda c: not c.freeze))

1
利用列表拥有快速删除和插入的特性:
  • 枚举固定元素并复制它们及其索引
  • 从列表中删除固定元素
  • 洗牌剩余子集
  • 将固定元素放回去
请参见https://dev59.com/0mkw5IYBdhLWcg3w8O2o#25233037,以获取更一般的解决方案。
这将使用内存开销较大的原地操作,取决于列表中固定元素的数量。时间复杂度为线性。可以使用shuffle_subset的一个可能的实现:
#!/usr/bin/env python
"""Shuffle elements in a list, except for a sub-set of the elments.

The sub-set are those elements that should retain their position in
the list.  Some example usage:

>>> from collections import namedtuple
>>> class CAnswer(namedtuple("CAnswer","x fixed")):
...             def __bool__(self):
...                     return self.fixed is True
...             __nonzero__ = __bool__  # For Python 2. Called by bool in Py2.
...             def __repr__(self):
...                     return "<CA: {}>".format(self.x)
...
>>> val = [3, 2, 0, 1, 5, 9, 4]
>>> fix = [2, 5]
>>> lst = [ CAnswer(v, i in fix) for i, v in enumerate(val)]

>>> print("Start   ", 0, ": ", lst)
Start    0 :  [<CA: 3>, <CA: 2>, <CA: 0>, <CA: 1>, <CA: 5>, <CA: 9>, <CA: 4>]

Using a predicate to filter.

>>> for i in range(4):  # doctest: +NORMALIZE_WHITESPACE
...     shuffle_subset(lst, lambda x : x.fixed)
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>]

>>> for i in range(4):                # doctest: +NORMALIZE_WHITESPACE
...     shuffle_subset(lst)           # predicate = bool()
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>]

"""
from __future__ import print_function
import random


def shuffle_subset(lst, predicate=None):
    """All elements in lst, except a sub-set, are shuffled.

    The predicate defines the sub-set of elements in lst that should
    not be shuffled:

      + The predicate is a callable that returns True for fixed
      elements, predicate(element) --> True or False.

      + If the predicate is None extract those elements where
      bool(element) == True.

    """
    predicate = bool if predicate is None else predicate
    fixed_subset = [(i, e) for i, e in enumerate(lst) if predicate(e)]

    fixed_subset.reverse()      # Delete fixed elements from high index to low.
    for i, _ in fixed_subset:
        del lst[i]

    random.shuffle(lst)

    fixed_subset.reverse()      # Insert fixed elements from low index to high.
    for i, e in fixed_subset:
        lst.insert(i, e)

if __name__ == "__main__":
    import doctest
    doctest.testmod()

利用列表具有快速删除和插入的特点:实际上,删除是线性时间,插入也是线性时间。只有添加操作是常数时间。 - juanpa.arrivillaga

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