Какой самый питонический способ извлечь случайный элемент из списка?

88

Скажем, у меня есть список xс неизвестной длиной, из которого я хочу случайным образом выбрать один элемент, чтобы список не содержал этот элемент впоследствии. Каков самый питонический способ сделать это?

Я могу сделать это, используя довольно неудобную комбинацию pop, random.randintи len, и хотел бы видеть более короткие или более приятные решения:

import random
x = [1,2,3,4,5,6]
x.pop(random.randint(0,len(x)-1))

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

Обратите внимание, что я использую Python 2.6 и не нашел никаких решений через функцию поиска.

Хенрик
источник
3
Я не особо питонист, но мне это очень нравится.
Мэтт Болл,
мной был проведен подробный анализ временной сложности, мой ответ можно найти где-нибудь в будущем. SHUFFLE НЕ ЭФФЕКТИВНО! но вы все равно можете использовать, если вам нужно как-то изменить порядок элементов. если вас беспокоит pop (0), используйте dequeue, упомянутый в моем анализе.
нихил свами
O (2) временная сложность написанного ответа. заверните его в функцию для быстрого использования. обратите внимание, что любой list.pop (n) кроме list.pop (-1) принимает O (n).
nikhil swami

Ответы:

94

То, что вы, кажется, задумали, в первую очередь не выглядит очень питоническим. Вы не должны удалять что-либо из середины списка, потому что списки реализованы как массивы во всех реализациях Python, о которых я знаю, так что это O(n)операция.

Если вам действительно нужна эта функция как часть алгоритма, вам следует проверить структуру данных, подобную той, blistкоторая поддерживает эффективное удаление из середины.

В чистом Python, что вы можете сделать, если вам не нужен доступ к остальным элементам, - это сначала перетасовать список, а затем перебрать его:

lst = [1,2,3]
random.shuffle(lst)
for x in lst:
  # ...

Если вам действительно нужен остаток (который немного пахнет кодом, ИМХО), по крайней мере, вы можете сейчас pop()с конца списка (что быстро!):

while lst:
  x = lst.pop()
  # do something with the element      

В общем, вы часто можете выразить свои программы более элегантно, если вы используете более функциональный стиль, а не изменяете состояние (как вы делаете со списком).

Никлас Б.
источник
3
Так что лучше (быстрее) было бы использовать random.shuffle(x)а потом x.pop()? Не понимаю, как сделать этот "функционал"?
Хенрик
1
@Henrik: Если у вас есть две коллекции (например, список словарей и список случайных чисел), и вы хотите перебирать их одновременно, вы можете zipполучить список пар (dict, number). Вы сказали что-то о нескольких словарях, каждый из которых хотите связать со случайным числом. zipидеально подходит для этого
Никлас Б.
2
Я должен добавить пост, когда проголосую против. Бывают случаи, когда нужно удалить элемент из середины списка ... Я должен сделать это прямо сейчас. Нет выбора: у меня есть упорядоченный список, мне нужно удалить элемент посередине. Это отстой, но единственный другой выбор - провести тяжелый рефакторинг кода для одной полуредкой операции. Проблема заключается в реализации [], которая ДОЛЖНА быть эффективной для таких операций, но это не так.
Марк Геролиматос
5
@NiklasB. OP использовал random в качестве примера (честно говоря, его следовало не использовать, это затуманило проблему). «Не делай этого» недостаточно. Лучшим ответом было бы предложить структуру данных Python, которая ДЕЙСТВИТЕЛЬНО поддерживает такие операции, обеспечивая при этом ДОСТАТОЧНУЮ скорость доступа (явно не так хорошо, как массив ... э ... список). В python 2 я не смог его найти. Если да, то отвечу. Обратите внимание, что из-за ошибки браузера я не смог добавить это в свой исходный комментарий, мне следовало добавить дополнительный комментарий. Спасибо, что держите меня честным :)
Марк Геролиматос
1
@MarkGerolimatos В стандартной библиотеке нет структуры данных с эффективным произвольным доступом и вставкой / удалением. Вероятно, вы захотите использовать что-то вроде pypi.python.org/pypi/blist Я все равно буду утверждать, что во многих случаях использования этого можно избежать
Никлас Б.
49

Вы не получите ничего лучше, но вот небольшое улучшение:

x.pop(random.randrange(len(x)))

Документация по random.randrange():

random.randrange ([start], stop [, step])
Возвращает случайно выбранный элемент из range(start, stop, step). Это эквивалентно choice(range(start, stop, step)), но на самом деле не создает объект диапазона.

Эндрю Кларк
источник
14

Чтобы удалить один элемент по случайному индексу из списка, если порядок остальных элементов списка не имеет значения:

import random

L = [1,2,3,4,5,6]
i = random.randrange(len(L)) # get random index
L[i], L[-1] = L[-1], L[i]    # swap with the last element
x = L.pop()                  # pop last element O(1)

Своп используется, чтобы избежать поведения O (n) при удалении из середины списка.

jfs
источник
9

Вот еще одна альтернатива: почему бы вам сначала не перетасовать список , а затем начать выталкивать его элементы, пока не останется больше элементов? как это:

