雙指標技巧 — 四種模式完整解析與面試實戰 | 資料結構與演算法

2026/07/01
雙指標技巧 — 四種模式完整解析與面試實戰 | 資料結構與演算法

雙指標(Two Pointers) 是用兩個索引在資料結構上同時移動,以 O(n) 時間解決原本需要 O(n²) 暴力枚舉問題的演算法技巧。它的核心思想是利用資料的有序性或特定結構性質,讓每次指標移動都能排除一批不可能的候選答案。掌握對撞指標、同向指標、快慢指標、雙序列指標四種模式,是破解 LeetCode 中高頻面試題的關鍵武器。

前言

想像你在一排書架上找兩本加起來總頁數恰好是 500 頁的書,書架已按頁數從小到大排列。暴力法是:拿起第一本,逐一比較其餘每本;再拿起第二本,再逐一比較——這就是 O(n²)。

但你可以更聰明:站在書架兩端,左手拿最薄的、右手拿最厚的。如果兩本加起來頁數超過 500,右手往內移一格(換更薄的書);如果不夠,左手往內移一格(換更厚的書)。這樣每次移動都排除了一本書,最多 n 步就能找到答案——這就是對撞指標的核心直覺。

讀完這篇文章,你將能夠:

  • 理解雙指標為何能將 O(n²) 降為 O(n),以及每種模式的使用前提
  • 掌握四種模式:對撞、同向、快慢、雙序列的完整思路與實作
  • 用 TypeScript 解出 LeetCode 167、15、11、283、142 五道高頻考題
  • 看懂 C++ 版本實作,面試時展示語言深度
  • 知道什麼時候選雙指標、什麼時候改用雜湊表

核心概念:四種模式總覽

雙指標(Two Pointers)並非單一技巧,而是一個技巧家族,依照指標移動方向與應用場景分為四種模式:

模式指標移動方向使用前提典型問題
對撞指標從兩端向中心收斂陣列有序,或可推導單調性Two Sum II、3Sum、Container With Most Water
同向指標同向,讀寫分離無需有序,維護讀寫不變量移除重複元素、移動零
快慢指標同向,1倍 vs 2倍速鏈結串列,利用速度差環偵測、找中間節點
雙序列指標各自在獨立序列前進兩序列各自有序合併兩個有序陣列、Diff 比對

為什麼雙指標能降低複雜度?

暴力法用雙重迴圈枚舉所有對,共 O(n²) 次操作。雙指標的關鍵洞察是:每次移動至少能淘汰一個元素。以對撞指標為例,當左右指標的和太大時,右指標左移——此時所有「以當前 right 為右端點的配對」全部被一次性排除。如此,兩個指標各自最多走 n 步,整體複雜度 O(n)。


模式一:對撞指標(Converging Pointers)

對撞指標(Converging Pointers)將左指標放在陣列開頭、右指標放在末尾,依據判斷條件讓其中一個向中間移動,直到兩者相遇。

模式圖示

陣列:[2, 7, 11, 15]  target = 9

初始:
  L                 R
  ↓                 ↓
[ 2,  7,  11,  15]

sum = 2 + 15 = 17 > 9  →  R 左移

  L           R
  ↓           ↓
[ 2,  7,  11,  15]

sum = 2 + 11 = 13 > 9  →  R 左移

  L      R
  ↓      ↓
[ 2,  7,  11,  15]

sum = 2 + 7 = 9 == target  ✓  找到答案!

使用前提:陣列必須有序,或能推導出某種單調性(如容器問題中,移動較短那側才有可能找到更大容量)。

實作一:Two Sum II(LeetCode 167)

// 已排序的陣列中找兩數之和等於 target,返回 1-indexed 位置
function twoSum(numbers: number[], target: number): number[] {
  let left = 0;
  let right = numbers.length - 1;

  while (left < right) {
    const sum = numbers[left] + numbers[right];

    if (sum === target) {
      return [left + 1, right + 1]; // 題目要求 1-indexed
    } else if (sum < target) {
      left++;  // 總和太小,左指標右移增大
    } else {
      right--; // 總和太大,右指標左移縮小
    }
  }

  return [-1, -1]; // 題目保證有解,不會到達這裡
}
// 時間:O(n)  空間:O(1)
// 前提:numbers 已排序(題目保證),對撞指標才能正確排除候選

