web-dev-qa-db-fra.com

HashMap obtenir / mettre la complexité

Nous avons l'habitude de dire que HashMapget/put opérations sont O (1). Cependant, cela dépend de la mise en oeuvre du hachage. Le hachage d'objet par défaut est en réalité l'adresse interne du segment de mémoire de la machine virtuelle Java. Sommes-nous sûrs qu'il est assez bon de prétendre que le get/put sont O(1)?

La mémoire disponible est un autre problème. Si je comprends bien les javadocs, le HashMapload factor devrait être 0,75. Que se passe-t-il si nous n’avons pas assez de mémoire dans la machine virtuelle Java et le load factor dépasse la limite?

Donc, il semble que O(1) n'est pas garanti. Est-ce que cela a du sens ou est-ce que je manque quelque chose?

112
Michael

Cela dépend de beaucoup de choses. C'est généralement O (1), avec un hachage décent qui est lui-même un temps constant ... mais vous pourriez avoir un hachage qui prend beaucoup de temps à calculer, et s'il y a Plusieurs éléments de la mappe de hachage qui renvoient le même code de hachage, get devra itérer dessus en appelant equals sur chacun d'eux pour trouver une correspondance.

Dans le pire des cas, un HashMap a une recherche O(n)) en raison du parcours de toutes les entrées dans le même panier de hachage (par exemple, si elles ont toutes le même code de hachage) Heureusement, selon mon expérience, le pire scénario ne se présente pas souvent dans la vie réelle. Donc non, O(1) n'est certainement pas garanti - mais c'est généralement ce que vous devriez assumer lors de l'examen quels algorithmes et structures de données à utiliser.

Dans JDK 8, HashMap a été modifié de sorte que, si les clés peuvent être comparées pour la commande, tous les compartiments densément peuplés sont implémentés sous forme d'arborescence, de sorte que même s'il existe de nombreuses entrées avec le même code de hachage, la complexité est O (log n). Cela peut poser problème si vous avez un type de clé où égalité et ordre sont différents, bien sûr.

Et oui, si vous n'avez pas assez de mémoire pour la carte de hachage, vous aurez des problèmes ... mais cela va être vrai quelle que soit la structure de données que vous utilisez.

194
Jon Skeet

Je ne suis pas sûr que le hashcode par défaut soit l'adresse. J'ai lu le code source OpenJDK pour la génération de hashcode il y a quelque temps, et je me souviens que c'était quelque chose d'un peu plus compliqué. Pas encore quelque chose qui garantit une bonne distribution, peut-être. Cependant, c'est dans une certaine mesure discutable, car peu de classes que vous utiliseriez comme clés dans un hashmap utilisent le hashcode par défaut - elles fournissent leurs propres implémentations, ce qui devrait être bon.

En plus de cela, ce que vous ne savez peut-être pas (encore une fois, cela est basé sur la source de lecture - ce n’est pas garanti), c’est que HashMap agite le hachage avant de l’utiliser, pour mélanger l’entropie de tout le mot dans les bits du bas, où se trouve nécessaire pour tous sauf le plus gros hashmaps. Cela aide à gérer les hashes qui ne le font pas spécifiquement eux-mêmes, bien que je ne puisse pas penser à des cas courants où vous verriez cela.

Enfin, lorsque la table est surchargée, elle dégénère en un ensemble de listes chaînées parallèles - la performance devient O (n). Plus précisément, le nombre de liens traversés sera en moyenne égal à la moitié du facteur de charge.

9
Tom Anderson

Il a déjà été mentionné que les hashmaps sont O(n/m) en moyenne, si n est le nombre d'éléments et m est la taille. Il a également été mentionné qu'en principe, le tout pourrait être réduit à une liste à lien unique avec O(n) temps de requête. (Tout cela suppose que le calcul du hachage est un temps constant).

Cependant, ce qui n'est pas souvent mentionné, c'est qu'avec une probabilité d'au moins 1-1/n (Donc pour 1000 éléments, une probabilité de 99,9%), le plus grand compartiment ne sera pas rempli plus que O(logn)! Par conséquent, la correspondance avec la complexité moyenne des arbres de recherche binaires. (Et la constante est bonne, une limite plus étroite est (log n)*(m/n) + O(1)).

Tout ce qui est nécessaire pour cette liaison théorique est que vous utilisiez une fonction de hachage relativement bonne (voir Wikipedia: niversal Hashing . Cela peut être aussi simple que a*x>>m). Et bien sûr, la personne qui vous donne les valeurs de hash ne sait pas comment vous avez choisi vos constantes aléatoires.

TL; DR: avec une probabilité très élevée, le pire des cas pour la complexité d'obtenir/mettre une table de hachage est O(logn).

8
Thomas Ahle

