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


159

J'ai vu des revendications intéressantes sur les hashmaps SO re Java 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 de l'un des algorithmes de hachage sur lesquels j'ai été acheté, il doit toujours exister un ensemble 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 sont O (1) et, si oui, comment ils y parviennent?


1
Je sais que ce n'est peut-être pas une réponse, mais je me souviens que Wikipédia a un très bon article à ce sujet. Ne manquez pas la section d' analyse des performances
victor hugo

28
La notation Big O donne une limite supérieure pour le type particulier d'analyse que vous effectuez. Vous devez toujours spécifier si vous êtes intéressé par le pire des cas, le cas moyen, etc.
Dan Homerick

Réponses:


127

Une caractéristique particulière d'un HashMap est que contrairement, disons, aux arbres équilibrés, son comportement est probabiliste. Dans ces cas, il serait généralement plus utile de parler de complexité en termes de probabilité qu'un événement du pire des cas se produise. Pour une carte de hachage, c'est bien sûr le cas d'une collision par rapport au niveau de remplissage de la carte. Une collision est assez facile à estimer.

p collision = n / capacité

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

O (n) = O (k * n)

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

p collision x 2 = (n / capacité) 2

C'est beaucoup plus bas. Étant donné que le coût de gestion d'une collision supplémentaire n'est pas pertinent pour les performances de Big O, nous avons trouvé un moyen d'améliorer les performances sans réellement changer l'algorithme! Nous pouvons généraliser ceci pour

p collision xk = (n / capacité) k

Et maintenant, nous pouvons ignorer un certain nombre arbitraire de collisions et nous retrouver avec une probabilité minime de plus de collisions que ce que nous comptons. Vous pouvez obtenir la probabilité à un niveau arbitrairement minuscule en choisissant le k correct, le tout sans modifier l'implémentation réelle de l'algorithme.

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


Même avec HTML, je ne suis toujours pas vraiment satisfait des fractions. Nettoyez-les si vous pouvez penser à une bonne façon de le faire.
SingleNegationElimination

4
En fait, ce que dit ce qui précède, c'est que les effets O (log N) sont enterrés, pour des valeurs non extrêmes de N, par le surcoût fixe.
Hot Licks

Techniquement, ce nombre que vous avez donné est la valeur attendue du nombre de collisions, qui peut être égale à la probabilité d'une seule collision.
Simon Kuang

1
Est-ce similaire à une analyse amortie?
lostsoul29

1
@ OleV.V. les bonnes performances d'un HashMap dépendent toujours d'une bonne distribution de votre fonction de hachage. Vous pouvez échanger une meilleure qualité de hachage contre une vitesse de hachage en utilisant une fonction de hachage cryptographique sur votre entrée.
SingleNegationElimination

38

Vous semblez confondre le comportement du 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'utilisant pas un hachage parfait) mais cela est rarement pertinent en pratique.

Toute implémentation de table de hachage fiable, associée à un hachage à moitié 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 une marge de variance très étroite.


6
J'ai toujours pensé que la limite supérieure était le pire des cas, mais il semble que je me suis trompé - vous pouvez avoir la limite supérieure pour le cas moyen. Il semble donc que les personnes affirmant O (1) auraient dû indiquer clairement que c'était pour un cas moyen. Le pire des cas est un ensemble de données dans lequel de nombreuses collisions le rendent O (n). Cela a du sens maintenant.
paxdiablo

2
Vous devriez probablement préciser que lorsque vous utilisez la grande notation O pour le cas moyen, vous parlez d'une limite supérieure sur la fonction d'exécution attendue qui est une fonction mathématique clairement définie. Sinon, votre réponse n'a pas beaucoup de sens.
ldog le

1
gmatt: Je ne suis pas sûr de comprendre votre objection: la notation big-O est une limite supérieure de la fonction par définition . Que pourrais-je donc dire d'autre?
Konrad Rudolph

