当前位置:   article > 正文

C++ 哈希表_c++哈希表

c++哈希表

目录

一、哈希概念

二、哈希冲突

三、哈希函数

四、哈希冲突解决

(一)闭散列

1、线性探测

2、实现线性探测 

1、节点+哈希表定义

2、插入

3、查找 

4、删除

3、线性探测总结

4、二次探测

(二)开散列

1、概念

2、节点

3、处理不同类型的哈希值

4、哈希表定义

5、插入 

6、查找

7、删除

8、析构函数

9、质数列表(扩容)

10、寻找最大桶长(测试)

11、哈希表优化策略

代码完整版+测试


unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。 

一、哈希概念

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

理想的搜索方法是能够直接从表中获取要搜索的元素,而不需要进行比较。

哈希方法通过构造一种存储结构,并使用一个哈希函数(hashFunc)使元素的存储位置与其关键码之间建立一一映射的关系。这样,在查找时可以通过哈希函数快速找到元素。

在哈希方法中,当向结构中:

  • 插入元素时,根据待插入元素的关键码,使用哈希函数计算出该元素的存储位置,并将其放置在该位置上。
  • 搜索元素时,同样使用哈希函数计算元素的存储位置,并在结构中按照该位置取出元素进行比较。如果关键码相等,则搜索成功。

这种方式称为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)或散列表。

例如,对于数据集合{1, 7, 6, 4, 5, 9},可以设置哈希函数为hash(key) = key % capacity,其中capacity表示存储元素的底层空间总大小。

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

二、哈希冲突

  • 哈希冲突指的是不同的关键字 ki 和 kj(其中 i 不等于 j)通过哈希函数计算得到相同的哈希地址,即Hash(ki) == Hash(kj),尽管它们的关键字不同。这种现象被称为哈希冲突或哈希碰撞。
  • 具有不同关键字但相同哈希地址的数据元素被称为"同义词"。

三、哈希函数

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

哈希函数设计原则:

1. 哈希函数的定义域必须包括需要存储的全部关键码,而值域必须在0到m-1之间,其中m是散列表的地址数量。
2. 哈希函数计算出来的地址应该能够均匀分布在整个地址空间中。
3. 哈希函数应该比较简单。

常见的哈希函数包括:

1. 直接定址法(常用):使用关键字的某个线性函数作为散列地址,即 Hash(Key) = A*Key + B。
   优点:简单且均匀。
   缺点:需要事先知道关键字的分布情况。
   使用场景:适合查找比较小且连续的情况,例如字符串中第一个只出现一次的字符。

2. 除留余数法(常用):选择一个不大于散列表地址数量m的质数p作为除数,通过哈希函数 Hash(key) = key % p (p <= m) 将关键码转换成哈希地址。

四、哈希冲突解决

解决哈希冲突两种常见的方法是:  闭散列开散列

(一)闭散列

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

1、线性探测

比如上图中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,  hashAddr为4, 因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

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

插入

通过哈希函数获取待插入元素在哈希表中的位置

如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

删除

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

2、实现线性探测 

1、节点+哈希表定义

  1. namespace openAddress
  2. {
  3. enum State
  4. {
  5. EMPTY,
  6. EXIST,
  7. DELETE
  8. };
  9. template<class K, class V>
  10. struct HashData
  11. {
  12. pair<K, V> _kv;
  13. State _state = EMPTY;
  14. };
  15. }

首先,定义了一个枚举类型 State,表示哈希表中每个元素的状态。它有三个可能的取值:

  • EMPTY:表示该位置为空,没有存储任何数据。
  • EXIST:表示该位置存储了有效的键值对数据。
  • DELETE:表示该位置存储的数据已被删除。

接下来,定义了一个模板结构体 HashData,用于存储哈希表中的每个元素的数据。它包含了一个键值对 _kv,用于存储键和值的对应关系,以及一个 _state,表示该元素的状态,默认为 EMPTY

