• 【C++哈希表】哈希碰撞,线性探测,二次探测 ,荷载因子,闭散列的实现及string需要特化


    目录

    1.哈希概念

    2.哈希碰撞

    3.解决哈希冲突

    4.哈希表闭散列实现 

            框架: 

             4.3插入


    1.哈希概念

    • 线性表以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。线性表查找时间复杂度为O(N),平衡树中为树的高度,即O(logN)搜索的效率取决于搜索过程中元素的比较次数

    假如理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(哈希)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

    • 插入元素

                    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

                     计算方法:关键码(数据)%数组的容量的元素个数(除留余数法)

    • 搜索元素

                    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比 较,若关键码相等,则搜索成功

    该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表 (Hash Table)(或者称散列表)

    插入579 ,查询任意元素的时间复杂度都是O(1)

     2.哈希碰撞

    当插入1,11,21,31,41,按计算方法取模的结果都是1 ,不同关键码通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

    • 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

    1. 直接定制法:取关键字的某个线性函数为散列地址:Hash(Key)= 直接把关键码转化哈希地址 优点:简单、均匀缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况 

    • 如果数据不集中那么会有大量的空间浪费

     2. 除留余数法:设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函 数:Hash(key) = key% p(p<=m)m是容量(通常直接使用m就行),将关键码转换成哈希地址

     3.解决哈希冲突

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

    3.1闭散列

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

    3.1.线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止(实际方法)

    1. size_t start = hash(kv.first) % _tables.size();
    2. size_t i = 0;
    3. size_t index = start;
    4. // 线性探测
    5. while (_tables[index]._status == EXIST)
    6. {
    7. i++;
    8. index =start+ i;
    9. index %= _tables.size();
    10. }

    3.2.二次探测 线性探测的缺陷是产生冲突的数据堆积在一块(效率会变低),这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测是为了避免该问题;不在一次一次加了,加二次方,如果算出来的哈希地址大于容量,就取模就好像又走了一遍

    1. size_t start = hash(kv.first) % _tables.size();
    2. size_t i = 0;
    3. size_t index = start;
    4. // 二次探测
    5. while (_tables[index]._status == EXIST)
    6. {
    7. i++;
    8. index =start + i * i;
    9. index %= _tables.size();
    10. }

    3.3荷载因子 

    哈希表什么情况下进行扩容?如何扩容? 

    哈希表的荷载因子a:      a=插入的元素/哈希表的容量;这个值a需限制在0.7-0.8之下,达到就扩容;如果不扩容哈希碰撞的可能性就非常大,效率下降。

    4.哈希表闭散列实现 

    框架: 

    1. //状态,防止元素被删除后哈希表查询不到一些位置
    2. enum Status
    3. {
    4. EXIST,
    5. EMPTY,
    6. DELETE,
    7. };
    8. template<class T, class V>
    9. struct HashData
    10. {
    11. pair _kv;
    12. Status _status = EMPTY;
    13. };
    14. //仿函数,字符串需要特殊处理
    15. template<class K>
    16. struct Hash
    17. // 特化
    18. template<>
    19. struct Hash
    20. template<class T, class V,class Func=Hash>
    21. class HashTable
    22. {
    23. public:
    24. //接口函数
    25. bool Erase(const T& key)
    26. HashData* Find(const T& key)
    27. bool Insert(const pair& kv)
    28. private:
    29. vector> _tables;
    30. //插入的元素个数
    31. size_t _n=0;
    32. };

     4.1.查询

    1. 查询逻辑:key先取模得到哈希地址,按照哈希地址找,如果不是那么说明,出现过哈希碰撞,找“下一个”位置,直到找到这个数或者找到“下一个”空位置说明没有这个元素
    1. HashData* Find(const T& key)
    2. {
    3. if (_tables.size() == 0)
    4. {
    5. return nullptr;
    6. }
    7. //可以先不管这个仿函数,就是取里面的值
    8. Func hash;
    9. size_t start = hash(key)% _tables.size();
    10. size_t i = 0;
    11. size_t index = start;
    12. while (_tables[index]._status != EMPTY)
    13. {
    14. if (_tables[index]._kv.first == key && _tables[index]._status == EXIST)
    15. {
    16. return &_tables[index];
    17. }
    18. i++;
    19. index =start + i * i;
    20. index %= _tables.size();
    21. }
    22. return nullptr;
    23. }

    4.2删除

    1. 删除查询的位置,有就把HashData指针的成员_status改为删除(_DELETE)标记一下,没有就删除失败返回false
    1. bool Erase(const T& key)
    2. {
    3. HashData* ret = Find(key);
    4. if (ret == nullptr)
    5. {
    6. return false;
    7. }
    8. else
    9. {
    10. --_n;
    11. ret->_status = DELETE;
    12. return true;
    13. }
    14. }

     4.3插入

    1. 插入逻辑:先用Find查询是否有这个元素,有就插入失败,再判断是否需要扩容,插入就好;
    1. bool Insert(const pair& kv)
    2. {
    3. HashData* ret = Find(kv.first);
    4. if (ret)
    5. {
    6. return false;
    7. }
    8. // 荷载因子到0.7,就扩容
    9. // 荷载因子越小,冲突概率越低,效率越高,空间浪费越多
    10. // 荷载因子越大,冲突概率越高,效率越低,空间浪费越少
    11. if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
    12. {
    13. //扩容
    14. size_t newHashSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
    15. HashTable newHT;
    16. newHT._tables.resize(newHashSize);
    17. for (size_t i = 0; i < _tables.size(); i++)
    18. {
    19. if (_tables[i]._status == EXIST)
    20. {
    21. newHT.Insert(_tables[i]._kv);
    22. }
    23. }
    24. _tables.swap(newHT._tables);
    25. }
    26. Func hash;
    27. size_t start = hash(kv.first) % _tables.size();
    28. size_t i = 0;
    29. size_t index = start;
    30. // 线性探测 or 二次探测
    31. while (_tables[index]._status == EXIST)
    32. {
    33. i++;
    34. index =start + i * i;
    35. //index =start + i;
    36. index %= _tables.size();
    37. }
    38. _tables[index]._kv = kv;
    39. _tables[index]._status = EXIST;
    40. ++_n;
    41. return true;
    42. }

    4.3.1.荷载因子为0.7了,扩容会导致容量变大,取模:key/哈希表容量,那么这个哈希表的映射关系不符合了;只能全部重新插入了

     4.3.2字符串需要怎样插入了?

     

    4.3.2.1仿函数及string的特化

    • 能转化为整形的就直接转化,string和自定义类型就需要一个特化版本
    1. //仿函数
    2. template<class K>
    3. struct Hash
    4. {
    5. size_t operator()(const K& key)
    6. {
    7. return key;
    8. }
    9. };
    10. // 特化
    11. template<>
    12. struct Hash
    13. {
    14. size_t operator()(const string& s)
    15. {
    16. size_t value = 0;
    17. for (auto e : s)
    18. {
    19. // BKDR,减少哈希冲突
    20. value *= 31;
    21. value += e;
    22. }
    23. return value;
    24. }
    25. };

     

  • 相关阅读:
    python爬取QQ音乐评论信息
    多频电磁法概述 - 2. 理论
    时序与空间结构
    仿游戏热血江湖游戏类31
    八年测开经验面试28K公司后,吐血整理出高频面试题和答案
    面向Ai设计的Mojo编程语言支持下载,当前只有Linux系统版本
    Java用栈实现表达式求值
    SQL 练习题目(入门级)
    创新驱动|RFID技术在智能半导体行业的应用
    敏捷开发笔记(第8章节)--单一职责原则(SRP)
  • 原文地址:https://blog.csdn.net/m0_72964546/article/details/127755934