25. Reverse Nodes in k-Group

Given a linked list, reverse every consecutive group of k nodes, leaving any trailing remainder in its original order. The trick: walk ahead k steps to confirm a full group exists, then do a clean in-place reversal using a dummy head and a groupPrev pointer.

HardLinked ListPointer ManipulationIn-Place ReversalTypeScript

PROBLEM What we're solving

Given the head of a linked list and an integer k, reverse the nodes of the list k at a time and return the modified list. Nodes that are left over (fewer than k remaining) stay in their original order.

Concrete example: list = 1 → 2 → 3 → 4 → 5, k = 2.

  • Group 1: [1, 2] → reversed → [2, 1]
  • Group 2: [3, 4] → reversed → [4, 3]
  • Remainder: [5] → fewer than k=2, left alone → [5]

Output: 2 → 1 → 4 → 3 → 5.

With k = 3 the same list gives 3 → 2 → 1 → 4 → 5 (one full group of three, two left over).

KEY IDEA Count k steps first, then reverse

Insight → Before touching any pointers, walk exactly k steps from the current position to verify a full group exists. If you reach null, you 're done. Only then reverse the k nodes in-place. A dummy head node eliminates special-casing for the very first group, and a groupPrev pointer always sits just before the group being processed so you can re-stitch the chain.

RECIPE Dummy → getKth → reverse → re-stitch → advance

  • 0 · Dummy head. Prepend a new ListNode(0, head). Set groupPrev = dummy. This lets the very first group be treated identically to later groups — no edge case for the head pointer.
  • 1 · Count ahead. Call getKth(groupPrev, k) which walks k steps from groupPrev. If it returns null, fewer than k nodes remain → break. Otherwise the returned node is the kth node of the current group.
  • 2 · Record boundaries. Save groupNext = kth.next (the node immediately after the group). The group runs from groupPrev.next to kth.
  • 3 · Reverse k nodes. Standard in-place reversal: prev = groupNext, curr = groupPrev.next. Loop while curr !== groupNext, redirecting curr.next = prev and advancing both.
  • 4 · Re-stitch. After reversal, kthis now the group's new head and the old head is the new tail. Set groupPrev.next = kth. Advance groupPrev to the old group head (now tail).
  • 5 · Repeat. Continue the while loop for the next group.
Classic confusion → After the reversal loop, which node is which?Remember: reversal flips the arrows, so the node that started as the group's first element (groupPrev.next before reversal) becomes the tailof the reversed group. It's tempting to set groupPrev = groupPrev.next before saving the old head — that loses the reference. Always saveconst tmp = groupPrev.next first, then update.

COST Complexity & trade-offs

Collect → reverse → rebuild
O(n) time, O(n) space
Convert to array, slice + reverse each group, rebuild list. Simple but wastes memory.
In-place pointer surgery
O(n) time, O(1) space
Every node is visited at most twice (once by getKth, once by the reversal). No extra allocation.

Why O(n) even though getKth walks k steps per group?

There are ⌊n/k⌋ full groups. Each group costs k steps for counting and k steps for reversal → 2k × (n/k) = 2ntotal steps. The remainder nodes are touched once. So it's truly O(n), not O(n·k).

Pattern transfer → the same dummy-head + groupPrev skeleton solves Swap Nodes in Pairs (k=2, fixed), Rotate List (find the pivot and re-link), Reverse Linked List II (reverse one contiguous subrange), and any problem where you must surgically re-wire a subchain without losing neighbouring references.

RUN IT Count k ahead, reverse each full group

