Master Data Structures With Linked Lists Chains of Connected Nodes

|
0 Topics
0 Examples
0 Interview Questions

πŸ”— Learn Linked Lists

Master the node-based structure that powers stacks, queues, and dynamic memory.

0 / 10 topics completed
0%
01

Introduction to Linked Lists

A Linked List is a linear data structure where elements are not stored in contiguous memory. Instead, each element (called a node) holds its data and a pointer to the next node. This chain of pointers is what gives linked lists their power and flexibility.

Why Learn Linked Lists?

  • Dynamic size β€” grows and shrinks at runtime without reallocation
  • Efficient insert/delete β€” O(1) at the head, no shifting required
  • Foundation for stacks & queues β€” used in real OS schedulers
  • Interview staple β€” reversal, cycle detection, merge problems are everywhere
10
next β†’
β†’
20
next β†’
β†’
30
next β†’
β†’
NULL

Head β†’ 10 β†’ 20 β†’ 30 β†’ NULL

πŸ’‘

Unlike arrays, linked list elements are not contiguous in memory. Each node can live anywhere on the heap β€” the pointer is what connects them. This makes random access O(n) but insertion/deletion at a known node O(1).

02

Node Structure

Every linked list is built from nodes. A node is a simple struct containing the data field and a next pointer that references the following node. The last node's next is always NULL, marking the end of the list.

C β€” Node Definition
// A single node in the linked list
struct Node {
    int data;           // payload
    struct Node* next;  // pointer to the next node
};

// Helper: create a new node on the heap
struct Node* createNode(int value) {
    struct Node* node = (struct Node*)malloc(sizeof(struct Node));
    node->data = value;
    node->next = NULL;
    return node;
}
Memory Layout
Node at 0x100          Node at 0x200          Node at 0x300
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ data:10β”‚ next:  │──▢│ data:20β”‚ next:  │──▢│ data:30β”‚ next:  │──▢ NULL
β”‚        β”‚ 0x200  β”‚   β”‚        β”‚ 0x300  β”‚   β”‚        β”‚ NULL   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜
⚑

Always initialize next = NULL when creating a node. A dangling pointer (uninitialized next) is one of the most common causes of segmentation faults in linked list code.

03

Traversal

Traversal visits every node exactly once by following the chain of next pointers from head until we reach NULL. It is the foundation of printing, searching, and computing aggregates.

C
void printList(struct Node* head) {
    struct Node* current = head;   // start at head

    while (current != NULL) {
        printf("%d", current->data);
        if (current->next != NULL)
            printf(" β†’ ");
        current = current->next;   // move to next node
    }
    printf(" β†’ NULL\n");
}
Output
10 β†’ 20 β†’ 30 β†’ NULL
Practice: Traversal

Write a traversal that prints the sum and count of all nodes in the list.

C
void sumAndCount(struct Node* head) {
    int sum = 0, count = 0;
    struct Node* curr = head;

    while (curr != NULL) {
        sum   += curr->data;
        count++;
        curr = curr->next;
    }
    printf("Sum = %d, Count = %d\n", sum, count);
}
// For list 10β†’20β†’30: Sum = 60, Count = 3
04

Insertion

Inserting a node into a linked list is O(1) when you already have a pointer to the insertion point β€” no shifting required. There are three common cases: at the head, at the tail, and at a specific position.

Insert at Head β€” O(1)

Point the new node's next to the current head, then update head to the new node.

C β€” Insert at Head
struct Node* insertAtHead(struct Node* head, int value) {
    struct Node* newNode = createNode(value);
    newNode->next = head;  // new node points to old head
    return newNode;        // new node becomes the head
}

// Usage
head = insertAtHead(head, 5);
// Before: 10 β†’ 20 β†’ 30 β†’ NULL
// After :  5 β†’ 10 β†’ 20 β†’ 30 β†’ NULL

Insert at Tail β€” O(n)

Traverse to the last node (where next == NULL), then attach the new node.

C β€” Insert at Tail
struct Node* insertAtTail(struct Node* head, int value) {
    struct Node* newNode = createNode(value);

    if (head == NULL) return newNode;  // empty list