實作二:盛水最多的容器(LeetCode 11)

這題不需要陣列有序,但利用了「面積的單調性」:移動較高板子,寬度縮小且高度上界不變(受限於較矮的板子),面積必然縮小;移動較矮板子,寬度縮小但高度有機會提升,存在找到更大面積的可能。

function maxArea(height: number[]): number {
  let left = 0;
  let right = height.length - 1;
  let maxWater = 0;

  while (left < right) {
    const h = Math.min(height[left], height[right]);
    const w = right - left;
    maxWater = Math.max(maxWater, h * w);

    // 關鍵策略:移動較矮的那一側
    // 若移動較高側:寬度 -1,高度上界不變(受限於較矮側)→ 面積必定縮小
    // 若移動較矮側:寬度 -1,但高度上界可能提升 → 有機會找到更大面積
    if (height[left] <= height[right]) {
      left++;
    } else {
      right--;
    }
  }

  return maxWater;
}
// 時間:O(n)  空間:O(1)
// 正確性:對每對 (i, j),可以證明被跳過的配對面積 ≤ 已計算的最大值

實作三:接雨水(LeetCode 42)

接雨水是對撞指標的進階應用,需要同時維護兩側的最大高度。

// 雙指標版(O(n) 時間,O(1) 空間)— 空間最優解
function trap(height: number[]): number {
  let left = 0;
  let right = height.length - 1;
  let leftMax = 0;
  let rightMax = 0;
  let water = 0;

  while (left < right) {
    if (height[left] < height[right]) {
      // 左側較矮:右側保證有 height[right] 以上的牆
      // 此格積水完全由 leftMax 決定(不受右側影響)
      leftMax = Math.max(leftMax, height[left]);
      water += leftMax - height[left]; // 若 height[left] 是新最大值,差為 0
      left++;
    } else {
      // 右側較矮:左側保證有 height[left] 以上的牆
      rightMax = Math.max(rightMax, height[right]);
      water += rightMax - height[right];
      right--;
    }
  }

  return water;
}
// 時間:O(n)  空間:O(1)
// 關鍵洞察:當 height[left] < height[right] 時,
// right 保證 rightMax >= height[right] > height[left],
// 所以 left 的積水量完全由 leftMax 獨立決定。

模式二:同向指標(Same Direction — 快慢讀寫)

同向指標讓兩個指標都從同一方向出發,但以不同速度或條件前進。常見用法是「讀寫分離」:fast(讀指標)掃描每個元素,slow(寫指標)記錄下一個寫入位置。

模式圖示

陣列:[1, 1, 2, 3, 3]  — 移除重複元素

slow(寫指標):指向下一個寫入位置
fast(讀指標):掃描每個元素

step 1: slow=0, fast=1  nums[fast]=1 == nums[slow]=1  → fast++(跳過重複)
step 2: slow=0, fast=2  nums[fast]=2 != nums[slow]=1  → 寫入,slow++, fast++
step 3: slow=1, fast=3  nums[fast]=3 != nums[slow]=2  → 寫入,slow++, fast++
step 4: slow=2, fast=4  nums[fast]=3 == nums[slow]=3  → fast++(跳過重複)
step 5: fast 越界,結束

結果:[1, 2, 3, _, _],有效長度 = slow + 1 = 3

實作一:移除排序陣列中的重複元素(LeetCode 26)

function removeDuplicates(nums: number[]): number {
  // writePos 從 1 開始:位置 0 已確定保留(第一個元素無重複可比)
  let writePos = 1;

  for (let read = 1; read < nums.length; read++) {
    // 不等於前一個元素,說明是新的唯一值
    if (nums[read] !== nums[read - 1]) {
      nums[writePos++] = nums[read]; // 寫入並移動寫指標
    }
    // 若等於前一個,跳過(read 自動在 for 迴圈遞增)
  }

  return writePos; // 有效元素數量
}
// 時間:O(n)  空間:O(1)
// 不變量:nums[0..writePos-1] 始終是已確認的唯一元素序列

實作二:移動零(LeetCode 283)

