快慢指針是雙指針的特例:兩個指針同方向出發,但速度不同(典型是慢的走一步、快的走兩步)。在無法用索引隨機存取的結構(如鏈結串列)上,靠兩者的相對位置就能取得「中點」「環」「倒數第 k 個」等資訊,全程只掃一兩遍、O(1) 額外空間。
找中點
快指針每次走兩步、慢指針走一步;快指針到底時,慢指針剛好在中點。
fun middleNode(head: ListNode?): ListNode? {
var slow = head
var fast = head
while (fast?.next != null) {
slow = slow?.next
fast = fast.next?.next
}
return slow // 偶數長度時回傳偏右的中點
}
偵測環(Floyd 龜兔演算法)
若有環,快指針終會「套圈」追上慢指針;若無環,快指針會先抵達 null。
fun hasCycle(head: ListNode?): Boolean {
var slow = head
var fast = head
while (fast?.next != null) {
slow = slow?.next
fast = fast.next?.next
if (slow === fast) return true // 相遇即有環
}
return false
}
找環的入口
相遇後,把其中一個指針移回 head,兩指針都改成每次一步,再次相遇處就是環的入口。
fun detectCycle(head: ListNode?): ListNode? {
var slow = head
var fast = head
while (fast?.next != null) {
slow = slow?.next
fast = fast.next?.next
if (slow === fast) { // 第一次相遇
var p = head
while (p !== slow) { // 同速前進,再會於入口
p = p?.next
slow = slow?.next
}
return p
}
}
return null
}
NOTE
為什麼有效?設頭到入口距 a、入口到相遇點距 b、環長 c。相遇時快指針走的距離是慢指針的兩倍:2(a+b) = a+b+kc,化簡得 a = kc - b。也就是「從頭走 a 步」與「從相遇點再走 a 步」會同時抵達入口。
找倒數第 k 個節點
讓快指針先走 k 步,再讓兩指針一起前進;快指針到底時,慢指針正好在倒數第 k 個。這就是「移除倒數第 k 個節點」的一趟解法。
fun removeNthFromEnd(head: ListNode?, n: Int): ListNode? {
val dummy = ListNode(0).apply { next = head }
var fast: ListNode? = dummy
var slow: ListNode? = dummy
repeat(n + 1) { fast = fast?.next } // 快指針先走 n+1 步
while (fast != null) {
fast = fast?.next
slow = slow?.next
}
slow?.next = slow?.next?.next // 跳過倒數第 n 個
return dummy.next
}
TIP
快慢的步差不限於「一步對兩步」,可依題目調整。核心永遠是:用兩指針的固定間距或速度差,一趟換到原本要兩趟才拿得到的位置資訊。
跨倉庫導讀
- 對應理論章節:線性資料結構
- 另見本書 Two Pointers 的相向/同向指針技法。