Chapitre 5 — Tables de hachage
Associer des clés à des valeurs pour accéder rapidement aux informations
| Positionnement dans le parcours — Après les listes, les piles et les files, ce chapitre introduit les structures associatives. Les tables de hachage préparent l’étude des dictionnaires efficaces, des caches, des ensembles et de nombreux algorithmes fondés sur un accès moyen en temps constant. |
Fiche pédagogique du chapitre
Objectifs d’apprentissage
À la fin de ce chapitre, l’étudiant devra être capable de :
- expliquer le principe qui associe une clé à un indice de table ;
- distinguer une clé, une valeur, une fonction de hachage et une case de stockage ;
- évaluer les qualités d’une fonction de hachage ;
- identifier une collision et choisir une stratégie de résolution appropriée ;
- implémenter le chaînage séparé et les principales variantes de l’adressage ouvert ;
- calculer le facteur de charge et prévoir un redimensionnement ;
- analyser les performances moyennes et défavorables des opérations ;
- concevoir une application utilisant un ensemble ou un dictionnaire fondé sur le hachage.
Prérequis
- Maîtrise des tableaux, des enregistrements et des listes chaînées.
- Connaissance des fonctions, procédures et types abstraits de données.
- Capacité à analyser une boucle et à utiliser les notations O, Ω et Θ.
- Compréhension des notions de clé de recherche, d’égalité et de comparaison.
Organisation proposée
Partie | Contenu principal | Durée indicative |
|---|---|---|
| 5.1 | Principe du hachage et vocabulaire | 2 h |
| 5.2 | Qualités et construction d’une fonction de hachage | 2 h |
| 5.3 | Collisions : chaînage et adressage ouvert | 4 h |
| 5.4 | Facteur de charge, redimensionnement et analyse | 2 h |
| Applications | Annuaire, dictionnaire, comptage, doublons et cache | 2 h |
| TD / TP | Conception, traces, implémentation et tests | 4 à 6 h |
Introduction
De nombreux programmes doivent retrouver une information à partir d’un identifiant : numéro d’inscription, adresse électronique, mot d’un dictionnaire, référence d’un produit ou résultat déjà calculé. Une recherche séquentielle coûte O(n), tandis qu’une structure ordonnée impose souvent un tri ou un arbre. La table de hachage propose une autre stratégie : transformer directement la clé en un indice de stockage.
Lorsque les clés sont bien réparties et que le taux d’occupation reste maîtrisé, la recherche, l’insertion et la suppression s’effectuent en temps moyen O(1). Cette performance n’est toutefois pas automatique. Elle dépend fortement de la fonction de hachage, de la stratégie de gestion des collisions et du redimensionnement de la table.
| Idée directrice — Le hachage ne supprime pas la recherche : il réduit fortement l’espace à explorer en calculant la zone dans laquelle la clé doit se trouver. |
5.1 Principe du hachage
5.1.1 Clé et valeur
Une table de hachage stocke généralement des associations de la forme clé → valeur. La clé identifie l’information, tandis que la valeur contient les données qui lui sont associées. Dans un ensemble, seule la clé est nécessaire ; dans un dictionnaire, chaque clé correspond à une valeur.
Élément | Rôle | Exemple dans un annuaire |
|---|---|---|
| Clé | Identifie de manière logique une entrée. | Adresse électronique |
| Valeur | Contient l’information associée à la clé. | Nom et numéro de téléphone |
| Égalité | Détermine si deux clés représentent la même entrée. | Deux chaînes identiques |
| Hachage | Transforme la clé en une valeur numérique. | Hacher("sara@exemple.ma") |
| Contrainte fondamentale — Une clé ne doit pas changer pendant qu’elle est stockée. Si une partie utilisée par la fonction de hachage est modifiée, l’entrée risque de devenir introuvable. |
5.1.2 Fonction de hachage
Une fonction de hachage produit un entier à partir d’une clé. Cet entier peut être très grand ou négatif selon le langage. Une opération de réduction le transforme ensuite en un indice valide compris entre 0 et m − 1, où m représente la taille de la table.
Schéma général
hashBrut <- Hacher(cle)
indice <- Normaliser(hashBrut) MOD m
Accéder à table[indice]La fonction Hacher dépend du type de clé. Pour un entier, une transformation simple peut suffire. Pour une chaîne, il faut combiner les codes des caractères. Pour un enregistrement, plusieurs champs stables peuvent être intégrés.
5.1.3 Indice calculé et table de hachage
La table est composée de m positions, souvent appelées cases, alvéoles ou buckets. L’indice calculé désigne le bucket initial de la clé. Si plusieurs clés conduisent au même indice, une collision apparaît.
Exemple : pour une table de taille m = 7 et la fonction h(k) = k MOD 7 :
Clé k | Calcul | Indice h(k) |
|---|---|---|
19 | 19 MOD 7 | 5 |
26 | 26 MOD 7 | 5 |
12 | 12 MOD 7 | 5 |
20 | 20 MOD 7 | 6 |
8 | 8 MOD 7 | 1 |
| Collision — Les clés 19, 26 et 12 produisent toutes l’indice 5. Elles doivent donc être distinguées à l’intérieur ou autour de cette case. |
5.1.4 Représentation abstraite
Une table de hachage fournit souvent l’interface d’un dictionnaire. Son utilisateur ne doit pas dépendre de la représentation interne des buckets.
TAD Dictionnaire<Cle, Valeur>
Inserer(cle, valeur)
Rechercher(cle) : ValeurOuABSENT
Contient(cle) : Booleen
Supprimer(cle) : Booleen
Taille() : Entier
EstVide() : Booleen
FinTADOpération | Résultat attendu | Coût moyen visé |
|---|---|---|
| Inserer | Ajoute une nouvelle association ou met à jour la valeur. | O(1) |
| Rechercher | Retourne la valeur associée ou signale l’absence. | O(1) |
| Contient | Indique si la clé existe. | O(1) |
| Supprimer | Retire l’association. | O(1) |
| Parcourir toutes les entrées | Visite les n associations. | O(n + m) |
Les coûts O(1) sont des coûts moyens sous l’hypothèse d’une bonne répartition. Dans le pire cas, toutes les clés peuvent se concentrer dans la même zone et conduire à un coût O(n).
5.1.5 Hachage et adressage direct
Dans l’adressage direct, chaque clé possible correspond à une case distincte. Cette approche est excellente si l’univers des clés est petit et dense, par exemple les jours du mois ou les codes compris entre 0 et 999. Elle devient gaspilleuse lorsque les clés possibles sont nombreuses mais que peu d’entre elles sont réellement présentes.
Critère | Adressage direct | Table de hachage |
|---|---|---|
| Indice | La clé ou une transformation bijective. | Une valeur réduite produite par h. |
| Collisions | Aucune si l’univers est représenté. | Possibles et obligatoirement gérées. |
| Mémoire | Proportionnelle à l’univers des clés. | Proportionnelle à la capacité choisie. |
| Accès | O(1) garanti. | O(1) moyen, O(n) au pire. |
| Usage | Clés petites et denses. | Clés nombreuses, complexes ou dispersées. |
5.2 Propriétés d’une fonction de hachage
La fonction de hachage joue un rôle central. Une table correctement dimensionnée peut être inefficace si la fonction concentre les clés dans quelques buckets. À l’inverse, une fonction adaptée répartit les entrées et limite la longueur des recherches locales.
5.2.1 Déterminisme
Pour une même clé, la fonction doit toujours produire le même résultat tant que la table utilise les mêmes règles. Sans déterminisme, une clé insérée dans une case pourrait être recherchée dans une autre.
| Invariant — Si a = b selon la relation d’égalité des clés, alors Hacher(a) doit être égal à Hacher(b). La réciproque n’est pas exigée : deux clés différentes peuvent avoir le même hash. |
5.2.2 Rapidité
La fonction est évaluée à chaque recherche, insertion ou suppression. Elle doit donc rester beaucoup moins coûteuse que le traitement évité. Pour une chaîne de longueur L, un coût O(L) est habituel, car les caractères utilisés doivent être lus au moins une fois.
- Éviter des conversions ou allocations inutiles.
- Ne pas trier les composants d’une clé pour calculer son hash.
- Mettre en cache le hash d’un objet immuable si le langage et le contexte le permettent.
- Limiter le nombre de champs hachés aux champs qui définissent réellement l’identité.
5.2.3 Répartition uniforme
Une bonne fonction distribue des clés réalistes sur l’ensemble des indices. Elle doit exploiter les différences entre les clés, y compris lorsque celles-ci suivent un motif régulier : numéros multiples de 10, identifiants partageant un préfixe ou mots proches.
Situation | Fonction fragile | Conséquence | Amélioration |
|---|---|---|---|
| Clés multiples de 10, m = 10 | h(k) = k MOD 10 | Toutes les clés vont en 0. | Choisir une taille adaptée ou mieux mélanger les bits. |
| Mots avec même première lettre | h(s) = code(s[0]) | Un bucket très chargé. | Combiner tous les caractères. |
| Identifiants séquentiels | Prendre seulement les bits faibles | Motif répétitif si m est une puissance de 2. | Mélange multiplicatif ou hash du langage. |
5.2.4 Limitation des collisions
Aucune fonction ne peut supprimer toutes les collisions lorsque le nombre de clés possibles dépasse le nombre de cases. Ce résultat découle du principe des tiroirs : au moins deux clés doivent partager un bucket. L’objectif est donc de rendre les collisions rares et équilibrées.
| À retenir — Une collision n’est pas une erreur. C’est un événement normal que la structure doit traiter sans confondre les clés. |
5.2.5 Exemple de hachage d’une chaîne
Une méthode pédagogique consiste à parcourir les caractères et à accumuler un résultat polynomial. La constante multiplicative, souvent un petit nombre premier, propage l’influence des caractères précédents.
Fonction de hachage polynomial
Fonction HacherChaine(s, m) : Entier
h <- 0
Pour chaque caractere c de s Faire
h <- (h * 31 + Code(c)) MOD m
FinPour
Retourner h
FinFonctionLe modulo effectué à chaque étape empêche la croissance excessive de h. Dans une bibliothèque réelle, on utilise généralement la fonction de hachage fournie par le langage, car elle est testée, optimisée et cohérente avec les règles d’égalité du type.
5.2.6 Évaluer expérimentalement une fonction
Un test simple consiste à insérer un échantillon représentatif, puis à mesurer la taille de chaque bucket. Une distribution parfaite n’est pas nécessaire, mais les écarts extrêmes doivent alerter.
Mesure | Interprétation |
|---|---|
| Nombre de buckets vides | Trop élevé malgré un facteur de charge important : répartition douteuse. |
| Longueur maximale | Indique la recherche la plus défavorable avec chaînage. |
| Longueur moyenne des buckets non vides | Montre la concentration réelle des clés. |
| Variance des tailles | Une variance élevée signale une répartition irrégulière. |
| Nombre de collisions | Permet de comparer plusieurs fonctions sur les mêmes données. |
5.2.7 Hachage de table et hachage cryptographique
Critère | Hachage pour table | Hachage cryptographique |
|---|---|---|
| But | Répartir rapidement des clés. | Produire une empreinte difficile à inverser ou à falsifier. |
| Priorité | Vitesse et distribution. | Résistance aux collisions et aux attaques. |
| Exemples d’usage | Dictionnaires, ensembles, caches. | Intégrité, signatures, stockage sécurisé des mots de passe avec sel et fonction adaptée. |
| Interchangeabilité | Non : une fonction cryptographique est souvent inutilement coûteuse. | Non : une fonction de table n’est pas sûre pour les mots de passe. |
| Sécurité — Ne jamais utiliser une fonction de hachage de table comme mécanisme de protection d’un mot de passe. Les objectifs et les garanties sont différents. |
5.3 Gestion des collisions
Lorsque deux clés différentes obtiennent le même indice initial, la table doit conserver les deux associations et vérifier l’égalité des clés lors de la recherche. Deux grandes familles de solutions sont utilisées : le chaînage séparé et l’adressage ouvert.
5.3.1 Chaînage séparé
Chaque bucket contient une collection, souvent une liste chaînée, de toutes les entrées dont l’indice initial est identique. La table stocke alors m références de tête de liste.
Exemple pour h(k) = k MOD 7 après insertion des clés 19, 26, 12, 20 et 8 :
Indice | Contenu du bucket |
|---|---|
0 | ∅ |
1 | 8 |
2 | ∅ |
3 | ∅ |
4 | ∅ |
5 | 19 -> 26 -> 12 |
6 | 20 |
Recherche par chaînage
Fonction Rechercher(T, cle) : ValeurOuABSENT
i <- Hacher(cle) MOD T.capacite
courant <- T.buckets[i]
TantQue courant != NULL Faire
Si courant.cle = cle Alors
Retourner courant.valeur
FinSi
courant <- courant.suivant
FinTantQue
Retourner ABSENT
FinFonctionInsertion ou mise à jour
Procedure Inserer(T, cle, valeur)
i <- Hacher(cle) MOD T.capacite
courant <- T.buckets[i]
TantQue courant != NULL Faire
Si courant.cle = cle Alors
courant.valeur <- valeur
Retourner
FinSi
courant <- courant.suivant
FinTantQue
nouveau <- NouveauNoeud(cle, valeur)
nouveau.suivant <- T.buckets[i]
T.buckets[i] <- nouveau
T.taille <- T.taille + 1
FinProcedureSuppression
Fonction Supprimer(T, cle) : Booleen
i <- Hacher(cle) MOD T.capacite
precedent <- NULL
courant <- T.buckets[i]
TantQue courant != NULL ET courant.cle != cle Faire
precedent <- courant
courant <- courant.suivant
FinTantQue
Si courant = NULL Alors Retourner FAUX
Si precedent = NULL Alors
T.buckets[i] <- courant.suivant
Sinon
precedent.suivant <- courant.suivant
FinSi
T.taille <- T.taille - 1
Retourner VRAI
FinFonction| Complexité — Si les n clés sont réparties uniformément dans m buckets, la longueur moyenne d’une chaîne vaut α = n/m. Les opérations coûtent alors O(1 + α) en moyenne. |
Avantages | Limites |
|---|---|
| Suppression directe dans la liste. | Mémoire supplémentaire pour les références. |
| Facteur de charge pouvant dépasser 1. | Localité mémoire moins bonne que dans un tableau compact. |
| Tolère mieux une capacité temporairement trop petite. | Pire cas O(n) si une chaîne concentre toutes les clés. |
| Implémentation conceptuellement simple. | Allocation dynamique fréquente selon la représentation. |
5.3.2 Adressage ouvert
Dans l’adressage ouvert, toutes les entrées sont stockées directement dans le tableau. Lorsqu’une case est occupée, une suite d’indices, appelée séquence de sondage, est examinée jusqu’à trouver la clé ou une case disponible.
Chaque case doit distinguer au moins trois états : VIDE, OCCUPÉE et SUPPRIMÉE. L’état SUPPRIMÉE, souvent appelé marqueur ou tombe, est indispensable pour ne pas interrompre une recherche qui doit poursuivre son sondage.
Recherche générique par adressage ouvert
Fonction Rechercher(T, cle) : ValeurOuABSENT
Pour j allant de 0 a T.capacite - 1 Faire
i <- IndiceSondage(cle, j, T.capacite)
Si T[i].etat = VIDE Alors Retourner ABSENT
Si T[i].etat = OCCUPEE ET T[i].cle = cle Alors
Retourner T[i].valeur
FinSi
FinPour
Retourner ABSENT
FinFonctionInsertion générique
Procedure Inserer(T, cle, valeur)
premiereTombe <- AUCUNE
Pour j allant de 0 a T.capacite - 1 Faire
i <- IndiceSondage(cle, j, T.capacite)
Si T[i].etat = OCCUPEE ET T[i].cle = cle Alors
T[i].valeur <- valeur
Retourner
FinSi
Si T[i].etat = SUPPRIMEE ET premiereTombe = AUCUNE Alors
premiereTombe <- i
FinSi
Si T[i].etat = VIDE Alors
cible <- i si premiereTombe = AUCUNE sinon premiereTombe
EcrireEntree(T[cible], cle, valeur)
T.taille <- T.taille + 1
Retourner
FinSi
FinPour
Signaler TABLE_PLEINE
FinProcedure5.3.3 Sondage linéaire
Le sondage linéaire examine les cases consécutives :
indice(j) = (h(cle) + j) MOD m, pour j = 0, 1, 2, …
Avec m = 7 et les clés 19, 26, 12, 20, 8, la suite d’insertions devient :
Clé | Indice initial | Cases testées | Case retenue |
|---|---|---|---|
19 | 5 | 5 | 5 |
26 | 5 | 5, 6 | 6 |
12 | 5 | 5, 6, 0 | 0 |
20 | 6 | 6, 0, 1 | 1 |
8 | 1 | 1, 2 | 2 |
Indice | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
Clé | 12 | 20 | 8 | VIDE | VIDE | 19 | 26 |
| Regroupement primaire — Le sondage linéaire crée des blocs de cases occupées. Les nouvelles clés dont l’indice tombe dans ou juste avant un bloc l’allongent, ce qui augmente le nombre moyen de sondages. |
5.3.4 Sondage quadratique
Le sondage quadratique augmente progressivement l’écart entre les cases examinées :
indice(j) = (h(cle) + c1·j + c2·j²) MOD m
Cette méthode réduit le regroupement primaire, mais deux clés ayant le même indice initial suivent encore la même séquence. De plus, selon m, c1 et c2, toutes les cases ne sont pas nécessairement visitées. Le dimensionnement et les constantes doivent donc être choisis avec soin.
j | Déplacement j² | Indice pour h(cle)=5 et m=11 |
|---|---|---|
0 | 0 | 5 |
1 | 1 | 6 |
2 | 4 | 9 |
3 | 9 | 3 |
4 | 16 | 10 |
5 | 25 | 8 |
5.3.5 Double hachage
Le double hachage utilise une seconde fonction pour calculer le pas de sondage :
indice(j) = (h1(cle) + j · h2(cle)) MOD m
Pour parcourir toutes les cases, le pas h2(cle) doit être premier avec m. Une pratique fréquente consiste à choisir m premier et à définir h2(cle) entre 1 et m − 1.
Exemple de fonctions
h1(cle) <- cle MOD m
h2(cle) <- 1 + (cle MOD (m - 1))
indice(j) <- (h1(cle) + j * h2(cle)) MOD mLe double hachage réduit fortement les regroupements, car deux clés partageant h1 ont de bonnes chances d’utiliser des pas différents.
5.3.6 Suppression et marqueurs
Dans une table à adressage ouvert, remettre immédiatement une case supprimée à l’état VIDE peut rendre certaines clés introuvables. Une recherche s’arrête sur VIDE ; elle doit au contraire continuer au-delà d’une case SUPPRIMÉE.
| Exemple — Si 19 est en case 5 et 26, ayant le même indice initial, est en case 6, marquer la case 5 VIDE après suppression de 19 ferait conclure à tort que 26 est absente. La case 5 doit être marquée SUPPRIMÉE. |
Un nombre excessif de marqueurs dégrade les recherches. Un redimensionnement ou une reconstruction périodique permet de les éliminer.
5.3.7 Comparaison des stratégies
Critère | Chaînage séparé | Sondage linéaire | Sondage quadratique | Double hachage |
|---|---|---|---|---|
| Stockage | Listes par bucket | Tableau | Tableau | Tableau |
| Suppression | Simple | Marqueur | Marqueur | Marqueur |
| Facteur de charge | Peut dépasser 1 | Doit rester nettement < 1 | Doit rester < 1 | Doit rester < 1 |
| Localité mémoire | Moyenne | Très bonne | Bonne | Bonne |
| Regroupement | Chaînes locales | Primaire important | Secondaire | Faible |
| Simplicité | Élevée | Très élevée | Moyenne | Moyenne |
5.4 Facteur de charge
5.4.1 Définition
Le facteur de charge mesure le rapport entre le nombre n d’entrées et la capacité m de la table :
α = n / m
Avec chaînage, α représente la longueur moyenne théorique des chaînes. Il peut dépasser 1. Avec adressage ouvert, α est la proportion de cases occupées et doit rester strictement inférieure à 1.
n | m | α | Interprétation |
|---|---|---|---|
30 | 100 | 0,30 | Table peu chargée. |
70 | 100 | 0,70 | Charge courante acceptable selon la stratégie. |
95 | 100 | 0,95 | Adressage ouvert fortement dégradé. |
150 | 100 | 1,50 | Possible avec chaînage : 1,5 entrée par bucket en moyenne. |
5.4.2 Influence sur les performances
Lorsque α augmente, les collisions deviennent plus fréquentes. Avec chaînage uniforme, une recherche coûte approximativement O(1 + α). Avec adressage ouvert, le nombre de sondages augmente très rapidement lorsque la table approche de la saturation.
Facteur de charge indicatif | Chaînage séparé | Adressage ouvert |
|---|---|---|
| α < 0,50 | Chaînes généralement très courtes. | Recherche et insertion rapides. |
| 0,50 ≤ α < 0,75 | Bon compromis fréquent. | Acceptable avec une bonne stratégie. |
| 0,75 ≤ α < 0,90 | Toujours utilisable, mais chaînes plus longues. | Dégradation sensible ; redimensionnement conseillé. |
| α ≥ 0,90 | Possible mais à surveiller. | Sondages nombreux ; insertion proche de l’échec. |
| Seuils — Les seuils exacts dépendent de l’implémentation. Une valeur de 0,70 ou 0,75 est souvent utilisée pédagogiquement pour déclencher l’agrandissement d’une table à adressage ouvert. |
5.4.3 Redimensionnement
Lorsque le seuil est dépassé, la table est remplacée par une table plus grande. Il ne suffit pas de copier les cases aux mêmes indices : comme l’indice dépend de m, toutes les entrées doivent être rehachées.
Procédure de redimensionnement
Procedure Redimensionner(T, nouvelleCapacite)
ancienneTable <- T.buckets
T.buckets <- TableauVide(nouvelleCapacite)
T.capacite <- nouvelleCapacite
T.taille <- 0
Pour chaque entree occupee de ancienneTable Faire
InsererSansRedimensionner(T, entree.cle, entree.valeur)
FinPour
FinProcedure1. Choisir une nouvelle capacité, souvent environ deux fois plus grande.
2. Créer une table vide avec la nouvelle capacité.
3. Parcourir toutes les anciennes entrées.
4. Recalculer leur indice avec la nouvelle capacité.
5. Insérer les entrées sans déclencher un nouveau redimensionnement.
| Coût amorti — Un redimensionnement coûte O(n), mais il est rare si la capacité croît géométriquement. Réparti sur de nombreuses insertions, le coût moyen amorti de l’insertion reste O(1). |
5.4.4 Choix de la capacité
- Avec une fonction modulo simple, une capacité première peut limiter certains motifs.
- Avec une puissance de deux, le calcul d’indice est rapide mais exige un bon mélange des bits.
- La capacité doit laisser une marge par rapport au nombre attendu d’entrées.
- Le rétrécissement peut être envisagé lorsque la table devient durablement très vide.
- Deux seuils distincts d’agrandissement et de réduction évitent des oscillations répétées.
5.4.5 Complexités récapitulatives
Opération | Moyenne avec bonne répartition | Pire cas | Remarque |
|---|---|---|---|
| Recherche | O(1) | O(n) | Chaîne unique ou sondage presque complet. |
| Insertion | O(1) amorti | O(n) | Redimensionnement ou collisions extrêmes. |
| Suppression | O(1) | O(n) | Recherche préalable de la clé. |
| Parcours | O(n + m) | O(n + m) | Les buckets ou cases doivent être examinés. |
| Redimensionnement | Rare | O(n) | Toutes les entrées sont réinsérées. |
Applications guidées
Application 1 — Annuaire
On souhaite associer une adresse électronique à une fiche contenant le nom et le numéro de téléphone. La clé est l’adresse normalisée, par exemple convertie en minuscules et débarrassée des espaces inutiles.
Opérations principales
Inserer(annuaire, email, fiche)
fiche <- Rechercher(annuaire, email)
Supprimer(annuaire, email)
Contient(annuaire, email)Une mise à jour réutilise la même clé et remplace la fiche. Il faut définir clairement si les adresses sont sensibles à la casse et appliquer la même normalisation lors de toutes les opérations.
Application 2 — Dictionnaire de mots
Un dictionnaire peut associer chaque mot à une définition, une fréquence, une catégorie grammaticale ou une liste de traductions. Les recherches exactes utilisent efficacement une table de hachage. En revanche, les recherches par préfixe sont mieux adaptées à un trie, qui sera étudié dans un niveau ultérieur.
Recherche d’un mot
mot <- Normaliser(LireChaine())
Si dictionnaire.Contient(mot) Alors
Afficher(dictionnaire.Rechercher(mot))
Sinon
Afficher("Mot absent")
FinSiApplication 3 — Comptage des occurrences
Chaque élément rencontré devient une clé et sa valeur représente le nombre d’occurrences. Cette technique remplace souvent une double boucle O(n²) par un parcours moyen O(n).
Comptage des mots
D <- DictionnaireVide()
Pour chaque mot du texte Faire
mot <- Normaliser(mot)
Si D.Contient(mot) Alors
D[mot] <- D[mot] + 1
Sinon
D.Inserer(mot, 1)
FinSi
FinPourTexte | Résultat |
|---|---|
| algo table algo hash table algo | {algo: 3, table: 2, hash: 1} |
| un deux deux trois trois trois | {un: 1, deux: 2, trois: 3} |
Application 4 — Détection de doublons
Un ensemble haché conserve les valeurs déjà rencontrées. Dès qu’un élément appartient à l’ensemble, un doublon est détecté.
Détecter un doublon
Fonction ContientDoublon(T) : Booleen
vus <- EnsembleVide()
Pour chaque x de T Faire
Si vus.Contient(x) Alors Retourner VRAI
vus.Ajouter(x)
FinPour
Retourner FAUX
FinFonction| Complexité — Le temps moyen est O(n) et la mémoire supplémentaire O(n). Une solution par tri coûte O(n log n) mais peut utiliser moins de mémoire selon le tri choisi. |
Application 5 — Mise en cache de résultats
Une cache mémorise le résultat d’un calcul associé à ses paramètres. Avant de recalculer, le programme consulte le dictionnaire. Cette technique, appelée mémoïsation dans un contexte algorithmique, évite les sous-problèmes répétés.
Fibonacci mémoïsé
Fonction Fibonacci(n, cache) : Entier
Si n <= 1 Alors Retourner n
Si cache.Contient(n) Alors Retourner cache[n]
resultat <- Fibonacci(n-1, cache) + Fibonacci(n-2, cache)
cache.Inserer(n, resultat)
Retourner resultat
FinFonctionLa table de hachage assure l’accès rapide aux résultats. Une cache réelle doit aussi définir une capacité, une politique d’expiration ou d’éviction et éventuellement une synchronisation entre plusieurs tâches.
Bonnes pratiques et erreurs fréquentes
Problème | Conséquence | Bonne pratique |
|---|---|---|
| Clé mutable | L’entrée devient introuvable après modification. | Utiliser des clés immuables ou des identifiants stables. |
| Égalité et hash incohérents | Deux clés égales peuvent être stockées séparément. | Garantir : clés égales ⇒ hash identique. |
| Table trop chargée | Collisions et sondages nombreux. | Suivre α et redimensionner avant saturation. |
| Fonction fondée sur une petite partie de la clé | Concentration dans quelques buckets. | Mélanger toutes les composantes significatives. |
| Suppression en mettant VIDE | Recherche interrompue trop tôt en adressage ouvert. | Utiliser le marqueur SUPPRIMÉE. |
| Ordre d’itération supposé | Résultats variables selon la capacité ou l’implémentation. | Ne pas dépendre de l’ordre sauf contrat explicite. |
| Hash de table utilisé pour les mots de passe | Aucune protection cryptographique. | Employer une fonction dédiée avec sel et paramètres de coût. |
| Robustesse — Dans un service exposé à des entrées non fiables, des clés choisies pour provoquer de nombreuses collisions peuvent dégrader les performances. Les bibliothèques modernes utilisent parfois une graine aléatoire ou des stratégies de protection. |
Travaux dirigés
TD 1 — Vocabulaire et calcul d’indices
On utilise une table de taille 11 avec h(k) = k MOD 11. Pour les clés 24, 35, 18, 29, 46 et 57 :
- calculer l’indice initial de chaque clé ;
- identifier les collisions ;
- représenter la table avec chaînage séparé, en insérant en tête.
TD 2 — Qualité d’une fonction
On souhaite stocker les matricules 20260010, 20260020, 20260030, … dans une table de taille 10. Étudier h1(k) = k MOD 10, puis proposer une amélioration et justifier le choix.
TD 3 — Chaînage séparé
Insérer les clés 14, 21, 28, 6, 13, 20 dans une table de taille 7 avec h(k) = k MOD 7. Effectuer ensuite la recherche de 13, la recherche de 15 et la suppression de 21. Donner les listes après chaque étape importante.
TD 4 — Sondage linéaire
Dans une table de taille 11, insérer 22, 1, 13, 11, 24, 33 avec h(k) = k MOD 11 et sondage linéaire. Indiquer les cases testées et le nombre total de sondages.
TD 5 — Suppression en adressage ouvert
À partir de la table du TD 4, supprimer 22 puis rechercher 33. Expliquer pourquoi l’état SUPPRIMÉE est nécessaire et montrer l’erreur obtenue si la case de 22 devient VIDE.
TD 6 — Sondage quadratique et double hachage
Pour m = 13 et les clés 18, 31, 44 et 57, comparer :
- le sondage quadratique indice(j) = (h(k) + j²) MOD 13 ;
- le double hachage avec h1(k) = k MOD 13 et h2(k) = 1 + (k MOD 12).
TD 7 — Facteur de charge
Une table à adressage ouvert possède une capacité de 64 cases et contient 43 entrées. Calculer α. Un seuil de 0,70 est utilisé : déterminer si la prochaine insertion doit provoquer un redimensionnement. Proposer une nouvelle capacité et expliquer le rehachage.
TD 8 — Fréquences de mots
Concevoir un algorithme qui lit une liste de mots, ignore la casse, compte les occurrences et affiche les mots apparaissant au moins trois fois. Donner la complexité moyenne.
TD 9 — Annuaire
Définir le TAD d’un annuaire indexé par adresse électronique. Préciser les règles de normalisation, les préconditions, les erreurs possibles et les tests minimaux.
TD 10 — Choix d’une stratégie
Pour chacun des contextes suivants, proposer une stratégie et justifier :
- grand nombre d’insertions et suppressions, taille difficile à prévoir ;
- table compacte, principalement des recherches, capacité connue ;
- données sensibles à la localité mémoire ;
- fonction de hachage imparfaite produisant des groupes de clés.
Corrigés indicatifs des travaux dirigés
Correction du TD 1
Clé | Indice |
|---|---|
24 | 2 |
35 | 2 |
18 | 7 |
29 | 7 |
46 | 2 |
57 | 2 |
Les collisions concernent le bucket 2 (24, 35, 46, 57) et le bucket 7 (18, 29). Avec insertion en tête : bucket 2 = 57 -> 46 -> 35 -> 24 et bucket 7 = 29 -> 18.
Correction du TD 2
Tous les matricules se terminent par 0, donc h1(k) = 0 pour toutes les clés : la fonction est très mauvaise sur ces données. Une amélioration consiste à diviser par 10 avant le modulo ou, de préférence, à utiliser un mélange de tous les chiffres puis un modulo avec une capacité non liée au motif, par exemple un nombre premier.
Exemple pédagogique
h2(k) <- ((k DIV 10) * 2654435761) MOD m
L’objectif n’est pas la constante particulière, mais la rupture du motif régulier des derniers chiffres.
Correction du TD 3
Les clés 14, 21 et 28 vont dans le bucket 0 ; les clés 6, 13 et 20 vont dans le bucket 6. Avec insertion en tête :
Bucket | Chaîne après insertion |
|---|---|
0 | 28 -> 21 -> 14 |
6 | 20 -> 13 -> 6 |
La recherche de 13 parcourt 20 puis trouve 13. La recherche de 15 examine le bucket 1, vide. Après suppression de 21, le bucket 0 devient 28 -> 14.
Correction du TD 4
Clé | h(k) | Cases testées | Position |
|---|---|---|---|
22 | 0 | 0 | 0 |
1 | 1 | 1 | 1 |
13 | 2 | 2 | 2 |
11 | 0 | 0, 1, 2, 3 | 3 |
24 | 2 | 2, 3, 4 | 4 |
33 | 0 | 0, 1, 2, 3, 4, 5 | 5 |
Le nombre total de sondages est 1 + 1 + 1 + 4 + 3 + 6 = 16. L’exemple met en évidence un regroupement primaire dans les cases 0 à 5.
Correction du TD 5
La case 0 est marquée SUPPRIMÉE. Pour rechercher 33, on ne s’arrête donc pas en 0 : les cases 1, 2, 3, 4 puis 5 sont examinées et 33 est trouvé. Si la case 0 était marquée VIDE, la recherche conclurait immédiatement à l’absence de 33, ce qui serait faux.
Correction du TD 6
Toutes les clés ont h1(k) = 5. Avec sondage quadratique, les positions successives sont 5, 6, 9 et 1 pour les quatre clés. Avec double hachage, les pas diffèrent :
Clé | h1 | h2 = 1 + k MOD 12 | Position trouvée |
|---|---|---|---|
18 | 5 | 7 | 5 |
31 | 5 | 8 | 0 |
44 | 5 | 9 | 1 |
57 | 5 | 10 | 2 |
Le double hachage répartit les clés sur des séquences différentes, ce qui limite le regroupement secondaire.
Correction du TD 7
α = 43 / 64 = 0,671875. Après une insertion, α = 44 / 64 = 0,6875, encore inférieur à 0,70. La prochaine insertion ne déclenche donc pas nécessairement le redimensionnement. La 45e entrée donnerait 45 / 64 ≈ 0,703, ce qui franchit le seuil. Une capacité de 128 ou une capacité première voisine peut être choisie. Toutes les entrées doivent être rehachées avec la nouvelle capacité.
Correction du TD 8
Comptage et filtrage
D <- DictionnaireVide()
Pour chaque mot lu Faire
mot <- Minuscules(mot)
D[mot] <- D.ObtenirOuDefaut(mot, 0) + 1
FinPour
Pour chaque (mot, compteur) de D Faire
Si compteur >= 3 Alors Afficher(mot, compteur)
FinPourLe premier parcours coûte O(n) en moyenne. Le parcours du dictionnaire coûte O(u + m), où u est le nombre de mots distincts et m la capacité interne ; on retient généralement O(n + u) dans une analyse abstraite.
Correction du TD 9
La clé est l’adresse normalisée en minuscules, après suppression des espaces externes. L’interface minimale contient InsererOuModifier, Rechercher, Supprimer, Contient et Taille. Une adresse vide est refusée ; une recherche absente retourne ABSENT ou déclenche une erreur documentée. Les tests portent sur une insertion, une mise à jour, une collision, une suppression, une adresse absente et deux écritures différentes d’une même adresse normalisée.
Correction du TD 10
Contexte | Choix possible | Justification |
|---|---|---|
| Insertions et suppressions nombreuses, taille imprévisible | Chaînage séparé | Suppression simple et tolérance à α > 1. |
| Table compacte, recherches dominantes, capacité connue | Adressage ouvert / double hachage | Bonne localité et faible surcharge mémoire. |
| Localité mémoire prioritaire | Sondage linéaire | Accès à des cases voisines, favorable aux caches processeur. |
| Fonction imparfaite et groupes de clés | Double hachage ou amélioration de la fonction | Réduit les séquences identiques et les amas. |
Travail pratique — Implémenter une table de hachage
Objectif
Réaliser un dictionnaire générique fondé sur le chaînage séparé, puis comparer ses performances avec une variante à adressage ouvert. L’implémentation peut être traduite en Python, C, Java ou dans un autre langage étudié.
Spécification minimale
- Créer une table avec une capacité initiale configurable.
- Insérer une association clé-valeur et mettre à jour une clé existante.
- Rechercher et supprimer une clé.
- Retourner la taille et le facteur de charge.
- Agrandir automatiquement la table à partir d’un seuil.
- Conserver des statistiques : collisions, longueur maximale des chaînes ou nombre de sondages.
- Fournir un programme de démonstration utilisant un annuaire ou un compteur de mots.
Étapes proposées
1. Définir les types Entrée et TableHachage.
2. Implémenter une fonction d’indice à partir du hash de la clé.
3. Écrire Inserer, Rechercher, Contient et Supprimer.
4. Ajouter le calcul du facteur de charge.
5. Implémenter Redimensionner et vérifier que toutes les entrées restent accessibles.
6. Instrumenter le code pour compter les collisions.
7. Tester avec des clés aléatoires et avec des clés provoquant volontairement des collisions.
8. Rédiger une analyse de complexité et une courte conclusion expérimentale.
Jeux de tests obligatoires
Test | Action | Résultat attendu |
|---|---|---|
| Table vide | Rechercher une clé | ABSENT sans erreur inattendue |
| Insertion simple | Insérer A -> 10 | Rechercher A retourne 10 |
| Mise à jour | Insérer A -> 20 | Taille inchangée, valeur = 20 |
| Collision | Insérer deux clés de même indice | Les deux restent accessibles |
| Suppression | Supprimer une clé présente | Clé absente et taille décrémentée |
| Suppression absente | Supprimer une clé inconnue | Retour FAUX ou erreur documentée |
| Redimensionnement | Dépasser le seuil | Capacité augmente, toutes les clés sont retrouvées |
| Charge élevée | Comparer avant/après agrandissement | Réduction des chaînes ou sondages moyens |
Extension — Variante à adressage ouvert
- Utiliser les états VIDE, OCCUPÉE et SUPPRIMÉE.
- Implémenter le sondage linéaire ou le double hachage.
- Mesurer le nombre moyen de sondages pour plusieurs facteurs de charge.
- Comparer la consommation mémoire, le temps et la simplicité avec le chaînage.
Grille d’évaluation indicative
Critère | Points |
|---|---|
Correction des opérations fondamentales | 6 |
Gestion des collisions | 4 |
Redimensionnement et facteur de charge | 3 |
Jeux de tests et cas limites | 3 |
Analyse de complexité et mesures | 2 |
Qualité du code et documentation | 2 |
Synthèse du chapitre
- Une table de hachage transforme une clé en un indice de stockage.
- Une fonction de hachage doit être déterministe, rapide et bien répartir les clés.
- Les collisions sont normales et doivent être gérées sans confondre les clés.
- Le chaînage séparé stocke une collection par bucket ; l’adressage ouvert sonde d’autres cases.
- Le sondage linéaire est simple mais crée des regroupements ; le double hachage les réduit.
- La suppression en adressage ouvert nécessite un état SUPPRIMÉE.
- Le facteur de charge α = n/m influence directement les performances.
- Le redimensionnement rehache toutes les entrées et permet de conserver un coût moyen O(1).
- Les tables de hachage sont adaptées aux annuaires, dictionnaires, ensembles, compteurs et caches.
- Le pire cas reste O(n), notamment si les clés sont mal réparties.
Glossaire
Terme | Définition |
|---|---|
| Bucket | Case ou collection associée à un indice de la table. |
| Clé | Information utilisée pour identifier une entrée. |
| Collision | Situation où deux clés différentes produisent le même indice initial. |
| Facteur de charge | Rapport α = nombre d’entrées / capacité. |
| Fonction de hachage | Fonction transformant une clé en une valeur numérique. |
| Rehachage | Réinsertion des entrées après changement de capacité ou de fonction. |
| Sondage | Suite des cases examinées en adressage ouvert. |
| Tombe / marqueur | État indiquant qu’une case a été occupée puis supprimée. |
| Chaînage séparé | Gestion des collisions par une collection distincte dans chaque bucket. |
| Double hachage | Sondage dont le pas est calculé par une seconde fonction. |
Auto-évaluation
Je suis capable de… | Oui | À revoir |
|---|---|---|
distinguer clé, valeur, hash et indice. | ☐ | ☐ |
expliquer pourquoi les collisions sont inévitables. | ☐ | ☐ |
tracer une insertion par chaînage séparé. | ☐ | ☐ |
tracer un sondage linéaire, quadratique ou double. | ☐ | ☐ |
expliquer le rôle du marqueur SUPPRIMÉE. | ☐ | ☐ |
calculer le facteur de charge. | ☐ | ☐ |
décrire un redimensionnement correct. | ☐ | ☐ |
analyser les coûts moyens et les pires cas. | ☐ | ☐ |
choisir une stratégie de collisions selon le contexte. | ☐ | ☐ |
concevoir une application fondée sur un dictionnaire ou un ensemble. | ☐ | ☐ |
| Conclusion — Les tables de hachage illustrent un principe essentiel de l’algorithmique : investir dans une représentation et une fonction de répartition adaptées afin de réduire fortement le coût des opérations les plus fréquentes. |