资讯详情

反转链表全解析:迭代、递归与头插法及常见变形

📅 2026/10/11 17:10:34 | 华诺云谱 👁 阅读
反转链表全解析:迭代、递归与头插法及常见变形
反转链表是链表操作的经典题也是我每次带新人必讲的第一道题。它表面上就一句话把单链表中的指针方向全部反过来返回新的头节点。但就是这一句话能同时考察你对指针引用的掌握、对边界条件的敏感度以及能不能把迭代和递归两套思维在同一道题里融会贯通。不管是应付考试、准备面试还是单纯想把自己的基本功打扎实这题都值得反复拆。一开始大多数人会去背代码背完第二天又忘。原因很实在没搞明白指针在每一步到底发生了什么。这篇文章我会把反转链表的三种主流写法拆开讲包括迭代、递归和头插法再把常见变形部分反转、K个一组反转也补上最后整理我平时调试链表代码的避坑经验。读完你不仅会写还能清晰解释每一步为什么这么写。1. 思路拆解反转链表到底在考什么1.1 题目描述与输入输出先明确题面。输入是一个单链表的头节点链表节点定义很简单常见的有两个字段val和next。next存储的是下一个节点的引用最后一个节点的next是空指针null或None。反转后原来的尾节点变成新链表的头节点原来的头节点变成新链表的尾节点原链表每个节点的next指向完全逆反。举个例子1 - 2 - 3 - 4 - 5 - null反转后是5 - 4 - 3 - 2 - 1 - null。这里有个高频细节返回值是新链表的头节点也就是反转前的尾节点。很多人写迭代公式时最后返回的是pre而不是cur就是因为反转结束时pre恰好指向新链表的头。这个细节我会在后面的代码里再强调。1.2 为什么这题能成为经典考察点它被称为链表操作经典题是因为一道题里塞进了四个基本功。第一节点的“断开”与“连接”。链表不像数组有连续的内存它通过引用串起来。反转动的是next指针不改动节点本身这要求你清楚知道什么时候该改哪个指针以及顺序不对会造成什么后果。第二边界条件的敏感度。空链表、只有一个节点、只有两个节点这三种情况是初学者最容易翻车的地方。题目能反复考就是因为即便只差一个判断结果可能从正常输出变成空指针崩溃。第三递归思维的验证。递归写法代码只有几行大神的写法非常简洁但能不能真正看懂取决于你是否理解函数调用栈以及“先处理子问题再处理当前节点”这种自底向上的推理。第四指针引用 vs 值拷贝。这个在 C/C 里尤其明显。很多人以为把head赋值给cur后head.next修改了cur还是原来的那个“旧图”实际上它们指向同一个对象。理解这一点面试时讲代码才能让人信服。1.3 三指针迭代法的直观逻辑迭代法是反转链表最友好的入门方式逻辑可以概括成三个指针pre前驱、cur当前、nxt后继。你可以想象成一副骨牌原本每张骨牌只有右边那只手搭着下一张牌。反转的动作就是让每个节点的“手”从指向右边改成指向左边。但麻烦在于当你试图把当前节点指向左边的节点时它右边原本抓着的那个节点会被“丢掉”因为链表没有索引从当前节点再往右走的路已经断了。所以必须用一个额外的指针nxt先把右边那只手要抓的节点拽住防止丢失。三个指针的循环移动可以总结成一句口诀先保存后路再掉转枪口最后整体前移。这个顺序不能换。保存后路是第1步掉转枪口是第2步指针整体前移是第3步。如果把第1步和第2步调换当前节点和它的后继之间的引用已经断了nxt拿到的可能是错误的位置代码就会陷入空指针或死循环。2. 迭代法从第一行代码到彻底吃透2.1 完整实现与逐行注释我给出两种语言的实现思路完全相同。大家平时刷题用 C 或 Python 比较多我两种都写。C 版本ListNode* reverseList(ListNode* head) { ListNode* pre nullptr; // 前驱指针最后会成为新链表的头 ListNode* cur head; // 当前要处理的节点 while (cur ! nullptr) { ListNode* nxt cur-next; // 1. 保存后路防止指针丢失 cur-next pre; // 2. 掉转枪口当前节点指向前驱 pre cur; // 3. 整体前移pre 移动到当前 cur nxt; // 4. 整体前移cur 移动到原本的后继 } return pre; }Python 版本def reverseList(self, head: ListNode) - ListNode: pre None cur head while cur: nxt cur.next # 保存当前节点的下一个节点 cur.next pre # 反转指针 pre cur # pre 前移 cur nxt # cur 前移 return pre我把pre初始化为null原因非常直观原链表的头节点在反转后会变成尾节点尾节点的next应该指向空。所以当cur第一次指向head时执行cur-next pre实际上就是在把head-next设置为null。这一步同时完成了“反转”和“封尾”。2.2 核心细节为什么要先保存 next 指针这是迭代法里最关键的一步也是我让新人画图最多的地方。假设链表是A - B - C当前cur指向Apre初始是null。如果直接执行cur-next pre那么A-next变成null也就是从A到B的那根线断了。而cur本身只知道A这个对象它没有索引能力去“找到”B因为通向B的唯一路径就是A-next这个路径已经被覆盖。所以必须提前用一个普通变量nxt把B保存下来。这种做法不只是反转链表特有你以后做链表删除节点、交换相邻节点、区间反转都会反复用到同一个原则改动引用前先备份可能丢失的路径。我把这个类比成搬家前先给房间拍照你打算把家具移走但移走之后你忘了原来哪个柜子在哪一侧怎么办先拍照记录位置再动手就不会乱。nxt就是那张照片。2.3 边界条件与虚拟头节点的讨论三指针迭代法天然能处理边界情况这点是它最大的优势。空链表head是nullcur head nullwhile循环根本不进入直接返回pre null。注意返回的是pre不是cur否则你会得到一个null的pre但cur也是null这里没问题但如果是空链表返回cur你返回的是空指针和返回pre的结果一样都说得通。我习惯统一返回pre因为反转结束后它一定指向新链表的头不管是空还是非空语义一致。单节点链表head只有一个节点nxt nullcur-next pre把它的next指向null然后pre headcur null循环退出返回head。这正好保证单节点链表反转前后都是它自己。双节点链表A - B。第一次循环A-next nullpre Acur B。第二次循环B-next Apre Bcur null。返回B得到B - A - null。你可以在草稿纸上推一遍这是理解整个循环过程最直观的训练。有人会问虚拟头节点dummy head在这题里有用吗反转整个链表一般不需要。虚拟头节点主要用于需要“在链表头部前插入一个哨兵”的场景比如删除节点或反转部分区间时用它避免对head做特殊判断。咱们后面的区间反转会用到。3. 递归反转一行逻辑背后的栈帧故事3.1 递归思路把反转拆成“子问题”和“接回自己”递归写法很难用“从头往后”的直觉理解我建议换个方向先看向后递归到尽头然后一层层往上“接”。递归函数reverseList(cur)的职责是接收一个头节点cur返回以cur为开头的这段链表反转后的新头节点。代码可以这样写def reverseList(self, head: ListNode) - ListNode: if not head or not head.next: return head new_head self.reverseList(head.next) head.next.next head # 把当前节点接到它后继节点的后面 head.next None # 封尾防止环 return new_head关键逻辑在这一句head.next.next head。假设当前节点是A它的后继是B。递归调用返回后B所在的那一串已经被反转并且B正好是反转后那段链表的尾节点。那么B.next现在是空的我们把B.next指向A就把A接到了整条反转链的尾部。这相当于在递归返回过程中每个节点都在“做同样一件事”把自己接到已经反转好的子链表后面。这里最反直觉的是整个过程中返回的头节点始终没变都是最开始那个尾节点。每层递归都只是在往它后面追加节点。3.2 递归与迭代的复杂度对照时间上两种方法都是 O(n)每个节点都只访问一次。空间上迭代法只用常数级额外空间也就是 O(1)。递归法因为要保存每一层函数的局部变量和返回地址空间复杂度是 O(n)。这个差异在链表很长时非常致命。正常刷题链表长度几千几万没问题但如果面试官扩展到百万节点递归写法可能导致调用栈溢出。C 默认栈空间有限Python 默认递归深度限制大概在 1000 层左右超过就会抛异常。所以如果面试时没有特别要求用递归我一般推荐迭代。但递归思想必须会因为面试官可能为了考察思维让你写递归也可能让你把递归转成迭代。两个方向都练才算真的掌握。另外有人说“尾递归优化可以解决栈溢出”我在这里解释下尾递归要求递归调用是函数返回前的最后一个操作并且不再依赖当前层的结果。反转链表这个递归写法递归调用之后还要执行head.next.next head所以它不是尾递归不能依赖优化。真要优化空间老老实实走迭代。3.3 递归调用过程逐步推进用一个小例子走一遍帮助大家彻底理解。链表1 - 2 - 3 - null。调用reverseList(1)1.next不是空进入递归调用reverseList(2)。 调用reverseList(2)2.next不是空进入递归调用reverseList(3)。 调用reverseList(3)3.next是空返回3给上一层。回到reverseList(2)此时head 2head.next 3而递归返回的new_head 3。 执行head.next.next head也就是3.next 2。 执行head.next null也就是2.next null。 返回new_head 3。回到reverseList(1)此时head 1head.next 2递归返回的new_head还是3。 执行head.next.next head也就是2.next 1。 执行head.next null也就是1.next null。 返回new_head 3。最终得到3 - 2 - 1 - null。如果你在纸上画出每次赋值后各个节点的next变化会发现非常清晰。初次接触时我建议至少手动推三遍直到不需要用笔记也能说清楚每一层在干嘛。4. 头插法与原地反转另一种视角的实操4.1 头插法反转思路迭代法是从前往后改指针方向还有一种思路是“头插法”。它模拟的是从原链表头部依次摘下节点每次都插到新链表的头部。这样就天然完成了逆序。实现时不需要新建链表在原链表基础上操作即可。需要两个辅助指针new_head表示已经反转好的新链表的头节点初始为nullcur表示当前要摘下的原链表节点。循环里做四件事用nxt保存cur.next防止摘除节点后失去原链表的遍历路径。把cur.next指向new_head也就是让当前节点成为新链表的头。更新new_head cur新链表头变为当前节点。更新cur nxt继续处理原链表的下一个节点。代码如下def reverseList(self, head: ListNode) - ListNode: new_head None cur head while cur: nxt cur.next cur.next new_head new_head cur cur nxt return new_head你会发现这段代码和前面的三指针迭代法结构几乎一样区别只是变量名和更新顺序。本质上它们是一体两面三指针法是“让每个节点指向前一个”头插法是“把当前节点插到新链表最前面”。理解了其中一个另一个顺手就能学会。4.2 为什么头插法在部分反转里更重要头插法真正的价值体现在“反转链表的第 m 到 n 个节点”这类题目中。因为这种题要求只反转中间一段两边的节点要保持原序用三指针迭代法处理时边界变量特别多容易漏链。头插法可以少记一个pre指针配合一个“区间前驱”固定节点就比较清晰。具体到反转整个链表时两种方法没有本质优劣面试时挑自己熟悉的那一种即可。但学习阶段我建议花时间把两种都写一遍因为它们的思维模式不同一种是“破坏原有链接再建立新链接”一种是“摘下来换个地方插回去”对未来接触更复杂的链表题很有帮助。4.3 三种方法横向对比我先列一张表方便你复盘。方法核心动作时间复杂度额外空间代码量易错点三指针迭代原地修改 next 指向O(n)O(1)7 行左右忘记保存 nxt递归子链表反转后接回当前节点O(n)O(n)5 行左右忘记封尾导致环头插法依次摘节点插到新链表头O(n)O(1)7 行左右更新顺序混乱递归代码最少但空间消耗最大。迭代和头插空间开销一样我在实际面试里更愿意写迭代因为便于一边写代码一边和面试官解释每一步一旦变量名写错了也容易定位。头插法适合在“部分反转”题里使用思路更直观。5. 从基础到变形反转链表还能怎么考5.1 反转部分区间第 m 到第 n 个节点这是反转链表最常见的变式。要求是输入一个区间比如第 2 到第 4 个节点只反转这部分其余保持原序。理想结果是1 - (4 - 3 - 2) - 5括号内是反转后的区间。处理思路有四步找到反转区间的前一个节点记作prev。如果从第 1 个节点开始prev就是空。为了防止对头节点做特殊处理这里通常使用虚拟头节点dummy让dummy - head最后返回dummy.next。在区间内执行反转可以用三指针也可以用头插法。反转结束后要把反转后的头节点接回prev.next把反转后的尾节点接到原来的nxt上。返回虚拟头节点的下一个节点也就是新链表真正的头。这个题比原始反转复杂的地方在于你不但要反转中间的指针还要额外保存“区间前驱”和“区间后继”等中间反转完再接线。漏接任何一个都会导致链表断裂。我建议先把基础反转写得滚瓜烂熟再拿这道题练手。5.2 K 个一组反转链表另一种变形是“每 K 个节点为一组组内反转不足 K 个不反转”。这个题难度高不少因为它同时考察了分组边界处理和递归或迭代逻辑。一种自然的解法是递归先判断从当前节点开始是否还能凑出一组 K 个节点不能就直接返回当前头节点。能的话翻转这 K 个节点然后递归处理剩余部分最后把反转后的尾节点接到剩余部分的新头节点上。这里“反转 K 个节点”是基础反转的封装只是限制长度。使用这种递归写法时每一层都要先数数统计剩余节点数所以总时间仍然是 O(n)但常数略高。你也可以用迭代完成不过需要维护多个内外层指针代码会更长。建议先把基础反转和部分反转吃透这道题放在后面再攻。5.3 与回文链表、环检测、两两交换的关联反转链表不只被直接考察它常常藏在更大的题目里。最常见的例子是判断回文链表。朴素做法是把链表里的值复制到数组然后用双指针判断但这样额外空间是 O(n)。常数空间的做法就用到反转先用快慢指针找到链表中点然后把后半段反转再同时从头部和反转后的后半段开始逐个比较。还有“两两交换链表中的节点”虽然不完全等价于反转但同样依赖“保存后继、改变指向、前移”这套方法论。我把这些题放在一起讨论是想说明一个现象链表操作题看起来五花八门核心模式其实很少。反转链表是其中最典型、最基础的一个母题只要真正吃透其他链表题学起来会快非常多。6. 常见问题与避坑实录6.1 常见错误速查表我按实际刷题和辅导里见到的错误整理成一张速查表。错误现象根本原因解决办法反转后只输出一个节点循环里没有正确移动cur导致只反转了头节点就退出检查循环最后是否有cur nxt程序陷入死循环反转后链表出现了环通常是尾节点的next没有置空在迭代/递归最后显式设置head.next None返回错位节点想返回新头结果返回了原头迭代返回pre递归返回new_head不是head在递归解法里栈溢出链表长度超过递归深度限制或递归退出条件写错检查if not head or not head.next或改用迭代反转部分区间时断裂prev和原始后继没保存好反转前先保存prev.next和区间结束后要接的节点修改head本身导致原链表丢失在迭代过程中直接对head做了赋值又没有备份用cur做遍历变量head只作为初值我特别把“尾节点的next没有置空”列出来这问题在递归写法里发生率最高。如果不置空最后两个节点之间会形成环输出链表时程序会一直遍历直到栈溢出或超时。判断办法是在本地调试时打印链表节点如果打印出重复节点基本就能确定是环。6.2 调试链表代码的实用技巧链表调试比数组麻烦因为你不能直接用下标查看任意位置。我的经验有三条。第一写一个打印函数printList(head)在每步关键操作之后打印整个链表。不要偷懒这对判断“指针是否丢失”“是否成环”非常有效。第二在纸上画简图用方框代表节点用箭头代表next。每执行一次cur-next pre就擦掉箭头重新画。我知道这听起来麻烦但亲手画三遍之后你对指针变化的直觉会上一个台阶。很多新人觉得自己懂了一画图就发现哪一步没想通。第三用极端输入测试空链表null单节点链表双节点链表五个节点的链表。这四个用例能覆盖绝大多数边界问题。先跑测试再提交能省去很多次失败的提交记录。6.3 面试现场如何讲解反转链表面试里写这道题除了代码正确更看重你能否清晰表达“为什么这么做”。我建议按下面的顺序回答先确认问题是单纯反转整个链表还是带有额外条件需不需要保持稳定性说明思路我先用三个指针pre、cur、nxt每次把cur.next指向pre然后整体右移。强调关键在改cur.next之前必须先保存nxt否则链表会断。主动谈边界空链表和单节点链表可以直接返回原头。最后补复杂度时间 O(n)空间 O(1)。面试官如果追问“能不能用递归”你再递进讲解递归版本。如果一开始就直接写递归很可能暴露简历上“熟悉数据结构”的含水量。我自己的习惯是先给面试官一个最稳定、最容易解释的方案再根据补充问题展开其他版本。这样既不显得背题又能体现思维的层次感。最后再分享一点经验反转链表是我见过的、唯一一个“代码短但能聊很久”的题目。带过不少同学之后我发现大家最大的障碍不是写不出来而是不敢把自己的逻辑按步骤说出来。如果你现在还在背代码我强烈建议你尝试用“保存后路—掉转枪口—整体前移”这十二个字对着白板讲一遍思路然后再写代码。讲着讲着你就会发现很多之前模糊的地方变清晰了。另外一个小技巧练习时把 C、Python 两种版本都写一遍。语言不同对指针和引用的表达方式也不同。能写出两种语言说明你是真的理解了算法的本质而不是只记住了一个语法模板。这题后续还可以往“反转双向链表”“随机指针链表”等方向扩展但底层思维还是这些。先把手上的基础版本练透后面的大题都会轻松不少。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