Linked Lists
Think of it like this
Imagine a treasure hunt where each clue tells you where the next clue is hidden. You can't jump directly to clue 5 — you must follow the chain: clue 1 → clue 2 → clue 3 → ... That's a linked list.
Compare this to an array (the apartment mailbox row): in an array, you jump directly to box 5. In a linked list, you must walk from the start every time. The tradeoff: inserting or removing from the front costs O(1) because you only update a pointer — no shifting.
Node Structure
Each node has two things: the value it holds, and a pointer (reference) to the next node.
Singly Linked List:
[head]
↓
[10 | •]──→[23 | •]──→[45 | •]──→[67 | •]──→ null
node 0 node 1 node 2 node 3
[val | next] ← each box is one Node object in memoryTypes of Linked Lists
Singly:
[val|next] → [val|next] → [val|next] → null
(can only go forward)
Doubly:
null ← [prev|val|next] ↔ [prev|val|next] ↔ [prev|val|next] → null
(can go forward AND backward — needed for LRU cache)
Circular:
[val|next] → [val|next] → [val|next]
↑__________________________|
(last node points back to head — used in round-robin schedulers)Time Complexity
| Operation | Singly | Doubly | Why |
|---|---|---|---|
| Access by index | O(n) | O(n) | Must walk from head |
| Search for value | O(n) | O(n) | Linear scan |
| Insert at head | O(1) | O(1) | Just update head pointer |
| Insert at tail | O(1)* | O(1) | *With tail pointer |
| Insert in middle | O(n) | O(n) | Must walk to position first |
| Delete head | O(1) | O(1) | Move head to head.next |
| Delete tail | O(n) | O(1) | Singly must walk to find prev |
| Delete middle | O(n) | O(n) | Must find prev node |
Space Complexity
O(n) — but ~2× the overhead of arrays because each node stores both the value and a pointer (8 bytes for a 64-bit reference).
Implementation from Scratch
class Node {
constructor(val) {
this.val = val;
this.next = null; // for doubly: also this.prev = null
}
}
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.size = 0;
}
// Insert at head — O(1)
prepend(val) {
const node = new Node(val);
node.next = this.head;
this.head = node;
if (!this.tail) this.tail = node; // first element
this.size++;
}
// Insert at tail — O(1) with tail pointer
append(val) {
const node = new Node(val);
if (!this.tail) {
this.head = this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.size++;
}
// Delete head — O(1)
deleteHead() {
if (!this.head) return null;
const val = this.head.val;
this.head = this.head.next;
if (!this.head) this.tail = null; // list is now empty
this.size--;
return val;
}
// Convert to array for debugging
toArray() {
const result = [];
let curr = this.head;
while (curr) { result.push(curr.val); curr = curr.next; }
return result;
}
}Core Techniques
Technique 1 — Fast & Slow Pointers (Floyd's Algorithm)
Two pointers at different speeds. The slow pointer moves 1 step at a time; the fast pointer moves 2. If there's a cycle, fast will eventually lap slow and they'll meet.
List with cycle:
1 → 2 → 3 → 4 → 5
↑ ↓
8 ← 7 ← 6
slow: 1, 2, 3, 4, 5, 6, 7, 8, 4, 5 ...
fast: 1, 3, 5, 7, 4, 6, 8, 5 ...
They meet inside the cycle → cycle detected!function hasCycle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true; // same node object
}
return false;
}
// Find middle — slow stops at middle when fast reaches end
function findMiddle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow; // middle node
}Linked List Cycle Detection (Floyd's)
Start both Slow and Fast pointers at the head of the linked list.
Technique 2 — Reversing In-Place
Three pointers: prev, curr, next. Redirect curr.next to point backward, then advance all three.
Before: null ← 1 → 2 → 3 → 4 → null
Step 1: prev=null, curr=1, next=2
curr.next = prev → null ← 1 2 → 3 → 4
prev=1, curr=2
Step 2: prev=1, curr=2, next=3
curr.next = prev → null ← 1 ← 2 3 → 4
prev=2, curr=3
...and so on until curr is null
After: null ← 1 ← 2 ← 3 ← 4 (prev = new head = 4)function reverseList(head) {
let prev = null, curr = head;
while (curr) {
const next = curr.next; // save next before overwriting
curr.next = prev; // reverse the pointer
prev = curr; // advance prev
curr = next; // advance curr
}
return prev; // prev is now the new head
}Technique 3 — Dummy Head Node
Add a fake node before the real head. This eliminates special-casing for operations that modify the head — the code stays the same for all positions.
function removeElements(head, val) {
const dummy = new Node(-1);
dummy.next = head;
let curr = dummy;
while (curr.next) {
if (curr.next.val === val) curr.next = curr.next.next; // skip it
else curr = curr.next;
}
return dummy.next; // real head (may have changed)
}Technique 4 — N Nodes from End
Use a gap of N between two pointers. Start both at head, advance the fast pointer N steps first. Then move both until fast reaches the end — slow will be N nodes from the end.
Remove 2nd from end: 1 → 2 → 3 → 4 → 5
fast = 5 (moved 2 steps ahead)
slow = 1
Move both: slow=2 (slow.next = 4 → skip 3 → slow.next = slow.next.next)
Fast reaches end when it's at 5, so slow is at 3 which is before the 2nd from endfunction removeNthFromEnd(head, n) {
const dummy = new Node(0);
dummy.next = head;
let fast = dummy, slow = dummy;
for (let i = 0; i <= n; i++) fast = fast.next; // advance fast by n+1
while (fast) { fast = fast.next; slow = slow.next; }
slow.next = slow.next.next; // remove the target
return dummy.next;
}Real-World Frontend Application
- React Fiber: Each fiber (component work unit) has a
sibling,child, andreturnpointer — essentially a doubly linked list of work to process - LRU Cache: Combines a
Map(O(1) lookup by key) with a doubly linked list (O(1) move-to-front and evict-from-tail) - Browser history: Forward/back navigation uses a doubly linked list —
prevfor Back,nextfor Forward - Undo/Redo in editors: Each state is a node; undo walks backward, redo walks forward
Common Mistakes
| Mistake | What Goes Wrong | Fix |
|---|---|---|
| Losing the tail | curr = curr.next before saving curr.next | Save next first: const next = curr.next |
| Not handling empty list | curr.val when curr is null | Always check if (!head) return null |
| Forgetting to update tail | After appending, tail still points to old last node | Update this.tail = node |
| Off-by-one in N-from-end | Gap between pointers is wrong | Use dummy head; advance fast n+1 times |
Key Problems to Solve
| # | Problem | Pattern | Difficulty |
|---|---|---|---|
| 1 | Reverse Linked List | Three-pointer reversal | Easy |
| 2 | Merge Two Sorted Lists | Two-pointer merge | Easy |
| 3 | Linked List Cycle | Fast/slow pointers | Easy |
| 4 | Middle of Linked List | Fast/slow pointers | Easy |
| 5 | Palindrome Linked List | Find mid + reverse + compare | Easy |
| 6 | Remove Nth Node from End | Gap technique | Medium |
| 7 | Add Two Numbers | Carry arithmetic | Medium |
| 8 | Reorder List | Find mid + reverse + interleave | Medium |
| 9 | LRU Cache | Doubly LL + HashMap | Medium |
| 10 | Copy List with Random Pointer | Cloning with hash map | Medium |
| 11 | Merge K Sorted Lists | Min-heap on list heads | Hard |