web-dev-qa-db-fra.com

Quels sont les avantages du palpage linéaire par rapport au chaînage séparé ou vice-versa lors de la mise en œuvre de tables de hachage?

J'ai revu les algorithmes et passé en revue ces deux méthodes d'implémentation des tables de hachage. Il semble qu'ils aient en grande partie des caractéristiques de performances et des besoins en mémoire similaires.

Je peux penser à certains inconvénients du palpage linéaire - à savoir, que l'élargissement de la matrice pourrait être coûteux (mais cela est fait, quoi, 2 log N fois au plus? Probablement pas un gros problème) et que la gestion des suppressions est un peu plus difficile . Mais je suppose qu'il y a aussi des avantages, sinon cela ne serait pas présenté dans les manuels comme une méthode d'implémentation viable à côté de l'implémentation la plus évidente.

Pourquoi choisiriez-vous l'un plutôt que l'autre?

8
Casey

Avec le sondage linéaire (ou tout autre sondage vraiment), une suppression doit être "douce". Cela signifie que vous devez saisir une valeur fictive (souvent appelée pierre tombale) qui ne correspondra à rien que l'utilisateur puisse rechercher. Ou vous auriez besoin de ressasser à chaque fois. Il est conseillé de ressasser quand trop de pierres tombales s'accumulent ou une stratégie pour défragmenter le cimetière.

Le chaînage séparé (chaque compartiment est un pointeur vers une liste de valeurs liées) présente l'inconvénient que vous finissez par rechercher une liste liée avec tous les problèmes liés au cache à portée de main.

Un autre avantage de la méthode de sondage est que les valeurs vivent toutes dans le même tableau. Cela facilite la copie sur écriture en copiant uniquement le tableau. Si vous pouvez être assuré que l'original n'est pas modifié par le biais d'un invariant de classe, alors prendre un instantané est O(1) et peut être fait sans verrouillage.

9
ratchet freak

Découvrez cette excellente réponse:

https://stackoverflow.com/questions/23821764/why-do-we-use-linear-probing-in-hash-tables-when-there-is-separate-chaining-link

Citant ici:

Je suis surpris que vous ayez vu le hachage chaîné plus rapide que le palpage linéaire - en pratique, le palpage linéaire est généralement beaucoup plus rapide que le chaînage. En fait, c'est la principale raison pour laquelle il est utilisé.

Bien que le hachage chaîné soit excellent en théorie et que le sondage linéaire présente certaines faiblesses théoriques connues (telles que la nécessité d'une indépendance à cinq voies dans la fonction de hachage pour garantir O(1) recherches attendues), en pratique le sondage linéaire est généralement beaucoup plus rapide en raison de la localité de référence. Plus précisément, il est plus rapide d'accéder à une série d'éléments dans un tableau que de suivre les pointeurs dans une liste chaînée, donc le sondage linéaire a tendance à surpasser le hachage chaîné même s'il doit enquêter plus d'éléments.

Il y a d'autres victoires dans le hachage chaîné. Par exemple, les insertions dans une table de hachage de sondage linéaire ne nécessitent pas de nouvelles allocations (à moins que vous ne ressassiez la table), donc dans des applications comme les routeurs réseau où la mémoire est rare, il est bon de savoir qu'une fois la table configurée, les éléments peuvent y être placés sans risque de défaillance du malloc.

4
bmpasini

Je vais sauter avec une réponse biaisée où je réellement préfère séparer le chaînage avec des listes liées individuellement et le trouver plus facile pour atteindre des performances avec eux (je ne dis pas qu'ils 'est optimal, juste plus facile pour mes cas d'utilisation), aussi contradictoire que cela puisse paraître.

Bien sûr, l'optimum théorique est toujours une table de hachage sans collision ou une technique de sondage avec un clustering minimal. Cependant, la solution de chaînage séparée n'a pas à traiter les problèmes de clustering que ce soit.

Cela dit, la représentation des données que j'utilise n'invoque pas d'allocation de mémoire distincte par nœud. Le voici en C:

struct Bucket
{
    int head;
};

struct BucketNode
{
    int next;
    int element;
};

struct HashTable
{
    // Array of buckets, pre-allocated in advance.
    struct Bucket* buckets;

    // Array of nodes, pre-allocated assuming the client knows
    // how many nodes he's going to insert in advance. Otherwise
    // realloc using a similar strategy as std::vector in C++.
    struct BucketNode* nodes;

    // Number of bucket heads.
    int num_buckets;

    // Number of nodes inserted so far.
    int num_nodes;
};

Les compartiments ne sont que des indices 32 bits (je n'utilise même pas de structure en réalité) et les nœuds ne sont que deux indices 32 bits. Souvent, je n'ai même pas besoin de l'index element car les nœuds sont souvent stockés en parallèle avec le tableau d'éléments à insérer dans la table, ce qui réduit la surcharge de la table de hachage à 32 bits par compartiment et 32 -bits par élément inséré. La vraie version que j'utilise le plus souvent ressemble à ceci:

struct HashTable
{
    // Array of head indices. The indices point to entries in the 
    // second array below.
    int* buckets;

    // Array of next indices parallel to the elements to insert.
    int* next_indices;

    // Number of bucket heads.
    int num_buckets;
};

De plus, si la localité spatiale se dégrade, je peux facilement effectuer une passe de post-traitement où je construis une nouvelle table de hachage où chaque nœud de compartiment est contigu à l'autre (fonction de copie triviale qui fait juste un passage linéaire à travers la table de hachage et en construit une nouvelle - en raison de la nature dans laquelle il parcourt la table de hachage, la copie se retrouve avec tous les nœuds voisins dans un compartiment contigu les uns aux autres).

En ce qui concerne les techniques de sondage, cela présente les avantages que la localité spatiale est déjà là depuis le début sans pools de mémoire ni tableau de sauvegarde comme j'utilise, et ils n'ont pas non plus la surcharge de 32 bits par compartiment et nœud, mais ensuite vous devrez peut-être faire face à des problèmes de clustering qui peuvent commencer à s'accumuler de manière vicieuse avec de nombreuses collisions.

Je trouve que la nature même du clustering est un casse-tête qui nécessite beaucoup d'analyse en cas de nombreuses collisions. L'avantage de cette solution est que je peux souvent obtenir un résultat décent la première fois sans une analyse et des tests aussi approfondis. De plus, si la table se redimensionne d'elle-même implicitement, j'ai rencontré des cas où de telles conceptions ont fini par exploser l'utilisation de la mémoire d'une manière qui dépassait de loin ce que cette solution de base qui nécessite un 32 bits par compartiment et 32 ​​bits par nœud fonctionnerait même dans le pire des cas. C'est une solution qui évite de devenir trop mauvaise même s'il y a un certain nombre de collisions.

La plupart de ma base de code tourne autour de structures de données qui stockent des indices et stockent souvent des indices en parallèle avec le tableau d'éléments à insérer. Cela réduit la taille de la mémoire, évite les copies profondes superflues des éléments à insérer et permet de raisonner très facilement sur l'utilisation de la mémoire. En dehors de cela, dans mon cas, j'ai tendance à bénéficier autant de performances prévisibles que de performances optimales. Un algorithme qui est optimal dans de nombreux scénarios courants mais qui peut fonctionner horriblement dans le pire des cas est souvent moins préférable pour moi qu'un algorithme qui fonctionne raisonnablement bien tout le temps et ne fait pas bégayer les fréquences d'images à des moments imprévisibles, et donc je ont tendance à privilégier ce type de solutions.

1
user204677