Supposons que vous ayez deux hashes H(A)
et H(B)
et que vous souhaitiez les combiner. J'ai lu qu'un bon moyen de combiner deux hash est de XOR
les, par exemple. XOR( H(A), H(B) )
.
La meilleure explication que j'ai trouvée est brièvement abordée ici sur ces instructions de la fonction de hachage :
XORing deux nombres avec une distribution à peu près aléatoire a pour résultat un autre nombre toujours avec une distribution à peu près aléatoire *, mais qui dépend maintenant des deux valeurs.
...
* À chaque bit des deux nombres à combiner, un 0 est émis si les deux bits sont égaux, sinon un 1. Autrement dit, dans 50% des combinaisons, un 1 est émis. Ainsi, si les deux bits d’entrée ont chacun une chance sur environ 50-50 d’être égaux à 0 ou 1, le bit de sortie le sera aussi.
Pouvez-vous expliquer l'intuition et/ou les mathématiques derrière pourquoi XOR devrait être l'opération par défaut pour combiner des fonctions de hachage (plutôt que OR ou AND etc.)?
En supposant des entrées uniformément aléatoires (1 bit), la distribution de probabilité de sortie de la fonction AND est de 75% 0
et 25% 1
. Inversement, OR vaut 25% 0
et 75% 1
.
La fonction XOR est à 50% 0
et 50% 1
, il est donc utile pour combiner des distributions de probabilité uniformes.
Ceci peut être vu en écrivant des tables de vérité:
a | b | a AND b
---+---+--------
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1
a | b | a OR b
---+---+--------
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1
a | b | a XOR b
---+---+--------
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0
Exercice: Combien de fonctions logiques de deux entrées 1 bit a
et b
ont cette distribution de sortie uniforme? Pourquoi XOR est-il le mieux adapté à l’objectif indiqué dans votre question?
xor est une fonction par défaut dangereuse à utiliser lors du hachage. C'est mieux que et et ou, mais ça ne dit pas grand chose.
xor étant symétrique, l'ordre des éléments est perdu. Donc "bad"
Va combiner la même chose que "dab"
.
xor mappe les valeurs identiques à zéro et vous devriez éviter de mapper les valeurs "communes" à zéro:
Ainsi, (a,a)
Est mappé sur 0 et (b,b)
Est également mappé sur 0. Comme ces paires sont plus courantes que le hasard ne pourrait l'impliquer, vous vous retrouvez avec beaucoup de collisions à zéro que vous n'auriez dû.
Avec ces deux problèmes, xor finit par être un combinateur de hachage qui semble à moitié décent en surface, mais pas après une inspection plus approfondie.
Sur du matériel moderne, on ajoute généralement à peu près aussi vite que xor (certes, il utilise probablement plus de puissance pour le faire). L'ajout de la table de vérité est similaire à xor sur le bit en question, mais il envoie également un bit au bit suivant lorsque les deux valeurs valent 1. Cela efface moins d'informations.
Donc, hash(a) + hash(b)
est préférable car si a==b
, Le résultat est plutôt hash(a)<<1
au lieu de 0.
Cela reste symétrique. Nous pouvons casser cette symétrie pour un coût modeste:
hash(a)<<1 + hash(a) + hash(b)
aka hash(a)*3 + hash(b)
. (calculer hash(a)
une fois et le stocker est conseillé si vous utilisez la solution de décalage). Toute constante impaire au lieu de 3
Mappera bijectivement une size_t
(Ou constante k-bit non signée) sur elle-même, car mapper sur des constantes non signées est math modulo 2^k
Pour un certain k
, et toute constante impaire est relativement prime pour 2^k
.
Pour une version encore plus sophistiquée, nous pouvons examiner boost::hash_combine
, Qui est effectivement:
size_t hash_combine( size_t lhs, size_t rhs ) {
lhs^= rhs + 0x9e3779b9 + (lhs << 6) + (lhs >> 2);
return lhs;
}
nous ajoutons ici quelques versions décalées de seed
avec une constante (qui est essentiellement aléatoire 0
s et 1
s - en particulier c’est l’inverse du nombre d’or une fraction de point fixe de 32 bits) avec quelques additions et un xor. Cela rompt la symétrie et introduit un "bruit" si les valeurs de hachage entrantes sont médiocres (imaginons que chaque composant soit haché à 0 - le traitement ci-dessus le gère bien, générant un frottis de 1
Et 0
s après chaque moissonneuse. Mine produit simplement un 0
).
Pour ceux qui ne sont pas familiers avec C/C++, un size_t
Est une valeur entière non signée suffisamment grande pour décrire la taille de tout objet en mémoire. Sur un système 64 bits, il s'agit généralement d'un entier non signé de 64 bits. Sur un système 32 bits, un entier non signé 32 bits.
Malgré ses propriétés pratiques de mélange de bits, XOR est pas = un bon moyen de combiner les hachages en raison de sa commutativité. Considérez ce qui se passerait si vous stockiez les permutations de {1, 2,…, 10} dans une table de hachage de 10 tuples.
Un bien meilleur choix est m * H(A) + H(B)
, où m est un grand nombre impair.
Crédit: Le combinateur ci-dessus était un conseil de Bob Jenkins.
Xor peut être le moyen "par défaut" de combiner des hachages mais la réponse de Greg Hewgill montre également pourquoi elle présente des pièges: le xor de deux valeurs de hachage identiques est zéro. Dans la vraie vie, il y a des hachages identiques qui sont plus courants qu'on aurait pu s'y attendre. Vous pourriez alors constater que dans ces cas de coin (pas si rares), les hachages combinées résultantes sont toujours les mêmes (zéro). Les collisions de hachage seraient beaucoup, beaucoup plus fréquentes que prévu.
Dans un exemple artificiel, vous pouvez associer des mots de passe hachés d'utilisateurs appartenant à différents sites Web que vous gérez. Malheureusement, un grand nombre d'utilisateurs réutilisent leurs mots de passe et une proportion surprenante des hachages résultants est nulle!
Il y a quelque chose que je veux signaler explicitement aux autres qui trouvent cette page. AND et OR restreint la sortie comme BlueRaja - Danny Pflughoe essaie de le signaler, mais peut être mieux défini:
Je veux d’abord définir deux fonctions simples que je vais utiliser pour expliquer ceci: Min () et Max ().
Min (A, B) renvoie la valeur inférieure entre A et B, par exemple: Min (1, 5) renvoie 1.
Max (A, B) renverra la valeur la plus grande entre A et B, par exemple: Max (1, 5) renvoie 5.
Si on vous donne: C = A AND B
Ensuite, vous pouvez trouver que C <= Min(A, B)
Nous le savons parce qu’il n’ya rien que vous puissiez ET avec les 0 bits de A ou B pour les transformer en 1. Ainsi, chaque bit zéro reste un bit zéro et chaque bit a une chance de devenir un bit zéro (et donc une valeur plus petite).
Avec: C = A OR B
Le contraire est vrai: C >= Max(A, B)
Avec cela, nous voyons le corollaire de la fonction AND. Un bit qui est déjà un un ne peut pas devenir un zéro, il reste donc un un, mais chaque bit zéro a une chance de devenir un et donc un nombre plus grand.
Cela implique que l'état de l'entrée applique des restrictions sur la sortie. Si vous ET utilisez quelque chose avec 90, vous savez que la sortie sera égale ou inférieure à 90, quelle que soit la valeur.
Pour XOR, il n'y a pas de restriction implicite basée sur les entrées. Il existe des cas spéciaux où vous pouvez constater que si XOR un octet contenant 255 fois l’inverse), tout octet possible peut en sortir. Tous les bits ont la possibilité de changer d’état en fonction de le même bit dans l'autre opérande.
Si vous XOR
une entrée aléatoire avec une entrée biaisée, la sortie est aléatoire. Il n'en va pas de même pour AND
ou OR
. Exemple:
00101001 XOR 00000000 = 00101001 00101001 ET 00000000 = 00000000 00101001 OR 11111111 = 11111111
Comme @Greg Hewgill le mentionne, même si les entrées les deux sont aléatoires, l'utilisation de AND
ou OR
entraînera une sortie biaisée.
La raison pour laquelle nous utilisons XOR
sur quelque chose de plus complexe est que, eh bien, il n’est pas nécessaire: XOR
fonctionne parfaitement et c’est extrêmement rapide.
Couvrez les 2 colonnes de gauche et essayez de déterminer quelles entrées utilisent uniquement la sortie.
a | b | a AND b
---+---+--------
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1
Lorsque vous avez vu un bit, vous auriez dû vous rendre compte que les deux entrées étaient à 1.
Maintenant, faites la même chose pour XOR
a | b | a XOR b
---+---+--------
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0
XOR ne dévoile rien de ses entrées.
XOR n'ignore pas certaines des entrées parfois comme OU et ET .
Si vous prenez AND (X, Y) par exemple, et alimentez l'entrée X avec false, alors l'entrée Y n'a pas d'importance ... et on voudrait probablement que l'entrée soit importante lors de la combinaison de hachages.
Si vous prenez XOR (X, Y) alors [~ # ~] les deux [~ # ~] entrées [~ # ~] toujours [~ # ~] est important. Il n'y aurait aucune valeur de X où Y n'a pas d'importance. Si X ou Y est modifié, la sortie le reflétera.
Le code source de diverses versions de hashCode()
in Java.util.Arrays est une excellente référence pour les algorithmes de hachage solides à usage général. Ils sont facilement compris et traduits dans d'autres langages de programmation.
En gros, la plupart des implémentations multi-attributs hashCode()
suivent ce modèle:
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
Vous pouvez rechercher d’autres questions et réponses sur StackOverflow pour plus d’informations sur la magie derrière 31
, Et pourquoi Java l’utilise si souvent. Il est imparfait, mais présente de très bonnes performances générales.