当前位置:   article > 正文

C++——哈希_c++建立哈希

c++建立哈希

目录

unordered系列关联式容器

unordered_mapunordered_map在线文档说明

 unordered_map的接口说明

unordered系列优势

哈希 

解决哈希冲突

1.闭散列——开放定址法

思考:哈希表什么情况下进行扩容?如何扩容?

插入元素代码

查找元素 

删除元素 

 用哈希统计某元素出现次数

 ​编辑

闭散列——线性探测完整代码

 线性探测缺陷

开散列

 拉链法(哈希桶)

插入

查找元素 

删除 

完整代码 


 

unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到$log_2
N$,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好
的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同。

unordered_map
unordered_map在线文档说明

1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与
其对应的value, unordered_map/ unordered_set都是无序的。
2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3. 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内
找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭
代方面效率较低。
5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问
value。
6. 它的迭代器至少是前向迭代器

 unordered_map的接口说明

 1. unordered_map的构造

函数声明功能介绍
unordered_map构造不同格式的unordered_map对象

 2. unordered_map的容量

函数声明功能介绍
bool empty() const检测unordered_map是否为空
size_t size() const获取unordered_map的有效元素个数

3. unordered_map的迭代器

函数声明功能介绍
begin返回unordered_map第一个元素的迭代器
end返回unordered_map最后一个元素下一个位置的迭代器
cbegin返回unordered_map第一个元素的const迭代器
cend返回unordered_map最后一个元素下一个位置的const迭代器

力扣

  1. class Solution {
  2. public:
  3. int repeatedNTimes(vector<int>& nums) {
  4. unordered_map<int,int> countMap;
  5. for(auto e:nums)
  6. countMap[e]++;
  7. for(auto& kv:countMap)
  8. {
  9. if(kv.second==nums.size()/2)
  10. return kv.first;
  11. }
  12. return 0;
  13. }
  14. };

最后如果不写这个return 0就会报错,这是编译错误,可能会在vs下正常跑,但是在这里会报错,所以要在最后面加return

 对比map和set的区别:

1.map和set遍历是有序的,unordered系列是无序的

2.map和set是双向迭代器,unordered系列是单向的

unordered系列在面对大量数据时,增删查改的效率更优,尤其是查

力扣:找交集

  1. class Solution {
  2. public:
  3. vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
  4. unordered_set<int> s1;
  5. for(auto e:nums1)
  6. {
  7. s1.insert(e);
  8. }
  9. unordered_set<int> s2;
  10. for(auto e:nums2)
  11. {
  12. s2.insert(e);
  13. }
  14. vector<int> A;
  15. for(auto e:s1)
  16. {
  17. if(s2.find(e)!=s2.end())
  18. A.push_back(e);
  19. }
  20. return A;
  21. }
  22. };

这里插入的时候就会去重 

unordered系列优势

 

 10000个数据进行插入时,set快,查找的时候unordered_set快,删除的时候set快

 十万个数据时,size是插入的值,因为可能有重复的,所以不是十万。数据较大时,unordered系列的优势便显现出来了。

当六百多万数据时,unordered_set的插入就比较慢

 比较程序

  1. void test_op()
  2. {
  3. int n = 1000000;
  4. vector<int> v;
  5. v.reserve(n);
  6. srand(time(0));
  7. for (int i = 0; i < n; ++i)
  8. {
  9. //v.push_back(i);
  10. v.push_back(rand() + i); // ظ
  11. //v.push_back(rand()); // ظ
  12. }
  13. size_t begin1 = clock();
  14. set<int> s;
  15. for (auto e : v)
  16. {
  17. s.insert(e);
  18. }
  19. size_t end1 = clock();
  20. size_t begin2 = clock();
  21. unordered_set<int> us;
  22. for (auto e : v)
  23. {
  24. us.insert(e);
  25. }
  26. size_t end2 = clock();
  27. cout << "size:" << s.size() << endl;
  28. cout << "set insert:" << end1 - begin1 << endl;
  29. cout << "unordered_set insert:" << end2 - begin2 << endl;
  30. size_t begin3 = clock();
  31. for (auto e : v)
  32. {
  33. s.find(e);
  34. }
  35. size_t end3 = clock();
  36. size_t begin4 = clock();
  37. for (auto e : v)
  38. {
  39. us.find(e);
  40. }
  41. size_t end4 = clock();
  42. cout << "set find:" << end3 - begin3 << endl;
  43. cout << "unordered_set find:" << end4 - begin4 << endl;
  44. size_t begin5 = clock();
  45. for (auto e : v)
  46. {
  47. s.erase(e);
  48. }
  49. size_t end5 = clock();
  50. cout << "set erase" << endl;
  51. size_t begin6 = clock();
  52. for (auto e : v)
  53. {
  54. us.erase(e);
  55. }
  56. size_t end6 = clock();
  57. cout << "unordered_set erase" << endl;
  58. cout << "set erase:" << end5 - begin5 << endl;
  59. cout << "unordered_set erase:" << end6 - begin6 << endl;
  60. unordered_map<string, int> countMap;
  61. countMap.insert(make_pair("ƻ", 1));
  62. }

