Python: List vs Dict dla tabeli przeglądowej

169

Mam około 10 milionów wartości, które muszę umieścić w jakiejś tabeli wyszukiwania, więc zastanawiałem się, która byłaby bardziej wydajna lista lub dyktowania ?

Wiem, że możesz zrobić coś takiego dla obu:

if something in dict_of_stuff:
    pass

i

if something in list_of_stuff:
    pass

Myślę, że dyktando będzie szybsze i wydajniejsze.

Dzięki za pomoc.

EDYCJA 1
Trochę więcej informacji o tym, co próbuję zrobić. Kłębuszek 92 . Robię przeglądową tabelę, aby sprawdzić, czy obliczona wartość została już obliczona.

EDYCJA 2
Wydajność wyszukiwania.

EDYCJA 3
Nie ma wartości powiązanych z wartością ... czy zestaw byłby lepszy?

nie
źródło
1
Skuteczność w sensie czego? Wstawić? Lookup? Zużycie pamięci? Czy sprawdzasz, czy istnieje czysta wartość, czy też są z nią powiązane metadane?
truppo
Na marginesie, nie potrzebujesz listy 10 milionów ani dyktowania dla tego konkretnego problemu, ale znacznie mniejszego.
sfotiadis

Odpowiedzi:

222

Prędkość

Odnośniki na listach to O (n), wyszukiwania w słownikach są amortyzowane O (1) w odniesieniu do liczby pozycji w strukturze danych. Jeśli nie musisz kojarzyć wartości, użyj zestawów.

Pamięć

Zarówno słowniki, jak i zestawy używają mieszania i zużywają znacznie więcej pamięci niż tylko do przechowywania obiektów. Według AM Kuchlinga w Beautiful Code implementacja stara się zachować 2/3 wartości skrótu, więc możesz stracić trochę pamięci.

Jeśli nie dodajesz nowych wpisów w locie (co robisz na podstawie zaktualizowanego pytania), warto posortować listę i użyć wyszukiwania binarnego. To jest O (log n) i prawdopodobnie będzie wolniejsze dla łańcuchów, niemożliwe dla obiektów, które nie mają naturalnego uporządkowania.

Torsten Marek
źródło
6
Tak, ale jest to jednorazowa operacja, jeśli zawartość nigdy się nie zmienia. Wyszukiwanie binarne to O (log n).
Torsten Marek
1
@John Fouhy: int nie są przechowywane w tablicy haszującej, tylko wskaźniki, tj. Masz 40M na int (no, nie bardzo, gdy wiele z nich jest małych) i 60M na tablicę haszującą. Zgadzam się, że w dzisiejszych czasach nie jest to aż tak duży problem, ale warto o tym pamiętać.
Torsten Marek
2
To stare pytanie, ale myślę, że zamortyzowane O (1) może nie mieć zastosowania w przypadku bardzo dużych zestawów / dykt. Według wiki.python.org/moin/TimeComplexity najgorszym scenariuszem jest O (n). Wydaje mi się, że zależy to od wewnętrznej implementacji haszowania, w którym momencie średni czas odbiega od O (1) i zaczyna zbiegać się do O (n). Możesz pomóc w wyszukiwaniu, dzieląc globalne zestawy na mniejsze sekcje w oparciu o jakiś łatwo rozpoznawalny atrybut (taki jak wartość pierwszej cyfry, następnie drugiej, trzeciej itd., Tak długo, jak potrzebujesz, aby uzyskać optymalny rozmiar zestawu) .
Nisan.H
3
@TorstenMarek To mnie wprawia w zakłopotanie. Na tej stronie wyszukiwanie list to O (1), a wyszukiwanie dict to O (n), co jest przeciwieństwem tego, co powiedziałeś. Czy nie rozumiem?
tymczasowy_nazwa_użytkownika
3
@Aerovistae Myślę, że źle odczytałeś informacje na tej stronie. Pod listą widzę O (n) dla „x in s” (wyszukiwanie). Pokazuje również wyszukiwanie set i dict jako przypadek średni O (1).
Dennis
45

Dykt to tablica mieszająca, więc znalezienie kluczy jest naprawdę szybkie. Więc między dict i list, dict byłby szybszy. Ale jeśli nie masz wartości do skojarzenia, jeszcze lepiej jest użyć zestawu. Jest to tablica mieszająca bez części „tabela”.


EDYCJA: w przypadku nowego pytania TAK, zestaw byłby lepszy. Po prostu utwórz 2 zestawy, jeden dla sekwencji zakończonych na 1, a drugi dla sekwencji zakończonych na 89. Z powodzeniem rozwiązałem ten problem używając zestawów.

