一.理论基础
1.数组就是一张哈希表
查询时间复杂度O(1)
2.哈希碰撞——解决方法
(1)拉链法
(2)线性探测法
3.常⻅的三种哈希结构
-
数组
-
set(集合)
-
map(映射)
-
区别
unordered_set
和unordered_map
都是 C++ 标准库中的关联容器,它们分别对应于集合和映射的概念,具有类似的接口和实现方式,但有一些关键的区别:- 元素类型:
unordered_set
存储的是唯一的键(key),没有关联的值(value)。每个元素都是一个独特的键。unordered_map
存储键-值对,每个元素包含一个键和一个关联的值。
- 存储方式:
unordered_set
仅存储键,而且每个键是唯一的。unordered_map
存储键值对,每个键是唯一的,但值可以重复。
- 访问元素:
- 在
unordered_set
中,你可以检查某个键是否存在于集合中。 - 在
unordered_map
中,你可以通过键访问对应的值。
- 在
- 元素类型:
当我们要使⽤集合来解决哈希问题的时候,优先使⽤unordered_set,因为它的查询和增删效率是最优的,如果需
要集合是有序的,那么就⽤set,如果要求不仅有序还要有重复数据的话,那么就⽤multiset。
那么再来看⼀下map ,在map 是⼀个key value 的数据结构,map中,对key是有限制,对value没有限制的,因
为key的存储⽅式使⽤红⿊树实现的。
其他语⾔例如:java⾥的HashMap ,TreeMap 都是⼀样的原理。可以灵活贯通。
虽然std::set、std::multiset 的底层实现是红⿊树,不是哈希表,std::set、std::multiset 使⽤红⿊树来索引和存
储,不过给我们的使⽤⽅式,还是哈希法的使⽤⽅式,即key和value。所以使⽤这些数据结构来解决映射问题的⽅
法,我们依然称之为哈希法。 map也是⼀样的道理。
unordered_set在C++11的时候被引⼊标准库了,⽽hash_set并没有,所以建议
还是使⽤unordered_set⽐较好,这就好⽐⼀个是官⽅认证的,hash_set,hash_map 是C++11标准之前⺠间⾼⼿⾃发造的轮⼦。
4.总结
总结⼀下,当我们遇到了要快速判断⼀个元素是否出现集合⾥的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使⽤额外的数组,set或者是map来存放数据,才能实现快速
的查找。
如果在做⾯试题⽬的时候遇到需要判断⼀个元素是否出现过的场景也应该第⼀时间想到哈希法!
数组就是简单的哈希表,但是数组的⼤⼩可不是⽆限开辟的
二.有效的字⺟异位词
#242 有效的字母异位词(简单 哈希表 字符串 排序)
√(哈希表)
1.思路:
(1)哈希表 :分别存哈希表 直接比较哈希表
(2)数组作哈希表
三.两个数组的交集
#349. 两个数组的交集(数组 哈希表 双指针 二分查找 排序)
√(哈希表)
思路:
(1)直接用哈希表 unordered_set nums_set(nums1.begin(), nums1.end());
(2)用数组作哈希表 int hash[1005] = {0}; // 默认数值为0
1.哈希表删除元素
auto it = dic1.find(nums1[k]);
dic1.erase(it);
2.数组(一维向量)添加元素
res.push_back(nums1[k]);
3.创建一个 unordered_set 存放 nums1 中的元素
unordered_set<int> nums_set(nums1.begin(), nums1.end());
//`unordered_set` 存储的是唯一的键(key),没有关联的值(value)。每个元素都是一个独特的键。
4.将元素加入结果集
result_set.insert(num); // 将元素加入结果集
5.将结果集转换为 vector 并返回
return vector<int>(result_set.begin(), result_set.end());
#350 两个数组的交集2 (数组 哈希表 双指针 二分查找 排序)
√(哈希表)
1.思路:
(1)直接用哈希表 unordered_map<int,int> dic; //键用于存元素,值用于存元素出现次数,当两个数组的哈希表的该元素的值都大于0时,将该元素添加至结果元素并将哈希表对应的值-1.
(2)用数组作哈希表
四.快乐数
#202 快乐数(简单 哈希表 数学 双指针)×
1.哈希表法:用哈希表来判断这个sum是否重复出现,如果重复了就是return false, 否则⼀直找到sum为1为⽌。
五.两数之和
#1 两数之和(简单)
1.语法
-
map.insert(pair<int, int>(nums[i], i));//向哈希表map插入(nums[i],i)
pair<int, int>
是C++中的一种数据结构,表示两个整数值的有序对。 -
auto it=dic.find(target-nums[i]);//
auto
关键字告诉编译器在声明变量时根据其初始化表达式的类型来推导变量的类型。 在这里,it
的类型会被推导为映射容器的迭代器类型。
2.思路:遍历数组的时候,只需要向map去查询是否有和⽬前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把⽬前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
#242 有效的字母异位词
1.⽤数组作为哈希表来解决哈希问题
#349. 两个数组的交集
1.通过set作为哈希表来解决哈希问题
六. 四数相加
#454 四数相加(中等)×
1.语法
for(int a : nums1)
是 C++11 引入的范围-based for 循环,也称为 foreach 循环。这个语法用于遍历容器(如数组、向量、列表等)中的元素,让我们来解释一下:for(int a : nums1)
表示对于容器nums1
中的每个元素,执行循环体,并在每次迭代中将当前元素的值赋给变量a
。
2.思路:
-
1.⾸先定义 ⼀个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
-
2.遍历⼤A和⼤B数组,统计两个数组元素之和,和出现的次数,放到map中。
-
3.定义int变量count,⽤来统计 a+b+c+d = 0 出现的次数。
-
4.在遍历⼤C和⼤D数组,找到如果 0-(c+d) 在map中出现过的话,就⽤count把map中key对应的value也就是出现次数统计出来。
-
5.最后返回统计值 count 就可以了
3.#0015.三数之和,#0018.四数之和不适合用哈希表解,本题是经典的哈希表使用;
七. 赎金信
#383 赎金信
1.语法
m = magazine.length();
的作用是获取字符串magazine
的长度record[magazine[i]-'a'] ++;
//将小写字母的ASCII码转化到0-26并将其出现的次数存放在数组record[26]
2.思路
- 哈希解法:数组作哈希表比
unordererd_map
作哈希表速度更快,空间消耗更少 - 数组解法:时间复杂度: O(n);空间复杂度: O(1)
八.三数之和
#15三数之和(中等)×
1.语法
result.push_back(vector<int>{nums[first],nums[second],nums[left],nums[right]});
//将一维数组添加到二维数组中sort(nums.begin(), nums.end());
//对数组升序排列
2.思路
-
哈希表法:(不推荐)
-
双指针法:
-
去重逻辑的思考:我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
-
a的去重
//三元组里的元素不能重复
``if (nums[i] == nums[i + 1]) {`
continue;
}
//三元组不能重复
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
-
b和c的去重提前 对效率没有提升 可以不用提前
while(right>left&&nums[right]==nums[right-1]) right--;
while(right>left&&nums[left]==nums[left+1]) left++;
#18.四数之和(中等)
1.语法
2.思路:四数之和,和15.三数之和是⼀个思路,都是使⽤双指针法, 基本解法就是在15.三数之和 的基础上再套⼀层for循
环。主要差别在于剪枝。
九.总结
#哈希表理论基础
-
⼀般来说哈希表都是⽤来快速判断⼀个元素是否出现集合⾥。
-
对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作⽤.
哈希函数是把传⼊的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍⽅式是拉链法和线性探测法。
-
接下来是常⻅的三种哈希结构:
数组
set(集合)
map(映射)
在C++语⾔中,set 和 map 都分别提供了三种数据结构,每种数据结构的底层实现和⽤途都有所不同,在关于哈希
表,你该了解这些!中我给出了详细分析,这⼀知识点很重要!
例如什么时候⽤std::set,什么时候⽤std::multiset,什么时候⽤std::unordered_set,都是很有考究的。
- 只有对这些数据结构的底层实现很熟悉,才能灵活使⽤,否则很容易写出效率低下的程序。
#哈希表经典题⽬
-
数组作为哈希表
-
⼀些应⽤场景就是为数组量身定做的。
-
在242.有效的字⺟异位词中,我们提到了数组就是简单的哈希表,但是数组的⼤⼩是受限的!这道题⽬包含⼩写字⺟,那么使⽤数组来做哈希最合适不过。
-
在383.赎⾦信中同样要求只有⼩写字⺟,那么就给我们浓浓的暗示,⽤数组!本题和242.有效的字⺟异位词很像,242.有效的字⺟异位词是求 字符串a 和 字符串b 是否可以相互组成,在383.赎⾦信中是求字符串a能否组成字符串b,⽽不⽤管字符串b 能不能组成字符串a。
-
⼀些同学可能想,⽤数组⼲啥,都⽤map不就完事了。上⾯两道题⽬⽤**map确实可以,但使⽤map的空间消耗要⽐数组⼤⼀些,因为map要维护红⿊树或者符号表,⽽****且还要做哈希函数的运算。所以数组更加简单直接有效!
-
-
set作为哈希表
-
在349. 两个数组的交集中我们给出了什么时候⽤数组就不⾏了,需要⽤set。这道题⽬没有限制数值的⼤⼩,就⽆法使⽤数组来做哈希表了。
-
**主要因为如下两点:**数组的⼤⼩是有限的,受到系统栈空间(不是数据结构的栈)的限制。如果数组空间够⼤,但哈希值⽐较少、特别分散、跨度⾮常⼤,使⽤数组就造成空间的极⼤浪费。所以此时⼀样的做映射的话,就可以使⽤set了。
-
关于set,C++ 给提供了如下三种可⽤的数据结构:(详情请看关于哈希表,你该了解这些!)
std::setstd::multiset
std::unordered_set
std::set和std::multiset底层实现都是红⿊树,std::unordered_set的底层实现是哈希, 使⽤unordered_set 读写效率是最⾼的,本题并不需要对数据进⾏排序,⽽且还不要让数据重复,所以选择unordered_set。
-
在202.快乐数中,我们再次使⽤了unordered_set来判断⼀个数是否重复出现过。
-
-
map作为哈希表
-
在1.两数之和中map正式登场。
来说⼀说:使⽤数组和set来做哈希法的局限。数组的⼤⼩是受限制的,⽽且如果元素很少,⽽哈希值太⼤会造成内存空间的浪费。set是⼀个集合,⾥⾯放的元素只能是⼀个key,⽽两数之和这道题⽬,不仅要判断y是否存在⽽且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能⽤。
-
map是⼀种 <key, value> 的结构,本题可以⽤key保存数值,⽤value在保存数值所在的下标。所以使⽤map最为合适。
C++提供如下三种map::(详情请看关于哈希表,你该了解这些!)
std::map
std::multimap
std::unordered_map
std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层实现是红⿊树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为⾯试题,考察对语⾔容器底层的理解),1.两数之和中并不需要key有序,选择std::unordered_map 效率更⾼!
-
在454.四数相加中我们提到了其实需要哈希的地⽅都能找到map的身影。本题咋眼⼀看好像和18. 四数之和,15.三数之和差不多,其实差很多!关键差别是本题为四个独⽴的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不⽤考虑重复问题,⽽18. 四数之和,15三数之和是⼀个数组(集合)⾥找到和为0的组合,可就难很多了!
-
⽤哈希法解决了两数之和,很多同学会感觉⽤哈希法也可以解决三数之和,四数之和。其实是可以解决,但是⾮常麻烦,需要去重导致代码效率很低。
在15.三数之和中我给出了哈希法和双指针两个解法,⼤家就可以体会到,使⽤哈希法还是⽐较麻烦的。所以18. 四数之和,15.三数之和都推荐使⽤双指针法!
-
#总结
-
对于哈希表的知识相信很多同学都知道,但是没有成体系。本篇我们从哈希表的理论基础到数组、set和map的经典应⽤,把哈希表的整个全貌完整的呈现给⼤家。
同时也强调虽然map是万能的,详细介绍了什么时候⽤数组,什么时候⽤set
#附录
- 学习内容大多来源于https://programmercarl.com/(代码随想录)
- #111 代表力扣题库题号111