Unit IV β Lists
Syllabus Coverage: List as ADT, Concept of linked organization of data against linked list. Singly linked list, doubly linked list, circular linked list implementation and Analysis. Operations on Linked list β insertion, deletion, searching. Representation & manipulations of polynomials/sets using linked lists. Dynamic memory management. Representation of sparse matrix. Addition and transpose of sparse matrix. (09 Hours)
Table of Contents
1 β Introduction to Linked Lists
- 1.1 List as an ADT
- 1.2 Array vs Linked Organization
- 1.3 Node Structure & Pointers
- 1.4 Dynamic Memory Management in C
2 β Singly Linked List
- 2.1 Structure & Visualization
- 2.2 Creating a Node
- 2.3 Traversal (Display)
- 2.4 Insertion β Beginning, End, At Position
- 2.5 Deletion β Beginning, End, At Position
- 2.6 Search
- 2.7 Reversing a Singly Linked List
- 2.8 Complexity Analysis
- Practice Questions β Singly Linked List
3 β Doubly Linked List
- 3.1 Structure & Visualization
- 3.2 Node Structure
- 3.3 Insertion Operations
- 3.4 Deletion Operations
- 3.5 Advantages & Disadvantages
- Practice Questions β Doubly Linked List
4 β Circular Linked List
- 4.1 Singly Circular Linked List
- 4.2 Doubly Circular Linked List
- 4.3 Operations on Circular Linked List
- Practice Questions β Circular Linked List
4b β Header Linked Lists
5 β Applications of Linked Lists
- 5.1 Polynomial Representation & Operations
- 5.2 Sparse Matrix Representation
- 5.3 Sparse Matrix Addition
- 5.4 Sparse Matrix Transpose
- Practice Questions β Applications
6 β Comparison of All Linked List Types
Glossary β Key Terms
| Term | Meaning |
|---|---|
| Linked List | A linear data structure where elements (nodes) are stored in non-contiguous memory, connected via pointers. |
| Node | A basic unit of a linked list containing data and one or more pointer(s) to other nodes. |
| Head | A pointer to the first node of the linked list. |
| Tail | The last node of the linked list (its next pointer is NULL in singly LL). |
| Singly Linked List | Each node has one pointer β next β pointing to the next node. Traversal: forward only. |
| Doubly Linked List | Each node has two pointers β prev and next. Traversal: both forward and backward. |
| Circular Linked List | The last node points back to the first node (instead of NULL), forming a circle. |
malloc() |
C function to allocate memory dynamically at runtime from the heap. |
free() |
C function to release dynamically allocated memory. |
| Sparse Matrix | A matrix where most elements are zero. Storing only non-zero elements saves memory. |
| Polynomial | An algebraic expression with terms of the form $ax^n$. Can be represented as a linked list of (coefficient, exponent) nodes. |
1 β Introduction to Linked Lists
1.1 List as an ADT
ADT List:
Data:
An ordered collection of elements
Operations:
insert(pos, element) β Insert at position β O(n)
delete(pos) β Delete element at position β O(n)
search(element) β Find element, return position β O(n)
get(pos) β Return element at position β O(n)
length() β Return count of elements β O(1) or O(n)
isEmpty() β Check if list is empty β O(1)
display() β Print all elements β O(n)
Implementations:
1. Array-based (sequential)
2. Linked list (pointer-based)
1.2 Array vs Linked Organization
| Feature | Array (Sequential) | Linked List (Linked) |
|---|---|---|
| Memory | Contiguous block | Scattered (non-contiguous) |
| Size | Fixed at declaration | Dynamic (grows/shrinks) |
| Access | Random β $O(1)$ by index | Sequential β $O(n)$ |
| Insertion | $O(n)$ β shifting | $O(1)$ if at known position |
| Deletion | $O(n)$ β shifting | $O(1)$ if at known position |
| Memory Overhead | None | Extra pointer(s) per node |
| Cache Performance | Excellent (contiguous) | Poor (scattered) |
| Memory Utilization | May waste (pre-allocated) | No waste (allocate as needed) |
When to use what?
- Array: When you need fast random access and know the size in advance
- Linked List: When you need frequent insertions/deletions and the size varies
1.3 Node Structure & Pointers
Each element in a linked list is stored in a node:
ββββββββ¬βββββββ
β data β next β β Singly linked list node
ββββββββ΄βββββββ
ββββββββ¬βββββββ¬βββββββ
β prev β data β next β β Doubly linked list node
ββββββββ΄βββββββ΄βββββββ
In C:
// Singly linked list node
struct Node {
int data;
struct Node* next;
};
// Doubly linked list node
struct DNode {
int data;
struct DNode* prev;
struct DNode* next;
};
1.4 Dynamic Memory Management in C
Dynamic memory is allocated from the heap at runtime using C library functions from <stdlib.h>.
| Function | Purpose | Syntax |
|---|---|---|
malloc() |
Allocate uninitialized memory | ptr = (type*)malloc(size) |
calloc() |
Allocate zero-initialized memory | ptr = (type*)calloc(n, size) |
realloc() |
Resize previously allocated memory | ptr = realloc(ptr, new_size) |
free() |
Release allocated memory | free(ptr) |
// Creating a new node dynamically
struct Node* createNode(int value) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
if (newNode == NULL) {
printf("Memory allocation failed!\n");
return NULL;
}
newNode->data = value;
newNode->next = NULL;
return newNode;
}
Memory Leaks: If you allocate memory with malloc() but never free() it, the memory remains occupied even when no longer needed. Always free nodes when deleting them!
Dangling Pointers: After free(ptr), the pointer ptr still holds the old address. Set it to NULL after freeing: free(ptr); ptr = NULL;
2 β Singly Linked List
2.1 Structure & Visualization
head β [10|β’] β [20|β’] β [30|β’] β [40|β’] β NULL
headis a pointer to the first node- Each nodeβs
nextpoints to the next node - The last nodeβs
nextisNULL(end marker)
2.2 Creating a Node
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
Node* createNode(int value) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value;
newNode->next = NULL;
return newNode;
}
π Line-by-Line Logic:
(Node*)malloc(sizeof(Node))βmallocallocates exactlysizeof(Node)bytes on the heap (enough for oneint+ one pointer). The cast(Node*)converts the genericvoid*returned bymallocinto our specific node pointer type.newNode->next = NULLβ Critical initialization! A newly created node doesnβt connect to anything yet. SettingnexttoNULLprevents a dangling pointer. Without this,nextwould contain garbage β leading to crashes when traversing.- Why return a pointer? β The node lives on the heap, not the stack. Returning a pointer lets the caller use this node long after
createNodereturns. If we returned a local struct, it would vanish when the function exits.
2.3 Traversal (Display)
void display(Node* head) {
Node* temp = head;
while (temp != NULL) {
printf("%d β ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
int length(Node* head) {
int count = 0;
Node* temp = head;
while (temp != NULL) {
count++;
temp = temp->next;
}
return count;
}
π Why This Logic:
Node* temp = headβ We use a temporary pointer so we donβt lose thehead. If we movedheaditself, weβd lose access to the beginning of the list permanently.while (temp != NULL)β A linked list has no length field β the only way to know when weβve reached the end is to check forNULL. This is the universal βend-of-listβ sentinel.temp = temp->nextβ This is the linked-list equivalent ofi++in arrays. Each step follows the pointer chain to the next node.- Time is $O(n)$ β We must visit every single node. Thereβs no shortcut because nodes arenβt contiguous in memory β unlike arrays where you can jump to any index.
Time: $O(n)$ β must visit each node
2.4 Insertion β Beginning, End, At Position
Insert at Beginning β $O(1)$
Node* insertAtBeginning(Node* head, int value) {
Node* newNode = createNode(value);
newNode->next = head; // New node points to old head
head = newNode; // Head now points to new node
return head;
}
π Why This Logic:
newNode->next = headβ The new nodeβsnextmust point to the current first node. This links the new node into the chain BEFORE updating head.head = newNodeβ Now head points to the new node, making it the first element.- Order matters! β If we did
head = newNodefirst, weβd lose the reference to the old first node. Always link the new node first, then update head.- Why $O(1)$? β No traversal needed. We directly manipulate the head pointer regardless of list length. This is the fastest linked list operation.
- Why return head? β In C, function parameters are passed by value. Changing
headinside the function doesnβt affect the callerβs copy. Returning the new head lets the caller update it:head = insertAtBeginning(head, 10);
Before: head β [20|β’] β [30|β’] β NULL
Insert 10 at beginning:
After: head β [10|β’] β [20|β’] β [30|β’] β NULL
Insert at End β $O(n)$
Node* insertAtEnd(Node* head, int value) {
Node* newNode = createNode(value);
if (head == NULL) // Empty list
return newNode;
Node* temp = head;
while (temp->next != NULL) // Traverse to last node
temp = temp->next;
temp->next = newNode;
return head;
}
π Line-by-Line Logic:
if (head == NULL) return newNodeβ Edge case: if the list is empty, the new node IS the entire list. Return it as head.while (temp->next != NULL)β We stop whentemp->nextisNULL, meaningtempis the last node. If we checkedtemp != NULLinstead, weβd go one step too far andtempwould beNULLβ we couldnβt attach the new node.temp->next = newNodeβ The old last node now points to the new node. The new nodeβsnextis alreadyNULL(set increateNode), so it becomes the new tail.- Why $O(n)$? β We must traverse the entire list to find the last node. This is the main disadvantage of singly linked lists. Maintaining a
tailpointer would make this $O(1)$.
Insert at Position β $O(n)$
Node* insertAtPosition(Node* head, int value, int pos) {
if (pos == 0) return insertAtBeginning(head, value);
Node* newNode = createNode(value);
Node* temp = head;
for (int i = 0; i < pos - 1 && temp != NULL; i++)
temp = temp->next;
if (temp == NULL) {
printf("Position out of range!\n");
free(newNode);
return head;
}
newNode->next = temp->next;
temp->next = newNode;
return head;
}
π Line-by-Line Logic:
if (pos == 0)β Special case: inserting at position 0 means inserting at the beginning. Reuse the existing function to avoid code duplication.for (int i = 0; i < pos - 1 ...)β We traverse to the node at positionpos-1(the node BEFORE where we want to insert). We need this predecessor to splice in the new node.pos - 1, notposβ To insert between nodes A and B, we need a pointer to A. If we traverse to positionpos, weβd be at B and couldnβt access A (noprevpointer in singly LL).temp == NULLcheck β If the user gives a position beyond the list length,tempbecomes NULL during traversal. We catch this, free the wasted node, and return.newNode->next = temp->next; temp->next = newNodeβ Two-step splice: (1) new node points to what was aftertemp, (2)temppoints to new node. Order matters β reversing these would lose the link to the rest of the list.
Insert 25 at position 2:
Before: head β [10|β’] β [20|β’] β [30|β’] β NULL
pos-1 β pos β
Step 1: newNode β [25|β’]
Step 2: newNode->next = temp->next (25 points to 30)
Step 3: temp->next = newNode (20 points to 25)
After: head β [10|β’] β [20|β’] β [25|β’] β [30|β’] β NULL
2.5 Deletion β Beginning, End, At Position
Delete at Beginning β $O(1)$
Node* deleteAtBeginning(Node* head) {
if (head == NULL) {
printf("List is empty!\n");
return NULL;
}
Node* temp = head;
head = head->next;
free(temp);
return head;
}
π Why This Logic:
Node* temp = headβ Save the current head before moving it. Withouttemp, afterhead = head->next, we lose access to the old first node and can neverfreeit β memory leak.head = head->nextβ The second node becomes the new head. If the list had only one node,head->nextisNULL, so head becomesNULL(empty list).free(temp)β Release the old headβs memory back to the heap. This is mandatory in C β thereβs no garbage collector.- Why $O(1)$? β We only touch the head. No traversal needed regardless of list size.
Delete at End β $O(n)$
Node* deleteAtEnd(Node* head) {
if (head == NULL) return NULL;
if (head->next == NULL) { // Only one node
free(head);
return NULL;
}
Node* temp = head;
while (temp->next->next != NULL) // Stop at second-to-last
temp = temp->next;
free(temp->next);
temp->next = NULL;
return head;
}
π Line-by-Line Logic:
head->next == NULLcheck β If thereβs only one node, just free it and returnNULL. Without this special case,temp->next->nextwould dereferenceNULL->nextβ crash.while (temp->next->next != NULL)β We need the second-to-last node, not the last one. Why? Because after deleting the last node, the second-to-last must become the new tail (itsnextset toNULL). Thereβs noprevpointer in singly LL, so we must stop one step early.free(temp->next); temp->next = NULLβ Delete the last node and make the second-to-last the new tail.- Why $O(n)$? β We must traverse to find the second-to-last node. A
tailpointer alone wouldnβt help because weβd still need the node beforetail. A doubly linked list solves this.
Delete at Position β $O(n)$
Node* deleteAtPosition(Node* head, int pos) {
if (head == NULL) return NULL;
if (pos == 0) return deleteAtBeginning(head);
Node* temp = head;
for (int i = 0; i < pos - 1 && temp->next != NULL; i++)
temp = temp->next;
if (temp->next == NULL) {
printf("Position out of range!\n");
return head;
}
Node* toDelete = temp->next;
temp->next = toDelete->next;
free(toDelete);
return head;
}
π Why This Logic:
pos == 0delegates todeleteAtBeginningβ Deleting position 0 changes the head, which needs special handling.- Traverse to
pos - 1β Same reason as insertion: we need the node BEFORE the deletion target so we can relink around it.toDelete = temp->nextβ Save a pointer to the node weβll delete. This is the node at positionpos.temp->next = toDelete->nextβ The predecessor now points to the node AFTER the deleted one, effectively removing the target from the chain.free(toDelete)β Release the deleted nodeβs memory. Without this, the node is unreachable but still occupying heap memory (memory leak).
2.6 Search
int search(Node* head, int key) {
Node* temp = head;
int pos = 0;
while (temp != NULL) {
if (temp->data == key)
return pos;
temp = temp->next;
pos++;
}
return -1; // Not found
}
// Time: O(n)
π Why This Logic:
- Linear scan β Unlike arrays, linked lists donβt support random access. We canβt jump to the middle, so even binary search is impossible. We must check nodes one by one.
poscounter β We track position as we traverse so we can return WHERE the key was found (not just whether it exists).return -1for not found β Convention:-1signals absence since valid positions are non-negative.- Why not binary search? β Binary search requires $O(1)$ access to the middle element. In a linked list, finding the middle takes $O(n)$ traversal, which defeats the purpose. This is where arrays excel.
2.7 Reversing a Singly Linked List
Reversing a linked list means making the last node the head and reversing every link direction.
Iterative Approach (3-Pointer Technique):
Node* reverseList(Node* head) {
Node *prev = NULL, *curr = head, *next = NULL;
while (curr != NULL) {
next = curr->next; // Save next node
curr->next = prev; // Reverse the link
prev = curr; // Move prev forward
curr = next; // Move curr forward
}
return prev; // prev is the new head
}
π Line-by-Line Logic:
- Three pointers:
prev(the reversed portion so far),curr(the node weβre processing),next(saved link to the rest of the list).next = curr->nextβ We MUST save the next node before overwritingcurr->next. Otherwise, we lose access to the rest of the list.curr->next = prevβ This is the actual reversal: the current node now points backward instead of forward.prev = curr; curr = nextβ Advance both pointers one step.prevmoves to the just-processed node;currmoves to the next unprocessed node.- Return
prevβ Whencurrbecomes NULL, weβve processed all nodes.prevpoints to the last node processed, which is the new head.
Dry Run: Reverse 10 β 20 β 30 β NULL
| Step | prev | curr | next | List State |
|---|---|---|---|---|
| Init | NULL | 10 | β | 10β20β30βNULL |
| 1 | 10 | 20 | 20 | NULLβ10 β 20β30βNULL |
| 2 | 20 | 30 | 30 | NULLβ10β20 β 30βNULL |
| 3 | 30 | NULL | NULL | NULLβ10β20β30 |
Result: 30 β 20 β 10 β NULL, new head = 30
Recursive Approach:
Node* reverseListRec(Node* head) {
if (head == NULL || head->next == NULL)
return head; // Base case: empty or single node
Node* newHead = reverseListRec(head->next);
head->next->next = head; // Make next node point back to current
head->next = NULL; // Remove forward link
return newHead;
}
π Why This Works:
- The recursion goes all the way to the last node (base case:
head->next == NULL), which becomes the new head.- On the way back (unwinding),
head->next->next = headmakes the node in front of us point backward to us.head->next = NULLremoves our forward pointer (it will be set by the previous call, except for the original head which should point to NULL).- Time: $O(n)$, Space: $O(n)$ due to recursion stack (iterative is better at $O(1)$ space).
Comparison:
| Approach | Time | Space | Notes |
|---|---|---|---|
| Iterative | $O(n)$ | $O(1)$ | Preferred β no stack overhead |
| Recursive | $O(n)$ | $O(n)$ | Elegant but uses call stack |
2.8 Complexity Analysis
| Operation | Time | Space |
|---|---|---|
| Insert at beginning | $O(1)$ | $O(1)$ |
| Insert at end | $O(n)$ | $O(1)$ |
| Insert at position | $O(n)$ | $O(1)$ |
| Delete at beginning | $O(1)$ | $O(1)$ |
| Delete at end | $O(n)$ | $O(1)$ |
| Delete at position | $O(n)$ | $O(1)$ |
| Search | $O(n)$ | $O(1)$ |
| Traversal | $O(n)$ | $O(1)$ |
| Access by index | $O(n)$ | $O(1)$ |
Practice Questions β Singly Linked List
Q1. Implement a singly linked list with all operations: insert (beginning, end, position), delete (beginning, end, position), search, display, and count.
Q2. Write a function to reverse a singly linked list.
Q3. Write a function to find the middle element of a singly linked list (use the two-pointer technique).
Q4. Write a function to detect a cycle in a linked list (Floydβs cycle detection).
Q5. Write a function to merge two sorted linked lists into one sorted list.
Q6. Explain why inserting at the beginning of a singly linked list is $O(1)$ but inserting at the end is $O(n)$.
3 β Doubly Linked List
3.1 Structure & Visualization
NULL β [β’|10|β’] β [β’|20|β’] β [β’|30|β’] β [β’|40|β’] β NULL
β head
Each node has two pointers: prev (points to previous node) and next (points to next node).
3.2 Node Structure
typedef struct DNode {
int data;
struct DNode* prev;
struct DNode* next;
} DNode;
DNode* createDNode(int value) {
DNode* newNode = (DNode*)malloc(sizeof(DNode));
newNode->data = value;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
π Why This Logic: Same as singly LL
createNode, but we now initialize TWO pointers toNULL. A doubly-linked node hasprevANDnext. Both must beNULLbecause the node isnβt connected to anything yet. The extra pointer per node is the cost of bidirectional traversal.
3.3 Insertion Operations
Insert at Beginning β $O(1)$
DNode* insertAtBeginning(DNode* head, int value) {
DNode* newNode = createDNode(value);
if (head != NULL) {
newNode->next = head;
head->prev = newNode;
}
return newNode; // New head
}
π Line-by-Line Logic:
if (head != NULL)β If the list isnβt empty, we need to link the new node to the existing first node. If the list IS empty, the new node is both head and tail β itsprevandnextstayNULL.newNode->next = headβ New node points forward to the old head (same as singly LL).head->prev = newNodeβ The old headβsprevnow points back to the new node. This is the EXTRA step that singly LL doesnβt have. Two-way linking requires updating pointers in BOTH directions.return newNodeβ The new node is always the new head, regardless of whether the list was empty.
Insert at End β $O(n)$
DNode* insertAtEnd(DNode* head, int value) {
DNode* newNode = createDNode(value);
if (head == NULL) return newNode;
DNode* temp = head;
while (temp->next != NULL)
temp = temp->next;
temp->next = newNode;
newNode->prev = temp;
return head;
}
π Why This Logic:
- Traversal to the last node is identical to singly LL β follow
nextuntilNULL.temp->next = newNodeβ Old tail points forward to new node.newNode->prev = tempβ New node points back to old tail. Both links established β fully connected.- Same $O(n)$ time issue as singly LL. Maintaining a
tailpointer would make this $O(1)$.
Insert After a Given Node β $O(1)$
void insertAfter(DNode* prevNode, int value) {
if (prevNode == NULL) return;
DNode* newNode = createDNode(value);
newNode->next = prevNode->next;
newNode->prev = prevNode;
if (prevNode->next != NULL)
prevNode->next->prev = newNode;
prevNode->next = newNode;
}
π Line-by-Line Logic (4 pointer updates):
newNode->next = prevNode->nextβ New nodeβs forward link set to what was after prevNode.newNode->prev = prevNodeβ New nodeβs backward link set to prevNode.prevNode->next->prev = newNodeβ The node that was after prevNode now points back to new node (instead of prevNode). Theifguard prevents crash when inserting at the tail.prevNode->next = newNodeβ prevNodeβs forward link updated to new node.- Why this order? β We must set
newNodeβs links first (steps 1-2), then update the surrounding nodes (steps 3-4). If we updatedprevNode->nextfirst (step 4), weβd lose the reference to the node that was after it.
Insert 25 after node containing 20:
Before: NULL β [10] β [20] β [30] β NULL
β prevNode
After: NULL β [10] β [20] β [25] β [30] β NULL
Steps:
1. newNode->next = prevNode->next (25 β 30)
2. newNode->prev = prevNode (25 β 20)
3. prevNode->next->prev = newNode (25 β 30)
4. prevNode->next = newNode (20 β 25)
3.4 Deletion Operations
Delete at Beginning β $O(1)$
DNode* deleteAtBeginning(DNode* head) {
if (head == NULL) return NULL;
DNode* temp = head;
head = head->next;
if (head != NULL)
head->prev = NULL;
free(temp);
return head;
}
π Why This Logic:
head = head->nextβ Advance head to the second node (same as singly LL).head->prev = NULLβ The new headβsprevused to point to the old head. We must sever this link so the new head has no predecessor. Without this,head->prevwould be a dangling pointer (pointing to freed memory).if (head != NULL)guard β If the list had only one node,headis nowNULL. We canβt dereferenceNULL->prev.
Delete a Specific Node β $O(1)$ (when node pointer is given)
DNode* deleteNode(DNode* head, DNode* delNode) {
if (head == NULL || delNode == NULL) return head;
// If deleting head
if (head == delNode)
head = delNode->next;
// Update next node's prev pointer
if (delNode->next != NULL)
delNode->next->prev = delNode->prev;
// Update previous node's next pointer
if (delNode->prev != NULL)
delNode->prev->next = delNode->next;
free(delNode);
return head;
}
π Line-by-Line Logic:
- This is the crown jewel of DLL β given a pointer to ANY node, delete it in $O(1)$. In a singly LL, youβd need $O(n)$ traversal to find the predecessor.
if (head == delNode)β Special case: if deleting the head, advance head pointer.delNode->next->prev = delNode->prevβ The node afterdelNodeshould skip overdelNodeand point back todelNodeβs predecessor. Theifguard handles deletion of the tail node (wherenextisNULL).delNode->prev->next = delNode->nextβ The node beforedelNodeshould skip overdelNodeand point todelNodeβs successor. Theifguard handles deletion of the head (whereprevisNULL).- Why $O(1)$? β Because
delNode->prevgives us the predecessor directly. No traversal needed. This is the fundamental advantage of the extraprevpointer.
Key Advantage of Doubly Linked List: If you have a pointer to a node, you can delete it in $O(1)$ time! In a singly linked list, youβd need to traverse from the head to find the previous node β $O(n)$.
3.5 Advantages & Disadvantages
| Aspect | Singly LL | Doubly LL |
|---|---|---|
| Memory per node | 1 pointer | 2 pointers (more memory) |
| Forward traversal | β | β |
| Backward traversal | β | β |
| Delete node (given pointer) | $O(n)$ β need predecessor | $O(1)$ |
| Insert before node | $O(n)$ | $O(1)$ |
| Complexity | Simpler | More complex (managing 2 pointers) |
Practice Questions β Doubly Linked List
Q1. Implement a doubly linked list with insert (beginning, end, after node) and delete (beginning, end, specific node) operations.
Q2. Write a function to reverse a doubly linked list.
Q3. Compare singly and doubly linked lists. When would you choose one over the other?
Q4. Why is deletion $O(1)$ in a doubly linked list when given a pointer to the node?
Q5. Implement a function to display a doubly linked list in both forward and reverse direction.
4 β Circular Linked List
4.1 Singly Circular Linked List
In a Singly Circular Linked List, the last nodeβs next pointer points back to the first node instead of NULL.
ββββββββββββββββββββββββββββββββββββ
β β
[10|β’] β [20|β’] β [30|β’] β [40|β’]βββ
β head
How to detect the end: A node is the last node if node->next == head.
// Traversal of circular linked list
void display(Node* head) {
if (head == NULL) return;
Node* temp = head;
do {
printf("%d β ", temp->data);
temp = temp->next;
} while (temp != head); // Stop when we return to head
printf("(back to head)\n");
}
π Why
do-whileinstead ofwhile?
- In a regular linked list,
while (temp != NULL)works because the end is marked byNULL.- In a circular list, there IS no
NULLβ every nodeβsnextpoints to another node. The termination condition istemp == head(weβve gone full circle).- But if we used
while (temp != head), the loop body would NEVER execute (becausetempstarts equal tohead). Thedo-whileensures we process the first node before checking the condition.- This is the defining pattern of circular list traversal:
do { ... } while (temp != head);
4.2 Doubly Circular Linked List
In a Doubly Circular Linked List, the last nodeβs next points to the first node, AND the first nodeβs prev points to the last node.
ββββββββββββββββββββββββββββββββββββββββ
β β
[β’|10|β’] β [β’|20|β’] β [β’|30|β’] β [β’|40|β’]
β β
ββββββββββββββββββββββββββββββββββββββββ
4.3 Operations on Circular Linked List
Insert at Beginning (Singly Circular)
Node* insertAtBeginning(Node* head, int value) {
Node* newNode = createNode(value);
if (head == NULL) {
newNode->next = newNode; // Points to itself
return newNode;
}
// Find the last node
Node* last = head;
while (last->next != head)
last = last->next;
newNode->next = head;
last->next = newNode; // Last node points to new head
return newNode; // New head
}
π Line-by-Line Logic:
newNode->next = newNodeβ When the list is empty, the single node must point to itself to maintain the circular property. A lone node is its own successor.- Finding the last node:
while (last->next != head)β The last node is identified bylast->next == head. We need it because inserting at the beginning of a CIRCULAR list requires updating the last nodeβsnextto point to the new head.last->next = newNodeβ The circle is maintained: last β newNode β old head β β¦ β last.- Why $O(n)$? β Finding the last node requires full traversal. Maintaining a
tailpointer would make this $O(1)$.
Insert at End (Singly Circular)
Node* insertAtEnd(Node* head, int value) {
Node* newNode = createNode(value);
if (head == NULL) {
newNode->next = newNode;
return newNode;
}
Node* last = head;
while (last->next != head)
last = last->next;
last->next = newNode;
newNode->next = head; // New node points back to head
return head;
}
π Why This Logic:
- Almost identical to insert-at-beginning, but the new node goes AFTER the last node instead of before head.
last->next = newNodeβ Old last node points to new node.newNode->next = headβ New node (now the last) completes the circle by pointing back to head.- Head doesnβt change β Unlike insert-at-beginning, the first element stays the same. We
return headunchanged.
Delete at Beginning (Singly Circular)
Node* deleteAtBeginning(Node* head) {
if (head == NULL) return NULL;
if (head->next == head) { // Only one node
free(head);
return NULL;
}
Node* last = head;
while (last->next != head)
last = last->next;
Node* temp = head;
head = head->next;
last->next = head; // Last node now points to new head
free(temp);
return head;
}
π Line-by-Line Logic:
head->next == headβ If the node points to itself, itβs the only node. Free it and returnNULL.- Finding last node again β Just like insertion, we need the last node so we can update
last->nextto point to the new head (maintaining the circle).last->next = headβ After advancingheadto the second node, the last node must now point to this new head. Without this, the circle would still include the freed node β dangling pointer and crash.- Pattern: Every circular LL operation that changes the head requires finding AND updating the last node. This is the operational cost of the circular structure.
Applications of Circular Linked List:
- Round-robin scheduling in operating systems
- Circular buffers in data streaming
- Multiplayer games β turns rotate among players
- Music playlist with repeat mode
- Implementation of circular queues
Practice Questions β Circular Linked List
Q1. Implement a singly circular linked list with insert and delete at beginning and end.
Q2. How do you detect the last node in a circular linked list? How does traversal differ from a regular linked list?
Q3. Write a function to count the number of nodes in a circular linked list.
Q4. Solve the Josephus Problem using a circular linked list.
Q5. Compare singly linked list, doubly linked list, and circular linked list.
4.4 Header Linked Lists
Definition: A Header Linked List is a linked list that begins with a special node called the header node (or sentinel node). The header node does not store actual data β it serves as a permanent first node that simplifies insertion and deletion operations.
Two Types:
1. Grounded Header List:
- The header nodeβs
nextpoints to the first actual data node - The last nodeβs
nextisNULL(grounded)
Header β [10] β [20] β [30] β NULL
β
(no data, just a sentinel)
2. Circular Header List:
- The header nodeβs
nextpoints to the first actual data node - The last nodeβs
nextpoints back to the header node (not NULL)
βββββββββββββββββββββββββββββββββββ
β β
Header β [10] β [20] β [30] ββββ
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef struct {
Node* header; // Permanent header node
} HeaderLinkedList;
void init(HeaderLinkedList* list) {
list->header = (Node*)malloc(sizeof(Node));
list->header->data = 0; // Header stores metadata (e.g., count)
list->header->next = NULL; // Grounded: NULL, Circular: list->header
}
void insertAfterHeader(HeaderLinkedList* list, int value) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value;
newNode->next = list->header->next;
list->header->next = newNode;
list->header->data++; // Increment count stored in header
}
void display(HeaderLinkedList* list) {
Node* temp = list->header->next; // Skip header
while (temp != NULL) { // For circular: while (temp != list->header)
printf("%d β ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
π Why Use a Header Node?
- No special cases for empty list β The header always exists, so
list->header->nextis always valid. Insertion at the beginning doesnβt need to update aheadpointer β we just insert after the header.- Metadata storage β The headerβs
datafield can store useful information like the count of nodes, so we donβt need to traverse to count.- Uniform operations β Every actual data node has a predecessor (at minimum, the header). This eliminates edge cases in insertion/deletion code.
- Circular header lists simplify traversals that need to βwrap aroundβ β we know weβve visited all nodes when we return to the header.
Header node vs Regular linked list:
| Feature | Regular Linked List | Header Linked List |
|---|---|---|
| First node | Data node | Header (sentinel) node |
| Empty list | head == NULL |
header->next == NULL (or header->next == header) |
| Insert at beginning | Must update head pointer |
Insert after header (header never changes) |
| Count of elements | Traverse to count: $O(n)$ | Stored in header: $O(1)$ |
| Edge cases | More NULL checks needed |
Fewer special cases |
5 β Applications of Linked Lists
5.1 Polynomial Representation & Operations
A polynomial like $5x^3 + 4x^2 + 2x + 1$ can be represented as a linked list where each node stores:
- Coefficient (e.g., 5, 4, 2, 1)
- Exponent (e.g., 3, 2, 1, 0)
- Pointer to next term
typedef struct PolyNode {
int coeff;
int exp;
struct PolyNode* next;
} PolyNode;
Representation Example
Polynomial: 5xΒ³ + 4xΒ² + 2x + 1
[5,3|β’] β [4,2|β’] β [2,1|β’] β [1,0|β’] β NULL
coeff,exp
Polynomial Addition
Two polynomials: $P1 = 5x^3 + 4x^2 + 2x + 1$ and $P2 = 3x^3 + 2x + 7$
PolyNode* addPolynomials(PolyNode* p1, PolyNode* p2) {
PolyNode* result = NULL;
while (p1 != NULL && p2 != NULL) {
if (p1->exp > p2->exp) {
// Add p1's term to result
appendTerm(&result, p1->coeff, p1->exp);
p1 = p1->next;
} else if (p1->exp < p2->exp) {
// Add p2's term to result
appendTerm(&result, p2->coeff, p2->exp);
p2 = p2->next;
} else {
// Same exponent β add coefficients
int sumCoeff = p1->coeff + p2->coeff;
if (sumCoeff != 0)
appendTerm(&result, sumCoeff, p1->exp);
p1 = p1->next;
p2 = p2->next;
}
}
// Copy remaining terms
while (p1 != NULL) {
appendTerm(&result, p1->coeff, p1->exp);
p1 = p1->next;
}
while (p2 != NULL) {
appendTerm(&result, p2->coeff, p2->exp);
p2 = p2->next;
}
return result;
}
π Line-by-Line Logic:
- Merge-like pattern β This algorithm is structurally identical to merging two sorted arrays. Both polynomials are sorted by exponent (descending), and we process them together using two pointers.
p1->exp > p2->expβ If p1βs current term has a higher exponent, it has no match in p2. Copy it directly to the result and advance p1.p1->exp < p2->expβ Similarly, p2βs term has no match. Copy it and advance p2.p1->exp == p2->expβ Same exponent! Add the coefficients. This is the actual polynomial addition: $ax^n + bx^n = (a+b)x^n$.if (sumCoeff != 0)β If coefficients cancel out (e.g., $5x^2 + (-5)x^2 = 0$), we donβt add a zero term. This keeps the result clean.- Remaining terms β After one polynomial is exhausted, the otherβs remaining terms are copied as-is (they have no counterparts to add with).
- Time: $O(m + n)$ where $m$ and $n$ are the number of terms in each polynomial.
Add:
$P1 = 5x^3 + 4x^2 + 2x + 1$
$P2 = 3x^3 + 2x + 7$
Step by step:
- $x^3$: $5 + 3 = 8$
- $x^2$: only in P1 β $4$
- $x^1$: $2 + 2 = 4$
- $x^0$: $1 + 7 = 8$
Result: $8x^3 + 4x^2 + 4x + 8$
5.2 Sparse Matrix Representation
A Sparse Matrix is a matrix in which most elements are zero. Storing all elements (including zeros) wastes memory. Instead, we store only the non-zero elements.
Why Use Sparse Representation?
For a $100 \times 100$ matrix with only 10 non-zero elements:
- Normal representation: $100 \times 100 = 10,000$ elements stored
- Sparse representation: Only 10 elements + their positions stored
Triplet Representation (Array-based)
Store each non-zero element as a triplet: (row, column, value)
typedef struct {
int row, col, val;
} Element;
typedef struct {
int rows, cols, numNonZero;
Element data[MAX]; // Array of triplets
} SparseMatrix;
Matrix:
0 0 3 0
0 4 0 0
5 0 0 0
0 0 0 6
Sparse Triplet Representation:
| Index | Row | Col | Value |
|---|---|---|---|
| 0 | 0 | 2 | 3 |
| 1 | 1 | 1 | 4 |
| 2 | 2 | 0 | 5 |
| 3 | 3 | 3 | 6 |
Metadata: rows=4, cols=4, numNonZero=4
Savings: 16 elements β 4 triplets (12 values) + metadata
Linked List Representation
Each non-zero element is a node with (row, col, value, next):
typedef struct SparseNode {
int row, col, val;
struct SparseNode* next;
} SparseNode;
head β [0,2,3|β’] β [1,1,4|β’] β [2,0,5|β’] β [3,3,6|β’] β NULL
5.3 Sparse Matrix Addition
SparseMatrix addSparse(SparseMatrix a, SparseMatrix b) {
SparseMatrix result;
result.rows = a.rows;
result.cols = a.cols;
result.numNonZero = 0;
int i = 0, j = 0;
while (i < a.numNonZero && j < b.numNonZero) {
// Compare positions (row-major order)
int posA = a.data[i].row * a.cols + a.data[i].col;
int posB = b.data[j].row * b.cols + b.data[j].col;
if (posA < posB) {
result.data[result.numNonZero++] = a.data[i++];
} else if (posA > posB) {
result.data[result.numNonZero++] = b.data[j++];
} else {
// Same position β add values
int sum = a.data[i].val + b.data[j].val;
if (sum != 0) {
result.data[result.numNonZero].row = a.data[i].row;
result.data[result.numNonZero].col = a.data[i].col;
result.data[result.numNonZero].val = sum;
result.numNonZero++;
}
i++; j++;
}
}
// Copy remaining
while (i < a.numNonZero)
result.data[result.numNonZero++] = a.data[i++];
while (j < b.numNonZero)
result.data[result.numNonZero++] = b.data[j++];
return result;
}
π Line-by-Line Logic:
posA = row * cols + colβ Converts 2D position (row, col) into a single linear index. This allows comparing positions of elements from both matrices in one comparison. E.g., position (1,3) in a 5-column matrix = $1 \times 5 + 3 = 8$. This is row-major order β the same way 2D arrays are stored in memory.posA < posBβ Element from matrix A comes first in row-major order. Copy it to result and advance Aβs pointer.posA > posBβ Element from B comes first. Copy it and advance B.posA == posBβ Both matrices have a non-zero element at the same position. Add their values. If the sum is zero, donβt include it (itβs no longer βnon-zeroβ).- Same merge pattern as polynomial addition! Both structures are sorted sequences merged element-by-element.
- Time: $O(a + b)$ where $a$ and $b$ are the number of non-zero elements.
Transpose swaps rows and columns: element at $(i, j)$ moves to $(j, i)$.
Simple Transpose β $O(cols \times numNonZero)$
SparseMatrix transposeSparse(SparseMatrix a) {
SparseMatrix result;
result.rows = a.cols;
result.cols = a.rows;
result.numNonZero = a.numNonZero;
int k = 0;
for (int col = 0; col < a.cols; col++) {
for (int i = 0; i < a.numNonZero; i++) {
if (a.data[i].col == col) {
result.data[k].row = a.data[i].col;
result.data[k].col = a.data[i].row;
result.data[k].val = a.data[i].val;
k++;
}
}
}
return result;
}
π Why This Logic:
- Transpose = swap rows and columns. Element at (row, col) moves to (col, row).
result.rows = a.colsβ The transposed matrix has its dimensions swapped.- Why the nested loop? β We want the result sorted by row (which is the original column). The outer loop iterates over columns 0, 1, 2, β¦ and for each column, the inner loop finds all elements in that column. This naturally produces results in row-major order.
- Why not just swap row/col directly? β We could, but the result wouldnβt be in sorted row-major order. The nested loop ensures sorted output.
- Time: $O(cols \times numNonZero)$ β For each of the
colscolumns, we scan allnumNonZeroelements. This is slow for large matrices. The fast transpose solves this.
Fast Transpose β $O(cols + numNonZero)$
Uses two auxiliary arrays to compute positions directly:
SparseMatrix fastTranspose(SparseMatrix a) {
SparseMatrix result;
result.rows = a.cols;
result.cols = a.rows;
result.numNonZero = a.numNonZero;
int rowTerms[a.cols]; // Count of elements in each column of a
int startPos[a.cols]; // Starting position in result for each column
// Initialize
for (int i = 0; i < a.cols; i++) rowTerms[i] = 0;
// Count non-zero elements in each column
for (int i = 0; i < a.numNonZero; i++)
rowTerms[a.data[i].col]++;
// Compute starting positions
startPos[0] = 0;
for (int i = 1; i < a.cols; i++)
startPos[i] = startPos[i-1] + rowTerms[i-1];
// Place elements in correct position
for (int i = 0; i < a.numNonZero; i++) {
int j = startPos[a.data[i].col]++;
result.data[j].row = a.data[i].col;
result.data[j].col = a.data[i].row;
result.data[j].val = a.data[i].val;
}
return result;
}
π Line-by-Line Logic:
- Two auxiliary arrays:
rowTerms[col]counts how many non-zero elements are in columncolof the original matrix.startPos[col]tells us where elements from columncolshould start in the result.- Step 1 β Count:
for (i) rowTerms[a.data[i].col]++β Scan all non-zero elements once, counting how many belong to each column.- Step 2 β Compute positions:
startPos[i] = startPos[i-1] + rowTerms[i-1]β Prefix sum! If column 0 has 2 elements and column 1 has 3 elements, column 1βs elements start at index 2, and column 2βs start at index 5. This is exactly how counting sort works.- Step 3 β Place:
j = startPos[col]++β Get the next available slot for this column and increment. Each element goes directly into its correct final position β no shifting needed.- Why $O(cols + numNonZero)$? β Step 1: $O(numNonZero)$. Step 2: $O(cols)$. Step 3: $O(numNonZero)$. Total: $O(cols + numNonZero)$ β much faster than the simple transposeβs $O(cols \times numNonZero)$.
- Key insight: This is the counting sort technique applied to matrix transposition. Instead of sorting by value, weβre sorting elements by their column index.
Fast Transpose uses the same counting principle as Counting Sort! It determines where each element should go before placing them, achieving linear time.
Practice Questions β Applications
Q1. Represent the polynomial $3x^4 + 2x^2 + 5$ as a linked list. Draw the diagram.
Q2. Write a C function to add two polynomials represented as linked lists.
Q3. What is a sparse matrix? When is sparse representation beneficial?
Q4. Represent the following matrix in triplet form:
0 0 0 5
0 3 0 0
0 0 0 0
2 0 0 0
Q5. Explain Fast Transpose with an example. Why is it $O(cols + numNonZero)$?
Q6. Write a C function to multiply two polynomials using linked lists.
6 β Comparison of All Linked List Types
6.1 Master Comparison Table
| Feature | Singly LL | Doubly LL | Singly Circular | Doubly Circular |
|---|---|---|---|---|
| Pointers per node | 1 (next) |
2 (prev, next) |
1 (next) |
2 (prev, next) |
| Traversal | Forward only | Forward & backward | Forward (circular) | Both (circular) |
| Last nodeβs next | NULL |
NULL |
Points to head | Points to head |
| First nodeβs prev | N/A | NULL |
N/A | Points to last |
| Insert at beginning | $O(1)$ | $O(1)$ | $O(n)$ or $O(1)$* | $O(1)$ |
| Delete given node | $O(n)$ | $O(1)$ | $O(n)$ | $O(1)$ |
| Memory | Least | More | Least | Most |
| Use Case | Simple lists | Browser history, editors | Round-robin, playlists | Complex circular structures |
*$O(1)$ if a tail pointer is maintained.
Comprehensive Unit IV Practice Set
Short Answer
1. Define a linked list. How does it differ from an array?
2. What is dynamic memory management? Explain malloc, calloc, realloc, and free.
3. Draw and explain node structures for singly, doubly, and circular linked lists.
4. What is a sparse matrix? Why is linked list representation useful for sparse matrices?
Implementation
5. Implement a complete singly linked list program in C with: create, insert (beginning, end, position), delete (beginning, end, position), search, display, count, and reverse.
6. Implement a doubly linked list with forward and reverse display.
7. Implement a circular linked list and solve a round-robin scheduling problem.
8. Write a C program to add two polynomials using linked lists:
- $P1 = 4x^3 + 3x^2 + 5$
- $P2 = 3x^4 + 3x^3 + 2x^2 + x$
9. Implement sparse matrix representation using triplets. Write functions for addition and fast transpose.
Tracing
10. Trace the following operations on a singly linked list (initially empty):
insertAtEnd(10), insertAtEnd(20), insertAtBeginning(5),
insertAtPosition(15, 2), deleteAtBeginning(),
deleteAtEnd(), display()
11. Trace insertion and deletion in a doubly linked list. Show pointer updates at each step.
Long Answer / Essay
12. Compare all types of linked lists (singly, doubly, circular, doubly circular) with diagrams, operations, complexities, and use cases.
13. Explain polynomial representation and addition using linked lists with a complete example. Write the algorithm and analyze its time complexity.
14. Explain sparse matrix representation, addition, and transpose (both simple and fast). Compare simple transpose vs fast transpose.
15. Discuss the trade-offs between arrays and linked lists for implementing the List ADT. When would you choose each?