web-dev-qa-db-fra.com

Est-ce qu'un Java hashmap est vraiment O (1)?

J'ai vu des affirmations intéressantes sur SO re Java hashmaps et leur O(1)) temps de recherche. Quelqu'un peut-il expliquer pourquoi il en est ainsi? À moins que ces hashmaps ne soient très différents des algorithmes de hachage sur lesquels j'ai été acheté, il doit toujours exister un jeu de données contenant des collisions.

Dans ce cas, la recherche serait O(n) plutôt que O(1).

Quelqu'un peut-il expliquer s’ils are O(1) et, dans l’affirmative, comment y parviennent-ils?

148
paxdiablo

HashMap se caractérise par le fait que contrairement aux arbres équilibrés, par exemple, son comportement est probabiliste. Dans ces cas, il est généralement plus utile de parler de complexité en termes de probabilité d’événement pire. Pour une carte de hachage, il s’agit bien entendu du cas d’une collision par rapport au niveau de remplissage de la carte. Une collision est assez facile à estimer.

pcollision = n/capacité

Ainsi, une carte de hachage comportant même un nombre modeste d’éléments est susceptible de subir au moins une collision. La notation Big O nous permet de faire quelque chose de plus convaincant. Observez cela pour toute constante arbitraire, fixe k.

O(n) = O(k * n)

Nous pouvons utiliser cette fonctionnalité pour améliorer les performances de la carte de hachage. Nous pourrions plutôt penser à la probabilité d'au plus 2 collisions.

pcollision x 2 = (n/capacité)2

C'est beaucoup plus bas. Comme le coût de la gestion d'une collision supplémentaire est sans incidence sur les performances Big O, nous avons trouvé un moyen d'améliorer les performances sans changer réellement l'algorithme! Nous pouvons généraliser cela à

pcollision x k = (n/capacité)k

Et maintenant, nous pouvons ignorer un nombre arbitraire de collisions et aboutir à une probabilité extrêmement infime de multiplier les collisions que nous ne prévoyons. Vous pouvez obtenir la probabilité à un niveau arbitrairement minime en choisissant le k correct, le tout sans modifier l'implémentation réelle de l'algorithme.

Nous en parlons en disant que la table de hachage a O(1) accès avec une probabilité élevée

117

Vous semblez confondre le comportement dans le pire des cas avec le temps d'exécution moyen (attendu). Le premier est en effet O(n) pour les tables de hachage en général (c'est-à-dire n'utilise pas de hachage parfait) mais cela est rarement pertinent dans la pratique.

Toute implémentation de table de hachage fiable, associée à un hachage légèrement décent, a une performance de récupération de O(1) avec un très petit facteur (2, en fait) dans le cas attendu, dans un délai très court. marge de variance étroite.

36
Konrad Rudolph

En Java, HashMap fonctionne en utilisant hashCode pour localiser un compartiment. Chaque compartiment est une liste d'éléments résidant dans ce compartiment. Les articles sont numérisés, en utilisant des égaux pour la comparaison. Lors de l'ajout d'éléments, HashMap est redimensionné dès qu'un certain pourcentage de charge est atteint.

Donc, parfois, il faudra comparer quelques éléments, mais généralement, il est beaucoup plus proche de O(1) que O (n). Pour des raisons pratiques, c'est tout ce que vous devez savoir.

29
FogleBird

N'oubliez pas que o(1) ne signifie pas que chaque recherche n'examine qu'un seul élément. Cela signifie que le nombre moyen d'éléments cochés reste constant par rapport au nombre d'éléments contenus dans le conteneur. prend en moyenne 4 comparaisons pour trouver un article dans un conteneur de 100 articles, mais en moyenne 4 comparaisons pour trouver un article dans un conteneur de 10000 articles, et pour tout autre nombre d'articles (il y a toujours un peu de variance , particulièrement autour des points où la table de hachage est réorganisée et quand il y a un très petit nombre d’articles).

Les collisions n'empêchent donc pas le conteneur d'avoir o(1)) opérations, tant que le nombre moyen de clés par compartiment reste dans une limite fixe.

28
Daniel James

Je sais que c'est une vieille question, mais il y a en fait une nouvelle réponse.

Vous avez raison de dire qu’une carte de hachage n’est pas vraiment O(1) à proprement parler, car à mesure que le nombre d’éléments devient arbitrairement grand, vous ne pourrez éventuellement pas effectuer de recherche à temps constant (et la notation O est définis en termes de nombres pouvant être arbitrairement grands).

Mais il ne s'ensuit pas que la complexité en temps réel est O(n)-- car aucune règle ne dit que les compartiments doivent être implémentés sous forme de liste linéaire.

En fait, Java 8 implémente les compartiments sous la forme TreeMaps une fois qu’ils dépassent un seuil, ce qui donne le temps réel O(log n).

12
ajb

O(1+n/k)k représente le nombre de compartiments.

Si l'implémentation définit k = n/alpha, Il s'agit de O(1+alpha) = O(1) puisque alpha est une constante.

4

Si le nombre de compartiments (appelez-le b) est maintenu constant (cas habituel), la recherche est en fait O (n).
Lorsque n devient grand, le nombre d'éléments dans chaque compartiment est en moyenne n/b. Si la résolution de la collision est effectuée de l’une des manières habituelles (liste liée par exemple), la recherche est alors O(n/b) = O (n).

