The Explicit Stack

There is a third approach to reversing a linked list, and it sits exactly between the recursive and iterative solutions — both conceptually and in memory usage. Instead of letting the language runtime manage a call stack, you manage one yourself.

The idea: traverse the list once, pushing every node onto an explicit stack (a plain array). When you are done, the last node you visited — the one that should be the new head — is sitting on top of the stack. Pop it. That is your new head. Pop the next node. Link it after the first. Pop again. Link again. Repeat until the stack is empty. The list is reversed.

stack12345

Tap Push to move nodes onto the stack.

In code:

1
function reverseWithStack(head: ListNode | null): ListNode | null {
2
  if (head === null) return null
3
  const stack: ListNode[] = []
4
  let curr = head
5
  while (curr) {
6
    stack.push(curr)
7
    curr = curr.next
8
  }
9
  const newHead = stack.pop()!
10
  let prev = newHead
11
  while (stack.length > 0) {
12
    const node = stack.pop()!
13
    prev.next = node
14
    prev = node
15
  }
16
  prev.next = null
17
  return newHead
18
}

Two loops, one stack. The first loop pushes; the second loop pops and relinks. The stack reverses the order — last in, first out — so the last node pushed becomes the first node popped. No recursion, no call stack growth, but O(n) extra space for the explicit stack. Same memory cost as the recursive approach, just allocated on the heap instead of the call stack. The difference matters: heap-allocated memory does not have a fixed depth limit, so this version won't crash on long lists. But it still uses O(n) space.

Compare all three approaches:

The iterative version wins on space. The recursive version wins on conciseness. The explicit stack version wins on... nothing, really. Same O(n) time as the others, but worse constants than iterative and O(n) extra space for the heap-allocated stack. No elegance advantage over recursive either. So why learn it?

Because it makes the “save” mechanism visible. In the recursive version, saved pointers live in stack frames you cannot see. In the iterative version, they live in three local variables that get recycled every iteration. In the explicit stack version, they live in an array you built yourself, on screen, in front of you.

Same invariant — save before you sever — but stored differently each time. The exercise below makes that claim concrete.

Stack to Loop

Tap to push each node onto the stack. Watch the order.

Stack
Size:0
1 / 4

One Invariant, Three Disguises

Seven screens. Three acts. One invariant.

In Act I (The Severed Link The Three-Pointer March) you shattered a list — watching nodes vanish into the void because you severed a pointer without saving the forward reference first. That moment of destruction taught you the rule that governs every reversal: save before you sever. The next = curr.next bookmark captures the forward link before curr.next = prev destroys it. You then built the four-step loop — save, sever, advance prev, advance curr — and learned that returning head instead of prev is the number-one reversal bug.

In Act II (Reverse the Middle The Four-Wire Stitch Groups of K) you applied that invariant inside a larger list. You walked to the boundary, saved a connection anchor, reversed a bounded sublist, then stitched the four wires back into place. You repeated the protocol in groups of k. The reversal loop body never changed — only the saves around it.

In Act III (The Hidden Stack Stack to Loop) you peeled back the curtain on two alternative implementations and discovered they are not really alternatives at all. The recursive version delegates reversal to deeper calls, but each call pushes a frame onto the call stack — and that frame IS the save mechanism. The explicit-stack version makes the same trade visible: push every node, then pop in reverse. Both use O(n) space. The iterative version uses O(1). Same result, three different memory strategies.

The insight that ties everything together: all three approaches obey the same invariant.

Save before severnext = curr.nextLocal variablehead parameterStack framestack.push(node)Array slot

The iterative version saves into a local variable. The recursive version saves into a stack frame. The explicit-stack version saves into a heap array. But the act of saving — capturing the reference before mutating the pointer — is identical in all three. The difference is storage, not strategy.

This matters beyond linked list reversal. Every in-place mutation algorithm has the same tension: you need the old value after overwriting it, so you must capture it first. Test it for yourself on a problem we have not covered — swap adjacent pairs (LC 24). Find the line that commits the save-before-sever invariant.

Transfer — LC 24 · Swap Pairs

This is the swap body for a -> b. Which line commits the *save-before-sever* invariant?

You did not memorize three algorithms. You understood one principle — applied three ways. That is the difference between knowing the answer and knowing why the answer works.