当前位置:   article > 正文

【C++】哈希(2万字)

【C++】哈希(2万字)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

前言

unordered系列关联式容器

unordered_map

unordered_map的文档介绍

unordered_map的接口说明

unordered_set

底层结构

哈希概念

哈希冲突

哈希函数

哈希冲突解决

闭散列

线性探测的实现并改造

二次探测

开散列

开散列概念

开散列实现并改造 + 迭代器的实现

开散列增容

开散列与闭散列比较

不同的类型转换成整型的操作

MyOrderedMap.h

MyOrderedSet.h

哈希的应用

位图

位图概念

位图的实现

位图应用

布隆过滤器

布隆过滤器提出

布隆过滤器概念

布隆过滤器的插入

布隆过滤器的查找

布隆过滤器删除

布隆过滤器优点

布隆过滤器缺陷

布隆过滤器的面试题

哈希切割

总结



前言

世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!


提示:以下是本篇文章正文内容,下面案例可供参考

unordered系列关联式容器

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

unordered_map

unordered_map的文档介绍

unordered_map文档介绍

  1. unordered_map是存储键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
  2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
  3. 在内部,unordered_map没有对按照任何特定的顺序排序,为了能在常数范围内找到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迭代器

4. unordered_map的元素访问

函数声明功能介绍
operator[]返回与key对应的value,没有一个默认值

注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中, 将key对应的value返回。

5. unordered_map的查询

函数声明功能介绍
iterator find(const K& key)返回key在哈希桶中的位置
size_t count(const K& key)返回哈希桶中关键码为key的键值对的个数

注意:unordered_map中key是不能重复的,因此count函数的返回值最大为1

6. unordered_map的修改操作

函数声明功能介绍
insert向容器中插入键值对
erase删除容器中的键值对
void clear()清空容器中有效元素个数
void swap(unordered_map&)交换两个容器中的元素

7. unordered_map的桶操作

函数声明功能介绍
size_t bucket count()const返回哈希桶中桶的总个数
size_t bucket size(size_t n) const返回n号桶中有效元素的总个数
size_t bucket(const K& key)返回元素key所在的桶号

unordered_set

unordered_set文档介绍

底层结构

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

哈希概念

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

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

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

当向该结构中:

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

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

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

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

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

哈希冲突

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

比如:5、25、45分别去%20,映射的位置都是5。

哈希函数

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

哈希函数设计原则:

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

常见哈希函数:

1. 直接定址法--(常用)一一映射

  • 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
  • 优点:简单、均匀
  • 缺点:需要事先知道关键字的分布情况
  • 使用场景:适合查找比较小且连续的情况

2. 除留余数法--(常用)

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

3. 平方取中法--(了解)

  • 假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
  • 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
  • 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4. 折叠法--(了解)

  • 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这 几部分叠加求和,并按散列表表长,取后几位作为散列地址。
  • 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

5. 随机数法--(了解)

  • 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数。
  • 通常应用于关键字长度不等时采用此法

6. 数学分析法--(了解)

  • 设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定 相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只 有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散 列地址。例如:

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

哈希冲突解决

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

闭散列

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

1. 线性探测

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

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

插入

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

查找

  • i = key % 表的大小  
  • 如果i为不是要查找的key值,就线性往后查找,直到找到或者遇到空,如果找到表的结尾位置,还没有找到key值,要往头回绕。

删除

  • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影 响。因此线性探测采用标记的伪删除法来删除一个元素。
  1. // 哈希表每个空间给个标记
  2. // EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
  3. enum State{EMPTY, EXIST, DELETE};