step 0 / 10
STARTList has 5 nodes, k = 2. Scanning groups of 2 from the front.
1// Definition for singly-linked list.
2class ListNode {
3 val: number;
4 next: ListNode | null;
5 constructor(val: number, next?: ListNode | null) {
6 this.val = val;
7 this.next = next ?? null;
8 }
9}
10
11function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
12 const dummy = new ListNode(0, head);
13 let groupPrev = dummy; // tail of the last reversed group
14
15 // eslint-disable-next-line no-constant-condition
16 while (true) {
17 // 1. Count k nodes ahead of groupPrev — bail if fewer remain
18 const kth = getKth(groupPrev, k);
19 if (!kth) break;
20
21 const groupNext = kth.next; // node just after the group
22 let prev: ListNode | null = groupNext;
23 let curr: ListNode | null = groupPrev.next;
24
25 // 2. Reverse exactly k nodes
26 while (curr !== groupNext) {
27 const tmp = curr!.next;
28 curr!.next = prev;
29 prev = curr;
30 curr = tmp;
31 }
32
33 // 3. Re-stitch: groupPrev.next was the group head, now becomes the group tail
34 const tmp = groupPrev.next!;
35 groupPrev.next = kth; // kth became the new group head after reversal
36 groupPrev = tmp; // advance groupPrev to the (old) group head (now tail)
37 }
38
39 return dummy.next;
40}
41
42/** Walk k steps from `node`; return that node, or null if fewer than k remain. */
43function getKth(node: ListNode, k: number): ListNode | null {
44 let curr: ListNode | null = node;
45 for (let i = 0; i < k; i++) {
46 curr = curr?.next ?? null;
47 if (!curr) return null;
48 }
49 return curr;
50}
k =2
1
[0]
2
[1]
3
[2]
4
[3]
5
[4]
lo pointerhi pointeractive groupreversed & placed
slowfast

TYPESCRIPT The solution, annotated

reverseKGroup.ts
// Definition for singly-linked list.
class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val: number, next?: ListNode | null) {
    this.val = val;
    this.next = next ?? null;
  }
}

function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
  const dummy = new ListNode(0, head);
  let groupPrev = dummy;           // tail of the last reversed group

  // eslint-disable-next-line no-constant-condition
  while (true) {
    // 1. Count k nodes ahead of groupPrev — bail if fewer remain
    const kth = getKth(groupPrev, k);
    if (!kth) break;

    const groupNext = kth.next;    // node just after the group
    let prev: ListNode | null = groupNext;
    let curr: ListNode | null = groupPrev.next;

    // 2. Reverse exactly k nodes
    while (curr !== groupNext) {
      const tmp = curr!.next;
      curr!.next = prev;
      prev = curr;
      curr = tmp;
    }

    // 3. Re-stitch: groupPrev.next was the group head, now becomes the group tail
    const tmp = groupPrev.next!;
    groupPrev.next = kth;          // kth became the new group head after reversal
    groupPrev = tmp;               // advance groupPrev to the (old) group head (now tail)
  }

  return dummy.next;
}

/** Walk k steps from `node`; return that node, or null if fewer than k remain. */
function getKth(node: ListNode, k: number): ListNode | null {
  let curr: ListNode | null = node;
  for (let i = 0; i < k; i++) {
    curr = curr?.next ?? null;
    if (!curr) return null;
  }
  return curr;
}

Reading it block by block

Lines 13–15 — dummy head & groupPrev. A sentinel node (dummy) with value 0 is prepended before head. groupPrevstarts here and will always point to the node just before the group we're about to reverse. This eliminates the need for a special case when the first group's head becomes the new list head.
Lines 20–22 — count k steps ahead. getKth(groupPrev, k) walks exactly k steps from groupPrev. If it hits null before finishing, fewer than k nodes remain and we break — those nodes stay untouched. Otherwise kth is the last node of the current group.
Lines 24–31 — in-place reversal. We set prev = groupNext(the node after the group, which is the target for the future last-node's next) andcurr = groupPrev.next (the group's current head). The loop redirects each curr.next backward until we've processed all k nodes. After the loop prev points to the group's new head (the old kth).
Lines 33–35 — re-stitch the chain. Save the old group head in tmp (it is now the group's tail). Set groupPrev.next = kth to connect the preceding chain to the newly reversed group. Advance groupPrev = tmp so it sits at the tail of the reversed group — ready to be the anchor for the next group.
Lines 40–48 — getKth helper. Walks k steps from node. Returns the node at position k or null if the list is too short. Using a helper keeps the main loop clean and easy to reason about.
Complexity → O(n) time — each node is visited at most twice (once during the k-count, once during reversal). O(1) extra space — only a handful of pointers; no arrays or recursion stack.

INTERVIEWFollow-ups they'll ask

  • "Can you do it recursively?" Yes: reverse the first k nodes, recurse on the rest, and stitch the tail. But recursion uses O(n/k) stack space, so the iterative version is preferred.
  • "What if you need to reverse the remainder too?" After the main loop, check if any nodes remain and reverse them the same way — or simply run one more reversal without the length check.
  • "Return the tail positions / new node values?" You can collect the new heads of each group in an array during the loop; the dummy-head pattern already tracks each groupPrev.
  • "What changes for k=1 or k=n?"k=1 means no reversal is needed (every group of one is already "reversed"). k=n reverses the whole list in one pass. Both are handled correctly by the algorithm without special cases.
  • "Brute force vs optimal?" Brute force converts the list to an array, reverses slices, and rebuilds — O(n) time but O(n) space. The pointer approach is O(1) extra space with the same time complexity.

OPTIMAL Linked List

reverseKGroup.ts
// Definition for singly-linked list.
class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val: number, next?: ListNode | null) {
    this.val = val;
    this.next = next ?? null;
  }
}

