Bonjour,
Dans notre leçon précédente, nous avons jeté les bases théoriques des tables de hachage. Nous avons compris que leur efficacité repose sur une fonction de hachage qui transforme une clé en indice, et que les collisions inévitables peuvent être gérées élégamment par la méthode du chaînage séparé, où chaque case du tableau principal pointe vers une liste chaînée.
Aujourd'hui, nous passons de la théorie à la pratique. L'objectif est de mettre en œuvre ce que nous avons appris. Concrètement, nous allons implémenter les opérations d'une table de hachage avec chaînage en Python. Nous construirons une classe HashTable de A à Z, en y intégrant les opérations fondamentales : insertion, recherche et suppression. Cette leçon est l'occasion parfaite de réutiliser notre maîtrise des listes chaînées acquise précédemment.
1. Préparation : Simulation manuelle
Avant de nous lancer dans le code, assurons-nous que la mécanique du chaînage est parfaitement claire. Une simulation manuelle est un excellent exercice pour cela.
Considérez l'exercice suivant, inspiré d'une ressource que nous allons utiliser.
Scénario :
- Vous disposez d'une table de hachage de taille .
- Votre fonction de hachage est .
- Vous devez insérer les clés suivantes dans l'ordre : 7, 12, 17, 22, 3, 8.
Pour chaque clé, calculez l'indice et décrivez l'état de la table (quels éléments se trouvent dans chaque "seau" ou liste chaînée).
Prenez un instant pour le faire sur papier. Vous pouvez ensuite comparer votre résultat avec la solution ci-dessous.
Solution de la simulation
- Clé 7 : . Le seau à l'indice 2 contient
[7]. - Clé 12 : . Collision ! On ajoute 12 à la liste. Le seau 2 contient
[7, 12]. - Clé 17 : . Collision ! Le seau 2 contient
[7, 12, 17]. - Clé 22 : . Collision ! Le seau 2 contient
[7, 12, 17, 22]. - Clé 3 : . Le seau 3 contient
[3]. - Clé 8 : . Collision ! Le seau 3 contient
[3, 8].
État final de la table :
- Indice 0 :
[] - Indice 1 :
[] - Indice 2 :
[7, 12, 17, 22] - Indice 3 :
[3, 8] - Indice 4 :
[]
Cet exercice simple, tiré du document "EXERCICES DE COURS : TABLES ET FONCTIONS DE HACHAGE" (LINK), illustre parfaitement le regroupement des clés dans les seaux suite aux collisions.
2. Architecture de notre table de hachage
Pour une implémentation propre, nous allons utiliser une approche orientée objet. Nous aurons besoin de deux classes :
- Une classe
Nodepour représenter les maillons de nos listes chaînées. Chaque nœud stockera une paire(clé, valeur)et un pointeurnext. - Une classe
HashTablequi encapsulera la logique principale : le tableau sous-jacent (une liste Python), la fonction de hachage, et les méthodesinsert,searchetremove.

