当前位置:   article > 正文

哈希表及其实现

哈希表及其实现

哈希概念

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

        哈希方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

        构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

        例如:数据集合{1,7,6,4,5,9}

        哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

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

哈希冲突

对于两个数据元素的关键字key1和 key2,有key1!=key2,但有:Hash(key1) == Hash(key2),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突

常见的哈希函数

1.直接定值法

        取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

        优点:不会发生Hash冲突,简单。

        缺点:可能会分布不均匀,使Hash表的一部分未被利用,导致浪费空间。

        例如:key={1,2,4,6,10000},哈希函数为Hash(key)= key。

        这样需要开一个大小为10000的数组但是仅仅存储这几个值,中间部分造成了极大的浪费。

2.除留余数法

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

        优点:可以自行规定表的大小,使插入数据可以均匀占据空间。

         缺点:造成哈希冲突。例如:15,22mod7都等于1,都占据1的位置。
        
        本文的Hash表模拟实现选用的Hash函数是 除留余数法。

如何解决Hash冲突?

闭散列的开放定值法

本质上是当前位置冲突,后面找一个合适的位置继续储存

线性探测法

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

例子:{1,4,24,34,7,44,17}(注意顺序!)

需要枚举类型表示一个位置的状态

  1. enum State
  2. {
  3. EMPTY,//空值,一开始一张表里都是空值
  4. EXIST,//有值,插入值时相应的位置变成有值
  5. DELETE//删除,在删除后,防止再次查找时,走到该位置停止
  6. };

注意:本质上需要有值和无值即可,但是这里有一个DELETE,它是在查找时起作用

哈希表结构

代码如下: 

  1. enum State
  2. {
  3. EMPTY,
  4. EXIST,
  5. DELETE
  6. };
  7. template<class K,class V>
  8. struct HashData
  9. {
  10. par<K, V> _data;
  11. State _state = EMPTY;//开始都标记为空
  12. };
  13. template<class K,class V>
  14. class HashTable
  15. {
  16. public:
  17. //插入
  18. ……
  19. //删除
  20. ……
  21. //寻找
  22. private:
  23. vector<HashData<K, V>> _table;
  24. size_t _n = 0;实际插入数据的个数
  25. };
 构造函数
  1. HashTable(size_t size = 10)//默认开10个类型的大小
  2. {
  3. _table.resize(size);
  4. }
插入函数

功能:给定一个值,通过哈希函数找到一个适合这个值位置的地址,插入。

        这种插入方法世间复杂度是O(1)。

        i=key%表的大小,如果i位置已经有值了,就线性往后找到空位置,放进去。

  1. //插入
  2. bool Insert(const pair<K,V>& kv)
  3. {
  4. size_t hashi = kv.first % _table.size();//根据值找位置
  5. while (_table[hashi]._state != EMPTY)//找到位置后判断是否发生哈希冲突
  6. {
  7. hashi++;//该位置有值(发生哈希冲突),向后移动。
  8. hashi %= _table.size();//防止越界(线性探测规则)
  9. }
  10. //解决哈希冲突后,在该位置插入值
  11. _table[hashi]._data = kv;
  12. _table[hashi]._state = EXIST;
  13. ++_n;
  14. return true;
  15. }
扩容问题

        空间是有限的,一直插入,总会有空间不够的情况。

负载因子------衡量哈希表的拥挤程度,空间的剩余情况

        负载因子越大,哈希表越拥挤,空间剩余越少,可能造成的哈希冲突越多,效率越低。

计算公式:负载因子=已有数据/表的大小

        一般控制在0.7左右,当负载因子大于等于0.7时扩容。

方法:再建一个两倍大小的哈希表,将原来哈希表的数据重新插入到这个新的哈希表,最后交换一下哈希表的地址。

  1. //扩容问题
  2. //当负载因子到达0.7时,此时扩容
  3. if (10 * _n / _table.size() >= 7)//此处同时乘以10,防止小数进行比较
  4. {
  5. HashTable<K, V> newHT(2 * _table());//新开一个空间是两倍的哈希表
  6. for (auto& e : _table)
  7. {
  8. if (e._state == EXIST)
  9. {
  10. newHT.Insert(e._data);//不会出现无穷递归,因为插入的新表里时负载因子小,不会走扩容,直接走线性探测插入
  11. }
  12. }
  13. _table.swap(newHT._table);//使用的是vector容器的swap
  14. }