// 將陣列中所有 0 移到末尾,保持非零元素的相對順序
function moveZeroes(nums: number[]): void {
  let writePos = 0; // 下一個非零元素的寫入位置

  // 第一步:將所有非零元素依序移到前面
  for (let read = 0; read < nums.length; read++) {
    if (nums[read] !== 0) {
      nums[writePos++] = nums[read];
    }
  }

  // 第二步:剩餘位置補零
  while (writePos < nums.length) {
    nums[writePos++] = 0;
  }
}
// 時間:O(n)  空間:O(1)
// 範例:[0,1,0,3,12] → [1,3,12,0,0]
// 讀指標掃描全部,寫指標只在遇到非零時前進

模式三:快慢指標(Fast-Slow — Floyd 龜兔演算法)

快慢指標(Fast-Slow Pointers)又稱 Floyd 龜兔演算法(Floyd’s Tortoise and Hare),讓慢指標每次走 1 步、快指標每次走 2 步。若鏈結串列存在環,快指標最終會從後面追上慢指標,製造出數學上的必然相遇。

模式圖示

鏈結串列:1 → 2 → 3 → 4 → 5 → 3(環,從節點 3 開始)

slow 每步走 1 格,fast 每步走 2 格

step 1: slow=2, fast=3
step 2: slow=3, fast=5
step 3: slow=4, fast=4  ← 相遇!確認有環

環入口偵測(Floyd 第二階段):
  將 slow 重置到 head,fast 留在相遇點
  兩者皆以速度 1 前進,再次相遇的位置即環入口

head  ←─────────────────┐
  1 → 2 → [3] → 4 → 5 ─┘
            ↑
          環入口(數學證明:head 到環入口距離 = 相遇點到環入口距離)

數學原理:設 head 到環入口距離為 a,環入口到相遇點距離為 b,環長為 c。slow 走了 a + b 步,fast 走了 a + b + k*c 步(繞環 k 圈)。因 fast 速度是 slow 的 2 倍:2(a+b) = a + b + k*c,化簡得 a = k*c - b。這表示 head 到環入口 = 從相遇點再走 k*c - b 步 = 環入口。

實作一:環偵測並找環入口(LeetCode 142)

class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val: number, next: ListNode | null = null) {
    this.val = val;
    this.next = next;
  }
}

function detectCycle(head: ListNode | null): ListNode | null {
  if (!head || !head.next) return null;

  // 階段一:確認是否有環(龜兔賽跑)
  let slow: ListNode | null = head;
  let fast: ListNode | null = head;

  while (fast !== null && fast.next !== null) {
    slow = slow!.next;          // 慢指標走 1 步
    fast = fast.next.next;      // 快指標走 2 步

    if (slow === fast) {
      // 階段二:找環入口
      // 根據 Floyd 數學原理:a = k*c - b
      // head 到環入口的距離 = 相遇點到環入口的距離(沿環方向)
      let entry: ListNode | null = head;
      while (entry !== slow) {
        entry = entry!.next;
        slow = slow!.next;
      }
      return entry; // 即環入口節點
    }
  }

  return null; // 無環
}
// 時間:O(n)  空間:O(1)
// 相較於 HashSet 方案(O(n) 空間),快慢指標以 O(1) 空間解決問題

實作二:找鏈結串列中間節點(LeetCode 876)

// fast 到達尾端時,slow 恰好在中間
// 偶數個節點時,返回第二個中間節點(偏右)
function middleNode(head: ListNode | null): ListNode | null {
  let slow: ListNode | null = head;
  let fast: ListNode | null = head;

  while (fast !== null && fast.next !== null) {
    slow = slow!.next;       // 走 1 步
    fast = fast.next.next;   // 走 2 步
  }

  return slow; // fast 到末尾,slow 恰在中間
}
// 時間:O(n)  空間:O(1)
// 奇數個節點(如 1→2→3→4→5):slow 停在 3(正中)
// 偶數個節點(如 1→2→3→4):  slow 停在 3(偏右中點)

模式四:雙序列指標(Two Sequence Pointers)

雙序列指標讓兩個指標分別在兩個獨立序列上前進,依比較結果決定哪個指標推進。最典型應用是歸併排序中的合併步驟。

模式圖示

A: [1,  3,  5,  7]
B: [2,  4,  6,  8]
    ↑              ← pA(從頭開始)
    ↑              ← pB(從頭開始)

step 1: A[pA]=1 < B[pB]=2  →  取 A[pA]=1,pA++
step 2: A[pA]=3 > B[pB]=2  →  取 B[pB]=2,pB++
step 3: A[pA]=3 < B[pB]=4  →  取 A[pA]=3,pA++
...
結果:[1, 2, 3, 4, 5, 6, 7, 8]

