web-dev-qa-db-fra.com

Fonctions de hachage simples

J'essaie d'écrire un programme C qui utilise une table de hachage pour stocker différents mots et je pourrais utiliser de l'aide.

Tout d'abord, je crée une table de hachage avec la taille d'un nombre premier qui est le plus proche du nombre de mots que je dois stocker, puis j'utilise une fonction de hachage pour trouver une adresse pour chaque mot. J'ai commencé avec la fonction la plus simple, en ajoutant les lettres ensemble, ce qui s'est soldé par une collision de 88%. Ensuite, j'ai commencé à expérimenter la fonction et j'ai découvert que quoi que je change, les collisions ne sont pas inférieures à 35%. En ce moment j'utilise

unsigned int stringToHash(char *Word, unsigned int hashTableSize){
  unsigned int counter, hashAddress =0;
  for (counter =0; Word[counter]!='\0'; counter++){
    hashAddress = hashAddress*Word[counter] + Word[counter] + counter;
  }
  return (hashAddress%hashTableSize);
}

qui est juste une fonction aléatoire que j'ai inventée, mais elle me donne les meilleurs résultats - environ 35% de collision.

J'ai lu des articles sur les fonctions de hachage au cours des dernières heures et j'ai essayé d'en utiliser quelques-uns simples, tels que djb2, mais tous m'ont donné des résultats encore pires. (Djb2 a entraîné une collision de 37%, ce qui est ' t bien pire, mais je m'attendais à quelque chose de mieux plutôt que de pire) Je ne sais pas non plus comment utiliser certains des autres, plus complexes, comme le murmure2, parce que je ne sais pas quels sont les paramètres (clé, len , graine) qu'ils absorbent sont.

Est-il normal d'obtenir plus de 35% de collisions, même en utilisant le djb2, ou est-ce que je fais quelque chose de mal? Quelles sont les valeurs clés, len et seed?

33
Hardell

Essayez sdbm:

hashAddress = 0;
for (counter = 0; Word[counter]!='\0'; counter++){
    hashAddress = Word[counter] + (hashAddress << 6) + (hashAddress << 16) - hashAddress;
}

Ou djb2:

hashAddress = 5381;
for (counter = 0; Word[counter]!='\0'; counter++){
    hashAddress = ((hashAddress << 5) + hashAddress) + Word[counter];
}

Ou Adler32:

uint32_t adler32(const void *buf, size_t buflength) {
     const uint8_t *buffer = (const uint8_t*)buf;

     uint32_t s1 = 1;
     uint32_t s2 = 0;

     for (size_t n = 0; n < buflength; n++) {
        s1 = (s1 + buffer[n]) % 65521;
        s2 = (s2 + s1) % 65521;
     }     
     return (s2 << 16) | s1;
}

// ...

hashAddress = adler32(Word, strlen(Word));

Cependant, rien de tout cela n'est vraiment génial. Si vous voulez vraiment de bons hachages, vous avez besoin de quelque chose de plus complexe comme lookup par exemple.

Notez qu'une table de hachage devrait avoir beaucoup de collisions dès qu'elle sera remplie de plus de 70-80%. Ceci est parfaitement normal et se produira même si vous utilisez un très bon algorithme de hachage. C'est pourquoi la plupart des implémentations de table de hachage augmentent la capacité de la table de hachage (par exemple capacity * 1.5 Ou même capacity * 2) Dès que vous ajoutez quelque chose à la table de hachage et que le ratio size / capacity Est déjà supérieur 0,7 à 0,8. Augmenter la capacité signifie qu'une nouvelle table de hachage est créée avec une capacité plus élevée, toutes les valeurs de la table actuelle sont ajoutées à la nouvelle (à cet effet, elles doivent toutes être retravaillées, car leur nouvel index sera différent dans la plupart des cas), le nouveau tableau hastable remplace l'ancien et l'ancien est libéré/libéré. Si vous prévoyez de hacher 1000 mots, une capacité de hachage d'au moins 1250 recommandée, mieux 1400 ou même 1500.

