双指针算法
一、什么是双指针算法?
双指针算法是一种通过使用两个指针(通常指向数组的不同位置)来高效解决问题的技巧。它主要用于:
-
减少时间复杂度(从 O(n²) 降到 O(n))
-
优化空间复杂度(常为 O(1))
-
处理有序或部分有序的数据
二、题目详解与个人思路
1. 283. 移动零
题目要求:将数组中的所有 0 移动到末尾,保持非零元素的相对顺序。
我的思路:
-
使用快慢指针:
left指向已处理区域的尾部,right指向未处理的头部 -
当
right遇到非零元素时,与left交换,然后left后移 -
这样可以保证所有非零元素都被移到前面,且顺序不变
cpp
void moveZeroes(vector<int>& nums) {
if (nums.size() <= 1) return;
int left = 0, right = 0;
while(right <= nums.size()-1) {
if(nums[right] != 0) {
swap(nums[left++], nums[right]);
}
right++;
}
}
// 时间复杂度:O(n) - 每个元素最多被访问一次
// 空间复杂度:O(1) - 只使用了常数个额外变量
2. 1089. 复写零
题目要求:遇到 0 就复写一次,数组长度不变,从末尾开始覆盖。
我的思路:
-
难点:原地修改,不能使用额外数组
-
关键:先确定哪些元素会被保留
-
步骤:
-
使用
cur和dest指针模拟复写过程 -
第一次遍历确定最终保留的元素位置
-
从后向前进行实际复写操作
-
cpp
void duplicateZeros(vector<int>& arr) {
int cur = 0, dest = 0;
// 确定最后保留的元素
for(int i = 0; i < arr.size()-1; i++) {
if(arr[i] != 0) dest++;
else dest += 2;
cur++;
if(dest >= arr.size()) break;
}
// 从后向前复写
while(cur >= 1) {
if(dest > arr.size()) {
dest = arr.size();
arr[dest-1] = 0;
cur--;
dest--;
} else {
if(arr[cur-1] == 0) {
arr[dest-1] = 0;
arr[dest-2] = 0;
cur--;
dest -= 2;
} else {
arr[dest-1] = arr[cur-1];
cur--;
dest--;
}
}
}
}
// 时间复杂度:O(n) - 两次遍历,第一次确定保留元素,第二次实际复写
// 空间复杂度:O(1) - 原地修改,只使用常数空间
3. 202. 快乐数
题目要求:判断一个数是否为快乐数(各位平方和最终变为1)。
我的思路:
-
核心问题:如何判断是否进入无限循环?
-
解决方案:快慢指针法(类似链表判断环)
-
快指针每次计算两次,慢指针计算一次
-
相遇时判断值是否为1
cpp
int bitSquareSum(int n) {
int sum = 0;
while(n > 0) {
int bit = n % 10;
sum += bit * bit;
n = n / 10;
}
return sum;
}
bool isHappy(int n) {
int slow = n, fast = n;
do {
slow = bitSquareSum(slow);
fast = bitSquareSum(fast);
fast = bitSquareSum(fast);
} while(slow != fast);
return slow == 1;
}
// 时间复杂度:O(k) - k为循环次数,最坏情况下可能达到循环上限
// 空间复杂度:O(1) - 只使用了常数个额外变量
// 分析:使用Floyd判圈算法,无需哈希表存储已访问数字
4. 11. 盛最多水的容器
题目要求:找到两条线,使它们与 x 轴构成的容器能容纳最多的水。
我的思路:
2. 左右指针
3. 滑动窗口
4. 中心扩展
四、常见易错点
-
双指针从两端向中间移动
-
容量 = 宽度 × 最小高度
-
每次移动高度较小的指针(因为容量受限于最小高度)
-
记录最大容量
cppint maxArea(vector<int>& height) { int max_cap = 0, w = 0, h = 0; int left = 0, right = height.size()-1; while(left < right) { h = min(height[left], height[right]); w = right - left; max_cap = max(max_cap, w * h); height[left] > height[right] ? right-- : left++; } return max_cap; } // 时间复杂度:O(n) - 双指针从两端向中间遍历一次 // 空间复杂度:O(1) - 只使用了常数个额外变量5. 15. 三数之和
题目要求:找到数组中所有和为 0 的不重复三元组。
我的思路:
-
先排序(O(nlogn))
-
固定一个数,转化为两数之和问题
-
使用双指针寻找另外两个数
-
关键:去重处理非常重要
cppvector<vector<int>> threeSum(vector<int>& nums) { vector<vector<int>> result; sort(nums.begin(), nums.end()); for(int i = 0; i < nums.size()-2; i++) { // 去重:跳过相同的第一个数 if(i > 0 && nums[i] == nums[i-1]) continue; int target = -nums[i]; int left = i+1, right = nums.size()-1; while(left < right) { int sum = nums[left] + nums[right]; if(sum < target) left++; else if(sum > target) right--; else { result.push_back({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--; } } } return result; } // 时间复杂度:O(n²) // - 排序:O(nlogn) // - 外层循环:O(n) // - 内层双指针:O(n) // - 总复杂度:O(nlogn) + O(n²) = O(n²) // 空间复杂度:O(1) 或 O(n)(取决于排序算法) // - 如果排序是原地排序:O(1) // - 如果排序需要额外空间:O(n)6. 18. 四数之和
题目要求:找到数组中所有和为 target 的不重复四元组。
我的思路:
-
扩展三数之和的思路
-
双重循环固定前两个数
-
内层使用双指针寻找后两个数
-
注意整型溢出问题(使用long long)
cppvector<vector<int>> fourSum(vector<int>& nums, int target) { sort(nums.begin(), nums.end()); int n = nums.size(); vector<vector<int>> result; for(int i = 0; i < n-3;) { for(int j = i+1; j < n-2;) { long long task = (long long)target - nums[i] - nums[j]; int left = j+1, right = n-1; while(left < right) { int sum = nums[left] + nums[right]; if(sum > task) right--; else if(sum < task) left++; else { result.push_back({nums[i], nums[j], nums[left++], nums[right--]}); while(left < right && nums[left] == nums[left-1]) left++; while(left < right && nums[right] == nums[right+1]) right--; } } j++; while(j < n-2 && nums[j] == nums[j-1]) j++; } i++; while(i < n-3 && nums[i] == nums[i-1]) i++; } return result; } // 时间复杂度:O(n³) // - 排序:O(nlogn) // - 第一层循环:O(n) // - 第二层循环:O(n) // - 内层双指针:O(n) // - 总复杂度:O(nlogn) + O(n³) = O(n³) // 空间复杂度:O(1) 或 O(n)(取决于排序算法) // - 输出结果的空间不算在空间复杂度内(题目要求)三、双指针技巧总结
1. 快慢指针
-
应用场景:链表环检测、数组原地修改
-
经典题目:移动零、复写零、快乐数
-
应用场景:有序数组、回文判断
-
经典题目:两数之和、三数之和、盛水容器
-
应用场景:子串、子数组问题
-
特点:同向移动的双指针
-
应用场景:回文子串
-
特点:从中心向两边扩展
-
边界条件:双指针移动时要确保不越界
-
去重处理:多指针问题中跳过重复元素很重要
-
指针更新顺序:先计算结果再移动指针,避免错过解
-
时间复杂度:双指针通常将 O(n²) 优化为 O(n)