题目描述
思路解析
一句话答案: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),正是进阶要的答案。要留意这个解法有副作用:比较结束后链表的后半段仍处于反转状态。严谨的做法是返回之前把后半段再反转一次复原;面试时主动提到这一点,能体现出「函数不应悄悄改坏输入」的工程意识。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
本题其实是两道基础题的合体:876 快慢找中点 + 206 反转链表。把后半段就地翻过来,再左右对撞比较,全程不开新空间。
① 快慢找中点 · 起步:找中点用快慢指针:slow 一次走 1 格、fast 一次走 2 格,都从头出发。fast 到尽头时,slow 正好停在中点。
① 第 1 轮 · slow 走 1 格:第 1 轮:slow 先稳稳走 1 格到下标 1。接着看 fast 跨两格——拆成两小跳看清楚。
① 第 1 轮 · fast 第 1 跳:fast 的第一小跳:跨到下标 1。
① 第 1 轮 · fast 第 2 跳:fast 的第二小跳:再跨到下标 2。一轮里 slow 走 1、fast 走 2 的2 倍速关系,让 fast 到头时 slow 恰好在中点。
① 第 2 轮 · slow 走 1 格:第 2 轮:slow 先稳稳走 1 格到下标 2。接着看 fast 跨两格——拆成两小跳看清楚。
① 第 2 轮 · fast 第 1 跳:fast 的第一小跳:跨到下标 3。
① 第 2 轮 · fast 第 2 跳:fast 的第二小跳:再跨到下标 4。一轮里 slow 走 1、fast 走 2 的2 倍速关系,让 fast 到头时 slow 恰好在中点。
① 第 3 轮 · slow 走 1 格:第 3 轮:slow 先稳稳走 1 格到下标 3。接着看 fast 跨两格——拆成两小跳看清楚。
① 第 3 轮 · fast 第 1 跳:fast 的第一小跳:跨到下标 5。
① 第 3 轮 · fast 第 2 跳:fast 的第二小跳:再跨到下标 6。一轮里 slow 走 1、fast 走 2 的2 倍速关系,让 fast 到头时 slow 恰好在中点。
② 反转后半段 · 准备:后半段从下标 3 到 6。要把它原地反转(206 那一招):cur 指向当前节点、prev 是已反转部分的新头(一开始为空)。逐个把箭头掉头。
② 反转 · 第 1 步 · 存后继:掉头前先存好后继:nxt = cur.next = 下标 4。不先存,掉头后就找不到下一个节点了(链表题最常见的丢 next 坑)。
② 反转 · 第 1 步 · 断尾:第一节最特殊:cur(下标 3) 反转后 next 指向空,它将成为反转后半的新尾巴。前半与后半在此断开(中间那根连线变 ·)。
② 反转 · 第 2 步 · 存后继:掉头前先存好后继:nxt = cur.next = 下标 5。不先存,掉头后就找不到下一个节点了(链表题最常见的丢 next 坑)。
② 反转 · 第 2 步 · 掉头:把 cur(下标 4) 的箭头掉头指向 prev(下标 3):那根连线变成 ←(反向)。后半段被一节一节翻过来。
② 反转 · 第 3 步 · 存后继:掉头前先存好后继:nxt = cur.next = 下标 6。不先存,掉头后就找不到下一个节点了(链表题最常见的丢 next 坑)。
② 反转 · 第 3 步 · 掉头:把 cur(下标 5) 的箭头掉头指向 prev(下标 4):那根连线变成 ←(反向)。后半段被一节一节翻过来。
② 反转 · 第 4 步 · 存后继:cur 在下标 6,它已经是后半最后一个,nxt 为空。
② 反转 · 第 4 步 · 掉头:把 cur(下标 6) 的箭头掉头指向 prev(下标 5):那根连线变成 ←(反向)。后半段被一节一节翻过来。
② 反转完成:反转结束:prev 落在下标 6,它是反转后半的新头。现在从两端往中间走,能逐一比对前半和「翻过来的后半」。
③ 对撞比较 · 第 1 对:第 1 对:p1=下标 0(值 1) 与 p2=下标 6(值 1) 比较 → 1 = 1,这对相等(变绿),继续往中间。
③ 对撞比较 · 第 2 对:第 2 对:p1=下标 1(值 2) 与 p2=下标 5(值 2) 比较 → 2 = 2,这对相等(变绿),继续往中间。
③ 对撞比较 · 第 3 对:第 3 对:p1=下标 2(值 3) 与 p2=下标 4(值 3) 比较 → 3 = 3,这对相等(变绿),继续往中间。
③ 比较结束:每一对都相等(全绿),是回文,返回 true。只比较了半条链——后半是前半的镜像,比到中点就够了。
边界先看空链表、单节点、奇数长。奇数时中点那一个值不参与比较,这套写法天然处理。
追问重点:O(1) 空间的动机、要不要恢复链表、奇偶长度如何天然处理。
参考代码
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 True复杂度
- 时间:O(n),找中点走半条 + 反转半条 + 比较半条,都是线性
- 空间:O(1),原地反转后半,只用常数个指针;这正是本题进阶要的
易错点
面试追问把动画讲成自己的话
追问为什么要做到 O(1) 空间?
追问比完后链表被改坏了,要不要恢复?
追问奇偶长度怎么天然处理?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
排序链表
LeetCode 148 · 中等 · 沿着 链表 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题