当前位置:   article > 正文

C++:哈希表

C++:哈希表

哈希表概念

哈希表可以简单理解为:把数据转化为数组的下标,然后用数组的下标对应的值来表示这个数据。如果我们想要搜索这个数据,直接计算出这个数据的下标,然后就可以直接访问数组对应的位置,所以可以用O(1)的复杂度直接找到数据。

其中,这个数据对应的数字叫做关键码(Key),这个把关键码转化为下标的规则,叫做哈希函数(Hash)

要注意的是,有一些数据并不是整型,比如字符串,对象等等。对于这种数据,我们要先用一套规则把它们转化为整数(关键码),然后再通过哈希函数映射为数组下标。


哈希函数

哈希函数原则:

  1. 哈希函数转换后,生成的地址(下标)必须小于哈希表的最大地址(下标)
  2. 哈希函数计算出来的地址(下标)必须均匀地分布
  3. 哈希函数尽可能简单
直接定址法

取关键字的某个线性函数为哈希表的地址:

除留余数法

假设哈希表的地址数目为m,取Keym取模后得到的值作为下标


闭散列 - 开放定址法

闭散列,也叫做开放定址法,当发生哈希冲突时,如果哈希表没有被装满,说明哈希表中还有空位置,那么我们可以把发生冲突的数据放到下一个空位置去。

基本结构

首先我们需要一个枚举,来标识哈希表的不同状态:

  1. enum State
  2. {
  3. EMPTY,
  4. EXIST,
  5. DELETE
  6. };

EMPTY:空节点
EXIST:数值存在
DELETE:数值被删除 

哈希表的基本结构:

  1. enum State
  2. {
  3. EMPTY,
  4. EXIST,
  5. DELETE
  6. };
  7. template<class K, class V>
  8. struct HashData
  9. {
  10. pair<K, V> _kv;
  11. State _state = EMPTY;//标记状态
  12. };
  13. template<class K, class V>
  14. class HashTable
  15. {
  16. public:
  17. HashTable(size_t size = 10)
  18. {
  19. _tables.resize(size);
  20. }
  21. private:
  22. vector<HashData<K, V>> _tables;//哈希表
  23. size_t _n = 0;//元素个数
  24. };

HashTable构造函数:

  1. HashTable(size_t size = 10)
  2. {
  3. _tables.resize(size);
  4. }

查找

想要在哈希表中查找数据,无非就遵顼以下规则:

通过哈希函数计算出数据对应的地址
去地址处查找,如果地址处不是目标值,往后继续查找
遇到EMPTY还没有找到,说明数据不存在哈希表中
遇到DELETEEXIST,继续往后查找

代码如下:

  1. HashData<K, V>* Find(const K& key)
  2. {
  3. size_t hashi = key % _tables.size();
  4. while (_tables[hashi]._state != EMPTY)
  5. {
  6. if (_tables[hashi]._kv.first == key
  7. && _tables[hashi]._state == EXIST)
  8. return &_tables[hashi];
  9. hashi++;
  10. hashi %= _tables.size();
  11. }
  12. return nullptr;
  13. }

但是当前的代码存在一个问题:哈希表作用于泛型,key % _tables.size()有可能是违法的行为,因为key可能不是一个数字。

对此我们可以在模板中多加一个仿函数的参数,用户可以在仿函数中自定义数据 -> 整型的转换规则,然后我们在对这个整型使用除留余数法获取地址。

在那之前,我们可以先写一个仿函数,用于处理整型 -> 整型的转化:

  1. struct HashFunc
  2. {
  3. size_t operator()(const K& key)
  4. {
  5. return (size_t)key;
  6. }
  7. };

在STL中,整型-> 整型转化的函数,被写为了一个模板,而这个string -> 整型被写为了一个模板特化:

  1. template<class K>
  2. struct HashFunc
  3. {
  4. size_t operator()(const K& key)
  5. {
  6. return (size_t)key;
  7. }
  8. };
  9. template<>
  10. struct HashFunc<string>
  11. {
  12. size_t operator()(const string& s)
  13. {
  14. size_t hash = 0;
  15. for (auto& e : s)//把字符串的每一个字符ASCII码值加起来
  16. {
  17. hash += e;
  18. hash *= 131; // 31, 131313(任意由13间断排列的数字)
  19. }
  20. return hash;
  21. }
  22. };

我们将这个HashFunc<K>仿函数作为哈希表的第三个模板参数的默认值:

  1. template<class K, class V, class Hash = HashFunc<K>>
  2. class HashTable
  3. {};

