Python: List vs Dict para procurar tabela

169

Eu tenho cerca de 10 milhões de valores que preciso colocar em algum tipo de tabela de pesquisa, então fiquei imaginando qual seria uma lista ou ditado mais eficiente .

Eu sei que você pode fazer algo assim para ambos:

if something in dict_of_stuff:
    pass

e

if something in list_of_stuff:
    pass

Meu pensamento é que o ditado será mais rápido e mais eficiente.

Obrigado pela ajuda.

EDIT 1
Pouco mais informações sobre o que estou tentando fazer. Problema de Euler 92 . Estou fazendo uma tabela de consulta para ver se um valor calculado já foi calculado.

EDIT 2
Eficiência para procurar.

EDIT 3
Não há valores associados ao valor ... então um conjunto seria melhor?

Não
fonte
1
Eficiência em termos de quê? Inserir? Olho para cima? Consumo de memória? Você está verificando a existência pura de valor ou há algum metadado associado a ele?
truppo
Como uma observação lateral, você não precisa de uma lista ou ditado de 10 milhões para esse problema específico, mas de um problema muito menor.
Sfotiadis

Respostas:

222

Rapidez

As pesquisas nas listas são O (n), as pesquisas nos dicionários são amortizadas O (1), em relação ao número de itens na estrutura de dados. Se você não precisar associar valores, use conjuntos.

Memória

Dicionários e conjuntos usam hash e usam muito mais memória do que apenas para armazenamento de objetos. De acordo com AM Kuchling no Beautiful Code , a implementação tenta manter o hash 2/3 cheio, para que você possa perder bastante memória.

Se você não adicionar novas entradas rapidamente (o que você faz, com base na sua pergunta atualizada), pode valer a pena classificar a lista e usar a pesquisa binária. É O (log n) e provavelmente é mais lento para seqüências de caracteres, impossível para objetos que não possuem uma ordem natural.

Torsten Marek
fonte
6
Sim, mas é uma operação pontual se o conteúdo nunca mudar. A pesquisa binária é O (log n).
Torsten Marek
1
@ John Fouhy: as entradas não são armazenadas na tabela de hash, apenas ponteiros, ou seja, temos 40M para as entradas (bem, não realmente quando muitas delas são pequenas) e 60M para a tabela de hash. Concordo que hoje em dia não há muito problema, mas vale a pena lembrar.
Torsten Marek
2
Esta é uma pergunta antiga, mas acho que O (1) amortizado pode não ser verdadeiro para conjuntos / dictos muito grandes. O pior cenário possível de acordo com wiki.python.org/moin/TimeComplexity é O (n). Eu acho que depende da implementação do hash interno em que ponto o tempo médio diverge de O (1) e começa a convergir em O (n). Você pode ajudar no desempenho da pesquisa compartimentando os conjuntos globais em seções menores com base em algum atributo facilmente discernível (como o valor do primeiro dígito, depois o segundo, terceiro, etc., pelo tempo necessário para obter o tamanho ideal do conjunto) .
Nisan.H
3
@TorstenMarek Isso me confunde. A partir desta página , lookup lista é O (1) e pesquisa dict é O (n), que é o oposto do que você disse. Estou entendendo mal?
temporary_user_name
3
@ Aerovistae Acho que você leu errado as informações dessa página. Na lista, vejo O (n) para "x em s" (pesquisa). Também mostra a pesquisa de conjunto e ditado como caso médio de O (1).
Dennis
45

Um ditado é uma tabela de hash, portanto é muito rápido encontrar as chaves. Então, entre dict e list, dict seria mais rápido. Mas se você não tem um valor para associar, é ainda melhor usar um conjunto. É uma tabela de hash, sem a parte "table".


EDIT: para sua nova pergunta, SIM, um conjunto seria melhor. Basta criar 2 conjuntos, um para as seqüências terminadas em 1 e outro para as seqüências terminadas em 89. Resolvi esse problema com êxito usando conjuntos.

nosklo
fonte
35

set()é exatamente o que você quer. O (1) pesquisas e menor que um ditado.

recursivo
fonte
31

Fiz alguns testes de desempenho e verifica-se que o dict é mais rápido que a lista e o conjunto para grandes conjuntos de dados, executando o python 2.7.3 em uma CPU i7 no linux:

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

    10 loops, o melhor de 3: 64,2 ms por loop

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

    10000000 loops, o melhor de 3: 0,0759 usec por loop

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

    1000000 loops, o melhor de 3: 0,226 usec por loop

Como você pode ver, o dict é consideravelmente mais rápido que a lista e cerca de 3 vezes mais rápido que o definido. Em alguns aplicativos, você ainda pode querer escolher um conjunto para a beleza dele. E se os conjuntos de dados forem realmente pequenos (<1000 elementos), as listas terão um desempenho muito bom.

