Skip to content

Singly linked lists

A linked list is a chain of nodes on the heap, each holding a key and the address of the next. Inserting means re-aiming two pointers, in the right order.

Helps to know first
This topic

First, the picture

An array keeps its items side by side. What if each item said where the next one is? Watch the arrows, not the row.

Inserting into a linked list

Four nodes on the heap, in the order they were allocated; arrows show the list's order

void insert_after(struct node *prev, int key){    struct node *n = malloc(sizeof *n);    if (n == NULL)        exit(1);    n->key = key;    n->next = prev->next;    prev->next = n;}int main(void){    struct node *head = NULL;    head = push_front(head, 40);    head = push_front(head, 20);    head = push_front(head, 10);    insert_after(head->next, 30);    print_list(head, 6);    free_list(head);    return 0;}

main has started. Its frame holds head, the address of the first node; it is not set yet.

Lines 25 and 26

Output

0.00 / 22.20 s
Describe this picture

Lines 19 to 27 (insert_after) and 47 to 57 (main) of list.c, with the line about to run marked, beside a picture of memory. At the top is the stack: main’s frame with head at 0x7fe8, and beside it the frame of the function main has called, when there is one, with its own boxes. Below is the heap: one row of nodes in address order, 0x1000 on the left, each labelled with its address. Each node is 16 bytes: its key, a narrow grey band for the 4 bytes of padding, and its next field. Arrows run from each pointer box to the node it holds the address of, arcing under the row between nodes; a NULL pointer is a short bar. A small tag above a node names a pointer of the called function that holds its address, such as prev or n. The box the last line wrote is outlined, and a node that no pointer leads to is drawn faded. The clip plays the program once, about 22 seconds, and rests when the list has been printed. First push_front runs line by line and makes the node holding 40 at 0x1000; the next two calls make 20 at 0x1020 and 10 at 0x1040, one beat each. Then insert_after runs a line at a time: the node at 0x1060 appears, takes the key 30, its arrow reaches 40, and 20’s arrow swings from 40 onto it. Last, print_list moves p from node to node while the output fills with 10 20 30 40. Under the picture are one sentence on the current step, then the switch, then the output once the program prints. The switch, “Lines 25 and 26”, with the positions “in this order” and “swapped”, can be pressed at any time; a press replays from the step before line 25. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as relink.step, and the switch as relink.order.

Nodes on the heap

Each item of the list is a node, a struct with two fields:

struct node {
    int key;
    struct node *next;
};

key is the value the list stores. next holds the address of the next node, so the struct holds a pointer to its own kind. From Structs (1.4): a struct’s fields sit in order, with padding where a field needs alignment. Here key takes 4 bytes, 4 bytes of padding follow so that next starts on a multiple of 8, and next takes 8. A node is 16 bytes.

From The heap: malloc and free (2.4): malloc(sizeof *n) finds 16 bytes on the heap and returns their address, or NULL when no memory is left; free gives a block back. And from Passing pointers to functions (2.2): n->key is the field key of the struct at the address in n, the same as (*n).key.

Now the clip. Each push_front makes one node and puts it in front of the list. The first makes 40 at 0x1000, the second 20 at 0x1020, the third 10 at 0x1040. After line 52 the list reads 10, 20, 40: start at the address in head, 0x1040, and follow each next.

The heap row shows the same nodes in address order: 40, 20, 10, left to right, because 40 was allocated first. So the list’s arrows jump right to left across the row. The list’s order is not the heap’s order. Nothing keeps the nodes side by side; the next fields alone decide the order.

head is not the first node either. It is an 8-byte pointer box in main’s frame, at 0x7fe8, holding the first node’s address. An empty list is head holding NULL, and the last node’s next is NULL.

Then insert_after(head->next, 30) puts 30 after 20. Watch the two lines that do it:

  • Line 25, n->next = prev->next;, copies 20’s next, 0x1000, into the new node. For a moment two arrows point at 40.
  • Line 26, prev->next = n;, writes 0x1060 into 20’s next. 20’s arrow swings onto the new node, and the list is 10, 20, 30, 40.

Last, print_list walks the list with a pointer p. p moves from node to node, and no arrow on the heap changes. The output is 10 20 30 40 .

Now a prediction, before you touch the switch. What if the two lines ran the other way round, prev->next = n; first? Which arrows would you see, and what would the walk print?

Set the switch to “swapped”. The replay starts just before line 25. First 20’s arrow swings to the new node, and nothing points at 40 any more: it greys. Then n->next = prev->next; reads 20’s next, which is now 0x1060, the new node’s own address. The new node points at itself, and the walk prints 10 20 30 30 30 30 .

The walk stops there only because print_list takes a limit, max, and prints at most 6 keys; without it, a list that loops would print for ever. The 40 node is still on the heap, but no pointer leads to it, so the program can never free it. Built with the address sanitiser, the swapped program reports a leak of 16 bytes in 1 object.

Key idea

Inserting re-aims two pointers. Set the new node’s next first, while the old link still says where the rest of the list is; then point the node before it at the new node.

The code and its cost

Walking a list

print_list is the walk:

void print_list(const struct node *head, int max)
{
    for (const struct node *p = head; p != NULL && max > 0; p = p->next) {
        printf("%d ", p->key);
        max = max - 1;
    }
    printf("\n");
}

The const says that print_list only reads the nodes. p starts at the first node and stops at NULL. The step p = p->next writes a new address into p, the box in print_list’s frame, and into nothing else. A walk leaves the list as it found it. Without the limit, the loop is the usual walk, for (p = head; p != NULL; p = p->next).

Inserting at the front

struct node *push_front(struct node *head, int key)
{
    struct node *n = malloc(sizeof *n);
    if (n == NULL)
        exit(1);
    n->key = key;
    n->next = head;
    return n;
}