L'article suivant sera notre guide principal pour cette implémentation. Il détaille la structure et la logique de chaque méthode de manière très claire.
Implementation of Hash Table in Python using Separate Chaining
Cet article de GeeksforGeeks fournit une implémentation complète et bien expliquée d'une table de hachage avec chaînage en Python. Il détaille la création des classes Node et HashTable et la logique de chaque opération.
Veuillez lire l'article en vous concentrant sur la structure des classes et l'algorithme de chaque méthode (insert, search, remove). Notez bien comment le parcours de la liste chaînée est au cœur de chaque opération pour gérer les collisions.
3. Implémentation des opérations
Basé sur la lecture précédente, décomposons la logique de chaque méthode.
Initialisation et Hachage
Le constructeur de notre classe HashTable doit initialiser un tableau (une liste Python) d'une capacité donnée, rempli de None pour indiquer que tous les seaux sont initialement vides.
class HashTable:
def __init__(self, capacity):
self.capacity = capacity
self.size = 0
self.table = [None] * capacity
La fonction de hachage privée, _hash, utilisera la fonction native hash() de Python, qui gère différents types de données, et appliquera l'opération modulo pour s'assurer que l'indice est bien dans les bornes de notre tableau.
def _hash(self, key):
return hash(key) % self.capacity
Insertion (insert)
L'algorithme d'insertion est le suivant :
- Calculer l'indice
index = self._hash(key). - Accéder au seau
self.table[index]. - Cas 1 : Le seau est vide (
None). On crée un nouveauNode(key, value)et on le place à cet indice. On incrémente la taille. - Cas 2 : Le seau contient déjà une liste chaînée (collision).
a. On parcourt la liste chaînée à partir de la tête.
b. À chaque nœud, on vérifie si sa clé est égale à lakeyque l'on souhaite insérer. Si oui, cela signifie que nous mettons à jour une valeur existante. On modifie lavaluedu nœud et on arrête l'opération.
c. Si on atteint la fin de la liste sans trouver la clé, cela signifie que c'est une nouvelle clé qui a collisionné. On crée un nouveauNode(key, value)et on l'insère en tête de la liste chaînée. (L'insertion en tête est en ). On incrémente la taille.
Recherche (search)
L'algorithme de recherche est plus simple :
- Calculer l'indice
index = self._hash(key). - Accéder au seau
self.table[index]. - Si le seau est vide (
None), la clé n'existe pas. On retourneNone(ou on lève une exception). - Si le seau contient une liste, on la parcourt. Pour chaque nœud, on compare sa clé avec la
keyrecherchée. - Si on trouve une correspondance, on retourne la
valueassociée. - Si on atteint la fin de la liste sans trouver la clé, elle n'est pas dans la table.
Suppression (remove)
La suppression est l'opération la plus délicate, car elle requiert une manipulation des pointeurs de la liste chaînée.
- Calculer l'indice
index = self._hash(key). - Accéder au seau
self.table[index]. - On parcourt la liste chaînée en gardant une référence au nœud
currentet au nœudprevious. - À chaque nœud, on compare sa clé avec la
keyà supprimer. - Si on trouve la clé :
a. Si le nœud à supprimer est la tête de la liste (previousestNone). On met à jour la tête de la liste dans le tableau principal :self.table[index] = current.next.
b. Sinon (le nœud est au milieu ou à la fin). On "court-circuite" le nœudcurrenten faisant pointerprevious.nextverscurrent.next.
c. On décrémente la taille et on arrête l'opération. - Si la fin de la liste est atteinte sans trouver la clé, on ne fait rien (ou on lève une exception).
4. Exercice de programmation
Il est temps de mettre tout cela en code. Voici le squelette de nos classes. Votre tâche est de compléter les méthodes insert, search, et remove dans la classe HashTable.
class Node:
"""Nœud pour la liste chaînée dans chaque seau."""
def __init__(self, key, value):
self.key = key
self.value = value
self.next = None
class HashTable:
"""Table de hachage avec résolution des collisions par chaînage séparé."""
def __init__(self, capacity=10):
self.capacity = capacity
self.size = 0
self.table = [None] * capacity
def _hash(self, key):
"""Calcule l'indice pour une clé donnée."""
return hash(key) % self.capacity
def insert(self, key, value):
"""Insère une paire (clé, valeur) ou met à jour la valeur si la clé existe."""
# VOTRE CODE ICI
# 1. Calculer l'indice
# 2. Vérifier si le seau est vide
# 3. Sinon, parcourir la liste pour trouver la clé (mise à jour)
# ou l'ajouter en tête (nouvelle insertion)
pass
def search(self, key):
"""Recherche une clé et retourne sa valeur, ou None si non trouvée."""
# VOTRE CODE ICI
# 1. Calculer l'indice
# 2. Parcourir la liste chaînée à cet indice
# 3. Retourner la valeur si la clé est trouvée
pass
def remove(self, key):
"""Supprime une clé de la table."""
# VOTRE CODE ICI
# 1. Calculer l'indice
# 2. Parcourir la liste avec des pointeurs 'previous' et 'current'
# 3. Gérer le cas où la clé est en tête
# 4. Gérer le cas où la clé est ailleurs dans la liste
pass
def __str__(self):
"""Affichage pour le débogage."""
elements = []
for i in range(self.capacity):
current = self.table[i]
while current:
elements.append((current.key, current.value))
current = current.next
return str(elements)
# Test
ht = HashTable()
ht.insert("France", "Paris")
ht.insert("Japon", "Tokyo")
ht.insert("Espagne", "Madrid") # Potentielle collision selon la fonction de hachage
print(ht)
print(f"Capitale du Japon : {ht.search('Japon')}")
ht.insert("Japon", "Kyoto") # Mise à jour
print(f"Nouvelle capitale du Japon : {ht.search('Japon')}")
ht.remove("France")
print(f"Après suppression de la France : {ht}")
Prenez le temps nécessaire pour implémenter ces méthodes. N'hésitez pas à vous référer à l'article de GeeksforGeeks (LINK) si vous êtes bloqué sur la logique de manipulation des pointeurs.
Proposition d'implémentation
class Node:
"""Nœud pour la liste chaînée dans chaque seau."""
def __init__(self, key, value):
self.key = key
self.value = value
self.next = None
class HashTable:
"""Table de hachage avec résolution des collisions par chaînage séparé."""
def __init__(self, capacity=10):
self.capacity = capacity
self.size = 0
self.table = [None] * capacity
def _hash(self, key):
"""Calcule l'indice pour une clé donnée."""
return hash(key) % self.capacity
def insert(self, key, value):
"""Insère une paire (clé, valeur) ou met à jour la valeur si la clé existe."""
index = self._hash(key)
current = self.table[index]
while current:
if current.key == key:
current.value = value
return
current = current.next
# Si la clé n'a pas été trouvée, on insère un nouveau noeud en tête
new_node = Node(key, value)
new_node.next = self.table[index]
self.table[index] = new_node
self.size += 1
def search(self, key):
"""Recherche une clé et retourne sa valeur, ou None si non trouvée."""
index = self._hash(key)
current = self.table[index]
while current:
if current.key == key:
return current.value
current = current.next
return None
def remove(self, key):
"""Supprime une clé de la table."""
index = self._hash(key)
previous = None
current = self.table[index]
while current:
if current.key == key:
if previous:
previous.next = current.next
else: # Le noeud à supprimer est la tête
self.table[index] = current.next
self.size -= 1
return
previous = current
current = current.next
# Si on arrive ici, la clé n'a pas été trouvée. On peut lever une exception.
raise KeyError(f"Key '{key}' not found.")
def __str__(self):
"""Affichage pour le débogage."""
elements = []
for i in range(self.capacity):
current = self.table[i]
if current:
chain_str = []
while current:
chain_str.append(f"({current.key}: {current.value})")
current = current.next
elements.append(f"Seau {i}: -> " + " -> ".join(chain_str))
return "\n".join(elements)
# Test
ht = HashTable()
ht.insert("France", "Paris")
ht.insert("Japon", "Tokyo")
ht.insert("Allemagne", "Berlin") # Peut entrer en collision avec Japon
ht.insert("Espagne", "Madrid")
print(ht)
print("-" * 20)
print(f"Capitale de l'Allemagne : {ht.search('Allemagne')}")
ht.insert("Japon", "Kyoto") # Mise à jour
print(f"Nouvelle capitale du Japon : {ht.search('Japon')}")
print("-" * 20)
ht.remove("Allemagne")
print(f"Après suppression de l'Allemagne :\n{ht}")
Conclusion
Félicitations ! Vous avez implémenté une table de hachage fonctionnelle à partir de zéro. Cette structure est au cœur de nombreux algorithmes et systèmes performants, et sa compréhension en profondeur est un atout majeur.
Les points clés de notre implémentation sont :
- Une classe
Nodepour construire les chaînes. - Une classe
HashTablequi orchestre un tableau de "seaux". - Les méthodes
insert,searchetremovequi opèrent toutes en deux temps :- Calcul du hachage pour trouver le bon seau en .
- Parcours de la liste chaînée dans ce seau, dont la durée dépend de la longueur de la chaîne.
Notre implémentation a une capacité fixe. Dans un système réel, si le nombre d'éléments augmente trop, les chaînes deviennent longues et les performances se dégradent. La solution est de redimensionner la table lorsque le facteur de charge dépasse un certain seuil.
Cette opération de redimensionnement est coûteuse ponctuellement, mais sa fréquence est faible. Dans notre prochaine leçon, nous aborderons la complexité amortie, l'outil mathématique essentiel pour analyser formellement le coût moyen d'opérations comme celle-ci dans des structures de données dynamiques, comme le tableau dynamique ou notre future table de hachage redimensionnable.