Jaki jest najbardziej pythonowy sposób na zdejmowanie losowego elementu z listy?

88

Powiedzmy, że mam listę xo nieznanej długości, z której chcę losowo zdjąć jeden element, aby lista nie zawierała później elementu. Jaki jest najbardziej pythonowy sposób na zrobienie tego?

Można to zrobić przy użyciu raczej niepraktyczny combincation o pop, random.randinti len, i chciałby, aby krótsze lub ładniejsze rozwiązania:

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

To, co próbuję osiągnąć, to kolejno zdejmować losowe elementy z listy. (tj. losowo przesuń jeden element i przenieś go do słownika, losowo przesuń inny element i przenieś go do innego słownika, ...)

Zauważ, że używam Pythona 2.6 i nie znalazłem żadnych rozwiązań za pomocą funkcji wyszukiwania.

Henrik
źródło
3
Nie jestem zbyt Pythonistą, ale z pewnością wygląda to całkiem nieźle.
Matt Ball
przeprowadziłem szczegółową analizę złożoności czasowej, zobacz moją odpowiedź gdzieś dalej. SHUFFLE NIE JEST WYDAJNE! ale nadal możesz go używać, jeśli chcesz jakoś zmienić kolejność elementów. jeśli pop (0) dotyczy ciebie, użyj dequeue, o którym mowa w mojej analizie.
Nikhil Swami
O (2) złożoność czasowa dla pisemnej odpowiedzi. zawiń go w funkcję do szybkiego użycia. proszę zauważyć, że każda list.pop (n) inna niż list.pop (-1) przyjmuje O (n).
Nikhil Swami

Odpowiedzi:

94

Wydaje się, że to, co zamierzasz, nie wygląda zbyt Pythonicznie. Nie powinieneś usuwać rzeczy ze środka listy, ponieważ listy są implementowane jako tablice we wszystkich implementacjach Pythona, które znam, więc jest to O(n)operacja.

Jeśli naprawdę potrzebujesz tej funkcji jako części algorytmu, powinieneś sprawdzić strukturę danych, taką jak ta, blistktóra obsługuje wydajne usuwanie od środka.

W czystym Pythonie, jeśli nie potrzebujesz dostępu do pozostałych elementów, wystarczy najpierw przetasować listę, a następnie ją powtórzyć:

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

Jeśli naprawdę potrzebujesz reszty (co jest odrobiną zapachu kodu, IMHO), przynajmniej możesz teraz pop()od końca listy (co jest szybkie!):

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

Ogólnie rzecz biorąc, możesz często bardziej elegancko przedstawiać swoje programy, jeśli używasz bardziej funkcjonalnego stylu zamiast mutowania stanu (tak jak robisz to z listą).

Niklas B.
źródło
3
Więc lepszym (szybszym) pomysłem byłoby użycie random.shuffle(x)a potem x.pop()? Nie rozumiem, jak zrobić to „funkcjonalne”?
Henrik
1
@Henrik: Jeśli masz dwie kolekcje (na przykład listę słowników i listę liczb losowych) i chcesz je iterować w tym samym czasie, możesz zipuzyskać listę par (dykt, liczba). Powiedziałeś coś o wielu słownikach, z których każdy chcesz powiązać z losową liczbą. zipjest do tego idealny
Niklas B.
2
Mam dodać post, kiedy głosuję przeciw. Są chwile, kiedy musisz usunąć element ze środka listy ... Muszę to zrobić teraz. Brak wyboru: mam uporządkowaną listę, muszę usunąć element w środku. To jest do bani, ale jedyną alternatywą jest wykonanie ciężkiej refaktoryzacji kodu dla jednej średnio rzadkiej operacji. Problem dotyczy implementacji [], która POWINNA być wydajna dla takich operacji, ale nie jest.
Mark Gerolimatos
5
@NiklasB. OP używał przykładu losowego (szczerze mówiąc, powinien był zostać pominięty, przesłaniał problem). „Nie rób tego” jest niewystarczające. Lepszą odpowiedzią byłoby zasugerowanie struktury danych w Pythonie, która OBSŁUGUJE takie operacje, zapewniając jednocześnie WYSTARCZAJĄCĄ prędkość dostępu (wyraźnie nie tak dobrą jak arra ... er ... list). W Pythonie 2 nie mogłem go znaleźć. Jeśli to zrobię, odpowiem tym. Zwróć uwagę, że z powodu wpadki przeglądarki nie mogłem dodać tego do mojego pierwotnego komentarza, powinienem był dodać komentarz dodatkowy. Dziękuję za zachowanie uczciwości :)
Mark Gerolimatos
1
@MarkGerolimatos W bibliotece standardowej nie ma struktury danych z wydajnym dostępem swobodnym i wstawianiem / usuwaniem. Prawdopodobnie chcesz użyć czegoś takiego jak pypi.python.org/pypi/blist. Nadal argumentowałbym, że w wielu przypadkach można tego uniknąć
Niklas B.
49

Nie dostaniesz nic lepszego, ale tutaj jest niewielka poprawa:

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

Dokumentacja dotycząca random.randrange():

random.randrange ([start], stop [, step])
Zwraca losowo wybrany element z range(start, stop, step). Jest to równoważne choice(range(start, stop, step)), ale w rzeczywistości nie tworzy obiektu zakresu.

Andrew Clark
źródło
14

Aby usunąć pojedynczy element o losowym indeksie z listy, jeśli kolejność pozostałych elementów listy nie ma znaczenia:

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)