通过仿函数来统一获得整型,再进行除留余数操作:

  1. Hash hs;
  2. size_t hashi = hs(key) % _tables.size();

插入

插入的基本逻辑如下:

  1. 先通过Find接口,查找目标值在不在哈希表中,如果目标值已经存在,返回flse,表示插入失败
  2. 通过哈希函数计算出目标值对应的下标
  3. 向下标中插入数据:如果下标对应的位置已经有数据,往后查找,直到某一个位置为EMPTY或者DELETE.如果下标对应的位置没有数据,直接插入
  4. 插入后,把对应位置的状态转化为EXIST

代码如下:

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. if (Find(kv.first))
  4. return false;
  5. Hash hs;//仿函数实例化出的对象
  6. size_t hashi = hs(kv.first) % _tables.size();//获得目标值对应的下标
  7. while (_tables[hashi]._state == EXIST)//往后查找合适的位置插入
  8. {
  9. hashi++;
  10. hashi %= _tables.size();
  11. }
  12. _tables[hashi]._kv = kv;//插入
  13. _tables[hashi]._state = EXIST;//改变状态
  14. _n++;//哈希表中的元素个数+1
  15. return true;
  16. }

当这个哈希表越满,我们查找数据的效率就越低,甚至说:如果查找一个不存在的数据,我们可能要用O(N)的复杂度遍历整个哈希表.因此我们因该把哈希表的负载率控制在一定值,当超过一定值,我们就要进行扩容操作。

  1. if ((double)_n / _tables.size() >= 0.7)
  2. {
  3. size_t newSize = _tables.size() * 2;
  4. HashTable<K, V, Hash> newHT(newSize);
  5. for (auto& e : _tables)
  6. {
  7. if (e._state == EXIST)
  8. newHT.Insert(e._kv);
  9. }
  10. _tables.swap(newHT._tables);
  11. }

插入总代码:

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. if (Find(kv.first))
  4. return false;
  5. if ((double)_n / _tables.size() >= 0.7)
  6. {
  7. size_t newSize = _tables.size() * 2;
  8. HashTable<K, V, Hash> newHT(newSize);
  9. for (auto& e : _tables)
  10. {
  11. if (e._state == EXIST)
  12. newHT.Insert(e._kv);
  13. }
  14. _tables.swap(newHT._tables);
  15. }
  16. Hash hs;
  17. size_t hashi = hs(kv.first) % _tables.size();
  18. while (_tables[hashi]._state == EXIST)
  19. {
  20. hashi++;
  21. hashi %= _tables.size();
  22. }
  23. _tables[hashi]._kv = kv;
  24. _tables[hashi]._state = EXIST;
  25. _n++;
  26. return true;
  27. }
删除

先通过Find接口找到要删除的值

  • 如果没找到,返回false,表示删除失败
  • 如果找到,把对应节点的状态改为DELETE

最后再把哈希表的_n - 1,表示存在的节点数少了一个。

代码如下:

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

总代码展示

  1. template<class K>
  2. struct HashFunc
  3. {
  4. size_t operator()(const K& key)
  5. {
  6. return (size_t)key;
  7. }
  8. };
  9. template<>
  10. struct HashFunc<string>
  11. {
  12. size_t operator()(const string& s)
  13. {
  14. size_t hash = 0;
  15. for (auto& e : s)//把字符串的每一个字符ASCII码值加起来
  16. {
  17. hash += e;
  18. hash *= 131; // 31, 131313(任意由13间断排列的数字)
  19. }
  20. return hash;
  21. }
  22. };
  23. enum State
  24. {
  25. EMPTY,
  26. EXIST,
  27. DELETE
  28. };
  29. template<class K, class V>
  30. struct HashData
  31. {
  32. pair<K, V> _kv;
  33. State _state = EMPTY;//标记状态
  34. };
  35. template<class K, class V, class Hash = HashFunc<K>>
  36. class HashTable
  37. {
  38. public:
  39. HashTable(size_t size = 10)
  40. {
  41. _tables.resize(size);
  42. }
  43. HashData<K, V>* Find(const K& key)
  44. {
  45. Hash hs;
  46. size_t hashi = hs(key) % _tables.size();
  47. while (_tables[hashi]._state != EMPTY)
  48. {
  49. if (_tables[hashi]._kv.first == key
  50. && _tables[hashi]._state == EXIST)
  51. return &_tables[hashi];
  52. hashi++;
  53. hashi %= _tables.size();
  54. }
  55. return nullptr;
  56. }
  57. bool Insert(const pair<K, V>& kv)
  58. {
  59. if (Find(kv.first))
  60. return false;
  61. if ((double)_n / _tables.size() >= 0.7)
  62. {
  63. size_t newSize = _tables.size() * 2;
  64. HashTable<K, V, Hash> newHT(newSize);
  65. for (auto& e : _tables)
  66. {
  67. if (e._state == EXIST)
  68. newHT.Insert(e._kv);
  69. }
  70. _tables.swap(newHT._tables);
  71. }
  72. Hash hs;
  73. size_t hashi = hs(kv.first) % _tables.size();
  74. while (_tables[hashi]._state == EXIST)
  75. {
  76. hashi++;
  77. hashi %= _tables.size();
  78. }
  79. _tables[hashi]._kv = kv;
  80. _tables[hashi]._state = EXIST;
  81. _n++;
  82. return true;
  83. }
  84. bool Erase(const K& key)
  85. {
  86. HashData<K, V>* ret = Find(key);
  87. if (ret)
  88. {
  89. ret->_state = DELETE;
  90. _n--;
  91. return true;
  92. }
  93. return false;
  94. }
  95. private:
  96. vector<HashData<K, V>> _tables;
  97. size_t _n = 0;//元素个数
  98. };

