文章目录
- 前言
- 一、题目
- 1、原题链接
- 2、题目描述
- 二、个人思路整理
- 1、思路分析
- 2、解题代码
- 三、知识风暴
前言
本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。
一、题目
1、原题链接
25.K 个一组翻转链表
2、题目描述
二、个人思路整理
1、思路分析
核心思路:先统计总长,用剩余长度控制循环,每组内部用双指针原地翻转,再完成组间拼接:
统计总长:先遍历一遍链表求出总节点数
n。之后每翻转一组就执行n -= k,只要n >= k就继续,这样无需每次翻转前“先探路走k kk步”判断剩余节点是否够一组,逻辑更简洁。初始化哨兵节点与组前驱指针:
dummy:哨兵节点,指向原始head,避免第一组翻转后头节点发生变化、需要特殊处理,让“头节点所在组被翻转”与“中间组被翻转”走同一套代码。p0:当前组的前驱节点,指向当前待翻转分组的前一个节点,初始指向dummy;每翻转完一组,就移动到当前组的新尾巴。
组内反转:
- 每进入新一组,都在循环内部重新声明
pre = nullptr、cur = p0->next,保证每组从干净状态开始翻转。 pre:已经翻转的部分指向反转链表的前一个节点,最终指向当前组的新头。cur:当前正在处理的节点,指向当前需要翻转的节点,最终停在下一组的头节点。- 用
for (int i = 0; i < k; i++)循环k kk次,执行最基础的链表反转(tmp = cur->next; cur->next = pre; pre = cur; cur = tmp;)。 - k kk次循环结束后,
pre停在当前组的新头节点,cur停在下一组的头节点,而p0->next依然指着当前组原本的头节点(现在变成了当前组的尾节点)。
- 每进入新一组,都在循环内部重新声明
组间拼接与指针推进:
- 先用
next_p0 = p0->next暂存当前组的新尾,因为下一步会覆盖p0->next。 - 让当前组的新尾连上下一组的头:
p0->next->next = cur; - 让上一组的尾巴连上当前组的新头:
p0->next = pre; - 将
p0推进到当前组的新尾(即next_p0),准备处理下一组。
- 先用
循环结束后返回
dummy.next:由于头节点所在组可能已被翻转,最终头节点不一定是原head,必须返回哨兵的后继。
2、解题代码
/** * 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) {} * }; */classSolution{public:ListNode*reverseKGroup(ListNode*head,intk){intn=0;ListNode*cur=head;while(cur!=nullptr){n++;cur=cur->next;}ListNodedummy(0,head);ListNode*p0=&dummy;// p0指向当前组的前驱节点while(n>=k){n-=k;// 每组重新开始翻转ListNode*pre=nullptr;ListNode*cur=p0->next;for(inti=0;i<k;i++){ListNode*tmp=cur->next;cur->next=pre;pre=cur;cur=tmp;}// 组间拼接ListNode*next_p0=p0->next;p0->next->next=cur;p0->next=pre;p0=next_p0;}returndummy.next;}};复杂度分析
- 时间复杂度:O ( N ) O(N)O(N),遍历两次,一次算长度,一次做翻转。
- 空间复杂度:O ( 1 ) O(1)O(1),仅在栈上分配了一个
dummy对象和几个指针。
三、知识风暴
链表分组翻转(Reverse Nodes in k-Group)是本题的核心:题目要求把链表每k kk个节点视为一组,组内原地翻转,不足k kk个的尾部保持原样。它本质上是「基础链表反转」的升级版——把一次整体反转拆成若干次定长反转,并在组与组之间做好衔接。
算法核心思想:
- 先统计总长,用长度控制循环:先遍历一遍求出节点总数
n,之后每翻转一组就执行n -= k,只要n >= k就继续。这样无需每次翻转前“探路走k kk步”判断剩余节点是否够一组,逻辑更简洁。 - 哨兵节点统一边界:引入
dummy指向head,让“头节点所在组被翻转”与“中间组被翻转”走同一套代码,避免单独处理头节点。 - 组内头插式反转:在每组内部用
pre/cur双指针循环k kk次完成反转,k kk次结束后pre是新头、cur是下一组的头,而p0->next仍指向本组旧头(即反转后的新尾)。 - 两步完成衔接:
p0->next->next = cur让本组新尾接上下一组;p0->next = pre让上一组尾巴接上本组新头;最后把p0推进到本组新尾,准备处理下一组。
常见对比:迭代原地翻转 vs 递归分组翻转
- 迭代(本题解法):用
dummy+p0+pre/cur在链表上原地操作,空间复杂度O ( 1 ) O(1)O(1),是面试中最推荐的写法;但指针较多,衔接顺序写错容易断链,需要理清“新头 / 新尾 / 下一组头”三者的关系。 - 递归分组翻转:先判断剩余节点是否够k kk个,够则翻转前k kk个,再递归处理后续部分并把本组新尾指向递归结果。代码更短、更贴近分治直觉,但递归深度为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 kk个的处理:由于循环条件是
n >= k,剩余不足k kk个的节点根本不会进入翻转逻辑,天然保持原顺序,无需额外判断。
使用要点:
- 长度先行:先算
n再翻转,是本题最省心的写法,务必牢记这个“用总长度控制循环”的套路。 - 哨兵节点必备:凡是涉及“可能翻转头节点”的链表题,
dummy都是降低边界复杂度的利器。 - 画图验证指针:组内反转 + 组间衔接共涉及 4 个指针,建议在纸上画出
dummy -> 组1 -> 组2的示意图,逐步核对每一步的指向。 - 返回
dummy.next:最终头节点可能已不是原head,必须返回dummy.next而非head。
算法变体与扩展:
- 两两交换链表中的节点(LeetCode 24):即k = 2 k = 2k=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=2k=2的特例)
- 143. 重排链表(链表拆分与合并的综合应用)