哈希 

 哈希也叫做散列,本质是让值跟存储位置建立映射的关联关系。

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


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


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

当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
为哈希表(Hash Table)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小

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

但有个问题,如果有个300,400,这俩个映射道德位置都是0,就会出现不同的值映射相同的问题,这叫哈希冲突/哈希碰撞。

解决哈希冲突

1.闭散列——开放定址法

a.线性探测

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,
使用线性探测找到下一个空位置,插入新元素

思考:哈希表什么情况下进行扩容?如何扩容?


 负载因子越小:冲突的概率越小,反之,越大。负载因子到一个基准值就扩容,基准值越大,冲突越多,效率越低,空间利用率越高。

当扩容后,进行插入,插入则会变为这样

插入1 4 5 6 7 44 9,原表中是这样

我们设置的基准值为7,>=7的时候就插入,当插入9的时候就会扩容,扩容并不是直接把旧表拷贝下来,而是要找相对应的位置再插入元素。下图为新表部分数据,通过7和11还有5就可以看出不是直接拷贝的旧表。

插入元素代码

  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. bool insert(const pair <K, V> & kv)
  18. {
  19. if (_table.size()==0||_size *10/ _table.size() >= 7)//负载因子大于7就扩容
  20. {
  21. //扩容不能直接开空间,拷贝数据,因为表大小变了,映射关系也变了,如10之前映射5,扩容后就不一定映射5
  22. size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
  23. //vector<HashData<K, V>> newTables;//建一张新表
  24. //newTables.resize(newSize);//让capacity和size保持一致
  25. 旧表的数据映射到新表
  26. //for ()
  27. //{
  28. //}
  29. //_table.swap(newTables);//旧表和新表交换,这种方法有点繁琐
  30. //我们可创建一个哈希表,对哈希表里面的table进行resize
  31. HashTable<K, V> newHT;//创建一个跟自己类型相同的对象
  32. newHT._table.resize(newSize);
  33. for (auto e: _table)
  34. {
  35. if (e._state == EXIST)
  36. {
  37. newHT.insert(e._kv);//再调insert
  38. }
  39. _table.swap(newHT._table);
  40. //数据交换后,不需要对新表进行释放空间,因为_table是vector,它有自己的析构函数
  41. }
  42. }
  43. size_t hashi = kv.first % _table.size();//这里要对size取模,不能对容量capacity取模,访问的下标必须是在size范围以内
  44. //线性探测
  45. while (_table[hashi]._state==EXIST)//如果这个位置有值了
  46. {
  47. hashi++;//往后走找空位置
  48. hashi %= _table.size();//走到结尾之后要回到最初位置,这里不会存在死循环,因为前面有扩容
  49. }
  50. _table[hashi]._kv = kv;//找到空位置了,把值放进去
  51. _table[hashi]._state = EXIST;//修改状态;
  52. ++_size;
  53. return true;
  54. }
  55. private:
  56. vector <HashData<K, V>> _table;
  57. size_t _size=0;//哈希表的有效数据,因为中间可能有空白间隔
  58. };

插入的时候负数也可以插入