开散列 - 哈希桶

在STL库中,采用的是更加优秀的开散列方案。

哈希表的数组vector中,不再直接存储数据,而是存储一个链表的指针。当一个数值映射到对应的下标后,就插入到这个链表中。其中每一个链表称为一个哈希桶,每个哈希桶中,存放着哈希冲突的元素.

基本结构

对于每一个节点,其要存储当前节点的值,也要存储下一个节点的指针,基本结构如下:

  1. template<class K, class V>
  2. struct HashNode
  3. {
  4. HashNode<K, V>* _next;
  5. pair<K, V> _kv;
  6. HashNode(const pair<K, V>& kv)
  7. :_kv(kv)
  8. ,_next(nullptr)
  9. {}
  10. };

哈希表:

  1. template<class K, class V, class Hash = HashFunc<K>>
  2. class HashTable
  3. {
  4. typedef HashNode<K, V> Node;
  5. public:
  6. HashTable(size_t size = 10)
  7. {
  8. _tables.resize(size);
  9. }
  10. private:
  11. vector<Node*> _tables; //链表指针数组
  12. size_t _n = 0;//元素个数
  13. };

析构函数,防止内存泄漏:

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

查找的基本逻辑如下:

  1. 先通过哈希函数计算出数据对应的下标
  2. 通过下标找到对应的链表
  3. 遍历链表,找数据:如果某个节点的数据匹配上了,返回该节点指针,如果遍历到了nullptr,返回空指针表示没找到

代码如下:

  1. Node* Find(const K& key)
  2. {
  3. Hash hs;
  4. size_t hashi = hs(key) % _tables.size();
  5. Node* cur = _tables[hashi];
  6. while (cur)
  7. {
  8. if (cur->_kv.first == key)
  9. return cur;
  10. cur = cur->_next;
  11. }
  12. return nullptr;
  13. }
插入

插入的基本逻辑如下:

  1. 先通过Find接口,查找目标值在不在哈希表中,如果目标值已经存在,返回flse,表示插入失败
  2. 通过哈希函数计算出目标值对应的下标
  3. 向下标中插入数据

代码如下:

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. if (Find(kv.first))
  4. return false;
  5. Hash hs;
  6. size_t hashi = hs(kv.first) % _tables.size();//计算下标
  7. Node* newNode = new Node(kv);//创建节点
  8. newNode->_next = _tables[hashi];//头插
  9. _tables[hashi] = newNode;
  10. ++_n;//更新元素个数
  11. return true;
  12. }

关于扩容:如果我们单纯的进行插入,就要把原先的所有节点释放掉,再创建新的节点。这样会浪费很多时间。我们最好把原先创建的节点利用起来,因此我们要重写一个逻辑,把原先的节点进行迁移。 

  1. if (_n == _tables.size())
  2. {
  3. vector<Node*> newTables(_tables.size() * 2, nullptr);
  4. for (size_t i = 0; i < _tables.size(); i++)
  5. {
  6. Node* cur = _tables[i];
  7. while (cur)
  8. {
  9. Node* next = cur->_next;
  10. size_t hashi = hs(cur->_kv.first) % newTables.size();
  11. cur->_next = newTables[hashi];
  12. newTables[hashi] = cur;
  13. cur = next;
  14. }
  15. _tables[i] = nullptr; //防止移交的节点被析构
  16. }
  17. _tables.swap(newTables);
  18. }
删除

