您的位置:首页 > 其它

每天一道LeetCode-----给定序列中2/3/4个元素的和为target的所有集合,或3个元素的和最接近target的集合

2017-10-13 17:17 701 查看
原题链接

2Sum

Two Sum



意思是给定一个数组,求数组中哪两个元素的和是给定值。

蛮力法的求解就是两层for循环,时间复杂度是O(n2)。

class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for(int i = 0; i < nums.size() - 1; ++i)
{
for(int j = 0; j < nums.size(); ++j)
{
if(i == j) continue;
if(nums[i] + nums[j] == target)
return {nums[i], nums[j]};
}
}
}
};


显然这种方式对于算法题来说复杂度过高了,仔细想一下,每次固定一个i,变化j的时候,小于i的那部分其实在之前已经访问过一次了,为什么呢

假设nums.大小为10,此时i为5,j从0到9计算nums[i] + nums[j]

nums[0] + nums[5],
nums[1] + nums[5],
...
nums[4] + nums[5],


当i = 0, 1, 2, 3, 4时是不是都计算过?,所以又重复计算了一遍,整个程序多计算了n遍,这便是复杂度的原因。

解决方法,首先想到的优化就是让j从i+1开始

class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for(int i = 0; i < nums.size() - 1; ++i)
{
for(int j = i+1; j < nums.size(); ++j)
{
if(nums[i] + nums[j] == target)
return {nums[i], nums[j]};
}
}
}
};


效率得到了一定优化,在考虑是否可以继续优化呢,想一下,在遍历j时

/* i == 5时遍历了 */
nums[6], nums[7], nums[8], nums[9];

/* i == 6时遍历了 */
nums[7], nums[8] ...


所以发现对于i后面的那些仍然会重复遍历n次,还有什么方法可以优化呢,其实到这,再优化的方法只能想办法让复杂度变为O(n),也就是让每一个元素只遍历一遍,那么就不能套循环,只能使用一层循环。

for(int i = 0; i < nums.size(); ++i)
{

}


当遍历某个nums[i]时,唯一可能知道的、程序可能会优化的就是从nums[0]到nums[i-1],因为nums[i]往后的元素还没有遍历过,根本不知道是什么。再想,可不可以不判断nums[i] + nums[j]的和而是直接判断i前面有没有nums[j]这个数呢?nums[j]是多少?(假设j是0到i-1中的某个数)

int left = target - nums[i];


我们只需要判断前i-1个数中有没有left就行了,那么就需要使用某种数据结构存储访问过的nums[i],什么数据结构可以达到o(1)的效果呢?哈希表

/* 通常使用unordered_map来代表哈希表 */
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hash;
for(int i = 0; i < nums.size(); ++i)
{
int left = target - nums[i];
if(hash.find(left) != hash.end())
return {hash[left], i};
else
hash[nums[i]] = i;
}

return {0, 0};
}
};


3Sum

扩展题型为Three Sum,原题链接3Sum



要求和2Sum差不多,区别在于是三个数的和,target为0,同时会有多个解,而且最要命的是竟然可以有重复的元素。

吸收了2Sum的教训,聪明的boy可能想这里我也要用unordered_map,于是乎写出如下代码

class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {

/* 为了处理重复元素,首先排序nums */
std::sort(nums.begin(), nums.end());
vector<vector<int>> ans;

unordered_map<int, int> hash;
for(int i = 0; i < nums.size() - 2; ++i)
{
int target = -nums[i];
hash.clear();
for(int j = i + 1; j < nums.size(); ++j)
{
int ntarget = target - nums[j];
if(hash.find(ntarget) != hash.end())
{
ans.push_back({nums[i], nums[j], ntarget});

/*
* 如果后面几个元素和当前元素重复,直接跳过,为什么可以直接跳过呢
* 如果nums[j] == nums[j + 1],那么当j++后仍然求出的是当前结果,因为
* ntarget只可以是在nums[j]前面的数
*/
while(j < nums.size() && nums[j + 1] == nums[j])
++j;
}
else
hash[nums[j]] = j;
}
/* 同理 */
while(i < nums.size() - 2 && nums[i] == nums[i + 1])
++i;
}

return ans;
}
};


于是乎兴奋的submit,却发现,额….



效率低的吓人,为什么呢,因为即使这样,仍然有着O(n2)的复杂度,唔…又开始进入优化的坑

对于现实主义者的我们来说O(n)是不可能了,和O(nlogn)有关的二分法好像也不太适用。首先判断肯定是要固定一个之后再遍历一遍,因为仍然有两个数是不确定的。

这里引入一种方法,模仿二分发left和right的移动。因为序列是有序的,那么仅仅需要判断nums[i + 1]和nums[nums.size() - 1]的和,从而得知是大(向左移),小(向右移动)

int left = 0;
int right = nums.size() - 1;
while(left < right)
{
if(nums[left] + nums[right] > target)
--right;
else if(nums[left] + nums[right] < target)
++left;
else
{
/* 结果中的一员 push到结果中*/

/* 防止重复 */
while(left < right && nums[left] == nums[left + 1])
++left;
while(left < right && nums[right] == nums[right - 1])
--right;

/*
* 为什么这里需要++和--
* 此时left是最后一个和之前的nums[left]重复的下标,需要++到第一个不重复的下标
* 因为nums[left]已经改变,nums[left] + nums[right]不可能再等于target,所以right无需保持在最后一个和之前nums[right]重复的位置,也向前移动--
* /
++left;
--right;
}
}


利用这种方法的效率比上面高一些,可能原因就在于是从两边同时向中间移动,但是仍然摆脱不了O(n3)的复杂度(我一直以为上面的方法可以达到O(logn)….错了好久),代码如下

class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
std::sort(nums.begin(), nums.end());
vector<vector<int>> ans;

for(int i = 0; i < nums.size(); ++i)
{

int target = -nums[i];
int left = i + 1;
int right = nums.size() - 1;

while(left < right)
{
if(nums[left] + nums[right] > target)
--right;
else if(nums[left] + nums[right] < target)
++left;
else
{
ans.push_back({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;
}
}
while(i < nums.size() - 2 && nums[i] == nums[i + 1])
++i;
}

return ans;
}
};


此时可能回想,2Sum我能不能也使用这种方法提高效率呢,想法是好的,可是要求是有序数组,而基于比较的最快的排序快排也只能是O(nlogn),显然得不偿失

4Sum

最后一个扩展为4Sum,原题链接4Sum



和3Sum完全一样,只是4个数的和,代码也类似,不再强调了。

唔…leetcode上的解法也都是O(n3),既然都这样就不想优化了

3Sum Closest

原题链接3Sum Closest



意思是给定一个数组,求数组中哪三个数的和最接近target,返回三个数的和。

这道题和3Sum是一样的,利用上面的思想,固定一个,剩下两个从两边开始找即可,当然需要排好序,代码如下

class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
std::sort(nums.begin(), nums.end());
int min_dis = INT_MAX;
int three_sum = 0;
for(int i = 0; i < nums.size(); ++i)
{
int left = i + 1;
int right = nums.size() - 1;
while(left < right)
{
/* 多出一部分用于比较和target的距离,记录和 */
int sum = nums[i] + nums[left] + nums[right];
if(abs(sum - target) < min_dis)
{
three_sum = sum;
min_dis = abs(sum - target);
}
if(sum > target)
--right;
else if(sum < target)
++left;
else
return sum;
}
}

return three_sum;
}
};


注:多数代码都在这里直接手打的,难免有错误,轻喷
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  leetcode
相关文章推荐