1. 题目简述(题目链接)
给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
限制:你不能只是单纯地改变节点内部的值,而是需要实际进行节点交换。

2. 核心实现方法:迭代 + 分组反转
这道题的最优解法是使用迭代法 配合虚拟头节点 (Dummy Node)。整体思路分为两步:
- 分组探路 :从当前组的起点向后走 k 步,如果能走满 k 步,说明这一组需要翻转;如果不足 k步,说明剩余节点保持原序即可,直接结束。
- 原地反转并拼接:在组内进行标准的链表反转,但需要注意将反转后的尾部与下一组的头部重新连接。
3.推演 (k=3 )
为了彻底讲清楚反转过程,我们避开简单的 k=2,采用一个更复杂的例子
链表:1 -> 2 -> 3 -> 4 -> 5,k = 3
3.1 初始状态与指针定义
我们先定义几个关键指针:
- dummy:虚拟头节点,指向 1。
- groupPrev:指向当前待翻转组的前一个节点。初始时 groupPrev = dummy。
- kth:用来探路,寻找当前组的第 k个节点。
- groupNext:当前组翻转后,应该连接的下一个节点。
初始链表状态:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
groupPrev 指向 dummy。
3.2 第一轮循环:翻转 1, 2, 3
Step 1: 探路寻找第 k 个节点
从 groupPrev 出发向后走 3 步,找到 kth = 3。
此时,groupNext = kth->next = 4。(记录下下一组的起点)
Step 2: 核心反转逻辑
我们使用 prev 和 curr 两个指针进行局部反转。
关键技巧:让 prev 初始指向 groupNext(即 4),curr 初始指向当前组的头节点(即 1)。
这样做的目的是:在反转的过程中,顺带把当前组的尾节点直接连到下一组的头上,省去后续的拼接步骤。
初始:prev = 4, curr = 1
第 1 次循环:
temp = curr->next (temp = 2)
curr->next = prev (1 -> 4)
prev = curr (prev = 1)
curr = temp (curr = 2)
此时局部状态: 1 -> 4*,* 2和 3还没处理。
第 2 次循环:
temp = curr->next (temp = 3)
curr->next = prev (2 -> 1)
prev = curr (prev = 2)
curr = temp (curr = 3)
此时局部状态: 2 -> 1 -> 4*。*
第 3 次循环:
temp = curr->next (temp = 4,即 groupNext)
curr->next = prev (3 -> 2)
prev = curr (prev = 3)
curr = temp (curr = 4)
此时局部状态: 3 -> 2 -> 1 -> 4*。*
Step 3: 循环结束与拼接
当 curr == groupNext (即 4) 时,反转循环结束。
此时,prev 指向了反转后的新头节点 3。
将前驱节点连上新头:groupPrev->next = prev; (dummy -> 3)
更新 groupPrev 为下一轮的前驱节点:groupPrev = temp; (注意这里的 temp 是第一次循环前保存的 1,即反转后的尾节点)。groupPrev 现在指向1。
第一轮结束后的链表状态:
dummy -> 3 -> 2 -> 1 -> 4 -> 5 -> null
groupPrev 指向 1。
3.3 第二轮循环:处理剩余节点 4, 5
Step 1: 探路失败
从 groupPrev (即 1) 出发向后走 3 步:
第 1 步到 4,第 2 步到 5,第 3 步发现 kth 变成了 nullptr。
因为不足 3 个节点,说明剩余节点不需要翻转。
Step 2: 退出循环
直接 break 跳出大循环。
3.4 最终结果
返回 dummy->next,得到最终链表:
3 -> 2 -> 1 -> 4 -> 5
4. 代码实现
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
// 创建虚拟头节点,避免处理头节点翻转的特殊情况
ListNode* dummy = new ListNode(0);
dummy->next = head;
// groupPrev 指向当前要翻转的 k 个节点的前一个节点
ListNode* groupPrev = dummy;
while (true) {
// 1. 检查剩余节点是否足够 k 个
ListNode* kth = groupPrev;
for (int i = 0; i < k && kth != nullptr; ++i) {
kth = kth->next;
}
// 如果不足 k 个,说明后面的不用翻转了,直接退出
if (kth == nullptr) {
break;
}
// 2. 记录下一组的起始节点
ListNode* groupNext = kth->next;
// 3. 翻转当前组的 k 个节点
// 核心技巧:prev 初始指向 groupNext,方便反转后直接连上后面
ListNode* prev = groupNext;
ListNode* curr = groupPrev->next;
while (curr != groupNext) {
ListNode* temp = curr->next;
curr->next = prev;
prev = curr;
curr = temp;
}
// 4. 连接翻转后的部分与前面的链表
ListNode* temp = groupPrev->next; // 原来的头节点,现在是尾节点
groupPrev->next = prev; // 前驱节点指向新头
groupPrev = temp; // groupPrev 移动到当前组的尾部,准备下一轮
}
return dummy->next;
}
};

5. 复杂度分析
时间复杂度:O(N)。每个节点最多被遍历两次(一次是探路寻找 kth,一次是反转操作),整体呈线性时间。
空间复杂度:O(1)。只使用了常数个额外的指针变量,满足题目"实际进行节点交换"且不消耗额外空间的要求。