• C++进阶篇5-哈希


    一、unordered系列关联式容器

    在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到log_2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最优的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器( unordered_set / unordered_multiset / unordered_map / unordered_multimap ),这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同
    这里就简单介绍一下unordered_map的常用函数,其他的用法类似,就不多介绍了
    bool empty() const检测unordered_map是否为空
    size_t size() const获取unordered_map的有效元素个数
    begin()
      返回unordered_map第一个元素的迭代器  
    end()返回unordered_map最后一个元素的迭代器
    operator[]
    返回与key对应的value,没有一个默认值, 该函数只有unordered_map有
    iterator find(const K&key)返回key在哈希桶中的位置
    size_t count(const K& key)
    返回哈希桶中关键码为key的键值对的个数
    insert
    erase
    void clear()清空有效元素

    二、底层结构

    1、哈希的概念

    无论是顺序结构还是平衡树,只要数据量变大,就不可避免的会增加查找的时间,即增加了比较关键字的次数,那么我们能不能通过一次对比就直接找到元素呢?即通过所给的关键字直接找到想要的元素

    最理想的方式:直接在key-value之间建立某种函数映射关系,使得每个key只对应一个value,那么我们就能根据所给的key一步找到value

    当向该结构中

    • 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
    • 搜索元素 :对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
    该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
    为哈希表(Hash Table)(或者称散列表)
    一般用取模运算将数组下标和关键字建立联系,如下

    但是这个哈希表结构是存在一定问题的,比如当我们再往里面插入一个24,就会发现下标为4的位置已经有值了,这个现象叫做哈希冲突或哈希碰撞即不同关键字通过相同哈希函数计算出相同的哈希地址

    2、哈希函数

    引起哈希冲突的一个原因可能是:哈希函数设计不够合理
    哈希函数设计原则
    • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有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. 数学分析法--(了解)
    注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

    3、哈希冲突的解决:闭散列和开散列


    闭散列

    也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
    空位置,那么可以把key存放到冲突位置中的"下一个"空位置中去
    1.线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
    插入:
            就拿上面的图来说,如果再向里面加24,由于该位置已经被占用,就只能一个一个往后走,看有没有空位置,最后放在下标为8的位置
    删除:
             采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,24查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。即需要三个状态来描述一个位置
    线性探测的实现---里面有很多的细节值得深究,感兴趣的可以自己去实现看看
    1. //该仿函数作用将各种其他类型的值转化成整形
    2. template<class K>
    3. struct HashFun
    4. {
    5. size_t operator()(const K& key)
    6. {
    7. return (size_t)key;
    8. }
    9. };
    10. template<>
    11. struct HashFun
    12. {
    13. size_t operator()(const string& key)
    14. {
    15. size_t hash = 0;
    16. for (auto& e : key)
    17. {
    18. hash *= 31;
    19. hash += e;
    20. }
    21. return hash;
    22. }
    23. };
    24. namespace zxws
    25. {
    26. enum Status {
    27. EMPTY,
    28. DELETED,
    29. EXIST
    30. };
    31. template<class K, class V>
    32. struct HashData {
    33. pair_kv;
    34. Status _st = EMPTY;
    35. HashData() = default;
    36. HashData(const pair& kv)
    37. :_st(EXIST)
    38. , _kv(kv)
    39. {}
    40. };
    41. template <class K, class V, class Hash = HashFun>
    42. class HashTable {
    43. public:
    44. HashTable() :_table(10) {}
    45. bool Insert(const pair& kv)
    46. {
    47. if (Find(kv.first))
    48. return false;
    49. if (_n * 10 / _table.size() == 7)//设置的负荷因子为0.7(具体是什么,下面会讲),if条件这样写是为了方便计算
    50. {
    51. size_t newSize = _table.size() * 2;
    52. HashTablenewHT;
    53. newHT._table.resize(newSize);
    54. for (int i = 0; i < _table.size(); i++)
    55. {
    56. if (_table[i]._st == EXIST)
    57. {
    58. newHT.Insert(_table[i]._kv);//这里直接复用Insert函数,不是递归,本质是两个对象调用该函数
    59. }
    60. }
    61. _table.swap(newHT._table);
    62. }
    63. Hash hf;
    64. size_t hashi = hf(kv.first) % _table.size();
    65. while (_table[hashi]._st == EXIST)
    66. {
    67. hashi++;
    68. hashi %= _table.size();
    69. }
    70. _table[hashi]._kv = kv;
    71. _table[hashi]._st = EXIST;
    72. _n++;
    73. return true;
    74. }
    75. HashData* Find(const K& key)
    76. {
    77. Hash hf;
    78. size_t hashi = hf(key) % _table.size();
    79. while (_table[hashi]._st != EMPTY)
    80. {
    81. if (_table[hashi]._st == EXIST
    82. && _table[hashi]._kv.first == key)
    83. {
    84. return &_table[hashi];
    85. }
    86. hashi++;
    87. hashi %= _table.size();
    88. }
    89. return nullptr;
    90. }
    91. bool Erase(const K& key)
    92. {
    93. HashData* ret = Find(key);
    94. if (ret->_st == EXIST)
    95. {
    96. ret->_st = DELETED;
    97. _n--;
    98. return true;
    99. }
    100. return false;
    101. }
    102. void Print()//测试用的函数
    103. {
    104. for (size_t i = 0; i < _table.size(); i++)
    105. {
    106. if (_table[i]._st == EXIST)
    107. cout << i << "->" << _table[i]._kv.first << endl;
    108. else if (_table[i]._st == DELETED)
    109. cout << i << "->" << "D" << endl;
    110. else
    111. cout << i << "->" << endl;
    112. }
    113. }
    114. private:
    115. vector>_table;
    116. size_t _n = 0;
    117. };
    118. }

    2.二次探测:即以1^2,2^2,3^2……往后找空位,防止出现堆积再一起的情况 


    开散列

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

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

    开散列的实现

    1. namespace zxws
    2. {
    3. template<class K,class V>
    4. struct HashNode
    5. {
    6. HashNode* next;
    7. pair_kv;
    8. HashNode(const pair&kv)
    9. :next(nullptr)
    10. ,_kv(kv)
    11. {}
    12. };
    13. template<class K, class V,class Hash=HashFun>//HashFun这个函数在闭散列的实现代码的最开始
    14. class HashTable
    15. {
    16. typedef HashNode Node;
    17. public:
    18. HashTable()
    19. {
    20. _table.resize(10);
    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. }
    34. }
    35. bool Insert(const pair& kv)
    36. {
    37. if (Find(kv.first))
    38. return false;
    39. Hash hf;
    40. if (_n == _table.size())//增容的条件在下面会讲
    41. {
    42. //复用之前的结点
    43. size_t sz = _table.size()*2;
    44. vectornewTable;
    45. newTable.resize(sz);
    46. for (size_t i = 0; i < _table.size(); i++)
    47. {
    48. Node* cur = _table[i];
    49. while (cur)
    50. {
    51. Node* next = cur->next;
    52. size_t hashi = hf(cur->_kv.first) % sz;//需要重新计算哈希地址
    53. cur->next = newTable[hashi];
    54. newTable[hashi] = cur;
    55. cur = next;
    56. }
    57. _table[i] = nullptr;
    58. }
    59. _table.swap(newTable);
    60. }
    61. size_t hashi = hf(kv.first) % _table.size();
    62. Node* newnode = new Node(kv);
    63. newnode->next = _table[hashi];
    64. _table[hashi] = newnode;
    65. ++_n;
    66. }
    67. Node* Find(const K& key)
    68. {
    69. Hash hf;
    70. size_t hashi = hf(key) % _table.size();
    71. Node* cur = _table[hashi];
    72. while (cur)
    73. {
    74. if (cur->_kv.first == key)
    75. return cur;
    76. cur = cur->next;
    77. }
    78. return nullptr;
    79. }
    80. bool Erase(const K& key)
    81. {
    82. Hash hf;
    83. size_t hashi = hf(key) % _table.size();
    84. Node* cur = _table[hashi];
    85. Node* prev = nullptr;
    86. while (cur)
    87. {
    88. if (cur->_kv.first == key)
    89. {
    90. if (prev == nullptr)
    91. _table[hashi] = cur->next;
    92. else
    93. prev->next = cur->next;
    94. delete cur;
    95. --_n;
    96. return true;
    97. }
    98. cur = cur->next;
    99. }
    100. return false;
    101. }
    102. private:
    103. vector_table;
    104. size_t _n;//有效元素个数
    105. };
    106. }
    开散列增容
    桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可
    能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
    表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,
    再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可
    以给哈希表增容。
    HashFun函数的作用
    因为我们写的哈希表的哈希函数为取模运算,只能是整形才能进行计算,所以如果是替他类型,我们就需要先将它转换成一个整形值,HashFun的作用就是将不同类型的值转成整形,我只写了string和一般的整形类型,如果是其他的自定义类型需要自己写仿函数,传入类模板
    开散列和闭散列相比
    • 优点1:不会出现不同的关键字哈希之后的地址相互影响的情况,就比如找24,闭散列要找5次,开散列只要2次,即查找速率加快
    • 优点2:链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

    三、unordered_set和unordered_map的模拟实现(与STL中的实现有所差异)

    1.底层结构HashTable

    1. template<class K>
    2. struct HashFun
    3. {
    4. size_t operator()(const K& key)
    5. {
    6. return (size_t)key;
    7. }
    8. };
    9. template<>
    10. struct HashFun
    11. {
    12. size_t operator()(const string& key)
    13. {
    14. size_t hash = 0;
    15. for (auto& e : key)
    16. {
    17. hash *= 31;
    18. hash += e;
    19. }
    20. return hash;
    21. }
    22. };
    23. namespace zxws
    24. {
    25. //函数模板的前置声明
    26. template<class K, class T, class Hash, class KeyOfData>
    27. class HashBucket;
    28. template <class T>
    29. struct HashNode
    30. {
    31. HashNode* next;
    32. T _data;
    33. HashNode(const T& data)
    34. :next(nullptr)
    35. , _data(data)
    36. {}
    37. };
    38. template<class K,class T, class Ref, class Ptr, class Hash, class KeyOfData>
    39. struct Iterator
    40. {
    41. typedef Iterator Self;
    42. HashNode* _cur;
    43. size_t _hashi;
    44. const HashBucket* _phb;//
    45. Iterator(HashNode*cur, size_t hashi, const HashBucket* phb)//
    46. :_cur(cur)
    47. ,_hashi(hashi)
    48. ,_phb(phb)
    49. {}
    50. Iterator(const Self& t)
    51. :_cur(t._cur)
    52. , _hashi(t._hashi)
    53. , _phb(t._phb)
    54. {}
    55. Self& operator++()
    56. {
    57. if (_cur->next)
    58. {
    59. _cur = _cur->next;
    60. return *this;
    61. }
    62. else
    63. {
    64. _hashi++;
    65. while (_hashi<_phb->_ht.size())//
    66. {
    67. if (_phb->_ht[_hashi])
    68. {
    69. _cur = _phb->_ht[_hashi];
    70. return *this;
    71. }
    72. _hashi++;
    73. }
    74. _cur = nullptr;//
    75. //_hashi = -1;
    76. return *this;
    77. }
    78. }
    79. bool operator==(const Self& t) const
    80. {
    81. return _cur == t._cur;
    82. }
    83. bool operator!=(const Self& t) const
    84. {
    85. return _cur != t._cur;
    86. }
    87. Self operator++(int)
    88. {
    89. Iterator it(*this);
    90. this->operator++();
    91. return it;
    92. }
    93. Ref operator*()
    94. {
    95. return _cur->_data;
    96. }
    97. Ptr operator->()
    98. {
    99. return &_cur->_data;
    100. }
    101. };
    102. template<class K,class T,class Hash,class KeyOfData>
    103. class HashBucket {
    104. //类模板的友元声明
    105. template<class K, class T,class Ref,class Ptr,class Hash, class KeyOfData>
    106. friend struct Iterator;
    107. public:
    108. typedef HashNode Node;
    109. typedef Iterator iterator;
    110. typedef Iteratorconst T&, const T*, Hash, KeyOfData> const_iterator;
    111. HashBucket():_ht(10) {}
    112. iterator begin()
    113. {
    114. for (size_t i = 0; i < _ht.size(); i++)
    115. {
    116. if (_ht[i])
    117. return iterator(_ht[i], i, this);
    118. }
    119. return end();
    120. }
    121. iterator end()
    122. {
    123. return iterator(nullptr, -1, this);
    124. }
    125. const_iterator begin() const
    126. {
    127. for (size_t i = 0; i < _ht.size(); i++)
    128. {
    129. if (_ht[i])
    130. return const_iterator(_ht[i], i, this);
    131. }
    132. return end();
    133. }
    134. const_iterator end() const
    135. {
    136. return const_iterator(nullptr, -1, this);
    137. }
    138. void swap(const HashBucket& tmp)
    139. {
    140. std::swap(_ht, tmp._ht);
    141. std::swap(_n, tmp._n);
    142. }
    143. pairbool> insert(const T& data)
    144. {
    145. Hash hf;
    146. KeyOfData kod;
    147. auto ret = find(kod(data));
    148. if (ret!=end())
    149. return make_pair(ret,false);
    150. if (_n == _ht.size())
    151. {
    152. vectorv(2 * _ht.size());
    153. for (size_t i = 0; i < _ht.size(); i++)
    154. {
    155. Node* cur = _ht[i];
    156. while (cur)
    157. {
    158. Node* next = cur->next;
    159. size_t hashi = hf(kod(cur->_data)) % v.size();
    160. cur->next = v[hashi];
    161. v[hashi] = cur;
    162. cur = next;
    163. }
    164. _ht[i] = nullptr;
    165. }
    166. _ht.swap(v);
    167. }
    168. size_t hashi = hf(kod(data)) % _ht.size();
    169. Node* newnode = new Node(data);
    170. newnode->next = _ht[hashi];
    171. _ht[hashi] = newnode;
    172. _n++;
    173. return make_pair(iterator(newnode, hashi, this), false);
    174. }
    175. iterator find(const K& key)
    176. {
    177. Hash hf;
    178. KeyOfData kod;
    179. size_t hashi = hf(key) % _ht.size();
    180. Node* cur = _ht[hashi];
    181. while (cur)
    182. {
    183. if (kod(cur->_data) == key)
    184. return iterator(cur, hashi, this);
    185. cur = cur->next;
    186. }
    187. return end();
    188. }
    189. bool erase(const K& key)
    190. {
    191. Hash hf;
    192. KeyOfData kod;
    193. size_t hashi = hf(key) % _ht.size();
    194. Node* cur = _ht[hashi];
    195. Node* pre = nullptr;
    196. while (cur)
    197. {
    198. if (kod(cur->_data) == key)
    199. {
    200. if (pre == nullptr)
    201. {
    202. _ht[hashi] = cur->next;
    203. }
    204. else
    205. {
    206. pre->next = cur->next;
    207. }
    208. delete cur;
    209. _n--;
    210. return true;
    211. }
    212. pre = cur;
    213. cur = cur->next;
    214. }
    215. return false;
    216. }
    217. iterator erase(iterator pos)
    218. {
    219. if (pos == end())
    220. return pos;
    221. KeyOfData kod;
    222. Node* cur = pos._cur;
    223. ++pos;
    224. erase(kod(cur->_data));
    225. return pos;
    226. }
    227. const_iterator erase(const_iterator pos)
    228. {
    229. if (pos._cur == nullptr)
    230. return pos;
    231. KeyOfData kod;
    232. Node* cur = pos._cur;
    233. ++pos;
    234. erase(kod(cur->_data));
    235. return pos;
    236. }
    237. size_t size()
    238. {
    239. return _n;
    240. }
    241. bool empty()
    242. {
    243. return _n == 0;
    244. }
    245. size_t bucket_count()
    246. {
    247. size_t cnt = 0;
    248. for (size_t i = 0; i < _ht.size(); i++)
    249. {
    250. if (_ht[i])
    251. cnt++;
    252. }
    253. return cnt;
    254. }
    255. size_t bucket_size(const K& key)
    256. {
    257. Hash hf;
    258. size_t cnt = 0;
    259. size_t hashi = hf(key);
    260. for (Node* cur = _ht[hashi]; cur; cur = cur->next)
    261. cnt++;
    262. return cnt;
    263. }
    264. private:
    265. vector _ht;
    266. size_t _n = 0;
    267. };
    268. }

     2.unordered_set的封装

    1. namespace zxws
    2. {
    3. template <class K,class Hash = HashFun>
    4. class unordered_set {
    5. struct KeyOfData
    6. {
    7. const K& operator()(const K& key)
    8. {
    9. return key;
    10. }
    11. };
    12. public:
    13. typedef typename HashBucket::const_iterator iterator;
    14. typedef typename HashBucket::const_iterator const_iterator;
    15. pairbool> insert(const K& data)
    16. {
    17. auto ret=_hb.insert(data);
    18. return make_pair(iterator(ret.first._cur, ret.first._hashi, ret.first._phb), ret.second);
    19. }
    20. iterator begin() const
    21. {
    22. return _hb.begin();
    23. }
    24. iterator end() const
    25. {
    26. return _hb.end();
    27. }
    28. iterator find(const K& key)
    29. {
    30. auto ret = _hb.find(key);
    31. return iterator(ret._cur, ret._hashi, ret._phb);
    32. }
    33. bool erase(const K& key)
    34. {
    35. return _hb.erase(key);
    36. }
    37. iterator erase(iterator pos)
    38. {
    39. return _hb.erase(pos);
    40. }
    41. size_t bucket_count()
    42. {
    43. return _hb.BucketCount();
    44. }
    45. size_t bucket_size(const K& key)
    46. {
    47. return _hb.BucketSize(key);
    48. }
    49. size_t size()const
    50. {
    51. return _hb.size();
    52. }
    53. bool empty()const
    54. {
    55. return _hb.empty();
    56. }
    57. private:
    58. HashBucket_hb;
    59. };
    60. }

     3.unordered_map的封装

    1. namespace zxws
    2. {
    3. template <class K,class V,class Hash=HashFun>
    4. class unordered_map {
    5. struct KeyOfData {
    6. const K& operator()(const pair& kv)
    7. {
    8. return kv.first;
    9. }
    10. };
    11. public:
    12. typedef typename HashBucketconst K, V>, Hash, KeyOfData>::iterator iterator;
    13. typedef typename HashBucketconst K, V>, Hash, KeyOfData>::const_iterator const_iterator;
    14. pairbool> insert(const pair& data)
    15. {
    16. return _hb.insert(data);
    17. }
    18. V& operator[](const K& key)
    19. {
    20. auto ret = _hb.insert(make_pair(key, V()));
    21. return ret.first->second;
    22. }
    23. iterator begin()
    24. {
    25. return _hb.begin();
    26. }
    27. iterator end()
    28. {
    29. return _hb.end();
    30. }
    31. const_iterator begin() const
    32. {
    33. return _hb.begin();
    34. }
    35. const_iterator end() const
    36. {
    37. return _hb.end();
    38. }
    39. iterator find(const K& key)
    40. {
    41. return _hb.find(key);
    42. }
    43. bool erase(const K& key)
    44. {
    45. return _hb.erase(key);
    46. }
    47. iterator erase(iterator pos)
    48. {
    49. return _hb.erase(pos);
    50. }
    51. size_t bucket_count()
    52. {
    53. return _hb.BucketCount();
    54. }
    55. size_t bucket_size(const K& key)
    56. {
    57. return _hb.BucketSize(key);
    58. }
    59. size_t size()const
    60. {
    61. return _hb.size();
    62. }
    63. bool empty()const
    64. {
    65. return _hb.empty();
    66. }
    67. private:
    68. HashBucketconst K,V>,Hash, KeyOfData>_hb;
    69. };
    70. }
  • 相关阅读:
    nohup安装和用法
    SpringBoot 配置文件加载优先级
    CentOS8中文乱码问题
    Python爬虫入门教程
    分成两栏后文字顺序混乱的问题解决【写期刊论文时】
    1.5.4 HDFS 客户端操作-hadoop-最全最完整的保姆级的java大数据学习资料
    js鼠标事件详解
    【示波器专题】示波器的频响方式
    ES6展开运算符—— 通俗易懂
    【无标题】
  • 原文地址:https://blog.csdn.net/V_zjs/article/details/134530726