Les tables de hachage ne sont pas censées être "remplies à ras bord", du moins pas si elles doivent être rapides et efficaces (elles devraient donc toujours avoir une capacité disponible). C'est la taille réduite des tables de hachage, elles sont rapides (O(1)), mais elles gaspillent généralement plus d'espace qu'il n'en faudrait pour stocker les mêmes données dans une autre structure (lorsque vous les stockez en tant que tableau trié, vous n'a besoin que d'une capacité de 1000 pour 1000 mots; la réduction est que la recherche ne peut pas être plus rapide que O(log n) dans ce cas). Une table de hachage sans collision n'est pas possible dans la plupart des cas. Presque toutes les implémentations de table de hachage s'attendent à ce que des collisions se produisent et ont généralement une sorte de moyen de les gérer (généralement les collisions ralentissent la recherche, mais la table de hachage fonctionnera toujours et battra d'autres structures de données dans de nombreux cas).

Notez également que si vous utilisez une fonction de hachage assez bonne, il n'y a aucune exigence, mais pas même un avantage, si la table de hachage a une puissance de 2 si vous recadrez des valeurs de hachage à l'aide de modulo (%) Dans la fin. La raison pour laquelle de nombreuses implémentations de table de hachage utilisent toujours une puissance de 2 capacités est que elles n'utilisent pas modulo , elles utilisent plutôt AND (&) pour le recadrage car une opération ET est parmi les opérations les plus rapides que vous trouverez sur la plupart des CPU (le modulo n'est jamais plus rapide que ET, dans le meilleur des cas il serait tout aussi rapide, dans la plupart des cas il est beaucoup plus lent). Si votre table de hachage utilise une puissance de 2 tailles, vous pouvez remplacer n'importe quel module par une opération ET:

x % 4  == x & 3
x % 8  == x & 7
x % 16 == x & 15
x % 32 == x & 31
...

Cela ne fonctionne que pour une puissance de 2 tailles. Si vous utilisez modulo, une puissance de 2 tailles ne peut acheter quelque chose que si le hachage est un très mauvais hachage avec une très mauvaise "distribution de bits". Une mauvaise distribution de bits est généralement causée par des hachages qui n'utilisent aucun type de décalage de bits (>> Ou <<) Ou toute autre opération qui aurait un effet similaire au décalage de bits.

J'ai créé une implémentation de lookup3 simplifiée pour vous:

#include <stdint.h>
#include <stdlib.h>

#define rot(x,k) (((x)<<(k)) | ((x)>>(32-(k))))

#define mix(a,b,c) \
{ \
  a -= c;  a ^= rot(c, 4);  c += b; \
  b -= a;  b ^= rot(a, 6);  a += c; \
  c -= b;  c ^= rot(b, 8);  b += a; \
  a -= c;  a ^= rot(c,16);  c += b; \
  b -= a;  b ^= rot(a,19);  a += c; \
  c -= b;  c ^= rot(b, 4);  b += a; \
}

#define final(a,b,c) \
{ \
  c ^= b; c -= rot(b,14); \
  a ^= c; a -= rot(c,11); \
  b ^= a; b -= rot(a,25); \
  c ^= b; c -= rot(b,16); \
  a ^= c; a -= rot(c,4);  \
  b ^= a; b -= rot(a,14); \
  c ^= b; c -= rot(b,24); \
}

uint32_t lookup3 (
  const void *key,
  size_t      length,
  uint32_t    initval
) {
  uint32_t  a,b,c;
  const uint8_t  *k;
  const uint32_t *data32Bit;

  data32Bit = key;
  a = b = c = 0xdeadbeef + (((uint32_t)length)<<2) + initval;

  while (length > 12) {
    a += *(data32Bit++);
    b += *(data32Bit++);
    c += *(data32Bit++);
    mix(a,b,c);
    length -= 12;
  }

  k = (const uint8_t *)data32Bit;
  switch (length) {
    case 12: c += ((uint32_t)k[11])<<24;
    case 11: c += ((uint32_t)k[10])<<16;
    case 10: c += ((uint32_t)k[9])<<8;
    case 9 : c += k[8];
    case 8 : b += ((uint32_t)k[7])<<24;
    case 7 : b += ((uint32_t)k[6])<<16;
    case 6 : b += ((uint32_t)k[5])<<8;
    case 5 : b += k[4];
    case 4 : a += ((uint32_t)k[3])<<24;
    case 3 : a += ((uint32_t)k[2])<<16;
    case 2 : a += ((uint32_t)k[1])<<8;
    case 1 : a += k[0];
             break;
    case 0 : return c;
  }
  final(a,b,c);
  return c;
}

Ce code n'est pas aussi hautement optimisé pour les performances que le code d'origine, il est donc beaucoup plus simple. Il n'est pas non plus aussi portable que le code d'origine, mais il est portable sur toutes les principales plates-formes grand public utilisées aujourd'hui. Il ignore également complètement le processeur endian, mais ce n'est pas vraiment un problème, cela fonctionnera sur les grands et petits processeurs endian. Gardez juste à l'esprit qu'il ne calculera pas le même hachage pour les mêmes données sur les gros et petits processeurs endian, mais ce n'est pas une exigence; il calculera un bon hachage sur les deux types de CPU et il est important qu'il calcule toujours le même hachage pour les mêmes données d'entrée sur une seule machine.

