Difficulty: Intermediate | Prerequisites: Linked List Concepts study notes, C pointers, malloc/free, pass-by-reference with double pointers
Once you understand what linked lists are, the next hurdle is writing correct C code that manipulates them. This is where most exam marks are won or lost. The patterns here, insertion, deletion, reversal, and the bugs that come with them, recur across every linked list question you will see. The single biggest source of errors is mishandling pointers: updating them in the wrong order, forgetting one, or passing the head pointer by value when you need to modify it. This document walks through the core code patterns and the most common bugs, with worked examples.
TL;DR: Linked list code in C revolves around a small set of pointer-manipulation patterns. Learn the order of pointer updates for insertion and deletion, know when you need a double pointer (Node**), and use do...while for circular list traversal. Most exam bugs test exactly these things.
Double pointer (struct Node**)
A pointer to a pointer. Used when a function needs to modify the caller's head pointer (e.g. inserting into an empty list, or deleting the head node). If you pass Node* instead, the function works on a local copy and the caller's pointer stays unchanged.
Think of it as handing someone the address of your address book, so they can rewrite the first entry, rather than handing them a photocopy.
Iterative reversal
The standard algorithm for reversing a singly linked list in place. Uses three pointers (prev, curr, next) and walks the list once, flipping each next pointer to point backward. Runs in O(n) time and O(1) extra space.
do...while loop (for circular lists)
A loop that executes its body at least once before checking the condition. Essential for circular linked list traversal, because the head node is never NULL and a standard while (temp != NULL) test would either skip the loop body or run forever.
The task: given a doubly linked list, find the first node whose data matches target and insert a new node with value val immediately after it.
The steps, in order:
Traverse with a temp pointer until you find the node where temp->data == target, or reach NULL.
If temp is NULL, the target was not found. Return early.
Allocate a new node. Set newNode->data = val.
Point newNode->next to temp->next (the node that currently follows the target).
Point newNode->prev to temp.
If temp->next is not NULL, update temp->next->prev to point back to newNode. (Skipping this step is a classic bug; the list looks fine going forward but breaks going backward.)
Finally, set temp->next = newNode.
void insertAfter(struct Node* head, int target, int val) {
struct Node* temp = head;
while (temp != NULL && temp->data != target) {
temp = temp->next;
}
if (temp == NULL) return;
struct Node* newNode = malloc(sizeof(struct Node));
newNode->data = val;
newNode->next = temp->next;
newNode->prev = temp;
if (temp->next != NULL)
temp->next->prev = newNode;
temp->next = newNode;
}
The critical ordering here: set the new node's pointers first, then update the existing nodes. If you set temp->next = newNode before setting newNode->next, you lose your reference to the rest of the list.
Deletion in a circular list is trickier than in a standard list because there is no NULL sentinel and the head node is a special case.
Two cases to handle:
Deleting the head node: You must find the tail (the node whose next points to head), update the tail's next to skip the old head, and advance the head pointer. If the list has only one node, set head to NULL. This is why you need a double pointer (Node**).
Deleting a non-head node: Traverse with prev and curr pointers starting from the head. Stop when curr->data == val or when curr wraps back to head (value not found). Unlink by setting prev->next = curr->next, then free(curr).
void deleteNode(struct Node** head, int val) {
if (*head == NULL) return;
struct Node *curr = *head, *prev = NULL;
// Special case: head node
if (curr->data == val) {
while (curr->next != *head) {
curr = curr->next;
}
struct Node* temp = *head;
if (*head == (*head)->next) {
*head = NULL;
} else {
curr->next = (*head)->next;
*head = (*head)->next;
}
free(temp);
return;
}
prev = *head;
curr = (*head)->next;
while (curr != *head && curr->data != val) {
prev = curr;
curr = curr->next;
}
if (curr == *head) return; // not found
prev->next = curr->next;
free(curr);
}
Notice the use of Node** so the function can reassign the caller's head pointer when the head itself is deleted.
This is one of the most commonly examined algorithms. Three pointers walk the list in a single pass:
prev starts at NULL (it will become the new head).
curr starts at head.
next is a temporary holder so you do not lose the rest of the list when you flip curr->next.
On each iteration: save curr->next in next, point curr->next backward to prev, advance prev to curr, advance curr to next. When curr is NULL, prev points to the new head.
struct Node* reverseList(struct Node* head) {
struct Node* prev = NULL;
struct Node* curr = head;
struct Node* next = NULL;
while (curr != NULL) {
next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
Trace through with 5 -> 10 -> 15:
Iteration 1: flip 5's next to NULL, prev=5, curr=10
Iteration 2: flip 10's next to 5, prev=10, curr=15
Iteration 3: flip 15's next to 10, prev=15, curr=NULL
Return prev (15). Result: 15 -> 10 -> 5.
Node* instead of Node** for head-modifying functionsThe buggy insertEnd function below takes struct Node* head. If the list is empty (head == NULL), the function dereferences NULL and crashes. Even if the list is non-empty, any change to head inside the function is invisible to the caller.
// BUGGY
void insertEnd(struct Node* head, int data) {
struct Node* newNode = malloc(sizeof(struct Node));
newNode->data = data;
newNode->next = NULL;
while (head->next != NULL) {
head = head->next;
}
head->next = newNode;
}
Fixes:
Change the parameter to struct Node** head and dereference accordingly, or
Return the (possibly new) head pointer and have the caller reassign it.
Add a NULL check before the while loop to handle the empty list.
while (temp != NULL) in a circular listIn a circular list, no node's next is NULL. A while (temp != NULL) loop will either run forever (if the list is non-empty) or behave unpredictably.
// BUGGY
int count(struct Node* head) {
int cnt = 0;
struct Node* temp = head;
while (temp != NULL) {
cnt++;
temp = temp->next;
if (temp == head) break;
}
return cnt;
}
The while (temp != NULL) condition is the wrong guard. If the list is non-empty, temp is never NULL, so the inner break is doing all the work, and the structure is fragile. The correct approach uses a do...while:
// FIXED
int count(struct Node* head) {
if (head == NULL) return 0;
int cnt = 0;
struct Node* temp = head;
do {
cnt++;
temp = temp->next;
} while (temp != head);
return cnt;
}
The do...while guarantees the body runs once (counting the head), then stops when it loops back.
prev pointer update in doubly linked list insertionIf you insert a new node after temp but only set newNode->prev = temp and temp->next = newNode without also updating temp->next->prev = newNode, the forward chain is correct but the backward chain still skips the new node. This produces a list that looks fine when printed forward but breaks on backward traversal. Exams love this one because the bug is invisible unless you specifically test backward traversal.
⚠️ Bug-tracing questions almost always involve one of the three bugs above. Read the function signature first: if a function modifies the head and takes Node* instead of Node**, that is likely the intended bug.
⚠️ Output-tracing questions on reversal are common. Practice tracing the three-pointer reversal by hand with a short list (3 or 4 nodes). Write out prev, curr, and next at each step.
⚠️ For circular list questions, always check: does the code handle the single-node case? Does it use a do...while? These are the two most common failure points.
⚠️ When writing insertion or deletion code for doubly linked lists on an exam, draw the pointer diagram before writing any code. Update the new node's pointers first, then the existing nodes'. That order prevents you from losing references.
True or False: In the iterative reversal algorithm, prev starts at head.
A: False. prev starts at NULL. After the loop, prev points to the new head.
Fill in the blank: To allow a function to modify the caller's head pointer, the parameter type should be struct Node** ____.
A: head (i.e. a double pointer to Node).
True or False: In a circular singly linked list, deleting the head node requires finding the tail first.
A: True. You need the tail so you can redirect its next pointer to the new head.
Q: Given the list 5 -> 10 -> 15, what is the result of calling the iterative reverseList function?
A: 15 -> 10 -> 5. Each node's next pointer is flipped to point to the previous node.
Q: The following insertEnd function takes struct Node* head. Name two bugs.
A: (1) It should take struct Node** head (or return the new head), because if the list is empty, the caller's pointer is never updated. (2) It does not check for head == NULL, so it will segfault on an empty list.
Q: A function counts nodes in a circular linked list using while (temp != NULL). What is wrong, and how do you fix it?
A: In a circular list, no node is NULL, so the condition is always true and the loop either depends on an inner break or runs forever. Replace with a do...while loop that terminates when temp returns to head, with a NULL check for the empty list before the loop.
Q: When inserting a new node into a doubly linked list, what happens if you forget to update the prev pointer of the node that follows the insertion point?
A: The forward chain (next pointers) will be correct, but the backward chain (prev pointers) will skip the new node. Backward traversal will behave as if the new node does not exist, making the list inconsistent.
Q: In the circular list deletion function, why is Node** used instead of Node*?
A: Because deleting the head node requires changing which node the caller's head pointer refers to. With Node*, the reassignment is local to the function and the caller still points to the freed (now dangling) old head.
Pointer manipulation at this level is directly reused in binary trees and binary search trees, where insertion and deletion involve the same "update child, update parent" logic with left/right pointers instead of next/prev. The double-pointer technique (Node**) reappears whenever a recursive or iterative tree function needs to modify a subtree root. The reversal algorithm is also a building block for more advanced list problems such as reversing in groups of k, which appears in later coursework and technical interviews.
linked list insertion C, linked list deletion C, doubly linked list insert after, circular linked list delete node, reverse singly linked list iteratively, three-pointer reversal, double pointer Node**, pass by reference C, linked list bugs, segfault empty list, do-while circular traversal, COP 3502, Computer Science I, UCF, linked list exam questions, bug tracing linked list