3
En général, dans la littérature informatique, vous voyez une grande notation O représentant une limite supérieure sur les fonctions d'exécution ou de complexité spatiale d'un algorithme. Dans ce cas, la limite supérieure est en fait sur l'espérance qui n'est elle-même pas une fonction mais un opérateur sur des fonctions (variables aléatoires) et est en fait une intégrale (lebesgue.) Le fait même que vous puissiez lier une telle chose ne doit pas être pris pour acquis et n'est pas anodin.
ldog

31

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 éléments sont scannés, en utilisant des égaux pour la comparaison. Lors de l'ajout d'éléments, le HashMap est redimensionné une fois qu'un certain pourcentage de charge est atteint.

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


11
Eh bien, puisque big-O est censé spécifier les limites, cela ne fait aucune différence qu'il soit plus proche de O (1) ou non. Même O (n / 10 ^ 100) est toujours O (n). Je comprends votre point de vue sur l'efficacité en réduisant le rapport, mais cela place toujours l'algorithme à O (n).
paxdiablo

4
L'analyse des hashtags se fait généralement sur le cas moyen, qui est O (1) (avec collusions) Dans le pire des cas, vous pouvez avoir O (n), mais ce n'est généralement pas le cas. concernant la différence - O (1) signifie que vous obtenez le même temps d'accès quelle que soit la quantité d'éléments sur le graphique, et c'est généralement le cas (tant qu'il y a une bonne proportion entre la taille du tableau et 'n ')
Liran Orevi

4
Il est également intéressant de noter que c'est toujours exactement O (1), même si l'analyse du seau prend un certain temps car il contient déjà des éléments. Tant que les seaux ont une taille maximale fixe, il s'agit simplement d'un facteur constant sans rapport avec la classification O (). Mais bien sûr, il peut y avoir encore plus d'éléments avec des clés «similaires», de sorte que ces buckets débordent et que vous ne pouvez plus garantir une constante.
sth

@sth Pourquoi les seaux auraient-ils jamais une taille maximale fixe !?
Navin

31

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 vérifiés reste constant par rapport au nombre d'éléments dans le conteneur. Donc, s'il faut en moyenne 4 comparaisons pour trouver un article dans un conteneur de 100 articles, il faut également 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, en particulier autour des points auxquels la table de hachage se répète, et lorsqu'il y a un très petit nombre d'éléments).

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


16

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

Vous avez raison, une carte de hachage n'est pas vraiment O(1) , à proprement parler, car comme le nombre d'éléments devient arbitrairement grand, vous ne pourrez finalement pas rechercher en temps constant (et la notation O est définie en termes de nombres qui peuvent devenir arbitrairement grand).

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

En fait, Java 8 implémente les buckets comme TreeMapsune fois qu'ils dépassent un seuil, ce qui rend l'heure réelle O(log n).


4

Si le nombre de compartiments (appelez-le b) est maintenu constant (le cas habituel), alors la recherche est en fait O (n).
Lorsque n devient grand, le nombre d'éléments dans chaque compartiment est en moyenne de n / b. Si la résolution de collision est effectuée de l'une des manières habituelles (liste chaînée par exemple), alors la recherche est 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 bon exemple. Nous choisissons le nombre de seaux en fonction du nombre d'éléments que nous prévoyons de traiter. Lorsque n est à peu près de la même taille que b, alors la recherche est à peu près en temps constant, mais nous ne pouvons pas l'appeler O (1) car O est défini en termes de limite comme n → ∞.



2

Nous avons établi que la description standard des recherches de table de hachage étant O (1) se réfère au temps moyen prévu dans le cas, pas aux performances strictes dans le pire des cas. Pour une table de hachage résolvant des collisions avec chaînage (comme la table de hachage de Java), c'est techniquement O (1 + α) avec une bonne fonction de hachage , où α est le facteur de charge de la table. Toujours constant tant que le nombre d'objets que vous stockez ne dépasse pas un facteur constant supérieur à la taille de la table.

