Quelle est la fonction de hachage d'entier qui accepte une clé de hachage d'entier?


Réponses:


47

Méthode multiplicative de Knuth:

hash(i)=i*2654435761 mod 2^32

En général, vous devez choisir un multiplicateur qui est dans l'ordre de votre taille de hachage ( 2^32dans l'exemple) et qui n'a pas de facteurs communs avec lui. De cette façon, la fonction de hachage couvre uniformément tout votre espace de hachage.

Edit: Le plus gros inconvénient de cette fonction de hachage est qu'elle préserve la divisibilité, donc si vos entiers sont tous divisibles par 2 ou par 4 (ce qui n'est pas rare), leurs hachages le seront aussi. C'est un problème dans les tables de hachage - vous pouvez vous retrouver avec seulement 1/2 ou 1/4 des seaux utilisés.


36
C'est une très mauvaise fonction de hachage, bien que associée à un nom célèbre.
Seun Osewa

5
Ce n'est pas du tout une mauvaise fonction de hachage si elle est utilisée avec des tailles de table de premier ordre. En outre, il est destiné au hachage fermé . Si les valeurs de hachage ne sont pas uniformément distribuées, le hachage multiplicatif garantit que les collisions d'une valeur ne risquent pas de «perturber» les éléments avec d'autres valeurs de hachage.
Paolo Bonzini

11
Pour les curieux, cette constante est choisie comme étant la taille de hachage (2 ^ 32) divisée par Phi
awdz9nld

7
Paolo: La méthode de Knuth est «mauvaise» dans le sens où elle ne fait pas d'avalanche sur les bits supérieurs
awdz9nld

9
En y regardant de plus près, il s'avère que 2654435761 est en fait un nombre premier. C'est probablement pourquoi il a été choisi plutôt que 2654435769.
karadoc

149

J'ai trouvé que l'algorithme suivant fournit une très bonne distribution statistique. Chaque bit d'entrée affecte chaque bit de sortie avec une probabilité d'environ 50%. Il n'y a pas de collisions (chaque entrée entraîne une sortie différente). L'algorithme est rapide sauf si le CPU n'a pas d'unité de multiplication d'entiers intégrée. Code C, en supposant qu'il intsoit 32 bits (pour Java, remplacer >>par >>>et supprimer unsigned):

unsigned int hash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = (x >> 16) ^ x;
    return x;
}

Le nombre magique a été calculé à l'aide d'un programme de test multi-thread spécial qui a duré plusieurs heures, qui calcule l'effet d'avalanche (le nombre de bits de sortie qui changent si un seul bit d'entrée est changé; devrait être proche de 16 en moyenne), indépendance de les changements de bits de sortie (les bits de sortie ne doivent pas dépendre les uns des autres), et la probabilité d'un changement dans chaque bit de sortie si un bit d'entrée est changé. Les valeurs calculées sont meilleures que le finaliseur 32 bits utilisé par MurmurHash , et presque aussi bonnes (pas tout à fait) que lors de l'utilisation d' AES . Un léger avantage est que la même constante est utilisée deux fois (cela l'a rendu légèrement plus rapide la dernière fois que j'ai testé, je ne sais pas si c'est toujours le cas).

Vous pouvez inverser le processus (obtenir la valeur d'entrée à partir du hachage) si vous remplacez le 0x45d9f3bpar 0x119de1f3(l' inverse multiplicatif ):

unsigned int unhash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = (x >> 16) ^ x;
    return x;
}

Pour les nombres 64 bits, je suggère d'utiliser ce qui suit, même si ce n'est peut-être pas le plus rapide. Celui-ci est basé sur splitmix64 , qui semble être basé sur l'article de blog Better Bit Mixing (mix 13).

uint64_t hash(uint64_t x) {
    x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
    x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
    x = x ^ (x >> 31);
    return x;
}

Pour Java, utilisez long, ajoutez Là la constante, remplacez >>par >>>et supprimez unsigned. Dans ce cas, l'inversion est plus compliquée:

uint64_t unhash(uint64_t x) {
    x = (x ^ (x >> 31) ^ (x >> 62)) * UINT64_C(0x319642b2d24d8ec3);
    x = (x ^ (x >> 27) ^ (x >> 54)) * UINT64_C(0x96de1b173f119089);
    x = x ^ (x >> 30) ^ (x >> 60);
    return x;
}

Mise à jour: vous pouvez également consulter le projet Hash Function Prospector , où d'autres constantes (éventuellement meilleures) sont répertoriées.


2
les deux premières lignes sont exactement les mêmes! y a-t-il une faute de frappe ici?
Kshitij Banerjee

3
Non, ce n'est pas une faute de frappe, la deuxième ligne mélange encore les bits. Utiliser une seule multiplication n'est pas aussi bon.
Thomas Mueller

