哈希表(散列表)介绍及实现
哈希表是一种高效的数据结构 不仅能够实现数据去重的功能 而且增删查操作的平均时间复杂度均为O(1) 在算法中我们经常可以用到
本文从0开始介绍哈希表的各种概念知识 并用开放定址法和链地址法实现哈希表
目录
哈希的概念
一些基本概念
哈希函数
除法散列法/除留余数法
乘法散列法
全域散列法
解决哈希冲突的方法
开放定址法
1.线性探测
实现
查找和删除的实现
insert实现
解决扩容后不是质数的问题
对非整形的解决
2.二次探测
3.双重散列(了解)
链地址法
介绍
实现
基本结构
接下来实现一下插入
insert的扩容
接下来实现find和erase
Hash.h 完整代码
哈希的概念
哈希,也称作散列,其名称源于英文单词“hash”的发音。通过哈希函数,在存储的值与其对应的存储位置之间构建起一种映射关系。在执行查找操作时,只需利用这一哈希函数计算出目标值的存储位置,即可实现快速定位与查找。
直接定址法
当关键字的范围比较集中时,直接定址法就是非常简单高效的方法
和之前的计数排序方式思路一样
比如有0~99大小的数据 我们就可以开一个100个空间的数组 每一个数据存在对应下标的位置 查找时候就可以直接通过下标来找该数据的次数
如果有100~200范围内容的数据 我们不需要开201个空间 只需要开101个空间 让100映射0下标的位置 200映射100下标的位置即可
但是这种对应下标的方式容易造成大量空间的浪费
例如有三个数据101 105 100001 此时也需要开100001-100+1个空间 这样就造成了大量空间的浪费
所以这里用一个新的方法来处理映射的关系 让每一个数据对一个值进行取模 取模后的结果作为它的下标
例如刚刚的101 105 100001 三个数据 让他们对10取模后的结果作为他们映射的下标位置 如下就只需要11个空间就完成了 但是这样可能出现一个新的问题
101和100001都在下标为1的位置了 这样的问题叫做哈希冲突
接下来先来了解几个概念帮助我们理解
一些基本概念
哈希冲突
两个不同的key可能会映射到同⼀个位置 这种问题我们叫做哈希冲突或者哈希碰撞。就像刚刚101和100001同时映射到了下标为1的位置
我们期望找出⼀个好的方法来避免哈希冲突 但是实际上 冲突是不可避免的 所以我们需要想出一种好的方法来尽量减少哈希冲突
哈希函数
上面我们提到的减少哈希冲突的方法其实就是哈希函数 ⼀个好的哈希函数应该让N个数据被均匀的散列分布到哈希表的M个空间中
负载因子
假设哈希表中已经映射存储了N个值,哈希表的大小为M,那么负载因⼦ =N/M 负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;
将关键字转为整数
如果是整数我们可以正常地进行映射 那么如果是小数或者字符串型我们又该怎样映射呢
我们需要找到一种方式先把小数或者字符来通过一种映射方式来转换为整数 然后把它当做整数进行二次映射处理 (这里先简单提一下 之后再解释)
如前所述,在设计哈希表时,选择高性能的哈希函数至关重要,其核心目标在于最小化哈希冲突的发生概率。接下来就来学习一下好的哈希函数
哈希函数
除法散列法/除留余数法
除法散列法也叫做除留余数法 其实就是上面提到的将key对一个值取余数 然后将结果作为下标的方法 不过这里除的那个值是哈希表的空间大小M 哈希函数为:h(key)=key%M。
对于这个取模的除数M 我们要尽量避免M为2的幂次方 因为key%2^x本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就一定是冲突的 如下面的例子
所以 当使用除法散列法时,建议M取不太接近2的整数次幂的⼀个质数(素数)。
但是 Java的HashMap采⽤除法散列法时M取的就是2的整数次幂做哈希表的大小 这样就不需要取模了 只需要通过位运算来得到结果(计算机处理除法的速度慢 所以除法一般就通过转换为位运算解决) 如下 是对这种方式的处理
除法散列法一般是用的比较多的 接下来的另外两种哈希函数---乘法散列法和全域散列法了解一下就好
乘法散列法
乘法散列法对哈希表大小M没有要求
乘法散列法为 先将key(需要存储的值)乘一个A 然后取结果的小数部分和空间大小M相乘然后再向下取整
这里的A是由我们决定的 在经过大量尝试计算分析后由Knuth确立为A =(根号5-1)/2 = 0.6180339887.... (黄金分割点) 该值是最合适的 最大程度上避免哈希冲突
接下来简单举个例子
例如 在M为1024 key为1234时 A*key = 762.6539420558,取小数部分为0.6539420558
M×这个小数部分=0.6539420558*1024= 669.6366651392 然后向下取整为669 所以key1234映射的位置就是669
全域散列法
全域散列法是一种通过随机选择散列函数来提高散列均匀性、降低冲突概率的技术,尤其适用于对抗恶意输入或需要高安全性的场景
如果存在⼀个恶意的对手,他针对我们提供的散列函数,特意构造出⼀个发生严重冲突的数据集, 让所有关键字全部落入同⼀个位置中。解决方法是给散列函数增加随机性,攻击者就无法找出确 定可以导致最坏情况的数据。这种⽅法叫做全域散列。
每次初始化哈希表时,随机选取全域散列函数组中的⼀个散列函数使⽤,后续增删查 改都固定使⽤这个散列函数,否则每次哈希都是随机选⼀个散列函数,那么插入是⼀个散列函数, 查找⼜是另⼀个散列函数,就会导致找不到插⼊的key了。
刚刚了解了一些哈希函数来尽量减少哈希冲突 但是哈希冲突还是不可避免的会发生 所以我们
需要有一些方法来应对哈希冲突的问题 主要有两种两种⽅法,开放定址法和链地址法。
解决哈希冲突的方法
开放定址法
当⼀个值用哈希函数计算出的位置已经有值了 即发生了哈希冲突 则按照某种规则找到⼀个没有存储数据的位置进⾏存储
这种方法在插入之前需要确保负载因⼦小于1 否则里面已经满了没有位置插入了
这⾥的规则有三种:线性探测、⼆次探测、双重探测
1.线性探测
从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛ 到哈希表尾,则回绕到哈希表头的位置。
如下图插入的例子
这种方法在用find查找一个值的时候 会先从应该映射的下标开始判断该位置的值是否是要找的 如果不是则到下一个位置继续判断 直到找到了这个值 或者该位置为空了---那么就说明没有找到
但是结合删除就会存在下面的问题
解决方法
我们可以通过给每一个位置增加一个表示状态的变量 {存在,删除,空} 在查找的时候遇到存在或者删除状态的位置都会到 下一个位置继续寻找 这样就解决这样的问题了
实现
接下来简单先用线性探测和除数取余法的方式来简单实现一下哈希表(存pair类型的)
先像list那里的节点一样写一个类用来表示里面存的数据及状态
然后创建一个HashTable类 里面的对象为一个vector类型的对象_table 里面存的是刚刚的HasDate类类型 然后用一个_n来表示里面此时映射数据的个数 (vector.size()算不出里面已经映射的数据 因为里面存的数据不是连续分布的)
这里注意 之前提到要对空间T来取余算出映射的位置 但是我们我们不是对capacity取余的 而是对size取余
如果插入位置的状态是存在需要找下一个位置 如果是最后一个位置需要到第一个位置所以需要对更新的位置%_size() 这样最后一个位置的下一个位置就会到第一个位置
初步的插入如下
bool insert(const pair<K, V>& kv)
{size_t hash0 = kv.first % _table.size(); while (_table[hash0]._state == EXIT) //如果该位置的状态是存在则需要找到位置状态是空或者删除才可以进行插入{hash0 = (hash0+1)%_table.size(); //需要取余 如果此时是最后一个位置下一个位置要到第一个位置}//找到一个状态为存在或者删除的位置进行插入_table[hash0]._kv = kv;_table[hash0]._state = EXIT;++_n;return true;
}
这样就已经可以简单使用了如下 他们的储存的位置和我们预期的一样
但是如果数据已经满了即负载因子为1了 此时就没有空间了 所以我们需要扩容
事实上如果负载因子接近了1此时如果要插入一个数据的话 最坏的情况下需要都遍历一遍 所以当负载因子大于等于0.7的时候 我们就进行扩容的操作
查找和删除的实现
HashData<K,V>* Find(const K& key)
{size_t hash0 = key % _table.size();while (_table[hash0]._state != EMPTY) //只要这个位置不是空就一直进行{if (_table[hash0]._state == EXIST && _table[hash0]._kv.first == key){return &_table[hash0];}else{hash0 = (hash0 + 1) % _table.size();}}return nullptr;
}
bool Erase(const K& key)
{HashData<K,V>* pos=Find(key);if (pos) //找到会返回对应位置的指针 否则为空指针 无法删除也{pos->_state = DELETE;return true;}return false;}
查找删除test
此时就能用find来实现不能插入重复数据的功能 让insert更完善
insert实现
bool insert(const pair<K, V>& kv)
{if (Find(kv.first) != nullptr) //这里先不支持插入重复的数据 如果找到该值就不能插入return false;if (_n * 1.0 / _table.size() >= 0.7) //负载因子大于等于0.7进行扩容 这里不是扩capacity 是size{HashTable<K, V> newhas;newhas._table.resize((_table.size() * 2)); //此时这里“扩容”后空间可能变为非质数了//扩容后size改变了 此时需要把原来的数据重新映射到新的vector类型的对象中for (auto& x : _table) //每一个x是一个HasDate类型的对象 直接复用insert{if (x._state == EXIST)newhas.insert(x._kv);}_table.swap(newhas._table);}size_t hash0 = kv.first % _table.size();while (_table[hash0]._state == EXIST) //如果该位置的状态是存在则需要找到位置状态是空或者删除才可以进行插入{hash0 = (hash0 + 1) % _table.size(); //需要取余 如果此时是最后一个位置下一个位置要到第一个位置}//找到一个状态为存在或者删除的位置进行插入_table[hash0]._kv = kv;_table[hash0]._state = EXIST;++_n;return true;
}
之前提到我们要让除数M尽量避免为2n次幂的质数
但是这里"空间"扩容的方式是二倍扩的方式 所以扩之后的size就不是我们要求的质数了
解决扩容后不是质数的问题
c++采取的是下面的这种方式
搞了一个质数表 每一次扩容时候不是临时以某种方式计算出的 而是取质数表中下一个质数来作为新的空间大小
如果这个函数传的是n 就会返回在质数表中大于等于n的下一个质数
例如
在初始化的时候调用这个函数传0 就会初始化空间大小为质数里面第一个53
这里扩容后的空间大小就用这个函数来解决 这个函数传原来空间大小+1就会返回下一个质数
对非整形的解决
回到之前提到的一个问题 此时这个哈希表对于整形是可以使用的 对于可以转换为整形的double float char这些也可以使用 但是对于string或者其他的自定义类型的数据是不可以使用的
这里既要支持整形又要支持string和自定义类型 我们可以要仿函数的方式来解决
提供一个仿函数来把string或者其他自定义类型通过某种映射的方式把他们转换为整形 然后再用这里的映射关系二次映射
这里用string来举例
我们可以用仿函数的方式来解决
但是这种string映射为int的方式是不好的 很容易就发生重复 例如 abc acb bbb他们映射为整形的结果是一模一样的
我们可以用下面这种映射关系把string映射为int 这里的131 就是一个质数 是有人专门研究过的 可以最大程度上减小冲突
这样哈希表就支持string类型的使用了
然后再给第三个模版参数一个缺省值 可以转换为整形的都强转为size_t类型 这样负数也可以支持了 对于浮点型只要是整数部分相同的第一次映射就会相同那么第二次映射的结果也一定是相同的 如果想的话我们也可以用类似string那样专门写一个针对浮点型的仿函数
其实因为string类型的使用很常见 在源码中string类型使用时候不需要传第三个参数 能这样做到是因为在类里面用到了再模版进阶那里学到的偏特化的方式
这样对于可以转换为int类型的或者string类型的这个哈希表都可以正常的使用了 对于自定义类型的Data等类型需要自己手动写仿函数 然后使用时候传第三个参数
这样我们对unordered_map的第三个参数什么时候需要传什么时候不用传就理解了 那么第四个参数又是干嘛的呢
在find的比较逻辑中 我们需要拿key来进行等于的判断 对于内置类型的int string这些可以支持==的比较 但是如果key是自定义类型的对象呢 就不能支持==的比较了 所以如果里面存的是自定义类型的对象 对于第四个参数还需要支持==的重载 传第四个参数
解决如下
之前提到的线性探测可能或导致堆积的问题 使得插入的效率降低 接下来的二次探测可以一定程度减少这样的情况
2.二次探测
从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为 ⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表 尾的位置;
hc(key,i) = hashi = (hash0±i ) % M , i = M {1,2,3,..., }
简易的二次探测 一直向后找 但是 一次+i^2
//线性探测
//size_t hash0 = kv.first % _table.size();
//while (_table[hash0]._state == EXIST) //如果该位置的状态是存在则需要找到位置状态是空或者删除才可以进行插入
//{
// hash0 = (hash0 + 1) % _table.size(); //需要取余 如果此时是最后一个位置下一个位置要到第一个位置
//}
//二次探测
size_t hash0 = kv.first % _table.size();
int i = 1,flag=1;
size_t hashi = hash0;
while (_table[hashi]._state == EXIST) //如果该位置的状态是存在则需要找到位置状态是空或者删除才可以进行插入
{hashi = (hash0 + i*i) % _table.size(); //需要取余 如果此时是最后一个位置下一个位置要到第一个位置++i;
}//找到一个状态为空或者删除的位置进行插入
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
如果要实现先向前i^2再向后i^2也很容易
//二次探测
size_t hash0 = kv.first % _table.size();
int i = 1,flag=1;
size_t hashi = hash0;
while (_table[hashi]._state == EXIST) //如果该位置的状态是存在则需要找到位置状态是空或者删除才可以进行插入
{hashi = (hash0 + i*i*flag) % _table.size(); //需要取余 如果此时是最后一个位置下一个位置要到第一个位置if (hashi < 0){hashi += _table.size(); }if (flag == 1)flag = -1;else{flag = 1;++i;}
}
//找到一个状态为空或者删除的位置进行插入
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
3.双重散列(了解)
第⼀个哈希函数计算出的值发⽣冲突,使⽤第⼆个哈希函数计算出⼀个跟key相关的偏移量值,不 断往后探测,直到寻找到下⼀个没有存储数据的位置为⽌。
h1(key) = hash0 = key % M , hash0位置冲突了 则双重探测公式为 hc(key,i) = hashi = (hash0+ i∗h(key)) % M i ={1,2,3,..., M}
要求 2 2 h(key) < h(key) M h(key) 且 2 {1,2,3,..., M} 和M互为质数 有两种简单的取值⽅法:1Java方式 当M为2整数幂时, 从[0,M-1]任选⼀个奇数;2.C++方式 当M为质数时 , 2 h(key) =key % (M −1) + 1
开放定址法都是通过找空位置占别人位置的方式 线性探测很容易造成堆积 二次探测和双重散列都是为了尽可能减少堆积的问题
接下来看一下更常用的链地址法
链地址法
介绍
链地址法中所有的数据不再直接存储在哈希表中 哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把 这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。
拿这组数据举例{19,30,5,36,13,20,21,12,24,96} 下面是这组数据用除法散列法的方式得到映射关系然后存储后的结果
开放定址法一个位置只能存一个值 负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了,可以⼤于1。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;
stl中 unordered_xxx的最⼤负载因⼦基本控制在1,⼤于1就扩容,我们下⾯实现也使⽤这个⽅式。
简单了解⼀下---- 如果极端场景下,某个桶特别⻓的话我们可以考虑使⽤全域散列法,这样就不容易被针对 了。但是偶然情况下,某个桶很⻓,查找效率很低怎么 办?这⾥在Java8的HashMap中当桶的⻓度超过⼀定阀值(8)时就把链表转换成红⿊树。⼀般情况下, 不断扩容,单个桶很⻓的场景还是⽐较少的,下⾯的实现就不搞这么复杂
实现
基本结构
先搞一个节点类型的HashNode 和一个HashTable
对于HashTable里面的成员变量也可以不用指针数组 而是直接用 vector<list<pair<K,V>>>这样的方式 但是这样对之后封装实现unorder_set和unordered_map就很麻烦了 所以这里还是用指针数组的方式
接下来实现一下插入
如下 插入一个新值的时候 给这个新值创建一个新的节点 然后直接在映射的位置采取头插的方式
如下图 插入一个16 先给这个16创建一个新的节点 然后让16的next指向5节点 然后让映射的位置下标5开始的位置为16这个节点
insert的扩容
当负载因子为1的时候扩容 这里就用类似于开放定址法的方式
不能直接把一连串的值直接转移 因为之前冲突的在新表中不一定冲突了 所以要一个一个数据处理
如果这里使用把节点里面的值一个个取出来然后直接复用insert的逻辑的方法的话 这样有两个问题
①每一个数据都需要重新开新节点 效率很低
②形参新表是局部对象 交换后是旧表在函数结束后自动调用vector的析构函数 但是不能把所有的数据给释放掉 因为里面存的是指针 不能把一连串的全部释放掉 会造成内存泄漏
而如果里面存的是之前提到的 vector<list<pair<K,V>>>这样的话 就会自动调用list的析构函数 把里面全部给析构掉 但使用这种方法还是之前提到的问题 迭代器的实现就很麻烦了
正确的方式来解决
算出旧表数据在新表中应该在的位置 然后一个一个节点来转移 在结束之后旧表vector里面每一个位置都为nullptr了 这样既避免了重复的申请空间开销 也解决了内存泄漏的问题
接下来实现find和erase
查找很简单
删除要注意分为两种情况
①为映射位置的头节点 此时只需要让此时头位置为要删除节点的下一个节点 然后释放该节点
②是中间节点 此时需要找到它的上一个节点 然后让上一个节点指向要删除节点的下一个节点 然后删除该节点
Node* find(K key)
{size_t hashi = key % _table.size();Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){return cur;}elsecur = cur->_next;}return nullptr;
}
bool erase(K key)
{size_t hashi = key % _table.size();Node* cur = _table[hashi];Node* pre = nullptr;while (cur){if (cur->_kv.first == key) //找到了{if (_table[hashi] == cur) // 头节点{_table[hashi] = cur->_next;}else //中间节点{pre->_next= cur->_next;}delete cur;return true;}else{pre = cur;cur = cur->_next;}}return false;
}
此时找到删除都可以按照我们预期使用了
在insert插入之前用find来判断一样 若该数据已经存在则直接返回 插入失败
为了支持除可以转换为int类型的使用 我们同样需要给第三个模版参数 在insert find erase对应地方需要用到这个仿函数 默认为支持K类型能转换为size_t的 然后特化的string类型的 对于其他自定义类型的自己写仿函数 然后传第三个参数
然后和之前一样继续补上第四个模版参数 在需要比较等于的find地方 用这个仿函数
Hash.h 完整代码
namespace open_address
{
enum State
{EXIST,DELETE,EMPTY
};template<class K, class V>
struct HashData //就像链表那里的节点一样 里面存在一个pair类型的数据和状态
{pair<K, V> _kv;State _state = EMPTY;
};
template<class K>
class intoint
{
public:size_t operator()(K x){return (size_t)x;}
};
template<>
class intoint<string>
{
public:int operator()(string s){int add = 0;for (char x : s){add *= 131;add += x;}return add;}
};
template <class K>
class ifsame
{
public:bool operator()(K key1,K key2){return key1 == key2;}
};template<class K, class V, class Hash = intoint<K>, class Hashsame = ifsame<K>>class HashTable{public:HashTable():_table(__stl_next_prime(0)), _n(0){}//bool insert(const pair<K, V>& kv)//{// if (Find(kv.first) != nullptr) //这里先不支持插入重复的数据 如果找到该值就不能插入// return false; // if (_n*1.0 / _table.size() >= 0.7) //负载因子大于等于0.7进行扩容 这里不是扩capacity 是size// {// HashTable<K, V> newhas; // newhas._table.resize((_table.size() * 2)); //此时这里“扩容”后空间可能变为非质数了// //扩容后size改变了 此时需要把原来的数据重新映射到新的vector类型的对象中// for (auto& x : _table) //每一个x是一个HasDate类型的对象 直接复用insert// {// if(x._state==EXIST)// newhas.insert(x._kv);// }// _table.swap(newhas._table);// }// size_t hash0 = kv.first % _table.size(); // while (_table[hash0]._state == EXIST) //如果该位置的状态是存在则需要找到位置状态是空或者删除才可以进行插入// {// hash0 = (hash0+1)%_table.size(); //需要取余 如果此时是最后一个位置下一个位置要到第一个位置// }// //找到一个状态为存在或者删除的位置进行插入// _table[hash0]._kv = kv;// _table[hash0]._state = EXIST;// ++_n;// return true;// //if (_table[hash0]._stale != EXIT) //刚开始映射的位置状态是空或者删除都可以进行插入// //{// // _table[hash0]._kv = kv;// // _table[hash0]._state = EXIT;// //}// //else //线性探测找状态为空或者删除的位置// //{// // int hash1 = (hash0 + 1);// // while(hash1)// //}//}inline unsigned long __stl_next_prime(unsigned long n){// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] = {53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;}bool insert(const pair<K, V>& kv){Hash toint;if (Find(kv.first) != nullptr) //这里先不支持插入重复的数据 如果找到该值就不能插入return false;if (_n * 1.0 / _table.size() >= 0.7) //负载因子大于等于0.7进行扩容 这里不是扩capacity 是size{HashTable<K, V, Hash> newhas;newhas._table.resize(__stl_next_prime(_table.size() + 1)); //此时这里“扩容”后空间可能变为非质数了//扩容后size改变了 此时需要把原来的数据重新映射到新的vector类型的对象中for (auto& x : _table) //每一个x是一个HasDate类型的对象 直接复用insert{if (x._state == EXIST)newhas.insert(x._kv);}_table.swap(newhas._table);}//线性探测//size_t hash0 = kv.first % _table.size();//while (_table[hash0]._state == EXIST) //如果该位置的状态是存在则需要找到位置状态是空或者删除才可以进行插入//{// hash0 = (hash0 + 1) % _table.size(); //需要取余 如果此时是最后一个位置下一个位置要到第一个位置//}//二次探测size_t hash0 = toint(kv.first) % _table.size();int i = 1, flag = 1;size_t hashi = hash0;while (_table[hashi]._state == EXIST) //如果该位置的状态是存在则需要找到位置状态是空或者删除才可以进行插入{hashi = (hash0 + i * i * flag) % _table.size(); //需要取余 如果此时是最后一个位置下一个位置要到第一个位置if (hashi < 0){hashi += _table.size();}if (flag == 1)flag = -1;else{flag = 1;++i;}}//找到一个状态为空或者删除的位置进行插入_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_n;return true;}HashData<K, V>* Find(const K& key){Hash toint;Hashsame ifsa;if (_table.size() == 0)return nullptr;size_t hash0 = toint(key) % _table.size();while (_table[hash0]._state != EMPTY) //只要这个位置不是空就一直进行{if (_table[hash0]._state == EXIST && ifsa(_table[hash0]._kv.first, key)){return &_table[hash0];}else{hash0 = (hash0 + 1) % _table.size();}}return nullptr;}bool Erase(const K& key){HashData<K, V>* pos = Find(key);if (pos) //找到会返回对应位置的指针 否则为空指针 无法删除也{pos->_state = DELETE;return true;}return false;}private:vector<HashData<K, V>> _table;size_t _n; //表示里面映射数据的个数 因为数据不是连续分布的 用size算不出里面数据的个数};
}namespace hash_bucket
{template<class K, class V>struct HashNode{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}};template<class K>class intoint{public:size_t operator()(K x){return (size_t)x;}};template<>class intoint<string>{public:int operator()(string s){int add = 0;for (char x : s){add *= 131;add += x;}return add;}};template <class K>class ifsame{public:bool operator()(K key1, K key2){return key1 == key2;}};template<class K, class V,class Hash=intoint<K>,class HashSame=ifsame<K>>class HashTable{public:typedef HashNode<K, V> Node;HashTable():_table(__stl_next_prime(0)),_n(0){}~HashTable(){for (auto x : _table){while (x){Node* next = x->_next;delete x;x = next;}}}inline unsigned long __stl_next_prime(unsigned long n){// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] = {53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;}bool insert(pair<K, V> kv){if (find(kv.first)) //新插入元素已经存在 则插入插入失败return false; Hash toint;if (_n /_table.size()==1) // 负载因子为1 扩容{HashTable<K, V,Hash> newhas;newhas._table.resize(__stl_next_prime(_table.size() + 1)); //扩容后size改变了 此时需要把原来的数据重新映射到新的vector类型的对象中for (auto& x : _table) //每一个x是一个节点指针 直接复用insert{Node* head = x;while (x){ //把旧表每一个值在新表映射的位置算出来 然后直接把节点移动到新表正确的位置Node* next = x->_next;size_t hashi2 = toint(x->_kv.first)%newhas._table.size();x->_next = newhas._table[hashi2];newhas._table[hashi2] = x;x = next;}}_table.swap(newhas._table);}size_t hashi = toint(kv.first) % _table.size();Node* newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;_n++;return true;}Node* find(K key){Hash toint;HashSame ifsa;size_t hashi =toint(key) % _table.size();Node* cur = _table[hashi];while (cur){if (ifsa(cur->_kv.first , key)){return cur;}elsecur = cur->_next;}return nullptr;}bool erase(K key){Hash toint;size_t hashi = toint(key) % _table.size();Node* cur = _table[hashi];Node* pre = nullptr;while (cur){if (cur->_kv.first == key) //找到了{if (_table[hashi] == cur) // 头节点{_table[hashi] = cur->_next;}else //中间节点{pre->_next= cur->_next;}delete cur;return true;}else{pre = cur;cur = cur->_next;}}return false;}private:vector<Node*> _table; // 指针数组size_t _n;};
}