写点什么

力扣 15 - 三数之和【奇妙的双指针】

作者:Fire_Shield
  • 2022 年 9 月 11 日
    浙江
  • 本文字数:5518 字

    阅读完需:约 18 分钟

力扣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:


以下是相关的题型,看完本题可以去继续去练练手


1.两数之和18. 四数之和

刷题的心路历程

  • 大一这一学年没有刷过题,也不知道去哪里刷题,直到这个暑假,才开始慢慢地在 LeetCode 上刷题,也渐渐地提升了自己的算法和编程能力。回想到自己初学 C 语言的时候,初次接触指针这个东西,完全就明白不了,指针指向内存中某块地址是何意思,慢慢地通过看 b 站上面各种关于指针的详细讲说,以及鹏哥对于指针的教授,还有在 C 站中对各种文章的浏览与学习,才指针地了解了指针,明白了指针在 C/C++中的重要性和地位,直到现今,我不仅可以轻松熟练使用指针,而且可以使用双指针来解题目,我感到非常开心与自豪,觉得我这一路走来以及这一路的学习并没有白费,而且我觉得自己对 C/C++这门语言了解得还不是很深刻,希望在接下来的学习中,可以更加深入地学习与了解更多新的知识


发布于: 刚刚阅读数: 4
用户头像

Fire_Shield

关注

语言观决定世界观 2022.09.02 加入

高校学生,热爱编程,喜欢写作

评论

发布
暂无评论
力扣15 - 三数之和【奇妙的双指针】_双指针_Fire_Shield_InfoQ写作社区