The new node points at the old first node, and becomes the first itself. push_front has its own copy of head, in its own frame at 0x7fd0, so writing to that copy would not change main’s head. It returns the new node’s address instead, and main stores it: head = push_front(head, 40);. If malloc finds no memory, the program stops with exit(1).

Inserting after a node

insert_after(prev, key) makes a node n the same way, then runs the two lines from the clip: n->next = prev->next;, then prev->next = n;. The order matters. The first line keeps the only record of where the rest of the list is, so it must copy that record before the second line overwrites it.

What it costs

Here n is the number of nodes in the list. Inserting after a node you already hold takes two pointer writes, plus the malloc and the key, however long the list is. Finding that node is the slow part: a list has no jump to its i-th node, so you walk from head, one step per node, up to n steps.

An array is the other way round. a[i] reaches any element in one step, but putting a new element at the front moves every element one place along. For a list of 1,000,000, that is 1,000,000 moves in the array and two pointer writes in the list.

Each node also pays for its link: 16 bytes for a 4-byte key on this platform.

Running it yourself

The code pane shows insert_after and main. The full program, list.c, is those with the three functions above and free_list, which gives every node back; Removing from a linked list (7.2) explains how it walks the list while freeing it. Build and run it with:

gcc -std=c11 -Wall -Wextra -pedantic list.c -o list
./list

Worked example

Three nodes sit on the heap, and head holds 0x1000:

AddressKeyNext
0x100050x1040
0x10209NULL
0x104070x1020

Read the list. Start at head: 0x1000 holds 5, and its next is 0x1040. 0x1040 holds 7, and its next is 0x1020. 0x1020 holds 9, and its next is NULL. So the list is 5, 7, 9, though the heap holds them 5, 9, 7.

Insert 8 after 7. insert_after gets prev = 0x1040, the node holding 7. Each line writes one box:

LineCodeBox writtenNew value
21struct node *n = malloc(sizeof *n);n0x1060
24n->key = key;key at 0x10608
25n->next = prev->next;next at 0x10600x1020
26prev->next = n;next at 0x10400x1060

The list is now 5, 7, 8, 9. Only two next fields were written, and the nodes holding 5 and 9 were not touched.

The swapped order. Run line 26 first: it writes 0x1060 into 7’s next. Then line 25 copies 7’s next, now 0x1060, into the new node. 8 points to 8, and nothing leads to 9. A walk with a limit of 6 would print 5 7 8 8 8 8 .

The cost. Put a key at the front of a 1,000,000-element array, and 1,000,000 elements shift; at the front of a list it is two pointer writes. Put it after the 500,000th element: the list walks 499,999 steps from head to reach that node, then writes two pointers; the array shifts the 500,000 elements after it.

The replay below builds this list with the page’s own functions: push_front(NULL, 5), then insert_after(head, 9) and insert_after(head, 7), then insert_after(head->next, 8). Before you play it: why does 7 land between 5 and 9? (Because it goes in after the node head points to, 5, and takes 5’s old next, 9.)

Inserting 8 after 7

Nodes allocated 5, 9, 7 and linked 5, 7, 9; then 8 goes in after 7

void insert_after(struct node *prev, int key){    struct node *n = malloc(sizeof *n);    if (n == NULL)        exit(1);    n->key = key;    n->next = prev->next;    prev->next = n;}int main(void){    struct node *head = push_front(NULL, 5);    insert_after(head, 9);    insert_after(head, 7);    insert_after(head->next, 8);    print_list(head, 6);    free_list(head);    return 0;}

main has started. Its frame holds head, not set yet.

Output

0.00 / 14.40 s
Describe this picture

Lines 19 to 27 and 47 to 56 of list-by-hand.c, the same functions as list.c with its own main, beside the same picture of memory: main’s frame with head at 0x7fe8 at the top, the called function’s frame beside it, and the heap row below. On a phone the nodes show their key and next without labels. The replay passes over the first three calls, one beat each, so that the nodes 5, 9 and 7 appear at 0x1000, 0x1020 and 0x1040, linked 5, 7, 9. Then insert_after(head->next, 8) runs a line at a time: prev holds 0x1040, the node 8 appears at 0x1060, its arrow reaches 9, and 7’s arrow swings from 9 onto it. The walk passes as one beat, and the replay rests with 5 7 8 9 in the output. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as byhand.step.

Where it’s used

Memory allocators. An allocator keeps the blocks that programs have freed on linked lists, one list per block size, with each block’s next pointer stored inside the free block itself. The C library’s malloc on this platform does this for small blocks.

Waiting tasks. Operating systems keep the tasks that are waiting, for a lock or for a device, on linked lists, so that a task can join or leave the line in a few pointer writes.

Hash tables. A common hash table keeps a short linked list in each of its slots, holding the keys that land there; Hash tables with chaining (18.2) builds one.

Items that must not move. A node stays at its address while the list around it grows and shrinks, so other parts of a program can hold its address safely. An array that grows may have to move all its elements to a larger block.

Reference card

CodeMeaning
struct node { int key; struct node *next; };a node: 16 bytes here (key 4, padding 4, next 8)
heada pointer to the first node; NULL for an empty list
for (p = head; p != NULL; p = p->next)the walk; it changes p and nothing else
n->next = head; then head = ninsert at the front (push_front returns n)
n->next = prev->next; then prev->next = n;insert after prev, in this order
the two lines swappedthe new node points at itself; the rest of the list is lost
insert after a node you holdtwo pointer writes, whatever the length
reach the i-th nodei − 1 steps from head; up to n for a list of n
insert at the front of an array of nn elements move

Platform: C11, gcc on x86-64 Linux: int 4 bytes, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look