Python: List vs Dict para la tabla de búsqueda

169

Tengo alrededor de 10 millones de valores que necesito poner en algún tipo de tabla de consulta, por lo que me preguntaba cuál sería más eficiente una lista o dict .

Sé que puedes hacer algo así para ambos:

if something in dict_of_stuff:
    pass

y

if something in list_of_stuff:
    pass

Mi pensamiento es que el dict será más rápido y más eficiente.

Gracias por tu ayuda.

EDITAR 1 Un
poco más de información sobre lo que estoy tratando de hacer. Problema de Euler 92 . Estoy haciendo una tabla de búsqueda para ver si un valor calculado ya está listo.

EDIT 2
Eficiencia para buscar.

EDITAR 3
No hay valores asociados con el valor ... entonces, ¿ sería mejor un conjunto ?

No
fuente
1
¿Eficiencia en términos de qué? ¿Insertar? ¿Buscar? Consumo de memoria? ¿Está comprobando la existencia pura de valor, o hay algún metadato asociado con él?
truppo
Como nota al margen, no necesita una lista de 10 millones o dict para ese problema específico, sino uno mucho más pequeño.
sfotiadis

Respuestas:

222

Velocidad

Las búsquedas en las listas son O (n), las búsquedas en los diccionarios se amortizan O (1), con respecto al número de elementos en la estructura de datos. Si no necesita asociar valores, use conjuntos.

Memoria

Tanto los diccionarios como los conjuntos usan hashing y usan mucha más memoria que solo para el almacenamiento de objetos. Según AM Kuchling en Beautiful Code , la implementación intenta mantener el hash 2/3 lleno, por lo que puede desperdiciar bastante memoria.

Si no agrega nuevas entradas sobre la marcha (lo que hace, según su pregunta actualizada), podría valer la pena ordenar la lista y usar la búsqueda binaria. Esto es O (log n), y es probable que sea más lento para las cadenas, imposible para los objetos que no tienen un orden natural.

Torsten Marek
fuente
66
Sí, pero es una operación única si el contenido nunca cambia. La búsqueda binaria es O (log n).
Torsten Marek
1
@John Fouhy: los ints no se almacenan en la tabla hash, solo los punteros, es decir, tienes 40M para los ints (bueno, no realmente cuando muchos de ellos son pequeños) y 60M para la tabla hash. Estoy de acuerdo en que no es un gran problema hoy en día, pero vale la pena tenerlo en cuenta.
Torsten Marek
2
Esta es una vieja pregunta, pero creo que O (1) amortizado puede no ser cierto para conjuntos / dictos muy grandes. El peor de los casos según wiki.python.org/moin/TimeComplexity es O (n). Supongo que depende de la implementación interna de hashing en qué punto el tiempo promedio diverge de O (1) y comienza a converger en O (n). Puede ayudar al rendimiento de búsqueda compartimentando los conjuntos globales en secciones más pequeñas basadas en algún atributo fácilmente discernible (como el valor del primer dígito, luego el segundo, tercero, etc., durante el tiempo que necesite obtener un tamaño de conjunto óptimo) .
Nisan.H
3
@TorstenMarek Esto me confunde. Desde esta página , la búsqueda de la lista es O (1) y la búsqueda de dict es O (n), que es lo opuesto a lo que dijo. ¿Estoy malentendido?
temporary_user_name
3
@Aerovistae Creo que leyó mal la información en esa página. En la lista, veo O (n) para "x en s" (búsqueda). También muestra la búsqueda establecida y dictada como O (1) caso promedio.
Dennis
45

Un dict es una tabla hash, por lo que es muy rápido encontrar las claves. Entonces, entre dict y list, dict sería más rápido. Pero si no tiene un valor para asociar, es aún mejor usar un conjunto. Es una tabla hash, sin la parte de "tabla".


EDITAR: para su nueva pregunta, SÍ, un conjunto sería mejor. Simplemente cree 2 conjuntos, uno para las secuencias terminadas en 1 y otro para las secuencias terminadas en 89. He resuelto con éxito este problema usando conjuntos.

nosklo
fuente
35

set()es exactamente lo que quieres O (1) búsquedas, y más pequeño que un dict.

recursivo
fuente
31

Hice algunas evaluaciones comparativas y resultó que dict es más rápido que la lista y el conjunto para conjuntos de datos grandes, ejecutando python 2.7.3 en una CPU i7 en Linux:

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

    10 bucles, lo mejor de 3: 64,2 ms por bucle

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

    10000000 bucles, lo mejor de 3: 0.0759 usec por bucle

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

    1000000 bucles, lo mejor de 3: 0.262 usec por bucle

Como puede ver, dict es considerablemente más rápido que la lista y aproximadamente 3 veces más rápido que el conjunto. Sin embargo, en algunas aplicaciones es posible que aún desee elegir el conjunto por su belleza. Y si los conjuntos de datos son realmente pequeños (<1000 elementos), las listas funcionan bastante bien.

