副标题 / 摘要
回文链表的核心是“对称比较”,但单链表不能从尾部往前走。最稳的工程化解法是:快慢指针找中点 -> 原地反转后半段 -> 比较 -> 再反转恢复结构,做到 O(n) 时间、O(1) 额外空间且不破坏链表。
- 预计阅读时长:10~14 分钟
- 标签:
Hot100、链表、快慢指针、原地反转 - SEO 关键词:回文链表, Palindrome Linked List, O(1) 空间, 快慢指针, 反转后半段, LeetCode 234
- 元描述:快慢指针定位中点,反转后半段与前半段逐一比较,最后恢复链表结构;O(n)/O(1) 判断单链表是否回文。
A — Algorithm(题目与算法)
题目还原
给你一个单链表的头节点 head,请你判断该链表是否为回文链表:
如果是回文,返回 true;否则返回 false。
输入输出
| 名称 | 类型 | 描述 |
|---|---|---|
| head | ListNode | 单链表头结点 |
| 返回 | bool | 是否为回文 |
示例 1
输入: 1 -> 2 -> 2 -> 1
输出: true
示例 2
输入: 1 -> 2
输出: false
目标读者
- 刷 Hot100,想掌握“链表中点 + 原地反转”组合拳的学习者
- 面试中经常遇到“回文/对称/镜像”类题的开发者
- 关注空间效率、且需要保证数据结构不被破坏的工程实践者
背景 / 动机
在数组里判断回文很简单:左右指针向中间收缩即可。
但在单链表里,你只能顺着 next 单向走,无法从尾部回看,这就让“对称比较”变得不那么直接。
工程上常见的约束也与题目一致:
- 结构不能改(不能改值、不能打标记、不能改 next 永久化)
- 额外内存有限(不想把所有节点拷贝到数组里)
因此我们需要一个 线性时间、常数空间、且能恢复结构 的模板解法。
核心概念
| 概念 | 含义 | 作用 |
|---|---|---|
| 回文 | 从左到右与从右到左相同 | 需要做“对称比较” |
| 快慢指针 | fast 每次两步、slow 每次一步 | O(n) 找到链表中点 |
| 原地反转 | 改指针方向把链表片段反转 | 把“后半段”变成可从前往后比较 |
| 结构恢复 | 比较完成后再反转回去并接回 | 满足“链表保持原结构”要求 |
C — Concepts(核心思想)
思路是怎么推出来的
Step 1:先看真正的困难,而不是“回文”两个字
例如:
1 -> 2 -> 3 -> 2 -> 1
判断回文,本质是在比:
- 第一个和最后一个
- 第二个和倒数第二个
但单链表只能往前走,不能往后退。 这才是这题最核心的障碍。
Step 2:先承认最直接的正确解
最容易写对的方法当然是:
- 把链表值拷到数组
- 用左右双指针判断回文
这个做法很稳,但额外空间是 O(n)。
所以真正的问题变成:
能不能不借助数组,也把“后半段倒着看”这件事做出来?
Step 3:什么变换能把“倒着比”改成“正着比”?
如果把后半段原地反转,那么左右两边就都能从前往后读。
例如:
1 -> 2 -> 3 -> 2 -> 1
把后半段反转后,可比较的两端会变成:
左侧正向:1 -> 2 -> 3
右侧正向:1 -> 2
这样就能像数组一样做对称比较了。
Step 4:先找清楚“后半段”从哪里开始
要反转后半段,先得知道中点在哪里。 快慢指针正好适合这件事:
fast = head
slow = head
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next
循环结束后:
- 奇数长度时,
slow在正中间 - 偶数长度时,
slow在左半段末尾
所以 slow.next 就是后半段起点。
Step 5:只反转后半段,不碰前半段
接着做:
second_half_start = reverse_list(first_half_end.next)
这一句的意义不是“已经判断完了”,而是把原本只能倒着看的后半段,变成可以正着扫描的链。
Step 6:比较完以后还要恢复
然后用两个指针:
p1从头结点开始p2从反转后的后半段开始
逐个比较即可。
比较结束后,再把后半段反转回去并接回原链表。 这一点在工程里很重要,因为调用方往往默认链表结构不会被永久改坏。
Step 7:慢速走一个例子
看:
1 -> 2 -> 2 -> 1
前半段末尾是第一个 2。
把后半段 2 -> 1 反转后,比较链就变成:
前半:1 -> 2
后半:1 -> 2
逐项比较都相等,于是返回 True,最后把后半段再翻回去。
Step 8:把方法压缩成一句模板
234 题可以记成:找中点,反转后半段,把“倒着比”改成“正着比”,比较后再恢复原链表。
Assemble the Full Code
from __future__ import annotations
class ListNode:
def __init__(self, val: int):
self.val = val
self.next: ListNode | None = None
def reverse_list(head: ListNode | None) -> ListNode | None:
prev = None
cur = head
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev
def end_of_first_half(head: ListNode) -> ListNode:
fast = head
slow = head
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next # type: ignore[assignment]
return slow
def is_palindrome(head: ListNode | None) -> bool:
if head is None or head.next is None:
return True
first_half_end = end_of_first_half(head)
second_half_start = reverse_list(first_half_end.next)
p1 = head
p2 = second_half_start
ok = True
while ok and p2 is not None:
if p1.val != p2.val:
ok = False
p1 = p1.next # type: ignore[assignment]
p2 = p2.next
# Restore list.
first_half_end.next = reverse_list(second_half_start)
return ok
def build(vals):
dummy = ListNode(0)
cur = dummy
for v in vals:
cur.next = ListNode(v)
cur = cur.next
return dummy.next
if __name__ == "__main__":
a = build([1, 2, 2, 1])
b = build([1, 2])
print(is_palindrome(a)) # True
print(is_palindrome(b)) # False
Reference Answer
from __future__ import annotations
class ListNode:
def __init__(self, val: int):
self.val = val
self.next: ListNode | None = None
def reverse_list(head: ListNode | None) -> ListNode | None:
prev = None
cur = head
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev
def end_of_first_half(head: ListNode) -> ListNode:
fast = head
slow = head
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next # type: ignore[assignment]
return slow
def is_palindrome(head: ListNode | None) -> bool:
if head is None or head.next is None:
return True
first_end = end_of_first_half(head)
second = reverse_list(first_end.next)
ok = True
p1 = head
p2 = second
while ok and p2 is not None:
if p1.val != p2.val:
ok = False
p1 = p1.next # type: ignore[assignment]
p2 = p2.next
first_end.next = reverse_list(second) # restore
return ok
方法归类
- 快慢指针找中点(Two pointers / Tortoise-Hare)
- 链表原地反转(In-place Reverse)
- 结构恢复(Restore after temporary mutation)
如何稳定处理奇偶长度?
一种非常稳的写法是先求“前半段末尾”:
- 若长度为奇数:前半段末尾是正中间节点(中点不参与比较)
- 若长度为偶数:前半段末尾是左中点
然后反转 first_half_end.next 作为后半段的头。
比较时只需要遍历后半段长度即可(后半段长度 <= 前半段长度)。
实践指南 / 步骤
- 若链表为空或只有 1 个节点,直接返回 true
- 用快慢指针找到前半段末尾
first_half_end - 原地反转
first_half_end.next,得到second_half_start - 用两个指针从
head与second_half_start同步比较 - 比较结束后,再反转
second_half_start并接回first_half_end.next(恢复结构) - 返回比较结果
Python 可运行示例(保存为 palindrome_list.py):
from __future__ import annotations
class ListNode:
def __init__(self, val: int):
self.val = val
self.next: ListNode | None = None
def reverse_list(head: ListNode | None) -> ListNode | None:
prev = None
cur = head
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev
def end_of_first_half(head: ListNode) -> ListNode:
fast = head
slow = head
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next # type: ignore[assignment]
return slow
def is_palindrome(head: ListNode | None) -> bool:
if head is None or head.next is None:
return True
first_half_end = end_of_first_half(head)
second_half_start = reverse_list(first_half_end.next)
p1 = head
p2 = second_half_start
ok = True
while ok and p2 is not None:
if p1.val != p2.val:
ok = False
p1 = p1.next # type: ignore[assignment]
p2 = p2.next
# Restore list.
first_half_end.next = reverse_list(second_half_start)
return ok
def build(vals):
dummy = ListNode(0)
cur = dummy
for v in vals:
cur.next = ListNode(v)
cur = cur.next
return dummy.next
if __name__ == "__main__":
a = build([1, 2, 2, 1])
b = build([1, 2])
print(is_palindrome(a)) # True
print(is_palindrome(b)) # False
E — Engineering(工程应用)
场景 1:事件序列的对称性校验(Python)
背景:在风控/行为分析中,你可能用链表(或链式结构)表达一次会话的事件序列;某些规则要求“对称结构”(例如进入/退出必须镜像匹配)。
为什么适用:不想把所有事件复制到数组里;并且希望校验后结构仍可被后续处理复用。
def is_symmetric_sequence(head):
# 直接复用 is_palindrome:把“对称性”抽象成回文
return is_palindrome(head)
场景 2:内存受限设备上的数据帧回文检测(C)
背景:某些嵌入式系统把采样值串成链表(例如内存池 + next 指针),需要快速判断是否满足对称约束以触发告警/自检。
为什么适用:O(1) 额外空间,无需申请大块缓冲区。
struct ListNode { int val; struct ListNode* next; };
static struct ListNode* reverse(struct ListNode* head) {
struct ListNode* prev = 0;
struct ListNode* cur = head;
while (cur) {
struct ListNode* nxt = cur->next;
cur->next = prev;
prev = cur;
cur = nxt;
}
return prev;
}
static struct ListNode* endFirstHalf(struct ListNode* head) {
struct ListNode* fast = head;
struct ListNode* slow = head;
while (fast->next && fast->next->next) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
int isPalindrome(struct ListNode* head) {
if (!head || !head->next) return 1;
struct ListNode* firstEnd = endFirstHalf(head);
struct ListNode* second = reverse(firstEnd->next);
int ok = 1;
struct ListNode* p1 = head;
struct ListNode* p2 = second;
while (ok && p2) {
if (p1->val != p2->val) ok = 0;
p1 = p1->next;
p2 = p2->next;
}
firstEnd->next = reverse(second); // restore
return ok;
}
场景 3:前端编辑器的操作栈对称检测(JavaScript)
背景:某些编辑器用链表记录操作(undo/redo 历史);你可能想检测“操作是否对称抵消”(例如某些模式下要求回文式操作序列)。
为什么适用:链式结构本身可直接处理;校验结束后需要继续使用原结构。
function reverse(head) {
let prev = null, cur = head;
while (cur) {
const nxt = cur.next;
cur.next = prev;
prev = cur;
cur = nxt;
}
return prev;
}
function endFirstHalf(head) {
let fast = head, slow = head;
while (fast.next && fast.next.next) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
function isPalindrome(head) {
if (!head || !head.next) return true;
const firstEnd = endFirstHalf(head);
const secondStart = reverse(firstEnd.next);
let p1 = head, p2 = secondStart;
let ok = true;
while (ok && p2) {
if (p1.val !== p2.val) ok = false;
p1 = p1.next;
p2 = p2.next;
}
firstEnd.next = reverse(secondStart); // restore
return ok;
}
R — Reflection(反思与深入)
复杂度分析
- 时间复杂度:O(n)(找中点 + 反转 + 比较 + 恢复都是线性)
- 空间复杂度:O(1)
替代方案对比
| 方法 | 思路 | 时间 | 额外空间 | 问题 |
|---|---|---|---|---|
| 数组拷贝 | 值拷贝到数组,双指针判断 | O(n) | O(n) | 内存占用高 |
| 栈 | 压入前半段,弹出比较后半段 | O(n) | O(n) | 仍占用线性空间 |
| 递归 | 利用递归栈回溯比较 | O(n) | O(n) | 容易栈溢出/不可控 |
| 反转后半段 | 中点 + 反转 + 比较 + 恢复 | O(n) | O(1) | 代码稍多但最工程可行 |
常见错误思路
- 反转后半段后忘记恢复:题目要求结构保持原样;工程里也常要求“校验不产生副作用”。
- 中点处理搞错(奇偶长度):导致比较区间不一致。推荐使用“前半段末尾 + 反转 next”的写法。
- 把节点对象当作值:回文判断比较的是
val;但恢复结构比较的是指针链接。
常见问题与注意事项
为什么比较时遍历
p2(后半段)就够了?
后半段长度 <= 前半段长度;回文成立时对应位置都要匹配,遍历后半段即可覆盖所有需要比较的对称对。如果链表有环怎么办?
题目保证无环。工程里若不保证,需要先判环(Floyd),否则“找中点/反转”可能死循环或破坏结构。反转会不会破坏原链表?
会“临时改变”,但我们在返回前把后半段再反转一次并接回,保证外部观测到的结构不变。
最佳实践与建议
- 统一采用模板:
first_half_end+reverse(first_half_end.next),避免奇偶分支 - 做题/写代码时强制写“恢复结构”步骤,防止遗漏
- 测试用例至少覆盖:
- 空链表、单节点
- 偶数长度回文与非回文
- 奇数长度回文与非回文
- 值重复很多的场景(容易误判)
S — Summary(总结)
核心收获
- 单链表不能从尾部回看,回文判断需要“借力”结构变换
- 快慢指针可在 O(n) 内定位前半段末尾
- 反转后半段能把“从尾到头”的比较变成“从头到尾”的比较
- 比较完成后再反转恢复,确保不破坏链表原结构
- 该模板可复用到很多“链表中点 + 半段处理”的题目(如重排、分割、判环扩展)
参考与延伸阅读
- LeetCode 234. Palindrome Linked List
- 链表反转(Reverse Linked List)与快慢指针(Middle of Linked List)经典题
- 任何数据结构的“临时变换 + 恢复”工程实践(避免副作用)
元信息
- 阅读时长:10~14 分钟
- 标签:Hot100、链表、回文、快慢指针、原地反转
- SEO 关键词:回文链表, Palindrome Linked List, 反转后半段, O(1) 空间, LeetCode 234
- 元描述:快慢指针定位中点,反转后半段对比并恢复结构;O(n)/O(1) 判断单链表是否回文。
行动号召(CTA)
建议你用同一套“中点 + 反转”模板再刷两题巩固:
- 重排链表(Reorder List);2) 反转链表(Reverse Linked List)。
如果你希望我把这些题也按同风格整理成 Hot100 系列文章,直接丢题目过来即可。
多语言参考实现(Python / C / C++ / Go / Rust / JS)
from __future__ import annotations
class ListNode:
def __init__(self, val: int):
self.val = val
self.next: ListNode | None = None
def reverse_list(head: ListNode | None) -> ListNode | None:
prev = None
cur = head
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev
def end_of_first_half(head: ListNode) -> ListNode:
fast = head
slow = head
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next # type: ignore[assignment]
return slow
def is_palindrome(head: ListNode | None) -> bool:
if head is None or head.next is None:
return True
first_end = end_of_first_half(head)
second = reverse_list(first_end.next)
ok = True
p1 = head
p2 = second
while ok and p2 is not None:
if p1.val != p2.val:
ok = False
p1 = p1.next # type: ignore[assignment]
p2 = p2.next
first_end.next = reverse_list(second) # restore
return ok
#include <stdio.h>
#include <stdlib.h>
struct ListNode {
int val;
struct ListNode* next;
};
static struct ListNode* reverse(struct ListNode* head) {
struct ListNode* prev = NULL;
struct ListNode* cur = head;
while (cur) {
struct ListNode* nxt = cur->next;
cur->next = prev;
prev = cur;
cur = nxt;
}
return prev;
}
static struct ListNode* endFirstHalf(struct ListNode* head) {
struct ListNode* fast = head;
struct ListNode* slow = head;
while (fast->next && fast->next->next) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
int isPalindrome(struct ListNode* head) {
if (!head || !head->next) return 1;
struct ListNode* firstEnd = endFirstHalf(head);
struct ListNode* second = reverse(firstEnd->next);
int ok = 1;
struct ListNode* p1 = head;
struct ListNode* p2 = second;
while (ok && p2) {
if (p1->val != p2->val) ok = 0;
p1 = p1->next;
p2 = p2->next;
}
firstEnd->next = reverse(second); // restore
return ok;
}
static struct ListNode* node(int v) {
struct ListNode* n = (struct ListNode*)malloc(sizeof(struct ListNode));
n->val = v;
n->next = NULL;
return n;
}
int main(void) {
// 1 -> 2 -> 2 -> 1
struct ListNode* a1 = node(1);
struct ListNode* a2 = node(2);
struct ListNode* a3 = node(2);
struct ListNode* a4 = node(1);
a1->next = a2; a2->next = a3; a3->next = a4;
printf("%d\n", isPalindrome(a1)); // 1
return 0;
}
#include <iostream>
struct ListNode {
int val;
ListNode* next;
explicit ListNode(int x) : val(x), next(nullptr) {}
};
static ListNode* reverse(ListNode* head) {
ListNode* prev = nullptr;
ListNode* cur = head;
while (cur) {
ListNode* nxt = cur->next;
cur->next = prev;
prev = cur;
cur = nxt;
}
return prev;
}
static ListNode* endFirstHalf(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast->next && fast->next->next) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
bool isPalindrome(ListNode* head) {
if (!head || !head->next) return true;
ListNode* firstEnd = endFirstHalf(head);
ListNode* second = reverse(firstEnd->next);
bool ok = true;
ListNode* p1 = head;
ListNode* p2 = second;
while (ok && p2) {
if (p1->val != p2->val) ok = false;
p1 = p1->next;
p2 = p2->next;
}
firstEnd->next = reverse(second); // restore
return ok;
}
int main() {
auto* a1 = new ListNode(1);
auto* a2 = new ListNode(2);
auto* a3 = new ListNode(2);
auto* a4 = new ListNode(1);
a1->next = a2; a2->next = a3; a3->next = a4;
std::cout << std::boolalpha << isPalindrome(a1) << "\n";
return 0;
}
package main
import "fmt"
type ListNode struct {
Val int
Next *ListNode
}
func reverse(head *ListNode) *ListNode {
var prev *ListNode
cur := head
for cur != nil {
nxt := cur.Next
cur.Next = prev
prev = cur
cur = nxt
}
return prev
}
func endFirstHalf(head *ListNode) *ListNode {
fast, slow := head, head
for fast.Next != nil && fast.Next.Next != nil {
fast = fast.Next.Next
slow = slow.Next
}
return slow
}
func isPalindrome(head *ListNode) bool {
if head == nil || head.Next == nil {
return true
}
firstEnd := endFirstHalf(head)
second := reverse(firstEnd.Next)
ok := true
p1, p2 := head, second
for ok && p2 != nil {
if p1.Val != p2.Val {
ok = false
}
p1 = p1.Next
p2 = p2.Next
}
firstEnd.Next = reverse(second) // restore
return ok
}
func main() {
a4 := &ListNode{Val: 1}
a3 := &ListNode{Val: 2, Next: a4}
a2 := &ListNode{Val: 2, Next: a3}
a1 := &ListNode{Val: 1, Next: a2}
fmt.Println(isPalindrome(a1))
}
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct ListNode {
val: i32,
next: Option<Rc<RefCell<ListNode>>>,
}
fn node(v: i32) -> Rc<RefCell<ListNode>> {
Rc::new(RefCell::new(ListNode { val: v, next: None }))
}
fn next_of(n: &Option<Rc<RefCell<ListNode>>>) -> Option<Rc<RefCell<ListNode>>> {
n.as_ref().and_then(|x| x.borrow().next.clone())
}
fn reverse(mut head: Option<Rc<RefCell<ListNode>>>) -> Option<Rc<RefCell<ListNode>>> {
let mut prev: Option<Rc<RefCell<ListNode>>> = None;
while let Some(cur) = head {
let nxt = cur.borrow().next.clone();
cur.borrow_mut().next = prev.clone();
prev = Some(cur);
head = nxt;
}
prev
}
fn end_first_half(head: &Option<Rc<RefCell<ListNode>>>) -> Option<Rc<RefCell<ListNode>>> {
let mut fast = head.clone();
let mut slow = head.clone();
loop {
let f1 = next_of(&fast);
let f2 = next_of(&f1);
if f1.is_none() || f2.is_none() {
break;
}
fast = f2;
slow = next_of(&slow);
}
slow
}
fn is_palindrome(head: Option<Rc<RefCell<ListNode>>>) -> bool {
if head.is_none() || next_of(&head).is_none() {
return true;
}
let first_end = end_first_half(&head).unwrap();
let second_start = reverse(first_end.borrow().next.clone());
let mut ok = true;
let mut p1 = head.clone();
let mut p2 = second_start.clone();
while ok && p2.is_some() {
let v1 = p1.as_ref().unwrap().borrow().val;
let v2 = p2.as_ref().unwrap().borrow().val;
if v1 != v2 {
ok = false;
}
p1 = next_of(&p1);
p2 = next_of(&p2);
}
// restore
first_end.borrow_mut().next = reverse(second_start);
ok
}
fn main() {
// 1 -> 2 -> 2 -> 1
let a1 = node(1);
let a2 = node(2);
let a3 = node(2);
let a4 = node(1);
a1.borrow_mut().next = Some(a2.clone());
a2.borrow_mut().next = Some(a3.clone());
a3.borrow_mut().next = Some(a4.clone());
println!("{}", is_palindrome(Some(a1)));
}
class ListNode {
constructor(val) {
this.val = val;
this.next = null;
}
}
function reverse(head) {
let prev = null, cur = head;
while (cur) {
const nxt = cur.next;
cur.next = prev;
prev = cur;
cur = nxt;
}
return prev;
}
function endFirstHalf(head) {
let fast = head, slow = head;
while (fast.next && fast.next.next) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
function isPalindrome(head) {
if (!head || !head.next) return true;
const firstEnd = endFirstHalf(head);
const secondStart = reverse(firstEnd.next);
let ok = true;
let p1 = head, p2 = secondStart;
while (ok && p2) {
if (p1.val !== p2.val) ok = false;
p1 = p1.next;
p2 = p2.next;
}
firstEnd.next = reverse(secondStart); // restore
return ok;
}
// demo: 1 -> 2 -> 2 -> 1
const a1 = new ListNode(1);
const a2 = new ListNode(2);
const a3 = new ListNode(2);
const a4 = new ListNode(1);
a1.next = a2; a2.next = a3; a3.next = a4;
console.log(isPalindrome(a1));