Deux pointeurs. Deux sens. Zéro limite.
Chaque nœud pointe uniquement vers le suivant. Parcours dans un seul sens.
NULL ← [42|→] → [7|→] → [13|NULL]
↑ next seulement
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
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ère | Singly | Doubly |
|---|---|---|
| Pointeurs par nœud | 1 (next) | 2 (prev + next) |
| Mémoire par nœud | +8 bytes | +16 bytes |
| Parcours | → seulement | ← et → les deux |
| Suppression d'un nœud | Besoin du précédent (O(n)) | Direct via prev (O(1)) |
| Insertion avant un nœud | Besoin du précédent | Direct via prev |
| Complexité d'impl. | Simple | Plus de cas à gérer |
| Usage typique | Piles, queues simples | Éditeurs, historique, LRU cache |
head pointe sur le premier nœud, tail pointe sur le dernier. Les pointeurs prev et next forment la chaîne bidirectionnelle.
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
node->next->prev == node), ta liste est corrompue et les parcours donneront des résultats faux ou crasheront./* 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 */
next, tu dois aussi mettre à jour prev du nœud suivant. Oublier l'un des deux casse la cohérence de la liste.#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);
}
next comme en singly. Un prev non initialisé pointer vers du garbage et corrompt la liste.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]
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);
}
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] */
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
node->prev == NULL && node->next == NULL, tu risques de mettre head à NULL mais pas tail (ou l'inverse) — ta liste devient incohérente./* ── 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);
}
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 */
}
prev — on sauvegarde juste next avant chaque free comme pour une singly./* 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 */
/* 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;
#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);
}
/* 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);
}
/* 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 ✅ */
/*
* 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)
*/