当前:LC206 · 反转链表:如何不丢掉后半段链表 · 首次出现于 Day 9 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC206 · Reverse Linked List · 链表

反转链表:把箭头逐个掉头

单链表只记得后继,不记得前驱。拧 Next 之前必须先把后继藏进口袋,否则整条链断在第一个节点。

迭代维护 prev、cur、nxt:每轮先把 cur.Next 暂存到 nxt,再把 cur.Next 指回已经反转好的 prev,然后 prev 跟上 cur、cur 跳到 nxt。cur 走空时,prev 停在原尾巴上,那就是新头。时间 O(n),空间 O(1)。

时间 O(n)空间 O(1)结论先行 · 全文约 6 节
导读

这是 LeetCode 206. Reverse Linked List。大白话:给你一条单链表的头节点 head,把整条链原地反转,返回反转后的新头。每个节点只有一个数字 Val 和一根指向后继的 Next,最后一个节点的 Next 是 nil。你只能顺着 Next 往前走,不能回头。

主例:1 → 2 → 3 → 4 → 5,要变成 5 → 4 → 3 → 2 → 1。空链表反转后仍是空;只有一个节点时,反转后还是它自己。题目不要求新建节点,改的是已有节点之间的指向。

看见「反转」,手会去开一个数组,扫进去再倒着建新链。结果对,但多占一份空间,也没有回答「指针怎么改」。更危险的直觉是站在 1 上直接写「让 1 指向空」。2 是靠 1.Next 找到的,先改 1.Next,后面全丢。缺的是前驱和后继同时在手里。

直觉陷阱:改箭头会断链

第一反应:把 1 → 2 变成 2 → 1 不就行了吗?方向听起来对,执行顺序却是错的。节点 1 的 Next 一旦改成 nil,手里就再也没有通往 2 的指针。节点还在内存里,但整条链断在第一个节点,3、4、5 一起失踪。

所以这道题真正难的不是「方向要反过来」这句话,而是改指针时不能把还没处理的后继弄丢。要把当前节点拧向左边,你需要知道左边是谁,这个角色叫前驱 prev;拧完还要继续处理右边,必须事先把右边是谁记下来,这个角色叫后继 nxt;正在拧的节点自己是 cur。

三个角色缺一个都不行。没有 prev,cur 不知道该指向谁;没有 nxt,执行 cur.Next = prev 之后,还没处理的链就丢了;没有 cur,你不知道此刻在拧哪一个。空链表没有节点可拧,新头就是 nil;单节点的 Next 本来就是 nil,拧完还是它。这两种输入必须落在同一套循环里,而不是靠额外的 if 硬补。

断链改 Next 之前先暂存下一个节点,这是链表题的铁律。先拧再读 cur.Next,读到的已经是左边,不是右边。
直接改 1.next 会丢掉 2
1
2
3
prev = nilcur = 1nxt = 21.next 指向 nil,2 就丢了

三指针滚动:prev 保持已反转部分

从断链事故反推步骤。初始化 prev = nil,cur = head。每一轮只处理 cur 这一个节点,而且必须按这个顺序做三步,一步都不能调换。第一步 nxt = cur.Next,先把后继藏进口袋,这一步不改任何指针。第二步 cur.Next = prev,把当前节点拧向已经反转好的那一截。第三步 prev = cur,cur = nxt,已反转的头更新为刚才拧完的节点,cur 走到口袋里的后继。

对第一个节点来说,prev 一开始是 nil,所以原头会被拧成新尾巴,这正是我们要的。对后面的节点来说,prev 已经是上一轮拧完的节点,当前节点接到它后面,已反转的那一截就变长一格。循环条件是 cur != nil。cur 变成 nil,说明原链上每个节点都已经拧过,此时 prev 停在原链最后一个被处理的节点上,也就是新头。返回 prev,不要返回原来的 head:原来的头现在是尾巴,它的 Next 已经是 nil。

主例 1 → 2 → 3 → 4 → 5。开始时 prev 是空,cur 在 1。第 1 轮:nxt 拿到 2,1.Next 从指向 2 改成指向 nil,prev 落到 1,cur 落到 2;已反转截是 nil ← 1,还未处理是 2 → 3 → 4 → 5。第 2 轮:nxt 拿到 3,2.Next 改成指向 1,已反转截变成 nil ← 1 ← 2。第 3、4 轮同样把 3、4 撕下来接到左边。第 5 轮 cur 是 5,nxt 是 nil,5.Next 指向 4,prev 变成 5,cur 变成 nil。循环结束,返回 5,新链是 5 → 4 → 3 → 2 → 1。

假如第 1 轮先写 1.Next = nil,再去读 1.Next 当后继,读到的是空,循环立刻结束,返回的新头是 1,后面全部失踪。三步的顺序是从这场事故里反推出来的。任意时刻左边一截已经全部反向,右边一截还保持原方向,cur 是右截的头,prev 是左截的头;右截撕空,左截就是整条反转后的链。空链表一次循环都不进,返回 nil;单节点只走一轮,自己既当新头也当新尾巴。

不变量循环的任意时刻:prev 是已反转那一截的头,cur 是还未处理那一截的头。每一轮只把 cur 从右截撕下来接到左截前面。
三指针逐节点反转 next
1
2
3
4
5
prev = nilcur = 1nxt = 2初始:prev=nil, cur=1

Go:迭代反转

solution.goGo
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
cur := head
for cur != nil {
nxt := cur.Next
cur.Next = prev
prev = cur
cur = nxt
}
return prev
}

1prev 初始为 nil,表示已经反转好的那一截还是空的;cur 从原头开始。

2nxt := cur.Next 必须写在改 cur.Next 之前。它是整道题里唯一防止断链的动作。

3cur.Next = prev 才是「反转」本身。前面的暂存、后面的挪指针,都是为了让这一句安全发生。

4prev = cur 再 cur = nxt:左截的头往前挪一格,右截的头换成口袋里的后继。

5循环结束不要返回 head。head 现在是尾巴,prev 才是新头。空链表一次都不进,返回的 prev 仍是 nil。

总结

拧 Next 之前先把后继藏进口袋;走空之后,prev 才是新头。

  • 先改 Next 再找后继,是断链的标准写法。三步顺序「暂存、拧向、前移」一步都不能调换。
  • prev 永远指向已反转部分的新头,cur 是还未处理的首个节点。返回 prev,不要返回原来的 head。
  • 空链表和单节点不必单独写:cur 一开始是 nil 或只走一轮,同一套循环就对了。
同族题目
LC92反转链表 IILC234回文链表(快慢+反转)LC25K 个一组翻转链表