一、算法逻辑(通顺讲解每一步思路)
我们从 isPalindrome
这个主函数入手:
步骤 1:找到链表的中间节点 middleNode
-
使用 快慢指针法(slow 和 fast)
-
快指针一次走两步,慢指针一次走一步。
-
当快指针走到链表末尾时,慢指针刚好到达链表中点。
-
目的:将链表划分成前后两部分。
步骤 2:反转后半部分链表 reverseList
-
从中点开始,就地反转链表。
-
通过不断修改
next
指针,实现链表的反转。 -
最终得到后半段链表的头指针
head2
。
步骤 3:比较前半部分和反转后的后半部分是否相等
-
现在前半部分从
head
开始,后半部分从head2
开始。 -
每次比较两个节点的值是否相等。
-
若有一对值不同,说明不是回文,立即返回
False
。 -
若后半段全部匹配完了,说明是回文链表,返回
True
。
二、核心点
-
快慢指针找中点 是核心技巧之一,它在 O(n) 时间内找出链表中点,无需额外空间。
-
就地反转链表 则是另一个关键,它允许我们不借助栈或数组完成后半段反转,依然保持 O(1) 空间复杂度。
-
最后通过同时遍历前半部分与后半部分,实现了线性时间的比较。
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = nextclass Solution:# 876. 链表的中间结点def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:slow = fast = headwhile fast and fast.next:slow = slow.nextfast = fast.next.nextreturn slow# 206. 反转链表def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:pre, cur = None, headwhile cur:nxt = cur.nextcur.next = prepre = curcur = nxtreturn predef isPalindrome(self, head: Optional[ListNode]) -> bool:mid = self.middleNode(head)head2 = self.reverseList(mid)while head2:if head.val != head2.val: # 不是回文链表return Falsehead = head.nexthead2 = head2.nextreturn True
三、时间复杂度分析
设链表长度为 n
:
-
middleNode(head)
:遍历一次链表的一半,O(n) -
reverseList(mid)
:最多遍历一半链表,O(n) -
遍历比较两个链表:最多也是 O(n)
总时间复杂度:O(n)
四、空间复杂度分析
-
所有操作都是原地进行的,没有使用额外的数据结构(如栈或数组)
-
使用的变量为常数个指针
总空间复杂度:O(1)(常数空间)
✅ 总结一句话
该算法巧妙地结合了快慢指针、链表反转和双指针比较,在线性时间和常数空间下高效判断链表是否为回文。核心在于利用快慢指针找到中点 + 原地反转链表来规避额外空间使用。
我的疑问与解答:
这段代码中为什么不能使用直接 反转链表然后判断是否原链表和反转后的相同,这样的判断方式不可行吗
❓问题本质:
你提到的思路是:
“直接把整个链表反转一遍,然后和原链表一一比较是否相等”。
听起来直观、简单,但在链表这种数据结构下,这么做实际上是不可行的,除非我们付出额外代价。我们分析一下为啥。
🚫 为什么不能直接这么做?
❌ 原链表一旦反转,结构就被破坏了
链表是单向的引用结构,一旦你改变 next
指针,原来的链表结构就消失了。例如:
原链表: A -> B -> C 反转后: C -> B -> A
反转后,你再也无法回到原链表,两个链表共享节点,无法保留两个版本。
❌ 比较“原链表”和“反转链表”时无法共存
不像数组可以直接复制出一个副本(空间 O(n)),链表如果你想保留“原链表”+“反转链表”,只能:
-
额外复制整个链表结构(用 O(n) 空间);
-
或者做深拷贝 + 比较。
那样时间和空间复杂度都增加了,失去了当前算法 O(1) 空间的优势。
✅ 现在这段代码做了什么优化?
✅ 它没有修改前半部分链表
只从链表中点开始反转,因此:
-
前半部分结构保留了;
-
后半部分结构反转后,可以直接比较;
-
全过程不需要额外空间。
🔁 可选做法(进阶)
✅ 如果你真的想“保留链表结构不变”,可以在比较完后再反转一次恢复结构。
这是一些面试官会追问的点:“你破坏了链表结构,要不要恢复?”
你可以在最后加一句:
self.reverseList(head2) # 恢复后半部分结构(非必须,取决于题意)
✅ 结论总结:
方式 | 是否可行 | 空间复杂度 | 是否破坏链表结构 | 是否高效 |
---|---|---|---|---|
整体反转 + 比较原链表 | ❌ 不可行 | O(n) or 破坏原链表 | 会破坏结构 | 否 |
复制一份 + 反转副本 + 比较 | ✅ 可行 | O(n) | 不破坏 | 否 |
当前解法(快慢指针+反转后半段) | ✅ 优秀解 | O(1) | 前半结构保留 | 是 ✔️ |