🔄 题目链接:234. 回文链表 - 力扣(LeetCode)
🔍 难度:简单 | 🏷️ 标签:链表、双指针、快慢指针、原地操作
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)(进阶要求)
🧠 题目分析
给定一个单链表的头节点 head,判断该链表是否为回文链表。
- 回文定义:正读和反读内容一致。例如
[1,2,2,1]是回文,而[1,2]不是。 - 链表特性:只能单向遍历,无法像数组那样通过索引随机访问。
- 关键挑战:
- 如何在 O(1) 空间 下完成判断?
- 如何避免额外存储全部节点值?
💡 面试高频考点:链表操作 + 空间优化 + 快慢指针应用
🔑 核心算法及代码讲解
✅ 快慢指针 + 链表反转(最优解)
这是唯一满足 O(n) 时间 + O(1) 空间 的解法,也是面试官最希望看到的思路。
🧩 算法思想拆解:
-
快慢指针找中点
- 慢指针
slow每次走 1 步,快指针fast每次走 2 步。 - 当
fast到达末尾时,slow恰好位于前半部分的尾节点。 - 若链表长度为奇数(如 5 个节点),中点归入前半部分(即前 3 个)。
- 慢指针
-
反转后半部分链表
- 从
slow->next开始反转,得到新的头节点secondHalfStart。 - 使用标准的 迭代反转链表 方法(LeetCode 206)。
- 从
-
双指针比较前后两半
p1从head开始,p2从反转后的头开始。- 逐个比较值,一旦不等,立即返回
false。
-
恢复链表结构(可选但推荐)
- 实际工程中,不应修改输入数据,因此需将后半部分再反转回来。
- 虽然 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 个开始,中间节点忽略。- 完美分割!
🧭 解题思路(分步流程)
- 边界处理:若
head == nullptr,直接返回true(空链表视为回文)。 - 找中点:调用
endOfFirstHalf(head)得到前半尾节点。 - 反转后半:对
firstHalfEnd->next执行反转,得到secondHalfStart。 - 双指针比对:
p1 = head,p2 = secondHalfStart- 循环直到
p2 == nullptr(后半结束) - 若任意
p1->val != p2->val,设result = false并继续(也可 break,但需保证后续能恢复链表)
- 恢复链表:将
secondHalfStart再次反转,并接回firstHalfEnd->next。 - 返回结果。
⚠️ 注意:即使提前发现非回文,也必须恢复链表!否则测试用例可能复用链表导致错误。
📊 算法分析
| 项目 | 复杂度 |
|---|---|
| 时间复杂度 | 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;
}
🎯 面试加分点
- 强调空间优化意识:主动提出 O(1) 解法,展示对内存敏感的工程思维。
- 提及恢复链表:说明“虽然题目不要求,但实际开发中应保持输入不变”。
- 对比三种方法:
- 数组复制法:简单但 O(n) 空间;
- 递归法:巧妙但隐含 O(n) 栈空间;
- 快慢指针法:最优,体现扎实基本功。
- 快慢指针细节:能解释为何
fast->next && fast->next->next能正确分割奇偶长度。
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第25题——141.环形链表(简单)
🔹 题目:给定一个链表,判断其是否有环。
🔹 核心思路:使用快慢指针(Floyd 判圈算法),快指针每次走两步,慢指针每次走一步,若相遇则有环。
🔹 考点:双指针、链表遍历、循环检测、空间优化。
🔹 难度:简单,但却是“链表环检测”问题的经典模型,常考于字节跳动、腾讯等大厂面试!
💡 提示:不要用哈希表存储节点地址,那样虽然直观但空间复杂度为 O(n),不是最优解!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!