Self-Reference & Linked Lists
A struct can contain a pointer to another instance of its own type — a small, strange-looking idea that unlocks the ability to build data structures that grow one node at a time, entirely at runtime.
Every structure you've written so far has been an island — a struct Student holds one student's data, and a second struct Student sitting next to it in memory has no idea the first one exists. Self-reference is the trick that lets a structure hold a pointer to another object of its own type, so one node can point at the next, that node can point at the one after it, and so on. Chain enough of these together and you get a linked list: a sequence of separately allocated nodes connected purely by pointers rather than by sitting next to each other in memory. This page builds that idea up from first principles — why the self-reference has to go through a pointer, how to allocate and connect nodes at runtime, the standard operations (push_front, push_back, pop_front, search, delete), the classic pitfalls that come with manual pointer bookkeeping, and how the same self-referential trick generalizes far beyond lists.
Why Self-Reference?
Consider a simple structure like struct Student { int id; char name[50]; };. Each Student object holds its own data just fine, but there's no relationship between one student and another — nothing in the type connects one instance to the next. Suppose you wanted to represent a sequence: Student 1 -> Student 2 -> Student 3. Somehow, each object needs to refer to the next one.
Nothing here lets one Student object reach another — there is no field that could hold a reference to a second Student.
A structure can gain exactly that ability by holding a pointer to another object of its own type: struct Node { int data; struct Node *next; };. The field struct Node *next; means next can store the address of another struct Node — nothing more exotic than that. A structure that contains a pointer to its own type like this is called a self-referential structure, and it's the foundation of linked lists and a whole family of other dynamic data structures.
next doesn't hold another Node's data directly — it holds an address. Following that address is what lets code hop from one node to the next.
struct Student
{
int id;
char name[50];
};struct Node
{
int data;
struct Node *next;
};Why Can't We Put the Structure Directly Inside Itself?
It's natural to wonder why the self-reference has to be a pointer at all — why not just embed the next Node directly as a member? That version is illegal in C and won't compile.
To lay this struct out in memory, the compiler would need to know struct Node's total size — but struct Node's size depends on the size of the struct Node member inside it, which depends on the size of the struct Node inside that one, forever. There's no finite size that satisfies the definition, so the compiler rejects it outright.
A pointer sidesteps the whole problem because a pointer's size is fixed and known regardless of what it points to — on a given platform, a struct Node * is always the same number of bytes, whether it points to one node or a million. The pointer doesn't contain another Node; it only stores an address, so the recursive-size problem never arises.
/* This does NOT compile */
struct Node
{
int data;
struct Node next; /* error: field has incomplete type */
};struct Node
{
int data;
struct Node *next; /* legal: fixed-size address, not another whole Node */
};A Basic Self-Referential Structure
With the struct Node definition in place, you can create plain Node objects and link them by hand, without any dynamic allocation yet — just to see the connection mechanism itself.
first and second are two independent Node objects. Writing first.next = &second; makes first's pointer hold second's address — that single assignment is the entire mechanism behind every linked list you'll build. second.next = NULL; marks second as the end of the chain, since there's nothing after it yet.
struct Node
{
int data;
struct Node *next;
};
struct Node first;
struct Node second;
first.next = &second;
second.next = NULL;Building a Node Chain by Hand
The same idea scales to more than two nodes. Here three ordinary (non-dynamic) Node variables are declared and wired together into a three-link chain: first -> second -> third -> NULL.
This program doesn't print anything — it only wires the three nodes together and exits. But structurally, this is already a linked list: three separate objects, each holding its own data, connected purely by next pointers rather than by their positions in memory.
data=30, next=NULL Three separate Node objects connected only by next pointers — first points to second, second points to third, and third's next is NULL, marking the end of the chain.
#include <stdio.h>
struct Node
{
int data;
struct Node *next;
};
int main(void)
{
struct Node first;
struct Node second;
struct Node third;
first.data = 10;
second.data = 20;
third.data = 30;
first.next = &second;
second.next = &third;
third.next = NULL;
return 0;
}What Is a Linked List?
A linked list is a collection of nodes where each node contains data and a link (a pointer) to another node. Unlike an array, the nodes don't have to be physically adjacent in memory at all — the next pointers connect them logically, so node #1 might live at address 0x1000 while node #2 lives somewhere completely different, like 0x7FE30.
Almost every linked list in C is built out of exactly the node shape you've already seen: one field for the payload, one field for the link to the next node.
struct Node
{
int data;
struct Node *next;
};Why Use Linked Lists?
An ordinary array provides contiguous storage — every element lives back-to-back in one memory block, which is exactly why array indexing is so fast. A linked list gives up that contiguity on purpose: nodes can be created and destroyed one at a time, wherever memory happens to be available, without requiring the whole collection to occupy one unbroken region. That makes growing and shrinking the collection cheap in a way a fixed-size array cannot match.
Dynamically Allocating a Node
A chain of plain local variables like first, second, and third only lasts as long as those variables are in scope, and you have to know the size of the list in advance. Real linked lists allocate each node on the heap with malloc, so nodes can be created on demand while the program runs.
malloc(sizeof *node) asks for exactly enough bytes to hold one struct Node and hands back its address. Checking for NULL before using the pointer is what makes the allocation safe — dereferencing a failed allocation is undefined behavior. Notice node->data and node->next: because node is a pointer, member access uses -> instead of the dot used for plain struct variables.
#include <stdlib.h>
struct Node
{
int data;
struct Node *next;
};
struct Node *node = malloc(sizeof *node);
if (node == NULL)
{
/* allocation failed */
}
node->data = 10;
node->next = NULL;Creating Multiple Nodes Dynamically
The same pattern repeats for as many nodes as you need, wiring each one's next pointer to the node allocated after it.
This is a heap-allocated version of the same three-node chain from before, but now first, second, and third are pointers to memory the program requested at runtime, not local struct variables.
data=30, next=NULL Functionally identical to the earlier hand-built chain, but every node here lives on the heap and was created with malloc, not declared as a local variable.
struct Node *first = malloc(sizeof *first);
struct Node *second = malloc(sizeof *second);
struct Node *third = malloc(sizeof *third);
first->data = 10;
first->next = second;
second->data = 20;
second->next = third;
third->data = 30;
third->next = NULL;The head Pointer
Once nodes are scattered across the heap, something has to keep track of where the list begins. That's the job of the head pointer — a single struct Node * that always points at the first node of the list (or holds NULL if the list is empty).
An empty linked list is nothing more than a single pointer set to NULL — no nodes exist yet, and head has nothing to point to.
The head pointer is extremely important, because it's the only handle the rest of the program has on the entire list. Every node reachable at all is reachable by starting at head and following next pointers — lose head, and every node after it becomes unreachable memory that can never be freed or used again.
Common mistake — losing the head pointer: writing head = head->next; advances the list correctly during traversal, but if that's done as part of removing a node without first saving the old head somewhere, the node that used to be first is leaked — its memory is still allocated, but nothing points to it anymore, so it can never be freed. Whenever a node is being removed, save a pointer to it before head moves on, as the pop_front pattern later on this page does.
struct Node *head = NULL; /* an empty list */Traversing a Linked List
Traversal means walking from head to the end of the list, one node at a time, by repeatedly following next. A local pointer variable — conventionally called current — is used so the walk doesn't disturb head itself.
current starts at head and is reassigned to current->next on every iteration, so it advances one node at a time. The loop stops naturally once current becomes NULL, which is exactly what the last node's next field was set to.
Wrapped in a function, this same loop becomes a reusable way to print any list, however long it is.
struct Node *current = head;
while (current != NULL)
{
printf("%d\n", current->data);
current = current->next;
}#include <stdio.h>
struct Node
{
int data;
struct Node *next;
};
void print_list(struct Node *head)
{
struct Node *current = head;
while (current != NULL)
{
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}Adding a Node at the Beginning
Inserting a brand-new node at the front of the list only takes two pointer assignments: point the new node's next at the current head, then move head to point at the new node. Because that's a fixed amount of work no matter how long the list already is, insertion at the front is O(1).
malloc a fresh node.
If allocation failed, bail out and change nothing.
Store value in the new node.
Point the new node's next at whatever head currently points to (the old first node, or NULL for an empty list).
Overwrite the caller's head so it now points at the new node.
The parameter type is struct Node **head — a pointer to the caller's head pointer, not the head pointer itself. That's deliberate: push_front needs to modify the caller's actual head variable, and in C, function arguments are always passed by value. A plain struct Node *head parameter would receive only a copy of the caller's pointer — modifying that copy inside the function would have no effect once the function returns. Taking a struct Node ** lets the function dereference back to the real variable and change what it points to, which is exactly what *head = new_node; does.
#include <stdlib.h>
void push_front(struct Node **head, int value)
{
struct Node *new_node = malloc(sizeof *new_node);
if (new_node == NULL)
return;
new_node->data = value;
new_node->next = *head;
*head = new_node;
}Adding a Node at the End
Appending works differently: without a separate tail pointer tracking the last node, the only way to find the end of the list is to walk all the way there, following next until a node's next field is NULL.
The empty-list case is handled specially: if *head is NULL, the new node simply becomes the whole list. Otherwise, current walks forward until it lands on the last existing node (the one whose next is NULL), and that node's next is pointed at the new node.
Because that walk has to visit every existing node, push_back without a tail pointer is O(n) — appending to a 10,000-node list takes roughly 10,000 times longer than appending to a 1-node list. Lists that append frequently often keep a separate tail pointer for exactly this reason, turning append back into an O(1) operation.
void push_back(struct Node **head, int value)
{
struct Node *new_node = malloc(sizeof *new_node);
if (new_node == NULL)
return;
new_node->data = value;
new_node->next = NULL;
if (*head == NULL)
{
*head = new_node;
return;
}
struct Node *current = *head;
while (current->next != NULL)
{
current = current->next;
}
current->next = new_node;
}Deleting the First Node
Removing the first node reverses push_front: save the current head somewhere, move head forward, then free the node that's no longer part of the list.
The order matters: temp captures the address of the old head before anything changes, head is then advanced to the second node, and only after head no longer refers to the old first node is that old node freed via temp. Save old node, move head, free old node — in that order, always.
Common mistake — use-after-free from freeing too early: if this were written as free(head); head = head->next;, the free() call destroys the node before head->next is read from it — that read is now accessing memory that's already been released, which is undefined behavior (it might work by accident, or it might crash, or it might silently corrupt something else). Always read whatever you need out of a node before you free it, never after.
This is the same save/move/free sequence wrapped as a reusable, safe function. Guarding against *head == NULL first also protects against calling pop_front on an already-empty list. Like push_front, this is O(1) — no traversal is needed to remove the first node.
Common mistake — forgetting free() entirely: every node created with malloc has to be released with free() exactly once when it's no longer needed, whether that's through pop_front, delete_value, or a full free_list pass at the end of the program. A node that's unlinked from the list but never freed is a memory leak — the memory is gone for the rest of the program's lifetime even though nothing can reach it anymore.
struct Node *temp = head;
head = head->next;
free(temp);void pop_front(struct Node **head)
{
if (*head == NULL)
return;
struct Node *temp = *head;
*head = (*head)->next;
free(temp);
}Searching a Linked List
Finding a value means the same kind of traversal as printing, except the loop stops early and returns a pointer as soon as a match is found.
find returns a pointer to the first node whose data matches value, or NULL if the value never appears in the list. Callers must check the return value against NULL before dereferencing it, exactly like any other pointer that can fail to find something.
Because there's no way to know where a value is without checking nodes one at a time from the front, searching a singly linked list is generally O(n) — there's no equivalent of an array's constant-time indexed lookup.
struct Node *find(struct Node *head, int value)
{
struct Node *current = head;
while (current != NULL)
{
if (current->data == value)
return current;
current = current->next;
}
return NULL;
}Deleting a Specific Node
Removing a node from the middle of the list is trickier than removing the first node, because the node before it needs to be re-linked around it. That requires tracking two pointers while traversing: current, the node being examined, and previous, the node right before it (or NULL, while current is still the first node).
If the match is the first node (previous is still NULL), the head pointer itself is updated to skip it. Otherwise, previous->next is pointed past current, splicing current out of the chain. Either way, current is freed only after the surrounding pointers have already been repaired.
Common mistake — incorrect pointer updates when inserting or deleting: it's easy to get the order of pointer assignments wrong and accidentally disconnect part of the list — for example, overwriting current->next before previous->next has been redirected to it would strand every node after current. Before changing any pointers, sketch the before-and-after chain on paper (or mentally) and make sure every node that should still be reachable actually is, once every assignment has run.
void delete_value(struct Node **head, int value)
{
struct Node *current = *head;
struct Node *previous = NULL;
while (current != NULL)
{
if (current->data == value)
{
if (previous == NULL)
{
*head = current->next;
}
else
{
previous->next = current->next;
}
free(current);
return;
}
previous = current;
current = current->next;
}
}The Empty List Case
An empty linked list is represented purely by head == NULL — there are no nodes to skip past or special-case in the struct itself, just a head pointer that doesn't point anywhere yet. Every function that touches the list has to consider this case before it does anything else.
Common mistake — dereferencing NULL: code like printf("%d", head->data); crashes (or worse, silently misbehaves) the moment head is NULL, because there is no data field to read — head isn't pointing at a node at all. Always check for NULL (if (head == NULL) ...) before dereferencing a pointer that might be empty, especially head itself and the result of find().
. vs ->
C gives you two different operators for reaching into a structure's members, and which one is legal depends on whether you're holding the structure itself or a pointer to it. Use the dot operator (.) on an actual structure object, and use the arrow operator (->) on a pointer to a structure.
ptr->data is defined to mean exactly the same thing as (*ptr).data — dereference the pointer to get the object, then use dot to reach the member. The arrow spelling exists purely because writing (*ptr).data everywhere would be tedious and easy to mis-parenthesize.
struct Node n;
n.data = 5; /* n is an object: use . */
struct Node *ptr = &n;
ptr->data = 5; /* ptr is a pointer: use -> */
(*ptr).data = 5; /* equivalent to ptr->data, just uglier */typedef for Cleaner Node Definitions
Writing struct Node repeatedly gets verbose. A typedef can attach a shorter alias, Node, to the same type, so later code can write Node * instead of struct Node *.
Notice that the next field inside the definition still has to say struct Node *next;, not Node *next; — the alias Node isn't actually introduced until the entire typedef statement finishes, so it isn't available for use yet inside the struct body itself. Using struct Node there (the tag name, which is visible immediately) sidesteps that ordering problem entirely.
typedef struct Node
{
int data;
struct Node *next;
} Node;
/* later: */
Node *head = NULL;A Complete Working Linked List Program
Putting push_front, print_list, and a cleanup function together produces a small but complete linked-list program — one that builds a list, prints it, and then releases every node it allocated.
Each push_front(&head, ...) call inserts at the front, so pushing 30, then 20, then 10 leaves the list in the order 10 -> 20 -> 30 — the most recently pushed value ends up first. print_list walks that chain and prints 10 20 30, and free_list walks it a second time afterward to release every node before the program exits.
head starts as NULL (empty list).
push_front(&head, 30): list becomes 30 -> NULL.
push_front(&head, 20): list becomes 20 -> 30 -> NULL.
push_front(&head, 10): list becomes 10 -> 20 -> 30 -> NULL.
print_list(head) prints: 10 20 30 free_list(head) releases all three nodes.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node
{
int data;
struct Node *next;
} Node;
void push_front(Node **head, int value)
{
Node *new_node = malloc(sizeof *new_node);
if (new_node == NULL)
return;
new_node->data = value;
new_node->next = *head;
*head = new_node;
}
void print_list(const Node *head)
{
while (head != NULL)
{
printf("%d ", head->data);
head = head->next;
}
printf("\n");
}
void free_list(Node *head)
{
while (head != NULL)
{
Node *next = head->next;
free(head);
head = next;
}
}
int main(void)
{
Node *head = NULL;
push_front(&head, 30);
push_front(&head, 20);
push_front(&head, 10);
print_list(head);
free_list(head);
return 0;
}Why free_list() Saves next First
free_list has to visit every node exactly once and free each one, but it also has to know where to go next after a node is destroyed — and it can't ask the node itself once that node has been freed.
The wrong version frees head and only then tries to read head->next — but head no longer refers to a valid, live object at that point, so reading from it is a use-after-free (undefined behavior). The fix is simply to read and save next before free() is called at all, exactly as free_list does in the complete program above.
Common mistake — use-after-free: this is the same underlying error as freeing pop_front's temp before reading its next field, and it's one of the most common linked-list bugs in C. The rule that avoids it is simple and universal: never dereference a pointer after the object it points to has been freed. Extract every piece of information you still need from a node before you call free() on it, never after.
/* WRONG: */
free(head);
head = head->next; /* use-after-free: head was just freed */
/* CORRECT: */
Node *next = head->next; /* read next BEFORE freeing */
free(head);
head = next;Singly, Doubly, and Circular Linked Lists
Every list built so far on this page is a singly linked list: each node knows only the node after it, so traversal can move forward but never directly backward — to reach an earlier node you'd have to restart from head.
A doubly linked list adds a second pointer, prev, so each node also knows the node before it. That makes traversal possible in both directions, at the cost of extra memory per node and more pointer updates to keep correct on every insertion and deletion — every insert or delete now has to fix up prev links as well as next links.
A circular linked list changes the ending condition instead of adding a pointer: the final node's next points back to the first node (last->next = head;) rather than to NULL. There is no NULL anywhere in a circular list once it has at least one node.
In a circular linked list the last node points back to the first instead of to NULL — the chain has no end, only a loop.
Common mistake — infinite traversal: the ordinary while (current != NULL) loop used throughout this page works perfectly for a singly (or doubly) linked list, but it never terminates on a circular list, because current is never NULL — it just keeps looping around the ring forever. Traversing a circular list needs a different stopping condition, such as a do-while loop that stops once current returns to head, or an explicit counter.
struct Node
{
int data;
struct Node *prev;
struct Node *next;
};Linked List Complexity
The core operations covered on this page have very different costs depending on where in the list they happen, and whether the list has a tail pointer available.
The single biggest difference from an array is random access: an array can compute the address of any element directly from its index (base_address + index * element_size), an O(1) calculation. A linked list has no such shortcut — reaching the i-th node generally means following i pointers from head, one at a time.
Operation Cost
----------------------------------------------
Access first node (head) O(1)
Access by index (i-th node) O(n)
Search for a value O(n)
Insert at head (push_front) O(1)
Delete head (pop_front) O(1)
Insert at end, no tail pointer O(n)
Delete a known node (have previous) O(1)
Traverse the entire list O(n)Linked List vs Array
Arrays offer contiguous storage, O(1) random access by index, and usually excellent cache locality, since neighboring elements sit right next to each other in memory — but inserting or deleting near the beginning typically costs O(n), because every later element has to shift over, and an array's dynamic-node story is limited (a fixed-size array can't grow at all; even a dynamic array that reallocates does so in large jumps, not one element at a time).
Linked lists flip most of those trade-offs: nodes can live anywhere in memory, random access degrades to O(n), and each node carries the extra memory overhead of at least one pointer — but insertion and deletion at the head are O(1), and new nodes can be created one at a time, dynamically, without disturbing the rest of the list. Because nodes are scattered rather than contiguous, linked lists also tend to have worse cache locality than arrays in practice.
Neither structure is automatically better — the right choice depends entirely on the access and modification pattern a program actually needs. Frequent indexed access favors arrays; frequent insertion and deletion, especially near the front or in the middle, favors linked lists.
Self-Reference Beyond Linked Lists
Self-referential structures aren't unique to linked lists — the same trick is the building block for trees (binary trees, search trees) and graphs (adjacency structures) as well. A binary tree node, for instance, needs two self-references instead of one: a pointer to a left child and a pointer to a right child, both of the same node type.
This compiles for exactly the same reason struct Node with a single next pointer compiles: left and right are fixed-size pointers, not embedded TreeNode objects, so there's no infinite-size problem no matter how large the tree eventually grows.
The fundamental idea carries over unchanged: a structure contains pointers that can refer to other instances of that same structure. A linked list is simply the special case where each node has exactly one such pointer, forming a single chain instead of a branching tree.
struct TreeNode
{
int data;
struct TreeNode *left;
struct TreeNode *right;
};Best Practices and the Key Mental Model
A handful of habits make manual pointer manipulation far less error-prone in practice. Always initialize pointers explicitly — Node *head = NULL;, never an uninitialized local. Initialize every new node's data and next fields as soon as it's allocated, so no field is ever left holding garbage. Check every allocation's return value against NULL before using it. Keep ownership clear — know exactly which part of the program is responsible for eventually freeing each node, so nodes are neither leaked nor freed twice. Encapsulate list operations behind small functions like push_front, push_back, pop_front, delete_value, find, print_list, and free_list, so the pointer manipulation lives in one well-tested place instead of being repeated (and re-broken) throughout the codebase. And whenever a node is about to be freed, save whatever links or data you still need from it first.
Underneath all of that, the mental model for a linked list stays simple: it's a chain of dynamically allocated objects connected by struct Node *next; pointers. The pointer is what creates the connection between nodes — the structure itself never contains another structure object directly, only the address of one. Every operation on this page, from building a three-node chain by hand to freeing a whole list safely, is just a careful application of that one idea.
What does it mean for a structure to be "self-referential," and why must the self-reference be a pointer rather than an embedded object of the same struct type?
A self-referential structure contains a member that can refer to another object of its own type. It has to be a pointer because a pointer has a small, fixed size regardless of what it points to; embedding the struct type directly would require the compiler to know the struct's total size, which depends recursively on itself and would need infinite memory — so C rejects that form at compile time.
Why must push_front take a struct Node **head parameter instead of struct Node *head?
Function arguments in C are passed by value, so a struct Node *head parameter would only receive a copy of the caller's pointer; changing that copy inside the function wouldn't affect the caller's actual head variable. Taking struct Node **head instead gives the function the address of the caller's head pointer, so dereferencing it with *head = new_node; changes the real variable the caller sees.
What's dangerous about writing free(current); current = current->next; and what should be written instead?
free(current) releases the node current points to, so the subsequent read of current->next accesses memory that's already been freed — a use-after-free, which is undefined behavior. The safe order is to read and save the next pointer first (Node *next = current->next;), then free the current node, and only then advance using the saved pointer.
Why is inserting a new node at the front of a linked list O(1), while appending a node at the end without a tail pointer is O(n)?
Inserting at the front only touches the new node and the head pointer — a fixed amount of work no matter how long the list is. Appending at the end without a tail pointer requires walking the entire list first to find the last node (the one whose next is NULL), and that walk grows longer as the list grows, making it O(n).
Why does traversing a circular linked list with the ordinary while (current != NULL) condition risk running forever, and what has to change?
In a circular list the last node's next points back to the first node instead of to NULL, so current is never NULL during traversal — the loop keeps looping around the ring indefinitely. Traversal needs a different stopping condition instead, such as looping until current comes back around to equal head again (for example with a do-while loop), rather than checking for NULL.
Write a C program that manually builds a chain of three struct Node values (without using malloc or any loop), links them together, and prints their data fields by traversing from the first node.
Three plain struct Node variables are declared and connected by hand with & to take each one's address, forming first -> second -> third -> NULL. A current pointer starting at &first then walks the chain the same way it would walk a heap-allocated list, proving that traversal only cares about the next-pointer links, not where the nodes actually live in memory.
Write a C program that implements a push_front(struct Node **head, int value) function to insert nodes at the beginning of a singly linked list, uses it to build a list, and prints the resulting list.
push_front takes a struct Node ** so it can update the caller's actual head variable. Each call allocates a new node, points its next at whatever head currently is, and then repoints head at the new node, so pushing 30, 20, then 10 leaves the list ordered 10 20 30 — the most recently inserted value ends up in front.
Write a C program that implements a push_back(struct Node **head, int value) function to append a node to the end of a singly linked list, uses it to build a list of several values, and prints the result.
push_back handles the empty-list case directly by making the new node the head, and otherwise walks to the current last node (the one whose next is NULL) before linking the new node after it. Appending 10, then 20, then 30 in that order produces the list 10 20 30, since each value is added at the tail rather than the front.
Write a C program that implements a pop_front(struct Node **head) function to remove and free the first node of a singly linked list, and demonstrate it removing nodes one at a time from a small list.
pop_front saves the current head node in temp before moving head to head->next, and only frees temp afterward, avoiding a use-after-free. Starting from the list 10 20 30, the first pop_front call removes 10 and leaves 20 30, and the second removes 20 and leaves just 30, each printed after the removal to show the change.
Write a C function find(struct Node *head, int value) that searches a singly linked list for a node containing a given value and returns a pointer to it (or NULL if not found), and demonstrate it in main() by searching for both a present and an absent value.
find walks the list from head, comparing each node's data against value, and returns the matching node's address the moment one is found, or NULL after reaching the end without a match. The program searches the built list 10 20 30 for 20 (present, prints Found: 20) and 99 (absent, prints Not found), showing both outcomes of the NULL check callers must always perform.
Write a C function delete_value(struct Node **head, int value) that removes the first node containing a given value from a singly linked list, correctly handling removal of the head node, a middle node, and freeing the removed node, and demonstrate it in main().
delete_value tracks previous alongside current so it always knows which node's next pointer must be redirected: if the match is the head node (previous is NULL) the head pointer itself is updated, otherwise previous->next is spliced past the matched node, and only then is the matched node freed. Starting from 10 20 30, removing 20 (a middle node) leaves 10 30, and then removing 10 (the head) leaves just 30.
Write a C function that counts the number of nodes in a singly linked list by traversing it, and demonstrate it on a list built with push_back.
count_nodes performs a standard traversal, incrementing a counter once per node instead of printing anything, and stops when it reaches NULL. Building the list with four push_back calls and then calling count_nodes reports a count of 4, matching the number of nodes actually allocated.
Write a C program that correctly frees every node in a singly linked list using a free_list function, structured so it never dereferences a node after that node has already been freed.
free_list saves each node's next pointer in a local variable before calling free() on that node, then advances using the saved pointer rather than reading head->next after head has already been freed. That ordering — read next, free the node, then move to next — is exactly what avoids the classic use-after-free bug in list cleanup code.
Write a C program that reverses a singly linked list in place, by rewiring each node's next pointer without allocating any new nodes, and prints the list before and after reversing.
reverse_list walks the list once, saving each node's original next before overwriting it to point backward at previous, then advancing both previous and current forward. Once current runs off the end, previous is sitting on what used to be the last node, which is now the new head, so the function returns previous. The list 10 20 30 prints as 30 20 10 after reversal.
Write a C program that builds a small doubly linked list with an insert_front function, using both next and prev pointers, and traverses it forward from the head and then backward from the tail.
insert_front links the new node's next to the old head and, when the list wasn't empty, also points the old head's prev back at the new node, keeping both directions consistent. The forward loop follows next pointers from head while remembering the last node it visits as tail, and the backward loop then follows prev pointers from that tail back to the front, printing 10 20 30 forward and 30 20 10 backward.