Module 9.1

Linked Lists in C

Master the fundamental dynamic data structure that forms the foundation of many advanced algorithms. Learn to create, manipulate, and traverse singly, doubly, and circular linked lists using pointers and dynamic memory allocation.

45 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Understanding linked list concepts
  • Implementing singly linked lists
  • Building doubly linked lists
  • Creating circular linked lists
  • Common operations and algorithms
Contents
01

Introduction to Linked Lists

Linked lists are dynamic data structures where elements (nodes) are connected through pointers. Unlike arrays, linked lists can grow and shrink at runtime, making them perfect for situations where the data size is unknown or frequently changes.

Arrays vs Linked Lists

To understand why linked lists exist, let us compare them with arrays:

Aspect Array Linked List
Memory Allocation Contiguous block, fixed size Non-contiguous, dynamic size
Access Time O(1) - direct index access O(n) - must traverse from head
Insertion/Deletion O(n) - requires shifting elements O(1) - just update pointers
Memory Efficiency No extra overhead Extra space for pointers
Size Flexibility Fixed at creation Can grow/shrink dynamically
Data Structure

Linked List

A linked list is a linear data structure where each element (called a node) contains data and one or more pointers to other nodes. The nodes are not stored in contiguous memory locations; instead, they are linked together using pointers.

Key components: Each node has two parts - a data field to store the value and a pointer field (called next) that stores the address of the next node in the sequence.

The Node Structure

The building block of any linked list is the node. Here is how we define a node in C:

// Basic node structure for a singly linked list
struct Node {
    int data;           // Data stored in the node
    struct Node *next;  // Pointer to the next node
};

// Using typedef for cleaner code
typedef struct Node {
    int data;
    struct Node *next;
} Node;

// Now we can create nodes easily
Node *head = NULL;  // Empty list starts with NULL head

Visual Representation

A linked list looks like a chain of boxes connected by arrows:

// Visual representation of a linked list with values 10, 20, 30:

// head -> [10|next] -> [20|next] -> [30|next] -> NULL
//          ↑             ↑             ↑
//        Node 1        Node 2        Node 3

// Each node contains:
// - Data (10, 20, or 30)
// - Pointer to next node (or NULL if last)

Types of Linked Lists

Singly Linked

Each node has one pointer to the next node. Can only traverse forward. Simplest form, uses least memory.

Doubly Linked

Each node has pointers to both next and previous nodes. Can traverse both directions. Uses more memory.

Circular Linked

Last node points back to first node. No NULL at end. Useful for round-robin scheduling.

Practice Questions

Task: Define a node structure that stores a student's name (char array) and grade (float).

Show Solution
typedef struct StudentNode {
    char name[50];           // Student name
    float grade;             // Student grade
    struct StudentNode *next; // Pointer to next student
} StudentNode;

// Create a new node
StudentNode *createStudent(const char *name, float grade) {
    StudentNode *newNode = malloc(sizeof(StudentNode));
    if (newNode != NULL) {
        strcpy(newNode->name, name);
        newNode->grade = grade;
        newNode->next = NULL;
    }
    return newNode;
}

Task: For each scenario, choose array or linked list and explain why:

  1. Storing 1000 fixed sensor readings
  2. Managing a music playlist with frequent additions/removals
  3. Implementing an undo feature in a text editor
Show Solution
  1. Array - Fixed size known, fast random access for analysis, no insertions/deletions
  2. Linked List - Frequent insertions/deletions, size changes often, sequential playback
  3. Linked List (or Stack) - Operations at one end (push/pop), dynamic history size
02

Singly Linked Lists

Singly linked lists are the simplest form of linked lists. Each node contains data and a single pointer to the next node. They are memory-efficient and perfect for applications that only need forward traversal.

Complete Implementation

// singly_linked_list.h
#ifndef SINGLY_LINKED_LIST_H
#define SINGLY_LINKED_LIST_H

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node;