线性探测的实现并改造
  1. // 开放定址法
  2. namespace open_address
  3. {
  4. // 状态
  5. enum State
  6. {
  7. EMPTY,
  8. EXIST,
  9. DELETE
  10. };
  11. template<class K, class V>
  12. struct HashData // 类模板名:哈希表的数据是结构体的变量(数据和状态)
  13. {
  14. pair<K, V> _kv;
  15. State _state = EMPTY;
  16. // 标记默认初始化为空,一旦存进去值,标记为存在,删除值之后,标记位删除
  17. };
  18. template<class K>
  19. struct HashFunc // 仿函数:将key转换成整型
  20. {
  21. size_t operator()(const K& key)
  22. {
  23. return (size_t)key;// 不传参数三,默认将key强转成整型
  24. }
  25. };
  26. // 特化 ---> 在实践当中string经常做key,所以做特化
  27. template<>
  28. struct HashFunc<string>
  29. {
  30. size_t operator()(const string& s)
  31. {
  32. size_t hash = 0;
  33. for (auto e : s)
  34. {
  35. hash += e;
  36. hash *= 131;
  37. }
  38. return hash;
  39. }
  40. };
  41. // stoi:只有阿拉伯的字符串数字"1224546"才能用stoi;像"比特"就不能用stoi
  42. // 将字符串强制转换成整型
  43. //struct HashFuncString
  44. //{
  45. // size_t operator()(const string& s)
  46. // {
  47. // // "abcd"
  48. // // "bcad"
  49. // // "aadd"
  50. // size_t hash = 0;
  51. // for (auto e : s)
  52. // {
  53. // // 将字符串中的每个字符ascll码值加起来
  54. // hash += e;
  55. // hash *= 131;// 这样可以避免ascll码值相加相等的情况
  56. // }
  57. //
  58. // return hash;
  59. // }
  60. //};
  61. // 参数三:默认缺省的仿函数Hash,没有传确定的仿函数,就用缺省的发仿函数HashFunc<K>
  62. template<class K, class V, class Hash = HashFunc<K>>
  63. class HashTable // 类模板名:哈希表
  64. {
  65. public:
  66. HashTable(size_t size = 10)
  67. {
  68. _tables.resize(size);// 使用resize的话,size和capcacity就相等了
  69. }
  70. HashData<K, V>* Find(const K& key)
  71. {
  72. Hash hs; // 仿函数的对象
  73. // 线性探测
  74. size_t hashi = hs(key) % _tables.size();
  75. while (_tables[hashi]._state != EMPTY)
  76. {
  77. if (key == _tables[hashi]._kv.first
  78. && _tables[hashi]._state == EXIST)
  79. {
  80. return &_tables[hashi];
  81. }
  82. ++hashi;// 如果++超出size,则取模从头再来
  83. hashi %= _tables.size();
  84. }
  85. return nullptr;
  86. }
  87. bool Insert(const pair<K, V>& kv)
  88. {
  89. // 如果已经有了,就返回false
  90. if (Find(kv.first))
  91. return false;
  92. // 扩容的问题 不强制类型转换成double的话,会有7/10==0的情况
  93. //if ((double)_n / (double)_tables.size() >= 0.7)
  94. if (_n * 10 / _tables.size() >= 7)
  95. {
  96. // 方法一:
  97. //size_t newSize = _tables.size() * 2;
  98. // 不能在原表的空间上扩容空间,因为这样会使映射关系混乱
  99. //vector<HashData> newTables(newSize); // 需要重新开辟一块新空间
  100. 遍历旧表,重新映射到新表,那么就得此处再次写一遍线性探测的代码,再让两个表交换一下
  101. ....
  102. //_tables.swap(newTables);
  103. // 方法二:
  104. HashTable<K, V, Hash> newHT(_tables.size() * 2);
  105. // 遍历旧表,插入到新表
  106. for (auto& e : _tables)
  107. {
  108. if (e._state == EXIST)
  109. {
  110. newHT.Insert(e._kv);
  111. // 这里新表调用Insert()函数,并不会陷入死循环,因为空间*2倍之后,不会再次进入if判断条件了
  112. // 直接复用线性探测的代码
  113. }
  114. }
  115. _tables.swap(newHT._tables);// 交换两表,那么旧表出了作用域就会调用析构函数,旧表数据会被释放
  116. }
  117. Hash hs;
  118. // 线性探测
  119. size_t hashi = hs(kv.first) % _tables.size(); // 除和取模都不能除或取模0
  120. // 这里要模取的是size,而不是capacity;假设表中的capacity和size是不一样的,
  121. // 放值是需要[]的,[]会检查i < size,如果值放在模capacity的那块区间,超出size会越界;
  122. // 所以只能放值在size区间处,放在size和capacity区间,则越界。
  123. while (_tables[hashi]._state == EXIST) // 此位置状态为存在
  124. {
  125. ++hashi;
  126. hashi %= _tables.size();// 模上一个size,走到尾之后,从头再来
  127. }
  128. // 此位置状态为空或被删除
  129. _tables[hashi]._kv = kv;
  130. _tables[hashi]._state = EXIST;
  131. ++_n; // 实际数据个数+1
  132. return true;
  133. }
  134. bool Erase(const K& key)
  135. {
  136. HashData<K, V>* ret = Find(key);
  137. if (ret)
  138. {
  139. _n--;
  140. ret->_state = DELETE; // 直接改状态就相当于删除了
  141. return true;
  142. }
  143. else
  144. {
  145. return false;
  146. }
  147. }
  148. private:
  149. vector<HashData<K, V>> _tables;
  150. size_t _n = 0; // 实际存储的数据个数
  151. };

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

  • 哈希冲突越多,效率就越低。
  • 负载因子/载荷因子 = 实际存进去数据个数/表的大小。
  • 闭散列(开放定址法):负载因子一般会控制在0.7左右。

线性探测优点:实现非常简单。

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

二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法 为:$H_i$ = ($H_0$ + $i^2$ )% m, 或者:$H_i$ = ($H_0$ - $i^2$ )% m。其中:i = 1,2,3…, $H_0$是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表 的大小。

对于下图中如果要插入44,产生冲突,使用解决后的情况为:

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

因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

开散列

开散列概念

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

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

开散列实现并改造 + 迭代器的实现
  1. template<class K>
  2. struct HashFunc // 仿函数:将key转换成整型
  3. {
  4. size_t operator()(const K& key)
  5. {
  6. return (size_t)key;// 不传参数三,默认将key强转成整型
  7. }
  8. };
  9. // 特化 ---> 在实践当中string经常做key,所以做特化
  10. template<>
  11. struct HashFunc<string>
  12. {
  13. size_t operator()(const string& s)
  14. {
  15. size_t hash = 0;
  16. for (auto e : s)
  17. {
  18. hash += e;
  19. hash *= 131;
  20. }
  21. return hash;
  22. }
  23. };
  24. // 哈希桶
  25. namespace hash_bucket
  26. {
  27. // T -> K
  28. // T -> pair<K, V>
  29. template<class T>
  30. struct HashNode
  31. {
  32. HashNode<T>* _next;
  33. T _data;
  34. HashNode(const T& data)
  35. :_next(nullptr)
  36. , _data(data)
  37. {}
  38. };
  39. // 编译器有一个原则:先定义或先声明,再使用。
  40. // 在使用一个变量、类型、函数,要先定义或先声明,再使用。因为编译器为了提高编译速度,有一个原则,
  41. // 比如:在使用一个变量、类型或函数时,编译器只会向上找,不会向下找,只向上找,编译速度会快很多。
  42. // 下面__HTIterator类模板中使用了HashTable<K, T, KeyOfT, Hash>,在上面没有HashTable的定义,
  43. // 所以编译器会报错,因为编译器不认识HashTable。
  44. // 类里面是不受影响的,因为类里面的规则,是在整个类域里面进行查找,编译器把类域当成一个整体。
  45. // 那我们如果把整个HashTable类模板放在__HTIterator类模板之前,也会有问题,
  46. // 因为HashTable类模板中也使用了__HTIterator类型,这个地方就是一个经典的互相引用。
  47. // 那么这时候就只能增加一个前置声明
  48. // 前置声明(声明中不能有缺省值)
  49. template<class K, class T, class KeyOfT, class Hash>
  50. class HashTable;
  51. template<class K, class T, class KeyOfT, class Hash>
  52. struct __HTIterator
  53. {
  54. typedef HashNode<T> Node;
  55. typedef HashTable<K, T, KeyOfT, Hash> HT;
  56. typedef __HTIterator<K, T, KeyOfT, Hash> Self;
  57. Node* _node;
  58. HT* _ht;
  59. __HTIterator(Node* node, HT* ht)
  60. :_node(node)
  61. , _ht(ht)
  62. {}
  63. T& operator*()
  64. {
  65. return _node->_data;
  66. }
  67. T* operator->()
  68. {
  69. return &_node->_data;
  70. }
  71. // 返回的是哈希表中对应的元素
  72. Self& operator++()
  73. {
  74. // 当前哈希表所在位置的桶没有走完
  75. if (_node->_next)
  76. {
  77. // 当前桶还是节点
  78. _node = _node->_next;
  79. }
  80. else
  81. {
  82. // 当前桶走完了,找下一个桶
  83. KeyOfT kot;
  84. Hash hs;
  85. // _tables是HashTable的私有,所以_tables无法使用。我们可以采用友元的方法
  86. size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();
  87. // 找下一个桶
  88. hashi++;
  89. while (hashi < _ht->_tables.size())
  90. {
  91. if (_ht->_tables[hashi])
  92. {
  93. _node = _ht->_tables[hashi];
  94. break;
  95. }
  96. hashi++;
  97. }
  98. // 后面没有桶了
  99. if (hashi == _ht->_tables.size())
  100. {
  101. _node = nullptr;
  102. }
  103. }
  104. return *this;
  105. }
  106. bool operator!=(const Self& s)
  107. {
  108. return _node != s._node;
  109. }
  110. };
  111. // 参数三:仿函数,对于set来说,返回key;对于map来说,返回pair<key,value>中的key
  112. // 参数四:转换成整型的仿函数
  113. template<class K, class T, class KeyOfT, class Hash>
  114. class HashTable
  115. {
  116. // 迭代器想要使用哈希表,就得把迭代器变成哈希表的友元
  117. template<class K, class T, class KeyOfT, class Hash>
  118. friend struct __HTIterator;// 普通类的友元,只有这一行代码;类模板的友元,得把模板参数声明一下
  119. typedef HashNode<T> Node;
  120. public:
  121. typedef __HTIterator<K, T, KeyOfT, Hash> iterator;
  122. iterator begin()
  123. {
  124. for (size_t i = 0; i < _tables.size(); i++)
  125. {
  126. // 找到第一个桶的第一个节点
  127. if (_tables[i])
  128. {
  129. // this就是哈希表对象的地址
  130. return iterator(_tables[i], this);
  131. }
  132. }
  133. // 找不到返回空
  134. return end();
  135. }
  136. iterator end()
  137. {
  138. return iterator(nullptr, this);// 调用的是__HTIterator的构造函数
  139. }
  140. HashTable()
  141. {
  142. _tables.resize(10, nullptr);
  143. _n = 0;
  144. }
  145. // 这里析构的是表中所挂的哈希桶中的节点;vector出了作用域之后会自己调用析构函数
  146. // 哪怕我们自己显示写了析构函数,自定义类型出了作用域也会显示调用析构
  147. ~HashTable()
  148. {
  149. for (size_t i = 0; i < _tables.size(); i++)
  150. {
  151. Node* cur = _tables[i];
  152. while (cur)
  153. {
  154. Node* next = cur->_next;
  155. delete cur;
  156. cur = next;
  157. }
  158. _tables[i] = nullptr;
  159. }
  160. }
  161. pair<iterator, bool> Insert(const T& data)
  162. {
  163. KeyOfT kot;
  164. // 此时Find()函数返回的是迭代器,不能转换成bool值,所以要拿迭代器进行比较
  165. // 之前Find()函数返回的是节点的指针,可以隐式类型转换成bool值
  166. /* if (Find(kot(data)) != end())
  167. return false;*/
  168. iterator it = Find(kot(data));
  169. if (it != end())
  170. return make_pair(it, false);
  171. Hash hs;
  172. // 负载因子到1就扩容
  173. if (_n == _tables.size())
  174. {
  175. // 创建一个新表
  176. vector<Node*> newTables(_tables.size() * 2, nullptr);// 调用HashTable的构造函数
  177. for (size_t i = 0; i < _tables.size(); i++)
  178. {
  179. // 取出旧表中节点,重新计算挂到新表桶中
  180. Node* cur = _tables[i];
  181. while (cur)
  182. {
  183. Node* next = cur->_next;// 保存下一个节点
  184. // 头插到新表
  185. size_t hashi = hs(kot(cur->_data)) % newTables.size();
  186. cur->_next = newTables[hashi];
  187. newTables[hashi] = cur;
  188. cur = next;// 查看下一个节点应该挂到那个桶中
  189. }
  190. _tables[i] = nullptr;// 将旧表置空
  191. }
  192. _tables.swap(newTables);// 交换两表之后,旧表出了作用域就被释放掉
  193. }
  194. size_t hashi = hs(kot(data)) % _tables.size();
  195. Node* newnode = new Node(data);
  196. // 头插
  197. newnode->_next = _tables[hashi];
  198. _tables[hashi] = newnode;
  199. ++_n;
  200. return make_pair(iterator(newnode, this), true);
  201. }
  202. iterator Find(const K& key)
  203. {
  204. KeyOfT kot;
  205. Hash hs;
  206. size_t hashi = hs(key) % _tables.size();
  207. Node* cur = _tables[hashi];
  208. while (cur)
  209. {
  210. if (kot(cur->_data) == key)
  211. {
  212. return iterator(cur, this);
  213. }
  214. cur = cur->_next;
  215. }
  216. return iterator(nullptr, this);
  217. }
  218. bool Erase(const K& key)
  219. {
  220. KeyOfT kot;
  221. Hash hs;
  222. size_t hashi = hs(key) % _tables.size();
  223. Node* prev = nullptr;
  224. Node* cur = _tables[hashi];
  225. while (cur)
  226. {
  227. if (kot(cur->_data) == key)
  228. {
  229. // 删除
  230. if (prev) // 不是桶中的第一个节点
  231. {
  232. prev->_next = cur->_next;
  233. }
  234. else // 是桶中的第一个节点
  235. {
  236. _tables[hashi] = cur->_next;
  237. }
  238. delete cur;
  239. --_n;
  240. return true;
  241. }
  242. prev = cur;
  243. cur = cur->_next;
  244. }
  245. return false;
  246. }
  247. private:
  248. vector<Node*> _tables; // 指针数组
  249. size_t _n;
  250. };
  251. }
开散列增容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可 以给哈希表增容。

开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

不同的类型转换成整型的操作

  1. struct Date
  2. {
  3. int _year;
  4. int _month;
  5. int _day;
  6. };
  7. // 将日期类转换成整型
  8. struct HashFuncDate
  9. {
  10. // 2024/6/3
  11. // 2024/3/6
  12. size_t operator()(const Date& d)
  13. {
  14. size_t hash = 0;
  15. hash += d._year;
  16. hash *= 131;
  17. hash += d._month;
  18. hash *= 131;
  19. hash += d._day;
  20. hash *= 131;
  21. return hash;
  22. }
  23. };
  1. struct Person
  2. {
  3. string _name;
  4. string _id; // 身份证号码
  5. string _tel;
  6. int _age;
  7. string _class;
  8. string _address; //
  9. //...
  10. };
  11. struct HashFuncPerson
  12. {
  13. // 2024/6/3
  14. // 2024/3/6
  15. size_t operator()(const Person& p)
  16. {
  17. size_t hash = 0;
  18. for (auto e : p._id)
  19. {
  20. hash += e;
  21. hash *= 131;
  22. }
  23. return hash;
  24. }
  25. };

MyOrderedMap.h

  1. #include"HashTable.h"
  2. namespace bit
  3. {
  4. template<class K, class V, class Hash = HashFunc<K>>
  5. class unordered_map
  6. {
  7. struct MapKeyOfT
  8. {
  9. const K& operator()(const pair<K, V>& kv)
  10. {
  11. return kv.first;
  12. }
  13. };
  14. public:
  15. typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
  16. iterator begin()
  17. {
  18. return _ht.begin();
  19. }
  20. iterator end()
  21. {
  22. return _ht.end();
  23. }
  24. pair<iterator, bool> insert(const pair<K, V>& kv)
  25. {
  26. return _ht.Insert(kv);
  27. }
  28. // Map要把[]实现出来,就得解决insert(),[]的本质就是insert()
  29. V& operator[](const K& key)
  30. {
  31. pair<iterator, bool> ret = insert(make_pair(key, V()));
  32. return ret.first->second;
  33. }
  34. iterator find(const K& key)
  35. {
  36. return _ht.Find(key);
  37. }
  38. bool erase(const K& key)
  39. {
  40. return _ht.Erase(key);
  41. }
  42. private:
  43. hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
  44. };
  45. void test_map1()
  46. {
  47. unordered_map<string, string> dict;
  48. dict.insert(make_pair("sort", ""));
  49. dict.insert(make_pair("left", ""));
  50. dict.insert(make_pair("right", "?"));
  51. for (auto& kv : dict)
  52. {
  53. //kv.first += 'x';
  54. kv.second += 'y';
  55. cout << kv.first << ":" << kv.second << endl;
  56. }
  57. }
  58. }

MyOrderedSet.h

  1. #include"HashTable.h"
  2. namespace bit
  3. {
  4. template<class K, class Hash = HashFunc<K>>
  5. class unordered_set
  6. {
  7. struct SetKeyOfT
  8. {
  9. const K& operator()(const K& key)
  10. {
  11. return key;
  12. }
  13. };
  14. public:
  15. typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;
  16. iterator begin()
  17. {
  18. return _ht.begin();
  19. }
  20. iterator end()
  21. {
  22. return _ht.end();
  23. }
  24. bool insert(const K& key)
  25. {
  26. return _ht.Insert(key);
  27. }
  28. pair<iterator, bool> find(const K& key)
  29. {
  30. return _ht.Find(key);
  31. }
  32. bool erase(const K& key)
  33. {
  34. return _ht.Erase(key);
  35. }
  36. private:
  37. hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
  38. };
  39. void test_set1()
  40. {
  41. unordered_set<int> us;
  42. us.insert(3);
  43. us.insert(1);
  44. us.insert(5);
  45. us.insert(15);
  46. us.insert(45);
  47. us.insert(7);
  48. unordered_set<int>::iterator it = us.begin();
  49. while (it != us.end())
  50. {
  51. //*it += 100;
  52. cout << *it << " ";
  53. ++it;
  54. }
  55. cout << endl;
  56. int x = 0;
  57. cin >> x;
  58. if (us.find(x) != us.end())
  59. {
  60. cout << "找到了" << endl;
  61. }
  62. else
  63. {
  64. cout << "没有找到" << endl;
  65. }
  66. for (auto e : us)
  67. {
  68. cout << e << " ";
  69. }
  70. cout << endl;
  71. }
  72. }
  1. int a[10];// 静态数组
  2. // 动态数组:malloc或new出来的数组是动态数组

哈希的应用

位图

位图概念

面试题

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。【腾讯】

  1. 遍历,时间复杂度O(N)
  2. 排序(O(NlogN)),利用二分查找: logN
  3. 位图解决 数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0 代表不存在。比如:

位图概念

  • 所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用 来判断某个数据存不存在的。

位图的实现

  1. namespace bit
  2. {
  3. // 用一个非类型模板参数来控制位图要开多大(位图是存在于数组里面的)
  4. template<size_t N>
  5. class bitset
  6. {
  7. public:
  8. bitset()
  9. {
  10. // 假如:N是50个比特位,50除以32是1个整型,还有18个比特位没有开出来,所以要向上取整
  11. // 多开一个整型
  12. _bits.resize(N / 32 + 1, 0);
  13. //cout << N << endl;
  14. }
  15. // 把x映射的位标记成1
  16. void set(size_t x)
  17. {
  18. assert(x <= N);// x不能超出N
  19. size_t i = x / 32;// 计算x在第几个整型上
  20. size_t j = x % 32;// 计算x在这个整型的第几个位上
  21. _bits[i] |= (1 << j);
  22. }
  23. // 把x映射的位标记成0
  24. void reset(size_t x)
  25. {
  26. assert(x <= N);
  27. size_t i = x / 32;
  28. size_t j = x % 32;
  29. _bits[i] &= ~(1 << j);
  30. }
  31. // 检测x映射的标记位是1还是0
  32. bool test(size_t x)
  33. {
  34. assert(x <= N);
  35. size_t i = x / 32;
  36. size_t j = x % 32;
  37. return _bits[i] & (1 << j);
  38. }
  39. private:
  40. vector<int> _bits;
  41. };
  42. void test_bitset()
  43. {
  44. bitset<100> bs1;
  45. bs1.set(50);
  46. bs1.set(30);
  47. bs1.set(90);
  48. for (size_t i = 0; i < 100; i++)
  49. {
  50. if (bs1.test(i))
  51. {
  52. cout << i << "->" << "在" << endl;
  53. }
  54. else
  55. {
  56. cout << i << "->" << "不在" << endl;
  57. }
  58. }
  59. bs1.reset(90);
  60. bs1.set(91);
  61. cout << endl << endl;
  62. for (size_t i = 0; i < 100; i++)
  63. {
  64. if (bs1.test(i))
  65. {
  66. cout << i << "->" << "在" << endl;
  67. }
  68. else
  69. {
  70. cout << i << "->" << "不在" << endl;
  71. }
  72. }
  73. // 这三种方式都可以开42亿9千万个位图大小的空间
  74. bitset<-1> bs2;
  75. bitset<UINT_MAX> bs3;
  76. bitset<0xffffffff> bs4;
  77. }

位图应用

  1. 快速查找某个数据是否在一个集合中
  2. 排序 + 去重
  3. 求两个集合的交集、并集等
  4. 操作系统中磁盘块标记

给定100亿个整数,设计算法找到只出现一次的整数?

思路:出现1次和1次以上的整数需要两个比特位:00 ---> 0次;01 ---> 1次;10 ---> 2次及以上。

代码展示:

  1. template<size_t N>
  2. class two_bit_set
  3. {
  4. public:
  5. void set(size_t x)
  6. {
  7. // 00 -> 01
  8. if (_bs1.test(x) == false
  9. && _bs2.test(x) == false)
  10. {
  11. _bs2.set(x);
  12. }
  13. // 01 -> 10
  14. else if (_bs1.test(x) == false
  15. && _bs2.test(x) == true)
  16. {
  17. _bs1.set(x);
  18. _bs2.reset(x);
  19. }
  20. }
  21. //int test(size_t x)
  22. //{
  23. // if (_bs1.test(x) == false
  24. // && _bs2.test(x) == false)
  25. // {
  26. // return 0;
  27. // }
  28. // else if (_bs1.test(x) == false
  29. // && _bs2.test(x) == true)
  30. // {
  31. // return 1;
  32. // }
  33. // else
  34. // {
  35. // return 2; // 2次及以上
  36. // }
  37. //}
  38. bool test(size_t x)
  39. {
  40. if (_bs1.test(x) == false
  41. && _bs2.test(x) == true)
  42. {
  43. return true;
  44. }
  45. return false;
  46. }
  47. private:
  48. bitset<N> _bs1;// 自定义类型的对象会去调用它的构造函数
  49. bitset<N> _bs2;
  50. };
  51. void test_bitset2()
  52. {
  53. int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };
  54. two_bit_set<100> bs;
  55. for (auto e : a)
  56. {
  57. bs.set(e);
  58. }
  59. for (size_t i = 0; i < 100; i++)
  60. {
  61. //cout << i << "->" << bs.test(i) << endl;
  62. if (bs.test(i))
  63. {
  64. cout << i << endl;
  65. }
  66. }
  67. }

给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

思路:分别set到两个位图,同时为1的就是交集。

1G内存是够的,100亿个整数,并不需要100亿个比特位,因为整数最多42亿9千万个,所以说映射的位图只需要42亿9千万个位,42亿9千万个比特位换算成1G,两个0.5G就是1G。

 1GB是2的30次方,是10亿字节,100亿字节是10G,那么100亿个整型是40G。

代码展示:

  1. void test_bitset3()
  2. {
  3. int a1[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };
  4. int a2[] = { 5,3,5,99,6,99,33,66 };
  5. bitset<100> bs1;
  6. bitset<100> bs2;
  7. for (auto e : a1)
  8. {
  9. bs1.set(e);
  10. }
  11. for (auto e : a2)
  12. {
  13. bs2.set(e);
  14. }
  15. for (size_t i = 0; i < 100; i++)
  16. {
  17. // 寻找交集
  18. if (bs1.test(i) && bs2.test(i))
  19. {
  20. cout << i << endl;
  21. }
  22. }
  23. }

位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

内存当中一般是存不下这些值,这些值都是存在文件里面的。位图不是开40亿,而是按照范围来开的(42亿9千万),因为它的范围是无符号的整数,(0~2^32-1)。

采用两个比特位:00 ---> 0次;01 ---> 1次;10 ---> 2次;11 ---> 3次及以上

给定100亿个整数,只有512M,需要在512M内存中设计算法找到只出现一次的整数?

因为1G是10亿字节,1G是2^30,1G是42亿9千万个比特位,整数的范围最大才到42亿9千万,所以100亿个整数中有大量是重复的数字,所以要在512M内存中查找只出现一次的整数,可以让42亿9千万个整数分成两份,因为512M是是42亿9千万个比特位的一半。

先查找前一半,再查找后一半,映射的过程中就是去重的过程。

布隆过滤器

布隆过滤器提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉 那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用 户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那 些已经存在的记录。 如何快速查找呢?

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理 了。
  3. 将哈希与位图结合,即布隆过滤器

布隆过滤器概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

布隆过滤器的插入

  1. #pragma once
  2. #include<bitset>
  3. #include<string>
  4. struct HashFuncBKDR
  5. {
  6. // BKDR
  7. size_t operator()(const string& s)
  8. {
  9. size_t hash = 0;
  10. for (auto ch : s)
  11. {
  12. hash *= 131;
  13. hash += ch;
  14. }
  15. return hash;
  16. }
  17. };
  18. struct HashFuncAP
  19. {
  20. // AP
  21. size_t operator()(const string& s)
  22. {
  23. size_t hash = 0;
  24. for (size_t i = 0; i < s.size(); i++)
  25. {
  26. if ((i & 1) == 0) // 偶数位字符
  27. {
  28. hash ^= ((hash << 7) ^ (s[i]) ^ (hash >> 3));
  29. }
  30. else // 奇数位字符
  31. {
  32. hash ^= (~((hash << 11) ^ (s[i]) ^ (hash >> 5)));
  33. }
  34. }
  35. return hash;
  36. }
  37. };
  38. struct HashFuncDJB
  39. {
  40. // DJB
  41. size_t operator()(const string& s)
  42. {
  43. size_t hash = 5381;
  44. for (auto ch : s)
  45. {
  46. hash = hash * 33 ^ ch;
  47. }
  48. return hash;
  49. }
  50. };
  51. // 参数三:三个哈希仿函数的个数,表示一个值能映射3个位
  52. template<size_t N,
  53. class K = string,
  54. class Hash1 = HashFuncBKDR,
  55. class Hash2 = HashFuncAP,
  56. class Hash3 = HashFuncDJB>
  57. class BloomFilter
  58. {
  59. public:
  60. void Set(const K& key)
  61. {
  62. // 比如:插入第一个数,映射0~M-1的比特位区间
  63. // 一个值要映射到三个比特位上,为了减少冲突
  64. size_t hash1 = Hash1()(key) % M;
  65. size_t hash2 = Hash2()(key) % M;
  66. size_t hash3 = Hash3()(key) % M;
  67. _bs->set(hash1);
  68. _bs->set(hash2);
  69. _bs->set(hash3);
  70. }
  71. // 这里不需要写reset()删除函数,因为删除百度,腾讯判断也可能不在了。因为百度和腾讯可能会映射到同一个位置
  72. bool Test(const K& key)
  73. {
  74. // 值映射的三个比特位上,只要有一个比特位为0,就是该值不在哈希表中
  75. size_t hash1 = Hash1()(key) % M;
  76. if (_bs->test(hash1) == false)
  77. return false;
  78. size_t hash2 = Hash2()(key) % M;
  79. if (_bs->test(hash2) == false)
  80. return false;
  81. size_t hash3 = Hash3()(key) % M;
  82. if (_bs->test(hash3) == false)
  83. return false;
  84. return true; // 存在误判(有可能3个位都是跟别人冲突的,所以误判)
  85. }
  86. private:
  87. // const size_t M = 10 * N;
  88. // 我们不能用这种成员变量,因为这个成员变量是属于对象的,只是声明,没有空间,只在初始化列表才会初始化
  89. // 加一个静态static就可以了,那么这个变量就在静态区,就不属于对象了,而是属于整个类
  90. // N:比特位。插入一个整数,也就是一个整数映射一个比特位,比特位扩容10倍的N
  91. static const size_t M = 10 * N; // 想降低误判率:可以增大比特位的空间
  92. bit::bitset<M> _bs;
  93. // 如果就是想要使用库里面的bitset,可以new在堆区开辟一个std::bitset<M>类型的空间,将空间的地址给_bs
  94. //std::bitset<M>* _bs = new std::bitset<M>;
  95. };
  96. // 库里面的stl::bitset<M>类型所开辟的空间是开在对象里面的,这个对象是一个静态数组
  97. // 我们自己用vector<>实现的bitset是调用resize()函数开辟空间是在堆上的
  98. void TestBloomFilter1()
  99. {
  100. string strs[] = { "百度","字节","腾讯" };// 中文是由多个字符构成的
  101. BloomFilter<10> bf;
  102. for (auto& s : strs)
  103. {
  104. bf.Set(s);
  105. }
  106. for (auto& s : strs)
  107. {
  108. cout << bf.Test(s) << endl;
  109. }
  110. for (auto& s : strs)
  111. {
  112. cout << bf.Test(s + 'a') << endl;
  113. }
  114. cout << bf.Test("摆渡") << endl;
  115. cout << bf.Test("百渡") << endl;
  116. }

布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特 位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为 零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。

注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可 能存在,因为有些哈希函数存在一定的误判。

比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其 他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

每个位置改成多个位的引用计数就可以支持。比如:一个映射位置给8个bit标记,但是这样空间的消耗就大了。

布隆过滤器优点

  1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无 关
  2. 哈希函数相互之间没有关系,方便硬件并行运算
  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
  5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
  6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

布隆过滤器缺陷

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再 建立一个白名单,存储可能会误判的数据)
  2. 不能获取元素本身
  3. 一般情况下不能从布隆过滤器中删除元素
  4. 如果采用计数方式删除,可能会存在计数回绕问题

布隆过滤器的面试题

给两个文件,分别有100亿个query(字符串),我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法?

小文件在找交集是没有误判的,因为已经读到内存当中了,不需要在使用布隆过滤器,直接将文件中的数据放到底层为哈希表或红黑树的容器中。

之前的算法要用布隆过滤器,因为数据在数据库中,都去数据库中查找太慢了,所以用布隆过滤,会效率高。

哈希切割

给一个超过100G大小的log file, log中存着IP地址,设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?

如果是top K ,就自己建立一个小堆,默认是大堆,我们还得写一个仿函数,因为不能用pair<string,int>类型比,我们要用pair<string,int>类型中的second来进行比较,控制成一个K个数的小堆。

海量数据问题特征:数据量大,内存存不下。

  1. 先考虑具有特点的数据结构能否解决?比如:位图、堆、布隆过滤器等。
  2. 大事化小思路。哈希切分(不能平均切分),切小以后,放到内存中能处理。

总结

好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。

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

闽ICP备14008679号