LeetCode 25. K个一组翻转链表:两种解法详解+避坑指南
LeetCode 难度为 Hard 的经典链表题——25. K个一组翻转链表,这道题是链表翻转的进阶题,考察对链表指针操作的熟练度,也是面试中的高频考点,很多人会在“组内翻转”“组间连接”“边界处理”上踩坑。
今天不仅会讲解题目核心,还会对比两份不同思路的代码,分析它们的优缺点、避坑点,帮大家彻底吃透这道题,下次遇到直接秒解!
一、题目解读(清晰易懂版)
题目核心需求很明确,一句话概括:给一个链表,每k个节点当成一组,组内翻转;如果最后剩下的节点不足k个,就保持原样。
关键约束(必看,避坑前提):
-
k是正整数,且k ≤ 链表长度(不用考虑k大于链表长度的情况);
-
不能只改节点的值,必须实际交换节点(排除“偷巧”解法);
-
组间顺序不变,只有组内节点翻转(比如链表1->2->3->4,k=2,结果是2->1->4->3,不是4->3->2->1)。
示例辅助理解:
-
输入:head = [1,2,3,4,5], k = 2 → 输出:[2,1,4,3,5]
-
输入:head = [1,2,3,4,5], k = 3 → 输出:[3,2,1,4,5]
-
输入:head = [1,2], k = 2 → 输出:[2,1]
二、链表节点定义(题目给出,直接复用)
先贴出题目给出的ListNode定义,两份解法都基于这个结构,不用额外修改:
class ListNode {
val: number
next: ListNode | null
constructor(val?: number, next?: ListNode | null) {
this.val = (val === undefined ? 0 : val)
this.next = (next === undefined ? null : next)
}
}
三、两种解法详解对比
下面分别讲解两份代码(reverseKGroup_1 和 reverseKGroup_2),从思路、执行流程、优缺点三个维度拆解,帮大家看清两种思路的差异。
解法一:reverseKGroup_1(全局翻转+局部调整+回滚,新手易上手但需避坑)
1. 核心思路
这种思路的核心是「边遍历边全局翻转,每凑够k个节点,就调整一次组间连接;最后如果不足k个节点,再把这部分翻转回去」。
可以类比成:把链表当成一串珠子,从头开始逐个翻转(珠子顺序颠倒),每翻k个,就把这k个珠子“固定”到正确的位置(连接好前后组);如果最后剩的珠子不够k个,就把这几个珠子再翻回来,恢复原样。
2. 关键变量说明
-
dummy:虚拟头节点,避免处理头节点翻转的特殊情况(所有链表题的通用技巧);
-
preGroup:每组翻转的“前置节点”,负责连接上一组的尾和当前组的头;
-
prev:翻转节点时的“前驱节点”,记录当前节点的前一个节点(用于翻转指针);
-
curr:当前正在遍历、翻转的节点;
-
count:组内节点计数器,用于判断是否凑够k个节点。
3. 代码执行流程(以 head=[1,2,3,4], k=2 为例)
-
初始状态:dummy(0)->1->2->3->4,preGroup=dummy,prev=dummy,curr=1,count=0;
-
遍历curr=1:count≠2,翻转1(1.next=prev=dummy),prev=1,curr=2,count=1;
-
遍历curr=2:count≠2,翻转2(2.next=prev=1),prev=2,curr=3,count=2;
-
凑够k=2个节点:调整组间连接——preGroup.next=prev=2(dummy->2),原组头lastNode=1,1.next=curr=3(2->1->3);更新preGroup=1,prev=1,count=0;
-
继续遍历curr=3:重复步骤2-3,翻转3、4,凑够k=2个节点,调整连接(1->4,3.next=null);
-
循环结束,count=0,无不足k个的节点,返回dummy.next=2,最终结果2->1->4->3(正确)。
4. 优点&缺点
优点:思路直观,新手容易理解(只需要掌握“单个节点翻转”的基础操作,再加上计数和回滚);代码结构清晰,逐步骤执行,容易调试。
缺点:存在冗余逻辑(比如单独处理“最后一组刚好k个节点”的else if分支);过度使用空值断言(!),有潜在空指针风险;最后回滚步骤增加了少量时间开销(虽然时间复杂度还是O(n))。
5. 核心避坑点
-
避免链表环:翻转后必须及时调整组尾的next指针(lastNode.next=curr),否则会出现“dummy<->1”的环,触发运行错误;
-
回滚逻辑不能漏:如果最后剩余节点不足k个,必须把这部分翻转的节点再翻回来,否则会破坏原有顺序;
-
空值判断:preGroup.next不可能为null,可移除多余的空值判断,避免错误返回null。
解法二:reverseKGroup_2(先找组边界+组内单独翻转,最优解法)
这是更推荐的解法,也是面试中更常考的思路——「先找每组的边界(头和尾),确认够k个节点后,再单独翻转这组节点;组间连接直接通过边界节点处理,无需回滚」。
类比:还是一串珠子,先找到前k个珠子(确定组头和组尾),把这k个珠子单独翻转,再连接好前后珠子;再找下k个珠子,重复操作;如果找不到k个,就直接结束,不用再调整。
1. 关键变量说明(新增/差异变量)
-
groupTail:当前组的尾节点,通过移动k次找到,同时判断剩余节点是否够k个;
-
groupHead:当前组的头节点(翻转后会变成组尾);
-
nextGroupHead:下一组的头节点,提前记录,避免翻转后找不到下一组。
2. 代码执行流程(以 head=[1,2,3,4], k=2 为例)
-
初始状态:dummy(0)->1->2->3->4,preGroup=dummy;
-
找第一组边界:groupTail从preGroup开始移动2次,找到groupTail=2(确认够k个节点);记录groupHead=1,nextGroupHead=3;
-
单独翻转当前组(1->2):prev初始化为nextGroupHead=3,curr=groupHead=1;循环翻转,直到curr=nextGroupHead,翻转后变成2->1;
-
连接组间:preGroup.next=groupTail=2(dummy->2),preGroup更新为groupHead=1(下一组的前置节点);
-
找第二组边界:groupTail从preGroup=1移动2次,找到groupTail=4;记录groupHead=3,nextGroupHead=null;
-
单独翻转当前组(3->4),连接组间;
-
下一次找组边界:移动不足2次,count<k,直接返回dummy.next=2,结果2->1->4->3(正确)。
3. 优点&缺点
优点:逻辑更高效,无需回滚(提前判断节点数量,不足k个直接返回);无冗余分支,代码更简洁;指针操作更严谨,避免链表环和空指针风险;时间复杂度O(n),空间复杂度O(1),是最优解法。
缺点:对指针操作的熟练度要求更高,需要提前规划好“找边界-翻转-连接”的流程,新手可能需要多调试几次才能理解。
4. 核心避坑点
-
找组边界时,必须同时判断节点数量:移动k次后,如果groupTail.next不存在,说明不足k个节点,直接返回;
-
翻转组内节点时,prev初始化为nextGroupHead:这样翻转后,组尾(原groupHead)的next会自动指向nextGroupHead,无需额外调整;
-
preGroup更新为原groupHead:翻转后,原groupHead变成组尾,作为下一组的前置节点,保证组间连接正确。
四、两份代码对比总结
| 对比维度 | reverseKGroup_1 | reverseKGroup_2 |
|---|---|---|
| 核心思路 | 全局翻转+组间调整+不足k个回滚 | 先找组边界+组内单独翻转+无回滚 |
| 时间复杂度 | O(n)(回滚最多增加O(k),可忽略) | O(n)(最优,每个节点只遍历一次) |
| 空间复杂度 | O(1) | O(1) |
| 可读性 | 高,新手易理解 | 中等,需熟练掌握指针操作 |
| 适用场景 | 新手刷题、快速调试 | 面试、生产环境(最优解) |
| 潜在坑点 | 链表环、回滚遗漏、空值断言 | 组边界判断、prev初始化 |
五、刷题建议&拓展思考
1. 刷题建议
-
新手:先吃透 reverseKGroup_1,掌握“翻转+计数+回滚”的思路,熟练后再过渡到 reverseKGroup_2;
-
进阶:重点练习 reverseKGroup_2,尝试自己手写“找边界-翻转-连接”的流程,避免依赖模板;
-
调试技巧:遇到指针混乱时,画链表结构图(比如用草稿纸写出每个节点的next指向),逐步骤跟踪指针变化,比单纯看代码更高效。
2. 拓展思考(面试高频追问)
-
如果k可以大于链表长度,该如何修改代码?(提示:在找组边界时,判断count是否等于链表长度,不足则不翻转);
-
如何用递归实现K个一组翻转链表?(提示:递归终止条件是剩余节点不足k个,递归逻辑是翻转当前组,再递归翻转下一组);
-
如果要求“每k个节点一组翻转,不足k个节点时全部翻转”,该如何修改?(提示:移除回滚逻辑,或不判断节点数量,直接翻转)。
六、最终优化版代码(推荐面试使用)
基于 reverseKGroup_2 优化,移除空值断言,增加防御性判断,代码更健壮、简洁,适配面试场景:
function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
if (k === 1 || !head || !head.next) return head;
const dummy = new ListNode(0, head);
let preGroup = dummy; // 每组翻转的前置节点
let count = 0;
while (true) {
// 第一步:找组尾,判断剩余节点是否够k个
let groupTail = preGroup;
count = 0;
while (count < k && groupTail.next) {
groupTail = groupTail.next;
count++;
}
if (count < k) return dummy.next; // 不足k个,直接返回
// 第二步:记录关键节点
const groupHead = preGroup.next;
const nextGroupHead = groupTail.next;
// 第三步:组内翻转
let prev: ListNode | null = nextGroupHead;
let curr = groupHead;
while (curr !== nextGroupHead) {
const next = curr?.next;
if (curr) curr.next = prev;
prev = curr;
curr = next;
}
// 第四步:组间连接
preGroup.next = groupTail;
preGroup = groupHead!;
}
}
七、总结
LeetCode 25题的核心是「组内翻转+组间连接」,两种解法的本质都是通过指针操作实现,但思路的高效性有差异。
无论哪种解法,都要记住三个核心要点:① 用虚拟头节点简化头节点处理;② 明确每组的边界(头、尾、下一组头);③ 翻转时避免链表环和空指针。
刷题不是背代码,而是理解思路、掌握技巧。建议大家多调试、多画图,熟练掌握指针操作,下次遇到类似的链表翻转题(比如两两翻转、指定区间翻转),就能举一反三、轻松应对!