【LeetCode】详解三数之和15. 3Sum Given an array nums of n integers, are there elements a, b, c in nums


前言

有一段时间没刷LeetCode了,发现有点生疏,临近大三,需要多刷题!通过这篇博客记录LeetCode15题三数之和的思路。


正文

原题:

链接:三数之和

Given an array nums of n integers, are there elements a, b, c in nums such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.
Note:
The solution set must not contain duplicate triplets.
Example:
Given array nums = [-1, 0, 1, 2, -1, -4],
A solution set is:
[
[-1, 0, 1],
[-1, -1, 2]
]

题目大意
给定一个整数型数组,找出符合a + b + c = 0的情况。

思路1:

最先想到的就是暴力枚举,使用三层循环依次遍历是否存在a + b + c == 0 的情况,其中a代表第一层循环的值,b代表第二层循环的值,c代表第三层循环的值,时间复杂度为O(N³)

代码

public List<List<Integer>> threeSum(int[] nums) {
   if (nums == null || nums.length < 3) {
        return new ArrayList<>();
    }
    // 先进行排序,方便去重
    Arrays.sort(nums);
    List<List<Integer>> list = new ArrayList<>();
    for (int i = 0; i < nums.length; i++) {
        for (int j = i + 1; j < nums.length; j++) {
            for (int k = j + 1; k < nums.length; k++) {
                if (nums[i] + nums[j] + nums[k] == 0) {
                    //添加到list中
                    list.add(Arrays.asList(nums[i], nums[j], nums[k]));
                }
            }
        }
    }
    // 去重,因为结果可能存在重复情况,这里使用HashSet对其去重
    HashSet set = new HashSet(list);   
    list.clear();   
    list.addAll(set); 
    return list;
}

代码讲解
一开始先判断为null或者长度小于3的情况,直接返回空集合。
对数组进行快排,快排的原因是为了方便去重,由于在判断a + b + c == 0时,可能存在[[-1,2,-1],[0,1,-1],[-1,0,1]]这种结果,即最后两种结果重复的情况,这时候若直接去重会比较麻烦,因此先进行排序,得到[[-1,-1,2],[-1,0,1],[-1,0,1]]类似这种结果,此时直接使用HashSet去重,就能得到示例的正确结果[[-1,-1,2],[-1,0,1]]。
但由于这种结果费时,直接提交肯定是Time Limit Exceeded
在这里插入图片描述

思路2:

由于3层循环耗时较长,我们可以尝试减少一层循环,使时间复杂度降到O(N²)
比如数组[-1, 0, 1, 2, -1, -4],第一层遍历a = -1,第二层遍历b = 0,此时使用map查找是否存在-(a + b)的值,即0 - a - b,也就是1,若存在1则将a b c添加到集合中
这里有个要注意的地方:map存储元素时,不应该放在查找map元素之前,而应该放在查找map元素之后(读者可以先思考一下,待会我会在后面进行说明)

代码

public List<List<Integer>> threeSum(int[] nums) {
    if (nums == null || nums.length < 3) {
        return new ArrayList<>();
    }
    Arrays.sort(nums);
    List<List<Integer>> list = new ArrayList<>();
    // 对全是0的情况特殊清理
    if (nums.length > 2 && nums[0] == 0 && nums[nums.length - 1] == 0) {
        list.add(Arrays.asList(0, 0, 0));
        return list;
    }
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        for (int j = i + 1; j < nums.length; j++) {
            if (map.containsKey(0 - nums[i] - nums[j])) {
                list.add(Arrays.asList(nums[i], nums[j], -(nums[i] + nums[j])));
            }
        }
        map.put(nums[i], i);
    }
    // 去重
    HashSet set = new HashSet(list);   
    list.clear();   
    list.addAll(set); 
    return list;
}

代码讲解
这里新增了对数组元素全部为0的特殊处理,对于全是为0的情况,我们就没必要使用两层for遍历或者使用额外的map空间。
在两层for中,下面的代码用来判断map是否存在0 - a - b的元素(其中a为nums[i]、b为nums[j]),若map中存在则将元素添加到集合中。

if (map.containsKey(0 - nums[i] - nums[j])) {
    list.add(Arrays.asList(nums[i], nums[j], -(nums[i] + nums[j])));
}

map存放元素的代码必须放在查找map之后,

map.put(nums[i], i);