import random

x = [1,2,3,4,5,6]
random.shuffle(x)

while x:
    p = x.pop()
    # do your stuff with p
Оскар Лопес
источник
3
@NiklasB. потому что мы удаляем элементы из списка. Если нет необходимости удалять элементы, да, я согласен с вами:[for p in x]
Оскар Лопес,
Поскольку это изменяет список, и если вы просто хотите выбрать половину элементов сейчас, а вторую половину позже, у вас будет оставшийся набор позже.
Хенрик
@Henrik: Хорошо, поэтому я спросил, нужен ли вам оставшийся список. Вы не ответили на это.
Никлас Б.
2

Один из способов сделать это:

x.remove(random.choice(x))
Симеон Виссер
источник
7
Это может стать проблемой, если элементы встречаются более одного раза.
Никлас Б.
2
Это удалит крайний левый элемент при наличии дубликатов, что приведет к не совсем случайному результату.
FogleBird
С помощью popвы можете указать имя на удаленный элемент, с этим вы не можете.
agf 06
Честно говоря, я согласен с тем, что это не очень случайно, когда элементы встречаются более одного раза.
Симеон Виссер
1
Помимо вопроса об искажении вашего дистрибутива, removeтребуется линейное сканирование списка. Это ужасно неэффективно по сравнению с поиском индекса.
aaronasterling
2

Не появляясь из списка, я столкнулся с этим вопросом в Google, пытаясь получить X случайных элементов из списка без дубликатов. Вот что я в итоге использовал:

items = [1, 2, 3, 4, 5]
items_needed = 2
from random import shuffle
shuffle(items)
for item in items[:items_needed]:
    print(item)

Это может быть немного неэффективно, поскольку вы перетасовываете весь список, но используете только небольшую его часть, но я не эксперт по оптимизации, поэтому могу ошибаться.

Ной Макилрайт
источник
3
random.sample(items, items_needed)
jfs
2

Я знаю, что это старый вопрос, но только для документации:

Если вы (человек, который задает тот же вопрос в Google) делаете то, что, я думаю, делаете вы, то есть выбираете k элементов случайным образом из списка (где k <= len (yourlist)), но следите за тем, чтобы каждый элемент никогда не выбирался больше чем один раз (= выборка без замены), вы можете использовать random.sample, например, @ jf-sebastian. Но, не зная больше о варианте использования, я не знаю, нужно ли это вам.

Дольф Андринга
источник
1

Этот ответ любезно предоставлен @ niklas-b :

« Возможно, вы захотите использовать что-то вроде pypi.python.org/pypi/blist »

Чтобы процитировать страницу PYPI :

... тип в виде списка с лучшей асимптотической производительностью и аналогичной производительностью в небольших списках

Блистинг представляет собой замену списка Python, который обеспечивает лучшую производительность при изменении больших списков. Пакет blist также предоставляет типы sortedlist, sortedset, weaksortedlist, weaksortedset, sorteddict и btuple.

Можно было бы предположить пониженную производительность в конце произвольного доступа / произвольного прогона , поскольку это структура данных «копирование при записи». Это нарушает многие предположения о вариантах использования в списках Python, поэтому используйте его с осторожностью .

ОДНАКО, если ваш основной вариант использования - сделать что-то странное и неестественное со списком (как в принудительном примере, предоставленном @OP, или моей проблеме с передачей очереди FIFO в Python 2.6), тогда это будет хорошо соответствовать требованиям .

Марк Геролиматос
источник
1

несмотря на то, что многие ответы предполагают использование random.shuffle(x)и x.pop()очень медленное использование больших данных. и время, требуемое для списка 10000элементов, 6 secondsкогда включено перемешивание. когда перемешивание отключено, скорость была0.2s

самый быстрый метод после тестирования всех приведенных выше методов оказался написан @jfs

import random

L = ['1',2,3,'4'...1000] #you can take mixed or pure list
i = random.randrange(len(L)) # get random index
L[i], L[-1] = L[-1], L[i]    # swap with the last element
x = L.pop()                  # pop last element O(1)

в подтверждение моего утверждения вот диаграмма временной сложности из этого источника введите описание изображения здесь


ЕСЛИ в списке нет дубликатов,

Вы также можете достичь своей цели, используя наборы. после того, как список объединен в набор, дубликаты будут удалены. remove by valueи remove randomстоить O(1), т.е. очень эффективно. это самый чистый метод, который я мог придумать.

L=set([1,2,3,4,5,6...]) #directly input the list to inbuilt function set()
while 1:
    r=L.pop()
    #do something with r , r is random element of initial list L.

В отличие от того, listsкакой A+Bвариант поддержки , setsтакже поддерживает A-B (A minus B)вместе с A+B (A union B)и A.intersection(B,C,D). очень полезно, когда вы хотите выполнять логические операции с данными.


ПО ЖЕЛАНИЮ

ЕСЛИ вам нужна скорость при операциях, выполняемых в начале и конце списка, используйте python dequeue (двусторонняя очередь) в поддержку моего утверждения, вот изображение. изображение - это тысяча слов.

введите описание изображения здесь

нихил свами
источник