【LeetCode/牛客】链表的回文结构:如何用 O(1) 空间"折叠"链表?
在判断字符串或数组是否回文时,我们通常使用双指针从两头向中间逼近。但对于单向链表,我们无法轻易地"从后往前"遍历。
最直观的方法是将链表元素存入数组或栈中,但这就需要 O(N)O(N)O(N) 的额外空间。如果题目卡死了 空间复杂度 O(1),我们该怎么办?
今天我们要拆解的,就是一种通过修改链表结构来实现回文判断的高阶解法。
1. 核心解题思路:找到中点,折叠比较
既然无法从后往前遍历,那我们不妨把链表的后半段反转过来,让它指向中间。这样,我们就可以分别从"头"和"尾"出发,向中间走,一一比对。
整个过程分为三步(这也是代码中的三个主要模块):
- 快慢指针找中点:确定链表的一半在哪里。
- 反转后半部分:将后半段链表的指针方向反转,使其指向中点。
- 双指针汇合判断:一个从头走,一个从尾走,检查数值是否相等。
2. 代码深度解析
让我们结合代码,一步步看懂这个过程。
第一步:快慢指针找中点
java
// 1、快慢双指针遍历找到中间元素
ListNode fast = A;
ListNode slow = A;
while(fast != null && fast.next != null)
{
fast = fast.next.next;
slow = slow.next;
}
- 原理 :
fast一次走两步,slow一次走一步。当fast走到链表末尾时,slow刚好走到链表的中点。 - 细节 :
- 如果是奇数个节点(如
1->2->3->2->1),slow会停在正中间(3)。 - 如果是偶数个节点(如
1->2->2->1),slow会停在前半段的最后一个(第一个2)。
- 如果是奇数个节点(如
第二步:反转后半部分(高能预警)
这是代码中最精妙也最容易晕的地方:
java
// 2、反转链表
ListNode cur = slow.next;
while(cur != null)
{
ListNode curN = cur.next;
cur.next = slow; // 【关键】反转指向,指向前一个节点
slow = cur; // slow 移动到当前节点,作为新的"头"
cur = curN; // cur 继续处理下一个
}
此时链表变成了什么样?
假设链表是 1 -> 2 -> 3 -> 2 -> 1。
经过这一步,原本向右指的箭头,在后半段变成了向左指:
结构变成了:1 -> 2 -> 3 <- 2 <- 1。
A依然指向头部1。- 注意 :此时变量
slow已经跑到了链表的最末尾 (也就是反转后的"新头"),即右边的1。
第三步:双向逼近判断
java
// 3、判断回文
while(A != slow)
{
// 值不相等,直接判定不是回文
if(A.val != slow.val)
{
return false;
}
// 【易错点高亮】偶数节点的特殊处理
if(A.next == slow)
{
return true;
}
slow = slow.next; // 从后往前走(因为指针反转了)
A = A.next; // 从前往后走
}
return true;
3. ⚠️ 易错点与逻辑陷阱(重点)
这段代码虽然短,但有两个非常容易写错的逻辑,必须重点标注!
易错点 1:反转逻辑中的节点连接
很多人在反转链表时习惯把 slow.next 置空,或者切断中间的联系。但在这个解法中,我们保留了中间的连接。
cur.next = slow:这一句把后半段的节点反向指回了slow。- 对于奇数链表,中间节点(如
3)成为了连接点,左边指向它,右边也指向它。
易错点 2:偶数链表的判断陷阱 (A.next == slow)
这是整段代码最容易被忽略的细节!
假设链表是偶数长度:1 -> 2 -> 2 -> 1。
- 反转后结构变成:
1 -> 2(前) <-> 2(后) <- 1。 A在左边1,slow在右边1。值相等,继续走。A走到2(前),slow走到2(后)。- 此时注意 :
A和slow指向的是两个不同的节点对象,虽然它们的值都是 2,但内存地址不同,所以while(A != slow)循环继续。 - 在下一轮循环前,如果没有
if(A.next == slow)这个判断:A会继续往后走到2(后)。slow会继续往前走到2(前)。- 两人"擦肩而过",导致循环多跑或者逻辑混乱。
必须提前判断:
当 A 的下一个节点就是 slow 时(即 A 和 slow 相邻),说明对于偶数链表来说,已经比对完毕了,直接返回 true。
4. 总结
这道题考核了对链表指针的绝对掌控力。
- 常规解法:栈/数组,空间 O(N)。
- 大神解法:快慢指针找中点 + 局部反转,空间 O(1)。
代码通过原地修改链表指向 ,巧妙地把一个单向链表变成了"两头往中间走"的结构,尤其是对偶数链表 A.next == slow 的判断,是点睛之笔。
虽然这种做法破坏了原链表的结构(如果需要还原,还需要再反转一次),但在算法题中,这是追求极致空间效率的标准答案。
这个算法在面试中非常高频,理解了它,链表类的题目基本就通了一半。