困难
给你链表的头节点 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]
提示:
- 链表中的节点数目为
n 1 <= k <= n <= 50000 <= Node.val <= 1000
进阶: 你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?
📝 核心笔记:K 个一组翻转链表 (Reverse Nodes in k-Group)
1. 核心思想 (一句话总结)
"先数数,够数就翻,翻完缝合。" 这道题本质上是 LeetCode 206 (反转链表) 和 LeetCode 92 (反转链表 II) 的结合体。 我们需要维护一个"哨兵" p0,它始终站在 "当前要翻转的这一组" 的前一个位置。
- 组内:标准的链表翻转(局部倒序)。
- 组间:翻转后,原来的头变尾,原来的尾变头,需要把它们和前后段重新接上(缝合)。
2. 算法流程 (三步走)
- 统计长度 (Count):
-
- 先遍历一遍链表,得到总长度
n。 - 作用:决定循环次数。只有当剩余节点数
n >= k时才翻转,不够k个直接保留不动。
- 先遍历一遍链表,得到总长度
- 组内翻转 (Reverse Inner):
-
- 循环
k次,执行标准的"反转链表"逻辑。 - 翻转结束后,
pre变成了当前组的新头,cur变成了下一组的开头。
- 循环
- 缝合接口 (Connect):
-
- 这是最难的一步。我们需要三个节点参与缝合:
-
-
p0:当前组的前驱(哨兵)。p0.next:当前组 原来的头 (翻转后变成了 现在的尾)。cur:下一组的头。pre:当前组 现在的头。
-
-
- 连接逻辑:
-
-
p0.next.next = cur:让本组现在的尾巴,连上下一组的头。p0.next = pre:让哨兵连上本组现在的头。p0 = ...:哨兵走到本组现在的尾巴位置,准备下一轮。
-
🔍 代码回忆清单
// 题目:LC 25. Reverse Nodes in k-Group
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
// 1. 统计节点总数
int n = 0;
for (ListNode cur = head; cur != null; cur = cur.next) {
n++;
}
ListNode dummy = new ListNode(0, head);
ListNode p0 = dummy; // p0 是每一组的前驱节点 (哨兵)
// cur 和 pre 放在外面初始化,因为它们在循环之间是传递的
ListNode pre = null;
ListNode cur = head;
// 2. 循环处理每一组
// 只要剩余节点数 n >= k,就进行翻转
for (; n >= k; n -= k) {
// 3. 组内翻转 (标准的 LC 206 逻辑)
// 翻转 k 个节点
for (int i = 0; i < k; i++) {
ListNode nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
// 循环结束后:
// pre 是当前组的新头
// cur 是下一组的头 (未处理的第一个)
// 4. 缝合 (核心难点)
// p0.next 是这一组"原来的头",翻转后它变成了"现在的尾"
ListNode nxt = p0.next; // 暂存一下"现在的尾"
// 现在的尾 -> 指向下一组的头
// (对应图解:1 连向 3)
p0.next.next = cur;
// 哨兵 -> 指向当前组的新头
// (对应图解:dummy 连向 2)
p0.next = pre;
// 5. 更新 p0
// p0 走到当前组的尾部,作为下一组的哨兵
p0 = nxt;
}
return dummy.next;
}
}
⚡ 快速复习 CheckList (易错点)
-
\] **为什么要有** **n****?**
-
- 如果不统计
n,你很难判断最后剩下的节点是否足k个。如果不足k个却翻转了,就不符合题意了。 n >= k是循环继续的硬性条件。
- 如果不统计
-
\] **缝合顺序乱了?**
-
- 口诀:先连尾巴,再连头。
- 先让 旧头(新尾) 连上
cur(下一段)。 - 再让 哨兵 连上
pre(新头)。
-
\] **p0 怎么移动?**
-
p0必须移动到当前子链表的末尾。- 也就是翻转前的
head,翻转后的tail。代码里巧妙地利用p0.next(在修改前)获取了这个节点。
🖼️ 数字演练
链表:1 -> 2 -> 3 -> 4 -> 5,k = 2``n = 5, dummy -> 1. p0 = dummy.
Round 1 (处理 1, 2):
- Inner Loop:
-
- 翻转后:
2 -> 1。 pre指向 2,cur指向 3。
- 翻转后:
- Wiring (缝合):
-
p0.next是 1 (旧头/新尾)。1.next = 3(连上下一个组)。dummy.next = 2(哨兵连新头)。- 链表变为:
dummy -> 2 -> 1 -> 3 -> 4 -> 5.
- Update :
p0移动到 1。
Round 2 (处理 3, 4):
- Inner Loop:
-
- 翻转后:
4 -> 3。 pre指向 4,cur指向 5。
- 翻转后:
- Wiring:
-
p0.next是 3。3.next = 5.1.next = 4.- 链表变为:
dummy -> 2 -> 1 -> 4 -> 3 -> 5.
- Update :
p0移动到 3。
End : n=1,1 < 2,循环结束。剩下的 5 不动。