一、知识点
哈希表理论基础
建议:大家要了解哈希表的内部实现原理,哈希函数,哈希碰撞,以及常见哈希表的区别,数组,set 和map。
什么时候用哈希法?
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
这句话很重要,大家在做哈希表题目都要思考这句话。
1、哈希表的内部实现原理
数组就是一张哈希表。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素
哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
2、哈希函数
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
3、哈希碰撞
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
解决方法
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
1)拉链法
拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
2)线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。
常见哈希表的区别
-
数组
-
set (集合)
红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
-
map(映射)
文章讲解:代码随想录
二、题目
242.有效的字母异位词
做题链接:
建议: 这道题目,大家可以感受到 数组 用来做哈希表 给我们带来的遍历之处。
做题思路
法1:排序
t 是 s 的异位词等价于「两个字符串排序后相等」法2:哈希。统计s和t里每个字符出现的次数,比较s和t
难点:如果出现 AlphaTable[pos]<0,则说明 t 包含一个不在 s 中的额外字符
法3:map
遍历 map 中的所有键值对
如果有任何一个键值对的值不为零,则表示两个字符串不互为异位词,返回 false
所有键值对的值均为零,则表示两个字符串互为异位词,返回 true
代码细节
// 1、快排sort函数
class Solution {
public:
bool isAnagram(string s, string t) {
// 法1:排序
// t 是 s 的异位词等价于「两个字符串排序后相等」
// 特殊情况:如果 s 和 t 的长度不同,t 必然不是 s 的异位词
if (s.length() != t.length()) return false;
// 对字符串 s 和 t 分别排序
sort(s.begin(), s.end());
sort(t.begin(), t.end());
// 看排序后的字符串是否相等即可判断
if (s == t) return true;
else return false;
}
};
class Solution {
public:
bool isAnagram(string s, string t) {
// 法2:哈希。统计s和t里每个字符出现的次数,比较s和t
// 特殊情况:如果 s 和 t 的长度不同,那么 t 不是 s 的异位词
if (s.length() != t.length()) return false;
// 初始化字母表的次数为0,记录26个英文字母出现的次数
vector<int> AlphaTable(26, 0);
// 循环遍历字符串s中的每一个字符,并将其赋值给变量ch
for (auto& ch : s)
{
// ch - 'a'表示将字符ch与字符'a'的ASCII码值相减,得到一个整数值,作为数组 AlphaTable 的下标
int pos = ch - 'a';
// 将pos对应的数组元素加1
AlphaTable[pos]++;
}
// 遍历字符串 t,减去 AlphaTable 中对应的频次
for (auto& ch : t)
{
AlphaTable[ch - 'a']--;
// 难点:如果出现 AlphaTable[pos]<0,则说明 t 包含一个不在 s 中的额外字符
if (AlphaTable[ch - 'a'] < 0) return false;
}
return true;
}
};
// 2、代码随想录
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++) {
// 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
record[s[i] - 'a']++;
}
for (int i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (record[i] != 0) {
// record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
// record数组所有元素都为零0,说明字符串s和t是字母异位词
return true;
}
};
// 3、自己写的
class Solution {
public:
bool isAnagram(string s, string t) {
// 法2:哈希。统计s和t里每个字符出现的次数,比较s和t
// 特殊情况:如果 s 和 t 的长度不同,那么 t 不是 s 的异位词
if (s.length() != t.length()) return false;
// 初始化字母表的次数为0,记录26个英文字母出现的次数
vector<int> AlphaTable(26, 0);
// 循环遍历字符串s中的每一个字符,并将其赋值给变量ch
for (auto& ch : s)
{
// ch - 'a'表示将字符ch与字符'a'的ASCII码值相减,得到一个整数值,作为数组 AlphaTable 的下标
int pos = ch - 'a';
// 将pos对应的数组元素加1
AlphaTable[pos]++;
}
// 遍历字符串 t,减去 AlphaTable 中对应的频次
// auto& ch意味着循环变量ch是对t中当前元素的引用,因此你可以在循环体内修改t中的元素
for (auto& ch : t)
{
AlphaTable[ch - 'a']--;
}
// 注意:遍历 AlphaTable 中的元素alpha
for (auto alpha : AlphaTable)
{
if (alpha != 0) return false;
}
return true;
}
};
class Solution {
public:
bool isAnagram(string s, string t) {
// 法3:map
if (s.length() != t.length()) // 如果两个字符串的长度不同,那么它们不可能互为字谜,直接返回 false
return false;
unordered_map<char, int> map1; // 定义 unordered_map 对象 map1,用于存储各个字符在字符串 s 中出现的次数
for (char i : s) // 遍历字符串 s,统计其中每个字符出现的次数,并将其存储在 map1 中
map1[i]++;
for (char i : t) // 遍历字符串 t,对于其中的每个字符,减少其在 map1 中的计数器
map1[i]--;
for (auto it : map1) { // 遍历 map1 中的所有键值对
if (it.second != 0) { // 如果有任何一个键值对的值不为零,则表示两个字符串不互为字谜,返回 false
return false;
}
}
return true; // 所有键值对的值均为零,则表示两个字符串互为字谜,返回 true
}
};
349. 两个数组的交集
建议:本题就开始考虑 什么时候用set 什么时候用数组,本题其实是使用set的好题,但是后来力扣改了题目描述和 测试用例,添加了 0 <= nums1[i], nums2[i] <= 1000 条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用set。
做题思路
1、注意,使用数组来做哈希的题目,是因为题目都限制了数值的大小。
而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。
如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
2、使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set
3、直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
4、找特定元素是否存在于集合中
如果 find() 方法找到了要查找的元素,它会返回指向该元素的迭代器;
如果未找到要查找的元素,它会返回一个指向集合的 end() 的迭代器,表示未找到。
通过比较find()方法返回的迭代器是否等于 end(),可以确定集合中是否有查找的元素。
if (mySet.find(i) != mySet.end())
{
// 判断元素是否在集合中, 只要不等于end(), 说明元素在集合中
}
代码细节
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
// 结果集去重
unordered_set<int> res;
// 创建并初始化 nums
// 遍历 nums1 的所有元素,并将去重过后的元素添加到 nums 中。
unordered_set<int> nums(nums1.begin(), nums1.end());
// 遍历nums2,在去重过的nums1里找nums2的元素
for (auto num :nums2)
{
// 在nums里找num(在去重过的nums1里找nums2的元素,找到插入num到结果集里)
if (nums.find(num) != nums.end()) res.insert(num);
}
// 注意点:遍历 res 的所有元素,并将元素添加到 inter 中。
vector<int> inter(res.begin(), res.end());
return inter;
}
};
202. 快乐数
建议:这道题目也是set的应用,其实和上一题差不多,就是 套在快乐数一个壳子
做题思路
1、题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
判断sum是否重复出现就可以使用unordered_set。
2、求和的过程,需要取数值各个位上的单数
代码细节(待)
class Solution {
public:
// 取数值各个位上的单数之和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 难点:如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
1. 两数之和
建议:本题虽然是 力扣第一题,但是还是挺难的,也是 代码随想录中 数组,set之后,使用map解决哈希问题的第一题。
建议大家先看视频讲解,然后尝试自己写代码,在看文章讲解,加深印象
视频链接:梦开始的地方,Leetcode:1.两数之和,学透哈希表,map使用有技巧!_哔哩哔哩_bilibili
做题思路
本题有四个重点:
- 为什么会想到用哈希表
当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
- 哈希表为什么用map
本题,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
- 本题map是用来存什么的
map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)
- map中的key和value用来存什么的
这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。
那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。
所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
把这四点想清楚了,本题才算是理解透彻了
过程:
代码细节(待)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int,int> map;
for(int i = 0; i < nums.size(); i++) {
// 遍历当前元素,并在map中寻找是否有匹配的key
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i};
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
三、总结
242. 有效的字母异位词:这道题目是用数组作为哈希表来解决哈希问题
349. 两个数组的交集:这道题目是通过set作为哈希表来解决哈希问题
1. 两数之和:这道题目是通过map作为哈希表来解决哈希问题