Two Pointers(双指针)
双指针是处理有序数组 的一大利器。核心思想是用两个指针从数组的两端(或不同位置)向中间移动,根据当前两指针所指元素的和与目标值的关系,决定移动哪一个指针,从而在 O(n) 时间内找到答案。
什么时候用双指针?
- 数组已经排序(或者你可以先排序)
- 问题一般涉及两个数 或三个数的和、差、某种关系
- 需要去重(排序后重复元素会相邻,容易跳过)
PDF 里选了 2 道题:Two Sum II 和 3Sum(外加一道 3Sum Closest 在示例中),我们把它们串起来讲。
1. 两数之和 II(Two Sum II, 167, Easy)
题目 :在一个已经升序排列的数组里,找两个数使得它们的和等于目标值,返回这两个数的索引(下标从 1 开始),而且假定有唯一解。
思路
因为数组有序,我们可以将两个指针分别放在最左 和最右:
- 如果两数之和
sum == target,直接返回; - 如果
sum < target,说明左边的数太小了,左指针右移,让和变大; - 如果
sum > target,说明右边的数太大了,右指针左移,让和变小。
每次只移动一个指针,时间复杂度 O(n)。
代码
java
public int[] twoSum(int[] numbers, int target) {
int left = 0, right = numbers.length - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) {
return new int[]{left + 1, right + 1}; // 返回1-based索引
} else if (sum < target) {
left++;
} else {
right--;
}
}
return new int[]{-1, -1}; // 题目保证有解,这里仅为编译需要
}
这题是双指针最朴素的应用,也说明了双指针为什么对有序两数和有效:每次比较都能确定一个指针的移动方向,不会错过正确解。
2. 三数之和(3Sum, 15, Medium)
题目 :给你一个整数数组 nums,找出所有和为 0 的三元组,且结果中不能包含重复的三元组。
核心思路
- 排序:先把数组从小到大排序。
- 固定一个数,变成两数之和 :遍历排序后的数组,对每个
nums[i],把它当成三元组的第一个数,然后在它后面 的子数组上用双指针找两个数,使它们的和等于-nums[i]。 - 去重 :
- 如果当前
nums[i] == nums[i-1],直接跳过,因为以这个数开头的三元组我们已经在上一轮找过了。 - 在双指针移动时,如果找到了一个解,还要分别跳过左边和右边所有与当前解重复的元素。
- 如果当前
- 双指针移动规则和 Two Sum II 完全一样。
代码
java
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums); // 1. 排序
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重i
int left = i + 1, right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
res.add(Arrays.asList(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 res;
}
时间复杂度 :O(n²),排序 O(n log n),外层循环 O(n),内层双指针 O(n)。
空间复杂度 :O(1)(忽略存储答案的空间)。
3. 最接近的三数之和(3Sum Closest, 16, Medium)
题目 :给定一个数组 nums 和一个目标值 target,找出三个整数的和,使得这个和与 target 最接近。返回这个和(假定只有唯一解)。
思路
和 3Sum 几乎一样,区别在于:
- 我们不需要寻找和为 0,而是寻找与
target差值最小的和。 - 每次计算三个数的和
sum,如果sum比当前记录的res更接近target,就更新res。 - 移动双指针的规则仍然是根据
sum与target的大小关系:- 如果
sum > target,我们希望和变小一点,所以右指针左移; - 如果
sum < target,我们希望和变大一点,所以左指针右移; - 如果
sum == target直接返回(因为差值已经是 0,最接近)。
- 如果
代码
java
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int n = nums.length;
int res = nums[0] + nums[1] + nums[2]; // 初始值随便取,后面会更新
for (int i = 0; i < n - 2; i++) {
int left = i + 1, right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (Math.abs(sum - target) < Math.abs(res - target)) {
res = sum; // 更接近了,更新
}
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
return sum; // 完全匹配,直接返回
}
}
}
return res;
}
这就是 3Sum 的一个轻量变种,只多加了一个"距离比较"。
4. 延伸:2Sum 用哈希表
PDF 里在 Two Sum 部分还给出了一个未排序数组 的解法(原版 Two Sum,LC 1),用 HashMap:
java
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int diff = target - nums[i];
if (map.containsKey(diff)) {
return new int[]{map.get(diff), i};
}
map.put(nums[i], i);
}
return null;
}
但双指针专题的核心还是排序后双指针 的 O(n) 解法,哈希表的方法更多用于无序且要求返回索引的场景。
总结:双指针在求和问题中的模板
- 排序(如果数组本身无序)
- 对外层元素进行遍历 (如 3Sum 中的
i) - 内层双指针 :
left = i + 1,right = n - 1- 计算
sum = nums[i] + nums[left] + nums[right](或两个数的和) - 根据
sum与target的关系移动left或right
- 去重 :排序后,相同值必然相邻,用
while跳过重复项即可。
复杂度 :对于 k-Sum 问题,排序 O(n log n),然后用双指针将暴力 O(n^k) 降到 O(n^(k-1))。