📌 题目链接:25. K 个一组翻转链表 - 力扣(LeetCode)
🔍 难度:困难 | 🏷️ 标签:链表、分组操作、递归、指针操作
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)(若使用迭代)或 O(n/k)(若使用递归,因函数调用栈)
🧠 题目分析
本题要求将链表按 每 k 个节点为一组进行反转,若最后一组不足 k 个,则保持原顺序不变。关键限制条件是:
- 不能仅修改节点值,必须实际交换节点指针;
- 要求 O(1) 额外空间(进阶要求),意味着不能借助数组、栈等辅助结构存储节点。
这是一道典型的链表分段处理 + 局部反转问题,考察对链表指针的精细控制能力,也是面试中高频出现的“链表变形”类题目。
🔑 核心算法及代码讲解
✅ 核心思想:递归 + 头插法局部反转
我们将整个链表划分为多个长度为 k 的子链表。对每个子链表:
- 先判断是否够 k 个节点;
- 若不够,直接返回头节点(不反转);
- 若够,递归处理后续链表;
- 再将当前 k 个节点反转,并拼接到已处理好的后半部分前面。
💡 为什么用递归?
因为“处理完后面再处理前面”天然适合递归结构。递归能自动帮我们保存“下一组”的头节点,无需手动维护多个指针。
💡 头插法反转原理:
每次从原链表头部取出一个节点,插入到新链表的头部,从而实现反转。这是链表反转的经典 O(1) 空间方法。
📜 代码详解(含逐行注释)
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
// Step 1: 检查当前是否有至少 k 个节点
ListNode* cur = head;
for (int i = 0; i < k; ++i) {
if (cur == nullptr) {
return head; // 不足 k 个,不反转,直接返回原头
}
cur = cur->next; // 移动 k 步,cur 指向第 k+1 个节点(即下一组的开头)
}
// Step 2: 递归处理后面的链表
// 注意:此时 cur 是下一组的起始节点
ListNode* dummy = new ListNode(); // 创建哑节点,作为当前组反转后的“新头”的前驱
dummy->next = reverseKGroup(cur, k); // 递归结果接在 dummy 后面(即已处理好的后半部分)
// Step 3: 头插法反转当前 k 个节点,并接到已处理好的部分前面
// 此时 head 仍指向当前组的第一个节点,我们要把 [head, ..., 第k个] 反转
for (int i = 0; i < k; ++i) {
ListNode* temp = head->next; // 保存下一个待处理节点
head->next = dummy->next; // 将 head 插入到 dummy 后面(头插)
dummy->next = head; // 更新 dummy 的 next 为新的头
head = temp; // 移动 head 到下一个节点
}
// 返回反转后的新头(即 dummy->next)
ListNode* result = dummy->next;
delete dummy; // 可选:避免内存泄漏(LeetCode 通常不要求,但工程中建议)
return result;
}
};
✅ 关键点解析:
cur用于预检查长度,确保不会对不足 k 的组进行反转;dummy节点简化了头插法的边界处理(无需特判空链表);- 递归调用
reverseKGroup(cur, k)返回的是下一组处理完后的头节点,我们只需把当前组反转后接上去即可;- 整个过程只使用了常数个额外指针,满足 O(1) 空间(忽略递归栈的话)。
⚠️ 注意:严格来说,递归解法的空间复杂度是 O(n/k),因为递归深度为
n/k。若要真正实现 O(1) 空间,需改用迭代写法(见下文扩展)。
🧩 解题思路(分步拆解)
- 预检查长度:从当前
head出发,走k步,若中途遇到nullptr,说明不足k个,直接返回head。 - 递归处理后继:假设从第
k+1个节点开始的链表已经按规则处理完毕,得到其新头节点。 - 反转当前组:使用头插法将当前
k个节点反转,并将反转后的链表头连接到“已处理好的后半部分”。 - 返回新头:当前组反转后的第一个节点即为整个链表的新头。
🎯 类比理解:
就像一串珠子,每k颗剪下来翻转一次,再串回去。递归就是“先处理后面的珠子串,再处理前面的”。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n):每个节点被访问常数次(一次用于检查长度,一次用于反转) |
| 空间复杂度 | O(n/k)(递归栈深度);若用迭代可优化至 O(1) |
| 稳定性 | 稳定:未改变非 k 组节点的相对顺序 |
| 适用场景 | 链表分块处理、批量反转、流式数据处理 |
💼 面试加分点:
- 能指出递归 vs 迭代的空间差异;
- 能手写迭代版本(更符合“O(1) 空间”要求);
- 能处理边界情况(k=1, k=n, 空链表等)。
💻 完整可运行代码(含测试)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 链表节点定义(LeetCode 已内置,此处为本地测试补充)
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) {
// Step 1: 检查当前是否有至少 k 个节点
ListNode* cur = head;
for (int i = 0; i < k; ++i) {
if (cur == nullptr) {
return head; // 不足 k 个,不反转
}
cur = cur->next;
}
// Step 2: 递归处理后面的链表
ListNode* dummy = new ListNode(); // 哑节点,辅助反转
dummy->next = reverseKGroup(cur, k); // cur 是下一组的开头
// Step 3: 头插法反转当前 k 个节点,并接到已处理好的部分前面
for (int i = 0; i < k; ++i) {
ListNode* temp = head->next;
head->next = dummy->next;
dummy->next = head;
head = temp;
}
ListNode* result = dummy->next;
delete dummy; // 可选:避免内存泄漏(LeetCode 通常不要求)
return result;
}
};
// 辅助函数:创建链表
ListNode* createList(vector<int>& vals) {
if (vals.empty()) return nullptr;
ListNode* head = new ListNode(vals[0]);
ListNode* cur = head;
for (int i = 1; i < vals.size(); ++i) {
cur->next = new ListNode(vals[i]);
cur = cur->next;
}
return head;
}
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head) {
cout << head->val;
if (head->next) cout << " -> ";
head = head->next;
}
cout << "\n";
}
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例 1
vector<int> v1 = {1,2,3,4,5};
ListNode* head1 = createList(v1);
ListNode* res1 = sol.reverseKGroup(head1, 2);
cout << "Test 1 (k=2): ";
printList(res1); // 预期输出: 2 -> 1 -> 4 -> 3 -> 5
// 测试用例 2
vector<int> v2 = {1,2,3,4,5};
ListNode* head2 = createList(v2);
ListNode* res2 = sol.reverseKGroup(head2, 3);
cout << "Test 2 (k=3): ";
printList(res2); // 预期输出: 3 -> 2 -> 1 -> 4 -> 5
// 测试用例 3: k=1(应原样返回)
vector<int> v3 = {1,2,3};
ListNode* head3 = createList(v3);
ListNode* res3 = sol.reverseKGroup(head3, 1);
cout << "Test 3 (k=1): ";
printList(res3); // 预期输出: 1 -> 2 -> 3
// 测试用例 4: k=5(刚好整除)
vector<int> v4 = {1,2,3,4,5};
ListNode* head4 = createList(v4);
ListNode* res4 = sol.reverseKGroup(head4, 5);
cout << "Test 4 (k=5): ";
printList(res4); // 预期输出: 5 -> 4 -> 3 -> 2 -> 1
return 0;
}
✅ 测试结果
Test 1 (k=2): 2 -> 1 -> 4 -> 3 -> 5
Test 2 (k=3): 3 -> 2 -> 1 -> 4 -> 5
Test 3 (k=1): 1 -> 2 -> 3
Test 4 (k=5): 5 -> 4 -> 3 -> 2 -> 1
🚀 扩展思考:如何实现真正的 O(1) 空间?(迭代版)
虽然递归简洁,但面试官可能要求迭代实现以满足严格 O(1) 空间。核心思路:
- 使用
dummy节点统一处理头节点变更; - 维护
pre(上一组的尾)、start(当前组头)、end(当前组尾); - 每次找到
end,若不足 k 则 break; - 反转
[start, end],并重新连接前后。
迭代版代码较长,但空间更优,建议掌握。
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第32题 —— 26.删除排序数组中的重复项(简单)
🔹 题目:给定一个升序排列的数组,原地删除重复元素,使每个元素只出现一次,返回新长度。
🔹 核心思路:双指针(快慢指针) —— 慢指针记录不重复位置,快指针遍历数组。
🔹 考点:双指针、原地修改、数组去重。
🔹 难度:简单,但却是“原地算法”的经典入门题,高频面试题!
💡 提示:不要使用额外数组!必须在 O(1) 空间内完成!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!