1
🔗 Create a Node — The Building Block
Define the Node struct, allocate with malloc, link two nodes manually
Basics
A linked list is built from nodes. Each node is a
struct with two fields: data (the value) and next (a pointer to the next node). Nodes are created on the heap using malloc — not the stack — so they live as long as you need them. The last node always points to NULL, which signals the end of the list. Here we create two nodes and link them manually to see how the chain forms.
what a linked list looks like in memory
#include <stdio.h> #include <stdlib.h> /* Step 1 — define the Node structure */ typedef struct Node { int data; /* the value stored in this node */ struct Node *next; /* pointer to the next node */ } Node; /* Step 2 — helper: allocate and initialise a new node */ Node* createNode(int val) { Node *n = (Node*)malloc(sizeof(Node)); if (!n) { printf("malloc failed\n"); exit(1); } n->data = val; n->next = NULL; /* new node points to nothing yet */ return n; } int main() { Node *a = createNode(10); Node *b = createNode(20); Node *c = createNode(30); /* Link manually: a → b → c → NULL */ a->next = b; b->next = c; /* c->next is already NULL */ printf("Node a: data=%d next=%p\n", a->data, (void*)a->next); printf("Node b: data=%d next=%p\n", b->data, (void*)b->next); printf("Node c: data=%d next=%p\n", c->data, (void*)c->next); printf("\nsizeof(Node) = %zu bytes\n", sizeof(Node)); free(a); free(b); free(c); return 0; }
Node a: data=10 next=0x55a3e2b010 Node b: data=20 next=0x55a3e2b030 Node c: data=30 next=(nil) sizeof(Node) = 16 bytes
Why
struct Node *next not Node *next? Inside the struct definition, the typedef name Node is not yet complete. You must use the full struct Node * form for self-referential pointers. Outside the struct you can freely write Node*.example 2
2
➕ Insert at Head — O(1) Prepend
New node becomes the first element — constant time, no traversal needed
Insert Head
Inserting at the head is the fastest linked list operation — O(1). The new node's
next is pointed at the current head, then head is updated to point to the new node. We pass Node **head (a pointer-to-pointer) so the function can change where head points in the caller. Calling it three times builds the list in reverse order of the calls.
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; Node* createNode(int v) { Node *n = (Node*)malloc(sizeof(Node)); n->data = v; n->next = NULL; return n; } /* Insert at HEAD — O(1) */ void insertHead(Node **head, int val) { Node *n = createNode(val); n->next = *head; /* new node → old head */ *head = n; /* head now points to new node */ } void printList(Node *h) { printf("HEAD → "); while (h) { printf("%d → ", h->data); h = h->next; } printf("NULL\n"); } int main() { Node *head = NULL; printf("Inserting 10, 20, 30 at head...\n\n"); insertHead(&head, 10); printf("After insertHead(10): "); printList(head); insertHead(&head, 20); printf("After insertHead(20): "); printList(head); insertHead(&head, 30); printf("After insertHead(30): "); printList(head); return 0; }
Inserting 10, 20, 30 at head... After insertHead(10): HEAD → 10 → NULL After insertHead(20): HEAD → 20 → 10 → NULL After insertHead(30): HEAD → 30 → 20 → 10 → NULL
Why
Node **head? We pass a pointer to the head pointer so the function can change what head points to. If we passed Node *head, changes to head inside the function would be local only — the caller's head would not update.example 3
3
➕ Insert at Tail — Append to End
Walk to the last node, attach new node — preserves insertion order
Insert Tail
Inserting at the tail preserves the original order of insertions — the list reads exactly as you added elements. The cost is O(n) because you must walk to the last node first. A
cur pointer advances until cur→next == NULL, then cur→next = newNode appends it. Special case: if the list is empty, the new node simply becomes the head.
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; Node* createNode(int v){Node *n=(Node*)malloc(sizeof(Node));n->data=v;n->next=NULL;return n;} void printList(Node *h){printf("HEAD → ");while(h){printf("%d → ",h->data);h=h->next;}printf("NULL\n");} /* Insert at TAIL — O(n) */ void insertTail(Node **head, int val) { Node *n = createNode(val); if (*head == NULL) { /* empty list — new node IS the head */ *head = n; return; } Node *cur = *head; while (cur->next != NULL) /* walk to last node */ cur = cur->next; cur->next = n; /* attach at the end */ } int main() { Node *head = NULL; printf("Appending 10, 20, 30, 40...\n\n"); insertTail(&head, 10); printf("After insertTail(10): "); printList(head); insertTail(&head, 20); printf("After insertTail(20): "); printList(head); insertTail(&head, 30); insertTail(&head, 40); printf("After insertTail(30,40): "); printList(head); return 0; }
Appending 10, 20, 30, 40... After insertTail(10): HEAD → 10 → NULL After insertTail(20): HEAD → 10 → 20 → NULL After insertTail(30,40): HEAD → 10 → 20 → 30 → 40 → NULL
insertHead vs insertTail: Head = O(1), adds in reverse order. Tail = O(n), preserves order. To get O(1) tail inserts, maintain a separate
tail pointer alongside head — then no traversal is needed.example 4
4
👁️ Traverse — Print Forward and Backward
Iterative forward print, recursive reverse print, nth-node access
Traversal
Traversal visits every node from
head to NULL. The golden rule: always use a temporary pointer — never move head itself, or you permanently lose access to the front of the list. Three techniques are shown: iterative forward print, recursive reverse print (visits end first, prints on the way back), and index-based access to the nth element.
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; Node* createNode(int v){Node *n=(Node*)malloc(sizeof(Node));n->data=v;n->next=NULL;return n;} /* 1. Print forward — iterative */ void printForward(Node *cur) { printf("Forward : HEAD"); while (cur) { /* cur is a temp copy — head unchanged */ printf(" → %d", cur->data); cur = cur->next; } printf(" → NULL\n"); } /* 2. Print backward — recursive */ void printBackward(Node *cur) { if (cur == NULL) return; /* base case: end of list */ printBackward(cur->next); /* recurse to end first */ printf("%d ", cur->data); /* print on the way back */ } /* 3. Get data at 0-based index n */ int nthNode(Node *head, int n) { int i = 0; while (head) { if (i == n) return head->data; head = head->next; i++; } return -1; /* index out of range */ } int main() { /* Build list: 10 → 20 → 30 → 40 → 50 */ Node *head = createNode(10); Node *cur = head; int vals[] = {20, 30, 40, 50}; for (int i = 0; i < 4; i++) { cur->next = createNode(vals[i]); cur = cur->next; } printForward(head); printf("Backward: "); printBackward(head); printf("\n"); printf("Node at index 0 : %d\n", nthNode(head, 0)); printf("Node at index 2 : %d\n", nthNode(head, 2)); printf("Node at index 4 : %d\n", nthNode(head, 4)); printf("Node at index 9 : %d (out of range)\n", nthNode(head, 9)); return 0; }
Forward : HEAD → 10 → 20 → 30 → 40 → 50 → NULL Backward: 50 40 30 20 10 Node at index 0 : 10 Node at index 2 : 30 Node at index 4 : 50 Node at index 9 : -1 (out of range)
Golden rule of traversal: Always use
cur = head to make a temporary copy. Move cur, never head. If head moves, everything before the new position is permanently lost — there is no way to get it back.example 5
5
🗑️ Delete a Node by Value
Three cases: empty list, delete head, delete middle or tail
Deletion
Deletion requires finding the target node and keeping a pointer to the node before it — so you can bypass it. Two pointers walk together:
prev trails behind cur. When found, set prev→next = cur→next to re-link the chain, then call free(cur). The head case is special — there is no prev, so we update head directly. Always re-link before freeing.
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; Node* createNode(int v){Node *n=(Node*)malloc(sizeof(Node));n->data=v;n->next=NULL;return n;} void printList(Node *h){printf("HEAD → ");while(h){printf("%d → ",h->data);h=h->next;}printf("NULL\n");} /* Delete first node with matching value */ void deleteNode(Node **head, int val) { if (*head == NULL) { printf("List is empty.\n"); return; } Node *cur = *head; Node *prev = NULL; while (cur && cur->data != val) { /* find the node */ prev = cur; cur = cur->next; } if (!cur) { printf("%d not found.\n", val); return; } if (!prev) /* deleting the head node */ *head = cur->next; else /* deleting middle or tail */ prev->next = cur->next; /* bypass cur */ printf("Deleted %d.\n", cur->data); free(cur); /* AFTER re-linking */ } int main() { Node *head = NULL; /* Build: 10 → 20 → 30 → 40 → 50 */ Node *t = createNode(10); head = t; t->next = createNode(20); t->next->next = createNode(30); t->next->next->next = createNode(40); t->next->next->next->next = createNode(50); printf("Original : "); printList(head); deleteNode(&head, 30); /* middle */ printf("After del : "); printList(head); deleteNode(&head, 10); /* head */ printf("After del : "); printList(head); deleteNode(&head, 50); /* tail */ printf("After del : "); printList(head); deleteNode(&head, 99); /* not found */ return 0; }
Original : HEAD → 10 → 20 → 30 → 40 → 50 → NULL Deleted 30. After del : HEAD → 10 → 20 → 40 → 50 → NULL Deleted 10. After del : HEAD → 20 → 40 → 50 → NULL Deleted 50. After del : HEAD → 20 → 40 → NULL 99 not found.
Order matters: always do
prev→next = cur→next before free(cur). If you free first, cur→next is garbage — you lose the rest of the list. Re-link, then free.example 6
6
🔍 Search — Find a Value, Return Its Position
Linear scan from head — return 0-based index or -1 if not found
Search
Linked list search is always O(n) — there is no random access. Walk from
head, compare each node's data. The function returns the 0-based index when found, or -1 if the value doesn't exist. A second helper contains() returns a simple true/false for cases where the position doesn't matter.
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; Node* createNode(int v){Node *n=(Node*)malloc(sizeof(Node));n->data=v;n->next=NULL;return n;} /* Returns 0-based index of first match, -1 if not found */ int search(Node *head, int val) { int idx = 0; while (head) { if (head->data == val) return idx; head = head->next; idx++; } return -1; } /* Returns 1 if value exists, 0 otherwise */ int contains(Node *head, int val) { while (head) { if (head->data == val) return 1; head = head->next; } return 0; } int main() { /* Build: 15 → 25 → 35 → 45 → 55 */ Node *head = createNode(15); Node *cur = head; int vals[] = {25, 35, 45, 55}; for (int i=0; i<4; i++) { cur->next=createNode(vals[i]); cur=cur->next; } printf("List: 15 → 25 → 35 → 45 → 55\n\n"); int targets[] = {35, 15, 55, 99}; for (int i = 0; i < 4; i++) { int t = targets[i]; int pos = search(head, t); if (pos != -1) printf("search(%2d) → found at index %d\n", t, pos); else printf("search(%2d) → not found\n", t); } printf("\ncontains(45) : %s\n", contains(head, 45) ? "yes" : "no"); printf("contains(77) : %s\n", contains(head, 77) ? "yes" : "no"); return 0; }
List: 15 → 25 → 35 → 45 → 55 search(35) → found at index 2 search(15) → found at index 0 search(55) → found at index 4 search(99) → not found contains(45) : yes contains(77) : no
Linked list search is always O(n) — unlike an array, you cannot jump to the middle. Every search starts at
head and walks forward. This is the core trade-off: linked lists excel at insert/delete, arrays excel at indexed access.example 7
7
🔢 Count Nodes, Sum, Min and Max
Four utility traversals — essential measurements of any list
Utilities
Four clean traversal utilities, each a single-pass O(n) loop: length counts all nodes, sum accumulates all values, minimum and maximum track extreme values as they walk. These are the building blocks for more complex algorithms — sorting, averaging, statistics. Also includes
freeList to properly release all heap memory.
#include <stdio.h> #include <stdlib.h> #include <limits.h> /* INT_MAX, INT_MIN */ typedef struct Node { int data; struct Node *next; } Node; Node* createNode(int v){Node *n=(Node*)malloc(sizeof(Node));n->data=v;n->next=NULL;return n;} int length(Node *h) { int c = 0; while (h) { c++; h = h->next; } return c; } int sum(Node *h) { int s = 0; while (h) { s += h->data; h = h->next; } return s; } int minimum(Node *h) { int m = INT_MAX; while (h) { if (h->data < m) m = h->data; h = h->next; } return m; } int maximum(Node *h) { int m = INT_MIN; while (h) { if (h->data > m) m = h->data; h = h->next; } return m; } void freeList(Node *h) { /* release all heap nodes */ Node *tmp; while (h) { tmp = h->next; free(h); h = tmp; } } int main() { /* Build: 42 → 7 → 93 → 18 → 55 → 31 */ Node *head = createNode(42); Node *c = head; int v[] = {7, 93, 18, 55, 31}; for (int i=0;i<5;i++){c->next=createNode(v[i]);c=c->next;} printf("List : 42 → 7 → 93 → 18 → 55 → 31\n\n"); printf("Length : %d\n", length(head)); printf("Sum : %d\n", sum(head)); printf("Average: %.2f\n", (float)sum(head) / length(head)); printf("Min : %d\n", minimum(head)); printf("Max : %d\n", maximum(head)); freeList(head); printf("\nAll memory freed.\n"); return 0; }
List : 42 → 7 → 93 → 18 → 55 → 31 Length : 6 Sum : 246 Average: 41.00 Min : 7 Max : 93 All memory freed.
freeList pattern: always save tmp = h→next before calling free(h). After free(h), accessing h→next is undefined behaviour — you've already given that memory back. Save next first, free, then move.
example 8
8
🔄 Reverse a Linked List In-Place
Three-pointer technique — reverse all links without extra memory
Reversal
Reversing a linked list in-place is a classic interview question. Three pointers —
prev, cur, and next — walk through the list. At each step: save cur→next, flip cur→next to point backward at prev, then advance all three pointers one step forward. When cur reaches NULL, prev is the new head.
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; Node* createNode(int v){Node *n=(Node*)malloc(sizeof(Node));n->data=v;n->next=NULL;return n;} void printList(Node *h){printf("HEAD → ");while(h){printf("%d → ",h->data);h=h->next;}printf("NULL\n");} /* Reverse in-place — O(n) time, O(1) space */ Node* reverseList(Node *head) { Node *prev = NULL; Node *cur = head; Node *next = NULL; while (cur != NULL) { next = cur->next; /* 1. save next node */ cur->next = prev; /* 2. flip the link */ prev = cur; /* 3. advance prev */ cur = next; /* 4. advance cur */ } return prev; /* prev is now the new head */ } int main() { Node *head = createNode(1); Node *c = head; int v[] = {2, 3, 4, 5}; for (int i=0;i<4;i++){c->next=createNode(v[i]);c=c->next;} printf("Original : "); printList(head); head = reverseList(head); printf("Reversed : "); printList(head); /* Reverse back to original */ head = reverseList(head); printf("Reversed : "); printList(head); return 0; }
Original : HEAD → 1 → 2 → 3 → 4 → 5 → NULL Reversed : HEAD → 5 → 4 → 3 → 2 → 1 → NULL Reversed : HEAD → 1 → 2 → 3 → 4 → 5 → NULL
The three-pointer reversal is O(n) time and O(1) space — it reverses the list in a single pass with no extra arrays or recursion. Step-by-step: for each node, save
next, point cur→next backward at prev, then slide both pointers one step right.example 9
9
📐 Insert in Sorted Order
Find the correct position by value and splice the new node in
Sorted Insert
Instead of appending to the tail, a sorted insert finds the right position to keep the list in ascending order. Two pointers walk together —
prev and cur — until cur→data >= val. The new node is spliced between prev and cur. Special case: insert before the head when the new value is the smallest. Building a list purely with sorted inserts produces a sorted list from any input order.
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; Node* createNode(int v){Node *n=(Node*)malloc(sizeof(Node));n->data=v;n->next=NULL;return n;} void printList(Node *h){printf("HEAD → ");while(h){printf("%d → ",h->data);h=h->next;}printf("NULL\n");} /* Insert so the list stays sorted ascending */ void sortedInsert(Node **head, int val) { Node *n = createNode(val); Node *cur = *head; Node *prev = NULL; /* Find position: stop when cur value >= val */ while (cur && cur->data < val) { prev = cur; cur = cur->next; } n->next = cur; /* new node → cur (or NULL) */ if (!prev) /* insert before head */ *head = n; else /* insert after prev */ prev->next = n; } int main() { Node *head = NULL; int input[] = {40, 10, 70, 25, 55, 5, 90}; int n = sizeof(input) / sizeof(input[0]); printf("Inserting in sorted order:\n"); for (int i = 0; i < n; i++) { sortedInsert(&head, input[i]); printf(" insert(%2d) → ", input[i]); printList(head); } return 0; }
Inserting in sorted order: insert(40) → HEAD → 40 → NULL insert(10) → HEAD → 10 → 40 → NULL insert(70) → HEAD → 10 → 40 → 70 → NULL insert(25) → HEAD → 10 → 25 → 40 → 70 → NULL insert(55) → HEAD → 10 → 25 → 40 → 55 → 70 → NULL insert( 5) → HEAD → 5 → 10 → 25 → 40 → 55 → 70 → NULL insert(90) → HEAD → 5 → 10 → 25 → 40 → 55 → 70 → 90 → NULL
No matter what order you insert in, the list always comes out sorted. This is because
sortedInsert finds the right slot on every call — O(n) per insert, O(n²) total to build the list. For a large dataset, sort the input array first and then build the list.example 10
10
🎓 Student Records — Complete Mini App
Linked list of structs — add, display, search by roll, delete, find topper
Complete App
Everything combined — a
Student struct is the payload inside each node instead of a plain int. Five operations are implemented: addStudent appends a full record at the tail, displayAll prints a formatted table, searchByRoll returns a pointer to a matching node, deleteByRoll removes a record, and findTopper scans for the highest marks. A complete mini student management system.
#include <stdio.h> #include <stdlib.h> #include <string.h> /* Payload — student data stored in each node */ typedef struct { char name[20]; int roll; float marks; } Student; /* Node wraps the Student struct */ typedef struct Node { Student data; struct Node *next; } Node; /* Helpers */ Node* newNode(Student s) { Node *n = (Node*)malloc(sizeof(Node)); n->data = s; n->next = NULL; return n; } /* 1. Add at tail */ void addStudent(Node **head, Student s) { Node *n = newNode(s); if (!*head) { *head = n; return; } Node *c = *head; while (c->next) c = c->next; c->next = n; } /* 2. Display all records */ void displayAll(Node *h) { printf("\n%-12s %5s %7s\n", "Name", "Roll", "Marks"); printf("---------------------------\n"); while (h) { printf("%-12s %5d %7.1f\n", h->data.name, h->data.roll, h->data.marks); h = h->next; } } /* 3. Search by roll number — returns pointer to node */ Node* searchByRoll(Node *h, int roll) { while (h) { if (h->data.roll == roll) return h; h = h->next; } return NULL; } /* 4. Delete by roll number */ void deleteByRoll(Node **head, int roll) { Node *cur = *head, *prev = NULL; while (cur && cur->data.roll != roll) { prev = cur; cur = cur->next; } if (!cur) { printf("Roll %d not found.\n", roll); return; } if (!prev) *head = cur->next; else prev->next = cur->next; printf("Deleted: %s (roll %d)\n", cur->data.name, cur->data.roll); free(cur); } /* 5. Find student with highest marks */ Node* findTopper(Node *h) { Node *best = h; while (h) { if (h->data.marks > best->data.marks) best = h; h = h->next; } return best; } int main() { Node *head = NULL; Student roster[] = { {"Ananta", 101, 88.5}, {"Priya", 102, 95.0}, {"Rahul", 103, 72.0}, {"Sneha", 104, 91.5}, {"Vikram", 105, 65.0} }; for (int i = 0; i < 5; i++) addStudent(&head, roster[i]); printf("=== All Students ==="); displayAll(head); /* Search */ Node *found = searchByRoll(head, 103); printf("\nSearch roll 103: %s\n", found ? found->data.name : "not found"); /* Delete */ printf("\n"); deleteByRoll(&head, 103); printf("=== After deletion ==="); displayAll(head); /* Topper */ Node *top = findTopper(head); printf("\n🏆 Topper: %s — %.1f marks\n", top->data.name, top->data.marks); return 0; }
=== All Students === Name Roll Marks --------------------------- Ananta 101 88.5 Priya 102 95.0 Rahul 103 72.0 Sneha 104 91.5 Vikram 105 65.0 Search roll 103: Rahul Deleted: Rahul (roll 103) === After deletion === Name Roll Marks --------------------------- Ananta 101 88.5 Priya 102 95.0 Sneha 104 91.5 Vikram 105 65.0 🏆 Topper: Priya — 95.0 marks
All five patterns in one program:
addStudent builds the list · displayAll traverses with a temp pointer · searchByRoll returns a node pointer · deleteByRoll uses prev/cur and free · findTopper scans and returns the best node. The node's payload is a full struct — not just an int. This is how real-world linked list applications are structured.checklist
- Ex 1 — Node = struct with
data+struct Node *next. Usemallocto create,freeto release. Last node'snext = NULL. - Ex 2 — insertHead is O(1). Pass
Node **headso the function can update the caller's head pointer. - Ex 3 — insertTail is O(n) — walk to last node, set
cur→next = newNode. Special-case empty list. - Ex 4 — Always traverse with a temp pointer. Recursive reverse print recurses to end first, prints on the way back.
- Ex 5 — Deletion uses
prev+cur. Re-link first (prev→next = cur→next), thenfree(cur). Three cases: empty, head, middle/tail. - Ex 6 — Search is always O(n).
search()returns index or -1.contains()returns 1 or 0. - Ex 7 —
freeList: savetmp = h→nextbeforefree(h), thenh = tmp. Never access freed memory. - Ex 8 — Three-pointer reversal: save next, flip link, advance prev and cur. O(n) time, O(1) space.
- Ex 9 — Sorted insert walks with prev/cur until
cur→data ≥ val. Splices new node between prev and cur. - Ex 10 — Node payload can be a full struct. Same insert/search/delete patterns — access fields via
node→data.field.