一、题目是啥?一句话说清
给定一个单链表,判断它是否为回文链表,即从前往后和从后往前读是一样的。
示例:
- 输入:1 → 2 → 2 → 1
- 输出:true
二、解题核心
使用快慢指针找到链表中点,反转后半部分链表,然后比较前半部分和反转后的后半部分是否相同。
这就像把链表从中间折断,反转后半段,然后比较两段是否匹配,就像检查镜子中的影像是否与原物一致。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 快慢指针找中点
- 是什么:使用快指针(每次两步)和慢指针(每次一步)找到链表的中间节点。
- 为什么重要:这样可以在一次遍历中找到中点,将链表分成两半,为后续反转和比较做准备。
2. 反转链表
- 是什么:将后半部分链表反转,使后半部分的节点顺序倒过来。
- 为什么重要:反转后,我们可以直接从后半部分的头节点开始遍历,与前半部分比较,判断是否相同。
3. 比较前后两半
- 是什么:同时遍历前半部分和反转后的后半部分,比较每个节点的值。
- 为什么重要:这是判断回文的关键步骤,如果所有节点值都相同,则是回文链表;否则不是。
四、看图理解流程(通俗理解版本)
假设链表为:1 → 2 → 3 → 2 → 1
-
找中点:
- 快慢指针:慢指针从1开始,快指针从1开始。
- 第一轮:慢指针走到2,快指针走到3。
- 第二轮:慢指针走到3,快指针走到1(快指针走两步后到达末尾)。
- 中点是节点3。将链表分成前半部分:1→2→3 和后半部分:2→1。
-
反转后半部分:
- 后半部分2→1,反转后变成1→2。
-
比较前后两半:
- 前半部分: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.反转链表--通俗讲解
关注公众号指针诗笺,获取更多底层机制/ 算法通俗讲解干货!