LeetCode 热题 100-----24.回文链表

一、前置知识

在做这道题之前,我们必须先掌握几个核心基础知识点,不然看代码会完全懵。我会用"大白话+例子"的方式,把每个知识点讲透,哪怕你从来没学过编程、没听过"链表",也能看明白。

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(下一个节点)→ ...

  1. 先保存cur的下一个节点(next_node),因为反转后cur的next会指向前面的节点,不保存会丢失后面的链表;

  2. 把cur的next指针,指向"前一个节点"(pre,初始为null,因为第一个节点反转后是最后一个节点,next为null);

  3. 把pre移动到cur的位置(pre = cur),准备下一次反转;

  4. 把cur移动到之前保存的next_node位置(cur = next_node),继续下一次反转;

  5. 重复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:数组转换法(简单易懂,新手首选)

核心思路:利用"数组可以直接访问任意位置"的特点,把链表的所有节点值先存到数组里,再用双指针判断数组是否是回文------ 间接判断链表是否是回文。

步骤拆解(能跟着走):

  1. 遍历链表:从head开始,把每个节点的val依次存入数组;

  2. 双指针对比:左指针指向数组开头(索引0),右指针指向数组末尾(索引len(arr)-1);

  3. 逐位对比:如果左指针和右指针指向的值不相等,直接返回false(不是回文);如果相等,左指针右移、右指针左移,继续对比;

  4. 结束判断:当左指针 >= 右指针时,所有值都对比完了,返回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])为例,一步步看代码怎么运行:

  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。

  1. 调用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。

  1. 主代码打印结果:"是否为回文链表: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])为例:

  1. main函数中,先定义arr1数组,计算长度n1=4(sizeof(arr1)=4*4=16字节,sizeof(arr1[0])=4字节,16/4=4);

  2. 调用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;

  1. 调用solution.isPalindrome(head1),逻辑和Python完全一致:
  • 遍历链表,把val存入vector arr,得到arr=[1,2,2,1];

  • 双指针对比,left和right移动,所有值相等,返回true;

  1. 打印结果时,用(result1 ? "true" : "false")------C++中bool类型不能直接打印,需要用三目运算符转换为字符串;

  2. 调用destroyLinkedList(head1),手动销毁每个节点:因为C++中用new分配的内存,不会自动释放,不销毁会导致内存泄漏(占用多余内存),不用深究,记住"创建链表后要销毁"即可。

四、解法2:快慢指针+反转链表(最优解,满足进阶要求)

核心思路:不使用额外数组(节省空间),原地操作链表,满足O(n)时间、O(1)空间的要求。

步骤拆解(分步理解):

  1. 找中点:用快慢指针,找到链表的中间节点(slow指针最终指向中点);

  2. 反转后半段:把中点之后的链表反转(比如1→2→2→1,中点是第二个2,反转后半段得到1→2);

  3. 双指针对比:用左指针(指向head,前半段开头)和右指针(指向反转后后半段的开头),逐位对比值;

  4. 返回结果:如果所有值都相等,返回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)为例,一步步看:

  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。

  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。

  1. 第三步:双指针对比:
  • 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)为例,逐步讲解:

  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。

  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完全一致。

  1. 第三步:双指针对比(和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)(只用到几个指针,不占用额外空间,最优解)
适用场景 新手练习、笔试快速答题(不用想复杂逻辑) 面试进阶、要求空间优化的场景(面试官重点考察)

(二)核心知识点回顾

这道题的核心是"链表操作"和"双指针用法",不管用哪种解法,都需要掌握以下知识点,记住这些,遇到类似题目也能轻松应对:

  1. 单链表的基本结构:节点由val(值)和next(指针)组成,只能从头节点开始顺序遍历;

  2. 回文的判断逻辑:正着读和反着读完全一致,核心是"首尾对比";

  3. 双指针的两种用法:① 普通双指针(首尾往中间走,用于对比);② 快慢指针(找链表中点,核心是"慢1快2");

  4. 反转链表的核心步骤:保存next节点→反转指针→移动pre和cur→循环直到cur为null;

  5. C++和Python的核心差异:C++需要手动管理内存(new创建、delete销毁),指针用->访问属性;Python自动管理内存,节点属性用.访问。

(三)学习建议

  1. 先掌握解法1(数组转换法):把链表转数组是最直观的思路,先保证能写出正确代码,理解回文的判断逻辑;

  2. 再攻克解法2(最优解):先单独练习"快慢指针找中点"和"反转链表"两个小技巧,再结合起来写完整代码,多走几遍运行过程,理解每一步指针的移动;

  3. 多测试边界案例:比如"链表只有1个节点""链表长度为奇数/偶数",确保代码能处理所有情况;

  4. 对比两种解法的代码:找出相同点和不同点,理解"空间优化"的思路------为什么反转链表能节省空间?因为它没有用额外容器存储节点值,原地操作链表。

(四)常见问题解答

  1. 为什么解法2中,快慢指针能准确找到中点?

答:因为fast每次走2步,slow每次走1步,当fast走到链表末尾时,fast走的总步数是slow的2倍,所以slow正好走了链表长度的一半,也就是中点位置(偶数长度时,slow指向后半段的开头;奇数长度时,slow指向正中间节点)。

  1. 反转链表时,为什么要保存next_node?

答:因为反转时会把cur->next指向pre(前一个节点),如果不保存next_node,就会丢失cur后面的链表(相当于"断链"),后续无法继续反转,所以必须先保存下一个节点的地址。

  1. C++中为什么要销毁链表?不销毁会怎么样?

答:因为C++中用new创建的节点,会占用系统内存,程序结束后不会自动释放,不销毁会造成"内存泄漏"(内存被占用,无法被其他程序使用),虽然小案例中影响不大,但实际开发中会导致程序卡顿、崩溃,所以必须手动用delete销毁每个节点。

  1. 链表长度为1时,两种解法为什么都能返回true?

答:解法1中,数组只有1个元素,left和right初始都指向这个元素,left >= right,直接返回true;解法2中,快慢指针循环后,slow指向唯一的节点,反转后pre指向这个节点,对比时left和right都指向它,值相等,循环结束返回true。

相关推荐
爱怪笑的小杰杰2 小时前
Leaflet 实现轨迹拐角自动圆弧化:基于球面几何的高精度平滑算法
前端·javascript·算法·无人机
历程里程碑3 小时前
53 多路转接select
linux·开发语言·数据结构·数据库·c++·sql·排序算法
ccLianLian3 小时前
图论·刷题总结
算法·深度优先·图论
_深海凉_3 小时前
LeetCode热题100-二叉树展开为链表
算法·leetcode·链表
ECT-OS-JiuHuaShan3 小时前
什么是认知,认知的本质是什么?
数据库·人工智能·算法·机器学习·数学建模
Black蜡笔小新3 小时前
自动化AI算法训练服务器DLTM:筑牢数据安全底座,赋能企业AI高效安全落地
人工智能·算法·自动化
月殇_木言3 小时前
算法进阶(上)
算法
c++之路3 小时前
外观模式(Facade Pattern)
算法·外观模式
MicroTech20253 小时前
量子退火赋能金融,MLGO微算法科技构建量子金融生态
科技·算法·金融