Data Structures & Algorithms: Industry Edition
Unit 2: Sorting, Searching & Linked Lists
Singly linked lists, doubly linked lists, header linked lists — with real examples from Spotify India, Google Docs, and the Linux Kernel.
🏢 Real Projects | 💻 2 Lab Programs (Python + C) | 📝 25 MCQs | 🎯 3 Interview Questions
Industry Hook — The Real-World Problem First
🎵 The Spotify India Problem: 100 Million Songs, One Playlist at a Time
Spotify India has over 80 million users and a library of 100+ million tracks. When you create a playlist and hit "Add to Queue," "Remove Song," or "Shuffle," something fascinating happens behind the scenes.
If playlists were stored as arrays, inserting a song in the middle of a 500-song playlist would require shifting up to 499 elements — O(n) per operation. Do this 10 times per second across 80 million users, and you need 400 billion element-shifts per second. No server farm on Earth handles that.
Instead, Spotify's playlist engine uses a structure where:
- Adding a song at any position takes O(1) — just rewire two pointers
- Removing a song takes O(1) — unlink the node, done
- Moving songs around (drag-and-drop reorder) is O(1) per move
- Playing next/previous is O(1) — follow the forward or backward pointer
The same structure powers Google Docs (undo/redo history), the Linux kernel (process scheduling), and every browser's back/forward button.
This is exactly the problem linked lists solve. Let's understand how.
Concept Explanation — Theory, Earned
2.1 The Array Problem: Why We Need Linked Lists
In Unit 1, we learned that arrays give us O(1) random access. But arrays have a fatal flaw:
| Operation | Array | Linked List | Winner |
|---|---|---|---|
| Access by index | O(1) ✅ | O(n) ❌ | Array |
| Insert at beginning | O(n) ❌ | O(1) ✅ | Linked List |
| Insert at middle | O(n) ❌ | O(1)* ✅ | Linked List |
| Delete any element | O(n) ❌ | O(1)* ✅ | Linked List |
| Memory allocation | Contiguous (rigid) | Scattered (flexible) | Linked List |
| Memory overhead | None | Extra pointer per node | Array |
*O(1) once you have a reference to the position. Finding the position is O(n).
2.2 Singly Linked List
Layer 1 — Intuition
Imagine a treasure hunt where each clue card has two things: the treasure at that location, and directions to the next clue. You must follow clues in order — you can't jump to clue #7 directly. But adding a new clue in the middle is easy: just change the "next clue" direction on one card.
Layer 2 — Visual: Memory Representation
Memory Layout
head
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ data: "Tum │───▶│ data: "Hi" │───▶│ data: "Se" │───▶│ data: "Pyaar"│───▶ NULL
│ next: 0x2000 │ │ next: 0x3000 │ │ next: 0x5000 │ │ next: NULL │
│ addr: 0x1000 │ │ addr: 0x2000 │ │ addr: 0x3000 │ │ addr: 0x5000 │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
Key insight: Nodes can be ANYWHERE in memory (0x1000, 0x2000, 0x3000, 0x5000).
They are NOT contiguous like arrays. The 'next' pointer links them.
Insertion at Beginning — O(1)
Visual
Before: head → [10] → [20] → [30] → NULL
Step 1: Create new node [5]
Step 2: new_node.next = head (point to old first node)
Step 3: head = new_node (update head)
After: head → [5] → [10] → [20] → [30] → NULL
Only 2 pointer changes! No shifting!
Deletion of a Node — O(1) when you have the previous node
Visual
Before: head → [10] → [20] → [30] → NULL
Delete node with value 20:
Step 1: Find node before 20 (node with 10) ← This is O(n)
Step 2: prev.next = target.next ← This is O(1)
Step 3: Free/delete the target node
After: head → [10] → [30] → NULL
The node [20] is "unlinked" — it still exists in memory but
nothing points to it. In C, you must free() it. In Python,
garbage collector handles it.
Layer 3 — Complexity Table
| Operation | Best | Average | Worst | Space |
|---|---|---|---|---|
| Access by index | O(1) | O(n) | O(n) | O(1) |
| Search by value | O(1) | O(n) | O(n) | O(1) |
| Insert at head | O(1) | O(1) | O(1) | O(1) |
| Insert at tail | O(n) | O(n) | O(n) | O(1) |
| Insert after a given node | O(1) | O(1) | O(1) | O(1) |
| Delete head | O(1) | O(1) | O(1) | O(1) |
| Delete by value (search + delete) | O(1) | O(n) | O(n) | O(1) |
| Traversal | O(n) | O(n) | O(n) | O(1) |
The Linux kernel uses linked lists so heavily that it has its own custom implementation: struct list_head. Every process in Linux is a node in a doubly linked list. The kernel's task_struct uses linked lists for the process list, run queue, wait queue, children list, and sibling list — all simultaneously!
2.3 Header Linked Lists
A header linked list has a special header node at the beginning that doesn't store actual data. It stores metadata (like count, or a sentinel value) and simplifies insertion/deletion logic because you never have to handle the "empty list" or "insert at head" as special cases.
Grounded Header Linked List
Visual
header
│
▼
┌────────────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ count: 3 │───▶│ 10 │───▶│ 20 │───▶│ 30 │───▶ NULL ← Grounded (ends at NULL)
│ (sentinel) │ │ │ │ │ │ │
└────────────┘ └─────┘ └─────┘ └─────┘
Advantage: Inserting before the "first real node" is just inserting
after the header — no special case needed!
Circular Header Linked List
Visual
header
│
▼
┌────────────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ count: 3 │───▶│ 10 │───▶│ 20 │───▶│ 30 │──┐
│ (sentinel) │ │ │ │ │ │ │ │
└────────────┘ └─────┘ └─────┘ └─────┘ │
▲ │
└──────────────────────────────────────────┘ ← Last node points BACK to header
Traversal ends when we reach the header node again.
Used in: Circular buffers, round-robin scheduling, game turn management.
Header nodes eliminate edge cases. Without a header, every insert/delete function needs if (head == NULL) or if (target == head) checks. With a header, the first real element is always header->next, and you always insert/delete "after some node" — uniform logic, fewer bugs.
2.4 Two-Way (Doubly) Linked List
Layer 1 — Intuition
A singly linked list is like a one-way street — you can only go forward. A doubly linked list is a two-way street — you can go forward AND backward. This is how your browser's Back/Forward buttons work: each page knows both the previous page and the next page.
Layer 2 — Visual
Memory Layout
head tail
│ │
▼ ▼
NULL ◀── ┌──────┐ ◀──▶ ┌──────┐ ◀──▶ ┌──────┐ ◀──▶ ┌──────┐ ──▶ NULL
│ 10 │ │ 20 │ │ 30 │ │ 40 │
│ prev │ │ prev │ │ prev │ │ prev │
│ next │ │ next │ │ next │ │ next │
└──────┘ └──────┘ └──────┘ └──────┘
Each node has THREE fields:
1. data — the actual value
2. prev — pointer to previous node (NULL for head)
3. next — pointer to next node (NULL for tail)
DLL Insertion After a Given Node — O(1)
Visual
Insert 25 after node [20]:
Before: ... ◀──▶ [20] ◀──▶ [30] ◀──▶ ...
Step 1: Create [25]
Step 2: [25].next = [20].next → [25] points forward to [30]
Step 3: [25].prev = [20] → [25] points backward to [20]
Step 4: [30].prev = [25] → [30]'s back pointer updated
Step 5: [20].next = [25] → [20]'s forward pointer updated
After: ... ◀──▶ [20] ◀──▶ [25] ◀──▶ [30] ◀──▶ ...
4 pointer changes. Constant time. No shifting.
Layer 3 — DLL Complexity Table
| Operation | Singly LL | Doubly LL | Why DLL is better |
|---|---|---|---|
| Insert at head | O(1) | O(1) | Same |
| Insert at tail | O(n)* | O(1)** | DLL with tail pointer |
| Delete given node | O(n)† | O(1) | DLL has prev pointer — no need to find predecessor |
| Traverse backward | Impossible | O(n) | prev pointer enables reverse traversal |
| Memory per node | data + 1 ptr | data + 2 ptrs | SLL uses less memory |
* O(1) if tail pointer maintained. ** Assumes tail pointer. † Must traverse to find predecessor.
Google Docs uses a doubly linked list for its undo/redo stack. Every edit creates a node with prev pointing to the state before the edit and next pointing to the state after. "Undo" follows prev; "Redo" follows next. Why is a DLL better than two separate stacks for this? (Hint: think about what happens when you undo 5 times, then make a new edit.)
Lab Program Implementations
LAB PROGRAM 1: Searching, Insertion, Deletion in Singly Linked List
Python Implementation — Complete Singly Linked List
Python
class Node:
"""A single node in the linked list."""
def __init__(self, data):
self.data = data
self.next = None
class SinglyLinkedList:
"""Complete singly linked list with all operations.
Industry parallel: Spotify's playlist is essentially this —
each song node points to the next song in the queue.
"""
def __init__(self):
self.head = None
self.size = 0
# ─── TRAVERSAL ───
def display(self):
"""Traverse and print all elements. O(n)."""
current = self.head
elements = []
while current:
elements.append(str(current.data))
current = current.next
print(" → ".join(elements) + " → NULL")
# ─── SEARCH ───
def search(self, key):
"""Search for a value. Returns position (0-indexed) or -1.
Time: O(n) — must traverse sequentially, no random access."""
current = self.head
position = 0
while current:
if current.data == key:
return position
current = current.next
position += 1
return -1 # Not found
# ─── INSERT AT BEGINNING ── O(1) ───
def insert_at_head(self, data):
"""Insert new node at the beginning. O(1).
Like adding a song to the top of your Spotify queue."""
new_node = Node(data)
new_node.next = self.head # Point to old head
self.head = new_node # Update head to new node
self.size += 1
# ─── INSERT AT END ── O(n) ───
def insert_at_tail(self, data):
"""Insert new node at the end. O(n) — must traverse to last node."""
new_node = Node(data)
if not self.head: # Empty list
self.head = new_node
self.size += 1
return
current = self.head
while current.next: # Traverse to last node
current = current.next
current.next = new_node # Link last node to new node
self.size += 1
# ─── INSERT AFTER A VALUE ── O(n) search + O(1) insert ───
def insert_after(self, target_value, data):
"""Insert after the first node containing target_value."""
current = self.head
while current:
if current.data == target_value:
new_node = Node(data)
new_node.next = current.next # New points to current's next
current.next = new_node # Current points to new
self.size += 1
return True
current = current.next
print(f"Value {target_value} not found.")
return False
# ─── DELETE AT HEAD ── O(1) ───
def delete_at_head(self):
"""Remove the first node. O(1)."""
if not self.head:
print("List is empty.")
return None
removed_data = self.head.data
self.head = self.head.next # Move head forward
self.size -= 1
return removed_data
# ─── DELETE BY VALUE ── O(n) ───
def delete_by_value(self, key):
"""Delete first node with given value. O(n).
Like removing a specific song from your playlist."""
if not self.head:
print("List is empty.")
return False
# Special case: deleting the head
if self.head.data == key:
self.head = self.head.next
self.size -= 1
return True
# General case: find the node BEFORE the target
current = self.head
while current.next:
if current.next.data == key:
current.next = current.next.next # Skip over target
self.size -= 1
return True
current = current.next
print(f"Value {key} not found.")
return False
# === Driver Code ===
playlist = SinglyLinkedList()
# Build playlist
playlist.insert_at_tail("Shape of You")
playlist.insert_at_tail("Blinding Lights")
playlist.insert_at_tail("Tum Hi Ho")
playlist.insert_at_head("Kesariya") # Add to top
playlist.insert_after("Shape of You", "Levitating")
print("Playlist:")
playlist.display()
print(f"\nSearch 'Tum Hi Ho': position {playlist.search('Tum Hi Ho')}")
print(f"Search 'Despacito': position {playlist.search('Despacito')}")
playlist.delete_by_value("Blinding Lights")
print("\nAfter removing 'Blinding Lights':")
playlist.display()
print(f"Size: {playlist.size}")
C Implementation — Complete Singly Linked List
C
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
struct Node* next;
};
/* Create a new node */
struct Node* createNode(int data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
if (!newNode) { printf("Memory allocation failed!\n"); exit(1); }
newNode->data = data;
newNode->next = NULL;
return newNode;
}
/* Display the list */
void display(struct Node* head) {
struct Node* curr = head;
while (curr) { printf("%d -> ", curr->data); curr = curr->next; }
printf("NULL\n");
}
/* Search for a value — returns position or -1 */
int search(struct Node* head, int key) {
struct Node* curr = head;
int pos = 0;
while (curr) {
if (curr->data == key) return pos;
curr = curr->next;
pos++;
}
return -1;
}
/* Insert at head — O(1) */
void insertAtHead(struct Node** head, int data) {
struct Node* newNode = createNode(data);
newNode->next = *head; // Point to old head
*head = newNode; // Update head pointer
}
/* Insert at tail — O(n) */
void insertAtTail(struct Node** head, int data) {
struct Node* newNode = createNode(data);
if (!*head) { *head = newNode; return; }
struct Node* curr = *head;
while (curr->next) curr = curr->next;
curr->next = newNode;
}
/* Delete by value — O(n) */
int deleteByValue(struct Node** head, int key) {
if (!*head) return 0;
// Deleting head
if ((*head)->data == key) {
struct Node* temp = *head;
*head = (*head)->next;
free(temp); // CRITICAL in C — prevent memory leak!
return 1;
}
struct Node* curr = *head;
while (curr->next) {
if (curr->next->data == key) {
struct Node* temp = curr->next;
curr->next = temp->next; // Skip over target
free(temp);
return 1;
}
curr = curr->next;
}
return 0;
}
int main() {
struct Node* head = NULL;
insertAtTail(&head, 10);
insertAtTail(&head, 20);
insertAtTail(&head, 30);
insertAtHead(&head, 5);
printf("List: "); display(head);
printf("Search 20: pos %d\n", search(head, 20));
deleteByValue(&head, 20);
printf("After delete 20: "); display(head);
return 0;
}
- Memory leak in C: Deleting a node without calling
free()— the node stays in memory forever. In a server running for months, this eventually crashes the system. - Losing the list: Setting
head = head->nextwithout savingheadfirst — the old head node is lost and can never be freed. - Not handling empty list: Calling
head->datawhenhead == NULLcauses a segfault. Always checkif (!head)first.
This program implements the singly linked list operations from Section 2.2. The insert_at_head() function demonstrates O(1) insertion — notice it only changes 2 pointers regardless of list size. Compare this to array insertion (Lab Program 1 in Unit 1) which shifts O(n) elements.
LAB PROGRAM 2: Searching, Insertion, Deletion in Doubly Linked List
Python Implementation — Complete Doubly Linked List
Python
class DNode:
"""Node for doubly linked list — has both prev and next."""
def __init__(self, data):
self.data = data
self.prev = None
self.next = None
class DoublyLinkedList:
"""Complete DLL — the backbone of browser history and undo systems.
Key advantage over SLL: Given a node reference, you can delete it
in O(1) because you have the prev pointer. In SLL, you'd need O(n)
to find the predecessor.
"""
def __init__(self):
self.head = None
self.tail = None # Maintaining tail for O(1) tail operations
self.size = 0
# ─── DISPLAY FORWARD ───
def display_forward(self):
curr = self.head
parts = []
while curr:
parts.append(str(curr.data))
curr = curr.next
print("NULL ◀ " + " ◀▶ ".join(parts) + " ▶ NULL")
# ─── DISPLAY BACKWARD (impossible with SLL!) ───
def display_backward(self):
curr = self.tail
parts = []
while curr:
parts.append(str(curr.data))
curr = curr.prev
print("NULL ◀ " + " ◀▶ ".join(parts) + " ▶ NULL")
# ─── SEARCH ── O(n) ───
def search(self, key):
curr = self.head
pos = 0
while curr:
if curr.data == key:
return pos
curr = curr.next
pos += 1
return -1
# ─── INSERT AT HEAD ── O(1) ───
def insert_at_head(self, data):
new_node = DNode(data)
if not self.head: # Empty list
self.head = self.tail = new_node
else:
new_node.next = self.head # New points forward to old head
self.head.prev = new_node # Old head points back to new
self.head = new_node # Update head
self.size += 1
# ─── INSERT AT TAIL ── O(1) with tail pointer ───
def insert_at_tail(self, data):
new_node = DNode(data)
if not self.tail: # Empty list
self.head = self.tail = new_node
else:
new_node.prev = self.tail # New points back to old tail
self.tail.next = new_node # Old tail points forward to new
self.tail = new_node # Update tail
self.size += 1
# ─── INSERT AFTER A VALUE ── O(n) search + O(1) insert ───
def insert_after(self, target, data):
curr = self.head
while curr:
if curr.data == target:
new_node = DNode(data)
new_node.prev = curr # Step 1: new.prev → curr
new_node.next = curr.next # Step 2: new.next → curr.next
if curr.next: # Step 3: if next exists
curr.next.prev = new_node # next.prev → new
else:
self.tail = new_node # new is the new tail
curr.next = new_node # Step 4: curr.next → new
self.size += 1
return True
curr = curr.next
print(f"'{target}' not found.")
return False
# ─── DELETE A NODE ── O(1) given the node ───
def _delete_node(self, node):
"""Internal: delete a specific node. O(1).
This is the DLL superpower — SLL needs O(n) for this."""
if node.prev:
node.prev.next = node.next # Prev skips over node
else:
self.head = node.next # Deleting head
if node.next:
node.next.prev = node.prev # Next's prev skips over node
else:
self.tail = node.prev # Deleting tail
self.size -= 1
# ─── DELETE BY VALUE ── O(n) search + O(1) delete ───
def delete_by_value(self, key):
curr = self.head
while curr:
if curr.data == key:
self._delete_node(curr)
return True
curr = curr.next
print(f"'{key}' not found.")
return False
# ─── DELETE AT HEAD ── O(1) ───
def delete_at_head(self):
if not self.head: return None
data = self.head.data
self._delete_node(self.head)
return data
# ─── DELETE AT TAIL ── O(1) with tail pointer ───
def delete_at_tail(self):
if not self.tail: return None
data = self.tail.data
self._delete_node(self.tail)
return data
# === Driver Code: Browser History ===
print("=== Browser History (DLL) ===")
history = DoublyLinkedList()
history.insert_at_tail("google.com")
history.insert_at_tail("github.com")
history.insert_at_tail("stackoverflow.com")
history.insert_at_tail("leetcode.com")
print("Forward: ")
history.display_forward()
print("Backward (hitting Back button): ")
history.display_backward()
print(f"\nSearch 'github.com': position {history.search('github.com')}")
history.insert_after("github.com", "docs.github.com")
print("\nAfter inserting 'docs.github.com' after 'github.com':")
history.display_forward()
history.delete_by_value("stackoverflow.com")
print("\nAfter deleting 'stackoverflow.com':")
history.display_forward()
print(f"Size: {history.size}")
C Implementation — Complete Doubly Linked List
C
#include <stdio.h>
#include <stdlib.h>
struct DNode {
int data;
struct DNode* prev;
struct DNode* next;
};
struct DNode* createDNode(int data) {
struct DNode* n = (struct DNode*)malloc(sizeof(struct DNode));
n->data = data; n->prev = NULL; n->next = NULL;
return n;
}
void displayForward(struct DNode* head) {
printf("NULL <-> ");
while(head) { printf("%d <-> ", head->data); head = head->next; }
printf("NULL\n");
}
void insertHead(struct DNode** head, int data) {
struct DNode* n = createDNode(data);
n->next = *head;
if(*head) (*head)->prev = n;
*head = n;
}
void insertTail(struct DNode** head, int data) {
struct DNode* n = createDNode(data);
if(!*head) { *head = n; return; }
struct DNode* curr = *head;
while(curr->next) curr = curr->next;
curr->next = n;
n->prev = curr;
}
int deleteByValue(struct DNode** head, int key) {
struct DNode* curr = *head;
while(curr) {
if(curr->data == key) {
if(curr->prev) curr->prev->next = curr->next;
else *head = curr->next; // Deleting head
if(curr->next) curr->next->prev = curr->prev;
free(curr);
return 1;
}
curr = curr->next;
}
return 0;
}
int search(struct DNode* head, int key) {
int pos = 0;
while(head) {
if(head->data == key) return pos;
head = head->next; pos++;
}
return -1;
}
int main() {
struct DNode* head = NULL;
insertTail(&head, 10);
insertTail(&head, 20);
insertTail(&head, 30);
insertHead(&head, 5);
displayForward(head);
printf("Search 20: pos %d\n", search(head, 20));
deleteByValue(&head, 20);
printf("After delete 20: "); displayForward(head);
return 0;
}
Forgetting to update BOTH prev and next pointers when inserting into a DLL. If you set new_node.next = curr.next but forget curr.next.prev = new_node, the backward traversal is broken. The list "looks right" going forward but crashes going backward. This bug is subtle and hard to catch without bidirectional testing.
This program implements the doubly linked list from Section 2.4. The _delete_node() method is the DLL superpower: given a node reference, deletion is O(1) because the prev pointer gives direct access to the predecessor. In an SLL, you'd need O(n) to find it. This is exactly why LRU caches (used at every tech company) combine a hash map + DLL.
Industry Case Studies
Case Study 1: Spotify India — Playlist Engine
Case Study 2: Google — Chrome Browser Tab Management
Case Study 3: Linux Kernel — Process Scheduler
struct list_head). Every task_struct (process descriptor) is a node. The kernel's custom linked list is embedded INSIDE each struct — no separate node allocation needed.Chapter Project — "Build It Yourself"
🎵 Music Playlist Manager with Shuffle, Repeat & History
Pitch: Build a Spotify-like playlist manager using a doubly linked list — with play next/previous, shuffle, repeat modes, and recently played history.
Problem Statement
You are a backend intern at a music streaming startup. Your task is to build the core playlist engine that supports: adding songs to the queue, removing songs, playing next/previous, shuffling the queue, and maintaining a "Recently Played" history that you can browse backward.
Step-by-Step Build Guide
- Step 1 — Song Queue (DLL): Represent the playlist as a DLL. Each node stores song name, artist, duration. Use Lab Program 2's DLL as the foundation.
- Step 2 — Play Controls: Maintain a
current_songpointer. "Next" moves tocurrent.next. "Previous" moves tocurrent.prev. Both are O(1). - Step 3 — Add/Remove Songs: Use Lab Program 2's insert/delete operations. "Add Next" inserts after
current_song. "Remove" unlinks the current node and advances to next. - Step 4 — Shuffle: Collect all node references into an array, Fisher-Yates shuffle the array, then re-link nodes in the new order.
- Step 5 — Repeat Mode: Make the DLL circular — last node's
nextpoints to head, head'sprevpoints to last. "Next" after the last song loops back to the first. - Step 6 — History (SLL): Maintain a separate singly linked list that records every played song. "Recently Played" traverses this list. Uses Lab Program 1's SLL.
Extension Challenges
- ⭐ Easy: Display the current queue with ▶ marker on the current song
- ⭐⭐ Medium: Implement "Repeat One" mode (current song loops) vs "Repeat All" (circular DLL)
- ⭐⭐⭐ Hard: Implement an LRU-style "Smart Shuffle" that avoids replaying recently heard songs
Interview Corner — "Crack It at Google/Amazon"
Q1: Reverse a Singly Linked List ⭐⭐ [Product Company Interview]
Problem: Reverse a singly linked list in-place. Return the new head.
Optimal Approach — O(n) time, O(1) space
Python
def reverse_list(head):
"""Reverse by redirecting all next pointers.
Uses three pointers: prev, current, next_node."""
prev = None
current = head
while current:
next_node = current.next # Save next (would be lost)
current.next = prev # Reverse the pointer
prev = current # Move prev forward
current = next_node # Move current forward
return prev # prev is the new head
List: 1 → 2 → 3 → NULL Step 1: prev=NULL, curr=1, next=2 → 1→NULL prev=1, curr=2 Step 2: prev=1, curr=2, next=3 → 2→1→NULL prev=2, curr=3 Step 3: prev=2, curr=3, next=NULL → 3→2→1→NULL prev=3, curr=NULL Return prev (node 3) = new head. Result: 3 → 2 → 1 → NULL ✓
Follow-ups: (1) Reverse in groups of K. (2) Reverse only the sublist from position m to n.
Q2: Detect Cycle in a Linked List (Floyd's Algorithm) ⭐⭐ [GATE + Interview]
Problem: Determine if a linked list has a cycle (a node's next points to an earlier node).
Python
def has_cycle(head):
"""Floyd's Tortoise and Hare algorithm.
Two pointers: slow (1 step), fast (2 steps).
If they meet → cycle exists. If fast reaches NULL → no cycle.
"""
slow = fast = head
while fast and fast.next:
slow = slow.next # Move 1 step
fast = fast.next.next # Move 2 steps
if slow == fast:
return True # They met — cycle!
return False # Fast reached end — no cycle
Time: O(n), Space: O(1). No hash set needed!
Follow-ups: (1) Find where the cycle starts. (2) Find the length of the cycle.
Q3: Merge Two Sorted Linked Lists ⭐⭐ [University + GATE]
Problem: Given two sorted linked lists, merge them into one sorted list.
Python
def merge_sorted(list1, list2):
"""Merge two sorted lists. Like merge step of merge sort.
Uses a dummy head to simplify edge cases."""
dummy = Node(0) # Dummy node avoids special-case for empty result
tail = dummy
while list1 and list2:
if list1.data <= list2.data:
tail.next = list1 # Append smaller node
list1 = list1.next
else:
tail.next = list2
list2 = list2.next
tail = tail.next
# Append remaining nodes (one list might not be empty)
tail.next = list1 if list1 else list2
return dummy.next # Skip dummy, return real head
Time: O(n + m), Space: O(1). The dummy node trick eliminates all edge-case checks — a pro technique used in production code.
MCQ Assessment Bank — 25 Questions
Level 1 — Recall ⭐ (5 Questions)
Each node in a singly linked list contains:
- Only data
- Data and a pointer to the next node
- Data and pointers to both previous and next nodes
- An index and data
📖 SLL node has data + one pointer (next). DLL has data + two pointers.
❌ (a) No pointer = can't link. (c) That's a DLL node. (d) No index in linked lists.
Time complexity of inserting at the head of a singly linked list:
- O(n)
- O(log n)
- O(1)
- O(n²)
📖 Only 2 operations: new_node.next = head; head = new_node. Constant time.
❌ (a) That's insertion at the tail or middle.
In a circular linked list, the last node points to:
- NULL
- The first node (head)
- Itself
- The second node
📖 This is what makes it "circular" — there's no NULL terminator.
❌ (a) That's a regular (grounded) list. (c)(d) Not standard circular.
A doubly linked list node contains how many pointers?
- 0
- 1
- 2
- 3
📖 prev and next — enabling bidirectional traversal.
❌ Others are incorrect counts.
The header node in a header linked list stores:
- The first actual data element
- Metadata (like count) or acts as a sentinel
- The last element
- Nothing — it's always empty memory
📖 The header simplifies insert/delete logic by eliminating edge cases for empty list or head operations.
❌ (a) It specifically does NOT store real data.
Level 2 — Conceptual Understanding ⭐⭐ (6 Questions)
Why can't you do binary search on a linked list efficiently?
- Linked lists can't store sorted data
- No random access — reaching the middle element requires O(n) traversal
- Binary search only works on integers
- Linked lists are always unsorted
📖 Binary search needs O(1) access to the middle element. In a linked list, finding the middle requires traversing n/2 nodes each time, making binary search O(n log n) instead of O(log n).
❌ (a)(d) Linked lists CAN store sorted data. (c) Binary search works on any comparable type.
Deleting a node in a DLL when you have a reference to that node is O(1), but in an SLL it's O(n). Why?
- DLL nodes are stored in contiguous memory
- DLL has a prev pointer — no need to traverse to find the predecessor
- SLL doesn't support deletion
- DLL uses less memory per node
📖 To delete node X, you need X's predecessor to update its next pointer. In DLL: X.prev gives it in O(1). In SLL: you must traverse from head to find it — O(n).
❌ (a) Memory layout is scattered. (c) SLL supports deletion. (d) DLL uses MORE memory.
Header linked lists simplify code because:
- They store data more efficiently
- They eliminate special-case handling for empty list and head operations
- They use less memory
- They enable O(1) access by index
📖 Without a header, every function needs
if (head == NULL) and if (target == head) checks. With a header, the first real node is always header.next — uniform logic.❌ (a)(c) Headers add overhead. (d) Still O(n) access.
A singly linked list with a tail pointer gives O(1) insertion at both head and tail. Therefore, it's always better than a doubly linked list.
- True — same performance with less memory
- False — SLL with tail pointer still can't delete the last node in O(1)
- True — DLL is unnecessarily complex
- False — SLL can't traverse backward
📖 Even with a tail pointer, deleting the LAST node requires finding the second-to-last node (O(n) traversal). DLL's prev pointer makes this O(1). Also, (d) is correct but (b) is the stronger reason.
❌ (a)(c) Missing critical DLL advantages.
Memory allocation for linked list nodes happens:
- At compile time, contiguously
- At runtime, dynamically, potentially non-contiguous
- In the stack segment
- In read-only memory
📖 malloc()/new allocates from the heap at runtime. Nodes can be anywhere in memory — the next/prev pointers link them logically.
❌ (a) That's arrays. (c) Local variables go on stack, not malloc'd nodes. (d) No.
What happens if you free() a node in C but other nodes still point to it?
- Nothing — it's safe
- The other pointers become dangling pointers — accessing them is undefined behavior
- The other pointers automatically become NULL
- The freed memory is still accessible
📖 free() releases memory but doesn't update other references. Those pointers now point to deallocated memory — using them can read garbage, crash, or cause security vulnerabilities.
❌ (a)(d) Dangerous misconception. (c) C doesn't auto-nullify.
Level 3 — Application ⭐⭐ (8 Questions)
Spotify needs to support "Play Previous Song" in O(1). Which structure is best?
- Array
- Singly linked list
- Doubly linked list
- Stack
📖 "Play Previous" = follow the prev pointer — O(1). SLL can only go forward. Array could work with an index but insert/delete is O(n).
❌ (a) O(1) access but O(n) insert/delete. (b) No backward traversal. (d) Stack is LIFO, not bidirectional.
In Floyd's cycle detection, the slow pointer moves 1 step and fast moves 2 steps. If they meet, it proves:
- The list is sorted
- A cycle exists
- The list has duplicate values
- The list is empty
📖 If no cycle, fast reaches NULL. If cycle exists, fast eventually "laps" slow inside the cycle — like two runners on a circular track.
❌ Other options are unrelated to Floyd's algorithm.
To reverse a singly linked list, you need how many pointers?
- 1 (current)
- 2 (current, next)
- 3 (prev, current, next_node)
- 4
📖 prev tracks the reversed part, current is the node being processed, next_node saves the next reference before we overwrite current.next.
❌ (a)(b) Can't reverse without all three. (d) Unnecessary.
Inserting at the end of a singly linked list is O(1) because you just create a node and set the last node's next to it.
- True — one pointer change
- False — finding the last node takes O(n) traversal first
- True — if you use a stack
- False — you can't insert at the end of an SLL
📖 The trap: creating the node is O(1), but FINDING the last node requires traversing the entire list — O(n). Unless you maintain a tail pointer, tail insertion is O(n).
❌ (a) Forgets traversal cost. (d) You can, it's just O(n).
An LRU (Least Recently Used) cache, used at every tech company, typically combines:
- Array + Stack
- Hash Map + Doubly Linked List
- Binary Tree + Queue
- Array + Binary Search
📖 Hash map gives O(1) lookup. DLL gives O(1) move-to-front and remove-from-back. Together: O(1) get, put, and eviction. This is used by every CDN, browser cache, and database buffer.
❌ Other combinations can't achieve O(1) for all operations.
What is the output? insertHead(10), insertHead(20), insertTail(30), deleteHead(), display()
- 10 → 30 → NULL
- 20 → 10 → 30 → NULL
- 10 → 30 → NULL
- 30 → 10 → NULL
📖 After insertHead(10): 10. After insertHead(20): 20→10. After insertTail(30): 20→10→30. After deleteHead(): 10→30.
❌ (b) That's before deleteHead. (d) Wrong order.
A circular singly linked list is used for:
- Binary search
- Round-robin CPU scheduling
- Sorting algorithms
- Stack implementation
📖 Round-robin needs to cycle through processes endlessly. A circular list naturally wraps around — after the last process, you're back at the first. No special "end of list" logic needed.
❌ (a)(c) Not related. (d) Stack uses LIFO, not circular.
Google Docs uses a DLL for undo/redo instead of two stacks because:
- DLL is simpler to implement
- When you undo 5 times and make a new edit, the DLL simply overwrites the "future" by relinking — no need to clear a separate redo stack
- DLL uses less memory
- Stacks don't support undo
📖 In a DLL: current pointer moves back on undo, forward on redo. New edit at current position just updates current.next to the new node — naturally discarding the old "future." With two stacks, you'd need to clear the entire redo stack.
❌ (a) DLLs are more complex. (c) DLL uses more memory per node.
Level 4 — Analysis & Tradeoff ⭐⭐⭐ (6 Questions)
An SLL of n nodes uses n × (data_size + pointer_size) bytes. A DLL uses n × (data_size + 2 × pointer_size). For 1 million nodes storing 4-byte integers on a 64-bit system (8-byte pointers), the DLL uses how much MORE memory?
- 4 MB
- 8 MB
- 12 MB
- 16 MB
📖 Extra cost per node = 1 extra pointer = 8 bytes. 1,000,000 × 8 = 8,000,000 bytes = ~8 MB. SLL: 12 bytes/node (4+8). DLL: 20 bytes/node (4+8+8). Difference: 8 bytes/node × 1M = 8MB.
❌ Math errors in other options.
You need a data structure for a real-time chat app where new messages arrive at the end and old messages are removed from the beginning (like a scrolling feed). Best choice?
- Array — O(1) access
- SLL with tail pointer — O(1) insert at tail, O(1) delete at head
- DLL — overkill for this use case
- Sorted array — messages need to be sorted
📖 Messages arrive at the tail (O(1) with tail pointer) and old messages are removed from the head (O(1)). No backward traversal needed. DLL would work but wastes memory on unused prev pointers.
❌ (a) O(n) delete at head. (c) Works but unnecessary. (d) Messages are already ordered by time.
Linked lists have better cache performance than arrays because nodes can be anywhere in memory.
- True — scattered memory is more flexible
- False — arrays have much better cache locality because elements are contiguous
- True — modern CPUs handle scattered memory well
- False — but it doesn't matter for performance
📖 CPU caches work by loading CONTIGUOUS blocks of memory. Arrays benefit enormously from this — accessing arr[i] likely caches arr[i+1] too. Linked list nodes are scattered, causing frequent cache misses. This is why arrays are often faster in practice than theoretical complexity suggests.
❌ (a)(c) Scattered memory is a DISADVANTAGE for cache. (d) Cache matters hugely.
For implementing a text editor's character buffer (like VS Code), which is best?
- Simple array of characters
- Linked list of characters (one per node)
- Array of arrays (piece table or gap buffer)
- Stack
📖 A pure array requires O(n) shifts for every keystroke. A linked list of individual chars wastes ~20 bytes per character (pointer overhead). Modern editors use gap buffers or piece tables — hybrid structures that combine array efficiency with fast insertion.
❌ (a) O(n) per insert. (b) Massive pointer overhead. (d) Not suitable.
The Linux kernel uses an "intrusive" linked list where the list_head struct is embedded inside the data struct. The advantage is:
- Simpler code
- No separate memory allocation for list nodes — zero allocation overhead for list operations
- Faster CPU execution
- Smaller binary size
📖 Normal linked lists allocate a separate Node struct that wraps the data. Intrusive lists embed prev/next pointers directly in the data struct. Insert/delete requires zero malloc/free calls — critical for an OS kernel where every nanosecond matters.
❌ (a) More complex actually. (c)(d) Not the primary advantage.
Which statement correctly describes when to choose arrays vs linked lists?
- Always use arrays — they're simpler
- Always use linked lists — they're more flexible
- Use arrays when access pattern is index-based; use linked lists when insert/delete-heavy with sequential access
- The choice doesn't affect performance
📖 Arrays: O(1) random access, great cache locality, but O(n) insert/delete. Linked lists: O(1) insert/delete at known positions, but O(n) access and poor cache locality. The right choice depends on YOUR access pattern — there's no universal winner.
❌ (a)(b) No "always" answer. (d) It absolutely matters.
Chapter Summary
5 Things Every Engineer Must Remember
- Linked lists trade random access for O(1) insertion/deletion — this is the fundamental trade-off. Spotify chose DLL over arrays because playlists need constant add/remove/reorder, not random access.
- Doubly linked lists are worth the extra pointer — the prev pointer enables O(1) deletion of any node (given a reference) and backward traversal. This powers browser history, undo/redo, and LRU caches.
- Header nodes eliminate edge cases — one extra sentinel node means you never write
if (head == NULL)again. Fewer conditions = fewer bugs. The Linux kernel's list_head does exactly this. - Always free() deleted nodes in C — forgetting this causes memory leaks. A server leaking 20 bytes per request crashes after a few million requests. Python's garbage collector handles this automatically.
- Cache locality matters more than theory suggests — arrays are often faster in practice than linked lists for small data because CPU caches favor contiguous memory. This is why real-world implementations like Timsort use arrays internally even for "linked" operations.
Master Comparison Table
| Structure | Access | Insert Head | Insert Tail | Delete | Space/Node | Use When | Avoid When |
|---|---|---|---|---|---|---|---|
| Array | O(1) | O(n) | O(1)* | O(n) | data only | Random access, small data | Frequent insert/delete |
| Singly LL | O(n) | O(1) | O(n)** | O(n)† | data + 1 ptr | Stack, queue (one direction) | Need backward traversal |
| Doubly LL | O(n) | O(1) | O(1) | O(1)‡ | data + 2 ptrs | Browser history, LRU cache | Memory-constrained systems |
| Circular SLL | O(n) | O(1) | O(n) | O(n) | data + 1 ptr | Round-robin scheduling | Need bidirectional access |
| Header LL | O(n) | O(1) | O(n) | O(n) | data + 1 ptr + header | Simplified edge-case handling | Memory-critical applications |
* Amortized with dynamic array. ** O(1) with tail pointer. † O(1) if deleting head. ‡ Given a node reference.
Coming Up Next: Unit 3 — Stacks, Queues & Recursion
You now understand how linked lists enable O(1) insertion and deletion. But what if you need a disciplined data structure — one where elements can only enter and exit from specific ends? Imagine Swiggy's order processing system: 50,000 orders come in per hour during peak dinner time (7-9 PM IST). Each order must be processed in the order it arrived (First-In-First-Out). But the system also needs to "undo" the last payment if a card is declined (Last-In-First-Out).
In Unit 3, we'll build stacks (LIFO — like Paytm's transaction rollback) and queues (FIFO — like Swiggy's order pipeline) using both arrays and linked lists. We'll convert mathematical expressions using Polish notation, solve the Tower of Hanoi, and implement Merge Sort and Quick Sort using recursion. These structures are everywhere — from function calls in your CPU to undo buttons in every app you use.