C++-哈希Hash

本期我们来学习哈希

目录

unordered系列关联式容器

unordered_map

unordered_set

性能比较

哈希概念

哈希冲突

哈希函数

哈希冲突解决

闭散列

模拟实现

开散列

模拟实现

全部代码


unordered系列关联式容器

C++98 中, STL 提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 $log_2N$,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11 中, STL 又提供了 4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同

unordered_map

1. unordered_map 是存储 <key, value> 键值对的关联式容器,其允许通过 keys 快速的索引到与其对应的value
2. unordered_map 中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3. 在内部 ,unordered_map 没有对 <kye, value> 按照任何特定的顺序排序 , 为了能在常数范围内
找到 key 所对应的 value unordered_map 将相同哈希值的键值对放在相同的桶中。
4. unordered_map 容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭代方面效率较低。
5. unordered_maps 实现了直接访问操作符 (operator[]) ,它允许使用 key 作为参数直接访问
value
6. 它的迭代器至少是前向迭代器

unordered是无序的意思,其实这里是早期取名时没取好,应该像java一样,这里应该叫hashmap,hashset,以及treemap和treeset,这是历史问题,我们了解即可

 我们首先要知道,set以及map的底层是红黑树,而unorderedmap和unorderedset底层是哈希表,使用它们是非常简单的

我们先简单看看

还有各种接口,它和map几乎相同,区别有以下几点,1.unordered_xxx是单向迭代器,2.unordered_xxx遍历出来不是有序的,3.unordered_xxx的性能更高一些

下面我们简单使用一下,我们先看看set

和set没啥区别,只是输出无序了,也就是说,它可以去重,但不能排序

再简单看看map的,也是一样

unordered_set

相比于set,和unordered_map和map的区别以及用法是一样的,这里就不过多介绍

性能比较

下面我们来比较测试一下unordered和原来的map和set性能差距,下面是测试代码,就是产生一堆随机数,然后插入查找等等,我们首先测插入

