Python: List vs Dict pour la table de recherche

169

J'ai environ 10 millions de valeurs que je dois mettre dans un type de table de recherche, alors je me demandais quelle serait la plus efficace une liste ou un dict ?

Je sais que vous pouvez faire quelque chose comme ça pour les deux:

if something in dict_of_stuff:
    pass

et

if something in list_of_stuff:
    pass

Je pense que le dict sera plus rapide et plus efficace.

Merci de votre aide.

EDIT 1 Un
peu plus d'informations sur ce que j'essaie de faire. Problème d'Euler 92 . Je crée une table de consultation pour voir si une valeur calculée a été entièrement calculée.

EDIT 2
Efficacité pour la recherche.

EDIT 3
Il n'y a pas de valeurs associées à la valeur ... alors un ensemble serait-il meilleur?

Nan
la source
1
L'efficacité en termes de quoi? Insérer? Chercher? Consommation de mémoire? Vérifiez-vous l'existence pure de valeur, ou y a-t-il des métadonnées qui lui sont associées?
truppo le
En passant, vous n'avez pas besoin d'une liste ou d'un dict de 10 millions pour ce problème spécifique, mais bien plus petit.
sfotiadis

Réponses:

222

La vitesse

Les recherches dans les listes sont O (n), les recherches dans les dictionnaires sont amorties O (1), par rapport au nombre d'éléments dans la structure de données. Si vous n'avez pas besoin d'associer des valeurs, utilisez des ensembles.

Mémoire

Les dictionnaires et les ensembles utilisent le hachage et ils utilisent beaucoup plus de mémoire que pour le stockage d'objets uniquement. Selon AM Kuchling dans Beautiful Code , l'implémentation essaie de garder le hachage 2/3 complet, vous risquez donc de perdre un peu de mémoire.

Si vous n'ajoutez pas de nouvelles entrées à la volée (ce que vous faites, en fonction de votre question mise à jour), il peut être utile de trier la liste et d'utiliser la recherche binaire. C'est O (log n), et est susceptible d'être plus lent pour les chaînes, impossible pour les objets qui n'ont pas un ordre naturel.

Torsten Marek
la source
6
Oui, mais c'est une opération ponctuelle si le contenu ne change jamais. La recherche binaire est O (log n).
Torsten Marek
1
@John Fouhy: les entiers ne sont pas stockés dans la table de hachage, seulement des pointeurs, c'est-à-dire que vous avez 40M pour les entiers (enfin, pas vraiment quand beaucoup d'entre eux sont petits) et 60M pour la table de hachage. Je suis d'accord que ce n'est pas vraiment un problème de nos jours, mais cela vaut la peine de le garder à l'esprit.
Torsten Marek
2
C'est une vieille question, mais je pense que O (1) amorti peut ne pas être vrai pour les très grands ensembles / dictionnaires. Le pire des cas selon wiki.python.org/moin/TimeComplexity est O (n). Je suppose que cela dépend de l'implémentation de hachage interne à quel point le temps moyen diverge de O (1) et commence à converger vers O (n). Vous pouvez améliorer les performances de recherche en compartimentant les ensembles globaux en sections plus petites en fonction d'un attribut facilement discernable (comme la valeur du premier chiffre, puis du deuxième, du troisième, etc., aussi longtemps que vous devez obtenir une taille d'ensemble optimale) .
Nisan.H
3
@TorstenMarek Cela me trouble. À partir de cette page , la recherche de liste est O (1) et la recherche de dict est O (n), ce qui est le contraire de ce que vous avez dit. Suis-je mal compris?
temporary_user_name
3
@Aerovistae Je pense que vous avez mal lu les informations sur cette page. Sous liste, je vois O (n) pour "x en s" (recherche). Il montre également la recherche de set et de dict comme cas moyen O (1).
Dennis
45

Un dict est une table de hachage, il est donc très rapide de trouver les clés. Ainsi, entre dict et list, dict serait plus rapide. Mais si vous n'avez pas de valeur à associer, il est encore mieux d'utiliser un ensemble. C'est une table de hachage, sans la partie "table".


EDIT: pour votre nouvelle question, OUI, un ensemble serait mieux. Créez simplement 2 ensembles, un pour les séquences terminées en 1 et l'autre pour les séquences terminées en 89. J'ai résolu avec succès ce problème en utilisant des ensembles.

nosklo
la source
35

set()est exactement ce que vous voulez. O (1) recherches, et plus petit qu'un dict.

récursif
la source
31

J'ai fait quelques analyses comparatives et il s'avère que dict est plus rapide que la liste et défini pour les grands ensembles de données, exécutant python 2.7.3 sur un processeur i7 sous Linux:

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

    10 boucles, le meilleur de 3: 64,2 ms par boucle

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

    10000000 boucles, meilleur de 3: 0,0759 usec par boucle

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

    1000000 boucles, meilleur de 3: 0,262 usec par boucle

Comme vous pouvez le voir, dict est considérablement plus rapide que list et environ 3 fois plus rapide que set. Dans certaines applications, vous voudrez peut-être toujours choisir un ensemble pour sa beauté. Et si les ensembles de données sont vraiment petits (<1000 éléments), les listes fonctionnent plutôt bien.

