¿Cuál es la forma más pitónica de sacar un elemento aleatorio de una lista?

88

Digamos que tengo una lista xcon una longitud desconocida de la que quiero sacar aleatoriamente un elemento para que la lista no contenga el elemento después. ¿Cuál es la forma más pitónica de hacer esto?

Puedo hacerlo usando un combincation en lugar de torpe pop, random.randinty len, y me gustaría ver soluciones más cortos o más agradables:

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

Lo que estoy tratando de lograr es sacar consecutivamente elementos aleatorios de una lista. (es decir, sacar aleatoriamente un elemento y moverlo a un diccionario, sacar aleatoriamente otro elemento y moverlo a otro diccionario, ...)

Tenga en cuenta que estoy usando Python 2.6 y no encontré ninguna solución a través de la función de búsqueda.

Henrik
fuente
3
No soy muy Pythonista, pero eso me parece bastante bueno.
Matt Ball
He realizado un análisis detallado de la complejidad del tiempo; vea mi respuesta en algún momento. ¡SHUFFLE NO ES EFICIENTE! pero aún puede usarlo si necesita cambiar el orden de los elementos de alguna manera. si pop (0) le preocupa, use dequeue, mencionado en mi análisis.
nikhil swami hace
O (2) complejidad de tiempo para la respuesta que he escrito. envuélvalo en una función para un uso rápido. tenga en cuenta que cualquier list.pop (n) que no sea list.pop (-1) toma O (n).
nikhil swami hace

Respuestas:

94

Lo que parece estar haciendo no parece muy Pythonic en primer lugar. No debe eliminar cosas del medio de una lista, porque las listas se implementan como matrices en todas las implementaciones de Python que conozco, por lo que esta es una O(n)operación.

Si realmente necesita esta funcionalidad como parte de un algoritmo, debe verificar una estructura de datos como la blistque admite la eliminación eficiente desde el medio.

En Python puro, lo que puede hacer si no necesita acceso a los elementos restantes es simplemente barajar la lista primero y luego iterar sobre ella:

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

Si realmente necesita el resto (que es un poco a código, en mi humilde opinión), al menos puede hacerlo pop()desde el final de la lista ahora (¡que es rápido!):

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

En general, a menudo puede expresar sus programas de manera más elegante si usa un estilo más funcional, en lugar de un estado mutante (como lo hace con la lista).

Niklas B.
fuente
3
Entonces, una idea mejor (más rápida) sería usar random.shuffle(x)y luego x.pop()? ¿No entiendo cómo hacer esto "funcional"?
Henrik
1
@Henrik: Si tiene dos colecciones (por ejemplo, una lista de diccionarios y una lista de números aleatorios) y desea iterarlas al mismo tiempo, puede zipobtener una lista de pares (dict, number). Dijiste algo sobre varios diccionarios de los cuales quieres asociar cada uno con un número aleatorio. zipes perfecto para esto
Niklas B.
2
Se supone que debo agregar una publicación cuando vote en contra. Hay ocasiones en las que es necesario eliminar un elemento de la mitad de una lista ... tengo que hacerlo ahora mismo. No hay elección: tengo una lista ordenada, tengo que eliminar un elemento en el medio. Apesta, pero la única otra opción es hacer una refactorización de código pesada para una operación semi-rara. El problema es la implementación de [], que DEBERÍA ser eficiente para tales operaciones, pero no lo es.
Mark Gerolimatos
5
@NiklasB. El OP estaba usando el azar como ejemplo (francamente, debería haberse dejado de lado, nubló el problema). "No hagas eso" es insuficiente. Una mejor respuesta habría sido sugerir una estructura de datos de Python que SÍ admite tales operaciones al tiempo que proporciona SUFICIENTE velocidad de acceso (claramente no tan buena como la lista de arreglos ... er ... En Python 2, no pude encontrar uno. Si lo hago, responderé con eso. Tenga en cuenta que debido a un error del navegador, no pude agregar eso a mi comentario original, debería haber agregado un comentario secundario. Gracias por mantenerme honesto :)
Mark Gerolimatos
1
@MarkGerolimatos No existe una estructura de datos con acceso aleatorio eficiente e inserción / eliminación en la biblioteca estándar. Probablemente desee usar algo como pypi.python.org/pypi/blist . Aún así, diría que en muchos casos de uso esto se puede evitar
Niklas B.
49

No será mucho mejor que eso, pero aquí hay una ligera mejora:

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

Documentación sobre random.randrange():

random.randrange ([start], stop [, step])
Devuelve un elemento seleccionado al azar de range(start, stop, step). Esto es equivalente a choice(range(start, stop, step)), pero en realidad no crea un objeto de rango.

Andrew Clark
fuente
14

Para eliminar un solo elemento en un índice aleatorio de una lista si el orden del resto de los elementos de la lista no 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)

