24 兩兩交換節點
func swapPairs(head *ListNode) *ListNode {
// 思路 涉及到連結串列增刪改操作,優先考慮使用虛擬頭節點 此處為雙指標加上虛擬頭節點
if head == nil || head.Next == nil {
return head
}
var dummyHead = &ListNode{0, head}
var prev = dummyHead
for prev.Next != nil && prev.Next.Next != nil { // 同時考慮奇數長度連結串列和偶數長度連結串列終止條件
node1 := prev.Next
node2 := prev.Next.Next
node1.Next = node2.Next
node2.Next = node1
prev.Next = node2
prev = node1
}
return dummyHead.Next
}
時間 跳著便利連結串列本質上n/2 n 空間 有限單變數 1
func swapPairs(head *ListNode) *ListNode {
// 思路 嘗試根據雙指標 寫出 遞迴
if head == nil || head.Next == nil {
return head
}
var dummyHead = &ListNode{0, head}
cur := dummyHead
swap(cur)
return dummyHead.Next
}
func swap(cur *ListNode) {
if cur.Next == nil || cur.Next.Next == nil { // 根據外層for迴圈判斷遞迴終止條件
return
}
// 迴圈體的交換節點結構
node1 := cur.Next
node2 := cur.Next.Next
node1.Next = node2.Next
node2.Next = node1
cur.Next = node2
// 遞迴條件,即變數的迭代條件prev = node1
swap(node1)
return
}
19 刪除連結串列倒數第n個節點
func removeNthFromEnd(head *ListNode, n int) *ListNode {
// 思路 連結串列的刪除操作,優先考慮虛擬頭節點
// 然後嘗試雙指標,看了影片後的思路很清晰,重點 難點是n就是兩個指標之間的間隔,cur == nil 就刪除 pre
var dummyHead = &ListNode{0, head}
var pre, cur = dummyHead, dummyHead
for i:=0 ; i<n; i++ { // 此處作用往後跳n個節點,題目已經說明n<size,所以不考慮越界情況
cur = cur.Next
}
for cur.Next != nil { // cur == nil 迴圈終止,刪除pre, 但是這樣寫我們沒辦法處理刪除將pre上一個元素與pre之後連線,除非單獨變數儲存,所以這裡採用cur.next != nil, pre儲存上一個變數,要刪除pre.Next
cur = cur.Next
pre = pre.Next // pre 永遠走在cur之後,所以不會空指標
}
// 遍歷完成,此時pre位於刪除元素上一個節點
pre.Next = pre.Next.Next // 直接next指向刪除元素下一個節點,刪除完成
return dummyHead.Next
}
// 時間 遍歷整個連結串列 n-常數間隔 = n 空間 有限單變數 1
160 判斷連結串列相交節點
func getIntersectionNode(headA, headB *ListNode) *ListNode {
// 思路 類似雙指標 快慢指標 快指標遍歷連結串列a, 慢指標遍歷連結串列b, 如果節點記憶體地址相等,視為相交
for headA != nil {
cur := headB // 由於是指標型別引用,所以這裡的記憶體地址相同與head2
for cur != nil { // 這裡使用cur而不是用head2是因為內部便利之後head已經變成尾節點了,不能直接使用了
if cur == headA { // 判斷記憶體地址是否相等
return headA
}
cur = cur.Next
}
headA = headA.Next
}
return nil
}
// 思考
為甚麼判斷記憶體地址 &cur == &headA 這樣子寫是錯誤的?
因為 cur, head 是指標,指向的是其他變數的記憶體地址,但是指標也是變數,也有自己的記憶體地址,如果對指標再取指標,那麼取得就是兩個指標變數的記憶體地址,就不是我們想要的對比儲存變數的記憶體地址結果了
時間 m*n 空間 1
-
迴圈連結串列的思路(時間複雜度m+n)
-
數學邏輯解釋
假設連結串列 A 的長度為 (m),連結串列 B 的長度為 (n),它們在節點 (C) 處相交。連結串列 A 在相交前的部分長度為 (a),連結串列 B 在相交前的部分長度為 (b),相交部分的長度為 (c)。
因此,我們有:
[ m = a + c ]
[ n = b + c ]
- 雙指標方法的邏輯
初始化:兩個指標 (pA) 和 (pB) 分別指向連結串列 A 和連結串列 B 的頭節點。
遍歷連結串列:兩個指標同時向前移動,如果到達連結串列末尾,則重定位到另一個連結串列的頭節點。
相遇:由於兩個指標遍歷的總長度相同,最終會在相交節點處相遇。
詳細解釋
當 (pA) 遍歷完連結串列 A 後,它將重定位到連結串列 B 的頭節點,並繼續遍歷連結串列 B。
當 (pB) 遍歷完連結串列 B 後,它將重定位到連結串列 A 的頭節點,並繼續遍歷連結串列 A。
經過第一次遍歷後,兩個指標分別走過的路徑長度為:
[ pA: a + c + b ]
[ pB: b + c + a ]
由於 (a + c + b = b + c + a),因此兩個指標在第二次遍歷中會在相交節點 (C) 處相遇。
- 數學推導
我們可以透過數學推導來驗證這種方法的正確性:
第一次遍歷:
指標 (pA) 走過的路徑長度為 (a + c),然後重定位到連結串列 B。
指標 (pB) 走過的路徑長度為 (b + c),然後重定位到連結串列 A。
第二次遍歷:
指標 (pA) 繼續走過連結串列 B 的長度 (b)。
指標 (pB) 繼續走過連結串列 A 的長度 (a)。
由於兩個指標走過的總路徑長度相同,最終會在相交節點 (C) 處相遇。
func getIntersectionNode(headA, headB *ListNode) *ListNode {
// 思路 迴圈連結串列的思路。只需要不到兩圈就能查詢到相交點
nodeA, nodeB := headA, headB
for nodeA != nodeB { // 未到相交點
// 遍歷連結串列a
if nodeA == nil { // 遍歷完a遍歷b
nodeA = headB
}else{
nodeA = nodeA.Next
}
// b同理
if nodeB == nil {
nodeB = headA
}else{
nodeB = nodeB.Next
}
}
return nodeA
}
func getIntersectionNode(headA, headB *ListNode) *ListNode {
// 思路 根據雙指標寫出遞迴
return checkInter(headA, headB, headA, headB)
}
func checkInter(nodeA, nodeB, headA, headB *ListNode) *ListNode {
if nodeA == nodeB { // 迴圈結束條件就是遞迴終止條件
return nodeA
}
// 遍歷連結串列a
if nodeA == nil { // 遍歷完a遍歷b
nodeA = headB
}else{
nodeA = nodeA.Next
}
// b同理
if nodeB == nil {
nodeB = headA
}else{
nodeB = nodeB.Next
}
return checkInter(nodeA, nodeB, headA, headB)
}
142 環形連結串列入口
-
快慢指標相遇:快指標一次移動兩個節點,慢指標一次移動一個節點,快指標和慢指標在環內某個節點相遇時,快指標已經比慢指標多走了一個或多個完整的環。
-
相遇後的距離關係:由於快指標比慢指標多走了一個或多個完整的環,我們可以得出從頭節點到環入口的距離 a 等於從相遇點沿著環再走 c 的距離。
-
找到環的入口:當快指標和慢指標相遇後,將快指標重新指向連結串列頭節點,然後快指標和慢指標每次都移動一步。由於從頭節點到環入口的距離等於從相遇點再走到環入口的距離,因此它們最終會在環的入口相遇。
func detectCycle(head *ListNode) *ListNode {
// 思路 快慢指標方法
// 極端情況處理
if head == nil || head.Next == nil {
return nil
}
fast, slow := head.Next.Next, head.Next
for fast != slow { // 迴圈結束條件,快慢指標相交,此時快指標移動距離 兩倍於 慢指標移動距離
if slow == nil || fast == nil || fast.Next == nil { // 無環 fast.Next == nil 這裡判斷防止 fast = fast.Next.Next
return nil
}
slow = slow.Next
fast = fast.Next.Next
}
// 此時fast,slow相交,然後新指標從頭節點遍歷,頭到環入口位置 = 慢指標移動常數圈 + 交點到入口距離,所以會相交於入口位置
fast = head
for fast != slow { // 此時已經判斷有環,所以不用考慮nil邊界情況
fast = fast.Next
slow = slow.Next
}
return fast // return slow
}