• 哈希及哈希表的实现


    目录

    一、哈希的引入 

    二、概念

    三、哈希冲突

    四、哈希函数

    常见的哈希函数

    1、直接定址法

    2、除留余数法

    五、哈希冲突的解决

    1、闭散列

    2、开散列


    一、哈希的引入 

    顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数。
    理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

    这种思想就是我们接下来要讲的哈希了。


    二、概念

    如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
    一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
    当向该结构中:
    插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
    搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
    该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。

    当有这些数据时:1,4,5,6,7,9。我们可以通过下图的方式去插入数据。

     用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。


    三、哈希冲突

    按照上面的方式,如果我们插入的是 1,2,32,222,7,9这几个数呢?

    我们发现,如果按照哈希的思想去插入的话,2,32,22将会被放在同一个位置,这样就会引起一些麻烦。如果我去访问下标为2位置的数据,到底访问的哪一个呢?我们将这种现象称为哈希冲突。

    不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。


    四、哈希函数

    引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

    常见的哈希函数

    1、直接定址法

    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。
    优点:简单、均匀。
    缺点:需要事先知道关键字的分布情况。
    使用场景:适合查找比较小且连续的情况。

    2、除留余数法

    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p (p<=m),将关键码转换成哈希地址。


    五、哈希冲突的解决

    1、闭散列

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

    那么这个下一个位置怎么确定呢?我们有两种方法来帮助我们寻找“下一个”位置。

    ~ 线性探测 

    比如三中的情况,插入2之后,现在需要插入元素32,先通过哈希函数计算哈希地址为2,因此32理论上应该插在该位置,但是该位置已经放了值为2的元素,即发生哈希冲突。这时我们就需要去寻找该位置后面的空位置了。

    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

    如下图,32只能插入在3的位置了。 

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

    在有限的空间内,随着我们插入的数据越来越多,冲突的概率也越来越大,查找效率越来越低,所以闭散列的冲突表不可能让它满了,所以我们就引入了负载因子:

    负载因子(载荷因子):等于表中的有效数据个数/表的大小,衡量表的满程度,在闭散列中负载因子不可能超过1(1代表满了)。一般情况下,负载因子一般在0.7左右。负载因子越小,冲突概率也越小,但是消耗的空间越大,负载因子越大,冲突概率越大,空间的利用率越高。

    1. template<class K>
    2. struct HashFunc
    3. {
    4. size_t operator()(const K& key)
    5. {
    6. return (size_t)key;
    7. }
    8. };
    9. //特化
    10. template<>
    11. struct HashFunc
    12. {
    13. size_t operator()(const string& key)
    14. {
    15. size_t val = 0;
    16. for (auto ch : key)
    17. {
    18. val *= 131;
    19. val += ch;
    20. }
    21. return val;
    22. }
    23. };
    24. namespace closehash
    25. {
    26. enum State
    27. {
    28. EMPTY,
    29. DELETE,
    30. EXIST
    31. };
    32. template<class K,class V>
    33. struct HashData
    34. {
    35. pair _kv;
    36. State _state = EMPTY;
    37. };
    38. template<class K, class V, class Hash = HashFunc>
    39. class HashTable
    40. {
    41. public:
    42. bool insert(const pair& kv)
    43. {
    44. if (Find(kv.first))
    45. {
    46. return false;
    47. }
    48. if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)
    49. {
    50. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
    51. HashTable newHT;
    52. newHT._tables.resize(newsize);
    53. for (auto& e : _tables)
    54. {
    55. if (e._state == EXIST)
    56. {
    57. newHT.insert(e._kv);
    58. }
    59. }
    60. _tables.swap(newHT._tables);
    61. }
    62. Hash hash;
    63. size_t index = hash(kv.first) % _tables.size();
    64. //如果kv.first是string类型,那么就无法取模,因此我们要使用仿函数将其转换成整型
    65. //线性探测
    66. while (_tables[index]._state == EXIST)
    67. {
    68. index++;
    69. index %= _tables.size();
    70. }
    71. _tables[index]._kv = kv;
    72. _tables[index]._state = EXIST;
    73. _size++;
    74. return true;
    75. }
    76. HashData* Find(const K& key)
    77. {
    78. if (_tables.size() == 0)
    79. {
    80. return nullptr;
    81. }
    82. Hash hash;
    83. size_t hashi = hash(key) % _tables.size();
    84. size_t start = hashi;
    85. while (_tables[hashi]._state != EMPTY)
    86. {
    87. if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
    88. {
    89. return &_tables[hashi];
    90. }
    91. hashi++;
    92. hashi %= _tables.size();
    93. if (hashi == start)
    94. {
    95. break;
    96. }
    97. }
    98. return nullptr;
    99. }
    100. bool Erase(const K& key)
    101. {
    102. HashData* ret = Find(key);
    103. if (ret)
    104. {
    105. ret->_state = DELETE;
    106. _size--;
    107. return true;
    108. }
    109. else
    110. return false;
    111. }
    112. private:
    113. vector> _tables;
    114. size_t _size = 0; //存储了多少个有效数据
    115. };

    2、开散列

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

     

    从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

    那么是不是我们只需要开固定的空间,然后其他的数据就一直连接到对应的桶的后面,那样桶是不是太长了呢?

    桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容。

    增容条件:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

    1. template<class K,class V>
    2. struct HashNode
    3. {
    4. pair _kv;
    5. HashNode* _next;
    6. HashNode(const pair& kv)
    7. :_kv(kv)
    8. , _next(nullptr)
    9. {}
    10. };
    11. template<class K, class V, class Hash = HashFunc>
    12. class HashTable
    13. {
    14. typedef HashNode Node;
    15. public:
    16. ~HashTable()
    17. {
    18. for (size_t i = 0; i < _tables.size(); i++)
    19. {
    20. Node* cur = _tables[i];
    21. while (cur)
    22. {
    23. Node* next = cur->_next;
    24. free(cur);
    25. cur = next;
    26. }
    27. _tables[i] = nullptr;
    28. }
    29. }
    30. bool insert(const pair& kv)
    31. {
    32. //去重
    33. if (Find(kv.first))
    34. {
    35. return false;
    36. }
    37. Hash hash;
    38. //扩容
    39. if (_size == _tables.size())
    40. {
    41. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 10;
    42. vector newTables;
    43. newTables.resize(newsize, nullptr);
    44. //将旧表中结点移动映射到新表
    45. for (size_t i = 0; i < _tables.size(); i++)
    46. {
    47. Node* cur = _tables[i];
    48. while (cur)
    49. {
    50. Node* next = cur->_next;
    51. size_t hashi = hash(cur->_kv.first) % newTables.size();
    52. cur->_next = newTables[hashi];
    53. newTables[hashi] = cur;
    54. cur = next;
    55. }
    56. _tables[i] = nullptr;
    57. }
    58. _tables.swap(newTables);
    59. }
    60. size_t hashi = hash(kv.first) % _tables.size();
    61. Node* newnode = new Node(kv);
    62. newnode->_next = _tables[hashi];
    63. _tables[hashi] = newnode;
    64. _size++;
    65. return true;
    66. }
    67. Node* Find(const K& key)
    68. {
    69. if (_tables.size() == 0)
    70. return nullptr;
    71. Hash hash;
    72. size_t hashi = hash(key) % _tables.size();
    73. Node* cur = _tables[hashi];
    74. while (cur)
    75. {
    76. if (cur->_kv.first == key)
    77. return cur;
    78. else
    79. cur = cur->_next;
    80. }
    81. return nullptr;
    82. }
    83. bool erase(const K& key)
    84. {
    85. if (_tables.size() == 0)
    86. {
    87. return nullptr;
    88. }
    89. Hash hash;
    90. size_t hashi = hash(key) % _tables.size();
    91. Node* cur = _tables[hashi];
    92. Node* prev = nullptr;
    93. while (cur)
    94. {
    95. if (cur->_kv.first == key)
    96. {
    97. // 1、头删
    98. // 2、中间删
    99. if (prev == nullptr)
    100. {
    101. _tables[hashi] = cur->_next;
    102. }
    103. else
    104. {
    105. prev->_next = cur->_next;
    106. }
    107. delete cur;
    108. _size--;
    109. return true;
    110. }
    111. prev = cur;
    112. cur = cur->_next;
    113. }
    114. return false;
    115. }
    116. private:
    117. vector _tables;
    118. size_t _size = 0;
    119. };

  • 相关阅读:
    移动端ios键盘的兼容性问题
    彩色文字界面尼姆游戏(Python类 + mypycolor 工具协作打造)
    应用OPC解决方案实现控制系统数据的安全交换
    Garnet: 力压Redis的C#高性能分布式存储数据库
    ChatTTS 开源文本转语音模型本地部署、API使用和搭建WebUI界面(建议收藏)
    opencv-python读取的图像分辨率太大不能完全显示
    又一恶意软件:1000多名受害者均在韩国,不排除其他地区被攻击的可能
    spark学习笔记(十二)——sparkStreaming-RDD队列/自定义数据源/kafka数据源/DStream转换/DStream输出/优雅关闭
    Python 教程之从头开始构建个人助理,如何在 Python 中构建概念验证个人助理:意图分类、语音到文本和文本到语音(教程含源码)
    什么是AOP?
  • 原文地址:https://blog.csdn.net/zdlynj/article/details/132450635