• C++-哈希Hash


    本期我们来学习哈希

    目录

    unordered系列关联式容器

    unordered_map

    unordered_set

    性能比较

    哈希概念

    哈希冲突

    哈希函数

    哈希冲突解决

    闭散列

    模拟实现

    开散列

    模拟实现

    全部代码


    unordered系列关联式容器

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

    unordered_map

    1. unordered_map 是存储 键值对的关联式容器,其允许通过 keys 快速的索引到与其对应的value
    2. unordered_map 中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
    键关联。键和映射值的类型可能不同。
    3. 在内部 ,unordered_map 没有对 按照任何特定的顺序排序 , 为了能在常数范围内
    找到 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性能差距,下面是测试代码,就是产生一堆随机数,然后插入查找等等,我们首先测插入

    1. const size_t N = 10000;
    2. unordered_set<int> us;
    3. set<int> s;
    4. vector<int> v;
    5. v.reserve(N);
    6. srand(time(0));
    7. for (size_t i = 0; i < N; ++i)
    8. {
    9. v.push_back(rand());
    10. //v.push_back(rand()+i);
    11. //v.push_back(i);
    12. }
    13. size_t begin1 = clock();
    14. for (auto e : v)
    15. {
    16. s.insert(e);
    17. }
    18. size_t end1 = clock();
    19. cout << "set insert:" << end1 - begin1 << endl;
    20. size_t begin2 = clock();
    21. for (auto e : v)
    22. {
    23. us.insert(e);
    24. }
    25. size_t end2 = clock();
    26. cout << "unordered_set insert:" << end2 - begin2 << endl;
    27. /*size_t begin3 = clock();
    28. for (auto e : v)
    29. {
    30. s.find(e);
    31. }
    32. size_t end3 = clock();
    33. cout << "set find:" << end3 - begin3 << endl;
    34. size_t begin4 = clock();
    35. for (auto e : v)
    36. {
    37. us.find(e);
    38. }
    39. size_t end4 = clock();
    40. cout << "unordered_set find:" << end4 - begin4 << endl << endl;
    41. cout <<"插入数据个数:"<< s.size() << endl;
    42. cout <<"插入数据个数:" << us.size() << endl << endl;;
    43. size_t begin5 = clock();
    44. for (auto e : v)
    45. {
    46. s.erase(e);
    47. }
    48. size_t end5 = clock();
    49. cout << "set erase:" << end5 - begin5 << endl;
    50. size_t begin6 = clock();
    51. for (auto e : v)
    52. {
    53. us.erase(e);
    54. }
    55. size_t end6 = clock();
    56. cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
    57. */
    58. 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 查找起来可能会受影
    响。因此 线性探测采用标记的伪删除法来删除一个元素

    模拟实现

    1. enum STATE //状态
    2. {
    3. EXIST,
    4. EMPTY,
    5. DELETE
    6. };
    7. template<class K,class V>
    8. struct HashData
    9. {
    10. pair _kv;
    11. STATE _state = EMPTY;
    12. };
    13. template<class K,class V>
    14. class HashTable
    15. {
    16. private:
    17. vector> _table;
    18. size_t _n;
    19. };

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

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

    答案是size

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

    1. bool Insert(const pair& kv)
    2. {
    3. //线性探测
    4. size_t hashi = kv.first % _table.size();
    5. while (_table[hashi]._state == EXIST)
    6. {
    7. ++hashi;
    8. hashi %= _table.size();//防止越界,这样可以回到数组开头
    9. }
    10. _table[hashi]._kv = kv;
    11. _table[hashi]._state = EXIST;
    12. ++_n;
    13. return true;
    14. }

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

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

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

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

    1. HashTable()
    2. {
    3. _table.resize(10);
    4. }
    5. bool Insert(const pair& kv)
    6. {
    7. //扩容
    8. //if ((double)_n / _table.size() >= 0.7)
    9. if (_n*10 / _table.size() >= 7)
    10. {
    11. size_t newSize = _table.size() * 2;
    12. //重新映射
    13. HashTable newHT;
    14. newHT._table.resize(newSize);
    15. for (size_t i = 0; i < _table.size(); i++)
    16. {
    17. if (_table[i]._state == EXIST)
    18. {
    19. newHT.Insert(_table[i]._kv);
    20. }
    21. }
    22. _table.swap(newHT._table);
    23. }
    24. //线性探测
    25. size_t hashi = kv.first % _table.size();
    26. while (_table[hashi]._state == EXIST)
    27. {
    28. ++hashi;
    29. hashi %= _table.size();//防止越界,这样可以回到数组开头
    30. }
    31. _table[hashi]._kv = kv;
    32. _table[hashi]._state = EXIST;
    33. ++_n;
    34. return true;
    35. }

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

    下面我们再来实现find

    1. pair* Find(const K& key)
    2. {
    3. //线性探测
    4. size_t hashi = key % _table.size();
    5. while (_table[hashi]._state != EMPTY)
    6. {
    7. if (_table[hashi]._state == EXIST
    8. && _table[hashi]._kv.first == key)
    9. {
    10. return &_table[hashi]._kv;
    11. }
    12. ++hashi;
    13. hashi %= _table.size();
    14. }
    15. return nullptr;
    16. }

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

    下面实现删除

    1. bool Erase(const K& key)
    2. {
    3. HashData* ret = Find(key);
    4. if (ret)
    5. {
    6. ret->_state = DELETE;
    7. --_n;
    8. return true;
    9. }
    10. return false;
    11. }

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

    此时我们的哈希表还有问题,那就是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,这样相对会分散一点,我们了解一下即可

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

    开散列

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

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

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

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

    模拟实现

    1. namespace hash_bucket
    2. {
    3. template<class K, class V>
    4. struct HashNode
    5. {
    6. pair _kv;
    7. HashNode* _next;
    8. };
    9. template<class K,class V>
    10. class HashTable
    11. {
    12. typedef HashNode Node;
    13. private:
    14. vector _table; //指针数组
    15. size_t _n; //有效数据
    16. };
    17. }

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

    1. HashTable()
    2. {
    3. _table.resize(10,nullptr);
    4. }
    5. bool Insert(const pair& kv)
    6. {
    7. size_t hashi = kv.first % _table.size();
    8. //头插
    9. Node* newnode = new Node(kv);
    10. newnode->_next = _table[hashi];
    11. _table[hashi] = newnode;
    12. ++_n;
    13. return true;
    14. }

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

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

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

    答案是不好的

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

    1. bool Insert(const pair& kv)
    2. {
    3. //负载因子到1时扩容
    4. if (_n == _table.size())
    5. {
    6. size_t newSize = _table.size() * 2;
    7. vector newTable;
    8. newTable.resize(newSize, nullptr);
    9. //遍历旧表,把节点迁下来挂到新表
    10. for (size_t i = 0; i < _table.size(); i++)
    11. {
    12. Node* cur = _table[i];
    13. while (cur)
    14. {
    15. Node* next = cur->_next;
    16. //头插到新表
    17. size_t hashi = cur->_kv.first % newSize;
    18. cur->_next = newTable[hashi];
    19. newTable[hashi] = cur;
    20. cur = next;
    21. }
    22. _table[i] = nullptr;
    23. }
    24. _table.swap(newTable);
    25. }
    26. size_t hashi = kv.first % _table.size();
    27. //头插
    28. Node* newnode = new Node(kv);
    29. newnode->_next = _table[hashi];
    30. _table[hashi] = newnode;
    31. ++_n;
    32. return true;
    33. }

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

    1. void Print()
    2. {
    3. for (size_t i = 0; i < _table.size(); i++)
    4. {
    5. printf("[%d]->", i);
    6. Node* cur = _table[i];
    7. while (cur)
    8. {
    9. cout << cur->_kv.first << "->";
    10. cur = cur->_next;
    11. }
    12. printf("NULL\n");
    13. }
    14. }

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

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

    没有问题 

    1. Node* Find(const K& key)
    2. {
    3. size_t hashi = key % _table.size();
    4. Node* cur = _table[hashi];
    5. while (cur)
    6. {
    7. if (cur->_kv.first == key)
    8. {
    9. return cur;
    10. }
    11. cur = cur->_next;
    12. }
    13. return nullptr;
    14. }
    15. bool Erase(const K& key)
    16. {
    17. HashFunc hf;
    18. size_t hashi = hf(key) % _table.size();
    19. Node* prev = nullptr;
    20. Node* cur = _table[hashi];
    21. while (cur)
    22. {
    23. if (cur->_kv.first == key)
    24. {
    25. if (prev == nullptr)
    26. {
    27. _table[hashi] = cur->_next;
    28. }
    29. else
    30. {
    31. prev->_next = cur->_next;
    32. }
    33. delete cur;
    34. return true;
    35. }
    36. prev = cur;
    37. cur = cur->_next;
    38. }
    39. --_n;
    40. return false;
    41. }

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

    就像这样

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

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

    答案是不需要的

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

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

    答案是需要的

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

    1. ~HashTable()
    2. {
    3. for (size_t i = 0; i < _table.size(); i++)
    4. {
    5. Node* cur = _table[i];
    6. while (cur)
    7. {
    8. Node* next = cur->_next;
    9. delete cur;
    10. cur = next;
    11. }
    12. _table[i] = nullptr;
    13. }
    14. }

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

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

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

     ​​​​​

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

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

    全部代码

    1. #pragma once
    2. #include
    3. #include
    4. using namespace std;
    5. //Hash.h
    6. template<class K>
    7. struct DefaultHashFunc
    8. {
    9. size_t operator()(const K& key)
    10. {
    11. return (size_t)key;
    12. }
    13. };
    14. template<>
    15. struct DefaultHashFunc
    16. {
    17. size_t operator()(const string& str)
    18. {
    19. //BKDR
    20. size_t hash = 0;
    21. for (auto ch : str)
    22. {
    23. hash *= 131;
    24. hash += ch;
    25. }
    26. return hash;
    27. }
    28. };
    29. namespace open_address {
    30. enum STATE //状态
    31. {
    32. EXIST,
    33. EMPTY,
    34. DELETE
    35. };
    36. template<class K, class V>
    37. struct HashData
    38. {
    39. pair _kv;
    40. STATE _state = EMPTY;
    41. };
    42. //struct stringHashFunc
    43. //{
    44. // size_t operator()(const string& str)
    45. // {
    46. // return str[0];
    47. // }
    48. //};
    49. template<class K, class V, class HashFunc = DefaultHashFunc>
    50. class HashTable
    51. {
    52. public:
    53. HashTable()
    54. {
    55. _table.resize(10);
    56. }
    57. bool Insert(const pair& kv)
    58. {
    59. if(Find(kv.first))
    60. {
    61. return false;
    62. }
    63. //扩容
    64. //if ((double)_n / _table.size() >= 0.7)
    65. if (_n * 10 / _table.size() >= 7)
    66. {
    67. size_t newSize = _table.size() * 2;
    68. //重新映射
    69. HashTable newHT;
    70. newHT._table.resize(newSize);
    71. for (size_t i = 0; i < _table.size(); i++)
    72. {
    73. if (_table[i]._state == EXIST)
    74. {
    75. newHT.Insert(_table[i]._kv);
    76. }
    77. }
    78. _table.swap(newHT._table);
    79. }
    80. //线性探测
    81. HashFunc hf;
    82. size_t hashi = hf(kv.first) % _table.size();
    83. while (_table[hashi]._state == EXIST)
    84. {
    85. ++hashi;
    86. hashi %= _table.size();//防止越界,这样可以回到数组开头
    87. }
    88. _table[hashi]._kv = kv;
    89. _table[hashi]._state = EXIST;
    90. ++_n;
    91. return true;
    92. }
    93. HashData<const K, V>* Find(const K& key)
    94. {
    95. //线性探测
    96. HashFunc hf;
    97. size_t hashi = hf(key) % _table.size();
    98. while (_table[hashi]._state != EMPTY)
    99. {
    100. if (_table[hashi]._state == EXIST
    101. && _table[hashi]._kv.first == key)
    102. {
    103. return (HashData<const K, V>*) & _table[hashi];
    104. }
    105. ++hashi;
    106. hashi %= _table.size();
    107. }
    108. return nullptr;
    109. }
    110. bool Erase(const K& key)
    111. {
    112. HashData<const K, V>* ret = Find(key);
    113. if (ret)
    114. {
    115. ret->_state = DELETE;
    116. --_n;
    117. return true;
    118. }
    119. return false;
    120. }
    121. private:
    122. vector> _table;
    123. size_t _n; //存储有效数据的个数
    124. };
    125. }
    126. namespace hash_bucket
    127. {
    128. template<class K, class V>
    129. struct HashNode
    130. {
    131. pair _kv;
    132. HashNode* _next;
    133. HashNode(const pair& kv)
    134. :_kv(kv)
    135. ,_next(nullptr)
    136. {}
    137. };
    138. template<class K,class V,class HashFunc = DefaultHashFunc>
    139. class HashTable
    140. {
    141. typedef HashNode Node;
    142. public:
    143. HashTable()
    144. {
    145. _table.resize(10,nullptr);
    146. }
    147. ~HashTable()
    148. {
    149. for (size_t i = 0; i < _table.size(); i++)
    150. {
    151. Node* cur = _table[i];
    152. while (cur)
    153. {
    154. Node* next = cur->_next;
    155. delete cur;
    156. cur = next;
    157. }
    158. _table[i] = nullptr;
    159. }
    160. }
    161. bool Insert(const pair& kv)
    162. {
    163. if (Find(kv.first))
    164. {
    165. return false;
    166. }
    167. HashFunc hf;
    168. //负载因子到1时扩容
    169. if (_n == _table.size())
    170. {
    171. size_t newSize = _table.size() * 2;
    172. vector newTable;
    173. newTable.resize(newSize, nullptr);
    174. //遍历旧表,把节点迁下来挂到新表
    175. for (size_t i = 0; i < _table.size(); i++)
    176. {
    177. Node* cur = _table[i];
    178. while (cur)
    179. {
    180. Node* next = cur->_next;
    181. //头插到新表
    182. size_t hashi = hf(cur->_kv.first) % newSize;
    183. cur->_next = newTable[hashi];
    184. newTable[hashi] = cur;
    185. cur = next;
    186. }
    187. _table[i] = nullptr;
    188. }
    189. _table.swap(newTable);
    190. }
    191. size_t hashi = hf(kv.first) % _table.size();
    192. //头插
    193. Node* newnode = new Node(kv);
    194. newnode->_next = _table[hashi];
    195. _table[hashi] = newnode;
    196. ++_n;
    197. return true;
    198. }
    199. Node* Find(const K& key)
    200. {
    201. HashFunc hf;
    202. size_t hashi = hf(key) % _table.size();
    203. Node* cur = _table[hashi];
    204. while (cur)
    205. {
    206. if (cur->_kv.first == key)
    207. {
    208. return cur;
    209. }
    210. cur = cur->_next;
    211. }
    212. return nullptr;
    213. }
    214. bool Erase(const K& key)
    215. {
    216. HashFunc hf;
    217. size_t hashi = hf(key) % _table.size();
    218. Node* prev = nullptr;
    219. Node* cur = _table[hashi];
    220. while (cur)
    221. {
    222. if (cur->_kv.first == key)
    223. {
    224. if (prev == nullptr)
    225. {
    226. _table[hashi] = cur->_next;
    227. }
    228. else
    229. {
    230. prev->_next = cur->_next;
    231. }
    232. delete cur;
    233. return true;
    234. }
    235. prev = cur;
    236. cur = cur->_next;
    237. }
    238. --_n;
    239. return false;
    240. }
    241. void Print()
    242. {
    243. for (size_t i = 0; i < _table.size(); i++)
    244. {
    245. printf("[%d]->", i);
    246. Node* cur = _table[i];
    247. while (cur)
    248. {
    249. cout << cur->_kv.first << ":" <_kv.second <<"->";
    250. cur = cur->_next;
    251. }
    252. printf("NULL\n");
    253. }
    254. cout << endl;
    255. }
    256. private:
    257. vector _table; //指针数组
    258. size_t _n = 0; //有效数据
    259. };
    260. }

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

    如有错误,还请指正

  • 相关阅读:
    pyqt5移动鼠标时显示鼠标坐标
    【JS】计算任意多个数字的和、差、积、商
    《深度学习入门:基于Python的理论与实现》再度笔记3
    大数据下一代变革之必研究数据湖技术Hudi原理实战双管齐下-上
    linux ethtool 命令详解
    JDBC与正则化
    Mybatis的二级缓存 (Redis方式)
    反序列化中_wakeup的绕过
    配置静态 Eth-trunk
    LeetCode 1425. 带限制的子序列和【动态规划,单调队列优化】2032
  • 原文地址:https://blog.csdn.net/KLZUQ/article/details/133409498