文章目录
前言
哈希表(Hash table),也叫散列表,是一种非常重要的数据结构。
从译名来看,有散乱排列的意思。本质就是通过哈希函数把关键字Key跟存储位建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进⾏快速查找。
一 、哈希概念
1.1 直接定址法
当关键字的范围⽐较集中时,直接定址法就是⾮常简单⾼效的⽅法,⽐如⼀组关键字都在[0,99]之间,那么我们开⼀个100个数的数组,每个关键字的值直接就是存储位置的下标。再⽐如⼀组关键字值都在[a,z]的⼩写字⺟,那么我们开⼀个26个数的数组,每个关键字acsii码-a的ascii码就是存储位置的下标。也就是说直接定址法本质就是⽤关键字计算出⼀个绝对位置或者相对位置。
让我们用一个案例题目来理解上面一段话
字符串中的第一个唯一字符——力扣(LeetCode)
class Solution {
public:
int firstUniqChar(string s) {
// 每个字⺟的ascii码-'a'的ascii码作为下标映射到count数组,数组中存储出现的次数
int count[26] = {
0};
// 统计次数
for(auto ch : s)
{
count[ch-'a']++;
}
for(size_t i = 0; i < s.size(); ++i)
{
if(count[s[i]-'a'] == 1)
return i;
}
return -1;
}
};
1.2 哈希冲突
当我们在对上面直接定址法进行操作时,当我们遇到关键字的范围⽐较分散时,就很浪费内存甚⾄内存不够⽤。这也是直接定址法的明显缺点。
假设我们有数据范围是[0,9999]的N个值,我们要映射到⼀个M个空间的数组中(⼀般情况下M>=N)
那么就要借助哈希函数(hash function)hf,关键字key被放到数组的h(key)位置,这⾥要注意的是h(key)计算出的值必须在[0,M)之间。
这⾥存在的⼀个问题就是,两个不同的key可能会映射到同⼀个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的⽅案。
1.3 负载因子
负载因子是用来衡量哈希表已使用空间与总空间的比例。
假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么负载因子就为N/M ,负载因⼦有些地⽅也翻译为载荷因⼦/装载因⼦等,他的英⽂为load factor
1.低负载因子:内存空间可能被浪费,因为哈希表中的许多槽位可能都处于空闲状态。
2.高负载因子:内存利用率较高,但操作效率可能下降。
1.4 将关键字转为整数
我们将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换成整数(目的是为了取模运算)。下⾯哈希函数部分我们讨论时,如果关键字不是整数,那么我们讨论的Key是关键字转换成的整数。(主要是利用仿函数来实现转换)
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
// BKDR
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 131;
}
return hash;
}
};
将上面两个类作为下面的模板即可完成转换操作l
二、哈希函数
⼀个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,但是实际中却 很难做到,但是我们要尽量往这个⽅向去考量设计。
下面的哈希函数中除第一个之外都是仅作了解的。
2.1 除法散列法/除留余数法(常用的一种)
• 除法散列法也叫做除留余数法,顾名思义,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key)=key%M。
• 当使⽤除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是 2的X次方,那么key%(2^X),转换成二进制的话,本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。如:
{63 , 31}看起来没有关联的值,如果M是16,也就是 ,那么计算出的哈希值都是15,因为63的⼆进制后8位是00111111,31的⼆进制后8位是00011111。如果是10的X次方,就更明显了,保留的都是10进值的后x位,如:{112,12312},如果M是100,也就是 ,那么计算出的哈希值都是12。
• 当使⽤除法散列法时,建议M取不太接近2的整数次幂的⼀个质数(素数)。
(本质上让更多的位参与运算,使映射更为均匀分布)
• 需要说明的是,实践中也是⼋仙过海,各显神通,Java的HashMap采⽤除法散列法时就是2的整数次冥做哈希表的⼤⼩M,这样玩的话,就不⽤取模,⽽可以直接位运算,相对⽽⾔位运算⽐模更⾼效⼀些。但是他不是单纯的去取模,⽐如M是2^16次⽅,本质是取后16位,那么⽤key’= key>>16,然后把key和key’异或的结果作为哈希值。也就是说我们映射出的值还是在[0,M)范围内,但是尽量让key所有的位都参与计算,这样映射出的哈希值更均匀⼀些即可。所以我们上⾯建议M取不太接近2的整数次幂的⼀个质数的理论是⼤多数数据结构书籍中写的理论吗,但是实践中,灵活运⽤,抓住本质,⽽不能死读书
仅作了解,本质是让更多的位参与运算,为了使映射出的值更均匀。其中((1<<16-1)==0xffff )=1。
2.2 乘法散列法
• 乘法散列法对哈希表⼤⼩M没有要求,他的⼤思路第⼀步:⽤关键字K乘上常数A(0<A<1),并抽
取出kA的⼩数部分。第⼆步:后再⽤M乘以kA的⼩数部分,再向下取整。
• h(key) = floor(M ×((A ×key)%1.0)),其中floor表⽰对表达式进⾏下取整,A∈(0,1),这⾥
最重要的是A的值应该如何设定,Knuth认为A =( − 5 1)/2 = 0.6180339887… (⻩⾦分割点])⽐较好。
• 乘法散列法对哈希表⼤⼩M是没有要求的,假设M为1024,key为1234,A=0.6180