一.题目描述
二.解题技巧
这道题和另外一道题Two Sum很类似,不过这道题是在数组中寻找三个数,使得其和为0,同时要求这三个数只能出现一次。如果单纯得使用暴力算法来做的话,时间复杂度为O(n^3),且很难判断这一组数是否已经出现过。
若是选择先排序,然后左右夹逼,复杂度:O(n2)。
这个方法可以推广到k-sum,先排序,然后做k-2 次循环,在最内层循环左右夹逼,时间复杂度是:O(max(n*logn, n^(k-1)))。
如果考虑将数组A(元素个数为n)进行升序排序,那么按照顺序将数组中的第i个数作为三个数中最小的数,寻找从A的第i+1个数到n-1个数中满足和为-A[i]的数,就可以找到满足三个数和为0的组合了,但是,单独考虑这种情况,会出现重复的问题。要保证不出现重复的情况,当i!=0时,如果第i个数与第i-1个数相同的话,则不进行处理,直接处理第i+1个元素。这样,只要保证三个数中最小的数是按照顺序递增的,那么算法找到的解就都是不重复的。
这道题的边界条件在于:由于我们选择的是三个数中的最小值,因此,对于这个数的循环是[0, n-2),同时,要对最小值进行消除重复的元素时,需要从第1个元素开始判断,如果从第0个元素开始判断的时候,可能会将0,0,0这种情况忽略掉,因此,在消除重复的最小元素时,必须从第1个元素才开始。
这道题在寻找数组A中的两个数的和为-A[i]时,可以考虑利用数组A是已经排序的条件,进行左右夹逼操作。在进行左右搜索的时候,需要将左右两边收缩到与当前元素不同的元素为止,这样做有两个原因:1.可以减少计算量;2.当当前的两个元素的和刚好等于-A[i]的时候,如果没有进行上面的缩放操作,那么就可能将重复的三个数保存下来,导致结果出错。
三.实现代码
class Solution
{
public:
vector<vector<int> > threeSum(vector<int> &num)
{
int Size = num.size();
vector<vector<int> > Result;
if (Size < 3)
{
return Result;
}
sort(num.begin(), num.end());
for (int Index_outter = 0; Index_outter < (Size - 2); Index_outter++)
{
int First = num[Index_outter];
int Second = num[Index_outter + 1];
const int Target = 0;
if ((Index_outter != 0) && (First == num[Index_outter - 1]))
{
continue;
}
int Start = Index_outter + 1;
int End = Size - 1;
while (Start < End)
{
Second = num[Start];
int Third = num[End];
int Sum = First + Second + Third;
if (Sum == Target)
{
vector<int> Tmp;
Tmp.push_back(First);
Tmp.push_back(Second);
Tmp.push_back(Third);
Result.push_back(Tmp);
Start++;
End--;
while (num[Start] == num[Start - 1])
{
Start++;
}
while (num[End] == num[End + 1])
{
End--;
}
}
if (Sum < Target)
{
Start++;
while (num[Start] == num[Start - 1])
{
Start++;
}
}
if (Sum > Target)
{
End--;
if (num[End] == num[End + 1])
{
End--;
}
}
}
}
return Result;
}
};
以下是网上的一种解决方法:
// LeetCode, 3Sum
// 先排序,然后左右夹逼,注意跳过重复的数,时间复杂度O(n^2),空间复杂度O(1)
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& num) {
vector<vector<int>> result;
if (num.size() < 3) return result;
sort(num.begin(), num.end());
const int target = 0;
auto last = num.end();
for (auto i = num.begin(); i < last - 2; ++i) {
auto j = i + 1;
if (i > num.begin() && *i == *(i - 1)) continue;
auto k = last - 1;
while (j < k) {
if (*i + *j + *k < target) {
++j;
while (*j == *(j - 1) && j < k) ++j;
}
else if (*i + *j + *k > target) {
--k;
while (*k == *(k + 1) && j < k) --k;
}
else {
result.push_back({ *i, *j, *k });
++j;
--k;
while (*j == *(j - 1) && *k == *(k + 1) && j < k) ++j;
}
}
}
return result;
}
};
四.总结
我自己的想法时,将数组进行排序,然后按照顺序选择数组A[1, n-1)中的元素作为三个数中的中位数来在剩下的已经排好序的数组中寻找满足条件的其他两个数。但是,选择中位数的这种情况需要考虑的边界条件比较复杂,并没有选择最小值来得方便一些,同时,选择中位数之后,将已经排序的数组分成了两段,这样在进行收缩的时候,需要分别判断两个两边与i的关系,也比较容易出错。
这道题通过对数组进行排序,从而按照顺序选择不同的最小值来避免出现重复的情况,这确实是一个比较好的编程技巧。