然后,定义了一个模板类HashTable,用于存放键值对。

  1. namespace openAddress
  2. {
  3. template<class K, class V>
  4. class HashTable
  5. {
  6. private:
  7. vector<HashData<K, V>> _tables;
  8. size_t _n = 0;
  9. };
  10. }
  • HashTable类中,有一个私有成员变量_tables,它是一个存储HashData<K, V>类型的向量,用于存储实际的数据。
  • HashTable类还有一个私有成员变量_n,用于记录存储的数据个数。

2、插入

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. if (Find(kv.first))
  4. return false;
  5. // 负载因子超过0.7就扩容
  6. //if ((double)_n / (double)_tables.size() >= 0.7)
  7. if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7) {
  8. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  9. HashTable<K, V> newht;
  10. newht._tables.resize(newsize);
  11. // 遍历旧表,重新映射到新表
  12. for (auto& data : _tables)
  13. {
  14. if (data._state == EXIST)
  15. {
  16. newht.Insert(data._kv);
  17. }
  18. }
  19. _tables.swap(newht._tables);
  20. }
  21. size_t hashi = kv.first % _tables.size();
  22. size_t i = 1, index = hashi;
  23. while (_tables[index]._state == EXIST) {
  24. index = hashi + i;
  25. index %= _tables.size();
  26. i++;
  27. }
  28. _tables[index]._kv = kv;
  29. _tables[index]._state = EXIST;
  30. _n++;
  31. return true;
  32. }
  • 首先会调用 Find 函数查找是否已存在相同的键,如果存在则返回 false,表示插入失败。
  • 如果哈希表的负载因子超过了 0.7(即元素个数除以表的大小大于等于 0.7),则进行扩容操作。扩容后创建一个新的哈希表 newht,将旧表中的数据重新映射到新表中。
  • 然后计算插入位置的哈希值 hashi,并使用线性探测法解决哈希冲突,找到合适的插入位置。
  • 最后将键值对存储到该位置,并将状态设置为 EXIST,同时更新元素个数 _n。插入成功后返回 true。

3、查找 

  1. HashData<K, V>* Find(const K& key)
  2. {
  3. if (_tables.size() == 0)
  4. return nullptr;
  5. size_t hashi = key % _tables.size();
  6. size_t i = 1, index = hashi;
  7. while (_tables[index]._state != EMPTY) {
  8. if (_tables[index]._state == EXIST
  9. && _tables[index]._kv.first == key) {
  10. return &_tables[index];
  11. }
  12. index = hashi + i++;
  13. index %= _tables.size();
  14. //如果已经查找一圈,则说明全是存在+删除
  15. if (index == hashi)
  16. break;
  17. }
  18. return nullptr;
  19. }
  • 首先判断哈希表是否为空,如果为空则返回 nullptr。
  • 然后计算键的哈希值 hashi,并使用线性探测法查找对应的位置。
  • 如果找到了状态为 EXIST 的元素且键匹配,则返回该元素的指针。
  • 如果遍历一圈回到了起始位置,则表示没有找到匹配的元素,返回 nullptr。

4、删除

  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. else
  11. {
  12. return false;
  13. }
  14. }
  • 调用 Find 函数查找要删除的元素,如果找到了则将其状态设置为 DELETE,同时更新元素个数 _n。删除成功返回 true,否则返回 false。

3、线性探测总结

线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。如何缓解呢?

4、二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题, 找下一个空位置的方法为:

其中:i =1,2,3… H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表 的大小。对于2.1中如果要插入44,产生冲突,使用解决后的情况为:

研究表明:  当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5 如果超出必须考虑增容。

因此,哈希表的一个主要缺陷是空间利用率相对较低。

(二)开散列

1、概念

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

这是一个使用链地址法解决冲突的哈希表实现。链地址法是一种常见的哈希冲突解决方法,它将哈希到同一槽位的元素链接在一起,形成一个链表。

这个哈希表的主要组成部分有:

2、节点

HashNode:哈希节点,包含一个键值对和一个指向下一个节点的指针。

  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. :_next(nullptr)
  8. ,_kv(kv)
  9. {}
  10. };

3、处理不同类型的哈希值

HashFunc:哈希函数,用于计算键的哈希值。这里提供了一个默认的实现,直接返回键本身作为哈希值。另外,还提供了一个特化版本,用于处理字符串键,采用BKDR哈希算法。

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

