每天一道LeetCode-----给定序列中2/3/4个元素的和为target的所有集合,或3个元素的和最接近target的集合
来源:互联网 发布:手机淘宝补差价 编辑:程序博客网 时间:2024/05/22 12:10
原题链接
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-----给定序列中2/3/4个元素的和为target的所有集合,或3个元素的和最接近target的集合
- 每天一道LeetCode-----找到给定序列中所有和为某个值的集合或集合个数,序列中可以有/无重复项,集合元素顺序不同算不同集合等
- 每天一道LeetCode-----计算给定序列中所有长度为k的滑动窗的最大值集合
- 给定N个整数集合是否存在两个其和刚好为指定常数的元素
- 给定一个整数数组,返回数组中两个元素的和为target的索引
- 每天一道LeetCode-----寻找地增序列中第一个大于等于目标元素的位置
- [LeetCode]—3Sum Closest 求数组中三个数之和最接近给定target的组合
- 每天一道LeetCode-----找出给定序列的所有子序列
- 在给定的数组中找出两个元素和为给定值的所有元素对
- 在给定的数组中找出两个元素和为给定值的所有元素对
- 给定正整数n和m,计算出n个元素的集合{1,2,...,n}可以划分为多少个不同的由m个元素组成的子集合
- 求m个元素集合中n个元素的所有子集(C/OC)
- LeetCode 16 3Sum Closest 找出最接近指定target的三个数的和
- 3Sum Closest 找3个数使得和最接近的target @LeetCode
- 描述一个运行时间为Θ(nlgn)的算法,给定n个整数的集合S和另一个整数x,该算法能确定S中是否存在两个其和刚好为x的元素
- 2.3-7 描述一个运行时间为Θ(nlgn)的算法,给定n个整数的集合S和另一个整数x,该算法能确定S中是否存在两个其和刚好为x的元素
- [LeetCode]-Combination Sum I&II 求相加和为target的集合
- java找出2个集合相同和不同的元素(以及去除List中的重复元素)
- 史上最简单的SpringCloud教程 | 第十篇: 高可用的服务注册中心
- 在mac上安装下pySpark,并且在pyCharm中python调用pyspark
- centos Linux安装eclipse
- JAVA设计模式 Build 模式
- UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习
- 每天一道LeetCode-----给定序列中2/3/4个元素的和为target的所有集合,或3个元素的和最接近target的集合
- 基于 webmagic 的知乎爬取[GitHub]
- *TEST 5 for NOIP 。。。
- WIFI基本知识整理
- php 编译
- 网页特效
- Python 多线程的三种创建方式
- DSPF28335学习笔记
- 在IDEA中使用 Spring Initializr 新建 spring boots 项目