哈希表的知识点对于我来说比较陌生,所以在这里进行一个合集总结,参照代码随想录的刷题顺序对哈希表的具体应用进行一个相对全面的总览。
哈希表理论基础
(黄色高亮的内容为知识点标记)
基本概念:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
用大白话来说——我们事先人为设定一个规律,根据这种规律,某一个数据应该放在表中的哪一个位置我们是可以推出来的,此后如果我们想知道这个数据有没有,我们只需要到理论中它应该存在的位置去找就可以,如果找不到那就说明这个数据是不存在的。
其中,某种规律其实就是哈希函数中的内部处理,也就是说我们通过哈希函数来确定这种规律。
从上述的大白话讲解中,我们也可以总结出哈希表的应用场景——
快速判断某一个数据是否在原集合中存在。
但有时候,我们设置的规律可能无法满足所有数据都能“一个萝卜一个坑”,例如设置规律为“对25取余”,但实际数据的范围为[0, 30],这时候有些数据就会发生冲突,我们将这种冲突称为哈希碰撞。
解决哈希碰撞的方法主要有两种:
-
拉链法
-
线性探测法
拉链法主要是对冲突的元素采用链表的方式进行连接,如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/2f4af74d51ddfa97f6e47496e5d36689.png)
线性探测法可以看成是一种和和气气的占座方式,比如说有两个人(A和B)都买到了位置2,但是A先到了,B就没有位置坐了,此时B就往后找位置,找到了位置3,但是位置3已经被本来就应该在这里的C占到了,这时候B就接着往后找,找到了位置4,位置4没有人,B就坐下了。过了一会儿,本来应该坐在位置4上的D上车了,发现位置4被B占领了,D没有生气,而是接着往后找位置坐,跟B的行为一致。这种方式就叫做线性探测法。
![](https://i-blog.csdnimg.cn/blog_migrate/27eb6325f48fd61ff2b121af5bd91985.png)
当然,大家都能和和气气让座占座的前提是大家都知道所有人一定都能坐得下,这就要求我们在预开数组的时候要让数组的容量大于所有参与到哈希函数中的元素数量。
除此之外,解决冲突的方法还有再哈希法以及建立公共溢出区等等,这里不做详述,以上四种方法也可以参考下面这篇博客
🔗解决Hash碰撞冲突方法总结_zeb_perfect的博客-CSDN博客
常见的三种哈希结构
常见的三种哈希结构有:
-
数组
-
set
-
map
在这三种哈希结构中,数组应用起来最为简便,它适合应用于数据量比较小、可数的情况,因为在使用数组时需要预先确定数组的大小,对于不能确定数量和大小的数据来说,数组就没办法满足需求。
那么当待处理的数据很大、数据量很多的时候,我们就需要借助set。set不需要预先设置总的大小,只需要当读取到相应数据时将其insert到set中即可。那么这种特性就使得set很容易处理数据分布稀疏的集合了
在C++中,set提供了以下三种结构:
![](https://i-blog.csdnimg.cn/blog_migrate/d06ef90eba18e70cdd33da62454e8b24.png)
(图源自代码随想录)
三种的区分点在于是否有序以及数值是否可以重复。
-
无序且数值不重复——unordered_set
-
有序且数值不重复——set
-
有序且数值可重复——multiset
其中unordered_set的底层实现是哈希表,set和multiset的底层实现是红黑树。
我们在一般使用时优先选择unordered_set,因为无论从时间还是空间,unordered_set的复杂度都最低。如果有“有序”或者“数值可重复”这些限制,我们再根据实际情况选择合适的结构。
除此之外,还有另一种哈希结构——map,这种哈希结构的优势在于可以存储一个数据的两个信息(即key和value),例如LeetCode1.两数之和这道题目就是map应用的一道典型例题。
C++同样为map提供了三种结构:
![](https://i-blog.csdnimg.cn/blog_migrate/c895415259a221f2acbce03a257a4083.png)
(图源自代码随想录)
区分点和上述set大致相同,选择顺序也一致,大家对照一下即可。
在C++标准库中,有现成的unordered_set、unordered_map、multiset、multimap供大家使用,在接下来的题目讲解中也会涉及到。
LeetCode题目练习
LeetCode242.有效的字母异位词
题目链接
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
视频讲解
学透哈希表,数组使用有技巧!Leetcode:242.有效的字母异位词_哔哩哔哩_bilibili
题目分析
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
![](https://i-blog.csdnimg.cn/blog_migrate/57b72b1e999b320ed01a49d357332ebe.png)
该题就是典型的判断一个数据是不是在集合中,所以直接考虑到用哈希法。
我们只需要设定一个int数组(初始化为{0}),将s中的各个字符按照规律映射到特定位置并且在该位置上存好出现的次数,而后根据t中的字符对相应位置存好的出现次数进行“减减”操作即可,当int数组中所有位置上全是0时,说明s中的各个字符出现次数和t中的各个字符出现次数一样,此时返回true,否则返回false。
这种题目对于初学者来说很不容易想到,但只要接触过之后,这种思路就基本上就能存在脑子了,之后按照这种思路进行变换应用即可。所以说,第一遍先学会,跟着前人的车辙走一遍才能在未来有机会自己创造新车辙。
上代码~
class Solution {
public:
bool isAnagram(string s, string t) {
int hash[26] = {0};
for(int i = 0; i < s.size(); i++) {
hash[s[i] - 'a']++;
}
for(int j = 0; j < t.size(); j++) {
hash[t[j] - 'a']--;
}
for(int m = 0; m < 26; m++) {
if(hash[m] != 0) return false;
}
return true;
}
};
LeetCode349. 两个数组的交集
题目链接
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
视频讲解
学透哈希表,set使用有技巧!Leetcode:349. 两个数组的交集_哔哩哔哩_bilibili
题目分析
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
![](https://i-blog.csdnimg.cn/blog_migrate/08a96988d94e889848c40488d7290efe.png)
这道题目最开始的时候是没有提示中的第一条限定(1 <= nums1.length, nums2.length <= 1000)第二条限定(0 <= nums1[i], nums2[i] <= 1000)的,如果没有,那么这道题目中nums1[i], nums2[i]中的数值可以很大很大,nums的长度也可以很大很大,这时候在使用数组就根本没办法确定一个合适的容量大小,开小了会导致空间不够用,开大了又会导致空间冗余浪费,所以在这里我们采用set进行求解,有什么我们存什么。
因为题目要求输出的每个元素均唯一且不需要在乎顺序,那么这就满足无序且不重复,于是我们直接选择unordered_set。
我们只需要将nums1遍历一遍并存入到unordered_set1中,再遍历nums2并查看nums2中的数据在unordered_set1中是否能找到即可,若能找到就将该数据存入到unordered_set2中,最终再return unordered_set2即可。
这里补充一些unordered_set的基本操作
在C++中使用时需要预先包含头文件#include<unordered_set>
初始化:
-
unordered_set<数据类型> 数据名
-
unordered_set<数据类型> 数据名 (起始元素,终止元素)
功能函数:
-
begin():返回指向容器中第一个元素的正向迭代器。
-
end():返回指向容器中最后一个元素之后位置的正向迭代器。
-
find(key):查找以值为 key 的元素,如果找到,则返回一个指向该元素的正向迭代器;反之,则返回一个指向容器中最后一个元素之后位置的迭代器(如果 end() 方法返回的迭代器)。
-
insert():向容器中添加新元素。
-
swap():交换 2 个 unordered_set 容器存储的元素,前提是必须保证这 2 个容器的类型完全相等。
更多具体的操作可以查看该链接🔗C++ STL unordered_set容器完全攻略
上代码~
去除限制条件,使用set进行题目操作求解
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
//若nums1[i]和nums2[i]没有范围限定
unordered_set<int> result;
unordered_set<int> temp(nums1.begin(), nums1.end());
for(int i = 0; i < nums2.size(); i++) {
if(temp.find(nums2[i]) != temp.end()) {
result.insert(nums2[i]);
}
}
return vector<int>(result.begin(), result.end());
}
};
针对现在该题目有两种限制条件的情况,我们也可以借助数组进行求解。
只需要初始化一个空间为1001的hash数组(int)类型即可。先将nums1中的数组遍历一遍(该过程可以使用unordered_set,也可以不使用,下面会给出两种解法分别对应),若数据存在就将对应位置的数字设为1,再根据nums2中的元素映射找到哈希表对应位置的数据是否为1,若为1就为交集元素。
有限制条件,借助unordered_set求解
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
//若nums1[i]和nums2[i]有范围限定(>= 0 && <= 1000)
int hash[1001] = {0};//一定注意初始化!!!
unordered_set<int> result;
for(int i = 0; i < nums1.size(); i++) {
hash[nums1[i]] = 1;
}
for(int j = 0; j < nums2.size(); j++) {
if(hash[nums2[j]] == 1) {
result.insert(nums2[j]);
}
}
return vector<int>(result.begin(), result.end());
}
};
有限制条件,不借助unordered_set求解,只使用数组
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
vector<int> temp1(1001, 0);
vector<int> result;
for(int i = 0; i < nums1.size(); i++) {
temp1[nums1[i]] = 1;
}
for(int j = 0; j < nums2.size(); j++) {
if(temp1[nums2[j]] == 1) {
temp1[nums2[j]] = 2;
}
}
for(int m = 0; m < 1001; m++) {
if(temp1[m] == 2) {
result.push_back(m);
}
}
return result;
}
};
LeetCode202.快乐数
题目链接
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
文章讲解
题目分析
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
![](https://i-blog.csdnimg.cn/blog_migrate/fbd67d94df47f06f283ce7807b4310b9.png)
这个题目看上去像是一个数学题,但是当我们深挖题干会发现,其实这个题目可以借助哈希表来完成。
对于一个数来说,只有两种情况:
-
重复过程直到变为1
-
无限循环但始终变不到1
从无限循环我们可以得知,如果一个数不是快乐数,那么这个数经过若干次上述过程后,一定会出现重复的数,这时候我们就可以借助哈希表来进行判断在这一过程中是否出现了重复的数。
根据题意,这个题目在存储时只要无序且不重复即可,所以我们选择unordered_set。进而我们可以通过set的find功能判断有没有重复的元素出现,一旦出现就说明这个数不是快乐数,return false即可,如果不出现,那么最终一定可以==1,此时就判定其为快乐数。
在此之前,我们还需要单独设置一个函数来分离数字n的各个位数并求平方和,这属于基本操作,直接上代码。
int GetSum(int n) {
int sum = 0;
while(n) {
sum += (n % 10) * (n % 10);//n % 10就是分离各个位数的操作
n /= 10;
}
return sum;
}
接下来就进行循环求解就可以了。
bool isHappy(int n) {
unordered_set<int> result;
while(1) {
int sum = GetSum(n);
if(sum == 1) return true;
if(result.find(sum) != result.end()) return false;//如果找到重复数据,就返回false
result.insert(sum);//否则,就将现有数据插入到unordered_set中去
n = sum;//将n替换成已有的sum,对应示例过程
}
LeetCode1.两数之和
题目链接
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
视频讲解
梦开始的地方,Leetcode:1.两数之和,学透哈希表,map使用有技巧!_哔哩哔哩_bilibili
题目解析
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
![](https://i-blog.csdnimg.cn/blog_migrate/91bbbce03a309029e053f877d809484d.png)
该题要求有两点:
-
找出和为目标值 target 的那两个整数
-
返回它们的数组下标
一看,这一题就是让我们根据一个元素去寻找另一个元素在集合中存不存在,那么就需要用到哈希表的方法。
同时,通过题目要求可知我们需要两种数据,一种是整数数值,另一种是数值的下标。这种情况下我们需要选择map。
又因为不能重复且对输出顺序没有要求,那么我们就首选unordered_map。
这么一分析感觉还挺顺畅的哈~
在上代码前先来补充一下unordered_map的一些基本操作。
在C++中使用时需要预先包含头文件#include<unordered_map>
初始化:
-
通过调用 unordered_map 模板类的默认构造函数,可以创建空的 unordered_map 容器。
std::unordered_map<std::string, std::string> umap;
-
在创建 unordered_map 容器的同时,可以完成初始化操作。
std::unordered_map<std::string, std::string> umap{
{"Python教程","http://c.biancheng.net/python/"},
{"Java教程","http://c.biancheng.net/java/"},
{"Linux教程","http://c.biancheng.net/linux/"} };
-
可以调用 unordered_map 模板中提供的复制(拷贝)构造函数,将现有 unordered_map 容器中存储的键值对,复制给新建 unordered_map 容器。
std::unordered_map<std::string, std::string> umap2(umap);
-
部分初始化或者跨数据类型初始化
std::vector<std::int> nums;
std::unordered_map<std::int, std::int> umap(nums.begin(), nums.end());
功能函数:
begin():返回指向容器中第一个键值对的正向迭代器。
end():返回指向容器中最后一个键值对之后位置的正向迭代器。
find(key):查找以 key 为键的键值对,如果找到,则返回一个指向该键值对的正向迭代器;反之,则返回一个指向容器中最后一个键值对之后位置的迭代器(如果 end() 方法返回的迭代器)。
insert():向容器中添加新键值对。
插入操作格式为insert(pair<数据类型,数据类型>(数据1,数据2))
重载运算符[]:umap[i]指的是umap中key为i的数据对应的value值。(若没有key为i的数据则自动创建)
小重点:返回key值时采用umap -> first,返回value值时采用umap -> second。
详细可参考链接🔗C++ STL unordered_map容器用法详解
此外还要补充一种新的数据类型auto,auto可以自动根据输入的数据来判断数据类型并自动转化。在此题题解中有使用。
上代码~
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> result;
for(int i = 0; i < nums.size(); i++) {
int m = target - nums[i];
//auto iter = result.find(m);//auto的用法
if(result.find(m) != result.end()) {
return {result.find(m)->second, i};
}
result.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
这个题目除了用哈希表之外,还可以用暴力双循环法进行解决的,这里也附上代码~
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> result;
result.push_back(0);
result.push_back(0);
for(int i = 0;i < nums.size() - 1; i++) {
for(int j = i+1; j < nums.size(); j++) {
if(nums[i] + nums[j] == target){
result[0] = i;
result[1] = j;
return result;
}
}
}
return result;
}
};
但是吧,时间复杂度那差的可就不是一星半点了,直接上图。
![](https://i-blog.csdnimg.cn/blog_migrate/6019d8871a2371ed6f27dc2023eec628.png)
懂了吧?😏
LeetCode454. 四数相加 II
题目链接
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
视频讲解
学透哈希表,map使用有技巧!LeetCode:454.四数相加II_哔哩哔哩_bilibili
题目分析
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
![](https://i-blog.csdnimg.cn/blog_migrate/24d7189280d6ba555f98f652e39e76b9.png)
此题和LeetCode15.三数之和,LeetCode18.四数之和两个题目并不相同,这个题目是可以使用哈希表法的,原因在于这个题目是从四个数组中分别找出一个元素来要求加和为0,而15和18则是在一个数组中查找相应的下标要求加和为0。
该题要求从四个数组中分别找一个元素要求加和为0,此时我们就可以将两个数组归为一组,将其中一组的数据遍历加和后存储所得数据,然后将另外一组中的两个数组遍历加和再在已存储数据中寻找是否有满足条件的数据,根据寻找结果记录次数返回最终元组个数即可。
这里我们只需要存储一种数据(即两个数组中各元素加和的数据)且不需要其有序,所以采用unordered_set即可。
有了大致思路之后,我们就上代码~
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> umap;
for(int a : nums1) {
for(int b : nums2) {
umap[a + b]++; //umap[a+b]是指在umap中创建一个key(= a+b)值且umap[a+b]表示这一key值对应下的value值,初始默认为0。
}
}
int count = 0; //记录a+b+c+d=0的次数
for(int c : nums3) {
for(int d : nums4) {
if(umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
补充说明一种新用法
for(int a : nums1){}
这是迭代器遍历,对于数组来说,等同于
for(int a = 0; a < nums1.length; a++){}
LeetCode383. 赎金信
题目链接
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
文章讲解
题目分析
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
![](https://i-blog.csdnimg.cn/blog_migrate/adf384c276ec5b860bc10a7c98a4325a.png)
这个题目就是哈希表数组的一个应用,这里就不做解析了,也希望大家能自己试试解出来,跟
直接上代码~
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int result[26] = {0};
for(int i = 0; i < magazine.size(); i++) {
result[magazine[i] - 'a']++;
}
for(int j = 0; j < ransomNote.size(); j++) {
result[ransomNote[j] - 'a']--;
}
for(int m = 0; m < 26; m++) {
if(result[m] < 0) return false;
}
return true;
}
};
总结
其实这部分还有两道题目:
但这里就不再展开来讲了,一方面是这两道题目不适合用哈希表来做,推荐解法是双指针,另一方面是我自己确实也弄得不是很明白,所以还是期待二刷能好好再理解一下。大家可以对照这两个题目和之前的四数相加Ⅱ的题目进行总结,看看哪种情况下适合哈希表求解,哪种情况下不要用。
哈希表到这里就告一段落啦!其实回过头来看,就是数组、set、map的几种用法,好好分析题目要求,选择对应的结构,就差不多没问题啦!
最重要的还要强调一下:
哈希表的应用场景——快速判断某一个数据是否在原集合中存在。
加油💪!