← HUB ✦ Holberton School — C Programming ✦

DOUBLY
LINKED LISTS

Deux pointeurs. Deux sens. Zéro limite.

SINGLY vs DOUBLY — LA DIFFÉRENCE FONDAMENTALE
🔗 CE QUI CHANGE PAR RAPPORT À LA SINGLY
SINGLY LINKED LIST

Chaque nœud pointe uniquement vers le suivant. Parcours dans un seul sens.

NULL ← [42|→] → [7|→] → [13|NULL]
              ↑ next seulement
DOUBLY LINKED LIST ✅

Chaque nœud pointe vers le suivant ET le précédent. Parcours dans les deux sens.

NULL ←[←|42|→]↔[←|7|→]↔[←|13|→]→ NULL
        ↑ prev ET next
🕹️ ANALOGIE GAMING

La singly list c'est une file d'attente à sens unique — tu avances mais jamais rebrousses chemin. La doubly list c'est un couloir avec des portes dans les deux sens — depuis n'importe quel nœud tu peux aller vers l'avant OU vers l'arrière, comme une playlist musicale avec les boutons ⏭️ ET ⏮️.

CritèreSinglyDoubly
Pointeurs par nœud1 (next)2 (prev + next)
Mémoire par nœud+8 bytes+16 bytes
Parcours→ seulement← et → les deux
Suppression d'un nœudBesoin du précédent (O(n))Direct via prev (O(1))
Insertion avant un nœudBesoin du précédentDirect via prev
Complexité d'impl.SimplePlus de cas à gérer
Usage typiquePiles, queues simplesÉditeurs, historique, LRU cache
VISUALISATION — UNE DOUBLY LIST EN MÉMOIRE
🗺️ ANATOMIE D'UNE DOUBLY LINKED LIST

head pointe sur le premier nœud, tail pointe sur le dernier. Les pointeurs prev et next forment la chaîne bidirectionnelle.

_
NULL
←
HEAD ▼
prev
NULL
42
next
*
node 1
→
←
_
prev
*
7
next
*
node 2
→
←
▼ TAIL
prev
*
13
next
NULL
node 3
_
NULL
Règles fondamentales :
  head->prev = NULL          ← le premier n'a pas de précédent
  tail->next = NULL          ← le dernier n'a pas de suivant
  node->next->prev = node    ← la relation est symétrique
  node->prev->next = node    ← toujours vérifier ces invariants
Les invariants de cohérence sont la clé. Si tu brises la relation symétrique (node->next->prev == node), ta liste est corrompue et les parcours donneront des résultats faux ou crasheront.
DÉCLARER LA STRUCTURE
🧱 LA STRUCT DLISTINT_T (STYLE HOLBERTON)
/* Convention Holberton pour les doubly linked lists */

typedef struct dlistint_s
{
    int                  n;     /* la donnée */
    struct dlistint_s  *prev;  /* ← pointeur vers le nœud PRÉCÉDENT */
    struct dlistint_s  *next;  /* → pointeur vers le nœud SUIVANT */
} dlistint_t;

/* Déclaration — toujours initialiser head ET tail à NULL */
dlistint_t *head = NULL;
dlistint_t *tail = NULL;  /* la doubly a souvent un tail aussi */
Dans une doubly list, si tu modifies next, tu dois aussi mettre à jour prev du nœud suivant. Oublier l'un des deux casse la cohérence de la liste.
CRÉER UN NŒUD
🆕 CREATE_DLISTINT_NODE
#include <stdlib.h>

dlistint_t *create_dlistint_node(int n)
{
    dlistint_t *node;

    node = malloc(sizeof(dlistint_t));
    if (node == NULL)
        return (NULL);
    node->n    = n;
    node->prev = NULL;  /* ← toujours initialiser prev à NULL */
    node->next = NULL;  /* ← toujours initialiser next à NULL */
    return (node);
}
Initialiser les deux pointeurs à NULL — pas seulement next comme en singly. Un prev non initialisé pointer vers du garbage et corrompt la liste.
AJOUTER DES NŒUDS
➕ ADD AU DÉBUT — DLISTINT_ADD_FRONT

