Another Way In

You have the iterative approach locked down. Three pointers, four steps, O(1) space, done. But there is another way to reverse a linked list — one that some people find more elegant and others find terrifying. It depends on your relationship with recursion.

The idea is seductive in its simplicity. Instead of marching three pointers through the list, you ask the function to do it for you: “Reverse everything after this node, then fix the one edge I'm responsible for.” Each call delegates the hard work to the next call, which delegates to the next, all the way down to the base case — a single node or null, which is already reversed by definition.

12345

Tap Delegate to start the recursive descent.

Here is the skeleton:

1
function reverseList(head: ListNode | null): ListNode | null {
2
  if (head === null || head.next === null) return head
3
  const newHead = reverseList(head.next)  // trust the recursion
4
  head.next.next = head                    // reverse this one edge
5
  head.next = null                         // sever forward link
6
  return newHead
7
}

Read it carefully. The recursive call on line 3 says: "Reverse everything from head.next onward, and give me back the new head." When that call returns, head.next still points to the node that used to follow head — but that node's next has been redirected by the recursion. So head.next.next = head redirects it back to head, completing the reversal for this pair. Then head.next = null severs the old forward link, exactly like the iterative save-before-sever you already know.

The elegance is real. Two lines of mutation, no explicit loop, no pointer march. But there is a cost hidden behind that elegance — literally hidden, inside the runtime's call stack. Every time reverseList calls itself, the system pushes a new stack frame onto the call stack. That frame holds the local variable head, the return address, and the reference to newHead. It sits there, suspended, waiting for the deeper calls to finish.

For a 5-node list, that means 5 frames stacked on top of each other before a single edge gets reversed. For a 10,000-node list, that means 10,000 frames — and most JavaScript runtimes have a stack limit somewhere between 10,000 and 25,000 frames. Cross the limit and you get a RangeError: Maximum call stack size exceeded. The elegant two-liner just crashed your program.

n=1n=510K+

1 frame on the stack — each holds a reference to its node.

The iterative version you built uses three pointer variables — O(1) space, no matter how long the list. The recursive version uses O(n) space in stack frames. Same result, radically different memory profiles.

Below, you will trace every recursive call and watch the call stack grow. You will see the frames pile up, the base case trigger, and the unwind begin. And you will discover that the “save” mechanism you learned in the iterative version — the invariant that says you must preserve a reference before overwriting it — is still here. It is just hiding inside the stack.

The Hidden Stack

Tap to call reverse(A). Watch the call stack grow.

Call Stack
Frames:0
1 / 5

Elegance Has a Price

You just watched five frames pile onto the call stack — one for every node in the list. Each frame held a reference to its node, suspended in time, waiting for the deeper calls to return. That is the hidden cost of recursion: every unsolved subproblem occupies memory until its answer arrives from below.

In the iterative version, there is no waiting. Each iteration processes one node and immediately moves on. The “save” is a local variable (next = curr.next), used once and overwritten on the next pass. At any point during the loop, only three variables exist: prev, curr, and next. That is O(1) space — constant, regardless of list length.

In the recursive version, the save mechanism is the stack frame itself. The parameter head in each frame acts as the saved reference. When the unwind reaches frame k, it uses head (which was saved when the frame was created) to redirect the edge. Same invariant, same two-step rhythm — save, then sever — but the save is implicit, performed by the language runtime when it pushes the frame.

Tap a panel to see its save mechanism

Same invariant — save before sever — three different homes.

This is why the recursive approach uses O(n) space. It is not because the algorithm is wasteful. It is because every node needs its reference preserved simultaneously, and the call stack is the data structure doing the preserving. The iterative approach reuses the same three variables at every step, throwing away the saved reference the moment it is no longer needed. The recursive approach hoards every reference until the very end.

Watch the gap grow as the list gets longer:

The practical consequence: for interview problems, both approaches are valid. LeetCode's test inputs rarely exceed a few thousand nodes, well within stack limits. But in production code — a linked list with millions of entries, or a recursive function called from deep within an existing call chain — the recursive version is a time bomb. One long list and the program crashes with a stack overflow, not because the logic is wrong, but because the memory model does not scale.

The iterative version is immune. Three variables, flat loop, O(1) space, any list length. The recursive version is elegant, concise, and carries a hidden O(n) liability that most textbooks mention in a footnote and most students forget until the interview.

You will not forget. You watched the frames stack up. You counted them. You traced the unwind. And you saw that the same invariant — save before you sever — lives at the heart of both approaches, wearing different clothes.