Comment set () est-il implémenté?

152

J'ai vu des gens dire que les setobjets en python ont une vérification d'appartenance O (1). Comment sont-ils mis en œuvre en interne pour permettre cela? Quel type de structure de données utilise-t-il? Quelles autres implications cette mise en œuvre a-t-elle?

Chaque réponse ici était vraiment éclairante, mais je ne peux en accepter qu'une, donc je vais aller avec la réponse la plus proche de ma question initiale. Merci à tous pour l'info!

Daenyth
la source

Réponses:

139

Selon ce fil :

En effet, les ensembles de CPython sont implémentés comme quelque chose comme des dictionnaires avec des valeurs factices (les clés étant les membres de l'ensemble), avec quelques optimisations qui exploitent ce manque de valeurs

Donc, fondamentalement, a setutilise une table de hachage comme structure de données sous-jacente. Cela explique la vérification de l'appartenance à O (1), car la recherche d'un élément dans une table de hachage est une opération O (1), en moyenne.

Si vous êtes si enclin, vous pouvez même parcourir le code source de CPython pour l'ensemble qui, selon Achim Domma , est principalement un copier-coller de l' dictimplémentation.

Justin Ethier
la source
18
IIRC, l' setimplémentation d' origine était en faitdict avec des valeurs factices, et elle a été optimisée plus tard.
dan04
1
Big O n'est-il pas le pire des scénarios? Si vous pouvez trouver une instance où le temps est O (n), alors c'est O (n) .. Je ne comprends rien pour l'instant de tous ces tutoriels.
Claudiu Creanga
4
Non, le cas moyen est O (1) mais le pire des cas est O (N) pour la recherche de table de hachage.
Justin Ethier le
4
@ClaudiuCreanga c'est un vieux commentaire, mais juste pour clarifier: la notation big-O vous indique les limites supérieures du taux de croissance des choses, mais vous pouvez limiter la croissance de la performance moyenne des cas et vous pouvez séparer la borne supérieure de la croissance du pire des cas performance.
Kirk Boyer
79

Quand les gens disent que les ensembles ont une vérification d'appartenance O (1), ils parlent du cas moyen . Dans le pire des cas (lorsque toutes les valeurs hachées entrent en collision), la vérification d'appartenance est O (n). Voir le wiki Python sur la complexité du temps .

L' article de Wikipédia indique que la meilleure complexité temporelle pour une table de hachage qui ne se redimensionne pas est O(1 + k/n). Ce résultat ne s'applique pas directement aux ensembles Python car les ensembles Python utilisent une table de hachage qui se redimensionne.

Un peu plus loin, l'article de Wikipedia dit que pour le cas moyen , et en supposant une simple fonction de hachage uniforme, la complexité temporelle est O(1/(1-k/n)), où k/npeut être délimitée par une constante c<1.

Big-O se réfère uniquement au comportement asymptotique comme n → ∞. Puisque k / n peut être borné par une constante, c <1, indépendante de n ,

O(1/(1-k/n))n'est pas plus grand que O(1/(1-c))ce qui équivaut à O(constant)= O(1).

Donc, en supposant un hachage simple et uniforme, en moyenne , la vérification d'appartenance pour les ensembles Python est O(1).

unutbu
la source
14

Je pense que c'est une erreur courante, la setrecherche (ou la table de hachage d'ailleurs) ne sont pas O (1).
sur Wikipédia

Dans le modèle le plus simple, la fonction de hachage est totalement non spécifiée et la table ne se redimensionne pas. Pour le meilleur choix possible de la fonction de hachage, une table de taille n avec adressage ouvert n'a pas de collisions et contient jusqu'à n éléments, avec une seule comparaison pour une recherche réussie, et une table de taille n avec chaînage et k clés a le minimum max. (0, kn) collisions et O (1 + k / n) comparaisons pour la recherche. Pour le pire choix de fonction de hachage, chaque insertion provoque une collision et les tables de hachage dégénèrent en recherche linéaire, avec Ω (k) comparaisons amorties par insertion et jusqu'à k comparaisons pour une recherche réussie.

Connexes: Un hashmap Java est-il vraiment O (1)?

