Reversing a singly linked list is the “hello world” of pointer manipulation, and it shows up in interviews both on its own and as a step inside harder problems (Reorder List, palindrome checks, reversing in groups of k). The loop is only four lines. What makes it worth studying is the habit it teaches: at every moment, know which variable is keeping each part of the list reachable. Lose track of that once and part of the list is gone, with no error to tell you.
The code here is JavaScript, run on Node 22. The same logic works line for line in Python, Java or Go.
Two lists in flight
Halfway through reversing 1 → 2 → 3 → 4, you don’t have one list. You have two:
- a reversed part,
2 → 1, whose head isprev - an untouched part,
3 → 4, whose head iscurr
Each step moves one node from the front of the untouched part to the front of the reversed part. When the untouched part is empty, prev is the head of the fully reversed list. That’s the invariant, and every line of the loop exists to preserve it:
class ListNode {
constructor(val, next = null) { this.val = val; this.next = next; }
}
function reverseList(head) {
let prev = null; // head of the reversed part
let curr = head; // head of the part not yet touched
while (curr !== null) {
const next = curr.next; // 1. save the rest of the list
curr.next = prev; // 2. flip one pointer
prev = curr; // 3. grow the reversed part
curr = next; // 4. shrink the untouched part
}
return prev;
}At the start, the reversed part is empty (prev = null) and the untouched part is the whole list, so the invariant holds trivially. With a log line added after step 4, here’s the real trace for [1, 2, 3, 4]:
reversed=[1] untouched=[2,3,4]
reversed=[2,1] untouched=[3,4]
reversed=[3,2,1] untouched=[4]
reversed=[4,3,2,1] untouched=[]
[ 4, 3, 2, 1 ]Each line shows both lists intact after the step. Nothing is ever unreachable. The empty list and a single node come back as [] and [ 7 ] without special cases, because the loop runs zero times or once.
Who owns the rest of the list?
Think about step 2, curr.next = prev, in terms of ownership. Before that line, the only thing pointing at node 3 (when curr is 2) is curr.next. The moment you overwrite it, node 3 and everything after it is reachable only through whatever variable you saved it in. That’s why step 1 exists. Skip it, and you lose the tail:
function reverseListBuggy(head) {
let prev = null, curr = head;
while (curr !== null) {
curr.next = prev; // flipped before saving curr.next
prev = curr;
curr = curr.next; // this is prev now, i.e. the old previous node
}
return prev;
}buggy: [ 1 ]On the first pass, curr.next = prev sets node 1’s next to null, and then curr = curr.next reads that null back. The loop ends after one iteration. Nodes 2, 3 and 4 are still in memory, but no variable points to them, so in JavaScript the garbage collector will reclaim them, and in C you’d have leaked them. No exception, just a wrong answer.
The ordering rule generalises to every pointer-rewiring problem: before you overwrite a pointer, make sure whatever it pointed to is reachable some other way. When an interviewer watches you write this loop, saying “I save next first because I’m about to overwrite the only reference to the rest of the list” is the kind of narration that signals you understand rather than recall.
Recursion is elegant and limited
The recursive version reverses everything after the head first, then hooks the head onto the end:
function reverseRecursive(head) {
if (head === null || head.next === null) return head;
const newHead = reverseRecursive(head.next);
head.next.next = head; // the node after me now points back to me
head.next = null; // and I become the tail of the reversed part
return newHead;
}The line that confuses people is head.next.next = head. After the recursive call returns, head.next still points to the node that used to follow head, and that node is now the tail of the reversed rest. Pointing its next back at head appends head to the end. Then head.next = null stops head from pointing forward, which would otherwise create a two-node cycle.
It returns [ 4, 3, 2, 1 ] on the small list. On a 100,000-node list, Node’s default stack isn’t deep enough:
recursive 100k: RangeError: Maximum call stack size exceeded
iterative 100k: [ 99999, 99998, 99997 ]Both versions run in O(n) time. The iterative one uses O(1) extra space; the recursive one uses O(n) stack frames, one per node. JavaScript engines don’t reliably perform tail-call elimination, and this recursion isn’t a tail call anyway (work happens after the call returns). If an interviewer asks for the recursive version, write it, then mention the stack cost unprompted.
Reversing just a sublist
A common extension: reverse only positions left to right (1-indexed). For 1 → 2 → 3 → 4 → 5 with left = 2 and right = 4, the result is 1 → 4 → 3 → 2 → 5. You now have four pointers to own: the node before the sublist, the sublist’s original first node (which becomes its tail), the reversed sublist’s new head, and the node after it.
function reverseBetween(head, left, right) {
const dummy = new ListNode(0, head);
let before = dummy;
for (let i = 1; i < left; i++) before = before.next; // node just before the sublist
const first = before.next; // becomes the sublist's tail
let prev = null, curr = first;
for (let i = left; i <= right; i++) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
before.next = prev; // reconnect the front to the new sublist head
first.next = curr; // reconnect the sublist's tail to the rest
return dummy.next;
}[ 1, 4, 3, 2, 5 ] [ 5, 4, 3, 2, 1 ] [ 5 ]The inner loop is the same four lines as before, run right - left + 1 times. After it, prev is the new sublist head and curr is the first node after the sublist, so two assignments stitch everything back together.
The dummy node is what makes left = 1 work. Without it, there’s no “node before the sublist” when the reversal starts at the head, and you’d need an if to update head itself. With a dummy pointing at the head, every position has a predecessor, and returning dummy.next gives the correct head whether or not it changed. The same trick simplifies Merge Two Sorted Lists and most deletion problems.
Interview practice: palindrome linked list
Return whether a singly linked list reads the same forwards and backwards, using O(1) extra space. Copying the values into an array would be O(n) space, so that’s out. Bonus: leave the list exactly as you found it.
Answer
Combine two patterns. Use fast and slow pointers to find the end of the first half, reverse the second half in place, then walk both halves together comparing values. Finally, reverse the second half again and reattach it, so the caller’s list is unchanged.
function isPalindrome(head) {
if (head === null) return true;
// 1. find the end of the first half (first middle on even lengths)
let slow = head, fast = head;
while (fast !== null && fast.next !== null && fast.next.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. reverse the second half in place
const secondHead = reverseList(slow.next);
// 3. compare the halves
let ok = true;
for (let a = head, b = secondHead; b !== null; a = a.next, b = b.next) {
if (a.val !== b.val) { ok = false; break; }
}
// 4. put the list back the way we found it
slow.next = reverseList(secondHead);
return ok;
}[1,2,2,1] true list afterwards: [1,2,2,1]
[1,2] false list afterwards: [1,2]
[1,2,3,2,1] true list afterwards: [1,2,3,2,1]
[1] true list afterwards: [1]
[] true list afterwards: []
[1,2,3,1] false list afterwards: [1,2,3,1]The loop condition stops slow on the last node of the first half: node 2 (the first of the two middles) for [1, 2, 2, 1], and the exact middle 3 for [1, 2, 3, 2, 1]. On odd lengths the middle node stays in the first half and is never compared, which is correct because it matches itself. The comparison loop runs until the second half ends, since it’s never longer than the first.
Time is O(n): one pass to find the middle, one to reverse, one to compare, one to restore. Extra space is O(1). The restore step is the part many candidates skip. It’s worth doing, or at least mentioning, because mutating an input the caller still owns is a real bug in production code, and interviewers notice when you think about it. Practise the building block on Reverse Linked List, then combine it with the middle-finding loop in Reorder List.
Further practice
- Data Structures and Algorithms interview questions
- DSA multiple-choice questions
- Linked List Cycles and Fast-Slow Pointers, for the middle-finding step
- Algorithm Space Complexity and Auxiliary Storage, on why recursion isn’t free
- MIT 6.006 Introduction to Algorithms (Spring 2020)