DSA sheet · Linked List · Reversal pattern

Reverse Nodes in k-Group

The last problem of the reversal pattern. LeetCode marks it Hard, but the teacher's point is that it is just two things we already know, joined together: Swap Nodes in Pairs (which is this problem with k = 2) and reversing a linked list with prev/curr/next. She first solves it with an array (brute force), then in place: find the k-th node, reverse the group, stitch it back, move on. Once you can draw one group being cut out, reversed and stitched back, the code writes itself.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Nodes, head and None

A linked list is a chain of nodes. Each node holds a value (val) and an arrow to the next node (next). The last arrow points to None, the end of the chain. The first node is the head, the only way into the list.

given by LeetCode, don't write this in the solution
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
[1] → [2] → [3] → [4] → [5] → None
 ↑
head

Walking, and why there is no index

There is no list[3] in a linked list. To reach the 4th node you start at head and follow 3 arrows: curr = curr.next, again and again. Reaching position i costs O(i) steps. Always walk with a helper like curr; never move head itself, or you lose the start of the list.

walk the list
curr = head
while curr is not None:
    # use curr.val
    curr = curr.next

Save next before you change it

A node only knows its own next node. The moment you overwrite curr.next, the old next node is unreachable unless you stored it in a variable first. That one rule is the whole reversal trick.

Reversing a list with prev / curr / nxt

To reverse, every arrow must turn around. We walk with three pointers:

