每天一道LeetCode-----给定序列中2/3/4个元素的和为target的所有集合,或3个元素的和最接近target的集合
2017-10-13 17:17
701 查看
原题链接
意思是给定一个数组,求数组中哪两个元素的和是给定值。
蛮力法的求解就是两层for循环,时间复杂度是O(n2)。
显然这种方式对于算法题来说复杂度过高了,仔细想一下,每次固定一个i,变化j的时候,小于i的那部分其实在之前已经访问过一次了,为什么呢
假设nums.大小为10,此时i为5,j从0到9计算nums[i] + nums[j]
当i = 0, 1, 2, 3, 4时是不是都计算过?,所以又重复计算了一遍,整个程序多计算了n遍,这便是复杂度的原因。
解决方法,首先想到的优化就是让j从i+1开始
效率得到了一定优化,在考虑是否可以继续优化呢,想一下,在遍历j时
所以发现对于i后面的那些仍然会重复遍历n次,还有什么方法可以优化呢,其实到这,再优化的方法只能想办法让复杂度变为O(n),也就是让每一个元素只遍历一遍,那么就不能套循环,只能使用一层循环。
当遍历某个nums[i]时,唯一可能知道的、程序可能会优化的就是从nums[0]到nums[i-1],因为nums[i]往后的元素还没有遍历过,根本不知道是什么。再想,可不可以不判断nums[i] + nums[j]的和而是直接判断i前面有没有nums[j]这个数呢?nums[j]是多少?(假设j是0到i-1中的某个数)
我们只需要判断前i-1个数中有没有left就行了,那么就需要使用某种数据结构存储访问过的nums[i],什么数据结构可以达到o(1)的效果呢?哈希表
要求和2Sum差不多,区别在于是三个数的和,target为0,同时会有多个解,而且最要命的是竟然可以有重复的元素。
吸收了2Sum的教训,聪明的boy可能想这里我也要用unordered_map,于是乎写出如下代码
于是乎兴奋的submit,却发现,额….
效率低的吓人,为什么呢,因为即使这样,仍然有着O(n2)的复杂度,唔…又开始进入优化的坑
对于现实主义者的我们来说O(n)是不可能了,和O(nlogn)有关的二分法好像也不太适用。首先判断肯定是要固定一个之后再遍历一遍,因为仍然有两个数是不确定的。
这里引入一种方法,模仿二分发left和right的移动。因为序列是有序的,那么仅仅需要判断nums[i + 1]和nums[nums.size() - 1]的和,从而得知是大(向左移),小(向右移动)
利用这种方法的效率比上面高一些,可能原因就在于是从两边同时向中间移动,但是仍然摆脱不了O(n3)的复杂度(我一直以为上面的方法可以达到O(logn)….错了好久),代码如下
此时可能回想,2Sum我能不能也使用这种方法提高效率呢,想法是好的,可是要求是有序数组,而基于比较的最快的排序快排也只能是O(nlogn),显然得不偿失
和3Sum完全一样,只是4个数的和,代码也类似,不再强调了。
唔…leetcode上的解法也都是O(n3),既然都这样就不想优化了
意思是给定一个数组,求数组中哪三个数的和最接近target,返回三个数的和。
这道题和3Sum是一样的,利用上面的思想,固定一个,剩下两个从两边开始找即可,当然需要排好序,代码如下
注:多数代码都在这里直接手打的,难免有错误,轻喷
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-----找到给定序列中所有和为某个值的集合或集合个数,序列中可以有/无重复项,集合元素顺序不同算不同集合等
- 每天一道LeetCode-----计算给定序列中所有长度为k的滑动窗的最大值集合
- 每天一道LeetCode-----找出给定序列的所有子序列
- 每天一道LeetCode-----给定一个矩阵,如果某个元素是0,就将所在行所在列上所有元素否置0
- 每天一道LeetCode-----二叉树逐层遍历,每一层存在一个序列中,返回所有序列集合
- 每天一道LeetCode-----删除序列中指定元素,将满足要求的元素移动到前面
- 每天一道LeetCode-----在给定序列中找到满足nums[i]>nums[i-1]&&nums[i]>nums[i+1]的位置,要求时间复杂度是O(logN)
- 每天一道LeetCode-----计算最长的元素连续序列长度
- 每天一道LeetCode-----计算给定范围内所有数的与运算结果
- 每天一道LeetCode-----找到二叉树所有和为给定值的路径
- 每天一道LeetCode-----寻找地增序列中第一个大于等于目标元素的位置
- 每天一道LeetCode-----数组序列,每个元素的值表示最多可以向后跳多远,计算最少跳多少次可以到达末尾
- 每天一道LeetCode-----找到1,2,...,n这n个数所有的组合,每个组合有k个元素,且元素大小递增
- 每天一道LeetCode-----将字符串切分成若干单词,使得每个单词都在给定的字典中,求出所有的切分结果
- 每天一道LeetCode-----在给定数组中找到一个子数组,使得这个子数组的元素乘积最大
- 每天一道LeetCode-----摩尔投票法寻找给定数组中出现个数大于n/2或n/3的元素
- 每天一道LeetCode-----一个整数序列,每个元素出现两次,只有一个(两个)出现一次,找到这个(这两个)元素
- 每天一道LeetCode-----给定大小为n+1的数组,元素大小在[1 : n]之间,只有一个元素会重复出现多次,找到重复的那个
- 每天一道LeetCode-----将字符串切分,使每个子串都是回文串,计算所有可能结果和最小切分次数
- 每天一道LeetCode-----找到给定数组的连续子数组,使这个子数组的和最大,要求复杂度为O(n)