Singly Linked Lists
The node, the empty-list discipline, head insertion, and why the tail costs you O(n) until you buy one.
The node is the whole idea
typedef struct SNode {
int value;
struct SNode *next; /* NULL ends the list */
} SNode;
The list is the head pointer. An empty list is SNode *head = NULL; โ
there is no list object to initialize. Every function that mutates the
list must handle three special cases: empty (head is NULL), single
element, and the head itself (which changes).
Head insertion is the cheap one โ O(1), no traversal, and it naturally reverses insertion order:
int push_front(SNode **head, int v) {
SNode *n = malloc(sizeof *n);
if (!n) return -1;
n->value = v;
n->next = *head; /* works for empty: *head is NULL */
*head = n;
return 0;
}
The SNode **head is not decoration. The function must change the
caller's head โ and to change a pointer you need its address. The
alternative (returning the new head) works but forces the caller to
reassign on every call, which is one forgotten head = ... away from a
leak.
Deletion is where lists punish you
To delete a node you need the previous node โ but singly-linked nodes
point only forward. Either walk with two fingers (prev/cur), or copy
the next node's payload into the doomed node and unlink it (great for
tail-agnostic O(1) delete, changes iteration semantics). The two-finger
walk is the honest default:
int remove_val(SNode **head, int v) {
SNode **link = head; /* points at whoever points at cur */
for (SNode *cur = *head; cur; cur = cur->next) {
if (cur->value == v) {
*link = cur->next; /* unlink: fixes head OR prev->next */
free(cur);
return 1;
}
link = &cur->next;
}
return 0;
}
SNode **link walks the links, not the nodes: *link is "whatever
points at cur" โ the head itself, or the previous node's next. One
code path for both, no special cases.
Destroying a list
Free every node exactly once, then null the head. The trap is
free(cur); cur = cur->next; โ reading a freed node. Save the next
pointer first, always.