reverse a whole list (we'll reuse this loop)
prev = None
curr = head
while curr is not None:     # stop when we fall off the end
    nxt = curr.next         # 1. save the way forward
    curr.next = prev        # 2. turn the arrow back
    prev = curr             # 3. prev steps forward
    curr = nxt              # 4. curr steps forward (using the saved copy)
# prev is now the new head
start:      None   [1] → [2] → [3] → None
             ↑      ↑
            prev   curr

after 1st:  None ← [1]   [2] → [3] → None
                    ↑     ↑
                   prev  curr

after 3rd:  None ← [1] ← [2] ← [3]    None
                                ↑      ↑
                               prev   curr  → stop, new head = 3

Two details matter for this problem: (1) the loop stops at None because we reverse "until the end"; (2) prev starts as None because the old first node becomes the last node and must point to the end. In Part B we will change both: stop at the next group instead of None, and start prev at the next group instead of None.

The dummy node

A dummy is a fake node placed before the head: dummy = ListNode(0, head). Here the head changes (the first group gets reversed, so its last node becomes the new head). With a dummy, the first group has a node in front of it just like every later group does, so no special case is needed, and the answer is always dummy.next. The teacher notes you could avoid the dummy with extra base-case code, but she always prefers the dummy because it's easier.


Part A · Brute force: reverse groups in an array

LeetCode 25 · Reverse Nodes in k-Group

1The question in simple words

You get the head of a list and a number k. Cut the list into groups of k nodes from the left. Reverse each full group. If the last group has fewer than k nodes, leave it as it is. Return the new head.

k = 2 (this is Swap Nodes in Pairs)
before: [1] → [2] │ [3] → [4] │ [5]
after:  [2] → [1] → [4] → [3] → [5] → None
                              5 alone: left as is
k = 3
before: [1] → [2] → [3] │ [4] → [5]
after:  [3] → [2] → [1] → [4] → [5] → None
                          4, 5: only 2 nodes, not reversed

With k = 2 the answer is exactly what Swap Nodes in Pairs gives. The new part is that k can be any number.

2What the constraints tell us

3Intuition: if it were an array

Pretend we had a Python list. Each group starts at index i and ends at index i + k − 1. Reversing one piece of an array is the classic two-pointer trick: put left at the start and right at the end, swap them, move both inward, stop when they meet.

index:  0   1   2   3   4          k = 3
arr:   [1,  2,  3,  4,  5]
        L       R              swap 1,3 → [3, 2, 1, 4, 5]
            L R                L == R → stop (middle stays)
next group: i = 3, end = 3+3-1 = 5 → out of range → stop

4Building the logic

How does i move? i++ or i += k?

After one group is done, the next group starts k places later, so i += k, not i += 1.

When is a group "full"?

A group from i needs index i + k − 1 to exist. That's the same as i + k ≤ n. In the k = 3 example, i = 3 gives 3 + 3 = 6 > 5, so the group (4, 5) is short and the loop stops. That's how the "leave the last short group alone" rule comes for free.

nki values (i + k ≤ n)groups reversed
530[0..2]; 3, 4 stay
520, 2[0..1], [2..3]; 4 stays
630, 3[0..2], [3..5]
440the whole list

Is it O(n²) because of a loop inside a loop?

The teacher warns against this guess. The outer loop runs only n / k times (once per group). The inner two-pointer loop does k / 2 swaps per group. Total swaps: (n / k) × (k / 2) = n / 2. So it's linear.

Turning it into a linked list solution

Walk the list with curr and copy the values into an array. Reverse the groups in the array. Walk the list again and overwrite each node's value. For 1→2→3→4→5, k = 3, we write 3, 2, 1, 4, 5 into the same five nodes and return head.

Doubt: is that really "reversing the nodes"?
→ No. The nodes stay in place; only their values change. The teacher points this out herself: it passes the judge, but LeetCode's statement asks you not to change values, and it uses O(n) extra space. That's why Part B exists.

5Approach steps

  1. If head is None or k == 1 → return head.
  2. Copy all values into arr.
  3. For i = 0, k, 2k, … while i + k ≤ len(arr): set left = i, right = i + k − 1, and swap inward while left < right.
  4. Write arr back into the nodes from the head.
  5. Return head.

6Code (Python)

Brute force: array + two pointers
class Solution:
    def reverseKGroup(self, head, k):
        if head is None or k == 1:          # nothing to reverse
            return head

        arr = []
        curr = head
        while curr is not None:             # copy values out
            arr.append(curr.val)
            curr = curr.next

        i = 0
        while i + k <= len(arr):            # only full groups
            left, right = i, i + k - 1
            while left < right:             # two-pointer reverse
                temp = arr[left]
                arr[left] = arr[right]
                arr[right] = temp
                left += 1
                right -= 1
            i += k                          # jump to the next group

        curr = head
        idx = 0
        while curr is not None:             # write values back
            curr.val = arr[idx]
            idx += 1
            curr = curr.next
        return head

7Code line by line

linewhat it means
if head is None or k == 1:Empty list or groups of one → the list doesn't change.
arr.append(curr.val)O(n) copy of all values.
while i + k <= len(arr):A group starting at i is full only if its last index i + k − 1 exists.
left, right = i, i + k - 1The two ends of this group.
while left < right: swap, move inwardReverse the group. When they meet (odd k) the middle stays put.
i += kJump over the group we just reversed.
curr.val = arr[idx]Overwrite the values in order. Head node is unchanged, so return head.

8Dry run: 1 → 2 → 3 → 4 → 5, k = 3

stepi / left / rightwhat changesarr after
1—copy values[1, 2, 3, 4, 5]
2i 0, L 0, R 2swap arr[0], arr[2][3, 2, 1, 4, 5]
3L 1, R 1L < R false → group done[3, 2, 1, 4, 5]
4i 33 + 3 = 6 > 5 → stop[3, 2, 1, 4, 5]
5curr walksoverwrite valueslist: 3 → 2 → 1 → 4 → 5

9Complexity & remember

Remember the brute forceCopy out → for i in steps of k while i + k ≤ n, two-pointer reverse [i, i+k−1] → copy back. Loop-in-loop but still n/2 swaps.

Part B · Optimal: cut, reverse and stitch in place

LeetCode 25 · O(1) extra space

1The question in simple words

Same question, but now really move the nodes by changing arrows, with no extra array.

2What the constraints tell us

3Intuition: four labelled spots around each group

For every group we name four places (example 1→2→3→4→5, k = 3):

[0] → [1] → [2] → [3] → [4] → [5] → None
 ↑     ↑           ↑     ↑
 │     │           │     group_next  (first node AFTER the group)
 │     │           kth   (last node of the group, found by k jumps)
 │     first node of the group = prev_group.next
prev_group  (node just BEFORE the group; the dummy for group 1)

The plan for one group is a three-step story:

  1. Find the cut. Starting from prev_group, jump k times to reach kth. If we fall off the list (None) before k jumps, this group is short: stop.
  2. Reverse inside the cut. Run the normal reversal, but stop when curr reaches group_next (not None), and start prev at group_next (not None), so the old first node ends up pointing straight to the rest of the list.
  3. Stitch the left side. Make prev_group.next point to kth (the new front of the group). Then the old first node, now at the back of the group, becomes prev_group for the next round.

4Building the logic from the teacher's drawing

Why do we need to reach the k-th node?

After reversing (1, 2, 3), the list must read 0 → 3 → 2 → 1 → 4. So 1 must point to 4, and the dummy must point to 3. Both 3 and 4 are k steps away from the dummy, so we walk there. The teacher first calls this walker curr, then renames it kth, because the name curr is needed for the reversal.

step 1: find the k-th node
kth = prev_group
for _ in range(k):
    kth = kth.next
    if kth is None:          # fewer than k nodes left
        return dummy.next    # leave this group as it is

Once we have kth, the node after the group is group_next = kth.next (4 here).

Why does prev start at group_next?

The teacher reasons node by node about where each arrow must point after the reversal:

In plain reversal prev starts at None because the first node becomes the tail of the whole list. Here the first node becomes the tail of the group, and it must hand over to the rest of the list. So prev = group_next, and curr = prev_group.next (node 1).

When does the reversal loop stop?

Reverse 1, reverse 2, reverse 3, and at 4 stop, because 4 belongs to the next group. 4 is exactly group_next, which is why it got its own name. So the loop is while curr is not group_next. The four lines inside are the usual ones: save next, turn the arrow, move prev, move curr = saved next (not curr.next, which now points backwards).

Snapshots of the cut-reverse loop (group 1, k = 3)

before the loop:   prev = group_next = [4],  curr = [1]
[0] → [1] → [2] → [3] → [4] → [5] → None
 ↑     ↑           ↑     ↑
 pg   curr        kth   group_next = prev

turn 1 (curr = 1):  1.next = 4
[0] → [1] → [4] → [5] → None
      [2] → [3] → [4]           (2 still leads on to 3)
prev = [1], curr = [2]

turn 2 (curr = 2):  2.next = 1
[0] → [1] → [4] → [5] → None
[2] → [1]                       (2 now points back to 1)
[3] → [4]
prev = [2], curr = [3]

turn 3 (curr = 3):  3.next = 2
[3] → [2] → [1] → [4] → [5] → None     (dummy [0] still → [1])
prev = [3] = kth, curr = [4] = group_next → loop stops

The missing stitch on the left

Now read the list from the dummy: 0 → 1 → 4 → 5. Where did 3 and 2 go? They're fine (3 → 2 → 1 → 4 → 5), but the dummy still points to 1. The dummy must point to the new front of the group, which is kth (3):

The left stitchprev_group.next = kth → 0 → 3 → 2 → 1 → 4 → 5

Where is prev_group for the next group?

For group (4, 5, 6) the node just before it is 1, the old first node of this group, now its last. Can we use head to find it? Only for the first group: head is always node 1, but for group (7, 8, 9) we'd need node 4. So the teacher saves the old first node in a temp variable before the left stitch overwrites prev_group.next:

step 3: stitch the left side and move on
temp = prev_group.next     # old first node (1); it's the group's tail now
prev_group.next = kth      # left side now points to the new front (3)
prev_group = temp          # 1 is the node before the next group
Doubt 1: why must temp be saved before prev_group.next = kth?
→ Before that line, prev_group.next is still node 1. After it, it's node 3, and we have no name left for node 1. Same "save before you overwrite" rule. (You can also save first = prev_group.next before the reversal starts; it's the same node.)
Doubt 2: what happens to the right side? Don't we need a right stitch too?
→ It's already done. Because prev started at group_next, the very first turn made 1 point to 4. The right side gets stitched inside the reversal; only the left side needs the extra line.
Doubt 3: what about the short last group (4, 5) with k = 3?
→ From prev_group = 1 we jump: 4, 5, then None on the 3rd jump. kth is None → stop without touching anything. 1 already points to 4 (from the reversal), so 4 → 5 stays attached in the original order.

