LC Patterns — Linked Lists

Siblings: heaps, two-pointers · Code: C++
Per-pattern template: What it is → Intuition → Approach → Pitfalls → LC problems → Worked examples

◆ 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.

The one non-negotiable habit: draw boxes and arrows before writing code, and say each rewiring out loud as you code it ("save next, point current back, advance both"). Silent pointer-juggling is where candidates die — a diagram turns the hardest debugging environment (invisible state) into the easiest (visible arrows).
Three rules that cut across everything. (1) Dummy (sentinel) head whenever the real head might change or be deleted — 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

C++ · the idioms (recite-able)
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 boundaryfast->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.

Reverse Linked List (LC 206) — save → rewire → advance, one step shown
mid-reversal: prev = 1, cur = 2, saved nxt = 3
1
prev
2
cur
3
nxt
 
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.

C++ · Reverse Linked List (LC 206)
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.

LC 206 Reverse Linked ListLC 92 Reverse Linked List IILC 25 Reverse Nodes in k-Group

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.

Linked List Cycle II (LC 142) — 3 → 2 → 0 → −4, where −4 loops back to 2
3
head
2
entry
0
 
−4
 
└──────── −4->next points back to 2 ────────┘
Phase 1: fast/slow meet inside the loop. Phase 2: restart one pointer at head, step both by 1 → they meet at the entry 2. Why: 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+kca = 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.

C++ · Linked List Cycle II (LC 142)
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].

LC 876 Middle of the Linked ListLC 141 Linked List CycleLC 142 Linked List Cycle II

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.

Remove Nth From End (LC 19) — 1→2→3→4→5, remove 2nd from end; lead gets an n+1 head start
D
trail
1
 
2
 
3
lead
4
 
5
 
lead starts n+1 = 3 ahead of the dummy; advance both to the end → trail lands just before node 4 → splice it out → 1 → 2 → 3 → 5.

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.

C++ · Remove Nth Node From End (LC 19)
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.

LC 19 Remove Nth From EndLC 160 Intersection of Two ListsLC 61 Rotate List

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.

Merge Two Sorted Lists (LC 21) — zipper with a dummy + tail
1
2
4
l1
1
3
4
l2
zip
1
1
2
3
4
4
take the smaller head each step; when one list empties, append the whole remainder (it's already sorted). Result 1 1 2 3 4 4.

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.

C++ · Merge Two Sorted Lists (LC 21)
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).

LC 21 Merge Two Sorted ListsLC 86 Partition ListLC 328 Odd Even Linked List

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.

Sort List (LC 148) — 4 → 2 → 1 → 3, split then merge
4
2
sort
2
4
merge with
1
3
=
1
2
3
4
both the split (fast/slow) and merge (zipper) are O(1)-extra on a list → no scratch array. Result 1 2 3 4.

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."

LC 148 Sort ListLC 23 Merge k Sorted ListsLC 147 Insertion Sort List

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.

Remove Linked List Elements (LC 203) — remove value 6; prev stays put after a delete
D
 
1
 
2
prev
6
cur
3
 
on a match, 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.

C++ · Remove Linked List Elements (LC 203)
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).

LC 82 Remove Duplicates IILC 203 Remove Linked List ElementsLC 237 Delete Node in a Linked List

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.

Palindrome Linked List (LC 234) — 1 → 2 → 2 → 1
1 · split at first middle
1
2
2
1
2 · reverse2nd half
now both read L→R
1
2
1
2
3 · compare
1=1 ✓
2=2 ✓
palindrome
reversing the back half is what lets us pair position 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.

C++ · Palindrome Linked List (LC 234)
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.

LC 234 Palindrome Linked ListLC 143 Reorder ListLC 2130 Maximum Twin Sum

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.

Add Two Numbers (LC 2) — 2→4→3 (342) + 5→6→4 (465), digits reversed
stepl1 + l2 + carryemitcarry
02 + 5 + 0 = 770
14 + 6 + 0 = 1001
23 + 4 + 1 = 880
result 7 → 0 → 8 (807). The 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.

C++ · Add Two Numbers (LC 2)
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).

LC 2 Add Two NumbersLC 445 Add Two Numbers IILC 1290 Binary Number to Integer

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 List with Random Pointer (LC 138) — the old→new map does the work
original (next + random)
A
B
map
unordered_map old→new
AA'
BB'
wire
copy
A'
B'
pass 1 creates copies keyed by original; pass 2 wires 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.

C++ · Copy List with Random Pointer (LC 138) — hashmap version
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.

LC 138 Copy List with Random PointerLC 430 Flatten Multilevel DLLLC 1669 Merge In Between Lists

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.

LRU Cache (LC 146) — hashmap points at DLL nodes; list order = recency
map key→node
1
3
H
head
3
MRU
1
 
T
tail
get/put move the touched node to the front (MRU); over capacity → evict 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).

LC 146 LRU CacheLC 707 Design Linked ListLC 460 LFU Cache

Recognition Cheat Sheet

#PatternTrigger phraseCore moveCanonical trap
1Reversal"reverse (part of) the list"save → rewire → advancesublist stitch seams
2Fast & slowmiddle, cycle, "constant space"1× and 2× pointers142's restart derivation
3Offset pointers"n-th from end", intersection, rotatefixed-gap walk; track-swapn vs n+1 head start
4Partition/weavemerge sorted, partition, odd-evendummy+tail per streamun-severed stream tails
5List sorting"sort in O(n log n)"merge sort: split + zip2-node infinite recursion
6Deletion walksremove/dedupe/filterprev = last survivoradvancing prev after delete
7Combo movepalindrome, reorder, i ↔ n−1−imiddle + reverse + weavemiddle ownership, the cut
8Digit arithmeticnumbers as digit listscarry loop; while l1||l2||carryfinal carry node
9Aux pointersrandom/child pointers, deep copyold→new correspondencepass separation; restore original
10Design component"O(1) get/put", LRUhashmap → DLL nodes + sentinelskey missing from node

Study Order & The Interview Sentence

1 (the motor skill — drill the triple until your hands do it) → 2 (incl. the 142 derivation as a recital) → 63457 (the composition payoff — do all three in one sitting) → 8910 (146 deserves two full reps — the most-asked design question in existence)
"Let me draw the pointers before coding — ⟨sketch⟩ — the plan is ⟨middle/reverse/gap-walk/dummy⟩, and I'll narrate each rewiring as I go. Edge cases I'll check at the end: empty list, single node, and ⟨the pattern's specific trap⟩."