454. 四数相加 II
之前做过使用hash表的1. 两数之和,使用的是unordered_map
,且只用了一个for循环的遍历,本题是四个数组,所以暴力解法是四个for循环,那么时间复杂度就是O(n^4),很明显是非常耗时的,所以使用unordered_map
来牺牲空间换取运算时间。
大体思路就是首先使用一个for循环遍历前两个数组,将其和存入map中,然后使用一个嵌套for循环遍历后两个数组再找到符合条件的答案。下面给出代码:
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
int ans = 0;
unordered_map<int,int> map;
for(int i = 0; i < nums1.size(); i++){
for(int j = 0; j < nums2.size(); j++){
map[nums1[i] + nums2[j]]++;
}
}
for(int i = 0; i < nums3.size(); i++){
for(int j = 0; j < nums4.size(); j++){
if(map.find(0-(nums3[i] + nums4[j])) != map.end()){
ans += map.find(0-(nums3[i] + nums4[j]))->second;
}
}
}
return ans;
}
};
本题需要注意的是ans += map.find(0-(nums3[i] + nums4[j]))->second;
。这段答案计数的代码加的是map中存放的value值,也就是在遍历前两个数组时所有满足条件的两数之和,因为本题是不需要去重的,所以应该加上所有的可能结果。
383. 赎金信
本题和之前做过的242. 有效的字母异位词有点相似,因为考虑到跟字母是否出现以及出现次数有关,所以用哈希表。跟242有区别的是,在遍历magzine的时候,一旦出现不存在ransomNote中的字母就可以直接跳出循环返回false
。下面给出使用数组作为哈希表的代码:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
if (ransomNote.size() > magazine.size()) {
return false;
}
int record[26] = {0};
for(char c : magazine){
record[c - 'a']++;
}
for(char c : ransomNote){
record[c - 'a']--;
if(record[c - 'a'] < 0) return false; //说明magazine中不含ransom的字母,false
}
return true;
}
};
下面再给出使用unordered_map
的代码:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
if(ransomNote.size() > magazine.size()) return false;
unordered_map<char,int> umap;
for(char c : magazine){
umap[c]++;
}
for(char c : ransomNote){
if(umap.find(c) != umap.end() && umap.find(c)->second != 0) umap.find(c)->second--;
}
int sum = 0;
for(auto it = umap.begin(); it != umap.end(); it++){
sum += it->second;
}
if(sum == magazine.size() - ransomNote.size()) return true;
return false;
}
};
上面的是使用数组,下面的是使用hash map,可以发现在数据量较小的时候,数组和map的内存消耗基本差不多。
15.三数之和
本题和下一题四数之和的难度相对于之前上升了不少,一方面是细节,还有一方面是去重和剪枝的操作。
在一个数组中找到不重复的三元组,就涉及到复杂的去重操作。
- 哈希法
如果使用hash法来写,思路大体是:使用一个嵌套for循环遍历数组,然后在内层循环的时候将不满足条件的元素存入hash table中,如果在内层循环找到符合条件的元素,还需要进行去重操作。下面看一下代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
sort(nums.begin(),nums.end());
for(int i = 0; i < nums.size(); i++){
if(nums[i] > 0) break; //剪枝
if(i > 0 && nums[i] == nums[i - 1]) continue; //第一次去重
unordered_set<int> set;
for(int j = i + 1; j < nums.size(); j++){
if(j > i + 2 && nums[j] == nums[j - 1] && nums[j - 1] == nums[j - 2]){
continue; //第二次去重
}
int c = 0 - nums[i] - nums[j];
if(set.find(c) != set.end()){
ans.push_back({c , nums[i] , nums[j]});
set.erase(c); //第三次去重
}else{
set.insert(nums[j]);
}
}
}
return ans;
}
};
这里需要注意的是,在去重操作时,需要考虑元素下标是否有意义,并且应该是if(nums[i] == nums[i - 1])
这样比较,与前一个对比,否则会造成漏解,下面的双指针法去重时一样需要注意这点。
- 双指针法:
需要补充的一点是,所有的剪枝和去重的逻辑都是建立在数组有序的情况下,所以第一件事应该是sort(nums.begin(),nums.end());
,给数组排序。
双指针法的思路大体是:遍历数组时,设置一个left和一个right指针,分别指向i + 1
和数组末尾,然后就是根据条件移动两个指针,涉及去重的操作也是通过移动指针来解决。代码如下,添加了部分注释:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
sort(nums.begin(),nums.end()); //排序
for(int i = 0; i < nums.size(); i++){
if(nums[i] > 0) break; //剪枝
if(i > 0 && nums[i] == nums[i - 1]) continue; //去重
int left = i + 1;
int right = nums.size() - 1;
//双指针逻辑
while(left < right){
if((nums[i] + nums[left] + nums[right]) > 0){
right--;
continue;
}else if ((nums[i] + nums[left] + nums[right]) < 0){
left++;
continue;
}else{
ans.push_back({nums[i] , 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 ans;
}
};
下面是两种做法的对比:
上面的是双指针法,下面的是hash法,可以发现,当数据量以及数据大小增加时,hash法占用的内存和计算hash映射函数的时间大大增加。
18. 四数之和
四数之和和上一题三数之和其实差不多,只不过多了一层循环,多了一次去重操作,并且多了一些细节,直接用代码解释:
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ans;
sort(nums.begin(),nums.end());
for(int k = 0; k < nums.size(); k++){
if(nums[k] > target && nums[k] > 0 && target > 0) break;
if(k > 0 && nums[k] == nums[k - 1]) continue; //k的去重
for(int i = k + 1; i < nums.size(); i++){
if(i > k + 1 && nums[i] == nums[i - 1]) continue; //i的去重
int left = i + 1;
int right = nums.size() - 1;
while(right > left){
if((long) nums[k]+nums[i]+nums[left]+nums[right] > target){
right--;
continue;
}else if((long) nums[k]+nums[i]+nums[left]+nums[right] < target){
left++;
continue;
}else{
ans.push_back({nums[k],nums[i],nums[left],nums[right]});
//left和right的去重
while(right > left && nums[right] == nums[right - 1]) right--;
while(right > left && nums[left] == nums[left + 1]) left++;
left++;
right--;
}
}
}
}
return ans;
}
};
需要注意的点:
- 对i去重时,判断条件
if(i > k + 1 && nums[i] == nums[i - 1])
可以看到是i > k + 1
。 - 在双指针逻辑中,对条件的判断时
if((long) nums[k]+nums[i]+nums[left]+nums[right] > target)
发现前面加了一个(long)
,这样做的目的是防止数据溢出,因为几个大型int
型变量相加可能会导致超出int
所能表示的数据范围。
其他的点就是,当遇到这种需要频繁循环的题,提交时多多少少会出现bug,所以最笨的办法就是根据自己的逻辑拿一张白纸把循环过程写出来,比如i > k + 1
这个条件我自己一开始写的时候没有注意到这个细节,模拟一遍之后才发现了问题。