Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Reverse the list in consecutive blocks of k, but only if a full block of k nodes remains — a leftover tail of fewer than k stays as-is. Before reversing each block, scan ahead k nodes to confirm there are enough; if not, stop. Reverse that block with the standard prev/curr flip, then stitch the previous block's tail to this block's new head and remember the current block's old head as the next block's tail anchor. Iterative gives O(n) time and O(1) space; the recursive version is the same logic with O(n/k) stack depth.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can reverse a linked list in fixed-size groups in place, check that a full group remains before reversing, and correctly re-link consecutive groups without losing nodes or creating cycles.
Where candidates slip
Reversing a trailing partial group that should be left alone, mis-stitching the boundary between groups, or forgetting that the old group head becomes the group's tail (the anchor for the next group).
How to stand out
Lead with the iterative O(1)-space solution, isolate the 'k nodes remain?' check, articulate the two boundary relinks per group, and offer the clean recursive version while naming its O(n/k) stack cost.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewReverse in blocks of kO(1) extra space required?YesNo / cleanerExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Iterative — Check, Flip, Stitch (O(1) Space)
1Hint 1Explore hintHide hint
Use a dummy node before head. Keep a groupPrev pointer at the node just before the block you're about to reverse.
2Hint 2Explore hintHide hint
Before reversing, walk k steps from groupPrev to find the k-th node of the block. If you fall off the end (null), fewer than k nodes remain — stop and leave them as is.
3Hint 3Explore hintHide hint
Reverse the block, then reconnect: groupPrev.next should point to the block's new head (the old k-th node), and the block's old head (now its tail) becomes the next groupPrev.
A · Core idea
This is partial reversal applied repeatedly, with careful hand-offs between blocks.
The dummy node and groupPrev. A dummy in front of head means the very first block's reversal (which changes the list head) needs no special case — groupPrev simply starts at the dummy. Throughout, groupPrev is the invariant 'node immediately before the block I'm reversing'. After each block it advances to the block's new tail, ready for the next block.
Check before you flip — the remainder rule. The defining twist of this problem is that a trailing block of fewer than k nodes must be left alone. So before touching any pointers, we walk k steps from groupPrev to locate the block's k-th node (kth). If that walk runs off the end into null, there aren't k nodes left, and we break — leaving the remainder untouched. Doing the check first is what prevents accidentally reversing a partial tail.
Reverse the block with prev seeded to groupNext. Once we know a full block exists, groupNext = kth.next is the start of whatever follows. We then run the standard three-pointer flip over the block — but we initialise prev = groupNext instead of null. That single choice makes the block's tail (its old first node) point directly at the next block as a side effect of the reversal, so one of the two boundary links is handled automatically. The flip stops when curr reaches groupNext.
Stitch the two boundaries. After the flip, the block is internally reversed and its tail already points to groupNext. Two assignments finish the wiring: we capture the block's old head (tmp = groupPrev.next) which is now the block's tail, set groupPrev.next = kth to connect the previous block to this block's new head, and finally move groupPrev = tmp so it sits on the new tail for the next iteration. Capturing tmp before overwriting groupPrev.next is essential — otherwise you lose the handle on the new tail.
Why this answers the follow-up. There's no recursion and no auxiliary structure — just a handful of pointers reused across blocks. That's genuine O(1) extra space, which is exactly what the 'O(1) memory' follow-up is probing.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
head = [1,2,3,4,5], k = 2Array state
block 1 done
reverse [1,2] → [2,1], stitch dummy→2
[2,1,4,3,5]C · Code with teaching comments
Reference implementation previewpublic class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; }Explore the full codeHide the full code
D · Why it works and complexityO(n) timeO(1) spaceOpen the invariant and proofExplore the proofHide the proof
The reversal itself is the easy part; the problem is really about bookkeeping between blocks. Two facts carry the whole solution: the block's old head becomes its tail (so it's the anchor that links to the next block), and seeding prev = groupNext pre-wires the tail to the suffix. Hold those two and the stitching is mechanical.
ComplexityO(n) time, O(1) space — Each node is visited a constant number of times — once by the 'k nodes remain?' scan and once by the reversal — so O(n) total. Only a fixed set of pointers is used, giving O(1) extra space (the answer to the follow-up).
E · Edge cases and pitfalls7k = 1 — Each block of one reverses to itself — the list is returned unchanged.Explore edge casesHide edge cases
Edge cases handled
k = 1Each block of one reverses to itself — the list is returned unchanged.k = nA single full block reverses the entire list once.n not a multiple of kThe trailing partial block fails the k-check and is left in original order.
Pitfalls
- Reversing a trailing block of fewer than k nodes — must leave it unchanged.
- Overwriting groupPrev.next before capturing the old head (the new tail).
- Seeding the flip's prev to null instead of groupNext, leaving the block tail dangling.
- Skipping the dummy node and special-casing the first block.
Approach 2 of 2 · Optimal
Recursive — Reverse the Front, Recurse the Rest
1Hint 1Explore hintHide hint
First check whether k nodes exist starting at head. If not, return head unchanged — that's both the base case and the remainder rule.
2Hint 2Explore hintHide hint
If they do, reverse just the first k nodes with the standard flip. After the flip, head is the tail of this block and the (k+1)-th node starts the rest.
3Hint 3Explore hintHide hint
Recurse on the (k+1)-th node and attach its result to head.next, then return the new head of this block.
A · Core idea
The recursive form expresses the same algorithm as 'reverse one block, then trust recursion for the rest'.
The base case doubles as the remainder rule. We first walk k nodes from head. If we hit null before counting k, this block is incomplete, so we return head unmodified. That single check serves two purposes: it's the recursion's termination condition and it enforces 'leftover fewer than k stays as-is'. No separate end-of-list handling is needed.
Reverse the front k, leaving head as the tail. When a full block exists, we run the classic prev/curr flip exactly k times over the first block. After the loop, prev is the block's new head (the old k-th node), head is the block's tail (it hasn't moved, but everything now points the other way), and curr sits on the (k+1)-th node — the head of the remainder.
The recursive leap of faith. We assume reverseKGroup(curr, k) correctly k-group-reverses everything after this block and returns the head of that processed remainder. Our job is only to attach it: head.next = reverseKGroup(curr, k). Because head is this block's tail, pointing its next at the recursively-processed remainder seamlessly joins block to block. Then we return prev, this block's new head, which the previous frame attaches in turn.
Why it costs O(n/k) stack. There's one frame per block, and the recursion only goes one level deeper per block (not per node), so the depth is the number of blocks, n/k. For n ≤ 5000 that's fine, but it's strictly more memory than the iterative O(1) version — which is precisely why the iterative form is the answer to the O(1)-space follow-up, and this is the elegant 'if recursion is allowed' alternative.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
head = [1,2,3,4,5], k = 3Interactive diagram
Picking the right approach
call(1)Iterative is O(1) space (the follow-up answer); recursion is cleaner but uses O(n/k) stack.
[3,2,1,4,5]C · Code with teaching comments
D · Why it works and complexityO(n) timeO(n/k) recursion stack spaceOpen the invariant and proofExplore the proofHide the proof
Recursion makes the block hand-off implicit: 'head is the tail, so head.next = (the rest, already solved)'. The trade is conceptual clarity for O(n/k) stack frames. Naming that trade out loud — 'cleaner, but not O(1) space' — shows you understand the cost, which is exactly what a hard-tagged problem wants.
ComplexityO(n) time, O(n/k) recursion stack space — Each node is reversed exactly once across all recursive calls → O(n) time. There's one stack frame per block, and there are n/k blocks, so the recursion depth (and stack space) is O(n/k).
E · Edge cases and pitfalls7k = 1 — Each call reverses one node (a no-op) and recurses — list unchanged.Explore edge casesHide edge cases
Edge cases handled
k = 1Each call reverses one node (a no-op) and recurses — list unchanged.exactly k nodesOne reversal, then recurse on null which returns null → single reversed block.very long list, small kn/k frames deep; fine for n ≤ 5000 but more memory than the iterative version.
Pitfalls
- Reversing before checking for k nodes — would reverse a partial trailing block.
- Forgetting head.next = recursion — disconnects the blocks.
- Returning head instead of prev — returns the tail of the block, not its new head.
- Ignoring the O(n/k) stack cost when O(1) space is required.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic ListNode reverseKGroup(ListNode head, int k) { // Base case + remainder rule: fewer than k nodes -> leave as is. ListNode node = head; for (int i = 0; i < k; i++) { if (node == null) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Reversing an incomplete trailing group
The spec says a leftover group of fewer than k nodes stays as-is. If you reverse without first confirming k nodes remain, the tail comes out reversed and wrong.
Explore the code comparisonHide the code comparison
Reversing an incomplete trailing group
The spec says a leftover group of fewer than k nodes stays as-is. If you reverse without first confirming k nodes remain, the tail comes out reversed and wrong.
Don't write this
# reverses whatever is left, even a partial group
prev, curr = None, group_prev.next
for _ in range(k):
if curr is None:
break # but the partial group is already half-reversed!
nxt = curr.next; curr.next = prev; prev = curr; curr = nxtWrite this instead
# check FIRST; only reverse a full group
kth = group_prev
for _ in range(k):
kth = kth.next
if kth is None:
break
if kth is None:
return dummy.next # leave the short tail untouched2Losing the new tail when stitching
The old group head becomes the group's tail and is the anchor the next group attaches to. If you overwrite groupPrev.next before saving it, you lose that handle and can't advance correctly.
Explore the code comparisonHide the code comparison
Losing the new tail when stitching
The old group head becomes the group's tail and is the anchor the next group attaches to. If you overwrite groupPrev.next before saving it, you lose that handle and can't advance correctly.
Don't write this
groupPrev.next = kth; // overwrote the old head reference
ListNode tmp = groupPrev.next; // this is now kth, NOT the old head
groupPrev = tmp; // wrong anchor for the next groupWrite this instead
ListNode tmp = groupPrev.next; // capture old head (new tail) FIRST
groupPrev.next = kth; // then stitch to the new head
groupPrev = tmp; // advance to the correct anchor3Recursive version returns the tail, not the head
After reversing the front k nodes, prev is the block's new head and head is its tail. Returning head returns the tail, so the caller links to the wrong node and the list breaks.
Explore the code comparisonHide the code comparison
Recursive version returns the tail, not the head
After reversing the front k nodes, prev is the block's new head and head is its tail. Returning head returns the tail, so the caller links to the wrong node and the list breaks.
Don't write this
prev = None; curr = head
for _ in range(k):
nxt = curr.next; curr.next = prev; prev = curr; curr = nxt
head.next = self.reverseKGroup(curr, k)
return head # head is the TAIL now — wrongWrite this instead
prev = None; curr = head
for _ in range(k):
nxt = curr.next; curr.next = prev; prev = curr; curr = nxt
head.next = self.reverseKGroup(curr, k)
return prev # prev is the new headRevise
Carry the pattern into the interview
Pattern to remember
Block-wise in-place reversal with a 'k nodes remain?' guard
Code pattern to memorize
dummy -> head; groupPrev = dummy
loop:
kth = walk k nodes from groupPrev; if kth is null: break # leftover < k
groupNext = kth.next
reverse [groupPrev.next .. kth] with prev = groupNext
tmp = groupPrev.next # old head -> becomes block tail
groupPrev.next = kth # stitch prev block tail -> new head
groupPrev = tmp
return dummy.nextRules to remember
- 1Check first: confirm k full nodes remain before reversing; a trailing block of fewer than k is left unchanged.
- 2Use a dummy node so the first block's reversal (which changes the head) needs no special case.
- 3Per block, reconnect two links: previous block's tail → this block's new head (the old kth node), and this block's new tail (old head) → the next block.
- 4Initialise the flip's
prevto groupNext so the reversed block's tail already points at the next block. - 5Recursive version: recurse on the remainder, then reverse the front k and link
head.nextto the recursive result; base case is fewer than k nodes → return head.
Use this pattern when
- Reverse a linked list in fixed-size chunks
- Any per-block in-place pointer rewrite with a remainder rule
- Batch/window processing of a singly linked list
- Follow-ups to Reverse Linked List and Reverse Linked List II
Choose another pattern when
- Reverse a single arbitrary segment → Reverse Linked List II (one window)
- Reverse the whole list → Reverse Linked List (no blocks)
- k = 2 only → Swap Nodes in Pairs (a special case)
Interview takeaway
Reverse Nodes in k-Group is partial reversal generalized to a stream of blocks. The hard part isn't the flip — it's the bookkeeping that stitches block N's tail to block N+1, plus the 'enough nodes left?' guard. Nail the iterative O(1)-space version and the recursive form falls out for free.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternBlock-wise in-place reversal with a remainder guard. A dummy node erases the first-block edge case; each block reverses with the standard flip and is stitched to its neighbours via two boundary links. Iterative is O(1) space (the follow-up answer); recursion trades O(n/k) stack for clarity. Same family as Reverse Linked List, Reverse Linked List II, and Swap Nodes in Pairs.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Reverse Linked ListThe atomic operation — reverse the whole list with the prev/curr/next flip; k-group reuses it per block.
- Reverse Linked List IIReverse a single bounded window [left, right] — one block instead of a stream of them.
- Swap Nodes in PairsComing soonExactly this problem with k = 2 — the simplest fixed-block reversal.
- Rotate ListComing soonConnect the tail to the head to form a ring, then break it k steps before the end — different rewiring, same pointer discipline.