查找元素 

  1. HashData<K,V>* Find(const K& kv)
  2. {
  3. if (_table.size() == 0)//表为空直接返回
  4. {
  5. return nullptr;
  6. }
  7. size_t hashi = kv % _table.size();
  8. while (_table[hashi]._state != EMPTY)//不为空就进行查找
  9. {
  10. size_t start = kv % _table.size();
  11. if (_table[hashi]._state!=DELETE&&_table[hahsi] == kv)//如果该位置既不是空也不是删除,且满足查找条件则返回该位置数值
  12. {
  13. return _table[hashi];
  14. }
  15. hashi++;
  16. hashi %= _table.size();
  17. if (hashi == start)
  18. {
  19. break;//找了一圈了没找到,跳出循环
  20. }
  21. }
  22. return nullptr;
  23. }

删除元素 

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

 用哈希统计某元素出现次数

  1. void TestHT2()
  2. {
  3. string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
  4. HashTable<string, int> countHT;
  5. for (auto& str : arr)
  6. {
  7. auto ptr = countHT.Find(str);
  8. if (ptr)
  9. {
  10. ptr->_kv.second++;
  11. }
  12. else
  13. {
  14. countHT.Insert(make_pair(str, 1));
  15. }
  16. }
  17. }

 

此时传的是string,在取模的时候会出现问题,C++在unordered_map中对这种问题有解决方法

 这里的hash<KEY>是一个仿函数,这个仿函数是处理不能取模的类型的,如string,它会把string转成一个能够取模的值。

我们增加一个哈希的仿函数

 插入函数中,不要直接取模,先调用仿函数,再取模

 find函数也一样

 这是就是把key交给仿函数转换一下,但是string不支持转

方法一 写一个针对string的转换,把string转换为整形

 这是把每个字符的ASCII码值加起来,然后返回去取模,但是在传参的时候要显示的去传

 在unordered_map中用string去做Key,不需要仿函数

 如果在使用哈希时,也不想每次都专门写一个仿函数,我们可以做特化

方法二 特化

如果是string, HashFunc会优先走特化版本

 HashFunc改进

HashFunc里面的成员函数是把所有字母的ASCII码值加起来的,但是遇到下面这种

abcd,bcda,dbca情况HashFunc此时功能就不太完整。

我们采用BKDR的方式,对每次的值*131,这是大佬设计的一个算法,

这些值看起来都挺接近,但是都不一样,这样可以解决上面的问题

