1. 哈希表理论基础
代码随想录: 原文
哈希表是根据关键码的值而直接进行访问的数据结构。
- 哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素
- 哈希表都是用来快速判断一个元素是否出现集合里
1.1 哈希函数
- 例如要查询一个名字是否在这所学校里。
- 哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了
- hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值
- 保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作
- 如果学生的数量大于哈希表的大小,会有几位学生的名字同时映射到哈希表 同一个索引下标的位置,导致哈希碰撞。
1.2 哈希哈希碰撞
两个同学都映射到了同一个索引的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
1.2.1 拉链法
小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
- 要选择适当的哈希表的大小
- 既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间
1.2.1 线性探测法
- 使用线性探测法,一定要保证tableSize大于dataSize
- 例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息
- 要求tableSize一定要大于dataSize,依靠哈希表中的空位来解决问题
1.3 常见的三种哈希结构
- 数组
- set (集合)
- map(映射)
1.3.1 set
set容器理论基础可以看:C++学习笔记13 - STL中set容器
其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | ||
std::multiset | 红黑树 | 有序 | 是 | 否 | ||
std::unordered_set | 哈希表 | 无序 | 否 | 否 |
std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
- 使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的
- 如果需要集合是有序的,那么就用set
- 如果要求不仅有序还要有重复数据的话,那么就用multiset
1.3.1 map
map容器理论基础可以看:C++学习笔记14 - STL中map容器
其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | ||
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | ||
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
面试题考点
- map 是一个key-value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
注意:
- 当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
- 哈希法也是牺牲了空间换取了时间 。
- unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好。map同理。
2. 哈希表-数组使用
代码随想录: 原文
力扣题目: 242.有效的字母异位词
2.1 思路
暴力法
- 两层for循环,同时记录字符是否重复出现。
- 时间复杂度:
哈希表
- 数组其实就是一个简单哈希表
- 定义一个数组,来记录字符串s里字符出现的次数
- 数组大小为26 ,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值
- 因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25
- 再遍历字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
- 遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作
- record数组如果有的元素不为零0,return false,否则,return true。
2.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. 哈希数据结构:set 1
代码随想录: 原文
力扣题目: 349. 两个数组的交集
3.1 思路
暴力方法
- 两层for循环,同时记录字符是否重复出现。
- 时间复杂度:
哈希表
- 题目没有限制数值的大小,无法使用数组来做哈希表了
- 如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费
- 使用哈希数据结构:unordered_set
- 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set
思路如图所示:
注意: set 不仅占用空间比数组大,而且速度要比数组慢。
3.2 代码实现
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
4. 哈希数据结构:set 2
代码随想录: 原文
力扣题目: 第202题. 快乐数
4.1 思路
- 求和的过程中,如果sum会重复出现,则陷入无限循环,return false。否则一直找到sum为1为止
- 要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了
- sum的数量大小没有限制,无法使用数组来做哈希表
- 使用哈希数据结构:unordered_set
4.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;
}
}
};
5. 哈希数据结构:map
代码随想录: 原文
力扣题目: 1. 两数之和
进阶要求: 时间复杂度小于
5.1 思路
第一时间想到哈希法:
- 查询一个元素是否出现过
- 一个元素是否在集合里的时候
同时,本题还要求:
- 知道元素对应的下标
- 需要使用 key-value结构来存放,key来存元素,value来存下标
- 使用map
如何使用map:
- 这道题目中并不需要key有序,选择std::unordered_map 效率更高
- map目的用来存放我们访问过的元素
- map中的存储结构为
- 遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素
5.2代码实现
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 {};
}
};
6. 总结
使用 哈希表 条件:
- 快速查询一个元素是否出现过
- 快速判断一个元素是否在集合里的时候
哈希表 使用数组 条件:
- 限制数值数量的大小
- 且哈希值不分散、跨度较小
哈希表 使用set 条件:
- 数值数量大小没有限制
- 哈希值比较少、特别分散、跨度非常大
- unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复
哈希表 使用map 条件:
- 在使用set 条件的基础上
- 需要使用 key-value结构来存放数组元素和数组元素对应的下标
评论区