Skip to content

Removing from a linked list

To remove a node, make the link that points to it point past it, then free it. A pointer to that link treats the head like any other link.

Helps to know firstSingly linked lists

Helps to know first
This topic

First, the picture

To take a node out of a chain, one arrow has to skip it. Which box holds that arrow? Watch the tag called link.

Describe this picture

Lines 36 to 46 (remove_key) and 48 to 62 (main) of remove.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. remove_key’s frame holds link at 0x7fd0, key at 0x7fcc and dead at 0x7fc0. Below is the heap: one row of 16-byte nodes in address order, 40 at 0x1000, 30 at 0x1020, 20 at 0x1040 and 10 at 0x1060, each with its key, a grey band of padding and its next field. Arrows run from each next field to the node it holds the address of, under the row, and from head down to the first node; a NULL pointer is a short bar. A pointer of the called function is a small tag on the node it points into: link’s tag sits on the node whose next field it holds the address of, and while link holds head’s address its box shows 0x7fe8 and there is no tag. The box the last line wrote is outlined, and a freed node is hatched. The clip plays once, about 26 seconds. The four pushes pass in a beat each and build 10, 20, 30, 40. Then remove_key(&head, 30) runs line by line: link holds 0x7fe8, then 0x1068, then 0x1048, 20’s next field; dead takes 0x1020; 20’s arrow swings past 30 onto 40, and 30 hatches. remove_key(&head, 10) runs the same lines with link still on head: head’s arrow swings to 20, and 10 hatches. remove_key(&head, 50) walks link to 0x1048 and 0x1008 and stops on NULL. Last, the output shows no 50 and 20 40, and the clip rests there. Under the picture are one sentence on the current step and 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 unlink.step.

The list is the one from Singly linked lists: push_front four times makes 10, 20, 30, 40, with head at 0x7fe8 holding 0x1060, the 10 node. The nodes sit on the heap in the order they were made, 40 first.

To remove 30, the program has to change the box that holds 30’s address. That box is 20’s next field, at 0x1048: the next field of a node starts 8 bytes into it. Change it to 30’s own next, 0x1000, and the chain skips 30. To remove 10, the box to change is head itself, because head holds 10’s address.

So every node has one box that points to it: head for the first node, a next field for every other. Call that box the node’s link. remove_key finds the link and changes it, and its parameter is the address of a link:

bool remove_key(struct node **link, int key)

struct node **link is a pointer to a pointer. It holds the address of a box, and that box holds a node’s address. Read it in steps:

  • link is an address, such as 0x1048: the address of 20’s next field.
  • *link is what that box holds, 0x1020: the address of the node after it.
  • (*link)->key is that node’s key, 30.
  • &(*link)->next is the address of that node’s next field: where link moves to next.

Watch link’s box in the clip. The first call gets &head, 0x7fe8: head’s own address. 10 is not 30, so link moves to 0x1068, 10’s next field; then to 0x1048, 20’s next field. There *link is the 30 node, and the loop stops. link points at the box that has to change, never at the node itself. In the picture, link’s tag sits on the node whose next field it points into, 10 and then 20; while link holds head’s address, its box shows 0x7fe8 and there is no tag.

Then two lines do the removal:

  • Line 43, *link = dead->next;, writes 0x1000 into 20’s next field. 20’s arrow swings past 30 onto 40.
  • Line 44, free(dead);, gives the 30 node back. It hatches.

The second call removes 10, the first node. The loop does not run at all, since head already holds 10’s address: link stays at 0x7fe8, and line 43 writes 0x1040 into head. The same three lines did it, with no extra case for the head.

The third call looks for 50. link walks to 0x1048 and then 0x1008, 40’s next field, which holds NULL. The loop stops on NULL, line 40 sees it, and remove_key returns false without writing anything.

Key idea

Every node has one link, the box holding its address: head for the first node, the next field before it for every other. Make that link point past the node, then free the node.

The code and its cost

bool remove_key(struct node **link, int key)
{
    while (*link != NULL && (*link)->key != key)
        link = &(*link)->next;
    if (*link == NULL)
        return false;
    struct node *dead = *link;
    *link = dead->next;
    free(dead);
    return true;
}