闭散列——线性探测完整代码

  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. //struct HashFuncString
  14. //{
  15. // size_t operator()(const string& key)
  16. // {
  17. // size_t val = 0;
  18. // for (auto ch : key)
  19. // {
  20. // val += ch;
  21. // }
  22. // return val;
  23. // }
  24. //};
  25. template<class K>
  26. struct HashFunc
  27. {
  28. size_t operator()(const K& key)//把key转成无符号整形
  29. {
  30. return (size_t)key;
  31. }
  32. };
  33. template<>
  34. struct HashFunc<string>
  35. {
  36. size_t operator()(const string& key)
  37. {
  38. size_t val = 0;
  39. for (auto ch : key)
  40. {
  41. val *= 131;
  42. val += ch;
  43. }
  44. return val;
  45. }
  46. };
  47. template<class K, class V,class Hash=HashFunc<K>>
  48. class HashTable
  49. {
  50. public:
  51. bool insert(const pair <K, V> & kv)
  52. {
  53. if (Find(kv.first))
  54. {
  55. return false;//该元素存在,就不插入了
  56. }
  57. if (_table.size()==0||_size *10/ _table.size() >= 7)//负载因子大于7就扩容
  58. {
  59. //扩容不能直接开空间,拷贝数据,因为表大小变了,映射关系也变了,如10之前映射5,扩容后就不一定映射5
  60. size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
  61. //vector<HashData<K, V>> newTables;//建一张新表
  62. //newTables.resize(newSize);//让capacity和size保持一致
  63. 旧表的数据映射到新表
  64. //for ()
  65. //{
  66. //}
  67. //_table.swap(newTables);//旧表和新表交换,这种方法有点繁琐
  68. //我们可创建一个哈希表,对哈希表里面的table进行resize
  69. HashTable<K, V,Hash> newHT;//创建一个跟自己类型相同的对象
  70. newHT._table.resize(newSize);
  71. for (auto e: _table)
  72. {
  73. if (e._state == EXIST)
  74. {
  75. newHT.insert(e._kv);//再调insert
  76. }
  77. _table.swap(newHT._table);
  78. //数据交换后,不需要对新表进行释放空间,因为_table是vector,它有自己的析构函数
  79. }
  80. }
  81. Hash hash;
  82. size_t hashi =hash( kv.first) % _table.size();//这里要对size取模,不能对容量capacity取模,访问的下标必须是在size范围以内
  83. //线性探测
  84. while (_table[hashi]._state==EXIST)//如果这个位置有值了
  85. {
  86. hashi++;//往后走找空位置
  87. hashi %= _table.size();//走到结尾之后要回到最初位置,这里不会存在死循环,因为前面有扩容
  88. }
  89. _table[hashi]._kv = kv;//找到空位置了,把值放进去
  90. _table[hashi]._state = EXIST;//修改状态;
  91. ++_size;
  92. return true;
  93. }
  94. void Erase(const K& key)
  95. {
  96. HashData<K, V>* ret = Find(key);
  97. if (ret)
  98. {
  99. ret->_state = DELETE;
  100. --_size;
  101. return true;
  102. }
  103. else
  104. return false;
  105. }
  106. HashData<K,V>* Find(const K& kv)
  107. {
  108. if (_table.size() == 0)//表为空直接返回
  109. {
  110. return nullptr;
  111. }
  112. size_t hashi = kv % _table.size();
  113. Hash hash;
  114. size_t start = hash(key) % _table.size();
  115. while (_table[hashi]._state != EMPTY)//不为空就进行查找
  116. {
  117. size_t start = kv % _table.size();
  118. if (_table[hashi]._state!=DELETE&&_table[hahsi] == kv)//如果该位置既不是空也不是删除,且满足查找条件则返回该位置数值
  119. {
  120. return _table[hashi];
  121. }
  122. hashi++;
  123. hashi %= _table.size();
  124. if (hashi == start)
  125. {
  126. break;//找了一圈了没找到,跳出循环
  127. }
  128. }
  129. return nullptr;
  130. }
  131. private:
  132. vector <HashData<K, V>> _table;
  133. size_t _size=0;//哈希表的有效数据,因为中间可能有空白间隔
  134. };
  135. void Test1()
  136. {
  137. int a[] = {1,11,4,15,26,7,44,9};
  138. HashTable<int, int> ht;
  139. for (auto e : a)
  140. {
  141. ht.insert(make_pair(e, e));
  142. }
  143. }

 线性探测缺陷

 

若有这样一组值,线性探测在某个位置冲突很多的情况下,会互相占用,导致一大片出现冲突,如这里下标为2的位置被占用,2只能去占另外的位置,为了解决这种问题,有种解决方式叫二次探测

b.二次探测 

 一次探测是每次+i,二次探测是每次加i²,是相对于其实位置每次加i²,i开始是1,若每次计算完,那个地方有元素,则i变为2,

 如这里插入12 12%10=2,2+2²

代码 

  1. Hash hash;
  2. szie_t start = hash( kv.first) % _table.size();
  3. szie_t i = 0;
  4. size_t hashi=start+i;
  5. while (_table[hashi]._state==EXIST)//如果这个位置有值了
  6. {
  7. ++i
  8. hashi=start+i*i;//往后走找空位置
  9. hashi %= _table.size();//走到结尾之后要回到最初位置,这里不会存在死循环,因为前面有扩容
  10. }
  11. _table[hashi]._kv = kv;//找到空位置了,把值放进去
  12. _table[hashi]._state = EXIST;//修改状态;
  13. ++_size;

 二次探测仍然是占用式:这个地方被占了,我就去占其它地方。有种更好的办法叫拉链法

开散列

 拉链法(哈希桶)

这个表是指针数组

当好多元素要填入同一个位置时,这个时候会把这些数据通过单链表连接起来,注意挂起来(挂起来的这一串数据可以看作是一个桶)的数据不一定有序,这里这是巧合 。

使用单链表的时间复杂度是O(N),最极端情况要插入的数据都在同一个位置,在一个位置全部挂起来,这里由于控制了复杂因子,出现这种极坏的情况是极低的。即使出现了,也是小范围性的出现。

