Quelle est la manière la plus pythonique de faire apparaître un élément aléatoire d'une liste?

88

Disons que j'ai une liste xavec une longueur inconnue à partir de laquelle je veux sauter un élément au hasard afin que la liste ne contienne pas l'élément par la suite. Quelle est la manière la plus pythonique de faire cela?

Je peux le faire en utilisant une combinaison plutôt peu pratique de pop, random.randintet len, et j'aimerais voir des solutions plus courtes ou plus agréables:

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

Ce que j'essaie de réaliser, c'est de faire apparaître consécutivement des éléments aléatoires dans une liste. (c'est-à-dire, pop aléatoirement un élément et le déplacer vers un dictionnaire, pop aléatoirement un autre élément et le déplacer vers un autre dictionnaire, ...)

Notez que j'utilise Python 2.6 et n'ai trouvé aucune solution via la fonction de recherche.

Henrik
la source
3
Je ne suis pas vraiment un Pythoniste, mais cela me semble plutôt bien.
Matt Ball
une analyse détaillée de la complexité du temps a été effectuée par moi, voir ma réponse quelque part sur la route. SHUFFLE N'EST PAS EFFICACE! mais vous pouvez toujours l'utiliser si vous devez modifier l'ordre des éléments d'une manière ou d'une autre. si pop (0) vous concerne, utilisez dequeue, mentionné dans mon analyse.
nikhil swami
O (2) complexité temporelle pour la réponse écrite. enveloppez-le dans une fonction pour une utilisation rapide. veuillez noter que tout list.pop (n) autre que list.pop (-1) prend O (n).
nikhil swami il y a

Réponses:

94

Ce que vous semblez faire ne semble pas très pythonique en premier lieu. Vous ne devriez pas supprimer des éléments du milieu d'une liste, car les listes sont implémentées sous forme de tableaux dans toutes les implémentations Python que je connais, c'est donc une O(n)opération.

Si vous avez vraiment besoin de cette fonctionnalité dans le cadre d'un algorithme, vous devriez consulter une structure de données comme celle blistqui prend en charge la suppression efficace du milieu.

En Python pur, ce que vous pouvez faire si vous n'avez pas besoin d'accéder aux éléments restants est simplement de mélanger d'abord la liste, puis de la parcourir:

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

Si vous avez vraiment besoin du reste (ce qui est un peu une odeur de code, à mon humble avis), au moins vous pouvez à pop()partir de la fin de la liste maintenant (ce qui est rapide!):

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

En général, vous pouvez souvent exprimer vos programmes avec plus d'élégance si vous utilisez un style plus fonctionnel, au lieu de changer d'état (comme vous le faites avec la liste).

Niklas B.
la source
3
Donc, une meilleure idée (plus rapide) serait d'utiliser random.shuffle(x)et ensuite x.pop()? Je ne comprends pas comment faire cela "fonctionnel"?
Henrik
1
@Henrik: Si vous avez deux collections (par exemple, une liste de dictionnaires et une liste de nombres aléatoires) et que vous souhaitez les itérer en même temps, vous pouvez ziples obtenir pour obtenir une liste de paires (dict, nombre). Vous avez dit quelque chose à propos de plusieurs dictionnaires dont vous souhaitez associer chacun à un nombre aléatoire. zipest parfait pour cela
Niklas B.
2
Je suis censé ajouter un message lorsque je vote contre. Il y a des moments où vous devez supprimer un élément du milieu d'une liste ... Je dois le faire maintenant. Pas de choix: j'ai une liste ordonnée, je dois supprimer un article au milieu. Ça craint, mais le seul autre choix est de faire un refactoring de code lourd pour une opération semi-rare. Le problème est celui de la mise en œuvre de [], qui DEVRAIT être efficace pour de telles opérations, mais ne l'est pas.
Mark Gerolimatos
5
@NiklasB. L'OP utilisait le hasard comme exemple (franchement, cela aurait dû être laissé de côté, cela a obscurci le problème). «Ne fais pas ça» est insuffisant. Une meilleure réponse aurait été de suggérer une structure de données Python qui prend en charge de telles opérations tout en fournissant une vitesse d'accès SUFFISANTE (clairement pas aussi bonne que arra ... euh ... liste). En python 2, je n'ai pas pu en trouver. Si je le fais, je répondrai avec cela. Notez qu'en raison d'un incident de navigateur, je n'ai pas pu ajouter cela à mon commentaire d'origine, j'aurais dû ajouter un commentaire secondaire. Merci de me garder honnête :)
Mark Gerolimatos
1
@MarkGerolimatos Il n'y a pas de structure de données avec à la fois un accès aléatoire efficace et une insertion / suppression dans la bibliothèque standard. Vous souhaitez probablement utiliser quelque chose comme pypi.python.org/pypi/blist.Je dirais toujours que dans de nombreux cas d'utilisation, cela peut être évité
Niklas B.
49

Vous n'obtiendrez pas beaucoup mieux que cela, mais voici une légère amélioration:

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

Documentation sur random.randrange():

random.randrange ([start], stop [, step])
Renvoie un élément sélectionné au hasard à partir de range(start, stop, step). Cela équivaut à choice(range(start, stop, step)), mais ne crée pas réellement un objet range.

