Skip to main content

Doubly Linked Lists

intermediate16 min readLesson 110 of 148

prev pointers make deletion O(1) — and give you two more pointers to keep consistent.

Pay two pointers, delete in O(1)

typedef struct DNode {
    int            value;
    struct DNode  *prev, *next;
} DNode;

With prev, a node carries its own "who points at me" — unlinking needs no search:

void unlink(DNode *n) {
    if (n->prev) n->prev->next = n->next; else /* was head */;
    if (n->next) n->next->prev = n->prev; else /* was tail */;
    n->prev = n->next = NULL;      /* node is now detached */
}

The price is four pointer updates per insert/delete instead of two, and a whole class of new bugs: forgetting one side leaves a node that thinks it is detached while the list still reaches it — or worse, a prev chain that disagrees with the next chain. After any structural change, a forward walk and a backward walk must visit the same nodes in reverse order. Tests should verify both directions.

Sentinels: trading one node for zero special cases

Keep a dummy node that is never stored-to; the real list runs between its two links. Head and tail operations stop having NULL cases:

DNode sentinel;                     /* never holds data */
sentinel.next = &sentinel;
sentinel.prev = &sentinel;          /* empty list: both point home */

Insertion becomes one statement regardless of position; iteration is for (DNode *p = s.next; p != &s; p = p->next). The cost: one extra node and the discipline that nothing may dereference the sentinel's value. Production list implementations (Linux list_head among them) are sentinel-based.