1. 题目描述
给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
解释:每2个节点一组翻转:1和2翻转,3和4翻转,5保留
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
解释:每3个节点一组翻转:1、2、3翻转,4和5保留
输入:head = [1,2,3], k = 1
输出:[1,2,3]
解释:每1个节点一组,等于不翻转
提示
链表中的节点数目为 n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
进阶
你能设计一个只用 O(1) 额外内存空间的算法解决此问题吗?
2. 核心思想
关键思想:模拟 + 翻转 + 组装
核心思路:先判断链表能分成几组,然后对每一组进行翻转,最后将各组连接起来。
这是第 24 题「两两交换链表中的节点」的进阶版------从每组 2 个扩展到每组 k 个。
以 [1,2,3,4,5], k = 2 为例:
第1步:先统计链表长度 n = 5
第2步:翻转第1组(节点1和2)
原始:1 → 2 → 3 → 4 → 5
翻转:2 → 1 → 3 → 4 → 5
此时:h = dummy, pre = 2, cur = 3
第3步:翻转第2组(节点3和4)
原始:3 → 4 → 5
翻转:4 → 3 → 5
连接:2 → 1 → 4 → 3 → 5
第4步:剩余1个节点,不翻转
最终:2 → 1 → 4 → 3 → 5 ✓
3. 多种方法解决
方法一:迭代法(推荐)✅
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) {
int n = 0;
ListNode* dummy = head;
// 第一步:统计链表长度
while (dummy) {
n++;
dummy = dummy->next;
}
// 第二步:创建虚拟头节点
dummy = new ListNode(0, head);
ListNode* h = dummy; // h 指向当前组的前一个节点
ListNode* pre = NULL; // 翻转指针
ListNode* cur = head; // 当前待处理节点
// 第三步:每 k 个节点一组进行翻转
for (; n >= k; n -= k) {
// 翻转当前 k 个节点
for (int i = 0; i < k; i++) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
// 保存下一组的起始节点
ListNode* nxt = h->next;
// 连接:当前组翻转后的尾节点指向下一组
h->next->next = cur;
// 连接:当前组的前一个节点指向翻转后的头
h->next = pre;
// h 移动到下一组的"前一个节点"位置
h = nxt;
}
return dummy->next;
}
};
复杂度: 时间 O(n),空间 O(1) ✅(满足进阶要求)
方法二:递归法
cpp
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
// 统计当前组是否有 k 个节点
int count = 0;
ListNode* cur = head;
while (cur && count < k) {
count++;
cur = cur->next;
}
// 不足 k 个,不翻转
if (count < k) {
return head;
}
// 翻转当前 k 个节点
ListNode* pre = NULL;
cur = head;
for (int i = 0; i < k; i++) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
// 递归处理后面的链表
head->next = reverseKGroup(cur, k);
return pre; // pre 是翻转后的头节点
}
};
复杂度: 时间 O(n),空间 O(n)(递归栈)❌(不满足进阶要求)
4. 图解过程
示例:head = [1,2,3,4,5], k = 2
初始状态:
dummy(0) → 1 → 2 → 3 → 4 → 5 → nullptr
↑
h, pre=NULL, cur=1, n=5
翻转第1组(k=2):
翻转后:pre=2→1→NULL, cur=3
nxt = h->next = 1
h->next->next = cur → 1→3
h->next = pre → dummy→2
h = nxt = 1
翻转第2组(k=2):
翻转后:pre=4→3→NULL, cur=5
nxt = h->next = 3
h->next->next = cur → 3→5
h->next = pre → 1→4
h = nxt = 3
退出(n=1 < k=2)
最终:2 → 1 → 4 → 3 → 5 → nullptr ✓
示例:head = [1,2,3,4,5], k = 3
初始:n=5
翻转第1组(k=3):
翻转后:pre=3→2→1→NULL, cur=4
nxt = h->next = 1
h->next->next = cur → 1→4
h->next = pre → dummy→3
h = nxt = 1
剩余 n=2 < k=3,不翻转
最终:3 → 2 → 1 → 4 → 5 → nullptr ✓
5. 方法优缺点比较
| 方法 | 时间 | 空间 | 满足进阶 | 缺点 |
|---|---|---|---|---|
| 迭代法 | O(n) ✅ | O(1) ✅ | ✅ | 代码较复杂 |
| 递归法 | O(n) ✅ | O(n) ❌ | ❌ | 递归栈开销,有栈溢出风险 |
推荐方法
迭代法 满足进阶要求,是面试和竞赛中的标准解法。
6. 完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
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) {
int n = 0;
ListNode* dummy = head;
while (dummy) {
n++;
dummy = dummy->next;
}
dummy = new ListNode(0, head);
ListNode* h = dummy;
ListNode* pre = NULL;
ListNode* cur = head;
for (; n >= k; n -= k) {
for (int i = 0; i < k; i++) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
ListNode* nxt = h->next;
h->next->next = cur;
h->next = pre;
h = nxt;
}
return dummy->next;
}
};
// 辅助函数:链表转 vector(方便打印)
vector<int> toVector(ListNode* head) {
vector<int> result;
while (head) {
result.push_back(head->val);
head = head->next;
}
return result;
}
// 辅助函数:vector 转链表
ListNode* toList(vector<int> vals) {
ListNode* dummy = new ListNode(0);
ListNode* curr = dummy;
for (int v : vals) {
curr->next = new ListNode(v);
curr = curr->next;
}
return dummy->next;
}
int main() {
Solution sol;
// 测试1:[1,2,3,4,5], k=2 → [2,1,4,3,5]
ListNode* h1 = toList({1, 2, 3, 4, 5});
vector<int> r1 = toVector(sol.reverseKGroup(h1, 2));
cout << "[1,2,3,4,5], k=2 → [";
for (int i = 0; i < r1.size(); i++)
cout << r1[i] << (i < r1.size()-1 ? "," : "");
cout << "]" << endl;
// 测试2:[1,2,3,4,5], k=3 → [3,2,1,4,5]
ListNode* h2 = toList({1, 2, 3, 4, 5});
vector<int> r2 = toVector(sol.reverseKGroup(h2, 3));
cout << "[1,2,3,4,5], k=3 → [";
for (int i = 0; i < r2.size(); i++)
cout << r2[i] << (i < r2.size()-1 ? "," : "");
cout << "]" << endl;
// 测试3:[1,2,3], k=1 → [1,2,3]
ListNode* h3 = toList({1, 2, 3});
vector<int> r3 = toVector(sol.reverseKGroup(h3, 1));
cout << "[1,2,3], k=1 → [";
for (int i = 0; i < r3.size(); i++)
cout << r3[i] << (i < r3.size()-1 ? "," : "");
cout << "]" << endl;
return 0;
}
输出:
[1,2,3,4,5], k=2 → [2,1,4,3,5]
[1,2,3,4,5], k=3 → [3,2,1,4,5]
[1,2,3], k=1 → [1,2,3]
7. 三道题对比总结
| 题目 | 核心技巧 | 关键点 |
|---|---|---|
| 删除倒数第n个 | 快慢指针差 n 步 | slow 停在目标前一个节点 |
| 两两交换 | 虚拟头 + 逐对交换 | pre 停在已交换部分末尾 |
| K个一组翻转 | 计数 + 翻转 + 组装 | n 记录剩余可翻转组数 |
三道题的共同套路:虚拟头节点统一处理头节点 + 画图理解指针变化 🔄