4、哈希表定义

HashTable:哈希表,包含一个哈希节点的向量,每个元素是一个链表的头节点。哈希表还包含一些基本操作,如插入、查找、删除等。 

  1. template<class K,class V,class Hash = HashFunc<K>>
  2. class HashTable
  3. {
  4. typedef HashNode<K, V> Node;
  5. private:
  6. vector<Node*> _tables;
  7. size_t _n = 0;
  8. public:
  9. ~HashTable()
  10. Node* Find(const K& key)
  11. bool Erase(const K& key)
  12. size_t GetNextPrime(size_t prime)
  13. bool Insert(const pair<K, V>& kv)

5、插入 

首先检查键是否已经存在,如果存在则插入失败。然后检查哈希表是否需要扩容,如果需要则进行扩容。最后,计算键的哈希值,找到对应的链表,然后在链表头部插入新的节点。

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. if (Find(kv.first))
  4. {
  5. return false;
  6. }
  7. Hash hash;
  8. // 负载因因子==1时扩容
  9. if (_n == _tables.size())
  10. {
  11. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  12. vector<Node*> newtables(newsize, nullptr);
  13. //for (Node*& cur : _tables)
  14. for (auto& cur : _tables)
  15. {
  16. while (cur)
  17. {
  18. Node* next = cur->_next;
  19. size_t hashi = hash(cur->_kv.first) % newtables.size();
  20. // 头插到新表
  21. cur->_next = newtables[hashi];
  22. newtables[hashi] = cur;
  23. cur = next;
  24. }
  25. }
  26. _tables.swap(newtables);
  27. }
  28. size_t hashi = hash(kv.first) % _tables.size();
  29. // 头插
  30. Node* newnode = new Node(kv);
  31. newnode->_next = _tables[hashi];
  32. _tables[hashi] = newnode;
  33. ++_n;
  34. return true;
  35. }
  1. 首先,通过调用Find函数来检查待插入的键是否已经存在于哈希表中。如果存在,则直接返回false,表示插入失败。

  2. 接下来,创建一个哈希函数对象hash,用于计算键的哈希值。

  3. 然后,判断当前哈希表的负载因子是否达到了扩容的条件。负载因子是指哈希表中已存储元素的数量与哈希表大小的比值。在这段代码中,负载因子等于1时触发扩容操作。

  4. 如果需要进行扩容,首先计算新的哈希表大小newsize。如果当前哈希表大小为0,则将新的哈希表大小设置为10;否则,将新的哈希表大小设置为当前哈希表大小的两倍。

  5. 创建一个新的哈希表newtables,其大小为newsize,并初始化所有槽位为nullptr

  6. 遍历旧的哈希表_tables,对每个槽位中的链表进行处理。通过循环遍历链表中的每个节点,将节点从旧表中取出,并根据节点的键重新计算哈希值,得到在新表中的槽位索引hashi

  7. 将节点插入到新表的对应槽位中,采用头插法将节点插入链表中。

  8. 完成旧表中所有节点的处理后,使用swap函数将新表newtables和旧表_tables交换,使得新表成为当前的哈希表。

  9. 计算待插入键的哈希值,得到在当前哈希表中的槽位索引hashi

  10. 创建一个新的节点newnode,将待插入的键值对kv存储在节点中。

  11. 将新节点插入到当前哈希表的槽位hashi的链表中,采用头插法将节点插入链表中。

  12. 增加哈希表中元素的数量_n

  13. 返回true,表示插入成功。

6、查找

Find:查找操作,首先计算键的哈希值,然后对哈希表的大小取模,得到槽位索引。然后在对应的链表中查找键。

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

7、删除

Erase:删除操作,同样首先找到键对应的链表,然后在链表中查找并删除节点。

  1. bool Erase(const K& key)
  2. {
  3. Hash hash;
  4. size_t hashi = hash(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==nullptr)
  12. _tables[hashi] = cur->_next;
  13. else
  14. prev->_next = cur->_next;
  15. delete cur;
  16. return true;
  17. }
  18. else
  19. {
  20. prev = cur;
  21. cur = cur->_next;
  22. }
  23. }
  24. return false;
  25. }

