手撕哈希表
引入:unordered_set /map是什么?
库里面除开set和map,还有unordered_set 和 unordered_map,区别在于:
①:set和map的底层结构是红黑树,而unordered_set和unordered_map的底层是哈希表
②:unordered_set和unordered_map迭代器遍历出来的结果是无序的,这也是为什么前缀是unordered(译为无序 )
一:哈希是什么?
1:理解哈希和哈希表的区别
哈希是一种思想,而哈希表是通过这种思想创建的数据结构
就好比:左子树小于根节点,右子树大于根节点,而红黑树则是基于这种思想构建的一种数据结构
2:哈希的概念
那么哈希是一种怎么样的思想?

3:哈希冲突
既然有哈希冲突,那肯定就有解决的方法~
4:哈希冲突的解决方法
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列:
开散列:
5:哈希表的结构
template<class K, class V, class Hash = HashFunc<K>>class HashTable{public://哈希表的构造函数 默认大小为10HashTable(size_t size = 10){_tables.resize(size);}//.....各种函数private://哈希表需要一个vector和一个反映实际存储的数据个数nvector<HashData<K, V>> _tables;size_t _n = 0; // 实际存储的数据个数};
解释:
这里只用先知道哈希表是在vector的基础上的数据结构 ,外加一个n(实际存储的元素个数)
二:闭散列的实现(开放定址法)
1:闭散列概念
此时再插入15, 本来应该放在下标为5的位置,但是由于被占了(哈希冲突),所以就要放到下一个空位置去,也就是如下所示,放到了下标为6的位置
此时再插入6,由于哈希冲突,所以6放在了7的位置,如下所示:
以上就是闭散列的思路。
2:总结闭散列的思路
插入时:
i = key %表的大小
如果i位置已经有值了,就线性往后走找到空位置,放进去
查找时:
i = key %表的大小
如果i为不是要查找的key就线性往后查找,直到找到或者遇到空(空代表该值不在哈希表内)
注意:
这种行为(当插入或查找数据时,如果哈希函数计算出的目标位置已被占用(发生冲突),就顺序向后探测(一次走一步),直到找到下一个空位置为止。)叫作线性探测~
思路理解起来很简单,但是需要完善很多细节!
3:负载因子
Q①:vector全部插满了,效率会怎么样?
Q②:空位置一定会有吗?
A①②:vector全部插满了,效率严重下降,比如查找一个值,从哈希函数计算得到的下标开始找,结果发现它存放在了该下标的前一个位置,我们的线性探测只能老老实实的找完所有的数据,此时的时间复杂度为O(N);并且插满了空位置就没有了。
所以我们需要一个东西:负载因子
负载因子:插入到哈希表中的元素个数 / 哈希表的长度
比如上图,负载因子就是6/10 = 0.6;
C++中一般将负载因子设置为0.75,没超过0.75代表可以继续插入,反之超过了,则需要扩容
总结:负载因子让哈希表变得高效!
4:状态变量
问题①:假设先删除15 再查找6 会出现找不到6的情况!
因为我们查找6,根据哈希函数得知,该从下标为6的位置开始找,结果6的位置就为空,按照线性探测的规矩,此时就代表找不到了,但是6在哈希表中
问题②:如何表示一个位置,本来就是空和是被删除后的才变成的空
如果问题①中,我们知道了删除15后,下标为6的位置是被删除后才变成的空,那我们就能继续往后找了
综上所述:状态变量即可解决!
vector中的每个位置可能的状态:空,存在,删除
空:代表该位置的空是因为没有值,插入和查找遇到这种状态,会停下
存在:代表该位置存有值
删除:代表该位置是空,该空是因为原本存储的值被删除后形成的,插入和查找遇到这种状态,不会停下
到此,闭散列的思路就讲完了,将其转换为代码即可:
5:仿函数的必要性
我们要将pair类型中的K值,对应一个整形值。然后该整形值,再通过哈希函数,转换得到哈希表中存储的起始下标(叫起始,是因为有可能由于哈希冲突不存储在这里)
Q1:K值不是整形值,而是字符串这种怎么转换成整形?
A1:仿函数解决即可,不同的类型的K值,都可以自己写对应的仿函数来让这个K值得到一个对应的整形值
比如字符串"abcd",我们以一个该字符串的ascll码值的和,去映射一个整形
Q2:这方法不还是会有哈希冲突吗?比如"acbd"和"abcd"映射出的整形一样啊!
A2:哈希冲突是不能避免的,我们只能用一种哈希函数来让哈希冲突大大的降低~
大佬们创建了很多的优秀的哈希函数去降低哈希冲突,这里博主直接介绍最优秀的一种:BKDR!
BKDR哈希函数
BKDRHash 是一种简单高效的字符串哈希算法,由 Brian Kernighan 和 Dennis Ritchie(即 C 语言经典的 K&R 著作作者)提出。它的核心思想是 “种子乘累加”,通过一个不断扩大的种子值(通常取素数)来计算字符串的哈希值,有效减少冲突。
例子:
hash = 0
seed = 31 // 经典种子值,也可用 131, 1313, 13131...
for (char c in str) {hash = hash * seed + c
}
return hash
关键点:
-
seed
的选择:通常取 31、131、1313 等素数,目的是让哈希值分布更均匀。 -
逐字符累加:每个字符的 ASCII 值(或 Unicode 值)都会影响最终的哈希值。
6:闭散列代码及其剖析
1:仿函数的实现
template<class K>
//仿函数 用于得到kv对应的哈希值
//默认的是能直接转换成整形值的 比如int 地址 指针 这种
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//特化
//string比较通用,所以特化
//采取DKBR
//使用循环遍历字符串中的每个字符e。
//将字符的ASCII值加到hash上,然后乘以一个常数131。
template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash += e;hash *= 131;}return hash;}
};
解释:
Q:为什么把K值为字符串的单独拿出来进行特化?
A:因为pair类型的K值为字符串的场景实在是太多,太常见了,所以进行特化;而且库中也是如此做的,所以我们用库中的unordered_set去对K值为字符串的类型进行操作的时候,也不用自己写对应的仿函数,因为库已经对其进行了特化
总结:
我们对能够自动转换为整形的类型和字符串转换为整形的类型进行了实现,第一个仿函数就是面对本身就是int,size_t,指针,地址,这种能直接映射一个整形值的类型
2:状态变量的实现
//表示一个位置的状态enum State{EMPTY,//空EXIST,//存在DELETE//删除};
解释:
使用 enum
定义槽位状态(EMPTY
、EXIST
、DELETE
)的好处:
a:直接用 EXIST
、EMPTY
、DELETE
等命名取代数字(如 0
、1
、2
),避免含义不明确,降低理解成本。
if (state == EMPTY) // 直观
vs
if (state == 0) // 需查文档或注释
b:调试友好
调试时直接显示状态名(如 EXIST
),而非数字,快速定位问题。
3:哈希表的框架
//闭散列 也叫开放定址法//参数除开kv 还有一个仿函数类用于让k值对应一个整形值template<class K, class V, class Hash = HashFunc<K>>class HashTable{public://哈希表的构造函数 默认大小为10HashTable(size_t size = 10){_tables.resize(size);}private://哈希表需要一个vector和一个反映实际存储的数据个数nvector<HashData<K, V>> _tables;size_t _n = 0; // 实际存储的数据个数};
解释:
Q1:哈希表初始化大小为10的好处?
A1:算插入数据的下标的时候 去模上size(),其为0怎么办?hashTable中的构造直接先给10个空间 这样szie不会为0了
Q2:成员变量n的意义?
A2:用于计算负载因子,n/哈希表的大小 == 负载因子
4:查找函数的实现
//查找函数->用于对用户输入的pair类型的值进行查找HashData<K, V>* Find(const K& key){//仿函数实例对象 用于得到kv对应的哈希值Hash hs;// 线性探测size_t hashi = hs(key) % _tables.size();//key值对应的整形模上空间大小,得到了存放 在vector中的初始查找位置的下标值hashi 但是由于线性探测 不一定就在该下标处//只要当前下标位置的状态不是EMPTY,就继续循环。//等于空代表查找失败 返回nullptr//所以从下标到空的前一个 这段区级找到了就是有 找不到就是不存在while (_tables[hashi]._state != EMPTY){//检查当前下标位置的键是否与要查找的键相等,并且状态为EXIST(表示存在有效数据)。if (key == _tables[hashi]._kv.first&& _tables[hashi]._state == EXIST){return &_tables[hashi];//找到了}++hashi;//确保下标值不会超出哈希表的大小,使用模运算使其循环回到起始位置。//让探测位置在超出哈希表末尾时循环回到开头hashi %= _tables.size();}//如果循环结束仍未找到对应的键值对,表示哈希表中不存在该键,返回nullptr。return nullptr;}
解释:
①:Find函数就会用到仿函数了,因为我们函数接受到的K类型的key值, 此时我们不知道K类型是是什么类型,所以我们要用仿函数示例出的对象去触发()的重载,得到其映射的整形值;当然,我们自己测试肯定使用一般的整形或者字符串,因为这两种类型我们写好了对应的仿函数
②:找的时候,我们不仅要找与函数参数中的key吻合的值,且该槽位的状态应该是存在EXIST才行,满足二者,才叫有效数据;因为我们删除的时候,仅仅是将该槽位的状态置为DELETE
Q:为什么删除的时候,仅仅把该槽位的状态置为DELETE,而不是真正的删除置空?
A:这样做的目的是 保持线性探测的连续性(避免因直接置为 EMPTY
导致后续键值对查找中断)。
5:插入函数的实现
//插入函数bool Insert(const pair<K, V>& kv){//在插入之前,首先调用Find函数检查kv是否已经存在于哈希表中。//如果存在,则不需要插入,直接返回false。if (Find(kv.first))return false;//来到这 则代表一定需要插入 先检测是否需要扩容//检查哈希表的负载因子//问题:负载因子为0.7,但是整数相除不可能为小数,也不可能刚好为0.7//以下两个if都可以解决//if ((double)_n / (double)_tables.size() >= 0.7)->强转一个分数的类型为double ,然后>=0.7if (_n * 10 / _tables.size() >= 7)//->某个分数*10 就不用强转了{//创建一个新的哈希表对象newHT 大小是原表的两倍//遍历原哈希表中的所有元素,将状态为EXIST的元素插入到新哈希表中//使用swap函数将新哈希表的表与原哈希表的表交换,实现扩容HashTable<K, V, Hash> newHT(_tables.size() * 2);// 遍历旧表,插入到新表for (auto& e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}_tables.swap(newHT._tables);}//仿函数实例对象 用于得到kv对应的哈希值Hash hs;//线性探测去插入//使用仿函数hs计算键的哈希值,并取模得到初始插入位置hashi。//如果该位置已经存在元素(状态为EXIST),则进行线性探测,即逐个检查后续位置,直到找到空位置或删除位置。size_t hashi = hs(kv.first) % _tables.size();while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size();//用于确保哈希索引 hashi 始终在哈希表的有效范围内}//在找到的空位置或删除位置插入键值对kv。将该位置的状态设置为EXIST。增加哈希表中已存储元素的数量_n。_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}
解释:
①:插入前的检查
在插入之前,首先调用Find函数检查kv是否已经存在于哈希表中;如果存在,则不需要插入,直接返回false。因为我们的unordered_set和set一样,都是有着去重的效果的
②:负载因子计算的问题
Q:我们计算负载因子的时候,假设负载因子为0.7,但是整数相除不可能为小数,也不可能刚好为0.7,那怎么办?
A:两种方法
方法一:强转一个分数的类型为double ,然后>=0.7
方法二:前面个分数*10 就不用强转了(70/10==7)
③:扩容的本质
创建一个新的且是原本大小2倍的哈希表对象newHT(hashtable<k,v> ),然后从旧哈希表对象的_tables中读取元素,若此元素的状态是存在,则复用哈希表类的插入函数去插入到新的哈希表。最后再交换两个哈希表中的vector成员变量!(说白了就是从旧的vector对象中读取数据,插入到新的更大的vector对象)
该方法的好处:因为创建的是哈希表的新对象,所以自己复用自己的插入函数,不用再写具体~
Q:那为什么不单纯创建一个新的更大的vector对象,然后读取旧的到新的,最后再把新的vector对象和哈希表中的vector对象交换
A:因为我们要复用插入函数,不用自己去写去计算每个元素在新表中的位置,所以才采用者方法去创建新的哈希表对象
④:插入的逻辑
使用仿函数对象hs计算键的哈希值,并取模得到初始插入位置hashi;如果该位置已经存在元素(状态为EXIST),则进行线性探测,即逐个检查后续位置,直到找到空位置或删除位置。
6:删除函数的实现
//删除函数bool Erase(const K& key){//调用查找函数Find来查找给定键的键值对。如果找到,Find函数返回指向该键值对的指针;如果未找到,返回nullptr。HashData<K, V>* ret = Find(key);if (ret){ //减少哈希表中已存储元素的数量_n。 更改该槽位的状态 返回true_n--;ret->_state = DELETE;return true;}else{return false;}}
解释:
先Find函数看一下在不在,在则改变槽位的状态即可
7:闭散列总代码
template<class K>
//仿函数 用于得到kv对应的哈希值
//默认的是能直接转换成整形值的 比如int 地址 指针 这种
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//特化
//string比较通用,所以特化
//采取DKBR
//使用循环遍历字符串中的每个字符e。
//将字符的ASCII值加到hash上,然后乘以一个常数131。
template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash += e;hash *= 131;}return hash;}
};namespace open_address
{//表示一个位置的状态enum State{EMPTY,//空EXIST,//存在DELETE//删除};//哈希表中的一个槽位所存储的值 换成链表来说就是一个节点的值//所以除开kv 还应该有状态 初始化为EMPTYtemplate<class K, class V>struct HashData{pair<K, V> _kv;State _state = EMPTY; // 标记};//闭散列 也叫开放定址法//参数除开kv 还有一个仿函数类用于让k值对应一个整形值template<class K, class V, class Hash = HashFunc<K>>class HashTable{public://哈希表的构造函数 默认大小为10HashTable(size_t size = 10){_tables.resize(size);}//查找函数->用于对用户输入的pair类型的值进行查找HashData<K, V>* Find(const K& key){//仿函数实例对象 用于得到kv对应的哈希值Hash hs;// 线性探测size_t hashi = hs(key) % _tables.size();//key值对应的整形模上空间大小,得到了存放在vector中的初始查找位置的下标值hashi 但是由于线性探测 不一定就在该下标处//只要当前下标位置的状态不是EMPTY,就继续循环。//等于空代表查找失败 返回nullptr//所以从下标到空的前一个 这段区级找到了就是有 找不到就是不存在while (_tables[hashi]._state != EMPTY){//检查当前下标位置的键是否与要查找的键相等,并且状态为EXIST(表示存在有效数据)。if (key == _tables[hashi]._kv.first&& _tables[hashi]._state == EXIST){return &_tables[hashi];//找到了}++hashi;//确保下标值不会超出哈希表的大小,使用模运算使其循环回到起始位置。hashi %= _tables.size();}//如果循环结束仍未找到对应的键值对,表示哈希表中不存在该键,返回nullptr。return nullptr;}//插入函数bool Insert(const pair<K, V>& kv){//在插入之前,首先调用Find函数检查kv是否已经存在于哈希表中。//如果存在,则不需要插入,直接返回false。if (Find(kv.first))return false;//来到这 则代表一定需要插入 先检测是否需要扩容//检查哈希表的负载因子//问题:负载因子为0.7,但是整数相除不可能为小数,也不可能刚好为0.7//以下两个if都可以解决//if ((double)_n / (double)_tables.size() >= 0.7)->强转一个分数的类型为double ,然后>=0.7if (_n * 10 / _tables.size() >= 7)//->某个分数*10 就不用强转了{//创建一个新的哈希表对象newHT 大小是原表的两倍//遍历原哈希表中的所有元素,将状态为EXIST的元素插入到新哈希表中//使用swap函数将新哈希表的表与原哈希表的表交换,实现扩容HashTable<K, V, Hash> newHT(_tables.size() * 2);// 遍历旧表,插入到新表for (auto& e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}_tables.swap(newHT._tables);}//仿函数实例对象 用于得到kv对应的哈希值Hash hs;//线性探测去插入//使用仿函数hs计算键的哈希值,并取模得到初始插入位置hashi。//如果该位置已经存在元素(状态为EXIST),则进行线性探测,即逐个检查后续位置,直到找到空位置或删除位置。size_t hashi = hs(kv.first) % _tables.size();while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size();//用于确保哈希索引 hashi 始终在哈希表的有效范围内}//在找到的空位置或删除位置插入键值对kv。将该位置的状态设置为EXIST。增加哈希表中已存储元素的数量_n。_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}//删除函数bool Erase(const K& key){//调用查找函数Find来查找给定键的键值对。如果找到,Find函数返回指向该键值对的指针;如果未找到,返回nullptr。HashData<K, V>* ret = Find(key);if (ret){ //减少哈希表中已存储元素的数量_n。 更改该槽位的状态 返回true_n--;ret->_state = DELETE;return true;}else{return false;}}private://哈希表需要一个vector和一个反映实际存储的数据个数nvector<HashData<K, V>> _tables;size_t _n = 0; // 实际存储的数据个数};
8:闭散列代码的测试
①:测试删除对槽位状态的影响
void TestHT1(){int a[] = { 1,4,24,34,7,44,17,37 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_kv.first << ":E" << endl;}else{cout << ret->_kv.first << ":D" << endl;}}cout << endl;ht.Erase(34);ht.Erase(4);for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_kv.first << ":E" << endl;}else{cout << e << ":D" << endl;}}cout << endl;}
解释:
代码逻辑:
①:
插入使得哈希表中存储的键值对为 {1:1, 4:4, 24:24, 34:34, 7:7, 44:44, 17:17, 37:37}
。
②:
Find(e) 查找键 e,键存在,输出 键:E(E 表示 Exist);键不存在,输出 键:D(D 表示 Delete)。第一次打印所有键都应输出 键:E(因为刚插入,全部存在)
③:
从哈希表中删除键 34
和 4
。后再次查找;如果键是之前删除的(34 或 4),Find(e) 会返回 nullptr(因为状态为 DELETE),输出 键:D。其他键仍存在,输出 键:E。
运行结果:
符合预期~
②:测试插入
//测试字典插入void TestHT2(){HashTable<string, string> dict;dict.Insert(make_pair("sort", "排序"));dict.Insert(make_pair("string", "字符串"));dict.Insert(make_pair("left", "左面"));dict.Insert(make_pair("left", "右面"));}
预期结果:
应该在监视窗口中的dict对象中的下标2是"right",下标3是"string",下标6是"sort",下标7是"left",
运行结果:
一模一样,完美~
注意:
若你的监视窗口不是这样,有可能是size_t在不同平台的编码不同,一个是8字节,一个是4字节;所以哈希计算的值就不一样了
三:开散列的实现(哈希桶)
1:开散列的概念
又叫哈希桶,首先对关键码集合用哈希函数计算哈希地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
如图所示:
2:总结开散列的思路
哈希冲突的时候,我们不再往后放,而是就头插在该下标所对应的链表中;若是查找比如24,通过哈希函数得到下标为4,然后对应的链表里面找就好了
Q1:哈希表的成员变量vector中存储的是什么
A1:存储的节点指针,为nullptr或一个链表的首节点的地址;当某个下标处没有链表的时候,该下标处就存储nullptr,反之存储链表的首节点的地址。
Q2:链表应该选择什么结构?单向双向?带头吗?循环吗?
A2:
选择单向,因为双向优势在于任意位置插入删除,而在这里我们只需头插即可,而不需要任意位置的插入删除;
选择不带头,因为没必要;
选择不循环,因为也没必要;
Q3:为什么插入时头插
A3:因为不必遵循后插入的值就一定要插入到后面,毕竟大家被找的概率又不确定
所以:当我们哈希表是用开散列来实现的时候,我们的成员变量vector如下:
vector<Node*> _tables;
注意:
负载因子和闭散列类似,不再多说;
状态变量在开散列不需要了,因为我们会直接插入到下标处对应的链表;
仿函数也是与闭散列类似,不再多说;
重点是节点类,构造函数,以及插入函数,删除函数,析构函数
3:节点类的实现
//这里叫HashNode 因为值存在链表中的一个个节点中哦~template<class K, class V>struct HashNode{HashNode<K, V>* _next;//指向下一个节点的指针pair<K, V> _kv;//存储的键值对//节点的构造HashNode(const pair<K, V>& kv):_next(nullptr)//next默认为空, _kv(kv){}};
解释:
闭散列每个元素里面存储的是pair和一个状态变量
开散列由于元素存储在链表的节点中,所以元素存储pair和一个指向下一个节点的指针
4:构造函数的实现
HashTable(){_tables.resize(10, nullptr);_n = 0;}
解释:因为vector下标处没链表的时候,应该存储nullptr,所以我们一开始直接默认全是nullptr
5:析构函数的实现
//析构 用于释放vector下面挂着的链表 //外层for循环->遍历vector//内层while循环->遍历每一个桶中的节点~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}
解释:
Q1:为什么闭散列没有实现哈希表的析构函数?
A1:因为闭散列的成员变量中的vector,是一个单纯的vector,vector会自己调用库中的析构函数;而开散列中的vector的某些位置下面挂着一个链表(vector的元素是一个节点指针,指向一个链表的首节点),所以析构函数用于释放vector下面挂着的链表
Q2:链表不是list吗,不是也会自己调用自己的析构函数吗?
A2:这个链表使我们自己实现出来的链表,它的析构函数 不会递归调用下一个节点的析构函数(除非手动实现)。
6:插入函数的实现
//插入bool Insert(const pair<K, V>& kv){//在插入之前,首先调用Find函数检查kv是否已经存在于哈希表中。//如果存在,则不需要插入,直接返回false。if (Find(kv.first))return false;//仿函数实例化Hash hs;// 负载因子到1就扩容if (_n == _tables.size()){//不再是开一个新哈希表对象了,而是一个新的2倍大小的vector//这样效率更高vector<Node*> newTables(_tables.size() * 2, nullptr);//依旧是外层for循环->遍历原来的vector//内层while循环->遍历每个桶内的节点for (size_t i = 0; i < _tables.size(); i++){// 取出旧表中节点,重新计算桶的位置挂到新表桶中Node* cur = _tables[i];while (cur){//保存下一个节点Node* next = cur->_next;// 头插到新表//计算新位置size_t hashi = hs(cur->_kv.first) % newTables.size();//让当前节点的 next 指向新表的桶头cur->_next = newTables[hashi];//把当前节点设为新表的桶头。newTables[hashi] = cur;cur = next;}//清空旧表的桶_tables[i] = nullptr;}//交换两个vector_tables.swap(newTables);}//走到这里代表已经扩容完毕 或者不需要扩容//直接仿函数对象得到哈希值hashisize_t hashi = hs(kv.first) % _tables.size();Node* newnode = new Node(kv);//然后头插即可 切记vector里面放的是节点指针 指向一个桶的首节点newnode->_next = _tables[hashi];_tables[hashi] = newnode;//插入n记得++++_n;return true;}
解释:
开散列的插入逻辑依旧是:
①:插入前的检查
②:是否扩容的判断
③:扩容的本质
④:插入的逻辑
开散列的③需要讲一下,其余和闭散列道理类似
Q1:为什么开散列的实现中的负载因子设置的是1,比闭散列大?应该设置成多少合适?
A1:
一般是设置为1。
2:扩容的本质
和闭散列有所不同,不再使用闭散列的方法:创建一个新的且是原本大小2倍的哈希表对象,然后读取旧对象复用inset函数,最后交换两个对象的vector!
因为:开散列采用的哈希桶方法,按照闭散列方法扩容时候会再开辟一次同样的节点(假设成千上万个节点),空间消耗相当高!;而闭散列的方法,不需要开辟新的东西(只用拷贝int)所以空间消耗低....
所以我们采取的扩容:把原来的节点按照扩容后的映射规则,放进新的vector中,然后再交换新的vector和旧的哈希表对象中的vector;也就是搬运扩容前的节点,而不是开辟新的节点
搬运:重新计算挂到新表对应的桶
代码如下:
// 负载因子到1就扩容if (_n == _tables.size()){//不再是开一个新哈希表对象了,而是一个新的2倍大小的vector//这样效率更高vector<Node*> newTables(_tables.size() * 2, nullptr);//依旧是外层for循环->遍历原来的vector//内层while循环->遍历每个桶内的节点for (size_t i = 0; i < _tables.size(); i++){// 取出旧表中节点,重新计算桶的位置挂到新表桶中Node* cur = _tables[i];while (cur){//保存下一个节点Node* next = cur->_next;// 头插到新表//计算新位置size_t hashi = hs(cur->_kv.first) % newTables.size();//让当前节点的 next 指向新表的桶头cur->_next = newTables[hashi];//把当前节点设为新表的桶头。newTables[hashi] = cur;cur = next;}//清空旧表的桶_tables[i] = nullptr;}//交换两个vector_tables.swap(newTables);}
解释:
//让当前节点的 next 指向新表的桶头cur->_next = newTables[hashi];//把当前节点设为新表的桶头。newTables[hashi] = cur;
①:我们从旧表拿到一个节点cur,此时要让cur头插进新表下标为[hashi]的位置,所以cur的_next应该指向vector下标为[hashi]处的链表的首节点,而vector中下标[hashi]处存储的元素就是一个Node*(链表头节点的指针),指向的就是首节点~
②:然后再把把当前节点设为新表的桶头
③:cur的nect是一个Node*,用[hashi]处的Node*赋值没问题~
7:删除函数的实现
和闭散列不同的是,我们不能再find+delete,因为单向链表删除节点后 无法链接前后节点!
//删除函数//不能像闭散列那样find+delete 因为我们单向链表删除后还要链接删除节点的前后节点//易错:得看前驱是否为空 即需要以防删除的就是首节点bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){// 情况1:删除的是中间或尾节点if (prev)//前驱不为空 {prev->_next = cur->_next;// 前驱节点直接跳过当前节点}//情况2:删除的是头节点else//前驱为空{_tables[hashi] = cur->_next;// 让桶头指向下一个节点}//释放delete cur;//n-1--_n;return true;}//没找到则继续遍历prev = cur;cur = cur->_next;}//遍历结束仍未找到 return false;}
解释:
①:我们随时都是用prev指针,记录前一个节点
②:通过对prev指针是否为空的判断,若为空,代表删除的就是首节点,因为此时prev还未赋值,所以我们单独处理成
_tables[hashi] = cur->_next;// 让桶头指向下一个节点
反之不为空,代表删除是其余的节点
prev->_next = cur->_next;//前驱节点直接跳过当前节点
这样能完美避免空指针的解引用~
8:开散列总代码
template<class K>
//仿函数 用于得到kv对应的哈希值
//默认的是能直接转换成整形值的 比如int 地址 指针 这种
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//特化
//string比较通用,所以特化
//采取DKBR
//使用循环遍历字符串中的每个字符e。
//将字符的ASCII值加到hash上,然后乘以一个常数131。
template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash += e;hash *= 131;}return hash;}
};//这里叫HashNode 因为值存在链表中哦~template<class K, class V>struct HashNode{HashNode<K, V>* _next;//只想下一个节点的指针pair<K, V> _kv;//存储的键值对//节点的构造HashNode(const pair<K, V>& kv):_next(nullptr)//next默认为空, _kv(kv){}};//哈希表类 依旧是一个仿函数用于得到kv对应的哈希值template<class K, class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public://哈希表的构造 默认开存储10个空指针的vector//相比于闭散列的构造 多了置nullptr这一步HashTable(){_tables.resize(10, nullptr);_n = 0;}//析构 用于释放vector下面挂着的链表 //外层for循环->遍历vector//内层while循环->遍历每一个桶中的节点~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}//插入bool Insert(const pair<K, V>& kv){//在插入之前,首先调用Find函数检查kv是否已经存在于哈希表中。//如果存在,则不需要插入,直接返回false。if (Find(kv.first))return false;//仿函数实例化Hash hs;// 负载因子到1就扩容if (_n == _tables.size()){//不再是开一个新哈希表对象了,而是一个新的2倍大小的vector//这样效率更高vector<Node*> newTables(_tables.size() * 2, nullptr);//依旧是外层for循环->遍历原来的vector//内层while循环->遍历每个桶内的节点for (size_t i = 0; i < _tables.size(); i++){// 取出旧表中节点,重新计算桶的位置挂到新表桶中Node* cur = _tables[i];while (cur){//保存下一个节点Node* next = cur->_next;// 头插到新表//计算新位置size_t hashi = hs(cur->_kv.first) % newTables.size();//让当前节点的 next 指向新表的桶头cur->_next = newTables[hashi];//把当前节点设为新表的桶头。newTables[hashi] = cur;cur = next;}//清空旧表的桶_tables[i] = nullptr;}//交换两个vector_tables.swap(newTables);}//走到这里代表已经扩容完毕 或者不需要扩容//直接仿函数对象得到哈希值hashisize_t hashi = hs(kv.first) % _tables.size();Node* newnode = new Node(kv);//然后头插即可 切记vector里面放的是节点指针 指向一个桶的首节点newnode->_next = _tables[hashi];_tables[hashi] = newnode;//插入n记得++++_n;return true;}//查找函数//找到返回该节点的指针 反之nullptrNode* Find(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}//删除函数//不能像闭散列那样find+delete 因为我们单向链表删除后还要链接删除节点的前后节点//易错:得看前驱是否为空 即需要以防删除的就是首节点bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){// 情况1:删除的是中间或尾节点if (prev)//前驱不为空 {prev->_next = cur->_next;// 前驱节点直接跳过当前节点}//情况2:删除的是头节点else//前驱为空{_tables[hashi] = cur->_next;// 让桶头指向下一个节点}//释放delete cur;//n-1--_n;return true;}//没找到则继续遍历prev = cur;cur = cur->_next;}//遍历结束仍未找到 return false;}//测试我们写的哈希桶 测试内容如下/*负载因子(load factor)总桶数量(all bucketSize)非空桶数量(bucketSize)最长链表长度(maxBucketLen)平均链表长度(averageBucketLen)*/void Some(){size_t bucketSize = 0;size_t maxBucketLen = 0;size_t sum = 0;double averageBucketLen = 0;for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){++bucketSize;}size_t bucketLen = 0;while (cur){++bucketLen;cur = cur->_next;}sum += bucketLen;if (bucketLen > maxBucketLen){maxBucketLen = bucketLen;}}averageBucketLen = (double)sum / (double)bucketSize;printf("load factor:%lf\n", (double)_n / _tables.size());printf("all bucketSize:%d\n", _tables.size());printf("bucketSize:%d\n", bucketSize);printf("maxBucketLen:%d\n", maxBucketLen);printf("averageBucketLen:%lf\n\n", averageBucketLen);}private://与闭散列不同的是 vector里面存储的是节点指针了vector<Node*> _tables; // 指针数组size_t _n;};
解释:
其中的some()函数是用来测试的,测试内容如下:
负载因子(load factor)
总桶数量(总的节点的个数)(all bucketSize)
非空桶数量(vector中元素不为nullptr的个数)(bucketSize)
最长链表长度(maxBucketLen)
平均链表长度(averageBucketLen)
9:开散列代码的测试
①:测试开散列的插入删除
//测试自己写的哈希桶的插入删除是否正确void TestHT1(){HashTable<int, int> ht;int a[] = { 1,4,24,34,7,44,17,37 };for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(5, 5));ht.Insert(make_pair(15, 15));ht.Insert(make_pair(25, 25));ht.Erase(5);ht.Erase(15);ht.Erase(25);ht.Erase(35);HashTable<string, string> dict;dict.Insert(make_pair("sort", "排序"));dict.Insert(make_pair("string", "字符串"));}
解释:和预期一致,这个监视窗口不好展示
②:测试some()函数
//测试自己写的哈希桶的Some();void TestHT2(){const size_t N = 30000;HashTable<int, int> ht;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; ++i){//v.push_back(rand()); // N比较大时,重复值比较多v.push_back(rand() + i); // 重复值相对少//v.push_back(i); // 没有重复,有序}// 插入数据到哈希表for (auto e : v){ht.Insert(make_pair(e, e));}ht.Some();}
解释:插入了3万个数据,运行结果如下:
负载因子:0.549
总的桶(总的节点):40960
非空桶(vector中不为空的槽位):21293
最长的桶(最长的链表):2
平均桶长度(平均链表长度):1.05
可以看出,开散列是一个非常优秀的结构,这也是库中的哈希表也是用的开散列的原因
四:总测试
测试自己写的哈希桶 和 库中的哈希桶 和 库中的set
//测试 自己写的哈希桶 和 库中的哈希桶 和 库中的setvoid TestHT3(){const size_t N = 30000;unordered_set<int> us;set<int> s;HashTable<int, int> ht;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; ++i){//v.push_back(rand()); // N比较大时,重复值比较多v.push_back(rand() + i); // 重复值相对少//v.push_back(i); // 没有重复,有序}// 21:15size_t begin1 = clock();for (auto e : v){s.insert(e);}size_t end1 = clock();cout << "set insert:" << end1 - begin1 << endl;size_t begin2 = clock();for (auto e : v){us.insert(e);}size_t end2 = clock();cout << "unordered_set insert:" << end2 - begin2 << endl;size_t begin10 = clock();for (auto e : v){ht.Insert(make_pair(e, e));}size_t end10 = clock();cout << "HashTbale insert:" << end10 - begin10 << endl << endl;size_t begin3 = clock();for (auto e : v){s.find(e);}size_t end3 = clock();cout << "set find:" << end3 - begin3 << endl;size_t begin4 = clock();for (auto e : v){us.find(e);}size_t end4 = clock();cout << "unordered_set find:" << end4 - begin4 << endl;size_t begin11 = clock();for (auto e : v){ht.Find(e);}size_t end11 = clock();cout << "HashTable find:" << end11 - begin11 << endl << endl;cout << "插入数据个数:" << us.size() << endl << endl;ht.Some();size_t begin5 = clock();for (auto e : v){s.erase(e);}size_t end5 = clock();cout << "set erase:" << end5 - begin5 << endl;size_t begin6 = clock();for (auto e : v){us.erase(e);}size_t end6 = clock();cout << "unordered_set erase:" << end6 - begin6 << endl;size_t begin12 = clock();for (auto e : v){ht.Erase(e);}size_t end12 = clock();cout << "HashTable Erase:" << end12 - begin12 << endl << endl;}
运行结果如下:
解释:
Q:库中的unodered_set比库中的set快,能理解,为什么我们自己写的哈希表比这两个都快?
A:我们只是实现了一个非常简略的版本,考虑的因素远远比不上库中的实现,所以显得快。
五:闭/开散列的总代码
①:HashTable.h
#pragma once
#include<iostream>
#include<vector>
#include<unordered_set>
#include<unordered_map>
#include<set>
using namespace std;template<class K>
//仿函数 用于得到kv对应的哈希值
//默认的是能直接转换成整形值的 比如int 地址 指针 这种
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//特化
//string比较通用,所以特化
//采取DKBR
//使用循环遍历字符串中的每个字符e。
//将字符的ASCII值加到hash上,然后乘以一个常数131。
template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash += e;hash *= 131;}return hash;}
};namespace open_address
{//表示一个位置的状态enum State{EMPTY,//空EXIST,//存在DELETEe//删除};//哈希表中的一个槽位所存储的值 换成链表来说就是一个节点的值//所以除开kv 还应该有状态 初始化为EMPTYtemplate<class K, class V>struct HashData{pair<K, V> _kv;State _state = EMPTY; // 标记};//闭散列 也叫开放定址法//参数除开kv 还有一个仿函数类用于让k值对应一个整形值template<class K, class V, class Hash = HashFunc<K>>class HashTable{public://哈希表的构造函数 默认大小为10HashTable(size_t size = 10){_tables.resize(size);}//查找函数->用于对用户输入的pair类型的值进行查找HashData<K, V>* Find(const K& key){//仿函数实例对象 用于得到kv对应的哈希值Hash hs;// 线性探测size_t hashi = hs(key) % _tables.size();//key值对应的整形模上空间大小,得到了存放在vector中的初始查找位置的下标值hashi 但是由于线性探测 不一定就在该下标处//只要当前下标位置的状态不是EMPTY,就继续循环。//等于空代表查找失败 返回nullptr//所以从下标到空的前一个 这段区级找到了就是有 找不到就是不存在while (_tables[hashi]._state != EMPTY){//检查当前下标位置的键是否与要查找的键相等,并且状态为EXIST(表示存在有效数据)。if (key == _tables[hashi]._kv.first&& _tables[hashi]._state == EXIST){return &_tables[hashi];//找到了}++hashi;//确保下标值不会超出哈希表的大小,使用模运算使其循环回到起始位置。hashi %= _tables.size();}//如果循环结束仍未找到对应的键值对,表示哈希表中不存在该键,返回nullptr。return nullptr;}//插入函数bool Insert(const pair<K, V>& kv){//在插入之前,首先调用Find函数检查kv是否已经存在于哈希表中。//如果存在,则不需要插入,直接返回false。if (Find(kv.first))return false;//来到这 则代表一定需要插入 先检测是否需要扩容//检查哈希表的负载因子//问题:负载因子为0.7,但是整数相除不可能为小数,也不可能刚好为0.7//以下两个if都可以解决//if ((double)_n / (double)_tables.size() >= 0.7)->强转一个分数的类型为double ,然后>=0.7if (_n * 10 / _tables.size() >= 7)//->某个分数*10 就不用强转了{//创建一个新的哈希表对象newHT 大小是原表的两倍//遍历原哈希表中的所有元素,将状态为EXIST的元素插入到新哈希表中//使用swap函数将新哈希表的表与原哈希表的表交换,实现扩容HashTable<K, V, Hash> newHT(_tables.size() * 2);// 遍历旧表,插入到新表for (auto& e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}_tables.swap(newHT._tables);}//仿函数实例对象 用于得到kv对应的哈希值Hash hs;//线性探测去插入//使用仿函数hs计算键的哈希值,并取模得到初始插入位置hashi。//如果该位置已经存在元素(状态为EXIST),则进行线性探测,即逐个检查后续位置,直到找到空位置或删除位置。size_t hashi = hs(kv.first) % _tables.size();while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size();//用于确保哈希索引 hashi 始终在哈希表的有效范围内}//在找到的空位置或删除位置插入键值对kv。将该位置的状态设置为EXIST。增加哈希表中已存储元素的数量_n。_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}//删除函数bool Erase(const K& key){//调用查找函数Find来查找给定键的键值对。如果找到,Find函数返回指向该键值对的指针;如果未找到,返回nullptr。HashData<K, V>* ret = Find(key);if (ret){ //减少哈希表中已存储元素的数量_n。 更改该槽位的状态 返回true_n--;ret->_state = DELETEe;return true;}else{return false;}}private://哈希表需要一个vector和一个反映实际存储的数据个数nvector<HashData<K, V>> _tables;size_t _n = 0; // 实际存储的数据个数};//测试删除是否影响槽位的状态void TestHT1(){int a[] = { 1,4,24,34,7,44,17,37 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_kv.first << ":E" << endl;}else{cout << ret->_kv.first << ":D" << endl;}}cout << endl;ht.Erase(34);ht.Erase(4);for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_kv.first << ":E" << endl;}else{cout << e << ":D" << endl;}}cout << endl;}//测试字典插入void TestHT2(){HashTable<string, string> dict;dict.Insert(make_pair("sort", "排序"));dict.Insert(make_pair("string", "字符串"));dict.Insert(make_pair("left", "左面"));dict.Insert(make_pair("right", "右面"));}}//开散列(哈希桶)的实现
namespace hash_bucket
{//这里叫HashNode 因为值存在链表中哦~template<class K, class V>struct HashNode{HashNode<K, V>* _next;//只想下一个节点的指针pair<K, V> _kv;//存储的键值对//节点的构造HashNode(const pair<K, V>& kv):_next(nullptr)//next默认为空, _kv(kv){}};//哈希表类 依旧是一个仿函数用于得到kv对应的哈希值template<class K, class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public://哈希表的构造 默认开存储10个空指针的vector//相比于闭散列的构造 多了置nullptr这一步HashTable(){_tables.resize(10, nullptr);_n = 0;}//析构 用于释放vector下面挂着的链表 //外层for循环->遍历vector//内层while循环->遍历每一个桶中的节点~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}//插入bool Insert(const pair<K, V>& kv){//在插入之前,首先调用Find函数检查kv是否已经存在于哈希表中。//如果存在,则不需要插入,直接返回false。if (Find(kv.first))return false;//仿函数实例化Hash hs;// 负载因子到1就扩容if (_n == _tables.size()){//不再是开一个新哈希表对象了,而是一个新的2倍大小的vector//这样效率更高vector<Node*> newTables(_tables.size() * 2, nullptr);//依旧是外层for循环->遍历原来的vector//内层while循环->遍历每个桶内的节点for (size_t i = 0; i < _tables.size(); i++){// 取出旧表中节点,重新计算桶的位置挂到新表桶中Node* cur = _tables[i];while (cur){//保存下一个节点Node* next = cur->_next;// 头插到新表//计算新位置size_t hashi = hs(cur->_kv.first) % newTables.size();//让当前节点的 next 指向新表的桶头cur->_next = newTables[hashi];//把当前节点设为新表的桶头。newTables[hashi] = cur;cur = next;}//清空旧表的桶_tables[i] = nullptr;}//交换两个vector_tables.swap(newTables);}//走到这里代表已经扩容完毕 或者不需要扩容//直接仿函数对象得到哈希值hashisize_t hashi = hs(kv.first) % _tables.size();Node* newnode = new Node(kv);//然后头插即可 切记vector里面放的是节点指针 指向一个桶的首节点newnode->_next = _tables[hashi];_tables[hashi] = newnode;//插入n记得++++_n;return true;}//查找函数//找到返回该节点的指针 反之nullptrNode* Find(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}//删除函数//不能像闭散列那样find+delete 因为我们单向链表删除后还要链接删除节点的前后节点//易错:得看前驱是否为空 即需要以防删除的就是首节点bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){// 情况1:删除的是中间或尾节点if (prev)//前驱不为空 {prev->_next = cur->_next;// 前驱节点直接跳过当前节点}//情况2:删除的是头节点else//前驱为空{_tables[hashi] = cur->_next;// 让桶头指向下一个节点}//释放delete cur;//n-1--_n;return true;}//没找到则继续遍历prev = cur;cur = cur->_next;}//遍历结束仍未找到 return false;}//测试我们写的哈希桶 测试内容如下/*负载因子(load factor)总桶数量(all bucketSize)非空桶数量(bucketSize)最长链表长度(maxBucketLen)平均链表长度(averageBucketLen)*/void Some(){size_t bucketSize = 0;size_t maxBucketLen = 0;size_t sum = 0;double averageBucketLen = 0;for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){++bucketSize;}size_t bucketLen = 0;while (cur){++bucketLen;cur = cur->_next;}sum += bucketLen;if (bucketLen > maxBucketLen){maxBucketLen = bucketLen;}}averageBucketLen = (double)sum / (double)bucketSize;printf("load factor:%lf\n", (double)_n / _tables.size());printf("all bucketSize:%d\n", _tables.size());printf("bucketSize:%d\n", bucketSize);printf("maxBucketLen:%d\n", maxBucketLen);printf("averageBucketLen:%lf\n\n", averageBucketLen);}private://与闭散列不同的是 vector里面存储的是节点指针了vector<Node*> _tables; // 指针数组size_t _n;};//测试自己写的哈希桶的插入删除是否正确void TestHT1(){HashTable<int, int> ht;int a[] = { 1,4,24,34,7,44,17,37 };for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(5, 5));ht.Insert(make_pair(15, 15));ht.Insert(make_pair(25, 25));ht.Erase(5);ht.Erase(15);ht.Erase(25);ht.Erase(35);HashTable<string, string> dict;dict.Insert(make_pair("sort", "排序"));dict.Insert(make_pair("string", "字符串"));}//测试自己写的哈希桶的Some();void TestHT2(){const size_t N = 30000;HashTable<int, int> ht;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; ++i){//v.push_back(rand()); // N比较大时,重复值比较多v.push_back(rand() + i); // 重复值相对少//v.push_back(i); // 没有重复,有序}// 插入数据到哈希表for (auto e : v){ht.Insert(make_pair(e, e));}ht.Some();}//测试 自己写的哈希桶 和 库中的哈希桶 和 库中的setvoid TestHT3(){const size_t N = 30000;unordered_set<int> us;set<int> s;HashTable<int, int> ht;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; ++i){//v.push_back(rand()); // N比较大时,重复值比较多v.push_back(rand() + i); // 重复值相对少//v.push_back(i); // 没有重复,有序}// 21:15size_t begin1 = clock();for (auto e : v){s.insert(e);}size_t end1 = clock();cout << "set insert:" << end1 - begin1 << endl;size_t begin2 = clock();for (auto e : v){us.insert(e);}size_t end2 = clock();cout << "unordered_set insert:" << end2 - begin2 << endl;size_t begin10 = clock();for (auto e : v){ht.Insert(make_pair(e, e));}size_t end10 = clock();cout << "HashTbale insert:" << end10 - begin10 << endl << endl;size_t begin3 = clock();for (auto e : v){s.find(e);}size_t end3 = clock();cout << "set find:" << end3 - begin3 << endl;size_t begin4 = clock();for (auto e : v){us.find(e);}size_t end4 = clock();cout << "unordered_set find:" << end4 - begin4 << endl;size_t begin11 = clock();for (auto e : v){ht.Find(e);}size_t end11 = clock();cout << "HashTable find:" << end11 - begin11 << endl << endl;cout << "插入数据个数:" << us.size() << endl << endl;ht.Some();size_t begin5 = clock();for (auto e : v){s.erase(e);}size_t end5 = clock();cout << "set erase:" << end5 - begin5 << endl;size_t begin6 = clock();for (auto e : v){us.erase(e);}size_t end6 = clock();cout << "unordered_set erase:" << end6 - begin6 << endl;size_t begin12 = clock();for (auto e : v){ht.Erase(e);}size_t end12 = clock();cout << "HashTable Erase:" << end12 - begin12 << endl << endl;}
}
②:test.h
#define _CRT_SECURE_NO_WARNINGS 1
#include"HashTable(未封装).h"
using namespace std;int main()
{//闭散列的测试// ↓//在闭散列实现中测试删除后对枚举中状态的影响//open_address::TestHT1();//在闭散列实现中测试字典插入//open_address::TestHT2();//哈希桶的测试// ↓ //测试自己写的哈希桶的插入删除是否正确//hash_bucket::TestHT1();//测试 自己写的哈希桶的Some函数 体现桶的平均//hash_bucket::TestHT2();//测试 自己写的哈希桶 和 库中的哈希桶 和 库中的set//hash_bucket::TestHT3();return 0;
}
哈希思想很美好,前辈们虽然没有完美的复刻实现出这种美好的思想,但是也创造出了一种相对与之前更优秀的数据结构!
正所谓:"理想如星辰,虽未摘得,却已在追光途中淬炼出更璀璨的自己。"