Vous utiliseriez cette fonction comme suit:

unsigned int stringToHash(char *Word, unsigned int hashTableSize){
  unsigned int initval;
  unsigned int hashAddress;

  initval = 12345;
  hashAddress = lookup3(Word, strlen(Word), initval);
  return (hashAddress%hashTableSize);
  // If hashtable is guaranteed to always have a size that is a power of 2,
  // replace the line above with the following more effective line:
  //     return (hashAddress & (hashTableSize - 1));
}

Vous vous demandez bien ce qu'est initval. Eh bien, c'est ce que vous voulez que ce soit. On pourrait appeler ça du sel. Cela influencera les valeurs de hachage, mais les valeurs de hachage ne s'amélioreront pas ou ne s'amélioreront pas à cause de cela (du moins pas dans le cas moyen, cela peut conduire à plus ou moins de collisions pour des données très spécifiques, cependant). Par exemple. vous pouvez utiliser différentes valeurs de initval si vous souhaitez hacher deux fois les mêmes données, mais chaque fois devrait produire une valeur de hachage différente (il n'y a aucune garantie que ce sera le cas, mais il est plutôt probable que initval est différent; s'il crée la même valeur, ce serait une coïncidence très malchanceuse que vous devez traiter cela comme une sorte de collision). Il n'est pas conseillé d'utiliser différentes valeurs initval lors du hachage des données pour la même table de hachage (cela provoquera plutôt plus de collisions en moyenne). Une autre utilisation pour initval est si vous souhaitez combiner un hachage avec d'autres données, auquel cas le hachage déjà existant devient initval lors du hachage des autres données (donc les deux, les autres données ainsi que l'influence de hachage précédente le résultat de la fonction de hachage). Vous pouvez même définir initval sur 0 Si vous aimez ou choisissez une valeur aléatoire lors de la création de la table de hachage (et utilisez toujours cette valeur aléatoire pour cette instance de table de hachage, mais chaque table de hachage a son propre aléatoire valeur).

Une note sur les collisions:

Les collisions ne sont généralement pas un problème si énorme dans la pratique, il n'est généralement pas rentable de gaspiller des tonnes de mémoire juste pour les éviter. La question est plutôt de savoir comment vous allez les gérer de manière efficace.

Vous avez dit que vous traitez actuellement 9 000 mots. Si vous utilisiez un tableau non trié, la recherche d'un mot dans le tableau nécessitera en moyenne 4500 comparaisons. Sur mon système, les comparaisons de 4500 chaînes (en supposant que les mots ont entre 3 et 20 caractères) nécessitent 38 microsecondes (0,000038 secondes). Ainsi, même un algorithme aussi simple et inefficace est assez rapide pour la plupart des applications. En supposant que vous triez la liste de mots et utilisez une recherche binaire, trouver un mot dans le tableau n'aura besoin que de 13 comparaisons en moyenne. 13 comparaisons ne sont presque rien en termes de temps, c'est trop peu pour même un benchmark fiable. Donc, si trouver un mot dans une table de hachage nécessite 2 à 4 comparaisons, je ne perdrais même pas une seule seconde sur la question de savoir si cela peut être un énorme problème de performances.

