15.三数之和

题目描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

题目链接

三数之和

测试用例

输入: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] 。
注意,输出的顺序和三元组的顺序并不重要。

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

解题思路

参考网址
暴力求解(本文没有代码实现):

  1. 最容易想到的思路:三个for循环遍历找到和为0的数,并用list进行记录。
  2. 存在的问题:题目中说三元组不能重复!用上述方式返回的三元组可能有重复的,还需要再次去重。【题解说可以利用哈希表去重,可以考虑一下】
  3. 不可行处:时间复杂度太高,一定会超时 O ( n 3 ) O(n^3) O(n3),并且去重消耗了额外的空间

题解思路:

  • 对于不重复问题
    • 可以考虑将原来数据进行排序
    • 得到类似a<=b<=c的结果,使得a+b+c=0(等号是由于题目中数组有重复元素,所以才是<=等号,类似于000)
  • 解决了不重复问题仍然是三个for循环,耗费时间较长
  • 但是在第一个for循环里面,实际是在找满足b+c=target的second和third,这是可以通过双指针实现
    • 当要枚举数组中的两个元素时,如果我们发现随着一个元素的递增,另一个元素是递减的,那么就可以利用双指针来实现
    • 双指针使得枚举的时间复杂度从 O ( n 2 ) O(n^2) O(n2)降到了O(n)
    • 因为左指针向右移动,右指针向左移动,两个指针一共移动的位置为O(n)
  • 通过上述方式,排序的时间复杂度为O(nlogn),枚举的时间复杂度为 O ( n 2 ) O(n^2) O(n2),因为第一重for循环里面的复杂度为O(n),所以总的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

代码实现

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        // a、b、c分别代表nums[first]、nums[second]、nums[third]对应的值
        //first、second、third代表下标/指针
        int length = nums.length;
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        int first, second, third;//利用三个值进行遍历

        //为了防止有重复的三元组,首先对数组进行排序,目的得到a<b<c的样式,使得a+b+c=0
        Arrays.sort(nums);//将数组进行排序

        //for循环遍历
        for(first = 0; first < length; first++){
            //需要枚举与上次不同的数,注意不要越界,即一定在first>0的情况下判断
            //注意是nums[first-1]而不是nums[first--],first--是需要修改first的值的
            if(first>0 && nums[first]==nums[first-1]){
                continue;
            }

            //寻找两数组元素和为-nums[first]的元素,利用双指针求解
            int target = -nums[first];

            //双指针,双重for循环求得答案,目标b<c的样式
            for(second = first+1; second<length; second++){
                //需要枚举与上次不同的数
                if(second>first+1 && nums[second]==nums[second-1]){
                    //枚举下一个second
                    continue;
                }
                //第三个指针从后往前遍历
                third = length-1;
                //b与c的加和应该为target,如果两者加和小于target,直接跳出循环
                //因为在b不变的情况下,c从后往前是越来越小的
                //这个if语句可以不写。不满足下面的while循环,直接跳出了while循环,去判断了b+c是否等于target
                if(nums[second]+nums[third] < target){
                    //注意是continue,需要判断下一个second
                    continue;
                }
                //如果加和大于targe,c对应的指针左移
                while(second < third && nums[second]+nums[third]>target){
                    third--;
                }

                //如果两个指针指向的位置相同,代表再也不会出现b<c的样式,应当退出循环
                //若b=c时,满足b+c=target,这时这个数组中应该有两个相同的b或者c,指针secon和third肯定是不同的
                //所以应当先判断该条件,再判断b+c是否等于target
                if(second == third){
                    break;
                }

                //如果b+c=target,则找到满足目标条件的abc,需要写入list
                if(nums[second] + nums[third] == target){
                    List<Integer> list = new ArrayList<Integer>();
                    list.add(nums[first]);
                    list.add(nums[second]);
                    list.add(nums[third]);
                    res.add(list);
                }
            }
        }
        return res;
    }
}

在这里插入图片描述

代码优化

  1. third = length-1;的位置在第二个for循环之上(循环遍历second之上)
    原因:当second往后遍历的时候,b的值越来越大,third对应的c应当比上一循环里的c要小,否则肯定达不到b+c=target的效果。
    ==双指针的右指针没必要每次都从数组尾部向前移动!==在上次移动后的位置的基础上向前移动即可!
  2. 不判断if(nums[second]+nums[third] < target),后面的while循环包含了判断的过程,不是主要原因,时间差不多3ms
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        // a、b、c分别代表nums[first]、nums[second]、nums[third]对应的值
        //first、second、third代表下标/指针
        int length = nums.length;
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        int first, second, third;//利用三个值进行遍历

        //为了防止有重复的三元组,首先对数组进行排序,目的得到a<b<c的样式,使得a+b+c=0
        Arrays.sort(nums);//将数组进行排序

        //for循环遍历
        for(first = 0; first < length; first++){
            //需要枚举与上次不同的数,注意不要越界,即一定在first>0的情况下判断
            //注意是nums[first-1]而不是nums[first--],first--是需要修改first的值的
            if(first>0 && nums[first]==nums[first-1]){
                continue;
            }

            //寻找两数组元素和为-nums[first]的元素,利用双指针求解
            int target = -nums[first];

            //第三个指针从后往前遍历
            //这个third写在第二个for循环之上,节省时间!
            //原因,当second往后遍历的时候,b的值越来越大,third对应的c应当比上一循环里的c要小,否则肯定达不到b+c=target的效果。
            third = length-1;

            //双指针,双重for循环求得答案,目标b<c的样式
            for(second = first+1; second<length; second++){
                //需要枚举与上次不同的数
                if(second>first+1 && nums[second]==nums[second-1]){
                    //枚举下一个second
                    continue;
                }
                
                //b与c的加和应该为target,如果两者加和小于target,直接跳出循环
                //因为在b不变的情况下,c从后往前是越来越小的
                //这个if语句可以不写。不满足下面的while循环,直接跳出了while循环,去判断了b+c是否等于target
                //不写节省时间!
                // if(nums[second]+nums[third] < target){
                //     //注意是continue,需要判断下一个second
                //     continue;
                // }
                //如果加和大于targe,c对应的指针左移
                while(second < third && nums[second]+nums[third]>target){
                    third--;
                }

                //如果两个指针指向的位置相同,代表再也不会出现b<c的样式,应当退出循环
                //若b=c时,满足b+c=target,这时这个数组中应该有两个相同的b或者c,指针secon和third肯定是不同的
                //所以应当先判断该条件,再判断b+c是否等于target
                if(second == third){
                    break;
                }

                //如果b+c=target,则找到满足目标条件的abc,需要写入list
                if(nums[second] + nums[third] == target){
                    List<Integer> list = new ArrayList<Integer>();
                    list.add(nums[first]);
                    list.add(nums[second]);
                    list.add(nums[third]);
                    res.add(list);
                }
            }
        }
        return res;
    }
}

在这里插入图片描述

注意事项

continuebreak

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值