哈希表基础知识
定义
基本概念:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数(或者哈希函数),存放记录的数组叫做散列表。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
一般哈希表都是用来快速判断一个元素是否出现集合里。
哈希函数
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。 哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
- 如果得到的哈希值大于哈希表的大小:为了保证映射出来的索引数值都落在哈希表上,会在再次对数值做一个取模的操作,就保证了学生姓名一定可以映射到哈希表上了。
- 如果整体学生数量大于哈希表的大小(避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置):哈希碰撞。
哈希碰撞
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
拉链法
拉链法主要是对冲突的元素采用链表的方式进行连接:
线性探测法
线性探测法可以看成是一种和和气气的占座方式,比如说有两个人(A和B)都买到了位置2,但是A先到了,B就没有位置坐了,此时B就往后找位置,找到了位置3,但是位置3已经被本来就应该在这里的C占到了,这时候B就接着往后找,找到了位置4,位置4没有人,B就坐下了。过了一会儿,本来应该坐在位置4上的D上车了,发现位置4被B占领了,D没有生气,而是接着往后找位置坐,跟B的行为一致。这种方式就叫做线性探测法。
- 注意:要求我们在预开数组的时候要让数组的容量大于所有参与到哈希函数中的元素数量。
其他方法
常见的三种哈希结构
- 数组
- set(集合)
- map(映射)
set
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
map
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的。map 是一个key:value 的数据结构。
总结
- 当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
- 当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
- 遇到需要判断一个元素是否出现过 的场景也应该第一时间想到哈希法。
- 哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
题目
242. 有效的字母异位词
思路
由于本题只是小写字母,则一共26个:
- 定义一个长为26大小的数组hash,且初始化为0。该数组的索引0-25位置里的数值分别用作记录a-z出现的次数;
- 遍历第一个字符串,进行字母出现次数记录,当出现一次就在对应索引位置++。
- 遍历第二个字符串,进行字母出现次数记录,当出现一次就在对应索引位置--。
- 最后遍历hash数组,若出现非0元素则不是异位词
本题哈希函数(即找到字母对应我们定义的这个hash数组位置的关系函数),用的是
hash[s[i] - 'a']。例如s[i] = c,则s[i] - 'a' = 2,用的是字母的ASCII码值进行计算出该字母在hash数组中的位置。
class Solution {
public:
bool isAnagram(string s, string t) {
int hash[26] = {0};
//遍历s字符串,做对应位置++
for(int i = 0;i < s.size();i++){
hash[s[i] - 'a']++; //s[i]-'a'即得到偏移量
}
//遍历t字符串,做对应位置--
for(int j = 0;j < t.size();j++){
hash[t[j] - 'a']--;
}
//遍历hash数组,若发现有非0元素,则不是异位词
for(int k = 0;k < 26;k++){
if(hash[k] != 0){
return false;
}
}
return true;
}
};
349. 两个数组的交集
自己的做法——暴力解法
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
//定义一个unordered_set类型的集合,unordered_set类型可以自己去重
unordered_set<int> result;
//双for遍历num1和num2,查找相同元素
for(int i = 0;i<nums1.size();i++){
for(int j = 0;j<nums2.size();j++){
if(nums1[i] == nums2[j]){
result.insert(nums1[i]);
}
}
}
//将set类型转换为vector类型
return vector<int>(result.begin(), result.end());
}
};
代码随想录思路
set解法——
- 将num1数组变为一个unordered_set类型的集合nums_set;
- 再定义一个unordered_set类型的集合result_set用于存放最终结果,unordered_set类型会自动去除重复元素
- 从num2中遍历元素,若发现nums2的元素 在nums_set里又出现过,则将该元素添加到result_set集合中;
- 由于函数返回的是vector类型数组,因此要将result_set集合转为vector数组。
要点
集合和数组的转换写法:
集合转数组:
vector<int>(result_set.begin(), result_set.end());数组转集合:
unordered_set<int> nums_set(nums1.begin(), nums1.end());
- 从数组中遍历元素的快捷写法:
for (int num : nums2)//定义num从num2中遍历元素
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());
//定义num从num2中遍历元素
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());
}
};
数组解法——
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
int hash[1005] = {0}; // 默认数值为0
for (int num : nums1) { // nums1中出现的字母在hash数组中做记录
hash[num] = 1;
}
for (int num : nums2) { // nums2中出现话,result记录
if (hash[num] == 1) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
202. 快乐数
思路
把一个数的每次sum结果存入到一个set中,如果这个sum在set中出现过,则表示不是快乐数,如果sum=1,则是快乐数。判断sum是否重复出现就可以使用unordered_set。
关键点:
- 如果不是快乐数,会无限循环,也就是说求和的过程中,sum会重复出现;
- 取一个数的每个位上的数:
n%10:取n个位上的数值。n/=10:舍去n的个位。
end():返回一个指向当前set末尾元素的下一位置的迭代器。
class Solution {
public:
int getSum(int n){
int sum = 0;
//取n的各个位上的数,并进行平方求和
while(n){
sum += (n%10) * (n%10);//取个位求平方
n/=10; //舍去个位
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> sum_set;
while(1){
int sum = getSum(n);
if(sum==1){
return true;
}
// 如果这个sum曾经出现过(还没有结束就找到了sum),说明已经陷入了无限循环了,立刻return false
if (sum_set.find(sum) != sum_set.end()) {
return false;
}
else {
sum_set.insert(sum);
}
//把得到的sum作为下一次的n
n = sum;
}
}
};
1.两数之和
自己的方法——暴力方法
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//存储结果的数组
vector<int> result(2,0);
//双for进行遍历相加
for(int i=0;i<nums.size();i++){
for(int j=i+1;j<nums.size();j++){
if((nums[i]+nums[j]) == target){
result[0] = i;
result[1] = j;
}
}
}
return result;
}
};
假设数组元素有重复——哈希法
思路:
- 定义
unordered_map(key:value)存储遍历过的(元素值:对应下标),map最开始为空。 - 遍历整个数组,用
s=target-nums[i]得到另一个使得与nums[i]相加等于target的元素s。 - 判断这个元素s是否在map里面出现过,如果出现过,则已经找到两个相加等于target的元素以及它们的下标并返回即可;如果没有出现过则把这个
nums[i]以及它的下标i存入map中。注意往map中存键值对要使用pair<T,T>(key,value)方法。 - 之后继续上两步的循环即可。
- 若循环完都没找到,返回空。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//定义unordered_map(key:value)存储遍历过的(元素值:对应下标)。
unordered_map<int,int> map;
//遍历整个数组,用s=target-nums[i]得到另一个使得与nums[i]相加等于target的元素s
for(int i=0;i<nums.size();i++){
int s = target - nums[i];
//判断这个元素s是否在map里面出现过
//在map里面找到了,则已经找到两个相加等于target的元素以及它们的下标,返回即可
if(map.find(s)!=map.end()){
//map.find(s)得到的是一个迭代器,我们要取下标,所以取second
return {map.find(s)->second,i};
}
//没找到,则把这个nums[i]以及它的下标i存入map中
map.insert(pair<int,int>(nums[i],i));
}
//循环完都没找到,返回空
return {};
}
};
总结
今日学习时长3.5h+30min