8、析构函数

在类模板的析构函数中,首先遍历哈希表的每个桶(bucket),然后释放每个桶中的链表节点(HashNode),最后将桶指针置为空指针。这样可以确保在析构哈希表时,释放了所有的节点内存。

  1. ~HashTable()
  2. {
  3. for (auto& cur : _tables)
  4. {
  5. while (cur)
  6. {
  7. Node* next = cur->_next;
  8. delete cur;
  9. cur = next;
  10. }
  11. cur = nullptr;
  12. }
  13. }

9、质数列表(扩容)

在哈希表的扩容过程中,通常会选择一个比当前容量更大的新容量。选择合适的新容量大小对于哈希表的性能和效率非常重要。这里使用了一个预定义的质数列表__stl_prime_list,其中包含了一系列递增的质数。

通过调用GetNextPrime函数,可以根据当前容量选择一个比它更大的质数作为新的容量。这样做的目的是为了减少哈希冲突的发生,提高哈希表的性能。质数的选择可以使得哈希函数的分布更加均匀,减少冲突的可能性。

  1. // size_t newsize = GetNextPrime(_tables.size());
  2. size_t GetNextPrime(size_t prime)
  3. {
  4. // SGI
  5. static const int __stl_num_primes = 28;
  6. static const unsigned long __stl_prime_list[__stl_num_primes] =
  7. {
  8. 53, 97, 193, 389, 769,
  9. 1543, 3079, 6151, 12289, 24593,
  10. 49157, 98317, 196613, 393241, 786433,
  11. 1572869, 3145739, 6291469, 12582917, 25165843,
  12. 50331653, 100663319, 201326611, 402653189, 805306457,
  13. 1610612741, 3221225473, 4294967291
  14. };
  15. size_t i = 0;
  16. for (; i < __stl_num_primes; ++i)
  17. {
  18. if (__stl_prime_list[i] > prime)
  19. return __stl_prime_list[i];
  20. }
  21. return __stl_prime_list[i];
  22. }

10、寻找最大桶长(测试)

MaxBucketSize:返回哈希表中最长链表的长度,这可以用来评估哈希表的负载情况。如果某个链表过长,可能意味着哈希函数的分布性不好,或者哈希表的大小不够。

  1. size_t MaxBucketSize()
  2. {
  3. size_t max = 0;
  4. for (size_t i = 0; i < _tables.size(); i++)
  5. {
  6. auto cur = _tables[i];
  7. size_t size = 0;
  8. while (cur)
  9. {
  10. size++;
  11. cur = cur->_next;
  12. }
  13. if (size > max)
  14. max = size;
  15. }
  16. return max;
  17. }
  18. void TestHashTableMax()
  19. {
  20. size_t N = 900000;
  21. HashTable<int, int> ht;
  22. srand(time(0));
  23. for (size_t i = 0; i < N; ++i)
  24. {
  25. size_t x = rand() + i;
  26. ht.Insert(make_pair(x, x));
  27. }
  28. cout << ht.MaxBucketSize() << endl;
  29. }

11、哈希表优化策略

1. 负载因子控制:

负载因子(load factor)是指已存储元素数量与哈希表大小的比值。负载因子控制是为了保持哈希表的性能和效率。当负载因子超过一定阈值(通常为0.7)时,表示哈希表中的元素数量接近或超过了哈希表的容量,此时冲突的可能性会增加。为了避免冲突过多,可以进行哈希表的扩容操作,即增加哈希表的大小,以减少负载因子。通过控制负载因子,可以平衡哈希表的空间利用率和性能。

2. 单个桶超过一定长度,改为红黑树:

在哈希表中,每个桶(bucket)存储一定数量的元素。当某个桶中的元素数量超过一定长度(通常为8或者更多)时,为了提高查找效率,可以将该桶中的元素改为使用红黑树进行存储。红黑树是一种自平衡的二叉搜索树,可以在O(logN)的时间复杂度内进行查找、插入和删除操作。通过将超过一定长度的桶改为红黑树,可以避免在长链表中进行线性查找,提高了哈希表的性能。

