LC Patterns — Linked Lists
◆ The Master Insight
A linked list is an array that gave up random access in exchange for O(1) local surgery: you can't jump to index i, but you can splice, cut, and rewire in constant time without shifting anything. Every pattern uses one side of that trade — and every difficulty comes from the other side (no going backward, no length, no peeking ahead without walking there).
Linked-list problems are not idea problems; they're execution problems. The algorithm is usually obvious in ten seconds ("reverse it", "find the middle") — the interview tests whether your pointer manipulation is precise while someone watches.
ListNode dummy(0); dummy.next = head; … return dummy.next;. (2) Save before you sever — any rewiring that overwrites ->next must first stash what it pointed to. (3) Termination discipline — after any surgery, ask "does my loop pointer still make progress?" Infinite loops here come from re-linked cycles, not off-by-ones.
§0Mechanics & Universal Pitfalls
struct ListNode { int val; ListNode* next; ListNode(int x): val(x), next(nullptr) {} };
// Standard walk with previous — powers deletion, reversal, insertion:
ListNode* prev = dummy; ListNode* cur = head;
while (cur) { /* ... */ }
// The reversal triple — THE core move; drill until automatic:
ListNode* nxt = cur->next; // 1. save
cur->next = prev; // 2. rewire
prev = cur; cur = nxt; // 3. advance
// Fast & slow:
ListNode* slow = head; ListNode* fast = head;
while (fast && fast->next) { slow = slow->next; fast = fast->next->next; }
Facts to have loaded
while (fast && fast->next) is the universal safe guard for two-stepping — works for odd and even lengths. Deletion needs the node before — hence prev-pointers and dummies. In C++ think about ownership: delete removed nodes (no GC). Doubly linked lists (design): every splice touches 4 pointers; sentinel head and tail remove all edge cases — if you write a->next = b without b->prev = a in the same breath, the structure is already corrupt.
Universal pitfalls
1. Losing the rest of the list — you overwrote ->next before saving it. Symptom: half your list vanishes.
2. Head edge cases — deletion/insertion at the head without a dummy; single-node and empty inputs. Test mentally on [] and [x] every time.
3. Null dereference at the boundary — fast->next->next when fast->next is null; guard order matters.
4. Accidental cycle — re-linking to an earlier node without cutting the old path. If your solution hangs, you built a ring.
5. Off-by-one in "n-th from…" — settle it with a concrete 5-node trace, never algebra alone.
6. Modifying while iterating — decide before the loop: am I moving cur, or is cur being spliced away? (Deletion loops advance prev only when not deleting.)
1In-Place Reversal (the core motor skill)
What it is: Reverse the whole list, a sublist [left, right], or every k-node group — by walking forward while pointing each node backward. The single most important mechanical skill in this file; the harder problems just aim it at a window.
nxt=cur->next; cur->next=prev; prev=cur; cur=nxt; → arrows flip one at a time. Final result 3 → 2 → 1 → ∅, return prev.Intuition: you can't walk a singly linked list backward — but you can make the arrows point backward as you pass, carrying a prev that grows into the reversed prefix. Sublist reversal (92) = walk to the node before left, reverse right−left+1 steps, then stitch both seams. K-group (25) = sublist reversal in a loop with a length probe (only reverse complete groups); 24 (swap pairs) is the k = 2 special case.
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* cur = head;
while (cur) {
ListNode* nxt = cur->next; // 1. save
cur->next = prev; // 2. rewire
prev = cur; cur = nxt; // 3. advance
}
return prev; // new head
}
Challenges & pitfalls
The stitch order in 92 (anchor->next->next = cur; anchor->next = prev — draw it, don't recall it); reversing one node too many/few (count right−left+1; trace a 5-node list with left=2, right=4); 25's probe (reversing an incomplete tail group is the wrong answer — probe first); the recursive 206 stack-depth caveat (O(n) stack); narrating while coding — this is the place interviewers watch your hands.
2Fast & Slow Pointers (Floyd: middles and cycles)
What it is: Two pointers, one twice as fast. When fast finishes, slow is at the middle; if there's a cycle they must meet inside it; and a second phase finds the exact node where the cycle begins.
a = kc − b.Intuition: in a cycle, fast gains exactly 1 node on slow each step, so the gap shrinks to 0 — they meet; no cycle ⇒ fast hits null. Cycle entry (142): with head-to-entry a, meeting point b into a cycle of length c, at meeting slow walked a+b, fast 2(a+b)=a+b+kc ⇒ a = kc − b — so a pointer from the head and the meeting-point pointer, both at speed 1, meet exactly at the entry. Rehearse this — it's the thing 142 tests.
ListNode* detectCycle(ListNode* head) {
ListNode* slow = head; ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next; fast = fast->next->next;
if (slow == fast) { // phase 1: they meet
ListNode* p = head;
while (p != slow) { p = p->next; slow = slow->next; } // phase 2: walk to entry
return p;
}
}
return nullptr; // no cycle
}
Challenges & pitfalls
Fumbling the 142 derivation live (rehearse the 4 sentences); the odd/even middle ambiguity biting downstream (Pattern 7); comparing node identity, never values (lists can have duplicate values!); using a seen-set when O(1) space was the point (mention the set as the baseline, then deliver Floyd); the guard order fast && fast->next. Cross-file: the same two-phase Floyd solves 287 on the implicit list i → nums[i].
3Offset / Anchored Two Pointers (gap walks)
What it is: Two pointers moving at the same speed but separated by a fixed gap, or synchronized across two lists — so when one hits a landmark, the other is exactly where you need to act. One pass, no length precomputation.
Intuition: n-th from the end (19): give the lead pointer an n-node head start; when it reaches the end, the trailer is n from the end — an invariant carried to the finish. Intersection (160): two lists sharing a tail — walk A through list1 then list2, B through list2 then list1; both walk m+n and arrive at the intersection together (or reach null together if disjoint). Rotate (61): close into a ring, walk n − k%n, cut.
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode dummy(0); dummy.next = head;
ListNode* lead = &dummy; ListNode* trail = &dummy;
for (int i = 0; i <= n; ++i) lead = lead->next; // n+1 ahead from dummy
while (lead) { lead = lead->next; trail = trail->next; }
trail->next = trail->next->next; // trailer stops BEFORE the victim
return dummy.next;
}
Challenges & pitfalls
19's off-by-one (n vs n+1 head start — resolved only by tracing "remove 2nd from end of [1..5]" by hand); 160's termination when there's no intersection (both hit null on the same step — both walked exactly m+n); forgetting k % n in 61; deletion without a dummy when the target is the head; stating the invariant ("the gap stays exactly n") — it's the proof and takes five seconds.
4Partition & Interleave (split into streams, weave together)
What it is: Break a list into sublists by a rule (less-than-x vs the rest; odd vs even positions), or weave sorted lists into one — building output lists tail-first with dummy heads. The stable-splice discipline: nodes keep their relative order because you only ever append.
Intuition: merge two sorted (21): a zipper with a dummy head + tail pointer, and when one list empties, append the remainder in one move. Partition (86): don't shuffle nodes in place — run two collector lists (< x and ≥ x) with their own dummies, then join; stability is automatic because appending preserves order. Odd-even (328): the same two-collector idea keyed by position parity, done in place by leapfrogging.
ListNode* mergeTwoLists(ListNode* a, ListNode* b) {
ListNode dummy(0); ListNode* tail = &dummy;
while (a && b) {
if (a->val <= b->val) { tail->next = a; a = a->next; }
else { tail->next = b; b = b->next; }
tail = tail->next;
}
tail->next = a ? a : b; // append the whole remainder
return dummy.next;
}
Challenges & pitfalls
The un-severed tail (terminate every stream's tail with nullptr before joining, or the last node still points into the old list — the #1 bug, showing up as a cycle or phantom suffix); losing a stream's head (that's what the dummies are for); 328's pointer-dance ordering (trace 5 nodes); over-engineering 86 by swapping in place; merging by value but advancing the wrong list's pointer (whoever donated the node advances).
5Sorting Linked Lists (merge sort is the only game in town)
What it is: Sort a linked list in O(n log n). Arrays reach for quicksort/heapsort; lists can't (no random access) — but merge sort actually gets nicer on lists: split via fast/slow, merge via Pattern 4's zipper, no auxiliary array.
Intuition: merge sort's two primitives — split in half, merge two sorted — are exactly Patterns 2 and 4, both O(1)-extra on lists. So 148 is a composition test: find the middle (first middle, then cut), recurse, zip. The true-O(1)-space follow-up is bottom-up merge sort (merge runs of width 1, 2, 4, … iteratively). Merge k lists (23): pairwise divide-and-conquer, O(N log k).
C++ · Sort List (LC 148)ListNode* sortList(ListNode* head) {
if (!head || !head->next) return head; // base case doubles as guard
ListNode* slow = head; ListNode* fast = head->next; // fast = head->next → left gets the shorter half
while (fast && fast->next) { slow = slow->next; fast = fast->next->next; }
ListNode* mid = slow->next; slow->next = nullptr; // SEVER the halves
return mergeTwoLists(sortList(head), sortList(mid));
}
Challenges & pitfalls
The 2-node infinite recursion (starting fast = head never shrinks a 2-node list — fast = head->next is the fix, memorize the reason); forgetting to cut before recursing; quicksort-on-lists as a trap answer (pivoting without random access loses its advantage); stack-depth commentary; complexity narration — "O(log n) levels × O(n) merge per level."
6Deletion & Filtering with a Dummy (the prev-pointer walks)
What it is: Remove nodes matching a condition — by value, duplicated-in-sorted, or any predicate. All powered by one loop shape: prev anchored on the last surviving node, cur scouting ahead, and the dummy making "before the head" a real place.
prev->next = cur->next and prev stays — so two 6s in a row both get removed. Result 1 → 2 → 3 → 4 → 5.Intuition: deletion = making the predecessor skip the victim — so the loop's real subject is prev, not cur. Remove all matching values (203): if cur matches, skip and prev stays (the next node needs vetting too); else advance prev. Dedupe-all-copies (82): prev advances only when exactly one copy passed. Contrast 83 (keep one copy — no dummy, no prev). The 82-vs-83 distinction — survivor-anchored vs current-anchored — is the interview.
ListNode* removeElements(ListNode* head, int val) {
ListNode dummy(0); dummy.next = head;
ListNode* prev = &dummy;
while (prev->next) {
if (prev->next->val == val) prev->next = prev->next->next; // skip; prev STAYS
else prev = prev->next; // advance only when kept
}
return dummy.next;
}
Challenges & pitfalls
Advancing prev after a deletion (adjacent matches survive — the bug of 203); 82's triple-pointer fog (an invariant comment cuts through it); value-equality runs at the very end (guard cur->next inside run scans); treating 237's value-copy trick as clean rather than flagging its caveats (flagging them is the senior move); empty-result edge (everything deleted → return dummy.next = null — works because of the dummy).
7The Combo Move: Middle + Reverse + Merge ★ the set-piece trio
What it is: The three-step composite that solves "compare/combine the two halves" problems in O(n) time, O(1) space: find the middle (Pattern 2), reverse the second half (Pattern 1), then walk/weave the two halves (Pattern 3/4). The single most-asked linked-list composition.
2=2 ✓
→ palindrome
i ↔ n−1−i by reading both halves in the same direction.Intuition: questions that pair position i with n−1−i (palindrome, reorder L0→Ln→L1→Ln−1…, twin sums) need to read the list from both ends — impossible singly-linked, unless you reverse the back half. The array alternative (copy out, two indices) is O(n) space; the combo is the O(1)-space answer the problem is fishing for — offer the array version as the baseline, then deliver the combo.
bool isPalindrome(ListNode* head) {
// 1. first middle + cut
ListNode* slow = head; ListNode* fast = head->next;
while (fast && fast->next) { slow = slow->next; fast = fast->next->next; }
ListNode* second = slow->next; slow->next = nullptr;
// 2. reverse the second half
ListNode* prev = nullptr;
while (second) { ListNode* n = second->next; second->next = prev; prev = second; second = n; }
// 3. walk both halves
for (ListNode* p = head, *q = prev; q; p = p->next, q = q->next)
if (p->val != q->val) return false;
return true;
}
Challenges & pitfalls
Odd-length middle ownership (the middle belongs to the first half and simply isn't paired — first-middle + cut makes this automatic); 143's double-save splice (four pointers live at once — the narrate-or-die moment); forgetting the cut (comparison walks into the un-severed tail → phantom failures); stating the space claim precisely ("O(1) extra — I modify the list; restoring it is one more reversal"); recognizing the family — any i ↔ n−1−i pairing = this combo.
8Digit Arithmetic on Lists (schoolbook with pointers)
What it is: Numbers stored as linked lists of digits — add them, with digits in forward or reverse order. The carry loop, re-hosted on list nodes.
| step | l1 + l2 + carry | emit | carry |
|---|---|---|---|
| 0 | 2 + 5 + 0 = 7 | 7 | 0 |
| 1 | 4 + 6 + 0 = 10 | 0 | 1 |
| 2 | 3 + 4 + 1 = 8 | 8 | 0 |
while (l1 || l2 || carry) loop is what catches a leftover carry (as in 99 + 1).Intuition: addition wants the least-significant digit first. LC 2 hands you exactly that (lists reversed) — one pass, dummy-headed output, a carry variable, loop while l1 || l2 || carry (the carry can outlive the digits). LC 445 stores most-significant first → either reverse both inputs (then it's LC 2) or use two stacks (no mutation, build by prepending). 1290 (binary→int) is the forward warm-up: Horner's rule num = num*2 + bit.
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode dummy(0); ListNode* tail = &dummy; int carry = 0;
while (l1 || l2 || carry) {
int s = carry;
if (l1) { s += l1->val; l1 = l1->next; }
if (l2) { s += l2->val; l2 = l2->next; }
carry = s / 10;
tail->next = new ListNode(s % 10);
tail = tail->next;
}
return dummy.next;
}
Challenges & pitfalls
The final-carry node (every plausible test set includes it); unequal lengths (guard with if (l1), not pre-padding); 445 without asking about mutation (choosing silently forfeits the design conversation — reverse = O(1) extra but mutates; stacks = O(n) extra, input pristine); Horner direction confusion (forward read = multiply-accumulate; backward carry = divmod — keep them labeled).
9Restructuring with Auxiliary Pointers (clone, flatten, splice)
What it is: Lists with extra pointers per node (random, child, prev) that must be deep-copied, flattened, or spliced — where the difficulty is keeping a correspondence between old and new structure without losing threads.
copy->next = map[orig->next], copy->random = map[orig->random]. Passes strictly separated so no random points at an un-copied node.Intuition: copy-with-random (138): the randoms point at original nodes; the copy needs them pointing at copied nodes → an old→new map. Hashmap version: two passes, O(n) space, correct. The O(1)-space set-piece interleaves copies into the original (A→A'→B→B'…) so each copy's random is orig->random->next, then unzips — and the unzip must fully restore the original. Flatten multilevel (430): DFS splicing child chains in with a stack of pending nexts.
Node* copyRandomList(Node* head) {
unordered_map<Node*, Node*> map;
for (Node* cur = head; cur; cur = cur->next) // pass 1: create copies
map[cur] = new Node(cur->val);
for (Node* cur = head; cur; cur = cur->next) { // pass 2: wire through the map
map[cur]->next = map[cur->next]; // map[nullptr] default-inserts nullptr
map[cur]->random = map[cur->random];
}
return map[head];
}
Challenges & pitfalls
138's unzip restoring the original exactly (checkers verify it); wiring randoms during interleave before all nodes have copies (hence pass separation); 430's dangling child pointers and broken prevs post-splice; treating these as exotic (they're Patterns 1–4 tools + one auxiliary invariant — "every original's copy is exactly its map entry"); grabbing head->next before unzipping so you don't lose the return.
10Linked List as a Design Component (LRU and friends)
What it is: Design problems where a linked list is chosen by you — because it's the only container with O(1) removal from the middle given a node reference. LRU cache is the canonical interview: a hashmap for O(1) lookup + a doubly linked list for O(1) recency reordering.
tail->prev (LRU). Two sentinels ⇒ zero edge cases. Each node stores its key so eviction can also erase the map entry.Intuition: LRU needs three O(1) ops: find an entry (hashmap → node), move to "most recent" (unlink + relink at one end — this is why it must be a linked list; an array shifts in O(n)), evict the least recent (remove at the other end). The composition — hashmap points at list nodes; list order encodes recency — is the single most important data-structure composition in interviews, and generalizes: LFU (460) = hashmap + a DLL per frequency bucket.
C++ · LRU Cache (LC 146)class LRUCache {
struct Node { int key, val; Node *prev, *next;
Node(int k, int v): key(k), val(v), prev(nullptr), next(nullptr) {} };
int cap;
unordered_map<int, Node*> map;
Node *head, *tail; // sentinels; head side = most recent
void remove(Node* n) { n->prev->next = n->next; n->next->prev = n->prev; }
void addFront(Node* n) { n->next = head->next; n->prev = head;
head->next->prev = n; head->next = n; }
public:
LRUCache(int capacity): cap(capacity) {
head = new Node(0,0); tail = new Node(0,0);
head->next = tail; tail->prev = head;
}
int get(int key) {
if (!map.count(key)) return -1;
Node* n = map[key]; remove(n); addFront(n); // promote
return n->val;
}
void put(int key, int value) {
if (map.count(key)) { Node* n = map[key]; n->val = value; remove(n); addFront(n); return; }
if ((int)map.size() == cap) { // evict LRU at the tail
Node* lru = tail->prev; remove(lru); map.erase(lru->key); delete lru;
}
Node* n = new Node(key, value); map[key] = n; addFront(n);
}
};
Challenges & pitfalls
Missing key in the node (eviction can't clean the map — the #1 LRU bug); the update-existing-key path in put (update value and promote and don't double-insert); capacity-check ordering (evict before or after insert — pick one); writing get/put monolithically instead of via remove/addFront helpers; the "why not a singly linked list?" question (removal needs the predecessor in O(1) → prev pointers).
▤Recognition Cheat Sheet
| # | Pattern | Trigger phrase | Core move | Canonical trap |
|---|---|---|---|---|
| 1 | Reversal | "reverse (part of) the list" | save → rewire → advance | sublist stitch seams |
| 2 | Fast & slow | middle, cycle, "constant space" | 1× and 2× pointers | 142's restart derivation |
| 3 | Offset pointers | "n-th from end", intersection, rotate | fixed-gap walk; track-swap | n vs n+1 head start |
| 4 | Partition/weave | merge sorted, partition, odd-even | dummy+tail per stream | un-severed stream tails |
| 5 | List sorting | "sort in O(n log n)" | merge sort: split + zip | 2-node infinite recursion |
| 6 | Deletion walks | remove/dedupe/filter | prev = last survivor | advancing prev after delete |
| 7 | Combo move | palindrome, reorder, i ↔ n−1−i | middle + reverse + weave | middle ownership, the cut |
| 8 | Digit arithmetic | numbers as digit lists | carry loop; while l1||l2||carry | final carry node |
| 9 | Aux pointers | random/child pointers, deep copy | old→new correspondence | pass separation; restore original |
| 10 | Design component | "O(1) get/put", LRU | hashmap → DLL nodes + sentinels | key missing from node |