In-Place Linked-List Reversal
Three-pointer pattern that reverses a list, sublist, or every k nodes without extra memory.
TL;DR
Reversing a singly-linked list in place is a three-pointer dance: prev, curr,
next. You walk the list once, and at each node you flip curr.next from
pointing forward to pointing back at prev. You save next before the flip so
you don’t lose the rest of the list. That’s it — every linked-list reversal
problem is a variation on this loop.
The pattern shows up in four flavors that all share the same body:
- Whole list — reverse from head to tail.
- Sublist
[m, n]— reverse only the nodes at positionsm..n, leaving the rest in place. - Groups of
k— reverse everykconsecutive nodes; either drop the trailing leftover or reverse it too, depending on the variant. - Swap pairs — the special case
k = 2, usually written without an inner loop because it’s so short.
All of them run in O(n) time and O(1) extra space. Recursive versions exist
but use O(n) stack frames, so prefer the iterative form unless interview
constraints push you otherwise. A dummy node placed before head removes
the special case where the head itself changes — use one whenever the reversal
might touch position 1.
When to reach for it
Signals that linked-list reversal is the right pattern:
- The problem says “reverse”, “reorder”, or “rotate” a linked list, or hands you a list and asks for output where some segment runs backward.
- You’re told to do it in place or with O(1) extra space — that rules out the “dump to array, reverse, rebuild” cheat.
- The problem has a k-sized window structure: every k nodes, do something. Group reversal is the canonical answer.
- Palindrome-check on a linked list — you reverse the second half in place and compare against the first half.
- You need to reorder a list as
L0 → Ln → L1 → Ln-1 → ...(LeetCode 143 Reorder List). Step two of the standard solution is reversing the back half.
Signals that it’s not the right pattern: the structure is doubly-linked (you can usually just swap head/tail pointers), the list is small enough that a list-to-array round-trip is fine, or the problem actually wants a sort, not a reversal.
Flavor 1 — reverse the entire list
The canonical loop. Three pointers, walk once.
from typing import Optional
class ListNode:
def __init__(self, val: int = 0, next: Optional["ListNode"] = None) -> None:
self.val = val
self.next = next
def reverse_list(head: Optional[ListNode]) -> Optional[ListNode]:
prev: Optional[ListNode] = None
curr = head
while curr is not None:
nxt = curr.next # 1. save the rest of the list
curr.next = prev # 2. flip the link
prev = curr # 3. advance prev
curr = nxt # 4. advance curr
return prev # prev is the new head
Worked example. Input list: 1 → 2 → 3 → 4 → 5 → None.
| iter | prev (before) | curr (before) | nxt | list state after the flip |
|---|---|---|---|---|
| 1 | None | 1 | 2 | 1 → None, remaining: 2 → 3 → 4 → 5 |
| 2 | 1 | 2 | 3 | 2 → 1 → None, remaining: 3 → 4 → 5 |
| 3 | 2 | 3 | 4 | 3 → 2 → 1 → None, remaining: 4 → 5 |
| 4 | 3 | 4 | 5 | 4 → 3 → 2 → 1 → None, remaining: 5 |
| 5 | 4 | 5 | None | 5 → 4 → 3 → 2 → 1 → None, remaining: empty |
Loop exits when curr is None. Return prev, which now points at 5 — the
new head.
Flavor 2 — reverse the sublist between positions m and n
LeetCode 92. Reverse only nodes at 1-indexed positions m..n, splice the
reversed chunk back in. Use a dummy node so position 1 is no longer special.
def reverse_between(head: Optional[ListNode], m: int, n: int) -> Optional[ListNode]:
dummy = ListNode(0, head)
before = dummy
for _ in range(m - 1):
before = before.next # node just before position m
# `start` is the node at position m — it will become the tail of the reversed chunk.
start = before.next
curr = start.next
# Reverse n - m links, splicing each new node to the front of the chunk.
for _ in range(n - m):
start.next = curr.next
curr.next = before.next
before.next = curr
curr = start.next
return dummy.next
The trick: instead of reversing in place and stitching afterward, you
head-insert each successor of start directly after before. The reversed
prefix grows one node at a time and is always correctly spliced.
Worked example. Input: 1 → 2 → 3 → 4 → 5, reverse positions 2..4. After
the first loop, before = node(1), start = node(2), curr = node(3). We do
n - m = 2 iterations.
| iter | before.next chain after splice | start.next | curr |
|---|---|---|---|
| init | 1 → 2 → 3 → 4 → 5 | 3 | 3 |
| 1 | 1 → 3 → 2 → 4 → 5 | 4 | 4 |
| 2 | 1 → 4 → 3 → 2 → 5 | 5 | 5 |
Final list: 1 → 4 → 3 → 2 → 5. start (node 2) is now the tail of the
reversed chunk and still points at node 5, so the suffix is preserved for free.
Flavor 3 — reverse in groups of k
LeetCode 25. Walk the list k nodes at a time; reverse each group; reconnect. The standard form drops nothing — if fewer than k nodes remain, leave them as-is.
def reverse_k_group(head: Optional[ListNode], k: int) -> Optional[ListNode]:
dummy = ListNode(0, head)
group_prev = dummy
while True:
# Find the kth node from group_prev; bail if fewer than k remain.
kth = group_prev
for _ in range(k):
kth = kth.next
if kth is None:
return dummy.next
group_next = kth.next
# Reverse the group [group_prev.next ... kth] using the Flavor 1 loop.
prev, curr = group_next, group_prev.next
while curr is not group_next:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
# Splice: group_prev → (new head = kth), old head → group_next.
tmp = group_prev.next
group_prev.next = kth
group_prev = tmp
return dummy.next
Worked example. 1 → 2 → 3 → 4 → 5, k = 2.
- Group 1: nodes
1, 2. Reverse →2 → 1. Splice →dummy → 2 → 1 → 3 → 4 → 5.group_prevadvances to node 1. - Group 2: nodes
3, 4. Reverse →4 → 3. Splice →dummy → 2 → 1 → 4 → 3 → 5.group_prevadvances to node 3. - Group 3: only node
5remains,kthwalk hitsNone, return.
Result: 2 → 1 → 4 → 3 → 5. The trailing 5 stays put because the group was
incomplete. Setting prev = group_next at the start of the inner loop is what
makes the new tail’s next correctly point at the next group’s head — no extra
fix-up step.
Why it works — the invariant
At the top of every iteration of the Flavor 1 loop, the following holds:
previs the head of the already-reversed prefix. Itsnextchain runs back to the original first node, which now points atNone.curris the head of the unreversed suffix. Itsnextchain is still in original order.- The two halves are completely disconnected — there are no dangling pointers, no cycles, no shared nodes.
Each iteration moves exactly one node from the front of the suffix to the front
of the prefix. Because both ends are well-defined and the move is a constant
number of pointer writes, the invariant is preserved and the loop terminates
when the suffix is empty (curr is None). Flavor 2 maintains the same
invariant locally inside [before.next ... ]; Flavor 3 reapplies it once per
k-group with group_next playing the role of None.
If you can’t state which node is the prefix head and which is the suffix head at every iteration, you’ll write off-by-one bugs. Write the invariant down before coding.
Complexity
- Time: O(n). Each node is visited and rewritten exactly once across all flavors. The k-group version touches each node a small constant number of times (find-kth pass + reverse pass), still O(n).
- Space: O(1). Three pointer variables plus a dummy node — independent of list length. The recursive variant is O(n) stack space; iterative is the default.
Common pitfalls
- Losing the rest of the list. If you write
curr.next = prevbefore savingnxt = curr.next, you’ve just orphaned everything aftercurr. Always save first, flip second. - Not using a dummy node when the head can change. Flavors 2 and 3 both
potentially rewrite position 1. Without a dummy, you need a special case for
“reversal includes the head” — easy to get wrong. The dummy node is one line
and makes
beforealways exist. - Off-by-one when k doesn’t divide n. In group reversal, decide explicitly
what to do with a trailing partial group. LeetCode 25 says “leave it as-is”;
some variants want it reversed. Pick a rule and write a test for
n = 7, k = 3to confirm. - Forgetting to reconnect the reversed sublist’s head and tail. In
Flavor 2, the original
startnode becomes the tail of the reversed chunk and must still point at the suffix that follows positionn. The head-insert technique handles this automatically; if you instead reverse in place and stitch later, you must explicitly setstart.next = node_at_position_n_plus_1. - Returning
headinstead ofprev. After Flavor 1 finishes, the originalheadis now the tail and points atNone. The new head isprev. Returningheadgives you a one-element list.
Where you see this in production
The pattern outlives interviews. A few real systems lean on it:
- CPython’s
list.reverse()—cpython/Objects/listobject.cswaps pointers in place with two indices walking toward each other; the same prev/curr discipline shows up incpython/Modules/_collectionsmodule.cforcollections.deque.reverse, which is genuinely linked-list-style. - Redis LRU and LFU eviction.
redis/src/evict.cand thequicklist/listpackcode paths splice and reorder doubly-linked node chains to move recently-used keys to the front — splicing is just reversal with extra back-pointers. - Database WAL / journal replay. Postgres’ WAL and SQLite’s rollback journal store records in append order but must be applied in reverse on recovery; the in-memory replay queue is reversed using the same prev/curr loop before commit hooks run.
- Undo stacks in editors. VS Code’s
monaco-editorand Vim’s undo tree represent edits as a linked structure; “redo to point X” walks a chain and reverses the segment between the current state and the target so it can be replayed forward.
Practice problems
| # | Problem | Difficulty | LeetCode | NeetCode |
|---|---|---|---|---|
| 1 | Reverse Linked List | Easy | LC 206 | walk-through |
| 2 | Reverse Linked List II | Medium | LC 92 | walk-through |
| 3 | Swap Nodes in Pairs | Medium | LC 24 | walk-through |
| 4 | Reverse Nodes in k-Group | Hard | LC 25 | walk-through |
| 5 | Palindrome Linked List | Easy | LC 234 | walk-through |
Resources
- NeetCode roadmap — neetcode.io/roadmap — pattern-by-pattern problem sets organised exactly like this site.
- NeetCode practice grid — neetcode.io/practice — track which problems you’ve solved per pattern.
- NeetCode YouTube — @NeetCode — clear, whiteboard-style walkthroughs for almost every LeetCode problem above.