目录
前言
本篇文章我们讲述的是unordered_map与unordered_set容器,我们在之前的内容中知道map和set的底层结构为红黑树,并且红黑树查找和搜索的效率都非常高,那么为什么后来又设计出了这两个容器呢,它们相较于map与set又有什么高效之处呢?下面就让我们一起来学习吧!
一、unordered系列关联式容器
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和unordered_set进行介绍,unordered_multimap和unordered_multiset可查看文档介绍。
关于它们的使用这里我就不做过多的讲述,因为它们跟map和set的使用其实是一致的,那么它们的区别到底在哪里呢?
二、底层结构
unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
2.1 哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
- 插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放- 搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
该方式即为 哈希(散列) 方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。
2.2 哈希冲突
对于两个数据元素的关键字 k i k_i ki和 k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) ==Hash( k j k_j kj),即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常见哈希函数:
1. 直接定址法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2. 除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
我们后面都是采用的除留余数法来定位关键码的存储位置的,直接定址法不适用于分散的数据,会浪费掉大量的空间!
2.3 解决哈希冲突
Q:发生哈希冲突该如何处理呢?
解决哈希冲突两种常见的方法是:闭散列和开散列(拉链法 / 哈希桶)。
2.4 闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
那如何寻找下一个空位置呢?
- 线性探测
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
- 插入
通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。如下图,4与44除模取余得出的关键码一致造成了哈希冲突,44一直向后寻找空位置来存储数据!!
- 删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。寻找的条件是找到空位置就停止,因为后面的元素都不会符合条件,假设删除4元素当前位置置空,查找44时我们计算得出的关键码为4位置,4号元素一删除该位置为空那么就无法向后进行寻找了!!查找某个元素是否在表中需要满足的条件,首先该元素一定是存在于表中位置,其次关键码也要相等。
因此线性探测采用标记的伪删除法来删除一个元素。哈希表每个空间给个标记:EMPTY 表示此位置空,EXIST 表示此位置已经有元素,DELETE 表示元素已经删除。
Q:下面我们来思考一个问题:哈希表什么情况下进行扩容?如何扩容?
当哈希表快要满时,此时的哈希冲突出现的概率非常大,存在你占我的位置,我占用别人的位置的情况。比如哈希表的大小为100,此时我插入了99个元素,还剩最后一个空位置了。当最后一个插入的元素关键码与最前面位置的关键码一致时,就需要浪费大量时间去找到最后一个空位置,所以哈希表的大小一定程度上影响了哈希冲突出现的频次和概率,那么为了减小哈希冲突的频次和概率,这时候哈希表就要扩容了。那么扩容为什么能解决问题呢?很简单哈希表变大了,能存储的位置变多了,自然发生哈希冲突踩踏的事件就会减少了!!
为了解决扩容问题,有大佬提出了负载因子(载荷因子)的概念。哈希表的负载因子等于填入表中的元素个数除以哈希表的长度。负载因子越小,哈希冲突的概率越小;负载因子越大,哈希冲突的概率越大。当负载因子到达一个基准值时,哈希表就需要扩容。基准越大,冲突越多,效率越低,空间利用率越高。
Q:我们还需注意另外一个问题:请问扩容之后是将原数组的数据直接拷贝到新数组吗?
哈希表扩容跟其他容器扩容不一样,它的代价是非常大的,因为扩容后哈希表的大小变化了,我们根据除留取余法计算出来的位置又不一样了。原来存在哈希冲突的数据,有可能就不冲突了,原来不冲突的,重新计算后有可能就冲突了;所以我们需要将原数组的关键码重新映射到新数组的位置上,并不能直接简单的将数据拷贝到原来的位置上。
Q:继续思考一个问题:上述我们讲解的都是整型的情况,那么数据有可能是string类型的啊,此时我们该如何去处理呢?
我们知道整型是最方便使用除留余数法的,同样的所以我们可以将string类型的数据转为整型后再去计算它的存储位置!
如何来设计这个哈希函数呢,下面给出大佬设计出的一种很高效的哈希函数:
struct HashStr
{
// BKDR
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 31;
}
return hash;
}
};
通过上述的分析,我们来总结一下线性探测的优缺点。
- 优点:实现非常简单。
- 缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?
- 二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i =1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
二次探测其实就是在线性探测的基础上扩大了下一跳的范围,这样是比之前一个个往下找发生哈希冲突概率小的,缓解了连续踩踏的情况,但是它本质上也解决不了哈希冲突!!这里我就不带大家去实现了,稍微改动一下代码就能完成。
我们来看看闭散列基于线性探测实现的代码:
namespace OpenAddress
{
// 三种状态 空、存在、删除
enum State
{
EMPTY,
EXIST,
DELETE
};
// 数据
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
// 特化
template<>
struct HashFunc<string>
{
// BKDR
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 31;
}
return hash;
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
bool Insert(const pair<K, V>& kv)
{
// 当插入元素已经存在于表中时, 此时我们不需要再次插入
if (Find(kv.first))
return false;
Hash hash;
// 负载因子超过0.7就扩容
//if ((double)_n / (double)_tables.size() >= 0.7)
if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
{
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V> newht;
newht._tables.resize(newsize);
// 遍历旧表,重新映射到新表
for (auto& data : _tables)
{
if (data._state == EXIST)
{
newht.Insert(data._kv);
}
}
_tables.swap(newht._tables);
}
// 计算关键码在表中的位置
size_t hashi = hash(kv.first) % _tables.size();
// 线性探测
//如果发生了哈希冲突, 那么就从当前位置往后一直找, 直到找到一个空位置或者删除状态的位置
size_t i = 1;
size_t index = hashi;
while (_tables[index]._state == EXIST)
{
index = hashi + i;
index %= _tables.size();
++i;
}
_tables[index]._kv = kv;
_tables[index]._state = EXIST;
_n++;
return true;
}
HashData<K, V>* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
size_t i = 1;
size_t index = hashi;
// 寻找某个元素 直至出现空位置 说明后面已经没有符合条件的元素了
while (_tables[index]._state != EMPTY)
{
// 当后续元素存在并且关键码一致时说明元素找到了
if (_tables[index]._state == EXIST
&& _tables[index]._kv.first == key)
{
return &_tables[index];
}
index = hashi + i;
index %= _tables.size();
++i;
// 又回到原来的位置, 说明已经查找一圈, 则表中全是存在+删除的位置
if (index == hashi)
{
break;
}
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_n; // 有效数据的数量--
return true;
}
else
{
return false;
}
}
private:
vector<HashData<K, V>> _tables;
size_t _n = 0; // 有效数据的数量 -- 删除状态不为有效数据
};
}
线性探测和二次探测都没有从本质上解决哈希冲突占用位置的问题,这时候就需要开散列的拉链法(哈希桶)。闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
2.5 开散列
- 开散列概念
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
注:拉链法并不会出现所有元素都在同一个桶中的情况,因为有负载因子的存在,也就不会出现O(N)的查找效率问题。如果还想进一步提高效率,桶中也可以挂红黑树,Java中使用的就是这种方式。这里我们为了简单描述哈希表是如何实现的就使用单向链表来挂接了,而且相比双向链表更节省空间。
Q:如何插入节点?
这里插入节点有俩种方法,头插和尾插都是可以的,但是这里头插更为的方便快捷,尾插每次还需要找到尾部将其链接!所以后面我们都选用头插。
Q:如何删除节点
删除节点同样的也十分简单,分俩种情况一种是删除链表头结点,所以我们需要把头结点更新为下一个位置;另一种情况是删除中间节点,我们只需记录上一个位置,将上一个位置与它的下一个位置链接起来即可。本质上插入和删除都是对链表进行的一系列操作,比较简单。
Q:什么时候扩容?
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
开散列代码如下:
namespace HashBucket
{
template<class K, class V>
struct HashNode
{
HashNode<K, V>* _next;
pair<K, V> _kv;
HashNode(const pair<K, V>& kv)
:_next(nullptr)
, _kv(kv)
{}
};
// int版本的key
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
// 特化版本:string
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto& ch : s)
{
hash += ch;
hash *= 31;
}
return hash;
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
Node* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
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;
}
return nullptr;
}
bool Erase(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;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
// 负载因因子==1时扩容
if (_n == _tables.size())
{
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newtables(newsize, nullptr);
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newtables.size();
// 头插到新表
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
}
_tables.swap(newtables);
}
size_t hashi = hash(kv.first) % _tables.size();
// 头插
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
~HashTable()
{
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
cur = nullptr;
}
}
private:
vector<Node*> _tables; // 指针数组
size_t _n = 0; // 存储有效数据个数
};
- 开散列与闭散列比较
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开放定址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子 a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开放定址法节省存储空间。
三、unordered_map与unordered_set的模拟实现
其实通过map与set的模拟实现我们可以从中得到很大的启发,首先map和set都是复用了红黑树的功能,对于大小的比较我们添加了一个仿函数KeyOfT来根据容器的不同提取出对应的数据参与比较;另一个map与set的迭代器都是复用的红黑树的迭代器,所以关于unordered系列的容器我们也采用同样的思路去设计。
3.1 HashTable的实现
增加一个仿函数KeyOfT提取出比较大小的数据,增加一个迭代器类用来实现迭代器!!
#pragma once
#include <vector>
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
// 特化
template<>
struct HashFunc<string>
{
// BKDR
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 31;
}
return hash;
}
};
namespace HashBucket
{
template<class T>
struct HashNode
{
HashNode<T>* _next;
T _data;
HashNode(const T& data)
:_next(nullptr)
, _data(data)
{}
};
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
typedef HashNode<T> Node;
public:
Node* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
bool Insert(const T& data)
{
KeyOfT kot;
if (Find(kot(data))
{
return false;
}
Hash hash;
// 负载因因子==1时扩容
if (_n == _tables.size())
{
size_t newsize = _tabels.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newtables(newsize, nullptr);
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(kot(cur->_data)) % newtables.size();
// 头插到新表
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
}
_tables.swap(newtables);
}
size_t hashi = hash(kot(data)) % _tables.size();
// 头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
~HashTable()
{
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
cur = nullptr;
}
}
private:
vector<Node*> _tables; // 指针数组
size_t _n = 0; // 存储有效数据个数
};
};
3.1.1 迭代器的设计
迭代器的设计:map和set迭代器的只需用到节点指针即可,但是此处哈希表需要用到节点指针和_tables指针数组,因为我们需要遍历每个桶下面的节点,而每个桶的遍历就需要用到_tables指针数组,判断它下面是否有节点。并且哈希表的const迭代器与红黑树的const迭代器有不同之处,从stl库中我们可以看到map与set的普通迭代器复用的是红黑树的const迭代器,而unordered_map与set中的普通迭代器复用就是普通迭代器,const迭代器复用的就是const迭代器,当然了这里区别其实不大,为何要这样实现大佬有自己的理由。
迭代器的设计如下:
// 前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct __HashIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, KeyOfT, Hash> HT;
typedef __HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
typedef __HashIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
Node* _node;
const HT* _ht;
__HashIterator(Node* node, const HT* ht)
:_node(node)
, _ht(ht)
{}
// const迭代器调用普通迭代器的构造实现
__HashIterator(const Iterator& it)
:_node(it._node)
, _ht(it._ht)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
Self& operator++()
{
if (_node->_next != nullptr)
{
_node = _node->_next;
}
else
{
// 找下一个不为空的桶
KeyOfT kot;
Hash hash;
// 算出当前的桶位置
size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
if (_ht->_tables[hashi])
{
_node = _ht->_tables[hashi];
break;
}
else
{
++hashi;
}
}
// 没有找到不为空的桶
if (hashi == _ht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
};
3.1.2 HashTable的整体实现
#pragma once
#include <vector>
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
// 特化
template<>
struct HashFunc<string>
{
// BKDR
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 31;
}
return hash;
}
};
namespace HashBucket
{
template<class T>
struct HashNode
{
HashNode<T>* _next;
T _data;
HashNode(const T& data)
:_next(nullptr)
, _data(data)
{}
};
// 前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct __HashIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, KeyOfT, Hash> HT;
typedef __HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
typedef __HashIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
Node* _node;
const HT* _ht;
__HashIterator(Node* node, const HT* ht)
:_node(node)
, _ht(ht)
{}
__HashIterator(const Iterator& it)
:_node(it._node)
, _ht(it._ht)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
Self& operator++()
{
if (_node->_next != nullptr)
{
_node = _node->_next;
}
else
{
// 找下一个不为空的桶
KeyOfT kot;
Hash hash;
// 算出我当前的桶位置
size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
if (_ht->_tables[hashi])
{
_node = _ht->_tables[hashi];
break;
}
else
{
++hashi;
}
}
// 没有找到不为空的桶
if (hashi == _ht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
};
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct __HashIterator; // 声明为它的友元类, 可以访问它的私有成员_tables
typedef HashNode<T> Node;
public:
typedef __HashIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef __HashIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
iterator begin()
{
Node* cur = nullptr;
for (size_t i = 0; i < _tables.size(); ++i)
{
cur = _tables[i];
if (cur)
{
break;
}
}
return iterator(cur, this);
}
iterator end()
{
return iterator(nullptr, this);
}
const_iterator begin() const
{
Node* cur = nullptr;
for (size_t i = 0; i < _tables.size(); ++i)
{
cur = _tables[i];
if (cur)
{
break;
}
}
return const_iterator(cur, this);
}
const_iterator end() const
{
return const_iterator(nullptr, this);
}
~HashTable()
{
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
cur = nullptr;
}
}
iterator Find(const K& key)
{
if (_tables.size() == 0)
return end();
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur, this);
}
cur = cur->_next;
}
return end();
}
bool Erase(const K& key)
{
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
pair<iterator, bool> Insert(const T& data)
{
KeyOfT kot;
iterator it = Find(kot(data));
if (it != end())
{
return make_pair(it, false);
}
Hash hash;
// 负载因因子==1时扩容
if (_n == _tables.size())
{
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newtables(newsize, nullptr);
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(kot(cur->_data)) % newtables.size();
// 头插到新表
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
}
_tables.swap(newtables);
}
size_t hashi = hash(kot(data)) % _tables.size();
// 头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return make_pair(iterator(newnode, this), false);;
}
private:
vector<Node*> _tables; // 指针数组
size_t _n = 0; // 存储有效数据个数
};
}
3.2 unordered_map的模拟实现
#pragma once
#include "HashTable.h"
namespace curry
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename HashBucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
typedef typename HashBucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::const_iterator 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 pair<K, V>& kv)
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
iterator find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
HashBucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
}
3.3 unordered_set的模拟实现
#pragma once
#include "HashTable.h"
namespace curry
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename HashBucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
typedef typename HashBucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator 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(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
HashBucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
};
}
从上述的模拟实现,一个类型 K 去做 map/set和 unordered_map/set 的模板参数要什么要求?
一个类型要做unordered_map/unordered_set的key,要满足该类型对象支持转换成取模的整形或者提供转换成整型的仿函数,还要求该类型对象可以支持等于比较或者提供等于比较的仿函数 (针对自定义类型);一个类型要做map/set的key,要满足支持小于比较或者显示提供小于比较的仿函数。