15. 三数之和(C++)
题目
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
分析与题解
核心思路是采取双指针遍历法来求解。首先因为所求容器为数值的vector集合,对于下标没有硬性要求,因此先对数组进行升序排列。
sort(nums.begin(), nums.end());
对于求取a+b+c=0
,我们优先固定a初始位置下标为i
,简化为双指针问题,再设置左指针为i+1
,右指针位置为nums.size()-1
。此时左右指针指向元素分别为考虑范围内的最大和最小值。
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
}
else if (sum < 0) {
left++;
}
else {
res.push_back(vector<int>{nums[i], nums[left], nums[right]});
//确认结果后,进行内层的去重
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
//去重后此时再收缩,两边界都不再是重复元素
left++;
right--;
}
}
三指针移动策略为:
- 当
a+b+c>0
,需要将右指针向左滑动,减少和大小 - 当
a+b+c<0
,需要将左指针向右滑动,增大和大小 - 当
a+b+c=0
,可以将当前一组数值push_back进结果列表中
另外根据我们对于题目的理解,可以额外对一些情况进行排除。由于数组在遍历前已经进行升序排列,当最小值a>0
,三数和不可能为0,可以直接排除这种情况。
for (int i = 0; i < size - 2; i++) {
// 升序数组的最小元素如果>0
// 三数之和不可能为0
if (nums[i] > 0) break;
//...
}
去重
尽管数组进行重新排序,但还有可能出现重复元素,而如果逐位进行结果的筛选,最终的vector列表中可能就会出现重复的组合,因此需要对结果进行筛选。
①内层循环去重(左右指针)
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
}
else if (sum < 0) {
left++;
}
else {
res.push_back(vector<int>{nums[i], nums[left], nums[right]});
//确认结果后,进行内层的去重
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
//去重后此时再收缩,两边界都不再是重复元素
left++;
right--;
}
}
去重的位置设置在找到满足a+b+c=0
的组合后,因为排序数组的重复元素肯定时相邻的,如果不加去重,相同数值的结果便会添加进列表。
②外层循环(首指针)
外层指针一般在找到满足条件的组合时相对来说位置都是固定的。只要保证每次提供给内层循环的数值不同即可实现外层去重。需要注意的是,以下这种去重方法是不严谨的
for (int i = 0; i < size - 2; i++) {
if (nums[i] == nums[i + 1])
continue;
}
因为这样会导致在进入内层循环前,首指针已经将选定数值的重复元素全部筛除掉。这样会漏掉首指针跟左指针数值相同的vector组合,例如[-1,-1,2]
。
因此正确的方法是遇到重复元素时,直接进行内层循环,退出后再将外层循环的重复元素进行排除。
if (i > 0 && nums[i] == nums[i - 1])
continue;
最后完整代码如下:
class Solution {
public:
vector<vector<int>> res;
vector<vector<int>> threeSum(vector<int>& nums) {
// 首先对数组进行排序
sort(nums.begin(), nums.end());
int size = nums.size();
//至少留有left、right两个位置,最大边界进行收缩
for (int i = 0; i < size - 2; i++) {
// 升序数组的最小元素如果>0
// 三数之和不可能为0
if (nums[i] > 0) break;
// 错误去重方式
// 会错过 [-1, -1, 2]的情况
// if (nums[i] == nums[i + 1])
// continue;
//对最外层进行去重
if (i > 0 && nums[i] == nums[i - 1])
continue;
int left = i + 1, right = size - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
}
else if (sum < 0) {
left++;
}
else {
res.push_back(vector<int>{nums[i], nums[left], nums[right]});
//确认结果后,进行内层的去重
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
//去重后此时再收缩,两边界都不再是重复元素
left++;
right--;
}
}
}
return res;
}
};