const size_t N = 10000;unordered_set<int> us;set<int> s;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; ++i){v.push_back(rand());//v.push_back(rand()+i);//v.push_back(i);}size_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 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 << endl;cout <<"插入数据个数:"<< s.size() << endl;cout <<"插入数据个数:" << us.size() << endl << endl;;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 << endl;*/return 0;

这是1w个数据时,是看不出什么的

再看10w和100w,差距就开始明显了 ,下面我们再把查找删除也加上

因为随机数会产生重复(随机数最多3w个),所以这里我们插入10w,实际上只有3w,插入是unordered快,查找和删除也是更快,整体上优势

 

100w也是,实际上只插入了3w数据

所以我们改用随机数+i,这样就有60w数据了,我们看性能,这时候差距小了很多,也就是说重复数据多的时候,unordered更占优势,重复数据不多时,优势就不明显了

当数据量达到1000w时,unordered甚至没优势了,不过我们发现,find是非常快的

我们再看看,当数据是有序时,插入1000w就是1000w,set更优一点,因为set底层是红黑树, 一直在旋转,高度不会太高,所以效率就会很高

所以世界是没有完美的东西的,大家根据情况选择即可,我们日常中10w量级和百万量级使用更多一点,或者有时候重复数据过多,unordered的效率都是更高的

哈希概念

顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素 时,必须要经过关键码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即 O($log_2 N$) ,搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功
该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称 为哈希表 (Hash Table)( 或者称散列表 )
例如:数据集合 {1 7 6 4 5 9}
哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小。

我们来看看以前我们在查找一个数时使用的方法,首先是暴力查找,我们都知道,过于低效,于是就有人发明了二分查找,效率就提升了很多,但是缺点是需要先排序,而且增删查改不方便,于是就有了平衡搜索树,不过也有缺点,所以就又有人发明了哈希,也叫散列表,它的本质是,存储的值和存储的位置建立出一个对应关系

我们举个例子,我们之前实现过计数排序,这就是一种哈希

比如这里,我们插入15时,用15减去15(最小值),就插入在0的位置,我们查找时也是一样,直接就可以在对应位置上找到,所以哈希的查找要更快一点

再举一个实际的例子,我们在图书馆找书就是一种哈希,比如图书馆的书是按字母顺序存放的,我们要找《西游记》的话,我们需要去x区(西的拼音开头x),到了x区,还会再次划分,我们要在x区里找y区(游的拼音开头y),这也是一种哈希

上面的计数排序也是有缺点的,比如数据很分散时就完了,我们统计字母时还好,只有26个,而我们统计数据时

就像这种,这可就不好玩了,我们上面的计数排序,以及图书馆都是直接定址法

而我们该如何把上面的数据存进去呢?这里要用到除留余数法,我们把数据%20即可,比如15%20就在15的位置,27%20就在7的位置

哈希冲突

但是这种方法会有一个缺陷,叫做哈希冲突/碰撞,不同的值可能会映射到同一个位置,值和位置是多对一的关系

我们可以用线性探测的方法来存储,比如1%20在1的位置,21%20也在1的位置,1的位置已经被1占了,所以我们就把21放在1的后面,如果后面也被占了,我们就继续往后走,直到有位置存放,不过这种方式也有缺陷,会影响别的数据

还有一种方法是哈希桶,我们把同一个位置的数据链接在一起,放在一个链表里,这种方法更主流一点,下面我们看看除了直接定址法和除留余数法还有哪些方法

哈希函数

引起哈希冲突的一个原因可能是: 哈希函数设计不够合理
哈希函数设计原则
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值
域必须在 0 m-1 之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单
常见哈希函数
1. 直接定址法 --( 常用 )
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2. 除留余数法 --( 常用 )
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,
按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
3. 平方取中法 --( 了解 )
假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 227 作为哈希地址;
再比如关键字为 4321 ,对它平方就是 18671041 ,抽取中间的 3 671( 710) 作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4. 折叠法 --( 了解 )
折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5. 随机数法 --( 了解 )
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中
random 为随机数函数。
通常应用于关键字长度不等时采用此法
6. 数学分析法 --( 了解 )
设有 n d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定
相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
列地址。例如: 假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是 相同 的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还 可以对抽取出来的数字进行反转 ( 1234 改成 4321) 、右环位移 ( 1234 改成 4123) 、左环移 位、前两数与后两数叠加 ( 1234 改成 12+34=46) 等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

 下面我们来看解决哈希冲突的方案

哈希冲突解决

解决哈希冲突 两种常见的方法是: 闭散列 开散列

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把 key 存放到冲突位置中的 下一个 空位置中去。 那如何寻找下一个空位置呢?

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入:
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

 插入如上图所示,如果我们要在里面再插入一个15,会一直走,会走到数组结尾还没有位置,那它会绕回到数组的开头,也就是在0位置插入,只有整个数组满的时候才会扩容

我们再看一个问题,假设我们要查找44,我们用44%10得到4,结果在4的位置没有,那就继续往后找,直到找到为止,如果要找的是54,我们会一直找,直到找到空位置停下(注意,不是一圈,如果是一圈就和暴力查找没区别了),在上图中,我们会找到0的位置,然后停止

这里也侧面说明了一个问题,如果这个表太满,那么查找一些值(比如不存在的值)的效率会大幅度下降,所以我们不能让它太满

还有一个问题,如果我们要删除6,我们要直接删除吗?答案是不行的,删除6后应该在这里填充一个值,不然我们查找元素(如44)时,就会卡bug,删除是会影响前后值的,该如何解决呢?我们应该使用一个标记,而且不能用值标记,应该用一个状态,这里我们会有三种状态,存在,空和删除

删除:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索 。比如删除元素 4 ,如果直接删除掉, 44 查找起来可能会受影
响。因此 线性探测采用标记的伪删除法来删除一个元素

模拟实现

enum STATE //状态
{EXIST,EMPTY,DELETE
};template<class K,class V>
struct HashData
{pair<K, V> _kv;STATE _state = EMPTY;
};template<class K,class V>
class HashTable
{
private:vector<HashData<K,V>> _table;size_t _n;
};

 首先我们先写出框架,它的底层结构是一个数组,大家观察可以发现,vector里是有size的,我们还写了一个n,这是为什么呢?这和后续扩容之类有关,是存储有效数据的个数

我们先来实现insert,我们看hashi,这里我们应该%capacity还是size?

答案是size

如果capacity是20,size是15,如果%capacity是可能出现比size大的,比如出现18,下面方括号检测是会报错的(vector只能访问0到size-1)

bool Insert(const pair<K, V>& kv){//线性探测size_t hashi = kv.first % _table.size();while (_table[hashi]._state == EXIST){++hashi;hashi %= _table.size();//防止越界,这样可以回到数组开头}_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_n;return true;}

这是基础版本,while条件是等于存在,如果不存在,或者删除,说明那个位置是空,可以插入,++hashi是防止越界,下面来解决其他问题,比如太满的情况,是需要扩容的

这里引入一个新概念叫载荷因子,也叫负载因子,代表插入元素占空间的比例,满载的程度,负载因子越大,哈希冲突的概率就越大,越小冲突概率就越小,比如100个空间,我们只存了10个数据,我们再插入数据时,冲突的概率是很小的,而当我们插入90个数据后,再插入的话冲突的概率就非常大了,不过负载因子越大,空间利用率也越高,所以也不全是坏处,考虑折中,所以当负载因子在0.7到0.8时扩容是最好的,比如java里是0.75,也就是说空间利用率最大是百分之75

我们写一个构造函数,这样size最开始就不是0了,然后我们来写扩容,扩容完后还有一个问题,扩容后有些值的映射是会改变的,比如原来10个空间,我们插入1和111,它们在1和2的位置,而当我们扩容到20,那么111的位置就应该到11,所以扩容后我们需要重新映射

 我们应该直接开一个新的vector,然后重新插入(不是拷贝,要改变映射)

HashTable(){_table.resize(10);}bool Insert(const pair<K, V>& kv){//扩容//if ((double)_n / _table.size() >= 0.7)if (_n*10 / _table.size() >= 7){size_t newSize = _table.size() * 2;//重新映射HashTable<K, V> newHT;newHT._table.resize(newSize);for (size_t i = 0; i < _table.size(); i++){if (_table[i]._state == EXIST){newHT.Insert(_table[i]._kv);}}_table.swap(newHT._table);}//线性探测size_t hashi = kv.first % _table.size();while (_table[hashi]._state == EXIST){++hashi;hashi %= _table.size();//防止越界,这样可以回到数组开头}_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_n;return true;}

最终是这样的,我们新开一个哈希表,然后把旧表里的数据重新插入进去,接着把新表给旧的即可

下面我们再来实现find

pair<K, V>* Find(const K& key){//线性探测size_t hashi = key % _table.size();while (_table[hashi]._state != EMPTY){if (_table[hashi]._state == EXIST&& _table[hashi]._kv.first == key){return &_table[hashi]._kv;}++hashi;hashi %= _table.size();}return nullptr;}

条件变了一下,遇到空的时候返回false,不是空就继续走,如果遇到要查找的值,首先我们要看看它是否存在

下面实现删除

    bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;--_n;return true;}return false;}

删除就比较简单了,状态改一改即可

此时我们的哈希表还有问题,那就是key是可以被修改的

我们期望是key不能被修改,value可以,下面来解决这个问题

我们在这些地方加上const

 

然后在find的return这里强转一下 

我们的问题还没有全部解决

这里的key是不一定能取模的,如果是string之类的该怎么办呢?

我们增加一个仿函数

然后在这些取模的地方调用,此时还是不能处理string等等的,我们先对整形进行测试

没有问题

接着我们加一个string的

接着我们修改这里,按需实例化

此时就没有问题了

我们还有更好的办法,就是特化,这是特化的使用方法,大家看一下

有了特化,我们不传stringHashFunc也没有问题

其实我们的问题还没有解决,我们上面用string的第一个字母的ASCII码取模,这也是不好的,因为第一个字母相同的就全冲突了

我们把所有的字母加起来,但是这样写还是有一些问题,我们看一看

还是有冲突的

字符串转为整形在实际中还是非常常见的,于是就有大佬对这些进行研究,就有了字符串哈希算法

这里参考大佬整理的

各种字符串Hash函数 - clq - 博客园 (cnblogs.com)

 根据最后的算法比较,我们选择BKDR哈希,AP哈希和DJB哈希这三个得分最高的,一会会说为什么选择3个

BKDR是*131

这样就不冲突了

至于这些算法的原理,我们也不探讨,这是数学方面的知识,如果感兴趣的话可以去看看大佬们当年发表的论文等等

另外这里字符串过长是会溢出的,不过不影响,是会截断的,插入时截断,查找时自然也截断,我们这里不需要算出准确值,只需要对应一下位置而已

我们上面的方法是闭散列的开放定址法,核心思路是线性探测,是有缺陷的,就是容易堵塞,比如我们插入4,5,6,7,后再插入25,44,位置就都被占完了,基于这种情况就有人又想到了二次探测,二次探测是探测二次方,举个例子,我们之前是hashi = key % n,然后hashi + i,二次探测是hashi + i^2,这样相对会分散一点,我们了解一下即可

开放定址法的缺点是冲突会互相影响,为了解决这个问题,就有了开散列,也叫拉链法或者哈希桶

开散列

开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中

 我们是开一个指针数组,然后插入时把它挂起来

就像这样,如果冲突比较多,那么也就是桶比较长而已

其实在库里面也是叫桶的,下面我们来实现它

模拟实现

namespace hash_bucket
{template<class K, class V>struct HashNode{pair<K, V> _kv;HashNode<K, V>* _next;};template<class K,class V>class HashTable{typedef HashNode<K, V> Node;private:vector<Node*> _table; //指针数组size_t _n; //有效数据};
}

一样的,我们先把框架写出来,这里有一个问题,我们在hashitable里使用了vector<Node*>,那么可以用vector<list>吗?答案是可以的,不过没必要,这里使用单链表即可,而且使用list还不容易实现迭代器

        HashTable(){_table.resize(10,nullptr);}bool Insert(const pair<K, V>& kv){size_t hashi = kv.first % _table.size();//头插Node* newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;++_n;return true;}

接着就是构造函数和insert,这里insert的头插看起来很怪,只有两句话,但就是这样的,大家可以画一下,大家想想这里用list的话,就会很麻烦了

另外,大家再想想这里要不要扩容呢?答案是需要的,因为不断插入的话,某些桶就会越来越长,就和链表没有差异了,效率就得不到保证,所以这里也需要一个负载因子,这里的负载因子可以适当放大一点,这里一般控制到1,也就意味着平均下来每个桶一个数据,不过这是理想状况,大多数桶的数据都是常数个,所以效率还是很高的

大家想一想,我们扩容和之前一样,把旧表的数据再插入到新表,这样好吗?

答案是不好的

我们重新插入时,是需要新开一个节点,比如这里的1和111,然后挂上去,而且我们后面还需要把旧表的节点一个一个释放掉 

bool Insert(const pair<K, V>& kv){//负载因子到1时扩容if (_n == _table.size()){size_t newSize = _table.size() * 2;vector<Node*> newTable;newTable.resize(newSize, nullptr);//遍历旧表,把节点迁下来挂到新表for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;//头插到新表size_t hashi = cur->_kv.first % newSize;cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newTable);}size_t hashi = kv.first % _table.size();//头插Node* newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;++_n;return true;}

我们直接把旧表的节点拿下来放到新表即可,这里可能不太好理解,大家最好画图理解一下

        void Print(){for (size_t i = 0; i < _table.size(); i++){printf("[%d]->", i);Node* cur = _table[i];while (cur){cout << cur->_kv.first << "->";cur = cur->_next;}printf("NULL\n");}}

我们再写一个print方便打印测试,这里使用printf更方便一点

 没有问题,我们再插入几个值,看看扩容后怎么样

没有问题 

Node* Find(const K& key){size_t hashi = key % _table.size();Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K& key){size_t hashi = key % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){//头删_table[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}cur = cur->_next;}}

接着我们来实现find和erase,find没什么好说的,erase需要分情况讨论,假设我们找到的key在桶里是唯一的一个元素(桶中只有一个key),那么这里就是头删,否则我们需要把前一个节点的next指向下一个,然后删除即可,写完find后,我们还需要在insert前加上

就像这样

我们再测试一下,也没有问题

大家想一想,我们之前的开放定址法需要写析构函数吗?

答案是不需要的

它是一个vector,里面是一个data,对自定义类型会调用析构

而我们的哈希桶要写析构吗?

答案是需要的

对于内置类型,不做处理,对于自定义类型会调用它的析构,桶的起始位置存的是一个节点的指针,是一个内置类型,所以必须要我们来释放空间

        ~HashTable(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = cur->_next;}_table[i] = nullptr;}}

这是析构函数,下面我们要让哈希桶和开散列一样,可以支持string等等类型

首先我们先把这两个函数放到全局

然后就和之前一样,给所有的取模都加上hf即可

 ​​​​​

修改一下print函数,kv都打印出来,我们测试一下,没有问题

这里保留了几个问题,接下来就是封装了,我们下一期再讲

全部代码

#pragma once
#include<vector>
#include<iostream>
using namespace std;
//Hash.h
template<class K>
struct DefaultHashFunc
{size_t operator()(const K& key){return (size_t)key;}
};template<>
struct DefaultHashFunc<string>
{size_t operator()(const string& str){//BKDRsize_t hash = 0;for (auto ch : str){hash *= 131;hash += ch;}return hash;}
};namespace open_address {enum STATE //状态{EXIST,EMPTY,DELETE};template<class K, class V>struct HashData{pair<K, V> _kv;STATE _state = EMPTY;};//struct stringHashFunc//{//	size_t operator()(const string& str)//	{//		return str[0];//	}//};template<class K, class V, class HashFunc = DefaultHashFunc<K>>class HashTable{public:HashTable(){_table.resize(10);}bool Insert(const pair<K, V>& kv){if(Find(kv.first)){return false;}//扩容//if ((double)_n / _table.size() >= 0.7)if (_n * 10 / _table.size() >= 7){size_t newSize = _table.size() * 2;//重新映射HashTable<K, V, HashFunc> newHT;newHT._table.resize(newSize);for (size_t i = 0; i < _table.size(); i++){if (_table[i]._state == EXIST){newHT.Insert(_table[i]._kv);}}_table.swap(newHT._table);}//线性探测HashFunc hf;size_t hashi = hf(kv.first) % _table.size();while (_table[hashi]._state == EXIST){++hashi;hashi %= _table.size();//防止越界,这样可以回到数组开头}_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_n;return true;}HashData<const K, V>* Find(const K& key){//线性探测HashFunc hf;size_t hashi = hf(key) % _table.size();while (_table[hashi]._state != EMPTY){if (_table[hashi]._state == EXIST&& _table[hashi]._kv.first == key){return (HashData<const K, V>*) & _table[hashi];}++hashi;hashi %= _table.size();}return nullptr;}bool Erase(const K& key){HashData<const K, V>* ret = Find(key);if (ret){ret->_state = DELETE;--_n;return true;}return false;}private:vector<HashData<K, V>> _table;size_t _n; //存储有效数据的个数};
}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 V,class HashFunc = DefaultHashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:HashTable(){_table.resize(10,nullptr);}~HashTable(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = cur->_next;}_table[i] = nullptr;}}bool Insert(const pair<K, V>& kv){if (Find(kv.first)){return false;}HashFunc hf;//负载因子到1时扩容if (_n == _table.size()){size_t newSize = _table.size() * 2;vector<Node*> newTable;newTable.resize(newSize, nullptr);//遍历旧表,把节点迁下来挂到新表for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;//头插到新表size_t hashi = hf(cur->_kv.first) % newSize;cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newTable);}size_t hashi = hf(kv.first) % _table.size();//头插Node* newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;++_n;return true;}Node* Find(const K& key){HashFunc hf;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K& key){HashFunc hf;size_t hashi = hf(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){//头删_table[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}cur = cur->_next;}}void Print(){for (size_t i = 0; i < _table.size(); i++){printf("[%d]->", i);Node* cur = _table[i];while (cur){cout << cur->_kv.first << ":" <<cur->_kv.second <<"->";cur = cur->_next;}printf("NULL\n");}cout << endl;}private:vector<Node*> _table; //指针数组size_t _n = 0; //有效数据};
}

以上即为本期全部内容,希望大家可以有所收获

如有错误,还请指正

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/146337.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【算法基础】一文掌握十大排序算法,冒泡排序、插入排序、选择排序、归并排序、计数排序、基数排序、希尔排序和堆排序

目录 1 冒泡排序&#xff08;Bubble Sort&#xff09; 2 插入排序&#xff08;Insertion Sort&#xff09; 3 选择排序&#xff08;Selection Sort&#xff09; 4. 快速排序&#xff08;Quick Sort&#xff09; 5. 归并排序&#xff08;Merge Sort&#xff09; 6 堆排序 …

【day10.01】使用select实现服务器并发

用select实现服务器并发&#xff1a; linuxlinux:~/study/1001$ cat server.c #include <myhead.h>#define ERR_MSG(msg) do{\printf("%d\n",__LINE__);\perror(msg);\ }while(0)#define PORT 8880#define IP "192.168.31.38"int main(int argc, c…

11链表-迭代与递归

目录 LeetCode之路——206. 反转链表 分析&#xff1a; 解法一&#xff1a;迭代 解法二&#xff1a;递归 LeetCode之路——206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head […

开绕组电机零序Bakc EMF-based无感控制以及正交锁相环inverse Park-based

前言 最近看论文遇到了基于反Park变换的锁相环&#xff0c;用于从开绕组永磁同步电机零序电压信号中提取转子速度与位置信息&#xff0c;实现无感控制。在此记录 基于零序Back EMF的转子估算 开绕组电机的零序反电动势 e 0 − 3 ω e ψ 0 s i n 3 θ e e_0-3\omega_e\psi_…

SoloX:Android和iOS性能数据的实时采集工具

SoloX&#xff1a;Android和iOS性能数据的实时采集工具 github地址&#xff1a;https://github.com/smart-test-ti/SoloX 最新版本&#xff1a;V2.7.6 一、SoloX简介 SoloX是开源的Android/iOS性能数据的实时采集工具&#xff0c;目前主要功能特点&#xff1a; 无需ROOT/越狱…

直播协议 python 常见直播协议

1. 推流、直播 和 点播分别是什么意思&#xff1f; 推流 主播将本地视频源和音频源推送到云服务器&#xff0c;也被称为“RTMP发布”。 直播 即直接观看主播实时推送过来的音视频数据。 点播 视频源已经事先存储于云服务器之上的音视频文件&#xff0c;观众随时可以观看。 目…

STM32晶振的选择与计算

目录 1、石英晶体特性和型号2、振荡器理论2.1负电阻2.2跨导2.3负阻振荡器原理 3、皮尔斯振荡器设计3.1 皮尔斯振荡器简介3.2反馈电阻器3.3负载电容3.4振荡器跨导3.5驱动电平和外部电阻计算3.5.1计算驱动电平3.5.2另一种驱动电平测量方法3.5.3计算外部电阻 3.6启动时间3.7晶体拉…

八个不可不知的SQL高级方法

结构化查询语言&#xff08;SQL&#xff09;是一种广泛使用的工具&#xff0c;用于管理和操作数据库。基本的SQL查询简单易学&#xff0c;但掌握高级SQL技术可以将您的数据分析和管理能力提升到新的高度。 高级SQL技术是指一系列功能和函数&#xff0c;使您能够对数据执行复杂…

记录:Unity脚本的编写

目录 前言添加脚本到unity编写c#脚本查看效果 前言 在学习软件构造这门课的时候&#xff0c;对unity和c#进行了 一定程度的学习&#xff0c;包括简单的建立地形&#xff0c;添加对象&#xff0c;添加材质等&#xff0c;前不久刚好学习了如何通过c#脚本对模型进行操控&#xff…

uniapp - 微信小程序实现腾讯地图位置标点展示,将指定地点进行标记选点并以一个图片图标展示出来(详细示例源码,一键复制开箱即用)

效果图 在uniapp微信小程序平台端开发,简单快速的实现在地图上进行位置标点功能,使用腾讯地图并进行标点创建和设置(可以自定义标记点的图片)。 你只需要复制代码,改个标记图标和位置即可。

Fiddler Orchestra用户指南:打造高效协同调试利器

引言&#xff1a;今天Fiddler更新到5.0版本后&#xff0c;小酋不经意间晃到了“Fiddler Orchestra”选项卡。爱折腾的小酋赶紧链接到官方用户指南一睹为快&#xff0c;看看这是什么东西&#xff0c;实现了什么新功能。下面是小酋看后做的一个翻译抢先版。 这是了解和设置Fiddl…

《 新手》web前端(axios)后端(java-springboot)对接简解

文章目录 <font color red>1.何为前后端对接?2.对接中关于http的关键点2.1. 请求方法2.2. 请求参数设置简解&#xff1a; 3.对接中的跨域(CROS)问题**为什么后端处理跨域尽量在业务之前进行&#xff1f;**3.总结 1.何为前后端对接? “前后端对接” 是指前端和后端两个…

ElementUI实现增删改功能以及表单验证

目录 前言 BookList.vue action.js 展示效果 前言 本篇还是在之前的基础上&#xff0c;继续完善功能。上一篇完成了数据表格的查询&#xff0c;这一篇完善增删改&#xff0c;以及表单验证。 BookList.vue <template><div class"books" style"pa…

picoctf_2018_can_you_gets_me

picoctf_2018_can_you_gets_me Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)32位&#xff0c;只开了NX 拿到这么大的程序&#xff0c;直接ROPchain看看 #!/usr/bin/env python2# execve …

低代码工作流程管理系统:提升企业运营效率的利器

业务运营状况是否良好&#xff0c;除了人员需要配合以外&#xff0c;真正发挥作用的是背后的工作流程。将重复的工作进行自动化处理&#xff0c;确保这些流程最终指向同一个目标、实现一致的运营结果。而设计和实施不佳的工作流程则产生相反的效果——导致处理时间延长、运营成…

好题分享

1.Problem - G - Codeforces &#xff08;1&#xff09;题意 &#xff08;2&#xff09;思路 因为最多13次&#xff0c;那么不如我们就问13次&#xff0c;然后考虑把每一个位置重新按二进制拆分成一个下标&#xff0c;因为C(13,6) > 1000,因此在数量上是满足得&#xff0c;我…

编程每日一练(多语言实现)基础篇:满足abcd=(ab+cd)^2的数 (增加Go语言实现)

文章目录 一、实例描述二、技术要点三、代码实现3.1 C 语言实现3.2 Python 语言实现3.3 Java 语言实现3.4 JavaScript 语言实现3.5 Go 语言实现 一、实例描述 假设 abcd 是一个四位整数&#xff0c;将它分成两段&#xff0c;即 ab 和 cd&#xff0c;使之相加求和后再平方。求满…

LeetCode 热题 HOT 100:回溯专题

LeetCode 热题 HOT 100&#xff1a;https://leetcode.cn/problem-list/2cktkvj/ 文章目录 17. 电话号码的字母组合22. 括号生成39. 组合总和46. 全排列补充&#xff1a;47. 全排列 II &#xff08;待优化)78. 子集79. 单词搜索124. 二叉树中的最大路径和200. 岛屿数量437. 路径…

【C++】C++的类型转换

文章目录 1. C语言中的类型转换2. C中的类型转换2.1 static_cast2.2 reinterpret_cast2.3 const_cast2.4 dynamic 1. C语言中的类型转换 在C语言中&#xff0c;经常会出现一种情况&#xff1a;运算符两边的类型不同&#xff0c;或者形参实参类型不匹配&#xff0c;此时就会发生…

工信部:杭州亚运会开幕式首创 5G 超密组网方案,场馆网络无缝覆盖

“工信 V 报”今日发布消息称&#xff0c;工信部经过精心统筹、周密部署&#xff0c;举全系统之力圆满完成了杭州亚运会开幕式各项保障任务。 据介绍&#xff0c;亚运会的指挥调度、安全保卫、通信网络、计时记分、电视转播等系统顺畅运行&#xff0c;对无线电安全、信息通信服…