Reverse a linked list: keep the unprocessed suffix reachableLESSON 2.15 · 15 OF 43 IN CHAPTER
PART A / Coding problems and trade-offs
Step 41 of 252
LESSON 2.15 · 15 OF 43 IN CHAPTERTry it, then open the solution

Reverse a linked list: keep the unprocessed suffix reachable

Constructed practice problem; no company attribution. Prerequisites: state invariants; this page introduces node identity and pointers.

Candidate brief

THE PROBLEM

A singly linked work queue must be reversed in place. Each node has a value and a link to the next node. Return the new head using the same node objects, with every link reversed. How will you preserve access to the remaining queue before overwriting a link?

Write this:

@dataclass(eq=False)
class Node:
    value: object
    next: "Node | None" = None

def reverse_list(head):
    ...
Contract Decision
Input Node head or None; each next link is a Node or None; values are opaque
Output New head of the reversed chain using exactly the original node identities
Boundaries Empty gives None; singleton is the same object; mutation is intentional
Invalid input Malformed links or cycles raise ValueError before any mutation
Excluded Shared ownership guarantees and concurrent readers/writers during reversal
Optional refresher · the underlying tool

A linked-list node holds a value and a reference to the next node, not a Python list index. Reversal changes links; saving next first keeps the unprocessed suffix reachable:

next_node = current.next
current.next = previous
previous, current = current, next_node

For 1 → 2 → 3 → None, the new head must yield 3 → 2 → 1 → None. Draw the three pointers and test the empty and one-node lists.

A design choice worth saying aloud

next_node is a temporary ownership handle: save it before assigning current.next = previous, or the rest of the input becomes unreachable. After each iteration, previous heads the reversed prefix and current heads the untouched suffix. Check both the returned head and that the old head now points to None.

What the interviewer expects

The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.

Done means: New head of the reversed chain using exactly the original node identities.

Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.

Test-case scenarios to settle before coding

01 · Representative

Input / starting state
a → b → c → None
Expected result
same objects as c → b → a → None

What it is testing: Identity, not copied values, defines success.

02 · Empty

Input / starting state
None
Expected result
None

What it is testing: No links are written.

03 · Singleton

Input / starting state
a → None
Expected result
the exact object a

What it is testing: Head identity stays the same.

04 · Repeated values

Input / starting state
three distinct nodes all storing 1
Expected result
all three identities reversed

What it is testing: Values cannot identify nodes.

05 · Cycle

Input / starting state
a → b → a
Expected result
ValueError before mutation

What it is testing: Validation must not partially destroy the structure.

06 · Malformed link

Input / starting state
reachable next is not Node/None
Expected result
ValueError before mutation

What it is testing: Atomic rejection is observable behavior.

For each case, show which branch or state change produces that result.

For distinct objects a → b → c → None, return object c with links c → b → a → None. Even if all three values are 7, all three identities must remain. A self-link a.next = a raises ValueError and remains unchanged.

Before opening the explanation, restate the contract, trace the smallest useful example, implement a baseline, and identify the repeated work. Then implement your improvement independently and derive tests from the contract. Say what your state means before saying which data structure stores it.

Worked lesson, changed requirements, and reference

Draw identities, not just values

A node is an object; next is a reference to another object, not its value. Two nodes with equal values are still different queue entries. A baseline can push node references onto a stack, pop them, and reconnect in reverse order: O(n) time and O(n) auxiliary space. Copying only values into new nodes would violate the identity contract. To remove the stack, keep three references: the reversed prefix, the current node, and its saved successor.

Diagram: Draw identities, not just values

At the start of each iteration, previous heads the already reversed prefix and current heads the untouched suffix. Together they contain each original node exactly once, with no link from the prefix into the suffix. Save current.next before redirecting it to previous; then move previous to current and current to the saved successor. Losing the successor first makes the suffix unreachable. The two regions still partition the original nodes, and one node moves from suffix to prefix. When current is None, the whole chain is reversed and previous is the new head.

Step for a → b → c previous current Newly written link
initial None a none
after a a b a.next = None
after b b c b.next = a
after c c None c.next = b

