在Python中随机交错两个数组

15

假设我有两个数组:

a = [1, 2, 3, 4]
b = [5, 6, 7, 8, 9]

我希望将这两个数组交错到变量'c'中(请注意'a'和'b'的长度不一定相等),但我不希望它们以确定的方式交错。简而言之,仅仅使用zip这两个数组是不够的。我不希望:

c = [1, 5, 2, 6, 3, 7, 4, 8, 9]

相反,我想要一些随机的东西,比如:

c = [5, 6, 1, 7, 2, 3, 8, 4, 9]

还需要注意,'a'和'b'的顺序在生成的数组'c'中保留。

我当前的解决方案需要使用for循环和一些随机数生成。我不喜欢它,希望有人能指点我一个更好的解决方案。

# resulting array
c = []

# this tells us the ratio of elements to place in c. if there are more elements 
# in 'a' this ratio will be larger and as we iterate over elements, we will place
# more elements from 'a' into 'c'.
ratio = float(len(a)) / float(len(a) + len(b))

while a and b:
    which_list = random.random()
    if which_list < ratio:
        c.append(a.pop(0))
    else:
        c.append(b.pop(0))

# tack on any extra elements to the end
if a:
    c += a
elif b:
    c += b

但是您只想随机交错还是整个数组?我的意思是,您需要保留原始数组的顺序吗? - C2H5OH
4
你对它有什么不喜欢的地方?你需要生成随机数,虽然可以用列表推导式替换许多循环,但这有什么意义呢? - James Thiele
1
是的,这对我来说看起来完全没问题。 我相信你可以写出更紧凑的代码,但“简单胜于复杂”。 - ludaavics
@JamesThiele 我在Python方面仍然是个新手,所以我一直在寻找让我的代码更具Python风格的方法。 - salil
4
除了有趣的 Python 之外,你真的需要考虑接下来应该做什么。如果你已经选择了1、2、3,那么下一个选择应该是50%的4 / 50%的5(即使按列表排序),还是在未选中的项目列表a中加权为16%的4和84%的5,与列表b中未选中的5个项目相比。 - Charles Merriam
显示剩余2条评论
15个回答

16

编辑: 我认为最近的这个是最好的:

a = [1, 2, 3, 4]
b = [5, 6, 7, 8, 9]
c = [x.pop(0) for x in random.sample([a]*len(a) + [b]*len(b), len(a)+len(b))]

更高效的方式:

c = map(next, random.sample([iter(a)]*len(a) + [iter(b)]*len(b), len(a)+len(b)))
请问翻译需要的语言是什么?
a = [1, 2, 3, 4]
b = [5, 6, 7, 8, 9]

c = []
tmp = [a]*len(a) + [b]*len(b)
while a and b:
    c.append(random.choice(tmp).pop(0))

c += a + b

这里还有一个选项,但只适用于您知道所有元素都不是假值(没有0''NoneFalse或空序列)的情况:

a = [1, 2, 3, 4]
b = [5, 6, 7, 8, 9]

ratio = float(len(a)) / float(len(a) + len(b))
c = [(not a and b.pop(0)) or (not b and a.pop(0)) or
     (random.random() < ratio and b.pop(0)) or a.pop(0)
     for _ in range(len(a) + len(b))]

3
太多了,你不需要创建那么多额外的列表。你可以从两个可迭代对象中选择。 - JBernardo
1
我不会创建额外的列表,tmp 仅包含对 ab 的引用。 - Andrew Clark
1
我可以看到有3个列表被创建了,但它们不需要存在。你的第二个解决方案似乎更好,但可读性不太好(而且你改变了原始列表)。 - JBernardo
2
@TryPyPy:这会产生不同的结果 - 你的结果总是a和b之间的50/50概率。 - FogleBird
2
@F.J:好的。我会把shuffle()放在列表推导式之外,这样代码(1)运行更快,(2)更明确(当前解决方案中的sample()确实进行了洗牌)。很高兴看到你的解决方案已经收敛到srgerg的原始解决方案上(使用迭代器和next())。 :) - Eric O. Lebigot
显示剩余5条评论

