一. 3Sum
Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.
Note: The solution set must not contain duplicate triplets.
For example, given array S = [-1, 0, 1, 2, -1, -4],
A solution set is:
[
[-1, 0, 1],
[-1, -1, 2]
]
Difficulty:Medium
TIME:TIMEOUT
解法
这道题其实和之前做过的Two Sum很类似,都是在数组中找几个数,这几个数的和为目标数值。不过这道从两个数变为了三个数。而且还要输出所有的满足条件的集合。
其实这道题的解法很容易能够想到,先遍历一遍数组将所有的数与目标数的差值存入集合中,之后遍历两遍数组,如果两个数的和在集合中,则这三个数就满足要求。因此复杂度大约为 O(n2) 。
想法是很简单,但实现起来却很麻烦,最重要的问题就是去重的问题,由于实在没有想到理想的去重方法,我就直接采用了集合来去重(之前有Increasing Subsequences将数组转成字符串存入map中来去重,但那那方法想想就有点奇怪,还是不要用了)。
但是代码写好之后,提交各种超时,都是卡在最后的3000样例数据上,3000的平方为900万,按理说是不会超时的,但是由于代码的时间复杂度虽然为 O(n2) ,但是常系数是很高的,特别是map的find方法(或者是下标引用,二维向量的下标引用竟然有8以上的常系数)。
而且一直没怎么想到处理很多元素相同的比较好的办法,一开始我是用二维向量,来保存处理过序列,但是二维向量的查找复杂度太高了,一直超时,就改成了预处理去重的方法。因为相同的元素超过了两次就没有意义,而且0超过了三次也没有意义(之前一直卡在全0的数据)。预处理之后,终于能够通过这道题了。
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> v;
if (nums.size() < 3)
return v;
set<vector<int>> s; //这个集合用来去除重复的数组序列
unordered_map<int, int> loc; //这个map用来存目标数以及需要该目标数的下标
unordered_map<int, int> dup; //这个是用来去除数组中重复的元素
int need = 0;
vector<int> tmp;
for (int i = 0; i < nums.size(); i++) {
if (dup[nums[i]] < 2 || (nums[i] == 0 && dup[nums[i]] < 3)) { //预处理去除重复元素
dup[nums[i]]++;
loc[-nums[i]] = tmp.size();
tmp.push_back(nums[i]);
}
}
for (int i = 0; i < tmp.size(); i++) {
for (int j = i + 1; j < tmp.size(); j++) {
need = tmp[i] + tmp[j];
if (loc.find(need) != loc.end()) {
if (loc[need] == i || loc[need] == j) //避免这两个数是需要目标数的数
continue;
vector<int> t{ tmp[i],tmp[j],-need };
sort(t.begin(), t.end());
s.insert(t); //插入的时候去除重复的序列
}
}
}
vector<vector<int>> result(s.begin(), s.end());
return result;
}
代码的时间复杂度为 O(n2) ,实际运行时间约为500ms。
优化
既然map会有那么高的常系数,而且处理重复序列也需要耗费那么多操作,那么有没有一种算法可以不依靠map和去重就能解决这道题呢,比如类似于Increasing Subsequences的第二种解法,利用问题的特性在解决问题的过程中就排除了重复的情况。
在处理这个问题我最大的失误就是没有考虑排序的情况(其实如果复杂度一定不会优于 O(nlogn) ,就可以优先考虑排序会不会让处理的问题更加简单)。
这道题在排序过后就能提供给我们很多额外的信息,而且还可以采取不同的解法。排序之后左边总是比右边的数小,这样如果一个数在数组左端一个数在数组右端,两个的数相加之后,如果我们想让这个数变小一点,就可以让右端的下标向左移,如果我们想让这个数变大一点,就可以让左端的下标向右移,注意,这种方式不会漏掉任意一个我们需要的和。
因此,优化后的算法就是基于这样的思想,能够去除所有的重复情况并且不依赖于map来求解。
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> v;
if (nums.size() < 3)
return v;
sort(nums.begin(), nums.end());
int left, right;
for (int i = 0; i < nums.size() - 1; i++) {
left = i + 1;
right = nums.size() - 1;
while (left < right) {
if (nums[left] + nums[right] + nums[i] < 0) //需要的数比目标数小,移动左边的下标
left++;
else if (nums[left] + nums[right] + nums[i] > 0) //需要的数比目标数大,移动右边的下标
right--;
else if (nums[left] + nums[right] + nums[i] == 0) { //刚好和目标数相等,则记录结果
vector<int> tmp{ nums[i],nums[left],nums[right] };
v.push_back(tmp);
//排除掉所有的重复数字(固定了三个数中两个,因此同样的数字已经没用了)
while (left + 1 < right && nums[left + 1] == nums[left])
left++;
while (left + 1 < right && nums[right - 1] == nums[right])
right--;
left++;
right--;
}
}
//继续排除掉重复的数字,最先出现的相同数字具有的情况比晚出现的多(和求递增子序列的思想一样)
while (i < nums.size() && nums[i + 1] == nums[i])
i++;
}
return v;
}
代码的时间复杂度为 O(n2) ,实际运行时间约为100ms。