El intercambio se utiliza para evitar el comportamiento de O (n) al eliminarlo de la mitad de una lista.

jfs
fuente
9

Aquí hay otra alternativa: ¿por qué no baraja la lista primero y luego comienza a mostrar elementos hasta que no queden más elementos? Me gusta esto:

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
fuente
3
@NiklasB. porque estamos eliminando elementos de la lista. Si no es absolutamente necesario quitar elementos, sí estoy de acuerdo contigo:[for p in x]
Óscar López
Porque altera la lista y si solo desea seleccionar la mitad de los elementos ahora y la otra mitad más tarde, tendrá el resto configurado más adelante.
Henrik
@Henrik: Bueno, por eso te pregunté si necesitas la lista restante. No respondiste eso.
Niklas B.
2

Una forma de hacerlo es:

x.remove(random.choice(x))
Simeon Visser
fuente
7
Esto podría volverse problemático si los elementos ocurren más a la vez.
Niklas B.
2
Esto eliminará el elemento situado más a la izquierda cuando haya duplicados, lo que provocará un resultado no perfectamente aleatorio.
FogleBird
Con poppuede apuntar un nombre al elemento eliminado, con esto no puede.
2012
Muy bien, estoy de acuerdo en que esto no es muy aleatorio cuando los elementos ocurren más de una vez.
Simeon Visser
1
Aparte de la cuestión de sesgar su distribución, removerequiere un escaneo lineal de la lista. Eso es terriblemente ineficiente en comparación con buscar un índice.
aaronasterling
2

Si bien no salgo de la lista, encontré esta pregunta en Google al intentar obtener X elementos aleatorios de una lista sin duplicados. Esto es lo que finalmente usé:

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

Esto puede ser un poco ineficaz ya que está barajando una lista completa pero solo usa una pequeña parte de ella, pero no soy un experto en optimización, por lo que podría estar equivocado.

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

Sé que esta es una pregunta antigua, pero solo por el bien de la documentación:

Si usted (la persona que busca en Google la misma pregunta) está haciendo lo que creo que está haciendo, que es seleccionar k número de elementos al azar de una lista (donde k <= len (su lista)), pero asegurándose de que cada elemento nunca se seleccione más de una vez (= muestreo sin reemplazo), puede usar random.sample como sugiere @ jf-sebastian. Pero sin saber más sobre el caso de uso, no sé si esto es lo que necesita.

Dolf Andringa
fuente
1

Esta respuesta es cortesía de @ niklas-b :

" Probablemente quieras usar algo como pypi.python.org/pypi/blist "

Para citar la página de PYPI :

... un tipo similar a una lista con mejor rendimiento asintótico y rendimiento similar en listas pequeñas

El blist es un reemplazo directo de la lista de Python que proporciona un mejor rendimiento al modificar listas grandes. El paquete blist también proporciona los tipos sortedlist, sortedset, thinsortedlist, debilsortedset, sorteddict y btuple.

Se supondría un rendimiento reducido en el extremo de ejecución aleatoria / acceso aleatorio , ya que es una estructura de datos de "copia en escritura". Esto viola muchos supuestos de casos de uso en las listas de Python, así que úselo con cuidado .

SIN EMBARGO, si su caso de uso principal es hacer algo extraño y antinatural con una lista (como en el ejemplo forzado dado por @OP, o mi problema Python 2.6 FIFO queue-with-pass-over), entonces esto encajará muy bien .

Mark Gerolimatos
fuente
1

a pesar de muchas respuestas que sugieren su uso random.shuffle(x)y x.pop()es muy lento en datos grandes. y el tiempo requerido en una lista de 10000elementos que tomó 6 secondscuando se habilitó la reproducción aleatoria. cuando la reproducción aleatoria está desactivada, la velocidad0.2s

el método más rápido después de probar todos los métodos dados anteriormente resultó ser 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)

en apoyo de mi afirmación, aquí está el gráfico de complejidad de tiempo de esta fuente ingrese la descripción de la imagen aquí


SI no hay duplicados en la lista,

también puede lograr su propósito usando conjuntos. una vez que la lista se haya convertido en un conjunto de duplicados, se eliminarán. remove by valuey remove randomcosto O(1), es decir, muy eficiente. Este es el método más limpio que se me ocurrió.

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.

A diferencia de listsqué A+Bopción de soporte , setstambién admite A-B (A minus B)junto con A+B (A union B)y A.intersection(B,C,D). super útil cuando desea realizar operaciones lógicas en los datos.


OPCIONAL

SI desea velocidad cuando las operaciones se realizan en la cabeza y la cola de la lista, use python dequeue (cola de dos extremos) en apoyo de mi afirmación, aquí está la imagen. una imagen son mil palabras.

ingrese la descripción de la imagen aquí

nikhil swami
fuente