回文链表判断是算法面试中的经典问题,也是检验程序员对链表操作基本功的试金石。题目要求我们判断一个单链表是否为回文结构,即链表节点值序列是否正读反读都相同。
在实际工程中,这种判断对称性的需求并不少见。比如在日志分析系统中,我们需要检测某些操作序列是否具有对称特征;在网络协议解析中,某些控制字段的排列也需要满足特定对称性。因此,掌握这个问题的解法具有实际应用价值。
关键点说明:
[1,2,3,2,1] 或 [1,2,2,1]数组辅助法是最直观的解决方案,其核心思想是利用数组的随机访问特性来解决链表无法回溯的问题。具体步骤如下:
提示:在实际编码中,使用 ArrayList 比普通数组更方便,因为它能动态扩容,无需预先知道链表长度。
时间复杂度:O(n)
空间复杂度:O(n)
适用场景:
java复制class Solution {
public boolean isPalindrome(ListNode head) {
// 边界条件处理
if (head == null || head.next == null) return true;
List<Integer> values = new ArrayList<>();
ListNode current = head;
// 遍历链表存储节点值
while (current != null) {
values.add(current.val);
current = current.next;
}
// 双指针比较
int left = 0, right = values.size() - 1;
while (left < right) {
// 使用equals()避免自动装箱带来的潜在问题
if (!values.get(left).equals(values.get(right))) {
return false;
}
left++;
right--;
}
return true;
}
}
关键注意事项:
equals() 而非 == 比较 Integer 对象这种优化空间复杂度的方法融合了两个经典技巧:
快慢指针找中点:
链表反转技巧:
前后半部分比较:
找中点的边界情况处理:
java复制// 快指针移动条件需要同时检查 fast 和 fast.next
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
链表反转的标准实现:
java复制private ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev; // 反转指针方向
prev = curr; // 移动prev
curr = nextTemp; // 移动curr
}
return prev; // 返回新的头节点
}
比较时的注意事项:
时间复杂度:O(n)
空间复杂度:O(1)
优势体现:
java复制class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) return true;
// 1. 使用快慢指针找到中点
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. 反转后半部分链表
ListNode reversedHalf = reverseList(slow);
// 3. 比较前后两部分
ListNode p1 = head, p2 = reversedHalf;
boolean result = true;
while (result && p2 != null) {
if (p1.val != p2.val) {
result = false;
}
p1 = p1.next;
p2 = p2.next;
}
// 4. 恢复链表结构(可选)
reverseList(reversedHalf);
return result;
}
private ListNode reverseList(ListNode head) {
ListNode prev = null, curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
}
问题1:空链表或单节点链表
java复制if (head == null || head.next == null) return true;
问题2:链表长度为2时的快慢指针处理
fast != null && fast.next != null问题:解法二反转了后半部分链表,是否会影响原链表结构?
java复制// 比较完成后执行
reverseList(reversedHalf);
问题:使用 == 比较 Integer 值可能出错
equals() 方法比较java复制if (!list.get(left).equals(list.get(right))) {
return false;
}
| 特性 | 数组辅助法 | 快慢指针+反转法 |
|---|---|---|
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(n) | O(1) |
| 实现难度 | 简单 | 中等 |
| 链表修改 | 不修改 | 临时修改后半部分 |
| 适用场景 | 内存充足,代码简洁优先 | 内存受限,追求最优解 |
进一步优化:
类似问题:
可视化辅助:
单元测试用例:
java复制// 测试用例示例
@Test
public void testPalindrome() {
// 偶数长度回文
ListNode head1 = buildList(new int[]{1,2,2,1});
assertTrue(isPalindrome(head1));
// 奇数长度回文
ListNode head2 = buildList(new int[]{1,2,3,2,1});
assertTrue(isPalindrome(head2));
// 非回文
ListNode head3 = buildList(new int[]{1,2,3});
assertFalse(isPalindrome(head3));
// 单节点
ListNode head4 = buildList(new int[]{1});
assertTrue(isPalindrome(head4));
// 空链表
assertTrue(isPalindrome(null));
}
java复制// 错误写法:可能抛出NullPointerException
while (fast.next != null && fast != null) {
// ...
}
// 正确写法:先检查fast是否为null
while (fast != null && fast.next != null) {
// ...
}
java复制// 错误写法:丢失next引用
curr.next = prev;
prev = curr;
curr = curr.next; // 此时curr.next已经是prev了!
// 正确写法:先保存next节点
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
java复制// 在数组法中,发现不匹配立即返回
while (left < right) {
if (!list.get(left).equals(list.get(right))) {
return false; // 提前终止
}
// ...
}
python复制def isPalindrome(self, head: ListNode) -> bool:
# 数组法示例
vals = []
current = head
while current:
vals.append(current.val)
current = current.next
return vals == vals[::-1]
Python特性利用:
cpp复制bool isPalindrome(ListNode* head) {
// 快慢指针法示例
if (!head || !head->next) return true;
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
// 反转后半部分
ListNode *prev = nullptr, *curr = slow;
while (curr) {
ListNode *next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
// 比较
ListNode *p1 = head, *p2 = prev;
while (p2) {
if (p1->val != p2->val) return false;
p1 = p1->next;
p2 = p2->next;
}
return true;
}
C++注意点:
nullptr 替代 null回文问题的通用解法模式:
链表问题的核心技巧:
基础能力:
优化意识:
沟通能力:
问题澄清:
解法演进:
代码实现:
数据校验:
日志分析:
数据结构验证:
java复制// 添加空值检查
public boolean isPalindrome(ListNode head) {
if (head == null) throw new IllegalArgumentException("链表不能为null");
// ...
}
java复制// 将链表反转提取为公共方法
public class LinkedListUtils {
public static ListNode reverseList(ListNode head) {
// 反转实现
}
}
java复制// 添加性能统计
long start = System.nanoTime();
boolean result = isPalindrome(head);
long duration = System.nanoTime() - start;
logger.debug("回文检查耗时:{}ns", duration);
快慢指针找中点:
code复制初始状态:
1 -> 2 -> 3 -> 2 -> 1
^slow
^fast
第一步后:
1 -> 2 -> 3 -> 2 -> 1
^slow
^fast
终止时:
1 -> 2 -> 3 -> 2 -> 1
^slow
^fast
链表反转过程:
code复制原链表:a -> b -> c -> d
第一步:null <- a b -> c -> d
第二步:null <- a <- b c -> d
第三步:null <- a <- b <- c d
最终: null <- a <- b <- c <- d
java复制public boolean isPalindrome(ListNode head) {
// 调试打印原始链表
printList("Original", head);
// ...算法逻辑...
// 调试打印反转后的部分
printList("Reversed", reversedHalf);
// ...
}
private void printList(String tag, ListNode head) {
System.out.print(tag + ": ");
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}
数组辅助法:
快慢指针法:
数组辅助法:
快慢指针法:
基本功能测试:
[1,2,3,2,1][1,2,3]边界条件测试:
[][1][1,1][1,2]性能测试:
java复制public class PalindromeTest {
@Test
public void testEvenLengthPalindrome() {
ListNode head = ListNodeUtils.buildList(new int[]{1,2,2,1});
assertTrue(new Solution().isPalindrome(head));
}
// 更多测试用例...
}
java复制public class ListNodeUtils {
public static ListNode buildList(int[] vals) {
// 根据数组构建链表
}
}
java复制class Solution {
// 主方法保持简洁
public boolean isPalindrome(ListNode head) {
if (head == null) return true;
ListNode mid = findMiddle(head);
ListNode reversed = reverseList(mid);
return compareLists(head, reversed);
}
// 辅助方法明确职责
private ListNode findMiddle(ListNode head) { /* ... */ }
private ListNode reverseList(ListNode head) { /* ... */ }
private boolean compareLists(ListNode l1, ListNode l2) { /* ... */ }
}
findMiddle, reverseListslowPtr, prevNodeprevious 而非 prev注释原则:
格式化建议:
问题:当链表太大无法完整放入内存时如何处理?
解决方案:
场景:链表数据分布在多个节点上
解决思路:
双向链表回文判断:
带环链表回文判断:
多级链表回文判断:
回文链表判断问题最早出现在算法教材中,用于教授:
早期解法:
优化突破:
现代变种:
在实际编码和面试准备中,我发现回文链表问题是一个绝佳的综合性练习。它不仅考察基本的编码能力,还需要考虑:
我建议初学者可以:
在面试场景中,沟通比完美更重要。即使不能立即写出最优解,清晰地表达思路并逐步优化,往往比沉默地写出正确代码更能获得面试官青睐。