A learning sheet β there are barely any "algorithms" here. It's pointer hygiene: a sentinel head, and never overwrite a pointer before you've saved what it pointed to.
A node is just { val, next }. You can't index it; you can only walk forward and rewire .next. Almost every bug is one of two things: you lost a reference (overwrote a pointer before saving what came next), or you special-cased the head and got it wrong.
head so "delete/insert at the front" stops being a special case β you always have a prev. (2) Save-before-rewire: before x.next = β¦, stash const next = x.next, or the tail of the list vanishes. And always: draw the arrows before you type.Reverse 1 β 2 β 3 in place, O(1) space. The naive cur.next = prev looks rightβ¦ and instantly orphans the rest of the list.
cur.next to point backward before remembering cur.next, you can never reach node 2 or 3 again.prev (already-reversed part), cur (node being flipped), and a saved next (the untouched rest). Each step: save next β flip β advance both.prev ends as the new head.next, flip the arrow backward, slide prev and cur forward.| step | prev | cur | saved next | after flip |
|---|---|---|---|---|
| 1 | β | 1 | 2 | 1ββ
|
| 2 | 1 | 2 | 3 | 2β1 |
| 3 | 2 | 3 | β | 3β2 Β· return prev = 3 |
| Ask | If yes β | Pattern |
|---|---|---|
| "Reverse" β whole, a sublist, or in k-groups? | prev/cur/next dance | Reversal (Β§4.1) |
| Middle, cycle, n-th from end, palindrome? | two runners at different speeds | Fast & slow (Β§4.2) |
| Insert / delete / merge where the head might change? | a sentinel dummy + splice | Dummy head (Β§4.3) |
| Reorder / rotate / swap nodes structurally? | compose the primitives above | Compose (Β§4.4) |
O(n) each time.head inside a loop β you've made it O(nΒ²); keep a running pointer instead.| Words in the prompt | Reach for |
|---|---|
| "reverse", "reverse between m and n", "reverse nodes in k-group" | Reversal Β§4.1 |
| "middle node", "has cycle", "cycle start", "n-th from the end", "palindrome list" | Fast & slow Β§4.2 |
| "merge two sorted", "remove n-th", "delete duplicates", "partition", "add two numbers" | Dummy head Β§4.3 |
| "reorder list", "rotate by k", "swap pairs / nodes" | Compose Β§4.4 |
Node shape throughout: class ListNode { val: number; next: ListNode | null }. Each pattern: a one-liner, a π invariant, a template, and a π€ interview lens.
βSave next, flip the arrow back, slide both pointers forward.β
prev heads the fully-reversed prefix, cur is the next node to flip, and the saved next still anchors the untouched rest. No node is ever unreachable, so nothing is lost.function reverseList(head: ListNode | null): ListNode | null {
let prev: ListNode | null = null, cur = head;
while (cur) {
const next = cur.next; // SAVE before rewiring
cur.next = prev; // flip the arrow backward
prev = cur; cur = next; // advance the window
}
return prev; // new head
}
mβ1 (use a dummy), reverse exactly nβm+1 nodes, reconnect the three boundaries. Reverse in k-groups: check k nodes exist, reverse them, recurse/iterate on the rest, stitch.O(1) space, then the recursive version (and its O(n) stack). Β· Escalation: reverse β reverse sublist [m,n] β reverse in k-groups (the boundary reconnection is the real test).βTwo speeds turn βwhere's the middle / is there a loop?β into one pass.β
Setup: slow += 1, fast += 2. This is the linked-list face of the fast & slow two-pointer β the proofs live there; here are the four staples.
fast has taken 2t steps, slow has taken t β so slow is always at the halfway mark of where fast reached. That single fact gives you the middle, and (with a cycle) forces a meeting.// Middle node (for even length, returns the second middle)
function middleNode(head: ListNode | null): ListNode | null {
let slow = head, fast = head;
while (fast && fast.next) { slow = slow!.next; fast = fast.next.next; }
return slow;
}
// Palindrome list: find middle β reverse 2nd half β compare
function isPalindrome(head: ListNode | null): boolean {
let slow = head, fast = head;
while (fast && fast.next) { slow = slow!.next; fast = fast.next.next; }
let prev: ListNode | null = null; // reverse from slow
while (slow) { const n = slow.next; slow.next = prev; prev = slow; slow = n; }
let a = head, b = prev; // compare halves
while (b) { if (a!.val !== b.val) return false; a = a!.next; b = b.next; }
return true;
}
while (fast && fast.next) guard, and why slow lands on the middle. Β· Escalation: middle β detect cycle β cycle start (the reset trick) β palindrome (restore the list afterward if asked).βPut a fake node in front so the head is never a special case.β
Setup: create dummy with dummy.next = head, keep a moving tail/prev, and return dummy.next. Deletions and merges that would touch the head now Just Work.
dummy.next at the end β whatever the head turned out to be.dummy.next is always the true current head, and tail is the last node of the result built so far. Because dummy never moves, the answer is one dereference away no matter how the front changes.// Merge two sorted lists
function mergeTwoLists(a: ListNode | null, b: ListNode | null): ListNode | null {
const dummy = new ListNode(0); let 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 ?? b; // attach the remaining run
return dummy.next;
}
// Remove n-th node from the end β one pass, dummy avoids the head edge-case
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
const dummy = new ListNode(0, head); let fast = dummy, slow = dummy;
for (let i = 0; i < n; i++) fast = fast.next!; // open a gap of n
while (fast.next) { fast = fast.next; slow = slow.next!; }
slow.next = slow.next!.next; // slow sits just before the target
return dummy.next;
}
n in remove-n-th. Β· Escalation: merge β "Merge k Sorted Lists" (β a heap; see the Heap sheet) β partition list β add two numbers (carry).βHard list problems = find-middle + reverse + merge, glued together.β
Setup: the toughest problems decompose into the primitives above. The discipline that makes them work: null-terminate between stages so you don't accidentally weld a cycle.
slow.next = null) before reversing the second, so the interleave step can't loop forever on a dangling link.// Reorder List: 1β2β3β4β5 becomes 1β5β2β4β3
function reorderList(head: ListNode | null): void {
if (!head || !head.next) return;
let slow = head, fast = head; // 1) middle
while (fast.next && fast.next.next) { slow = slow.next!; fast = fast.next.next; }
let second: ListNode | null = slow.next; slow.next = null; // cut!
let prev: ListNode | null = null; // 2) reverse 2nd half
while (second) { const n = second.next; second.next = prev; prev = second; second = n; }
let first: ListNode | null = head; second = prev; // 3) interleave
while (second) {
const f = first!.next, s = second.next;
first!.next = second; second.next = f;
first = f; second = s;
}
}
// Swap nodes in pairs β dummy + careful rewire
function swapPairs(head: ListNode | null): ListNode | null {
const dummy = new ListNode(0, head); let prev = dummy;
while (prev.next && prev.next.next) {
const a = prev.next, b = a.next!;
a.next = b.next; b.next = a; prev.next = b; // prev β b β a β β¦
prev = a;
}
return dummy.next;
}
| Operation | Time | Space | Note |
|---|---|---|---|
| Reverse / traverse / fast-slow / splice | O(n) | O(1) | iterative; recursion adds O(n) stack |
| Access the i-th node | O(n) | O(1) | no random access β this is the whole trade-off vs an array |
| Merge k lists (with a heap) | O(n log k) | O(k) | heap of the k current heads |
O(1) space and O(n) time as long as you never re-walk from head inside a loop. The instant you do (e.g. "find length, then for each node re-scan"), you've slipped to O(nΒ²). Keep running pointers.| Bug | The input that exposes it |
|---|---|
Overwriting .next before saving it β orphaning the tail. | Any reverse of length β₯ 2 without a temp β you lose everything after node 1. |
| No dummy head β mishandling deletion of the first node. | Remove the head, or a list that becomes empty β null-pointer or wrong head returned. |
Fast/slow without the fast && fast.next guard. | Empty list [] or single node [1] β crash on fast.next.next. |
| Forgetting to null-terminate after splitting. | Reorder / palindrome on even length β the interleave/compare loops forever (a welded cycle). |
Off-by-one in remove-n-th (gap of n vs n+1). | n == length (remove the head) β without the dummy + gap-of-n, you skip the wrong node. |
| Comparing node identity when you meant value (or vice-versa). | Cycle detection vs duplicate values β equal values are not the same node. |
// 86 Partition List β two lists behind two dummy heads, then stitch
function partition(head: ListNode | null, x: number): ListNode | null {
const lessHead = new ListNode(0), geHead = new ListNode(0);
let l = lessHead, g = geHead;
for (let n = head; n; n = n.next) {
if (n.val < x) { l.next = n; l = n; }
else { g.next = n; g = n; }
}
g.next = null; // the last β₯x node may still point into the <x part β cycle if left
l.next = geHead.next;
return lessHead.next;
}
// 708 Insert into a Sorted Circular Linked List
function insertCircular(head: ListNode | null, x: number): ListNode {
if (!head) { const n = new ListNode(x); n.next = n; return n; }
let prev = head, cur = head.next!;
do {
if (prev.val <= x && x <= cur.val) break; // normal slot
if (prev.val > cur.val && (x >= prev.val || x <= cur.val)) break; // wrap point: max β min
prev = cur; cur = cur.next!;
} while (prev !== head); // one full lap (all-equal list falls through)
prev.next = new ListNode(x, cur);
return head;
}
g.next = null is mandatory. 708: three cases β normal slot, wrap point, full lap with no match (insert anywhere) β plus the empty list. Return the original head.Own each pattern and these are in range. Bold = Hard tier.
| Pattern | Representative LeetCode | Tier |
|---|---|---|
| Reverse | 206 Reverse List Β· 92 Reverse II Β· 25 Reverse Nodes in k-Group | Easy β Hard |
| Fast & slow | 141/142 Cycle Β· 876 Middle Β· 234 Palindrome Linked List | Easy β Med |
| Dummy head | 21 Merge Two Β· 19 Remove Nth from End Β· 83/82 Remove Dups Β· 2 Add Two Numbers Β· 86 Partition List | Easy β Med |
| Compose | 143 Reorder List Β· 148 Sort List Β· 138 Copy w/ Random Pointer Β· 708 Sorted Circular Insert Β· 23 Merge k Sorted Lists | Med β Hard |
Cover the right column (blurred β hover/tap to reveal). Say your answer out loud first.
| Prompt | Answer |
|---|---|
| What are the two superpowers that prevent most linked-list bugs? | A dummy/sentinel head (kills the head special-case) and save-before-rewire (stash next before overwriting a pointer). |
| In reversal, name the three pointers and the per-step order. | prev, cur, next. Each step: save next β cur.next = prev β advance prev and cur. |
| Why does the dummy head simplify deletion? | You always have a real prev node, even for the first element, so "delete the head" is no longer special. Return dummy.next. |
| How do you remove the n-th node from the end in one pass? | Dummy + two pointers; advance fast n steps first, then move both until fast.next is null β slow sits just before the target. |
| What three primitives compose "Reorder List," and what's the easy-to-miss step? | Find middle, reverse second half, interleave β and slow.next = null to cut, so you don't weld a cycle. |
| When should you copy a list to an array instead? | When you need random access, binary search, or backward traversal β a singly list can't do those in O(1). |
What turns an O(n) list solution into O(nΒ²)? | Re-walking from head inside a loop. Keep a running pointer instead. |
| Partition List β why must the "greater" list's tail be set to null? | Its last node may still point to a node now in the "less" list; leaving it creates a cycle. |
| Insert into a sorted circular list β the cases? | Normal slot (prev β€ x β€ cur); wrap point (prev > cur and x β₯ prev or x β€ cur); full lap with no match (all equal) β insert anywhere; empty list β self-loop node. |
reverseList (and the recursive form)middleNode + isPalindromemergeTwoLists + removeNthFromEndreorderList + swapPairsFor each: draw the before/after arrows and mark where you save a pointer, then write the code.