實作一:合併兩個有序陣列(LeetCode 88)

這題的技巧在於「從後往前合併」,避免覆蓋 nums1 中尚未處理的元素。

// nums1 有 m 個有效元素,後面 n 個空位;nums2 有 n 個元素
function merge(nums1: number[], m: number, nums2: number[], n: number): void {
  let p1 = m - 1;        // nums1 有效元素末尾
  let p2 = n - 1;        // nums2 末尾
  let write = m + n - 1; // 寫入位置(從後往前)

  // 從大到小填入:比較兩個序列末尾,較大者放入 write 位置
  while (p1 >= 0 && p2 >= 0) {
    if (nums1[p1] >= nums2[p2]) {
      nums1[write--] = nums1[p1--];
    } else {
      nums1[write--] = nums2[p2--];
    }
  }

  // nums2 若有剩餘,直接填入(nums1 剩餘已在正確位置)
  while (p2 >= 0) {
    nums1[write--] = nums2[p2--];
  }
}
// 時間:O(m+n)  空間:O(1)
// 關鍵:write 指標永遠在 p1 右側,從後往前不會覆蓋未處理元素
// 範例:nums1=[1,2,3,0,0,0], m=3, nums2=[2,5,6], n=3
//       → nums1=[1,2,2,3,5,6]

C++ 對照實作

三數之和(LeetCode 15)— STL 版

#include <vector>
#include <algorithm>
using namespace std;

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end()); // 排序是對撞指標的前提

        for (int i = 0; i < (int)nums.size() - 2; i++) {
            if (nums[i] > 0) break;                        // 剪枝:最小值 > 0,無解
            if (i > 0 && nums[i] == nums[i-1]) continue;  // 去重:跳過重複的固定值

            int left = i + 1, right = (int)nums.size() - 1;

            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];

                if (sum == 0) {
                    result.push_back({nums[i], nums[left], nums[right]});
                    // upper_bound / lower_bound 利用有序性快速跳過重複值
                    auto nextLeft  = upper_bound(nums.begin() + left,
                                                 nums.begin() + right, nums[left]);
                    auto nextRight = lower_bound(nums.begin() + left + 1,
                                                 nums.begin() + right, nums[right]);
                    left  = (int)(nextLeft  - nums.begin());
                    right = (int)(nextRight - nums.begin()) - 1;
                } else if (sum < 0) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return result;
    }
};
// 時間:O(n²)  空間:O(1)(不計結果)

環偵測(LeetCode 142)— C++ 版

struct ListNode {
    int val;
    ListNode* next;
    explicit ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
        if (!head || !head->next) return nullptr;

        ListNode* slow = head;
        ListNode* fast = head;

        // 階段一:找相遇點
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;

            if (slow == fast) {
                // 階段二:找環入口
                ListNode* entry = head;
                while (entry != slow) {
                    entry = entry->next;
                    slow  = slow->next;
                }
                return entry;
            }
        }
        return nullptr; // 無環
    }
};

合併有序容器 — STL 泛型版

#include <vector>
#include <algorithm>
using namespace std;

// 雙序列指標的泛型版(與 std::merge 等價,此為教學示範)
template<typename InputIt1, typename InputIt2, typename OutputIt>
OutputIt mergeSorted(InputIt1 first1, InputIt1 last1,
                     InputIt2 first2, InputIt2 last2,
                     OutputIt out) {
    while (first1 != last1 && first2 != last2) {
        if (*first1 <= *first2) {
            *out++ = *first1++;
        } else {
            *out++ = *first2++;
        }
    }
    out = copy(first1, last1, out); // 剩餘部分直接複製
    out = copy(first2, last2, out);
    return out;
}
// 實際開發中直接用 std::merge,此為演示底層邏輯

複雜度分析總表

問題模式時間空間使用前提
Two Sum II(有序)對撞O(n)O(1)陣列有序
三數之和 3Sum固定 + 對撞O(n²)O(1)排序後有序
盛水最多的容器對撞O(n)O(1)面積單調性
接雨水對撞O(n)O(1)無特殊前提
移除重複元素同向讀寫O(n)O(1)陣列有序
移動零同向讀寫O(n)O(1)無特殊前提
鏈結串列環偵測快慢O(n)O(1)鏈結串列
找鏈結串列中點快慢O(n)O(1)鏈結串列
合併兩個有序陣列雙序列O(m+n)O(1)兩序列有序

