C Mastery / Linked Lists: Singly, Doubly, Circular
Part 8 — Data Structures and Algorithms in C

Linked Lists: Singly, Doubly, Circular

This chapter implements singly linked lists, doubly linked lists, and circular lists, with intrusive and container variants.

Why This Matters

Linked lists are the canonical pointer-based structure. They teach pointer discipline, node ownership, and the trade-offs between contiguous and linked storage. Doubly and circular variants are the backbone of many real systems (LRU caches, schedulers, allocators).

Prerequisites

Core Concept

Singly linked list

Each node has a value and a next pointer:

typedef struct Node {
    int value;
    struct Node *next;
} Node;

Operations: prepend (O(1)), append (O(1) with a tail pointer, else O(n)), search (O(n)), delete (O(n) to find predecessor, or O(1) with a pointer-to-pointer).

Doubly linked list

Each node has next and prev:

typedef struct Node {
    int value;
    struct Node *next;
    struct Node *prev;
} Node;

Enables O(1) deletion at a known node and backward traversal.

Circular list

The last node's next points back to the head (singly circular) or both ends link (doubly circular). Useful for round-robin scheduling and ring structures.

Examples

Singly linked list (intrusive)

#include <stdlib.h>

typedef struct Node {
    int value;
    struct Node *next;
} Node;

Node *node_new(int value)
{
    Node *n = malloc(sizeof *n);
    if (n) { n->value = value; n->next = NULL; }
    return n;
}

void list_push_front(Node **head, int value)
{
    Node *n = node_new(value);
    if (n) { n->next = *head; *head = n; }
}

void list_free(Node *head)
{
    while (head) {
        Node *next = head->next;
        free(head);
        head = next;
    }
}

Doubly linked list deletion

void dlist_remove(Node **head, Node *n)
{
    if (n->prev) n->prev->next = n->next;
    else         *head = n->next;
    if (n->next) n->next->prev = n->prev;
    free(n);
}

How It Works

Each node is a separately allocated object; the list is a chain of pointers. Insertion/deletion rewire pointers without shifting elements, unlike arrays. The cost is O(n) search and poor cache locality (pointer chasing).

Variations

Intrusive lists

The link fields live inside your own struct (e.g., a task control block), so the list does not allocate nodes separately. This is the kernel/embedded idiom.

Sentinel nodes

A dummy head/tail node simplifies edge cases (no NULL checks) at the cost of one extra node.

Common Mistakes

Undefined Behavior

Portability

Under the Hood

Each node is a separate heap allocation; traversal follows pointers, causing cache misses (c.perf.2). Intrusive lists avoid per-node allocation overhead.

Practical Usage

Exercises

1. Implement push_front, push_back, find, and remove for a singly linked list. 2. Implement a doubly linked list with insert and remove. 3. Build a circular list and traverse it exactly once. 4. Convert the singly list to an intrusive design.

Deep Challenge

Implement an intrusive doubly linked list (with macros for container_of-style access) and use it to build an LRU cache. Explain the ownership and why intrusive is preferable here.

References

Verification

verified.`

Progress

Concept checkboxes

Mastery levels

ConceptCurrent level (0–8)Target level
c.ds.linked-list06
c.ds.doubly-list06
c.ds.circular-list05