Reverse Linked List: three pointers and why the order matters
A linked list has no index. You can't reach node i without walking from the head, one .next at a time. That single constraint — the only path forward is through .next — means reversing the list is entirely about who owns .next at each step. Flip it to point backward. Move forward. Repeat. The danger is that flipping .next on the current node immediately cuts off your path to the rest of the list. So you save that path first. That's the whole algorithm.
Constraints and what they actually force
The list has at most 5000 nodes and values in [-5000, 5000]. The value range is irrelevant — we never compare values, only rewire pointers. The node count is modest enough that both iterative O(1) space and recursive O(n) space solutions are technically feasible.
But 5000 nodes is also exactly where Python's default recursion limit of 1000 frames becomes a concrete problem for the recursive approach. That asymmetry matters: two solutions with identical O(n) time but radically different operational risk. The constraint doesn't rule out recursion — it just makes the choice non-trivial.
The list can also be empty. A null head is a valid input and has to produce a null output, not a crash. Neither approach needs a special-case branch for this — it falls out naturally from both algorithms.
The initial shape of the problem
Before touching any code, here is what [1, 2, 3, 4, 5] looks like, and where we need to end up:
Every arrow that points right needs to point left. The node structure itself doesn't change — only .next changes on each node.
Why the order of operations is the hard part
Set up three pointers: prev = None, curr = head. You'll use a third variable, next_node, inside the loop. At each step:
- Save
curr.nextbefore you touch it. - Flip
curr.nextto point atprev. - Advance
prevtocurr. - Advance
currto the savednext_node.
If you do step 2 before step 1, you've just orphaned everything from curr.next onward. There's no pointer to that part of the list anymore, and it's gone. The save comes first. Always.
Here is what the pointer state looks like after each iteration through [1, 2, 3, 4, 5]:
Iterative solution: O(n) time, O(1) space
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
prev = None
curr = head
while curr is not None:
next_node = curr.next # save before overwriting
curr.next = prev # flip the link
prev = curr # advance prev
curr = next_node # advance curr
return prevpublic class Solution {
public ListNode ReverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextNode = curr.next; // save before overwriting
curr.next = prev; // flip the link
prev = curr; // advance prev
curr = nextNode; // advance curr
}
return prev;
}
}This is the version I'd ship. O(1) space, no stack risk, straightforward to read in a code review six months later.
Recursive solution: O(n) time, O(n) space
The recursive approach is worth understanding because it shows up in harder problems. Reverse Nodes in k-Group (25) builds on the same idea. The logic: recurse all the way to the tail, then fix links on the way back up.
Base case: if head is None or head.next is None, return head. A single node is already reversed.
Recursive case: call reverseList(head.next) and let it return the new head of the reversed sublist. At that point, head.next still points to the node that's now the tail of the reversed suffix. Make that tail node's .next point back to head, then set head.next = None to cap the list. Return the new head — it never changes throughout the recursion.
The recursion tree for [1, 2, 3] before the unwind:
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
new_head = self.reverseList(head.next)
head.next.next = head # the old second node now points back
head.next = None # head becomes the new tail
return new_headpublic class Solution {
public ListNode ReverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode newHead = ReverseList(head.next);
head.next.next = head; // the old second node now points back
head.next = null; // head becomes the new tail
return newHead;
}
}The head.next.next = head line is the one that trips people up. Before the recursive call returns, head.next still points to the old second node — which is now sitting at the tail of the reversed suffix. So head.next.next is that tail's .next field, currently None. You set it to head. Then you set head.next = None to sever the old forward link. Without that second line you get a cycle: head.next still points forward, and head.next.next now points back to head.
Step through [1, 2, 3] on the unwind:
reverseList(3)is the base case — returns3.- Back in
reverseList(2):new_head = 3,2.next.next = 2sets3.next = 2, then2.next = None. List is3 -> 2 -> None. Returns3. - Back in
reverseList(1):new_head = 3,1.next.next = 1sets2.next = 1, then1.next = None. List is3 -> 2 -> 1 -> None. Returns3.
Comparing both approaches
| Approach | Time | Space | Notes |
|---|---|---|---|
| Iterative | O(n) | O(1) | Preferred. No stack risk. Clean under code review. |
| Recursive | O(n) | O(n) | Elegant. O(n) call stack; Python hits recursion limit at 1000 frames. |
Both touch every node once — time complexity is identical. The space difference is not theoretical. With 5000 nodes, the recursive solution builds 5000 call frames. In Python that exceeds the default recursion limit and raises RecursionError unless you call sys.setrecursionlimit(6000) first. In C# the stack is deeper and survives, but you're still burning O(n) memory for no algorithmic reason. I'd only reach for the recursive version if the problem explicitly called for recursion or if I was working through the logic of k-group reversal.
Edge cases, one by one
Empty list (head = None): The iterative loop never executes. prev is None from initialization. Returns None. For recursive: the first if not head check hits, returns head (which is None). Both handle it without a special branch.
Single node: Iterative loop runs once. next_node = None. curr.next = None (it already was). prev = curr. curr = None. Returns prev. Correct — a single node is its own reversal. Recursive: not head.next is true, returns head immediately.
Two nodes (1 -> 2 -> None): Iterative runs twice. After iteration 1: None <- 1, curr=2. After iteration 2: None <- 1 <- 2, curr=None. Returns 2. This is the smallest non-trivial case and it's worth tracing manually once.
List at max constraint (5000 nodes): Iterative handles this in O(n) time, O(1) space, no issues. Recursive will hit Python's default stack limit at 1000 frames — either bump sys.setrecursionlimit or use iterative.
All identical values: Values are irrelevant — the algorithm only touches .next pointers. A list of 5000 nodes all valued 0 reverses identically to a list with all distinct values. No degenerate behavior.
The common mistake is forgetting to save next_node before overwriting curr.next. That destroys the reference to the rest of the list and there's no recovery. The second most common is returning head instead of prev at the end of the iterative loop — head still points to the original first node, which is now the tail of the reversed list.
The mental model and when to reach for it
The keyword triggers are "reverse," "backward," and "flip" applied to a linked list. More broadly: any problem where you need to modify .next pointers in-place without extra storage. The shape is always the same — you need to track where you were, where you are, and where you're going simultaneously. That's exactly what prev, curr, and next_node do.
This reversal is a building block. Harder problems decompose into "reverse a portion of the list," and the three-pointer technique ports directly. The mental model: grow the reversed chain on the left while the original chain shrinks on the right.
This is the first problem in the linked list series. It establishes the fundamental pointer manipulation habit — save before you overwrite — that every subsequent problem builds on.
Related problems and the twist each adds
- 92. Reverse Linked List II — reverse a sublist from position
lefttoright. The same three-pointer technique, but you have to locate the start of the subrange first and stitch the reversed segment back into the surrounding list. - 25. Reverse Nodes in k-Group — reversal applied to windows of length
k. You call the iterative reversal helper repeatedly as you walk the list; the tricky part is handling the last group when fewer thanknodes remain. - 24. Swap Nodes in Pairs — a special case of k-group reversal with
k = 2. The constraints allow a recursive solution more naturally here, since the maximum list length is 100 (no stack overflow risk). - 143. Reorder List — find the midpoint (Floyd's cycle detection), reverse the second half using this exact algorithm, then interleave the two halves. Reversal is one of three distinct steps.
- 234. Palindrome Linked List — reverse the second half, compare element-by-element with the first half, optionally restore. Reversal here is a subroutine with a cleanup requirement.
The muscle memory being built here is "save before you overwrite." Every linked list problem that modifies .next in-place relies on exactly that habit.
Part of the series: Linked List
Occasional notes on what I'm building
Get an email when I publish a new post — engineering write-ups, no spam. Unsubscribe anytime.
Comments
No comments yet. Be the first.
