当前位置:   article > 正文

【数据结构】哈希表的开散列和闭散列模拟

【数据结构】哈希表的开散列和闭散列模拟

哈希思想


在顺序和树状结构中,元素的存储与其存储位置之间是没有对应关系,因此在查找一个元素时,必须要经过多次的比较。

顺序查找的时间复杂度为0(N),树的查找时间复杂度为log(N)。

我们最希望的搜索方式:通过元素的特性,不需要对比查找,而是直接找到某个元素。

这一个通过key与存储位置建立一一的思想就是hash思想。

哈希表就是基于哈希思想的一种具体实现。哈希表也叫散列表,是一种数据结构。无论有多少条数据,插入和查找的时间复杂度都是O(1),因此由于其极高的效率,被广泛使用。

建立映射关系:
例如集合{8,5,6,3,7,2,1,0}

key为每个元素的值,capaticy为哈希表元素的容量。

357801d7e27342f283f999b121998957.png

映射过程:
元素8   key=8  8%10=8 映射在数组下标为第8的位置上

元素7   映射在下标为7的位置上

  1. 直接定值法:(关键数范围集中,量不大的时候)关键字和存储位置是一一对应,不存在哈希冲突
  2. 除留余数法:(关键字很分散,量很大)关键字和存储位置是一对多的关系,存在哈希冲突

哈希冲突

对于两个数据元素的关键字 eq?k_%7Bi%7D 和 eq?k_j%7B%7D (i != j),有 eq?k_%7Bi%7D != eq?k_j%7B%7D ,但有:Hash(eq?k_%7Bi%7D) == Hash(eq?k_j%7B%7D),即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

例如上述的举例:
key的值为 18  15的时候

hashi计算的方法得出 需要映射到8 和5的位置上,但是8 和5的位置已经存在其它值。这就产生了冲突


哈希冲突的解决

1.开放定址法(闭散列)

a:线性探测

        如果发生冲突,就往后一次一步寻找为空的位置。

b:二次探测

        发生冲突,每次往后走俩步,寻找没有冲突的位置。

线性探测的缺点:容易产生成片的冲突

二次探测的缺点:虽然解决了容易产生成片冲突,但是空间利用率也不高

2.开散列

又称开链法、哈希桶,计算如果产生了哈希冲突,就以链表的形式将冲突的值链接起来。


哈希表的闭散列实现

闭散列哈希中的,每个位置不仅需要存储数据,还需要标注状态,方便查找删除。

enum State { EMPTY, EXIST, DELETE };


标记状态的意义?

在一个哈希表中,如果需要存放,我们会计算出key映射位置。如果key映射位置被占走,会往后继续寻找到删除/空的位置放置。

在查找时,在映射位置找不到时,需要往后寻找,我们不可能一直往后寻找O(N).,那就失去哈希表的价值,当我们遇到存在/删除位置时继续往后寻找,直到找到空位置,说明没有该元素。

因此在存储时,每个位置都必须有状态和数据

  1. struct Elem
  2. {
  3. pair<K, V> _val;
  4. State _state;
  5. };

框架

希表还需要维持容量的问题。因此需要_size表示实际存放,来维持负载因子

  1. template<class K,class V> //k—v结构
  2. class HashTable
  3. {
  4. public:
  5. //...
  6. private:
  7. vector<Elem> _ht;
  8. size_t _size; //实际存储
  9. size_t _totalSize; // 哈希表中的所有元素:有效和已删除, 扩容时候要用到
  10. };

哈希表的插入

  1. 根据K查找是为空,是则返回false
  2. 计算负载因子,是否需要扩容
  3. 插入新元素
  4. 更新位置状态,有效数目增加

扩容的方法

  • 开新的哈希表(默认空间为原来的2倍)
  • 遍历旧表,调用哈希表的插入。
  • 交换俩个表。

  1. // 插入
  2. bool Insert(const pair<K, V>& val)
  3. {
  4. if (Find(val.first) != -1)
  5. return false;
  6. //负载因子为7时,扩容
  7. if ((_size * 10) / _ht.size() == 7)
  8. {
  9. size_t newsize = _ht.size() * 2;
  10. HashTable<K, V>newht;
  11. newht._ht.resize(newsize);
  12. //遍历旧表
  13. for (size_t i = 0; i < _ht.size(); i++)
  14. {
  15. if (_ht[i]._state == EXIST)
  16. newht.Insert(_ht[i]._val);
  17. }
  18. _ht.swap(newht._ht);
  19. }
  20. //出入新元素
  21. size_t hashi = HashFunc(val.first);
  22. while (_ht[hashi]._state == EXIST)
  23. {
  24. ++hashi;
  25. hashi %= _ht.size();
  26. }
  27. _ht[hashi]._val = val;
  28. _ht[hashi]._state = EXIST;
  29. ++_size;
  30. ++_totalSize;
  31. return true;
  32. }

哈希表的查找

