Les gens peuvent-ils recommander des moyens simples et rapides pour combiner les codes de hachage de deux objets. Je ne m'inquiète pas trop des collisions car j'ai une table de hachage qui gérera cela efficacement. Je veux juste quelque chose qui génère un code le plus rapidement possible.
En lisant autour de SO et du Web, il semble y avoir quelques candidats principaux:
Que recommanderaient les gens et pourquoi?
Personnellement, je voudrais éviter XOR - cela signifie que deux valeurs égales auront pour résultat 0 - donc hash (1, 1) == hash (2, 2) == hash (3, 3) etc. 5, 0) == hash (0, 5), etc., qui peuvent apparaître de temps en temps. Je l'ai utilisé délibérément pour le hachage d'ensemble - si vous souhaitez hacher une séquence d'éléments et que vous ne le faites pas vous souciez de l'ordre, c'est Nice.
J'utilise habituellement:
unchecked
{
int hash = 17;
hash = hash * 31 + firstField.GetHashCode();
hash = hash * 31 + secondField.GetHashCode();
return hash;
}
C'est la forme suggérée par Josh Bloch dans Effective Java. La dernière fois que j'ai répondu à une question similaire, j'ai réussi à trouver un article où il a été discuté en détail - IIRC, personne ne sait vraiment pourquoi cela fonctionne bien, mais c'est le cas. Il est également facile à retenir, facile à mettre en œuvre et à étendre à un grand nombre de domaines.
Alors que le modèle décrit dans la réponse de Jon Skeet fonctionne bien en général en tant que famille de fonctions de hachage, le choix des constantes est important et les valeurs de départ de 17
et du facteur 31
, comme indiqué dans la réponse, ne fonctionnent pas du tout pour les cas d'utilisation courants. Dans la plupart des cas d'utilisation, les valeurs hachées sont beaucoup plus proches de zéro que int.MaxValue
et le nombre d'éléments copiés conjointement est de quelques dizaines ou moins.
Pour le hachage d'un tuple entier {x, y}
où -1000 <= x <= 1000
et -1000 <= y <= 1000
, le taux de collision est abyssal de près de 98,5%. Par exemple, {1, 0} -> {0, 31}
, {1, 1} -> {0, 32}
, etc. Si nous élargissons la couverture pour inclure également des n-uplets où 3 <= n <= 25
, le résultat est moins terrible, avec un taux de collision d'environ 38%. Mais nous pouvons faire beaucoup mieux.
public static int CustomHash(int seed, int factor, params int[] vals)
{
int hash = seed;
foreach (int i in vals)
{
hash = (hash * factor) + i;
}
return hash;
}
J'ai écrit une boucle de recherche d'échantillonnage de Monte Carlo qui testait la méthode ci-dessus avec différentes valeurs pour les valeurs de départ et de facteur sur divers n-uplets aléatoires d'entiers aléatoires i
. Les plages autorisées étaient 2 <= n <= 25
(où n
était aléatoire mais biaisé vers le bas de la plage) et -1000 <= i <= 1000
. Au moins 12 millions de tests de collision uniques ont été effectués pour chaque paire de semences et de facteurs.
Après environ 7 heures de fonctionnement, la meilleure paire trouvée (où la valeur de départ et le facteur étaient limités à 4 chiffres ou moins) était: seed = 1009
, factor = 9176
, avec un taux de collision de 0,1131%. Dans les zones à 5 et 6 chiffres, des options encore meilleures existent. Mais j’ai choisi le plus performant à 4 chiffres pour la brièveté, et il s’affiche assez bien dans tous les scénarios de hachage int
et char
courants. Cela semble également fonctionner correctement avec des nombres entiers de magnitudes beaucoup plus grandes.
Il convient de noter qu'être "primordial" ne semblait pas être une condition préalable générale pour obtenir de bons résultats en tant que graine et/ou facteur, bien que cela aide probablement. 1009
noté ci-dessus est en fait premier, mais 9176
ne l’est pas. J'ai explicitement testé des variantes de ce modèle dans lesquelles j'ai changé factor
en différents nombres premiers proches de 9176
(tout en laissant seed = 1009
) et ils ont tous moins bien performé que la solution ci-dessus.
Enfin, j'ai également comparé la famille de fonctions de recommandation générique ReSharper de hash = (hash * factor) ^ i;
et la fonction CustomHash()
d'origine, comme indiqué ci-dessus, qui la surclassent considérablement. Le style ReSharper XOR semble avoir des taux de collision compris entre 20 et 30% pour les hypothèses de cas d'utilisation courantes et ne devrait pas être utilisé à mon avis.
Je présume que l’équipe .NET Framework a fait un travail décent en testant leur implémentation System.String.GetHashCode () , je l’utiliserais donc:
// System.String.GetHashCode(): http://referencesource.Microsoft.com/#mscorlib/system/string.cs,0a17bbac4851d0d4
// System.Web.Util.StringUtil.GetStringHashCode(System.String): http://referencesource.Microsoft.com/#System.Web/Util/StringUtil.cs,c97063570b4e791a
public static int CombineHashCodes(IEnumerable<int> hashCodes)
{
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
int i = 0;
foreach (var hashCode in hashCodes)
{
if (i % 2 == 0)
hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ hashCode;
else
hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ hashCode;
++i;
}
return hash1 + (hash2 * 1566083941);
}
Une autre implémentation est issue de System.Web.Util.HashCodeCombiner.CombineHashCodes (System.Int32, System.Int32) et System.Array.CombineHashCodes (System.Int32, System.Int32) méthodes. Celui-ci est plus simple, mais n'a probablement pas une aussi bonne distribution que la méthode ci-dessus:
// System.Web.Util.HashCodeCombiner.CombineHashCodes(System.Int32, System.Int32): http://referencesource.Microsoft.com/#System.Web/Util/HashCodeCombiner.cs,21fb74ad8bb43f6b
// System.Array.CombineHashCodes(System.Int32, System.Int32): http://referencesource.Microsoft.com/#mscorlib/system/array.cs,87d117c8cc772cca
public static int CombineHashCodes(IEnumerable<int> hashCodes)
{
int hash = 5381;
foreach (var hashCode in hashCodes)
hash = ((hash << 5) + hash) ^ hashCode;
return hash;
}
Utilisez la logique de combinaison dans Tuple. L'exemple utilise des tuples c # 7.
(field1, field2).GetHashCode();
Si vous recherchez de la vitesse et que vous n'avez pas trop de collisions, alors XOR est le plus rapide. Pour éviter un regroupement autour de zéro, vous pouvez faire quelque chose comme ceci:
finalHash = hash1 ^ hash2;
return finalHash != 0 ? finalHash : hash1;
Bien sûr, certains prototypes devraient vous donner une idée de la performance et du clustering.
En supposant que vous ayez une fonction toString () pertinente (où vos différents champs apparaîtront), je vous renverrais simplement son hashcode:
this.toString().hashCode();
Ce n'est pas très rapide, mais cela devrait éviter très bien les collisions.
Si vos hachages d'entrée ont la même taille, sont répartis de manière égale et ne sont pas liés les uns aux autres, un XOR devrait être OK. En plus c'est rapide.
La situation pour laquelle je suggère ceci est celle que vous souhaitez faire.
H = hash(A) ^ hash(B); // A and B are different types, so there's no way A == B.
bien sûr, si on peut s’attendre à ce que A et B aient la même valeur avec une probabilité raisonnable (non négligeable), vous ne devriez pas utiliser XOR de cette manière.