文章目录
前言
本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。
一、题目
1、原题链接
2、题目描述
二、个人思路整理
1、思路分析
核心思路:利用总长度控制循环,用头插法原地翻转:
-
统计总长:先遍历一遍链表求出总节点数
n。这样通过判断n >= k就能知道剩余节点够不够一组,省去了每次翻转前"先探路走 k k k 步"的麻烦。 -
定位四个核心指针:
dummy:哨兵节点,指向head,用来统一处理头节点被翻转的情况。p0:当前待翻转组的上一个节点(即上一组的尾巴)。初始指向dummy。pre和cur:标准的反转链表双指针,初始pre = nullptr,cur = p0->next。
-
组内反转与衔接:
- 在组内循环 k k k 次,执行最基础的链表反转(
tmp = cur->next; cur->next = pre; pre = cur; cur = tmp;)。 - k k k 次循环结束后,
pre停在当前组的新头节点,cur停在下一组的头节点,而p0->next依然指着当前组原本的头节点(现在变成了当前组的尾节点)。 - 只需两步即可完成拼接与指针推进:
- 让当前组的尾巴连上下一组的头:
p0->next->next = cur; - 让上一组的尾巴连上当前组的新头:
p0->next = pre; - 将 p0 移动到当前组的尾巴(即原先的
p0->next),准备处理下一组。
- 让当前组的尾巴连上下一组的头:
- 在组内循环 k k k 次,执行最基础的链表反转(
2、解题代码
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) {
// 1. 统计链表总长度
int n = 0;
ListNode* tmp = head;
while (tmp) {
tmp = tmp->next;
n++;
}
// 2. 初始化哨兵节点与指针
ListNode dummy(0, head);
ListNode* p0 = &dummy; // p0 始终指向待翻转那一组的前驱节点
ListNode* pre = nullptr;
ListNode* cur = head;
// 3. 只要剩余节点数 >= k,就继续翻转
int tmpk = k;
while (n >= k) {
n -= k;
tmpk = k;
while (tmpk--) {
// 标准反转链表逻辑,循环 k 次
ListNode* tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
// 此时:
// pre 是当前数组反转后的【新头】
// cur 是下一组的【头】
// p0->next 是当前组反转前的【旧头】(即反转后的【新尾】)
ListNode* next_p0 = p0->next; // 先暂存当前组的新尾,将它作为下一轮的 p0
p0->next->next = cur; // 当前组的新尾 -> 指向下一组的头
p0->next = pre; // 上一组的尾(p0)->指向当前组的新头
p0 = next_p0; // p0 推进到当前组的新尾
}
return dummy.next;
}
};
复杂度分析
- 时间复杂度: O ( N ) O(N) O(N),遍历两次,一次算长度,一次做翻转。
- 空间复杂度: O ( 1 ) O(1) O(1),仅在栈上分配了一个
dummy对象和几个指针。
三、知识风暴
链表分组翻转(Reverse Nodes in k-Group) 是本题的核心:题目要求把链表每 k k k 个节点视为一组,组内原地翻转,不足 k k k 个的尾部保持原样。它本质上是「基础链表反转」的升级版------把一次整体反转拆成若干次定长反转,并在组与组之间做好衔接。
算法核心思想:
- 先统计总长,用长度控制循环 :先遍历一遍求出节点总数
n,之后每翻转一组就执行n -= k,只要n >= k就继续。这样无需每次翻转前"探路走 k k k 步"判断剩余节点是否够一组,逻辑更简洁。 - 哨兵节点统一边界 :引入
dummy指向head,让"头节点所在组被翻转"与"中间组被翻转"走同一套代码,避免单独处理头节点。 - 组内头插式反转 :在每组内部用
pre/cur双指针循环 k k k 次完成反转, k k k 次结束后pre是新头、cur是下一组的头,而p0->next仍指向本组旧头(即反转后的新尾)。 - 两步完成衔接 :
p0->next->next = cur让本组新尾接上下一组;p0->next = pre让上一组尾巴接上本组新头;最后把p0推进到本组新尾,准备处理下一组。
常见对比:迭代原地翻转 vs 递归分组翻转
- 迭代(本题解法) :用
dummy+p0+pre/cur在链表上原地操作,空间复杂度 O ( 1 ) O(1) O(1),是面试中最推荐的写法;但指针较多,衔接顺序写错容易断链,需要理清"新头 / 新尾 / 下一组头"三者的关系。 - 递归分组翻转 :先判断剩余节点是否够 k k k 个,够则翻转前 k k k 个,再递归处理后续部分并把本组新尾指向递归结果。代码更短、更贴近分治直觉,但递归深度为 O ( n / k ) O(n/k) O(n/k),存在栈开销。
- 借助数组 / 栈 :把每组节点值压入栈再弹出重写,思路直观、不易出错,但需要额外 O ( k ) O(k) O(k) 空间,且只改值不改节点,不符合"原地翻转节点"的题意要求。
指针衔接的关键细节:
- 为什么先存
next_p0:执行p0->next = pre之后,原来的p0->next(本组新尾)就被覆盖了,所以必须先用next_p0暂存,再把它作为下一轮的p0。 pre的复位 :每进入新一组前,pre需要重新置为nullptr,否则会把上一组的节点错误地接到本组新头之后。- 不足 k k k 个的处理 :由于循环条件是
n >= k,剩余不足 k k k 个的节点根本不会进入翻转逻辑,天然保持原顺序,无需额外判断。
使用要点:
- 长度先行 :先算
n再翻转,是本题最省心的写法,务必牢记这个"用总长度控制循环"的套路。 - 哨兵节点必备 :凡是涉及"可能翻转头节点"的链表题,
dummy都是降低边界复杂度的利器。 - 画图验证指针 :组内反转 + 组间衔接共涉及 4 个指针,建议在纸上画出
dummy -> 组1 -> 组2的示意图,逐步核对每一步的指向。 - 返回
dummy.next:最终头节点可能已不是原head,必须返回dummy.next而非head。
算法变体与扩展:
- 两两交换链表中的节点(LeetCode 24) :即 k = 2 k = 2 k=2 的特例,可用同样的分组翻转框架,也可用递归实现,适合作为本题的热身。
- 反转链表(LeetCode 206) :本题组内反转的基础模板,理解
pre/cur双指针反转是解决本题的前提。 - 反转链表 II(LeetCode 92) :只翻转指定区间 l e f t , r i g h t left, right left,right,与本题"定长分组翻转"互为补充,可对比"区间边界定位"与"分组边界定位"的差异。
- K 个一组翻转链表(递归写法):同一道题的递归版本,可与本题迭代写法对照,体会"迭代控指针、递归控结构"的取舍。
相关 LeetCode 例题:
- 206. 反转链表(组内反转的基础模板)
- 92. 反转链表 II(指定区间翻转对比)
- 24. 两两交换链表中的节点( k = 2 k=2 k=2 的特例)
- 143. 重排链表(链表拆分与合并的综合应用)