La notation O concerne ce qui se passe lorsque n devient de plus en plus grand. Cela peut être trompeur lorsqu'il est appliqué à certains algorithmes, et les tables de hachage en sont un exemple. Nous choisissons le nombre de compartiments en fonction du nombre d'éléments auxquels nous nous attendons. Lorsque n a environ la même taille que b, la recherche est à peu près constante, mais nous ne pouvons pas l'appeler O(1) car O est défini en termes de limite égale à n → ∞.

4
I. J. Kennedy

Nous avons établi que la description standard des recherches dans les tables de hachage étant O(1) fait référence à la durée attendue pour le cas moyen, et non à la performance la plus défavorable dans le cas le plus défavorable. enchaînant (comme le hashmap de Java), techniquement, O (1 + α) avec ne bonne fonction de hachage , où α est le facteur de charge de la table. Reste constant tant que le nombre d'objets que vous stockez est pas plus qu'un facteur constant supérieur à la taille de la table.

Il a également été expliqué qu'il était possible à proprement parler de construire une entrée nécessitant des recherches O ( n ) pour toute fonction de hachage déterministe. Mais il est également intéressant de considérer le pire temps attendu , qui est différent du temps de recherche moyen. En utilisant le chaînage c'est O (1 + la longueur de la plus longue chaîne), par exemple (log n /log log n ) lorsque α = 1.

Si vous êtes intéressé par des solutions théoriques pour obtenir les recherches dans le pire des cas, attendues à un temps constant, vous pouvez en savoir plus sur hachage parfait dynamique qui résout les collisions de manière récursive avec une autre table de hachage!

2
jtb

C'est O(1) seulement si votre fonction de hachage est très bonne. L'implémentation Java de la table de hachage ne protège pas contre les mauvaises fonctions de hachage.

Que vous souhaitiez agrandir la table lorsque vous ajoutez des éléments ou non n’est pas pertinent pour la question, car il s’agit de la durée de la recherche.

2
Antti Huima

Les éléments contenus dans HashMap sont stockés sous la forme d'un tableau de liste liée (nœud). Chaque liste liée du tableau représente un compartiment pour la valeur de hachage unique d'une ou de plusieurs clés.
Lors de l'ajout d'une entrée dans HashMap, le hashcode de la clé est utilisé pour déterminer l'emplacement du compartiment dans le tableau, quelque chose comme:

location = (arraylength - 1) & keyhashcode

Ici l'opérateur & représente bitwise AND.

Par exemple: 100 & "ABC".hashCode() = 64 (location of the bucket for the key "ABC")

Pendant l'opération get, il utilise la même manière pour déterminer l'emplacement du compartiment pour la clé. Dans le meilleur des cas, chaque clé a un hashcode unique et génère un compartiment unique pour chaque clé. Dans ce cas, la méthode get consacre du temps uniquement à déterminer l'emplacement du compartiment et à récupérer la valeur qui est constante O (1).

Dans le pire des cas, toutes les clés ont le même hashcode et sont stockées dans le même compartiment, ce qui conduit à parcourir toute la liste, ce qui conduit à O (n).

Dans le cas de Java 8, le compartiment Liste liée est remplacé par un TreeMap si la taille dépasse 8, cela réduit l'efficacité de la recherche dans le pire des cas à O (log n).

2
Ramprabhu

Les universitaires mis à part, d’un point de vue pratique, HashMaps devrait être accepté comme ayant un impact sur les performances sans conséquence (sauf indication contraire de votre profileur).

1
Ryan Emerle

Cela vaut pour la plupart des implémentations de table de hachage dans la plupart des langages de programmation, car l'algorithme lui-même ne change pas vraiment.

S'il n'y a pas de collisions dans la table, vous ne devez effectuer qu'une seule recherche. Le temps d'exécution est donc O (1). S'il y a des collisions, vous devez faire plus d'une recherche, ce qui réduit les performances vers O (n).

1
Tobias Svensson

Cela dépend de l'algorithme que vous choisissez pour éviter les collisions. Si votre implémentation utilise un chaînage séparé, le pire des cas se produit lorsque chaque élément de données est haché à la même valeur (mauvais choix de la fonction de hachage par exemple). Dans ce cas, la recherche de données n’est pas différente d’une recherche linéaire sur une liste chaînée, par exemple O (n). Cependant, la probabilité que cela se produise est négligeable et les recherches dans les cas optimaux et moyens restent constantes, c’est-à-dire O (1).

1
Nizar Grira

Seulement dans les cas théoriques, lorsque les codes de hachage sont toujours différents et que le compartiment pour chaque code de hachage est également différent, le O(1) existera. Sinon, il est d'ordre constant, c'est-à-dire lors de l'incrément de hachage, son ordre de recherche reste constant.

1
sn.anurag

Bien sûr, les performances du hashmap dépendront de la qualité de la fonction hashCode () pour l'objet donné. Cependant, si la fonction est implémentée de telle sorte que le risque de collision soit très faible, les performances seront très bonnes (il ne s’agit pas strictement O(1) dans tous = cas possible mais c'est dans la plupart cas).

Par exemple, l’implémentation par défaut dans JRE Oracle consiste à utiliser un nombre aléatoire (qui est stocké dans l’instance d’objet pour qu’il ne change pas - mais il désactive également le verrouillage biaisé, mais c’est une autre discussion), de sorte que le risque de collision est très lent.

0
Grey Panther