Skip to content

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 positions m..n, leaving the rest in place.
  • Groups of k — reverse every k consecutive 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.

iterprev (before)curr (before)nxtlist state after the flip
1None121 → None, remaining: 2 → 3 → 4 → 5
21232 → 1 → None, remaining: 3 → 4 → 5
32343 → 2 → 1 → None, remaining: 4 → 5
43454 → 3 → 2 → 1 → None, remaining: 5
545None5 → 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.

iterbefore.next chain after splicestart.nextcurr
init1 → 2 → 3 → 4 → 533
11 → 3 → 2 → 4 → 544
21 → 4 → 3 → 2 → 555

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_prev advances to node 1.
  • Group 2: nodes 3, 4. Reverse → 4 → 3. Splice → dummy → 2 → 1 → 4 → 3 → 5. group_prev advances to node 3.
  • Group 3: only node 5 remains, kth walk hits None, 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:

  • prev is the head of the already-reversed prefix. Its next chain runs back to the original first node, which now points at None.
  • curr is the head of the unreversed suffix. Its next chain 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 = prev before saving nxt = curr.next, you’ve just orphaned everything after curr. 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 before always 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 = 3 to confirm.
  • Forgetting to reconnect the reversed sublist’s head and tail. In Flavor 2, the original start node becomes the tail of the reversed chunk and must still point at the suffix that follows position n. The head-insert technique handles this automatically; if you instead reverse in place and stitch later, you must explicitly set start.next = node_at_position_n_plus_1.
  • Returning head instead of prev. After Flavor 1 finishes, the original head is now the tail and points at None. The new head is prev. Returning head gives 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.c swaps pointers in place with two indices walking toward each other; the same prev/curr discipline shows up in cpython/Modules/_collectionsmodule.c for collections.deque.reverse, which is genuinely linked-list-style.
  • Redis LRU and LFU eviction. redis/src/evict.c and the quicklist/listpack code 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-editor and 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

#ProblemDifficultyLeetCodeNeetCode
1Reverse Linked ListEasyLC 206walk-through
2Reverse Linked List IIMediumLC 92walk-through
3Swap Nodes in PairsMediumLC 24walk-through
4Reverse Nodes in k-GroupHardLC 25walk-through
5Palindrome Linked ListEasyLC 234walk-through

Resources

  • NeetCode roadmapneetcode.io/roadmap — pattern-by-pattern problem sets organised exactly like this site.
  • NeetCode practice gridneetcode.io/practice — track which problems you’ve solved per pattern.
  • NeetCode YouTube@NeetCode — clear, whiteboard-style walkthroughs for almost every LeetCode problem above.