The outer loop and the return

We repeat "find kth → reverse → stitch" for as long as full groups exist. The teacher writes it as while True and breaks out when kth becomes None. At the end, return dummy.next: head is still node 1, and returning it would give 1 → 4 → 5, losing 3 and 2.

5Approach steps

  1. If head is None or k == 1 → return head.
  2. dummy = ListNode(0, head), prev_group = dummy.
  3. Loop forever: walk kth k steps from prev_group. If it hits None → break.
  4. group_next = kth.next.
  5. prev = group_next, curr = prev_group.next; while curr is not group_next: save next, curr.next = prev, prev = curr, curr = saved.
  6. temp = prev_group.next; prev_group.next = kth; prev_group = temp.
  7. Return dummy.next.

6Code (Python)

Optimal: reverse each k-group in place
class Solution:
    def reverseKGroup(self, head, k):
        if head is None or k == 1:
            return head

        dummy = ListNode(0, head)
        prev_group = dummy                  # node before the current group

        while True:
            # 1. find the k-th node of this group
            kth = prev_group
            for _ in range(k):
                kth = kth.next
                if kth is None:
                    break
            if kth is None:                 # short group: leave it
                break

            group_next = kth.next           # first node after the group

            # 2. reverse the group; first node will point to group_next
            prev = group_next
            curr = prev_group.next
            while curr is not group_next:
                nxt = curr.next
                curr.next = prev
                prev = curr
                curr = nxt

            # 3. stitch the left side and move to the next group
            temp = prev_group.next          # old first node = new group tail
            prev_group.next = kth           # kth is the new group front
            prev_group = temp

        return dummy.next