3
J'ai changé le nombre magique parce que, selon un cas de test, j'ai écrit la valeur 0x45d9f3b fournit une meilleure confusion et une meilleure diffusion , spécialement que si un bit de sortie change, chaque autre bit de sortie change avec à peu près la même probabilité (en plus de tous les bits de sortie changent avec le même probabilité si un bit d'entrée change). Comment avez-vous mesuré que 0x3335b369 fonctionne mieux pour vous? Un int 32 bits est-il pour vous?
Thomas Mueller

3
Je recherche une fonction de hachage intéressante pour un int non signé 64 bits à un int non signé 32 bits. Est-ce que pour ce cas, le nombre magique ci-dessus sera le même? J'ai décalé 32 bits au lieu de 16 bits.
alessandro

3
Je pense que dans ce cas, un facteur plus important serait préférable, mais vous auriez besoin d'exécuter des tests. Ou (c'est ce que je fais) utilisez d'abord x = ((x >> 32) ^ x), puis utilisez les multiplications 32 bits ci-dessus. Je ne sais pas ce qui est mieux. Vous pouvez également consulter le finaliseur 64 bits pour Murmur3
Thomas Mueller

29

Cela dépend de la façon dont vos données sont distribuées. Pour un simple compteur, la fonction la plus simple

f(i) = i

sera bon (je soupçonne optimal, mais je ne peux pas le prouver).


3
Le problème avec ceci est qu'il est courant d'avoir de grands ensembles d'entiers qui sont divisibles par un facteur commun (adresses mémoire alignées sur des mots, etc.). Maintenant, si votre table de hachage est divisible par le même facteur, vous vous retrouvez avec seulement la moitié (ou 1/4, 1/8, etc.) des seaux utilisés.
Rafał Dowgird

8
@Rafal: C'est pourquoi la réponse dit "pour un simple compteur" et "Cela dépend de la façon dont vos données sont distribuées"
erikkallen

5
C'est en fait l'implémentation par Sun de la méthode hashCode () dans java.lang.Integer grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk
Juande Carrion

5
@JuandeCarrion C'est trompeur car ce n'est pas le hachage utilisé. Après être passé à l'utilisation de la puissance de deux tailles de table, Java reformule chaque hachage retourné .hashCode(), voir ici .
Esailija

8
La fonction d'identité est assez inutile en tant que hachage dans de nombreuses applications pratiques en raison de ses propriétés distributives (ou de son absence), à ​​moins, bien sûr, que la localité soit un attribut souhaité
awdz9nld

12

Les fonctions de hachage rapides et bonnes peuvent être composées de permutations rapides avec des qualités moindres, comme

  • multiplication avec un entier impair
  • rotations binaires
  • xorshift

Pour produire une fonction de hachage avec des qualités supérieures, comme démontré avec PCG pour la génération de nombres aléatoires.

C'est en fait aussi la recette que rrxmrrxmsx_0 et murmur hash utilisent, sciemment ou non.

J'ai personnellement trouvé

uint64_t xorshift(const uint64_t& n,int i){
  return n^(n>>i);
}
uint64_t hash(const uint64_t& n){
  uint64_t p = 0x5555555555555555ull; // pattern of alternating 0 and 1
  uint64_t c = 17316035218449499591ull;// random uneven integer constant; 
  return c*xorshift(p*xorshift(n,32),32);
}

être assez bon.

Une bonne fonction de hachage devrait

  1. être bijectif pour ne pas perdre d'informations, si possible et avoir le moins de collisions
  2. cascade autant et aussi régulièrement que possible, c'est-à-dire que chaque bit d'entrée doit retourner chaque bit de sortie avec une probabilité de 0,5.

Regardons d'abord la fonction d'identité. Il satisfait 1. mais pas 2.:

fonction d'identité

Le bit d'entrée n détermine le bit de sortie n avec une corrélation de 100% (rouge) et aucune autre, ils sont donc bleus, donnant une ligne rouge parfaite.

Un xorshift (n, 32) n'est pas beaucoup mieux, donnant une ligne et demie. Toujours satisfaisant 1., car il est inversible avec une deuxième application.

xorshift

Une multiplication avec un entier non signé est bien meilleure, en cascade plus fortement et en retournant plus de bits de sortie avec une probabilité de 0,5, ce que vous voulez, en vert. Il satisfait 1. comme pour chaque entier impair il y a un inverse multiplicatif.

knuth

La combinaison des deux donne le résultat suivant, toujours satisfaisant 1. car la composition de deux fonctions bijectives produit une autre fonction bijective.

knuth • xorshift

Une deuxième application de multiplication et de xorshift donnera les résultats suivants:

hash proposé

Ou vous pouvez utiliser des multiplications de terrain Galois comme GHash , elles sont devenues raisonnablement rapides sur les processeurs modernes et ont des qualités supérieures en une seule étape.

   uint64_t const inline gfmul(const uint64_t& i,const uint64_t& j){           
     __m128i I{};I[0]^=i;                                                          
     __m128i J{};J[0]^=j;                                                          
     __m128i M{};M[0]^=0xb000000000000000ull;                                      
     __m128i X = _mm_clmulepi64_si128(I,J,0);                                      
     __m128i A = _mm_clmulepi64_si128(X,M,0);                                      
     __m128i B = _mm_clmulepi64_si128(A,M,0);                                      
     return A[0]^A[1]^B[1]^X[0]^X[1];                                              
   }

gfmul: Le code semble être un pseudo-code, car vous ne pouvez pas utiliser de crochets avec __m128i. Toujours très intéressant. La première ligne semble dire "prenez un __m128i unitaire (I) et xor avec (paramètre) i. Dois-je lire ceci comme initialiser I avec 0 et xor avec i? Si oui, serait-ce la même chose que charger I avec i et effectuer une opération non sur moi?
janvier

@Jan ce que je voudrais faire, c'est __m128i I = i; //set the lower 64 bits, mais je ne peux pas, alors j'utilise ^=. 0^1 = 1donc pas non impliqué. En ce qui concerne l'initialisation avec {}mon compilateur ne s'est jamais plaint, ce n'est peut-être pas la meilleure solution, mais ce que je veux, c'est initialiser tout à 0 pour que je puisse faire ^=ou |=. Je pense avoir basé ce code sur cet article de blog qui donne également l'inversion, très utile: D
Wolfgang Brehm

6

Cette page répertorie quelques fonctions de hachage simples qui ont tendance à être décentes en général, mais tout hachage simple a des cas pathologiques où cela ne fonctionne pas bien.


6
  • Méthode multiplicative 32 bits (très rapide) voir @rafal

    #define hash32(x) ((x)*2654435761)
    #define H_BITS 24 // Hashtable size
    #define H_SHIFT (32-H_BITS)
    unsigned hashtab[1<<H_BITS]  
    .... 
    unsigned slot = hash32(x) >> H_SHIFT
    
  • 32 bits et 64 bits (bonne distribution) à: MurmurHash

  • Fonction de hachage d'entier

3

Il y a un bon aperçu de certains algorithmes de hachage chez Eternally Confuzzled . Je recommanderais le hachage un par un de Bob Jenkins qui atteint rapidement l'avalanche et peut donc être utilisé pour une recherche efficace de table de hachage.


4
C'est un bon article, mais il est axé sur les clés de chaîne de hachage, pas sur les entiers.
Adrian Mouat

Pour être clair, bien que les méthodes de l'article fonctionnent pour les entiers (ou pourraient être adaptées), je suppose qu'il existe des algorithmes plus efficaces pour les entiers.
Adrian Mouat

2

La réponse dépend de beaucoup de choses comme:

  • Où comptez-vous l'utiliser?
  • Qu'essayez-vous de faire avec le hachage?
  • Avez-vous besoin d'une fonction de hachage crytographiquement sécurisée?

Je vous suggère de jeter un œil à la famille de fonctions de hachage Merkle-Damgard comme SHA-1, etc.


1

Je ne pense pas que nous puissions dire qu'une fonction de hachage est "bonne" sans connaître vos données à l'avance! et sans savoir ce que vous allez en faire.