nosklo
źródło
35

set()jest dokładnie tym, czego chcesz. O (1) wyszukiwania i mniejsze niż dict.

rekurencyjny
źródło
31

Przeprowadziłem trochę testów porównawczych i okazuje się, że dict jest szybszy niż lista i zestaw dla dużych zestawów danych, uruchamiając Pythona 2.7.3 na procesorze i7 w systemie Linux:

  • python -mtimeit -s 'd=range(10**7)' '5*10**6 in d'

    10 pętli, najlepiej 3: 64,2 ms na pętlę

  • python -mtimeit -s 'd=dict.fromkeys(range(10**7))' '5*10**6 in d'

    10000000 pętli, najlepiej 3: 0,0759 usek na pętlę

  • python -mtimeit -s 'from sets import Set; d=Set(range(10**7))' '5*10**6 in d'

    1000000 pętli, najlepiej 3: 0,262 usek na pętlę

Jak widać, dict jest znacznie szybszy niż list i około 3 razy szybszy niż set. Jednak w niektórych aplikacjach nadal możesz chcieć wybrać zestaw ze względu na jego piękno. A jeśli zbiory danych są naprawdę małe (<1000 elementów), listy działają całkiem dobrze.

EriF89
źródło
Czy nie powinno być dokładnie odwrotnie? Lista: 10 * 64,2 * 1000 = 642000 usek, dict: 10000000 * 0,0759 = 759000 usek i ustaw: 1000000 * 0,262 = 262000 usek ... więc zestawy są najszybsze, po których następuje lista i dict jako ostatnie w twoim przykładzie. A może coś mi brakuje?
andzep
1
... ale pytanie dla mnie tutaj brzmi: co właściwie mierzą te czasy? Nie czas dostępu do danej listy, dyktowania lub zestawu, ale znacznie więcej, czas i pętle do utworzenia listy, dyktowania, ustawiania i wreszcie znajdowania i uzyskiwania dostępu do jednej wartości. Czy ma to w ogóle coś wspólnego z pytaniem? ... Jest to jednak interesujące ...
izep
8
@andzep, mylisz się, -sopcja jest do ustawienia timeitśrodowiska, czyli nie wlicza się do całkowitego czasu. -sOpcja jest uruchamiane tylko raz. W Pythonie 3.3 otrzymuję następujące wyniki: gen (zakres) -> 0,229 usek, lista -> 157 ms, dict -> 0,0806 usek, set -> 0,0807 usek. Wydajność zestawu i dyktowania jest taka sama. Jednak inicjalizacja dyktowania zajmuje nieco więcej czasu niż ustawienie (całkowity czas 13,580s w porównaniu z 11,803s)
sleblanc
1
dlaczego nie skorzystać z wbudowanego zestawu? Właściwie uzyskuję znacznie gorsze wyniki z setami. Set () niż z wbudowanym set ()
Thomas Guyot-Sionnest
2
@ ThomasGuyot-Sionnest Wbudowany zestaw został wprowadzony w Pythonie 2.4, więc nie jestem pewien, dlaczego nie użyłem go w moim proponowanym rozwiązaniu. python -mtimeit -s "d=set(range(10**7))" "5*10**6 in d"Osiągam dobrą wydajność przy użyciu Pythona 3.6.0 (10000000 pętli, najlepiej 3: 0,0608 usek na pętlę), mniej więcej tyle samo, co test porównawczy dict, więc dziękuję za komentarz.
EriF89
6

Chcesz dyktanda.

W przypadku (nieposortowanych) list w Pythonie operacja „in” wymaga czasu O (n) - nie jest dobra, gdy masz dużą ilość danych. Z drugiej strony dykt jest tablicą mieszającą, więc możesz spodziewać się czasu wyszukiwania O (1).

Jak zauważyli inni, możesz zamiast tego wybrać zestaw (specjalny typ dyktowania), jeśli masz tylko klucze, a nie pary klucz / wartość.

Związane z:

  • Wiki Pythona : informacje o złożoności czasowej operacji kontenerowych w języku Python.
  • System operacyjny: czas operacji kontenera Pythona i złożoność pamięci