数据挂起来时采用头插,尾插都行,我们这里采用头插

插入

这里插入不能像前面那样复用Insert,因为会创建新节点,而且释放旧表又要写析构函数,比较麻烦,我们可以直接将旧表的桶搬下来,给新表使用,这里新表不手动释放原因,新表是一个局部变量,除了作用域后会空间会被释放

 

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. //去重,如果有重复元素就不插入
  4. if (Find(kv.first))
  5. return false;
  6. //扩容,负载因子到1就扩容
  7. if (_size == _tables.size())
  8. {
  9. //这里不能复用Inert,因为会创建新节点,而且要写析构函数释放旧表
  10. size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  11. vector<Node*> newTables;
  12. newTables.resize(newSize, nullptr);
  13. //旧表中的节点移动映射到新表
  14. for (size_t i=0;i<_tables.size();++i)
  15. {
  16. Node* cur = _tables[i];
  17. while (cur)
  18. {
  19. size_t hashi = cur->kv.first % newTables.size();
  20. cur->_next = newTables[hashi];
  21. newTables[hashi] = cur;
  22. cur = next;
  23. }
  24. _tables[i] = nullptr;//旧表中的桶置为空
  25. }
  26. _tables.swap(newTables);
  27. }
  28. size_t hashi = kv.first % _tables.size();//计算要插入元素的位置
  29. //头插 新节点->next指向头,之后更新头节点为新节点
  30. Node* newnode = new Node(kv);
  31. newnode->_next = _tables[hashi];
  32. _tables[hashi] = newnode;
  33. ++_size;
  34. return true;
  35. }

查找元素 

  1. Node* Find(const K& key)
  2. {
  3. if (_tables.size() == 0)
  4. {
  5. return nullptr;
  6. }
  7. size_t hashi = key % _table.size();//找到桶所在位置
  8. Node* cur = _tables[hashi];
  9. while (cur)
  10. {
  11. if (key.first == cur->kv.first)
  12. {
  13. return cur;
  14. }
  15. cur = cur->_next;
  16. }
  17. return nullptr;
  18. }

 进行插入测试,插入22的时候会进行扩容

 原表:1:1和11是一个桶     6:26    9:99

            4:14和4                   7:7

            5:55,15                 8:78

增容映射后新表:把11直接挂到了11这个位置,1挂到1这个位置

删除 

 类似于链表的删除,不能直接用Find找到就删除,因为这里是单链表删除后还要把前后连接起来。

情况1 cur是第一个节点,prev是nullptr,这种情况需单独处理

情况2 下图最左边这种情况,只需让prev的next指向cur的next

  1. bool Erase(const K& key)
  2. {
  3. if (_tables.size() == 0)//如果该哈希表为空,就直接返回
  4. {
  5. return nullptr;
  6. }
  7. size_t hashi = key % _tables.size();
  8. Node* prev = nullptr;//要找的节点的前一个节点
  9. Node* cur = _tables[hashi];
  10. while (cur)
  11. {
  12. if (cur->_kv.first == key)
  13. {
  14. //cur符合情况1
  15. if (prev == nullptr)
  16. {
  17. _tables[hashi] = cur->_next;
  18. }
  19. //中间情况,情况2
  20. else
  21. {
  22. prev->_next = cur->_next;
  23. }
  24. delete cur;
  25. --_size;
  26. return true;
  27. }
  28. prev = cur;
  29. cur = cur->_next;
  30. }
  31. return false;
  32. }

 仿函数,跟前面一样需要增加一个仿函数,处理string类。

尽量让哈西表的大小是一个素数(这个优化不是必须的),这种情况下冲突会降低,STL源码中有素数表。

  1. inline size_t __stl_next_prime(size_t n)//获取下一个素数
  2. {
  3. static const size_t __stl_num_primes = 28;
  4. static const size_t __stl_prime_list[__stl_num_primes] =
  5. {
  6. 53, 97, 193, 389, 769,
  7. 1543, 3079, 6151, 12289, 24593,
  8. 49157, 98317, 196613, 393241, 786433,
  9. 1572869, 3145739, 6291469, 12582917, 25165843,
  10. 50331653, 100663319, 201326611, 402653189, 805306457,
  11. 1610612741, 3221225473, 4294967291
  12. };
  13. for (size_t i = 0; i < __stl_num_primes; ++i)
  14. {
  15. if (__stl_prime_list[i] > n)
  16. {
  17. return __stl_prime_list[i];
  18. }
  19. }
  20. return -1;
  21. }