O(1) — le plus rapide. Insère avant le head actuel.

dlistint_t *dlistint_add_front(dlistint_t **head, int n)
{
    dlistint_t *node = create_dlistint_node(n);

    if (node == NULL)
        return (NULL);

    node->next = *head;         /* nouveau → ancien head */
    node->prev = NULL;         /* nouveau est le premier : pas de prev */

    if (*head != NULL)
        (*head)->prev = node;   /* ← CRUCIAL : l'ancien head pointe vers le nouveau */

    *head = node;               /* head devient le nouveau nœud */
    return (node);
}

Visualisation — ajouter 99 en tête :

Avant :  head → [NULL|42|→] ↔ [←|7|→] ↔ [←|13|NULL]

Étapes :
  1. node = [NULL|99|NULL]
  2. node->next = head           → [NULL|99|→]  →  [NULL|42|→] ↔ ...
  3. head->prev = node           → [NULL|99|→]  ↔  [←|42|→] ↔ ...
  4. head = node

Après : head → [NULL|99|→] ↔ [←|42|→] ↔ [←|7|→] ↔ [←|13|NULL]
➕ ADD EN FIN — DLISTINT_ADD_END

O(1) avec tail, O(n) sans. Insère après le dernier nœud.

/* Version avec tail passé en paramètre — O(1) */
dlistint_t *dlistint_add_end(dlistint_t **head, dlistint_t **tail, int n)
{
    dlistint_t *node = create_dlistint_node(n);

    if (node == NULL)
        return (NULL);

    if (*head == NULL)           /* liste vide */
    {
        *head = node;
        *tail = node;
        return (node);
    }
    node->prev  = *tail;         /* nouveau ← ancien tail */
    node->next  = NULL;          /* nouveau est le dernier : pas de next */
    (*tail)->next = node;         /* ← CRUCIAL : ancien tail → nouveau */
    *tail = node;                 /* tail devient le nouveau nœud */
    return (node);
}

/* Version sans tail — O(n) */
dlistint_t *dlistint_add_end_no_tail(dlistint_t **head, int n)
{
    dlistint_t *node = create_dlistint_node(n);
    dlistint_t *tmp;

    if (node == NULL) return (NULL);
    if (*head == NULL) { *head = node; return (node); }
    tmp = *head;
    while (tmp->next != NULL)
        tmp = tmp->next;          /* parcourir jusqu'au dernier */
    tmp->next  = node;
    node->prev = tmp;             /* ← lier le prev aussi ! */
    return (node);
}
INSERTION AU MILIEU — LA NOUVEAUTÉ DES DOUBLY
🎯 INSÉRER AVANT UN NŒUD DONNÉ — O(1) !

C'est l'avantage majeur de la doubly list. En singly, insérer avant un nœud nécessite de connaître le précédent (O(n) de recherche). En doubly, node->prev te le donne directement → O(1).

/* Insérer new_node juste avant target */
void insert_before(dlistint_t **head, dlistint_t *target, int n)
{
    dlistint_t *node = create_dlistint_node(n);

    if (!node || !target) return;

    node->next = target;           /* nouveau → target */
    node->prev = target->prev;     /* nouveau ← ce qui était avant target */

    if (target->prev != NULL)
        target->prev->next = node; /* l'ancien précédent → nouveau */
    else
        *head = node;              /* target était head → nouveau devient head */

    target->prev = node;           /* target ← nouveau */
}

