跳至主要内容

Two Pointers

雙指標(Two Pointers)是一種用兩個索引(指標)同時在陣列或字串上移動來解題的技巧,通常能把原本需要雙層迴圈的 O(n^2) 暴力解,降到只需要單層迴圈的 O(n)。

白話理解

想像在一本很厚、已經按頁碼排好序的電話簿裡,找兩個加起來等於某個數字的頁碼。與其每一頁都跟其他頁兩兩配對(暴力法),不如一根手指放在最前面、一根手指放在最後面,往中間慢慢逼近,效率會好上非常多。

兩種常見手法​

1. 相向雙指標(Opposite Direction)​

一個指標從最前面開始,一個指標從最後面開始,往中間逼近,通常用在已排序的陣列上。

def two_sum(numbers, target):
# 前提:numbers 已經是排序好的陣列
left = 0
right = len(numbers) - 1

while left < right:
total = numbers[left] + numbers[right]
if total == target:
return [left, right] # 找到了
elif total < target:
left += 1 # 總和太小,把左指標往右移,讓數字變大一點
else:
right -= 1 # 總和太大,把右指標往左移,讓數字變小一點
return [-1, -1] # 找不到

逐步拆解:以 numbers = [2, 7, 11, 15]、target = 9 為例​

輪次leftrightnumbers[left] + numbers[right]判斷動作
10 (2)3 (15)2 + 15 = 1717 > 9,太大了right--
20 (2)2 (11)2 + 11 = 1313 > 9,太大了right--
30 (2)1 (7)2 + 7 = 9剛好等於 target!回傳 [0, 1]

因為陣列已經排序好,只要總和太大就代表「右邊的數字太大」,往左移一定會變小;反之總和太小就往右移。每一步都朝正確方向逼近,全程只掃過陣列一次。

注意

這個手法的前提是陣列必須已經排序好。如果陣列是亂序的,sum < target 不代表移動 left 一定能讓總和變大(因為右邊可能還有更小的數字),這時候需要先排序,或改用 Hash Table 來解(用空間換時間)。

2. 同向雙指標(Same Direction,又稱 Fast & Slow Pointers)​

兩個指標從同一端出發,但移動速度不同,常用在 Linked List 相關問題。

最經典的應用是判斷 Linked List 是否有環,也就是 Floyd's Cycle Detection(又稱龜兔賽跑演算法):

def has_cycle(head):
slow = head # 龜:一次走一步
fast = head # 兔:一次走兩步

while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast: # 兩者相遇,代表有環
return True
return False # fast 先走到底(None),代表沒有環
白話理解

如果操場跑道是一個圈(有環),跑得快的人(fast)遲早會從後面追上跑得慢的人(slow),兩人一定會在某一點相遇;如果跑道是一直線(沒有環),跑得快的人會先抵達終點,不會有相遇的機會。

除了判斷「有沒有環」,還可以進一步找出「環的起點」,這部分的推導與完整實作寫在 Floyd's Cycle Detection 裡,歡迎前去參考。

複雜度​

手法時間複雜度空間複雜度
相向雙指標O(n)O(1)
同向雙指標(Fast & Slow)O(n)O(1)

兩者都只需要常數個額外變數(幾個指標),完全不需要像 Hash Table 那樣額外的儲存空間,這也是雙指標技巧最大的優勢。

適用情況​

  • 已排序陣列中找符合條件的配對:例如兩數之和(Two Sum,排序後版本)、三數之和(3Sum)。
  • 判斷迴文(Palindrome):從頭尾同時往中間檢查字元是否對稱。
  • 盛水問題:Leetcode: Container With Most Water,從最外側開始,每次移動較短的那一邊。
  • Linked List 相關問題:判斷是否有環、找出環的起點、找出中點(快指標到底時,慢指標剛好在中間)、找出倒數第 k 個節點。
  • 原地移除或去除重複元素:例如移除陣列中的特定值、移除已排序陣列中的重複元素,用一個指標記錄「目前處理到哪」、一個指標記錄「下一個有效位置」。

常見誤區​

  • 在未排序的陣列上直接套用相向雙指標:如前面提醒的,這個手法高度仰賴「已排序」這個前提,用在亂序資料上邏輯會整個失效。
  • Fast & Slow Pointers 速度設反:一定是 fast 走得比 slow 快(通常一次 2 步、slow 一次 1 步),如果兩者速度一樣,永遠不會相遇(在有環的情況下也一樣會一直繞圈错过彼此)。
  • 忘記檢查邊界:使用 Fast & Slow Pointers 時,如果沒有同時檢查 fast 和 fast.next 是否存在,容易在鏈結串列長度為奇數或偶數時,對 null 取 .next 而出錯。
  • 搞混「移動哪一個指標」的方向:相向雙指標中,移動的判斷邏輯(該讓 left 前進還是 right 後退)要對應題目的目標,寫之前建議先想清楚「往哪個方向移動,才會讓結果往正確方向逼近」。