zweiterlinde
źródło
1
Nawet w przypadku posortowanych list „in” to O (n).
2
W przypadku listy połączonej tak - ale „listy” w Pythonie są tym, co większość ludzi nazwałaby wektorami, które po posortowaniu zapewniają indeksowany dostęp w O (1) i operację znajdowania w O (log n).
zweiterlinde
Czy chcesz powiedzieć, że inoperator zastosowany do posortowanej listy działa lepiej niż zastosowany do nieposortowanej listy (w celu wyszukiwania losowej wartości)? (Nie sądzę, czy są one zaimplementowane wewnętrznie jako wektory, czy jako węzły w połączonej liście).
martineau
4

jeśli dane są unikalne, set () będzie najbardziej wydajne, ale z dwóch - dict (co również wymaga unikalności, ups :)

SilentGhost
źródło
zdałem sobie sprawę, kiedy zobaczyłem, że moja odpowiedź została opublikowana%)
SilentGhost
2
@SilentGhost jeśli odpowiedź jest błędna, dlaczego jej nie usunąć? szkoda dla głosów pozytywnych, ale tak się stało (no, stało się )
Jean-François Fabre
3

Jako nowy zestaw testów do pokazania, @ EriF89 wciąż ma rację po tych wszystkich latach:

$ python -m timeit -s "l={k:k for k in xrange(5000)}"    "[i for i in xrange(10000) if i in l]"
1000 loops, best of 3: 1.84 msec per loop
$ python -m timeit -s "l=[k for k in xrange(5000)]"    "[i for i in xrange(10000) if i in l]"
10 loops, best of 3: 573 msec per loop
$ python -m timeit -s "l=tuple([k for k in xrange(5000)])"    "[i for i in xrange(10000) if i in l]"
10 loops, best of 3: 587 msec per loop
$ python -m timeit -s "l=set([k for k in xrange(5000)])"    "[i for i in xrange(10000) if i in l]"
1000 loops, best of 3: 1.88 msec per loop

Tutaj również porównujemy a tuple, o których wiadomo, że są szybsze niż lists(i zużywają mniej pamięci) w niektórych przypadkach użycia. W przypadku tabeli przeglądowej rozszerzenietuple nie wypadło lepiej.

Zarówno dictiset spisały się bardzo dobrze. Daje to interesującą kwestię związaną z odpowiedzią @SilentGhost na temat unikalności: jeśli OP ma 10M wartości w zestawie danych i nie wiadomo, czy są w nich duplikaty, warto byłoby równolegle zachować zestaw / dyktando jego elementów z rzeczywistym zbiorem danych i testowaniem istnienia w tym zbiorze / dyktandzie. Możliwe, że 10M punktów danych ma tylko 10 unikalnych wartości, co oznacza znacznie mniejszą przestrzeń do wyszukiwania!

Błąd SilentGhost dotyczący dykt jest w rzeczywistości naświetlony, ponieważ można użyć dyktu do skorelowania zduplikowanych danych (w wartościach) w nie powielony zestaw (klucze), a tym samym zachować jeden obiekt danych do przechowywania wszystkich danych, a jednocześnie być szybki jak tabela przeglądowa. Na przykład kluczem dict może być szukana wartość, a wartością może być lista indeksów na wyimaginowanej liście, na której ta wartość wystąpiła.

Na przykład, jeśli lista danych źródłowych do przeszukania była l=[1,2,3,1,2,1,4], można ją zoptymalizować zarówno pod kątem wyszukiwania, jak i pamięci, zastępując ją tym dyktowaniem:

>>> from collections import defaultdict
>>> d = defaultdict(list)
>>> l=[1,2,3,1,2,1,4]
>>> for i, e in enumerate(l):
...     d[e].append(i)
>>> d
defaultdict(<class 'list'>, {1: [0, 3, 5], 2: [1, 4], 3: [2], 4: [6]})

Z tym dyktando można wiedzieć:

  1. Jeśli wartość znajdowała się w oryginalnym zbiorze danych (tj. 2 in dZwraca True)
  2. Gdzie wartość była w oryginalnym zbiorze danych (czyli d[2]wraca lista indeksów, gdzie dane zostały znalezione w oryginalnej listy danych: [1, 4])
hamx0r
źródło
W przypadku ostatniego akapitu przeczytanie go ma sens, ale byłoby miło (i prawdopodobnie łatwiejsze do zrozumienia) zobaczyć rzeczywisty kod, który próbujesz wyjaśnić.
kaiser
0

W rzeczywistości nie musisz przechowywać 10 milionów wartości w tabeli, więc i tak nie jest to wielka sprawa.

Wskazówka: zastanów się, jak duży może być Twój wynik po wykonaniu pierwszej operacji sumy kwadratów. Największy możliwy wynik będzie znacznie mniejszy niż 10 milionów ...

Kiv
źródło