【算法】234.回文链表--通俗讲解

26 阅读5分钟

一、题目是啥?一句话说清

给定一个单链表,判断它是否为回文链表,即从前往后和从后往前读是一样的。

示例:

  • 输入:1 → 2 → 2 → 1
  • 输出:true

二、解题核心

使用快慢指针找到链表中点,反转后半部分链表,然后比较前半部分和反转后的后半部分是否相同。

这就像把链表从中间折断,反转后半段,然后比较两段是否匹配,就像检查镜子中的影像是否与原物一致。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 快慢指针找中点

  • 是什么:使用快指针(每次两步)和慢指针(每次一步)找到链表的中间节点。
  • 为什么重要:这样可以在一次遍历中找到中点,将链表分成两半,为后续反转和比较做准备。

2. 反转链表

  • 是什么:将后半部分链表反转,使后半部分的节点顺序倒过来。
  • 为什么重要:反转后,我们可以直接从后半部分的头节点开始遍历,与前半部分比较,判断是否相同。

3. 比较前后两半

  • 是什么:同时遍历前半部分和反转后的后半部分,比较每个节点的值。
  • 为什么重要:这是判断回文的关键步骤,如果所有节点值都相同,则是回文链表;否则不是。

四、看图理解流程(通俗理解版本)

假设链表为:1 → 2 → 3 → 2 → 1

  1. 找中点

    • 快慢指针:慢指针从1开始,快指针从1开始。
    • 第一轮:慢指针走到2,快指针走到3。
    • 第二轮:慢指针走到3,快指针走到1(快指针走两步后到达末尾)。
    • 中点是节点3。将链表分成前半部分:1→2→3 和后半部分:2→1。
  2. 反转后半部分

    • 后半部分2→1,反转后变成1→2。
  3. 比较前后两半

    • 前半部分:1→2→3(但比较时只用到1和2,因为后半部分只有两个节点)
    • 反转后半部分:1→2
    • 比较:1==1,2==2,所有节点匹配,是回文链表。

如果不匹配,例如链表1→2→3→3→1:

  • 反转后半部分3→1变成1→3
  • 比较:1==1,但2≠3,不是回文。

五、C++ 代码实现(附详细注释)

#include <iostream>
using namespace std;

// 链表节点定义
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) {
        if (head == nullptr || head->next == nullptr) {
            return true; // 空链表或单节点链表是回文
        }
        
        // 使用快慢指针找到链表中点
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast->next != nullptr && fast->next->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
        }
        
        // 反转后半部分链表
        ListNode* secondHalf = reverseList(slow->next);
        ListNode* firstHalf = head;
        
        // 比较前后两半
        while (secondHalf != nullptr) {
            if (firstHalf->val != secondHalf->val) {
                return false;
            }
            firstHalf = firstHalf->next;
            secondHalf = secondHalf->next;
        }
        return true;
    }
    
private:
    // 反转链表函数
    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* curr = head;
        while (curr != nullptr) {
            ListNode* nextTemp = curr->next;
            curr->next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
};

// 辅助函数:打印链表
void printList(ListNode* head) {
    while (head != nullptr) {
        cout << head->val << " ";
        head = head->next;
    }
    cout << endl;
}

// 测试代码
int main() {
    // 创建示例链表:1->2->2->1
    ListNode* head = new ListNode(1);
    head->next = new ListNode(2);
    head->next->next = new ListNode(2);
    head->next->next->next = new ListNode(1);
    
    Solution solution;
    bool result = solution.isPalindrome(head);
    
    cout << (result ? "true" : "false") << endl; // 输出:true
    
    // 释放内存(简单示例,实际中可能需要更完整的释放)
    while (head != nullptr) {
        ListNode* temp = head;
        head = head->next;
        delete temp;
    }
    
    return 0;
}

六、时间空间复杂度

  • 时间复杂度:O(n),其中n是链表长度。找中点、反转链表和比较都是线性时间。
  • 空间复杂度:O(1),只使用了常数额外空间(几个指针),没有使用递归或其他数据结构。

七、注意事项

  • 空链表和单节点链表:直接返回true,因为它们被认为是回文。
  • 快慢指针的起始点:快慢指针都从头节点开始,快指针每次两步,慢指针每次一步,确保慢指针指向中点或中点前一个节点(取决于链表长度奇偶性)。
  • 反转链表:在反转时,注意保存下一个节点,避免链表断裂。
  • 比较时的节点数:由于链表长度可能为奇数,比较时以后半部分长度为准,因为前半部分可能多一个节点(但不需要比较中间节点)。
  • 恢复链表:如果题目要求保持链表原状,可以在比较后再次反转后半部分恢复原链表,但本题只要求返回布尔值,所以可以不恢复。

算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解
【算法】【链表】148.排序链表--通俗讲解
【算法】【链表】160.相交链表--通俗讲解
【算法】【链表】203.移除链表元素--通俗讲解
【算法】【链表】206.反转链表--通俗讲解


关注公众号指针诗笺,获取更多底层机制/ 算法通俗讲解干货!