反转链表 图解题解
反转链表不用新建节点——三个指针 prev/cur/next 协作,每轮把一根箭头掉头,走一遍就反转完毕。
反转链表就像把一串珠子逐颗摘下重新倒着穿,每颗要走四步:①用 next 先记住下一颗(这步必须最先做,一旦动了线就找不到后面了);②把 cur 的线扭向 prev(真正的掉头);③prev 踩到 cur 的位置;④cur 踩到 next 记住的那颗。四步走完,这颗珠子反向穿好,再循环处理下一颗。cur 走到 null,prev 就指着新头;全程只改指针方向,不新建节点,O(1) 空间。
这道题到底在问什么
- 输入
- 1 → 2 → 3 → 4 → 5 → 6 → null
- 输出
- 6 → 5 → 4 → 3 → 2 → 1 → null
最优解:为什么这么做
一句话答案:LeetCode 206 反转链表的最优解是三指针迭代原地反转:prev、cur、next 三个指针沿链走一遍,每个节点先用 next 记住后继,再把指针掉头指向 prev,然后整体右移。循环结束 prev 就是新头。时间 O(n)、空间 O(1),比递归写法省掉 O(n) 的调用栈。
这道题真正在问什么
给单链表的头节点 head,把每个节点的 next 指向全部反过来,返回反转后的新头。例如输入 1→2→3→4→5→6→null,反转后应返回 6→5→4→3→2→1→null。题目要求原地完成、只用 O(1) 额外空间——这一条排除了「把所有值倒进数组再倒序重建一条链」的偷懒做法,逼你直接在原链表上改指针。
为什么不能上来就把指针掉头
单链表的致命约束是:想找到一个节点的后继,只能靠它的 next 指针,没有第二条路。假设当前站在节点 cur,直接把 cur.next 改成指向前面——后面那一整截链表就此失联,再也访问不到了。
这个约束反过来就把解法逼出来了:每一轮改指针之前,必须先用一个临时指针 next 把 cur 的后继保存下来。于是每轮固定三步——记住 next、把 cur.next 掉头指向 prev、prev 和 cur 双双右移一格。整个算法就是这三步的重复,没有任何别的分支。
循环维持的不变量是什么
任意一轮循环开始时都成立这样一个性质:prev 是「已反转部分」的头,从 prev 沿 next 走是一段方向已经掉转的链;cur 起往后是「未处理部分」,还保持原来的方向。两段合起来恰好覆盖所有节点,一个不丢。
每执行一轮上述三步,cur 这一个节点就从「未处理」搬进「已反转」,两段的分界线右移一格,性质原样保持。循环做 n 轮后未处理部分清空,整条链都掉转完毕——正确性不靠背模板,就靠这条不变量一路推到底。
为什么 prev 初始是 null、返回值也是 prev
prev 的初值必须是 null 而不是 head:原来的头节点反转后变成新链表的尾巴,尾巴的 next 应当指向 null。第一轮掉头时 cur.next = prev 恰好把原头指向 null,这个初值正好充当了新尾的终点,一行都不用特判。
循环条件是 cur 非空,退出时 cur 已经走到 null,prev 停在最后一个被处理的节点上——它就是反转后的新头,所以返回 prev 而不是 cur。误返回 cur 是这道题最常见的错误,那样返回值永远是 null。
复杂度怎么算,为什么首选迭代而不是递归
以上讲的正是参考代码采用的迭代解法。时间 O(n):每个节点恰好被访问一次、改一次指针。空间 O(1):全程只有 prev、cur、next 三个指针变量,不随链表长度增长。空链表也不用特判,循环一次都不执行,直接返回 null。
另一条路是递归:递到链表末端把最后一个节点当新头返回,回溯途中逐层把后继的 next 指回自己。思路优雅,但每层递归都占一个栈帧,空间是 O(n),链表很长时还有栈溢出风险,所以迭代版是首选。这套「记后继、掉头、右移」的手法也是反转部分链表(LeetCode 92)和 K 个一组反转(LeetCode 25)的地基。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住这套「记 next → 掉头指 prev → prev/cur 右移」,下面每一轮都在重复它。
- 4prev=null;cur=1开局:prev = null(cur 左边什么都没有),cur 指向第一个节点 1,所有箭头都还朝右。我们要把它们一根根掉头。
- 5cur=1;next=2第 1 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 2——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
- 61.next = null第 1 轮 · 掉头。把 cur(节点 1)的箭头从「指向右边」改成「指向 prev」。节点 1 原来是头,掉头后指向 null,它将成为反转链表的新尾巴。
- 7prev=1;cur=2第 1 轮 · 右移。prev 挪到刚处理完的 1,cur 挪到 下一个 2。进入下一轮,继续掉头。
- 8cur=2;next=3第 2 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 3——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
- 92.next = 1第 2 轮 · 掉头。把 cur(节点 2)的箭头从「指向右边」改成「指向 prev」。现在 2 反过来指向 1 了(箭头变 ←)。
- 10prev=2;cur=3第 2 轮 · 右移。prev 挪到刚处理完的 2,cur 挪到 下一个 3。进入下一轮,继续掉头。
- 11cur=3;next=4第 3 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 4——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
- 123.next = 2第 3 轮 · 掉头。把 cur(节点 3)的箭头从「指向右边」改成「指向 prev」。现在 3 反过来指向 2 了(箭头变 ←)。
- 13prev=3;cur=4第 3 轮 · 右移。prev 挪到刚处理完的 3,cur 挪到 下一个 4。进入下一轮,继续掉头。
- 14cur=4;next=5第 4 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 5——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
- 154.next = 3第 4 轮 · 掉头。把 cur(节点 4)的箭头从「指向右边」改成「指向 prev」。现在 4 反过来指向 3 了(箭头变 ←)。
- 16prev=4;cur=5第 4 轮 · 右移。prev 挪到刚处理完的 4,cur 挪到 下一个 5。进入下一轮,继续掉头。
- 17cur=5;next=6第 5 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 6——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
- 185.next = 4第 5 轮 · 掉头。把 cur(节点 5)的箭头从「指向右边」改成「指向 prev」。现在 5 反过来指向 4 了(箭头变 ←)。
- 19prev=5;cur=6第 5 轮 · 右移。prev 挪到刚处理完的 5,cur 挪到 下一个 6。进入下一轮,继续掉头。
- 20cur=6;next=null第 6 轮 · 先记住 next。cur 的下一个是 null,记下来——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
- 216.next = 5第 6 轮 · 掉头。把 cur(节点 6)的箭头从「指向右边」改成「指向 prev」。现在 6 反过来指向 5 了(箭头变 ←)。
- 22prev=6;cur=null第 6 轮 · 右移。prev 挪到刚处理完的 6,cur 挪到 null(走到头了)。cur 已是 null,循环结束!prev 停在 6,它就是新头。
- 23新头 = 6反转完成!所有箭头都掉成了 ←,链表从 6→5→4→3→2→1 读下来。新头是 6,全程只用了 prev、cur、next 三个指针,没开任何额外数组。
⚠️ 容易写错的地方
✗ 错:先掉头再记 next
✓ 对:必须先用 next 记住下一个,再掉头
一旦 cur.next 被改写,没记的话后面整截链表就丢了
✗ 错:循环结束返回 cur
✓ 对:返回 prev
cur 最后是 null,prev 才停在新头
✗ 错:prev 初始化成 head
✓ 对:prev 初始为 null
新链表的尾节点要指向 null,prev=null 正好充当这个终点
完整代码(Python / Java / C++)
Python
def reverseList(head):
prev = None
cur = head
while cur:
nxt = cur.next # 1) 记住下一个
cur.next = prev # 2) 掉头指向 prev
prev = cur # 3) prev 右移
cur = nxt # cur 右移
return prev # prev 是新头Java
public ListNode reverseList(ListNode head) {
ListNode prev = null, cur = head;
while (cur != null) {
ListNode nxt = cur.next; // 1) 记住下一个
cur.next = prev; // 2) 掉头
prev = cur; // 3) prev 右移
cur = nxt; // cur 右移
}
return prev;
}C++
ListNode* reverseList(ListNode* head) {
ListNode *prev = nullptr, *cur = head;
while (cur) {
ListNode* nxt = cur->next; // 1) 记住下一个
cur->next = prev; // 2) 掉头
prev = cur; // 3) prev 右移
cur = nxt; // cur 右移
}
return prev;
}复杂度
时间
O(n)
每个节点只访问一次,指针走一遍链表
空间
O(1)
只用 prev/cur/next 三个指针,原地反转
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 反转链表 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能用递归反转吗?+
能。递归到底层返回最后一个节点当新头,回溯时执行 head.next.next = head; head.next = null 逐层掉头。思路漂亮但递归栈是 O(n) 空间,不如迭代版省内存。
只反转链表的前 k 个 / 第 m 到 n 个怎么办?+
同一套「记 next→掉头→右移」,只是先走到起点、循环跑 k 次或到第 n 个,再把反转段的头尾和前后接回去(LC92 反转链表 II / LC25 K 个一组)。
怎么判断反转写对了?+
画三个节点手跑一遍,盯着每一步 prev/cur/next 的指向;或反转两次应还原成原链表。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 反转链表 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。