變體與延伸

三指標:三數之和(LeetCode 15)

三數之和(3Sum)是對撞指標最具代表性的延伸題。外層固定一個值 nums[i],內層用對撞指標找剩下兩數。去重是實作中最容易踩坑的地方。

function threeSum(nums: number[]): number[][] {
  const result: number[][] = [];
  nums.sort((a, b) => a - b); // 排序是對撞指標的前提

  for (let i = 0; i < nums.length - 2; i++) {
    // 剪枝:nums[i] > 0 表示三數之和必然 > 0,可直接結束
    if (nums[i] > 0) break;
    // 去重:i > 0 才能比前一個,相同值跳過避免重複三元組
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    let left = i + 1;
    let right = nums.length - 1;

    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];

      if (sum === 0) {
        result.push([nums[i], nums[left], nums[right]]);
        // 去重:跳過 left 和 right 位置的重複值(必須在移動前執行)
        while (left < right && nums[left] === nums[left + 1]) left++;
        while (left < right && nums[right] === nums[right - 1]) right--;
        left++;
        right--;
      } else if (sum < 0) {
        left++;  // 總和太小,左指標右移增大
      } else {
        right--; // 總和太大,右指標左移縮小
      }
    }
  }

  return result;
}
// 時間:O(n²)  空間:O(1)(不計結果空間)
// 常見錯誤:
//   1. 忘記排序
//   2. 找到答案後忘記去重,導致重複三元組
//   3. 去重時沒有確保 left < right 的邊界條件

荷蘭國旗問題(Dutch National Flag — LeetCode 75)

荷蘭國旗問題(Dutch National Flag Problem)是三指標的經典應用:用 lowmidhigh 三個指標在一次遍歷內將 [0, 1, 2] 三種值排序。

function sortColors(nums: number[]): void {
  let low = 0;                    // [0, low) 全是 0
  let mid = 0;                    // [low, mid) 全是 1
  let high = nums.length - 1;     // (high, end] 全是 2

  while (mid <= high) {
    if (nums[mid] === 0) {
      // 當前是 0:與 low 交換,兩者都前進
      [nums[low], nums[mid]] = [nums[mid], nums[low]];
      low++;
      mid++;
    } else if (nums[mid] === 1) {
      // 當前是 1:已在正確區間,mid 前進
      mid++;
    } else {
      // 當前是 2:與 high 交換,high 後退,mid 不動(換來的值需再次檢查)
      [nums[mid], nums[high]] = [nums[high], nums[mid]];
      high--;
    }
  }
}
// 時間:O(n)  空間:O(1)
// 不變量(Invariant):
//   nums[0..low-1]   全為 0
//   nums[low..mid-1] 全為 1
//   nums[mid..high]  未處理區間
//   nums[high+1..n-1] 全為 2

刪除鏈結串列倒數第 N 個節點(LeetCode 19)

快慢指標的另一個應用:讓 fast 先走 n+1 步,再兩者同步前進,fast 到末尾時 slow 恰好在目標節點的前一個。

function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
  const dummy = new ListNode(0);
  dummy.next = head;

  let fast: ListNode | null = dummy;
  let slow: ListNode | null = dummy;

  // fast 先走 n+1 步(+1 讓 slow 停在目標節點的前一個)
  for (let i = 0; i <= n; i++) {
    fast = fast!.next;
  }

  // 兩者同步前進直到 fast 到達末尾
  while (fast !== null) {
    fast = fast.next;
    slow = slow!.next;
  }

  // slow.next 就是要刪除的節點
  slow!.next = slow!.next!.next;
  return dummy.next;
}
// 時間:O(n)  空間:O(1)
// dummy 節點技巧:統一處理刪除頭節點的邊界情況

面試考點:雙指標 vs 雜湊表

面試中最常問:什麼時候選雙指標,什麼時候選雜湊表?

條件雙指標雜湊表
陣列是否已排序優先選擇未排序也可用
空間限制O(1) 空間O(n) 額外空間
需要找所有配對配合去重邏輯效率高容易重複計算
單次查詢排序後才能用O(1) 直接查詢
資料結構陣列(對撞/同向)、鏈結串列(快慢)任何資料

