news 2026/10/19 13:51:29

LeetCode反转链表:迭代法详解与指针操作避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode反转链表:迭代法详解与指针操作避坑指南

1. 反转链表到底在反转什么

链表反转是LeetCode上出镜率最高的题目之一,没有之一。我刷了这么多题,面试里被问到的链表题,十道里有六道跟反转沾边——要么是完整反转,要么是反转前K个,要么是两两交换,要么是区间反转。所以把这个基础款吃透,后面那些变种就是在这个骨架上加参数而已。

先把问题说清楚。给你一个单链表的头节点,比如1 -> 2 -> 3 -> 4 -> 5 -> NULL,要求你把它变成5 -> 4 -> 3 -> 2 -> 1 -> NULL。看起来简单,但真正动手写的时候,很多人会在指针操作上翻车。为什么?因为单链表只能单向遍历,你把当前节点的next指向前一个节点之后,就找不到后面的节点了。所以核心难点就一个:在改变指针方向之前,先把后面的路记下来。

这篇文章适合谁看?如果你刚开始刷LeetCode,对指针操作还不太有手感,那这篇就是写给你的。如果你已经刷过一些题但每次写反转链表都要想半天,那这篇也能帮你把思路固化下来。我会用C语言来写,因为C的指针操作最赤裸,没有语法糖帮你兜底,把C写明白了,其他语言就是换个写法的事。

2. 解题思路的拆解与方案选型

2.1 为什么迭代法是最优先的选择

反转链表的解法主要有两种:迭代和递归。我先说结论——面试里优先写迭代法。原因不复杂:迭代法空间复杂度是O(1),递归法虽然代码短,但空间复杂度是O(n),因为每一层递归都要压栈。而且递归法在链表特别长的时候有栈溢出的风险,虽然LeetCode的测试用例一般不会长到那个程度,但面试官如果追问“有没有更优的空间方案”,你递归写到一半就尴尬了。

迭代法的核心思想用一句话概括:用三个指针,一边遍历一边掉头。这三个指针分别是:

  • prev:指向前一个节点,初始为NULL
  • curr:指向当前节点,初始为head
  • next:临时存储下一个节点,防止断链

你可以把整个过程想象成一群人排队,原本每个人都面朝前方,现在要求所有人转身面朝后方。你不能让一个人直接转,因为他转过去之后,他前面那个人就找不到后面的人了。所以你需要一个人一个人地处理,每处理一个人之前,先记住他后面是谁。

2.2 递归法的适用场景与局限

递归法不是不能用,但你要清楚它的适用边界。递归的思路是从最后一个节点开始反转,然后逐层往回走。代码确实简洁,但有几个实际问题:

第一,当链表长度超过几万的时候,递归深度就危险了。第二,递归法的思路在面试中解释起来反而更绕,面试官如果让你画图说明,迭代法画起来更直观。第三,很多变种题(比如反转区间、K个一组反转)用迭代法更容易控制边界条件。

所以我的建议是:先把迭代法练到闭着眼睛都能写对,递归法作为知识储备了解即可。面试的时候主动写迭代,如果面试官问“还有别的方法吗”,你再把递归思路讲出来,这样节奏就很舒服。

2.3 三个指针的初始状态为什么这样设

很多人会问,为什么prev要初始化为NULL?因为反转之后,原来的头节点会变成尾节点,而尾节点的next必须是NULL。你初始把prev设为NULL,第一次循环的时候curr->next = prev就自然把第一个节点的next置为NULL了,不需要额外处理尾节点。

那curr初始为head这个没什么好说的,从头开始遍历。关键是next指针,它必须在每次改变curr->next之前就保存好curr->next的原始值。顺序不能乱:

next = curr->next; // 先记住后面的路 curr->next = prev; // 再掉头 prev = curr; // prev往前挪 curr = next; // curr往前挪

这四行的顺序是铁律,任何一行提前或延后都会导致断链或者死循环。我见过有人把curr->next = prev写在第一行,结果next拿到的已经是修改后的值了,整个链表直接断掉。

3. 完整代码实现与逐行解析

3.1 迭代法完整实现

先给出结构体定义,这是LeetCode的标准定义:

struct ListNode { int val; struct ListNode *next; };

然后是反转函数:

struct ListNode* reverseList(struct ListNode* head) { struct ListNode *prev = NULL; struct ListNode *curr = head; struct ListNode *next = NULL; while (curr != NULL) { next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; }

就这十几行,但每一行都有讲究。我来逐行拆解。

prev = NULL:前面说过了,为了处理尾节点。curr = head:从头开始。next = NULL:初始化而已,进循环后第一件事就是给它赋值。

循环条件是curr != NULL,不是curr->next != NULL。为什么?因为你要处理每一个节点,包括最后一个节点。如果用curr->next != NULL作为条件,最后一个节点就不会被处理,它的next还指向倒数第二个节点,结果就是尾节点没有正确置NULL。

循环体内四行代码的顺序前面已经强调过了。循环结束后,curr已经是NULL了,prev指向的是原来的最后一个节点,也就是反转后的头节点。所以返回prev。

3.2 手动模拟执行过程

光看代码可能还不够直观,我带你走一遍1 -> 2 -> 3 -> NULL这个例子。

初始状态:prev = NULL,curr = 1,next = NULL。

第一轮循环:

  • next = curr->next,所以next = 2
  • curr->next = prev,所以1->next = NULL
  • prev = curr,所以prev = 1
  • curr = next,所以curr = 2

此时链表状态:1 -> NULL,prev指向1,curr指向2。

第二轮循环:

  • next = curr->next,所以next = 3
  • curr->next = prev,所以2->next = 1
  • prev = curr,所以prev = 2
  • curr = next,所以curr = 3

此时链表状态:2 -> 1 -> NULL,prev指向2,curr指向3。

第三轮循环:

  • next = curr->next,所以next = NULL
  • curr->next = prev,所以3->next = 2
  • prev = curr,所以prev = 3
  • curr = next,所以curr = NULL

此时链表状态:3 -> 2 -> 1 -> NULL,prev指向3,curr为NULL。

循环条件不满足,退出。返回prev,也就是节点3,反转完成。

注意:手动模拟看起来简单,但真正写代码的时候,很多人会在prev和curr的移动顺序上犯错。记住一个原则——先保存,再掉头,最后移动。

3.3 递归法实现与对比

递归法的代码是这样的:

struct ListNode* reverseList(struct ListNode* head) { if (head == NULL || head->next == NULL) { return head; } struct ListNode *newHead = reverseList(head->next); head->next->next = head; head->next = NULL; return newHead; }

递归的终止条件是链表为空或者只有一个节点,直接返回。否则先递归处理后面的部分,得到反转后的新头节点newHead。然后关键的两行:head->next->next = head让后一个节点指回当前节点,head->next = NULL断开当前节点原来的指向。

这两行的顺序也不能反。如果先执行head->next = NULL,那head->next->next就找不到原来的后一个节点了。

递归法的空间复杂度是O(n),因为递归调用栈的深度等于链表长度。迭代法是O(1)。时间复杂度两者都是O(n),每个节点处理一次。

对比维度迭代法递归法
时间复杂度O(n)O(n)
空间复杂度O(1)O(n)
代码长度稍长更短
栈溢出风险无链表过长时有
面试推荐度优先备选

4. 实操中容易踩的坑与排查方法

4.1 指针操作顺序错误导致断链

这是最常见的错误,没有之一。我刚开始写的时候也犯过,把curr->next = prev写在next = curr->next前面,结果next拿到的是已经被修改的值,链表从中间断开,后面的节点全部丢失。

排查方法很简单:在纸上画三个格子代表三个指针,每执行一行就更新一次格子的指向,画完一轮循环你就知道哪里错了。不要偷懒在脑子里模拟,链表操作必须画图。

4.2 循环条件写错导致尾节点未处理

有人把while (curr != NULL)写成while (curr->next != NULL),结果最后一个节点没有被反转,它的next还指向倒数第二个节点,形成环或者尾节点没有正确置NULL。

这个错误的排查方法是:用两个节点的链表测试。1 -> 2 -> NULL,如果反转后变成2 -> 1 -> 2 -> ...这种环,那就是循环条件写错了。

4.3 返回值写错

循环结束后应该返回prev,不是curr,也不是head。因为curr已经是NULL了,head现在是尾节点。返回prev才是反转后的新头节点。

这个错误比较隐蔽,因为如果你在LeetCode上提交,返回错误的节点会导致输出完全不对,但代码逻辑看起来没问题。记住一个口诀:循环结束,prev是新头。

4.4 空链表和单节点链表的边界处理

空链表head == NULL的情况,迭代法里curr = head = NULL,循环条件不满足,直接返回prev = NULL,正确。单节点链表1 -> NULL,循环执行一次,prev变成1,curr变成NULL,返回1,也正确。

所以迭代法天然处理了这两种边界情况,不需要额外加if判断。但递归法必须加head == NULL || head->next == NULL的判断,否则会空指针解引用。

提示:写完代码后,至少用三种情况测试——空链表、单节点、三节点以上的正常链表。这三种覆盖了所有边界。

4.5 常见问题速查表

问题现象可能原因排查方法
输出只有第一个节点next保存时机错误检查四行代码顺序
输出形成环循环条件写错用两节点链表测试
输出顺序不对返回值写错确认返回的是prev
程序崩溃空指针解引用检查递归终止条件
尾节点next不为NULL循环少执行一次检查while条件

5. 从反转链表延伸出去的变种题

5.1 反转前K个节点

这是反转链表的直接变种。思路是先找到第K个节点,记录它的next,然后把前K个节点当成一个独立的链表反转,最后把反转后的尾节点接上之前记录的next。

关键点在于:反转之前要先保存第K+1个节点的位置,否则反转之后你就找不到后面了。这个变种题在面试中出现频率很高,因为它同时考察了链表遍历、反转和边界处理。

5.2 K个一组反转链表

这是上一题的升级版,要求每K个节点一组进行反转,最后不足K个的保持原样。思路是先用一个指针走K步,如果走不满K步就直接返回,如果走满了就反转这K个节点,然后递归或迭代处理后面的部分。

这道题的难点在于组与组之间的衔接。反转完一组之后,原来的头节点变成了尾节点,它需要指向下一组反转后的新头节点。所以你需要额外记录上一组的尾节点。

5.3 反转指定区间

给你一个链表和两个整数left和right,要求反转从位置left到位置right的节点。这道题需要你先走到left位置的前一个节点,然后对left到right这一段执行反转,最后把前后接起来。

这道题最容易出错的地方是left等于1的情况,也就是从头节点开始反转。这时候你没有“前一个节点”,需要用一个哑节点(dummy node)来简化处理。哑节点是链表题里的万能技巧,很多边界问题加一个哑节点就迎刃而解。

5.4 两两交换链表中的节点

这是K等于2的特殊情况。可以用迭代法,也可以用递归法。迭代法的思路是每次处理两个节点,交换它们的指向,然后移动到下一对。递归法的思路更简洁,但空间复杂度是O(n)。

这道题在面试中经常作为反转链表的follow-up出现,面试官想看看你能不能把学到的指针操作迁移到新场景。

6. 我刷这道题的一些个人体会

反转链表这道题,我前前后后刷了不下十遍。不是因为我笨,而是因为每次隔一段时间不写,指针操作的细节就会模糊。后来我总结了一个方法:不要背代码,背口诀。

口诀就是八个字:先存后改,先移后判。

“先存后改”说的是在改变curr->next之前,必须先保存curr->next的原始值。“先移后判”说的是在判断循环条件之前,必须先移动指针。这八个字覆盖了迭代法最容易出错的两个点。

另外,我强烈建议你在纸上画图。链表题不像数组题,数组题你在脑子里过一遍就行,链表题必须画图。我见过太多人在面试的时候代码写得飞快,但面试官让他画图解释的时候就开始卡壳。画图不仅能帮你理清思路,还能让面试官看到你的思考过程。

还有一点,C语言写链表题的时候,struct ListNode *这个类型名太长了,写起来很烦。我一般会在函数开头用typedef简化一下,或者直接用ListNode *如果题目已经typedef过了。但要注意,LeetCode的C语言环境里,结构体定义是固定的,你不能改,所以只能在函数内部用局部变量的时候尽量简洁。

最后说一个心态问题。反转链表是链表题的基础,但基础不代表简单。很多人觉得这道题太简单了,刷一遍就过了,结果面试的时候被问到变种题就懵了。我的建议是:把反转链表当成一个模板来练,练到你能在五分钟内写出无bug的迭代版本,然后再去刷变种题。这样你的链表题正确率会有质的提升。

后续如果你想把链表这块彻底吃透,我建议按这个顺序刷:反转链表 -> 反转前K个 -> K个一组反转 -> 反转区间 -> 两两交换 -> 重排链表 -> 合并两个有序链表 -> 合并K个有序链表。这一套下来,链表的基本操作你就全覆盖了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 13:51:23

从零配置Maven环境:避开JDK版本、settings.xml与依赖下载的坑

上周帮A同学折腾新电脑,一天之内踩遍了配置 Maven 环境的所有经典坑:mvn 命令找不到、JAVA_HOME 路径不合法、依赖下载到一半卡死、IDEA 里构建正常但命令行一跑就报错。很多教程把 Maven 配置写成"下载解压、配两个变量、完事",但…

作者头像 李华
网站建设 2026/10/19 13:49:27

open-code-review 实战:构建自动化代码审查流程的经验与踩坑

做技术 Leader 这几年,最让我头疼的不是系统架构,而是一个听起来很基础的事:代码审查。团队从几个人涨到几十人,PR 堆积如山,人工 Review 越看越麻木,低级错误反而更容易漏到线上。后来我在团队里引入了 op…

作者头像 李华
网站建设 2026/10/19 13:47:18

TCP TIME_WAIT 详解:产生机制、排查思路与内核参数调优实践

最近又收到一条凌晨三点的告警:某服务的连接失败率突然飙到 20%,登录后台看了下,ss -ant | grep TIME_WAIT | wc -l一刷就是 6 万多条。这种场景我遇到过不止一次,很多朋友第一反应就是“赶紧把tcp_tw_recycle打开”,其…

作者头像 李华
网站建设 2026/10/19 13:44:40

Seedance 2.0视频生成实战:从提示词工程到可控出片的完整学习路径

2026年的AI视频生成赛道,已经过了“能出片就欢呼”的阶段。真正的分水岭,是你能不能稳定产出自己想要的内容,而不是靠概率抽卡。Seedance 2.0就是在这个背景下被反复讨论的一个模型——我前前后后试过不少视频生成工具,从最早那种…

作者头像 李华
网站建设 2026/10/19 13:43:29

SMP语言作用域与变量解析:同名遮蔽如何让你取错数据?

上周排查了一单SMP语言的作用域问题,过程不算复杂,但绕了差不多两个小时。原因是绑定表达式里的一个变量解析到了行级上下文中的同名字段,明明页面上方大片区域都在使用数据上下文中的“金额”,结果到列表行里取回来的却是另一份数…

作者头像 李华