Quelles valeurs dois-je transmettre pour créer une structure efficace basée sur HashMap
/HashMap
pour N éléments?
Dans un ArrayList
, le nombre efficace est N (N suppose déjà une croissance future). Quels devraient être les paramètres d'un HashMap
? ((int) (N * 0,75d), 0,75d)? Plus? Moins? Quel est l'effet de la modification du facteur de charge?
En ce qui concerne le facteur de charge, je citerai simplement le HashMap javadoc :
En règle générale, le facteur de charge par défaut (0,75) offre un bon compromis entre le temps et les coûts d'espace. Des valeurs plus élevées diminuent la surcharge d'espace mais augmentent le coût de recherche (reflété dans la plupart des opérations de la classe HashMap, y compris get et put). Le nombre prévu d'entrées dans la carte et son facteur de charge doivent être pris en compte lors de la définition de sa capacité initiale, afin de minimiser le nombre d'opérations de reprise. Si la capacité initiale est supérieure au nombre maximal d'entrées divisé par le facteur de charge, aucune opération de reprise ne se produira jamais.
Cela signifie que le facteur de charge ne doit pas être modifié de .75
, sauf si vous avez une optimisation spécifique à effectuer. La capacité initiale est la seule chose que vous souhaitez modifier, et définissez-la en fonction de votre valeur N
- c'est-à-dire (N / 0.75) + 1
, ou quelque chose dans ce domaine. Cela garantira que la table sera toujours suffisamment grande et aucun ressassement ne se produira.
J'ai exécuté quelques tests unitaires pour voir si ces réponses étaient correctes et il s'est avéré qu'en utilisant:
(int) Math.ceil(requiredCapacity / loadFactor);
car la capacité initiale donne ce que vous voulez pour un HashMap
ou un Hashtable
. Par "ce que vous voulez", je veux dire que l'ajout d'éléments requiredCapacity
à la carte ne provoquera pas le redimensionnement du tableau qu'il enveloppe et le tableau ne sera pas plus grand que nécessaire. Étant donné que la capacité de charge par défaut est de 0,75, l'initialisation d'un HashMap comme cela fonctionne:
... = new HashMap<KeyType, ValueType>((int) Math.ceil(requiredCapacity / 0.75));
Puisqu'un HashSet n'est en fait qu'un wrapper pour un HashMap, la même logique s'applique également ici, c'est-à-dire que vous pouvez construire un HashSet efficacement comme ceci:
.... = new HashSet<TypeToStore>((int) Math.ceil(requiredCapacity / 0.75));
La réponse de @Yuval Adam est correcte dans tous les cas, sauf lorsque (requiredCapacity / 0.75)
est une puissance de 2, auquel cas il alloue trop de mémoire.
La réponse de @ NotEdible utilise trop de mémoire dans de nombreux cas, car le constructeur de HashMap lui-même traite les problèmes qu'il souhaite que le tableau de cartes ait une taille qui est une puissance de 2.
Dans les bibliothèques de goyaves de Google, il existe une fonction qui crée un HashMap optimisé pour un nombre attendu d'éléments: newHashMapWithExpectedSize
à partir des documents:
Crée une instance HashMap, avec une "capacité initiale" suffisamment élevée pour contenir les éléments attendusTaille sans croissance ...
Il est également à noter que le fait d'avoir un HashMap sur le petit côté rend les collisions de hachage plus probables, ce qui peut ralentir la recherche. Par conséquent, si vous vous inquiétez vraiment de la vitesse de la carte, et moins de sa taille, il pourrait être utile de la rendre un peu trop grande pour les données dont elle a besoin. La mémoire étant bon marché, j'initialise généralement les HashMaps pour un nombre connu d'éléments avec
HashMap<Foo> myMap = new HashMap<Foo>(numberOfElements * 2);
N'hésitez pas à être en désaccord, en fait j'aimerais bien que cette idée soit vérifiée ou rejetée.
La réponse que Yuval a donnée n'est correcte que pour Hashtable. HashMap utilise la puissance de deux seaux, donc pour HashMap, Zarkonnen est en fait correct. Vous pouvez le vérifier à partir du code source:
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
Ainsi, bien que le facteur de charge de 0,75f soit toujours le même entre Hashtable et HashMap, vous devez utiliser une capacité initiale n * 2 où n est le nombre d'éléments que vous prévoyez de stocker dans HashMap. Cela garantira les vitesses get/put les plus rapides.
Se référer au code source de HashMap vous aidera.
Si le nombre d'entrées atteint le seuil (capacité * facteur de charge), le re-hachage se fait automatiquement. Cela signifie qu'un facteur de charge trop petit peut entraîner un ressassement fréquent à mesure que les entrées augmentent.
Dans une ArrayList, le nombre efficace est N (N suppose déjà une croissance future).
Euh, non, à moins que je ne comprenne mal ce que vous dites ici. Lorsque vous passez un entier dans le constructeur Arraylist, il créera un tableau sous-jacent exactement de cette taille. S'il s'avère que vous avez besoin d'un seul élément supplémentaire, ArrayList devra redimensionner le tableau sous-jacent lors de votre prochain appel à add (), ce qui prendra cet appel à prendre beaucoup plus de temps que d'habitude.
Si d'un autre côté vous parlez de votre valeur de N en tenant compte de la croissance - alors oui, si vous pouvez garantir que la valeur ne dépassera jamais cela, alors appeler un tel constructeur Arraylist est approprié. Et dans ce cas, comme l'a souligné Hank, le constructeur analogue d'une carte serait N et 1.0f. Cela devrait fonctionner raisonnablement même si vous excédez N (mais si vous vous attendez à ce que cela se produise régulièrement, vous souhaiterez peut-être passer un plus grand nombre pour la taille initiale).
Le facteur de charge, au cas où vous ne le seriez pas, est le point auquel la carte verra sa capacité augmentée, en tant que fraction de la capacité totale.
Edit : Yuval a probablement raison de penser qu'il vaut mieux laisser le facteur de charge autour de 0,75 pour une carte à usage général. Un facteur de charge de 1,0 fonctionnerait brillamment si vos clés avaient des codes de hachage séquentiels (tels que des clés entières séquentielles), mais pour toute autre chose, vous rencontrerez probablement des collisions avec les compartiments de hachage, ce qui signifie que les recherches prennent plus de temps pour certains éléments. Créer plus de compartiments que ce qui est strictement nécessaire réduira ce risque de collision, ce qui signifie qu'il y a plus de chances que les éléments soient dans leurs propres compartiments et soient ainsi récupérables dans les plus brefs délais. Comme le disent les docs, il s'agit d'un compromis entre le temps et l'espace. Si l'un ou l'autre est particulièrement important pour vous (comme le montre un profileur plutôt que de l'optimiser prématurément!), Vous pouvez le souligner; sinon, respectez la valeur par défaut.
Il est sûr dans la plupart des cas d'initialisation List
et Map
de faire List
ou Map
avec les paramètres de taille suivants.
List<T>(numElements + (numElements / 2));
Map<T,T>(numElements + (numElements / 2));
cela suit la .75 règle ainsi que permet d'économiser un peu de frais généraux sur la * 2
opération décrite ci-dessus.
Pour les très grands HashMaps dans les systèmes critiques, où le mauvais calcul de la capacité initiale peut être très problématique, vous devrez peut-être des informations empiriques pour déterminer la meilleure façon d'initialiser votre carte.
CollectionSpy ( collectionspy.com ) est un nouveau Java profiler qui vous permet de voir en un clin d'œil quels HashMaps sont sur le point de devoir être ressassés, combien de fois ils ont a été remanié dans le passé, et plus encore. Un outil idéal pour déterminer des arguments de capacité initiale sûrs pour les constructeurs de conteneurs basés sur la capacité.