题目描述
思路解析
一句话答案: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)的地基。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这套「记 next → 掉头指 prev → prev/cur 右移」,下面每一轮都在重复它。
准备 · prev=null, cur=节点1:开局:prev = null(cur 左边什么都没有),cur 指向第一个节点 1,所有箭头都还朝右。我们要把它们一根根掉头。
第 1 轮 · 记 next:第 1 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 2——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
第 1 轮 · 掉头指 prev:第 1 轮 · 掉头。把 cur(节点 1)的箭头从「指向右边」改成「指向 prev」。节点 1 原来是头,掉头后指向 null,它将成为反转链表的新尾巴。
第 1 轮 · prev/cur 右移:第 1 轮 · 右移。prev 挪到刚处理完的 1,cur 挪到 下一个 2。进入下一轮,继续掉头。
第 2 轮 · 记 next:第 2 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 3——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
第 2 轮 · 掉头指 prev:第 2 轮 · 掉头。把 cur(节点 2)的箭头从「指向右边」改成「指向 prev」。现在 2 反过来指向 1 了(箭头变 ←)。
第 2 轮 · prev/cur 右移:第 2 轮 · 右移。prev 挪到刚处理完的 2,cur 挪到 下一个 3。进入下一轮,继续掉头。
第 3 轮 · 记 next:第 3 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 4——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
第 3 轮 · 掉头指 prev:第 3 轮 · 掉头。把 cur(节点 3)的箭头从「指向右边」改成「指向 prev」。现在 3 反过来指向 2 了(箭头变 ←)。
第 3 轮 · prev/cur 右移:第 3 轮 · 右移。prev 挪到刚处理完的 3,cur 挪到 下一个 4。进入下一轮,继续掉头。
第 4 轮 · 记 next:第 4 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 5——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
第 4 轮 · 掉头指 prev:第 4 轮 · 掉头。把 cur(节点 4)的箭头从「指向右边」改成「指向 prev」。现在 4 反过来指向 3 了(箭头变 ←)。
第 4 轮 · prev/cur 右移:第 4 轮 · 右移。prev 挪到刚处理完的 4,cur 挪到 下一个 5。进入下一轮,继续掉头。
第 5 轮 · 记 next:第 5 轮 · 先记住 next。用 next 指针记下 cur 的下一个节点 6——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
第 5 轮 · 掉头指 prev:第 5 轮 · 掉头。把 cur(节点 5)的箭头从「指向右边」改成「指向 prev」。现在 5 反过来指向 4 了(箭头变 ←)。
第 5 轮 · prev/cur 右移:第 5 轮 · 右移。prev 挪到刚处理完的 5,cur 挪到 下一个 6。进入下一轮,继续掉头。
第 6 轮 · 记 next:第 6 轮 · 先记住 next。cur 的下一个是 null,记下来——因为下一步要把 cur 的箭头掉头,不先记住就再也找不到后面那截了。
第 6 轮 · 掉头指 prev:第 6 轮 · 掉头。把 cur(节点 6)的箭头从「指向右边」改成「指向 prev」。现在 6 反过来指向 5 了(箭头变 ←)。
第 6 轮 · prev/cur 右移:第 6 轮 · 右移。prev 挪到刚处理完的 6,cur 挪到 null(走到头了)。cur 已是 null,循环结束!prev 停在 6,它就是新头。
反转完成:反转完成!所有箭头都掉成了 ←,链表从 6→5→4→3→2→1 读下来。新头是 6,全程只用了 prev、cur、next 三个指针,没开任何额外数组。
边界先想清:空链表和单节点都靠「prev 初始为 null + 返回 prev」自然处理,不用特判。
三个高频追问:递归写法、区间/分组反转的变体、以及怎么自测正确性。
参考代码
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 是新头复杂度
- 时间:O(n),每个节点只访问一次,指针走一遍链表
- 空间:O(1),只用 prev/cur/next 三个指针,原地反转
易错点
面试追问把动画讲成自己的话
追问能用递归反转吗?
追问只反转链表的前 k 个 / 第 m 到 n 个怎么办?
追问怎么判断反转写对了?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
合并两个有序链表
LeetCode 21 · 简单 · 沿着 链表 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题