#25 链表指针 困难

K 个一组翻转链表

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

示例 2

输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

示例 3

输入:head = [1], k = 1
输出:[1]

💭 模拟答题者思考

1. 我先想暴力:把节点值复制到数组,按每 k 个一组反转数组片段再重建链表——能过,但题目要求「实际进行节点交换」,且多用了 O(n) 额外空间。

2. 重复在哪里?#24 两两交换是 k=2 的特例;核心仍是「确定一组边界 → 局部反转 → 把反转后的组接回主链 → 前驱移到本组尾节点」。

3. 难点是边界:剩余节点不足 k 个时不翻转。因此每轮先从 group_prevk 步找 kth;找不到就直接结束。

4. 找到 kth 后,在 [group_prev.next, kth] 闭区间内做标准链表反转,反转边界是 group_next = kth.next。反转完把 group_prev.next 指向新头 kth,再令 group_prev = 原组头 处理下一组。

5. 第一组翻转会改变头节点——加 dummy 哨兵后,group_prevdummy 出发,与 #24、合并链表等题同一套路;整体时间 O(n),每个节点最多被访问常数次。

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
dummyListNode*定义:哨兵头节点,dummy.next = head
维护:始终位于真实头节点之前,统一处理「第一组翻转后新头节点」的边界
更新:创建后不再移动,最终返回 dummy.next
group_prevListNode*定义:当前待翻转 k 组的前驱指针
维护:每组翻转完成后,group_prev 应停在「刚翻转完的那一组的尾节点」(即翻转前的组头)
更新:翻转一组后 group_prev = group_start(原组头变尾),下一轮从 group_prev.next 继续
kthListNode*定义:从 group_prev 出发向后走 k 步得到的节点,即当前组的尾节点
维护:若 kthnull,说明剩余不足 k 个节点,整题结束
更新:每轮用 getKth(group_prev, k) 重新计算
group_nextListNode*定义:当前组尾节点 kth 的下一个节点,即下一组的起点
维护:翻转时作为内层反转循环的终止边界(curr != group_next
更新:每轮在确认 kth 存在后令 group_next = kth.next
prev / currListNode*定义:组内局部反转的双指针,prev 初始为 group_nextcurr 初始为 group_prev.next
维护:标准单链表反转:保存 tmp = curr.nextcurr.next = prev,前移 prevcurr
更新:当 curr == group_next 时本组反转完成;此时 kth 成为新组头,group_start 成为新组尾

⌨️ 落码步骤

1. 创建哨兵 dummy = ListNode(0, head)group_prev = dummy

2. 循环:调用 getKth(group_prev, k) 找当前组尾 kth;若为 null 则剩余不足 k 个,跳出循环

3. 记录 group_next = kth.nextgroup_start = group_prev.next(翻转后将变成本组尾)

4. 在 [group_start, kth] 内局部反转:prev = group_nextcurr = group_start,标准三指针翻转直到 curr == group_next

5. 接回主链:group_prev.next = kth(新组头),group_prev = group_start(新组尾作下轮前驱);返回 dummy.next

💻 代码实现

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        dummy = ListNode(0, head)   # 哨兵,统一处理头节点变化
        group_prev = dummy

        while True:
            kth = self.getKth(group_prev, k)
            if not kth:
                break

            group_next = kth.next
            group_start = group_prev.next

            # 在 [group_start, kth] 内局部反转,边界为 group_next
            prev, curr = group_next, group_start
            while curr != group_next:
                tmp = curr.next
                curr.next = prev
                prev = curr
                curr = tmp

            group_prev.next = kth          # 前驱接到翻转后的新头
            group_prev = group_start       # 原组头变尾,作为下一组前驱

        return dummy.next

    def getKth(self, curr: ListNode, k: int) -> Optional[ListNode]:
        while curr and k > 0:
            curr = curr.next
            k -= 1
        return curr
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode dummy(0, head);  // 哨兵,统一处理头节点变化
        ListNode* group_prev = &dummy;

        while (true) {
            ListNode* kth = getKth(group_prev, k);
            if (!kth) break;

            ListNode* group_next = kth->next;
            ListNode* group_start = group_prev->next;

            // 在 [group_start, kth] 内局部反转,边界为 group_next
            ListNode* prev = group_next;
            ListNode* curr = group_start;
            while (curr != group_next) {
                ListNode* tmp = curr->next;
                curr->next = prev;
                prev = curr;
                curr = tmp;
            }

            group_prev->next = kth;       // 前驱接到翻转后的新头
            group_prev = group_start;     // 原组头变尾,作为下一组前驱
        }
        return dummy.next;
    }

private:
    ListNode* getKth(ListNode* curr, int k) {
        while (curr && k > 0) {
            curr = curr->next;
            k--;
        }
        return curr;
    }
};
// 时间 O(n),空间 O(1)

📈 复杂度分析

时间复杂度 O(n)
空间复杂度 O(1)

⚠️ 常见坑

忘记先检查剩余是否足 k 个:不足时应保持原顺序直接结束,不能强行翻转。

局部反转边界写错:prev 应初始化为 group_next(不是 null),否则翻转后无法与后续链表正确衔接。

翻转后忘记更新 group_prev:应移到原组头(现组尾),否则下一轮会从已翻转区域重复操作或断链。

🔍 必测边界 Case

Case 1:k = 1
head = [1,2,3], k = 1 → [1,2,3](每组 1 个,等价于不翻转)
Case 2:节点数恰为 k 的倍数
head = [1,2,3,4], k = 2 → [2,1,4,3](全部参与翻转)
Case 3:尾部不足 k 个
head = [1,2,3,4,5], k = 3 → [3,2,1,4,5](最后 4、5 保持原序)