本系列总计六篇文章,是 基于STL实现的笔试题常考七大基本数据结构 该文章在《代码随想录》和《labuladong的算法笔记》题目中的具体实践,每篇的布局是这样的:开头是该数据结构的总结,然后是在不同场景的应用,以及不同的算法技巧。本文是系列第三篇,介绍了哈希表的相关题目,重点是要掌握STL的set和map,注意N数之和问题,用双指针最方便。
下面文章是在《代码随想录》和《labuladong的算法笔记》题目中的具体实践:
【笔记】数组
【笔记】链表
【笔记】哈希表
【笔记】字符串
【笔记】栈与队列
【笔记】二叉树
0、总结
-
哈希表用来快速判断一个元素是否出现在集合里
-
一般常用数组、
unordered_set
、unordered_map
这三种数据结构实现哈希表,后两者的底层实现是哈希表,无序、数值(key)不可重复、数值(key)不可修改、查询和增删效率都是 O(1) -
set,multiset,map,multimap底层是红黑树,当要求key有序、重复时,可以选用
-
两数、三数、四数之和问题,用 双指针法 最方便
1、set 作为哈希表
349. 两个数组的交集 - 力扣(LeetCode)
思路:输出交集,要求去重、不考虑输出结果的顺序,因此选用unordered_set
注意:set和vector互相转化可以用构造函数直接实现,不需要遍历;set增加元素用insert;用auto更方便
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
vector<int> result;
// 要去重,所以用unordered_set
unordered_set<int> result_set, nums1_set;
// unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int i : nums1)
nums1_set.insert(i);
for (int i : nums2) {
if (nums1_set.find(i) != nums1_set.end()) {
result_set.insert(i);
}
}
// return vector<int>(result_set.begin(), result_set.end());
for (auto it = result_set.begin(); it != result_set.end(); it++)
result.push_back(*it);
return result;
}
};
202. 快乐数 - 力扣(LeetCode)
思路:用set判断集合中的某个元素是否出现过,若出现过,代表有循环,此数并非快乐数,应及时return
注意:按位取数的写法应牢记,n不为0的情况下,不断地先%10,再/10,这样可以低位到高位取数
class Solution {
public:
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) return true;
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
int getSum(int n) {
int sum = 0;
while (n) {
sum += pow(n % 10, 2);
n /= 10;
}
return sum;
}
};
2、map 作为哈希表
350. 两个数组的交集 II - 力扣(LeetCode)
思路:与349的区别是,不去重,若公共元素是a,返回结果中a出现的次数,应与a在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值),要用unordered_map
进阶:
-
如果给定的数组已经排好序呢?你将如何优化你的算法?用双指针只需 O(n) 的时间复杂度
-
如果 nums1 的大小比 nums2 小,哪种方法更优?较小的数组用hash存储,然后在另一个数组中寻找
-
如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
还是较小的数组用hash存储,然后在另一个数组中,每次读取出一部分数据进行寻找
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
vector<int> result;
// 题目不要求去重
unordered_map<int, int> map;
for (int i : nums1)
map[i]++;
for (int i : nums2) {
// 若是双方的重复元素,输出
if (map[i] > 0) {
result.push_back(i);
map[i]--;
}
}
return result;
}
};
242. 有效的字母异位词 - 力扣(LeetCode)
思路:t
是否是 s
的字母异位词,若 s
和 t
中每个字符出现的次数都相同,则称 s
和 t
互为字母异位词。本题需要统计字符及其对应出现次数,用map,由于不要求key有序,最终选用unordered_map
或者是:比较字符串排序后的结果是否相等
class Solution {
public:
bool isAnagram(string s, string t) {
unordered_map<char, int> map;
for (char c : s)
map[c]++;
for (char c : t)
map[c]--;
// for (auto it = map.begin(); it != map.end(); it++) {
for (unordered_map<char, int>::iterator it = map.begin(); it != map.end(); it++) {
// 只要有次数不是0的,说明不是异位词
if (it->second != 0)
return false;
}
return true;
}
};
383. 赎金信 - 力扣(LeetCode)
思路:判断 ransomNote
能不能由 magazine
里面的字符构成,与上题相似,magazine
中的每个字符只能在 ransomNote
中使用一次,也就是说magazine
可以有剩余。当magazine
中元素个数不够或是不含ransomNote
中的字符时,返回false。
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
unordered_map<char, int> map;
for (char c : magazine)
map[c]++;
for (char c : ransomNote)
map[c]--;
for (auto it = map.begin(); it != map.end(); it++) {
if (it->second < 0)
return false;
}
return true;
}
};
49. 字母异位词分组 - 力扣(LeetCode)
思路:本题要将相同的异位词总结在一起,异位词排序后的结果是相同的,所有想到将 字符串排序后的结果作为key,其原本的值作为value,然后分组输出,可以按任意顺序返回结果列表,选用unordered_map
注意:map的value不为int时候的插入语法,用push_back
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>> result;
unordered_map<string, vector<string>> map;
for (string str : strs) {
// 保存原来的值
string s = str;
// 当前str排序
sort(str.begin(), str.end());
// 排序后的值作为key,插入原来的值
map[str].push_back(s);
}
for (auto it = map.begin(); it != map.end(); it++)
result.push_back(it->second);
return result;
}
};
438. 找到字符串中所有字母异位词 - 力扣(LeetCode)
思路:要找的异位词是子串,联想到用滑动窗口+双指针,且本题中的窗口大小固定,始终是p.size()
。利用unordered_map
,记录need
需要的字符及其数量和window
窗口中符合要求的字符及其数量
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> result;
int valid = 0;
int left = 0, right = 0;
unordered_map<char, int> window, need;
for (char c : p)
need[c]++;
while (right < s.size()) {
char c = s[right];
right++;
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
while (right - left >= p.size()) {
if (valid == need.size())
result.push_back(left);
char d = s[left];
left++;
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
return result;
}
};
454. 四数相加 II - 力扣(LeetCode)
思路:利用map,遍历前两个数组,把两两之和 以及 和对应出现的次数写入map,然后遍历后两个数组,看0-(c+d)是否在map中,若有,则将其对应出现的次数累加
注意:本题只能计算出有多少个元组满足条件,与n数之和要求的返回下标的题目,完全不同
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> map;
for (int a : nums1)
for (int b : nums2)
map[a + b]++;
int count = 0;
for (int c : nums3)
for (int d : nums4) {
auto it = map.find(0 - c - d);
if (it != map.end()) {
count += it->second;
}
}
return count;
}
};
3、n Sum 问题 - 排序+双指针
1. 两数之和 - 力扣(LeetCode)
思路:不用set用map是因为要返回下标。
陷阱:排序+双指针法不能使用!因为1.两数之和要求返回的是索引下标,而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。但是如果要求返回的是数值的话,就可以使用双指针法了
注意:map中insert一对值,必须是以pair的形式插入
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {
auto it = map.find(target - nums[i]);
if (it != map.end())
return {i, it->second};
else
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
167. 两数之和 II - 输入有序数组 - 力扣(LeetCode)
思路:数组已经有序,数值大小和下标大小正相关关系,可以用双指针法
注意:换算index和数组下标
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0, right = numbers.size() - 1;
while (left < right) {
if (numbers[left] + numbers[right] > target) right--;
else if (numbers[left] + numbers[right] < target) left++;
else {
// 左右指针记录数组下标,需要转换成index
return {left + 1, right + 1};
}
}
return {};
}
};
15. 三数之和 - 力扣(LeetCode)
思路:map不推荐,因为包含了复杂的去重逻辑。先排序!两数之和,直接左右双指针即可;三数之和,i和左右双指针,一层for循环控制i的遍历;四数之和,i,j以及左右双指针,两层for循环分别控制i、j的遍历…
注意:由于本题求和为0,因此nums[i] > 0可以直接return;a的去重写法;b、c的去重,放在前面if-else分支也可以。保证了三元组之间没有重复,但三元组之内可以重复
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0) return result;
// a去重
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--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
// 找到三数之和为0
else {
result.push_back({nums[i], nums[left], nums[right]});
// b、c去重,放在前面if分支也可以
while (left < right && nums[right] == nums[right - 1]) right--;
while (left < right && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return result;
}
};
18. 四数之和 - 力扣(LeetCode)
思路:先排序!两层for循环分别控制i、j的遍历,每层开始时候都需要剪枝
注意:
1、本题的和为target,不是0,剪枝的逻辑有区别
// 一级剪枝
if (nums[i] > target && nums[i] >= 0) break;
// 二级剪枝
if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;
2、对a、b的去重写法没有区别;
3、双指针写法和收缩写法没有区别
4、防止溢出,不要写成四数之和形式
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
// 四数之和,是i,j还有左右双指针
// 对i正数要剪枝
if (nums[i] > target && nums[i] >= 0) break; // 这里使用break,统一通过最后的return返回
// 对nums[i]去重
if (i > 0 && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < nums.size(); j++) {
// 对j进行2级剪枝
if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;
// 对nums[j]去重
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
int left = j + 1;
int right = nums.size() - 1;
while (left < right) {
// 小心溢出
if (nums[i] + nums[j] > target - nums[left] - nums[right]) right--;
else if (nums[i] + nums[j] < target - nums[left] - nums[right]) left++;
else {
result.push_back({nums[i], nums[j], nums[left], nums[right]});
// 对nums[left]和nums[right]去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
}
return result;
}
};