1. 回文链表问题概述
回文链表是力扣(LeetCode)平台上的一道经典算法题,编号为234题。题目要求判断一个单链表是否为回文结构,即正读和反读都相同的链表。这道题在技术面试中频繁出现,尤其是对初级和中级的软件工程师岗位考察频率极高。
链表结构本身的特点是只能单向遍历,这与判断回文所需的双向对比形成了天然矛盾。因此这道题目很好地考察了以下几个核心能力:
- 对链表数据结构的深入理解
- 指针操作的熟练程度
- 时间/空间复杂度的权衡意识
- 边界条件的处理能力
在实际面试中,面试官通常会要求给出时间复杂度O(n)且空间复杂度O(1)的解法,这就需要对链表进行原地(in-place)操作。下面我将详细介绍三种不同思路的C++实现,并分析各自的优劣。
2. 基础解法:使用额外存储空间
2.1 数组复制法
最直观的思路是将链表的值复制到数组中,然后使用双指针法判断数组是否为回文:
bool isPalindrome(ListNode* head) { vector<int> vals; while (head != nullptr) { vals.push_back(head->val); head = head->next; } int left = 0, right = vals.size() - 1; while (left < right) { if (vals[left] != vals[right]) return false; left++; right--; } return true; }复杂度分析:
- 时间复杂度:O(n),遍历链表和数组各一次
- 空间复杂度:O(n),需要额外数组存储所有节点值
注意:这种方法虽然简单,但在面试中通常不会被接受为最优解,因为它没有利用链表的特性,且空间复杂度不符合O(1)的要求。
2.2 递归解法
利用递归栈隐式存储节点值的解法:
ListNode* frontPointer; bool recursivelyCheck(ListNode* currentNode) { if (currentNode != nullptr) { if (!recursivelyCheck(currentNode->next)) return false; if (currentNode->val != frontPointer->val) return false; frontPointer = frontPointer->next; } return true; } bool isPalindrome(ListNode* head) { frontPointer = head; return recursivelyCheck(head); }复杂度分析:
- 时间复杂度:O(n),需要完整遍历链表
- 空间复杂度:O(n),递归调用栈的深度与链表长度成正比
技巧:递归解法展示了函数调用栈如何隐式存储节点信息,但实际面试中应优先考虑迭代解法,因为递归可能引发栈溢出问题。
3. 优化解法:O(1)空间复杂度
3.1 快慢指针找中点
要实现O(1)空间复杂度,关键是在不存储全部节点的情况下比较前半部分和后半部分。具体步骤:
- 使用快慢指针找到链表中点
- 反转后半部分链表
- 比较前后两部分
- 恢复链表原状(可选)
bool isPalindrome(ListNode* head) { if (head == nullptr || head->next == nullptr) return true; // 步骤1:找到中点 ListNode *slow = head, *fast = head; while (fast->next != nullptr && fast->next->next != nullptr) { slow = slow->next; fast = fast->next->next; } // 步骤2:反转后半部分 ListNode *prev = nullptr, *curr = slow->next; while (curr != nullptr) { ListNode *nextTemp = curr->next; curr->next = prev; prev = curr; curr = nextTemp; } // 步骤3:比较前后部分 ListNode *p1 = head, *p2 = prev; bool result = true; while (p2 != nullptr) { if (p1->val != p2->val) { result = false; break; } p1 = p1->next; p2 = p2->next; } // 步骤4:恢复链表(面试中常被忽略但实际工程中很重要) curr = prev; prev = nullptr; while (curr != nullptr) { ListNode *nextTemp = curr->next; curr->next = prev; prev = curr; curr = nextTemp; } slow->next = prev; return result; }复杂度分析:
- 时间复杂度:O(n),遍历链表多次但都是线性时间
- 空间复杂度:O(1),只使用了固定数量的指针变量
避坑指南:快慢指针的终止条件容易写错,常见错误有:
while (fast != nullptr && fast->next != nullptr)会导致slow停在正确位置的后一个节点- 忘记处理链表长度为奇数的情况
3.2 边找中点边反转
更优化的解法可以在找中点的同时反转前半部分链表:
bool isPalindrome(ListNode* head) { ListNode *slow = head, *fast = head, *prev = nullptr, *temp; while (fast != nullptr && fast->next != nullptr) { fast = fast->next->next; // 反转slow指针经过的节点 temp = slow->next; slow->next = prev; prev = slow; slow = temp; } // 处理奇数长度情况 if (fast != nullptr) slow = slow->next; // 比较两部分 while (slow != nullptr) { if (slow->val != prev->val) return false; slow = slow->next; prev = prev->next; } return true; }这种实现减少了链表遍历次数,但代码逻辑更为复杂,适合对指针操作非常熟悉的面试者。
4. 边界条件与测试用例
完善的解法需要考虑各种边界情况,以下是必须测试的场景:
- 空链表(nullptr)
- 单节点链表(天然回文)
- 双节点相同值链表(如 1->1)
- 双节点不同值链表(如 1->2)
- 奇数长度回文链表(如 1->2->3->2->1)
- 偶数长度回文链表(如 1->2->2->1)
- 非回文链表(如 1->2->3)
- 长链表(测试性能和栈溢出)
在LeetCode上提交前,建议至少测试上述前7种情况。实际面试中,面试官可能会要求你口头描述这些测试用例。
5. 算法选择与面试策略
不同解法的适用场景:
| 解法类型 | 时间复杂度 | 空间复杂度 | 适用场景 | 面试评价 |
|---|---|---|---|---|
| 数组复制 | O(n) | O(n) | 笔试快速实现 | 基本合格 |
| 递归 | O(n) | O(n) | 展示递归思维 | 中等评价 |
| 快慢指针 | O(n) | O(1) | 技术面试 | 高度评价 |
| 边找边反 | O(n) | O(1) | 高阶面试 | 优秀评价 |
面试时的回答策略建议:
- 先说明最简单的数组解法,展示问题理解
- 分析其空间复杂度问题
- 提出快慢指针优化思路
- 讨论边界条件和实现细节
- 最后提及可能的进一步优化(如边找边反)
经验分享:在实际面试中,写出完整正确的快慢指针解法通常就能获得肯定。不必一开始就追求最优解,而是展示出逐步优化的思考过程更为重要。
6. 链表操作常见错误
在实现回文链表判断时,以下几个错误最为常见:
快慢指针终止条件错误:
// 错误写法:会导致slow多走一步 while (fast != nullptr && fast->next != nullptr) // 正确写法: while (fast->next != nullptr && fast->next->next != nullptr)反转链表时的指针丢失: 忘记保存next节点就直接修改next指针,导致链表断裂。
奇数长度处理不当: 忘记在比较前调整slow指针位置:
if (fast != nullptr) slow = slow->next;未恢复链表原状: 虽然不影响判断结果,但在实际工程中修改输入数据结构是不良实践。
内存访问越界: 没有检查nullptr就直接访问节点成员,如
fast->next->next。
7. 相关题目拓展
掌握回文链表后,可以尝试解决以下变种问题:
- 回文链表II(力扣143):判断链表是否可以通过重排成为回文
- 回文数(力扣9):不使用字符串判断整数是否为回文
- 最长回文子串(力扣5):字符串中的最长回文子串
- 回文对(力扣336):找出所有能组成回文的字符串对
- 回文排列(力扣266):判断字符串能否排列成回文
这些题目都基于相似的回文判断思想,但数据结构和应用场景各不相同,适合用来巩固和扩展回文相关的算法能力。