Shay Erlichmen
la source
4
Mais ils prennent un temps constant pour rechercher des éléments: python -m timeit -s "s = set (range (10))" "5 in s" 10000000 boucles, le meilleur de 3: 0,0642 usec par boucle <--> python - m timeit -s "s = set (range (10000000))" "5 in s" 10000000 boucles, le meilleur de 3: 0,0634 usec par boucle ... et c'est le plus grand ensemble qui ne lance pas MemoryErrors
Jochen Ritzel
2
@ THC4k Tout ce que vous avez prouvé, c'est que la recherche de X se fait en temps constant, mais cela ne signifie pas que le temps de recherche de X + Y prendra le même temps, ce qui correspond à O (1).
Shay Erlichmen
3
@intuited: Oui, mais le test ci-dessus ne prouve pas que vous pouvez rechercher "5" en même temps que vous pouvez rechercher "485398", ou un autre nombre qui pourrait se trouver dans un horrible espace de collision. Il ne s'agit pas de rechercher le même élément dans un hachage de taille différente dans le même temps (en fait, ce n'est pas du tout nécessaire), mais plutôt de savoir si vous pouvez accéder à chaque entrée dans le même laps de temps dans la table actuelle - quelque chose qui est fondamentalement impossible pour les tables de hachage car il y aura généralement toujours des collisions.
Nick Bastin
3
En d'autres termes, le temps nécessaire pour effectuer une recherche dépend du nombre de valeurs stockées, car cela augmente la probabilité de collisions.
intuitu
3
@intuited: non, c'est incorrect. Lorsque le nombre de valeurs stockées augmente, Python augmente automatiquement la taille de la table de hachage et le taux de collision reste à peu près constant. En supposant un algorithme de hachage O (1) uniformément réparti, la recherche de table de hachage est amortie O (1). Vous voudrez peut-être regarder la présentation vidéo "The Mighty Dictionary" python.mirocommunity.org/video/1591/…
Lie Ryan
13

Nous avons tous un accès facile à la source , où le commentaire précédent set_lookkey()dit:

/* set object implementation
 Written and maintained by Raymond D. Hettinger <[email protected]>
 Derived from Lib/sets.py and Objects/dictobject.c.
 The basic lookup function used by all operations.
 This is based on Algorithm D from Knuth Vol. 3, Sec. 6.4.
 The initial probe index is computed as hash mod the table size.
 Subsequent probe indices are computed as explained in Objects/dictobject.c.
 To improve cache locality, each probe inspects a series of consecutive
 nearby entries before moving on to probes elsewhere in memory.  This leaves
 us with a hybrid of linear probing and open addressing.  The linear probing
 reduces the cost of hash collisions because consecutive memory accesses
 tend to be much cheaper than scattered probes.  After LINEAR_PROBES steps,
 we then use open addressing with the upper bits from the hash value.  This
 helps break-up long chains of collisions.
 All arithmetic on hash should ignore overflow.
 Unlike the dictionary implementation, the lookkey function can return
 NULL if the rich comparison returns an error.
*/


...
#ifndef LINEAR_PROBES
#define LINEAR_PROBES 9
#endif

/* This must be >= 1 */
#define PERTURB_SHIFT 5

static setentry *
set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash)  
{
...
gimel
la source
2
Cette réponse bénéficierait de C la mise en évidence de . La mise en évidence de la syntaxe Python du commentaire semble vraiment mauvaise.
user202729
En ce qui concerne le commentaire "Cela nous laisse avec un hybride de sondage linéaire et d'adressage ouvert", le sondage linéaire n'est-il pas une sorte de résolution de collision en adressage ouvert, comme décrit dans en.wikipedia.org/wiki/Open_addressing ? Par conséquent, le sondage linéaire est un sous-type d'adressage ouvert et le commentaire n'a aucun sens.
Alan Evangelista le
2

Pour souligner un peu plus la différence entre set'set dict's, voici un extrait des setobject.csections de commentaires, qui clarifie la principale différence entre les sets et les dictionnaires.

Les cas d'utilisation des ensembles diffèrent considérablement des dictionnaires où les clés recherchées sont plus susceptibles d'être présentes. En revanche, les ensembles concernent principalement les tests d'appartenance où la présence d'un élément n'est pas connue à l'avance. En conséquence, l'implémentation d'ensemble doit être optimisée pour le cas trouvé et non trouvé.

source sur github

user1767754
la source