一、算法邏輯(通順講解每一步思路)
我們從 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) | 前半結構保留 | 是 ?? |