删除逻辑:

  1. 通过哈希函数计算出对应的下标
  2. 到对应的哈希桶中查找目标值
  • 如果找到,删除对应的节点
  • 如果没找到,返回false表示删除失败
  • _n - 1表示删除了一个元素

代码如下:

  1. bool Erase(const K& key)
  2. {
  3. Hash hs;
  4. size_t hashi = hs(key) % _tables.size();
  5. Node* prev = nullptr;
  6. Node* cur = _tables[hashi];
  7. while (cur)
  8. {
  9. if (cur->_kv.first == key)
  10. {
  11. if (prev)
  12. prev->_next = cur->_next;
  13. else
  14. _tables[hashi] = cur->_next;
  15. delete cur;
  16. --_n;
  17. return true;
  18. }
  19. prev = cur;
  20. cur = cur->_next;
  21. }
  22. return false;
  23. }

代码展示

  1. template<class K>
  2. struct HashFunc
  3. {
  4. size_t operator()(const K& key)
  5. {
  6. return (size_t)key;
  7. }
  8. };
  9. template<>
  10. struct HashFunc<string>
  11. {
  12. size_t operator()(const string& s)
  13. {
  14. size_t hash = 0;
  15. for (auto& e : s)//把字符串的每一个字符ASCII码值加起来
  16. {
  17. hash += e;
  18. hash *= 131; // 31, 131313(任意由13间断排列的数字)
  19. }
  20. return hash;
  21. }
  22. };
  23. template<class K, class V>
  24. struct HashNode
  25. {
  26. HashNode<K, V>* _next;
  27. pair<K, V> _kv;
  28. HashNode(const pair<K, V>& kv)
  29. :_kv(kv)
  30. ,_next(nullptr)
  31. {}
  32. };
  33. template<class K, class V, class Hash = HashFunc<K>>
  34. class HashTable
  35. {
  36. typedef HashNode<K, V> Node;
  37. public:
  38. HashTable(size_t size = 10)
  39. {
  40. _tables.resize(size);
  41. }
  42. ~HashTable()
  43. {
  44. for (size_t i = 0; i < _tables.size(); i++)
  45. {
  46. Node* cur = _tables[i];
  47. while (cur)
  48. {
  49. Node* next = cur->_next;
  50. delete cur;
  51. cur = next;
  52. }
  53. _tables[i] = nullptr;
  54. }
  55. }
  56. Node* Find(const K& key)
  57. {
  58. Hash hs;
  59. size_t hashi = hs(key) % _tables.size();
  60. Node* cur = _tables[hashi];
  61. while (cur)
  62. {
  63. if (cur->_kv.first == key)
  64. return cur;
  65. cur = cur->_next;
  66. }
  67. return nullptr;
  68. }
  69. bool Insert(const pair<K, V>& kv)
  70. {
  71. if (Find(kv.first))
  72. return false;
  73. Hash hs;
  74. //哈希桶情况下,负载因子到1才扩容
  75. if (_n == _tables.size())
  76. {
  77. vector<Node*> newTables(_tables.size() * 2, nullptr);
  78. for (size_t i = 0; i < _tables.size(); i++)
  79. {
  80. Node* cur = _tables[i];
  81. while (cur)
  82. {
  83. Node* next = cur->_next;
  84. size_t hashi = hs(cur->_kv.first) % newTables.size();
  85. cur->_next = newTables[hashi];
  86. newTables[hashi] = cur;
  87. cur = next;
  88. }
  89. _tables[i] = nullptr; //防止移交的节点被析构
  90. }
  91. _tables.swap(newTables);
  92. }
  93. size_t hashi = hs(kv.first) % _tables.size();
  94. Node* newNode = new Node(kv);
  95. newNode->_next = _tables[hashi];
  96. _tables[hashi] = newNode;
  97. ++_n;
  98. return true;
  99. }
  100. bool Erase(const K& key)
  101. {
  102. Hash hs;
  103. size_t hashi = hs(key) % _tables.size();
  104. Node* prev = nullptr;
  105. Node* cur = _tables[hashi];
  106. while (cur)
  107. {
  108. if (cur->_kv.first == key)
  109. {
  110. if (prev)
  111. prev->_next = cur->_next;
  112. else
  113. _tables[hashi] = cur->_next;
  114. delete cur;
  115. --_n;
  116. return true;
  117. }
  118. prev = cur;
  119. cur = cur->_next;
  120. }
  121. return false;
  122. }
  123. private:
  124. vector<Node*> _tables; //链表指针数组
  125. size_t _n = 0;//元素个数
  126. };

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/823541
推荐阅读
相关标签
  

闽ICP备14008679号