πŸ”— Linked List

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.

🧠 1 · How to think (and invent it)

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.

Your two superpowers (1) The sentinel / dummy head: a fake node in front of 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.

Watch it get invented β€” Reverse a List

Reverse 1 β†’ 2 β†’ 3 in place, O(1) space. The naive cur.next = prev looks right… and instantly orphans the rest of the list.

  1. The trap. If you flip cur.next to point backward before remembering cur.next, you can never reach node 2 or 3 again.
  2. The fix. Carry three pointers: prev (already-reversed part), cur (node being flipped), and a saved next (the untouched rest). Each step: save next β†’ flip β†’ advance both.
  3. Payoff. One pass, no extra memory; prev ends as the new head.
1 2 3 prev cur next (saved) flip 2.next β†’ 1, but only after saving 3
The whole topic in one image: save next, flip the arrow backward, slide prev and cur forward.
stepprevcursaved nextafter flip
1βˆ…121β†’βˆ…
21232β†’1
323βˆ…3β†’2 Β· return prev = 3

🎯 2 · Attack plan

AskIf yes β†’Pattern
"Reverse" β€” whole, a sublist, or in k-groups?prev/cur/next danceReversal (Β§4.1)
Middle, cycle, n-th from end, palindrome?two runners at different speedsFast & slow (Β§4.2)
Insert / delete / merge where the head might change?a sentinel dummy + spliceDummy head (Β§4.3)
Reorder / rotate / swap nodes structurally?compose the primitives aboveCompose (Β§4.4)
When a linked list is the WRONG shape Β· You need random access / sorting by value / binary search β€” copy to an array first; indexing a singly-linked list is O(n) each time.
Β· You need to walk backward β€” a singly list can't; use a doubly-linked list, a stack, or reverse it.
Β· The "trick" you reach for re-traverses from head inside a loop β†’ you've made it O(nΒ²); keep a running pointer instead.

πŸ”Ž 3 Β· Recognition map

Words in the promptReach 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

🧩 4 · The four core patterns

Node shape throughout: class ListNode { val: number; next: ListNode | null }. Each pattern: a one-liner, a πŸ”‘ invariant, a template, and a 🎀 interview lens.

4.1 Β· Reversal β€” the prev / cur / next dance

β€œSave next, flip the arrow back, slide both pointers forward.”

πŸ”‘ Invariant β€” why it's safe At every step, 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
}
Builds on itself Reverse between [m, n]: walk to node 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.
🎀 Interview lens Probed: save-next-first, 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).

4.2 Β· Fast & slow runners

β€œ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.

πŸ”‘ Invariant β€” why it's safe When 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;
}
🎀 Interview lens Probed: the 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).

4.3 Β· Dummy head & splicing

β€œ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 1 3 5 tail advances as you splice
Return dummy.next at the end β€” whatever the head turned out to be.
πŸ”‘ Invariant β€” why it's safe 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;
}
🎀 Interview lens Probed: exactly why the dummy removes the head special-case; the gap of n in remove-n-th. Β· Escalation: merge β†’ "Merge k Sorted Lists" (β†’ a heap; see the Heap sheet) β†’ partition list β†’ add two numbers (carry).

4.4 Β· Compose the primitives

β€œ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.

1 Β· find middle 2 Β· reverse 2nd half 3 Β· interleave 1 β†’ 2 β†’ 3 β†’ 4 split at middle 1 β†’ 2   4 β†’ 3 1 β†’ 4 β†’ 2 β†’ 3
"Reorder List" is just three primitives in sequence β€” each one you already know.
πŸ”‘ Invariant β€” why it's safe Each stage leaves a clean, null-terminated list for the next. Cut the first half off (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;
}
🎀 Interview lens Probed: decompose into named primitives, and the null-terminate step that prevents an accidental cycle. Β· Escalation: reorder β†’ "Rotate List by k" (close into a ring, walk to the new tail, break) β†’ swap pairs β†’ swap k-th node from both ends.

⏱️ 5 · Complexity

OperationTimeSpaceNote
Reverse / traverse / fast-slow / spliceO(n)O(1)iterative; recursion adds O(n) stack
Access the i-th nodeO(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
The one cost to remember Everything is 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.

⚠️ 6 Β· Pitfalls β€” and the test that catches each

BugThe 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.
πŸ”— This pattern, elsewhere Fast & slow is the two-pointer engine from two-pointers.html β€” and "Find the Duplicate Number" treats an array as a linked list to reuse it. Merge two lists is the two-sequence merge (two-pointers.html); merge k lists upgrades it with a heap. Reverse & merge have clean recursive forms β€” see recursion.html.
βž• Two more list shapes β€” Partition List (86) Β· Insert into Sorted Circular List (708)
86 is the two-dummy-heads trick (same family as Odd Even, 328). 708 is pure edge-case discipline: a normal slot, the wrap point, and the all-equal list.
// 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;
}
⚠️ Traps 86: 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.

πŸ“Š Coverage β€” pattern β†’ LeetCode (medium & hard)

Own each pattern and these are in range. Bold = Hard tier.

PatternRepresentative LeetCodeTier
Reverse206 Reverse List Β· 92 Reverse II Β· 25 Reverse Nodes in k-GroupEasy β†’ Hard
Fast & slow141/142 Cycle Β· 876 Middle Β· 234 Palindrome Linked ListEasy β†’ Med
Dummy head21 Merge Two Β· 19 Remove Nth from End Β· 83/82 Remove Dups Β· 2 Add Two Numbers Β· 86 Partition ListEasy β†’ Med
Compose143 Reorder List Β· 148 Sort List Β· 138 Copy w/ Random Pointer Β· 708 Sorted Circular Insert Β· 23 Merge k Sorted ListsMed β†’ Hard

βœ… 7 Β· Self-check

Cover the right column (blurred β€” hover/tap to reveal). Say your answer out loud first.

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

Write these cold (no peeking)

For each: draw the before/after arrows and mark where you save a pointer, then write the code.