Two Sum 的選擇邏輯

  • 未排序:用雜湊表 O(n),不需排序
  • 已排序:用雙指標 O(n),不需額外空間
  • 需要所有結果(如 3Sum):排序後用雙指標,搭配去重邏輯

面試回答模板:「如果陣列已排序且空間有限,我會用對撞指標;如果需要 O(1) 查詢且不在意空間,用雜湊表;如果是鏈結串列問題,快慢指標是首選。」


常見陷阱

陷阱一:對撞指標用於無序陣列

// 錯誤:陣列無序時對撞指標會跳過正確答案
// 反例:nums = [3, 1, 2], target = 4
// left=3, right=2: sum=5>4, right--
// left=3, right=1: sum=4,但正確答案 [1,3] 已被跳過!

// 正確:對撞指標前必須排序(若需原始索引,用 Map 記錄)
nums.sort((a, b) => a - b);

陷阱二:3Sum 忘記去重

// 錯誤:找到答案後直接移動,未跳過重複值
if (sum === 0) {
  result.push([nums[i], nums[left], nums[right]]);
  left++;  // 若 nums[left+1] === nums[left],下次會推入相同三元組
  right--;
}

// 正確:移動前先跳過所有重複值,且需確保 left < right
if (sum === 0) {
  result.push([nums[i], nums[left], nums[right]]);
  while (left < right && nums[left] === nums[left + 1]) left++;
  while (left < right && nums[right] === nums[right - 1]) right--;
  left++;
  right--;
}

陷阱三:快慢指標初始化不一致

// 錯誤:fast 從 head.next 開始,邊界行為不一致
let slow = head;
let fast = head.next; // 起始點不同,偶數個節點時中點偏左

// 正確:slow 和 fast 都從 head 出發
let slow: ListNode | null = head;
let fast: ListNode | null = head;
while (fast !== null && fast.next !== null) {
  slow = slow!.next;
  fast = fast.next.next;
}

陷阱四:接雨水的積水量可能為負

// 錯誤:直接相減,未更新 leftMax 時可能得到負數
totalWater += leftMax - height[left]; // 若 height[left] > leftMax,結果為負!

// 正確:先更新 leftMax,再計算差值
leftMax = Math.max(leftMax, height[left]);
totalWater += leftMax - height[left]; // 若 height[left] 是新最大值,差值為 0(正確)

LeetCode 練習題單

#題目難度模式核心技巧
167Two Sum II - Input Array Is SortedMedium對撞有序陣列直接對撞
153SumMedium固定 + 對撞排序 + 去重
11Container With Most WaterMedium對撞(面積單調性)移動較矮側
283Move ZeroesEasy同向讀寫讀寫指標分離
142Linked List Cycle IIMedium快慢Floyd 兩階段

建議練習順序:先做 167(最純粹的對撞指標),再做 283(同向指標),接著 11(理解單調性論證),然後 142(快慢指標),最後 15(綜合去重技巧)。


總結

雙指標(Two Pointers)是演算法工具箱中高性價比最高的技巧之一:思路直觀,實作精簡,效果顯著——O(n²) 直降 O(n),且不需額外空間。

這篇文章涵蓋了四種模式:

  • 對撞指標:有序陣列兩端收斂,每步排除一個元素;適合求和、容器、接雨水
  • 同向指標:讀寫分離,寫指標只在條件成立時前進;適合去重、移動零
  • 快慢指標:速度差製造必然相遇,Floyd 演算法的數學之美;適合鏈結串列環偵測
  • 雙序列指標:兩序列各自推進,依比較結果決定哪側前進;適合歸併類問題

掌握這四種模式後,面對任何雙指標題目,你的第一反應應該是:「資料有序嗎?需要找配對還是維護窗口?是陣列還是鏈結串列?」答案會自然指引你選出正確模式。

下一篇文章將介紹滑動視窗(Sliding Window)——同向雙指標的進化版,維護動態窗口內更複雜的不變量,用來解決子陣列/子字串類的最優化問題,敬請期待。

希望這篇文章能幫助你全面掌握雙指標技巧,在面試和實戰中都能靈活運用。如有任何問題或疑惑,歡迎透過 聯絡頁面 與我交流!

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。