// Function prototypes
Node *createNode(int data);
void insertAtBeginning(Node **head, int data);
void insertAtEnd(Node **head, int data);
void insertAfter(Node *prevNode, int data);
void deleteNode(Node **head, int key);
Node *search(Node *head, int key);
void printList(Node *head);
void freeList(Node **head);
int getLength(Node *head);

#endif
// singly_linked_list.c
#include "singly_linked_list.h"

// Create a new node
Node *createNode(int data) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// Insert at the beginning - O(1)
void insertAtBeginning(Node **head, int data) {
    Node *newNode = createNode(data);
    newNode->next = *head;  // Point new node to current head
    *head = newNode;        // Update head to new node
}

// Insert at the end - O(n)
void insertAtEnd(Node **head, int data) {
    Node *newNode = createNode(data);
    
    // If list is empty, new node becomes head
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    
    // Traverse to the last node
    Node *current = *head;
    while (current->next != NULL) {
        current = current->next;
    }
    current->next = newNode;
}

// Insert after a given node - O(1)
void insertAfter(Node *prevNode, int data) {
    if (prevNode == NULL) {
        printf("Previous node cannot be NULL\n");
        return;
    }
    Node *newNode = createNode(data);
    newNode->next = prevNode->next;
    prevNode->next = newNode;
}

// Delete a node by value - O(n)
void deleteNode(Node **head, int key) {
    Node *current = *head;
    Node *prev = NULL;
    
    // If head node holds the key
    if (current != NULL && current->data == key) {
        *head = current->next;
        free(current);
        return;
    }
    
    // Search for the key
    while (current != NULL && current->data != key) {
        prev = current;
        current = current->next;
    }
    
    // Key not found
    if (current == NULL) {
        printf("Key %d not found in list\n", key);
        return;
    }
    
    // Unlink and free the node
    prev->next = current->next;
    free(current);
}

// Search for a value - O(n)
Node *search(Node *head, int key) {
    Node *current = head;
    while (current != NULL) {
        if (current->data == key) {
            return current;
        }
        current = current->next;
    }
    return NULL;  // Not found
}

