算法双指针系列-Day6-三数之和


前言

本题依旧是使用双指针解决,需要用到上一个题目两数之和的知识,没看过上一篇文章的不用着急,可以先接着往下看,看不懂了再去看两数之和内个题目,这是链接和为s的两个数。本题的难度要比两数之和难上许多,当然后面还有个四数之和,他们的关系是层层递进的。
讲完四数之和后,我们的双指针系列也到了尾声,下一个系列是滑动窗口。


一、题目链接

三数之和


二、题目描述

在这里插入图片描述


三、题目分析

  1. 题目中要求判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j!= k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。即数组中的每一个元素只能使用一次,不能重复使用,而且还得满足这三个数加起来等于0.
  2. 题目中要求答案中不可以包含重复的三元组,
    输入: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 = 0 和0 + 1 + (-1) = 0顺序不一样,但它们的三个元素值是一样的,所以是重复的,只保留一个就可以。
    不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
    注意,输出的顺序和三元组的顺序并不重要。

四、解法一(暴力解法,会超时)

可能会有小伙伴问,为什么每次都要讲暴力解法呢?直接来个最优的解法不就行了。我想说的是,我们的最优解往往是在暴力解法的基础上进行优化得来的,所以在讲最优解之前,先熟悉一下暴力解法我认为还是有必要的。

暴力嘛。那肯定是直接三层for循环把每一种情况都遍历一遍,看看符不符合结果,就是时间复杂度会有点高,是O(n^3)。
另一个问题是去重,我们可以讲结果先排个序,检测两组答案中的元素一样不一样金星去重(可以利用set容器去重)。
例如:上面例子中的[-1,0,1]、[0,1,-1]、将每一组中的元素进行排序题目就变成了[-1,0,1]、[-1,0,1],这样利用set就可以很容易的进行去重了。
具体代码就不编写了,我们重点看优化后的解法二。


五、解法二(排序+双指针)

1.我们可以先对数组排个序,因为排序之后,我们列出来的元素就是有序的,而且相同的数字会挨在一起,便于我们去重。
2. 要求三数之和为0,我们可以先固定一个数a,然后在后面的区间里找两数之和等于-a。两个数之和等于-a那不就是我上篇文章刚讲过的内容嘛,这样问题不就迎刃而解了。不熟悉的小伙伴可以先去看和为s的两个数这篇文章,比较简单,大家可以迅速看完再回来。
总体流程:
两数之和对应代码:

while(left<right)//两数之和的方法
            {
                int n=nums[right]+nums[left];
                if(target>n) left++;
                else if(target<n) right--;
                else //else里面的内容可以暂且不管,后面会详细讲解。
                {
                    ret.push_back({nums[i],nums[left],nums[right]});
                    left++;
                    right--;
                    //去重操作,left和right
                    while(left<right && nums[left]==nums[left-1]) left++;
                    while(left<right && nums[right]==nums[right+1]) right--;
                }  

1.排序 2.固定一个数a 3.在该数后面的区间内利用双指针算法快速找到两个数之和等于-a即可。
在这里插入图片描述

其中有个小优化,那就是固定的内个数a一定是<=0的,因为数组是有序的,如果a>0那么a后面区间的元素肯定都>0,加起来是不可能等于0的,因此我们只需要讨论a<=0的情况

处理细节问题
1.去重:
(1)找到一种结果后left和right指针要跳过重复元素。
以下图为例分析:
我们可以看到left指向的元素和left-1指向的元素相同,那三个数已经有两个数固定了,最终right找下的第三个数肯定也是一样的啊,所以一定会重复,此时我们只需要继续让left++,直到它和left-1指向的元素不再相等就可以了。我们可以这样写:

 while (nums[left] == nums[left - 1])
       left++;

在这里插入图片描述
同理right也是一样,如果right指向的元素和right+1指向的元素相同,那三个数已经有两个数固定了,最终left找下的第三个数肯定也是一样的啊,所以一定会重复,此时我们只需要继续让right–,直到它和right+1指向的元素不再相等就可以了。我们可以这样写:

while (nums[right] == nums[right + 1])
      right--;

但是这里有个问题那就是一直left++或者right–越界了怎么办,所以我们要加个限制条件left<right防止越界。因此完整的代码应该这样写:

// 去重:left right
while (left < right && nums[left] == nums[left - 1])
      left++;
while (left < right && nums[right] == nums[right + 1])
      right--;

(2)当使用完一次双指针算法后,i也要跳过重复元素。
以下图为例分析:
当第一个-4处理完之后要i++我们发现第二个也是-4,那不就i和i-1指向的元素就又一样了,这就又会导致重复为了解决问题,我们依旧让i继续++,直到i和i-1指向的元素不同为止。
在这里插入图片描述
我们可以在for循环内部这样写:

 // 去重:i
i++;
while (i < n && nums[i] == nums[i - 1]) i++;

既然for循环内部实现i++了那我们就不这样写了:

for (int i = 0; i < n;i++)

而是:

for (int i = 0; i < n;)

2.不漏:
找到一种结果后不要停,缩小区间继续寻找。即left++;right–
在这里插入图片描述


六、代码编写

部分解释:

ret.push_back({nums[i],nums[left],nums[right]});
//因为返回值是vector<vector<int>>,所以在ret调用push_back时括号内也应该放一个vector<int>类的对象。
//括号内的{nums[i],nums[left],nums[right]}会自动构建一个vector<int>类的对象,然后被push_back。

C++完整代码:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) 
    {
        vector<vector<int>> ret;
        //1.排序
        sort(nums.begin(),nums.end());
        //2.利用双指针解决问题的
        int n=nums.size();
        for(int i=0;i<n;)//固定数a
        {
            int left=i+1,right=n-1,target=-nums[i];
            if(nums[i]>0) break;//小优化
            while(left<right)//两数之和的方法
            {
                int n=nums[right]+nums[left];
                if(target>n) left++;
                else if(target<n) right--;
                else
                {
                    ret.push_back({nums[i],nums[left],nums[right]});
                    left++;
                    right--;
                    //去重操作,left和right
                    while(left<right && nums[left]==nums[left-1]) left++;
                    while(left<right && nums[right]==nums[right+1]) right--;
                }  
            }
            i++;
            //去重i
            while(i<n && nums[i]==nums[i-1]) i++;
        }
        return ret;
        
    }
};

运行结果:
在这里插入图片描述
Java完整代码:

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> ret = new ArrayList<>();
        // 1. 排序
        Arrays.sort(nums);
        // 2. 利用双指针解决问题
        int n = nums.length;
        for (int i = 0; i < n;) // 固定数 a
        {
            if (nums[i] > 0)
                break; // 小优化
            int left = i + 1, right = n - 1, target = -nums[i];
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum > target)
                    right--;
                else if (sum < target)
                    left++;
                else {
                    // nums[i] nums[left] num[right]
                    ret.add(new ArrayList<Integer>(Arrays.asList(nums[i],
                            nums[left], nums[right])));
                    left++;
                    right--; // 缩小区间继续寻找
                    // 去重:left right
                    while (left < right && nums[left] == nums[left - 1])
                        left++;
                    while (left < right && nums[right] == nums[right + 1])
                        right--;
                }
            }
            // 去重:i
            i++;
            while (i < n && nums[i] == nums[i - 1])
                i++;
        }
        return ret;
    }
}

运行结果:
在这里插入图片描述


总结

勤加练习!


本文到这里就结束了,希望友友们可以支持一下一键三连哈。嗯,就到这里吧,再见啦!!!

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

争不过朝夕,又念着往昔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值