Q206 反转链表
题目:给你单链表的头节点
head,请你反转链表,并返回反转后的链表。输入:1 → 2 → 3 → 4 → 5,输出:5 → 4 → 3 → 2 → 1。
1 简要回答
递归版把问题拆成两步:
| |
- 每次递归返回:反转后链表的头节点(也就是原链表的尾节点,全程不变)。
- 自己这一层只做两件事:
head->next->next = head(把原后继反过来指向自己)、head->next = nullptr(断开旧指针,防止成环)。 - 时间复杂度 $O(n)$,空间复杂度 $O(n)$(递归栈深度)。
2 递归三步法:把它当成数学归纳法
- 基例:空链表或只有一个节点,反转后还是它自己,直接返回。
- 假设子问题已解决:
reverseList(head->next)返回的last,就是head后面那段链表反转后的新头。 - 只处理自己:把
head挂到新链表末尾,并断开原来的next。
关键是第 2 步的"信任"。递归不是让你跟踪每一层调用,而是假设"更短的链表已经被别人正确反转好了"。
| |
2.1 为什么 head->next->next = head 就完成了"挂到末尾"
递归返回后,head->next 仍然是原来的后继节点,但它已经变成反转后子链表的最后一个节点:
| |
所以 head->next->next = head 的意思是:“让子链表的尾巴再指向我”,于是 head 就被接到了末尾。
2.2 完整回溯过程(1 → 2 → 3 → 4)
递归先一路走到 4,返回 4(基例)。然后从 3 开始逐层往回处理:
| 层 | 执行前 | 执行 head->next->next = head | 再执行 head->next = nullptr | 返回 |
|---|---|---|---|---|
| head=3 | 1→2→3→4→∅ | 1→2→3→4→3(暂时成环) | 1→2→3→∅,4→3 | 4 |
| head=2 | 1→2→3→∅,4→3 | 1→2→3→2(暂时成环) | 1→2→∅,4→3→2 | 4 |
| head=1 | 1→2→∅,4→3→2 | 1→2→1(暂时成环) | 1→∅,4→3→2→1 | 4 |
最终:4→3→2→1→∅,返回的 last 一直是 4。
关键直觉:每个节点在回溯时只做一件小事——“让后面的节点指回我,然后我断掉旧指针”。所有节点都做完,整条链就反过来了。
3 迭代版(对照组)
| |
迭代版是"从前往后"逐条改箭头;递归版是"从后往前"逐层改箭头,顺序正好相反。
4 双指针改递归:不靠想象,靠机械改写
如果"信任"递归有困难,别急着理解经典版。把迭代版机械改写成递归,两者完全等价,每一步都摊在明面上。
改写只有四条规则:
| 迭代里的东西 | 改写成 |
|---|---|
循环变量 pre、cur | 函数参数 reverse(pre, cur) |
循环条件 cur != nullptr | 开头 if (cur == nullptr) return pre; |
| 循环体 | 原样搬进函数体 |
循环末尾 pre = cur; cur = temp; | 递归调用 return reverse(cur, temp); |
| |
把它展开看,就是一串循环:
| |
最后一层 cur == NULL 返回 pre(就是 4),一路原样传回,和迭代版 return pre 完全一致。
信任的本质:递归不需要你"想象"全过程,只需要验证两件事——基例对不对(空链表或最后一个节点返回正确),当前层在子问题正确的假设下对不对。两个都验证过,递归就永远正确。
经典版里
reverseList(head->next)返回的last,就是"循环跑完时的pre",全程不变。
5 虚拟头 + 头插法:把每个节点变成同一种节点
加一个虚拟头 dummy,每次把当前节点插到 dummy 后面(头插),所有节点的操作完全一样,不用特判头节点:
| |
用 1→2→3 走一遍:
| |
和 pre = nullptr 的写法是同一个思想:nullptr/虚拟头让"第一个节点"也被统一处理。这套头插法也是反转区间、K 个一组反转的基础。
易错:② 让新节点先接住"当前最前面的节点"(已反转的那段链),③ 才把新节点放到最前面。顺序不能反:先 ③ 后 ② 会让新节点的
next指向自己,成环。规则和"改指针前先保存/接好后面的链"是同一个——先接后面,再改前面。
6 易错点
- 忘记
head->next = nullptr:1→2→1会成环,遍历直接死循环。head->next->next = head;和head->next = nullptr;必须成对出现。 - 返回了
head而不是last:会得到"局部反转 + 后半段丢失"的错链。 - 基例只写
head == nullptr不够:单节点链表也要直接返回,否则head->next->next会解引用空指针。 - 链表很长(如 $10^5$ 级)时递归会爆栈,工程上优先用迭代。
6.1 遇到 AddressSanitizer: heap-use-after-free 怎么定位
LeetCode 判题器跑完你的代码后会释放链表(freeList)。如果反转后的链表里有环,freeList 会先删掉某个节点,稍后又从环的另一条路走到这个已经删除的节点——于是报 heap-use-after-free(读到了已释放的内存)。
读报错三步:
- 看错误类型和位置:
heap-use-after-free+ 出错函数是freeList→ 不是你的代码当场崩,而是返回的链表结构坏了。 - 想
freeList怎么写的:while (head) { next = head->next; delete head; head = next; }——它会读head->next,只有链表成环(或一个节点被两条链指着)才会读到已删除节点的next。 - 回代码找"谁可能制造环":反转链表里制造反向指针的是
head->next->next = head,如果不同时head->next = nullptr,就成了1→2→1的环。
细节:报错里的
16-byte region正好是一个ListNode(val+next各 8 字节),READ of size 8读的正是next指针。这些信息都在印证:freeList 走到一个被删过的节点上了。
自查方法:本地打印反转后的链表,最多走 $n+1$ 步;如果没遇到 nullptr 还在走,就是环。