9

编辑以去除多余的杂物: 这是一个适用于任意数量输入列表的解决方案,不会破坏输入列表,也不会复制它们:

import random

def interleave(*args):
    iters = [i for i, b in ((iter(a), a) for a in args) for _ in xrange(len(b))]
    random.shuffle(iters)
    return map(next, iters)

Stackoverflow用户EOL友善地提供了我解决方案的增强版本:

def interleave(*args):
    iters = sum(([iter(arg)]*len(arg) for arg in args), [])
    random.shuffle(iters)
    return map(next, iters)

使用以下命令来运行:

a = [1,2,3,4]
b = [5,6,7,8,9]
print interleave(a, b)

产生以下作为许多可能结果之一:
[5, 6, 7, 1, 8, 2, 3, 9, 4]

编辑:应EOL的要求,我更新了时间代码。不幸的是,由于被接受的解决方案修改了其输入,因此我需要在每次迭代中都制作一份新的副本。我已经为F.J和我自己的解决方案都做了这个更改,以便使结果可比较。以下是F.J方案的计时:

$ python -m timeit -v -s "from srgerg import accepted" -s "a = list(xrange(40000))" -s "b = list(xrange(60000))" "accepted(list(a), list(b))"
10 loops -> 10.5 secs
raw times: 10.3 10.1 9.94
10 loops, best of 3: 994 msec per loop

这是我版本的函数运行时间。
$ python -m timeit -v -s "from srgerg import original" -s "a = list(xrange(40000))" -s "b = list(xrange(60000))" "original(list(a), list(b))"
10 loops -> 0.616 secs
raw times: 0.647 0.614 0.641
10 loops, best of 3: 61.4 msec per loop

以下是EOL增强版的时间安排:

$ python -m timeit -v -s "from srgerg import eol_enhanced" -s "a = list(xrange(40000))" -s "b = list(xrange(60000))" "eol_enhanced(list(a), list(b))"
10 loops -> 0.572 secs
raw times: 0.576 0.572 0.588
10 loops, best of 3: 57.2 msec per loop

如果我从增强版的 EOL 循环中删除列表复制,我会得到这个结果:
$ python -m timeit -v -s "from srgerg import eol_enhanced" -s "a = list(xrange(40000))" -s "b = list(xrange(60000))" "eol_enhanced(a, b)"
10 loops -> 0.573 secs
raw times: 0.572 0.575 0.565
10 loops, best of 3: 56.5 msec per loop

又一次编辑: F.J 提供了一个更新的解决方案并要求我添加时序:

$ python -m timeit -v -s "from srgerg import fj_updated" -s "a = list(xrange(40000))" -s "b = list(xrange(60000))" "fj_updated(list(a), list(b))"
10 loops -> 0.647 secs
raw times: 0.652 0.653 0.649
10 loops, best of 3: 64.9 msec per loop

+1:我相信这应该是被采纳的答案:它比我的解决方案更快,更通用,更优雅(尤其是我的simplified version of interleave()),而且只需要大约两倍的内存。 :) - Eric O. Lebigot
你的计时应该有所不同:你应该使用ab的设置来初始化计时器,并且只计算函数调用的时间。实际上,也许列表的创建占据了总时间的很大一部分。我还相信,如果你使用更长的示例列表(比如100000个数字),计时差异会更加明显。 - Eric O. Lebigot
@EOL 我同意计时代码非常粗糙。我只是想满足自己,确保我的函数在运行时间上与其他解决方案具有竞争力。我很快会更新它。 - srgerg
1
此外,一些解决方案修改它们的输入事实意味着我必须在每次迭代中制作输入的新副本... @EOL - srgerg
1
非常好的时间结果。请注意,list(xrange(40000)) 应该写成 range(40000),因为这是通常的做法(实际上,如果之后要构建一个列表,使用 xrange 没有任何意义)。 - Eric O. Lebigot
显示剩余3条评论

7

