当前位置:   article > 正文

哈希表/散列表(HashTable)c++实现_除留余数法c++实现散列存储

除留余数法c++实现散列存储

目录

哈希表实现的思想

除留余数法 

哈希冲突

第一种方法:探测法实现哈希表

探测法的思想 

结点类 

插入数据(insert)

冲突因子

数据扩容

哈希值 

插入的代码实现以及哈希类

查找数据(find)

删除数据(erase)

第二种方法:拉链法实现哈希表

结点类

哈希类的成员

插入(insert)

扩容

插入数据

插入代码总结:

查找(find)

删除数据(erase)

删除代码总结:


哈希表实现的思想

除留余数法 

哈希表的实现方法是通过每个值映射出一个下标位置,再存储到一个数组中的下标所对应的位置(有点类似计数排序)

这个值我们叫做哈希值

如图:我们有一个数组

假如我们此时插入一个14,那么直接映射到数组对应下标即可,此时哈希值也为14

 如图数组中通过这种直接映射的方法我们可以直接存储哈希值为0-15的数据,并且数据所对应的哈希值就是它本身

但如果我们此时要在数组中存储一个大于15的数据(例如50)怎么办呢?

解决方法也很简单,前文中哈希值等于数据本身,那么我们此时让数据除(%)数组大小,而余下来的数据就为它的哈希值

如图我们在数组中插入一个50

 这种方法我们叫做除留余数法

哈希冲突

如图数组

我们分别使用除留余数法插入两个数组(0和16)试试

可以看到,此时0和16的哈希值冲突了,这也就是所谓的哈希冲突

而以下说的两种方法都是为了解决哈希冲突 

第一种方法:探测法实现哈希表

探测法的思想 

 探测法的思想很简单,就是如果哈希值发生冲突,那么就从这个哈希值的后面进行探测

探测下一个位置是否为空/删除状态,如果不是,则继续往后探测,直到遇到为空/删除状态为止

 如图数组

我们利用探测法 分别插入0,8,16

 结点类 

哈希表中数据存储的是一个键值对(pair)

而除了键值对pair以外,用探测法实现的哈希表还需要有一个变量来表示结点此时处于删除/空/存在状态,这个状态我们采用的是用枚举实现

于是结点类的实现如下

  1. enum State
  2. {
  3. EMPTY,//空
  4. DELETE,//删除
  5. EXIST//存在
  6. };
  7. template <class K, class V>
  8. struct HashData
  9. {
  10. pair<K, V> _kv;
  11. State _state = EMPTY;
  12. };

插入数据(insert)

在插入数据之前我们需要知道的是,哈希表是什么扩容的?存储满再扩容吗?

答案是并不是,原因是当vector的存储数据个数已经快满了以后,哈希表再进行探测插入的话探测的次数就会变多,而此时效率也会变低,所以我们需要在哈希表快满的时候就进行扩容,那么什么时候算快满了呢?

冲突因子

冲突因子 = 哈希表中有的数据 / 哈希表的大小

而我们通过控制冲突因子来控制扩容,当冲突因子的大小>某个值时,我们就进行扩容

这里的某个值跟vector的扩容一样,是由用户自定义的

一般我们设置为0.7即可

数据扩容

哈希表的扩容不能跟vector一样直接拷贝数据到新表

因为哈希表需要根据数据的哈希值映射到不同的地方存储

哈希值会根据表大小的不同算出不同的哈希值

例如:

96在原大小为10的的表中哈希值为6,但如果表大小扩到20,那么此时96映射的位置为16

既然如此如何处理呢?那么我们就需要先创建一个新表,然后把数据一个一个的重新映射到新表当中

哈希值 

此时还面临一个问题,我们以上举例都为整形家族的,可以直接求出哈希值,但如果这个类型不是整形家族的呢?例如:string、日期类。。。

那么此时我们需要一个仿函数来求出这个类型的哈希值

