如何判断一个单链表是否为回文链表?
简化版
最优解 O(1) 空间三步走:快慢指针找中点 → 反转后半段 → 从两头向中间逐个比对。前半段从头走、后半段从反转后的尾走,值一路相等就是回文。比完可以再把后半段反转回去还原链表。时间 O(n)、空间 O(1)。
详细版
单链表不能像数组那样从两端往中间夹(没有 prev 往回走),所以要想 O(1) 空间判回文,得先把「后半段反转」制造出一个能从尾往前走的链。
boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) return true;
// 1. 快慢指针找中点(slow 停在前半段末尾/中间)
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next; fast = fast.next.next;
}
// 2. 反转后半段(slow.next 起)
ListNode second = reverse(slow.next);
// 3. 双指针比对
ListNode p1 = head, p2 = second;
boolean ok = true;
while (p2 != null) {
if (p1.val != p2.val) { ok = false; break; }
p1 = p1.next; p2 = p2.next;
}
slow.next = reverse(second); // 可选:还原链表
return ok;
}
ListNode reverse(ListNode h) {
ListNode prev = null;
while (h != null) { ListNode nx = h.next; h.next = prev; prev = h; h = nx; }
return prev;
}
完整版教学
一、为什么不能直接双指针夹
数组判回文很简单:左右两个指针往中间靠,比 a[l] 和 a[r]。但单链表只能从头往后走,没有 prev 指针,右指针没法往左移动。所以必须想办法「制造出一个能反向走的结构」——要么额外存值(O(n) 空间),要么原地反转后半段(O(1) 空间)。
二、朴素解法:借助额外空间
- 拷进数组/列表:遍历链表把值全放进
ArrayList,再用数组的双指针夹着比。O(n) 时间、O(n) 空间,最好写。 - 用栈:把后半段(或全部)压栈,再一边出栈一边和前半段比。栈的 LIFO 天然实现「反向」。
面试能先说这个证明思路清晰,再给出 O(1) 空间的进阶解。
三、O(1) 空间三步法详解
- 找中点:快指针一次走两步、慢指针一步,快指针到尾时慢指针在中间。用
fast.next和fast.next.next做条件能让slow停在「前半段的最后一个节点」,方便把slow.next之后当后半段。 - 反转后半段:标准的链表反转,把后半段变成从原尾节点往回的链。
- 对比:一个指针从
head(前半段头)、一个从反转后的后半段头,同步向中间推进,逐个比值。后半段较短或等长,以p2 != null为界比完即可(奇数长度时正中间那个不用比)。
四、奇偶长度的处理
- 偶数个节点(如 1→2→2→1):前后两半等长,比对到 p2 走完即可。
- 奇数个节点(如 1→2→3→2→1):正中间的 3 是对称轴,不需要比较。用上面的快慢指针写法,中点会被划到前半段,后半段更短,
while (p2 != null)自然跳过中间节点,无需特判。
五、要不要还原链表
反转后半段破坏了原链表结构。如果调用方之后还要用这个链表,应当在比完后再反转一次后半段接回去(代码里 slow.next = reverse(second))。面试中主动提这一点是加分项——说明你考虑到了副作用。
| 解法 | 时间 | 额外空间 | 是否修改链表 |
|---|---|---|---|
| 拷贝到数组后双指针 | O(n) | O(n) | 不修改 |
| 栈保存后半段/全部节点值 | O(n) | O(n) | 不修改 |
| 找中点 + 反转后半段 | O(n) | O(1) | 会修改,最好还原 |
具体例子:1→2→3→2→1 中,快慢指针让 slow 停在 3,反转 slow.next 得到 1→2,然后用 head 的 1→2 与反转后的 1→2 比对。中间的 3 是对称轴,不需要参与比较。
回文链表 O(1) 空间解法的代价是“临时改链”。如果面试官关心副作用,记得说会把后半段再反转接回去。
六、常见误区与追问
- 误区:单链表可以像数组一样左右夹逼。 单链表没有前驱指针,右侧不能往左走,必须借助额外空间或反转后半段。
- 误区:奇数长度必须单独删除中间节点。 用前半段末尾作为 slow 时,后半段从
slow.next开始,中间节点自然跳过。 - 误区:比对时应该循环到两个指针都为空。 后半段长度小于或等于前半段,通常以
p2 != null为准。 - 追问:为什么要还原链表? 如果函数调用者还要继续使用原链表结构,不还原会留下被反转的后半段。
- 追问:能否用递归判断回文? 可以借助递归栈模拟后序比较,但空间仍是 O(n),不如 O(1) 的反转后半段。
- 追问:空链表和单节点是否是回文? 通常认为是回文,因为没有任何一对不相等的对称元素。
七、加强记忆
单链表判回文 O(1) 空间三步:快慢指针找中点 → 反转后半段 → 两头向中间逐个比值(因为单链表不能像数组那样反向夹,得先造出可反向走的后半段)。奇数长度时中间节点自动跳过。比完最好把后半段反转回去还原链表。图省事可拷进数组用双指针,O(n) 空间。