Section 4 · 9 min read
Linked lists
Singly, doubly and circular linked lists in C: node layout, insertion and deletion, reversal, two-pointer techniques, cycle detection and operation costs.
On this page
Structure
A linked list is a chain of nodes, each holding data and a pointer to the next node. The list is reached through a head pointer; the last node's next is NULL.
#include <stdlib.h>
struct node {
int v;
struct node *next;
};
struct node *newNode(int v, struct node *next) {
struct node *n = malloc(sizeof *n);
n->v = v;
n->next = next;
return n;
}
Unlike an array, nodes are not contiguous, so there is no access by index. In exchange, inserting or removing a node needs no shifting once you hold a pointer to the right place.
Variants
- Singly linked: one
nextpointer per node. Traversal only forward. - Doubly linked:
prevandnext. Traversal both ways, and a node can be removed given only a pointer to it. - Circular: the last node points back to the first. Often accessed through a pointer to the last node, which gives access to both the last node and the first (
last->next). - Header (sentinel) node: a dummy node before the first real node, so insertion and deletion at the front need no special case.
Operation costs
With a head pointer only, for a list of nodes:
| Operation | Singly linked | Doubly linked | Singly with tail pointer |
|---|---|---|---|
| Insert at front | |||
| Insert at end | |||
| Delete first | |||
| Delete last | , with tail | ||
| Delete a node given a pointer to it | in general | ||
| Search | |||
| Access th node |
Deleting the last node of a singly linked list stays even with a tail pointer, because you must find the node before it to set its next to NULL.
A trick sometimes tested: to delete a node p of a singly linked list in when p is not the last node, copy p->next->v into p and unlink p->next instead. It fails for the last node.
Binary search on a linked list is not : finding the middle element costs linear time, so the total is .
Core operations in C
Insertion
/* insert at front: returns the new head */
struct node *pushFront(struct node *head, int v) {
return newNode(v, head);
}
/* insert after node p */
void insertAfter(struct node *p, int v) {
p->next = newNode(v, p->next);
}
The order of pointer assignments matters. When inserting n after p, set n->next = p->next before changing p->next, or the rest of the list is lost.
Deletion
/* delete the first node with value v; returns the new head */
struct node *removeValue(struct node *head, int v) {
struct node *prev = NULL, *cur = head;
while (cur && cur->v != v) { prev = cur; cur = cur->next; }
if (!cur) return head; /* not found */
if (prev) prev->next = cur->next;
else head = cur->next; /* deleting the head */
free(cur);
return head;
}
Reversal
Iterative reversal walks the list once, pointing each node back at the reversed prefix:
struct node *reverse(struct node *h) {
struct node *prev = NULL, *nxt;
while (h) {
nxt = h->next;
h->next = prev;
prev = h;
h = nxt;
}
return prev;
}
Time , extra space . A recursive reversal also takes time but uses stack space.
Two-pointer techniques
Middle node
struct node *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
After iterations slow is at node and fast at node (1-based). For odd it stops at the exact middle, node . For even it stops at node , the second of the two middle nodes. The loop condition decides this, so trace it rather than assume.
th node from the end
Move a lead pointer steps ahead, then move both pointers together until the lead falls off the end. The trailing pointer is then th from the end.
Cycle detection (Floyd)
Move slow by one and fast by two. If there is no cycle, fast reaches NULL. If there is a cycle, they must meet inside it, because once both are in the cycle the gap between them shrinks by one each step. The meeting happens within steps, where is the distance from the head to the cycle start and is the cycle length.
To find the cycle start: after they meet, reset one pointer to the head and move both one step at a time. They meet again at the first node of the cycle. To find , keep one pointer fixed and walk the other around until it returns.
Doubly linked lists
struct dnode { int v; struct dnode *prev, *next; };
To remove node X from the middle (both neighbours exist):
X->prev->next = X->next;
X->next->prev = X->prev;
The two statements can be in either order because neither changes X's own fields. To insert a new node N after P (with P->next not NULL):
N->prev = P;
N->next = P->next;
P->next->prev = N; /* must run before P->next changes */
P->next = N;
GATE likes asking which ordering of these four statements is correct. The safe rule: fill in the new node's pointers first, then update the old neighbours, and never read P->next after overwriting it.
Circular lists and the Josephus problem
In a circular list, a traversal stops when it returns to the start node, not at NULL. Deleting every th node around a circle until one remains is the Josephus problem. With 0-based positions,
and the survivor in 1-based numbering is . For there is a closed form: write with ; the survivor is .
Arrays versus linked lists
| Array | Linked list | |
|---|---|---|
| Access by index | ||
| Insert or delete at a known position | shifting | pointer changes |
| Memory | contiguous, no pointer overhead | extra pointer per node |
| Cache behaviour | good | poor |
| Size | fixed, or reallocation | grows freely |
Worked examples
Worked example 1: deleting alternate nodes
The list holds 1, 2, 3, 4, 5, 6. What does it hold after this loop?
struct node *p = head;
while (p != NULL && p->next != NULL) {
struct node *q = p->next;
p->next = q->next;
free(q);
p = p->next;
}
pat 1: unlink 2, move to 3.pat 3: unlink 4, move to 5.pat 5: unlink 6,pbecomesNULL.
The list is 1, 3, 5. With an odd length such as 1 to 5, the loop stops at 5 because p->next is NULL.
Worked example 2: recursion that skips nodes
void pr(struct node *p) {
if (p == NULL) return;
if (p->next) pr(p->next->next);
printf("%d ", p->v);
}
For the list 1 to 6: pr(1) calls pr(3), which calls pr(5). Node 5's next is 6 and 6's next is NULL, so pr(5) calls pr(NULL), which returns. Printing happens on the way back: 5, then 3, then 1. Output: 5 3 1.
Worked example 3: inserting into a doubly linked list
P is a node with successor S. Which order of statements inserts N between them?
N->next = P->next; N->prev = P; P->next = N; N->next->prev = N;P->next = N; N->next = P->next; N->prev = P; S->prev = N;
Order 1 works: N->next is set to S while P->next still points to S; after P->next = N, the expression N->next->prev is S->prev, which correctly becomes N.
Order 2 fails: after P->next = N, the statement N->next = P->next makes N point to itself.
Worked example 4: Floyd on a concrete list
Nodes 1 to 8 are linked in order, and node 8 points back to node 4. Both pointers start at node 1.
| Step | slow | fast |
|---|---|---|
| 1 | 2 | 3 |
| 2 | 3 | 5 |
| 3 | 4 | 7 |
| 4 | 5 | 4 |
| 5 | 6 | 6 |
They meet at node 6 after 5 steps. Here (nodes 1, 2, 3 before the cycle) and (nodes 4 to 8). Restarting one pointer at node 1 and moving both by one: 1 and 6, then 2 and 7, then 3 and 8, then 4 and 4. They meet at node 4, the start of the cycle.
Common traps
- Overwriting
p->nextbefore saving it, which loses the rest of the list. - Forgetting the special case for deleting or inserting at the head (or use a header node).
- Assuming a tail pointer makes deleting the last node in a singly linked list.
- Off-by-one on the middle node for even lengths.
- Dereferencing
fast->next->nextwithout checkingfast->next. - In circular lists, looping until
NULL, which never comes. - Reading a node after
freeon it.
Quick revision
- Singly linked list with head only: front operations , end operations , search .
- Doubly linked: delete a given node in ; two statements, order free.
- Insert after
p: set the new node'snextfirst. - Reversal: three pointers, time, space.
- Slow and fast pointers: middle node, th from end, cycle detection.
- Floyd: meet inside the cycle; reset one pointer to head to find the cycle start.
- Josephus: , survivor ; for , .