【LeetCode Hot100 刷题日记(24/100)】234. 回文链表 —— 链表、双指针、快慢指针、原地操作🔄

16 阅读6分钟

🔄 题目链接:234. 回文链表 - 力扣(LeetCode)

🔍 难度:简单 | 🏷️ 标签:链表、双指针、快慢指针、原地操作

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)(进阶要求)


🧠 题目分析

给定一个单链表的头节点 head,判断该链表是否为回文链表

  • 回文定义:正读和反读内容一致。例如 [1,2,2,1] 是回文,而 [1,2] 不是。
  • 链表特性:只能单向遍历,无法像数组那样通过索引随机访问。
  • 关键挑战
    • 如何在 O(1) 空间 下完成判断?
    • 如何避免额外存储全部节点值?

💡 面试高频考点:链表操作 + 空间优化 + 快慢指针应用


🔑 核心算法及代码讲解

✅ 快慢指针 + 链表反转(最优解)

这是唯一满足 O(n) 时间 + O(1) 空间 的解法,也是面试官最希望看到的思路。

🧩 算法思想拆解:

  1. 快慢指针找中点

    • 慢指针 slow 每次走 1 步,快指针 fast 每次走 2 步。
    • fast 到达末尾时,slow 恰好位于前半部分的尾节点
    • 若链表长度为奇数(如 5 个节点),中点归入前半部分(即前 3 个)。
  2. 反转后半部分链表

    • slow->next 开始反转,得到新的头节点 secondHalfStart
    • 使用标准的 迭代反转链表 方法(LeetCode 206)。
  3. 双指针比较前后两半

    • p1head 开始,p2 从反转后的头开始。
    • 逐个比较值,一旦不等,立即返回 false
  4. 恢复链表结构(可选但推荐)

    • 实际工程中,不应修改输入数据,因此需将后半部分再反转回来。
    • 虽然 LeetCode 不检查结构是否恢复,但面试中体现工程素养!

🧪 核心函数详解(附行注释)

// 反转链表:标准迭代写法,O(n) 时间,O(1) 空间
ListNode* reverseList(ListNode* head) {
    ListNode* prev = nullptr;        // 前一个节点,初始为空
    ListNode* curr = head;           // 当前节点
    while (curr != nullptr) {
        ListNode* nextTemp = curr->next; // 保存下一个节点
        curr->next = prev;               // 反转当前指针
        prev = curr;                     // prev 前移
        curr = nextTemp;                 // curr 前移
    }
    return prev; // 新的头节点
}

// 快慢指针找前半部分尾节点
ListNode* endOfFirstHalf(ListNode* head) {
    ListNode* fast = head;
    ListNode* slow = head;
    // 注意条件:fast->next 和 fast->next->next 都不能为 null
    // 这样能确保 slow 停在“前半部分”的最后一个节点
    while (fast->next != nullptr && fast->next->next != nullptr) {
        fast = fast->next->next; // 快指针走两步
        slow = slow->next;       // 慢指针走一步
    }
    return slow; // 返回前半部分的尾节点
}

为什么条件是 fast->next && fast->next->next

  • 若链表有偶数个节点(如 4 个):fast 最终停在第 4 个,slow 在第 2 个 → 后半从第 3 个开始。
  • 若奇数个(如 5 个):fast 停在第 5 个,slow 在第 3 个 → 后半从第 4 个开始,中间节点忽略。
  • 完美分割!

🧭 解题思路(分步流程)

  1. 边界处理:若 head == nullptr,直接返回 true(空链表视为回文)。
  2. 找中点:调用 endOfFirstHalf(head) 得到前半尾节点。
  3. 反转后半:对 firstHalfEnd->next 执行反转,得到 secondHalfStart
  4. 双指针比对
    • p1 = headp2 = secondHalfStart
    • 循环直到 p2 == nullptr(后半结束)
    • 若任意 p1->val != p2->val,设 result = false 并继续(也可 break,但需保证后续能恢复链表)
  5. 恢复链表:将 secondHalfStart 再次反转,并接回 firstHalfEnd->next
  6. 返回结果