Il existe de meilleures structures de données que les tables de hachage pour des tailles de données inconnues (je suppose que vous faites le hachage d'une table de hachage ici). J'utiliserais personnellement une table de hachage lorsque je sais que j'ai un nombre "fini" d'éléments qui doivent être stockés dans une quantité limitée de mémoire. J'essaierais de faire une analyse statistique rapide de mes données, de voir comment elles sont distribuées, etc. avant de commencer à penser à ma fonction de hachage.


1

Pour les valeurs de hachage aléatoires, certains ingénieurs ont dit que le nombre premier du nombre d'or (2654435761) est un mauvais choix, avec mes résultats de test, j'ai trouvé que ce n'est pas vrai; à la place, 2654435761 distribue assez bien les valeurs de hachage.

#define MCR_HashTableSize 2^10

unsigned int
Hash_UInt_GRPrimeNumber(unsigned int key)
{
  key = key*2654435761 & (MCR_HashTableSize - 1)
  return key;
}

La taille de la table de hachage doit être une puissance de deux.

J'ai écrit un programme de test pour évaluer de nombreuses fonctions de hachage pour les entiers, les résultats montrent que GRPrimeNumber est un très bon choix.

J'ai essayé:

  1. total_data_entry_number / total_bucket_number = 2, 3, 4; où total_bucket_number = taille de la table de hachage;
  2. mapper le domaine de valeur de hachage dans le domaine d'index du compartiment; c'est-à-dire convertir la valeur de hachage en index de compartiment par Logical And Operation avec (hash_table_size - 1), comme indiqué dans Hash_UInt_GRPrimeNumber ();
  3. calculer le numéro de collision de chaque godet;
  4. enregistrez le compartiment qui n'a pas été mappé, c'est-à-dire un compartiment vide;
  5. connaître le nombre maximal de collisions de tous les compartiments; c'est-à-dire la plus longue longueur de chaîne;

Avec les résultats de mes tests, j'ai constaté que Golden Ratio Prime Number a toujours le moins de seaux vides ou zéro seau vide et la longueur de chaîne de collision la plus courte.

Certaines fonctions de hachage pour les entiers sont prétendument bonnes, mais les résultats des tests montrent que lorsque total_data_entry / total_bucket_number = 3, la longueur de chaîne la plus longue est supérieure à 10 (nombre de collisions max> 10), et de nombreux seaux ne sont pas mappés (seaux vides ), ce qui est très mauvais, comparé au résultat de zéro seau vide et de la plus longue longueur de chaîne 3 par Golden Ratio Prime Number Hashing.

BTW, avec mes résultats de test, j'ai trouvé qu'une version des fonctions de hachage shifting-xor est plutôt bonne (elle est partagée par mikera).

unsigned int Hash_UInt_M3(unsigned int key)
{
  key ^= (key << 13);
  key ^= (key >> 17);    
  key ^= (key << 5); 
  return key;
}

2
Mais alors pourquoi ne pas déplacer le produit vers la droite, afin de conserver les éléments les plus mélangés? C'était comme ça que ça devait fonctionner
harold

1
@harold, le nombre premier du nombre d'or est soigneusement choisi, même si je pense que cela ne fera aucune différence, mais je vais tester pour voir si c'est beaucoup mieux avec les "bits les plus mélangés". Bien que mon argument soit que «ce n'est pas un bon choix». n'est pas vrai, comme le montrent les résultats des tests, il suffit de saisir la partie inférieure des bits, c'est assez bon, et même mieux que de nombreuses fonctions de hachage.
Chen-ChungChia

(2654435761, 4295203489) est un nombre d'or de nombres premiers.
Chen-ChungChia

(1640565991, 2654435761) est également un nombre d'or de nombres premiers.
Chen-ChungChia

@harold, le déplacement du produit vers la droite devient pire, même si le simple déplacement vers la droite d'une position (divisé par 2), cela devient encore pire (bien que toujours zéro seau vide, mais la plus longue longueur de chaîne est plus grande); en décalant vers la droite par plus de positions, le résultat devient plus mauvais. Pourquoi? Je pense que la raison est la suivante: déplacer le produit vers la droite fait que plus de valeurs de hachage ne sont pas coprimes, juste à mon avis, la vraie raison implique la théorie des nombres.
Chen-ChungChia

1

J'utilise splitmix64(indiqué dans la réponse de Thomas Mueller ) depuis que j'ai trouvé ce fil. Cependant, je suis récemment tombé sur le rrxmrrxmsx_0 de Pelle Evensen , qui a donné une distribution statistique extrêmement meilleure que le finaliseur d'origine MurmurHash3 et ses successeurs ( splitmix64et autres mélanges). Voici l'extrait de code en C:

#include <stdint.h>

static inline uint64_t ror64(uint64_t v, int r) {
    return (v >> r) | (v << (64 - r));
}

uint64_t rrxmrrxmsx_0(uint64_t v) {
    v ^= ror64(v, 25) ^ ror64(v, 50);
    v *= 0xA24BAED4963EE407UL;
    v ^= ror64(v, 24) ^ ror64(v, 49);
    v *= 0x9FB21C651E98DF25UL;
    return v ^ v >> 28;
}

Pelle fournit également une analyse en profondeur du mélangeur 64 bits utilisé dans l'étape finale de MurmurHash3et les variantes les plus récentes.


2
Cette fonction n'est pas bijective. Pour tout v où v = ror (v, 25), à savoir tous les 0 et tous les 1, il produira la même sortie à deux endroits. Pour toutes les valeurs v = ror64 (v, 24) ^ ror64 (v, 49), qui sont au moins deux de plus et identiques avec v = ror (v, 28), ce qui donne encore 2 ^ 4, totalisant environ 22 collisions inutiles . Deux applications de splitmix sont probablement tout aussi bonnes et tout aussi rapides, mais toujours inversibles et sans collision.
Wolfgang Brehm
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.