EriF89
fuente
¿No debería ser exactamente lo contrario? Lista: 10 * 64.2 * 1000 = 642000 usec, dict: 10000000 * 0.0759 = 759000 usec y set: 1000000 * 0.262 = 262000 usec ... entonces los conjuntos son los más rápidos, seguidos de la lista y con dict como último en su ejemplo. ¿O me estoy perdiendo algo?
andzep
1
... pero la pregunta para mí aquí es: ¿qué mide realmente este tiempo? No es el tiempo de acceso para una lista dada, dict o set, sino mucho más, el tiempo y los bucles para crear la lista, dict, set y finalmente para encontrar y acceder a un valor. Entonces, ¿tiene esto algo que ver con la pregunta? ... Aunque es interesante ...
andzep
8
@andzep, está equivocado, la -sopción es configurar el timeitentorno, es decir, no cuenta en el tiempo total. La -sopción se ejecuta solo una vez. En Python 3.3, obtengo estos resultados: gen (rango) -> 0.229 usec, lista -> 157 mseg, dict -> 0.0806 usec, set -> 0.0807 usec. Establecer y dictar el rendimiento es el mismo. Sin embargo, Dict tarda un poco más en inicializarse que el conjunto (tiempo total 13.580s v. 11.803s)
sleblanc
1
¿Por qué no usar el juego incorporado? De hecho, obtengo resultados mucho peores con los sets.Set () que con el set incorporado ()
Thomas Guyot-Sionnest
2
@ ThomasGuyot-Sionnest El conjunto integrado se introdujo en Python 2.4, así que no estoy seguro de por qué no lo utilicé en mi solución propuesta. python -mtimeit -s "d=set(range(10**7))" "5*10**6 in d"Obtuve un buen rendimiento con el uso de Python 3.6.0 (10000000 bucles, lo mejor de 3: 0.0608 usec por bucle), aproximadamente lo mismo que el punto de referencia dict, así que gracias por su comentario.
EriF89
6

Quieres un dict.

Para las listas (sin clasificar) en Python, la operación "in" requiere un tiempo O (n) --- no es bueno cuando tiene una gran cantidad de datos. Un dict, por otro lado, es una tabla hash, por lo que puede esperar el tiempo de búsqueda O (1).

Como otros han señalado, puede elegir un conjunto (un tipo especial de dict) en su lugar, si solo tiene claves en lugar de pares clave / valor.

Relacionado:

  • Wiki de Python : información sobre la complejidad temporal de las operaciones de contenedor de Python.
  • SO : tiempo de operación del contenedor Python y complejidades de memoria
zweiterlinde
fuente
1
Incluso para listas ordenadas, "in" es O (n).
2
Para una lista vinculada, sí --- pero las "listas" en Python son lo que la mayoría de las personas llamarían vectores, que proporcionan acceso indexado en O (1) y una operación de búsqueda en O (log n), cuando se ordenan.
zweiterlinde
¿Está diciendo que el inoperador aplicado a una lista ordenada funciona mejor que cuando se aplica a una lista sin clasificar (para una búsqueda de un valor aleatorio)? (No creo que sea relevante si se implementan internamente como vectores o como nodos en una lista vinculada).
Martineau
4

si los datos son únicos, set () será el más eficiente, pero de dos dict (que también requiere unicidad, oops :)

SilentGhost
fuente
me di cuenta cuando vi mi respuesta publicada%)
SilentGhost
2
@SilentGhost si la respuesta es incorrecta, ¿por qué no eliminarla? lástima para los votos, pero eso sucede (bueno, sucedió )
Jean-François Fabre
3

Como un nuevo conjunto de pruebas para mostrar @ EriF89 todavía tiene razón después de todos estos años:

$ 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

Aquí también comparamos a tuple, que se sabe que son más rápidos que lists(y usan menos memoria) en algunos casos de uso. En el caso de la tabla de búsqueda, el tuplecarenado no mejoró.

Tanto el dicty setfuncionó muy bien. Esto trae un punto interesante relacionado con la respuesta de @SilentGhost sobre la unicidad: si el OP tiene valores de 10M en un conjunto de datos, y se desconoce si hay duplicados en ellos, entonces valdría la pena mantener un conjunto / dict de sus elementos en paralelo con el conjunto de datos real y las pruebas de existencia en ese conjunto / dict. ¡Es posible que los 10M puntos de datos solo tengan 10 valores únicos, que es un espacio mucho más pequeño para buscar!

El error de SilentGhost sobre los dictos es realmente esclarecedor porque uno podría usar un dict para correlacionar datos duplicados (en valores) en un conjunto no duplicado (claves) y, por lo tanto, mantener un objeto de datos para contener todos los datos, pero aún así ser rápido como una tabla de búsqueda. Por ejemplo, una clave dict podría ser el valor que se busca, y el valor podría ser una lista de índices en una lista imaginaria donde ocurrió ese valor.

Por ejemplo, si la lista de datos de origen a buscar fuera l=[1,2,3,1,2,1,4], podría optimizarse tanto para la búsqueda como para la memoria reemplazándola con este dict:

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

Con este dict, uno puede saber:

  1. Si un valor estaba en el conjunto de datos original (es decir, 2 in ddevuelve True)
  2. Cuando el valor era en el conjunto de datos original (es decir, d[2]devuelve la lista de índices, donde se encontró datos en la lista de datos original: [1, 4])
hamx0r
fuente
Para su último párrafo, si bien tiene sentido leerlo, sería bueno (y probablemente más fácil de entender) ver el código real que está tratando de explicar.
kaiser
0

En realidad, no necesita almacenar 10 millones de valores en la tabla, por lo que no es un gran problema de ninguna manera.

Sugerencia: piense en cuán grande puede ser su resultado después de la primera operación de suma de cuadrados. El mayor resultado posible será mucho menor que 10 millones ...

Kiv
fuente