使用联合体来实现将单个桶超过一定长度改为挂载红黑树的操作可以通过以下步骤实现:

1. 定义一个联合体,包含两个成员:链表指针和红黑树指针。这样的联合体可以用来存储桶中的元素。

  1. union BucketData{
  2. LinkedListNode* linkedList;
  3. RedBlackTree* redBlackTree;
  4. };

2. 在哈希表的每个桶中,使用上述定义的联合体类型来存储元素。初始时,将桶中的元素存储为链表。

  1. struct Bucket{
  2.     BucketData data;
  3.     int length; // 记录桶中元素的数量
  4. };

3. 当桶中的元素数量超过一定长度时,将联合体中的链表指针转换为红黑树指针,并将元素逐个插入红黑树中。

  1. if (bucket.length > threshold) {
  2.     // 创建红黑树
  3.     RedBlackTree* tree = createRedBlackTree();
  4.     
  5.     // 将链表中的元素逐个插入红黑树
  6.     LinkedListNode* currentNode = bucket.data.linkedList;
  7.     while (currentNode != NULL) {
  8.         insertIntoRedBlackTree(tree, currentNode->data);
  9.         currentNode = currentNode->next;
  10.     }
  11.     
  12.     // 更新桶中的数据为红黑树指针
  13.     bucket.data.redBlackTree = tree;
  14. }

4. 在进行查找、插入或删除操作时,根据桶中的数据类型进行相应的操作。

  1. if (bucket.data.linkedList != NULL) {
  2.     // 链表操作
  3.     // ...
  4. } else if (bucket.data.redBlackTree != NULL) {
  5.     // 红黑树操作
  6.     // ...
  7. }

通过使用联合体,可以在同一内存空间中存储链表和红黑树,根据需要进行切换。这样可以实现将单个桶超过一定长度改为挂载红黑树的操作,并在操作时根据桶中的数据类型选择相应的操作方式。这种方式可以提高哈希表的性能和效率。

