目录
[<1> 构造](#<1> 构造)
[<2> operator*](#<2> operator*)
[<3> operator->](#<3> operator->)
[<4> operator!=](#<4> operator!=)
[<5> operator++](#<5> operator++)
[(7)Begin(const迭代器)](#(7)Begin(const迭代器))
[(8)End(const迭代器)](#(8)End(const迭代器))
[5、operator[ ]](#5、operator[ ])
一、前言
在STL中,unordered_set和unordered_map是基于哈希实现的高效容器,相比于红黑树实现的set、map而言,set、map走的是平衡二叉搜索树的查找路线,其查找效率为O(logN),而哈希实现的unordered_set和unordered_map提供了O(1)平均时间复杂度的查找,其中unordered_set用于存储唯一元素、unordered_map用于存储键值对,它们都属于C++11引入的无序容器,具有平均常数时间复杂度的插入、查找和删除效率。本文将围绕unordered系列容器展开介绍,并通过哈希桶来模拟实现,哈希桶通过存储一个链表来处理哈希冲突,当发生哈希冲突时,新元素会被添加到对应哈希桶的链表中,以此来处理哈希冲突。
二、哈希桶
1、结点
哈希桶结点实现为模板类型,实现为链表结构来解决哈希冲突,每个结点存放该结点的数据,T
cpp
template<class T>
struct Hashnode
{
Hashnode(const T& data)
:_data(data)
, _next(nullptr)
{
}
T _data;
Hashnode<T>* _next;
};
_data,以及指向下一个结点的_next指针,即Hashnode<T>*_next,Hashnode构造函数将_data初始化为data,_next初始化为nullptr,就完成了Hashnode结点的构造。
2、哈希桶实现
(1)构造
为了减少哈希冲突,哈希桶以素数容量进行扩容,通过内联函数_stl_next_prime来实现,函数中
cpp
inline unsigned long __stl_next_prime(unsigned long n)
{
static const int __stl_num_primes = 28;
static const unsigned long __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
};
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list + __stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last - 1) : *pos;
}
template<class K, class T, class KeyofT,class Hash = Hashfunc<K>>
class Hashbucket
{
using Node = Hashnode<T>;
Hashbucket()
:_v(__stl_next_prime(0))
, _n(0)
{
}
private:
vector<Node*> _v;
size_t _n = 0;
};
}
有一个素数数组_stl_prime_list,lower_bound用于取素数数组中不小于n的下一个素数,从而实现素数扩容,以此减少哈希冲突。Hashfunc将key的类型转化成无符号整形,using Node=Hashnode<T>,Node指代Hashnode<T>结点,哈希桶结构通过一个存放Node*的指针数组来实现,即vector<Node*> _v,_n表示有效哈希桶的个数,_v指针数组容量初始为_stl_next_prime(0)容量大小,_n初始化为0,就完成了哈希桶的构造。
(2)析构
哈希桶的析构,先处理单个哈希桶的析构,Node*cur=_v[i],若哈希桶不为nullptr,Node*next=cur->_next,先保存cur的下一个结点指针,delete cur,释放cur,cur=next,通过
cpp
~Hashbucket()
{
for (size_t i = 0;i < _v.size();i++)
{
Node* cur = _v[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_v[i] = nullptr;
}
}
while循环依次释放哈希桶一个链表的各个结点,_v[i]=nullptr,最后将_v[i]置为nullptr,通过for循环,逐个释放每个哈希桶链表的结点,就完成了哈希桶的析构。
(3)哈希仿函数
哈希仿函数Hashfunc用于将key转化为无符号整形,并减少哈希冲突,实现为模板类型,并对
cpp
template<class K>
struct Hashfunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct Hashfunc<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 131;
}
return hash;
}
};
string进行了特化,通过hash+=ch,hash*=131,使多位数据参与运算,以此来减少哈希冲突。
(4)迭代器实现
<1> 构造
哈希桶迭代器的实现可先用一个类型封装结点的指针,再通过重载运算符实现迭代器像指针一样访
cpp
template<class K,class T,class KeyofT,class Hash=Hashfunc<K>>
class Hashbucket;//前置声明
template<class K,class T,class Ref,class Ptr,class KeyofT,class Hash>
struct HTiterator
{
using Node = Hashnode<T>;
using HT = Hashbucket<K,T,KeyofT,Hash>;
using Self = HTiterator<K,T, Ref, Ptr, KeyofT, Hash>;
HTiterator(Node* PNode, const PHT* PHT)
:_PNode(PNode)
,_PHT(PHT)
{}
Node* _PNode;
const HT* _PHT;
};
问的行为,using Node=Hashnode<T>,using HT=Hashbucket<K,T,KeyofT,Hash>,using Self=HTiterator<K,T,Ref,Ptr,KeyofT,Hash>,Node指代Hashnode<T>结点,HT指代哈希桶Hashbucket<K,T,KeyofT,Hash>,Self指代迭代器HTiterator<K,T,Ref,Ptr,KeyofT,Hash>,需要注意的是哈希桶的迭代器是单向迭代器,除了结点的指针Node*_PNode,还需要有哈希桶对象的指针const HT*_PHT,这样如果当前桶走完了,要找到下一个哈希桶就容易多了,用key值计算出当前桶位置,依次往后找下一个不为空的桶即可,HTiterator构造函数将结点指针_PNode初始化为PNode,哈希桶指针_PHT初始化为PHT,就完成了迭代器的构造。
<2> operator*
operator*重载用于返回结点指针所指向的结点数据,即return _PNode->_data,返回类型为Ref,
cpp
Ref operator*()
{
return _PNode->_data;
}
根据迭代器是普通迭代器还是const修饰的迭代器来确定Ref的类型,对于普通迭代器,Ref为T&,对于const迭代器,Ref为const T&,这样通过一个迭代器模板就可以一起实现普通迭代器和const迭代器了。
<3> operator->
operator->重载用于返回结点指针_PNode所指向的结点数据地址,即return &_PNode->_data,返
cpp
Ptr operator->()
{
return &_PNode->_data;
}
回类型为Ptr,与operator*类似,根据迭代器是普通迭代器还是const修饰的迭代器来确定Ptr的类型,对于普通迭代器,Ptr为T*,对于const迭代器,Ptr为const T*。
<4> operator!=
operator!=重载用于判断迭代器是否相等,返回类型为bool,可通过迭代器的结点指针_PNode来进
cpp
bool operator!=(const Self& s)
{
return _PNode != s._PNode;
}
行判断,即return _PNode!=s._PNode。
<5> operator++
operator++重载实现就较为复杂一些,首先判断当前哈希桶下面是否还有结点,若_PNode->_next
cpp
Self& operator++()
{
if (_PNode->_next)
{
_PNode = _PNode->_next;
}
else
{
Hash hash;
KeyofT kot;
size_t hash1 = hash(kot(_PNode->_data)) % (_PHT->_v.size());
++hash1;
while (hash1 < _PHT->_v.size())
{
_PNode = _PHT->_v[hash1];
if (_PNode)
break;
else
++hash1;
}
if (hash1 == _PHT->_v.size())
{
_PNode = nullptr;
}
}
return *this;
}
不为nullptr,则结点的指针指向下一个结点即可,即_PNode=_PNode->_next,若_PNode->_next为空,则表明当前哈希桶已经走完了,则需要找到下一个桶,这时就需借助_PHT哈希桶指针,KeyofT kot,kot用于取出_data的key,即kot(_PNode->_data),hash用于将key转化为无符号整形,即hash(kot(_PNode->_data)),通过key计算出当前哈希桶的位置,即size_t hash1=hash(kot(_PNode->_data))%(_PHT->_v.size()),再++hash1,通过while循环依次往后找下一个不为空的桶,循环条件为hash1<_PHT->_v.size(),_PNode=_PHT->_v[hash1],若_PNode不为空,则_PNode则为operator++的结果,else则++hash1,继续往下查找不为空的桶,while循环结束,则hash1==_PHT->_v.size(),表明已走到哈希桶结尾,则_PNode置为空,即_PNode=nullptr,最后return *this即可。
(5)Begin
实现了哈希桶的迭代器,就可以实现与迭代器相关的接口了,将HTiterator迭代器模板声明为友元
cpp
template<class K, class T, class KeyofT,class Hash = Hashfunc<K>>
class Hashbucket
{
template<class K,class T,class Ref,class Ptr,class KeyofT,class Hash>
friend struct HTiterator;
using Node = Hashnode<T>;
using Iterator = HTiterator<K,T, T&, T*, KeyofT,Hash>;
public:
Iterator Begin()
{
if (_n == 0)
{
return End();
}
for (int i = 0;i < _v.size();i++)
{
Node* cur = _v[i];
if (cur)
{
return Iterator(cur, this);
}
}
return End();
}
private:
vector<Node*> _v;
size_t _n = 0;
};
}
关系,即friend struct HTiterator,方便访问其内部成员,HTiterator<K,T,T&,T*,KeyofT,Hash>即为普通迭代器,using Iterator=HTiterator<K,T,T&,T*,KeyofT,Hash>,Iterator指代普通迭代器HTiterator<K,T,T&,T*,KeyofT,Hash>,begin()返回第一个桶中第一个结点指针构造的迭代器,通过for循环进行查找,Node*cur=_v[i],若cur不为空,则返回cur和this指针所构造的迭代器,即return Iterator(cur,this)。
(6)End
End()返回的迭代器用nullptr来表示,返回nullptr和this指针所构造的迭代器,即return Iterator
cpp
Iterator End()
{
return Iterator(nullptr, this);
}
(nullptr,this)。
(7)Begin(const迭代器)
const修饰的迭代器实现与普通迭代器类似,区别在于二者operator*、operator->返回类型的差异
cpp
template<class K, class T, class KeyofT,class Hash = Hashfunc<K>>
class Hashbucket
{
template<class K,class T,class Ref,class Ptr,class KeyofT,class Hash>
friend struct HTiterator;
using Node = Hashnode<T>;
using Iterator = HTiterator<K,T, T&, T*, KeyofT,Hash>;
using Constiterator = HTiterator<K,T, const T&, const T*, KeyofT, Hash>;
public:
Constiterator Begin() const
{
if (_n == 0)
{
return End();
}
for (int i = 0;i < _v.size();i++)
{
Node* cur = _v[i];
if (cur)
{
return Constiterator(cur, this);
}
}
return End();
}
private:
vector<Node*> _v;
size_t _n = 0;
};
}
对于普通迭代器,operator*、operator->返回的类型分别为T&、T*,而const修饰的迭代器,operator*、operator->返回的类型分别为const T&、const T*,HTiterator<K,T,const T&,const T*,KeyofT,Hash>即为const修饰的迭代器,using Constiterator=HTiterator<K,T,const T&,const T*,KeyofT,Hash>,用Constiterator指代const迭代器HTiterator<K,T,const T&,const T*,KeyofT,Hash>,const迭代器的Begin与普通迭代器类似,也是返回第一个桶中第一个结点指针构造的迭代器,若_n==0,则return End(),通过for循环进行查找,Node*cur=_v[i],若cur不为空,则返回cur和this指针所构造的const迭代器,即return Constiterator(cur,this),for循环结束,说明cur为nullptr,则return End()。
(8)End(const迭代器)
与普通迭代器类似,End()返回的const迭代器用nullptr表示,即返回nullptr和this指针所构造的
cpp
Constiterator End() const
{
return Constiterator(nullptr, this);
}
const迭代器,即return Constiterator(nullptr,this)。
(9)find
find用于哈希桶数据的查找,hash实现将key转化成无符号整形,kot用于取出_data的key,先计算
cpp
Iterator find(const K& key)
{
Hash hash;
KeyofT kot;
size_t hash1 = hash(key) % (_v.size());
Node* cur = _v[hash1];
while (cur)
{
if (kot(cur->_data) == key)
{
return Iterator(cur,this);
}
cur = cur->_next;
}
return End();
}
key的哈希值,即size_t hash1=hash(key) % (_v.size()),再根据该哈希值进行查找,即Node*cur=_v[hash1],通过while循环进行查找,若cur不为空,且kot(cur->_data)==key,则返回cur和this指针构造的迭代器,return Iterator(cur,this),else则cur=cur->_next,继续在该哈希桶里查找,while循环结束,则cur为nullptr,查找失败,return End()。
(10)erase
erase用于哈希桶结点的删除,先计算key的哈希值,即size_t hash1=key%(_v.size()),再将该哈希
cpp
bool erase(const K& key)
{
KeyofT kot;
size_t hash1 = key % (_v.size());
Node* prev = nullptr;
Node* cur = _v[hash1];
while (cur)
{
if (kot(cur->_data) == key)
{
if (prev == nullptr)
{
_v[hash1] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
值位置的结点指针赋值给cur,即Node*cur=_v[hash1],prev充当cur的后继指针,用于标记cur的位置,便于删除后结点指针的连接,prev初始化为nullptr,通过while循环,KeyofT kot,kot用于取出_data的key,若kot(cur->_data)==key,且prev==nullptr,说明删除结点为哈希桶的首结点,则先将cur的_next指针赋值给_v[hash1],即_v[hash1]=cur->_next,再释放cur,delete cur,--_n,return true删除成功。若prev不为空,则说明删除结点为哈希桶的中间结点,则先将prev的_next指向cur的_next,即prev->_next=cur->_next,完成删除后结点的连接,再释放cur,delete cur,--_n,return true删除成功,else即kot(cur->_data)与key不相等,则继续在该哈希桶里查找,prev=cur,cur=cur->_next,while循环结束,则cur走到空,表明哈希桶里没有该数据,return false,删除失败。
(11)insert
insert用于哈希桶数据的插入,返回类型为pair<Iterator,bool>,若数据插入成功,则Iterator为插
cpp
pair<Iterator,bool> insert(const T& data)
{
KeyofT kot;
Iterator it = find(kot(data));
if(it!=End())
{
return {it,false};
}
Hash hash;
if (_n == _v.size())
{
vector<Node*> newvn(__stl_next_prime(_v.size()+1));
for (int i = 0;i < _v.size();i++)
{
Node* cur = _v[i];
while (cur)
{
Node* next = cur->_next;
size_t hash1 = hash(kot(cur->_data)) % (newvn.size());
cur->_next = newvn[hash1];
newvn[hash1] = cur;
cur = next;
}
_v[i] = nullptr;
}
_v.swap(newvn);
}
size_t hash2 = hash(kot(data)) % (_v.size());
Node* newnode = new Node(data);
newnode->_next = _v[hash2];
_v[hash2] = newnode;
++_n;
return {Iterator(newnode,this),true};
}
入位置的迭代器,bool返回true,若插入失败,则Iterator返回已存在数据位置的迭代器,bool返回false,Iterator it=find(kot(data)),插入前先判断data是否已经在哈希桶中,若it!=End(),说明data已在哈希桶中,则插入失败,并返回其所在位置的迭代器,即return {it,false},else即it==End(),说明data不在哈希桶中,可以插入data,KeyofT kot,kot用于提取data的key,Hash hash,hash用于将key类型转化成无符号整形,插入前先判断哈希桶是否需要扩容,哈希桶一般在负载因子为1时进行扩容,即_n==_v.size()时进行扩容,采取素数容量进行扩容,以此减少哈希冲突,即vector<Node*> newvn(_stl_next_prime(_v.size()+1)),通过for循环,实现每个哈希桶的逐一拷贝,Node*cur=_v[i],cur通过while循环,实现哈希桶每个结点的拷贝,先保存cur的_next结点指针,Node*next=cur->_next,再计算cur在新哈希桶的哈希值,即size_t hash1=hash(kot(cur->_data))%(newvn.size()),结点采取头插方式,相比尾插,头插可直接访问哈希桶最近插入的数据,访问起来较为方便,即cur->_next=newvn[hash1],newvn[hash1]=cur。cur=next,继续访问原哈希桶的下一个结点,重复上面步骤,直到cur为空,则原哈希桶遍历结束,且原哈希桶的结点均已完成在新哈希桶上的指针连接,最后将原哈希桶的指针置为空,即_v[i]=nullptr,防止哈希桶结点的多次析构,最后将原哈希桶和新哈希桶交换,即_v.swap(newvn),就完成了哈希桶的扩容,扩容完成后,进行data的插入,先计算出data在哈希桶对应的哈希值,即size_t hash2=hash(kot(data))%(_v.size()),再进行头插,即Node*newnode=new Node(data),newnode->_next=_v[hash2],_v[hash2]=newnode,++_n,就完成了data的插入,返回插入位置的迭代器及bool值构造的pair<Iterator,bool>,即return {Iterator(newnode,this),true}。
三、unordered_set
1、结构实现
有了前面已实现的哈希桶结构,就可以借助哈希桶来实现unordered_set了,Hash_bucket::Hash
cpp
#pragma once
#include"Hashtable.h"
namespace Kzy
{
template<class K,class Hash=Hashfunc<K>>
class unordered_set
{
struct SetkeyofT
{
const K& operator()(const K& key)
{
return key;
}
};
private:
Hash_bucket::Hashbucket<K, const K, SetkeyofT,Hash> _ht;
};
}
bucket<K,const K,SetkeyofT,Hash> _ht,即为unordered_set底层的哈希桶,第1个模板参数K为unordered_set的find、erase接口所需的函数参数类型,第2个模板参数const K为unordered_set存放的数据类型,且unordered_set不支持修改,故需用const修饰。第3个模板参数SetkeyofT,SetkeyofT仿函数用于返回unordered_set的数据key,哈希桶模板参数这么设计是为了与unordered_map的实现兼容,Hash用于将key类型转化为无符号整形,这样就实现了unordered_set底层的哈希桶结构。
2、insert
有了哈希桶成员变量_ht,unordered_set的insert就可以直接调用_ht的insert接口来实现,即return
cpp
pair<iterator,bool> insert(const K& key)
{
return _ht.insert(key);
}
_ht.insert(key)。
3、find
cpp
iterator find(const K& key)
{
return _ht.find(key);
}
同理unordered_set的find直接调用_ht的find接口就可以实现,即return _ht.find(key)。
4、erase
cpp
bool erase(const K& key)
{
return _ht.erase(key);
}
同理unordered_set的erase直接调用_ht的erase接口就可以实现,即return _ht.erase(key)。
5、迭代器
Hash_bucket::Hashbucket<K,const K,SetkeyofT,Hash>::Iterator即为普通迭代器,
cpp
#pragma once
#include"Hashtable.h"
namespace Kzy
{
template<class K,class Hash=Hashfunc<K>>
class unordered_set
{
struct SetkeyofT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
using iterator = typename Hash_bucket::Hashbucket<K, const K, SetkeyofT,Hash>::Iterator;
using const_iterator = typename Hash_bucket::Hashbucket<K, const K, SetkeyofT,Hash>::Constiterator;
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin() const
{
return _ht.Begin();
}
const_iterator end() const
{
return _ht.End();
}
private:
Hash_bucket::Hashbucket<K, const K, SetkeyofT,Hash> _ht;
};
}
using iterator=typename Hash_bucket::Hashbucket<K,const K,SetkeyofT,Hash>::Iterator,iterator指代unordered_set的普通迭代器,typename用于声明类型,Hash_bucket::Hashbucket<K,const K,SetkeyofT,Hash>::Constiterator即为const修饰的迭代器,using const_iterator=typename Hash_bucket::Hashbucket<K,const K,SetkeyofT,Hash>::Constiterator,const_iterator指代unordered_set的const迭代器,同理unordered_set的普通和const迭代器的begin、end都可以直接调用_ht的Begin、End接口来实现,即return _ht.Begin(),return _ht.End()。
四、unordered_map
1、结构实现
与unordered_set实现类似,Hash_bucket::Hashbucket<K,pair<const K,V>,MapkeyofT,Hash> _ht即为unordered_map底层的哈希桶,第一个模板参数K为unordered_map的find、erase
cpp
#pragma once
#include"Hashtable.h"
namespace Kzy
{
template<class K,class V,class Hash=Hashfunc<K>>
class unordered_map
{
struct MapkeyofT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
private:
Hash_bucket::Hashbucket<K, pair<const K, V>, MapkeyofT,Hash> _ht;
};
}
接口所需的函数参数类型,第二个模板参数pair<const K,V>为unordered_map存放的数据类型,且key数据类型K不支持修改,故需用const修饰,第三个模板参数MapkeyofT,MapkeyofT仿函数用于返回unordered_map的数据key,即return kv.first,Hash用于将key类型转化为无符号整形,这样就实现了unordered_map底层的哈希桶结构。
2、insert
与unordered_set类似,有了底层哈希桶的结构,unordered_map的insert就可以直接调用_ht的
cpp
pair<iterator,bool> insert(const pair<K, V>& kv)
{
return _ht.insert(kv);
}
insert接口来实现,即return _ht.insert(kv)。
3、find
cpp
iterator find(const K& key)
{
return _ht.find(key);
}
同理unordered_map的find就可以直接调用_ht的find接口实现,即return _ht.find(key)。
4、erase
cpp
bool erase(const K& key)
{
return _ht.erase(key);
}
同理unordered_map的erase就可以直接调用_ht的erase接口实现,即return _ht.erase(key)。
5、operator[ ]
由于unordered_map的operator[ ]结合了查找、插入和修改的功能,故可通过insert来实现operator
cpp
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert({ key,V()});
return ret.first->second;
}
\],pair\