LeetCode 234. 回文链表
题目描述


题意分析
判断链表节点值组成的序列是否为回文,也就是从头向后读和从尾向前读完全相同。比较的是对称位置的值,不是节点对象是否相同;奇数长度时,中间节点不需要与其他节点配对。
单链表无法通过当前节点直接走向前驱。题目的进阶要求是线性时间、常数额外空间,因此不能把所有值复制到数组或压入栈后再倒序比较。下面的实现会暂时调整后半段指针,并在返回前恢复链表。
解法:快慢指针 + 反转后半链表
核心思路
[!blue]
回文需要比较第一项与最后一项、第二项与倒数第二项,依次向中间靠近。只把后半段反转,就能让它按原链表从尾向前的顺序出现;随后两个指针都沿
next向前移动,就可以完成首尾配对。先用快慢指针找分界。两者从头出发,快指针每次走两步,但仅在它后面仍有两个节点时继续。这使偶数长度时
slow停在前半段尾部,奇数长度时停在中间节点。统一从slow.next开始反转,就恰好取到需要配对的后半段,奇数长度的中点自然被排除。反转时,用
previous表示已经反好的部分,用current表示尚未处理的节点。先保存原后继,再把当前节点指向previous,最后推进两个指针,这样不会丢失未处理部分。让
first从原头节点出发,second从反转后的头节点出发,逐对比较值。比较次数以后半段为准:它恰好包含所有需要配对的节点;如果都相等就是回文,只要一对不同就可以确定失败。另外保存后半段的头节点
secondHead,比较完后再次反转并接回slow.next。即使发现不等,也只记录失败并退出比较,恢复之后才返回,确保两种结果都不留下链表结构变化。
解题步骤
- 空链表或只有一个节点时直接返回
true。- 从头启动快慢指针,在
fast.next和fast.next.next均存在时移动,找到约定的分界节点slow。- 反转
slow.next开始的后半段,并保存反转后的头节点secondHead。- 从原链头和
secondHead同时前进,直到后半段结束;遇到值不相等就记录失败并结束比较。- 用
secondHead再次反转后半段,将结果接回slow.next,最后返回记录的比较结果。
代码实现
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head;
ListNode fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 从中点之后反转,奇数长度的中间节点不参与配对。
ListNode secondHead = reverse(slow.next);
ListNode first = head;
ListNode second = secondHead;
boolean palindrome = true;
while (second != null) {
if (first.val != second.val) {
palindrome = false;
break;
}
first = first.next;
second = second.next;
}
// 成功或失败都要恢复后半段,再返回比较结果。
slow.next = reverse(secondHead);
return palindrome;
}
private ListNode reverse(ListNode head) {
ListNode previous = null;
ListNode current = head;
while (current != null) {
ListNode next = current.next;
current.next = previous;
previous = current;
current = next;
}
return previous;
}
}
func isPalindrome(head *ListNode) bool {
if head == nil || head.Next == nil {
return true
}
slow, fast := head, head
for fast.Next != nil && fast.Next.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
// 从中点之后反转,奇数长度的中间节点不参与配对。
secondHead := reverseList(slow.Next)
first, second := head, secondHead
palindrome := true
for second != nil {
if first.Val != second.Val {
palindrome = false
break
}
first = first.Next
second = second.Next
}
// 成功或失败都要恢复后半段,再返回比较结果。
slow.Next = reverseList(secondHead)
return palindrome
}
func reverseList(head *ListNode) *ListNode {
var previous *ListNode
current := head
for current != nil {
next := current.Next
current.Next = previous
previous = current
current = next
}
return previous
}
复杂度分析
- 时间复杂度:$O(n)$,找中点、反转、比较和恢复只是常数次线性遍历。
- 空间复杂度:$O(1)$。只使用固定数量的指针和一个布尔变量。
关键点总结
[!green]
- 反转后半段只是为了改变遍历顺序,让单链表也能按首尾对应关系比较。
- 中点定位、反转起点与比较长度必须配套,才能同时覆盖奇偶长度。
- 保存后半段入口,并让恢复操作位于所有比较结果之后,避免提前返回破坏输入。
易错点总结
[!yellow]
- 改用
fast != null && fast.next != null找中点,却仍从slow.next反转,会在偶数长度时多跳过一个节点,漏掉一组比较。- 反转时先改后继、后保存原后继,会丢失尚未处理的链表部分。
- 比较节点引用而不是节点值,会把数值相等的对称位置误判为不同。
- 以第一段走到末尾作为比较终点,会在奇数长度时继续访问后半段的空指针;应以后半段是否结束为准。
- 不相等时直接返回,会跳过恢复;拿已经移动过的
second去恢复,也会丢失后半段入口。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 2130. 链表最大孪生和 | 中等 | 同样把链表首尾位置对齐,原题求孪生和最大值,本题比较对应值是否相等。 |
| 206. 反转链表 | 简单 | 原地反转后半段是O1空间比较的关键子过程。 |
| 143. 重排链表 | 中等 | 拆分链表后反转并重新连接;本题反转后半段后比较对称值,该题从中点拆分并交替合并首尾。 |
| 92. 反转链表 II | 中等 | 拆分链表后反转并重新连接;本题反转后半段后比较对称值,该题只反转指定区间。 |
| 25. K 个一组翻转链表 | 困难 | 拆分链表后反转并重新连接;本题反转后半段后比较对称值,该题按固定长度分组反转。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!