Clé → Hash → Index → Valeur. Instantané.
Une hash table (table de hachage) est une structure de données qui stocke des paires clé → valeur et permet d'accéder à n'importe quelle valeur en O(1) — temps constant, peu importe la taille de la table. Elle utilise une fonction de hachage pour convertir une clé (string, int...) en un index de tableau.
Imagine un vestiaire de stade. Tu arrives avec ton manteau ("France"), le vestiairier prend ton nom, calcule un numéro de casier (la fonction de hachage), et range ton manteau directement dans ce casier. Quand tu veux récupérer "France", il recalcule le même numéro et va direct au bon casier — sans chercher dans tous les casiers. C'est ça O(1).
L'identifiant unique. Ex: "name", "age"
Convertit la clé en index. Doit être déterministe
La donnée associée à la clé. Ex: "Alice", 42
ht_set(table, "name", "Alice")
↓
hash("name") % table_size = 3 ← calculer l'index
↓
table->array[3] = node{"name", "Alice"} ← stocker
ht_get(table, "name")
↓
hash("name") % table_size = 3 ← même calcul → même index
↓
table->array[3]->value = "Alice" ← accès direct O(1) !
/* ── 1. Le nœud (une paire clé/valeur) ─────────────────────── */
typedef struct hash_node_s
{
char *key; /* la clé — une string */
char *value; /* la valeur — une string */
struct hash_node_s *next; /* chaînage en cas de collision */
} hash_node_t;
/* ── 2. La table elle-même ──────────────────────────────────── */
typedef struct hash_table_s
{
unsigned long int size; /* nombre de cases dans le tableau */
hash_node_t **array; /* tableau de pointeurs vers des nœuds */
} hash_table_t;
Visualisation de la structure en mémoire :
hash_table_t
┌──────────┬──────────────────────────────────────────┐
│ size = 8 │ array (pointeur vers tableau de ptr) │
└──────────┴──────────────────────────────────────────┘
│
▼
array[0] → NULL
array[1] → NULL
array[2] → [key:"age" | val:"25" | next:NULL]
array[3] → [key:"name" | val:"Alice"| next:NULL]
array[4] → NULL
array[5] → [key:"city" | val:"Paris"| next:NULL]
array[6] → NULL
array[7] → NULL
/* ── FONCTION SIMPLE — somme des ASCII ─────────────────────── */
unsigned long int hash_simple(const char *key, unsigned long int size)
{
unsigned long int hash = 0;
while (*key)
hash += *key++; /* somme des valeurs ASCII */
return (hash % size); /* modulo pour rester dans le tableau */
}
/* Problème : "ab" et "ba" donnent le même hash → trop de collisions */
/* ── FONCTION DJB2 — la référence (utilisée à Holberton) ─────── */
unsigned long int hash_djb2(const unsigned char *str)
{
unsigned long int hash = 5381; /* valeur magique initiale */
int c;
while ((c = *str++))
hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
return (hash);
}
/* Utilisation : index = hash_djb2(key) % table->size */
5381) et sa formule (hash * 33 + c). Elle distribue très bien les clés et minimise les collisions.Une collision se produit quand deux clés différentes donnent le même index après hachage. C'est inévitable (pigeonhole principle) — une bonne table doit savoir les gérer.
/* Exemple de collision */
hash("name") % 8 = 3
hash("mane") % 8 = 3 ← même index ! collision !
/* Les deux doivent cohabiter à l'index 3 */
Deux joueurs arrivent au vestiaire — le vestiairier calcule le numéro de casier pour chacun, et tombe sur le même casier 3. Solution : il enchaîne les affaires avec un crochet supplémentaire dans le casier (linked list). Pour retrouver "name", il ouvre le casier 3 et parcourt la chaîne jusqu'à trouver le bon.
La méthode utilisée à Holberton — chaque case du tableau contient une linked list de nœuds. En cas de collision, on ajoute en tête de liste.
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
hash_table_t *hash_table_create(unsigned long int size)
{
hash_table_t *ht;
unsigned long int i;
ht = malloc(sizeof(hash_table_t));
if (ht == NULL)
return (NULL);
ht->size = size;
ht->array = malloc(sizeof(hash_node_t *) * size);
if (ht->array == NULL)
{
free(ht);
return (NULL);
}
i = 0;
while (i < size)
ht->array[i++] = NULL; /* initialiser toutes les cases à NULL */
return (ht);
}
int hash_table_set(hash_table_t *ht, const char *key, const char *value)
{
hash_node_t *node;
hash_node_t *tmp;
unsigned long int idx;
if (!ht || !key || !value)
return (0);
idx = key_index((const unsigned char *)key, ht->size); /* calculer l'index */
/* ── Vérifier si la clé existe déjà → mise à jour ──────── */
tmp = ht->array[idx];
while (tmp != NULL)
{
if (strcmp(tmp->key, key) == 0) /* clé trouvée ! */
{
free(tmp->value);
tmp->value = strdup(value); /* mettre à jour la valeur */
return (tmp->value != NULL);
}
tmp = tmp->next;
}
/* ── Créer un nouveau nœud ──────────────────────────────── */
node = malloc(sizeof(hash_node_t));
if (!node)
return (0);
node->key = strdup(key); /* copier la clé */
node->value = strdup(value); /* copier la valeur */
if (!node->key || !node->value)
{
free(node->key);
free(node->value);
free(node);
return (0);
}
/* ── Insérer en TÊTE de liste ───────────────────────────── */
node->next = ht->array[idx]; /* le nouveau pointe vers l'ancien head */
ht->array[idx] = node; /* le nouveau devient le head */
return (1);
}
char *hash_table_get(const hash_table_t *ht, const char *key)
{
hash_node_t *node;
unsigned long int idx;
if (!ht || !key)
return (NULL);
idx = key_index((const unsigned char *)key, ht->size); /* même index */
node = ht->array[idx];
while (node != NULL)
{
if (strcmp(node->key, key) == 0) /* clé trouvée ! */
return (node->value);
node = node->next; /* parcourir la liste de collision */
}
return (NULL); /* clé absente */
}
get est O(1). En cas de collisions nombreuses, il devient O(n) sur la liste de collision. C'est pourquoi une bonne hash function est cruciale.void hash_table_print(const hash_table_t *ht)
{
hash_node_t *node;
unsigned long int i;
if (!ht)
return;
printf("{");
i = 0;
while (i < ht->size)
{
node = ht->array[i];
while (node != NULL)
{
printf("'%s': '%s', ", node->key, node->value);
node = node->next;
}
i++;
}
printf("}\n");
}
void hash_table_delete(hash_table_t *ht)
{
hash_node_t *node;
hash_node_t *tmp;
unsigned long int i;
if (!ht)
return;
i = 0;
while (i < ht->size)
{
node = ht->array[i];
while (node != NULL) /* libérer chaque nœud de la liste */
{
tmp = node->next; /* sauvegarder next avant free ! */
free(node->key); /* libérer la clé */
free(node->value); /* libérer la valeur */
free(node); /* libérer le nœud */
node = tmp;
}
i++;
}
free(ht->array); /* libérer le tableau */
free(ht); /* libérer la table */
}
/* Fonction helper utilisée par set et get */
unsigned long int key_index(const unsigned char *key,
unsigned long int size)
{
return (hash_djb2(key) % size);
/* ↑ hash_djb2 donne un grand nombre
↑ % size le ramène dans [0, size-1] */
}
/* Exemple :
hash_djb2("name") = 2090756233
2090756233 % 1024 = 393
→ "name" va dans table->array[393] */
unsigned long int potentiellement énorme. Le modulo (%) garantit que l'index est dans les limites du tableau [0, size-1].| Opération | Cas moyen | Pire cas | Pourquoi |
|---|---|---|---|
| set (insert) | O(1) | O(n) | Hash + insertion en tête = O(1). Pire cas = toutes les clés collisionnent |
| get (lookup) | O(1) | O(n) | Hash + accès direct = O(1). Pire cas = parcourir toute la liste |
| delete (node) | O(1) | O(n) | Hash + suppression = O(1) moyen |
| print (all) | O(n+m) | O(n+m) | n = taille table, m = nombre d'entrées — obligé de tout parcourir |
| delete (table) | O(n+m) | O(n+m) | Libérer chaque nœud de chaque liste |
| Recherche | O(1) | O(n) |
| Insertion | O(1) | O(1)* |
| Suppression | O(1) | O(n) |
| Recherche | O(1) | O(n) |
| Insertion | O(1) | O(1) |
| Accès par clé | O(1) | O(n) |
/* 1. Stocker le pointeur original */
node->key = key; /* ❌ si key change dehors */
/* → utiliser strdup(key) ✅ */
/* 2. Oublier le modulo */
idx = hash_djb2(key); /* ❌ index hors tableau */
/* → idx = hash_djb2(key) % size ✅ */
/* 3. Comparer les pointeurs au lieu des valeurs */
if (node->key == key) /* ❌ compare adresses */
/* → strcmp(node->key, key) == 0 ✅ */
/* 4. Libérer dans le mauvais ordre */
free(node);
free(node->key); /* ❌ node déjà libéré ! */
/* 5. Oublier d'init l'array à NULL */
ht->array = malloc(size * sizeof(...));
/* ❌ contient du garbage → crash */
/* 1. Copier les strings */
node->key = strdup(key); /* ✅ */
/* 2. Toujours modulo */
idx = hash_djb2(key) % ht->size; /* ✅ */
/* 3. Comparer les valeurs */
if (strcmp(node->key, key) == 0) /* ✅ */
/* 4. Libérer dans l'ordre */
free(node->key); /* ✅ 1. key */
free(node->value); /* ✅ 2. value */
free(node); /* ✅ 3. node */
/* 5. Initialiser à NULL */
while (i < size)
ht->array[i++] = NULL; /* ✅ */
#include <stdio.h>
#include <stdlib.h>
#include "hash_tables.h"
int main(void)
{
hash_table_t *ht;
char *val;
ht = hash_table_create(1024); /* créer une table de 1024 cases */
if (!ht)
return (1);
hash_table_set(ht, "name", "Alice");
hash_table_set(ht, "age", "25");
hash_table_set(ht, "city", "Paris");
hash_table_set(ht, "name", "Bob"); /* mise à jour */
val = hash_table_get(ht, "name");
printf("name = %s\n", val); /* → "Bob" */
val = hash_table_get(ht, "city");
printf("city = %s\n", val); /* → "Paris" */
val = hash_table_get(ht, "unknown");
printf("unknown = %p\n", (void*)val); /* → NULL */
hash_table_print(ht);
hash_table_delete(ht);
return (0);
}
/* Compter le nombre de paires dans la table */
unsigned long int hash_table_count(const hash_table_t *ht)
{
hash_node_t *node;
unsigned long int i;
unsigned long int count = 0;
if (!ht)
return (0);
i = 0;
while (i < ht->size)
{
node = ht->array[i];
while (node != NULL) /* parcourir la liste de collision */
{
count++;
node = node->next;
}
i++;
}
return (count);
}
/* Pattern universel d'itération :
for each index i → for each node in list → do something */
Version avancée avec liste doublement chaînée maintenue triée par ordre d'insertion — utilisée dans les projets Holberton pour l'affichage ordonné.
/* Structure nœud pour sorted hash table */
typedef struct shash_node_s
{
char *key;
char *value;
struct shash_node_s *next; /* liste de collision */
struct shash_node_s *sprev; /* liste triée ← prev */
struct shash_node_s *snext; /* liste triée → next */
} shash_node_t;
typedef struct shash_table_s
{
unsigned long int size;
shash_node_t **array;
shash_node_t *shead; /* tête de la liste triée */
shash_node_t *stail; /* queue de la liste triée */
} shash_table_t;
/* À l'impression, on utilise la liste shead→stail
pour afficher dans l'ordre alphabétique des clés */
/*
* LES 2 STRUCTURES
* ─────────────────────────────────────────────────────────
* hash_node_t → { char *key, char *value, node_t *next }
* hash_table_t → { unsigned long int size, node_t **array }
*
* LE FLUX
* ─────────────────────────────────────────────────────────
* SET : key → hash_djb2(key) % size → idx → insert en tête
* GET : key → hash_djb2(key) % size → idx → strcmp dans liste
*
* LA FONCTION DJB2
* ─────────────────────────────────────────────────────────
* hash = 5381
* while (c = *str++)
* hash = hash * 33 + c (ou (hash << 5) + hash + c)
*
* LES COLLISIONS
* ─────────────────────────────────────────────────────────
* Gérées par chaining (linked list par case)
* Insertion toujours EN TÊTE → O(1)
*
* COMPLEXITÉ
* ─────────────────────────────────────────────────────────
* set / get / delete → O(1) moyen, O(n) pire cas
* print / delete_table → O(n + m) toujours
*
* RÈGLES D'OR
* ─────────────────────────────────────────────────────────
* 1. strdup() pour key et value — jamais stocker les ptr originaux
* 2. Toujours hash % size — jamais utiliser le hash brut comme index
* 3. strcmp() pour comparer les clés — jamais == sur char*
* 4. Initialiser array à NULL — jamais laisser du garbage
* 5. free(key) → free(value) → free(node) → free(array) → free(ht)
* 6. Insertion en tête — O(1) garanti
*/