EriF89
la source
Ne devrait-il pas être exactement le contraire? Liste: 10 * 64.2 * 1000 = 642000 usec, dict: 10000000 * 0.0759 = 759000 usec et set: 1000000 * 0.262 = 262000 usec ... les ensembles sont donc les plus rapides, suivis de la liste et avec dict comme dernier sur votre exemple. Ou est-ce que je manque quelque chose?
andzep
1
... mais la question pour moi est la suivante: que mesurent ces temps? Pas le temps d'accès pour une liste, dict ou ensemble donné, mais bien plus encore, le temps et les boucles pour créer la liste, dict, set et enfin pour trouver et accéder à une valeur. Alors, est-ce que cela a à voir avec la question? ... C'est intéressant cependant ...
andzep
8
@andzep, vous vous trompez, l' -soption est de paramétrer l' timeitenvironnement, c'est à dire qu'il ne compte pas dans le temps total. L' -soption n'est exécutée qu'une seule fois. Sur Python 3.3, j'obtiens ces résultats: gen (plage) -> 0,229 usec, liste -> 157 msec, dict -> 0,0806 usec, set -> 0,0807 usec. Les performances de set et de dict sont les mêmes. Dict prend cependant un peu plus de temps à s'initialiser que défini (temps total 13,580s contre 11,803s)
sleblanc
1
pourquoi ne pas utiliser un ensemble intégré? En fait, j'obtiens des résultats bien pires avec les sets.Set () qu'avec les sets intégrés ()
Thomas Guyot-Sionnest
2
@ ThomasGuyot-Sionnest L'ensemble intégré a été introduit dans python 2.4 donc je ne sais pas pourquoi je ne l'ai pas utilisé dans ma solution proposée. python -mtimeit -s "d=set(range(10**7))" "5*10**6 in d"J'obtiens de bonnes performances avec l' utilisation de Python 3.6.0 (10000000 boucles, le meilleur de 3: 0,0608 usec par boucle), à ​​peu près les mêmes que le benchmark dict alors merci pour votre commentaire.
EriF89
6

Vous voulez un dict.

Pour les listes (non triées) en Python, l'opération "in" nécessite un temps O (n) --- pas bon lorsque vous avez une grande quantité de données. Un dict, en revanche, est une table de hachage, vous pouvez donc vous attendre à un temps de recherche O (1).

Comme d'autres l'ont noté, vous pouvez choisir un ensemble (un type spécial de dict) à la place, si vous n'avez que des clés plutôt que des paires clé / valeur.

En relation:

  • Wiki Python : informations sur la complexité temporelle des opérations de conteneur Python.
  • SO : temps de fonctionnement du conteneur Python et complexités de la mémoire
zweiterlinde
la source
1
Même pour les listes triées, "in" est O (n).
2
Pour une liste chaînée, oui --- mais les "listes" en Python sont ce que la plupart des gens appelleraient des vecteurs, qui fournissent un accès indexé dans O (1) et une opération de recherche dans O (log n), une fois triés.
zweiterlinde
Êtes-vous en train de dire que l' inopérateur appliqué à une liste triée fonctionne mieux que lorsqu'il est appliqué à une liste non triée (pour une recherche d'une valeur aléatoire)? (Je ne pense pas qu'ils soient implémentés en interne en tant que vecteurs ou en tant que nœuds dans une liste chaînée est pertinent.)
martineau
4

si les données sont uniques, set () sera le plus efficace, mais de deux - dict (qui nécessite également unicité, oups :)

SilentGhost
la source
J'ai réalisé quand j'ai vu ma réponse publiée%)
SilentGhost
2
@SilentGhost si la réponse est fausse, pourquoi ne pas la supprimer? tant pis pour les votes positifs, mais ça arrive (enfin, c'est arrivé )
Jean-François Fabre
3

En tant que nouvel ensemble de tests à montrer, @ EriF89 a toujours raison après toutes ces années:

$ 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

Ici, nous comparons également a tuple, qui sont connus pour être plus rapides que lists(et utiliser moins de mémoire) dans certains cas d'utilisation. Dans le cas d'une table de consultation, le tuplecarénage n'est pas meilleur.

Le dictetset ont très bien performé. Cela soulève un point intéressant lié à la réponse @SilentGhost sur l'unicité: si l'OP a 10M de valeurs dans un ensemble de données, et qu'on ne sait pas s'il y a des doublons, alors il vaudrait la peine de garder un ensemble / dict de ses éléments en parallèle avec l'ensemble de données réel, et tester l'existence dans cet ensemble / dict. Il est possible que les 10 millions de points de données n'aient que 10 valeurs uniques, ce qui est un espace beaucoup plus petit pour la recherche!

L'erreur de SilentGhost à propos des dictionnaires est en fait éclairante car on pourrait utiliser un dict pour corréler des données dupliquées (en valeurs) dans un ensemble non dupliqué (clés), et ainsi conserver un objet de données pour contenir toutes les données, tout en restant rapide comme table de recherche. Par exemple, une clé dict pourrait être la valeur recherchée, et la valeur pourrait être une liste d'indices dans une liste imaginaire où cette valeur s'est produite.

Par exemple, si la liste de données source à rechercher était l=[1,2,3,1,2,1,4], elle pourrait être optimisée pour la recherche et la mémoire en la remplaçant par ce 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]})

Avec ce dict, on peut savoir:

  1. Si une valeur était dans l'ensemble de données d'origine (c.-à-d. 2 in dRenvoie True)
  2. la valeur était dans l'ensemble de données d' origine (c. -à- d[2]retourne la liste des indices où les données ont été énumérées dans la liste de données d' origine: [1, 4])
hamx0r
la source
Pour votre dernier paragraphe, bien qu'il soit logique de le lire, il serait bien (et probablement plus facile à comprendre) de voir le code réel que vous essayez d'expliquer.
kaiser
0

Vous n'avez pas vraiment besoin de stocker 10 millions de valeurs dans la table, ce n'est donc pas un gros problème de toute façon.

Astuce: pensez à la taille de votre résultat après la première opération de somme des carrés. Le plus grand résultat possible sera bien inférieur à 10 millions ...

Kiv
la source