7Code line by line

linewhat it means
dummy = ListNode(0, head) prev_group = dummyThe first group gets a "node before it" too.
while True:Handle one group per round; we break out when a group is short.
kth = prev_group for _ in range(k): kth = kth.nextk jumps from the node before the group land on the group's last node.
if kth is None: breakFewer than k nodes left (inner break), then leave the outer loop too. The short tail stays as it is.
group_next = kth.nextRemember where the rest of the list starts. Both the stop sign and the first prev.
prev = group_nextSo the first node of the group ends up pointing to the rest of the list (right stitch).
curr = prev_group.nextStart reversing at the group's first node.
while curr is not group_next:Turn exactly k arrows, then stop at the next group.
nxt = curr.next curr.next = prev prev = curr curr = nxtThe standard reversal: save, turn, move prev, move curr with the saved copy.
temp = prev_group.nextStill the old first node. Save it before the next line overwrites the arrow.
prev_group.next = kthLeft stitch: the node before the group now points to its new front.
prev_group = tempThe old first node is now the tail of the group, so it sits right before the next group.
return dummy.nextThe new head (the kth node of the first group).

8Dry run: 1 → 2 → 3 → 4 → 5 → 6 → 7, k = 3

Expected answer: 3 → 2 → 1 → 6 → 5 → 4 → 7 (two full groups, 7 left alone).

