📝 题目描述

题目链接三数之和

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

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

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

示例 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
  • -10^5 <= nums[i] <= 10^5

💡 解题思路

方法一:双指针✅️

因为题目要求我们数字不能重复,所以可以将数组中的元素从小到大进行排序,保证枚举的三元组 (a,b,c) 满足 a≤b≤c,保证了只有 (a,b,c) 这个顺序会被枚举到,同时排序也可以让相同的数字相邻,便于我们后续去重判重。

步骤如下:

  1. 先将数组排序。
  2. 然后,我们从0索引开始,固定一个数nums[i]
  3. 接下来我们就可以在剩下的区间中,使用左右指针 L(i + 1)R(指向最后一个元素) 寻找另外两个数字。
    • 如果nums[i] + nums[L] + nums[R] < 0,说明和太小,L 往右移;
    • 如果 nums[i] + nums[L] + nums[R] > 0,说明和太大,R 往左移;
    • 如果 == 0,记录答案(不能break,因为可能还有其他答案),并同时移动 LR,跳过重复值。

这里还有一个比较好的剪枝思路,就是我们固定第一个数nums[i]时,如果nums[i] > 0,那么我们可以直接退出循环。因为数组已经排序,而题目要求和为0,如果nums[i] > 0,那么后续的数字肯定也都大于0,也就不存在和为0的组合了。

方法二:类似两数之和

复用两数之和的思路,首先也是需要先排序,便于后面跳过重复值。
接下来则需要提前将数字存放在哈希表中,由于之前排了序,这时重复的元素只记录的最后的索引,其余的被覆盖了。
然后同样固定一个数nums[i],那么剩下的就是寻找和为0 - nums[i]的数了,变成了两数之和问题,但这里找到答案时,需要限制(i < j) && (j < k),防止出现重复。

🔧 代码实现

1、双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
int n = nums.size();
sort(nums.begin(), nums.end()); // 1. 排序

for (int i = 0; i < n; i++) {
// 剪枝:如果当前数字已经大于0,因为已经排序,后面不可能凑出0了
if (nums[i] > 0) break;

// 去重:对于固定的第一个数,如果和前一个一样,跳过
if (i > 0 && nums[i] == nums[i-1]) continue;

int L = i + 1;
int R = n - 1;

while (L < R) {
int sum = nums[i] + nums[L] + nums[R];

if (sum == 0) {
ans.push_back({nums[i], nums[L], nums[R]});

// 去重:左指针右移直到指向不一样的数
while (L < R && nums[L] == nums[L+1]) L++;
// 去重:右指针左移直到指向不一样的数
while (L < R && nums[R] == nums[R-1]) R--;

L++;
R--;
}
else if (sum < 0) {
L++; // 和小了,左指针右移变大
}
else {
R--; // 和大了,右指针左移变小
}
}
}
return ans;
}
};

2、类似两数之和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
unordered_map<int, int> hash_map;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); i++){
// 很重要,保证同一个数字的索引在最后
hash_map[nums[i]] = i;
}
for(int i = 0; i < nums.size(); i++){
if ((i > 0) && (nums[i] == nums[i-1])){
continue;
}
for(int j = i + 1; j < nums.size(); j++){
if ((j > i + 1) && (nums[j] == nums[j-1])){
continue;
}
if(hash_map.find(0 - nums[i] - nums[j]) != hash_map.end()){
int k = hash_map[0 - nums[i] - nums[j]];
if((i < j) && (j < k)){
res.push_back({nums[i], nums[j], 0 - nums[i] - nums[j]});
}
}
}
}
return res;
}
};

📊 复杂度分析

1、双指针

  • 时间复杂度:双重循环 O(N2)O(N^2) 的时间
  • 空间复杂度:只需要 O(1)O(1) 的空间

2、类似两数之和

  • 时间复杂度:双重循环 O(N2)O(N^2) 的时间
  • 空间复杂度:使用了额外的 O(N)O(N) 空间

🎯 总结

  • 核心思想:关键是想到“排序+相邻两次枚举的元素不能相同”来避免重复。