function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
  const dummy = new ListNode(0, head);
  let groupPrev = dummy;           // tail of the last reversed group

  // eslint-disable-next-line no-constant-condition
  while (true) {
    // 1. Count k nodes ahead of groupPrev — bail if fewer remain
    const kth = getKth(groupPrev, k);
    if (!kth) break;

    const groupNext = kth.next;    // node just after the group
    let prev: ListNode | null = groupNext;
    let curr: ListNode | null = groupPrev.next;

    // 2. Reverse exactly k nodes
    while (curr !== groupNext) {
      const tmp = curr!.next;
      curr!.next = prev;
      prev = curr;
      curr = tmp;
    }

    // 3. Re-stitch: groupPrev.next was the group head, now becomes the group tail
    const tmp = groupPrev.next!;
    groupPrev.next = kth;          // kth became the new group head after reversal
    groupPrev = tmp;               // advance groupPrev to the (old) group head (now tail)
  }

  return dummy.next;
}

/** Walk k steps from `node`; return that node, or null if fewer than k remain. */
function getKth(node: ListNode, k: number): ListNode | null {
  let curr: ListNode | null = node;
  for (let i = 0; i < k; i++) {
    curr = curr?.next ?? null;
    if (!curr) return null;
  }
  return curr;
}
Complexity → O(n) time — each node is visited at most twice (once during the k-count, once during reversal). O(1) extra space — only a handful of pointers; no arrays or recursion stack.

ALT 1 Recursive

O(n) time · O(n/k) stack

Reverse the first k nodes, then recurse on the rest and attach the recursed result as the new tail.

approach-2.ts
// Definition for singly-linked list.
class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val: number, next?: ListNode | null) {
    this.val = val;
    this.next = next ?? null;
  }
}

function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
  // 1. Walk k steps to confirm a full group exists.
  let kth: ListNode | null = head;
  for (let i = 0; i < k; i++) {
    if (!kth) return head; // fewer than k remain — leave this tail as-is
    kth = kth.next;
  }
  // Now kth points to the (k+1)-th node, i.e. the head of the *rest*.

  // 2. Recurse on the rest first; its result becomes our group's new tail.
  const newTail: ListNode | null = reverseKGroup(kth, k);

  // 3. Reverse exactly k nodes, threading them onto newTail.
  let prev: ListNode | null = newTail;
  let curr: ListNode | null = head;
  for (let i = 0; i < k; i++) {
    const next: ListNode | null = curr!.next;
    curr!.next = prev;
    prev = curr;
    curr = next;
  }

  // prev is now the new head of this reversed group.
  return prev;
}
Note → The cleanest mental model: flip the first group, then trust recursion for the rest. Because head becomes the group's tail, we pre-seed prev with the already-processed remainder so the stitch is automatic. The catch is the call stack: one frame per group means O(n/k) extra space, which the iterative version avoids.

ALT 2 Stack-based

O(n) time · O(k) space

Push k nodes onto a stack, then pop them to relink in reversed order.

approach-3.ts
// Definition for singly-linked list.
class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val: number, next?: ListNode | null) {
    this.val = val;
    this.next = next ?? null;
  }
}