代码完整版+测试

  1. #pragma once
  2. #include <vector>
  3. #include <iostream>
  4. using namespace std;
  5. namespace openAddress
  6. {
  7. enum State
  8. {
  9. EMPTY,
  10. EXIST,
  11. DELETE
  12. };
  13. template<class K, class V>
  14. struct HashData
  15. {
  16. pair<K, V> _kv;
  17. State _state = EMPTY;
  18. };
  19. template<class K, class V>
  20. class HashTable
  21. {
  22. public:
  23. bool Insert(const pair<K, V>& kv)
  24. {
  25. if (Find(kv.first))
  26. return false;
  27. // 负载因子超过0.7就扩容
  28. //if ((double)_n / (double)_tables.size() >= 0.7)
  29. if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7) {
  30. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  31. HashTable<K, V> newht;
  32. newht._tables.resize(newsize);
  33. // 遍历旧表,重新映射到新表
  34. for (auto& data : _tables)
  35. {
  36. if (data._state == EXIST)
  37. {
  38. newht.Insert(data._kv);
  39. }
  40. }
  41. _tables.swap(newht._tables);
  42. }
  43. size_t hashi = kv.first % _tables.size();
  44. size_t i = 1, index = hashi;
  45. while (_tables[index]._state == EXIST) {
  46. index = hashi + i;
  47. index %= _tables.size();
  48. i++;
  49. }
  50. _tables[index]._kv = kv;
  51. _tables[index]._state = EXIST;
  52. _n++;
  53. return true;
  54. }
  55. HashData<K, V>* Find(const K& key)
  56. {
  57. if (_tables.size() == 0)
  58. return nullptr;
  59. size_t hashi = key % _tables.size();
  60. size_t i = 1, index = hashi;
  61. while (_tables[index]._state != EMPTY) {
  62. if (_tables[index]._state == EXIST
  63. && _tables[index]._kv.first == key) {
  64. return &_tables[index];
  65. }
  66. index = hashi + i++;
  67. index %= _tables.size();
  68. if (index == hashi)
  69. break;
  70. }
  71. return nullptr;
  72. }
  73. bool Erase(const K& key)
  74. {
  75. HashData<K, V>* ret = Find(key);
  76. if (ret)
  77. {
  78. ret->_state = DELETE;
  79. --_n;
  80. return true;
  81. }
  82. else
  83. {
  84. return false;
  85. }
  86. }
  87. private:
  88. vector<HashData<K, V>> _tables;
  89. size_t _n = 0;
  90. };
  91. void TestHashTable()
  92. {
  93. HashTable<int, std::string> ht;
  94. // 插入键值对
  95. ht.Insert(std::make_pair(1, "One"));
  96. ht.Insert(std::make_pair(2, "Two"));
  97. ht.Insert(std::make_pair(3, "Three"));
  98. ht.Insert(std::make_pair(4, "Four"));
  99. ht.Insert(std::make_pair(5, "Five"));
  100. // 查找键值对
  101. HashData<int, std::string>* data = ht.Find(3);
  102. if (data)
  103. {
  104. std::cout << "Key: " << data->_kv.first << ", Value: " << data->_kv.second << std::endl;
  105. }
  106. else
  107. {
  108. std::cout << "Key not found." << std::endl;
  109. }
  110. // 删除键值对
  111. bool erased = ht.Erase(4);
  112. if (erased)
  113. {
  114. std::cout << "Key 4 erased successfully." << std::endl;
  115. }
  116. else
  117. {
  118. std::cout << "Key 4 not found." << std::endl;
  119. }
  120. // 再次查找键值对
  121. data = ht.Find(4);
  122. if (data)
  123. {
  124. std::cout << "Key: " << data->_kv.first << ", Value: " << data->_kv.second << std::endl;
  125. }
  126. else
  127. {
  128. std::cout << "Key not found." << std::endl;
  129. }
  130. }
  131. }
  132. namespace HashBucket1
  133. {
  134. template<class K,class V>
  135. struct HashNode
  136. {
  137. HashNode<K, V>* _next;
  138. pair<K, V> _kv;
  139. HashNode(const pair<K,V>& kv)
  140. :_next(nullptr)
  141. ,_kv(kv)
  142. {}
  143. };
  144. template<class K>
  145. struct HashFunc
  146. {
  147. size_t operator()(const K& key)
  148. {
  149. return key;
  150. }
  151. };
  152. template<>
  153. struct HashFunc<string>
  154. {
  155. //BKDR
  156. size_t operator()(const string& s)
  157. {
  158. size_t hash = 0;
  159. for (auto ch : s)
  160. {
  161. hash += ch;
  162. hash *= 31;
  163. }
  164. return hash;
  165. }
  166. };
  167. template<class K,class V,class Hash = HashFunc<K>>
  168. class HashTable
  169. {
  170. typedef HashNode<K, V> Node;
  171. private:
  172. vector<Node*> _tables;
  173. size_t _n = 0;
  174. public:
  175. ~HashTable()
  176. {
  177. for (auto& cur : _tables)
  178. {
  179. while (cur)
  180. {
  181. Node* next = cur->_next;
  182. delete cur;
  183. cur = next;
  184. }
  185. cur = nullptr;
  186. }
  187. }
  188. Node* Find(const K& key)
  189. {
  190. if (_tables.size() == 0)
  191. return nullptr;
  192. Hash hash;
  193. size_t hashi = hash(key) % _tables.size();
  194. Node* cur = _tables[hashi];
  195. while (cur)
  196. {
  197. if (cur->_kv.first == key)
  198. return cur;
  199. cur = cur->_next;
  200. }
  201. return nullptr;
  202. }
  203. bool Erase(const K& key)
  204. {
  205. Hash hash;
  206. size_t hashi = hash(key) % _tables.size();
  207. Node* prev = nullptr;
  208. Node* cur = _tables[hashi];
  209. while (cur)
  210. {
  211. if (cur->_kv.first == key)
  212. {
  213. if(prev==nullptr)
  214. _tables[hashi] = cur->_next;
  215. else
  216. prev->_next = cur->_next;
  217. delete cur;
  218. return true;
  219. }
  220. else
  221. {
  222. prev = cur;
  223. cur = cur->_next;
  224. }
  225. }
  226. return false;
  227. }
  228. // size_t newsize = GetNextPrime(_tables.size());
  229. size_t GetNextPrime(size_t prime)
  230. {
  231. // SGI
  232. static const int __stl_num_primes = 28;
  233. static const unsigned long __stl_prime_list[__stl_num_primes] =
  234. {
  235. 53, 97, 193, 389, 769,
  236. 1543, 3079, 6151, 12289, 24593,
  237. 49157, 98317, 196613, 393241, 786433,
  238. 1572869, 3145739, 6291469, 12582917, 25165843,
  239. 50331653, 100663319, 201326611, 402653189, 805306457,
  240. 1610612741, 3221225473, 4294967291
  241. };
  242. size_t i = 0;
  243. for (; i < __stl_num_primes; ++i)
  244. {
  245. if (__stl_prime_list[i] > prime)
  246. return __stl_prime_list[i];
  247. }
  248. return __stl_prime_list[i];
  249. }
  250. bool Insert(const pair<K, V>& kv)
  251. {
  252. if (Find(kv.first))
  253. {
  254. return false;
  255. }
  256. Hash hash;
  257. // 负载因因子==1时扩容
  258. if (_n == _tables.size())
  259. {
  260. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  261. vector<Node*> newtables(newsize, nullptr);
  262. //for (Node*& cur : _tables)
  263. for (auto& cur : _tables)
  264. {
  265. while (cur)
  266. {
  267. Node* next = cur->_next;
  268. size_t hashi = hash(cur->_kv.first) % newtables.size();
  269. // 头插到新表
  270. cur->_next = newtables[hashi];
  271. newtables[hashi] = cur;
  272. cur = next;
  273. }
  274. }
  275. _tables.swap(newtables);
  276. }
  277. size_t hashi = hash(kv.first) % _tables.size();
  278. // 头插
  279. Node* newnode = new Node(kv);
  280. newnode->_next = _tables[hashi];
  281. _tables[hashi] = newnode;
  282. ++_n;
  283. return true;
  284. }
  285. size_t MaxBucketSize()
  286. {
  287. size_t max = 0;
  288. for (size_t i = 0; i < _tables.size(); i++)
  289. {
  290. auto cur = _tables[i];
  291. size_t size = 0;
  292. while (cur)
  293. {
  294. size++;
  295. cur = cur->_next;
  296. }
  297. if (size > max)
  298. max = size;
  299. }
  300. return max;
  301. }
  302. };
  303. void TestHashTableMax()
  304. {
  305. size_t N = 900000;
  306. HashTable<int, int> ht;
  307. srand(time(0));
  308. for (size_t i = 0; i < N; ++i)
  309. {
  310. size_t x = rand() + i;
  311. ht.Insert(make_pair(x, x));
  312. }
  313. cout << ht.MaxBucketSize() << endl;
  314. }
  315. void TestHashTable()
  316. {
  317. // 创建哈希表对象
  318. HashBucket1::HashTable<int, std::string> ht;
  319. // 插入元素
  320. ht.Insert(std::make_pair(1, "One"));
  321. ht.Insert(std::make_pair(2, "Two"));
  322. ht.Insert(std::make_pair(3, "Three"));
  323. ht.Insert(std::make_pair(4, "Four"));
  324. ht.Insert(std::make_pair(5, "Five"));
  325. // 查找元素
  326. HashBucket1::HashNode<int, std::string>* node = ht.Find(3);
  327. if (node) {
  328. std::cout << "Found: " << node->_kv.second << std::endl;
  329. }
  330. else {
  331. std::cout << "Not found" << std::endl;
  332. }
  333. // 删除元素
  334. bool erased = ht.Erase(4);
  335. if (erased) {
  336. std::cout << "Element erased successfully" << std::endl;
  337. }
  338. else {
  339. std::cout << "Element not found" << std::endl;
  340. }
  341. // 获取哈希表中最长链表的长度
  342. size_t maxBucketSize = ht.MaxBucketSize();
  343. std::cout << "Max bucket size: " << maxBucketSize << std::endl;
  344. }
  345. }
本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号