描述
将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表
如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样
你不能更改节点中的值,只能更改节点本身。
数据范围: 0≤n≤2000 0≤n≤2000 , 1≤k≤20001≤k≤2000 ,链表中每个元素都满足 0≤val≤10000≤val≤1000
要求空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)
例如:
给定的链表是 1→2→3→4→51→2→3→4→5
对于 k=2k=2 , 你应该返回 2→1→4→3→52→1→4→3→5
对于 k=3k=3 , 你应该返回 3→2→1→4→53→2→1→4→5
解题思路:
- 确认是否有足够的节点做一次 k-个的翻转
- 代码用变量 tail 从 head 开始向后移动 k 步(循环 i 从 0 到 k-1,每次 tail = tail.next)。
- 目的:检查当前段是否至少有 k 个节点;同时移动后的 tail 将指向“当前段之后的第一个节点”(也就是下一段的 head)。
- 如果在这 k 次移动中遇到 tail == null,说明剩余节点不足 k 个,函数直接返回 head(不做反转,保持原链表不变)。
- 直观理解:先看够不够翻。如果不够,就把剩下的直接连上,不动。
- 局部翻转当前这 k 个节点(head 到 tail 之间)
- 设 pre = null,cur = head。循环条件是 while (cur != tail),也就是把从 head 开始直到 tail 前的所有节点反转。
- 在循环里:
- 保存 cur.next 到临时变量 temp(因为接下来会改 cur.next),
- 把 cur.next 指向 pre(完成一步反转),
- 然后把 pre 移到 cur,cur 移到 temp,继续下一步。
- 循环结束时:
- pre 指向已经反转好的这一段的新头(原来的 head 现在是段的尾),
- cur == tail(指向下一段的起点或 null)。
- 连接当前反转后的段与后续处理结果(递归)
- 现在当前段已经反转完,原来的 head 节点变成了这段的尾节点。要把它的 next 指向后面处理好的链表。
- 代码用 head.next = reverseKGroup(tail, k); 递归调用,把 tail(即下一段的起点)作为新一轮的 head 去处理并返回处理后的头,接着把当前段的尾接到它上面。
- 最后返回 pre(当前段反转后新的头),作为这次调用的返回值。
- 为什么用递归?
- 递归把问题自然拆成两部分:先处理好当前一段(反转 k 个),然后把当前段连到后面递归解决的结果上。
- 递归终止条件是:剩余节点不足 k 个(上面第一步检测到 tail == null),直接返回 head 保持不变。
- 递归形式使得代码简洁,逻辑清晰:反转一段 + 连接下一段的结果。
举个简单例子帮助理解
- 假设链表 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7,k = 3。
- 第一段是 1,2,3;tail 移动后指向 4。把 1,2,3 反转成 3->2->1;然后 head(原 1)的 next 指向 reverseKGroup(4,3) 的结果。
- 递归处理从 4 开始的链表:4,5,6 反转成 6->5->4,然后把 1.next 指向 6->5->4,接着处理剩下 7(不足 3),直接返回 7。
- 最终合并:3->2->1->6->5->4->7。
时间与空间复杂度(简要)
- 时间复杂度:O(n)。每个节点被访问、操作常数次。
- 空间复杂度:O(n/k) 递归栈深度,最坏情况下 O(n)(当 k 很小时)。如果需要常数额外空间,可以用迭代的方法实现,但递归写法更直观。
常见易错点与注意事项
- 判断 tail 是否为 null 的位置必须在移动之前或在循环里检查,否则会越界。
- while 循环条件使用 cur != tail 很巧妙:因为 tail 指向“下一段的第一个节点”,这样就能准确只反转当前这 k 个节点。
- 递归连接时用 head.next(原 head 是段尾)去接递归结果,是关键的一步,容易写错成 pre.next。
总结(一句话)
- 先检查当前是否有 k 个节点,若足够:把这 k 个节点就地反转,再把反转后的尾(原 head)接到递归处理后的后半链表上;若不足 k 个则返回原链表不变。
import java.util.*;
public class Solution {
public ListNode reverseKGroup (ListNode head, int k) {
//找到每次翻转的尾部
ListNode tail = head;
//遍历k次到尾部
for(int i = 0; i < k; i++){
//如果不足k到了链表尾,直接返回,不翻转
if(tail == null)
return head;
tail = tail.next;
}
//翻转时需要的前序和当前节点
ListNode pre = null;
ListNode cur = head;
//在到达当前段尾节点前
while(cur != tail){
//翻转
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
//当前尾指向下一段要翻转的链表
head.next = reverseKGroup(tail, k);
return pre;
}
}
如果递归难以理解也可采用以下思路:
- 程序先统计长度,然后按组迭代:找到每组的头尾,调用局部反转函数把该组翻转,再把各组正确连接起来,直到无法再组成完整的一组为止;整体为原地、线性时间的分组反转实现。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* public ListNode(int val) {
* this.val = val;
* }
* }
*/
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode reverseKGroup (ListNode head, int k) {
// write code here
//当链表为空时直接返回null
if (head == null || k == 1) {
return head;
}
//遍历链表得出链表的长度
ListNode h1 = head;
int count = 0;
while (h1 != null) {
count++;
h1 = h1.next;
}
//如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样
if (count < k) {
return head;
}
//count>k
int tag = 1;
ListNode start = head;
ListNode end = head;
ListNode temp = null;
ListNode temp1 = new ListNode(0);
while (tag * k <= count) {
int i = k - 1; //
//将end从start向后k-1个(start到end共k个)
while (i-- > 0) {
end = end.next;
}
temp = end.next;
//将start与end之间的链表next进行反转
reverse(start, end);
//标记将每k个一组翻转后得到的新链表头节点
if (tag == 1) {
h1 = end;
}
start.next = temp;
temp1.next = end;
temp1 = start;
//
start = temp;
end = temp;
tag++;
}
return h1;
}
private void reverse(ListNode start, ListNode end) {
ListNode prev = null;
ListNode curr = start;
ListNode endNext =
end.next; // 反转终止条件(当curr到达endNext时停止)
while (curr != endNext) {
ListNode next = curr.next; // 保存下一个节点
curr.next = prev; // 反转指针
prev = curr; // 移动prev
curr = next; // 移动curr
}
}
}