程式碼隨想錄day4 | 24 兩兩交換連結串列節點 19 刪除倒數第n個節點 142 環形連結串列

周公瑾55發表於2024-07-20

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 環形連結串列入口

image

  • 快慢指標相遇:快指標一次移動兩個節點,慢指標一次移動一個節點,快指標和慢指標在環內某個節點相遇時,快指標已經比慢指標多走了一個或多個完整的環。

  • 相遇後的距離關係:由於快指標比慢指標多走了一個或多個完整的環,我們可以得出從頭節點到環入口的距離 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

}

相關文章