// Print the list
void printList(Node *head) {
    Node *current = head;
    printf("List: ");
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

// Free all nodes
void freeList(Node **head) {
    Node *current = *head;
    Node *next;
    while (current != NULL) {
        next = current->next;
        free(current);
        current = next;
    }
    *head = NULL;
}

// Get length of list - O(n)
int getLength(Node *head) {
    int count = 0;
    Node *current = head;
    while (current != NULL) {
        count++;
        current = current->next;
    }
    return count;
}

Using the Singly Linked List

// main.c - Demonstration
#include "singly_linked_list.h"

int main() {
    Node *head = NULL;  // Start with empty list
    
    // Insert elements
    insertAtEnd(&head, 10);
    insertAtEnd(&head, 20);
    insertAtEnd(&head, 30);
    printList(head);  // List: 10 -> 20 -> 30 -> NULL
    
    // Insert at beginning
    insertAtBeginning(&head, 5);
    printList(head);  // List: 5 -> 10 -> 20 -> 30 -> NULL
    
    // Insert after node with value 10
    Node *node10 = search(head, 10);
    if (node10 != NULL) {
        insertAfter(node10, 15);
    }
    printList(head);  // List: 5 -> 10 -> 15 -> 20 -> 30 -> NULL
    
    // Delete node
    deleteNode(&head, 15);
    printList(head);  // List: 5 -> 10 -> 20 -> 30 -> NULL
    
    printf("Length: %d\n", getLength(head));  // Length: 4
    
    // Clean up
    freeList(&head);
    return 0;
}
Why use double pointer (Node **head)? We pass the address of the head pointer so the function can modify where head points. Without this, changes to head inside the function would not affect the original pointer in main().
Practice Questions

Task: Write a function to count how many times a given value appears in a linked list.

Show Solution
int countOccurrences(Node *head, int key) {
    int count = 0;
    Node *current = head;
    
    while (current != NULL) {
        if (current->data == key) {
            count++;
        }
        current = current->next;
    }
    
    return count;
}

// Usage:
// List: 1 -> 2 -> 1 -> 3 -> 1 -> NULL
// countOccurrences(head, 1) returns 3

Task: Write a function to reverse a singly linked list in place.

Show Solution
void reverseList(Node **head) {
    Node *prev = NULL;
    Node *current = *head;
    Node *next = NULL;
    
    while (current != NULL) {
        next = current->next;    // Store next
        current->next = prev;    // Reverse the link
        prev = current;          // Move prev forward
        current = next;          // Move current forward
    }
    
    *head = prev;  // Update head to last node
}

// Visual:
// Before: 1 -> 2 -> 3 -> NULL
// After:  3 -> 2 -> 1 -> NULL

// Step by step:
// prev=NULL, curr=1, next=2: 1->NULL, prev=1, curr=2
// prev=1, curr=2, next=3: 2->1, prev=2, curr=3
// prev=2, curr=3, next=NULL: 3->2, prev=3, curr=NULL
// head = 3

Task: Find the middle element of a linked list in one pass using the slow/fast pointer technique.

Show Solution
Node *findMiddle(Node *head) {
    if (head == NULL) return NULL;
    
    Node *slow = head;  // Moves 1 step at a time
    Node *fast = head;  // Moves 2 steps at a time
    
    // When fast reaches end, slow is at middle
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    return slow;
}

// Example:
// List: 1 -> 2 -> 3 -> 4 -> 5 -> NULL
// 
// Initial: slow=1, fast=1
// Step 1:  slow=2, fast=3
// Step 2:  slow=3, fast=5
// Step 3:  fast->next is NULL, stop
// Middle is 3

// For even length: 1 -> 2 -> 3 -> 4 -> NULL
// Returns 3 (second middle element)
03

Doubly Linked Lists

Doubly linked lists extend singly linked lists by adding a pointer to the previous node. This allows bidirectional traversal and makes some operations more efficient, at the cost of extra memory per node.

Doubly Linked List Structure

// Doubly linked list node
typedef struct DNode {
    int data;
    struct DNode *prev;  // Pointer to previous node
    struct DNode *next;  // Pointer to next node
} DNode;

// Visual representation:
// NULL <- [prev|10|next] <-> [prev|20|next] <-> [prev|30|next] -> NULL
//              ↑                   ↑                   ↑
//            head                                    tail

Complete Implementation

// doubly_linked_list.c
#include <stdio.h>
#include <stdlib.h>

typedef struct DNode {
    int data;
    struct DNode *prev;
    struct DNode *next;
} DNode;

// Create a new node
DNode *createDNode(int data) {
    DNode *newNode = (DNode *)malloc(sizeof(DNode));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        exit(1);
    }
    newNode->data = data;
    newNode->prev = NULL;
    newNode->next = NULL;
    return newNode;
}

// Insert at the beginning - O(1)
void insertAtBeginningD(DNode **head, int data) {
    DNode *newNode = createDNode(data);
    
    newNode->next = *head;
    
    if (*head != NULL) {
        (*head)->prev = newNode;
    }
    
    *head = newNode;
}

// Insert at the end - O(n) without tail pointer
void insertAtEndD(DNode **head, int data) {
    DNode *newNode = createDNode(data);
    
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    
    DNode *current = *head;
    while (current->next != NULL) {
        current = current->next;
    }
    
    current->next = newNode;
    newNode->prev = current;
}

// Insert after a given node - O(1)
void insertAfterD(DNode *prevNode, int data) {
    if (prevNode == NULL) {
        printf("Previous node cannot be NULL\n");
        return;
    }
    
    DNode *newNode = createDNode(data);
    
    newNode->next = prevNode->next;
    newNode->prev = prevNode;
    
    if (prevNode->next != NULL) {
        prevNode->next->prev = newNode;
    }
    
    prevNode->next = newNode;
}

