本文最后更新于:25 天前
15. 三数之和
题目描述
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。
请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
1 2 3 4 5 6 7 8
| 输入: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:
1 2 3
| 输入:nums = [0,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。
|
示例 3:
1 2 3
| 输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。
|
提示:
- 3 <= nums.length <= 3000
- -10^5 <= nums[i] <= 10^5
题解
解法一:暴力枚举 + 去重
最容易想到的方法,直接枚举三个数的组合,当和为 0 时,将当前组合排序,然后遍历结果集,如果当前结果集中没有这个组合则加入结果集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| std::vector<std::vector<int>> threeSum(std::vector<int>& nums) { std::vector<std::vector<int>> res; sort(nums.begin(), nums.end()); for (int i = 0; i < nums.size() - 2; i++) { if (i > 0 && nums[i] == nums[i - 1]) continue; for (int j = i + 1; j < nums.size() - 1; j++) { if (j > i + 1 && nums[j] == nums[j - 1]) continue; for (int k = j + 1; k < nums.size(); k++) { if (nums[i] + nums[j] + nums[k] == 0) { res.push_back({nums[i], nums[j], nums[k]}); break; } } } } return res; }
|
但是时间复杂度为 $O(n^3)$,提交后显示超时。
解法二:排序 + 双指针
解法一在去重过程需要先排序,不妨在最开始先进行排序。
当排序后,我们在遍历三元组时,当下标移动时遇到相同元组,直接跳过即可,避免了每次向结果集查找去重的操作。
遍历三元组时,必然先固定一个元素,nums[i],然后继续向后遍历 nums[j] 和 nums[k]。但是这样一次枚举就会导致时间复杂度过高。
可以发现,如果我们固定了前两重循环枚举到的元素 a 和 b,那么只有唯一的 c 满足 a+b+c=0
。当第二重循环往后枚举一个元素 b′ 时,由于 b′>b,那么满足 a+b′+c′=0 的 c′ 一定有 c′ < c
,即 c′ 在数组中一定出现在 c 的左侧。也就是说,我们可以从小到大枚举 b,同时从大到小枚举 c,即第二重循环和第三重循环实际上是并列的关系。
有了这样的发现,我们就可以保持第二重循环不变,而将第三重循环变成一个从数组最右端开始向左移动的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| std::vector<std::vector<int>> threeSum(std::vector<int>& nums) { std::vector<std::vector<int>> res; sort(nums.begin(), nums.end()); for (int i = 0; i < nums.size() - 2; i++) { if (nums[i] > 0) break; if (i > 0 && nums[i] == nums[i - 1]) continue; int m = i + 1, n = nums.size() - 1; while (m < n) { if (nums[n] < 0) break; if (nums[i]+nums[m]+nums[n] == 0) { res.push_back({nums[i], nums[m], nums[n]}); } if (nums[i]+nums[m]+nums[n] <= 0) { int x = nums[m]; while(x == nums[++m] && m < n); } else { int x = nums[n]; while(x == nums[--n] && m < n); } } } return res; }
|
总结
- 双指针,优化第二个数和第三个数遍历过程,将第三个数遍历过程变成一个从数组最右端开始向左移动的指针;
- 排序时间复杂度为 $O(n\log n)$,双指针时间复杂度为 $O(n^2)$,总时间复杂度为 $O(n^2)$。