Description

給定兩個單向鏈結串列的頭節點 headAheadB,找出兩個鏈結串列的交叉節點。若無交叉則回傳 null。注意交叉是以節點引用判斷,不是節點值。

Example:

Input: listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3 Output: Intersected at ‘8’

Intuition#

核心思路:兩個指針分別從 A 和 B 出發,走完自己的串列後切換到對方的串列,最終會在交叉點相遇。

  • 兩個串列長度可能不同,直接同步遍歷無法對齊
  • 關鍵觀察:A 的長度 + B 的長度 = B 的長度 + A 的長度,所以兩指針走完兩條路後一定對齊
  • 若無交叉,兩指針最後都會到 null,也會同時結束

Approaches#

1: HashSet#

  • 概念: 先將 A 的所有節點存入 HashSet,再遍歷 B 找第一個存在於 Set 中的節點
  • 時間複雜度: O(m + n)
  • 空間複雜度: O(m) - 儲存 A 的所有節點
class Solution {
    fun getIntersectionNode(headA: ListNode?, headB: ListNode?): ListNode? {
        val visited = HashSet<ListNode>()
        var curr = headA
        while (curr != null) {
            visited.add(curr)
            curr = curr.next
        }
        curr = headB
        while (curr != null) {
            if (curr in visited) return curr
            curr = curr.next
        }
        return null
    }
}

⭐2: Two Pointers (Switch Lists)#

  • 概念: 指針 A 走完 listA 後接到 listB 的頭,指針 B 走完 listB 後接到 listA 的頭,兩者會在交叉點相遇
  • 時間複雜度: O(m + n)
  • 空間複雜度: O(1)
class Solution {
    fun getIntersectionNode(headA: ListNode?, headB: ListNode?): ListNode? {
        var pA = headA
        var pB = headB
        while (pA !== pB) {
            pA = if (pA != null) pA.next else headB
            pB = if (pB != null) pB.next else headA
        }
        return pA
    }
}

比較節點時必須用 !==(引用比較),不是 !=(值比較)。Kotlin 中 == 對 nullable 型別可能呼叫 equals。

🔑 Takeaways#

  • Pattern: 兩指針切換串列的技巧本質是讓兩者走相同的總路徑長度,從而自動對齊
  • 關鍵技巧: 當 pA 走到 null 時切到 headB(不是 headB.next),這樣能正確處理長度差