Reversing a Linked List
Reversing a linked list means flipping the direction of its next pointers so that the last node becomes the first and the first becomes the last, without allocating a new list. It is one of the most common linked list problems in coding interviews, and mastering it teaches the pointer-manipulation skills you need for harder list problems, like detecting cycles, merging lists, or reversing only part of a list in place.
Overview: How Reversing a Linked List Works
A singly linked list is a chain of nodes where each node only knows about the node that comes after it, via its next pointer. There is no pointer back to the previous node, so reversal cannot be done by simply swapping head and tail the way you might with an array-backed structure — every single next pointer in the chain has to be flipped to point the other way.
Picture the list 1 -> 2 -> 3 -> None. To reverse it in place, you walk the list once, and for every node you visit you redirect its next pointer to the node you just came from instead of the node ahead of it. You need three references to do this safely:
prev— the node that should come after the current node once reversed (starts asNone, since the original head becomes the new tail and should point to nothing).current— the node you are currently rewiring (starts athead).next_node— a temporary that savescurrent.nextbefore you overwrite it, so you don’t lose the rest of the list.
That third variable is the whole trick. If you overwrite current.next before saving where it used to point, you permanently lose access to every node after it — nothing else in the list points forward from that spot except the pointer you just destroyed. This is why the standard iterative pattern always looks the same: save the next node, rewire the current node, advance both prev and current.
Reversal can also be expressed recursively: reverse everything after the head first, then attach the head to the end of that already-reversed tail. Both approaches produce the same result; they differ in space usage, covered next.
Time and Space Complexity
Reversing a singly linked list requires visiting every node exactly once, no matter which approach you use, so the time complexity is always O(n), where n is the number of nodes. There is no way to do it faster, because you must at minimum touch every next pointer once to flip it.
Space complexity is where the iterative and recursive approaches diverge:
| Approach | Time | Space | Why |
|---|---|---|---|
| Iterative (three pointers) | O(n) | O(1) | Only a fixed number of pointer variables are used, regardless of list length. |
| Recursive | O(n) | O(n) | Each recursive call adds a stack frame; with n nodes, the call stack grows to depth n before unwinding. |
Reverse a sublist (positions left to right) |
O(n) | O(1) | Still a single pass; only the pointers inside the sublist range are rewired. |
The recursive version is elegant and worth understanding, but for large lists it is a real risk: Python’s default recursion limit is around 1000 frames, so recursively reversing a list of, say, 10,000 nodes will raise a RecursionError before it finishes. In production code, or on interview problems where list size isn’t bounded, prefer the iterative O(1)-space version.
Examples
Example 1: Iterative Reversal
This is the version you should default to. It uses the three-pointer technique described above and runs in O(n) time with O(1) extra space.
class ListNode:
def __init__(self, val: int, next: "ListNode | None" = None) -> None:
self.val = val
self.next = next
def build_linked_list(values: list[int]) -> "ListNode | None":
head = None
tail = None
for value in values:
node = ListNode(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def print_linked_list(head: "ListNode | None") -> None:
values = []
current = head
while current is not None:
values.append(str(current.val))
current = current.next
print(" -> ".join(values))
def reverse_list(head: "ListNode | None") -> "ListNode | None":
prev = None
current = head
while current is not None:
next_node = current.next
current.next = prev
prev = current
current = next_node
return prev
original = build_linked_list([1, 2, 3, 4, 5])
print_linked_list(original)
reversed_head = reverse_list(original)
print_linked_list(reversed_head)
Output:
1 -> 2 -> 3 -> 4 -> 5
5 -> 4 -> 3 -> 2 -> 1
build_linked_list turns [1, 2, 3, 4, 5] into the chain 1 -> 2 -> 3 -> 4 -> 5, which the first print confirms. Inside reverse_list, prev starts at None and current at the node holding 1. On each iteration the code saves next_node before touching anything, then points current.next backward at prev, then shuffles prev and current one step forward. After five iterations current becomes None and the loop exits, leaving prev pointing at the node holding 5 — the new head. The second print walks from that new head and shows the fully reversed chain.
Example 2: Recursive Reversal
The recursive version reverses everything after the head first, then splices the head onto the end of that reversed section.
class ListNode:
def __init__(self, val: int, next: "ListNode | None" = None) -> None:
self.val = val
self.next = next
def build_linked_list(values: list[int]) -> "ListNode | None":
head = None
tail = None
for value in values:
node = ListNode(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def print_linked_list(head: "ListNode | None") -> None:
values = []
current = head
while current is not None:
values.append(str(current.val))
current = current.next
print(" -> ".join(values))
def reverse_list_recursive(head: "ListNode | None") -> "ListNode | None":
if head is None or head.next is None:
return head
new_head = reverse_list_recursive(head.next)
head.next.next = head
head.next = None
return new_head
original = build_linked_list([10, 20, 30, 40])
reversed_head = reverse_list_recursive(original)
print_linked_list(reversed_head)
Output:
40 -> 30 -> 20 -> 10
The base case (head is None or head.next is None) fires when the recursion reaches the last node, 40, and returns it unchanged as new_head. As the recursion unwinds, each call takes the node it was given and makes the node right after it point back at it, then sets its own next to None so it becomes the new tail of the section reversed so far. new_head — always the original last node — is passed untouched all the way back up, which is why it ends up as the final result.
Example 3: Reversing Only Part of a List
A common interview variant reverses only the nodes between two positions, leaving the rest of the list untouched. This uses a dummy node so the code doesn’t need a special case for when left is 1 (the reversal starts at the head).
class ListNode:
def __init__(self, val: int, next: "ListNode | None" = None) -> None:
self.val = val
self.next = next
def build_linked_list(values: list[int]) -> "ListNode | None":
head = None
tail = None
for value in values:
node = ListNode(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def print_linked_list(head: "ListNode | None") -> None:
values = []
current = head
while current is not None:
values.append(str(current.val))
current = current.next
print(" -> ".join(values))
def reverse_between(head: "ListNode | None", left: int, right: int) -> "ListNode | None":
dummy = ListNode(0, head)
before = dummy
for _ in range(left - 1):
before = before.next
current = before.next
prev = None
for _ in range(right - left + 1):
next_node = current.next
current.next = prev
prev = current
current = next_node
before.next.next = current
before.next = prev
return dummy.next
original = build_linked_list([1, 2, 3, 4, 5, 6])
result = reverse_between(original, 2, 5)
print_linked_list(result)
Output:
1 -> 5 -> 4 -> 3 -> 2 -> 6
The list starts as 1 -> 2 -> 3 -> 4 -> 5 -> 6, and reverse_between(original, 2, 5) reverses only positions 2 through 5 (values 2, 3, 4, 5). before walks forward left - 1 times to land on the node just before the sublist (the node holding 1). The inner loop runs the same three-pointer reversal as Example 1, but only for right - left + 1 = 4 nodes, leaving current pointing at the node just after the sublist (6) once it finishes. The code then reconnects the seams: the old first node of the sublist (2) gets wired to whatever comes after the sublist (6), and the node before the sublist (1) gets wired to the new head of the reversed section (5). The untouched head, node 1, remains the head of the whole list.
How It Works, Step by Step
Trace the iterative algorithm on the small list 10 -> 20 -> 30 -> None:
| Step | prev | current | next_node | Action |
|---|---|---|---|---|
| Start | None | 10 | — | Initial state before the loop begins. |
| 1 | 10 | 20 | 20 | Save next_node = 20, set 10.next = None, advance prev to 10 and current to 20. |
| 2 | 20 | 30 | 30 | Save next_node = 30, set 20.next = 10, advance prev to 20 and current to 30. |
| 3 | 30 | None | None | Save next_node = None, set 30.next = 20, advance prev to 30 and current to None. |
| End | 30 | None | — | current is None, loop exits; return prev, the node holding 30. |
Following the final next pointers from the returned head gives 30 -> 20 -> 10 -> None — the original list, reversed, with no new nodes ever created; only the existing nodes’ pointers changed direction.
Common Mistakes
Mistake 1: Overwriting next before saving it
The single most common bug in linked list reversal is rewiring current.next before you have saved where it used to point:
def reverse_list_buggy(head):
prev = None
current = head
while current is not None:
current.next = prev # bug: overwritten before saving the rest of the list
prev = current
current = current.next # this now reads the pointer we just destroyed
return prev
This code doesn’t throw an error — it runs and returns something — which makes it a nasty bug to catch. On the first iteration, current.next = prev sets the head node’s next pointer to None, destroying the only reference to the rest of the list before current = current.next reads that same, now-None, pointer. The loop exits after a single iteration, and every node after the head is silently lost — not reversed, just gone. The fix is to always capture next_node = current.next as the very first thing inside the loop, before touching current.next at all:
prev = None
current = head
while current is not None:
next_node = current.next # save the rest of the list before overwriting
current.next = prev
prev = current
current = next_node
Saving the reference first means you can safely overwrite current.next and still know where to advance to next.
Mistake 2: Off-by-one loop bounds when reversing a sublist
When reversing only part of a list (Example 3), it’s easy to get the number of iterations wrong, since the range depends on two positions instead of the whole list length:
def reverse_between_buggy(head, left, right):
dummy = ListNode(0, head)
before = dummy
for _ in range(left - 1):
before = before.next
current = before.next
prev = None
for _ in range(right - left): # bug: off by one, stops one node too early
next_node = current.next
current.next = prev
prev = current
current = next_node
before.next.next = current
before.next = prev
return dummy.next
Using range(right - left) instead of range(right - left + 1) runs the reversal loop one time too few. For left = 2 and right = 5, right - left is 3, but the sublist actually contains 4 nodes (positions 2, 3, 4, and 5), so the last node in the range never gets rewired — the seam-reconnection lines afterward wire things up incorrectly, usually corrupting part of the list rather than raising an exception. Because the sublist spans both endpoints inclusively, the node count is always right - left + 1:
for _ in range(right - left + 1): # inclusive of both endpoints
next_node = current.next
current.next = prev
prev = current
current = next_node
Whenever a loop bound is computed from two positions instead of counted directly, double-check whether both endpoints are meant to be inclusive.
Best Practices
- Default to the iterative, three-pointer approach (
O(1)space) unless you have a specific reason to prefer recursion, such as when a problem is naturally expressed recursively and the list is known to be short. - Always save
current.nextinto a temporary variable before you reassigncurrent.next— this single habit prevents the most common reversal bug. - Use a dummy (sentinel) node when the head of the list might change or when reversal might start at position 1, so you don’t need a separate code path for reversals that include the head.
- After writing a reversal, trace it by hand on a list of 2-3 nodes and a list of exactly 1 node; the 1-node case is a great sanity check that your loop terminates correctly.
- If you need the original list order preserved elsewhere in your program, build a new reversed list instead of reversing in place — in-place reversal destroys the original ordering of
nextpointers. - For deep or unbounded lists, avoid the recursive approach in production code; Python’s recursion limit (around 1000) makes it fail on lists that the iterative version handles without issue.
Practice Exercises
- Write a function that reverses a singly linked list iteratively, then use it to check whether the list is a palindrome (reverse a copy, and compare values node by node against the original). For the input
[1, 2, 3, 2, 1], your function should report that it is a palindrome. - Given a linked list and an integer
k, reverse the nodes in groups ofk(leave any leftover group at the end, shorter thank, unreversed). For[1, 2, 3, 4, 5, 6, 7]withk = 3, the expected result is[3, 2, 1, 6, 5, 4, 7]. - Write a recursive function that reverses a linked list, then add a print statement showing the maximum recursion depth reached for a 6-node list. (Hint: it should equal the number of nodes.)
Summary
- Reversing a singly linked list means flipping every
nextpointer so the list runs the other direction, done in place with no extra nodes. - The iterative three-pointer technique (
prev,current,next_node) runs inO(n)time andO(1)space, and is the standard approach to reach for. - The recursive approach also runs in
O(n)time but usesO(n)space for the call stack, and risksRecursionErroron long lists. - Reversing only a sublist (between two positions) uses the same core loop, just bounded to
right - left + 1nodes, plus careful seam reconnection with a dummy node. - The number one bug to avoid: always save
current.nextbefore overwriting it, or you will silently lose the rest of the list.