Zamiana jest używana do uniknięcia zachowania O (n) przy usuwaniu ze środka listy.

jfs
źródło
9

Oto kolejna alternatywa: dlaczego nie można przetasować listę pierwszy , a następnie rozpocząć pojawiały elementy, dopóki nie więcej elementy pozostają? lubię to:

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
źródło
3
@NiklasB. ponieważ usuwamy elementy z listy. Jeśli usunięcie elementów nie jest absolutnie konieczne, tak, zgadzam się z tobą:[for p in x]
Óscar López
Ponieważ zmienia listę i jeśli chcesz tylko wybrać połowę elementów teraz, a drugą połowę później, pozostały zestaw będziesz miał później.
Henrik
@Henrik: OK, dlatego zapytałem, czy potrzebujesz pozostałej listy. Nie odpowiedziałeś na to.
Niklas B.
2

Jednym ze sposobów jest:

x.remove(random.choice(x))
Simeon Visser
źródło
7
Może to być problematyczne, jeśli elementy występują więcej raz.
Niklas B.
2
Spowoduje to usunięcie skrajnego lewego elementu, gdy istnieją duplikaty, powodując niezupełnie losowy wynik.
FogleBird
Z popmożesz wskazać nazwę na usunięty element, z tym nie możesz.
agf
W porządku, zgadzam się, że nie jest to zbyt przypadkowe, gdy elementy występują więcej niż raz.
Simeon Visser
1
Oprócz kwestii wypaczenia dystrybucji, removewymaga liniowego skanowania listy. To strasznie nieefektywne w porównaniu z wyszukiwaniem indeksu.
aaronasterling
2

Chociaż nie wyskoczyłem z listy, napotkałem to pytanie w Google, próbując pobrać X losowych pozycji z listy bez duplikatów. Oto, czego ostatecznie użyłem:

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

Może to być nieco nieefektywne, ponieważ tasujesz całą listę, ale używasz tylko niewielkiej jej części, ale nie jestem ekspertem od optymalizacji, więc mogę się mylić.

Noah McIlraith
źródło
3
random.sample(items, items_needed)
jfs
2

Wiem, że to stare pytanie, ale ze względu na dokumentację:

Jeśli Ty (osoba wpisująca w Google to samo pytanie) robisz to, co myślę, że robisz, czyli losowo wybierasz k liczby pozycji z listy (gdzie k <= len (twoja lista)), ale upewniając się, że każdy element nigdy nie zostanie wybrany więcej niż jeden raz (= próbkowanie bez zamiany), możesz użyć random.sample, jak sugeruje @ jf-sebastian. Ale nie wiedząc więcej o przypadku użycia, nie wiem, czy tego potrzebujesz.

Dolf Andringa
źródło
1

Ta odpowiedź jest dzięki uprzejmości @ niklas-b :

Prawdopodobnie chcesz użyć czegoś takiego jak pypi.python.org/pypi/blist

Aby zacytować stronę PYPI :

... typ listowy z lepszą wydajnością asymptotyczną i podobną wydajnością na małych listach

Blist jest zamiennikiem listy Pythona, który zapewnia lepszą wydajność podczas modyfikowania dużych list. Pakiet blist udostępnia również typy sortlist, sortset, weaksortedlist, weaksortedset, sortdict i btuple.

Można by założyć obniżoną wydajność na końcu losowego dostępu / losowego uruchomienia , ponieważ jest to struktura danych „kopia przy zapisie”. To narusza wiele założeń dotyczących przypadków użycia na listach Pythona, więc używaj go ostrożnie .

JEDNAK, jeśli twoim głównym przypadkiem użycia jest zrobienie czegoś dziwnego i nienaturalnego z listą (jak w wymuszonym przykładzie podanym przez @OP lub moim problemem kolejki FIFO w Pythonie 2.6), to będzie to dobrze pasować do rachunku .

Mark Gerolimatos
źródło
1

pomimo wielu odpowiedzi sugerujących użycie random.shuffle(x)i x.pop()bardzo powolny w przypadku dużych danych. a czas potrzebny na liście 10000elementów zajął mniej więcej 6 secondspo włączeniu odtwarzania losowego. gdy odtwarzanie losowe jest wyłączone, prędkość wynosiła0.2s

najszybszą metodą po przetestowaniu wszystkich powyższych metod okazał się @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)

Na poparcie mojego twierdzenia jest tutaj wykres złożoności czasowej z tego źródła wprowadź opis obrazu tutaj


JEŚLI na liście nie ma duplikatów,

możesz osiągnąć swój cel również za pomocą zestawów. gdy lista zostanie utworzona jako duplikaty zestawu, zostanie usunięta. remove by valuei remove randomkoszt O(1), czyli bardzo efektywny. to najczystsza metoda, jaką mogłem wymyślić.

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.

W przeciwieństwie do tej opcji listswsparcia A+B, setsobsługuje również A-B (A minus B)wraz z A+B (A union B)i A.intersection(B,C,D). bardzo przydatne, gdy chcesz wykonywać operacje logiczne na danych.


OPCJONALNY

Jeśli chcesz, aby operacje wykonywane na początku i końcu listy były szybkie, użyj dekolejki Pythona (kolejka podwójna) na poparcie mojego twierdzenia, oto obraz. obraz to tysiąc słów.

wprowadź opis obrazu tutaj

Nikhil Swami
źródło