Linked Lists — Complete Guide with Interactive Walkthroughs
A linked list stores an ordered sequence like an array — but as node objects connected by pointers, not contiguous memory. Each node holds a value and a next reference to the following node. That single idea unlocks O(1) insert/delete when you already hold the right pointer, plus elegant O(1)-space algorithms that interviewers love.
This guide follows the same format as [Two Pointers](/blog/two-pointers-technique), [Hashing](/blog/hashing-technique), and [Binary Search](/blog/binary-search-technique): concepts, interactive walkthroughs, code in three languages, and a practice ladder.
You should know basic OOP (classes, objects, fields) before continuing.
---
Nodes vs Array Elements
An array like [1, 2, 3] stores values contiguously. Indexing arr[6] jumps straight to an address — O(1) random access.
A linked list stores the same logical sequence as nodes:
[1|•] → [2|•] → [3|•] → null
headEach node is an object: val plus next. There is no list[150000] — finding the 150,000th element means walking from the head 150,000 times.
Keep a reference to the head. It is the only node from which you can reach every element. Lose the head and you lose the list.
---
Core Patterns
| Pattern | Pointers | When to use |
|---|---|---|
| Traversal | curr = curr.next | Sum, length, convert — never lose head |
| Insert / delete | prev + newNode | O(1) if you already hold the node before i |
| Fast & slow | fast = fast.next.next | Middle, cycle detect, k-th from end |
| Reverse | prev, curr, nextNode | Full reverse or reverse a sublist / second half |
| Pointer choreography | dummy + prev | Swap pairs, reorder, splice without arrays |
---
Building a List
Building a Singly Linked List
1 → 2 → 3 — each node holds val and next
Create node 1 (head)
head.val = 1, head.next = null
class ListNode {
constructor(val, next = null) {
this.val = val;
this.next = next;
}
}
const head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
// 1 → 2 → 3 → null---
Linked List vs Array
| Linked list | Array | |
|---|---|---|
Access index i | O(n) | O(1) |
| Insert/delete with pointer | O(1) | O(n) |
| Contiguous memory | No | Yes |
| Overhead | Extra pointers | Packed |
| Operation | Linked list | Dynamic array |
|---|---|---|
| Access index i | O(n) | O(1) |
| Insert/delete with prev ref | O(1) | O(n) |
| Insert/delete at arbitrary i | O(n) | O(n) |
| Append (with tail / sentinel) | O(1) | O(1)* |
| Per-element overhead | Pointer(s) | Packed |
* Amortized — occasional resize is O(n). Interview tip: don't dump the list into an array unless asked.
For interviews: the list is usually given as input. You rarely choose list vs array — you choose how to move pointers without dumping into an array (that almost always fails the interview).
---
Mechanics — Assignment, Traversal, `.next`
Assignment: ptr = head makes ptr refer to the same object. Changing head = head.next does not move ptr — only ptr = something moves it.
Chaining: head.next.next means “the next of (head.next)”. On 1 → 2 → 3, that is 2.next (the node 3).
Traversal:
curr = head
while curr:
// use curr.val
curr = curr.nextWhen curr is the last node, curr.next is null and the loop ends.
Use a dummy pointer for traversal so you never overwrite your only reference to head.
---
Insert and Delete (Singly Linked)
To insert or delete at position i, you normally need a pointer to the node at i − 1 (prev).
Insert at Position i
Insert 99 after node 1 in 1 → 2 → 3
Start with 1 → 2 → 3
Want to insert after the node at i − 1 (value 1)
Delete at Position i
Delete 2 from 1 → 2 → 3
prev points at 1
prev.next is the node to delete (2)
function insertAfter(prev, val) {
const node = new ListNode(val);
node.next = prev.next;
prev.next = node;
}
function deleteAfter(prev) {
if (!prev.next) return;
prev.next = prev.next.next;
}Order matters: set newNode.next = prev.next before prev.next = newNode, or you lose the rest of the list.
Without a prev reference, finding position i is O(n) from the head.
Doubly linked lists
Each node also has prev. You only need a reference to the node at i (not i − 1), but you must update both next and prev links.
Sentinel (dummy) nodes
Sentinel head / tail nodes sit outside the real list. Real head is sentinel.next. They simplify edge cases (empty list, delete last node) and give O(1) operations at both ends when you keep a sentinel tail.
---
Fast and Slow Pointers
Same idea as [Two Pointers](/blog/two-pointers-technique), but on a list: usually fast moves two steps, slow moves one.
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.nextCheck fast.next so you never do null.next.
Middle of the list
When fast reaches the end, slow is in the middle — half the speed, half the distance.
Middle Node — Fast & Slow
1 → 2 → 3 → 4 → 5 — find the middle value
slow = head, fast = head
fast moves 2× as fast as slow
function middleNode(head) {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}Cycle detection — LC 141
On a straight path, fast finishes first. On a cycle, fast eventually laps slow. If they meet (after the start), there is a cycle.
Detect a Cycle
Floyd's tortoise and hare — do slow and fast ever meet?
No cycle — linear track
Fast finishes; slow never catches up
function hasCycle(head) {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}Why fast can't skip slow forever: after entering the cycle, each iteration closes the gap by exactly one step.
Hashing visited nodes also works, but uses O(n) space — Floyd uses O(1).
k-th node from the end
Advance fast by k, then move both together. When fast hits the end, slow is k behind.
k-th Node From the End
1 → 2 → 3 → 4 → 5, k = 2 → return 4
Advance fast by k steps
Gap of k between slow and fast
function kthFromEnd(head, k) {
let slow = head;
let fast = head;
for (let i = 0; i < k; i++) {
fast = fast.next;
}
while (fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}---
Reversing a Linked List
To reverse 1 → 2 → 3 → 4, at each curr you need curr.next = prev — but then you lose the original next. Use three pointers: prev, curr, nextNode.
Reverse a Linked List
1 → 2 → 3 → 4 becomes 4 → 3 → 2 → 1
prev = null, curr = head
Need nextNode before flipping curr.next
function reverseList(head) {
let prev = null;
let curr = head;
while (curr) {
const nextNode = curr.next;
curr.next = prev;
prev = curr;
curr = nextNode;
}
return prev; // new head
}Thought process: need previous → store prev → need original next before flipping → store nextNode → advance. Elegant solutions come from listing requirements, then ordering the pointer moves.
Swap Nodes in Pairs — LC 24
1 → 2 → 3 → 4 → 5 → 6 → 2 → 1 → 4 → 3 → 6 → 5.
Break the work down: save the rest of the list, flip B → A, connect previous pair to D, use a dummy so you still return the new head (B). Handle odd length by linking the leftover single node.
Swap Nodes in Pairs
1 → 2 → 3 → 4 → 5 → 6 becomes 2 → 1 → 4 → 3 → 6 → 5
Pair A, B = 1, 2
Save nextNode = C before flipping B.next
function swapPairs(head) {
const dummy = new ListNode(0, head);
let prev = dummy;
while (head && head.next) {
const a = head;
const b = head.next;
const nextPair = b.next;
b.next = a;
a.next = nextPair;
prev.next = b;
prev = a;
head = nextPair;
}
return dummy.next;
}Reversal as a tool — Twin Sum
[LC 2130. Maximum Twin Sum](https://leetcode.com/problems/maximum-twin-sum-of-a-linked-list/): find middle (fast/slow) → reverse second half → walk both halves for pair sums. Patterns compose.
---
Complexity Cheat Sheet
| Problem | Time | Space |
|---|---|---|
| Traverse / sum / length | O(n) | O(1) |
| Middle (fast & slow) | O(n) | O(1) |
| Cycle detect (Floyd) | O(n) | O(1) |
| k-th from end | O(n) | O(1) |
| Reverse list | O(n) | O(1) |
| Swap pairs | O(n) | O(1) |
Hashing for cycles is also O(n) time but O(n) space — prefer Floyd when interviewers want constant space.
---
Common Mistakes
Losing the head. Always keep a stable reference or a dummy; traverse with a copy.
Wrong order when splicing. Link the new node to the rest before updating prev.next.
null.next. Guard with while (fast && fast.next) (or curr) before chaining.
Array cheat. Pushing every value into an array “solves” middle / k-th / reverse — and fails the point of the interview.
Forgetting dummy on head changes. Swap pairs, remove head, reverse — dummy simplifies return value.
Updating only one link in a doubly list. Both next and prev must stay consistent.
---
Practice Problems (Easiest → Hardest)
Easiest → Hardest
Step through the animations first, then open any problem on LeetCode.
---
Quick Reference — Pattern Picker
Pattern Picker
Click a node to trace the path — cards below update with your selection
---
Key Takeaways
- Nodes +
next— ordered sequence without contiguous memory; no random access. - Keep the head (or a sentinel) — it is your only entry point.
- Insert/delete is O(1) only with a pointer to the right place; otherwise O(n) to find it.
- Fast & slow — middle, cycles, k-th from end in O(n) time / O(1) space.
- Reverse —
prev/curr/nextNode; save next before flipping. - Dummy nodes — clean edge cases when the real head moves.
- Break problems into pointer steps — list requirements, then order the assignments.
- Compose patterns — middle + reverse half unlocks twin sum, reorder list, and more.
Linked lists train pointer discipline more than trivia. Pair this with [Two Pointers](/blog/two-pointers-technique) for the speed/gap intuition, and [Hashing](/blog/hashing-technique) when a set-based cycle check is acceptable.