这是因为若放在查找之前,会出现重复现象。
举个例子:比如数组[-1, 2, 1, 2],此时在查找之前已经将-1存放到map中,假设a = -1,b = 2,在if语句块中判断map是否存在0 - (-1) - (2),也就是判断-1是否存在,结果为true,因为查找之前已经将-1存进map中,可能会得到这样的匹配结果[-1, -1, 2],而在这个例子中-1只出现了一次而已,答案显然是错误的,这就是为什么需要在查找map之后再存放元素,避免出现重复的情况,提交结果Success,答案显然比第一种好很多了。
在这里插入图片描述

思路3:

先排序,再使用左右指针往中间夹的原理

代码

public List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);
    List<List<Integer>> list = new LinkedList<>();
    for (int i = 0; i < nums.length; i++) {
        // 去重
        if (i > 0 && nums[i] == nums[i - 1]) {
            continue;
        }
        int b = i + 1;
        int c = nums.length - 1;
        while (b < c) {
            if (nums[i] + nums[b] + nums[c] < 0) {
                b++;
            } else if (nums[i] + nums[b] + nums[c] > 0) {
                c--;
            } else {
                list.add(Arrays.asList(nums[i], nums[b], nums[c]));
                // 去重
                while (b < c && nums[b] == nums[b + 1]) b++;
                while (b < c && nums[c] == nums[c - 1]) c--;
                b++;
                c--;
            }
        }
    }
    return list;
}

代码讲解
跟前两种不同,这里只使用了一次for遍历,在确定a时,同时确定b跟c相加是否等于-a的情况。
由于数组是有序的,所以我们不需要再进行循环遍历是否匹配,我们可以使用两个变量分别指向数组(不包括a)的开头以及结尾,需要讨论的情况如下所示:

若a + b + c < 0,则说明需要增大其中一个数,而由于a是固定的,c又是最大元素,c无法变得更大,因此b需要往右移动一个位置,即b++
若a + b + c > 0,则说明需要减小其中一个数,同理,b是最小的(除了a),只能让c变小(往左移动一个位置),即c–
若a + b + c = 0,则b跟c同时往中间移动一个位置,即b++,c–

下面通过图片来解释,帮助大家理解,假设排序后的数组为[-4, -1, -1, 0, 1, 2]
第一层循环,先固定a,即a = -4,b指向a后面的位置,c指向最后一个元素的位置
在这里插入图片描述
经过上面的第一次比较发现三数小于0,此时b++,指向下一个位置,即指向2的位置
在这里插入图片描述
经过上面的第二次比较,三数还是比0小,b++,b=3指向0的位置
在这里插入图片描述
经过上面的第三次比较还是比0小,b继续++,指向1的位置
在这里插入图片描述
经过上面第四次比较发现还是比0小,此时b不能再往前了,b跟c相遇时,a需要往右移动一个位置,b跟c的位置变成如下所示:
在这里插入图片描述
根据上面所示,三数之和为0,符合情况,添加到集合中,并同时移动b和c,如下所示:
在这里插入图片描述
符合情况,添加到集合中,同时b跟c相遇,a需要指向下一个元素,重复上面的步骤,下面就不再继续画图了。
这里说明以下代码的作用,由于排序后的数组可能会出现这种情况[-4, -1, -1, -1, -1, -1, -1, 0, 1, 2],这时候若i停在第一个-1的下标时,就没有必要继续往下判断了,因为下面的情况都是类似的,若两个相邻的值相等,则使用continue语句跳过。

if (i > 0 && nums[i] == nums[i - 1]) {
    continue;
}

同理,下面的代码是为了去除b或者c指向的元素出现重复的情况,若b的下一个值等于b,则b往右移;若c的上一个值等于c,则c往左移。

while (b < c && nums[b] == nums[b + 1]) b++;
while (b < c && nums[c] == nums[c - 1]) c--;

复杂度分析:由于bc在移动过程中,最差移动了n-1次,我们可以看成是移动了n次,再加上第一层循环,综合起来时间复杂度为O(N²)
提交代码Success,还不错,比上面两种方法快了不少,内存使用情况也还算乐观,此题结束。
在这里插入图片描述


总结

又到了总结时刻,最近在刷题的同时,也在补Java、计网等知识点,有很多东西要补,尤其是Java,涉及到JVM、面向对象、IO、集合等等等等。

总之,接下来要更加努力就是了!不忘初心,坚持不懈!

对啦,四数之和的博客我也写啦!感兴趣的同学可以直接戳右边( ̄▽ ̄)"!?详解四数之和

发布了57 篇原创文章 · 获赞 282 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/weixin_41463193/article/details/91859621
今日推荐