Qual é a maneira mais pythônica de retirar um elemento aleatório de uma lista?

88

Digamos que eu tenha uma lista xcom comprimento desconhecido da qual desejo pop aleatoriamente um elemento para que a lista não contenha o elemento posteriormente. Qual é a maneira mais pítônica de fazer isso?

Eu posso fazer isso usando um combincation vez unhandy de pop, random.randinte len, e gostaria de ver soluções mais curtos ou mais agradáveis:

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

O que estou tentando alcançar é pop consecutivamente elementos aleatórios de uma lista. (ou seja, pop aleatoriamente um elemento e movê-lo para um dicionário, pop aleatoriamente outro elemento e movê-lo para outro dicionário, ...)

Observe que estou usando Python 2.6 e não encontrei nenhuma solução por meio da função de pesquisa.

Henrik
fonte
3
Não sou muito Pythonista, mas isso me parece muito bom.
Matt Ball
uma análise detalhada da complexidade do tempo foi realizada por mim, veja minha resposta em algum lugar no caminho. SHUFFLE NÃO É EFICIENTE! mas você ainda pode usar se precisar alterar a ordem dos itens de alguma forma. se pop (0) diz respeito a você, use dequeue, mencionado em minha análise.
nikhil swami
O (2) complexidade de tempo para a resposta ive escrita. envolva-o em uma função para uso rápido. observe que qualquer list.pop (n) diferente de list.pop (-1) recebe O (n).
nikhil swami

Respostas:

94

O que você parece estar fazendo não parece muito pitônico em primeiro lugar. Você não deve remover coisas do meio de uma lista, porque as listas são implementadas como matrizes em todas as implementações de Python que eu conheço, então esta é uma O(n)operação.

Se você realmente precisa dessa funcionalidade como parte de um algoritmo, deve verificar uma estrutura de dados como a blistque oferece suporte à exclusão eficiente do meio.

No Python puro, o que você pode fazer se não precisar de acesso aos elementos restantes é apenas embaralhar a lista primeiro e, em seguida, iterar sobre ela:

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

Se você realmente precisa do resto (que é um pouco cheiro de código, IMHO), pelo menos você pode pop()partir do final da lista agora (que é rápido!):

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

Em geral, você pode expressar seus programas com mais elegância se usar um estilo mais funcional, em vez de um estado de mutação (como você faz com a lista).

Niklas B.
fonte
3
Portanto, uma ideia melhor (mais rápida) seria usar random.shuffle(x)e então x.pop()? Eu não entendo como fazer isso "funcional"?
Henrik,
1
@Henrik: Se você tem duas coleções (por exemplo, uma lista de dicionários e uma lista de números aleatórios) e deseja iterá-las ao mesmo tempo, pode zipobtê-las para obter uma lista de pares (dict, number). Você disse algo sobre vários dicionários dos quais deseja associar cada um a um número aleatório. zipé perfeito para isso
Niklas B.
2
Devo adicionar uma postagem quando votar contra. Há momentos em que você precisa remover um item do meio de uma lista ... Tenho que fazer isso agora. Sem escolha: tenho uma lista ordenada, tenho que remover um item do meio. É uma merda, mas a única outra opção é fazer uma refatoração pesada de código para uma operação semi-rara. A questão é a implementação de [], que DEVE ser eficiente para tais operações, mas não é.
Mark Gerolimatos
5
@NiklasB. O OP estava usando o aleatório como exemplo (francamente, deveria ter sido deixado de fora, isso turvou o problema). "Não faça isso" é insuficiente. Uma resposta melhor teria sido sugerir uma estrutura de dados Python que suporte tais operações enquanto fornece velocidade de acesso SUFICIENTE (claramente não tão boa quanto arra ... er ... lista). No python 2, não consegui encontrar um. Se o fizer, responderei com isso. Observe que, devido a um acidente do navegador, não fui capaz de adicionar isso ao meu comentário original, deveria ter adicionado um comentário secundário. Obrigado por me manter honesto :)
Mark Gerolimatos
1
@MarkGerolimatos Não há estrutura de dados com acesso aleatório eficiente e inserção / exclusão na biblioteca padrão. Você provavelmente deseja usar algo como pypi.python.org/pypi/blist. Eu ainda diria que em muitos casos de uso isso pode ser evitado
Niklas B.
49

Você não vai conseguir muito melhor do que isso, mas aqui está uma pequena melhoria:

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

Documentação em random.randrange():

random.randrange ([start], stop [, step])
Retorna um elemento selecionado aleatoriamente range(start, stop, step). Isso é equivalente a choice(range(start, stop, step)), mas na verdade não cria um objeto de intervalo.

Andrew Clark
fonte
14

Para remover um único elemento em um índice aleatório de uma lista se a ordem do restante dos elementos da lista não importa:

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)