完整的插入代码:

  1. bool Insert(const pair<K,V>& kv)
  2. {
  3. //扩容问题
  4. //当负载因子到达0.7时,此时扩容
  5. if (10 * _n / _table.size() >= 7)//此处同时乘以10,防止小数进行比较
  6. {
  7. HashTable<K, V> newHT(2 * _table());//新开一个空间是两倍的哈希表
  8. for (auto& e : _table)
  9. {
  10. if (e._state == EXIST)
  11. {
  12. newHT.Insert(e._data);//不会出现无穷递归,因为插入的新表里时负载因子小,不会走扩容,直接走线性探测插入
  13. }
  14. }
  15. _table.swap(newHT._table);//使用的是vector容器的swap
  16. }
  17. //线性探测
  18. size_t hashi = kv.first % _table.size();
  19. while (_table[hashi]._state != EMPTY)
  20. {
  21. hashi++;
  22. hashi %= _table.size();
  23. }
  24. _table[hashi]._data = kv;
  25. _table[hashi]._state = EXIST;
  26. ++_n;
  27. return true;
  28. }
查找函数

功能:给定key值查找该节点的地址。

        查找的时间复杂度是O(1),因为直接根据哈希函数定位到地址,如果有哈希冲突,进行常阶的移位比较。(几乎不需要比较!)

        i=key%表的大小,如果i不是要查找的key就线性往后查找,直到找到或者遇到空(如果是DELETE,还要继续往后查找)

        如果找到了表的结尾,就要回到头查找。

  1. HashData<K, V>* Find(const K& key)
  2. {
  3. size_t hashi = key % _table.size();
  4. //开始寻找
  5. while (_table.[hashi]._state != EMPTY)
  6. {
  7. if (_table.[hashi]._state == EXIST
  8. && key == _table[hashi]._data.first)
  9. {
  10. return &_table[hashi];
  11. }
  12. //探测规则(这里是线性探测)
  13. hashi++;//如果没找到,说明冲突了,向后线性探测。
  14. hashi %= _tables.size();//hashi向后++时,可能会越界,应该按照取余的方式控制在这个数组长度的范围内
  15. }
  16. return nullptr;
  17. }
删除函数

功能:给定key值,根据key值找到某节点,删除该节点

        很简单,只要用Find找到要删除的即可。

  1. //删除
  2. bool Erase(const K& key)
  3. {
  4. HashData<K, V>* ret = Find(key);//通过key找到节点的位置
  5. if (ret)//若节点存在
  6. {
  7. --_n;
  8. ret->_state = DELETE;//删除
  9. return true;
  10. }
  11. else
  12. {
  13. return false;
  14. }
  15. }
总代码
  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> _data;
  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. _table.resize(size);
  20. }
  21. //插入
  22. bool Insert(const pair<K,V>& kv)
  23. {
  24. //扩容问题
  25. //当负载因子到达0.7时,此时扩容
  26. if (10 * _n / _table.size() >= 7)//此处同时乘以10,防止小数进行比较
  27. {
  28. HashTable<K, V> newHT(2 * _table.size());//新开一个空间是两倍的哈希表
  29. for (auto& e : _table)
  30. {
  31. if (e._state == EXIST)
  32. {
  33. newHT.Insert(e._data);//不会出现无穷递归,因为插入的新表里时负载因子小,不会走扩容
  34. }
  35. }
  36. _table.swap(newHT._table);
  37. }
  38. //线性探测
  39. size_t hashi = kv.first % _table.size();
  40. while (_table[hashi]._state != EMPTY)
  41. {
  42. hashi++;
  43. hashi %= _table.size();
  44. }
  45. _table[hashi]._data = kv;
  46. _table[hashi]._state = EXIST;
  47. ++_n;
  48. return true;
  49. }
  50. //寻找
  51. HashData<K, V>* Find(const K& key)
  52. {
  53. size_t hashi = key % _table.size();
  54. while (_table[hashi]._state != EMPTY)
  55. {
  56. if (_table[hashi]._state == EXIST
  57. && key == _table[hashi]._data.first)
  58. {
  59. return &_table[hashi];
  60. }
  61. //探测规则(这里是线性探测)
  62. hashi++;//如果没找到,说明冲突了,向后线性探测。
  63. hashi %= _table.size();//hashi向后++时,可能会越界,应该按照取余的方式控制在这个数组长度的范围内
  64. }
  65. return nullptr;
  66. }
  67. //删除
  68. bool Erase(const K& key)
  69. {
  70. HashData<K, V>* ret = Find(key);
  71. if (ret->_state == EXIST)
  72. {
  73. --_n;
  74. ret->_state = DELETE;
  75. return true;
  76. }
  77. else
  78. {
  79. return false;
  80. }
  81. }
  82. private:
  83. vector<HashData<K, V>> _table;
  84. size_t _n = 0;
  85. };

