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

原题链接

2Sum

Two Sum

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;
    }
};

注:多数代码都在这里直接手打的,难免有错误,轻喷

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值