← HUB ✦ Holberton School — C Programming ✦

HASH
TABLES

Clé → Hash → Index → Valeur. Instantané.

C'EST QUOI UNE HASH TABLE ?
🗄️ DÉFINITION — PARTIR DE ZÉRO

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.

🕹️ ANALOGIE GAMING

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).

🔑
CLÉ (KEY)

L'identifiant unique. Ex: "name", "age"

⚙️
HASH FUNCTION

Convertit la clé en index. Doit être déterministe

📦
VALEUR (VALUE)

La donnée associée à la clé. Ex: "Alice", 42

COMMENT ÇA MARCHE — LE MÉCANISME COMPLET
⚙️ LE FLUX : CLÉ → INDEX → VALEUR
"name"
la clé
→
HASH
FUNCTION
djb2 / sum...
→
3
l'index
→
array[3]
la case
→
"Alice"
la valeur
1

SET — Stocker une paire clé/valeur

ht_set(table, "name", "Alice")
       ↓
hash("name") % table_size = 3   ← calculer l'index
       ↓
table->array[3] = node{"name", "Alice"} ← stocker
2

GET — Récupérer une valeur

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) !
Pourquoi O(1) ? La fonction de hachage calcule toujours le même index pour la même clé. Pas besoin de chercher — on sait directement où regarder. C'est comme connaître le numéro de casier par cœur.
LES STRUCTURES — DÉCLARER UNE HASH TABLE
🧱 LES 2 STRUCTS INDISPENSABLES
/* ── 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
hash_node_t **array : c'est un tableau de pointeurs vers des nœuds (pas les nœuds directement). Chaque case peut pointer vers NULL (vide) ou vers une linked list de nœuds (collision).
LA FONCTION DE HACHAGE — LE CŒUR DU SYSTÈME
⚡ PROPRIÉTÉS D'UNE BONNE HASH FUNCTION
✅ QUALITÉS REQUISES
  • 🎯 Déterministe — même clé = même index
  • ⚡ Rapide — calcul en O(1)
  • 📊 Uniforme — distribue bien dans le tableau
  • 🔢 Index valide — résultat dans [0, size-1]
❌ CE QU'IL NE FAUT PAS
  • 💩 Concentrer les données dans peu de cases
  • 💩 Retourner des indices hors du tableau
  • 💩 Être trop lente à calculer
  • 💩 Produire des résultats aléatoires
/* ── 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 */
La fonction djb2 est celle attendue à Holberton. Mémorise sa valeur initiale (5381) et sa formule (hash * 33 + c). Elle distribue très bien les clés et minimise les collisions.
LES COLLISIONS — QUAND DEUX CLÉS ONT LE MÊME INDEX
💥 C'EST QUOI UNE COLLISION ?

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 */
🕹️ ANALOGIE GAMING

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.

🔗 SOLUTION : CHAINING — LISTES CHAÎNÉES

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.

HASH TABLE — CHAINING (taille = 8)
0
NULL
1
NULL
2
"age"→"25"
→ NULL
3
"mane"→"Bob"
→
"name"→"Alice"
→ NULL ← COLLISION!
4
NULL
5
"city"→"Paris"
→ NULL
6
NULL
7
NULL
À l'index 3, "mane" a été ajouté en tête de la linked list, avant "name". C'est le comportement standard à Holberton : on insère toujours en tête (O(1)) et non en queue (O(n)).
IMPLÉMENTATION COMPLÈTE EN C
🏗️ HT_CREATE — CRÉER UNE HASH TABLE
#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);
}
Initialiser chaque case à NULL est crucial — sinon tu lirais du garbage au lieu de NULL et penseras qu'une case est occupée alors qu'elle ne l'est pas.
✍️ HT_SET — INSÉRER / METTRE À JOUR
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);
}
Pourquoi strdup ? On fait une copie de la clé et de la valeur plutôt que de stocker les pointeurs originaux. Ainsi la table est propriétaire de ses données et indépendante des variables de l'appelant.
🔍 HT_GET — RÉCUPÉRER UNE VALEUR
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 */
}
Dans le cas idéal (pas de collision), 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.
🖨️ HT_PRINT — AFFICHER LA TABLE
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");
}
🗑️ HT_DELETE — LIBÉRER LA TABLE (VALGRIND !)
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 */
}
Ordre de libération obligatoire : 1) key → 2) value → 3) node → 4) array → 5) ht. Libérer dans le mauvais ordre = accès à de la mémoire déjà libérée → segfault + erreur Valgrind.
KEY_INDEX — LA FONCTION HELPER
🔢 KEY_INDEX — COMBINER HASH + MODULO
/* 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] */
Pourquoi le modulo ? La fonction djb2 retourne un unsigned long int potentiellement énorme. Le modulo (%) garantit que l'index est dans les limites du tableau [0, size-1].
COMPLEXITÉ — POURQUOI LES HASH TABLES SONT RAPIDES
⚡ ANALYSE DE COMPLEXITÉ
OpérationCas moyenPire casPourquoi
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
HASH TABLE vs TABLEAU
RechercheO(1)O(n)
InsertionO(1)O(1)*
SuppressionO(1)O(n)
HASH TABLE vs LINKED LIST
RechercheO(1)O(n)
InsertionO(1)O(1)
Accès par cléO(1)O(n)
ERREURS CLASSIQUES — ❌ vs ✅
❌ LES ERREURS FRÉQUENTES
/* 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 */
✅ LES BONNES PRATIQUES
/* 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;  /* ✅ */
LES 3 BOSS — EXEMPLES PROGRESSIFS
🐣 BOSS 1 — UTILISER UNE HASH TABLE
⚔️ LVL 1 — FACILE
#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);
}
⚔️ BOSS 2 — ITÉRER SUR TOUS LES ÉLÉMENTS
🔥 LVL 2 — INTERMÉDIAIRE
/* 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 */
💀 BOSS FINAL — SORTED HASH TABLE (Holberton)
💀 LVL 3 — HOLBERTON STYLE

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 */
RÉSUMÉ — TOUT EN UN COUP D'ŒIL
🗺️ LA CARTE MENTALE COMPLÈTE
/*
 *  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
 */