最近刷力扣热题 100Hot 100时我反复在评论区看到同一个现象好几道中等难度题目的题解里大家都会绕回来先补一遍“反转链表”。链表这种结构不反转时觉得哪儿都顺一涉及反转指针一多人就容易晕。偏偏反转链表自己就是热题 100 里的常客而且它的迭代写法、递归写法、区间反转写法几乎是后续“K 个一组翻转链表”“回文链表”“重排链表”这些题的地基。如果你正在刷热题清单这题值得你停下来把每一步指针变化都在纸上跑一遍而不是背代码。这篇博文我会从“为什么这题重要”开始把迭代、递归、区间反转、K 个一组翻转这些变体一次讲透顺便把我踩过的坑和排查指针问题的思路一并分享出来。1. 这题凭什么在热题 100 里占一个稳定位置1.1 高频出现的底层原因反转链表在力扣上的题号是 206难度是简单。但是“简单”这俩字很容易误导人。真正到面试和笔试场景里面试官很少直接让你原样写 206他更常做的是把反转逻辑缝进另一道题里比如判断回文链表、反转区间、按组反转。你的基本功扎不扎实在那些题目里会暴露得非常彻底。为什么反转链表能进入热题 100我自己的判断有三点。第一链表题本身就是面试官检验“指针操作能力”的经典容器。数组题你可以靠下标思维硬推树题你可以靠递归框架套模板但链表题强调的“只改指针、不改值”思维是很多人的思维盲区。反转链表恰好把这种思维压缩到了一个最小题目里考察价值极高。第二它能通过一道题区分出“背过答案”和“真懂原理”两种人。迭代法三行核心代码谁都会背但只要你把问题改成“只反转链表的第 2 到第 4 个节点”立刻就能筛掉一批人。热题 100 本身是给大多数人准备的高频题集合它优先收录那些“覆盖面广、衍生题多”的题目反转链表完全符合。第三这道题的解法密度很高。迭代、递归、头插法、双指针、栈辅助每一种写法背后都对应着一类链表问题的通用解法。比如递归法练的是“相信函数定义”的思维头插法练的是“虚拟头节点维护边界”的思维这两种思维在后面的二叉树、排序链表里都会反复出现。所以我的建议是别把它当一道简单题刷完就过。你需要在这道题上做到三种解法都能脱离编译器直接写对并且能讲清楚每一种解法的空间复杂度差异这题才算真正吃透。1.2 先建立链表题的统一心法聊具体解法前我先给你一个我认为最重要的链表题心法链表题不要尝试“同时思考所有指针”而要一次只追踪一个指针的变化。很多人反转链表写到一半卡住是因为脑子里同时装了 pre、cur、next 三个变量还要想它们下一步怎么走。这完全没必要。你盯住 cur 这一个节点问自己三个问题它现在指向谁它应该指向谁谁会在下一步接替它想清楚这三件事代码自然是顺的。另一个统一心法是边界靠虚拟头节点找补。链表题最容易翻车的地方是头节点被改动、链表为空、只有一个节点。虚拟头节点dummy node能统一处理这些边界让代码逻辑变得对称。反转整条链表时题目本身允许返回新头节点所以不一定需要 dummy但一旦遇到区间反转、分组反转这类问题dummy 基本就是标配。在往下看代码之前你先把这两个心法记在心里。后面所有解法都是这两个心法的展开。2. 迭代反转把箭头一根根掰过来2.1 三指针分工与循环不变量迭代法的核心思路一句话就能说清遍历链表把当前节点的 next 指向前一个节点。但因为你把 next 改了原来的下一个节点就丢了所以必须有一个指针先把下一个节点存下来。很多教材里的三指针叫 pre、cur、next我建议你记住它们各自负责的事情pre已经完成反转的链表部分的头节点也就是“当前节点的前一个节点”。cur正在处理中的节点它的 next 即将被修改。nextcur 原来的下一个节点防止 cur 改指向之后断链。代码骨架是这样的func reverseList(head *ListNode) *ListNode { var pre *ListNode cur : head for cur ! nil { next : cur.Next cur.Next pre pre cur cur next } return pre }我写的是 Go 版本因为这几年用 Go 刷题的越来越多。你用 Python、Java、C 也都一样逻辑没区别。关键点在于循环不变量每轮循环结束后pre 指向“已经反转好的新链表头”cur 指向“还没处理的原链表头”。这个不变量成立整个算法就不会乱。你在纸上模拟的时候不要去看整条链只盯住这个不变量检查基本不会错。2.2 为什么是返回 pre而不是 cur新手最容易问的问题循环结束时 cur 不是已经变成 nil 了吗为什么返回的是 pre对循环结束的唯一条件就是 cur nil。这意味着原链表已经走到头了所有节点都被处理过一遍。此时 pre 指向的正是原链表的尾节点也就是新链表的头节点。如果返回 cur你返回的是个空指针整条链表就丢了。这里我提供一个自检技巧你写完代码后用三个节点的链表 [1,2,3] 在纸上跑一遍。跑完后你应该得到 pre3、curnil。如果哪里对不上说明循环体里的指针交接顺序写错了。指针交接顺序是这道题唯一容易出错的地方一定是先保存 next再修改 cur.Next最后移动 pre 和 cur。顺序一乱结果必然出错。2.3 虚拟头节点也能做反转头插法思路迭代法还有一种等价写法新建一个虚拟头节点 dummy然后遍历原链表每到一个节点就把它插到 dummy 的后面。因为每次都是插到最前面所以遍历结束后dummy.Next 就是反转后的链表头。func reverseList(head *ListNode) *ListNode { dummy : ListNode{} cur : head for cur ! nil { next : cur.Next cur.Next dummy.Next dummy.Next cur cur next } return dummy.Next }这种头插法在“整链反转”场景里看起来比三指针法繁琐但在“区间反转”里会非常舒服因为它的边界处理天然统一。我建议你把这两种写法都掌握区间反转时优先用头插法整链反转时优先用三指针法。2.4 迭代法的复杂度与面试话术迭代法的时间复杂度是 O(n)空间复杂度是 O(1)因为它只用了常数个额外指针。面试时如果面试官让你“写个反转链表”迭代法通常是默认答案因为它空间占用小也不存在递归深度问题。你需要能脱口而出这组复杂度并且解释清楚为什么空间是 O(1)因为所有操作都只发生在原链表上没有额外分配节点。3. 递归反转先走到链表尾巴再一路改回来3.1 递归的思考方式不是“递归过程”而是“函数定义”递归解法的代码非常短但理解门槛比迭代高。问题在于大多数人试图在脑子里展开递归栈去模拟每一层发生了什么。我建议你换一种方式先相信函数定义再写代码。递归函数的定义是reverseList(head)返回“从 head 开始反转后的新头节点”。那么如果我们调用了reverseList(head.Next)会得到什么会得到“从 head.Next 开始反转后的新头节点”也就是原来链表尾部的那个节点。此时我们假设原链表是 1 - 2 - 3 - 4 - nil。调用reverseList(head.Next)之后从 2 开始的子链表已经被反转变成 1 - 2 - 3 - 4其中 2 的 Next 还是指向 3但是 3 的 Next 已经变成了 24 的 Next 变成了 3。返回值是 4。接下来要做的事只剩两件把 2 的 Next 指向 1再把 1 的 Next 置为 nil。代码就是func reverseList(head *ListNode) *ListNode { if head nil || head.Next nil { return head } newHead : reverseList(head.Next) head.Next.Next head head.Next nil return newHead }3.2 递归代码里最容易漏的那一行很多人递归解法写到newHead : reverseList(head.Next)就停住了因为他们不知道接下来怎么把当前节点接回去。答案就是那两行head.Next.Next head head.Next nil第一行让当前节点的下一个节点反过来指向当前节点第二行断开当前节点原本向后的指针。这两行必须成对出现缺一不可。只写第一行链表会成环只写第二行链表会断成两截。我给一个直观类比递归的过程就像你沿着一条单向通道走到最深处然后从最后一个房间开始把通道里每一扇门的方向都调转过来。你回头走的每一步都要做两件事把前一扇门的把手装到后一扇门上同时把你自己这扇门原来的把手卸掉。两件事都做完整条通道才是单向畅通的。3.3 递归的空间复杂度分析递归解法的时间复杂度同样是 O(n)但空间复杂度是 O(n)。原因很简单递归深度是 n每层递归都会占用一个栈帧。虽然每个栈帧很小但当链表长度达到几万甚至上十万时递归可能触发栈溢出。这也是为什么在工程代码里反转链表几乎都写迭代法。面试时的加分回答是先写递归再说一句“这个写法空间复杂度是 O(n)如果链表很长会有栈溢出风险工程上我倾向用迭代法空间 O(1)”。这样既展示了递归思维又体现了工程判断力。3.4 用“头插法思维”理解递归返回值还有一个理解递归返回值的角度递归函数返回的 newHead 始终是原链表的尾节点。在整个递归过程中这个返回值一路不变从最深层传回最外层。你可以把它理解为“新链表的定海神针”——不管上面怎么改指针最终新头就是它。在纸上模拟递归时不要展开每一层的完整状态。你只需要在“第一次回溯”的那个节点上停下来把它的 Next 关系画清楚剩下的每一层动作完全一样。理解了第一层回溯你就理解了全部。4. 区间反转热题 100 衍生题里最常出现的变体4.1 力扣 92反转链表 II 的四个关键位置反转单链表升级版是反转链表 II题号 92。题目要求把链表中第 m 个节点到第 n 个节点反转其余部分保持原样。这道题在热题 100 里也经常被拿来当中间难度题考察。你要先明确四个位置m 的前一个节点叫 pre。第 m 个节点叫 left。第 n 个节点叫 right。right 的后一个节点叫 tail。反转区间结束后要让 pre.Next 指向 right让 left.Next 指向 tail。也就是说区间内部的箭头全部调头区间两端的箭头要重新接上。一种直观做法是先找到 pre 和 left然后对区间内的节点做一次标准反转最后接回 pre 和 tail。但这里的细节比整链反转多因为你不知道 pre 是不是 nil。如果 m 正好是 1pre 就是 nil处理起来很啰嗦。4.2 用虚拟头节点统一边界让 m1 不再特殊解决办法就是老朋友 dummy 节点。在 head 前面加一个虚拟头节点pre 的初始位置指向 dummy。这样即使 m1pre 也不是 nil代码逻辑完全统一。实现步骤拆开看创建 dummydummy.Next headpre dummy。pre 向后移动 m-1 次到达第 m 个节点的前一个节点。cur 指向 pre.Next也就是第 m 个节点。对区间内 n-m 个节点执行头插法反转。返回 dummy.Next。头插法在区间反转里的写法每次把 cur.Next 摘下来插到 pre.Next 的位置。这个操作看起来简单但它同时用了两个容易混淆的指针pre 在整个反转过程中不移动移动的只有 cur 和 cur.Next。很多人写着写着把 pre 也往后挪了结果反转区间变短链表结构被破坏。4.3 区间反转的代码模板与解释func reverseBetween(head *ListNode, m int, n int) *ListNode { dummy : ListNode{Next: head} pre : dummy for i : 0; i m-1; i { pre pre.Next } cur : pre.Next for i : 0; i n-m; i { next : cur.Next cur.Next next.Next next.Next pre.Next pre.Next next } return dummy.Next }这段代码我刚学的时候读了三遍才顺过来。核心操作是cur.Next next.Next这相当于让 cur 跳过 next直接连到 next 后面的节点。然后把 next 插到 pre 后面。这个过程中cur 一直不动它始终指向区间里的第一个节点但它的 Next 会随着每次插入不断后移直到区间全部处理完。如果你第一次写区间反转建议在 n-m2 的小例子上跑一遍比如链表 [1,2,3,4,5]m2n4。手动模拟完一轮插入你就能理解为什么 cur 不需要移动了因为每次被摘走的都是 cur 的下一个节点cur 自己从未改变指向。5. K 个一组翻转反转链表的天花板变体5.1 为什么学完反转链表下一站就是它热题 100 里还有一道更难的延伸题K 个一组翻转链表题号 25。它要求把链表每 K 个节点一组进行反转最后一组不足 K 个就保持原样。这道题其实就是“区间反转”的循环版只是每次区间的起点要动态更新。如果你已经吃透了反转链表和反转区间做 25 题的路径会非常清晰先数出剩余节点够不够 K 个够就反转这一段不够就停止。每处理完一组把 pre 移动到这一组的末尾开始下一组。但这里有个陷阱怎么“数出 K 个节点”你不能只数一次因为每反转完一组链表的结构就变了剩余节点数量也变了。所以标准做法是每轮循环先调用一个辅助函数从当前节点开始数 K 个数不够就返回 false。5.2 分组反转最容易错的“接缝”位置分组反转的难点不在单组反转而在组与组之间的衔接。假设我们处理完第一组此时第一组内部的箭头已经全部调头。接下来要让第一组的尾部接到第二组的头部。很多人直接拿 pre 去处理结果把组间连接弄成环。一个稳妥的做法是每一轮维护好四个指针——这一组的前一个节点 pre、这一组的起点 start、这一组的终点 end、end 后面的 nextGroup。反转完这一组后pre 变成 endstart 变成 nextGroup 的前驱。如果不小心把 nextGroup 丢了下一组就没法继续。我在做 25 题时连续错了好几次原因都是没有在反转前保存 nextGroup。这和整链反转时先保存 next 是同一个道理只要你打算修改一组节点的内部指针这一组对外连接的指针就必须提前备份。你可以把这条规则推广到所有链表题改内部指针之前先问自己“外部有哪些指针指向这一块区域”。5.3 K 个一组翻转的核心代码思路func reverseKGroup(head *ListNode, k int) *ListNode { dummy : ListNode{Next: head} pre : dummy for head ! nil { tail : pre for i : 0; i k; i { tail tail.Next if tail nil { return dummy.Next } } nextGroup : tail.Next newHead : reverseRange(head, tail) pre.Next newHead head.Next nextGroup pre head head nextGroup } return dummy.Next }这里的 reverseRange 可以复用我们在区间反转里学到的头插法逻辑也可以是标准的区间反转。把大问题拆成小问题后25 题就不再是“天花板”它只是“循环调用反转区间”的简单组合。5.4 这类变体题对面试的意义面试里出现 K 个一组翻转面试官一般不只是看你写不写得出来而是看你会不会拆解。一个能把 25 题拆成“判断长度 区间反转 组间连接”三个子问题的候选人和背过完整答案的候选人在代码组织和解释方式上有明显区别。你不妨在训练时故意按这个顺序去讲先讲子问题拆分再讲每组内部逻辑最后讲组间衔接。这种分层表达习惯在系统设计和算法面试里都通用。6. 反转链表常见坑位盘点与调试技巧6.1 空链表与单节点边界条件不是摆设力扣的测试用例一定会包含 head nil 和 head 单节点这两种输入。你写的代码必须在这两种输入下不报错、不返回错误结果。迭代法中while cur ! nil 这个条件天然处理了空链表的情况。单节点链表进去后会立即退出循环返回 pre而 pre 此时就是那个单节点本身。递归法的退出条件是head nil || head.Next nil这里的head.Next nil就是为单节点准备的。边界条件看似简单但在区间反转里边界条件会变得隐蔽。比如 mn 时区间长度为 1理论上不需要反转但你的代码应该能正确处理。再比如 n 大于链表长度时题目通常保证输入合法但你自己写测试用例时最好把非法输入也测一遍防止 LeetCode 的隐藏用例在变异处给你致命一击。6.2 手动模拟的正确姿势用三个节点的链表起步我强烈建议你准备一张纸、一支笔画一个三个节点的链表从头到尾模拟一遍反转过程。画的时候请注意三个细节每个节点画一个方框方框里写值方框右边拉出一个小箭头表示 Next 指针。没修改指针前不要擦掉旧箭头用新颜色的笔在新位置画箭头。每画完一步用“当前 cur 指向哪个节点”“当前 pre 指向哪个节点”这句话描述一下状态。等到三个节点能流畅跑通再换五个节点加入更复杂的分支条件。这个过程看着笨但它是排查指针错误最有效的办法。很多时候你代码写错了但逻辑看起来完全正确就是因为脑子里自动补全了正确指针实际运行时指针却跑偏了。6.3 打印链表是调试最快的路径本地调试时我不会只看返回结果基本都会写一个辅助打印函数把链表从头到尾打出来。func printList(head *ListNode) { for head ! nil { fmt.Print(head.Val, - ) head head.Next } fmt.Println(nil) }如果反转结果不对就在每个关键步骤后调用打印函数对比实际输出和预期输出很快就能定位是哪一行改错了指针。尤其是做区间反转和 K 个一组反转时我会在每次组间连接完成后打印一次确认连接点是否有环。有一次我排查了很久最后发现是pre.Next和head.Next的更新顺序反了打印函数在倒数第二步就暴露了问题。6.4 警惕“值反转”陷阱有一种反面写法很诱人把链表每个节点的值取出来反转后填回去。比如用栈保存所有节点的值再重新遍历链表并依次赋值。这种写法在功能上没问题也能通过力扣的测试用例。但它完全违背了链表题的教学意义。链表反转要求的是指针层面的操作核心是理解节点之间连接的改变。如果真的只是换值那链表就退化成数组了面试官大概率会立刻追问“那为什么要用链表”我的建议是训练时只看指针操作不要用值替换偷懒。这不光是为了面子更是因为后续的链表排序、链表合并、回文判断很多算法都依赖于“移动节点本身”而不是“移动值”。6.5 递归写法中容易被忽视的栈溢出风险递归解法虽然代码好看但我在本地测试一个包含十万个节点的链表时直接栈溢出了。这不是递归写法的 bug而是递归本质的代价。如果你平时主要用 Java 或 Go默认栈大小并不算大长链表场景下递归尤其危险。面试结束时如果面试官追问“还有别的写法吗”或者“你这个写法的缺点是什么”你就可以很自然地引出这一点。这比硬背一段“递归简洁但迭代更优”的话术要可信得多因为你是从实际测试中得出的结论。7. 反转链表背后的能力和刷题节奏建议7.1 面试官真正在考察的三种能力反转链表表面上考的是一段十几行的代码实际上考的是三种能力。第一种是抽象能力能不能把“把当前节点的 next 指向前一个节点”这句话翻译成三行指针操作。很多科班出身的同学在 Python 里写过a, b b, a就觉得自己理解引用和指针了但链表的指针操作比这复杂得多尤其是多个指针同时移动时理解“每个变量是对某个对象的引用”这件事会被真正检验。第二种是边界思维能力空链表、单节点、头节点变化、尾节点变化每一类边界都需要在编码前预判。面试时你能主动说出“需要考虑链表长度为 0 或 1 的情况”比写完代码再补判断要好一个档次。第三种是变体迁移能力反转链表只是模板能不能把模板迁移到回文链表、反转区间、两两交换、K 个一组反转才是面试官更想看到的。你不需要背十几个模板你只需要理解一个模板的“为什么”变形题自然能推。7.2 我自己整理的一份反转链表面试清单这份清单可以辅助你复习把热题 100 里涉及链表反转的题放在一起找规律。力扣题号题目与反转的核心关系206反转链表基础模板迭代与递归92反转链表 II区间反转虚拟头节点24两两交换链表中的节点反转的迷你版K2 特殊形式25K 个一组翻转链表区间反转循环版143重排链表后半段反转后交叉合并234回文链表后半段反转后逐个比较我建议按 206 - 92 - 24 - 25 的顺序刷最后再去做 143 和 234。这个顺序的好处是每一步都在前一步上叠加新的复杂度不会出现突然跳级的情况。很多人喜欢按难度排序从简单到难但链表反转这条线按“反转范围从小到大”排序会更舒服整链、区间、成对、分组、交叉。7.3 别急着刷难题先把这道题讲给空气听我之前带过几个新人发现一个共性他们能准确写出反转链表的代码但让他解释“为什么 head.Next.Next head 这行不会丢节点”他就卡住了。能写和能讲是两种能力面试里后者更重要。我推荐的训练方法是“讲给空气听”合上所有资料打开一个空文档假装对面坐着面试官开始讲反转链表的迭代法。讲清楚每一步指针变化讲清楚循环不变量讲清楚空节点边界。讲完之后再讲递归法。如果你讲得磕磕绊绊说明某些细节你还没真正理解回到纸上继续模拟。这个训练方法不花多少时间但对链表这类基础题的掌握度提升得非常明显。7.4 后续扩展从反转链表到更多链表技巧反转链表掌握之后你积累的“改指针前先备份”“虚拟头节点统一边界”“循环不变量”三件套几乎可以平移推广到所有链表题。做链表合并时你会自然想到用 dummy 来建新链做链表排序时你会自然想到用快慢指针找中点再配合反转和合并做链表相交时你会自然想到长度对齐。这些都不是天外来客它们都是从一次一次耐心的指针模拟中长出来的肌肉记忆。所以我的建议始终是慢就是快。反转链表这道题值得你花四十分钟纸上跑三遍讲给自己听一遍。这一小时不会再浪费它会在你后面几十道链表题里反复给你回报。最后分享一个小体会力扣热题 100 这个清单最大的价值不是让你追求完成数量而是帮你把高频考点练成条件反射。反转链表是这些条件反射里最基础的一个。我现在的习惯是每天开始刷题之前先用两分钟在纸上默写一遍反转链表的迭代法和递归法不需要跑代码就当热身。坚持一个月之后你会发现所有涉及链表指针的题目你的思路都会清晰很多。