Python - 对列表中的部分元素进行洗牌

19

我想要对一个列表中第三个到最后一个位置的元素进行洗牌,以便前两个元素始终保持不变。

list = ['a?','b','c','d','e']

转换为

list = ['a?','b','d','e','c']

由于某种原因,这样做不起作用:

list = ['a?','b','c','d','e']
import random
random.shuffle(list[2:])    
print list

我做错了什么?

到目前为止,唯一有效的方法是这个(已编辑):

lists = [['a?','b','c','d','e'],['1?','2','3','4','5','6','7']]
import random

for list in lists:
    copy = list[2:]
    random.shuffle(copy)
    list[2:] = copy

print lists

我认为这正是我所需要的。


3
list[2:] 创建了一个全新的列表,没有被其他地方引用,因此结果就丢失了。 - Felix Kling
@FelixKling:将其作为答案发布,并附上一个小的代码示例,说明应该如何修复[如何分配]。 - amit
lst[2:] = random.sample(lst[2:], len(lst[2:])) 或查看以下函数版本。 - dansalmo
有关真正的原地洗牌请参见此处 - norok2
8个回答

12

你需要做的是:

copy = list[2:]
random.shuffle(copy)    

它对原始列表没有做太多事情。尝试这样做:

copy = list[2:]
random.shuffle(copy)
list[2:] = copy # overwrite the original

缺点是这将带来两份副本,对于大型列表来说可能会很昂贵。 - orlp
是的,那跟我刚才发布的有点类似...问题在于我需要对包含多个列表的列表执行此操作,例如[[],[],[],[],[]]。 - user1227065

11

如果您想进行不复制的洗牌操作,可以尝试编写自己的可变切片类,如下所示(这是一个粗略的实现草图,没有边界检查等):

class MutableSlice(object):
    def __init__(self, baselist, begin, end=None):
        self._base = baselist
        self._begin = begin
        self._end = len(baselist) if end is None else end

    def __len__(self):
        return self._end - self._begin

    def __getitem__(self, i):
        return self._base[self._begin + i]

    def __setitem__(self, i, val):
        self._base[i + self._begin] = val

然后将原始列表包装在它里面,并输入标准的 shuffle 方法:

>>> mylist = [1,2,3,4,5,6]
>>> slice = MutableSlice(mylist, 2)
>>> import random
>>> random.shuffle(slice)
>>> mylist
[1, 2, 4, 3, 5, 6]

8
为了在不进行复制的情况下,就地对列表的一部分进行随机排序,我们可以使用 Knuth shuffle
import random
def shuffle_slice(a, start, stop):
    i = start
    while (i < stop-1):
        idx = random.randrange(i, stop)
        a[i], a[idx] = a[idx], a[i]
        i += 1

它的功能与random.shuffle相同,只不过作用于一个切片:

>>> a = [0, 1, 2, 3, 4, 5]
>>> shuffle_slice(a, 0, 3)
>>> a
[2, 0, 1, 3, 4, 5]

6

您可以创建自己的洗牌函数,以允许您在可变序列中洗牌切片。它处理对切片副本进行采样并重新赋值到切片中。您必须传递slice()参数,而不是更常见的[2:]符号。

from random import sample
def myShuffle(x, *s):
    x[slice(*s)] = sample(x[slice(*s)], len(x[slice(*s)]))

用法:

>>> lst = ['a?','b','c','d','e']   #don't use list as a name
>>> myShuffle(lst, 2)              #shuffles lst[:2]
>>> lst
['b', 'a?', 'c', 'd', 'e']
>>> myShuffle(lst, 2, None)        #shuffles lst[2:]
>>> lst
['b', 'a?', 'd', 'e', 'c']

5

l[2:] 构造一个新列表,而 random.shuffle 试图“原地”改变列表,这对 l 本身没有影响。

您可以使用 random.sample 来实现:

l[2:] = random.sample(l[2:], len(l)-2)

使用sample的技巧不错,但在这种情况下,子列表也会首先被复制。 - bereal
myShuffle = lambda x: sample(x, len(x)), l[2:] = myShuffle(l[2:])myShuffle = lambda x: sample(x, len(x))l[2:] = myShuffle(l[2:]) - dansalmo