开散列法

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

        本质:当使用除留余数法在同一个位置存储多个值时(发生哈希冲突),则在同一个位置以链表的形式储存多个值。 

例子:{1,4,24,34,7,44,17}(不是很注重顺序)

哈希表结构 

代码如下: 

  1. template<class K,class V>
  2. struct HashNode
  3. {
  4. HashNode<K, V>* next;
  5. pair<K, V> _kv;
  6. };
  7. template<class K,class V>
  8. class HashTable
  9. {
  10. typedef HashNode<K,V> Node;
  11. public:
  12. //插入
  13. //寻找
  14. //删除
  15. private:
  16. vector<Node*> _table;
  17. size_t _n = 0;
  18. };

构造函数

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

析构函数

        为什么需要写析构函数?对于vector有它自己的析构函数,而我们对每个vector储存的指针指向链表没有析构,会导致内存泄漏。

        下面是对HashNode的节点组成的链表进行析构。

  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. }

插入

功能:给定节点的key值,插入哈希表

原理:先根据key值计算出要插入节点的位置,再头插入链表。

  1. //插入
  2. bool Insert(const pair<K, V>& kv)
  3. {
  4. size_t hashi = kv.first % _table.size();//计算位置
  5. Node* newnode = new Node(kv);
  6. //头插
  7. newnode->_next = _table[hashi];//将新节点的_next指向最开始的节点,而vector保存的就是开始节点的指针
  8. _table[hashi] = newnode;//vector存储的指针换成刚插入的节点
  9. ++_n;
  10. return true;
  11. }
扩容问题

当实际节点数等于表的大小时,扩容。

原理:开个两倍的新表,将原来节点重新定址后插入新表中,交换两个表。

注:不要重新开节点,直接将老表的节点重新定值插入新表中,防止创建新节点造成过多的开销。

  1. //插入
  2. bool Insert(const pair<K, V>& kv)
  3. {
  4. //--------------------------------------------------------------------------------------
  5. //扩容
  6. if (_n == _table.size())//当实际位置的节点个数等于表的大小时进行扩容
  7. {
  8. //1.开一个大小为旧表两倍的新表
  9. vector<Node*> newTable(_table.size() * 2, nullptr);
  10. //2.遍历旧表每一个vector的每一个链表,将每个节点插入到新表
  11. for (size_t i = 0; i < _table.size(); i++)//遍历每一个表中vector单元
  12. {
  13. //取出旧表中的节点,重新定址后挂到新表桶中
  14. Node* cur = _table[i];
  15. while (cur)
  16. {
  17. Node* next = cur->_next;//保存第一个节点的下一个节点
  18. //头插到新表
  19. size_t hashi = cur->_kv.first % newTable.size();//重新定址
  20. cur->_next = newTable[hashi];//这个vector单元保存的是第一个节点的指针
  21. newTable[hashi] = cur;//将vector保存的节点替换成新头插的节点
  22. cur = next;//再去刚才没有移动的下一个节点
  23. }
  24. _table[i] = nullptr;//原表vector处置空
  25. }
  26. //3.交换两表
  27. _table.swap(newTable);
  28. }
  29. //--------------------------------------------------------------------------------------
  30. size_t hashi = kv.first % _table.size();//计算位置
  31. Node* newnode = new Node(kv);
  32. //头插
  33. newnode->_next = _table[hashi];//将新节点的_next指向最开始的节点,而vector保存的就是开始节点的指针
  34. _table[hashi] = newnode;//vector存储的指针换成刚插入的节点
  35. ++_n;
  36. return true;
  37. }