/* Visualisation — insérer 99 avant le nœud contenant 7 :
  Avant : [42] ↔ [7] ↔ [13]
  Étapes :
    1. node = [99]
    2. node->next = target(7)      → [99 →7]
    3. node->prev = target->prev(42)  → [42← 99 →7]
    4. target->prev->next = node    → [42 ↔ 99]
    5. target->prev = node          → [99 ↔ 7]
  Après : [42] ↔ [99] ↔ [7] ↔ [13] */
SUPPRIMER DES NŒUDS — LES 4 CAS
🗑️ DELETE — GÉRER TOUS LES CAS

La suppression en doubly est plus complexe qu'en singly car il faut mettre à jour les deux pointeurs voisins. Il y a 4 cas distincts à gérer.

void delete_node(dlistint_t **head, dlistint_t **tail, dlistint_t *node)
{
    if (!node)
        return;

    /* CAS 1 — Nœud a un précédent : le passer sur le suivant */
    if (node->prev != NULL)
        node->prev->next = node->next;
    else
        *head = node->next;  /* CAS 2 — Pas de précédent = c'était le HEAD */

    /* CAS 3 — Nœud a un suivant : le passer sur le précédent */
    if (node->next != NULL)
        node->next->prev = node->prev;
    else
        *tail = node->prev;  /* CAS 4 — Pas de suivant = c'était le TAIL */

    free(node);
}

Visualisation des 4 cas :

CAS A — Supprimer un nœud du MILIEU
  Avant : [42] ↔ [7] ↔ [13]
  Action : 42->next = 13  ET  13->prev = 42
  Après : [42] ↔ [13]

CAS B — Supprimer le HEAD
  Avant : [42] ↔ [7] ↔ [13]
  Action : head = 7  ET  7->prev = NULL
  Après : [7] ↔ [13]

CAS C — Supprimer le TAIL
  Avant : [42] ↔ [7] ↔ [13]
  Action : tail = 7  ET  7->next = NULL
  Après : [42] ↔ [7]

CAS D — Supprimer le SEUL nœud
  Avant : [42]
  Action : head = NULL  ET  tail = NULL
  Après : liste vide
