- Published on
Working with Linked Lists in C Intermediate C Concepts Part 2
- Authors
- Name
- Md Nasim Sheikh
- @nasimStg
Welcome to the second part of our Intermediate C Concepts series! In the previous post, we explored Dynamic Memory Allocation in C, mastering malloc
, calloc
, realloc
, and free
. Now, we'll put that knowledge to practical use by diving into one of the most fundamental and versatile dynamic data structures: the Linked List. Get ready to see how pointers and dynamic memory allocation unlock flexible ways to organize data!
Table of Contents
- What is a Linked List? Why Not Just Use Arrays?
- The Building Block: The Node Structure
- Core Operations on Singly Linked Lists
- 1. Creating a New Node
- 2. Insertion
- 3. Traversal (Printing the List)
- 4. Searching for a Node
- 5. Deletion
- 6. Freeing the Entire List
- Example Usage (main function)
- Variations (Briefly)
- Conclusion
- Further Reading & Related Posts
What is a Linked List? Why Not Just Use Arrays?
Arrays are great for storing collections of similar data types. However, they have a significant limitation: their size is fixed at compile time (or when dynamically allocated, fixed until reallocated). If you need a collection that can easily grow or shrink during program execution, or if you need to insert/delete elements frequently in the middle, arrays become inefficient.
This is where Linked Lists shine. A linked list is a linear data structure where elements are not stored at contiguous memory locations. Instead, each element (called a Node) contains two parts:
- Data: The actual value stored in the node.
- Pointer (or Link): An address pointing to the next node in the sequence.
The last node in the list points to NULL
, indicating the end of the list. The entry point to the list is typically a pointer called head
, which points to the first node. If head
is NULL
, the list is empty.
Advantages over Arrays:
- Dynamic Size: Linked lists can grow and shrink easily at runtime by adding or removing nodes.
- Efficient Insertion/Deletion: Inserting or deleting elements doesn't require shifting subsequent elements (unlike arrays). You just need to update the pointers of the adjacent nodes.
Disadvantages:
- No Random Access: You cannot directly access the i-th element like
array[i]
. You must traverse the list from thehead
node. - Extra Memory: Each node requires extra memory to store the
next
pointer.
For this tutorial, we will focus on the most common type: the Singly Linked List, where each node points only to the next node.
The Building Block: The Node Structure
Everything in a linked list revolves around the Node
. We define it in C using a struct
. Since the struct
needs to contain a pointer to itself (to point to the next node of the same type), this is a classic example of a self-referential structure.
#include <stdio.h>
#include <stdlib.h> // Needed for malloc and free
// Define the structure for a node in the linked list
struct Node {
int data; // The data stored in the node (can be any type)
struct Node *next; // Pointer to the next node in the list
};
// Typedef for convenience (optional but common)
typedef struct Node Node;
// Global head pointer (or pass it to functions)
// Initialized to NULL, indicating an empty list
Node *head = NULL;
Here:
data
holds the value (we're usingint
for simplicity).next
is a pointer of typestruct Node*
, which will hold the memory address of the subsequent node.- The
typedef
allows us to simply writeNode
instead ofstruct Node
. head
is a pointer that will always point to the first node of the list. It's crucial!
Core Operations on Singly Linked Lists
Let's implement the essential operations for managing a linked list.
1. Creating a New Node
We need a way to create a new node when we want to add data. This involves dynamically allocating memory for the node structure and initializing its fields. A helper function is good practice.
// Function to create a new node with given data
Node* createNode(int data) {
// Allocate memory for the new node
Node* newNode = (Node*)malloc(sizeof(Node));
// Check if malloc was successful
if (newNode == NULL) {
fprintf(stderr, "Error: Memory allocation failed for new node!\n");
// exit(1); // Or handle error appropriately
return NULL; // Indicate failure
}
// Initialize the node's data and next pointer
newNode->data = data;
newNode->next = NULL; // The new node initially points to nothing
return newNode; // Return the pointer to the newly created node
}
- We use
malloc
to get space for oneNode
structure. - Crucially, we check if
malloc
returnedNULL
. - We assign the passed
data
to the node'sdata
field. newNode->next
is set toNULL
because when first created, it's not linked to anything yet.
2. Insertion
Nodes can be inserted at various positions. Let's look at the most common ones:
a) Inserting at the Beginning (Prepend)
This is the simplest and most efficient insertion method (O(1) time complexity).
// Function to insert a node at the beginning of the list
void insertAtBeginning(int data) {
// Create the new node
Node* newNode = createNode(data);
if (newNode == NULL) {
return; // Node creation failed
}
// Link the new node to the current head
newNode->next = head;
// Update the head to point to the new node
head = newNode;
printf("Inserted %d at the beginning.\n", data);
}
- Create the node.
- Point the
next
of the new node to the currenthead
of the list. - Update the
head
pointer to point to thenewNode
, making it the new first node.
b) Inserting at the End (Append)
This requires traversing the list to find the last node (O(n) time complexity).
// Function to insert a node at the end of the list
void insertAtEnd(int data) {
// Create the new node
Node* newNode = createNode(data);
if (newNode == NULL) {
return; // Node creation failed
}
// If the list is empty, the new node becomes the head
if (head == NULL) {
head = newNode;
printf("Inserted %d as the first node (at the end).\n", data);
return;
}
// Traverse to the last node
Node* current = head;
while (current->next != NULL) {
current = current->next; // Move to the next node
}
// Link the last node's next pointer to the new node
current->next = newNode;
printf("Inserted %d at the end.\n", data);
}
- Create the node.
- Handle the special case: If the list is empty (
head == NULL
), the new node is the head. - Otherwise, start from the
head
and iterate (while (current->next != NULL)
) until you find the node whosenext
pointer isNULL
. This is the last node. - Set the
next
pointer of that last node to point to thenewNode
.
3. Traversal (Printing the List)
To see the contents of the list, we need to traverse it from beginning to end.
// Function to print all elements in the linked list
void printList() {
// Start from the head
Node* current = head;
if (current == NULL) {
printf("List is empty.\n");
return;
}
printf("List: ");
// Traverse until the end of the list (current becomes NULL)
while (current != NULL) {
printf("%d -> ", current->data); // Print the data
current = current->next; // Move to the next node
}
printf("NULL\n"); // Indicate the end of the list
}
- Use a temporary pointer (
current
) initialized tohead
. - Loop as long as
current
is notNULL
. - Inside the loop, process the
current->data
(here, we print it). - Crucially, update
current = current->next
to move to the next node for the next iteration.
4. Searching for a Node
Find if a node with specific data exists in the list.
// Function to search for a node with specific data
Node* searchNode(int data) {
Node* current = head;
while (current != NULL) {
if (current->data == data) {
printf("Found node with data %d at address %p.\n", data, (void*)current);
return current; // Return pointer to the found node
}
current = current->next;
}
printf("Node with data %d not found.\n", data);
return NULL; // Data not found
}
- Traverse the list similar to printing.
- In each iteration, check if
current->data
matches the targetdata
. - If found, return the pointer to that node (
current
). - If the loop finishes without finding the data, return
NULL
.
5. Deletion
Removing nodes also requires careful pointer manipulation and memory management (free
).
a) Deleting the First Node
Relatively simple (O(1)).
// Function to delete the first node in the list
void deleteFirstNode() {
if (head == NULL) {
printf("Cannot delete from an empty list.\n");
return;
}
// Store the current head in a temporary pointer
Node* temp = head;
// Update head to point to the second node
head = head->next;
// Free the memory of the original first node
printf("Deleting first node with data %d.\n", temp->data);
free(temp);
temp = NULL; // Good practice
}
- Check if the list is empty.
- Create a temporary pointer (
temp
) to hold the currenthead
. - Move the
head
pointer to the next node (head = head->next
). free()
the memory pointed to bytemp
.
b) Deleting a Node with Specific Data
This is more complex as you need to find the node before the one you want to delete (O(n)).
// Function to delete a node with specific data
void deleteNodeByData(int data) {
if (head == NULL) {
printf("List is empty, cannot delete.\n");
return;
}
Node* current = head;
Node* previous = NULL; // Keep track of the previous node
// Special case: Deleting the head node
if (current != NULL && current->data == data) {
head = current->next; // Move head
printf("Deleting head node with data %d.\n", data);
free(current); // Free old head
current = NULL;
return;
}
// Traverse to find the node to delete, keeping track of the previous one
while (current != NULL && current->data != data) {
previous = current;
current = current->next;
}
// If the node was not found
if (current == NULL) {
printf("Node with data %d not found for deletion.\n", data);
return;
}
// Node found, bypass it in the list
// Link the previous node's next to the current node's next
previous->next = current->next;
// Free the memory of the node to be deleted
printf("Deleting node with data %d.\n", data);
free(current);
current = NULL;
}
- Handle the empty list case.
- Handle the special case where the
head
node itself needs to be deleted (similar todeleteFirstNode
). - Use two pointers:
current
starts athead
, andprevious
starts atNULL
. - Iterate through the list. In each step, update
previous = current
before updatingcurrent = current->next
. - Stop when
current
points to the node to be deleted (or whencurrent
becomesNULL
if not found). - If found, link the
previous
node directly to the node aftercurrent
(previous->next = current->next
). This effectively removescurrent
from the chain. free(current)
.
6. Freeing the Entire List
Crucial for preventing memory leaks when the list is no longer needed.
// Function to free all nodes in the linked list
void freeList() {
Node* current = head;
Node* nextNode = NULL;
printf("Freeing the entire list...\n");
while (current != NULL) {
nextNode = current->next; // Store pointer to the next node
printf("Freeing node with data %d at %p\n", current->data, (void*)current);
free(current); // Free the current node
current = nextNode; // Move to the next node
}
// Reset head to NULL as the list is now empty
head = NULL;
printf("List freed successfully.\n");
}
- Use two pointers:
current
andnextNode
. - Iterate while
current
is notNULL
. - Inside the loop: First, save the address of the next node (
nextNode = current->next
). Then,free(current)
. Finally, updatecurrent = nextNode
to move to the next node you saved earlier. (If you freedcurrent
first, you'd lose the link to the rest of the list!) - Set
head = NULL
at the end.
main
function)
Example Usage (Let's put some of these functions together:
// (Include struct definition and all function definitions from above here)
int main() {
printList(); // List is empty.
insertAtBeginning(10);
insertAtBeginning(5);
insertAtEnd(20);
insertAtEnd(30);
insertAtBeginning(1);
printList(); // List: 1 -> 5 -> 10 -> 20 -> 30 -> NULL
searchNode(10); // Found node with data 10...
searchNode(99); // Node with data 99 not found.
deleteNodeByData(10); // Deleting node with data 10.
printList(); // List: 1 -> 5 -> 20 -> 30 -> NULL
deleteFirstNode(); // Deleting first node with data 1.
printList(); // List: 5 -> 20 -> 30 -> NULL
deleteNodeByData(30); // Deleting node with data 30.
printList(); // List: 5 -> 20 -> NULL
deleteNodeByData(5);
deleteNodeByData(20);
printList(); // List is empty.
// Test inserting into an empty list after deletion
insertAtEnd(55);
printList(); // List: 55 -> NULL
// CRITICAL: Always free the list when done!
freeList();
printList(); // List is empty.
return 0;
}
Variations (Briefly)
- Doubly Linked List: Each node has pointers to both the
next
and theprevious
node. Allows traversal in both directions but uses more memory. - Circular Linked List: The
next
pointer of the last node points back to thehead
node instead ofNULL
, forming a circle.
Conclusion
Linked lists are a cornerstone data structure, offering dynamic resizing and efficient insertions/deletions at the cost of direct access. Understanding how to manipulate nodes using pointers and managing their memory lifecycle with malloc
and free
is a vital skill for any C programmer. You've now learned how to implement the fundamental operations for a singly linked list.
In our next C intermediate tutorial, we'll build upon these concepts to implement Stacks and Queues, which can often be efficiently realized using linked lists!