【刷题日记】三数之和

67 阅读4分钟

1、 题目描述

LeetCode 15.三数之和

😫给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

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

在这里插入图片描述

2、思路

思路历程

  1. 简单的思路,三重for循环,分别是num1、num2、num3 暴力枚举所有的数组元素,求和后与0比对 相等则加入最后的返回值中,最后对返回值去重,简单粗暴可以得出结果,但是leetcode最终会执行超时。

  2. 优化思路,暴力穷举的问题有哪些:

  • 重复的值最终收集一次就行了,穷举会有多余的计算。

  • 穷举会完全遍历每一个元素 ,时间复杂度达到了 n3 ,既然是规则的计算 是否存在逻辑 可以不进行遍历所有的元素,只需要遍历部分元素计算即可

解决暴力穷举的方案

重复的值最终收集一次就行了,穷举会有多余的计算 解决方案
首先对原数组进行排序(排序后很好定位num1、num2、num3的值 便于去重),num1选择一个元素后穷举排序后的num2与num3,得到此时 num1 值的所有三数之和为0 的num2与num3。
后续遍历时 如果选择的num1以前选择过 那么这时num1对应的num2与num3肯定是重复的,可以跳过遍历。

是否存在逻辑 可以不进行遍历所有的元素
针对排序后的数组,选择确定 num1 后,可以利用双指针的逻辑 ,num2 从 num1 后一个元素向右遍历,num3 从数组末尾向左遍历。

  1. 如果 num1 大于0,由于排序后的num1之后的数肯定大于0 ,那么可以肯定 在此之后再也没有能等于 零的三数之和了可以直接返回。

  2. 如果此时 三数之和大于0,那么将 num3 向左移 减小 num3

  3. 如果此时 三数之和小于0,那么将 num2 向右移 增大 num2。

  4. 如果此时 三数之和等于0,收集。

3、代码

为了不啰里八嗦 😂,只提供优化后的思路代码

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        // 排序数组 方便去重
        Arrays.sort(nums);
        // 结果
        List<List<Integer>> res = new ArrayList<>();
        
        // 第一个数索引
        for (int num1Index = 0; num1Index < nums.length - 2; num1Index++) {
            int num1 = nums[num1Index];
            // 此后不存在三数之和等于0
            if(num1 > 0) {
                return res;
            }
            // 去重 num1
            if (num1Index > 0 && num1 == nums[num1Index - 1]) {
                continue;
            }
            // 分别从左边取一个数与右边取一个数
            int num2Index = num1Index + 1;
            int num3Index = nums.length - 1;
            while (num2Index < num3Index) {
                int threeNumSum = num1 + nums[num2Index] + nums[num3Index];
                if (threeNumSum == 0) {
                    List<Integer> resArr = new ArrayList<>();
                    resArr.add(num1);
                    resArr.add(nums[num2Index]);
                    resArr.add(nums[num3Index]);
                    res.add(resArr);
                    // 去重 num2
                    while (num2Index < num3Index && nums[num2Index] == nums[num2Index + 1]) {
                        num2Index++;
                    }
                    // 去重 num3
                    while (num2Index < num3Index && nums[num3Index] == nums[num3Index - 1]) {
                        num3Index--;
                    }
                    num2Index++;
                    num3Index--;
                } else if (threeNumSum < 0) {
                    num2Index++;
                } else {
                    num3Index--;
                }
            }
        }
        return res;
    }
}



4、算法分析

复杂度分析

时间复杂度: num1 遍历一遍,num2与num3 共用遍历一遍 ,总时间复杂度为 O(n2)
空间复杂度: 数组排序算法O(nlogn),修改了输入的原始数组 可以当成使用了数组的副本 O(n),总空间复杂度O(n)
实际执行耗时与内存占用率:
在这里插入图片描述

5、总结

  1. 暴力穷举能解决问题,但是一般总是可以优化的,优化的点无非就是暴力的缺点,分析出暴力的缺点就可以针对性的提出改进的思路。

  2. 没有好的解题思路 先暴力做出来,再优化也是不错的。

6、tips

上述实现的代码中 ,内存占用偏高,还有优化空间,我自己也实验了近三十个版本。最有效的提升空间利用率到方式是 将 new ArrayList 对象改为使用 Arrays.asList ,执行效率如图:
在这里插入图片描述

那么问题又来了 ,new ArrayList 与 Arrays.asList 的区别是什么呢?

一句话讲清楚 Arrays.asList是什么!!!