力扣15 - 三数之和【奇妙的双指针】
Hello大家好,又做到一题比较有挑战性的题目,一种详细而又巧妙的解法送给大家:gift_heart:
@TOC
一、原题描述
原题传送门
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。(很烦:angry:)
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = [0,1,1]
输出:[]
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
二、思路分析
1、题型引入
- 之前有做过一道题叫做力扣1.两数之和,是用unordered_map分别记录数组元素的值和所属下标,然后去查找是否有符合两数之和等于target的元素值,若是有,则将其下标返回,若不是,则将其放入map中做匹配用
既然说到这份上了,就顺便给出代码吧:ear_of_rice:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> map;
for(int i = 0;i < nums.size(); ++i)
{
auto iter = map.find(target - nums[i]);
if(iter != map.end())
return {iter->second,i};
else
map.insert(pair<int,int>(nums[i],i));
}
return {};
}
};
- 然后对于这道三数之和,可谓大相径庭,不仅是需要三个数,而且返回的不是下标,是那个所属元素,不过最复杂的还是答案中不可以包含重复的三元组这句话,比较烦人,因为这就需要考虑三个数去重后的情况
- 这题其实也可以利用==哈希法==来解,但是去重的部分过于复杂而且效率又不是很高,便不做细解,给出C++代码给大家看一下
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
2、对比分析
- 最后回归我们的正题,就是用双指针的方法来解这道题,我觉得这是比较优的而且也比较巧妙的一种方法,==如果您还能想出比此方法还优的解法记得私信我哦==,讲一下整体的这个思路,主要就是先对给到nums数组做一个排序,这样才可以对它进行一个前后的移位遍历,两数之和是要返回下标的话去排序的话顺序就都乱了,但三数之和返回的不是下标,只是数字,因此可以将其重新打乱顺序,==这里的排序也是这题的一个重要环节,对后面的所有代码起着关键作用==
- 将三个数设置为a,b,c,题目要求是a + b + c = 0,所以我们要去获取a,b,c三个数,这里的a,可以用起始下标i获取,对于b,c,我这里是设置了一个前后指针left和right,去前后不断地搜寻符合条件的这两个数
- 这题的双指针很灵巧又易于理解,但去重是一个关键,因为题目讲了需要的是一个不重复的三元组,也就是你result中有{1,2,3}了,就不可以再包含重复的与这三个数一样的一个三元组了,==关于具体的操作和代码,且听我慢慢道来==
三、代码详解
1、分步骤解析
- 首先,你需要有一个最终的结果集去收集并返回,而且前面我们说过,在最前面要对所给数组排个序,这步操作是后面代码的最重要基础,不可忽略:key:,==因为只有排了序,后面的前后指针才可以进行移位替补操作==
vector<vector<int>> result;
sort(nums.begin(),nums.end()); //返回不是下标,因而可以排序
//a + b + c = 0
//nums[i] = a nums[left] = b nums[right] = c
- 然后就是去遍历这个数组
for(int i = 0;i < nums.size(); ++i)
- 接下来首先应该去考虑的就是特殊一点的情况,因为是排过序的数组,前面一定是最小的,如果第一个数a > 0,则相加不可能为0,因此一旦碰到第一个数就为0的情况,直接return result即可。
- 接着我们就要去考虑对第一个数a去重,有些小伙伴可能直接写nums[i] == nums[i + 1],这就不对了,你拿一个数和它后一个数去做比较,但a的后一个数接的是left指针,这相当于错位混乱了,更像是去判断一个结果集中是否有重复元素,正确的写法应该是nums[i - 1]才对,让nums[i]去和它前一个数做比较,相等和不相等都不会影响后面的left指针
- 但还有一点也要考虑到,就是我们示例1中给到的这种,[-1,-1,2],你想要是判断到-1 == -1,直接continue了,那不是缺失了这一种结果了吗,所以更加严谨的写法应该多加一个判断,那就是i > 0,如果所属下标值的元素满足这种情况,则强制进入下一循环,放弃这一循环的遍历
if(nums[i] > 0) //因为是排过序的数组,前面一定是最小的,如果a > 0,则相加不可能为0
return result;
//对a去重
//要考虑到[-1,-1,2],若不写i > 0,则会略过这一种情况
// if(nums[i] == nums[i - 1]) continue;
if(i > 0 && nums[i] == nums[i - 1]) continue;
接下去轮到本题的==重头戏——双指针==登场了,准备头脑风暴:ocean:
- 前面给出过图示,left指针就位于下标i之后,right指针位于本数组的最后一位
- 然后就是两个指针的判断和交替移动,直到left == right为止,有些小伙伴很疑惑为什么不是left <= right,因为left和right不能相等,而且所求集合为三元组,若left与 right相等,则指向同一元素,只有两个数便不符合题目要求了,因而将此作为循环的退出条件
int left = i + 1; //左指针为i + 1
int right = nums.size() - 1; //右指针为nums.size() - 1
while(left < right)
//left和right不能相等,因为所求集合为三元组,若left == right,则指向同一元素
- 这是就又有人心急着想要对b,c进行去重了,但是在循环一开始就去重的话是不妥的,也是会丢掉一种[0,0,0]的情况,因为我们在后面对符合要求的三元组要进行取值操作,如果在循环开始就判断left和right是否相同,那两个内嵌while循环一直移动判定,直到left和right指向同一个0为止,就直接会判定此结果不符合,便跳出外层while循环了,所以我们不能在此处对b,c进行去重
//此处不可对左右指针去重,否则会漏掉[0,0,0]这一种情况
// if(left < right && nums[left] == nums[left + 1]) left++;
// if(left < right && nums[right] == nums[right - 1]) right--;
- 最后就是内部循环的操作,前面说过,因为排了序,数据呈现一个升序,操作起来就会很方便,我们直接对nums[i] + nums[left] + nums[right]是否 == 0进行一个判断,
①如果三者之和 < 0,则说明和不够大,这时就将左指针后移,取得更大数字;
②如果三者之和 > 0,则说明和太大,这时就将右指针前移,取得更小数字;
③如果恰好 = 0,那么则说明刚好取到一组之和为0的三个数,就定义一个小结果集三个数放入其中并将小结果集push_back()进大结果集中
//因排过序,为升序
if(nums[i] + nums[left] + nums[right] < 0)
left++; //左指针后移,取得更大数字
else if(nums[i] + nums[left] + nums[right] > 0)
right--; //右指针前移,取得更小数字
else{ //表示刚好取到一组之和为0的三个数
result.push_back(vector<int>{nums[i],nums[left],nums[right]});
//在获取一个三元组后,前后指针继续移动去重b,c
//需使用while,直到符合正确条件为止,因为b,c可能连着相等
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
//找到正确答案后,双指针同时收缩
left++;
right--;
- 在获取一个三元组后,我们便可以操纵前后指针继续移动去重b,c,因为此时已经找到了一组符合条件的值,在此处去重时,比较重要的一点是==要使用while循环一直改变left和right的指向==,不然找到一个相同就跳出进行下一外循环了,这就是我第一次提交后的反馈,很明显,这里的b,c还没有去重,右指针在指向第二个1之后便直接退出了,因此多收集了一组结果
- 最后,在找到正确答案后,双指针同时收缩即可,直到left == right为止,便跳出外层while循环
2、指针数据变化表
接着给出一个完整的收集数据的过程展示,就不一张张图片显示了,大家可以对照着开头的初始指针指向图和下一环节的动画展示自己在画图软件里推敲一下:mag:会加深对这道题的理解
3、动画展示
前面有说到一组[-1,-1,2]的情况,用动画呈现(微信手机端看不到)
[video(video-nmuJmJOG-1661677932597)(type-csdn)(url-https://live.csdn.net/v/embed/235166)(image-https://video-community.csdnimg.cn/vod-84deb4/26ed089b61c34f37b43ed60175f0d7a6/snapshots/a41628d13e6a4726bcc8cc3259597ca0-00002.jpg?auth_key=4815256456-0-0-d39ac41a750a518b5a343942675c27a0)(title-)]
4、整体代码展示
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(),nums.end()); //返回不是下标,因而可以排序
//a + b + c = 0
//nums[i] = a nums[left] = b nums[right] = c
for(int i = 0;i < nums.size(); ++i)
{
if(nums[i] > 0) //因为是排过序的数组,前面一定是最小的,如果a > 0,则相加不可能为0
return result;
//对a去重
//要考虑到[-1,-1,2],若不写i > 0,则会略过这一种情况
// if(nums[i] == nums[i - 1]) continue;
if(i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1; //左指针为i + 1
int right = nums.size() - 1; //右指针为nums.size() - 1
while(left < right)
{
//left和right不能相等,因为所求集合为三元组,若left == right,则指向统一元素
//此处不可对左右指针去重,否则会漏掉[0,0,0]这一种情况
// if(left < right && nums[left] == nums[left + 1]) left++;
// if(left < right && nums[right] == nums[right - 1]) right--;
//因排过序,为升序
if(nums[i] + nums[left] + nums[right] < 0)
left++; //左指针后移,取得更大数字
else if(nums[i] + nums[left] + nums[right] > 0)
right--; //右指针前移,取得更小数字
else{ //表示刚好取到一组之和为0的三个数
result.push_back(vector<int>{nums[i],nums[left],nums[right]});
//在获取一个三元组后,前后指针继续移动去重b,c
//需使用while,直到符合正确条件为止,因为b,c可能连着相等
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
//找到正确答案后,双指针同时收缩
left++;
right--;
}
}
}
return result; //返回最后的可能结果
}
};
四、回顾总结
本题的讲解到这里就结束了,看完了这种双指针的方法,您是否又多了一种解题的方法和思路,其实有很多题目都可以用双指针这个算法来实现,也会显得比较精确而巧妙,如果您对讲解的哪处有所疑问,可以于评论区或者私信我,感谢您对本文的观看:rose:
以下是相关的题型,看完本题可以去继续去练练手
刷题的心路历程
- 大一这一学年没有刷过题,也不知道去哪里刷题,直到这个暑假,才开始慢慢地在LeetCode上刷题,也渐渐地提升了自己的算法和编程能力。回想到自己初学C语言的时候,初次接触指针这个东西,完全就明白不了,指针指向内存中某块地址是何意思,慢慢地通过看b站上面各种关于指针的详细讲说,以及鹏哥对于指针的教授,还有在C站中对各种文章的浏览与学习,才指针地了解了指针,明白了指针在C/C++中的重要性和地位,直到现今,我不仅可以轻松熟练使用指针,而且可以使用双指针来解题目,我感到非常开心与自豪,觉得我这一路走来以及这一路的学习并没有白费,而且我觉得自己对C/C++这门语言了解得还不是很深刻,希望在接下来的学习中,可以更加深入地学习与了解更多新的知识
- 点赞
- 收藏
- 关注作者
评论(0)