用两个下标在数组上协同移动,往往能把一层嵌套循环"拆"掉,从 O(n²) 降到 O(n)。
双指针是指在数组(或字符串)上同时维护两个下标,让它们按照某种规则协同移动,一次遍历就能完成原本需要嵌套循环才能做的事。常见的用法有两种:对撞指针——一个从最左边出发,一个从最右边出发,两边向中间靠拢;快慢指针——两个指针同方向移动,但移动的速度或条件不一样。
经典问题:给一个已经排好序的数组,找出其中两个数,使它们的和正好等于目标值。暴力做法是双层循环,枚举所有的两数组合,时间复杂度 O(n²)。
利用数组"已经排好序"这个性质,可以用对撞指针把它优化到 O(n):left 指向最左边,right 指向最右边,看两者之和与目标值的关系——和太小,说明需要换一个更大的数,只能把 left 往右移;和太大,说明需要换一个更小的数,只能把 right 往左移;正好相等,就是答案。
| 1 | pair<int, int> TwoSumSorted(vector<int> arr, int target) |
| 2 | { |
| 3 | int left = 0, right = arr.size() - 1; |
| 4 | while (left < right) |
| 5 | { |
| 6 | int sum = arr[left] + arr[right]; |
| 7 | if (sum == target) |
| 8 | { |
| 9 | return {left, right}; |
| 10 | } |
| 11 | else if (sum < target) |
| 12 | { |
| 13 | left++; // 和太小,换一个更大的数 |
| 14 | } |
| 15 | else |
| 16 | { |
| 17 | right--; // 和太大,换一个更小的数 |
| 18 | } |
| 19 | } |
| 20 | return {-1, -1}; // 没找到 |
| 21 | } |
left,和太大就移 right"是安全的,不会漏掉答案?假设当前 arr[left]+arr[right] 太小了——因为数组是有序的,arr[right] 已经是 right 这一侧能选到的最大值,如果连这个最大值配上 arr[left] 都不够大,那 arr[left] 配上 right 左边那些更小的数,结果只会更小——所以"arr[left] 配任何 ≤right 的数"这条路已经彻底走不通了,left 必须往右换成更大的数。同理,right 太大时,arr[right] 配任何 ≥left 的数也都行不通,只能把 right 往左移。7+11=18),暴力双层循环最坏情况要比较 12 次。两个指针一共最多移动 n 次(left 只会前进、right 只会后退,加起来不会超过数组长度),所以整体是严格的 O(n)。另一种常见用法是快慢指针——两个指针同方向移动,但角色不同:fast 负责"探路",把数组从头到尾扫一遍;slow 负责"占坑",记录"目前已经处理好、不重复的部分"应该写到哪个位置。经典场景:给一个已经排好序的数组,原地去掉所有重复的元素,只保留每个数值一份。
| 1 | int RemoveDuplicates(vector<int>& arr) |
| 2 | { |
| 3 | if (arr.empty()) return 0; |
| 4 | int slow = 0; // slow指向"已确认不重复区域"的最后一个位置 |
| 5 | for (int fast = 1; fast < arr.size(); fast++) |
| 6 | { |
| 7 | if (arr[fast] != arr[slow]) // fast发现了一个新值 |
| 8 | { |
| 9 | slow++; |
| 10 | arr[slow] = arr[fast]; // 把新值"占坑"写到slow的位置 |
| 11 | } |
| 12 | } |
| 13 | return slow + 1; // 去重后的新长度 |
| 14 | } |
| fast | arr[fast] | arr[slow] | 是否新值 | 操作 |
|---|---|---|---|---|
| 1 | 1 | 1 | 否,相同 | 跳过 |
| 2 | 2 | 1 | 是,不同 | slow→1,arr[1]=2 |
| 3 | 2 | 2 | 否,相同 | 跳过 |
| 4 | 2 | 2 | 否,相同 | 跳过 |
| 5 | 3 | 2 | 是,不同 | slow→2,arr[2]=3 |
| 6 | 4 | 3 | 是,不同 | slow→3,arr[3]=4 |
| 7 | 4 | 4 | 否,相同 | 跳过 |
| 8 | 5 | 4 | 是,不同 | slow→4,arr[4]=5 |
| 最终 slow=4,去重后数组:{1, 2, 3, 4, 5},长度 5 | ||||
fast 像探路者一样一格不漏地往前走,slow 只在"发现新值"的时候才往前挪一格——挪动的那一刻,顺便把新值写到 slow 现在的位置上,相当于把不重复的数字一个个"搬"到数组前面,整个过程不需要额外的数组空间,原地完成。| 类型 | 移动规则 | 典型应用 |
|---|---|---|
| 对撞指针 | 从两端向中间靠拢 | 有序数组两数之和、判断回文、反转数组 |
| 快慢指针 | 同方向移动,速度或触发条件不同 | 原地去重、移除特定元素、链表找环 |