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:
prev is null, curr is at node 1, next bookmarks node 2.
Each iteration follows the same four steps:
next = curr.next — capture the forward reference before it's destroyedcurr.next = prev — flip the arrow backward, attaching curr to the reversed chainprev = curr — slide prev forward so it points to the node you just reversedcurr = next — slide curr forward to the next unprocessed node using the bookmarkSteps 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:
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?
Before touching any pointers, what do you need to save?
Here's the complete reversal function. Seven lines of working code — and you've already performed every one of them by hand:
function reverseList(head: ListNode | null): ListNode | null { let prev: ListNode | null = null let curr = head while (curr !== null) { const next = curr.next // save the bookmark curr.next = prev // sever the forward link prev = curr // grow the reversed chain curr = next // march forward } return prev}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
currin the original list have been reversed into a chain whose head isprev, and the nodes FROMcurronward are still in their original forward order. That statement is true at iteration 0 (zero nodes reversed,prev = null), and thefour-stepbody preserves it: save captures the forward link, sever wirescurronto 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 isprev— 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-bearing — swap any two adjacent lines and something breaks. Try it:
Toggle between the original list and the reversed result to see what those four steps produce:
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:
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:
This loop body is the atomic unit of linked list manipulation. It never changes. What changes in harder problems is the scaffolding around it:
m through n), you walk past the first m - 1 nodes before you begin reversing. The loop body is identical once you start.while (curr !== null), you count k steps and stop. Same four operations inside the loop, different termination condition outside it.That's Act II. Same loop body. Different bookkeeping.