回文链表 图解题解
同一条链表,从头读和从尾读都一样?O(1) 空间的做法把两道经典题拼在了一起。
判断一条珠串是否回文,最省空间的办法:先用快慢指针找到中间那颗,把后半段原地反转成一条新链,然后让两根手指分别从原链头部和反转后半链头部同向往前走,逐颗比对——全对就是回文。整个过程是找中点和反转链表两招的合体,只多用几个临时指针,不另开数组。
这道题到底在问什么
- 输入
- 1→2→3→4→3→2→1
- 输出
- true
- 输入
- 1→2
- 输出
- false
最优解:为什么这么做
一句话答案:LeetCode 234 回文链表的进阶解法分三步:快慢指针找到中点,原地反转后半段,再让前后两半从各自开头同步比较,比完可再反转复原。整体 O(n) 时间、O(1) 空间,避开了把链表倒进数组再对撞的 O(n) 额外内存,本质是 876 找中点与 206 反转链表两道基础题的组合。
判断回文,链表比数组难在哪
回文的定义是正着读和反着读一样。数组版本很好办:左右两个位置向中间对撞比较即可。链表的麻烦在于单向——每个节点只认识自己的 next,没法从尾巴往回走。最省事的做法是把所有值复制进数组再对撞,时间 O(n) 但空间也是 O(n);题目的进阶要求是 O(n) 时间、O(1) 空间,等于禁止复制,逼你在链表本身的指针上做文章。
为什么反转后半段就能原地比较
回文有一个等价说法:把整条链的后半段方向翻转,它应该和前半段逐个相等。顺着这个观察,解法只需三步:先找到中点,再把从中点开始的后半段就地反转——此时原来的尾节点变成了可以正向遍历的头——最后让两个指针分别从原链头和这个新头出发同步比较。单链表不能倒着走的缺陷,被「把后半段翻个方向」绕开了,全程只改指针、不建新节点。
快慢指针为什么恰好停在中点
找中点用快慢指针:slow 每次走一步,fast 每次走两步,fast 到达链尾时 slow 恰好走完一半路程,停在中间。这一步就是 LeetCode 876 链表的中间结点,可以单独拿出来练。更妙的是奇偶长度被统一处理:奇数长度时 slow 停在正中那个节点,它不属于任何一半,天然不参与比较;偶数长度时前后正好各占一半——两种情况共用同一套代码,不需要分支特判。
反转和比较这两步,正确性各在哪里
反转来自 LeetCode 206:遍历后半段,把每个节点的 next 掉头指向前驱。关键是掉头之前必须先用临时变量存住原来的 next,否则指针一改链就断了,后继再也找不回来。
比较阶段以后半段指针走到 null 为终止条件:反转后的后半段长度不超过前半段,它走完就意味着所有成对的位置都比完了,继续多走没有意义;途中任何一对值不相等,立刻返回 false 即可。
复杂度、副作用与面试加分点
三个阶段分别扫链表的一半到一遍,总时间 O(n);只用了常数个指针变量,空间 O(1),正是进阶要的答案。要留意这个解法有副作用:比较结束后链表的后半段仍处于反转状态。严谨的做法是返回之前把后半段再反转一次复原;面试时主动提到这一点,能体现出「函数不应悄悄改坏输入」的工程意识。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3本题其实是两道基础题的合体:876 快慢找中点 + 206 反转链表。把后半段就地翻过来,再左右对撞比较,全程不开新空间。
- 4slow=0, fast=0找中点用快慢指针:slow 一次走 1 格、fast 一次走 2 格,都从头出发。fast 到尽头时,slow 正好停在中点。
- 5slow=1, fast=0第 1 轮:slow 先稳稳走 1 格到下标 1。接着看 fast 跨两格——拆成两小跳看清楚。
- 6slow=1, fast=1fast 的第一小跳:跨到下标 1。
- 7slow=1, fast=2fast 的第二小跳:再跨到下标 2。一轮里 slow 走 1、fast 走 2 的2 倍速关系,让 fast 到头时 slow 恰好在中点。
- 8slow=2, fast=2第 2 轮:slow 先稳稳走 1 格到下标 2。接着看 fast 跨两格——拆成两小跳看清楚。
- 9slow=2, fast=3fast 的第一小跳:跨到下标 3。
- 10slow=2, fast=4fast 的第二小跳:再跨到下标 4。一轮里 slow 走 1、fast 走 2 的2 倍速关系,让 fast 到头时 slow 恰好在中点。
- 11slow=3, fast=4第 3 轮:slow 先稳稳走 1 格到下标 3。接着看 fast 跨两格——拆成两小跳看清楚。
- 12slow=3, fast=5fast 的第一小跳:跨到下标 5。
- 13slow=3, fast=6fast 的第二小跳:再跨到下标 6。一轮里 slow 走 1、fast 走 2 的2 倍速关系,让 fast 到头时 slow 恰好在中点。
- 14cur=3, prev=null后半段从下标 3 到 6。要把它原地反转(206 那一招):cur 指向当前节点、prev 是已反转部分的新头(一开始为空)。逐个把箭头掉头。
- 15cur=3, prev=null, nxt=4掉头前先存好后继:nxt = cur.next = 下标 4。不先存,掉头后就找不到下一个节点了(链表题最常见的丢 next 坑)。
- 16cur=3.next=null第一节最特殊:cur(下标 3) 反转后 next 指向空,它将成为反转后半的新尾巴。前半与后半在此断开(中间那根连线变 ·)。
- 17cur=4, prev=3, nxt=5掉头前先存好后继:nxt = cur.next = 下标 5。不先存,掉头后就找不到下一个节点了(链表题最常见的丢 next 坑)。
- 18cur=4→prev=3把 cur(下标 4) 的箭头掉头指向 prev(下标 3):那根连线变成 ←(反向)。后半段被一节一节翻过来。
- 19cur=5, prev=4, nxt=6掉头前先存好后继:nxt = cur.next = 下标 6。不先存,掉头后就找不到下一个节点了(链表题最常见的丢 next 坑)。
- 20cur=5→prev=4把 cur(下标 5) 的箭头掉头指向 prev(下标 4):那根连线变成 ←(反向)。后半段被一节一节翻过来。
- 21cur=6, prev=5, nxt=nullcur 在下标 6,它已经是后半最后一个,nxt 为空。
- 22cur=6→prev=5把 cur(下标 6) 的箭头掉头指向 prev(下标 5):那根连线变成 ←(反向)。后半段被一节一节翻过来。
- 23后半新头 = 下标 6反转结束:prev 落在下标 6,它是反转后半的新头。现在从两端往中间走,能逐一比对前半和「翻过来的后半」。
- 24cmp(0,6): 1 vs 1 → 相等第 1 对:p1=下标 0(值 1) 与 p2=下标 6(值 1) 比较 → 1 = 1,这对相等(变绿),继续往中间。
- 25cmp(1,5): 2 vs 2 → 相等第 2 对:p1=下标 1(值 2) 与 p2=下标 5(值 2) 比较 → 2 = 2,这对相等(变绿),继续往中间。
- 26cmp(2,4): 3 vs 3 → 相等第 3 对:p1=下标 2(值 3) 与 p2=下标 4(值 3) 比较 → 3 = 3,这对相等(变绿),继续往中间。
- 27回文 = true每一对都相等(全绿),是回文,返回 true。只比较了半条链——后半是前半的镜像,比到中点就够了。
⚠️ 容易写错的地方
✗ 错:反转时直接 cur.next=prev
✓ 对:先用 nxt 存好 cur.next 再掉头
不存后继,掉头后整条链就断了,找不到下一个
✗ 错:比较走满整条链
✓ 对:后半比完(p2 到头)就结束
后半是前半的镜像,比到中点即可;多走会重复甚至越界
✗ 错:三步顺序乱来
✓ 对:严格「找中点 → 反转后半 → 比较」
没先定位中点就反转,会把整条链都翻掉
完整代码(Python / Java / JavaScript / C++)
Python
def isPalindrome(head):
# ① 快慢找中点
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# ② 从 slow 起反转后半
prev = None
cur = slow
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
# ③ 首尾对撞比较
p1, p2 = head, prev
while p2:
if p1.val != p2.val:
return False
p1 = p1.next
p2 = p2.next
return TrueJava
class Solution {
public boolean isPalindrome(ListNode head) {
// ① 快慢找中点
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// ② 从 slow 起反转后半
ListNode prev = null, cur = slow;
while (cur != null) {
ListNode nxt = cur.next;
cur.next = prev;
prev = cur;
cur = nxt;
}
// ③ 首尾对撞比较
ListNode p1 = head, p2 = prev;
while (p2 != null) {
if (p1.val != p2.val) return false;
p1 = p1.next;
p2 = p2.next;
}
return true;
}
}JavaScript
function isPalindrome(head) {
// ① 快慢找中点
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
// ② 从 slow 起反转后半
let prev = null, cur = slow;
while (cur) {
const nxt = cur.next;
cur.next = prev;
prev = cur;
cur = nxt;
}
// ③ 首尾对撞比较
let p1 = head, p2 = prev;
while (p2) {
if (p1.val !== p2.val) return false;
p1 = p1.next;
p2 = p2.next;
}
return true;
}C++
bool isPalindrome(ListNode* head) {
// ① 快慢找中点
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
// ② 从 slow 起反转后半
ListNode *prev = nullptr, *cur = slow;
while (cur) {
ListNode* nxt = cur->next;
cur->next = prev;
prev = cur;
cur = nxt;
}
// ③ 首尾对撞比较
ListNode *p1 = head, *p2 = prev;
while (p2) {
if (p1->val != p2->val) return false;
p1 = p1->next;
p2 = p2->next;
}
return true;
}复杂度
时间
O(n)
找中点走半条 + 反转半条 + 比较半条,都是线性
空间
O(1)
原地反转后半,只用常数个指针;这正是本题进阶要的
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 回文链表 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么要做到 O(1) 空间?+
链表可能很长,倒进数组要 O(n) 内存。原地反转后半段不额外开空间,是面试想考的进阶点。
比完后链表被改坏了,要不要恢复?+
严谨做法是再把后半反转回去复原。面试可主动提一句,体现对副作用的意识。
奇偶长度怎么天然处理?+
快慢指针让 slow 停在中点,奇数时中间那个值不参与比较,偶数时正好前后各半,写法统一。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 回文链表 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。