这是一个适用于任意数量可迭代对象的解决方案:

import random

def interleave(*args):
  iters = map(iter, args)
  while iters:
    it = random.choice(iters)
    try:
      yield next(it)
    except StopIteration:
      iters.remove(it)

print list(interleave(xrange(1, 5), xrange(5, 10), xrange(10, 15)))

2
+1 不错的回答:这个函数具有理想的特性,可以接受任意数量的参数,不修改其输入,不复制其输入,并且不要求其输入支持 len() 方法。唯一的缺点是它比我的解决方案慢约三分之一,但如果没有 try-except 语句,它可能会更快。 - srgerg
Stackoverflow用户Mark Byers在回答另一个问题时对这种方法的随机性发表了一些有价值的评论。我进行了一些测试,得到了一些有趣的结果。 - srgerg
真是个很棒的解决方案。我已经提交了一个修改版,可以在有大量迭代器时加快删除空迭代器的速度:https://dev59.com/OWgv5IYBdhLWcg3wYvyi#40896022 - Isaac Turner

7

PS: 请考虑阅读@srgerg的答案: 在我看来,这是最佳解决方案(尽管F.J的答案也相当不错)。与下面的解决方案相比,它更为通用,甚至更为简单明了,并且只需要大约两倍的内存。

下面是一个既简单高效的东西:

[(a if random.randrange(0, len(a)+len(b)) < len(a) else b).pop(0) for _ in range(len(a)+len(b))]

这种解决方案避免了明确测试 a 或者 b 是否为空的特定情况。

这种方法使用了一些关键点:

  • 使用 randrange() 可以简单地处理整数(不需要计算比率)。
  • 它自动适应空列表(即 < len(a) 测试),无需进行其他测试,如 a or b[… a and b]+a+b

这种方法很好地处理了不同大小的列表:较短列表的元素在结果中分布得相当均匀。这种方法还具有“不变性”:可能结果列表的概率分布仅取决于当前 ab 列表的内容。

通过使用更快的 .pop() 而不是 .pop(0)(由于列表被制作成快速执行 pop() 但不是 pop(0)),它甚至可以更加高效:

a.reverse(); b.reverse()
[(a if random.randrange(0, len(a)+len(b)) < len(a) else b).pop() for _ in range(len(a)+len(b))]

也许我有什么误解,但是你说的解决方案是“避免对a或b的长度进行测试”,但是在解决方案中明显调用了len(a)len(b),这是怎么回事? - srgerg
1
@srgerg:好发现:我是指“避免额外的测试”(已编辑帖子)。实际上,许多解决方案会添加非零列表长度测试,例如while a and bnot a or等。没有必要特别处理异常情况(即零长度列表)。 - Eric O. Lebigot
1
不错。我喜欢使用randrange的方式。 - Joel Cornett
1
如果解决方案能够将较短的列表均匀地分配到较长的列表中,则加1分。Stackoverflow用户Mark Byers在此评论中写道,我也在另一个问题中发表了一些看法。 - srgerg

6

在TryPyPy的建议下进行了编辑:

from random import choice

l = [a, b]
c = [choice(l).pop(0) for i in range(len(a) + len(b)) if (a and b)] + a + b

1
选择 + 弹出 在阅读上真的很好理解。那么这个怎么样:[choice(l).pop(0) for i in range(len(a+b)) if (a and b) ] + a + b - TryPyPy
2
有趣而且相当简单的解决方案。然而,这个解决方案的一个“特点”是,如果其中一个列表比另一个列表长得多,那么短列表很可能会很快耗尽。在结果列表中更均匀地分布较短的元素可能是可取的。 - Eric O. Lebigot
你说得对。上述方法只适用于大致相等大小的列表。 - Joel Cornett
为了解决这个问题,可以调整l中对ab的引用比例。例如,如果a的长度是b的两倍,则l = [a, b, b]。我想知道实现这个的有效方法是什么... - Joel Cornett
@JoelCornett:我的回答帖子提供了一种高效的实现方式。 :) - Eric O. Lebigot