需要注意的是:这个类型通过仿函数求出的哈希值必须是整形家族的,而这个哈希值我们也必须在插入以后能再次找到

插入的代码实现以及哈希类

  1. //hash为仿函数模板
  2. template <class K, class V, class hash>
  3. class HashTable
  4. {
  5. typedef HashData<K, V> Data;
  6. private:
  7. vector<Data> _table;//哈希表
  8. size_t _count = 0;//有效数据的个数
  9. public:
  10. bool insert(const pair<K, V> &val)
  11. {
  12. //1、判断数据是否存在
  13. if (Find(val.first))
  14. {
  15. return false;
  16. }
  17. //2、判断是否扩容
  18. //因为_count / _table.size() = 浮点数,所以要把_count * 10 ,这里的冲突因子>=0.7就扩容
  19. if (_table.size() == 0 || _count * 10 / _table.size() >= 7)
  20. {
  21. HashTable<K, V, hash> newtable;
  22. size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
  23. newtable._table.resize(newsize);
  24. for (int i = 0; i < _table.size(); ++i)
  25. {
  26. newtable.insert(_table[i]._kv);
  27. }
  28. newtable._table.swap(_table);
  29. }
  30. //3、求出哈希值
  31. hash hs;
  32. size_t hashi = hs(val.first);
  33. hashi %= _table.size();
  34. //4、找到插入的位置
  35. while (_table[hashi]._state == EXIST)
  36. {
  37. hashi++;
  38. hashi %= _table.size();
  39. }
  40. _table[hashi]._kv = val;
  41. _table[hashi]._state = EXIST;
  42. _count++;
  43. return true;
  44. }
  45. };

 查找数据(find)

查找数据我们就把要查找的值的哈希值求出来,再直接通过哈希值映射到对应的下标后用探测法找数据即可

  1. Data *Find(const K key)
  2. {
  3. if (!_table.size())
  4. {
  5. return nullptr;
  6. }
  7. //求出哈希值并映射下标
  8. hash hs;
  9. size_t hashi = hs(key);
  10. hashi %= _table.size();
  11. //探测法找数据
  12. while (_table[hashi]._state == EXIST)
  13. {
  14. if (_table[hashi]._kv.first == key)
  15. {
  16. return &_table[hashi];
  17. }
  18. else
  19. {
  20. hashi++;
  21. hashi %= _table.size();
  22. }
  23. }
  24. return nullptr;
  25. }

删除数据(erase)

删除数据我们需要先找到这个数据,再把这个数据的状态设置为DELETE即可

  1. bool erase(const K& key)
  2. {
  3. Data* pNode = Find(key);
  4. if(pNode == nullptr)
  5. {
  6. //没找到
  7. return false;
  8. }
  9. else
  10. {
  11. //找到了
  12. pNode->_state = DELETE;
  13. return true;
  14. }
  15. }

第二种方法:拉链法实现哈希表

第二种方法也是为了解决哈希冲突而实现的

我们知道探测法的解决办法是发生冲突的位置往后探测,直到遇到空/删除的位置插入即可

拉链法是把发生冲突的数据用一个链表串联起来

如图

 由上图可知,用拉链法实现的哈希表是由一个个链表组成的,所以此方法下的哈希表我们可以使用指针数组实现,并且这个链表我们用单链表实现即可

结点类

此方法下的结点类不需要再有状态因子了,只需要有数据+指针即可

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

哈希类的成员

成员是由一个存储链表的数组实现的,并且还需要有一个值来存储有效数据的个数

  1. template<class K,class V,class Hash = HashFunc<K>>
  2. class HashTable
  3. {
  4. typedef HashData<K,V> Data;
  5. private:
  6. std::vector<Data*> _table;//表
  7. size_t _count = 0;//表内有效数据
  8. public:
  9. //...
  10. }

插入(insert)

插入需要分为两步:

第一步:考虑扩容

第二步:插入数据

扩容

