【题目】 已知链表的头节点 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
【方法一】 模拟
思路与算法:
1)需要把链表节点按照 k 个一组分组,所以可以使用一个指针 head 依次指向每组的头节点。这个指针每次向前移动 k 步,直至链表结尾。
2)对于每个分组,我们先判断它的长度是否大于等于 k。若是,我们就翻转这部分链表,否则不需要翻转。
3)对于一个子链表,除了翻转其本身之外,还需要将子链表的头部与上一个子链表连接,以及子链表的尾部与下一个子链表连接。
因此,在翻转子链表的时候,我们不仅需要子链表头节点 head,还需要有 head 的上一个节点 pre,以便翻转完后把子链表再接回 pre。
4)新建一个节点,把它接到链表的头部,让它作为 pre 的初始值,这样 head 前面就有了一个节点,我们就可以避开链表头部的边界条件。
5)反复移动指针 head 与 pre,对 head 所指向的子链表进行翻转,直到结尾,我们就得到了答案。
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
// 创建虚拟头节点,简化边界处理
ListNode hair = new ListNode(0);
hair.next = head;
ListNode pre = hair; // pre 指向当前待反转组的前一个节点
while (head != null) {
ListNode tail = pre;
// 检查剩余节点是否足够 k 个
for (int i = 0; i < k; ++i) {
tail = tail.next;
if (tail == null) {
return hair.next; // 不足 k 个,直接返回结果
}
}
ListNode nex = tail.next; // 保存下一组的头节点
// 反转当前组 [head, tail]
ListNode[] reverse = doReverse(head, tail);
head = reverse[0]; // 反转后的新头节点
tail = reverse[1]; // 反转后的新尾节点
// 将反转后的子链表接回原链表
pre.next = head; // 前一组的尾 -> 当前组的新头
tail.next = nex; // 当前组的新尾 -> 下一组的头
// 移动指针,准备处理下一组
pre = tail;
head = tail.next;
}
return hair.next; // 返回处理后的链表头
}
public ListNode[] doReverse(ListNode head, ListNode tail) {
// 初始 prev 为 tail 的下一个节点(关键!)
// 为什么prev初始化为tail.next?反转后,原头节点head需指向tail的后继,因此prev需初始化为tail.next。
ListNode prev = tail.next;
ListNode p = head;
// 反转循环条件prev != tail的意义? 确保反转范围仅包含当前组[head, tail],避免影响下一组。
while (prev != tail) { // 当 prev 到达 tail 时停止(反转到 tail 为止)
ListNode nex = p.next; // 保存下一个节点
p.next = prev; // 当前节点指向前驱
prev = p; // 前驱后移
p = nex; // 当前节点后移
}
return new ListNode[]{tail, head}; // 返回反转后的新头和新尾
}
}
对于上面的代码,关键点解析如下。
1)虚拟头节点 hair:
避免处理头节点反转的特殊情况,简化边界条件。
2)分组检查:
使用tail指针从pre开始移动k步,若中途tail为null,说明剩余节点不足k个,直接返回。
3)反转逻辑:
doReverse函数反转从head到tail的节点:
初始条件:prev指向tail.next(即下一组的头节点)。
循环终止:当prev移动到tail时停止,确保只反转当前组。
返回值:{tail, head},即反转后的新头和新尾。
4)子链表连接:
反转后,pre.next指向新头节点,tail.next指向下一组的头节点nex,确保链表不断裂。
5)指针更新:
pre移动到当前组的新尾节点tail,head移动到下一组的头节点tail.next。
6)复杂度分析:
时间复杂度:O(n),其中 n 为链表的长度。head 指针会在 O(⌊n/k⌋) 个节点上停留,每次停留需要进行一次 O(k) 的翻转操作。
空间复杂度:O(1),我们只需要建立常数个变量。
采用C++代码实现的方案如下:
class Solution {
public:
// 反转从head到tail的子链表,并返回新的头和尾节点
// 返回值:使用pair返回反转后的新头和新尾,便于连接操作。pair:存储两个节点指针,分别表示反转后的新头和新尾。
pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {
ListNode* prev = tail->next; // prev初始化为尾节点的下一个节点
ListNode* p = head; // 当前处理的节点
while (prev != tail) { // 当prev未到达tail时继续反转
ListNode* nex = p->next; // 保存下一个节点
p->next = prev; // 当前节点指向前驱
prev = p; // 前驱后移
p = nex; // 当前节点后移
}
return {tail, head}; // 返回反转后的新头和新尾(原尾变为新头,原头变为新尾)
}
ListNode* reverseKGroup(ListNode* head, int k) {
// 统一处理头节点可能被反转的情况,避免边界条件判断。
ListNode* hair = new ListNode(0); // 创建虚拟头节点
hair->next = head; // 虚拟头指向原头节点
ListNode* pre = hair; // pre指向当前待反转组的前一个节点
while (head) { // 当剩余节点存在时继续处理
ListNode* tail = pre;
// 检查剩余节点是否足够k个
for (int i = 0; i < k; ++i) {
tail = tail->next;
if (!tail) { // 不足k个,直接返回结果
return hair->next;
}
}
ListNode* nex = tail->next; // 保存下一组的头节点
// 反转当前组,获取新的头和尾
// tie:C++17 特性,用于解构pair,直接赋值给head和tail。
tie(head, tail) = myReverse(head, tail);
// 将反转后的子链表接回原链表
pre->next = head; // 前一组的尾连接到新头
tail->next = nex; // 新尾连接到下一组的头
// 移动指针到下一组
pre = tail; // pre移动到当前组的新尾
head = tail->next; // head移动到下一组的头
}
return hair->next; // 返回处理后的链表头
}
};
对上面代码进行复杂度分析如下。
时间复杂度:O (n),每个节点被遍历两次(检查长度和反转)。
空间复杂度:O (1),仅使用常数级额外空间。