Le CAS D (nœud unique) est le plus piégeux. Si tu ne testes pas node->prev == NULL && node->next == NULL, tu risques de mettre head à NULL mais pas tail (ou l'inverse) — ta liste devient incohérente.
PARCOURIR LA LISTE — LES DEUX SENS
🔄 TRAVERSÉE AVANT ET ARRIÈRE
/* ── AVANT : head → tail (comme singly) ────────────────────── */
size_t print_dlistint(const dlistint_t *h)
{
    size_t count = 0;

    while (h != NULL)
    {
        printf("%d\n", h->n);
        h = h->next;
        count++;
    }
    return (count);
}

/* ── ARRIÈRE : tail → head (unique à la doubly !) ────────────── */
size_t print_dlistint_reverse(const dlistint_t *tail)
{
    size_t count = 0;

    while (tail != NULL)
    {
        printf("%d\n", tail->n);
        tail = tail->prev;   /* ← utiliser prev au lieu de next */
        count++;
    }
    return (count);
}

/* ── COMPTER LES NŒUDS ──────────────────────────────────────── */
size_t dlistint_len(const dlistint_t *h)
{
    size_t len = 0;

    while (h != NULL) { len++; h = h->next; }
    return (len);
}
LIBÉRER LA LISTE — FREE PROPRE POUR VALGRIND
🧹 FREE_DLISTINT — ZÉRO LEAK
void free_dlistint(dlistint_t *head)
{
    dlistint_t *tmp;

    while (head != NULL)
    {
        tmp  = head->next;   /* sauvegarder next AVANT free */
        free(head);          /* libérer le nœud actuel */
        head = tmp;          /* avancer */
    }
    /* prev n'a pas besoin d'être utilisé ici — on parcourt dans un sens */
}

/* Version avec remise à NULL de head et tail */
void free_dlistint_safe(dlistint_t **head, dlistint_t **tail)
{
    dlistint_t *tmp;

    while (*head != NULL)
    {
        tmp     = (*head)->next;
        free(*head);
        *head = tmp;
    }
    *tail = NULL;  /* remettre tail à NULL aussi */
}
Pour libérer une doubly list, tu peux parcourir dans un seul sens (forward). Pas besoin d'utiliser prev — on sauvegarde juste next avant chaque free comme pour une singly.
ERREURS CLASSIQUES — ❌ vs ✅
❌ LES ERREURS FRÉQUENTES
/* 1. Oublier de mettre à jour prev */
node->next = new_node;
/* ❌ new_node->prev toujours NULL */
/* → new_node->prev = node ✅ */

/* 2. Mauvais ordre de mise à jour */
node->prev = new_node;
/* ❌ on perd la référence à l'ancien prev */

/* 3. Oublier le cas head lors d'un delete */
node->prev->next = node->next;
/* ❌ crash si node est le head (prev = NULL) */

/* 4. Oublier le cas nœud unique */
*head = node->next;
/* ❌ tail pointe encore vers le nœud libéré */

/* 5. Ne pas init prev à NULL */
node->next = *head;
*head = node;
/* ❌ prev de l'ancien head pas mis à jour */
✅ LES BONNES PRATIQUES
/* 1. Toujours mettre à jour les 2 sens */
node->next           = new_node;
new_node->prev       = node;    /* ✅ */

/* 2. Sauvegarder avant de modifier */
old_prev       = node->prev;    /* ✅ sauvegarder d'abord */
node->prev     = new_node;

/* 3. Toujours tester NULL avant d'accéder */
if (node->prev != NULL)        /* ✅ */
    node->prev->next = node->next;
else
    *head = node->next;

/* 4. Mettre à jour tail aussi */
if (node->next == NULL)
    *tail = node->prev;         /* ✅ */

/* 5. Toujours init prev à NULL */
node->prev           = NULL;
node->next           = *head;
if (*head) (*head)->prev = node; /* ✅ */
*head = node;
LES 3 BOSS — EXEMPLES PROGRESSIFS
🐣 BOSS 1 — CRÉER ET AFFICHER UNE DOUBLY LIST
⚔️ LVL 1 — FACILE
#include <stdio.h>
#include <stdlib.h>

typedef struct dlistint_s {
    int               n;
    struct dlistint_s *prev;
    struct dlistint_s *next;
} dlistint_t;

dlistint_t *add_front(dlistint_t **h, int n)
{
    dlistint_t *node = malloc(sizeof(dlistint_t));
    if (!node) return (NULL);
    node->n    = n;
    node->prev = NULL;
    node->next = *h;
    if (*h) (*h)->prev = node;
    *h = node;
    return (node);
}

int main(void)
{
    dlistint_t *head = NULL;
    dlistint_t *tmp;

    add_front(&head, 13);
    add_front(&head, 7);
    add_front(&head, 42);

    /* Afficher en avant */
    tmp = head;
    while (tmp) { printf("%d ", tmp->n); tmp = tmp->next; }
    printf("\n");  /* → 42 7 13 */

    /* Aller jusqu'au tail puis afficher en arrière */
    tmp = head;
    while (tmp->next) tmp = tmp->next;  /* atteindre le tail */
    while (tmp) { printf("%d ", tmp->n); tmp = tmp->prev; }
    printf("\n");  /* → 13 7 42 */
    return (0);
}
⚔️ BOSS 2 — TROUVER LE NŒUD AU MILIEU
🔥 LVL 2 — INTERMÉDIAIRE
/* Technique des deux pointeurs — slow/fast */
dlistint_t *find_middle(dlistint_t *head)
{
    dlistint_t *slow = head;
    dlistint_t *fast = head;

    while (fast != NULL && fast->next != NULL)
    {
        slow = slow->next;        /* avance de 1 */
        fast = fast->next->next;  /* avance de 2 */
    }
    return (slow);  /* quand fast est en fin, slow est au milieu */
}

/* Ou utiliser la bidirectionnalité de la doubly :
   pointer depuis les deux bouts vers le milieu */
dlistint_t *find_middle_bidirectionnal(dlistint_t *head, dlistint_t *tail)
{
    while (head != tail && head->prev != tail)
    {
        head = head->next;  /* avancer depuis la tête */
        tail = tail->prev;  /* reculer depuis la queue */
    }
    return (head);
}
💀 BOSS FINAL — INVERSER LA LISTE EN PLACE
💀 LVL 3 — HOLBERTON STYLE
/* Inverser une doubly list — échanger prev et next de chaque nœud */
dlistint_t *reverse_dlistint(dlistint_t **head)
{
    dlistint_t *current = *head;
    dlistint_t *tmp     = NULL;

    while (current != NULL)
    {
        /* Échanger prev et next de chaque nœud */
        tmp          = current->prev;
        current->prev = current->next;
        current->next = tmp;
        /* Avancer — mais "avancer" = aller vers l'ancien next = prev actuel */
        current      = current->prev;
    }
    /* Après la boucle, tmp pointe vers le nouveau head */
    if (tmp != NULL)
        *head = tmp->prev;
    return (*head);
}

/* Visualisation :
  Avant  : [NULL←42→] ↔ [←7→] ↔ [←13→NULL]   head=42
  Iter 1 : [→42←NULL] (prev/next échangés)
  Iter 2 : [→7←]      (prev/next échangés)
  Iter 3 : [NULL→13←] (prev/next échangés)
  Après  : [NULL←13→] ↔ [←7→] ↔ [←42→NULL]   head=13 ✅ */
RÉSUMÉ — TOUT EN UN COUP D'ŒIL
🗺️ LA CARTE MENTALE COMPLÈTE
/*
 *  LA STRUCTURE
 *  ─────────────────────────────────────────────────────────
 *  typedef struct dlistint_s {
 *      int               n;
 *      struct dlistint_s *prev;   ← pointe vers le précédent
 *      struct dlistint_s *next;   → pointe vers le suivant
 *  } dlistint_t;
 *
 *  head->prev = NULL   (toujours)
 *  tail->next = NULL   (toujours)
 *  node->next->prev = node  (invariant de cohérence)
 *
 *  LES OPÉRATIONS — COMPLEXITÉS
 *  ─────────────────────────────────────────────────────────
 *  add_front          → O(1)    mettre à jour prev de l'ancien head
 *  add_end (+ tail)   → O(1)    mettre à jour next de l'ancien tail
 *  add_end (- tail)   → O(n)    parcourir jusqu'au dernier
 *  insert_before      → O(1)    grace à prev — avantage vs singly !
 *  delete (any node)  → O(1)    grace à prev — avantage vs singly !
 *  search             → O(n)    parcourir (peut aller dans les 2 sens)
 *  print forward      → O(n)    head → tail via next
 *  print backward     → O(n)    tail → head via prev
 *  free               → O(n)    libérer chaque nœud
 *
 *  LES 4 CAS DU DELETE
 *  ─────────────────────────────────────────────────────────
 *  1. node a un prev → prev->next = node->next
 *  2. node est HEAD  → head = node->next
 *  3. node a un next → next->prev = node->prev
 *  4. node est TAIL  → tail = node->prev
 *
 *  RÈGLES D'OR
 *  ─────────────────────────────────────────────────────────
 *  1. Toujours init prev ET next à NULL
 *  2. Toute modification de next doit s'accompagner de prev
 *  3. Tester NULL avant d'accéder à node->prev ou node->next
 *  4. Gérer le cas nœud unique (head == tail)
 *  5. Si tu as un tail, le maintenir à jour à chaque opération
 *  6. free : sauvegarder next AVANT free (même chose qu'en singly)
 */