扩容同样需要创建新表,但不需要把原表中的结点释放掉,因为原表中的每一个结点都是我们申请的,如果我们直接释放掉原表中的结点再重新开辟,显然就会导致效率变低

我们只需要把原表的结点重新映射到新表中即可

如下图

我们只需把表中的数据全部放到新表中,再把旧表里的结点全部置为空即可

插入数据

插入数据分为步:

第一步:求出哈希值

第二步:哈希值映射下标

第三步:单链表的插入

插入代码总结:

  1. bool insert(const std::pair<K,V> val)
  2. {
  3. //扩容
  4. if(find(val.first))
  5. {
  6. return false;
  7. }
  8. Hash hs;
  9. if(_count == _table.size())//冲突因子 = 1
  10. {
  11. size_t newCapacity = _table.size() == 0 ? 10 : 2 * _table.size();
  12. HashTable<K,V,Hash> newTable;
  13. newTable._table.resize(newCapacity);
  14. //拷贝一张表的数据
  15. for(size_t i = 0 ; i < _table.size() ; ++i)
  16. {
  17. Data* cur = _table[i];
  18. //拷贝一串链表的数据
  19. while(cur)
  20. {
  21. size_t hashi = hs(cur->_kv.first) % newTable._table.size();
  22. if(newTable._table[hashi])
  23. {
  24. cur->_next = newTable._table[hashi]->_next;
  25. newTable._table[hashi]->_next = cur;
  26. }
  27. else
  28. {
  29. newTable._table[hashi] = cur;
  30. }
  31. cur = cur->_next;
  32. }
  33. _table[i] = nullptr;
  34. }
  35. *this = newTable;
  36. }
  37. //插入数据
  38. Data* node = new Data(val);
  39. size_t hashi = hs(node->_kv.first);
  40. hashi %= _table.size();
  41. if(_table[hashi])
  42. {
  43. node->_next = _table[hashi]->_next;
  44. _table[hashi]->_next = node;
  45. }
  46. else
  47. {
  48. _table[hashi] = node;
  49. }
  50. _count++;
  51. return true;
  52. }
  53. };

查找(find)

  1. Data* find(const K& key)
  2. {
  3. Hash hs;
  4. size_t hashi = hs(key);
  5. hashi %= _table.size();
  6. Data* cur = _table[hashi];
  7. while (cur)
  8. {
  9. if (cur->_kv.first == key)
  10. {
  11. return cur;
  12. }
  13. cur = cur->_next;
  14. }
  15. return nullptr;
  16. }

 删除数据(erase)

删除数据也并不复杂,首先我们需要找到数据,再用删除单链表结点的方式进行删除即可

我们需要求出哈希值找到对应结点所在的链表,再用cur逐个遍历这个链表,并且还需要一个prev结点来记录cur的前一个结点,方便单链表的中间删除

当找到数据后要分为两种情况:

第一种情况:结点是所在链表的头节点

如图

第二种情况:结点不是所在链表的头节点

如图

 删除代码总结:

  1. bool erase(const K& key)
  2. {
  3. Hash hs;
  4. size_t hashi = hs(key);
  5. hashi %= _table.size();
  6. Data* cur = _table[hashi];
  7. Data* prev = _table[hashi];//这个结点是用来记录cur的前一个结点,方便单链表不是头的删除
  8. while (cur)
  9. {
  10. if (cur->_kv.first == key)
  11. {
  12. //第一种情况,删除链表的头节点
  13. if (cur == _table[hashi])
  14. {
  15. Data* next = cur->_next;
  16. _table[hashi] = next;
  17. delete cur;
  18. return true;
  19. }
  20. //第二种情况:删除不是头的结点
  21. else
  22. {
  23. prev->_next = cur->_next;
  24. delete cur;
  25. return true;
  26. }
  27. }
  28. prev = cur;
  29. cur = cur->_next;
  30. }
  31. //没找到
  32. return false;
  33. }
  34. };

那么我们这期HashTable内容就到这了,感谢大家的支持

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

闽ICP备14008679号