今天依然是打卡哈希表的题目,在上一天的基础上加了一些难度,我发现自己之前每次投入的时间有点过多,考虑到后面时间有限,如果一个题目没有思路或者很久没有做出来,那我就直接看答案,然后按照答案的思路求解,顺带在题目上进行标记,方便自己以后进行回顾。
一、题目打卡
1.1 四数相加ii(借助答案思路)
题目链接:. - 力扣(LeetCode)
这个题目一开始想到了暴力的解法,但是这样嵌套四层循环肯定会时间超时,考虑到这次是哈希表专题,但是没有想到合适利用哈希表的方法,然后看了答案做出的解法如下:
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> umap;
for(auto &i : nums1){
for(auto &j : nums2){
umap[i + j]++;
}
}
int res = 0;
for(auto &i : nums3){
for(auto &j : nums4){
if(umap.find(0-(i+j)) != umap.end()){
// res++;
res += umap[-i-j];
}
}
}
return res;
}
};
想法:
题目本质上利用的是加法的结合律,然后分组进行计算,这样的话就把暴力法 O(n^4) 的复杂度降低到了O(n^2),是一种很不一样的思路,有一个小的错误是后面在统计返回值的时候,由于之前统计的 key-val 的 val 是在前两组中和等于特定 key 的数目总和,所以这里不能是简单的 res++ 。
1.2 赎金信
leetcode 题目链接:. - 力扣(LeetCode)
和前面的同构词的思路类似:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
unordered_map<char, int> umap;
for(auto &it : magazine){
umap[it]++;
}
for(auto &it : ransomNote){
if(umap.find(it) == umap.end() || umap[it] <= 0) return false;
if(umap.find(it) != umap.end()) umap[it]--;
}
return true;
}
};
想法:
题目本身没有太大难度,注意的是在第二次循环判断的时候,有一个题目的条件是 magazine 中的字符在 randomNote 中只能出现一次,所以如果进行遍历的时候,出现过的字符在 umap 中统计次数需要减 1。
进一步地,如果想要继续优化,那么就可以使用数组的思路进行映射,这样省去了哈希表 umap 所构造哈希函数使用的额外开销,参考答案的解法:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
// 因为只能使用一次,所以如果 ransomNode 的空间更大,肯定是不成立的
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;
}
};
1.3 三数之和(借助答案思路)
题目链接:. - 力扣(LeetCode)
class Solution {
private:
bool checkRepeat(vector<vector<int>> &res, vector<int> &tmp){
for(auto &i : res){
unordered_map<int,int> _tmp;
unordered_map<int,int> __tmp;
for(auto& j : i){
_tmp[j]++;
}
for(auto& it : tmp){
__tmp[it]++;
}
if(_tmp == __tmp) return false;
}
return true;
}
public:
vector<vector<int>> threeSum(vector<int>& nums) {
unordered_map<int,int> umap;
for(auto& it: nums){
umap[it]++;
}
vector<int> tmp;
vector<vector<int>> res;
// for(auto& it : umap){
// cout << it.first << " " << it.second << endl;
// }
for(int i = 0 ; i < nums.size() ; i++){
unordered_map<int,int> _umap(umap);
for(int j = i + 1 ; j < nums.size() ; j++){
// for(auto& it : umap){
// cout << it.first << " " << "-" << " " << it.second << endl;
// }
tmp.push_back(nums[i]);
tmp.push_back(nums[j]);
// 这里的 i 是临时减的
umap[nums[i]]--;
umap[nums[j]]--;
// for(auto& it : umap){
// cout << it.first << " " << "--" << " " << it.second << endl;
// }
if(umap.find(-nums[i]-nums[j]) != umap.end() && umap[-nums[i]-nums[j]] >= 1){
tmp.push_back(-nums[i]-nums[j]);
// res.push_back(tmp);
if(checkRepeat(res,tmp)) res.push_back(tmp);
}
umap[nums[i]]++;
// for(auto& it : umap){
// cout << it.first << " " << "---" << " " << it.second << endl;
// }
tmp.clear();
}
// 这里必须要拷贝一份,不然在下一轮j的时候,会导致map对应的数字没有复原
umap = _umap;
umap[nums[i]]--;
// for(auto& it : umap){
// cout << it.first << " ---- " << it.second << endl;
// }
}
return res;
}
};
想法:
这个题目原本想借助哈希表将 O(n^3) 的复杂度降低到 O(n^2),但是在做的过程中发现这个题目没有那么简单,首先是在我的方法下,对映射的 umap 中的键值会频繁改变,这样就需要设计这个键值改变的逻辑,而且还会涉及到拷贝,导致了空间和时间的浪费,最关键的是最终的结果不能包含重复的元组,基于这一点,我设计了一个检查重复的函数,但是这样在数据量很大的时候会超时。
然后看了参考答案的思路,重新使用双指针对题目进行了设计:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(),nums.end());
for(int i = 0; i < nums.size();i++){
if(nums[i] > 0) return res;
if(i > 0 && nums[i] == nums[i-1]) continue; // 对 i 进行去重
int left = i + 1;
int right = nums.size() - 1;
while(left < right){
// while(nums[right] == nums[right - 1] && left < right) right--; // 去重c
// while(nums[left] == nums[left + 1] && left < right) left++; // 去重b
if(left < right && nums[i] + nums[left] + nums[right] > 0) right--;
else if(left < right && nums[i] + nums[left] + nums[right] < 0 ) left++;
else{
while(nums[right] == nums[right - 1] && left < right) right--; // 去重c
while(nums[left] == nums[left + 1] && left < right) left++; // 去重b
res.push_back({nums[i],nums[left],nums[right]});
left++;
right--;
}
}
}
return res;
}
};
这里答案的思路其实就是在排序了以后,重复的元素会在一起,这样每轮进行取值的时候,想办法只进行一次取值,这样的话有一个容易犯的小错误就是对于第二个数字 b ,和第三个数值 c 去重时候,应该先添加进了结果了以后再进行去重,否则类似 [0,1,1] 这种,left 和 right 在一开始去重了以后,都会指向索引为 1 的位置,并且最后还会加入到 res 中(在 else 的条件中,对于left >= right 的条件没有进行限制,只是默认了 a + b + c == 0 会进入这个 else 执行语句)。
1.4 四数之和
这个题目和三数之和的思路差不多,但是有更多的坑,自己基本通过写出了大部分的案例,最后出现的一些问题以及解决方法:
(1)一开始忘记处理了第一个数字和第二个数字的重复,并且我最后才发现我处理的方式和答案的有一些不一样,我是后续处理的,那个在一开始也出现了很多问题,是处理的思路不够恰当导致了越界。
(2)这个案例里面存在越界的情况,处理的方法参考的答案,这样也算是学会了一种强制类型转化的方法,也就是在一串计算式的最前面加上 (long) 就可以处理。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
if(nums.size() < 4) return {};
target = double(target);
vector<vector<int>> res;
sort(nums.begin(),nums.end());
for(int i = 0 ; i < nums.size() ; i++){
// cout << "after i = " << i << endl;
if(target > 0 && nums[i] > target) break;
for(int j = i + 1; j < nums.size() ;j++){
// cout << "i=" << i << endl;
if(nums[i] + nums[j] > target && nums[i] > 0) break;
int l = j + 1;
int r = nums.size() - 1;
while(l < r){
// cout << i << j << l << r<<endl;
if(l < r && (long) nums[i] + nums[j] + nums[l] + nums[r] > target) r--;
else if(l < r && (long) nums[i] + nums[j] + nums[l] + nums[r] < target) l++;
else{
while(l < r && nums[r] == nums[r - 1]) r--;
while(l < r && nums[l] == nums[l + 1]) l++;
// cout << "l=" << l << " r=" << r << endl;
res.push_back({nums[i],nums[j],nums[l],nums[r]});
l++;
r--;
}
}
// cout <<"i=" << i << "j=" << j << endl;
// while(i < nums.size() && nums[j] == nums[i]){
// // cout <<"i=" << i << "j=" << j << endl;
// j=i;
// i++;
// }
// cout <<"i=" << i << "j=" << j << endl;
while(i + 1<nums.size() && nums[i+1] == nums[i]) i++;
while(j + 1<nums.size() && nums[j+1] == nums[j]) j++;
}
}
return res;
}
};