A troca é usada para evitar o comportamento O (n) na exclusão de um meio de uma lista.

jfs
fonte
9

Aqui está outra alternativa: por que você não embaralha a lista primeiro e, em seguida, começa a destacar elementos dela até que não haja mais elementos? como isso:

import random

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

while x:
    p = x.pop()
    # do your stuff with p
Óscar López
fonte
3
@NiklasB. porque estamos removendo elementos da lista. Se não for absolutamente necessário remover elementos, sim, concordo com você:[for p in x]
Óscar López
Porque altera a lista e se você deseja apenas selecionar metade dos elementos agora e a outra metade mais tarde, você terá o conjunto restante posteriormente.
Henrik
@Henrik: Ok, é por isso que perguntei se você precisa da lista restante. Você não respondeu isso.
Niklas B.
2

Uma maneira de fazer isso é:

x.remove(random.choice(x))
Simeon Visser
fonte
7
Isso pode ser problemático se os elementos ocorrerem mais de uma vez.
Niklas B.
2
Isso removerá o elemento mais à esquerda quando houver duplicatas, causando um resultado não perfeitamente aleatório.
FogleBird
Com popvocê pode apontar um nome para o elemento removido, com isso você não pode.
agf
É justo, concordo que isso não é muito aleatório quando os elementos ocorrem mais de uma vez.
Simeon Visser,
1
Além da questão de distorcer sua distribuição, removerequer uma varredura linear da lista. Isso é terrivelmente ineficiente em comparação com a pesquisa de um índice.
aaronasterling
2

Apesar de não ter saído da lista, encontrei esta pergunta no Google ao tentar obter X itens aleatórios de uma lista sem duplicatas. Aqui está o que acabei usando:

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

Isso pode ser um pouco ineficiente, pois você está embaralhando uma lista inteira, mas usando apenas uma pequena parte dela, mas não sou um especialista em otimização, então posso estar errado.

Noah McIlraith
fonte
3
random.sample(items, items_needed)
jfs de
2

Eu sei que esta é uma pergunta antiga, mas apenas para fins de documentação:

Se você (a pessoa pesquisando a mesma pergunta no Google) está fazendo o que eu acho que está fazendo, que é selecionar k número de itens aleatoriamente de uma lista (onde k <= len (sua lista)), mas certificando-se de que cada item nunca seja selecionado mais de uma vez (= amostragem sem substituição), você pode usar random.sample como @ jf-sebastian sugere. Mas sem saber mais sobre o caso de uso, não sei se é disso que você precisa.

Dolf Andringa
fonte
1

Esta resposta é cortesia de @ niklas-b :

" Você provavelmente deseja usar algo como pypi.python.org/pypi/blist "

Para citar a página PYPI :

... um tipo de lista com melhor desempenho assintótico e desempenho semelhante em listas pequenas

O blist é um substituto imediato para a lista Python que fornece melhor desempenho ao modificar listas grandes. O pacote blist também fornece sortedset, sortedset, weaksortedlist, weaksortedset, Sortedict e tipos btuple.

Seria de se supor um desempenho reduzido no final de acesso aleatório / execução aleatória , pois é uma estrutura de dados de "cópia na gravação". Isso viola muitas suposições de casos de uso nas listas Python, portanto , use-o com cuidado .

NO ENTANTO, se o seu principal caso de uso é fazer algo estranho e não natural com uma lista (como no exemplo forçado dado por @OP, ou meu problema de fila FIFO com passagem de Python 2.6), então isso se encaixará perfeitamente .

Mark Gerolimatos
fonte
1

apesar de muitas respostas sugerindo uso random.shuffle(x)e x.pop()é muito lento em grandes dados. e o tempo necessário em uma lista de 10000elementos levou cerca de 6 secondsquando o shuffle está ativado. quando o shuffle está desativado, a velocidade era0.2s

o método mais rápido depois de testar todos os métodos fornecidos acima foi escrito por @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)

em apoio à minha afirmação, aqui está o gráfico de complexidade de tempo desta fonte insira a descrição da imagem aqui


SE não houver duplicatas na lista,

você pode atingir seu objetivo usando conjuntos também. uma vez que a lista feita em duplicatas definidas será removida. remove by valuee remove randomcusto O(1), ou seja, muito eficiente. este é o método mais limpo que eu poderia inventar.

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.

Ao contrário de listsqual A+Bopção de suporte , setstambém suporte A-B (A minus B)junto com A+B (A union B)e A.intersection(B,C,D). super útil quando você deseja realizar operações lógicas nos dados.


OPCIONAL

SE você quiser velocidade quando as operações forem executadas no início e no fim da lista, use python dequeue (fila dupla) para apoiar minha afirmação, aqui está a imagem. uma imagem vale mil palavras.

insira a descrição da imagem aqui

nikhil swami
fonte