先测试1w个数据

  1. void TestHT3()
  2. {
  3. int n = 10000;
  4. vector<int> v;
  5. v.reserve(n);
  6. srand(time(0));
  7. for (int i = 0; i < n; ++i)
  8. {
  9. v.push_back(rand()); // 重复多
  10. }
  11. size_t begin1 = clock();
  12. HashTable<int, int> ht;
  13. for (auto e : v)
  14. {
  15. ht.Insert(make_pair(e, e));
  16. }
  17. size_t end1 = clock();
  18. cout << "数据个数:" << ht.Size() << endl;
  19. cout << "表的长度:" << ht.TablesSize() << endl;
  20. cout << "桶的个数:" << ht.BucketNum() << endl;
  21. cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;
  22. cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
  23. cout << "负载因子:" << (double)ht.Size() / (double)ht.TablesSize() << endl;
  24. }

 

大多数桶的长度是一俩个,最长的桶长度是2个 

 100w个数据

1000w

1500w

1600w

完整代码 

  1. #define _CRT_SECURE_NO_WARNINGS
  2. template<class K>
  3. struct HashFunc
  4. {
  5. size_t operator()(const K& key)
  6. {
  7. return (size_t)key;
  8. }
  9. };
  10. template<>
  11. struct HashFunc<string>
  12. {
  13. // BKDR
  14. size_t operator()(const string& key)
  15. {
  16. size_t val = 0;
  17. for (auto ch : key)
  18. {
  19. val *= 131;
  20. val += ch;
  21. }
  22. return val;
  23. }
  24. };
  25. namespace HashBucket
  26. {
  27. template<class K,class V>
  28. struct HashNode
  29. {
  30. pair<K, V> _kv;
  31. HashNode<K, V>* _next;
  32. HashNode(const pair<K,V>& key)
  33. :_kv(lv),
  34. _next(nullptr)
  35. {}
  36. };
  37. template<class K,class V,class Hash=HashFunc<K>>
  38. class HashTable
  39. {
  40. ~HashTable()//数组会被自动释放,可桶需要手动释放
  41. {
  42. for (size_t i = 0; i < _table.size(); ++i)
  43. {
  44. Node* cur = _tables.size();
  45. while (cur)
  46. {
  47. Node* next = cur->next;
  48. free(cur);
  49. cur = next;
  50. }
  51. _tables[i] = nullptr;
  52. }
  53. }
  54. typedef HashNode<K, V> Node;
  55. public:
  56. inline size_t __stl_next_prime(size_t n)//获取下一个素数
  57. {
  58. static const size_t __stl_num_primes = 28;
  59. static const size_t __stl_prime_list[__stl_num_primes] =
  60. {
  61. 53, 97, 193, 389, 769,
  62. 1543, 3079, 6151, 12289, 24593,
  63. 49157, 98317, 196613, 393241, 786433,
  64. 1572869, 3145739, 6291469, 12582917, 25165843,
  65. 50331653, 100663319, 201326611, 402653189, 805306457,
  66. 1610612741, 3221225473, 4294967291
  67. };
  68. for (size_t i = 0; i < __stl_num_primes; ++i)
  69. {
  70. if (__stl_prime_list[i] > n)
  71. {
  72. return __stl_prime_list[i];
  73. }
  74. }
  75. return -1;
  76. }
  77. bool Insert(const pair<K, V>& kv)
  78. {
  79. //去重,如果有重复元素就不插入
  80. if (Find(kv.first))
  81. return false;
  82. Hash hash;
  83. //扩容,负载因子到1就扩容
  84. if (_size == _tables.size())
  85. {
  86. //这里不能复用Inert,因为会创建新节点,而且要写析构函数释放旧表
  87. size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  88. vector<Node*> newTables;
  89. newTables.resize(newSize, nullptr);
  90. //旧表中的节点移动映射到新表
  91. for (size_t i=0;i<_tables.size();++i)
  92. {
  93. Node* cur = _tables[i];
  94. while (cur)
  95. {
  96. size_t hashi = hash(cur->kv.first) % newTables.size();
  97. cur->_next = newTables[hashi];
  98. newTables[hashi] = cur;
  99. cur = next;
  100. }
  101. _tables[i] = nullptr;//旧表中的桶置为空
  102. }
  103. _tables.swap(newTables);
  104. }
  105. Hash hash;
  106. size_t hashi = hash(kv.first) % _tables.size();//计算要插入元素的位置
  107. //头插 新节点->next指向头,之后更新头节点为新节点
  108. Node* newnode = new Node(kv);
  109. newnode->_next = _tables[hashi];
  110. _tables[hashi] = newnode;
  111. ++_size;
  112. return true;
  113. }
  114. Node* Find(const K& key)
  115. {
  116. if (_tables.size() == 0)
  117. {
  118. return nullptr;
  119. }
  120. Hash hash;
  121. size_t hashi = hash(key) % _table.size();//找到桶所在位置
  122. Node* cur = _tables[hashi];
  123. while (cur)
  124. {
  125. if (key.first == cur->kv.first)
  126. {
  127. return cur;
  128. }
  129. cur = cur->_next;
  130. }
  131. return nullptr;
  132. }
  133. bool Erase(const K& key)
  134. {
  135. if (_tables.size() == 0)//如果该哈希表为空,就直接返回
  136. {
  137. return nullptr;
  138. }
  139. Hash hash;
  140. size_t hashi = hash(key) % _tables.size();
  141. Node* prev = nullptr;//要找的节点的前一个节点
  142. Node* cur = _tables[hashi];
  143. while (cur)
  144. {
  145. if (cur->_kv.first == key)
  146. {
  147. //cur符合情况1
  148. if (prev == nullptr)
  149. {
  150. _tables[hashi] = cur->_next;
  151. }
  152. //中间情况,情况2
  153. else
  154. {
  155. prev->_next = cur->_next;
  156. }
  157. delete cur;
  158. --_size;
  159. return true;
  160. }
  161. prev = cur;
  162. cur = cur->_next;
  163. }
  164. return false;
  165. }
  166. //有效数据个数
  167. size_t Size()
  168. {
  169. return _size;
  170. }
  171. size_t BucketNum()//返回桶的数量
  172. {
  173. size_t num = 0;
  174. for (size_t i = 0; i < _tables.size(); ++i)
  175. {
  176. if (_tables[i])
  177. {
  178. ++num;
  179. }
  180. }
  181. }
  182. //表的长度
  183. size_t TableSize()
  184. {
  185. return _tables.size();
  186. }
  187. size_t MaxBucketLenth()
  188. {
  189. size_t Maxlen = 0;
  190. for (size_t i = 0; i < _tables.size(); ++i)
  191. {
  192. size_t len = 0;
  193. Node* cur = _tables[i];
  194. while (cur)
  195. {
  196. ++len;
  197. cur = cur->_next;
  198. }
  199. if(len>0)
  200. printf("[%d]号桶长度:%d\n", i, len);
  201. Maxlen = len > Maxlen ? len : Maxlen;
  202. }
  203. }
  204. private:
  205. vector<Node*> _tables;
  206. size_t _size=0;//存储有效数据个数
  207. };
  208. }
  209. void TestHT3()
  210. {
  211. int n = 10000;
  212. vector<int> v;
  213. v.reserve(n);
  214. srand(time(0));
  215. for (int i = 0; i < n; ++i)
  216. {
  217. v.push_back(rand()); // 重复多
  218. }
  219. size_t begin1 = clock();
  220. HashTable<int, int> ht;
  221. for (auto e : v)
  222. {
  223. ht.Insert(make_pair(e, e));
  224. }
  225. size_t end1 = clock();
  226. cout << "数据个数:" << ht.Size() << endl;
  227. cout << "表的长度:" << ht.TablesSize() << endl;
  228. cout << "桶的个数:" << ht.BucketNum() << endl;
  229. cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;
  230. cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
  231. cout << "负载因子:" << (double)ht.Size() / (double)ht.TablesSize() << endl;
  232. }

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号