- 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 theheadnode. - Extra Memory: Each node requires extra memory to store the
nextpointer.
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:
dataholds the value (we're usingintfor simplicity).nextis a pointer of typestruct Node*, which will hold the memory address of the subsequent node.- The
typedefallows us to simply writeNodeinstead ofstruct Node. headis 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
mallocto get space for oneNodestructure. - Crucially, we check if
mallocreturnedNULL. - We assign the passed
datato the node'sdatafield. newNode->nextis set toNULLbecause 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
nextof the new node to the currentheadof the list. - Update the
headpointer 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
headand iterate (while (current->next != NULL)) until you find the node whosenextpointer isNULL. This is the last node. - Set the
nextpointer 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
currentis notNULL. - Inside the loop, process the
current->data(here, we print it). - Crucially, update
current = current->nextto 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->datamatches 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
headpointer 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
headnode itself needs to be deleted (similar todeleteFirstNode). - Use two pointers:
currentstarts athead, andpreviousstarts atNULL. - Iterate through the list. In each step, update
previous = currentbefore updatingcurrent = current->next. - Stop when
currentpoints to the node to be deleted (or whencurrentbecomesNULLif not found). - If found, link the
previousnode directly to the node aftercurrent(previous->next = current->next). This effectively removescurrentfrom 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:
currentandnextNode. - Iterate while
currentis notNULL. - Inside the loop: First, save the address of the next node (
nextNode = current->next). Then,free(current). Finally, updatecurrent = nextNodeto move to the next node you saved earlier. (If you freedcurrentfirst, you'd lose the link to the rest of the list!) - Set
head = NULLat the end.
Example Usage (main function)
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
nextand thepreviousnode. Allows traversal in both directions but uses more memory. - Circular Linked List: The
nextpointer of the last node points back to theheadnode 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!