The loop stops in one of two places: on the link that holds the key’s node, or on the last next field, which holds NULL. main calls it as remove_key(&head, 30), so the walk starts at head. On an empty list, head holds NULL, so the loop never runs and remove_key returns false at once.

The order of lines 43 and 44 matters. After free(dead), the node’s 16 bytes belong to the C library again, and the program must not read them. *link = dead->next reads dead’s next field, so it must run while the node is still the program’s. The worked example below swaps them and shows what the address sanitiser says.

The version with prev and cur

The other common way walks two pointers: cur on the node being looked at, and prev on the one before it.

struct node *remove_first(struct node *head, int key)
{
    struct node *prev = NULL;
    struct node *cur = head;
    while (cur != NULL && cur->key != key) {
        prev = cur;
        cur = cur->next;
    }
    if (cur == NULL)
        return head;
    if (prev == NULL)
        head = cur->next;
    else
        prev->next = cur->next;
    free(cur);
    return head;
}

It works, but the box to change is either head or prev’s next field, so it needs the if (prev == NULL) to tell them apart, and it has to return the new head for main to store: head = remove_first(head, 30);. The pointer to a link has no such case, because head and a next field are both just boxes holding a node’s address.

Freeing a whole list

void free_list(struct node *head)
{
    while (head != NULL) {
        struct node *next = head->next;
        free(head);
        head = next;
    }
}

The same rule: read the next address first, while the node is still the program’s, then free it, then move on. Without next, the line after free(head) would have to read head->next from a freed node.

What it costs

Here n is the number of nodes in the list. Finding the link walks up to n steps, one per node, as any walk does. Unlinking and freeing is a fixed few steps, however long the list is.

Running it yourself

The code pane shows remove_key and main. The full program, remove.c, starts with struct node, push_front, print_list and free_list; the later programs of this chapter copy those lines unchanged. Build and run it with:

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

It prints no 50, then 20 40 .

Worked example

Remove 20 from 20 → 40 by hand. head is at 0x7fe8 and holds 0x1040, the 20 node; 20’s next field holds 0x1000, the 40 node. Call remove_key(&head, 20):

Line that runsWhat happensBox written
38link holds 0x7fe8; *link is the 20 node, whose key is 20: the loop does not runnone
40*link is not NULLnone
42dead holds 0x1040dead
43*link = dead->nexthead, now 0x1000
44free(dead)the 20 node is freed

head now holds 40’s address, and the list is 40. It is the first node again, and no line was different.

Free first, then unlink. Swap lines 43 and 44, so that free(dead); runs before *link = dead->next;, and build the program with the address sanitiser. gcc already warns that the pointer dead is used after free. The run stops in the first call with:

ERROR: AddressSanitizer: heap-use-after-free
READ of size 8
SUMMARY: AddressSanitizer: heap-use-after-free free-first.c:44 in remove_key

The read is 8 bytes, the size of a next field: the program read dead’s next after giving the node back. Without the sanitiser, the C library may already have written its own bookkeeping into those bytes, and the list would then point into it.

Emptying a list of 1,000. Remove every key from a 1,000-node list, one call each. Front first, each key is in the first node, so link never moves: 0 moves of link in all. Back first, the last node of a list of k nodes is k − 1 moves away, so the calls take 999 + 998 + … + 0 = 499,500 moves of link. The same keys, the same lines, and the order alone makes the difference.

Where it’s used

Operating systems. A kernel keeps tasks on linked lists, and a task that finishes is unlinked from its list in a few pointer writes.

Memory allocators. An allocator keeps freed blocks on lists by size; handing a block out removes it from its list.

Hash tables. A table with chaining keeps a short list in each slot, and removing a key is this walk on that list; Hash tables with chaining (18.2) uses it. Deleting from a binary search tree (13.2) uses the same pointer to a link, with a left or right field in place of next. And Memory errors (2.5) has more of the reports the sanitiser gives.

Reference card

CodeMeaning
struct node **linkthe address of a box that holds a node’s address: head, or a next field
*linkthe node’s address; (*link)->key its key
link = &(*link)->nextmove on to that node’s next field
*link = dead->next; then free(dead);unlink, then free, in this order
remove_key(&head, key)removes the first node holding key; false if there is none
next = head->next; free(head); head = next;free a list: save next before the free
remove from a list of nup to n steps to find the link; a fixed few to unlink

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