    struct Node* curr = head;
    while (curr->next != NULL)
        curr = curr->next;            // reach last node

    curr->next = newNode;             // attach
    return head;
}

// Before: 10 β†’ 20 β†’ 30 β†’ NULL
// After : 10 β†’ 20 β†’ 30 β†’ 40 β†’ NULL
⚠️

Always handle the empty list case (head == NULL) before traversing. Calling curr->next on a NULL pointer is immediate undefined behaviour.

Practice: Insertion

Insert the value 25 after the node that contains 20 in the list: 10 β†’ 20 β†’ 30 β†’ NULL

C
void insertAfter(struct Node* prevNode, int value) {
    if (prevNode == NULL) return;

    struct Node* newNode  = createNode(value);
    newNode->next         = prevNode->next;  // new β†’ 30
    prevNode->next        = newNode;         // 20  β†’ new
}

// Find the node with data == 20, then:
insertAfter(nodeWith20, 25);
// Result: 10 β†’ 20 β†’ 25 β†’ 30 β†’ NULL
05

Deletion

Deletion rewires the next pointer of the predecessor node to skip the target, then frees the target's memory. Handling head deletion separately is essential.

C β€” Delete by Value
struct Node* deleteNode(struct Node* head, int key) {
    // Case 1: delete the head node
    if (head != NULL && head->data == key) {
        struct Node* temp = head;
        head = head->next;
        free(temp);
        return head;
    }

    // Case 2: find predecessor of the target node
    struct Node* curr = head;
    while (curr != NULL && curr->next != NULL) {
        if (curr->next->data == key) {
            struct Node* temp  = curr->next;
            curr->next         = temp->next;  // bypass target
            free(temp);
            return head;
        }
        curr = curr->next;
    }
    return head;  // key not found
}

// Before: 10 β†’ 20 β†’ 30 β†’ NULL
// Delete 20
// After : 10 β†’ 30 β†’ NULL
⚑

Always free() the deleted node to avoid memory leaks. In C++, use delete. In Java/Python the garbage collector handles it, but you still need to unlink the node first.

Practice: Deletion

Delete the tail node from 5 β†’ 10 β†’ 15 β†’ 20 β†’ NULL and print the resulting list.

C
struct Node* deleteTail(struct Node* head) {
    if (head == NULL) return NULL;
    if (head->next == NULL) { free(head); return NULL; }

    struct Node* curr = head;
    while (curr->next->next != NULL)
        curr = curr->next;        // stop at second-to-last

    free(curr->next);
    curr->next = NULL;
    return head;
}
// Result: 5 β†’ 10 β†’ 15 β†’ NULL
06

Searching

Unlike arrays, linked lists have no random access. Searching always requires traversal from the head β€” giving it an O(n) worst-case time complexity regardless of the key's position.

C β€” Search by Value
// Returns the node if found, NULL otherwise
struct Node* search(struct Node* head, int key) {
    struct Node* curr = head;
    int position = 0;

    while (curr != NULL) {
        if (curr->data == key) {
            printf("Found %d at position %d\n", key, position);
            return curr;
        }
        curr = curr->next;
        position++;
    }
    printf("%d not found in list\n", key);
    return NULL;
}
Example
List    : 10 β†’ 20 β†’ 30 β†’ 40 β†’ NULL
Search  : 30
Output  : Found 30 at position 2
πŸ”

Search is O(n) time, O(1) space. There is no Binary Search on a singly linked list because you cannot jump to the middle in O(1). Use a skip list or convert to an array first if you need faster lookups.

07

Doubly Linked Lists

A Doubly Linked List extends each node with a prev pointer in addition to next. This allows traversal in both directions and O(1) deletion when given any node (no predecessor search needed).

NULL
⇄
10
prev / next
⇄
20
prev / next
⇄
30
prev / next
⇄
NULL
C β€” Doubly Node
struct DNode {
    int data;
    struct DNode* prev;
    struct DNode* next;
};

struct DNode* createDNode(int value) {
    struct DNode* node = (struct DNode*)malloc(sizeof(struct DNode));
    node->data = value;
    node->prev = NULL;
    node->next = NULL;
    return node;
}