function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
  const dummy = new ListNode(0, head);
  let groupPrev: ListNode = dummy; // tail of the last emitted group

  let curr: ListNode | null = head;
  while (curr) {
    const stack: ListNode[] = [];

    // 1. Try to collect k nodes onto the stack.
    let node: ListNode | null = curr;
    let count = 0;
    while (node && count < k) {
      stack.push(node);
      node = node.next;
      count++;
    }

    // 2. Fewer than k remain? Leave them in their original order and stop.
    if (count < k) {
      groupPrev.next = curr;
      break;
    }

    // 3. Pop the stack to relink the k nodes in reversed order.
    while (stack.length > 0) {
      groupPrev.next = stack.pop()!;
      groupPrev = groupPrev.next;
    }

    // 4. node is the first node after this group — advance and detach the tail.
    groupPrev.next = node;
    curr = node;
  }

  return dummy.next;
}
Note → The stack makes the reversal visually obvious — last pushed is first relinked. If we run out before k nodes, we splice the original curr back onto groupPrevso the short remainder keeps its order. Space is O(k) for the stack, strictly worse than the O(1) pointer surgery, but it's an easy approach to derive under pressure.

ALT 3 Brute force — dump to array, reverse full groups, rebuild

O(n) time · O(n) space

Copy every value into an array, reverse each complete block of kvalues in place (leaving a trailing remainder of fewer than kalone), then write the values back into the original nodes in order.

approach-4.ts
function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
  // 1. Collect node references and their values.
  const nodes: ListNode[] = [];
  for (let node = head; node !== null; node = node.next) nodes.push(node);

  // 2. Reverse each full group of k values within the array.
  for (let start = 0; start + k <= nodes.length; start += k) {
    let i = start;
    let j = start + k - 1;
    while (i < j) {
      const tmp = nodes[i].val;
      nodes[i].val = nodes[j].val;
      nodes[j].val = tmp;
      i++;
      j--;
    }
  }
  // Trailing fewer-than-k nodes keep their original order automatically.

  return head;
}
Note → By rewriting valfields instead of relinking, the logic is dead simple, but the array of node references is O(n) extra space — and many interviewers consider value-swapping a cop-out when the task is really about pointer manipulation. The in-place reversal achieves the same result with O(1) space by re-wiring next pointers directly.

MNEMONIC The one-liner

"Walk k steps to see if the group fits, then flip the arrows and re-stitch the seams."

TRIGGERS When you see ___ → reach for ___

"reverse every k nodes"dummy head + getKth + in-place reversal
surgically re-wire a subchainsave groupPrev before entering the group
"leave remainder untouched"break when getKth returns null
reverse a subrange of a linked listgroupPrev pointer anchors the re-stitch

SKELETON The reusable shape

skeleton.ts
const dummy = new ListNode(0, head);
let groupPrev = dummy;

while (true) {
  const kth = getKth(groupPrev, k);   // walk k steps
  if (!kth) break;                    // fewer than k remain — leave them

  const groupNext = kth.next;
  let prev = groupNext, curr = groupPrev.next;

  while (curr !== groupNext) {         // reverse k nodes
    const tmp = curr!.next;
    curr!.next = prev;
    prev = curr;  curr = tmp;
  }
  const tmp = groupPrev.next!;
  groupPrev.next = kth;               // kth is new group head
  groupPrev = tmp;                    // old head is new group tail
}
return dummy.next;

FLASHCARDS Tap to flip

Why prepend a dummy head?
It gives groupPrev a valid starting position before the first node, so the first group is handled identically to all subsequent ones — no special case for updating head.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
What is the time complexity of the iterative reverseKGroup?
QUESTION 02
Why do we prepend a dummy head node?
QUESTION 03
getKth returns null. What should the algorithm do?
QUESTION 04
Trace: list = 1→2→3→4→5, k = 2. What is the output?
QUESTION 05
After reversing a group, why must we set groupPrev = tmp (the old group head)?
QUESTION 06
What is the extra space complexity of the iterative solution?
QUESTION 07
Why is prev initialized to groupNext (not null) at the start of each group reversal?
QUESTION 08
#25 · Reverse Nodes in k-GroupCount k nodes ahead; if fewer remain, leave them as-is. Reverse each full k-node group by rewiring pointers, then connect it to the previous group tail and advance the pointer forward.Which algorithmic approach does this primarily use?
QUESTION 09
#25 · Reverse Nodes in k-GroupCount k nodes ahead; if fewer remain, leave them as-is. Reverse each full k-node group by rewiring pointers, then connect it to the previous group tail and advance the pointer forward.Which implementation correctly solves it?