雙指標技巧 — 四種模式完整解析與面試實戰 | 資料結構與演算法
雙指標(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)是三指標的經典應用:用 low、mid、high 三個指標在一次遍歷內將 [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 練習題單
| # | 題目 | 難度 | 模式 | 核心技巧 |
|---|---|---|---|---|
| 167 | Two Sum II - Input Array Is Sorted | Medium | 對撞 | 有序陣列直接對撞 |
| 15 | 3Sum | Medium | 固定 + 對撞 | 排序 + 去重 |
| 11 | Container With Most Water | Medium | 對撞(面積單調性) | 移動較矮側 |
| 283 | Move Zeroes | Easy | 同向讀寫 | 讀寫指標分離 |
| 142 | Linked List Cycle II | Medium | 快慢 | Floyd 兩階段 |
建議練習順序:先做 167(最純粹的對撞指標),再做 283(同向指標),接著 11(理解單調性論證),然後 142(快慢指標),最後 15(綜合去重技巧)。
總結
雙指標(Two Pointers)是演算法工具箱中高性價比最高的技巧之一:思路直觀,實作精簡,效果顯著——O(n²) 直降 O(n),且不需額外空間。
這篇文章涵蓋了四種模式:
- 對撞指標:有序陣列兩端收斂,每步排除一個元素;適合求和、容器、接雨水
- 同向指標:讀寫分離,寫指標只在條件成立時前進;適合去重、移動零
- 快慢指標:速度差製造必然相遇,Floyd 演算法的數學之美;適合鏈結串列環偵測
- 雙序列指標:兩序列各自推進,依比較結果決定哪側前進;適合歸併類問題
掌握這四種模式後,面對任何雙指標題目,你的第一反應應該是:「資料有序嗎?需要找配對還是維護窗口?是陣列還是鏈結串列?」答案會自然指引你選出正確模式。
下一篇文章將介紹滑動視窗(Sliding Window)——同向雙指標的進化版,維護動態窗口內更複雜的不變量,用來解決子陣列/子字串類的最優化問題,敬請期待。
希望這篇文章能幫助你全面掌握雙指標技巧,在面試和實戰中都能靈活運用。如有任何問題或疑惑,歡迎透過 聯絡頁面 與我交流!