steppointerswhat changeslist from dummy afteranswer so far
1prev_group 0jumps 1, 2, 3 → kth 3, group_next 40→1→2→3→4→5→6→7—
2prev 4, curr 11.next = 40→1→4→5→6→7
3prev 1, curr 22.next = 10→1→4… (2→1 hidden)
4prev 2, curr 33.next = 2; curr = 4 = group_next → stop0→1→4… (3→2→1→4 hidden)
5temp 10.next = 3; prev_group = 10→3→2→1→4→5→6→73 2 1
6prev_group 1jumps 4, 5, 6 → kth 6, group_next 7same
7prev 7, curr 44.next = 70→3→2→1→4→7
8prev 4, curr 55.next = 4(5→4→7 hidden)
9prev 5, curr 66.next = 5; curr = 7 → stop(6→5→4→7 hidden)
10temp 41.next = 6; prev_group = 40→3→2→1→6→5→4→73 2 1 6 5 4
11prev_group 4jumps 7, None → short group, breaksamereturn 3→2→1→6→5→4→7
snapshot A: group 1 cut out (before reversing)
[0] → [1] → [2] → [3] → [4] → [5] → [6] → [7] → None
 ↑     ↑           ↑     ↑
pg    curr        kth   group_next = prev
snapshot B: group 1 reversed, stitched; group 2 cut out
[0] → [3] → [2] → [1] → [4] → [5] → [6] → [7] → None
                   ↑     ↑           ↑     ↑
                  pg    curr        kth   group_next
snapshot C: group 2 done, short group 3 found
[0] → [3] → [2] → [1] → [6] → [5] → [4] → [7] → None
                                     ↑     ↑      ↑
                                    pg    jump1  jump2 = None → stop

9Complexity & remember

Remember the in-place version Dummy, prev_group = dummy. Each round: k jumps → kth (None → stop) · group_next = kth.next · reverse with prev = group_next until curr is group_next · temp = prev_group.next, prev_group.next = kth, prev_group = temp. Return dummy.next.

The teacher's warning: if reversing a list isn't automatic for you yet, this problem will feel hard. Go back to plain reversal and Swap Nodes in Pairs, then come back.


Part C · Revision page

Brute force (array)Optimal (in place)
what movesvaluesnodes (arrows)
finding a full groupi + k ≤ nk jumps from prev_group don't hit None
reversing a grouptwo pointers, swap inwardprev/curr/nxt, prev starts at group_next
next groupi += kprev_group = old first node
returnheaddummy.next
time / spaceO(5n/2) = O(n) / O(n)O(n) / O(1)
Plain reversalReversal inside a k-group
prev starts atNonegroup_next
loop stops when curr isNonegroup_next
after the loopnew head = prevprev_group.next = kth
If you remember only 5 lines 1. k = 2 is Swap Nodes in Pairs; k = n is reversing the whole list.
2. Label four spots: prev_group, first, kth, group_next.
3. k jumps find kth; None means a short group → stop.
4. Reverse with prev = group_next until curr is group_next (right stitch is automatic).
5. Save the old first node, point prev_group to kth, then move prev_group to the saved node.
Mistakes to avoid ✗ starting prev at None (the group gets cut off from the rest)
✗ looping until None instead of until group_next (reverses everything)
✗ forgetting prev_group.next = kth (nodes vanish when read from the dummy)
✗ using head as the next prev_group (only right for the first group)
✗ reversing the short last group
✗ i += 1 instead of i += k in the array version
✗ returning head
test it yourself (paste under either solution)
def build(vals):
    dummy = ListNode()
    tail = dummy
    for v in vals:
        tail.next = ListNode(v)
        tail = tail.next
    return dummy.next

def to_list(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out

s = Solution()
print(to_list(s.reverseKGroup(build([1, 2, 3, 4, 5]), 2)))        # [2, 1, 4, 3, 5]
print(to_list(s.reverseKGroup(build([1, 2, 3, 4, 5]), 3)))        # [3, 2, 1, 4, 5]
print(to_list(s.reverseKGroup(build([1, 2, 3, 4, 5, 6, 7]), 3)))  # [3, 2, 1, 6, 5, 4, 7]
print(to_list(s.reverseKGroup(build([1, 2, 3]), 3)))              # [3, 2, 1]
print(to_list(s.reverseKGroup(build([1, 2]), 1)))                 # [1, 2]

Based on this video: Reverse Nodes in k-Group