题目:四数相加 II
- 给你四个整数数组
nums1
、nums2
、nums3
和nums4
,数组长度都是n
,请你计算有多少个元组(i, j, k, l)
能满足:0 <= i, j, k, l < n
;nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
题解
-
本题是使用哈希法的经典题目,这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况。本题解题步骤:
-
首先定义 一个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) { unordered_map<int,int> count12; for(int u:nums1){ for(int v:nums2){ ++count12[u+v]; } } int res =0; for(int x:nums3){ for(int y:nums4){ // if(count12.count(-x-y)){ // res += count12[-x-y]; // } if(count12.find(0-x-y)!=count12.end()){ res += count12[-x-y];} } } return res; } };
-
对于 A 和 B,我们使用二重循环对它们进行遍历,得到所有 A[i]+B[j] 的值并存入哈希映射中。对于哈希映射中的每个键值对,每个键表示一种 A[i]+B[j],对应的值为 A[i]+B[j] 出现的次数。
-
对于 C 和 D,我们同样使用二重循环对它们进行遍历。当遍历到 C[k]+D[l] 时,如果 −(C[k]+D[l]) 出现在哈希映射中,那么将 −(C[k]+D[l]) 对应的值累加进答案中。最终即可得到满足 A[i]+B[j]+C[k]+D[l]=0 的四元组数目。
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)。我们使用了两次二重循环,时间复杂度均为 O ( n 2 ) O(n^2) O(n2)。在循环中对哈希映射进行的修改以及查询操作的期望时间复杂度均为 O(1),因此总时间复杂度为 O ( n 2 ) O(n^2) O(n2)。空间复杂度: O ( n 2 ) O(n^2) O(n2),即为哈希映射需要使用的空间。在最坏的情况下,A[i]+B[j] 的值均不相同,因此值的个数为 n 2 n^2 n2,也就需要 O ( n 2 ) O(n^2) O(n2) 的空间。
题目:赎金信
-
给你两个字符串:
ransomNote
和magazine
,判断ransomNote
能不能由magazine
里面的字符构成。如果可以,返回true
;否则返回false
。magazine
中的每个字符只能在ransomNote
中使用一次。 -
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
题解
-
题目要求使用字符串 magazine 中的字符来构建新的字符串 ransomNote,且ransomNote 中的每个字符只能使用一次,只需要满足字符串 magazine 中的每个英文字母 (’a’-’z’) 的统计次数都大于等于 ransomNote 中相同字母的统计次数即可。
-
如果字符串 magazine 的长度小于字符串 ransomNote 的长度,则我们可以肯定 magazine 无法构成 ransomNote,此时直接返回 false。
-
首先统计 magazine 中每个英文字母 a 的次数 cnt[a],再遍历统计 ransomNote 中每个英文字母的次数,如果发现 ransomNote 中存在某个英文字母 c 的统计次数大于 magazine 中该字母统计次数 cnt[c],则此时我们直接返回 false。
-
-
class Solution { public: bool canConstruct(string ransomNote, string magazine) { if(ransomNote.size()>magazine.size()) return false; int countR[26]={ 0}; int countM[26]={ 0}; for(int i=0;i<ransomNote.size();i++){ countR[ransomNote[i]-97]++; } for(int i=0;i<magazine.size();i++){ countM[magazine[i]-97]++; } for(int i=0;i<26;i++){ if(countR[i]>countM[i]) return false; } return true; } };
-
本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点。
-
第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思” 这里说明杂志里面的字母不可重复使用。
-
第二点 “你可以假设两个字符串均只含有小写字母。” 说明只有小写字母,这一点很重要。
-
-
因为题目说只有小写字母,那可以采用空间换取时间的哈希策略,用一个长度为26的数组来记录magazine里字母出现的次数。然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
-
在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!
-
class Solution { public: bool canConstruct(string ransomNote, string magazine) { int record[26] = { 0}; //add if (ransomNote.size() > magazine.size()) { return false; } for (int i = 0; i < magazine.length(); i++) { // 通过record数据记录 magazine里各个字符出现次数 record[magazine[i]-'a'] ++; } for (int j = 0; j < ransomNote.length(); j++) { // 遍历ransomNote,在record里对应的字符个数做--操作 record[ransomNote[j]-'a']--; // 如果小于零说明ransomNote里出现的字符,magazine没有 if(record[ransomNote[j]-'a'] < 0) { return false; } } return true; } };//一个数组就够了
-
时间复杂度:O(m+n),其中 m 是字符串 ransomNote 的长度,n 是字符串 magazine 的长度,我们只需要遍历两个字符一次即可。空间复杂度:O(∣S∣),S 是字符集,这道题中 S 为全部小写英语字母,因此 ∣S∣=26。
题目:三数之和
- 给你一个整数数组
nums
,判断是否存在三元组[nums[i], nums[j], nums[k]]
满足i != j
、i != k
且j != k
,同时还满足nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为0
且不重复的三元组。注意:答案中不可以包含重复的三元组。
题解
-
题目中要求找到所有「不重复」且和为 0 的三元组,这个「不重复」的要求使得我们无法简单地使用三重循环枚举所有的三元组。这是因为在最坏的情况下,数组中的元素全部为 0。任意一个三元组的和都为 0。如果我们直接使用三重循环枚举三元组,会得到 O ( N 3 ) O(N^3) O(N3) 个满足题目要求的三元组(其中 N 是数组的长度)时间复杂度至少为 O ( N 3 ) O(N^3) O(N3)。在这之后,我们还需要使用哈希表进行去重操作,得到不包含重复三元组的最终答案,又消耗了大量的空间。这个做法的时间复杂度和空间复杂度都很高,因此我们要换一种思路来考虑这个问题。15. 三数之和 - 力扣(LeetCode)
-
「不重复」的本质是什么?我们保持三重循环的大框架不变,只需要保证:
-
第二重循环枚举到的元素不小于当前第一重循环枚举到的元素;
-
第三重循环枚举到的元素不小于当前第二重循环枚举到的元素。
-
-
也就是说,我们枚举的三元组 (a,b,c) 满足 a≤b≤c,保证了只有 (a,b,c) 这个顺序会被枚举到,而 (b,a,c)、(c,b,a) 等等这些不会,这样就减少了重复。要实现这一点,我们可以将数组中的元素从小到大进行排序,随后使用普通的三重循环就可以满足上面的要求。同时,对于每一重循环而言,相邻两次枚举的元素不能相同,否则也会造成重复。举个例子,如果排完序的数组为
-
{ 0,1,2,2,2,3}
-
我们使用三重循环枚举到的第一个三元组为 (0,1,2),如果第三重循环继续枚举下一个元素,那么仍然是三元组 (0,1,2),产生了重复。因此我们需要将第三重循环「跳到」下一个不相同的元素,即数组中的最后一个元素 3,枚举三元组 (0,1,3)。
-
ums.sort() for first = 0 .. n-1 // 只有和上一次枚举的元素不相同,我们才会进行枚举 if first == 0 or nums[first] != nums[first-1] then for second = first+1 .. n-1 if second == first+1 or nums[second] != nums[second-1] then for third = second+1 .. n-1 if third == second+1 or nums[third] != nums[third-1] then // 判断是否有 a+b+c==0 check(first, second, third)
-
这种方法的时间复杂度仍然为 O ( N 3 ) O(N^3) O(N3),毕竟我们还是没有跳出三重循环的大框架。然而它是很容易继续优化的,可以发现,如果我们固定了前两重循环枚举到的元素 a 和 b,那么只有唯一的 c 满足 a+b+c=0。当第二重循环往后枚举一个元素 b′时,由于 b′>b,那么满足 a+b′+c′=0 的 c′ 一定有 c′<c,即 c′ 在数组中一定出现在 c 的左侧。也就是说,我们可以从小到大枚举 b,同时从大到小枚举 c,即第二重循环和第三重循环实际上是并列的关系。
-
有了这样的发现,我们就可以保持第二重循环不变,而将第三重循环变成一个从数组最右端开始向左移动的指针,从而得到下面的伪代码:
-
nums.sort() for first = 0 .. n-1 if first == 0 or nums[first] != nums[first-1] then // 第三重循环对应的指针 third = n-1 for second = first+1 .. n-1 if second == first+1 or nums[second] != nums[second-1] then // 向左移动指针,直到 a+b+c 不大于 0 while nums[first]+nums[second]+nums[third] > 0 third = third-1 // 判断是否有 a+b+c==0 check(first, second, third)
-
这个方法就是我们常说的「双指针」,当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复杂度从 O ( N 2 ) O(N^2) O(N2) 减少至 O(N)。为什么是 O(N) 呢?这是因为在枚举的过程每一步中,「左指针」会向右移动一个位置(也就是题目中的 b),而「右指针」会向左移动若干个位置,这个与数组的元素有关,但我们知道它一共会移动的位置数为 O(N),均摊下来,每次也向左移动一个位置,因此时间复杂度为 O(N)。
-
注意到我们的伪代码中还有第一重循环,时间复杂度为 O(N),因此枚举的总时间复杂度为 O ( N 2 ) O(N^2) O(N2)。由于排序的时间复杂度为 O ( N log N ) O(N \log N) O(NlogN),在渐进意义下小于前者,因此算法的总时间复杂度为 O ( N 2 ) O(N^2) O(N2)。
-
class Solution { public: vector<vector<int>> threeSum(vector<int>& nums) { // vector<vector<int>> res; // unordered_set temp_set; // for(int item:nums){ // temp_set.insert(item); // } // vector<int> temp_vec.assign(temp_set.begin(),temp_set.end()); // sort(temp_vec.begin,temp_vec.end()); int n=nums.size(); sort(nums.begin(),nums.end()); vector<vector<int>> res; for(int first=0;first<n;first++){ if(first>0&&nums[first]==nums[first-1]) continue; int third = n-1; int first_num = -nums[first]; for(int second=first+1;second<n;++second){ if(second>first+1&&nums[second]==nums[second-1]) continue; while(second<third&&nums[second]+nums[third]>first_num){ third--; } if(second==third) break; if(nums[second]+nums[third]==first_num) res.push_back({ nums[first],nums[second],nums[third]}); } } return res; } };
-
-
时间复杂度: O ( N 2 ) O(N^2) O(N2),其中 NNN 是数组 nums 的长度。空间复杂度: O ( log N ) O(\log N) O(logN)。我们忽略存储答案的空间,额外的排序的空间复杂度为 O ( log N ) O(\log N) O(logN)。然而我们修改了输入的数组 nums,在实际情况下不一定允许,因此也可以看成使用了一个额外的数组存储了 nums 的副本并进行排序,空间复杂度为 O(N)。