反转链表:把箭头逐个掉头
单链表只记得后继,不记得前驱。拧 Next 之前必须先把后继藏进口袋,否则整条链断在第一个节点。
迭代维护 prev、cur、nxt:每轮先把 cur.Next 暂存到 nxt,再把 cur.Next 指回已经反转好的 prev,然后 prev 跟上 cur、cur 跳到 nxt。cur 走空时,prev 停在原尾巴上,那就是新头。时间 O(n),空间 O(1)。
这是 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,读到的已经是左边,不是右边。
三指针滚动: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 从右截撕下来接到左截前面。
Go:迭代反转
func reverseList(head *ListNode) *ListNode {var prev *ListNodecur := headfor cur != nil {nxt := cur.Nextcur.Next = prevprev = curcur = 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 或只走一轮,同一套循环就对了。