查找

功能:给定一个(key)值,找到这个储存这个值的地址。

原理:根据key值计算索引位置,去该索引位置处遍历所挂的链表,直到找到key值所在的节点

  1. //寻找
  2. Node* Find(const K& key)
  3. {
  4. size_t hashi = key % _table.size();//计算这个值的位置
  5. Node* cur = _table[hashi];//找到这个位置
  6. //遍历这个位置挂的节点
  7. while (cur)
  8. {
  9. if (cur->_kv.first == key)
  10. {
  11. return cur;
  12. }
  13. cur = cur->_next;
  14. }
  15. return nullptr;
  16. }

删除

功能:给定一个(key)值,删除储存该值的节点

原理:根据key值计算出对应vector的位置,然后遍历比较vector下挂的链表,并删除节点

  1. //删除
  2. bool Erase(const K& key)
  3. {
  4. size_t hashi = key % _table.szie();//计算要删节点的位置
  5. Node* prev = nullptr;
  6. Node* cur = _table[hashi];
  7. while (cur)
  8. {
  9. if (cur->_kv.first == key)//找到了该值所对应的节点
  10. {
  11. if (prev)//删除中间节点
  12. {
  13. prev->_next = cur->_next;//跳过cur节点,将prev节点与cur的下一个节点连接
  14. }
  15. else//删除第一个节点
  16. {
  17. _table[hashi] = cur->_next;//将vector中储存第二个节点
  18. }
  19. delete cur;
  20. --_n;
  21. return true;
  22. }
  23. //向下搜索
  24. prev = cur;
  25. cur = cur->_next;
  26. }
  27. return false;
  28. }

总代码

  1. namespace syb1
  2. {
  3. template<class K,class V>
  4. struct HashNode
  5. {
  6. HashNode<K, V>* _next;
  7. pair<K, V> _kv;
  8. HashNode(const pair<K,V>& kv)
  9. :_next(nullptr)
  10. ,_kv(kv)
  11. {}
  12. };
  13. template<class K,class V>
  14. class HashTable
  15. {
  16. typedef HashNode<K,V> Node;
  17. public:
  18. HashTable(size_t size = 10)
  19. {
  20. _table.resize(size);
  21. }
  22. ~HashTable()
  23. {
  24. for (size_t i = 0; i < _table.size(); i++)
  25. {
  26. Node* cur = _table[i];
  27. while (cur)//将该节点的链表全部删除
  28. {
  29. Node* next = cur->_next;
  30. delete cur;
  31. cur = next;
  32. }
  33. _table[i] = nullptr;
  34. }
  35. }
  36. //插入
  37. bool Insert(const pair<K, V>& kv)
  38. {
  39. //扩容
  40. if (_n == _table.size())//当实际位置的节点个数等于表的大小时进行扩容
  41. {
  42. vector<Node*> newTable(_table.size() * 2, nullptr);
  43. for (size_t i = 0; i < _table.size(); i++)//遍历每一个表中vector单元
  44. {
  45. //取出旧表中的节点,重新定址后挂到新表桶中
  46. Node* cur = _table[i];
  47. while (cur)
  48. {
  49. Node* next = cur->_next;//保存第一个节点的下一个节点
  50. //头插到新表
  51. size_t hashi = cur->_kv.first % newTable.size();//重新定址
  52. cur->_next = newTable[hashi];//这个vector单元保存的是第一个节点的指针
  53. newTable[hashi] = cur;//将vector保存的节点替换成新头插的节点
  54. cur = next;//再去刚才没有移动的下一个节点
  55. }
  56. _table[i] = nullptr;//原表vector处置空
  57. }
  58. _table.swap(newTable);
  59. }
  60. size_t hashi = kv.first % _table.size();//计算位置
  61. Node* newnode = new Node(kv);
  62. //头插
  63. newnode->_next = _table[hashi];//将新节点的_next指向最开始的节点,而vector保存的就是开始节点的指针
  64. _table[hashi] = newnode;//vector存储的指针换成刚插入的节点
  65. ++_n;
  66. return true;
  67. }
  68. //寻找
  69. Node* Find(const K& key)
  70. {
  71. size_t hashi = key % _table.size();//计算这个值的位置
  72. Node* cur = _table[hashi];//找到这个位置
  73. //遍历这个位置挂的节点
  74. while (cur)
  75. {
  76. if (cur->_kv.first == key)
  77. {
  78. return cur;
  79. }
  80. cur = cur->_next;
  81. }
  82. return nullptr;
  83. }
  84. //删除
  85. bool Erase(const K& key)
  86. {
  87. size_t hashi = key % _table.szie();//计算要删节点的位置
  88. Node* prev = nullptr;
  89. Node* cur = _table[hashi];
  90. while (cur)
  91. {
  92. if (cur->_kv.first == key)
  93. {
  94. if (prev)//删除中间节点
  95. {
  96. prev->_next = cur->_next;//跳过cur节点,将prev节点与cur的下一个节点连接
  97. }
  98. else//删除第一个节点
  99. {
  100. _table[hashi] = cur->_next;//将vector中储存第二个节点
  101. }
  102. delete cur;
  103. --_n;
  104. return true;
  105. }
  106. //向下搜索
  107. prev = cur;
  108. cur = cur->_next;
  109. }
  110. return false;
  111. }
  112. private:
  113. vector<Node*> _table;
  114. size_t _n = 0;
  115. };
  116. }

 

