在刚开始进入力扣时,我们可能会遇到刷题时期的第一道题,也是力扣的001号,两数之和。
对于当时连vector是什么都不知道的我,使用暴力解法通过的成就感也是无可比拟的。
在经历了后面的三数之和,四数之和的冲击后,发现这类题都是双指针的表现舞台。
两数之和
给定一个数组和一个目标值。找到和为目标值的两个整数,返回两个数值。
注意我们这里调整了题目,返回的是数值。(后面说明区别)
暴力/哈希解法
暴力解法是比较好想的,两个for循环去找和为目标值的那一组。
哈希解法可以降低时间复杂度。循环每一个x,去哈希表中找target-x是否存在。
但这些方法在三数、四数时会变得复杂,所以不是我们思考的重点。
排序+双指针
既然是一个数组中找两个数,我们很自然地想到双指针。一个从左边走,另一个从右边走,然后判断它们的和。
需要注意的是数组需要有序才能从头尾开始,这样在大于target或者小于target时才知道该移动哪边的指针。
在排序之后,左右指针就可以移动了。
因为没有重复的元素,所以不需要去重。只需要在和大于target时缩小右指针(想让总和小一点),和小于target时扩大左指针(想让总和大一点)
vector<int> twoSum(vector<int>& nums, int target) {
sort(nums.begin(),nums.end());
int left=0;
int right=nums.size()-1;
while(left<right){
int sum=nums[left]+nums[right];
if(sum<target){
left++;
}
else if(sum>target){
right--;
}
else
return {nums[left],nums[right]};
}
return {};
}
代码比较简单。
去重
**那如果有重复元素呢?**比如排序后[1,1,1,2,2,3,3],target=3时,如果不考虑重复则会输出多组[1,2]。
(本题遇到一个结果就返回了,如果需要返回多个结果就需要去重)
所以去重也是我们需要掌握的点(三数、四数都有)
其实去重对于排序数组来说比较简单:
- left指针只要考虑它的后一位,如果相等就++
- right指针只要考虑它的前一位,如果相等就–
而且去重这一步可以在找到结果后进行
vector<int> twoSum(vector<int>& nums, int target) {
sort(nums.begin(),nums.end());
vector<vector<int>>res;
int left=0;
int right=nums.size()-1;
while(left<right){
int sum=nums[left]+nums[right];
if(sum<target){
left++;
while(left<right&&nums[left]==nums[left-1]) left++;
}
else if(sum>target){
right--;
while(left<right&&nums[right]==nums[right+1]) right--;
}
else{
res.push_back( {nums[left],nums[right]});
while(left<right&&nums[left]==nums[left+1]) left++;//去重
while(left<right&&nums[right]==nums[right-1]) right--;//去重
right--;
left++;
}
}
return {};
}
与上面的情况的区别在于:
- 找到答案存入vector后,进行去重操作
- 因为答案不唯一,所以指针还需要收缩移动到下一位
在sum>target和sum<target时也可以加入去重操作,当然不加也可以。
为了严谨,我们这部分去重也加上。
需要注意,当结果不匹配时,left和right已经移动了,我们去重是要比较前面走过的。
当结果匹配时,我们去重要比较的是left和right的下一位。
三数之和
掌握了上面的去重双指针方法之后,N数之和都可以轻松拿下了。
先来看三数
判断哪三个数的和为0。这里特例化为0,其实也可以是target
我们的思路依旧是排序+双指针。
不过这里是三个数,所以我们先用for循环控制一个数,再去使用左右指针。
看代码就会明白这种思路,就是在for循环里加入左右指针的while循环。
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>>res;
sort(nums.begin(),nums.end());
for(int i=0;i<nums.size();i++){
if(nums[i]>0)//如果排序后第一个大于0,那么和一定不为0
return res;
if(i>0&&nums[i]==nums[i-1]){//去重
continue;
}
int left=i+1;
int right=nums.size()-1;
while(left<right){
if(nums[i]+nums[left]+nums[right]>0){
right--;
while(right>left&&nums[right]==nums[right+1])
right--;
}
else if(nums[i]+nums[left]+nums[right]<0){
left++;
while(right>left&&nums[left]==nums[left-1])
left++;
}
else{
res.push_back({nums[i],nums[left],nums[right]});
while(right>left&&nums[right]==nums[right-1])
right--;
while(right>left&&nums[left]==nums[left+1])
left++;
left++;
right--;
}
}
}
return res;
}
后面的过程和我们之前的两数模板一样。
for循环中有三处需要注意的地方:
-
if(nums[i]>0)这里,是因为我们的target比较特殊,刚好是0,所以可以这样判断。但如果target是任意值就不可以了。
-
去重操作if(i>0&&nums[i]==nums[i-1]),一定要注意去重 要和前一个比,不能和后一个比,因为和后一个还没有遍历到的值比就可能会漏掉结果。
-
left开始的位置不是0,是i的下一个,因为三个数分别是nums[i],nums[left],nums[right]
四数之和
同理,四数之和我们也可以轻松解决。只需要先控制两个数进行for循环嵌套,剩下的两个数依旧使用左右指针。
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>>res;
sort(nums.begin(),nums.end());
for(int i=0;i<nums.size();i++){
if(i>0&&nums[i]==nums[i-1]){//去重
continue;
}
for(int k=i+1;k<nums.size();k++){
if(k>i+1&&nums[k]==nums[k-1]){//去重
continue;
}
int left=k+1;
int right=nums.size()-1;
while(left<right){
if(nums[i]+nums[k]>target-(nums[left]+nums[right])){
right--;
while(right>left&&nums[right]==nums[right+1])
right--;
}
else if(nums[i]+nums[k]<target-(nums[left]+nums[right])){
left++;
while(right>left&&nums[left]==nums[left-1])
left++;
}
else{
res.push_back({nums[i],nums[k],nums[left],nums[right]});
while(right>left&&nums[right]==nums[right-1])
right--;
while(right>left&&nums[left]==nums[left+1])
left++;
left++;
right--;
}
}
}
}
return res;
}
代码看上去有点长,但实际步骤还是一样的。
nums[i]+nums[k]>target-(nums[left]+nums[right])这步的处理方法是因为如果将四者相加有可能会溢出,所以需要做一下减法。
复杂度
如果我们使用暴力解法,那么对于三数之和来说,就是三个for循环。时间复杂度就是O(n的三次方)
但是使用双指针后可以减少一个for循环,而内部的left和right其实就是一个O(n)的复杂度。
所以时间复杂度是O(n的平方)(排序复杂度O(nlogn),小于平方)
空间复杂度因为我们修改了数组(排序),所以相当于存储了副本,为O(n)
同理,四数之和也就是多了一个量级,时间复杂度为O(n的三次方)
四数之和2
有一道非常像四数之和的题目
给定四个整数数组,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0
其实这里就是要找四个数相加为0,但是不考虑重复的问题,所以用哈希表就可以完美解决。
因为最后返回的是个数,所以我们需要unordered_map来统计和以及对应出现的次数
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int>myMap;
for(int a:nums1)
{
for(int b:nums2)
{
myMap[a+b]++;//记录a+b的和以及出现次数
}
}
int count=0;
for(int c:nums3)
{
for(int d:nums4)
{
if(myMap.count(0-(c+d)))//寻找互补,找到就记录次数。
{
count+=myMap[0-(c+d)];//
}
}
}
return count;
}
回到两数之和
我们在开篇说到了两数之和,并且对问题做了一些修改,返回了数值。
而原题其实是返回下标的。
在返回下标时,我们就不能使用双指针法了,因为排序破坏了原有的顺序。
这时候使用哈希表可以降低复杂度。
因为结果要输出下标,所以我们需要unordered_map对应这个数以及它的下标。
锁定i,然后寻找target-nums[i]。
如果没有,把当前的数和下标存入。
如果有,返回找到的这个数的下标以及当前的i,就是最后需要的下标数组
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int>res;
for(int i=0;i<nums.size();i++){
if(res.count(target-nums[i])){
auto ite=res.find(target-nums[i]);
return {ite->second,i};
}
res.insert(pair<int,int>(nums[i],i));
}
return {};
}