2
如何将标志数组连接起来,然后随机重排,再使用它来挑选每个项所在的数组?
import random

a = [1, 2, 3, 4]
b = [5, 6, 7, 8, 9]

c = list('a' * len(a) + 'b' * len(b)) # Flags for taking items from each array
random.shuffle(c) # Randomize from where we take items

aa, bb = a[:], b[:] # Copy the arrays for popping 
d = [aa.pop(0) if source == 'a' else bb.pop(0) for source in c]
# Take items in order, randomly from each array

FogleBird提出了一种更高效的方法:
c = [a[:]] * len(a) + [b[:]] * len(b)
random.shuffle(c) # Randomize from where we take items

d = [x.pop(0) for x in c] # Take items in order, randomly from each array

1
为什么要使用标志(flag),而不是直接使用列表(list)的引用(reference)呢? - FogleBird
我可以说相对于其他答案,我更容易理解洗牌标志和获取项目,但事实是我从误读问题开始学习洗牌。你能提出一种方法,使其像洗牌标志一样明显,但使用列表的直接引用吗? - TryPyPy
很漂亮,我从来没有想过那个 :) - TryPyPy

1

这个解决方案为您提供了一个生成器,并通过随机交换尚未发出的列表(a)和(b)的部分来工作。

import random

a = [1,2,3,4]
b = [5,6,7,8,9]

def interleave(a,b):
   while a or b:
      (a,b)=(a,b) if len(a) and (random.random()<0.5 or not len(b)) else (b,a)
      yield a.pop(0)

print list(interleave(a,b))

1

这是一个使用未记录的Python功能(具体来说,它使用列表迭代器对象的__length_hint__方法,该方法告诉您迭代器中还剩下多少项)将其压缩到列表推导式中的示例。更多是为了好玩,而不是实际上的实用性。

itera, iterb = iter(a), iter(b)
morea, moreb = itera.__length_hint__, iterb.__length_hint__
c = [next(itera) if not moreb() or morea() and random.random() < ratio
     else next(iterb) for c in xrange(len(a) + len(b))]

1
我会这样解决这个问题:
import random

LResult = []

LLists = [[1, 2, 3, 4], [5, 6, 7, 8, 9]]

while LLists[0] or LLists[1]:
    LResult.append(LLists[random.choice([int(len(LLists[0])==0), int(len(LLists[1])!=0)])].pop(0))

LLists是一个多维列表,它存储两个列表(来自您的示例中的a和b)。该语句等同于:LLists = [a[:], b[:]],但出于简单和清晰的考虑,我在列表中明确编码了它们。

LResult是您示例中的c,最终存储结果数组。

while循环将循环直到LLists子0和LLists子1完全为空。在循环内部,LResult从LLists子0或LLists子1中附加一个值。选择哪个子列表的值由random.choice()语句决定,该语句取两个参数(在本例中),然后随机返回其中一个。

random.choice()提供的选项由LLists中每个子列表的长度确定。如果LLists子0的长度大于零,则语句int(len(LLists[0])==0)将返回零作为选择号1。对于random.choice()的第二个选项,如果LLists子1的长度大于零,则语句int(len(LLists[1])!=0)将返回1。在这两种情况下,如果一个子列表的长度为零,则相应的语句将返回相反的数字。也就是说,如果LLists[0]的长度为零,而LLists[1]的长度大于零,则结果语句将是random.choice(1, 1)。在这种情况下,random.choice()将返回1和1之间的选择(当然是1)。

一旦决定从哪个子列表中提取值,该子列表的第一项将被弹出到LResult中,即.pop(0)。


0
如果列表1和列表2之间的比率保持不变,您可以创建如下函数:

如果列表1和列表2之间的比率保持不变,您可以创建如下函数:

def selectFromTwoList(ratioFromList1):
    final_list = []
    for i in range(len(list1)):
        rand = random.randint(1, 100)
        if rand <= ratioFromList1*100:
            final_list.append(list1.pop(0))
        else:
            final_list.append(list2.pop(0))
        return final_list

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