Description

給定一個鏈結串列的頭節點,移除倒數第 n 個節點,並回傳頭節點。要求只用一次遍歷完成。

Example:

Input: head = [1,2,3,4,5], n = 2 Output: [1,2,3,5]

Intuition#

核心思路:讓快指針先走 n 步,然後快慢指針同時走,當快指針到底時,慢指針恰好在目標節點的前一個。

  • 「倒數第 n 個」等價於「正數第 (length - n + 1) 個」
  • 兩次遍歷:先算長度,再找到目標。但題目要求一次遍歷
  • 快慢指針間距為 n,當快指針到達尾端,慢指針就在目標前一位

Approaches#

1: Two-Pass (Count Length)#

  • 概念: 第一次遍歷計算長度 L,第二次遍歷走到第 (L - n) 個節點進行刪除
  • 時間複雜度: O(n) - 兩次遍歷
  • 空間複雜度: O(1)
class Solution {
    fun removeNthFromEnd(head: ListNode?, n: Int): ListNode? {
        val dummy = ListNode(0)
        dummy.next = head

        var length = 0
        var curr = head
        while (curr != null) {
            length++
            curr = curr.next
        }

        curr = dummy
        for (i in 0 until length - n) {
            curr = curr!!.next
        }
        curr!!.next = curr.next?.next

        return dummy.next
    }
}

⭐2: One-Pass (Two Pointers)#

  • 概念: 使用 dummy head,快指針先走 n+1 步,然後快慢指針同時走,快指針到底時慢指針在目標前一個節點
  • 時間複雜度: O(n) - 只遍歷一次
  • 空間複雜度: O(1)
class Solution {
    fun removeNthFromEnd(head: ListNode?, n: Int): ListNode? {
        val dummy = ListNode(0)
        dummy.next = head
        var fast: ListNode? = dummy
        var slow: ListNode? = dummy

        // 快指針先走 n + 1 步
        for (i in 0..n) {
            fast = fast?.next
        }

        // 同時移動直到快指針到底
        while (fast != null) {
            fast = fast.next
            slow = slow?.next
        }

        // 刪除目標節點
        slow?.next = slow?.next?.next

        return dummy.next
    }
}

Dummy head 是關鍵:當要刪除的是第一個節點(n == list length)時,沒有 dummy head 會需要特殊處理。

🔑 Takeaways#

  • Pattern: 快慢指針保持固定間距,是解決「倒數第 k 個」類型問題的標準技巧
  • 關鍵技巧: Dummy head + 快指針先走 n+1 步(而非 n 步),讓慢指針停在目標的前一個節點,方便刪除操作