LC 25 — Reverse Nodes in K-Group — is the final boss of linked list reversal. Not because the core operation is harder. The reversal loop is identical. The stitching protocol is identical. What changes is the orchestration: instead of one reversal on one zone, you perform multiple reversals on consecutive zones, each exactly k nodes long, stitching each reversed group back into the chain before moving to the next.
The setup: given a linked list and an integer k, reverse the nodes in groups of k. If the total number of nodes is not a multiple of k, leave the remaining nodes in their original order. For a list [1, 2, 3, 4, 5, 6, 7, 8] with k = 3, the output is [3, 2, 1, 6, 5, 4, 7, 8]. Two full groups reversed ([1,2,3] becomes [3,2,1], [4,5,6] becomes [6,5,4]), and the remainder [7,8] stays put because 2 < 3.
Most first ideas collapse before they reach code. "Reverse the whole list, then flip every window of size k“ discards the very boundaries we need — once reversed, you cannot tell which group a node originally belonged to without counting from the new tail, which is the original walk in disguise and uses double the pointer writes. ”Copy values into an array, reverse in blocks, rebuild the list" is O(n) extra space — the interviewer asked for in-place, and this variant ignores the constraint that makes linked lists interesting. "Recurse: reverse the first k, then recurse on the rest" is elegant on paper but silently uses O(n/k) stack frames; on a 100,000-node list with k = 2 that is 50,000 deep, enough to stack-overflow most JavaScript engines.
The one naive approach that almost works is the compiler-brain greedy: “walk, reverse, walk, reverse.” It is the right shape. But it glosses over three sharp moments: the first group has nothing in front of it to reconnect to, every other group needs a handle on the previous group's tail, and the last partial group must be detected before any pointers move. Miss any of these and the output corrupts silently — the list still traverses, just not in the order the problem demands.
Every correctly written reverseKGroup has the same three-beat rhythm inside the outer loop. Beat one: save groupPrev. Before reversing, you must already hold the tail of the previously reversed group (for the first group, this is a dummy node pointing at the real head). Beat two: reverse exactly k nodes in-place. The core loop from LC 206 runs here, bounded by a counter instead of a null terminator. Beat three: stitch forward. Write groupPrev.next = prev (the new head of the reversed group) and advance groupPrev to the node that started the group — it is now the group's tail, ready to anchor the next iteration.
That rhythm is the whole algorithm. The walk, the counter, the dummy node, the edge cases — all exist to make each beat safe.
Slide k to see how grouping changes. Remainder nodes stay untouched.
The algorithm decomposes into a loop that processes one group per iteration. Each iteration follows the same four-step protocol from SC-4: walk to find the group boundary, save the anchor points, reverse k nodes with the standard loop, stitch the reversed group back. Then advance the anchors to the new boundary and repeat.
But there are two complications that the single-zone version did not have.
Complication 1: the remainder must not be reversed. The algorithm must look ahead k nodes before committing to a reversal. If fewer than k nodes remain, it stops. This means the loop has a two-phase cadence: first, count k nodes ahead to check if a full group exists; second, reverse the group. The count phase is pure traversal — no mutation. It exists solely as a guard. Without it, the algorithm would reverse partial groups, producing incorrect output.
The guard is subtle: before reversing anything, walk k steps forward and count. Don't mutate. Don't save .next pointers. Just advance a temporary pointer kth and tick a counter. If the walk runs off the end of the list before count reaches k, we know the current segment is a partial group and we break out of the outer loop with the list untouched. If the walk succeeds, we know the reversal is safe to commit — the next k nodes exist and have known identities.
This is a powerful design move worth naming: count-ahead is a dry run. Writing destructively and then trying to undo would require rolling back every pointer change, which is only possible if you also saved every mutation — which costs as much as the reversal itself. A read-only walk commits to nothing. It is cheap (O(k) per group, O(n) total across the whole list — same asymptotic cost as the reversal itself), and it decouples the “can we?” question from the “do it” action. Many linked-list algorithms share this pattern: walk once to measure, walk again to mutate.
Tap Count to probe k nodes ahead.
Complication 2: the first group is special. In sublist reversal, connection was a real node — the node at position m-1. In k-group reversal, there is no node before the first group. The list head itself is part of the first group. After reversing group 1, the new head of the entire list changes: it becomes prev (the last node of the original first group, now the front of the reversed group). For all subsequent groups, stitching uses groupPrev.next = prev — where groupPrev is the tail of the previously reversed group. But for the first group, there is no groupPrev yet. You must either use a dummy node (so groupPrev starts as dummy) or handle the first-group case with an if.
The first group has a problem: there is no previous-group tail to stitch from. The list head IS the first node. Special-casing this in code looks like if (groupPrev === null) head = prev else groupPrev.next = prev — a branch on every iteration that only ever fires once. Interviewers notice this kind of asymmetry, and worse, it sprinkles null-checks through logic that is otherwise uniform.
The idiom is to allocate a dummy node before the real head: const dummy = { val: 0, next: head }. Now the “previous group's tail” is always dummy on the first iteration. The branch collapses into a single groupPrev.next = prev that works uniformly across every group. At the end, return dummy.next — which holds whichever node won the front position after all the rewiring (the first reversed group's new head on a full first group, or the original head if the entire list was a short partial group). One extra ListNode of space buys one fewer special case, and the code reads like the algorithm itself.
Check if groupPrev is null. If so, set head = prev. Otherwise, groupPrev.next = prev.
The architecture of the solution looks like this:
while (curr !== null) { // Phase A: count k nodes ahead let count = 0, check = curr while (check && count < k) { check = check.next; count++ } if (count < k) break // remainder — stop here // Phase B: reverse k nodes (standard loop) let prev = null, node = curr for (let i = 0; i < k; i++) { /* save-sever-advance */ } // Phase C: stitch if (first group) newHead = prev else groupPrev.next = prev groupPrev = curr // curr is now the tail of the reversed group curr = check // advance to next group}Three phases per iteration. The count phase guards against partial groups. The reversal phase is the unchanged core loop. The stitch phase connects the latest reversed group to everything that came before it. After all iterations, groupPrev.next must point at whatever remains — either the start of the unmodified remainder or null.
Below, you will work through this on an 8-node list with k = 3. Two full groups and one remainder. The strategy is to predict what happens at each boundary — the first group's head reassignment, the second group's stitching, the remainder's treatment — then verify.
You have 8 nodes and k=3. The constraint is in-place (no new nodes, O(1) extra space). What is the overall strategy?
Three algorithms. One invariant. One loop body. The differences are entirely in the wrapper code — the bookkeeping around the reversal.
LC 206 — Reverse Linked List. The simplest case. No walk (start at head). No saves (no surrounding segments to reconnect). No stitch (the entire list is reversed). The loop runs until curr === null. Return prev. Four lines of meaningful code.
LC 92 — Reverse Linked List II. Walk to m-1, save connection and tail, reverse n - m nodes with a bounded loop, stitch with two assignments. The loop body is identical to LC 206. The wrapper adds the walk, the saves, and the stitch. Six additional lines, all of which serve the boundary protocol.
LC 25 — Reverse Nodes in K-Group. Wrap the LC 92 protocol in an outer loop. Each iteration: count k ahead (guard), reverse k nodes (same loop body), stitch the reversed group to the chain. First group gets the head-reassignment edge case. Subsequent groups use groupPrev.next = prev. Remainder exits early without reversing.
The progression reveals a pattern in the pattern. Every variant uses the same inner loop. The complexity lives in boundary management.
Same core loop. The wrapper grows — the reversal does not.
Tap each row to expand:
Tap a row to expand. Same inner loop -- different wrapper.
This table is the mental model. Once you internalize it, you can write any reversal variant by asking three questions: Where does the reversal start? When does it stop? What must be reconnected?
The invariant you discovered in Act I — save before you sever — is the foundation. Every variant trusts that invariant. The walk trusts it. The bounded loop trusts it. The stitch trusts it. Even the k-group orchestration trusts it: when you count k nodes ahead, you are not changing any pointers. The pointers change only inside the reversal loop, which always saves before severing.
const next = curr.next // savecurr.next = prev // severprev = curr // advance prevcurr = next // advance currFour lines. They appear in LC 206, LC 92, and LC 25 — identical, unchanged, carrying the same invariant. Everything else is orchestration.
When you see LC 206, LC 92, and LC 25 side by side, here's what is invariant and what is variable. The invariant is the four-line body above — the save-sever-advance-advance rhythm, unchanged across all three problems. Everything else is scaffolding. The walk is scaffolding: it exists only so the inner loop starts at the right node. The saves are scaffolding: they exist only so the stitch can run after the loop. The counter is scaffolding: it tells the inner loop when to stop when there is no null terminator. The first-group branch is scaffolding: it exists only because the list head has no predecessor. The count-ahead guard is scaffolding: it protects against partial groups without actually reversing anything.
That reframing is what interviews test. The naive way to “study” these three problems is to memorize three solutions. The synthesis way is to memorize one loop body and a decision tree for the scaffolding: Where does the reversal start? (head / walked-to position / loop-advanced position). When does it stop? (null / counter / counter). What must be reconnected? (nothing / two endpoints / previous group's tail + next group's head). When an interviewer says “now do it for groups of k,” you do not start from scratch. You copy the four-line body, then reason about which scaffolding you need.
You now have all three. The core loop is muscle memory. The boundary protocol is systematic. The edge cases are cataloged. Act II is complete.