系列文章目录
《双指针算法第一弹(移动零 复写零 快乐数)》链接:http://t.csdnimg.cn/Nqdvn
目录
系列文章目录
前言
1. 查找总价格为目标值的两个商品
(1)题目及示例
(2)思路(由浅入深)
2. 三数之和
(1)题目及示例
(2)一般思路
(2)双指针优化
3. 四数之和
(1)题目及示例
(2)一般思路
(3)双指针优化
总结
前言
本篇文章开启双指针算法第二弹,一起来感受双指针算法的魅力。这三道OJ题思路相似,难度由易到难,可以按照下面顺序自己挑战一下。每道题目中都带有链接,不用再去Leetcode寻找了!
1. 查找总价格为目标值的两个商品
(1)题目及示例
题目:购物车内的商品价格按照升序记录于数组
price
。请在购物车中找到两个商品的价格总和刚好是target
。若存在多种情况,返回任一结果即可。链接:. - 力扣(LeetCode)
示例 1:
输入:price = [3, 9, 12, 15], target = 18
输出:[3,15] 或者 [15,3]
示例 2:
输入:price = [8, 21, 27, 34, 52, 66], target = 61
输出:[27,34] 或者 [34,27]
(2)思路(由浅入深)
分析:这道题目让我们在给出的数组中寻找两个数字的总和刚好是target。其中这个数组是个升序数组,已经排好序了。
如果我们用正常思维,最先想到应该是暴力解法。写两层for循环,第一层循环固定第一个数,第二层循环从固定数后一个数开始寻找符合题目要求的数,然后固定的数从首元素开始到倒数第二个元素结束。两层for循环,最坏的情况是遍历n-1,n-2,……,1,加起来就是级别,时间复杂度O(),空间复杂度是O(1)。如果你把下面的代码写到Leetcode中去提交,大概率过不了,会超出时间限制。
vector<int> twoSum(vector<int>& price, int target) {int n = price.size();for(int i = 0; i < n - 1; i++)for (int j = i + 1; j < n; j++)if (price[i] + price[j] == target)return {price[i], price[j]};return {-1, -1};//照顾编译器}
我们该如何优化呢?需要注意到题目给出的数组是升序的,我们可以利用这个性质。我们使用两个变量表示数组元素下标,用来指向数组元素。此时两元素之和为sum。
- 如果其中一个变量加1,即指向后一个元素,因为数组是单调递增的,sum的值都会增大。
- 如果其中一个变量减1,指向前一个元素,那么指向元素比之前的元素小,sum的值会减小。
如下图,target等于31,left和right表示数组元素的下标。我们不让left和right一开始都指向首元素,让left指向首元素,right指向末尾元素。sum此时为首尾元素之和,跟target无非就大于,小于和等于三种关系。
- 下图中,sum小于target。如果使用暴力解法,right需要指向第二个元素9开始,然后往后挪动,寻找匹配的元素。可末尾元素是最大的,它和首元素相加都小于target,那么中间元素加上首元素肯定也小于target。因此,此时只需要将left++,指向后一个元素,sum才会增大,继续与target比较。
- 同理,此时target为37,sum大于target。如果使用暴力解法,那么left固定在末尾元素之前,都会跟末尾元素相加。首元素与末尾元素相加大于target,况且中间元素全部都比首元素大,那么中间元素加上末尾元素肯定也大于target。此时,只需要right--,指向前一个元素,使得sum减小,才有可能与target相等。
- 根据上面的分析再来实现代码,是很简单的。先定义三个变量left,right,sum,分别表示数组元素的下标和下标元素之和。使用while循环,循环条件是left小于right。
- 循环内部就是sum赋值为两元素之和,然后跟target比较。当sum大于target时,需要减小肃穆,right--,同理sum小于target时,就是left++。
- 如果相等,直接使用花括号返回两个元素,这样子的形式是隐士类型转换。
- 最后再循环外面再随便返回两个值,题目中没有说明找不到的情况返回什么,但是不返回的话,编译器会报错,认为如果循环走完没有返回值,就会出问题,所以可以随便返回两个值。
vector<int> twoSum(vector<int>& price, int target) {int left = 0, right = price.size() - 1, sum = 0;while(left < right){sum = price[left] + price[right];//三种情况if (sum > target)right--;else if (sum < target)left++;elsereturn {price[left], price[right]};//return vector{price[left], price[right]};}return {-1, -1};//照顾编译器}
2. 三数之和
(1)题目及示例
题目:给你一个整数数组
nums
,判断是否存在三元组[nums[i], nums[j], nums[k]]
满足i != j
、i != k
且j != k
,同时还满足nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为0
且不重复的三元组。注意:答案中不可以包含重复的三元组。链接:. - 力扣(LeetCode)
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[ [0,0,0] ]
解释:唯一可能的三元组和为 0 。
(2)一般思路
这道题我们使用暴力枚举,写三层for循环,找出所有的三元组合。再求和,寻找和为0的三元组。但是题目还有要求,输出的三元组不能重复,所以还需要检查和为0的三元组,进行去重操作。
- 如下图,示例1中的数组,有三个和为0的数组,肉眼观察就知道前两个是重复,那程序怎么辨别呢?可以对每个符合要求的三元组进行排序,再进行比较就可以去重。
- 但是符合要求的三元组非常多,每一个进行排序,消耗很大,效率低。因此,可以一开始就排排升序,让和为0的三元组按顺序存放,然后再使用set自动去重,也可以手动去重。
下面的代码就是按照上面的思路实现的。
- 数组先进行了排序,排序时间复杂度O(),但是使用了三层for循环,暴力枚举出所有三元组,时间复杂度是O()。总的时间复杂度是 O( + )。在这个时间复杂度中,n^3 项是主导项,因此可以简化为 O()。
- vector 存储最终的三元组,最坏情况下需要 O() 的空间,即所有可能的组合都不重复。set 用于去重,最坏情况下也需要 O() 的空间,以存储所有不重复的三元组。因此,总的空间复杂度是 O()。
vector<vector<int>> threeSum(vector<int>& nums)
{vector<vector<int>> ret;set<vector<int>> unique; // 使用set去重sort(nums.begin(), nums.end()); // 先对数组进行排序for (int i = 0; i < nums.size(); ++i) for (int j = i + 1; j < nums.size(); ++j) for (int k = j + 1; k < nums.size(); ++k) if (nums[i] + nums[j] + nums[k] == 0) {vector<int> tmp = {nums[i], nums[j], nums[k]};unique.insert(tmp); // 将三元组插入set中,自动去重}// 将set中的三元组拷贝到结果数组中for (const auto& e : unique) ret.push_back(e);return result;
}
(2)双指针优化
这道题其实跟上一道题解法类似,可以说是它的拓展。我们先对数组排升序,然后利用升序的性质使用双指针。不过这次需要寻找三个数字,可以固定首元素,寻找和为首元素的相反数的两个数。但是上一个题目只有寻找一对数。
在这道题中,如果找到一对符合要求的数字,还要继续寻找,直到两个变量相等,即指向同一个元素。需要注意,还要执行三个去重操作。target为固定元素的相反值,sum为left和right下标元素之和。
- 如下图,先固定首元素,使用双指针寻找和为target的两个元素。第四个元素之前,sum小于target,所以left需要不断加1,往后移动。直到指向第四个元素时,sum等于target,left指向前一个元素,right指向后一个元素,并且需要跳过相同的数
- 下面数组中,left和right指向的元素之和刚好为target。left++,right--,指向中间的元素,不过有两个2,相同的元素,right需要跳过去。
- 此时left指向的元素值是1,right指向元素的值也是1,符合要求,会记录下来。left和right指向的元素相邻,说明这一轮已经结束。first需要指向后面的元素,并且它也要跳过相同的元素,避免重复。
当我们知道需要三个去重操作之后,再去实现代码就比较容易。
- 其中排序消耗的时间复杂度是O(),两层for循环的时间复杂度是O(),综合起来,时间复杂度是O()。
- 空间上,使用了几个变量,还使用ret数组,数组的大小取决于输入数组中三元组的数量,最多也是O()。
- 首先对数组进行排序。其次使用for循环,固定首元素,直到倒数第三个元素。i表示固定元素的下标,我们一开始就判断,固定的元素跟前一个元素是否重复,重复就跳过,并且要注意先判断i大于0,不然会越界访问到前面内存空间并报错。
- for循环内部就是寻找和为target的二元组,跟第一道题目类似。只不过再找到一个二元组时,也需要跳过重复的数字,进行去重操作。这里也需要注意while循环中继续的条件,先写left<right,这个是保证两个变量相等时,不会发生对数组的元素发生二次遍历。
vector<vector<int>> threeSum(vector<int>& nums)
{sort(nums.begin(), nums.end());//对数组进行排序int n = nums.size();vector<vector<int>> ret;for (int i = 0; i < n - 2; ++i) {if (i > 0 && nums[i] == nums[i - 1]) continue; // 跳过重复的iint target = -nums[i];int left = i + 1, right = n - 1;while (left < right) {int sum = nums[left] + nums[right];if (sum < target) ++left;else if (sum > target) --right;else { ret.push_back({nums[i], nums[left], nums[right]});++left;while (left < right && nums[left] == nums[left - 1]) ++left; // 跳过重复的left--right;while (left < right && nums[right] == nums[right + 1]) --right; // 跳过重复的right}}}return ret;
}
3. 四数之和
(1)题目及示例
题目:给你一个由
n
个整数组成的数组nums
,和一个目标值target
。请你找出并返回满足下述全部条件且不重复的四元组[nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
链接:. - 力扣(LeetCode)
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
(2)一般思路
这道题目是三数之和的升级,寻找符合要求的四个数。
暴力解法也类似,先进行排序,再嵌套四层for循环,枚举出所有四元组,再求和比较是否与target相等。使用set去重,或者手动去重。这里就不在写代码了。
暴力解法中,使用sort排序时间复杂度O(),再使用了四层for循环,枚举出所有四元组,时间复杂度是O()。总的来说,时间复杂度就是O()。
(3)双指针优化
四数之和,是三数之和的延伸。两道题思路大体是一样的,先写两层for循环来固定两个数,下标分别为i和j,然后再转换成寻找和为target-nums[i]-nums[j]的二元组,又变成了如何解决第一道题目。不过这道题需要去重的地方有四处。
- first固定第一个数,跳过重复元素。
- second固定第二个数,跳过重复元素。
- left和right双指针,跳过重复元素。
实现代码时,需要注意Leetcode给出的测试用例中,target超出int整型范围,会造成溢出,需要换成long long来定义变量。
vector<vector<int>> fourSum(vector<int>& nums, int target) {sort(nums.begin(), nums.end());//对数组进行排序int n = nums.size();vector<vector<int>> ret;//存储四元组for (int i = 0; i < n - 3; ++i) //固定第一个数{if (i > 0 && nums[i - 1] == nums[i])//跳过重复的数字continue;//利用三数之和for (int j = i + 1; j < n - 2; ++j) //固定第二个数{if (j > i + 1 && nums[j - 1] == nums[j])//跳过重复的数字,需要注意不能写成j>0continue;int left = j + 1, right = n - 1;//示例target太大,会整型溢出,用long long类型转换一下long long aim = (long long)target - nums[i] - nums[j];// 利用双指针解决while(left < right){int sum = nums[left] + nums[right];if (sum > aim)--right;else if (sum < aim)++left;else{ret.push_back({nums[i], nums[j], nums[left], nums[right]});++left;while(left < right && nums[left] == nums[left - 1])++left;// 跳过重复的left--right;while(left < right && nums[right] == nums[right + 1])--right;// 跳过重复的right}}}}return ret;}
总结
通过这三道题目的锻炼,想必对双指针算法有自己的理解,尽量捋顺思路后,自己动手实现代码,看看有哪些坑点。多说无益,自己动手吧!
创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!