Andrew Clark
la source
14

Pour supprimer un seul élément à un index aléatoire d'une liste si l'ordre du reste des éléments de la liste n'a pas d'importance:

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)

Le swap est utilisé pour éviter le comportement O (n) lors de la suppression du milieu d'une liste.

jfs
la source
9

Voici une autre alternative: pourquoi ne pas vous mélangez la liste d' abord , puis commencer à éclater les éléments de celui - ci jusqu'à plus restent des éléments? comme ça:

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
la source
3
@NiklasB. parce que nous supprimons des éléments de la liste. S'il n'est pas absolument nécessaire de supprimer des éléments, oui je suis d'accord avec vous:[for p in x]
Óscar López
Parce que cela modifie la liste et si vous voulez juste sélectionner la moitié des éléments maintenant et l'autre moitié plus tard, vous aurez le jeu restant plus tard.
Henrik
@Henrik: D'accord, c'est pourquoi je vous ai demandé si vous aviez besoin de la liste restante. Vous n'avez pas répondu à cela.
Niklas B.
2

Une façon de le faire est:

x.remove(random.choice(x))
Siméon Visser
la source
7
Cela pourrait devenir problématique si les éléments se produisent plusieurs fois.
Niklas B.
2
Cela supprimera l'élément le plus à gauche lorsqu'il y a des doublons, provoquant un résultat pas parfaitement aléatoire.
FogleBird
Avec popvous pouvez pointer un nom sur l'élément supprimé, avec cela vous ne pouvez pas.
agf
Assez juste, je conviens que ce n'est pas très aléatoire lorsque des éléments se produisent plus d'une fois.
Simeon Visser
1
Hormis la question de biaiser votre distribution, removenécessite un balayage linéaire de la liste. C'est terriblement inefficace par rapport à la recherche d'un index.
aaronasterling
2

Sans sortir de la liste, j'ai rencontré cette question sur Google en essayant d'obtenir X éléments aléatoires à partir d'une liste sans doublons. Voici ce que j'ai finalement utilisé:

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

Cela peut être légèrement inefficace car vous mélangez une liste entière mais n'en utilisez qu'une petite partie, mais je ne suis pas un expert en optimisation, donc je peux me tromper.

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

Je sais que c'est une vieille question, mais juste pour la documentation:

Si vous (la personne qui recherche la même question sur Google) faites ce que je pense que vous faites, c'est-à-dire sélectionner k nombre d'éléments au hasard dans une liste (où k <= len (votre liste)), mais en vous assurant que chaque élément n'est plus sélectionné plus d'une fois (= échantillonnage sans remplacement), vous pouvez utiliser random.sample comme le suggère @ jf-sebastian. Mais sans en savoir plus sur le cas d'utilisation, je ne sais pas si c'est ce dont vous avez besoin.

Dolf Andringa
la source
1

Cette réponse est une gracieuseté de @ niklas-b :

" Vous souhaitez probablement utiliser quelque chose comme pypi.python.org/pypi/blist "

Pour citer la page PYPI :

... un type de type liste avec de meilleures performances asymptotiques et des performances similaires sur les petites listes

Le blist est un remplacement instantané de la liste Python qui offre de meilleures performances lors de la modification de grandes listes. Le package blist fournit également les types trié, trié, trié faible, trié faible, trié trié et btuple.

On supposerait des performances réduites à la fin de l'accès aléatoire / exécution aléatoire , car il s'agit d'une structure de données «copie à l'écriture». Cela viole de nombreuses hypothèses de cas d'utilisation sur les listes Python, alors utilisez-le avec précaution .

CEPENDANT, si votre cas d'utilisation principal est de faire quelque chose d'étrange et de contre-nature avec une liste (comme dans l'exemple forcé donné par @OP, ou mon problème de file d'attente avec transfert Python 2.6 FIFO), cela conviendra parfaitement. .

Mark Gerolimatos
la source
1

malgré de nombreuses réponses suggérant l'utilisation random.shuffle(x)et x.pop()son très lent sur les données volumineuses. et le temps requis sur une liste d' 10000éléments a pris environ 6 secondslorsque la lecture aléatoire est activée. lorsque la lecture aléatoire est désactivée, la vitesse était0.2s

la méthode la plus rapide après avoir testé toutes les méthodes données ci-dessus s'est avérée être écrite par @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)

à l'appui de ma réclamation, voici le graphique de complexité temporelle de cette source entrez la description de l'image ici


SI il n'y a pas de doublons dans la liste,

vous pouvez également atteindre votre objectif en utilisant des ensembles. une fois la liste transformée en doublons, elle sera supprimée. remove by valueet remove randomcoût O(1), c'est-à-dire très efficace. c'est la méthode la plus propre que je puisse proposer.

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.

Contrairement à listsquelle A+Boption de support , setssupporte également A-B (A minus B)avec A+B (A union B)et A.intersection(B,C,D). super utile lorsque vous souhaitez effectuer des opérations logiques sur les données.


OPTIONNEL

SI vous voulez de la vitesse lorsque les opérations sont effectuées sur la tête et la queue de la liste, utilisez python dequeue (file d'attente double) à l'appui de ma réclamation, voici l'image. une image est mille mots.

entrez la description de l'image ici

nikhil swami
la source