⚠️ 注意:即使提前发现非回文,也必须恢复链表!否则测试用例可能复用链表导致错误。


📊 算法分析

项目复杂度
时间复杂度O(n)
• 找中点:O(n/2)
• 反转后半:O(n/2)
• 比较:O(n/2)
• 恢复:O(n/2)
总计 ≈ O(2n) = O(n)
空间复杂度O(1)
仅使用常数个指针变量,无递归栈或额外数组

满足进阶要求!


💻 完整代码(保留原模板 + 行注释)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 链表节点定义(LeetCode 已内置,此处仅为本地编译兼容)
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) {}
};

// 反转链表函数
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;
}

// 快慢指针找前半部分尾节点
ListNode* endOfFirstHalf(ListNode* head) {
    ListNode* fast = head;
    ListNode* slow = head;
    while (fast->next != nullptr && fast->next->next != nullptr) {
        fast = fast->next->next;
        slow = slow->next;
    }
    return slow;
}

// 主解法函数
bool isPalindrome(ListNode* head) {
    if (head == nullptr) {
        return true;
    }

    // 步骤1:找到前半部分尾节点
    ListNode* firstHalfEnd = endOfFirstHalf(head);
    // 步骤2:反转后半部分
    ListNode* secondHalfStart = reverseList(firstHalfEnd->next);

    // 步骤3:双指针比较
    ListNode* p1 = head;
    ListNode* p2 = secondHalfStart;
    bool result = true;
    while (result && p2 != nullptr) {
        if (p1->val != p2->val) {
            result = false;
        }
        p1 = p1->next;
        p2 = p2->next;
    }

    // 步骤4:恢复链表(重要!)
    firstHalfEnd->next = reverseList(secondHalfStart);
    return result;
}

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 测试用例1: [1,2,2,1]
    ListNode* head1 = new ListNode(1);
    head1->next = new ListNode(2);
    head1->next->next = new ListNode(2);
    head1->next->next->next = new ListNode(1);
    cout << "Test 1: " << (isPalindrome(head1) ? "true" : "false") << "\n"; // true

    // 测试用例2: [1,2]
    ListNode* head2 = new ListNode(1);
    head2->next = new ListNode(2);
    cout << "Test 2: " << (isPalindrome(head2) ? "true" : "false") << "\n"; // false

    // 测试用例3: [1](单节点)
    ListNode* head3 = new ListNode(1);
    cout << "Test 3: " << (isPalindrome(head3) ? "true" : "false") << "\n"; // true

    // 测试用例4: [1,2,3,2,1](奇数回文)
    ListNode* head4 = new ListNode(1);
    head4->next = new ListNode(2);
    head4->next->next = new ListNode(3);
    head4->next->next->next = new ListNode(2);
    head4->next->next->next->next = new ListNode(1);
    cout << "Test 4: " << (isPalindrome(head4) ? "true" : "false") << "\n"; // true

    return 0;
}

🎯 面试加分点

  1. 强调空间优化意识:主动提出 O(1) 解法,展示对内存敏感的工程思维。
  2. 提及恢复链表:说明“虽然题目不要求,但实际开发中应保持输入不变”。
  3. 对比三种方法
    • 数组复制法:简单但 O(n) 空间;
    • 递归法:巧妙但隐含 O(n) 栈空间;
    • 快慢指针法:最优,体现扎实基本功。
  4. 快慢指针细节:能解释为何 fast->next && fast->next->next 能正确分割奇偶长度。

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第25题——141.环形链表(简单)

🔹 题目:给定一个链表,判断其是否有环。

🔹 核心思路:使用快慢指针(Floyd 判圈算法),快指针每次走两步,慢指针每次走一步,若相遇则有环。

🔹 考点:双指针、链表遍历、循环检测、空间优化。

🔹 难度:简单,但却是“链表环检测”问题的经典模型,常考于字节跳动、腾讯等大厂面试!

💡 提示:不要用哈希表存储节点地址,那样虽然直观但空间复杂度为 O(n),不是最优解!

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!