1

利用列表具有快速删除和插入的事实,并扩展先前的解决方案(https://dev59.com/-Gct5IYBdhLWcg3wPbEm#25229111):

列表项

  • 枚举固定元素并复制它们及其索引
  • 从列表中删除固定元素
  • 混洗剩余子集
  • 将固定元素放回去

这将使用内存开销的原地操作,该开销取决于列表中固定元素的数量。时间复杂度为线性。 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>]

Exclude certain postions from the shuffle.  For example, exclude the
first two elements:

>>> fix = [0, 1]
>>> 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>]
>>> for i in range(4):                # doctest: +NORMALIZE_WHITESPACE
...     shuffle_subset(lst, fix)
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>]

Using a selector with the same number of elements as lst:

>>> fix = [0, 1]
>>> lst = [CAnswer(v, i in fix) for i, v in enumerate(val)]
>>> sel = [(i in fix) for i, _ in enumerate(val)]
>>> print("Start   ", 0, ": ", lst)
Start    0 :  [<CA: 3>, <CA: 2>, <CA: 0>, <CA: 1>, <CA: 5>, <CA: 9>, <CA: 4>]
>>> for i in range(4):                # doctest: +NORMALIZE_WHITESPACE
...     shuffle_subset(lst, sel)
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>]

A generator as selector works fine too:

>>> fix = [0, 1]
>>> 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>]
>>> for i in range(4):                # doctest: +NORMALIZE_WHITESPACE
...     sel = ((i in fix) for i, _ in enumerate(val))
...     shuffle_subset(lst, sel)
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>]

"""
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.

      + The predicate is an iterable that is True for fixed elements
      or len(predicate) == len(lst).

      + The predicate is a list of indices of fixed elements in lst
      with len(predicate) < len(lst).

    """
    def extract_fixed_elements(pred, lst):
        try:
            if callable(pred) or pred is None:
                pred = bool if pred is None else pred
                fixed_subset = [(i, e) for i, e in enumerate(lst) if pred(e)]
            elif (hasattr(pred, '__next__') or len(pred) == len(lst)):
                fixed_subset = [(i, lst[i]) for i, p in enumerate(pred) if p]
            elif len(pred) < len(lst):
                fixed_subset = [(i, lst[i]) for i in pred]
            else:
                raise TypeError("Predicate {} not supported.".format(pred))
        except TypeError as err:
            raise TypeError("Predicate {} not supported. {}".format(pred, err))
        return fixed_subset
    #
    fixed_subset = extract_fixed_elements(predicate, lst)
    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()

1

我从random.shuffle函数中复制了洗牌功能,并进行了修改,使其只在指定范围内对列表进行洗牌:

import random
a = range(0,20)
b = range(0,20)

def shuffle_slice(x, startIdx, endIdx):
    for i in reversed(xrange(startIdx+1, endIdx)):
       # pick an element in x[:i+1] with which to exchange x[i]
       j = random.randint(startIdx, i)
       x[i], x[j] = x[j], x[i]

#Shuffle from 5 until the end of a
shuffle_slice(a, 5, len(a))    
print a

#Shuffle b from 5 ... 15
shuffle_slice(b, 5, 15)
print b

上面的代码仅对指定范围内的元素进行洗牌。洗牌是原地完成的,即不会创建列表的副本。

0

试试这个方法,它更简单,而且不会复制列表。
你可以通过调整列表索引来固定任何元素。

步骤:

  1. 创建一个仅包含要洗牌的元素的新列表。

  2. 对新列表进行洗牌。

  3. 从原始列表中删除您想要洗牌的那些元素。

  4. 将新创建的列表插入到旧列表的适当索引处

    import random
    list = ['a?', 'b', 'c', 'd', 'e']
v = [] p = [v.append(list[c]) for c in range(2,len(list))] #步骤1 random.shuffle(v) #步骤2 for c in range(2,len(list)): list.remove(list[c]) #步骤3 list.insert(c,v[c-2]) #步骤4 #由于要洗牌的部分从列表的这个索引开始,因此为c-2
print(list)

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