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

来源:互联网 发布:手机淘宝补差价 编辑:程序博客网 时间:2024/05/22 12:10

原题链接

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

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

阅读全文
0 0
原创粉丝点击