Ch. 18 · Data Structures & Algorithms

Reverse a Linked List: Iterative, Recursive and Sublist Versions

Reverse a linked list by tracking who keeps each node reachable: a traced JavaScript loop, the lost-tail bug, sublists and palindrome checks.

~6 min readintermediateupdated Oct 11, 2026

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 is prev
  • an untouched part, 3 → 4, whose head is curr

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;
}
JavaScript

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 ]
Text

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;
}
JavaScript
buggy: [ 1 ]
Text

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;
}
JavaScript

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 ]
Text

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;
}
JavaScript
[ 1, 4, 3, 2, 5 ] [ 5, 4, 3, 2, 1 ] [ 5 ]
Text

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;
}
JavaScript
[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]
Text

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

More in Data Structures & Algorithms

read ✓Data Structures & Algorithms · mid

Linked List Cycles and Fast-Slow Pointers

Floyd's tortoise and hare in O(1) space: why fast meets slow, the null check order, finding the cycle's entry and the duplicate-number twist.

~7 min readread →
read ✓Data Structures & Algorithms · easy

Two Pointers and Monotonic Search Decisions

Two pointers work when each move rules out answers you can prove are worse: sorted pair sums, Container With Most Water, 3Sum and palindromes.

~7 min readread →
esc