一、前置知识
在做这道题之前,我们必须先掌握几个核心基础知识点,不然看代码会完全懵。我会用"大白话+例子"的方式,把每个知识点讲透,哪怕你从来没学过编程、没听过"链表",也能看明白。
1. 什么是单链表?
单链表是一种"链式存储"的数据结构,不像数组那样可以直接通过下标(比如arr[0])访问任意元素,它是由一个个"节点"串起来的,就像一串糖葫芦:
-
每个"糖葫芦山楂"就是一个「节点」(对应题目里的ListNode);
-
每个节点里有两个东西:① 节点的值(val):就是山楂的"口味",比如1、2、3;② 下一个节点的指针(next):就是串山楂的"签子",指向后面那个山楂(节点)。
举个例子:链表 1→2→3,拆解后是这样的:
-
第一个节点(头节点head):val=1,next指向第二个节点;
-
第二个节点:val=2,next指向第三个节点;
-
第三个节点:val=3,next指向null(空),表示这是最后一个节点,后面没有山楂了。
关键特点:只能从头节点(head)开始,顺着next指针一步步往后走,不能直接访问后面的节点,也不能往回走(比如从第二个节点不能直接回到第一个节点)------ 这也是这道题的核心难点。
2. 什么是回文?
回文就是"正着读和反着读完全一样"的序列,比如:
-
数字:121、1221(正读1221,反读1221,一样);
-
字符串:"aba"、"abba";
-
链表:1→2→2→1(正读1、2、2、1,反读1、2、2、1,一样),就是回文链表;1→2(正读1、2,反读2、1,不一样),就不是。
3. 双指针(基础工具)
双指针就是用两个"指针变量",配合着移动,用来做"对比、遍历"等操作。比如这道题里,我们会用"左指针"(从开头往中间走)和"右指针"(从结尾往中间走),逐位对比值是否相等。
举个例子:判断数组[1,2,2,1]是否为回文,左指针先指向1(索引0),右指针指向1(索引3),对比相等;然后左指针右移(指向2,索引1),右指针左移(指向2,索引2),对比相等;最后左指针超过右指针,结束,判断是回文。
4. 快慢指针(找链表中点的神器)
快慢指针是双指针的一种特殊用法,两个指针同时从 head 出发,「慢指针(slow)每次走1步」,「快指针(fast)每次走2步」,当快指针走到链表末尾时,慢指针正好走到链表的「中点」。
举个例子:链表1→2→2→1
-
初始:slow=head(1),fast=head(1);
-
第一次移动:slow走1步到2(第二个节点),fast走2步到2(第三个节点);
-
第二次移动:slow走1步到1(第四个节点),fast走2步到null(空);
此时fast走到末尾,slow正好在链表中点(第三个节点,val=2)------ 这个技巧能帮我们快速找到链表一半的位置,为后续优化解法打基础。
5. 反转链表(优化解法的核心)
反转链表就是把链表的"指针方向颠倒过来",比如原本是1→2→3,反转后变成3→2→1。
核心思路(用3个指针实现,一步步来):
假设要反转的链表是:cur(当前节点)→ next_node(下一个节点)→ ...
-
先保存cur的下一个节点(next_node),因为反转后cur的next会指向前面的节点,不保存会丢失后面的链表;
-
把cur的next指针,指向"前一个节点"(pre,初始为null,因为第一个节点反转后是最后一个节点,next为null);
-
把pre移动到cur的位置(pre = cur),准备下一次反转;
-
把cur移动到之前保存的next_node位置(cur = next_node),继续下一次反转;
-
重复1-4,直到cur变成null,此时pre就是反转后链表的头节点。
举个例子:反转1→2→3
-
初始:pre=null,cur=1(第一个节点);
-
第一步:保存next_node=2,cur.next=pre(null),pre=1,cur=2;
-
第二步:保存next_node=3,cur.next=pre(1),pre=2,cur=3;
-
第三步:保存next_node=null,cur.next=pre(2),pre=3,cur=null;
结束,反转后链表是3→2→1,pre就是反转后的头节点。
6. 主函数
我们写的isPalindrome函数(判断回文的核心函数),需要一个"入口"来调用它,这个入口就是主函数(main函数,C++里)、或者直接写测试代码(Python里)。
主函数的作用:① 创建链表(模拟题目输入);② 调用isPalindrome函数,传入链表头节点;③ 输出函数返回的结果(true/false);④ (C++)销毁链表,避免内存浪费。
二、题目解析
题目:给你一个单链表的头节点head,判断这个链表是否是回文链表。是,返回true;不是,返回false。
输入输出示例(帮你理解):
示例1:输入head = [1,2,2,1](链表1→2→2→1),输出true(是回文);
示例2:输入head = [1,2](链表1→2),输出false(不是回文);
约束条件:链表节点数1~10^5(最多10万个节点),节点值0~9(都是单个数字)。
进阶要求:能不能用O(n)时间(遍历一次链表)、O(1)空间(不额外用数组等存储所有节点值)解决?(后面会讲这个最优解法)
三、解法1:数组转换法(简单易懂,新手首选)
核心思路:利用"数组可以直接访问任意位置"的特点,把链表的所有节点值先存到数组里,再用双指针判断数组是否是回文------ 间接判断链表是否是回文。
步骤拆解(能跟着走):
-
遍历链表:从head开始,把每个节点的val依次存入数组;
-
双指针对比:左指针指向数组开头(索引0),右指针指向数组末尾(索引len(arr)-1);
-
逐位对比:如果左指针和右指针指向的值不相等,直接返回false(不是回文);如果相等,左指针右移、右指针左移,继续对比;
-
结束判断:当左指针 >= 右指针时,所有值都对比完了,返回true(是回文)。
复杂度(了解即可):
-
时间:O(n)(遍历链表一次,遍历数组一次,总共遍历2n个元素,n是链表节点数);
-
空间:O(n)(需要一个数组存储所有n个节点的值,占用额外空间)。
(一)Python 代码(每句带注释,含测试主代码)
python
from typing import Optional # 导入Optional,用于标注链表节点的类型(允许为None)
# 题目已给出的链表节点定义(不用改,理解即可)
# 每个节点有两个属性:val(节点值)、next(下一个节点的指针)
class ListNode:
# 节点的构造函数,创建节点时可以传入值和下一个节点(默认值为0和None)
def __init__(self, val=0, next=None):
self.val = val # 给当前节点赋值
self.next = next # 给当前节点指向的下一个节点赋值
# 核心函数:判断链表是否为回文链表
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
# 1. 第一步:把链表的所有节点值存入数组(因为数组可以直接访问任意位置)
arr = [] # 创建一个空数组,用来存链表节点的值
cur = head # 定义一个临时指针cur,初始指向头节点head(相当于从第一个山楂开始拿)
# 循环遍历链表:只要cur不是null(还有山楂没拿),就一直循环
while cur:
arr.append(cur.val) # 把当前节点的值存入数组
cur = cur.next # cur指针移动到下一个节点(拿下一个山楂)
# 2. 第二步:用双指针判断数组是否是回文
left = 0 # 左指针,初始指向数组开头(第一个元素)
right = len(arr) - 1 # 右指针,初始指向数组末尾(最后一个元素)
# 循环对比:只要左指针在右指针左边,就继续对比
while left < right:
# 如果左右指针指向的值不相等,说明不是回文,直接返回false
if arr[left] != arr[right]:
return False
left += 1 # 左指针右移一位(往中间走)
right -= 1 # 右指针左移一位(往中间走)
# 所有值都对比完了,没有不相等的,说明是回文,返回true
return True
# ---------------------- 主测试代码(重点看,模拟题目输入和输出)----------------------
# 辅助函数:根据数组创建链表(比如输入[1,2,2,1],创建出1→2→2→1的链表)
# 不用纠结这个函数的实现,会用就行,核心是帮我们快速创建链表
def create_linked_list(arr):
if not arr: # 如果数组为空,返回空链表(没有节点)
return None
# 创建头节点,值为数组的第一个元素
head = ListNode(arr[0])
cur = head # 临时指针cur,指向头节点,用来连接后续节点
# 遍历数组剩下的元素,依次创建节点并连接
for val in arr[1:]:
cur.next = ListNode(val) # 给当前节点的next指向新创建的节点
cur = cur.next # cur移动到新创建的节点,继续连接下一个
return head # 返回创建好的链表的头节点
# 辅助函数:打印链表(可选,用来查看我们创建的链表是否正确)
def print_linked_list(head):
cur = head
while cur:
print(cur.val, end="→") # 打印当前节点值,不换行,用→连接
cur = cur.next
print("null") # 链表末尾打印null,表示结束
# 测试案例1:题目给出的案例1(输入[1,2,2,1],预期输出true)
print("测试案例1:")
arr1 = [1,2,2,1]
head1 = create_linked_list(arr1) # 创建链表
print("创建的链表:", end="")
print_linked_list(head1) # 打印链表,确认是否正确
solution = Solution() # 创建Solution对象(用来调用isPalindrome函数)
result1 = solution.isPalindrome(head1) # 调用核心函数,传入链表头节点
print("是否为回文链表:", result1) # 输出结果(预期true)
# 测试案例2:题目给出的案例2(输入[1,2],预期输出false)
print("\n测试案例2:")
arr2 = [1,2]
head2 = create_linked_list(arr2)
print("创建的链表:", end="")
print_linked_list(head2)
result2 = solution.isPalindrome(head2)
print("是否为回文链表:", result2) # 预期false
# 自定义测试案例3:链表只有1个节点(输入[5],预期true,因为单个节点肯定是回文)
print("\n自定义测试案例3:")
arr3 = [5]
head3 = create_linked_list(arr3)
print("创建的链表:", end="")
print_linked_list(head3)
result3 = solution.isPalindrome(head3)
print("是否为回文链表:", result3) # 预期true
# 自定义测试案例4:链表有3个节点(输入[1,3,1],预期true)
print("\n自定义测试案例4:")
arr4 = [1,3,1]
head4 = create_linked_list(arr4)
print("创建的链表:", end="")
print_linked_list(head4)
result4 = solution.isPalindrome(head4)
print("是否为回文链表:", result4) # 预期true
# 自定义测试案例5:链表有5个节点(输入[1,2,3,4,5],预期false)
print("\n自定义测试案例5:")
arr5 = [1,2,3,4,5]
head5 = create_linked_list(arr5)
print("创建的链表:", end="")
print_linked_list(head5)
result5 = solution.isPalindrome(head5)
print("是否为回文链表:", result5) # 预期false
(二)Python 代码运行过程讲解(逐句跟)
以测试案例1(arr1=[1,2,2,1])为例,一步步看代码怎么运行:
- 先运行create_linked_list(arr1),创建链表1→2→2→1:
-
初始创建head=ListNode(1)(头节点val=1),cur=head;
-
遍历arr1[1:]([2,2,1]):
-
第一个val=2:cur.next=ListNode(2)(头节点的next指向val=2的节点),cur移动到val=2的节点;
-
第二个val=2:cur.next=ListNode(2)(val=2的节点的next指向另一个val=2的节点),cur移动到这个节点;
-
第三个val=1:cur.next=ListNode(1)(这个节点的next指向val=1的节点),cur移动到val=1的节点;
-
循环结束,返回head(头节点),此时链表创建完成:1→2→2→1→null。
- 调用solution.isPalindrome(head1),开始判断:
-
初始化arr=[ ],cur=head1(指向val=1的节点);
-
循环遍历链表:
-
cur=1≠null:arr.append(1) → arr=[1],cur=cur.next(指向val=2的节点);
-
cur=2≠null:arr.append(2) → arr=[1,2],cur=cur.next(指向第二个val=2的节点);
-
cur=2≠null:arr.append(2) → arr=[1,2,2],cur=cur.next(指向val=1的节点);
-
cur=1≠null:arr.append(1) → arr=[1,2,2,1],cur=cur.next(指向null);
-
循环结束,arr=[1,2,2,1]。
-
双指针对比:left=0,right=3(arr长度4,4-1=3);
-
left=0 < right=3:arr[0]=1 vs arr[3]=1 → 相等;left=1,right=2;
-
left=1 < right=2:arr[1]=2 vs arr[2]=2 → 相等;left=2,right=1;
-
循环结束(left >= right),返回true。
- 主代码打印结果:"是否为回文链表:True",和预期一致。
其他测试案例的运行过程和这个类似,可以自己跟着走一遍,加深理解。
(三)C++ 代码(每句带注释,含测试主函数)
cpp
#include <iostream> // 导入输入输出流(用来打印结果)
#include <vector> // 导入vector(相当于Python的数组,用来存链表节点值)
using namespace std; // 简化代码,不用每次都写std::
// 题目已给出的链表节点定义(不用改,理解即可)
struct ListNode {
int val; // 节点的值
ListNode *next; // 指向 next 节点的指针(地址)
// 构造函数1:无参数,默认值val=0,next=nullptr(空指针)
ListNode() : val(0), next(nullptr) {}
// 构造函数2:只传节点值,next默认nullptr
ListNode(int x) : val(x), next(nullptr) {}
// 构造函数3:传节点值和next指针,手动指定下一个节点
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
// 核心函数:判断链表是否为回文链表
class Solution {
public:
bool isPalindrome(ListNode* head) {
// 1. 第一步:把链表的所有节点值存入vector(数组)
vector<int> arr; // 创建一个int类型的vector,用来存节点值
ListNode* cur = head; // 临时指针cur,初始指向头节点head
// 循环遍历链表:只要cur不是空指针(还有节点没遍历),就继续
while (cur != nullptr) {
arr.push_back(cur->val); // 把当前节点的值存入vector(cur->val表示访问cur指针指向的节点的val)
cur = cur->next; // cur指针移动到下一个节点(cur->next是当前节点的next指针指向的地址)
}
// 2. 第二步:双指针对比,判断vector是否为回文
int left = 0; // 左指针,指向vector开头(索引0)
int right = arr.size() - 1; // 右指针,指向vector末尾(索引=长度-1)
// 循环对比:左指针在右指针左边,就继续
while (left < right) {
// 如果左右指针指向的值不相等,直接返回false(不是回文)
if (arr[left] != arr[right]) {
return false;
}
left++; // 左指针右移一位
right--; // 右指针左移一位
}
// 所有值都对比完,没有不相等的,返回true(是回文)
return true;
}
};
// ---------------------- 主测试函数(重点看,模拟输入输出)----------------------
// 辅助函数1:根据数组创建链表(输入int数组和数组长度,返回链表头节点)
ListNode* createLinkedList(int arr[], int n) {
if (n == 0) { // 如果数组长度为0,返回空链表
return nullptr;
}
// 创建头节点,值为数组第一个元素
ListNode* head = new ListNode(arr[0]);
ListNode* cur = head; // 临时指针cur,用来连接后续节点
// 遍历数组剩下的元素,创建节点并连接
for (int i = 1; i < n; i++) {
cur->next = new ListNode(arr[i]); // 给当前节点的next指向新创建的节点(new用来分配内存)
cur = cur->next; // cur移动到新创建的节点
}
return head; // 返回链表头节点
}
// 辅助函数2:打印链表(用来查看创建的链表是否正确)
void printLinkedList(ListNode* head) {
ListNode* cur = head;
while (cur != nullptr) {
cout << cur->val << "→"; // 打印当前节点值,用→连接
cur = cur->next;
}
cout << "null" << endl; // 末尾打印null,换行
}
// 辅助函数3:销毁链表(C++必须手动销毁,避免内存泄漏,了解即可)
void destroyLinkedList(ListNode* head) {
ListNode* cur = head;
while (cur != nullptr) {
ListNode* temp = cur; // 保存当前节点
cur = cur->next; // cur移动到下一个节点
delete temp; // 销毁当前节点,释放内存
}
}
// 主函数(程序入口,所有代码从这里开始执行)
int main() {
// 测试案例1:题目给出的案例1(输入[1,2,2,1],预期输出true)
cout << "测试案例1:" << endl;
int arr1[] = {1, 2, 2, 1}; // 输入数组
int n1 = sizeof(arr1) / sizeof(arr1[0]); // 计算数组长度(总字节数/单个元素字节数)
ListNode* head1 = createLinkedList(arr1, n1); // 创建链表
cout << "创建的链表:";
printLinkedList(head1); // 打印链表
Solution solution; // 创建Solution对象(用来调用isPalindrome函数)
bool result1 = solution.isPalindrome(head1); // 调用核心函数,传入头节点
cout << "是否为回文链表:" << (result1 ? "true" : "false") << endl; // 输出结果(true/false)
destroyLinkedList(head1); // 销毁链表,释放内存
// 测试案例2:题目给出的案例2(输入[1,2],预期输出false)
cout << "\n测试案例2:" << endl;
int arr2[] = {1, 2};
int n2 = sizeof(arr2) / sizeof(arr2[0]);
ListNode* head2 = createLinkedList(arr2, n2);
cout << "创建的链表:";
printLinkedList(head2);
bool result2 = solution.isPalindrome(head2);
cout << "是否为回文链表:" << (result2 ? "true" : "false") << endl;
destroyLinkedList(head2);
// 自定义测试案例3:链表只有1个节点(输入[5],预期true)
cout << "\n自定义测试案例3:" << endl;
int arr3[] = {5};
int n3 = sizeof(arr3) / sizeof(arr3[0]);
ListNode* head3 = createLinkedList(arr3, n3);
cout << "创建的链表:";
printLinkedList(head3);
bool result3 = solution.isPalindrome(head3);
cout << "是否为回文链表:" << (result3 ? "true" : "false") << endl;
destroyLinkedList(head3);
// 自定义测试案例4:链表有3个节点(输入[1,3,1],预期true)
cout << "\n自定义测试案例4:" << endl;
int arr4[] = {1, 3, 1};
int n4 = sizeof(arr4) / sizeof(arr4[0]);
ListNode* head4 = createLinkedList(arr4, n4);
cout << "创建的链表:";
printLinkedList(head4);
bool result4 = solution.isPalindrome(head4);
cout << "是否为回文链表:" << (result4 ? "true" : "false") << endl;
destroyLinkedList(head4);
// 自定义测试案例5:链表有5个节点(输入[1,2,3,4,5],预期false)
cout << "\n自定义测试案例5:" << endl;
int arr5[] = {1, 2, 3, 4, 5};
int n5 = sizeof(arr5) / sizeof(arr5[0]);
ListNode* head5 = createLinkedList(arr5, n5);
cout << "创建的链表:";
printLinkedList(head5);
bool result5 = solution.isPalindrome(head5);
cout << "是否为回文链表:" << (result5 ? "true" : "false") << endl;
destroyLinkedList(head5);
return 0; // 主函数结束,返回0表示程序正常运行
}
(四)C++ 代码运行过程讲解(和Python类似,重点看差异)
还是以测试案例1(arr1=[1,2,2,1])为例:
-
main函数中,先定义arr1数组,计算长度n1=4(sizeof(arr1)=4*4=16字节,sizeof(arr1[0])=4字节,16/4=4);
-
调用createLinkedList(arr1, n1),创建链表1→2→2→1:
-
用new ListNode(arr[0])创建头节点(val=1),分配内存,cur指向头节点;
-
循环i从1到3(n1=4),依次创建val=2、2、1的节点,用cur->next连接,最后返回head;
- 调用solution.isPalindrome(head1),逻辑和Python完全一致:
-
遍历链表,把val存入vector arr,得到arr=[1,2,2,1];
-
双指针对比,left和right移动,所有值相等,返回true;
-
打印结果时,用(result1 ? "true" : "false")------C++中bool类型不能直接打印,需要用三目运算符转换为字符串;
-
调用destroyLinkedList(head1),手动销毁每个节点:因为C++中用new分配的内存,不会自动释放,不销毁会导致内存泄漏(占用多余内存),不用深究,记住"创建链表后要销毁"即可。
四、解法2:快慢指针+反转链表(最优解,满足进阶要求)
核心思路:不使用额外数组(节省空间),原地操作链表,满足O(n)时间、O(1)空间的要求。
步骤拆解(分步理解):
-
找中点:用快慢指针,找到链表的中间节点(slow指针最终指向中点);
-
反转后半段:把中点之后的链表反转(比如1→2→2→1,中点是第二个2,反转后半段得到1→2);
-
双指针对比:用左指针(指向head,前半段开头)和右指针(指向反转后后半段的开头),逐位对比值;
-
返回结果:如果所有值都相等,返回true;否则返回false。
为什么这样可行?
因为反转后半段后,后半段的顺序就和"原链表后半段的逆序"一样了,此时对比"前半段"和"反转后的后半段",就相当于对比"原链表正序"和"原链表逆序"------ 正好是回文的判断标准。
复杂度(重点):
-
时间:O(n)(快慢指针遍历1次,反转后半段遍历1次,对比遍历1次,总共3n,还是O(n));
-
空间:O(1)(只用到几个指针变量,没有用额外的数组/链表存储所有节点值)。
(一)Python 代码(每句带注释,含测试主代码)
python
from typing import Optional
# 链表节点定义(和解法1一样,不用改)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# 核心函数:快慢指针+反转链表,判断回文
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
# 第一步:快慢指针找链表中点
# 初始时,slow和fast都指向头节点head
slow = fast = head
# 循环条件:fast不为null,且fast的next不为null(避免fast.next.next报错)
# 因为fast每次走2步,所以要判断fast和fast.next都不是null,才能继续走
while fast and fast.next:
slow = slow.next # 慢指针走1步
fast = fast.next.next # 快指针走2步
# 循环结束后,fast走到链表末尾(或末尾的前一个),slow正好在链表中点
# 比如:1→2→2→1,slow最终指向第二个2;1→2→3→2→1,slow最终指向3(中间节点)
# 第二步:反转slow之后的后半段链表
pre = None # 定义pre指针,初始为null(反转后链表的末尾,next为null)
cur = slow # cur指针,初始指向中点slow(从这里开始反转后半段)
while cur: # 只要cur不是null,就继续反转
next_node = cur.next # 保存cur的下一个节点(因为反转后cur.next会变,不保存会丢)
cur.next = pre # 反转cur的指针:让cur指向pre(原来的前一个节点)
pre = cur # pre指针移动到cur的位置(准备下一次反转)
cur = next_node # cur指针移动到之前保存的next_node(继续下一次反转)
# 反转结束后,pre就是反转后后半段链表的头节点
# 比如:原后半段1→2(slow指向2),反转后pre指向1,链表变成1→2(和前半段一样)
# 第三步:双指针对比前半段和反转后的后半段
left = head # 左指针,指向原链表头节点(前半段开头)
right = pre # 右指针,指向反转后后半段的头节点(后半段逆序开头)
while right: # 只要right不是null(后半段还有节点),就继续对比
# 如果左右指针指向的值不相等,说明不是回文,返回false
if left.val != right.val:
return False
left = left.next # 左指针右移(前半段往后走)
right = right.next # 右指针右移(反转后的后半段往后走)
# 所有节点对比完,没有不相等的,返回true
return True
# ---------------------- 主测试代码(和解法1一样,直接复用)----------------------
def create_linked_list(arr):
if not arr:
return None
head = ListNode(arr[0])
cur = head
for val in arr[1:]:
cur.next = ListNode(val)
cur = cur.next
return head
def print_linked_list(head):
cur = head
while cur:
print(cur.val, end="→")
cur = cur.next
print("null")
# 测试案例(和解法1完全一样,验证最优解的正确性)
print("测试案例1:")
arr1 = [1,2,2,1]
head1 = create_linked_list(arr1)
print("创建的链表:", end="")
print_linked_list(head1)
solution = Solution()
result1 = solution.isPalindrome(head1)
print("是否为回文链表:", result1) # 预期true
print("\n测试案例2:")
arr2 = [1,2]
head2 = create_linked_list(arr2)
print("创建的链表:", end="")
print_linked_list(head2)
result2 = solution.isPalindrome(head2)
print("是否为回文链表:", result2) # 预期false
print("\n自定义测试案例3:")
arr3 = [5]
head3 = create_linked_list(arr3)
print("创建的链表:", end="")
print_linked_list(head3)
result3 = solution.isPalindrome(head3)
print("是否为回文链表:", result3) # 预期true
print("\n自定义测试案例4:")
arr4 = [1,3,1]
head4 = create_linked_list(arr4)
print("创建的链表:", end="")
print_linked_list(head4)
result4 = solution.isPalindrome(head4)
print("是否为回文链表:", result4) # 预期true
print("\n自定义测试案例5:")
arr5 = [1,2,3,4,5]
head5 = create_linked_list(arr5)
print("创建的链表:", end="")
print_linked_list(head5)
result5 = solution.isPalindrome(head5)
print("是否为回文链表:", result5) # 预期false
(二)Python 最优解运行过程讲解(重点看中点和反转)
还是以测试案例1(arr1=[1,2,2,1],链表1→2→2→1)为例,一步步看:
- 第一步:快慢指针找中点:
-
初始:slow=head(1),fast=head(1);
-
第一次循环:fast≠null,fast.next(2)≠null → slow走1步到2(第二个节点),fast走2步到2(第三个节点);
-
第二次循环:fast≠null,fast.next(1)≠null → slow走1步到1(第四个节点),fast走2步到null;
-
循环结束,slow指向第四个节点(val=1)?不对,这里纠正一下:原链表1→2→2→1,slow的移动过程:
初始:slow=1,fast=1;
第一次移动后:slow=2(第二个节点),fast=2(第三个节点);
第二次移动:fast.next是1(第三个节点的next是第四个节点),所以fast继续走2步:fast=fast.next.next = 1.next = null;
此时slow=2(第二个节点)------ 哦对,这里才是中点!因为链表长度是4(偶数),中点是第二个节点(val=2),后半段是2→1。
- 第二步:反转后半段(slow=2,后半段是2→1):
-
初始:pre=null,cur=slow(2);
-
第一次循环:cur=2≠null → next_node=cur.next=1;cur.next=pre=null;pre=2;cur=1;
-
第二次循环:cur=1≠null → next_node=cur.next=null;cur.next=pre=2;pre=1;cur=null;
-
反转结束,pre=1(反转后后半段的头节点),反转后的后半段是1→2。
- 第三步:双指针对比:
-
left=head(1),right=pre(1);
-
第一次对比:left.val=1 vs right.val=1 → 相等;left=2(第二个节点),right=2(反转后后半段的第二个节点);
-
第二次对比:left.val=2 vs right.val=2 → 相等;left=2(第三个节点),right=null;
-
循环结束(right=null),返回true。
再看测试案例2(arr2=[1,2],链表1→2):
-
快慢指针找中点:slow=2(第二个节点),fast=null;
-
反转后半段(只有2):pre=2,反转后后半段是2;
-
对比:left=1,right=2 → 1≠2,返回false,正确。
(三)C++ 代码(每句带注释,含测试主函数)
cpp
#include <iostream>
#include <vector>
using namespace std;
// 链表节点定义(和解法1一致)
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
// 核心函数:快慢指针+反转链表,判断回文
class Solution {
public:
bool isPalindrome(ListNode* head) {
// 第一步:快慢指针找链表中点
ListNode* slow = head; // 慢指针,初始指向头节点
ListNode* fast = head; // 快指针,初始指向头节点
// 循环条件:fast不为空,且fast->next不为空(避免fast->next->next报错)
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next; // 慢指针走1步
fast = fast->next->next; // 快指针走2步
}
// 第二步:反转slow之后的后半段链表
ListNode* pre = nullptr; // pre指针,初始为空(反转后链表的末尾)
ListNode* cur = slow; // cur指针,从slow(中点)开始反转
while (cur != nullptr) {
ListNode* next_node = cur->next; // 保存cur的下一个节点,避免丢失
cur->next = pre; // 反转cur的指针,指向pre
pre = cur; // pre移动到cur的位置
cur = next_node; // cur移动到保存的next_node
}
// 反转结束,pre是反转后后半段的头节点
// 第三步:双指针对比前半段和反转后的后半段
ListNode* left = head; // 左指针,指向原链表头节点
ListNode* right = pre; // 右指针,指向反转后后半段的头节点
while (right != nullptr) { // 只要后半段还有节点,就继续对比
if (left->val != right->val) { // 值不相等,返回false
return false;
}
left = left->next; // 左指针右移
right = right->next; // 右指针右移
}
// 所有节点对比完成,返回true
return true;
}
};
// ---------------------- 辅助函数和主测试函数(和解法1一致,复用)----------------------
ListNode* createLinkedList(int arr[], int n) {
if (n == 0) {
return nullptr;
}
ListNode* head = new ListNode(arr[0]);
ListNode* cur = head;
for (int i = 1; i < n; i++) {
cur->next = new ListNode(arr[i]);
cur = cur->next;
}
return head;
}
void printLinkedList(ListNode* head) {
ListNode* cur = head;
while (cur != nullptr) {
cout << cur->val << "→";
cur = cur->next;
}
cout << "null" << endl;
}
void destroyLinkedList(ListNode* head) {
ListNode* cur = head;
while (cur != nullptr) {
ListNode* temp = cur;
cur = cur->next;
delete temp;
}
}
int main() {
// 测试案例1:[1,2,2,1] → true
cout << "测试案例1:" << endl;
int arr1[] = {1, 2, 2, 1};
int n1 = sizeof(arr1) / sizeof(arr1[0]);
ListNode* head1 = createLinkedList(arr1, n1);
cout << "创建的链表:";
printLinkedList(head1);
Solution solution;
bool result1 = solution.isPalindrome(head1);
cout << "是否为回文链表:" << (result1 ? "true" : "false") << endl;
destroyLinkedList(head1);
// 测试案例2:[1,2] → false
cout << "\n测试案例2:" << endl;
int arr2[] = {1, 2};
int n2 = sizeof(arr2) / sizeof(arr2[0]);
ListNode* head2 = createLinkedList(arr2, n2);
cout << "创建的链表:";
printLinkedList(head2);
bool result2 = solution.isPalindrome(head2);
cout << "是否为回文链表:" << (result2 ? "true" : "false") << endl;
destroyLinkedList(head2);
// 自定义测试案例3:链表只有1个节点([5])→ true
cout << "\n自定义测试案例3:" << endl;
int arr3[] = {5};
int n3 = sizeof(arr3) / sizeof(arr3[0]);
ListNode* head3 = createLinkedList(arr3, n3);
cout << "创建的链表:";
printLinkedList(head3);
bool result3 = solution.isPalindrome(head3);
cout << "是否为回文链表:" << (result3 ? "true" : "false") << endl;
destroyLinkedList(head3);
// 自定义测试案例4:3个节点([1,3,1])→ true
cout << "\n自定义测试案例4:" << endl;
int arr4[] = {1, 3, 1};
int n4 = sizeof(arr4) / sizeof(arr4[0]);
ListNode* head4 = createLinkedList(arr4, n4);
cout << "创建的链表:";
printLinkedList(head4);
bool result4 = solution.isPalindrome(head4);
cout << "是否为回文链表:" << (result4 ? "true" : "false") << endl;
destroyLinkedList(head4);
// 自定义测试案例5:5个节点([1,2,3,4,5])→ false
cout << "\n自定义测试案例5:" << endl;
int arr5[] = {1, 2, 3, 4, 5};
int n5 = sizeof(arr5) / sizeof(arr5[0]);
ListNode* head5 = createLinkedList(arr5, n5);
cout << "创建的链表:";
printLinkedList(head5);
bool result5 = solution.isPalindrome(head5);
cout << "是否为回文链表:" << (result5 ? "true" : "false") << endl;
destroyLinkedList(head5);
return 0; // 主函数结束,返回0表示程序正常运行
}
(四)C++ 最优解运行过程讲解(重点看指针操作和内存)
和Python最优解的逻辑完全一致,核心差异在于C++的指针操作和内存管理,我们还是以测试案例1(arr1=[1,2,2,1],链表1→2→2→1)为例,逐步讲解:
- 第一步:快慢指针找中点(和Python完全一样,只是指针写法不同):
-
初始:slow = head(指向val=1的节点),fast = head(指向val=1的节点);
-
第一次循环:fast≠nullptr,fast->next(指向val=2的节点)≠nullptr → slow = slow->next(指向val=2的第二个节点),fast = fast->next->next(指向val=2的第三个节点);
-
第二次循环:fast≠nullptr,fast->next(指向val=1的第四个节点)≠nullptr → slow = slow->next(指向val=2的第三个节点),fast = fast->next->next(指向nullptr);
-
循环结束,slow指向第三个节点(val=2),也就是链表中点,后半段链表是2→1。
- 第二步:反转后半段链表(指针操作是重点,和Python逻辑一致,写法有差异):
-
初始:pre = nullptr(空指针),cur = slow(指向中点val=2的节点);
-
第一次循环:cur≠nullptr → ① 保存next_node = cur->next(指向val=1的节点);② cur->next = pre(让val=2的节点指向nullptr);③ pre = cur(pre指向val=2的节点);④ cur = next_node(cur指向val=1的节点);
-
第二次循环:cur≠nullptr → ① 保存next_node = cur->next(指向nullptr);② cur->next = pre(让val=1的节点指向val=2的节点);③ pre = cur(pre指向val=1的节点);④ cur = next_node(cur指向nullptr);
-
反转结束,pre指向val=1的节点,反转后的后半段链表是1→2,和前半段链表1→2完全一致。
- 第三步:双指针对比(和Python逻辑一致):
-
初始:left = head(指向val=1的节点),right = pre(指向val=1的节点);
-
第一次对比:left->val(1) == right->val(1) → 相等;left = left->next(指向val=2的第二个节点),right = right->next(指向val=2的节点);
-
第二次对比:left->val(2) == right->val(2) → 相等;left = left->next(指向val=2的第三个节点),right = right->next(指向nullptr);
-
循环结束(right==nullptr),返回true,判断为回文链表。
补充说明(C++专属):
-
指针写法:Python中cur.next是访问节点的next属性,C++中用cur->next(因为cur是指针,->用来访问指针指向对象的属性);
-
内存管理:主函数最后必须调用destroyLinkedList(head),销毁所有用new创建的节点,释放内存,否则会造成内存泄漏(Python会自动回收内存,不用手动处理)。
五、两种解法对比与总
为了让清晰区分两种解法,快速选择适合自己的方式,这里做一个详细的对比,同时梳理核心要点,帮你巩固知识点。
(一)两种解法核心对比
| 对比维度 | 解法1:数组转换法 | 解法2:快慢指针+反转链表 |
|---|---|---|
| 核心思路 | 链表转数组,双指针对比数组 | 快慢指针找中点,反转后半段,双指针对比 |
| 代码难度 | 简单(首选),逻辑直观,几乎无复杂操作 | 中等(进阶必备),需要掌握快慢指针和反转链表 |
| 时间复杂度 | O(n)(遍历链表+遍历数组,共2n次操作) | O(n)(快慢指针+反转+对比,共3n次操作,仍是O(n)) |
| 空间复杂度 | O(n)(需要数组存储所有节点值,占用额外空间) | O(1)(只用到几个指针,不占用额外空间,最优解) |
| 适用场景 | 新手练习、笔试快速答题(不用想复杂逻辑) | 面试进阶、要求空间优化的场景(面试官重点考察) |
(二)核心知识点回顾
这道题的核心是"链表操作"和"双指针用法",不管用哪种解法,都需要掌握以下知识点,记住这些,遇到类似题目也能轻松应对:
-
单链表的基本结构:节点由val(值)和next(指针)组成,只能从头节点开始顺序遍历;
-
回文的判断逻辑:正着读和反着读完全一致,核心是"首尾对比";
-
双指针的两种用法:① 普通双指针(首尾往中间走,用于对比);② 快慢指针(找链表中点,核心是"慢1快2");
-
反转链表的核心步骤:保存next节点→反转指针→移动pre和cur→循环直到cur为null;
-
C++和Python的核心差异:C++需要手动管理内存(new创建、delete销毁),指针用->访问属性;Python自动管理内存,节点属性用.访问。
(三)学习建议
-
先掌握解法1(数组转换法):把链表转数组是最直观的思路,先保证能写出正确代码,理解回文的判断逻辑;
-
再攻克解法2(最优解):先单独练习"快慢指针找中点"和"反转链表"两个小技巧,再结合起来写完整代码,多走几遍运行过程,理解每一步指针的移动;
-
多测试边界案例:比如"链表只有1个节点""链表长度为奇数/偶数",确保代码能处理所有情况;
-
对比两种解法的代码:找出相同点和不同点,理解"空间优化"的思路------为什么反转链表能节省空间?因为它没有用额外容器存储节点值,原地操作链表。
(四)常见问题解答
- 为什么解法2中,快慢指针能准确找到中点?
答:因为fast每次走2步,slow每次走1步,当fast走到链表末尾时,fast走的总步数是slow的2倍,所以slow正好走了链表长度的一半,也就是中点位置(偶数长度时,slow指向后半段的开头;奇数长度时,slow指向正中间节点)。
- 反转链表时,为什么要保存next_node?
答:因为反转时会把cur->next指向pre(前一个节点),如果不保存next_node,就会丢失cur后面的链表(相当于"断链"),后续无法继续反转,所以必须先保存下一个节点的地址。
- C++中为什么要销毁链表?不销毁会怎么样?
答:因为C++中用new创建的节点,会占用系统内存,程序结束后不会自动释放,不销毁会造成"内存泄漏"(内存被占用,无法被其他程序使用),虽然小案例中影响不大,但实际开发中会导致程序卡顿、崩溃,所以必须手动用delete销毁每个节点。
- 链表长度为1时,两种解法为什么都能返回true?
答:解法1中,数组只有1个元素,left和right初始都指向这个元素,left >= right,直接返回true;解法2中,快慢指针循环后,slow指向唯一的节点,反转后pre指向这个节点,对比时left和right都指向它,值相等,循环结束返回true。