EriF89
fonte
Não deveria ser exatamente o oposto? Lista: 10 * 64,2 * 1000 = 642000 usec, dict: 10000000 * 0,0759 = 759000 usec e defina: 1000000 * 0,226 = 262000 usec ... para que os conjuntos sejam os mais rápidos, seguidos pela lista e com dict como o último no seu exemplo. Ou eu estou esquecendo de alguma coisa?
andzep
1
... mas a pergunta para mim aqui é: o que esses tempos estão realmente medindo? Não é o tempo de acesso a uma determinada lista, ditado ou conjunto, mas muito mais, o tempo e os loops para criar a lista, ditar, definir e, finalmente, encontrar e acessar um valor. Então, isso tem a ver com a pergunta? ... É interessante ...
andzep
8
@andzep, você está enganado, a -sopção é configurar o timeitambiente, ou seja, ele não conta no tempo total. A -sopção é executada apenas uma vez. No Python 3.3, obtenho esses resultados: gen (intervalo) -> 0,229 usec, lista -> 157 msec, dict -> 0,0806 usec, conjunto -> 0,0807 usec. Definir e ditar o desempenho é o mesmo. Dict no entanto demora um pouco mais para inicializar do que set (tempo total 13.580s v 11.803s.)
sleblanc
1
por que não usar o conjunto embutido? Na verdade, eu tenho resultados muito piores com sets.Set () do que com builtin set ()
Thomas Guyot-Sionnest
2
@ ThomasGuyot-Sionnest O conjunto interno foi introduzido no python 2.4, então não sei por que não o usei na minha solução proposta. Eu obtenho um bom desempenho com o python -mtimeit -s "d=set(range(10**7))" "5*10**6 in d"uso do Python 3.6.0 (10000000 loops, o melhor de 3: 0,0608 usec por loop), aproximadamente o mesmo que o benchmark dict, então obrigado pelo seu comentário.
EriF89
6

Você quer um ditado.

Para listas (não ordenadas) em Python, a operação "in" requer tempo O (n) --- não é bom quando você tem uma grande quantidade de dados. Um ditado, por outro lado, é uma tabela de hash, portanto, você pode esperar o tempo de pesquisa de O (1).

Como outros observaram, você pode escolher um conjunto (um tipo especial de ditado), se tiver apenas chaves em vez de pares chave / valor.

Palavras-chave:

  • Wiki do Python : informações sobre a complexidade temporal das operações do contêiner Python.
  • SO : tempo de operação do contêiner Python e complexidades da memória
zweiterlinde
fonte
1
Mesmo para listas classificadas, "in" é O (n).
2
Para uma lista vinculada, sim --- mas "listas" em Python são o que a maioria das pessoas chamaria de vetores, que fornecem acesso indexado em O (1) e uma operação de busca em O (log n), quando classificados.
zweiterlinde
Você está dizendo que o inoperador aplicado a uma lista classificada tem um desempenho melhor do que quando aplicado a uma lista não classificada (para uma pesquisa de um valor aleatório)? (Eu não acho que se eles são implementados internamente como vetores ou como nós em uma lista ligada é relevante.)
martineau
4

se os dados forem únicos, set () será o mais eficiente, mas com dois dict (o que também exige exclusividade, oops :)

SilentGhost
fonte
eu percebi quando vi minha resposta postada%)
SilentGhost
2
@SilentGhost se a resposta estiver errada, por que não excluí-la? muito ruim para os upvotes, mas isso acontece (bem, aconteceu )
Jean-François Fabre
3

Como um novo conjunto de testes para mostrar @ EriF89 ainda está certo após todos esses anos:

$ 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

Aqui também comparamos a tuple, que é conhecido por ser mais rápido do que lists(e usa menos memória) em alguns casos de uso. No caso da tabela de pesquisa, a tuplefeira não melhorou.

Tanto dicte setteve um ótimo desempenho. Isso traz um ponto interessante relacionado à resposta do @SilentGhost sobre a exclusividade: se o OP possui valores de 10 milhões em um conjunto de dados e não se sabe se há duplicatas, vale a pena manter um conjunto / ditado de seus elementos em paralelo com o conjunto de dados real e testando a existência nesse conjunto / dict. É possível que os 10 milhões de pontos de dados tenham apenas 10 valores exclusivos, o que é um espaço muito menor para pesquisar!

O erro do SilentGhost sobre dicts é realmente esclarecedor porque se pode usar um dict para correlacionar dados duplicados (em valores) em um conjunto não duplicado (chaves) e, assim, manter um objeto de dados para armazenar todos os dados, mas ainda assim ser rápido como uma tabela de pesquisa. Por exemplo, uma chave dict pode ser o valor que está sendo pesquisado e o valor pode ser uma lista de índices em uma lista imaginária em que esse valor ocorreu.

Por exemplo, se a lista de dados de origem a ser pesquisada fosse l=[1,2,3,1,2,1,4], ela poderia ser otimizada para pesquisa e memória, substituindo-a por este ditado:

>>> 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]})

Com este ditado, pode-se saber:

  1. Se um valor estava no conjunto de dados original (ou seja, 2 in dretorna True)
  2. Onde o valor foi no conjunto de dados original (ie d[2]retorna lista de índices de onde os dados foram encontrados em lista de dados original: [1, 4])
hamx0r
fonte
Para o seu último parágrafo, embora faça sentido lê-lo, seria bom (e provavelmente mais fácil de entender) ver o código real que você está tentando explicar.
Kaiser
0

Na verdade, você não precisa armazenar 10 milhões de valores na tabela, portanto não é grande coisa de qualquer maneira.

Dica: pense no tamanho do seu resultado após a primeira soma dos quadrados. O maior resultado possível será muito menor que 10 milhões ...

Kiv
fonte