Introduction
A C program to implement linked list is a fundamental exercise for any programmer beginning their journey into data structures and algorithms. Linked lists provide a flexible way to store collections of data where each element, called a node, contains both the data and a reference (or pointer) to the next element in the sequence. Unlike arrays, linked lists do not require a fixed size at compile time, making them ideal for scenarios where the amount of data is unknown or changes dynamically. This article walks you through the complete process of building a singly linked list in C, explains the underlying concepts, and answers common questions to ensure you grasp both the practical implementation and the theory behind it Took long enough..
Steps to Implement a Linked List in C
Creating a Node Structure
The first step is to define a structure that represents a node. This structure typically holds the data and a pointer to the next node.
struct Node {
int data; // The actual data stored in the node
struct Node* next; // Pointer to the next node in the list
};
The struct Node is the building block of our linked list. The data field can be of any type—commonly int, char, or a custom struct. The next field is a pointer that points to the subsequent node, forming a chain Most people skip this — try not to..
Quick note before moving on That's the part that actually makes a difference..
Initializing the List
Before any operations, the list must be empty. We usually set the head pointer to NULL Took long enough..
struct Node* head = NULL; // Empty list
The head variable is a pointer that references the first node. When the list is empty, head is NULL, indicating there are no nodes present Not complicated — just consistent. Surprisingly effective..
Inserting Elements at the Beginning
Insertion at the beginning is the simplest operation. It creates a new node, assigns the current head to its next pointer, and updates the head to point to the new node Small thing, real impact. Surprisingly effective..
void insertAtBeginning(struct Node** headRef, int newData) {
// Allocate memory for the new node
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
if (!newNode) {
printf("Memory error\n");
return;
}
// Store data and link the old list at the next of new node
newNode->data = newData;
newNode->next = (*headRef);
// Move the head to point to the new node
(*headRef) = newNode;
}
Key points:
- Dynamic memory allocation using
mallocensures the node lives on the heap. - The double pointer
headRefallows the function to modify the originalheadvariable.
Inserting Elements at a Given Position
For more control, you can insert a node after a specific position. This requires traversing the list to the desired index and adjusting pointers accordingly.
void insertAtPosition(struct Node** headRef, int position, int newData) {
if (position < 0) {
printf("Invalid position\n");
return;
}
if (position == 0) {
insertAtBeginning(headRef, newData);
return;
}
struct Node* current = *headRef;
// Move to the node just before the insertion point
for (int i = 0; i < position - 1 && current != NULL; i++) {
current = current->next;
}
if (current == NULL) {
printf("Position out of range\n");
return;
}
// Allocate new node
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
if (!newNode) {
printf("Memory error\n");
return;
}
newNode->data = newData;
newNode->next = current->next;
current->next = newNode;
}
Traversing the List
Traversal is essential for reading or processing each element. It starts at the head and follows each next pointer until NULL is reached It's one of those things that adds up..
void traverse(struct Node* head) {
struct Node* current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
During traversal, you can perform operations such as printing, counting nodes, or applying a function to each element.
Deleting a Node
Deletion removes a node from the list, preserving the chain. There are two common scenarios: deleting the head node and deleting a node at a specific position.
void deleteNode(struct Node** headRef, int key) {
struct Node* temp = *headRef;
// If the head node itself holds the key
if (temp != NULL && temp->data == key) {
*headRef = temp->next;
free(temp);
return;
}
// Search for the key to be deleted
while (temp != NULL && temp->data != key) {
temp = temp->next;
}
// If key was not present
if (temp == NULL) return;
// Unlink the node from the list
struct Node* nextNode = temp->next;
free(temp);
// If the node to be deleted is not the last node
if (nextNode != NULL) {
// Optionally, you could update a tail pointer here
}
}
Memory management is crucial: always call free() on a dynamically allocated node after removing it from the list to avoid leaks That alone is useful..
Freeing All Memory (Cleanup)
When the program ends or the list is no longer needed, it is good practice to release all allocated memory.
void freeList(struct Node** headRef) {
struct Node* current = *headRef;
struct Node* next;
while (current != NULL) {
next = current->next;
free(current);
current = next;
}
*headRef = NULL;
}
Scientific Explanation
A linked list is a linear data structure where elements are not stored in contiguous memory locations. Instead, each element (node) contains a pointer to the next element, creating a chain that can grow or shrink dynamically. This design contrasts with arrays, which require a fixed size and store elements contiguously.
Most guides skip this. Don't.
Memory Management in C
In C, dynamic memory allocation is performed using functions like malloc, calloc, and free. When a node is created, malloc reserves a block of memory on the heap. The pointer inside the node points to another block, forming the linked chain. Proper use of free ensures that the heap does not accumulate unused memory, which could lead to memory leaks.
Time Complexity
- Insertion at the beginning: O(1) – only the head pointer needs updating.
- Insertion at a given position: O(n) – you must traverse the list up to the desired index.
- Deletion:
Deletion: O(n) – in the worst case, you must traverse the list to find the node preceding the target (or the target itself, depending on implementation) before unlinking it. Deleting the head node is O(1) Not complicated — just consistent. Worth knowing..
- Search/Access by value: O(n) – requires sequential traversal from the head.
- Access by index: O(n) – no random access; the list must be walked node by node.
Space Complexity
- Auxiliary Space: O(1) for iterative operations (traversal, insertion, deletion) as only a few temporary pointers are used.
- Total Space: O(n) – each node requires memory for the data payload plus a pointer (typically 8 bytes on 64-bit systems), resulting in higher per-element overhead compared to arrays.
Cache Locality Considerations
Because nodes are allocated individually on the heap, they are scattered across memory addresses. This poor spatial locality leads to frequent cache misses during traversal, making linked lists significantly slower than arrays for sequential access on modern CPU architectures, despite identical asymptotic complexity.
Comparison with Dynamic Arrays
| Operation | Singly Linked List | Dynamic Array (e.g., std::vector, ArrayList) |
|---|---|---|
| Insert at Head | O(1) | O(n) (requires shifting) |
| Insert at Tail | O(1) with tail pointer / O(n) without | O(1) amortized |
| Insert at Middle | O(n) search + O(1) insert | O(n) search + O(n) shift |
| Delete at Head | O(1) | O(n) (requires shifting) |
| Random Access | O(n) | O(1) |
| Memory Overhead | High (pointer per node) | Low (contiguous buffer) |
| Cache Performance | Poor | Excellent |
| Max Size | Limited by heap fragmentation | Limited by contiguous address space |
When to choose a Linked List:
- Frequent insertions/deletions at the head or middle (where iterators/references to nodes are stable).
- Unknown or highly variable upper bound on size where contiguous allocation might fail.
- Implementing complex structures like hash table buckets, adjacency lists for graphs, or undo/redo stacks.
When to choose a Dynamic Array:
- Random access or binary search is required.
- Iteration performance is critical (cache friendliness).
- Memory overhead must be minimized.
- Elements are small (e.g.,
int,float), where pointer overhead doubles memory usage.
Common Pitfalls and Best Practices
- Memory Leaks: Forgetting
free()indeleteNodeorfreeListis the most common defect. Tools like Valgrind or AddressSanitizer (-fsanitize=address) are essential for detection. - Dangling Pointers: Accessing a node after
free()causes undefined behavior. Set pointers toNULLimmediately after freeing if they persist in scope. - Lost Head Pointer: In functions modifying the head (insert/delete at front), always pass
struct Node**(pointer to pointer) or return the new head. Passingstruct Node*by value creates a local copy; changes to the head are lost on return. - Off-by-One Errors in Traversal: Loop conditions
current != NULLvscurrent->next != NULLdictate whethercurrentlands on the last node or the node before the last. Verify logic against empty (0), single (1), and multi-node lists. - Concurrency: Singly linked lists are not thread-safe. Concurrent modification requires mutexes, read-write locks, or lock-free algorithms (e.g., CAS-based Michael-Scott queues).
Conclusion
The singly linked list remains a foundational data structure in computer science, not merely as a pedagogical tool, but as a practical building block for systems where dynamic sizing, stable node addresses, and efficient front-end mutation are critical. While its O(n) access time and poor cache locality render it suboptimal for general-purpose storage compared to dynamic arrays, its O(1) insertion and deletion at known positions—without invalidating references to other elements—make it indispensable for implementing queues, stacks, hash tables, memory allocators, and kernel scheduling queues.
Mastering the pointer manipulation required for list operations—malloc, pointer-to-pointer arguments, and the current->next = newNode dance—sharpens a developer's understanding of memory layout, ownership, and the mechanics of indirection that underpin all higher-level abstractions. Whether you are writing a kernel module in C, a garbage collector in Rust, or a custom container in C++, the logic of the linked list is the logic of the heap itself.