// Insert at head of doubly linked list
struct DNode* insertDHead(struct DNode* head, int value) {
    struct DNode* newNode = createDNode(value);
    newNode->next = head;
    if (head != NULL) head->prev = newNode;
    return newNode;
}
🧩

Doubly linked lists use O(2n) pointer memory vs O(n) for singly linked. The trade-off: you gain O(1) backwards traversal and O(1) deletion at any known node β€” critical for LRU Cache implementations.

08

Circular Linked Lists

In a Circular Linked List, the last node's next pointer points back to the head instead of NULL. This is useful for round-robin schedulers, media playlists, and buffer implementations.

C β€” Circular Traversal
// Create circular list: 10 β†’ 20 β†’ 30 β†’ (back to 10)
void makeCircular(struct Node* head) {
    if (head == NULL) return;
    struct Node* curr = head;
    while (curr->next != NULL)
        curr = curr->next;
    curr->next = head;  // tail points to head
}

// Traverse a circular list (stop after one full loop)
void printCircular(struct Node* head) {
    if (head == NULL) return;
    struct Node* curr = head;
    do {
        printf("%d β†’ ", curr->data);
        curr = curr->next;
    } while (curr != head);    // stop when we're back at head
    printf("(head)\n");
}
// Output: 10 β†’ 20 β†’ 30 β†’ (head)
⚠️

A regular while (curr != NULL) loop will run forever on a circular list. Always use a do-while that checks curr != head, or track a visited flag to detect the cycle.

09

Linked Lists vs Arrays

Choosing between a linked list and an array depends on which operations dominate your use case. Neither is universally better.

Operation Array Linked List
Access by index O(1) βœ“ O(n) βœ—
Search O(n) O(n)
Insert at head O(n) βœ— O(1) βœ“
Insert at tail O(1) βœ“ O(n)*
Delete at head O(n) βœ— O(1) βœ“
Delete at tail O(1) βœ“ O(n) βœ—
Memory overhead Low βœ“ High (ptrs) βœ—
Cache performance Excellent βœ“Poor βœ—

* O(1) with a tail pointer maintained separately.

πŸ’‘

Use an array when you need fast random access or cache-friendly iteration. Use a linked list when you need frequent insertions and deletions at arbitrary positions without shifting.

10

Linked List Interview Tips

Linked list problems are a favourite in FAANG interviews. They test pointer manipulation, edge-case awareness, and in-place algorithmic thinking.

Must-Know Techniques

  • Two Pointers (Fast & Slow) β€” detect cycles, find midpoint
  • Reverse a Linked List β€” in-place iterative and recursive
  • Merge Two Sorted Lists β€” dummy head pattern
  • Find Nth from End β€” two pointers with N gap
  • Detect Cycle (Floyd's) β€” hare & tortoise algorithm
  • Always ask: singly or doubly linked? Head pointer only, or tail too?
  • Draw the list on paper before writing code β€” pointer bugs are visual.
  • Handle edge cases explicitly: empty list, single node, two nodes.
  • Use a dummy head node to avoid special-casing head deletion.
  • Don't lose the reference to a node before re-pointing its predecessor.
Classic Interview Problem

Reverse a Singly Linked List in-place (iterative).

Input : 1 β†’ 2 β†’ 3 β†’ 4 β†’ 5 β†’ NULL
Output: 5 β†’ 4 β†’ 3 β†’ 2 β†’ 1 β†’ NULL
C
struct Node* reverseList(struct Node* head) {
    struct Node* prev    = NULL;
    struct Node* current = head;
    struct Node* next    = NULL;

    while (current != NULL) {
        next          = current->next;  // save next
        current->next = prev;           // reverse link
        prev          = current;        // advance prev
        current       = next;           // advance current
    }
    return prev;  // prev is now the new head
}

// Step-by-step trace:
// NULL ← 1    2 β†’ 3 β†’ 4 β†’ 5
// NULL ← 1 ← 2    3 β†’ 4 β†’ 5
// NULL ← 1 ← 2 ← 3    4 β†’ 5
// NULL ← 1 ← 2 ← 3 ← 4    5
// NULL ← 1 ← 2 ← 3 ← 4 ← 5  ← new head