算法训练营 Day5 哈希表 1 | 242.有效的字母异位词 | 349.两个数组的交集 | 202.快乐数 | 1.两数之和
查阅文档地址:programmercarl.com/
本期题目地址:
本期题目答案地址:
本期同类题目地址:
目录:
- 基本概念(做题前要理解的概念)
- 我的解法
- 疑问点(过程中产生了问题并且查找资料解决)
语言
采用C++,一些分析也是用于 C++,请注意。
基本概念
- 哈希表(hash table),国外也叫散列表。一般哈希表是用来快速判断一个元素是否出现在集合里面。
- 哈希函数(hash function),哈希函数使得哈希表的索引和值有一一对应的映射关系;可以通过查询索引下标快速查值,类似数组。
- 哈希表内部实现原理:哈希函数通过 hashCode 把名字通过特定的编码方式转化为数值。如果 hashCode 得到的值大于哈希表的大小,内部会进行一个取模操作。难以避免映射到同一个索引下标,发生哈希碰撞。
- 哈希碰撞:解决方法有两种,一是拉链法,二是线性探测法。拉链法:以一种链式形式让发生哈希冲突的元素存储再同一个索引下标;线性探测法:使用其他空余的索引下标来存放,所以哈希表的大小一定要足够大。
- 常见哈希结构 - 数组、set、map 的区别:
- 数组
- set(集合):类似于数学概念中的集合。
- map(映射):类似于 python 中的 map 字典的概念。
| 集合 | 底层实现 | 查询效率 | 增删效率 |
|---|---|---|---|
| std::set | 红黑树 | O(log n) | O(log n) |
| std::multiset | 红黑树 | O(log n) | O(log n) |
| std::unordered_set | 哈希表 | O(1) | O(1) |
- 通过命名/底层实现结构可以区分是否有序、受否可以重复 的性质;查询效率和增删效率和底层实现结构有关。
- 红黑树是一种平衡二叉搜索树,key 值是有序的,key 不可以修改,所以只能删除和增加。
| 集合 | 底层实现 | 查询效率 | 增删效率 |
|---|---|---|---|
| std::map | 红黑树 | O(log n) | O(log n) |
| std::multimap | 红黑树 | O(log n) | O(log n) |
| std::unordered_map | 哈希表 | O(1) | O(1) |
- 通过命名/底层实现容器可以区分是否有序、受否可以重复 的性质;查询效率和增删效率和底层实现结构有关。
- 这些都属于 C++ 标准库。
242.有效的字母异位词
法一:字符串可以使用 sort 函数进行排序再次比较。
法二:因为只包含 26 个小写字母,开长度为 26 的动态数组容器(模拟哈希表)可以存下所有的计数结果。
我的代码
// 使用 map 容器
// 时间复杂度:O(n+m)
// 空间复杂度:O(n)
class Solution {
public:
bool isAnagram(string s, string t) {
if(s.length() != t.length()) {
return false;
}
map<char, int> mp;
for(auto ss : s) {
mp[ss] ++;
}
for(auto ss : t) {
mp[ss] --;
if(mp[ss] < 0) {
return false;
}
}
return true;
}
};
我的疑惑
- 我刚开始使用 vector str;尝试存储计数结果,原来使用 vector vec;也可以,只需要计数的时候与处理一下 str - 'a';
- 进阶,可以计数 unicode 的字符,核心在于字符是离散且未知范围的。所以采用哈希表来维护。使用 map 或者 set 容器。
383.赎金信
我的代码
// 题目限定在 26 个小写字母,本题可以开长度 26 的动态数组模拟哈希数组
// 时间复杂度:O(n+m)
// 空间复杂度:O(n);n = 26 长度;
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
if(ransomNote.length() > magazine.length()) {
return false;
}
vector<int> vec(26, 0);
for(auto ss : magazine) {
vec[ss-'a'] ++;
}
for(auto ss : ransomNote) {
vec[ss-'a'] --;
if(vec[ss-'a'] < 0) {
return false;
}
}
return true;
}
};
49.字母异位词分组
题目说明:strs[i]仅包含小写字母的字符串。
我的代码
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for(auto ss : strs) {
int counts[26] = {0};
for(char c : ss) {
counts[c-'a'] ++;
}
string key = "";
for(int i = 0; i < 26; i ++) {
key.push_back(i+'a');
key.push_back(counts[i]);
}
mp[key].push_back(ss); // vector 类型使用 push_back
}
vector<vector<string>> res;
for(auto ans : mp) {
res.push_back(ans.second);
}
return res;
}
};
我的疑惑
- 法一排序法,哈希表可以存放<string, vector>;
- 没想到,使用词频来构造特征词。力扣官答 - 链接。
- 在 C++ 里直接用 + 来拼接字符串,每次拼接都会生成新的字符串对象,这会带来较大的性能开销。C++ 里推荐用 std::stringstream 或者 std::string 的 append 方法。
- to_string:把整数类型转换为字符串类型。
- std::stringstream 来避免性能问题,同时将 (i + 'a') 强制转换为 char 类型。
stringstream KeyStream;
KeyStream << static_cast<char>(i+'a') << to_string(counts[i]);
return KsyString.str();
key.push_back(i+'a');
key.push_back(counts[i]);
438.找到字符串中所有字母异位分词
我的代码
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
// 特殊情况
if(s.size() < p.size()) {
return vector<int>();
}
// 初始化
vector<int> vec(26,0);
vector<int> res;
int diff = 0;
for(int i = 0; i < p.size(); i ++) {
++vec[p[i] - 'a']; // p 是+
--vec[s[i] - 'a']; // s 是-
}
for(int i = 0; i < 26; i ++) {
if(vec[i] != 0)diff ++;
}
if(diff == 0)res.emplace_back(0);
int pL = p.size();
for(int l = 1; l < s.size() - pL + 1; l ++) {
// 移除左端口
// 问题一:移除的元素下标是 l-1
++vec[s[l-1]-'a'];
if(vec[s[l-1]-'a'] == 0) {
diff --;
} else if(vec[s[l-1]-'a'] == 1){
diff ++;
}
// 添加右端口
int r = l + pL - 1;
--vec[s[r]-'a'];
if(vec[s[r]-'a'] == 0) {
diff --;
} else if(vec[s[r]-'a'] == -1){
diff ++;
}
if(diff == 0) {
res.emplace_back(l);
}
}
return res;
}
};
我的疑惑
- 先写下特殊情况,当 s.size() < p.size() 返回空,以防后面忘记。
- 动态数组直接进行比较,sCounts == pCounts,相等要求数值和空间大小一样。
- ans.emplace_back(0) 与 ans.push_back(0) 的区别。在添加单个元素,推荐第一个写法,性能更优。
- diff 可以统计数组非零的个数。
349. 两个数组的交集
我的代码
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> nums{nums1.begin(), nums1.end()};
vector<int> res;
for(auto it : nums2) {
if(nums.find(it) != nums.end()) {
nums.erase(it);
res.emplace_back(it);
}
}
return res;
}
};
我的疑惑
- 我想到哈希,但是没有想到用 set 容器,现成的集合容器。unordered_set us 用法可以去都包里面搜索一下。unordered_set us{ nums1.begin(), nums1.end() };是使用迭代器范围来初始化 us;
- 法二:也可以借助指针和排序。可以见官答了解。
- 这道题目没有限制数值的大小,就无法使用数组来做哈希表了。
350. 两个数组的交集 II
我的代码
// 简单高效
class Solution {
public:
std::vector<int> intersect(std::vector<int>& nums1, std::vector<int>& nums2) {
std::unordered_map<int, int> hashmap;
std::vector<int> res;
for (auto nn : nums1) {
hashmap[nn]++;
}
for (auto nn : nums2) {
if (hashmap.count(nn) && hashmap[nn] > 0) {
--hashmap[nn];
res.emplace_back(nn);
}
}
return res;
}
};
我的疑惑
- if (m[key]) 会让 unordered_map 插入键 1 并将对应的值初始化为 0。运行该代码后,会发现 unordered_map 的大小变为 1。
- 为避免这种不必要的插入操作,推荐使用 find 或者 count 方法来检查键是否存在:
- 使用 find 方法:find 方法会返回一个迭代器,若键存在,迭代器指向该键值对;若不存在,迭代器等于 end()。
- 使用 count 方法:count 方法会返回键在容器中出现的次数,由于 unordered_map 和 map 里键是唯一的,所以返回值要么是 0(键不存在),要么是 1(键存在)。
- 仅仅依靠 hashmap.count(nn) 来判断元素是否存在,那么在遍历 nums2 时,只要元素在 hashmap 里存在过,就会将其添加到结果数组中,不会考虑该元素在 nums1 中的实际出现次数。
- 如果给定的数组已经排好序呢?你将如何优化你的算法?如果 nums1 的大小比 nums2 小,哪种方法更优?如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
第 202 题。快乐数
我的代码
class Solution {
public:
int action(int num) {
int ans = 0;
while(num) {
int temp = num % 10;//个位上的数字
ans += temp * temp;
num /= 10;
}
return ans;
}
bool isHappy(int n) {
// 问题一:如何判断他是无限循环的数字
unordered_set<int> hashmap;
while(n != 1 && !hashmap.count(n)) {
hashmap.insert(n);
n = action(n);
}
return n == 1;
}
};
我的疑惑
- 如何取数字上各个数字这个问题已经很熟练了。
- 如何判断他是无限循环,可以用链表、快慢指针、哈希表。
1. 两数之和
题目前提只会存在一个有效答案。
我的代码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hashmap;
for(int i = 0; i < nums.size(); i ++) {
int temp = target - nums[i];
if(hashmap.find(temp) != hashmap.end()) {
return {hashmap.find(temp)->second, i}; // 问题二:注意返回格式
}
hashmap[nums[i]] = i;
}
return {}; // 问题一:{}表示返回空数组
}
};
我的疑惑
- 为了减少暴力法中 target-x 的查找时间,使用哈希表 hashmap.find(x).,时间复杂度降低到从 O(N) 降低到 O(1)。
- 创建哈希表,对于每一个 x,首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 重复匹配。
总结
算法训练营 Day5 聚焦哈希表,242、349 等多道力扣题,介绍哈希基本概念、常见结构及区别。用 C++ 实现各题解法,含代码、时间空间复杂度分析,还提出疑惑点及解决思路,如容器使用、字符串拼接性能等。