四数相加II
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。
例如:
输入:
-
A = [ 1, 2]
-
B = [-2,-1]
-
C = [-1, 2]
-
D = [ 0, 2]
输出:
2
解释:
两个元组如下:
-
(0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
-
(1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0
-
力扣454题
思路
本题是使用哈希法的经典题目,而三数之和 ,四数之和并不合适使用哈希法,因为三数之和和四数之和这两道题目使用哈希法在不超时的情况下做到对结果去重是很困难的,很有多细节需要处理。
而这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和,题目15.三数之和,还是简单了不少!
本题解题步骤:
-
首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
-
遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
-
定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
-
在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
-
最后返回统计值 count 就可以了
class Solution { public: int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) { int count = 0; //统计满足题意条件的次数 unordered_map<int, int> map; //first=key用来存放nums1+nums2对应的值,second=value用来存放nums1+nums2重复的次数 int sum = 0;//记录nums1+nums2的和 for (int i = 0; i < nums1.size(); i++) { for (int j = 0; j < nums2.size(); j++) { sum=nums1[i]+nums2[j]; map[sum]++; //对value进行累加,如果sum重复则加1,重复原因是根据下标对应的值进行相加,可能重复 } } sum=0; //归0 记录nums3+nums4的和 for (int i = 0; i < nums3.size(); i++) { for (int j = 0; j < nums4.size(); j++) { sum=nums3[i]+nums4[j]; if(map.find(0-sum)!=map.end()){ //因为num1+num2=-(num3+num4) 所以可以逆推思想判断-(num3+num4)在map集合用有没有出现,有的话说明存在使得4者之和为0的值 count=count+map[0-sum]; //将num1+num2的结果(key)所出现重复的次数(value)累加到count中 } } } return count; //最后进行返回 } };
简便写法:
class Solution { public: int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) { int count = 0; //统计满足题意条件的次数 unordered_map<int, int> map; //first=key用来存放nums1+nums2对应的值,second=value用来存放nums1+nums2出现重复的次数 for (int a : nums1) { //增强for for (int b : nums2) { map[a+b]++; //对value进行累加,如果sum重复则加1,重复原因是根据下标对应的值进行相加,可能重复 } } for (int c : nums3) { for (int d : nums4) { if(map.find(0-(c+d))!=map.end()){ //因为num1+num2=-(num3+num4) 所以可以逆推思想判断-(num3+num4)在map集合用有没有出现,有的话说明存在使得4者之和为0的值 count=count+map[0-(c+d)]; //将num1+num2的结果(key)所出现重复的次数(value)累加到count中 } } } return count; //最后进行返回 } };
-
时间复杂度: O(n^2)
-
空间复杂度: O(n^2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n^2
赎金信
-
题目:
-
给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
注意:
你可以假设两个字符串均只含有小写字母。
canConstruct("a", "b") -> false canConstruct("aa", "ab") -> false canConstruct("aa", "aab") -> true
-
力扣383题
思路:
这道题目和242.有效的字母异位词很像,242.有效的字母异位词相当于求 字符串a 和 字符串b 是否可以相互组成 ,而这道题目是求 字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。
本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点。
-
第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思” 这里说明杂志里面的字母不可重复使用。
-
第二点 “你可以假设两个字符串均只含有小写字母。” 说明只有小写字母,这一点很重要
暴力解法
-
那么第一个思路其实就是暴力枚举了,两层for循环,不断去寻找
class Solution { public: bool canConstruct(string ransomNote, string magazine) { for(int i=0;i<magazine.length();i++){ // 在ransomNote中找到和magazine相同的字符 for(int j=0;j<ransomNote.length();j++){ if(ransomNote[j]==magazine[i]){ ransomNote.erase(ransomNote.begin()+j);//删除ransomNote上的当前满足条件(在magazine中有该元素)的元素,因为ransomNote可能有重复的 break; //删完直接跳出内循环,避免magazine中一个元素与ransomNote的多个相同元素对应,而误删,导致长度不够 eg: ransomNote:"aa" magazine:"aaa" } } } if(ransomNote.length()==0){ return true; } return false; } };
-
时间复杂度: O(n^2)
-
空间复杂度: O(1)
这里时间复杂度是比较高的,而且里面还有一个字符串删除也就是erase的操作,也是费时的,当然这段代码也可以过这道题。
哈希解法
-
因为题目说只有小写字母,那可以采用空间换取时间的哈希策略,用一个长度为26的数组来记录magazine里字母出现的次数。
-
然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
-
依然是数组在哈希法中的应用。
-
在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!
class Solution { public: bool canConstruct(string ransomNote, string magazine) { //判断 ransomNote 能不能由 magazine 里面的字符构成 int hash[26] ={0}; if (ransomNote.size() > magazine.size()) { //如果ransomNote的长度大于magazine,直接返回false return false; } for(int i=0;i<magazine.length();i++){ //注意:一定是先将magzine进行添加,然后对ransomNote进行减减操作,如果反过来且if是判断大于0,会导致hash数组提前结束循环返回false //因为ransomNote一定是小于等于magazine eg:magazine:{a,a,b} , hash数组里存ransomNote:{a,a},一开始hash={2,0...} // 但是在magazine进行第一次循环的时候将hash[0]的元素减到1而进入if直接返回false,而第二个a无法进行hash[0]--; // 通过hash数据记录 magazine里各个字符出现次数, hash[magazine[i]-'a']++; } for(int i=0;i<ransomNote.length();i++){ // 遍历ransomNote,在hash里对应的字符个数做--操作 hash[ransomNote[i]-'a']--; // 如果小于零说明ransomNote里出现的字符,magazine没有 if(hash[ransomNote[i]-'a']<0){ return false; } } return true; } };
-
时间复杂度: O(n)
-
空间复杂度: O(1)
三数之和
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: -1 -1 -1 0 0 1 2 nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。
-
力扣15题
双指针法
其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。
而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。
-
双指针法,这道题目使用双指针法 要比哈希法高效一些
拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
时间复杂度:O(n^2)。
class Solution { public: vector<vector<int>> threeSum(vector<int>& nums) { vector<vector<int>> result;//记录结果集 sort(nums.begin(), nums.end()); //对nums进行排序(小->大),方便后面双指针的移动 // 找出a + b + c = 0 // a = nums[i], b = nums[left], c = nums[right] for(int i=0;i<nums.size();i++){ // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了 if(nums[i]>0) return result; // 错误去重a方法,将会漏掉-1,-1,2 这种情况 nums=[-1,-1,2,...] /* if (nums[i] == nums[i + 1]) { 因为i的下标本来是0,但是与i+1=1的元素相等,然后continue 导致i变成1,跳过了第一个1,所以漏掉了-1+(-1)+2=0 continue; } */ // 正确去重a方法 if(i>0&&nums[i]==nums[i-1]){ //因为nums是排好顺序的,所以重复元素出现在nums[i]的两边 continue; 三元组元素nums[i]去重 } int left=i+1; //定义指针指向i+1 int right=nums.size()-1;//定义指针指向最后一个位置 //left与right从两边向中间移动寻找合适的值 while(right>left){ //如果left=right,不满足三个数的条件,所以不加等号 // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组 /* while (right > left && nums[right] == nums[right - 1]) right--; while (right > left && nums[left] == nums[left + 1]) left++; */ if(nums[i]+nums[left]+nums[right]>0){ //如果三数之和大于0,将right-- 减小三数之和 right--; }else if(nums[i]+nums[left]+nums[right]<0){//如果三数之和小于0,将left++ 增加三数之和 left++; }else{ //相等的话直接存入result result.push_back(vector<int>{nums[i],nums[left],nums[right]}); // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重 //eg: -1 -1 -1 -1 0 1 1 1 1 已经收货了结果集-1 0 1 所以left后面和right前面有重复的,可直接移动指针 while(right>left&&nums[right]==nums[right-1]){ //找到之后若发现还有重复的,可直接right-- right--; } while(right>left&&nums[left]==nums[left+1]){ //与nums[i]去重不同,这个也是找到之后发现left有重复的,直接left++ left++; } // 找到答案时,双指针同时收缩 right--; left++; } } } return result; } };
-
时间复杂度: O(n^2)
-
空间复杂度: O(1)
a的去重
说到去重,其实主要考虑三个数的去重。 a, b ,c, 对应的就是 nums[i],nums[left],nums[right]
a 如果重复了怎么办,a是nums里遍历的元素,那么应该直接跳过去。
但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。
这俩虽然都是和 nums[i]进行比较,是比较它的前一个,还是比较它的后一个。
如果我们的写法是 这样:
if (nums[i] == nums[i + 1]) { // 去重操作 continue; }
那我们就把 三元组中出现重复元素的情况直接pass掉了。 例如{-1, -1 ,2} 这组数据,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。
我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
所以这里是有两个重复的维度。
那么应该这么写:
if (i > 0 && nums[i] == nums[i - 1]) { continue; }
这么写就是当前使用 nums[i],我们判断前一位是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。
这是一个非常细节的思考过程。
b与c的去重
-
与a又不同
-
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重 //eg: -1 -1 -1 -1 0 1 1 1 1 已经收货了结果集-1 0 1 所以left后面和right前面有重复的,可直接移动指针 while(right>left&&nums[right]==nums[right-1]){ //找到之后若发现还有重复的,可直接right-- right--; } while(right>left&&nums[left]==nums[left+1]){ //与nums[i]去重不同,这个也是找到之后发现left有重复的,直接left++ left++; }
-
写本题的时候,如果去重的逻辑多加了 对right 和left 的去重:(代码中注释部分)
while (right > left) { if (nums[i] + nums[left] + nums[right] > 0) { right--; // 去重 right while (left < right && nums[right] == nums[right + 1]) right--; } else if (nums[i] + nums[left] + nums[right] < 0) { left++; // 去重 left while (left < right && nums[left] == nums[left - 1]) left++; } else { } }
但细想一下,这种去重其实对提升程序运行效率是没有帮助的。
拿right去重为例,即使不加这个去重逻辑,依然根据 while (right > left)
和 if (nums[i] + nums[left] + nums[right] > 0)
去完成right-- 的操作。
多加了 while (left < right && nums[right] == nums[right + 1]) right--;
这一行代码,其实就是把 需要执行的逻辑提前执行了,但并没有减少 判断的逻辑。
最直白的思考过程,就是right还是一个数一个数的减下去的,所以在哪里减的都是一样的。
所以这种去重 是可以不加的。 仅仅是 把去重的逻辑提前了而已。
四数之和
-
给你一个由
n
个整数组成的数组nums
,和一个目标值target
。请你找出并返回满足下述全部条件且不重复的四元组[nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
-
0 <= a, b, c, d < n
-
a
、b
、c
和d
互不相同 -
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8 输出:[[2,2,2,2]]
-
力扣18题
思路
-
四数之和和15.三数之和的基础上再套一层for循环。
-
但是有一些细节需要注意,例如: 不要判断
nums[k] > target
就返回了,三数之和 可以通过nums[i] > 0
就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1]
,target
是-10
,不能因为-4 > -10
而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)
就可以了。
-
15.三数之和 的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下标作为双指针,找到nums[i] + nums[left] + nums[right] == 0。
-
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^3) 。
-
那么一样的道理,五数之和、六数之和等等都采用这种解法。
-
对于15.三数之和双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。
-
之前做过哈希表的经典题目:454.四数相加II ,相对于本题简单很多,因为本题是要求在一个集合中找出四个数相加等于target,同时四元组不能重复。
-
而454.四数相加II是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于本题还是简单了不少!
class Solution { public: vector<vector<int>> fourSum(vector<int>& nums, int target) { //在三数之和的基础上,再嵌套一层for循环,来遍历nums[k] //nums[k] + nums[i] + nums[left] + nums[right]==target vector<vector<int>> result; //存放结果集 sort(nums.begin(),nums.end());//进行排序,方便双指针的移动 for(int k=0;k<nums.size();k++){ //第一层遍历 if(nums[k]>target&&target>0&&nums[k]>0){ //因为target可正可负,所以只有当num[k]>0&&target>0时沿用三数之和的剪枝 eg:target=-5 nums=[-4,-1,0,0] break; // 这里使用break,统一通过最后的return返回 } // 对nums[k]去重 if(k>0&&nums[k]==nums[k-1]){ continue; } for(int i=k+1;i<nums.size();i++){// // 2级剪枝处理 if(nums[i]+nums[k]>target&&nums[i]+nums[k]>0&&target>0){ //将nums[i]+nums[k]看成一个整体进行剪枝 break; } // 对nums[i]去重 if(i>k+1&&nums[i]==nums[i-1]){ //去重因为取i-1,所以要保证i>k+1 continue; } int right=nums.size()-1; int left=i+1; while(left<right){ // nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出 //强转一个,后面会自动往上升 if((long)nums[k]+nums[i]+nums[right]+nums[left]>target){ right--; // nums[k] + nums[i] + nums[left] + nums[right] < target 会溢出 }else if((long)nums[k]+nums[i]+nums[right]+nums[left]<target){ left++; }else{ result.push_back(vector<int>{nums[k],nums[i],nums[left],nums[right]}); // 对nums[left]和nums[right]去重 while(left<right&&nums[right]==nums[right-1]){ right--; } while(left<right&&nums[left]==nums[left+1]){ left++; } // 找到答案时,双指针同时收缩 right--; left++; } } } } return result; } };
-
时间复杂度: O(n^3)
-
空间复杂度: O(1)
补充:
二级剪枝的部分:
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) { break; }
可以优化为:
if (nums[k] + nums[i] > target && nums[i] >= 0) { break; }
因为只要 nums[k] + nums[i] > target,那么 nums[i] 后面的数都是正数的话,就一定 不符合条件了。