TL;DR
Iterative three-pointer reversal — O(n) time, O(1) space (recursion is the classic O(n)-stack alternative).
Approach 1 — Brute force: copy values, rebuild backwards
Walk the list once collecting values, then build a brand-new list from the values in reverse.
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
from typing import Optional
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
vals = []
node = head
while node:
vals.append(node.val)
node = node.next
new_head = None
for v in vals:
new_head = ListNode(v, new_head)
return new_head
Complexity: O(n) time, O(n) space.
The constraints don’t kill it — the point of the exercise does: the interviewer wants the pointers of the existing nodes rewired in O(1) extra space, not a value copy.
Approach 2 — Iterative pointer reversal
The insight: maintain an invariant with two pointers — prev heads the fully-reversed prefix, curr heads the untouched suffix. One step of work extends the invariant: save curr.next, flip curr.next to point at prev, slide both pointers forward. When curr runs off the end, prev is the new head.
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
from typing import Optional
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
prev = None
curr = head
while curr:
nxt = curr.next # save the rest of the list
curr.next = prev # flip the link
prev = curr # advance the reversed prefix
curr = nxt # advance into the suffix
return prev
Walkthrough on 1 -> 2 -> 3 -> 4 -> 5:
| step | reversed part (prev) | remaining part (curr) |
|---|
| start | ∅ | 1 -> 2 -> 3 -> 4 -> 5 |
| 1 | 1 | 2 -> 3 -> 4 -> 5 |
| 2 | 2 -> 1 | 3 -> 4 -> 5 |
| 3 | 3 -> 2 -> 1 | 4 -> 5 |
| 4 | 4 -> 3 -> 2 -> 1 | 5 |
| 5 | 5 -> 4 -> 3 -> 2 -> 1 | ∅ → return prev |
Complexity: O(n) time, O(1) space.
Approach 3 — Recursive reversal
The insight: trust the recursion to reverse the sublist after head and hand back its new head. At that point head.next still points at what is now the tail of the reversed sublist — so head.next.next = head appends head behind it, and head.next = None marks it as the new tail.
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
from typing import Optional
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None or head.next is None:
return head
new_head = self.reverseList(head.next)
head.next.next = head
head.next = None
return new_head
Walkthrough on 1 -> 2 -> 3: recursion dives to node 3 (base case, returned as new_head). Unwinding at node 2: 2.next is 3, so 3.next = 2 and 2.next = None, giving 3 -> 2. Unwinding at node 1: 1.next is 2 (now the tail), so 2.next = 1, 1.next = None, giving 3 -> 2 -> 1. new_head = 3 is passed up unchanged.
Complexity: O(n) time, O(n) space for the call stack. At the constraint’s 5000 nodes this brushes against Python’s default recursion limit of 1000 — a real reason to prefer the iterative form.
Common pitfalls
- Flipping
curr.next before saving it — the rest of the list becomes unreachable and the loop ends after one node.
- Returning
curr (which is None when the loop exits) instead of prev.
- In the recursive version, forgetting
head.next = None: the old head keeps a forward link and the list ends in a 2-cycle.
- Assuming a non-empty list: both versions must return
None untouched for an empty input (the base/loop conditions above already do).
Pattern takeaway
In-place linked-list surgery is always the same discipline: save the pointer you’re about to overwrite, then rewire. The prev/curr/nxt three-pointer walk is the atom of list manipulation — reversal here, and later the inner step of “reverse in k-groups”, “reorder list”, and palindrome checks.