// Delete a node - O(1) if node pointer is given
void deleteDNode(DNode **head, DNode *nodeToDelete) {
    if (*head == NULL || nodeToDelete == NULL) {
        return;
    }
    
    // If deleting head
    if (*head == nodeToDelete) {
        *head = nodeToDelete->next;
    }
    
    // Update next node's prev pointer
    if (nodeToDelete->next != NULL) {
        nodeToDelete->next->prev = nodeToDelete->prev;
    }
    
    // Update prev node's next pointer
    if (nodeToDelete->prev != NULL) {
        nodeToDelete->prev->next = nodeToDelete->next;
    }
    
    free(nodeToDelete);
}

// Print forward
void printForward(DNode *head) {
    DNode *current = head;
    printf("Forward:  ");
    while (current != NULL) {
        printf("%d <-> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

// Print backward (from tail)
void printBackward(DNode *head) {
    if (head == NULL) {
        printf("Backward: NULL\n");
        return;
    }
    
    // Go to tail
    DNode *current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    
    // Print backward
    printf("Backward: ");
    while (current != NULL) {
        printf("%d <-> ", current->data);
        current = current->prev;
    }
    printf("NULL\n");
}

Using Doubly Linked List

int main() {
    DNode *head = NULL;
    
    insertAtEndD(&head, 10);
    insertAtEndD(&head, 20);
    insertAtEndD(&head, 30);
    
    printForward(head);   // Forward:  10 <-> 20 <-> 30 <-> NULL
    printBackward(head);  // Backward: 30 <-> 20 <-> 10 <-> NULL
    
    insertAtBeginningD(&head, 5);
    printForward(head);   // Forward:  5 <-> 10 <-> 20 <-> 30 <-> NULL
    
    // Delete node with value 20
    DNode *node20 = head->next->next;  // Navigate to 20
    deleteDNode(&head, node20);
    printForward(head);   // Forward:  5 <-> 10 <-> 30 <-> NULL
    
    return 0;
}
Advantage: O(1) Deletion

In a doubly linked list, if you have a pointer to the node you want to delete, deletion is O(1) because you can directly access the previous node. In a singly linked list, you would need O(n) to find the previous node.

Practice Questions

Task: Write a function to count nodes traversing backward from the tail.

Show Solution
int countFromTail(DNode *head) {
    if (head == NULL) return 0;
    
    // Find tail
    DNode *tail = head;
    while (tail->next != NULL) {
        tail = tail->next;
    }
    
    // Count backward
    int count = 0;
    while (tail != NULL) {
        count++;
        tail = tail->prev;
    }
    
    return count;
}

Task: Write a function to insert a node before a given node in a doubly linked list.

Show Solution
void insertBefore(DNode **head, DNode *nextNode, int data) {
    if (nextNode == NULL) {
        printf("Next node cannot be NULL\n");
        return;
    }
    
    DNode *newNode = createDNode(data);
    
    newNode->prev = nextNode->prev;
    newNode->next = nextNode;
    
    // If inserting before head
    if (nextNode->prev == NULL) {
        *head = newNode;
    } else {
        nextNode->prev->next = newNode;
    }
    
    nextNode->prev = newNode;
}

// Usage:
// Before: 10 <-> 20 <-> 30
// insertBefore(&head, node20, 15)
// After:  10 <-> 15 <-> 20 <-> 30
04

Circular Linked Lists

Circular linked lists have no NULL at the end - the last node points back to the first node. This creates a continuous loop, useful for applications like round-robin scheduling, circular buffers, and multiplayer game turns.

Circular Singly Linked List

// Circular singly linked list
// Visual: head -> [10] -> [20] -> [30] -+
//          ^                            |
//          +----------------------------+

typedef struct CNode {
    int data;
    struct CNode *next;
} CNode;

// Create a new node
CNode *createCNode(int data) {
    CNode *newNode = (CNode *)malloc(sizeof(CNode));
    if (newNode == NULL) exit(1);
    newNode->data = data;
    newNode->next = newNode;  // Points to itself initially
    return newNode;
}

// Insert at the end of circular list
void insertAtEndC(CNode **head, int data) {
    CNode *newNode = createCNode(data);
    
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    
    // Find the last node (the one pointing to head)
    CNode *current = *head;
    while (current->next != *head) {
        current = current->next;
    }
    
    // Insert new node
    current->next = newNode;
    newNode->next = *head;
}

// Insert at the beginning
void insertAtBeginningC(CNode **head, int data) {
    CNode *newNode = createCNode(data);
    
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    
    // Find the last node
    CNode *current = *head;
    while (current->next != *head) {
        current = current->next;
    }
    
    newNode->next = *head;
    current->next = newNode;
    *head = newNode;
}

// Print circular list (be careful of infinite loop!)
void printCircular(CNode *head) {
    if (head == NULL) {
        printf("Empty list\n");
        return;
    }
    
    CNode *current = head;
    printf("Circular List: ");
    do {
        printf("%d -> ", current->data);
        current = current->next;
    } while (current != head);
    printf("(back to %d)\n", head->data);
}

// Delete a node from circular list
void deleteCNode(CNode **head, int key) {
    if (*head == NULL) return;
    
    CNode *current = *head;
    CNode *prev = NULL;
    
    // Find the node to delete
    while (current->data != key) {
        if (current->next == *head) {
            printf("Key %d not found\n", key);
            return;
        }
        prev = current;
        current = current->next;
    }
    
    // Only one node in list
    if (current->next == current) {
        *head = NULL;
        free(current);
        return;
    }
    
    // Deleting head
    if (current == *head) {
        // Find last node
        prev = *head;
        while (prev->next != *head) {
            prev = prev->next;
        }
        *head = current->next;
        prev->next = *head;
    } else {
        prev->next = current->next;
    }
    
    free(current);
}

Circular Doubly Linked List

// Circular doubly linked list
typedef struct CDNode {
    int data;
    struct CDNode *prev;
    struct CDNode *next;
} CDNode;

// Visual:
//    +------------------------------------------+
//    |                                          |
//    v                                          |
//   [prev|10|next] <-> [prev|20|next] <-> [prev|30|next]
//    ^                                          |
//    |                                          |
//    +------------------------------------------+

// Insert at end
void insertAtEndCD(CDNode **head, int data) {
    CDNode *newNode = (CDNode *)malloc(sizeof(CDNode));
    newNode->data = data;
    
    if (*head == NULL) {
        newNode->next = newNode;
        newNode->prev = newNode;
        *head = newNode;
        return;
    }
    
    CDNode *tail = (*head)->prev;  // Last node
    
    newNode->next = *head;
    newNode->prev = tail;
    tail->next = newNode;
    (*head)->prev = newNode;
}

Real-World Application: Round Robin

// Round-robin process scheduler simulation
typedef struct Process {
    int pid;
    int burstTime;
    struct Process *next;
} Process;

void roundRobinDemo() {
    Process *queue = NULL;
    
    // Add processes
    insertProcess(&queue, 1, 10);
    insertProcess(&queue, 2, 5);
    insertProcess(&queue, 3, 8);
    
    int timeQuantum = 3;
    Process *current = queue;
    
    printf("Round Robin Scheduling (quantum=%d):\n", timeQuantum);
    
    while (queue != NULL) {
        printf("Running Process %d (remaining: %d)\n", 
               current->pid, current->burstTime);
        
        if (current->burstTime <= timeQuantum) {
            // Process completes
            printf("Process %d completed!\n", current->pid);
            Process *toDelete = current;
            current = current->next;
            deleteProcess(&queue, toDelete->pid);
            if (queue == NULL) break;
        } else {
            // Process needs more time
            current->burstTime -= timeQuantum;
            current = current->next;
        }
    }
}
Practice Questions

Task: Write a function to check if a linked list is circular.

Show Solution
// Using Floyd's cycle detection (tortoise and hare)
int isCircular(Node *head) {
    if (head == NULL) return 0;
    
    Node *slow = head;
    Node *fast = head;
    
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
        
        if (slow == fast) {
            return 1;  // Cycle detected
        }
    }
    
    return 0;  // No cycle
}

// Simple check for circular list that loops back to head
int isCircularToHead(CNode *head) {
    if (head == NULL) return 0;
    
    CNode *current = head->next;
    while (current != NULL && current != head) {
        current = current->next;
    }
    
    return current == head;
}

Task: Split a circular linked list into two halves.

Show Solution
void splitCircularList(CNode *head, CNode **head1, CNode **head2) {
    if (head == NULL) {
        *head1 = NULL;
        *head2 = NULL;
        return;
    }
    
    CNode *slow = head;
    CNode *fast = head;
    
    // Find middle using slow/fast pointers
    while (fast->next != head && fast->next->next != head) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    // If even number of nodes, move fast one more
    if (fast->next->next == head) {
        fast = fast->next;
    }
    
    // Set up first half
    *head1 = head;
    
    // Set up second half
    *head2 = slow->next;
    
    // Make first half circular
    slow->next = *head1;
    
    // Make second half circular
    // Find end of second half (currently points to original head)
    CNode *temp = *head2;
    while (temp->next != head) {
        temp = temp->next;
    }
    temp->next = *head2;
}

// Example:
// Original: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> (back to 1)
// After split:
// head1: 1 -> 2 -> 3 -> (back to 1)
// head2: 4 -> 5 -> 6 -> (back to 4)
05

Advanced Operations

Beyond basic CRUD operations, linked lists support many powerful algorithms. These operations are common interview questions and demonstrate deep understanding of pointer manipulation.

Merge Two Sorted Lists

// Merge two sorted linked lists into one sorted list
Node *mergeSortedLists(Node *list1, Node *list2) {
    // Create dummy head to simplify code
    Node dummy;
    dummy.next = NULL;
    Node *tail = &dummy;
    
    while (list1 != NULL && list2 != NULL) {
        if (list1->data <= list2->data) {
            tail->next = list1;
            list1 = list1->next;
        } else {
            tail->next = list2;
            list2 = list2->next;
        }
        tail = tail->next;
    }
    
    // Attach remaining nodes
    if (list1 != NULL) {
        tail->next = list1;
    } else {
        tail->next = list2;
    }
    
    return dummy.next;
}

// Usage:
// list1: 1 -> 3 -> 5 -> NULL
// list2: 2 -> 4 -> 6 -> NULL
// merged: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> NULL

Detect and Remove Loop

// Detect if list has a loop (cycle)
Node *detectLoop(Node *head) {
    Node *slow = head;
    Node *fast = head;
    
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
        
        if (slow == fast) {
            return slow;  // Meeting point inside loop
        }
    }
    
    return NULL;  // No loop
}

// Remove loop from list
void removeLoop(Node *head) {
    Node *meetPoint = detectLoop(head);
    if (meetPoint == NULL) return;  // No loop
    
    // Find loop start
    Node *ptr1 = head;
    Node *ptr2 = meetPoint;
    
    while (ptr1->next != ptr2->next) {
        ptr1 = ptr1->next;
        ptr2 = ptr2->next;
    }
    
    // ptr2 is now at the node before loop start
    ptr2->next = NULL;  // Break the loop
}

Find Nth Node from End

// Find nth node from end in one pass
Node *findNthFromEnd(Node *head, int n) {
    Node *first = head;
    Node *second = head;
    
    // Move first pointer n nodes ahead
    for (int i = 0; i < n; i++) {
        if (first == NULL) {
            return NULL;  // List has fewer than n nodes
        }
        first = first->next;
    }
    
    // Move both pointers until first reaches end
    while (first != NULL) {
        first = first->next;
        second = second->next;
    }
    
    return second;  // second is now nth from end
}

// Example:
// List: 1 -> 2 -> 3 -> 4 -> 5 -> NULL
// findNthFromEnd(head, 2) returns node with value 4

Check if Palindrome

// Check if linked list is palindrome
int isPalindrome(Node *head) {
    if (head == NULL || head->next == NULL) {
        return 1;  // Empty or single node is palindrome
    }
    
    // Find middle
    Node *slow = head;
    Node *fast = head;
    while (fast->next != NULL && fast->next->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    // Reverse second half
    Node *secondHalf = reverseListReturn(slow->next);
    
    // Compare first and second half
    Node *p1 = head;
    Node *p2 = secondHalf;
    int result = 1;
    
    while (p2 != NULL) {
        if (p1->data != p2->data) {
            result = 0;
            break;
        }
        p1 = p1->next;
        p2 = p2->next;
    }
    
    // Restore original list (optional)
    slow->next = reverseListReturn(secondHalf);
    
    return result;
}

// Helper: Reverse and return new head
Node *reverseListReturn(Node *head) {
    Node *prev = NULL;
    Node *current = head;
    while (current != NULL) {
        Node *next = current->next;
        current->next = prev;
        prev = current;
        current = next;
    }
    return prev;
}

// Example:
// 1 -> 2 -> 3 -> 2 -> 1 is palindrome
// 1 -> 2 -> 3 -> 4 -> 5 is not palindrome

Time Complexity Summary

Operation Singly Doubly Notes
Insert at beginning O(1) O(1) Just update head pointer
Insert at end O(n) O(1)* *With tail pointer
Delete by pointer O(n) O(1) Singly needs to find prev
Search O(n) O(n) Linear traversal
Access by index O(n) O(n) Must traverse
Reverse O(n) O(n) Visit each node once
Practice Questions

Task: Remove duplicates from a sorted linked list.

Show Solution
void removeDuplicates(Node *head) {
    if (head == NULL) return;
    
    Node *current = head;
    
    while (current->next != NULL) {
        if (current->data == current->next->data) {
            // Remove duplicate
            Node *duplicate = current->next;
            current->next = current->next->next;
            free(duplicate);
        } else {
            current = current->next;
        }
    }
}

// Example:
// Before: 1 -> 1 -> 2 -> 3 -> 3 -> 3 -> NULL
// After:  1 -> 2 -> 3 -> NULL

Task: Add two numbers represented as linked lists (digits in reverse order).

Show Solution
// Numbers are stored in reverse order
// 342 is stored as 2 -> 4 -> 3
// 465 is stored as 5 -> 6 -> 4
// Sum: 807 stored as 7 -> 0 -> 8

Node *addTwoNumbers(Node *l1, Node *l2) {
    Node dummy;
    dummy.next = NULL;
    Node *tail = &dummy;
    int carry = 0;
    
    while (l1 != NULL || l2 != NULL || carry != 0) {
        int sum = carry;
        
        if (l1 != NULL) {
            sum += l1->data;
            l1 = l1->next;
        }
        
        if (l2 != NULL) {
            sum += l2->data;
            l2 = l2->next;
        }
        
        carry = sum / 10;
        
        Node *newNode = createNode(sum % 10);
        tail->next = newNode;
        tail = newNode;
    }
    
    return dummy.next;
}

// Example:
// l1: 2 -> 4 -> 3 (represents 342)
// l2: 5 -> 6 -> 4 (represents 465)
// Result: 7 -> 0 -> 8 (represents 807)

Key Takeaways

Dynamic Data Structure

Linked lists grow/shrink at runtime; no fixed size like arrays

Singly Linked

One pointer per node; forward traversal only; memory efficient

Doubly Linked

Two pointers; bidirectional traversal; O(1) deletion with pointer

Circular Linked

Last node points to first; useful for round-robin algorithms

O(1) Insert/Delete

Insertion and deletion are fast once position is known

Two-Pointer Technique

Slow/fast pointers solve many problems efficiently

Knowledge Check

Quick Quiz

Test what you have learned about linked lists in C

1 What is the time complexity to access the nth element in a linked list?
2 Why do we use a double pointer (Node **head) in insert/delete functions?
3 What is the main advantage of a doubly linked list over a singly linked list?
4 In a circular linked list, what does the last node point to?
5 What technique uses two pointers moving at different speeds to find the middle of a list?
6 What is the time complexity of inserting at the beginning of a singly linked list?
Answer all questions to check your score