目录
哈希的概念
哈希又称散列,是一种组织数据的方式
它的本质是通过哈希函数把关键字key跟存储位置建立一个映射关系,查找时再通过这个哈希函数计算出key存储的位置,进行快速查找
所以它的查找时间复杂度能达到恐怖的O(1)
直接定址法
直接定址法可以是一个26个大小的数组arr[26]来代表26个英文字母,这时候这26个空间就和这26个英文字母建立了映射关系
class Solution {
public:
int firstUniqChar(string s) {
int hash[26];
for (auto str : s)
hash[str - 'a']++;
for (int i = 0; i < s.size(); i++)
if (hash[s[i] - 'a'] == 1)
return i;
return -1;
}
};
哈希冲突
当我们使用直接定址法的时候,当数据映射到同一个位置的时候就把它叫做哈希冲突或者哈希碰撞
哈希冲突是不可避免的
为了减少哈希冲突,我们可以使用一个比较好的哈希函数来减少哈希冲突
负载因子
若哈希表的大小为M,已经映射存储的数据个数为N,那么 负载因子 = N / M
负载因子越大,哈希冲突的概率越高,空间利用率越高
负载因子越小,哈希冲突的概率越低,空间利用率越低
哈希函数
除法散列法/除留余数法
假设哈希表的大小为M
那么通过key除以M的余数作为映射位置的下标
哈希函数为:hashi = key % M
乘法散列法
乘法散列法对哈希表的大小M没有要求
第一步:用关键字key乘上常数A(0 < A < 1),并抽取key*A的小数部分
第二部:后再用M乘以key*A的小数部分,再向下取整
哈希函数为:hashi = floor(M * ((A * key) % 1.0))
这里最重要的是A的值如何设定
Knuth认为A = 0.6180339887......(黄金分割点)比较好
处理哈希冲突
主要有两种方法,开放定址法和链地址法
开放定址法
线性探测
从发生冲突的位置开始,依次线性向后探测,直到寻找到下一个没有存储数据的位置为止
如果走到哈希表尾,则回绕到哈希表头的位置
h(key) = hash0 = key % M,若hash0冲突,则线性探测公式为
hc(key, i) = hashi = (hash0 + i) % M, i = {1, 2, 3..., M - 1}(负载因子小于1,最多探测M-1次,一定能找到一个位置存储)
二次探测
从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下一个没有存储数据的位置为止,如果从右走到哈希表尾,则回绕到哈希表头的位置,如果往左走到哈希表头,则回绕到哈希表尾的位置
h(key) = hash0 = key % M,若hash0冲突,则二次线性探测公式为
hc(key, i) = hashi = (hash0 +/- i^2) % M, i = {1, 2, 3, ....., M / 2}
当hashi < 0时,需要hash += M
双重散列
当第一个哈希函数计算出的值发生冲突,使用第二个哈希函数计算出一个跟key相关的偏移量值,不断往后探测,直到寻找到下一个没有数据的位置为止
h1(key) = hash0 = key % M,hash0位置冲突了,则双重探测公式为
hc(key, i) = hashi = (hash0 + i * h2(key)) % M,i = {1, 2, 3, ....., M}
链地址法
开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中
哈希表里只需要存储一个指针,当没有数据映射这个位置时,指针为空,当有多个数据映射这个位置时,我们把这些冲突的数据链接成一个链表,挂在哈希表当前位置的下面
链地址法也叫做拉链法或者哈希桶
哈希表的实现
哈希表的结构
enum State
{
EXIST,
EMPTY,
DELETE
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
因为我们删除一个值后无法判断这个值是存在还是删除,所以我们可以用一个State状态来标记当前位置的是一个什么状态,所以我们需要枚举出三个状态区分
template<class K&g