关于不同类型的key值

        对于key值是int型参数时,直接进行取模运算,即可得到映射的位置。但是对于字符串类型(string)的key值,或者是结构体类型的key值,如何确定其映射的位置呢?我们可以利用仿函数将其他类型的key值转化。

插入浮点类型的值

原理:非常简单,直接强转

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

插入字符串类型的值

原理:读取string每一个字符,将其的ASCII值每次乘一个值后加到Hash变量中,最后返回这个值Hash,这样就对每一个不同字符串都得到一个不同的key值。

关于溢出问题:如果字符串太长,通过这样方式转换成整形后溢出怎么办?不用管,自动截断,得到一个key值。

关于每次乘的值:选31 131 1313 13131 131313这些值都可以

  1. struct HashFuncString
  2. {
  3. size_t operator()(const string& s)
  4. {
  5. size_t hash = 0;
  6. for (auto e : s)
  7. {
  8. hash += e;
  9. hash *= 131;//可以防止顺序不同造成的位置相同
  10. }
  11. return hash;
  12. }
  13. };

插入结构体类型的值

 原理:同上,将每个元素转换成整形后乘上131加在一起。

  1. struct HashFuncDate
  2. {
  3. size_t operator()(const Date& d)
  4. {
  5. size_t hash = 0;
  6. hash += d._year;
  7. hash *= 131;
  8. hash += d._month;
  9. hash *= 131;
  10. hash += d._day;
  11. hash *= 131;
  12. }
  13. };

总结:以后对于各种各样的key值,我们可以把每个元素划分出来(结构体类型的将每个元素划分出来,string类型的划分成一个个char型)乘上一个特定的数加在一起,从而得到一个int型的key值。

使用:当key值为不同的类型时,使用仿函数作用在key值上,使key值变成int型数据。

以查找函数为例:

  1. HashData<K, V>* Find(const K& key)//<------传string类型数据
  2. {
  3. //-----------------------------------------------------------------------------------------
  4. HashFuncString hs;//类构造一个对象
  5. size_t hashi = hs(key) % _table.size();用仿函数将key转换成int类型的数据
  6. //-----------------------------------------------------------------------------------------
  7. while (_table[hashi]._state != EMPTY)
  8. {
  9. if (_table[hashi]._state == EXIST
  10. && key == _table[hashi]._data.first)
  11. {
  12. return &_table[hashi];
  13. }
  14. hashi++;
  15. hashi %= _table.size();
  16. }
  17. return nullptr;
  18. }

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

闽ICP备14008679号