【双指针】N数之和
N数之和
- 两数之和
- 题目
- 题目解析
- 暴力思路
- 双指针优化
- 三数之和
- 题目
- 题目解析
- 暴力思路
- 双指针优化
- 四数之和
- 题目
- 题目解析
- 暴力思路
- 双指针优化
两数之和
题目
题目链接: 查找总价格为目标值的两个商品
虽然题目名字不是两数之和, 但是由于和后面的三数之和, 四数之和是连起来的, 于是换一个名字.
正统的两数之和是这个连接: 两数之和, 它的区别是返回的是下标, 而不是数字本身
题目解析
非常容易理解, 就是返回两个数字, 这两个数字的和要是 target
暴力思路
暴力思路也就是经典的枚举出所有的二元组, 依次查看和是否为 target 即可
使用一个简单的双重for循环就可以达到这个效果
class Solution {
public int[] twoSum(int[] price, int target) {
for(int i = 0; i < price.length; i++){
for(int j = i + 1; j < price.length; j++){
if(price[i] + price[j] == target){
return new int[]{price[i] ,price[j]};
}
}
}
return null;
}
}
但是由于这个题目的数组长度最大是10^5
, 因此时间复杂度为O(n^2)
的算法是一定会超时的
双指针优化
实际上这一题有一个条件在我们使用暴力解法的时候没有用到, 就是按照升序记录. 也就是说这个数组是已经排好序了, 这是一个非常重要的提示.
此时我们就要想到题目的要求, 实际上是求a + b = target
. 那么实际上在暴力枚举的过程中, 就只有三种情况会发生, 分别是a + b > target
, a + b < target
, a + b = target
那么此时就有一个双指针思路非常适合去一个有序的数组中寻找a + b = target
这种情况, 具体步骤如下:
首先先让 left 和 right 指向左侧和右侧, 然后对下列情形分别进行不同操作, 直到left > right
left + right > target
, 此时right–left + right < target
, 此时left++left + right = target
, 此时找到答案
此时的这个指针由于两者向内, 因此也有一个特殊的名称叫做对撞指针
那么为什么可以这样操作呢? 实际上这是也是由于有序数组的单调性决定的. 我们使用上面题目的例子来看
可以看到, 我们要找61, 刚开始left = 8, right = 66.
此时8 + 66 > 61
, 很明显此时计算的结果是更大的, 那么此时我们如果要找到61这个值, 就需要让我们的left + right
的值变小. 由于数组是有序的, 如果我们想要让left + right
变小, 就只能让 right 往左走了.
下一步到达了8 + 52 < 61
, 此时同上, 结果太小, 需要让结果变大, 那么此时就只能让left往右走了.
随后一直循环进行判断, 就会走到left = 27 right = 34
的情况
思路还是较为简单的, 下面就是代码
class Solution {
public int[] twoSum(int[] price, int target) {
// 指向左侧和右侧
int left = 0, right = price.length - 1;
while(left <= right){
// 分别对应三种情况
if(price[left] + price[right] > target) right--;
else if(price[left] + price[right] < target) left++;
else return new int[]{price[left], price[right]};
}
return null;
}
}
三数之和
题目
题目链接: 三数之和
题目解析
题目本身描述的就是找三个数令其和为0, 同时这三个数字不能是同一个. 我们同样可以通过例子来理解
首先看第一个例子, 可以看出实际上就是要你从数组中找三个的数字, 让其和为0. 这三个数字的顺序不重要
同时我们可以从第二个例子中看出, 他要求的是不同的数, 因为他选不了0 0 0
暴力思路
这里的暴力思路就和两数之和的一样, 遍历所有三元组. 就是一个三重for循环, 时间复杂度为O(n^3), 这里我们就不书写了
双指针优化
对于这一题, 实际上核心点在于两个: 1. 找三个数 2. 去重
对于第一个点, 我们可以化繁为简. 我们这一题的要求是求出三个数, 令其结果是0. 那么我们是否可以换一种思路, 我们找三个数a b c
, 能否去找b + c = -a
呢? 答案是当然可以的, 同时求这个b + c = -a
的过程, 我们就可以参考两数之和的求解过程. 这种化繁为简, 将难题化为简单的题的思路还是比较常用的.
那么参考两数之和的求解, 首先我们就需要对数组进行排序, 然后从第一个数开始, 把每一个数当为a
, 然后在其后方区域进行两数之和的操作. 下面是一个例子
解决了第一个点, 接下来就是第二个点, 去重. 这里有一个非常简单的方法就是使用自带的集合类去去重, 比如Java中的HashSet. 但是我们这里就不使用这种方法了, 而是使用手动的方式来实现.
上面我们说过, 我们是需要对数组进行排序的, 那么相对应的就是, 相同的数字会在同一个部分, 如下所示
那么此时我们就可以在过程中, 当当前数字和上一个数字不同的时候, 存储数字, 如果当前数字和上一个数字相同, 那么就跳过这些相同的数字. 但是这里要注意的是, 无论是选择a
, 或者是b + c
的过程, 都需要跳过相同的数字
这题的思路相较于上面的两数之和还是较为复杂的, 不过根据我们上面的分析, 大致可以分为如下几个步骤:
- 对数据进行排序
- 使用循环从第一个数字开始往后逐个作为数字 a, 同时需要进行去重
- 对 a 后方的数字进行寻找b c 的操作, 使用两数之和的方式寻找, 同时需要进行去重
下面是代码
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 对数组进行排序
Arrays.sort(nums);
// 创建返回值
List<List<Integer>> ret = new ArrayList<>();
int n = nums.length;
// 从第一个数字开始往后循环
for(int a = 0; a < n - 2;){
// 创建left和right用于执行两数之和
int left = a + 1;
int right = n - 1;
int target = -nums[a];
// 执行两数之和
while(left < right){
int sum = nums[left] + nums[right];
if(sum > target) right--;
else if(sum < target) left++;
else {
// 此时找到了一个元组, 放入nums
ret.add(Arrays.asList(nums[a], nums[left], nums[right]));
// 两侧指针向内移动
left++;
right--;
// 去重
while(left < right && nums[left] == nums[left - 1]) left++;
while(left < right && nums[right] == nums[right + 1]) right--;
}
}
// 对a去重
a++;
while(a < n - 2 && nums[a] == nums[a - 1]) a++;
}
return ret;
}
}
四数之和
题目
题目链接: 四数之和
题目解析
经历了上面两道题, 这道题也十分好理解, 就是求不同的四个数使其总和为 target
暴力思路
暴力思路也和上面的一样, 经典嵌套for循环, 这里是求4个数那么自然就是四重for循环, 时间复杂度为O(n^4), 这里不再阐述
双指针优化
双指针优化和三数之和的思路一致, 先找一个数, 在后方区间求三数之和, 然后三数之和的思路就和前面一样, 那么总结下来的流程如下
- 先排序数组
- 使用一个数 a 遍历数组, 设 a 后方区间为[m, n], 那么就是在[m, n]区间内使用三数之和思路寻找 target - nums[a]
- 同三数之和思路, 在[m, n]区间内使用一个 b 遍历区间, 同时在 b 后方使用两数之和思路寻找 target - nums[a] - nums[b]
- 两数之和思路不在阐述
- 中间过程均需要去重
最终代码如下
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
// 排序
Arrays.sort(nums);
List<List<Integer>> ret = new ArrayList<>();
int n = nums.length;
// 四数之和
for(int i = 0; i < n;){
// 三数之和
for(int j = i + 1; j < n;){
// 两数之和
int left = j + 1;
int right = n - 1;
// 这里使用long是由于恶心人的测试用例
long targetTwo = (long)target - nums[i] - nums[j];
while(left < right){
int sum = nums[left] + nums[right];
if(sum > targetTwo) right--;
else if(sum < targetTwo) left++;
else{
// 找到元组, 放入并收缩
ret.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
left++;
right--;
// 去重
while(left < right && nums[right] == nums[right + 1]) right--;
while(left < right && nums[left] == nums[left - 1]) left++;
}
}
// 去重
j++;
while(j < n && nums[j] == nums[j - 1]) j++;
}
// 去重
i++;
while(i < n && nums[i] == nums[i - 1]) i++;
}
return ret;
}
}