Случайно перемежать 2 массива в Python

Предположим, что у меня есть два массива:

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

Я хочу переместить эти два массива в переменную 'c' (примечание 'a' и 'b' не обязательно одинаковой длины), но я не хочу, чтобы они чередовали детерминированный путь. Короче говоря, этого недостаточно, чтобы просто закрепить эти два массива. Я не хочу:

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

Ответы

Ответ 1

edit: Я думаю, что это последнее лучшее:

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)))

Обратите внимание, что первый метод выше изменяет исходные списки (как ваш код), а второй - нет. На Python 3.x вам нужно будет сделать list(map(...)), так как map возвращает итератор.

оригинальный ответ ниже:

Вот вариант, который сохраняет несколько строк:

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, '', None, False или пустые последовательности):

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))]

Ответ 2

Отредактировано для удаления лишнего беспорядка: Здесь решение, которое работает с любым количеством входных списков, не уничтожает входные списки и не копирует их:

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.Js:

$ 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

Ответ 3

PS: Пожалуйста, подумайте о том, чтобы читать ответ @srgerg: это, на мой взгляд, лучшее решение (хотя FJ приходит относительно близко). По сравнению с решением ниже, он более общий, даже немного более простой, и он занимает примерно вдвое больше памяти.

Вот что-то, что является простым и эффективным:

[(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...

Это решение красиво обрабатывает списки разных размеров: элементы более короткого списка распределяются довольно равномерно в результате. Этот подход также имеет "инвариантность": распределение вероятностей возможных списков результатов зависит только от текущего содержимого списков a и b.

Его можно было бы сделать еще более эффективным, используя более быстрый .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))]

Ответ 4

Отредактировано предложением 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

Ответ 5

Вот решение, которое работает с произвольным числом итераций:

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)))

Ответ 6

Как насчет конкатенирования, затем перетасовки массива флагов, а затем его использования для выбора массива для переноса каждого элемента?

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

Ответ 7

Это решение дает вам генератор и работает путем случайной замены частей списков (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))

Ответ 8

Вот что использует недокументированный 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))]

Ответ 9

Я бы решил эту проблему следующим образом:

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 sub 0 и LLists sub 1 не будут полностью пустыми. Внутри цикла LResult добавляется значение из LLists sub 0 или LLists sub 1. Решение относительно того, какое значение суб-списка выбрано, определяется оператором random.choice(), который принимает два (в данном случае) аргументы, тогда возвращает одно из них случайным образом.

Параметры, предоставляемые random.choice(), определяются длиной каждого дополнительного списка в LLists. Если длина LLists sub 0 больше нуля, тогда выбор номер 1 возвращается как ноль с помощью оператора int (len (LLists [0]) == 0). Для второго варианта random.choice(), если LLists sub 1 length больше нуля, тогда оператор int (len (LLists [1])!= 0) вернет 1. В обоих случаях, если один длины суб-списка равна нулю, тогда соответствующая инструкция вернет противоположное число. То есть, если длина LLists [0] равна нулю, а длина LLists [1] больше нуля, то полученный оператор будет random.choice(1, 1). В этом случае random.choice() вернет выбор между 1 и 1 (что, конечно, 1).

Как только будет принято решение о том, какой суб-список вытащить значение, первый элемент - этот подпункт выставляется,.pop(0), в LResult.

Ответ 10

Вы можете сделать что-то вроде этого:

(L, l) = (a, b) if len(a) > len(b) else( b, a)
positions = random.sample(range(len(L)), len(l))
for i in range(len(positions)):
    L.insert(positions[i], l[i])

но по моему скромному мнению, у вас все отлично. Это работает, просто:

Ответ 11

Как насчет этой идеи:

import random as rn

a = [1, 2, 3, 4]
b = [5, 6, 7, 8, 9]
n = 100 #Here i am picking an arbitrary number, it should probably be a function of 
        # lengths of a and b


a_ind = sorted(rn.sample(range(n),len(a))) #sorting the indexes insures that order of 
b_ind = sorted(rn.sample(range(n),len(b))) # a and b is preserved

big_list = zip(a,a_ind) + zip(b,b_ind)

big_list.sort(key  = lambda k: k[1])

result = list(zip(*big_list)[0])

Результат:

>>> result
[1, 5, 2, 6, 3, 7, 8, 9, 4]

Ответ 12

Слово "чередующееся" в описании может вводить в заблуждение. Если вы просто добавите списки входных данных, тогда перетасовать полученный результат, вы получите тот же результат. Перемешивание необходимо только в том случае, если сохраняется сохранение чередования.

Некоторые коды:

>>> import random
>>> 
>>> a, b = [1,2,3,4], [5,6,7,8]
>>> c = sum([a,b], [])
>>> random.shuffle(c)
>>> c
[6, 5, 8, 2, 7, 4, 1, 3] 

Ответ 13

Вероятно, очень неэффективный, но другой подход, который работает:

import random

def interleave(*args):
    indices=[(i,j) for i in range(len(args)) for j in range(len(args[i]))]
    random.shuffle(indices)
    indices.sort(key=lambda x:x[1])
    return [args[i][j] for i,j in indices]

Ответ 14

Я адаптировал @NPE решение, чтобы он удалял пустые итераторы в постоянном, а не в линейном времени *. Он принимает любое количество входных списков и возвращает итератор, который чередует их случайным образом, сохраняя порядок, заданный входными списками.

def interleave(*args):
  iters = [iter(x) for x in args]
  while iters:
    i = random.randrange(len(iters))
    try:
      yield next(iters[i])
    except StopIteration:
      # swap empty iterator to end and remove
      iters[i],iters[-1] = iters[-1],iters[i]
      iters.pop()

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

* Общее время выполнения O(N), а не O(N+M^2), где N - общее количество элементов, а M - количество списков.