How to Create a Linked List in C
Lets start by explaining what is a linked list and why we even use it.
What is a Linked List?
To understand what a linked list is, let's start by defining what a traditional array is.
An array is a data structure where each element is stored in a linear fashion. Here is a representation of what an array looks like in memory:
In the diagram above, each array element is stored in contiguous memory. You can also think of it as a collection of elements (in our case, 7 integer elements) that are tied together. One advantage of using an array is fast access. Accessing an element in an array has a time complexity of O(1), making it really fast. In most programming languages, you can do this by writing something like arr[2] to get the 3rd item in the array (note that array indexing starts from '0'). Here's another way of doing the same thing in C:"
int arr[5] = {10, 12, 6, 7, 9}; int *p = &arr[0] + 2; printf("\The third element is: %d", *p);
This is a great way to understand how it works. Here, we can see that we first get the address of the first item in the array, and then we simply move two steps forward to access the 3rd item. This works because the addresses of the values in the array are stored in a single memory block arranged linearly.
Alright, enough about arrays. The real question here is: what is a linked list, and why would we use it instead of an array?
A linked list is a linear collection of nodes, where each node points to the next one. Think of it like a chain: there is a head and a tail, and each part of the node is connected to another node. Here’s a diagram representing how it looks:
In the diagram above, we can see that the node with the value 8 points to 6, 6 points to 11, and so on, until 13 points to 4. The value 4 is the last node, so it points to NULL, meaning it doesn't reference any data in memory.
Now back to our original question: why should we use a linked list over an array? Remember when I mentioned that arrays are really fast at accessing data? That’s true, but the problem with arrays is that they are slow when it comes to insertion or deletion. If you look at the array diagram, now imagine I ask you how to insert the value 10 between 11 and 5. What would your answer be? The solution is to set the 3rd index to 5 and shift all the values after it by one position. The same applies to deletion, except the values afterward would shift backward by one. You can see how this could take longer, especially with a large array. This process takes O(n) time in the worst case.
Now, look at the linked list diagram. How do you think insertion and deletion work here? To insert a new node, I would make 11 point to 10 (the new node) and 10 point to 5. Notice that we didn’t have to rearrange the list. If it’s still unclear, think of it like a chain with 5 links connected to each other, and you want to add one link in the middle. You would simply open the chain in the middle, insert the new link, and then reconnect the other part of the chain. No need for rearranging. The same logic applies to deletion—this time, you’re removing a node and connecting the two ends together. This process takes O(1) time in the worst case.
However, accessing a node in a linked list is more time-consuming. Unlike arrays, linked lists are not stored in contiguous memory blocks. Each node contains a pointer to the next node. To access an element in a linked list, you have to move through each node sequentially until you reach the desired element, which takes O(n) time in the worst case.
In contrast, elements in arrays are stored in contiguous memory. This means that to access an element in an array, you simply get the address of the first element and add the index value to it (see the code above to see this implemented in C).
Coding Starts Here
Now it's time to start coding our linked list in C.
I like to start with the objectives we want our code to accomplish:
- Create a
student
struct that includes the student's name, age, grade, and subjects taken. - This is a linked list, so each student should point to the next student.
- Print out the list in the console.
- Create new nodes using a function.
- Retrieve a student from the list using their name.
- Insert a new node into the list.
- Delete a node from the list.
- BONUS: Reverse the linked list.
Creating the Student struct
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct Student { char* name; char* subject; unsigned int age; char grade[3]; struct Student* next; // Points to the next student } Student;
Printing our list
void printList(Student* student) { if (student == NULL) { fprintf(stderr, "Empty student data in printList\n"); return; } Student* tmp = student; while (tmp->next != NULL) { printf("(%s, %s, %s, %d)->", tmp->name, tmp->subject, tmp->grade, tmp->age); tmp = tmp->next; } printf("(%s, %s, %s, %d)", tmp->name, tmp->subject, tmp->grade, tmp->age); }
The tmp
variable starts by being equal to the head of the list. After that, we check if the next node is NULL
(which would indicate the tail node, since it doesn’t point to any other node). Inside the while
loop, on each iteration, we set the tmp
variable to be equal to the next node.
At the end of the while
loop, tmp
will be equal to the tail node, and then we print it out. Notice that in the printf
function inside the loop, we end with ->
, but we don’t do this for the one outside the loop. This is for formatting purposes, since the tail node doesn’t point to any other node.
Create a new nodes using a function.
void inserNodeToHead(Student** head, Student* newNode) { if (newNode == NULL) return; newNode->next = *head; *head = newNode; }
Our list is designed so that nodes are inserted at the head. This function is meant to handle that. The head
argument is a double pointer because we need to update the head node variable within the main
function.
Student* createNode(char* name, char* subject, char grade[3], const int age) { Student* student = malloc(sizeof(*student)); student->name = name; student->subject = subject; student->age = age; snprintf(student->grade, sizeof(student->grade), "%s", grade); student->next = NULL; return student; }
We create our node using this function. Notice how we are using malloc
here; this helps us dynamically allocate memory for our list as it grows. So, if we have one node, we ensure we are only allocating memory for that node, and if we decide to add another node, we allocate memory for that one as well. This approach is better because it ensures we are only using what we need. For now, we will set student->next
to NULL
, and later in the main
function, after creating our node, we will pass it to the insertNodeToHead
function.
Retrieve a student from the list using their name.
Student* getNode(Student* head, const char* name) { Student* tmp = head; while (tmp != NULL && strcmp(tmp->name, name) != 0) { tmp = tmp->next; } return tmp; }
To retrieve a student, we first set the tmp
variable to the head node and loop until tmp->name
matches our desired name. Once found, we return the node.
Insert a new node into the list.
void insertNode(Student* nodeToInsertAt, Student* newNode) { if (newNode == NULL) return; newNode->next = nodeToInsertAt->next; nodeToInsertAt->next = newNode; }
This can be confusing, so I will explain what is happening here as clearly as I can.
The newNode
we created is pointing to the next node of nodeToInsertAt
, and then we set nodeToInsertAt->next
(which is the next node) to point to our newNode
. Here’s a diagram representing what this code is doing:
Deleting a node from the list.
void deleteNode(Student** head, const char* name) { Student* tmp = *head; Student* prev = NULL; while (tmp->next != NULL && tmp->name != name) { prev = tmp; tmp = tmp->next; } if (prev != NULL) { prev->next = tmp->next; } else { // This means the node to delete is the head node *head = tmp->next; } }
This is somewhat similar to the insertion; the only difference here is that we have a prev
variable to track our previous node. The reason for having the prev
variable is that once we find the node we want to delete, we simply set the next node for prev
to be equal to the next node of the node we want to delete. Think back to the chain example— all we are doing is removing a link and joining the two disconnected ends.
Lets create our main function
typedef struct studentData { char* name; char* subject; char grade[3]; unsigned int age; } studentData; int main(void) { studentData data[10] = { {"Mark", "Science", "B", 11}, {"Jeff", "English", "C+", 12}, {"Sam", "History", "A", 10}, {"Clifford", "Maths", "A+", 10}, {"Alex", "Geography", "B+", 11}, {"James", "Maths", "D", 13}, {"Tom", "Science", "C", 12}, {"Peter", "English", "B", 11}, {"John", "History", "A", 10}, {"Steve", "Geography", "C+", 12} }; Student* head = NULL; const int studentDataLen = sizeof(data) / sizeof(data[0]); for (int i = 0; i < studentDataLen; i++) { Student* newNode = createNode(data[i].name, data[i].subject, data[i].grade, data[i].age); inserNodeToHead(&head, newNode); } Student* nodeToInsertAt = getNode(head, "Clifford"); Student* newNode = createNode("Cassie", "Science", "A+", 7); insertNode(nodeToInsertAt, newNode); deleteNode(&head, "James"); printList(head); return 0; }
First, we create a studentData
struct to help us generate some dummy data. Inside our for loop, we create a new node for each piece of data and insert it at the head. The remaining lines of code simply call the functions we created earlier.
Run the function to see the printed output. If you don’t know C and just want to run the program, you can check out the Hello World
tutorial I wrote - Hello world
BONUS: Reverse the linked list.
I’m going to make this an assignment for you to try your hand at: reverse the returned linked list. You can check out my solution here once you're done: Reverse Linked List.
Also, try to clean up the linked list from memory if you can. I will include the solution for this in the GitHub code. Happy coding! 🎉
You can find the full code here:
https://github.com/cliffordaustin/LinkedList/blob/5761a7e71b2956d236899ae4d1e66a6d9116b1b5/main.c.