L'opération HashMap est un facteur dépendant de l'implémentation de hashCode. Pour le scénario idéal, disons que la bonne implémentation de hachage fournit un code de hachage unique pour chaque objet (pas de collision de hachage), alors le meilleur, le pire et le moyen des scénarios serait O (1). Prenons un scénario dans lequel une mauvaise implémentation de hashCode renvoie toujours 1 ou un hachage de ce type ayant une collision de hachage. Dans ce cas, la complexité temporelle serait O (n).

Pour en venir à la deuxième partie de la question sur la mémoire, alors oui, la contrainte de mémoire serait prise en charge par la JVM.

7
Pranav

Je suis d'accord avec:

  • la complexité générale amortie de O (1)
  • une mauvaise implémentation de hashCode() peut entraîner plusieurs collisions, ce qui signifie que dans le pire des cas, chaque objet est dirigé vers le même compartiment, donc O ( [~ # ~] n [ ~ # ~] ) si chaque compartiment est sauvegardé par un List.
  • since Java 8 HashMap remplace dynamiquement les noeuds (liste liée) utilisés dans chaque compartiment avec TreeNodes (arbre rouge-noir lorsqu'une liste dépasse 8 éléments), ce qui donne lieu à un pire performances de O ( logN ).

Mais ceci n'est PAS la vérité si nous voulons être précis à 100%. L'implémentation de hashCode(), le type de clé Object (immuable/mis en cache ou constituant une collection) peut également affecter la complexité réelle de manière stricte.

Supposons les trois cas suivants:

  1. HashMap<Integer, V>
  2. HashMap<String, V>
  3. HashMap<List<E>, V>

Ont-ils la même complexité? Eh bien, la complexité amortie du premier est, comme prévu, O (1). Mais pour le reste, nous devons également calculer hashCode() de l'élément de recherche, ce qui signifie que nous pourrions devoir parcourir des tableaux et des listes dans notre algorithme.

Supposons que la taille de tous les tableaux/listes ci-dessus est égale à k . Ensuite, HashMap<String, V> Et HashMap<List<E>, V> Auront O(k) complexité amortie et pareillement, O ( k + logN ) pire des cas en Java8.

* Notez que l'utilisation d'une touche String est un cas plus complexe car elle est immuable et Java met en cache le résultat de hashCode() dans une variable privée hash, ce n'est donc calculé qu'une fois.

/** Cache the hash code for the string */
    private int hash; // Default to 0

Mais ce qui précède a également son propre pire cas, car la mise en œuvre de la fonction String.hashCode() de Java vérifie si hash == 0 Avant de calculer hashCode. Mais bon, il y a des chaînes non vides qui génèrent un hashcode égal à zéro, tel que "f5a5a608", voir ici , auquel cas la mémorisation peut ne pas être utile.

3
Kostas Chalkias

En pratique, il s’agit de O (1), mais c’est une simplification terrible et mathématiquement dénuée de sens. La notation O() indique comment l'algorithme se comporte lorsque la taille du problème tend vers l'infini. Hashmap get/put fonctionne comme un algorithme O(1) pour une taille limitée. La limite est assez grande de la mémoire de l'ordinateur et du point de vue de l'adressage, mais loin de l'infini.

Quand on dit que hashmap get/put est O(1), il faut vraiment dire que le temps nécessaire pour obtenir/mettre est plus ou moins constant et ne dépend pas du nombre d'éléments dans le hashmap autant que le hashmap peut être présenté sur le système informatique réel. Si le problème dépasse cette taille et que nous avons besoin de hashmaps plus grands, après un certain temps, le nombre de bits décrivant un élément augmentera également à mesure que nous aurons épuisé les différents éléments descriptibles possibles. Par exemple, si nous utilisons une table de hachage pour stocker des nombres 32 bits et que nous augmentons ensuite la taille du problème de manière à avoir plus de 2 ^ 32 éléments dans la table de hachage, les éléments individuels seront décrits avec plus de 32 bits.

Le nombre de bits nécessaires pour décrire les éléments individuels est log (N), où N est le nombre maximal d'éléments. Par conséquent, get et put sont vraiment O (log N).

Si vous le comparez à un ensemble d'arbres, qui est O (log n), alors le hachage est défini à O(long(max(n)) et nous pensons simplement qu'il s'agit de O (1), car certaine implémentation max (n) est fixe, ne change pas (la taille des objets que nous stockons est mesurée en bits) et l’algorithme de calcul du code de hachage est rapide.

Enfin, si la recherche d'un élément dans une structure de données était O(1), nous créerions des informations à partir de rien. Ayant une structure de données de n élément, je peux sélectionner un élément de n manière différente. Avec cela, je peux encoder les informations de log (n) bits. Si je peux encoder cela en bit zéro (c'est ce que signifie O(1)), alors j'ai créé un algorithme Zip à compression infinie.

2
Peter Verhas