回文链表是LeetCode上经典的链表操作题目,编号234。这道题之所以被广泛讨论,是因为它巧妙地结合了链表遍历、指针操作和空间复杂度优化等多个核心概念。在实际面试中,这道题出现的频率相当高,因为它能很好地考察面试者对链表结构的理解程度和算法优化能力。
所谓回文链表,是指正序和逆序读取节点值完全一致的链表。例如1→2→2→1和1→3→1都是典型的回文链表。判断链表是否为回文,最直观的做法是将链表值存入数组,然后用双指针法判断数组是否为回文。但这种方法需要O(n)的额外空间,不符合题目对空间复杂度的要求。
最优解法采用"快慢指针找中点+反转右半链表+双指针比对"的三步策略。这个设计的精妙之处在于:
这种分步处理的方式既保证了时间效率,又完美满足了空间复杂度的要求。
快慢指针法是链表问题中的常用技巧。在这个问题中:
设链表长度为n:
这种定位方式比先遍历计算长度再定位中点要高效得多,只需要一次遍历。
任何链表问题都需要首先考虑边界条件:
这些边界情况可以提前处理,避免后续不必要的计算。
实现快慢指针时需要注意:
java复制ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
关键点:
反转链表是另一个基础但重要的操作:
java复制ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
这个实现使用了迭代法,空间复杂度O(1)。需要注意:
比对阶段需要注意:
java复制ListNode left = head;
ListNode right = reversedRight;
while (right != null) {
if (left.val != right.val) {
return false;
}
left = left.next;
right = right.next;
}
return true;
这里只需要比较到右半部分结束即可,因为:
让我们详细分析每个步骤的时间复杂度:
虽然常数因子是1.5,但在大O表示法中仍记为O(n)。
整个算法只使用了固定数量的指针变量:
虽然这个解法已经是最优解,但仍有可以微调的地方:
java复制// 错误写法
while (fast != null) { // 可能抛出NullPointerException
slow = slow.next;
fast = fast.next.next;
}
java复制// 错误写法
ListNode curr = head;
while (curr != null) {
curr.next = prev; // 丢失了原curr.next的引用
prev = curr;
curr = curr.next; // curr已经指向prev,导致死循环
}
java复制// 错误写法
while (left != null && right != null) { // 对于奇数长度会多比较一次
...
}
全面的测试用例应包括:
java复制/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
// 1. 使用快慢指针找到中点
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. 反转后半部分链表
ListNode reversedRight = reverseList(slow);
// 3. 比较前后两部分
ListNode left = head;
ListNode right = reversedRight;
while (right != null) {
if (left.val != right.val) {
return false;
}
left = left.next;
right = right.next;
}
return true;
}
private ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
}
java复制if (head == null || head.next == null) {
return true;
}
处理了空链表和单节点链表的特殊情况。
java复制while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
确保fast可以安全地移动两步。
java复制ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
经典的链表反转四步操作,注意保存next节点。
java复制while (right != null) {
if (left.val != right.val) {
return false;
}
left = left.next;
right = right.next;
}
只需要比较到右半部分结束即可。
这种"找中点+反转+比对"的思路可以解决多种链表问题:
朴素解法(使用栈存储):
最优解法:
在LeetCode评测系统中:
虽然时间差距不大,但空间使用差异明显,特别是对于大规模链表。
但算法核心思想在所有语言中都适用。
回文链表问题看似简单,但蕴含着链表操作的精髓。在实际编码中,我发现以下几点特别重要:
这个算法最巧妙的地方在于通过反转后半部分来避免使用额外空间,这种"空间换时间"或"时间换空间"的权衡在算法设计中非常常见。掌握这种思路后,很多链表问题都能迎刃而解。