双指针(Two Pointers)是算法中最常用、最高频的技巧之一,尤其适用于数组类问题。本篇文章将通过两道经典题目:
-
LeetCode 15:三数之和(3Sum)
-
LeetCode 42:接雨水(Trapping Rain Water)
来全面讲解双指针思想,并结合具体代码、详尽的时间复杂度与空间复杂度分析。
一、双指针思想是什么?
双指针就是:
使用两个索引在数组上移动,通过条件判断来减少不必要的遍历,从而优化复杂度。
常见的双指针形式:
-
左右指针(L / R)
-
快慢指针
-
滑动窗口
本篇重点讲 左右指针 在算法中的应用。
二、三数之和(3Sum):排序 + 双指针的典型用法
题目要求
给定一个数组 nums,找出所有和为 0 的三元组。不能包含重复的三元组。
解题核心思想
-
先排序(为双指针法创造有序性)
-
固定一个数
nums[i] -
在
[i+1, n-1]区间内使用双指针寻找:
nums[i] + nums[left] + nums[right] == 0 -
根据 sum 大小移动左右指针:
-
sum > 0 → right--
-
sum < 0 → left++
-
-
用循环跳过重复元素,避免重复解
双指针代码实现
cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// 思路: 遍历数组i,针对每个i在他的后面取left 和 right
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
int n = nums.size();
for (int i = 0; i < n - 1; ++i) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
ans.push_back({nums[i], nums[left], nums[right]});
left++;
while (left < right && nums[left] == nums[left - 1]){
left++;
}
right--;
while (left < right && nums[right] == nums[right + 1]) {
right--;
}
}
else if (sum > 0) right--;
else left++;
}
}
return ans;
}
};
时间复杂度分析
1. 排序:
cpp
O(n log n)
2. 遍历 + 双指针:
外层循环 n 次
内层双指针最多跑 (n-i) 次
整体为:
cpp
O(n^2)
最终时间复杂度:
O(n²)
空间复杂度分析
-
排序在 C++ 中是就地排序 →
O(1) -
输出答案需要空间,但不算在算法额外空间里
空间复杂度:O(1)(不计结果集)
三、接雨水:前缀最大值 + 后缀最大值(双指针思想衍生)
虽然接雨水有 O(1) 双指针最优解,但你的代码是 前缀 + 后缀数组法,属于"预处理 + 双指针思想"的经典版本。
题目核心
每个位置的储水量 = 左侧最大值与右侧最大值的较小者 -- 当前高度
前缀最大值 + 后缀最大值解法
1. 预处理两个数组:
-
pre_max[i]:从左到 i 的最高柱子 -
suf_max[i]:从右到 i 的最高柱子
2. 再遍历一次计算水量:
cpp
water += min(pre_max[i], suf_max[i]) - height[i]
原代码的问题(已修复)
你写的:
cpp
suf_max[i] = max(height[i], suf_max[i - 1]);
应该看右边,所以应该是:
cpp
suf_max[i] = max(height[i], suf_max[i + 1]);
正确代码
cpp
class Solution {
public:
int trap(vector<int>& height) {
// 思路: 把前面的最大值和后面的最大值先求出来,再最后遍历一次得到ans
int ans = 0;
int n = height.size();
vector<int> pre_max(n);
vector<int> suf_max(n);
pre_max[0] = height[0];
suf_max[n - 1] = height[n - 1];
for (int i = 1; i < n; ++i) {
pre_max[i] = max(height[i], pre_max[i - 1]);
}
for (int i = n - 2; i >= 0; --i) {
suf_max[i] = max(height[i], suf_max[i + 1]);
}
for (int i = 0; i < n; ++i) {
ans += min(suf_max[i], pre_max[i]) - height[i];
}
return ans;
}
};
时间复杂度分析
预处理前缀最大值:O(n)
预处理后缀最大值:O(n)
计算水量:O(n)
总时间复杂度:O(n)
使用两次遍历和一次合并遍历,总共是线性时间。
空间复杂度分析
使用了两个长度为 n 的数组:
cpp
pre_max[n]
suf_max[n]
因此:
空间复杂度:O(n)
(可用双指针优化到 O(1),但不是本文重点)
四、两题的双指针思想对比总结
| 题目 | 双指针方式 | 时间复杂度 | 空间复杂度 | 核心思想 |
|---|---|---|---|---|
| 三数之和 | 排序 + 左右夹逼 | O(n²) | O(1) | 有序数组中根据 sum 调整指针 |
| 接雨水 | 前后缀数组(双指针思想) | O(n) | O(n) | 左右区间最大值决定当前位置水量 |
五、双指针常用场景总结
| 场景 | 典型题目 | 指针移动策略 |
|---|---|---|
| 在排序数组里查找目标值 | 三数之和 | left / right 根据 sum 调节 |
| 区间最大最小问题 | 接雨水、盛水最多的容器 | 哪侧更小,就移动哪侧 |
| 去重 | 删除排序数组重复项 | 快慢指针 |
| 滑动窗口 | 长度最短子数组 | 两指针都向右 |
结语
双指针不是一个具体的算法,而是一类极其重要的思维模式:
通过左右夹逼、滑动、区间缩小等方式降低复杂度,避免暴力枚举。
掌握好这类模式,你可以轻松解决大量中等难度甚至困难题。