One Node at a Time

You know the rule: save curr.next before you overwrite it. That's one operation — the crucial first operation. But a complete reversal needs four operations per node, executed in strict order, repeated for every node in the list. The save-before-sever invariant is step 1. What are the other three?

The technique uses three pointers that march through the list in lockstep:

prev — the tail of the reversed portion. It starts at null (nothing has been reversed yet) and grows by one node each iteration. Think of it as a bucket collecting reversed nodes. After the very first iteration, prev points to what used to be the head. After two iterations, it points to the second node, whose next now points backward to the first. The reversed chain builds from front to back, with prev always sitting at the newest addition.

curr — the node currently being processed. This is where all four operations happen. Once they're done, curr advances to the next unprocessed node and the cycle repeats. When curr reaches null, every node has been processed and the loop is finished.

next — the bookmark. Before you sever curr.next, you save it here. After the sever, next is the only remaining path to the rest of the list. It exists for exactly one purpose: to survive the destruction of curr.next so that curr can advance forward after the flip. It's temporary — overwritten fresh at the start of every iteration — but without it, the march stops dead.

Tap through to watch all three pointers march in lockstep:

1234nullprev →∅currnext

prev is null, curr is at node 1, next bookmarks node 2.

Each iteration follows the same four steps:

  1. Save: next = curr.next — capture the forward reference before it's destroyed
  2. Sever: curr.next = prev — flip the arrow backward, attaching curr to the reversed chain
  3. Advance prev: prev = curr — slide prev forward so it points to the node you just reversed
  4. Advance curr: curr = next — slide curr forward to the next unprocessed node using the bookmark

Steps 1 and 2 are the invariant you discovered on the last screen — save, then sever. Steps 3 and 4 are the march: sliding the prev and curr pointers one position forward so the next iteration can repeat the same rhythm on the next node. Four steps, strict order, every iteration identical.

Here's what one iteration looks like on a list 1 → 2 → 3, starting with prev = null and curr = 1. Walk through each sub-step:

123nullprev →∅curr

Starting state: prev = null, curr = node 1, list is 1→2→3.

After this single iteration, the reversed portion is just [1] pointing to null, and the remaining list is [2 → 3]. Three pointers moved, zero nodes lost, one arrow flipped. Three more iterations and the entire list is reversed.

Below is a four-node list. You'll march through all four iterations with scaffolding that gradually drops away — first with prediction gates that check your understanding, then with just the action queue, then from memory alone.

Can you reverse the entire list without dropping a single node?

March Through It

March phase, iteration 1 of 4, guided tier
1 / 4
🧭Guided
prev=null
curr=1
next=???

Before touching any pointers, what do you need to save?

The Loop Body

Here's the complete reversal function. Seven lines of working code — and you've already performed every one of them by hand:

1
function reverseList(head: ListNode | null): ListNode | null {
2
  let prev: ListNode | null = null
3
  let curr = head
4
  while (curr !== null) {
5
    const next = curr.next   // save the bookmark
6
    curr.next = prev          // sever the forward link
7
    prev = curr               // grow the reversed chain
8
    curr = next               // march forward
9
  }
10
  return prev
11
}

Walk through it line by line. let prev = null — the reversed chain starts empty. null isn't a placeholder; it's semantically correct. Before the loop begins, zero nodes have been reversed, so the reversed chain is literally nothing. (This null also becomes the next value of the old head node after the first sever — which is exactly right, because the old head becomes the new tail, and tails point to null.)

let curr = head — the march begins at the front of the list. The first node to be processed is the first node in the original list.

while (curr !== null) — the march continues until curr walks off the end. Remember the final iteration of your march: next was null (because the last node's next is null), then curr = next set curr to null, and the loop condition caught it. No special-casing for the last node. No off-by-one. The null sentinel at the end of every linked list is doing real work here — it's the natural termination condition.

Edge cases — and how the same four lines handle them. What if the list is empty? head === null, so curr = null from the start, the loop condition curr !== null fails immediately, the body never runs, and we return prev, which is still the initial null. Exactly right: reversing an empty list gives an empty list. What if the list has a single node? curr = head (the one node), one iteration runs: next = curr.next (which is null), curr.next = prev (which is null, so the single node now terminates with null — unchanged), prev = curr, curr = next (which is null, exiting the loop). We return prev, the same single node. No branches, no null-guards, no special cases — the same four lines handle zero, one, and many nodes. That is what elegant loop design looks like.

Loop invariant. At the start of every iteration, the nodes BEFORE curr in the original list have been reversed into a chain whose head is prev, and the nodes FROM curr onward are still in their original forward order. That statement is true at iteration 0 (zero nodes reversed, prev = null), and the four-step body preserves it: save captures the forward link, sever wires curr onto the reversed chain, and the two advances slide both pointers one node forward. When the loop exits (curr = null), the invariant says the entire list has been reversed into a chain whose head is prev — which is exactly what we return.

The four-step body maps directly to the metaphors you've been using. const next = curr.next takes the photo. curr.next = prev walks through the door. prev = curr collects the current node into the reversed chain. curr = next uses the bookmark to keep marching forward. The order is load-bearingswap any two adjacent lines and something breaks. Try it:

1const next = curr.next
2curr.next = prev
3prev = curr
self-loop!next123null
Viewing: Correct order

Toggle between the original list and the reversed result to see what those four steps produce:

1234null

return prev — and here's the trap that catches almost everyone at least once. Why prev and not head? After the loop, curr is null — it marched past the last node. But head was never reassigned inside the loop. The variable head still references the original first node. And that node? Its next pointer was severed to null way back in the very first iteration. It's the tail now. See the difference:

4321nullprev ✓

prev is the new head. Full reversed chain: 4→3→2→1→null.

You caught this in the interactive — or it caught you. Either way: always return prev.

Time complexity: O(n). Each node is visited exactly once. The loop marches forward through the list, never doubling back, never revisiting a node. One pass, start to finish.

Space complexity: O(1). Three pointer variables — prev, curr, next — regardless of whether the list has four nodes or four million. This is the whole point of the iterative approach. The recursive version of reversal uses O(n) stack space (one call frame per node), which is why interviewers will often ask for the iterative technique specifically — and why this four-step loop body is worth memorizing.

Drag the slider and feel how flat the cost stays — one pointer flip per node, no matter how long the chain:

nodes =4
pointer flips:3
One pass. O(n). Always.

This loop body is the atomic unit of linked list manipulation. It never changes. What changes in harder problems is the scaffolding around it:

  • Where the march starts: In sublist reversal (reverse nodes m through n), you walk past the first m - 1 nodes before you begin reversing. The loop body is identical once you start.
  • Where the march stops: Instead of while (curr !== null), you count k steps and stop. Same four operations inside the loop, different termination condition outside it.
  • What you stitch afterward: Reversing a subsection means the reversed portion has two boundary edges — one connecting it to the prefix of the list, one connecting it to the suffix. Tracking those four boundary pointers (the node before the reversed section, the first reversed node, the last reversed node, the node after the reversed section) is the hard part. The reversal itself is the same four steps you just practiced.

That's Act II. Same loop body. Different bookkeeping.