前段时间开始更新的 「力扣高频题」 系列文章中,第一篇文章就是 大名鼎鼎的 两数之和 问题 。
想必 两数之和 问题也是难倒了好多刚准备刷力扣的小伙伴,今天我们继续升级,来看一道中等难度的 「三数之和」问题 。
关注公众号,在 主页合集 中可以查看更多相关文章哦~~~
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
)。
- 若
arr[L] + arr[R] < -num
,则需要L++
增大数值; - 若
arr[L] + arr[R] > -num
,则需要R--
减小数值; - 若
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;
}
代码本身并不难理解,相信小伙伴能够很轻易的写出来。
下面我们 从代码层面 上做一个小小的改进。
在threeSum
中cur.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版 ~
在看 + 转发
让你的小伙伴们一起来学算法吧!!