Dans votre cas, une liste triée avec recherche binaire peut même de loin battre une table de hachage. Bien sûr, 13 comparaisons nécessitent plus de temps que 2 à 4 comparaisons, cependant, dans le cas d'une table de hachage, vous devez d'abord hacher les données d'entrée pour effectuer une recherche. Le hachage seul peut déjà prendre plus de 13 comparaisons! Le mieux le hachage, le plus long il faudra pour le même nombre de données à hacher. Ainsi, une table de hachage n'est rentable en termes de performances que si vous avez une énorme quantité de données ou si vous devez mettre à jour les données fréquemment (par exemple, ajouter/supprimer constamment des mots dans/de la table, car ces opérations sont moins coûteuses pour une table de hachage qu'elles sont pour une liste triée). Le fait qu'un hashatble soit O(1) signifie seulement que quelle que soit sa taille, une recherche sera d'env. ont toujours besoin du même temps. O(log n) signifie seulement que la recherche se développe logarithmiquement avec le nombre de mots, cela signifie plus de mots, une recherche plus lente. Pourtant, la notation Big-O ne dit rien sur la vitesse absolue! C'est un gros malentendu. Il n'est pas dit qu'un algorithme O(1) fonctionne toujours plus vite qu'un O(log n) one. La notation Big-O vous indique seulement que si l'algorithme O(log n) est plus rapide pour un certain nombre de valeurs et que vous continuez à augmenter le nombre de valeurs, l'algorithme O(1) dépassera certainement le O(log n) algorithme à un moment donné, mais votre nombre de mots actuel peut être bien inférieur à ce point. Sans comparer les deux approches, vous ne pouvez pas dire laquelle est la plus rapide en regardant simplement la notation Big-O.

Retour aux collisions. Que devez-vous faire en cas de collision? Si le nombre de collisions est petit, et ici je ne parle pas du nombre total de collisions (le nombre de mots qui entrent en collision dans la table de hachage) mais celui par index (le nombre de mots stockés dans le même index de table de hachage, donc dans votre cas, peut-être 2-4), l'approche la plus simple consiste à les stocker sous forme de liste chaînée. S'il n'y a pas eu de collision jusqu'à présent pour cet index de table, il n'y a qu'une seule paire clé/valeur. En cas de collision, il existe une liste liée de paires clé/valeur. Dans ce cas, votre code doit parcourir la liste liée et vérifier chacune des clés et renvoyer la valeur si elle correspond. D'après vos chiffres, cette liste chaînée n'aura pas plus de 4 entrées et faire 4 comparaisons est insignifiant en termes de performances. Donc, trouver l'index est O(1), trouver la valeur (ou détecter que cette clé n'est pas dans le tableau) est O(n), mais ici n n'est que le nombre de entrées de liste chaînée (il s'agit donc de 4 au maximum).

Si le nombre de collisions augmente, une liste chaînée peut devenir trop lente et vous pouvez également stocker un tableau trié de taille dynamique de paires clé/valeur, qui permet des recherches de O(log n) et encore, n n'est que le nombre de clés de ce tableau, pas de toutes les clés de l'hastable. Même s'il y avait 100 collisions à un indice, trouver la bonne paire clé/valeur prend au plus 7 comparaisons. C'est encore presque rien. Malgré le fait que si vous avez vraiment 100 collisions à un index, soit votre algorithme de hachage n'est pas adapté à vos données clés, soit la table de hachage est beaucoup trop petite. L'inconvénient d'un tableau trié de taille dynamique est que l'ajout/la suppression de clés représente un peu plus de travail que dans le cas d'une liste chaînée (au niveau du code, pas nécessairement au niveau des performances). Donc, utiliser une liste chaînée est généralement suffisant si vous maintenez un nombre de collisions suffisamment bas et il est presque trivial d'implémenter une telle liste chaînée vous-même en C et de l'ajouter à une implémentation de table de hachage existante.

La plupart des implémentations de table de hachage semblent avoir recours à un tel "repli vers une autre structure de données" pour gérer les collisions. L'inconvénient est que ceux-ci nécessitent un peu de mémoire supplémentaire pour stocker la structure de données alternative et un peu plus de code pour rechercher également des clés dans cette structure. Il existe également des solutions qui stockent les collisions à l'intérieur de la table de hachage elle-même et qui ne nécessitent aucune mémoire supplémentaire. Cependant, ces solutions présentent quelques inconvénients. Le premier inconvénient est que chaque collision augmente les chances de multiplier les collisions à mesure que davantage de données sont ajoutées. Le deuxième inconvénient est que si les temps de recherche des clés diminuent de façon linéaire avec le nombre de collisions jusqu'à présent (et comme je l'ai dit précédemment, chaque collision conduit à encore plus de collisions à mesure que des données sont ajoutées), les temps de recherche des clés qui ne sont pas dans la table de hachage diminuent encore pire et à la fin, si vous effectuez une recherche pour une clé qui ne se trouve pas dans la table de hachage (pourtant vous ne pouvez pas savoir sans effectuer la recherche), la recherche peut prendre aussi longtemps qu'une recherche linéaire sur toute la table de hachage (YUCK !!!) . Donc, si vous pouvez économiser de la mémoire supplémentaire, optez pour une autre structure pour gérer les collisions.

73
Mecki

Tout d'abord, je crée une table de hachage avec la taille d'un nombre premier qui correspond au nombre de mots que je dois stocker, puis j'utilise une fonction de hachage pour trouver une adresse pour chaque mot.

...

return (hashAddress% hashTableSize);

Étant donné que le nombre de hachages différents est comparable au nombre de mots, vous ne pouvez pas vous attendre à des collisions beaucoup plus faibles.

J'ai fait un test statistique simple avec un hachage aléatoire (qui est le meilleur que vous puissiez réaliser) et j'ai trouvé que 26% est le taux de collision limitant si vous avez # mots == # hachages différents.

2
Emanuele Paolini