力扣解题-25. K 个一组翻转链表
给你链表的头节点 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 <= 5000
0 <= Node.val <= 1000
进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?
Related Topics
递归、链表
第一次解答
解题思路
核心方法:迭代分块翻转法(O(1)空间),通过哑节点+分块处理的思路,将链表按k个节点为一组拆分,逐组翻转后重新拼接,剩余不足k个的节点保持原顺序,满足进阶的O(1)额外空间要求,是本题的最优迭代解法。
核心逻辑拆解
K个一组翻转链表的核心是“分块→翻转→拼接”,关键在于精准定位每组的边界并处理拼接逻辑:
- 边界处理:若链表为空(
head==null)或k=1(无需翻转),直接返回原链表; - 哑节点初始化:创建
dummy哑节点(dummy.next=head),prevGroupEnd指针初始指向dummy(作为上一组翻转后的尾节点); - 循环分块处理:
- 定位当前组尾节点:
end指针从prevGroupEnd开始遍历k步,若中途end==null(剩余节点不足k个),直接返回结果; - 标记关键节点:
start:当前组的头节点(prevGroupEnd.next);nextGroupStart:下一组的头节点(end.next);
- 断开当前组:将
end.next置为null(便于独立翻转当前组); - 翻转当前组:调用
reverseList翻转start到end的节点,翻转后end变为当前组新头,start变为当前组新尾; - 重新拼接链表:
prevGroupEnd.next = end(上一组尾节点连接当前组新头);start.next = nextGroupStart(当前组新尾连接下一组头节点);
- 更新指针:
prevGroupEnd = start(将当前组新尾作为下一组的“上一组尾节点”);
- 定位当前组尾节点:
- 返回结果:循环结束后返回
dummy.next(跳过哑节点)。
具体步骤(以示例1 head=[1,2,3,4,5]、k=2为例)
| 步骤 | 操作 | 关键指针状态 | 链表状态 |
|---|---|---|---|
| 1 | 初始化dummy→1→2→3→4→5 | prevGroupEnd=dummy | - |
| 2 | 定位end=2(k=2步) | end=2, start=1, nextGroupStart=3 | - |
| 3 | 断开end.next=null | end.next=null | dummy→1→2(独立组) |
| 4 | 翻转当前组 | 翻转后1←2 | - |
| 5 | 拼接:prevGroupEnd.next=2 | dummy→2→1 | - |
| 6 | 拼接:start.next=3 | dummy→2→1→3→4→5 | - |
| 7 | 更新prevGroupEnd=1 | prevGroupEnd=1 | - |
| 8 | 定位end=4(k=2步) | end=4, start=3, nextGroupStart=5 | - |
| 9 | 重复步骤3-7 | - | dummy→2→1→4→3→5 |
| 10 | 定位end=5(仅1步,不足k) | end=null,返回结果 | 最终链表:2→1→4→3→5 |
性能说明
- 时间复杂度:O(n)(每个节点仅被访问两次:一次分块遍历,一次翻转,n为节点总数);
- 空间复杂度:O(1)(仅使用指针变量,无额外数据结构),满足进阶要求;
- 优势:
- 纯迭代实现,无递归栈开销,空间效率最优;
- 分块处理逻辑清晰,边界条件(不足k个节点)天然处理;
- 哑节点避免了头节点翻转的特殊处理。
public ListNode reverseKGroup(ListNode head, int k) {
if(head==null || k==1){
return head;
}
ListNode dummy = new ListNode(0);
dummy.next=head;
ListNode prevGroupEnd=dummy;
while(true){
//找到当前组的尾巴节点
ListNode end=prevGroupEnd;
for(int i=0;i<k;i++){
end=end.next;
if(end==null){
return dummy.next;
}
}
//当前组第一个节点
ListNode start=prevGroupEnd.next;
//下一组反转的起点
ListNode nextGroupStart=end.next;
//断开尾巴
end.next=null;
//开始反转
reverseList(start);
//重新连接三段
prevGroupEnd.next=end;//end是新的头
start.next=nextGroupStart;//start是新的尾巴,连上下一组
//继续下一组
prevGroupEnd=start;
}
}
public ListNode reverseList(ListNode head) {
if (head == null) {
return head;
}
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
示例解答
解题思路
解法1:递归法(思路简洁,空间O(n/k))
核心方法:递归分治翻转,将“K个一组翻转”拆解为“翻转当前组 + 递归翻转剩余组”,逻辑更简洁但递归栈深度为n/k(空间复杂度O(n/k)),不满足进阶的O(1)空间要求,但易于理解。
代码实现
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || k == 1) {
return head;
}
// 步骤1:检查当前组是否有k个节点
ListNode curr = head;
int count = 0;
while (curr != null && count < k) {
curr = curr.next;
count++;
}
// 不足k个,直接返回原头
if (count < k) {
return head;
}
// 步骤2:翻转当前组的k个节点
ListNode pre = null;
ListNode cur = head;
count = 0;
while (cur != null && count < k) {
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
count++;
}
// 步骤3:递归翻转剩余组,当前组尾节点(原head)连接剩余组的头
head.next = reverseKGroup(cur, k);
// 返回当前组的新头(pre)
return pre;
}
核心逻辑说明
- 递归终止条件:
- 链表为空或k=1,直接返回;
- 当前组节点不足k个,返回原头节点(剩余节点不翻转);
- 翻转当前组:手动翻转前k个节点,
pre变为当前组新头,cur指向剩余组头; - 递归拼接:当前组原头节点(
head)的next指向剩余组翻转后的头节点; - 返回结果:返回当前组新头(
pre)。
性能说明
- 时间复杂度:O(n)(每个节点仅被访问一次);
- 空间复杂度:O(n/k)(递归栈深度为分组数,n/k);
- 优势:代码极简,递归分治思路符合直觉,无需处理复杂的指针拼接;
- 劣势:递归栈有额外空间开销,不满足进阶的O(1)空间要求。
解法2:迭代法优化(原地翻转,无需断开组)
核心方法:原地翻转优化,在原迭代法基础上,无需断开当前组(去掉end.next=null),直接在原链表中翻转k个节点,减少指针操作步骤,代码更简洁。
代码实现
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || k == 1) {
return head;
}
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prevGroupEnd = dummy;
ListNode curr = head;
int count = 0;
while (curr != null) {
count++;
// 每累计k个节点,翻转一次
if (count == k) {
// 翻转prevGroupEnd.next 到 curr 的k个节点
prevGroupEnd = reverseBetween(prevGroupEnd, curr.next);
// 重置计数,curr指向新的起始节点
curr = prevGroupEnd.next;
count = 0;
} else {
curr = curr.next;
}
}
return dummy.next;
}
// 翻转from.next 到 to.prev 的节点,返回翻转后的尾节点
private ListNode reverseBetween(ListNode from, ListNode to) {
ListNode pre = from;
ListNode cur = from.next;
ListNode start = cur; // 翻转后的尾节点
while (cur != to) {
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
// 拼接:from连接新头(pre),原头(start)连接to
from.next = pre;
start.next = to;
return start;
}
核心逻辑说明
- 计数分块:遍历链表时累计节点数,每达到k个节点触发一次翻转;
- 原地翻转:调用
reverseBetween直接翻转from.next到to.prev的k个节点,无需断开链表; - 返回尾节点:
reverseBetween返回翻转后的尾节点,作为下一组的prevGroupEnd; - 优势:减少“断开-翻转-拼接”的步骤,指针操作更高效,代码更简洁。
性能说明
- 时间复杂度:O(n)(与原迭代法一致);
- 空间复杂度:O(1)(与原迭代法一致);
- 优势:无需断开链表,减少指针操作次数,执行效率略高;
- 劣势:
reverseBetween函数增加了代码抽象度,新手理解稍难。
总结
- 迭代分块翻转法(第一次解答):O(n)时间+O(1)空间,满足进阶要求,分块逻辑清晰,是工程首选的最优解法;
- 递归分治翻转法:O(n)时间+O(n/k)空间,代码极简、易理解,但有递归栈开销,不满足进阶要求;
- 迭代原地翻转优化版:O(n)时间+O(1)空间,减少指针操作步骤,执行效率更高,代码稍抽象;
- 关键技巧:
- 核心思想:K个一组翻转的本质是“分块处理+局部翻转+链表拼接”,哑节点是处理头节点的关键;
- 空间优化:纯迭代法可实现O(1)空间,递归法易理解但有栈开销;
- 边界处理:遍历分块时需检查剩余节点是否≥k,不足则直接返回,保证剩余节点顺序不变。