
不想给自己留退路直接说我第一次在面试里见到“反转链表”这道题时的真实反应心里想的是“这不就是把链表倒过来吗”手上却写了二十多分钟才把指针整明白。后来刷LeetCode刷到它才知道这道题是234题以外被问得最多的链表题之一也是“LeetCode热门100题”里的常客。如果你最近在准备面试、刷LeetCode或者想把链表基础打牢这道题绕不开。反转链表解决的问题很直接给你一个单链表的头节点返回反转后的链表。它听起来简单但考察的是你对指针/引用的理解、边界条件的敏感度以及对“原地修改”与“新建链表”两种思路的分辨能力。常用解法有两套迭代和递归各有各的适用场景。这篇文章我会把两道经典版本的思路、代码、坑点一起讲透适合刚入门到刷题中期的读者参考。1. 题目前瞻反转链表在LeetCode体系里为什么是“必刷”题1.1 题目本质与链表结构拆解先看题目本身给定单链表的头节点head反转链表返回反转后的链表头节点。单链表的结构很朴素每个节点里面只有两样东西val当前节点的值和next指向下一个节点的引用。它不像数组那样可以通过下标随机访问你只能沿着next一个接一个地走。所以“反转”这件事在链表里意味着把所有节点的next方向彻底掉头。很多人第一反应是“把链表遍历一遍存到一个数组里再倒着串起来”。这个思路对不对对但不理想。因为面试官基本都想看到你原地反转也就是不额外开辟 O(n) 空间只靠改指针完成。这也是为什么反转链表是“面试基础题”里最具代表性的题目之一它考察的不是你会不会调 API而是你是否理解引用类型在内存里是怎么“牵一发动全身”的。在 Python 里是对象引用在 C/C 里是指针在 Java 里也是引用语言不同本质一样。1.2 反转链表的核心难点与常见误区反转链表表面上是“改next方向”但真正让新手翻车的点有三个第一断链恐惧。当你想让cur.next prev时cur后面的节点就暂时找不到了所以必须先保存next节点否则链表就断了。这个“先保存后赋值”的顺序是这道题第一个门槛。第二头节点变化。反转之后原来的尾节点变成了新头节点返回值必须指向它。很多人把head传进去之后最后返回的还是原来的head白忙一场。第三边界条件。空链表、只有一个节点的链表应该原样返回。很多解法如果不处理head None或head.next None会在运行时直接报空指针异常。至少两个误区我亲眼见过有人把pre初始化为head然后cur从head.next开始走结果四个节点的链表反转后只剩三个节点还有人用递归解法时没考虑 Python 默认递归深度限制链表一长直接RecursionError。这些坑在后面的章节我会逐个展开。2. 核心解法拆解迭代法、递归法到底怎么选2.1 迭代法三指针移动全流程解析迭代法是最推荐新手掌握的写法因为它直白、可控、不会爆栈。核心思路是维护三个指针prev、cur、nxt。cur指向当前要处理的节点prev指向已反转部分的新头nxt用来保存cur.next防止断链。我直接给 Python 版的完整代码def reverseList(head): prev None cur head while cur: nxt cur.next # 先保存下一个节点 cur.next prev # 当前节点指向前一个节点 prev cur # prev 后移 cur nxt # cur 后移 return prev走一遍流程会更好懂。假设链表是1 - 2 - 3 - None初始prev Nonecur 1。第一次循环nxt 21.next Noneprev 1cur 2。第二次循环nxt 32.next 1prev 2cur 3。第三次循环nxt None3.next 2prev 3cur None。循环结束返回prev此时prev是节点 3链表变为3 - 2 - 1 - None。为什么返回prev而不是cur因为循环结束时cur已经越界变成None而prev恰好停在原链表的尾节点也就是反转后的头节点。这个细节很容易被忽略但恰恰是最关键的。C 语言版的核心逻辑也一样只是用指针更直观struct ListNode* reverseList(struct ListNode* head) { struct ListNode *prev NULL, *cur head, *nxt; while (cur) { nxt cur-next; cur-next prev; prev cur; cur nxt; } return prev; }迭代法的空间复杂度是 O(1)时间复杂度是 O(n)只遍历了一遍链表。面试时如果你先写迭代法基本能拿满分。2.2 递归法的本质从后往前看问题递归法能写成的人比迭代少但正因为递归的思维反直觉所以反而成了一部分面试官喜欢追问的点。写递归前必须接受一个设定递归函数reverseList(head)的返回值是“以head为起点的链表反转后的新头节点”。先看代码def reverseList(head): if not head or not head.next: return head new_head reverseList(head.next) head.next.next head head.next None return new_head这个写法里有三步关键操作new_head reverseList(head.next)先递归反转后续所有节点拿到反转后的头节点。head.next.next head让当前节点head的后继节点反过来指向自己。举个例子链表1 - 2 - 3递归到head 2时2.next.next 2就是让 3 指向 2。head.next None切断当前节点与原后继的连接防止出现环。递归法的空间复杂度是 O(n)因为递归调用栈会吃内存。LeetCode 上链表长度一般在几百到几千不会爆栈但如果你在本地测试超长链表或者在面试时被问到“链表有 10 万节点怎么办”递归法就不太合适了这时候迭代法更稳。2.3 复杂度分析与面试官追问反转链表的复杂度分析本身不难但面试官喜欢在这个基础上做文章。我整理过一套问答思路基本是这么展开的维度迭代法递归法时间复杂度O(n)O(n)空间复杂度O(1)O(n)是否原地修改是是适合链表长度无限制不建议超长链表面试推荐度首选加分项追问一为什么递归空间复杂度是 O(n)因为每一次递归还没返回时调用栈上都有当前帧n 个节点就有 n 层调用每层栈帧占固定内存所以是 O(n)。追问二能不能用尾递归优化能但 Python 官方解释器不支持尾递归消除所以写了也没用。如果面试官用 C 或者函数式语言可以提一下尾递归版本。追问三如果要求“每 K 个一组反转”怎么处理这就是 LeetCode 25 题了后面第 3 节我会展开说。3. 进阶变形从反转链表到LeetCode热门100题里的“大题”3.1 区间反转LeetCode 92 的核心处理技巧反转链表不只有 206 这一道。LeetCode 92 是“反转链表 II”要求反转从位置left到位置right的区间节点。这道题在“LeetCode热门100题”里也时常出现因为它更贴近实际业务里的局部修改场景。思路比完整反转多一步“找到区间边界”然后再做一次小范围反转。我常用一个虚拟头节点dummy来避免处理left 1时的头节点变化问题def reverseBetween(head, left, right): dummy dummy_start ListNode(0) dummy.next head for _ in range(left - 1): dummy_start dummy_start.next cur dummy_start.next prev None for _ in range(right - left 1): nxt cur.next cur.next prev prev cur cur nxt dummy_start.next.next cur dummy_start.next prev return dummy.next这段代码里最关键的是最后两步反转完之后prev是区间内的新头cur是区间后的第一个节点。需要把区间原来的第一个节点现在变成尾节点接到cur再把dummy_start的next指向prev。顺序错了链表就会断成两截。3.2 K个一组翻转链表LeetCode 25的经典套路LeetCode 25 是“K 个一组翻转链表”也是很多面试官口中“反转链表的进阶考法”。它的核心是每 K 个节点作为一组组内反转组与组之间保持原有顺序。如果剩余节点不足 K 个保持不变。这类题的套路是先写出一个“反转区间”的小函数然后在主函数里分组调用。分组时需要两个指针一个start指向当前组头一个end指向当前组尾先跑 K 步确认这组够长def reverseKGroup(head, k): dummy ListNode(0) dummy.next head pre dummy while head: tail pre for _ in range(k): tail tail.next if not tail: return dummy.next nxt tail.next new_head, new_tail reverse_range(head, tail) pre.next new_head new_tail.next nxt pre new_tail head nxt return dummy.next其中reverse_range可以复用 206 题的核心逻辑只是明确指定反转起点和终点。能独立把这道题写出来的人对“引用”、“断链”、“边界”的理解基本就到面试合格线了。3.3 这道题的“阴影部分”反转链表在更复杂题里的应用场景反转链表裸题只是表象它最值钱的地方在于“作为零件”广泛镶嵌在其他高频题里。我见过的最典型四类回文链表LeetCode 234先找中点然后反转后半段再和前半段逐个比较。如果不会反转链表这道题只能靠栈空间复杂度直接被压下去。两个链表相加LeetCode 2、445需要“右对齐”两个链表时反转链表是最高效的预处理手段。反转每 K 个一组LeetCode 25在“K 个一组翻转链表”里206 的迭代逻辑是内层循环的直接基础。反转偶数长度组LeetCode 2074反转思路完全一样只是判定条件换成“当前组是否偶数长度”。所以我说刷题不要满足于 206 这一道题能过而是要能在一看到“反转”两个字时马上自动联想到“改 next 方向 保存后继 原地操作”这个心智模型。掌握了这一点哪怕换个壳比如某次周赛里出现的“073 爱吃香蕉的狒狒”这类套壳题目你也能快速把题干翻译成数据结构问题反而不容易被题目的花活带偏。4. 实操记录从编译报错到通过全流程复盘4.1 我踩过的三个低级坑与排查思路反转链表虽然简单但我在带别人刷题、以及自己平时练习时确实见过不少重复出现的坑。挑三个最有代表性的说坑一没有先保存next。有人写循环时上来就是cur.next prev结果下一轮cur cur.next时取到的是已经被改过的prev链表直接变成环。排查方法很简单在 LeetCode 本地调试时打印cur.val如果出现重复值八成就是断链或成环。坑二返回了head而不是prev。反转完成后原来的head变成了尾节点它的next已经指向None。如果返回head从head开始遍历只能看到一个节点。这种错误在 LeetCode 上会显示为“输出和预期不符”仔细看输出长度就能发现只输出了一个节点。坑三递归解法里忘写head.next None。只写head.next.next head不写切断链表会在反转后形成环LeetCode 提交时可能直接超时不是报错是死循环。这个问题比较隐蔽本地小数据测试往往发现不了但压测数据一长栈和循环都扛不住。我自己刷 LeetCode 时还有一个习惯拿到题目先画三张图原始链表、反转过程第一步、反转完成状态。尤其是反转链表这种引用操作画图和“脑内模拟指针移动”比硬记代码有效得多。4.2 常见问题速查表这里整理一张我在实际答疑过程中被问得最多的“问题-原因-解法”速查表可以直接收藏当作自查清单问题现象可能原因解决思路空链表报错没有处理head is None循环前加if not head or not head.next: return head输出只剩一个节点返回值写成了原head返回prev或递归里的new_head输出顺序不对prev和cur初始化反了prev必须是None不是head出现环提交超时递归法忘写head.next None补上断链操作本地测试正常提交超时递归深度超限改用迭代法面试手写卡壳没理解“先保存后继”把“保后继、改指针、双指针移动”六个字写在草稿纸上这张表是我从实际答疑和复盘里提炼的基本覆盖了这道题 90% 的出错点。如果你提交失败时不知道从哪查建议把“返回的是哪个节点”“循环退出条件是什么”“有没有成环”这三个问题先问一遍自己多半能找到问题。5. 一点刷题经验单刷 206 反转链表十分钟能过不算本事真正扎实的刷法是把这道题当作一个“母题”把迭代、递归、区间反转、K 个一组这四种版本都写一遍并且保证打断几天后还能不看答案写出来。我自己的实测体会是反转链表这道题的代码量不大但它对“引用思维”的锻炼价值非常高。很多链表题写不出来的根源不是算法不懂而是对“变量保存的是引用而不是节点本身”这个点没有形成直觉。这道题恰恰能用最少的代码量把这个直觉打出来。最后分享一个我在刷题营带新人时反复讲的小技巧如果你今天只有 20 分钟刷题时间与其开一道新题不如把 206 用三种写法各做一遍并口头模拟一遍指针移动过程。这个练习坚持两周你会发现自己在做 92、25、234 这些题时速度明显不一样。