Il a également été expliqué qu'à proprement parler, il est possible de construire une entrée qui nécessite des recherches O ( n ) pour toute fonction de hachage déterministe. Mais il est également intéressant de prendre en compte le temps prévu le plus défavorable , 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 moyens théoriques pour obtenir des recherches dans le pire des cas attendus à temps constant, vous pouvez en savoir plus sur le hachage dynamique parfait qui résout les collisions de manière récursive avec une autre table de hachage!


2

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

Que vous ayez besoin d'agrandir la table lorsque vous ajoutez des éléments ou non n'est pas pertinent pour la question car il s'agit du temps de recherche.


2

Les éléments à l'intérieur du HashMap sont stockés sous forme de 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 plusieurs clés.
Lors de l'ajout d'une entrée dans le 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, le & représente l'opérateur AND au niveau du bit.

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

Pendant l'opération get, il utilise la même méthode pour déterminer l'emplacement du compartiment pour la clé. Dans le meilleur des cas, chaque clé a un hashcode unique et aboutit à un compartiment unique pour chaque clé.Dans ce cas, la méthode get passe du temps uniquement à déterminer l'emplacement du compartiment et à récupérer la valeur qui est la 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 bucket, cela se traduit par une traversée de la liste entière qui mène à O (n).

Dans le cas de java 8, le compartiment Linked List est remplacé par un TreeMap si la taille augmente à plus de 8, cela réduit l'efficacité de la recherche dans le pire des cas à O (log n).


1

Cela vaut essentiellement 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 présentes dans la table, vous n'avez qu'à faire une seule recherche, donc le temps d'exécution est O (1). S'il y a des collisions, vous devez effectuer plus d'une recherche, ce qui réduit les performances vers O (n).


1
Cela suppose que le temps d'exécution est limité par le temps de recherche. Dans la pratique, vous trouverez de nombreuses situations où la fonction de hachage fournit la limite (String)
Stephan Eggermont

1

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 où 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, c'est-à-dire O (n). Cependant, la probabilité que cela se produise est négligeable et les cas de recherche meilleurs et moyens restent constants, c'est-à-dire O (1).


1

Les universitaires mis à part, d'un point de vue pratique, les HashMaps devraient être acceptés comme ayant un impact sans conséquence sur les performances (à moins que votre profileur ne vous dise le contraire).


4
Pas dans les applications pratiques. Dès que vous utilisez une chaîne comme clé, vous remarquerez que toutes les fonctions de hachage ne sont pas idéales et que certaines sont très lentes.
Stephan Eggermont

1

Seulement dans le cas théorique, 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 ie sur incrément de hashmap, son ordre de recherche reste constant.


0

Bien entendu, 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 la possibilité de collisions est très faible, elle aura de très bonnes performances (ce n'est pas strictement O (1) dans tous les cas possibles mais c'est dans la plupart cas).

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


"c'est dans la plupart des cas". Plus précisément, le temps total tendra vers K fois N (où K est constant) lorsque N tendra vers l'infini.
ChrisW

7
C'est faux. L'index dans la table de hachage va être déterminé via hashCode % tableSizece qui signifie qu'il peut certainement y avoir des collisions. Vous n'utilisez pas pleinement le 32 bits. C'est un peu le but des tables de hachage ... vous réduisez un grand espace d'indexation à un petit.
FogleBird

1
"vous avez la garantie qu'il n'y aura pas de collisions" Non, vous ne l'êtes pas car la taille de la carte est plus petite que la taille du hachage: par exemple si la taille de la carte est de deux, alors une collision est garantie (peu importe ce que le hachage) si / quand j'essaye d'insérer trois éléments.
ChrisW

Mais comment convertir une clé en adresse mémoire dans O (1)? Je veux dire comme x = array ["clé"]. La clé n'est pas l'adresse de la mémoire, il faudrait donc toujours une recherche O (n).
paxdiablo

1
"Je crois que si vous n'implémentez pas hashCode, il utilisera l'adresse mémoire de l'objet". Il pourrait l'utiliser, mais le hashCode par défaut pour Oracle Java standard est en fait un nombre aléatoire de 25 bits stocké dans l'en-tête de l'objet, donc 64/32 bits n'a aucune conséquence.
Boann
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.