手撕哈希表
引入: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;
}
哈希思想很美好,前辈们虽然没有完美的复刻实现出这种美好的思想,但是也创造出了一种相对与之前更优秀的数据结构!
正所谓:"理想如星辰,虽未摘得,却已在追光途中淬炼出更璀璨的自己。"
相关文章:
手撕哈希表
引入:unordered_set /map是什么? 库里面除开set和map,还有unordered_set 和 unordered_map,区别在于: ①:set和map的底层结构是红黑树,而unordered_set和unordered_map的底层是哈希表 ②&…...
编程题python常用技巧-持续
1.字典 1.1排序 在Python中,要按照字典的值进行排序,可以按照以下步骤操作: 方法说明 获取键值对列表:使用 dict.items() 获取字典的键值对视图。排序键值对:使用 sorted() 函数,并通过 key 参…...
大模型蒸馏技术
提问:请写一篇关于蒸馏大模型的详细解说(论文),要求配有图并不少于8000字。 Deepseek: 大模型蒸馏技术:原理、方法与产业实践 ——基于知识压缩与效率优化的深度解析 目录 引言:大模型时代的…...
深入理解C语言中的整形提升与算术转换
深入理解C语言中的整形提升与算术转换 一.整形提升:概念与原理 在C语言中,整形提升(Integer Promotion)是一个重要但容易被忽视的概念。它指的是在表达式中,任何小于int类型的整型(如char、short…...
企业经营系统分类及功能详解
近年来互联网行业下行,招聘少,要求离谱,年龄学历背景已经卡的死死的,技术再突出也没用。 但对于软件开发来说,互联网只是一小部分,企业级系统软件开发,虽然不如互联网大起大落,但重…...
IRF2.0IRF3.1
1、IRF3定义 IRF3是一种能够提高网络接入层的接入能力和管理效率的纵向网络整合虚拟化技术,采用IEEE 802.1BR标准协议实现。IRF3将多台PEX设备(Bridge Port Extender)连接到父设备(Parent device)上,将每台…...
【C++】类和对象【中下】
目录 一、类与对象1、运算符重载1.2 赋值运算符重载1.3 <<运算符和>>运算符1.4 前置与后置 2、 const成员函数3、取地址运算符重载 个人主页<—请点击 C专栏<—请点击 一、类与对象 本期的主题是一步步完善日期类的编写,将要讲解的知识融入在代…...
ThreadLocal详解
什么是 ThreadLocal? ThreadLocal 是 Java 中的一个工具类,用于为每个线程提供独立的变量副本,使得每个线程可以独立操作自己的变量,避免多线程环境下的数据竞争问题。它的核心思想是线程封闭(Thread Confi…...
Vue3 + OpenLayers 企业级应用进阶
1. 企业级架构设计 1.1 微前端架构集成 // src/micro-frontend/map-container.ts import { Map } from ol; import { registerMicroApps, start } from qiankun;export class MapMicroFrontend {private map: Map;private apps: any[];constructor(map: Map) {this.map map;…...
如何提升自我执行力?
提升个人执行力是一个系统性工程,需要从目标管理、习惯养成、心理调节等多方面入手。 以下是具体方法,结合心理学和行为科学原理,帮助你有效提升执行力: 一、明确目标:解决「方向模糊」问题 1. 用SMART原则设定目标 …...
L3-041 影响力
下面给出基于“切比雪夫距离”(Chebyshev 距离)之和的高效 O(nm) 解法。核心思想是把 ∑ u 1 n ∑ v 1 m max ( ∣ u − i ∣ , ∣ v − j ∣ ) \sum_{u1}^n\sum_{v1}^m\max\bigl(|u-i|,|v-j|\bigr) u1∑nv1∑mmax(∣u−i∣,∣v−j∣) 拆成两个…...
【ESP32】st7735s + LVGL使用-------图片显示
【ESP32】st7735s + LVGL使用-------图片显示 1、文件准备2、工程搭建3、代码编写4、应用部分5、函数调用6、显示效果移植部分参考这个博客: 【ESP32】st7735s + LVGL移植 1、文件准备 本次图片放在内部存储,先使用转换工具将要显示的图片转换好。 文件名保存为xx.c,xx这…...
MERGE存储引擎(介绍,操作),FEDERATED存储引擎(介绍,操作),不同存储引擎的特性图
目录 MERGE存储引擎(合并) 介绍 创建表 语法 示例 查看.mrg文件 操作 查询结果 示例 重建逻辑表 FEDERATED存储引擎 结盟 介绍 编辑 应用场景 操作 开启 创建表 对本地表进行数据插入 EXAMPLE存储引擎 不同存储引擎的特性编辑 MERGE存储引擎(合并) 介绍…...
初学者如何学习AI问答应用开发范式
本文是根据本人2年大模型应用开发5年小模型开发经验,对AI问答应用的开发过程进行总结。 技术范式 现在超过80%的AI问答是 提示词 大模型, 然后就是RAG 方案,这两种无疑是主流方案。 1、提示词大模型 适合于本身业务不超过大模型的知识范围…...
GESP2024年6月认证C++八级( 第二部分判断题(1-5))
判断题2: #include <iostream> #include <iomanip> using namespace std;int main() {double a 1e308;double b 1e-10;double orig_a a, orig_b b;a a b;b a - b;a a - b;cout << fixed << setprecision(20);cout << "…...
npm命令介绍(Node Package Manager)(Node包管理器)
文章目录 npm命令全解析简介基础命令安装npm(npm -v检插版本)初始化项目(npm init)安装依赖包(npm install xxx、npm i xxx)卸载依赖包(npm uninstall xxx 或 npm uni xxx、npm remove xxx&…...
小刚说C语言刷题—1602总分和平均分
1.题目描述 期末考试成绩出来了,小明同学语文、数学、英语分别考了 x、y、z 分,请编程帮助小明计算一下,他的总分和平均分分别考了多少分? 输入 三个整数 x、y、z 分别代表小明三科考试的成绩。 输出 第 11行有一个整数&…...
python类私有变量
在Python中,要将一个属性定义为类的内部属性(也就是私有属性),通常会在属性名称前加一个下划线(_)或两个下划线(__)。这两种方式有不同的效果: 单下划线(_&a…...
前端如何转后端
前端转后端是完全可行的,特别是你已经掌握了 JavaScript / TypeScript,有一定工程化经验,这对你学习如 Node.js / NestJS 等后端技术非常有利。下面是一条 系统化、实践导向 的路线,帮助你高效完成从前端到后端的转型。 ✅ 一、评…...
数字智慧方案5976丨智慧农业顶层设计建设与运营方案(59页PPT)(文末有下载方式)
详细资料请看本解读文章的最后内容。 资料解读:智慧农业顶层设计建设与运营方案 在现代农业发展进程中,智慧农业成为推动农业转型升级、提升竞争力的关键力量。这份《智慧农业顶层设计建设与运营方案》全面且深入地探讨了智慧农业的建设现状、需求分析、…...
软件工程国考
软件工程-同等学力计算机综合真题及答案 (2004-2014、2017-2024) 2004 年软工 第三部分 软件工程 (共 30 分) 一、单项选择题(每小题 1 分,共 5 分) 软件可用性是指( )…...
linux python3安装
1 安装依赖环境 yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel 2 mkdir -p /usr/python3 3 cd usr/python3; tar -zxvf Python-3.8.3.tgz;cd Python-3.8.3 4 ./confi…...
软件测评中心如何保障软件质量与性能?评测范围和标准有哪些?
软件测评中心对保障软件质量与性能有关键作用,它像软件世界里的质量卫士,会评测各类软件,能为用户选出真正优质好用的软件,我将从多个方面向大家介绍软件测评中心。 评测范围 软件测评中心的评测范围很广,它涵盖了常…...
从MCP基础到FastMCP实战应用
MCP(https://github.com/modelcontextprotocol) MCP(模型上下文协议) 是一种专为 基于LLM的工具调用外部工具而设计的协议 , 本质上是 LLM ↔ 工具之间的RPC(远程过程调用) 的一种安全且一致的处理方式, 是…...
【云备份】服务端工具类实现
1.文件实用工具类设计 不管是客户端还是服务端,文件的传输备份都涉及到文件的读写,包括数据管理信息的持久化也是如此,因此首先设 计封装文件操作类,这个类封装完毕之后,则在任意模块中对文件进行操作时都将变的简单化…...
如何在Cursor中使用MCP服务
前言 随着AI编程助手的普及,越来越多开发者选择在Cursor等智能IDE中进行高效开发。Cursor不仅支持代码补全、智能搜索,还能通过MCP(Multi-Cloud Platform)服务,轻松调用如高德地图API、数据库等多种外部服务ÿ…...
PB的框架advgui反编译后控件无法绘制的处理(即导入pbx的操作步骤)
advguiobjects.pbl反编译后,涉及到里面一个用pbni开发的一个绘制对象需要重新导入才可以。否则是黑色的无法绘制控件: 对象的位置在: 操作: 导入pbx文件中的对象。 恢复正常: 文章来源:PB的框架advgui反编译…...
第 11 届蓝桥杯 C++ 青少组中 / 高级组省赛 2020 年真题,选择题详细解释
一、选择题 第 2 题 在二维数组按行优先存储的情况下,元素 a[i][j] 前的元素个数计算如下: 1. **前面的完整行**:共有 i 行,每行 n 个元素,总计 i * n 个元素。 2. **当前行的前面元素**:在行内&#x…...
Python 装饰器基础知识科普
装饰器定义与基本原理 装饰器本质上是一个可调用的对象,它接收另一个函数(即被装饰的函数)作为参数。装饰器可以对被装饰的函数进行处理,之后返回该函数,也可以将其替换为另一个函数或可调用对象。 代码示例理解 有…...
数字基带信号和频带信号的区别解析
数字基带信号和数字频带信号是通信系统中两种不同的信号形式,它们的核心区别在于是否经过调制以及适用的传输场景。以下是两者的主要区别和分析: 1. 定义与核心区别 数字基带信号(Digital Baseband Signal) 未经调制的原始数字信号…...
Nginx Proxy Manager 中文版安装部署
目录 Nginx Proxy Manager 中文版安装部署教程一、项目简介1.1 主要功能特点1.2 项目地址1.3 系统架构与工作原理1.4 适用场景 二、系统要求2.1 硬件要求2.2 软件要求 三、Docker环境部署3.1 CentOS系统安装Docker3.2 Ubuntu系统安装Docker3.3 安装Docker Compose 四、安装Ngin…...
类和对象(拷贝构造和运算符重载)下
类和对象(拷贝构造和运算符重载)下 这一集的主要目标先是接着上一集讲完日期类。然后再讲一些别的运算符的重载,和一些语法点。 这里我把这一集要用的代码先放出来:(大家拷一份代码放在编译器上先) Date.h #include <iostream> #include <cassert> …...
Codeforces Round 1008 (Div. 2) C
C 构造 题意:a的数据范围大,b的数据范围小,要求所有的a不同,考虑让丢失的那个a最大即可。问题变成:构造一个最大的a[i] 思路:令a2是最大的,将a1,a3,a5....a2*n1,置为最大的b,将a4,a…...
操作系统(1)多线程
在当今计算机科学领域,多线程技术已成为提高程序性能和响应能力的关键手段。无论是高性能计算、Web服务器还是图形用户界面应用程序,多线程都发挥着不可替代的作用。本文将全面介绍操作系统多线程的概念、实现原理、同步机制以及实际应用场景,…...
系统架构设计师:设计模式——创建型设计模式
一、创建型设计模式 创建型模式抽象了实例化过程,它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。 随着系统演化得越来越依赖于对象复合而不是类…...
使用Set和Map解题思路
前言 Set和Map这两种数据结构,在解决一些题上,效率很高。跟大家简单分享一些题以及如何使用Set和Map去解决这些题目。 题目链接 136. 只出现一次的数字 - 力扣(LeetCode) 138. 随机链表的复制 - 力扣(LeetCode) 旧…...
Java 算法入门:从基础概念到实战示例
在计算机科学领域,算法如同魔法咒语,能够将无序的数据转化为有价值的信息。对于 Java 开发者而言,掌握算法不仅是提升编程能力的关键,更是解决复杂问题的核心武器。本文将带领你走进 Java 算法的世界,从基础概念入手&a…...
【大模型】图像生成:ESRGAN:增强型超分辨率生成对抗网络的革命性突破
深度解析ESRGAN:增强型超分辨率生成对抗网络的革命性突破 技术演进与架构创新核心改进亮点 环境配置与快速入门硬件要求安装步骤 实战全流程解析1. 单图像超分辨率重建2. 自定义数据集训练3. 视频超分处理 核心技术深度解析1. 残差密集块(RRDB࿰…...
记录搭建自己的应用中心-需求看板搭建
记录搭建自己的应用中心-需求看板搭建 人员管理新增用户组织用户登录和操作看板状态看板任务通知任务详情 人员管理 由于不是所有人都有应用管理权限,所以额外做了一套应用登录权限,做了一个新的组织人员表,一个登录账户下的所有应用人员共享…...
探秘数据结构:构建高效算法的灵魂密码
摘要 数据结构作为计算机科学的基石,其设计与优化直接影响算法效率、资源利用和系统可靠性。本文系统阐述数据结构的基础理论、分类及其核心操作,涵盖数组、链表、栈、队列、树、图、哈希表与堆等经典类型。深入探讨各结构的应用场景与性能对比…...
多节点监测任务分配方法比较与分析
多监测节点任务分配方法是分布式系统、物联网(IoT)、工业监测等领域的核心技术,其核心目标是在资源受限条件下高效分配任务,以优化系统性能。以下从方法分类、对比分析、应用场景选择及挑战等方面进行系统阐述: 图1 多…...
spring-boot-maven-plugin 将spring打包成单个jar的工作原理
spring-boot-maven-plugin 是 Spring Boot 的 Maven 插件,它的核心功能是将 Spring Boot 项目打包成一个独立的、可执行的 Fat JAR(包含所有依赖的 JAR 包)。以下是它的工作原理详解: 1. 默认 Maven 打包 vs Spring Boot 插件打包…...
盐化行业数字化转型规划详细方案(124页PPT)(文末有下载方式)
资料解读:《盐化行业数字化转型规划详细解决方案》 详细资料请看本解读文章的最后内容。 该文档聚焦盐化行业数字化转型,全面阐述了盐化企业信息化建设的规划方案,涵盖战略、架构、实施计划、风险及效益等多个方面,旨在通过数字化…...
开源革命:从技术共享到产业变革——卓伊凡的开源实践与思考-优雅草卓伊凡
开源革命:从技术共享到产业变革——卓伊凡的开源实践与思考-优雅草卓伊凡 一、开源的本质与行业意义 1.1 开源软件的定义与内涵 当卓伊凡被问及”软件开源是什么”时,他给出了一个生动的比喻:”开源就像将食谱公之于众的面包师,…...
解锁 C++26 的未来:从语言标准演进到实战突破
一、C26 的战略定位与开发进展 C26 的开发已进入功能冻结阶段,预计 2026 年正式发布。作为 C 标准委员会三年一迭代的重要版本,其核心改进聚焦于并发与并行性的深度优化,同时在内存管理、元编程等领域实现重大突破。根据 ISO C 委员会主席 H…...
terraform实现本地加密与解密
在 Terraform 中实现本地加密与解密(不依赖云服务),可以通过 OpenSSL 或 GPG 等本地加密工具配合 External Provider 实现。以下是完整的安全实现方案: 一、基础架构设计 # 文件结构 . ├── secrets │ ├── encrypt.sh …...
黄雀在后:外卖大战新变局,淘宝+饿了么开启电商大零售时代
当所有人以为美团和京东的“口水战”硝烟渐散,外卖大战告一段落时,“螳螂捕蝉,黄雀在后”,淘宝闪购联合饿了么“闪现”外卖战场,外卖烽火再度燃起。 4 月30日,淘宝天猫旗下即时零售业务“小时达”正式升级…...
基本功能学习
一.enum枚举使用 E_SENSOR_REQ_NONE 的定义及用途 在传感器驱动开发或者电源管理模块中,E_SENSOR_REQ_NONE通常被用来表示一种特殊的状态或请求模式。这种状态可能用于指示当前没有活动的传感器请求,或者是默认初始化状态下的一种占位符。 可能的定义…...
59常用控件_QComboBox的使用
目录 代码示例:使用下拉框模拟麦当劳点餐 代码示例:从文件中加载下拉框的选项 QComboBox表示下拉框 核心属性 属性说明currentText当前选中的文本currentIndex当前选中的条目下标。 从 0 开始计算。如果当前没有条目被选中,值为 -1editable是否允许修改…...
卡洛诗西餐的文化破圈之路
在餐饮市场的版图上,西餐曾长期被贴上“高端”“舶来品”“纪念日专属”的标签,直到卡洛诗以高性价比西餐的定位破局,将意大利风情与家庭餐桌无缝衔接。这场从异国符号到家常选择的转型,不仅是商业模式的创新,更是一部…...