通过hash函数映射到hashi,往后一直比对,遇到存在比对,不是要找的val就往后需要,遇到删除也往后对比。直到遇到空返回。

  1. // 查找
  2. size_t Find(const K& key)
  3. {
  4. size_t hashi = HashFunc(key);
  5. while (_ht[hashi]._state != EMPTY)
  6. {
  7. if (_ht[hashi]._state == EXIST
  8. && _ht[hashi]._val.first == key)
  9. {
  10. return hashi;
  11. }
  12. ++hashi;
  13. hashi %= _ht.size();
  14. }
  15. return -1;
  16. }

哈希表的删除

删除是比较简单,是一种伪删除,不需要对数据清楚,只需要修改状态为删除,减少有效个数

  1. 调用find,没有则返回flase
  2. 修改为状态
  3. 减少个数
  1. bool Erase(const K& key)
  2. {
  3. int hashi = Find(key);
  4. if (hashi == -1) return false;
  5. _ht[hashi]._state = DELETE;
  6. --_size;
  7. return true;
  8. }

这三部分就是闭散列的主体结构。需要维持负载因子和状态。

Gitee: 闭散列哈希代码


哈希桶


开散列哈希表就不要需要状态的使用,是由一个链表的数组构成。

就是一排一排的桶。想要查找数据,只需要映射位置,在桶中寻找,是O(1)的放法.

特别极端情况下可能达到O(N)。

框架


底层可以依赖单链表,只需要简单的头插即可。

链表的结点:需要包含下一个位置的指针,需要包含pair键值对

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

同样需要记录表中有效元素的个数,但是一般情况下,负载因子在80%-90%效率最大

我们为了简单实现,在100%时才扩容。 

  1. template<class K, class V>
  2. class HashTable
  3. {
  4. public:
  5. //...
  6. private:
  7. vector<Node*> _table; //哈希表
  8. size_t _n = 0; //哈希表中的有效元素个数
  9. };

哈希桶的插入

  1. 检查是否为已经存在的Key
  2. 检查负载因子,为1就扩容
  3. 往hashi位置头插插入
  4. 修改个数

扩容的方法

  1. rasize一个二倍数量的原表
  2. 遍历旧表,将一个元素从链表的头取下,插入到新表中的hashi位置上。注意保存下一个位置!
  3. 交换俩张表

  1. bool Inset(const pair<K, V>& kv)
  2. {
  3. if (Find(kv.first))
  4. {
  5. return false;
  6. }
  7. hash hf;
  8. //扩容
  9. if (_tables.size() == _n)
  10. {
  11. size_t newsize = _tables.size() * 2;
  12. vector<Node*> newtable;
  13. newtable.resize(newsize, nullptr);
  14. for (size_t i = 0; i < (_tables.size()); i++)
  15. {
  16. Node* cur = _tables[i];
  17. while (cur)
  18. {
  19. Node* next = cur->_next;
  20. size_t hashi = hf(cur->_kv.first % newtable.size());
  21. //头插
  22. cur->_next = newtable[hashi];
  23. newtable[hashi] = cur;
  24. cur = next;
  25. }
  26. _tables[i] = nullptr;
  27. }
  28. _tables.swap(newtable);
  29. }
  30. size_t hashi = hf(kv.first) % _tables.size();
  31. Node* newnode = new Node(kv);
  32. newnode->_next = _tables[hashi];
  33. _tables[hashi] = newnode;
  34. _n++;
  35. return true;
  36. }


哈希桶的查找

  • 计算hashi
  • 遍历单链表
  • 为空则返回flase
  1. Node* Find(const K& key)
  2. {
  3. hash fc;
  4. size_t hashi = fc(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. bool Erase(const K& key)
  2. {
  3. hash fc;
  4. size_t hashi = fc(key) % _tables.size();
  5. Node* cur = _tables[hashi];
  6. Node* prev = nullptr;
  7. while (cur)
  8. {
  9. //找到了
  10. if (cur->_kv.first == key)
  11. {
  12. //头删
  13. if (prev == nullptr)
  14. {
  15. _tables[hashi] = cur->_next;
  16. }
  17. else
  18. {
  19. prev->_next = cur->_next;
  20. }
  21. delete cur;
  22. return true;
  23. }
  24. }
  25. return false;
  26. }

 Gitee: 开散列哈希桶代码


关于仿函数HashFunc

仿函数是一种回调,可以定义出函数对象。

是对不同类型转化为key,之前在位图就已经介绍,本文用的是BDK算法

对于string字符串类型会有存在冲突,但是可以通过不同的算法映射到不到的位置上,通过几个值的比对能减少失误的概率。

  1. template<class K>
  2. struct DefaultHash
  3. {
  4. size_t operator()(const K& key)
  5. {
  6. return (size_t)key;
  7. }
  8. };
  9. //特化 针对字符串
  10. template<>
  11. struct DefaultHash<string>
  12. {
  13. size_t operator()(const string& key)
  14. {
  15. //BKDR
  16. size_t hash = 0;
  17. for (auto ch : key)
  18. {
  19. hash = hash * 131 + ch;
  20. }
  21. return hash;
  22. }
  23. };

 

 

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

闽ICP备14008679号