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 as None, since the original head becomes the new tail and should point to nothing).
  • current — the node you are currently rewiring (starts at head).
  • next_node — a temporary that saves current.next before 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.next into a temporary variable before you reassign current.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 next pointers.
  • 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

  1. 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.
  2. Given a linked list and an integer k, reverse the nodes in groups of k (leave any leftover group at the end, shorter than k, unreversed). For [1, 2, 3, 4, 5, 6, 7] with k = 3, the expected result is [3, 2, 1, 6, 5, 4, 7].
  3. 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 next pointer so the list runs the other direction, done in place with no extra nodes.
  • The iterative three-pointer technique (prev, current, next_node) runs in O(n) time and O(1) space, and is the standard approach to reach for.
  • The recursive approach also runs in O(n) time but uses O(n) space for the call stack, and risks RecursionError on long lists.
  • Reversing only a sublist (between two positions) uses the same core loop, just bounded to right - left + 1 nodes, plus careful seam reconnection with a dummy node.
  • The number one bug to avoid: always save current.next before overwriting it, or you will silently lose the rest of the list.