C++:哈希
哈希
unordered_set和unordered_map
unordered_set

set和unordered_set的功能高度相似,只是底层结构不同。
unordered_set与set的差异
- unordered_set和set的第⼀个差异是对key的要求不同,set要求Key支持小于比较,而unordered_set要求Key支持转成整形且支持等于比较。
- unordered_set和set的第⼆个差异是迭代器的差异,set的iterator是双向迭代器,unordered_set是单向迭代器,其次set底层是红黑树,红黑树是⼆叉搜索树,⾛中序遍历是有序的,所以set迭代器遍历是有序+去重。而unordered_set底层是哈希表,迭代器遍历是无序+去重。
- unordered_set和set的第三个差异是性能的差异,整体而言大多数场景下,unordered_set的增删查改更快一些,因为红黑树增删查改效率是O(logN) ,而哈希表增删查平均效率是O(1) 。
unordered_map

map和unordered_map也是高度相似,只有些许差异。
- map要求Key支持小于比较,而unordered_map要求Key支持转成整形且支持等于比较。
- map的iterator是双向迭代器,unordered_map是单向迭代器,map底层是红黑树,unordered_map底层是哈希表,迭代器遍历是 Key无序+去重。
- 大多数场景下,unordered_map的增删查改更快一些,因为红黑树增删查改效率是O(logN) ,而哈希表增删查平均效率是 O(1) 。
代码使用
#define_CRT_SECURE_NO_WARNINGS#include<iostream>#include<unordered_set>#include<unordered_map>usingnamespace std;intmain(){ unordered_set<int> s ={3,1,6,7,8,2,1,1,5,6,7,6}; unordered_set<int>::iterator it = s.begin();while(it != s.end()){ cout <<*it <<" ";++it;} cout << endl;return0;}
unordered_multiset/unordered_multimap
unordered_multiset/unordered_multimap 与 multiset/multimap也是高度相似,支持Key冗余。差异也如上。
哈希概念
哈希(hash)又称散列,是⼀种组织数据的方式。本质就是通过哈希函数把关键字Key跟存储位置建立一个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进行快速查找。
直接定址法
当关键字的范围比较集中时,如一组关键字都在[0,99]之间,那么开一个100个数的数组,每个关键字的值直接就是存储位置的下标。再比如一组关键字值都在[a,z]的小写字母,那么我们开⼀个26个数的数组,每个关键字ASCII码 - 字符a 得到的值就是存储位置的下标。也就是说直接定址法本质即用关键字计算出一个绝对位置 或者 相对位置 。(比如计数排序)
哈希冲突
使用直接定址法,当关键字的范围比较分散时,会很浪费内存甚至内存不够用。假设我们只有数据范围是[0, 9999]的N个值,要映射到⼀个M个空间的数组中(⼀般M >= N),那么就要借助哈希函数h(x),关键字key被放到数组的h(key)位置,注意,h(key)计算出的值必须在[0, M)之间。
这里存在的⼀个问题是,两个不同的key可能会映射到同一个位置,这种问题即哈希冲突,或者哈希碰撞。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的方案。
负载因子
假设哈希表中已经映射存储了N个值,哈希表的大小为M,那么 负载因子=N/M 。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低 。
将关键字转为size_t
将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换成整数。源码中用仿函数Hash把key转换成size_t 。如果遇到像string、Date无法强转的,则自己实现,string较为常用,可以特化模板。
template<classK>structHashFunc{ size_t operator()(const K& key){return(size_t)key;}};template<>structHashFunc<string>{ size_t operator()(const string& s){ size_t hash =0;for(auto& e : s)//可以有效避免"abcd","bcad"冲突的情况{ hash += e; hash *=131;}return hash;}};哈希函数
⼀个好的哈希函数应该让N个关键字被等概率且均匀地散列分布到哈希表的M个空间中,实际中很难做到,但是要尽量往这个方向去考量设计。
除法散列法/除留余数法
除法散列法也叫除留余数法,假设哈希表的大小为M,key除以M的余数作为映射位置的下标,即哈希函数为:h(key) = key % M 。
使用除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是 2x,key % 2x 本质相当于保留key的后X位(把key转成二进制表示的位),那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。如果是 10x,保留的都是10进值的后x位,如:{112, 12312},如果M是100,也就是102 ,那么计算出的哈希值都是12。
所以,使用除法散列法时,建议M取不太接近2的整数次幂的一个素数。
sgi版本的哈希表使用的方法,给了一个近似2倍的素数表。
inlineunsignedlong__stl_next_prime(unsignedlong n)//会频繁调用,inline修饰,减少消耗{// Note: assumes long is at least 32 bits.staticconstint __stl_num_primes =28;staticconstunsignedlong __stl_prime_list[__stl_num_primes]={//一个数组53,97,193,389,769,1543,3079,6151,12289,24593,49157,98317,196613,393241,786433,1572869,3145739,6291469,12582917,25165843,50331653,100663319,201326611,402653189,805306457,1610612741,3221225473,4294967291};constunsignedlong* first = __stl_prime_list;constunsignedlong* last = __stl_prime_list + __stl_num_primes;constunsignedlong* pos =lower_bound(first, last, n);//在下标[first,last)范围内,找大于等于n的数return pos == last ?*(last -1):*pos;}初始时哈希表的大小M可以为 __stl_next_prime(0),即M=53,此时M既不接近25,也不接近26,同时也是素数。扩容后M=97,也实现了近似2倍扩容。
哈希函数还有其他方法,如乘法散列法、全域散列法等。
处理哈希冲突:开放定址法
在开放定址法中,所有的元素都放到哈希表,当⼀个关键字key用哈希函数计算出的位置冲突了,则按照某种规则找到一个没有存储数据的位置进行存储。规则有三种:线性探测、⼆次探测、双重探测。开放定址法要求负载因子小于1。
线性探测
- 从发生冲突的位置开始,依次线性向后探测,直到寻找到下一个没有存储数据的位置为止,如果走到哈希表尾,则回绕到哈希表头的位置。
- h(key)=hash0=key % M,hash0位置冲突了,则线性探测公式为:hc(key,i)=hashi=(hash0+i) % M,i={1,2,3……M-1} 。由于负载因子小于1,哈希表中一定有空位置,所以最多探测M-1次,一定能找到一个存储key的位置。
- 如果hash0位置连续冲突,hash0,hash1,hash2位置已经存储数据,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位置,这种现象叫做群集/堆积。二次探测可在一定程度上改善。

二次探测
- 从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下⼀个没有存储数据的位置为止,如果往右走到哈希表尾,则回绕到哈希表头的位置;如果往左走到哈希表头,则回绕到哈希表尾的位置。
- h(key)=hash0=key % M,hash0位置冲突了,则二次探测公式为:hc(key,i)=hashi=(hash0 ± i2) % M,i={1,2,3……M/2} 。
- 当hashi=(hash0 - i2) % M时,若hashi<0,则hashi+=M 。

开放定址法线性探测代码实现
要给每个存储位置加一个状态标识,否则删除一些值以后,会影响后面冲突的值的查找。例如删除30,查找20,用哈希函数求得映射下标为9,但是下标为9的位置已经为空,没找到,但哈希表中其实又是有20的。
给每个存储位置加一个标识状态**{EXIST,DELETE,EMPTY}。查找一个key,求得映射位置h(key),发现状态为EMPTY,说明整个哈希表中没有key**。如果哈希表的其他位置存在key,表明因为冲突,h(key)位置已经被别的值占了,则h(key)位置的状态应是EXIST或DELETE,矛盾,所以整个哈希表中绝对没有key。

控制负载因子到0.7时扩容。
如果key不能取模则用仿函数转成size_t。
#include<vector>enumState{ EXIST, EMPTY, DELETE };template<classK,classV>structHashData{ pair<K, V> _kv; State _state = EMPTY;};template<classK>structHashFunc{ size_t operator()(const K& key){return(size_t)key;}};template<>structHashFunc<string>{ size_t operator()(const string& s){ size_t hash =0;for(auto& e : s){ hash += e; hash *=131;}return hash;}};inlineunsignedlong__stl_next_prime(unsignedlong n){// Note: assumes long is at least 32 bits.staticconstint __stl_num_primes =28;staticconstunsignedlong __stl_prime_list[__stl_num_primes]={//一个数组53,97,193,389,769,1543,3079,6151,12289,24593,49157,98317,196613,393241,786433,1572869,3145739,6291469,12582917,25165843,50331653,100663319,201326611,402653189,805306457,1610612741,3221225473,4294967291};constunsignedlong* first = __stl_prime_list;constunsignedlong* last = __stl_prime_list + __stl_num_primes;constunsignedlong* pos =lower_bound(first, last, n);return pos == last ?*(last -1):*pos;}namespace open_address {template<classK,classV,classHash= HashFunc<K>>classHashTable{public:HashTable():_tables(__stl_next_prime(0)),_n(0){}boolInsert(const pair<K, V>& kv){if(Find(kv.first))returnfalse;if(_n *10/ _tables.size()>=7)//扩容{ HashTable<K, V, Hash> newht; newht._tables.resize(__stl_next_prime(_tables.size()+1));for(auto& e : _tables){if(e._state == EXIST) newht.Insert(e._kv);} _tables.swap(newht._tables);} Hash hash; size_t hash0 =hash(kv.first)% _tables.size(); size_t hashi = hash0; size_t i =1;while(_tables[hashi]._state == EXIST){ hashi =(hash0 + i)% _tables.size(); i++;} _tables[hashi]._kv = kv; _tables[hashi]._state = EXIST;++_n;returntrue;} HashData<K, V>*Find(const K& key){ Hash hash; size_t hash0 =hash(key)% _tables.size(); size_t hashi = hash0; size_t i =1;while(_tables[hashi]._state != EMPTY){if(_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)return&_tables[hashi]; hashi =(hash0 + i)% _tables.size(); i++;}returnnullptr;}boolErase(const K& key){ HashData<K, V>* ret =Find(key);if(ret){ ret->_state = DELETE;returntrue;}elsereturnfalse;}private: vector<HashData<K, V>> _tables; size_t _n;//记录数据个数};}测试
#include"HashTable.h"intmain(){ open_address::HashTable<int,int> ht;int a[]={19,30,5,36,13,20,21,12};for(auto e : a){ ht.Insert({ e,e });}if(ht.Find(13)) ht.Erase(13);else cout <<"没找到"<< endl;return0;}


如果key为日期类,则要自己实现仿函数。
structDate{int _year;int _month;int _day;Date(int year=1,int month=1,int day=1):_year(year),_month(month),_day(day){}booloperator==(const Date& d){return _year == d._year && _month == d._month && _day == d._day;}};structDateHashFunc{ size_t operator()(const Date& d){ size_t hash =0; hash += d._year; hash *=131; hash += d._month; hash *=131; hash += d._day; hash *=131;return hash;}};intmain(){ string a1[]={"sort","insert","abcd","bcad","aadd"}; open_address::HashTable<string, string> ht;for(auto& e : a1) ht.Insert({ e,e });int a2[]={-19,-30,5,36,13,20,21,12}; open_address::HashTable<int,int> ht1;for(auto& e : a2) ht1.Insert({ e,e }); open_address::HashTable<Date,int, DateHashFunc> ht2; ht2.Insert({{2026,1,2},1}); ht2.Insert({{2026,2,1},2});return0;}




处理哈希冲突:链地址法
开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,则把这些冲突的数据链接成一个链表,挂在哈希表这个位置下面,链地址法也叫做拉链法或者哈希桶。

链地址法对负载因子没有要求,我们把负载因子控制在1,如果某个桶特别长时,当桶的长度大于8,就把链表转成红黑树。
代码实现
namespace hash_bucket {template<classK,classV>structHashNode{ pair<K, V> _kv; HashNode<K, V>* _next;HashNode(const pair<K,V>& kv):_kv(kv),_next(nullptr){}};template<classK,classV,classHash=HashFunc<K>>classHashTable{typedef HashNode<K, V> Node;public:HashTable():_tables(__stl_next_prime(0)),_n(0){}//拷贝构造HashTable(const HashTable<K, V, Hash>& ht){ _tables.resize(ht._tables.size());for(int i =0;i < ht._tables.size();i++){ Node* htcur = ht._tables[i];while(htcur){ Node* newnode =newNode(htcur->_kv);//尾插 Node* cur = _tables[i];if(cur ==nullptr) _tables[i]= newnode;else{while(cur->_next){ cur = cur->_next;} cur->_next = newnode;} htcur = htcur->_next;}} _n = ht._n;}~HashTable(){for(int i =0;i < _tables.size();i++){ Node* cur = _tables[i];while(cur){ Node* next = cur->_next;delete cur; cur = next;} _tables[i]=nullptr;}}boolInsert(const pair<K, V>& kv){if(Find(kv.first))returnfalse; Hash hash;if(_n == _tables.size())//扩容{ vector<Node*>newTable(__stl_next_prime(_tables.size()+1));for(int i =0;i < _tables.size();i++){ Node* cur = _tables[i];while(cur){ Node* next = cur->_next; size_t hashi =hash(kv.first)% newTable.size();//找到新表的映射位置//头插 cur->_next = newTable[hashi]; newTable[hashi]= cur; cur = next;} _tables[i]=nullptr;} _tables.swap(newTable);} size_t hashi =hash(kv.first)% _tables.size(); Node* newnode =newNode(kv);//头插 newnode->_next = _tables[hashi]; _tables[hashi]= newnode;++_n;returntrue;} Node*Find(const K& key){ Hash hash; size_t hashi =hash(key)% _tables.size(); Node* cur = _tables[hashi];while(cur){if(cur->_kv.first == key)return cur; cur = cur->_next;}returnnullptr;}boolErase(const K& key){ Hash hash; size_t hashi =hash(key)% _tables.size(); Node* prev =nullptr; Node* cur = _tables[hashi];while(cur){if(cur->_kv.first == key)//找到了{if(prev ==nullptr)//待删除节点为头节点 _tables[hashi]= cur->_next;else//待删除节点为中间节点 prev->_next = cur->_next;delete cur;--_n;returntrue;}else{ prev = cur; cur = cur->_next;}}returnfalse;}private: vector<Node*> _tables; size_t _n =0;};}测试
intmain(){int a[]={19,30,5,36,13,20,21,12,24,96}; hash_bucket::HashTable<int,int> ht;for(auto& e : a){ ht.Insert({ e,e });} ht.Insert({100,100}); ht.Insert({101,101}); cout << ht.Find(19)<< endl; cout << ht.Find(36)<< endl; cout << ht.Find(96)<< endl; cout << ht.Find(101)<< endl << endl; ht.Erase(19); ht.Erase(36); ht.Erase(96); ht.Erase(101); cout << ht.Find(19)<< endl; cout << ht.Find(36)<< endl; cout << ht.Find(96)<< endl; cout << ht.Find(101)<< endl << endl;return0;}
哈希表封装unordered_set和unordered_map
与红黑树封装set和map类似,HashTable也实现了泛型,其中HashTable用链地址法实现,仿函数KeyOfT取出T类型对象的Key对象,仿函数Hash转成size_t 。
代码实现
//HashTable.h#include<vector>#include<string>usingnamespace std;template<classK>structHashFunc{ size_t operator()(const K& key){return(size_t)key;}};template<>structHashFunc<string>{ size_t operator()(const string& s){ size_t hash =0;for(auto& e : s){ hash += e; hash *=131;}return hash;}};inlineunsignedlong__stl_next_prime(unsignedlong n){// Note: assumes long is at least 32 bits.staticconstint __stl_num_primes =28;staticconstunsignedlong __stl_prime_list[__stl_num_primes]={//一个数组53,97,193,389,769,1543,3079,6151,12289,24593,49157,98317,196613,393241,786433,1572869,3145739,6291469,12582917,25165843,50331653,100663319,201326611,402653189,805306457,1610612741,3221225473,4294967291};constunsignedlong* first = __stl_prime_list;constunsignedlong* last = __stl_prime_list + __stl_num_primes;constunsignedlong* pos =lower_bound(first, last, n);return pos == last ?*(last -1):*pos;}namespace hash_bucket {template<classT>structHashNode{ T _data; HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}};//前置声明template<classK,classT,classKeyOfT,classHash>classHashTable;template<classK,classT,classRef,classPtr,classKeyOfT,classHash>structHTIterator{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self; Node* _node;const HT* _ht;//用在operator++HTIterator(Node* node,const HT* ht):_node(node),_ht(ht){} Ref operator*()//解引用{return _node->_data;} Ptr operator->()//取数据{return&_node->_data;}booloperator!=(const Self& s){return _node != s._node;} Self&operator++()//函数只是让当前迭代器走到新位置,最后返回该迭代器{if(_node->_next) _node = _node->_next;else//当前桶已走完{ KeyOfT kot; Hash hash; size_t hashi =hash(kot(_node->_data))% _ht->_tables.size();//当前桶的位置 hashi++;while(hashi < _ht->_tables.size()){ _node = _ht->_tables[hashi];if(_node)break;else hashi++;}if(hashi == _ht->_tables.size()) _node =nullptr;}return*this;}};template<classK,classT,classKeyOfT,classHash=HashFunc<T>>//T本身是K,或含有KclassHashTable{//友元声明template<classK,classT,classRef,classPtr,classKeyOfT,classHash>friendstructHTIterator;typedef HashNode<T> Node;public:typedef HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;typedef HTIterator<K, T,const T&,const T*, KeyOfT, Hash> ConstIterator; Iterator Begin(){if(_n ==0)returnEnd();for(int i =0;i < _tables.size();i++)//找到哈希表中不为空的位置{ Node* cur = _tables[i];if(cur)returnIterator(cur,this);}returnEnd();} Iterator End(){returnIterator(nullptr,this);} ConstIterator Begin()const{if(_n ==0)returnEnd();for(int i =0;i < _tables.size();i++){ Node* cur = _tables[i];if(cur)returnConstIterator(cur,this);}returnEnd();} ConstIterator End()const{returnConstIterator(nullptr,this);}HashTable():_tables(__stl_next_prime(0)),_n(0){}~HashTable(){for(int i =0;i < _tables.size();i++){ Node* cur = _tables[i];while(cur){ Node* next = cur->_next;delete cur; cur = next;} _tables[i]=nullptr;}} pair<Iterator,bool>Insert(const T& data){ KeyOfT kot;auto it =Find(kot(data));if(it !=End())return{ it,false}; Hash hash;if(_n == _tables.size())//扩容{ vector<Node*> _newTable; _newTable.resize(__stl_next_prime(_tables.size()+1));for(int i =0;i < _tables.size();i++){ Node* cur = _tables[i];while(cur){ Node* next = cur->_next; size_t hashi =hash(kot(cur->_data))% _newTable.size();//找到新表的映射位置//头插 cur->_next = _newTable[hashi]; _newTable[hashi]= cur; cur = next;}} _tables.swap(_newTable);} size_t hashi =hash(kot(data))% _tables.size(); Node* newnode =newNode(data);//头插 newnode->_next = _tables[hashi]; _tables[hashi]= newnode; _n++;return{Iterator(newnode,this),true};} Iterator Find(const K& key){ KeyOfT kot; Hash hash; size_t hashi =hash(key)% _tables.size(); Node* cur = _tables[hashi];while(cur){if(kot(cur->_data)== key)return{ cur,this}; cur = cur->_next;}returnEnd();}boolErase(const K& key){ Hash hash; KeyOfT kot; size_t hashi =hash(key)% _tables.size(); Node* cur = _tables[hashi]; Node* prev =nullptr;while(cur){if(kot(cur->_data)== key)//找到了{if(prev ==nullptr)//待删除节点为头节点{ _tables[hashi]= cur->_next;}else//待删除节点为中间节点{ prev->_next = cur->_next;}delete cur;--_n;returntrue;}else{ prev = cur; cur = cur->_next;}}returnfalse;}private: vector<Node*> _tables; size_t _n=0;};}//UnorderedSet.h#include"HashTable.h"namespace mine {template<classK,classHash=HashFunc<K>>classunordered_set{structSetKeyOfT{const K&operator()(const K& key){return key;}};public:typedeftypenamehash_bucket::HashTable<K,const K, SetKeyOfT, Hash>::Iterator iterator;typedeftypenamehash_bucket::HashTable<K,const K, SetKeyOfT, Hash>::ConstIterator const_iterator; iterator begin(){return _ht.Begin();} iterator end(){return _ht.End();} const_iterator begin()const{return _ht.Begin();} const_iterator end()const{return _ht.End();} pair<iterator,bool>insert(const K& key){return _ht.Insert(key);} iterator find(const K& key){return _ht.Find();}boolerase(const K& key){return _ht.Erase(key);}private: hash_bucket::HashTable<K,const K, SetKeyOfT, Hash> _ht;};voidprint(const unordered_set<int>& us)//测试const迭代器{ unordered_set<int>::const_iterator cit = us.begin(); cout <<typeid(us).name()<< endl;while(cit != us.end()){ cout <<*cit <<" ";++cit;} cout << endl;for(auto e : us){ cout << e <<" ";} cout << endl << endl;}}//UnorderedMap.h#include"HashTable.h"namespace mine {template<classK,classV,classHash=HashFunc<K>>classunordered_map{structMapKeyOfT{const K&operator()(const pair<K, V>& kv){return kv.first;}};public:typedeftypenamehash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;typedeftypenamehash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator; iterator begin(){return _ht.Begin();} iterator end(){return _ht.End();} const_iterator begin()const{return _ht.Begin();} const_iterator end()const{return _ht.End();} V&operator[](const K& key){auto ret =insert({ key,V()});return ret.first->second;} pair<iterator ,bool>insert(const pair<K, V>& kv){return _ht.Insert(kv);} iterator find(const K& key){return _ht.Find(key);}boolerase(const K& key){return _ht.Erase(key);}private: hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;};voidprint(const unordered_map<string, string>& um)//测试const迭代器{ unordered_map<string, string>::const_iterator cit = um.begin();while(cit != um.end()){ cout << cit->first <<":"<< cit->second << endl;++cit;} cout << endl;for(auto& e : um){ cout << e.first <<":"<< e.second << endl;}}}测试mine::unordered_set
#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespace std;#include"UnorderedSet.h"#include"UnorderedMap.h"intmain(){int a[]={3,11,86,7,88,82,1,881,5,6,7,6}; mine::unordered_set<int> us;for(auto e : a){ us.insert(e);}auto it = us.begin();while(it != us.end()){ cout <<*it <<" ";++it;} cout << endl;for(auto& e : us){ cout << e <<" ";} cout << endl << endl; mine::print(us);return0;}
测试mine::unordered_map
intmain(){ mine::unordered_map<string, string> dict; dict.insert({"sort","排序"}); dict.insert({"字符串","string"}); dict.insert({"left","左"}); dict.insert({"right","右"});for(auto& e : dict){ cout << e.first <<":"<< e.second << endl;} cout << endl; dict["left"]="左,剩余"; dict["insert"]="插入"; dict["string"];auto it = dict.begin();while(it != dict.end()){//it->second += "x"; cout << it->first <<":"<< it->second << endl;++it;} cout << endl;print(dict);return0;}