2023年3月7日,2023年3月12日 补卡。
7 第三章 哈希表
今日任务
● 454.四数相加II
● 383. 赎金信
● 15. 三数之和
● 18. 四数之和
● 总结
454.四数相加II
【链接】(文章,视频,题目)
建议:本题是 使用map 巧妙解决的问题,好好体会一下 哈希法 如何提高程序执行效率,降低时间复杂度,当然使用哈希法 会提高空间复杂度,但一般来说我们都是舍空间 换时间, 工业开发也是这样。
题目链接/文章讲解/视频讲解:代码随想录
【第一想法与实现(困难)】
-
暴力,四层for循环,时间复杂度O(n^4),超出时间限制
-
为什么用哈希表,因为需要大量查询是否出现过相反数,及其(下标组合)出现次数
-
类似两数之和,能否前两个数组之和作为key, 次数作为value,后两个数组之和作为key,次数作为value,然后存起来?这样的时间复杂度是平方
【看后想法】与我的想法基本一致
-
for冒号表达式,在不需要下表情况下很简洁,注意要写类型int,或者自动推断auto
- for (int num : nums_vector) {}
-
无序映射的插入,直接使用中括号即可。umap[key]++;如果key不存在,则自动创建为0,适合表达为value是key的出现次数
【实现困难】
【自写代码】
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
// 用无序映射,key是两数组各自元素和,value是次数
// 代码简洁重构
std::unordered_map<int, int> umap; // key是前两数组各自元素之和,value是出现次数
for (int n1 : nums1) {
for (int n2 : nums2) {
umap[n1 + n2]++; // 自动创建新key
}
}
int count = 0;
for (int n3 : nums3) {
for (int n4 : nums4) {
if (umap.find(-n3-n4) != umap.end()) {
count += umap[-n3-n4];
}
}
}
return count;
}
};
【收获与时长】半小时,umap操作熟悉,设计哈希表的值,不需要是下标,只要是次数即可
383. 赎金信
【链接】(文章,视频,题目)
建议:本题 和 242.有效的字母异位词 是一个思路 ,算是拓展题
题目链接/文章讲解:代码随想录
【第一想法与实现(困难)】都是小写字母,可以用数组。但是长度可能有1e5,会重复多次查询某字母的出现次数,考虑用无序映射。
【看后想法】
-
暴力枚举,对于每个杂志字母,消掉可能的赎金信字母,最终如果赎金信字符串length=0。说明可以组成。关注思路与字符串操作。在第二层for中找到赎金信字母之后要break,保证不会重复删除,下表正确性
-
str.erase(str.begin() + j); // 删除str[j]
-
字符串长度str.length()
-
-
注意本意已知key只会是小写字母情况下,可以直接用数组更好,避免了map的额外空间,避免红黑树,哈希表,哈希函数额外时间
【实现困难】
【自写代码】
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
// 使用数组
if (ransomNote.length() > magazine.length()) {
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;
}
}
return true;
}
};
【收获与时长】半小时。限定了key的小范围之后,优先考虑使用数组,节省时间
【链接】(文章,视频,题目)
15. 三数之和
建议:本题虽然和 两数之和 很像,也能用哈希法,但用哈希法会很麻烦,双指针法才是正解,可以先看视频理解一下 双指针法的思路,文章中讲解的,没问题 哈希法很麻烦。
题目链接/文章讲解/视频讲解:代码随想录
【第一想法与实现(困难)】
-
暴力,三层for,超时,308 / 312 个通过的测试用例
-
哈希表,类似两数之和,把i, j的和与下标存起来。但是这样可能问题是相同的和(key)对应不同的两个元素,下标
-
即使告诉我双指针,也没想到是什么,快慢,双向?
【看后想法】
-
哈希表,难点在于手动去重…去重逻辑比较难
-
双指针。因为要返回的是元素,而不是下标,可以直接对数组进行排序
-
a, b, c,对应下标i, left, right。由于已经排序,在nums[i]=a固定情况下,left++右移,或者right–左移,直至相遇
-
a去重
-
如果a>0,已经结束直接return,因为正数只会越来越大,不会=0了
-
先收获再去重。因此i要与之前的i-1比较,而不是与之后的i+1比较
- 我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的
-
-
b,c去重,left, right
- 先收获,再去重。去重只要去掉连续出现的值即可
-
【实现困难】
- 记得先排序!双指针方法一般利用题目数据的顺序特性,需要是有序数组
【自写代码】
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// 由于返回元素三元组,而不是下标,可以先排序
// i, left, right,双指针,两层,平方时间复杂度
vector<vector<int>> res;
std::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]) { // a去重,前面已经遍历过
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (left < right) { // 三元组,不能取等号
int sum = nums[i] + nums[left] + nums[right];
// std::cout << "i, left, right:" << i << ',' << left << ',' << right << ",nums:" << nums[i] << ',' << nums[left] << ',' << nums[right] << std::endl;
if (sum < 0) {
left++;
} else if (sum > 0) {
right--;
} else {
// 找到了三元组
std::vector<int> vec = {nums[i], nums[left], nums[right]};
res.emplace_back(std::move(vec));
// b, c去重
while (left < right && nums[left] == nums[left+1]) {
left++;
}
while (left < right && nums[right] == nums[right-1]) {
right--;
}
left++;
right--;
}
}
}
return res;
}
};
【收获与时长】
-
整体感觉这题思路比较难,一个半小时。
-
双指针方法比较巧妙。为什么能想到双指针呢?首先,暴力解法是立方时间复杂度。本题之哟啊返回元素三元组,而不需要下标。可以先自带排序,NlogN时间复杂度。这样就可以利用有序信息,根据与目标结果的大小比比较,做双指针的移动。i, left, right。
-
哈希方法没看明白对于b, c去重的部分
-
有一句评论比较精辟:说白了就是降维处理, 由三维降到二维, 针对任意索引i的nums[i] 求[i+1,size )范围内不重复的twoSum target = - nums[i];
- 后面还有一个四数之和, 一样的问题, 先降到三维, 再降到二维
18. 四数之和
【链接】(文章,视频,题目)
建议: 要比较一下,本题和 454.四数相加II 的区别,为什么 454.四数相加II 会简单很多,这个想明白了,对本题理解就深刻了。 本题 思路整体和 三数之和一样的,都是双指针,但写的时候 有很多小细节,需要注意,建议先看视频。
题目链接/文章讲解/视频讲解:代码随想录
【第一想法与实现(困难)】
-
与三数之和类似,也是不能重复,那就i, j, left, right?由于只要返回元素而不是下标,可以先排序(O(NlogN)),时间复杂度O(n^3)
-
运行问题,int超出范围,越界
-
想到两个for不需要都从左边开始,可以一个从左边,一个从右边,中间再用left, right,也就是i, left, right, j
-
注意别忘了先排序,注意break条件
【看后想法】
实现方法与我想的差不多。唯一不太一样的是剪枝与二级剪枝。他使用的是判断大于0,并且要求
【实现困难】
【自写代码】
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
std::sort(nums.begin(), nums.end());
vector<vector<int>> res;
for (int i = 0; i < nums.size(); i++) {
if ((long int) nums[i] * 4 > target) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = nums.size() - 1; j >= 0; j--) {
if ((long int) nums[j] * 4 < target) {
break;
}
if (j < nums.size() - 1 && nums[j] == nums[j + 1]) {
continue;
}
int left = i + 1;
int right = j - 1;
while (left < right) {
long int leftover = (long int) nums[i] + nums[left] + nums[right] + nums[j] - target;
if (leftover < 0) {
left++;
} else if (leftover > 0) {
right--;
} else {
res.emplace_back(vector<int>{nums[i], nums[left], nums[right], nums[j]});
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
}
return res;
}
};
【收获与时长】1.2小时