The reference first checks shape and uses slow/fast pointers to reject cycles, then mutates. This extra pass preserves the “no partial mutation on invalid input” contract while retaining O(n) total time and O(1) auxiliary space. The output is only a reference; existing nodes are not counted as new storage. Recursive reversal is elegant but uses O(n) call-stack space and can exceed Python's recursion limit on a deep chain.

Watch the saved successor arrive before the link changes, then follow the two variable references as they advance. The node objects stay in place.

Save the successor, redirect b.next, then advance previous and current

Follow-up 1: reverse only an interior segment

Predict the links that must be repaired outside the reversed region. For the inclusive node segment b through d, both attachment points matter:

Diagram: Follow-up 1: reverse only an interior segment

Save predecessor, old segment head, and the node after the segment before rewiring. The old segment head becomes its tail. Validate positional bounds before mutation if failure must leave the original list intact. A dummy head simplifies a segment that starts at position zero.

Ownership requirement Can existing nodes be rewired? Suitable representation
Exclusive mutable chain yes current in-place reversal
Readers need old chain concurrently no copied immutable chain or separately published snapshot

The same node cannot simultaneously have its old and new next through one field. Copying introduces O(n) new storage and new identities; an indirection layer needs version semantics. A senior candidate tests identity, tail termination, and reversing twice. A lead candidate settles mutation ownership and publication before adding locks or promising stable readers; the local routine has no concurrency protocol.

Run and check

From the repository root:

cd curriculum/01-code/02-data-structures-algorithms/problems/14-reverse-linked-list
python -m unittest -v test_solution.py

Reference implementation (download file, source below) · Contract and oracle tests (download file, source below). Read the tests after your attempt. A green reference suite verifies the supplied implementation; it does not demonstrate independent transfer. Reimplement one follow-up with the reference closed and explain which old invariant no longer holds.

Reference implementation · solution.py
from dataclasses import dataclass

@dataclass(eq=False)
class Node:
    value: object
    next: "Node | None" = None

def _next(node):
    if node is None:
        return None
    if not isinstance(node, Node) or (node.next is not None and not isinstance(node.next, Node)):
        raise ValueError("links must be Node objects or None")
    return node.next

def reverse_list(head):
    slow = fast = head
    while fast is not None:
        fast = _next(_next(fast))
        slow = _next(slow)
        if fast is not None and slow is fast:
            raise ValueError("input must be acyclic")
    previous, current = None, head
    while current is not None:
        following = current.next
        current.next = previous
        previous, current = current, following
    return previous
Contract and oracle tests · test_solution.py
import unittest
from solution import Node, reverse_list

class ReverseTests(unittest.TestCase):
    def test_identity_and_involution(self):
        for size in [0, 1, 2, 3, 10000]:
            nodes = [Node(7) for _ in range(size)]
            for left, right in zip(nodes, nodes[1:]):
                left.next = right
            head = nodes[0] if nodes else None
            reversed_head = reverse_list(head)
            current = reversed_head
            for expected in reversed(nodes):
                self.assertIs(current, expected)
                current = current.next
            self.assertIsNone(current)
            self.assertIs(reverse_list(reversed_head), head)
            self.assertTrue(all(node.next is (nodes[i+1] if i+1 < size else None)
                                for i, node in enumerate(nodes)))

    def test_invalid_input_is_not_partially_mutated(self):
        a, b, c = Node(1), Node(2), Node(3)
        a.next, b.next, c.next = b, c, b
        with self.assertRaises(ValueError):
            reverse_list(a)
        self.assertIs(a.next, b)
        self.assertIs(b.next, c)
        self.assertIs(c.next, b)
        c.next = 42
        with self.assertRaises(ValueError):
            reverse_list(a)
        self.assertIs(a.next, b)
        self.assertEqual(c.next, 42)
        a.next = a
        with self.assertRaises(ValueError):
            reverse_list(a)
        with self.assertRaises(ValueError):
            reverse_list("bad")

if __name__ == "__main__":
    unittest.main()