【力扣高频题】015.三数之和

前段时间开始更新的 力扣高频题 系列文章中,第一篇文章就是 大名鼎鼎的 两数之和 问题 。

想必 两数之和 问题也是难倒了好多刚准备刷力扣的小伙伴,今天我们继续升级,来看一道中等难度的 「三数之和」问题 。

关注公众号,在 主页合集 中可以查看更多相关文章哦~~~

15. 三数之和

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

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

示例 1:

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

示例 2:

输入: nums = [0,1,1]

输出: []

解释: 唯一可能的三元组和不为 0 。

示例 3:

输入: nums = [0,0,0]

输出: [[0,0,0]]

解释: 唯一可能的三元组和为 0 。

  • 提示:
  • 3 <= nums.length <= 3000
  • − 1 0 5 -10^5 105 <= nums[i] <= 1 0 5 10^5 105

思路分析

对于 三和为零 问题,我们可以由 两数之和 的思想展开思考。

如果说确定了 其中的一个数字,那么该题就变成了寻找 另外两个数字之和 等于确定数值 相反数 的题目。(这不就是大名鼎鼎的 两数之和 问题嘛),因此三重 暴力循环 很容易想到,时间复杂度高。

而且,本题还有个特殊点是,要求三元组 不重复

那很容易想到使用 哈希表去重,但该方法会浪费很多的空间,又增加了空间复杂度。


那换个思路:

如果数组是 有序 的,二元组问题就可以使用双指针解决,大大提高了效率。

在第一层循环确定了第一个数值num后,剩下的二元组问题,使用左右指针分别向中间移动,判断此时两指针指向的 数值之和 是否符合要求(arr[L] + arr[R] == -num)。

  1. arr[L] + arr[R] < -num,则需要L++增大数值;
  2. arr[L] + arr[R] > -num,则需要R--减小数值;
  3. arr[L] + arr[R] = -num,则需要记录此时的答案。

注意:

为了避免重复,在循环确定元素时,若发现 前一个元素与自己相等(数组已经有序了,相等元素必相邻),则 直接跳过 就行,因为在 前一个元素 的求解中已经得到了相应的答案,不必重复操作。

初版代码

public static List<List<Integer>> threeSum(int[] nums) {
    // 先排序
    Arrays.sort(nums);
    // 存放答案列表
    List<List<Integer>> ans = new ArrayList<>();
    // 三元组,至少三个数,所以第一个数 最多到 倒数第3个 下标
    for (int i = 0; i < nums.length - 2; i++) {
        // 数组第一个数 或者 与前一个数不相等再计算
        // 若与前一个相等,不必重新计算
        if (i == 0 || nums[i - 1] != nums[i]) {
            // 从下一个位置开始调用 二元组 求解问题
            List<List<Integer>> nexts = twoSum(nums, i + 1, -nums[i]);
            // 将答案加入到列表中
            for (List<Integer> cur : nexts) {
                // 由于数组已经有序,把 num[i] 放到第一个位置上
                cur.add(0, nums[i]);
                // 加入答案中
                ans.add(cur);
            }
        }
    }
    return ans;
}

public static List<List<Integer>> twoSum(int[] nums, int begin, int target) {
    int L = begin;
    int R = nums.length - 1;
    List<List<Integer>> ans = new ArrayList<>();
    // 从 begin 开始往后遍历
    while (L < R) {
        if (nums[L] + nums[R] > target) {
            R--;
        } else if (nums[L] + nums[R] < target) {
            L++;
        } else {
            // 若已经求过答案了,直接跳过,否则增添一个新的列表
            if (L == begin || nums[L - 1] != nums[L]) {
                List<Integer> cur = new ArrayList<>();
                cur.add(nums[L]);
                cur.add(nums[R]);
                ans.add(cur);
            }
            // 继续往下移动
            L++;
        }
    }
    return ans;
}

代码本身并不难理解,相信小伙伴能够很轻易的写出来。


下面我们 从代码层面 上做一个小小的改进。

threeSumcur.add(0, nums[i])需把第一层确定的数值 放在首位,由于使用的是Java 中 list.add()方法,本题需要将后面元素 整体往后移动 后,再增加 0 号位置元素,增大了复杂度。

因此,我们可以稍微改动下代码:倒着算,从右往左遍历

这样,新增的元素能够直接加入到后面,在 常数时间复杂度 上有所提高。

优化代码

public static List<List<Integer>> threeSum2(int[] nums) {
    Arrays.sort(nums);
    int N = nums.length;
    List<List<Integer>> ans = new ArrayList<>();
    // 从右往左遍历
    for (int i = N - 1; i > 1; i--) {
        if (i == N - 1 || nums[i] != nums[i + 1]) {
            List<List<Integer>> nexts = twoSum2(nums, i - 1, -nums[i]);
            for (List<Integer> cur : nexts) {
                cur.add(nums[i]);
                ans.add(cur);
            }
        }
    }
    return ans;
}

public static List<List<Integer>> twoSum2(int[] nums, int end, int target) {
    int L = 0;
    int R = end;
    List<List<Integer>> ans = new ArrayList<>();
    while (L < R) {
        if (nums[L] + nums[R] > target) {
            R--;
        } else if (nums[L] + nums[R] < target) {
            L++;
        } else {
            // 更新最新列表
            if (L == 0 || nums[L - 1] != nums[L]) {
                List<Integer> cur = new ArrayList<>();
                cur.add(nums[L]);
                cur.add(nums[R]);
                ans.add(cur);
            }
            L++;
        }
    }
    return ans;
}

当然,还可以从算法思路上进行适当的优化,比如:

当第一层循环已经遍历到 大于 0 的数组元素时,就可以 提前终止 了(后面的元素一定也大于 0 ,其和不可能为 0 )。

小伙伴们还有什么其他的思路优化么,可以在 评论区讨论一下 哦!


前面的算法文章,更新了许多 专题系列 。包括:滑动窗口、动态规划、加强堆、二叉树递归套路 等。

接下来的一段时间,将持续 「力扣高频题」 系列文章,想刷 力扣高频题 的小伙伴可以关注一波哦 ~

~ 点赞 ~ 关注 ~ 星标 ~ 不迷路 ~!!!
回复「1024」获取 Java 面试资源 ~
回复「ACM紫书」获取 ACM 算法书籍 ~
回复「算法导论」获取 算法导论第3版 ~

在看 + 转发

让你的小伙伴们一起来学算法吧!!

  • 36
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

强连通子图

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

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

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

打赏作者

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

抵扣说明:

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

余额充值