目录
一、unordered系列关联式容器的引入
二、容器使用
2.1 unordered_map的文档说明
2.2 unordered_map的使用
2.3 unordered_set
三、底层结构
3.1 哈希概念
3.2 哈希表
3.3 哈希冲突
3.4 哈希函数
3.5 哈希冲突解决
3.5.1 闭散列
3.5.2 开散列
3.5.3 思考
四、模拟实现
4.1 模板参数列表的改造
4.2 增加迭代器
4.3 模拟实现HashTable
4.4 模拟实现 unodered_set
4.5 模拟实现 unodered_map
一、unordered系列关联式容器的引入
在C++98中,STL(标准模板库)提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 log_2 N ,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。
C++11引入了一组新的容器类型:unordered系列关联式容器,它们提供了一种不同于传统关联式容器(如map和set)的存储和访问方式。unordered系列容器基于哈希表实现,因此它们通常在查找、插入和删除操作上提供平均时间复杂度为O(1)的性能,但最坏情况下可能会退化到O(n)。
主要成员:
unordered_map:存储键值对(key-value pairs),其中每个键都是唯一的,它允许快速检索与给定的键相关联的值。
unordered_multimap:与unordered_map类似,但它允许键值对中的键不唯一,即多个值可以与同一个键关联。
unordered_set:存储唯一的元素,它允许快速判断一个元素是否存在于集合中。
unordered_multiset:与unordered_set类似,但它允许集合中有多个相同的元素。
二、容器使用
2.1 unordered_map的文档说明
在线文档说明
- unordered_map是存储<key, value>键值对的关联式容器,其允许通过key快速的索引到与其对应的value。
- 在unordered_map中,键值key通常用于惟一地标识元素,而映射值value是一个对象,其内容与此键关联。键和映射值的类型可能不同。
- 在内部实现上,unordered_map没有按照key或value的任何特定顺序排列。为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
- unordered_map容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭代方面效率较低。
- unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
- 它的迭代器少是前向迭代器,可以使用迭代器从前向后遍历容器中的元素,但不能保证在双向或随机访问迭代器上提供相同的高效性能。
2.2 unordered_map的使用
unordered_map的使用与map的使用类似,在此不做过多说明。
2.3 unordered_set
参考 unordered_set 在线文档说明 ,它的用法也和set无明显区别。
三、底层结构
unordered系列的关联式容器是基于哈希表实现的,因此效率比较高
3.1 哈希概念
通过某种函数将要查找的关键字key和另一个值建立一个关联关系,使得他们在一种存储结构中一 一映射。查找时通过函数可以很快地从存储结构中找到该元素。
插入元素:根据待插入元素的key,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
这种方式叫做哈希方法。哈希方法中使用的函数叫做哈希函数(hashFunc)。构造的结构成为哈希表(hashTable)。
3.2 哈希表
哈希表通过一个哈希函数将key转换为数组索引。
哈希表底层是一个数组,数组的每个位置或桶(bucket)用于存储一个或多个元素。这些元素可能是pair<key, value>或单个key。
这种方式查询速度极快,比如我们需要查询数组中是否有数字3,正常的做法就是从头遍历数组;当采用哈希函数存储的时候我们可以通过计算数字存储地址直接找到对应的位置,一次就能判断数组中是否有某个数以及数字的地址,这个特点在数据多的时候更为明显。
3.3 哈希冲突
哈希冲突(Hash Collision),也称为哈希碰撞,是指在使用哈希函数将键(key)映射到哈希表中的位置时,两个或多个不同的键产生了相同的哈希值的现象。由于哈希表的存储空间是有限的,而可能的键值是无限的,因此哈希冲突在理论上是不可能完全避免的。
3.4 哈希函数
哈希冲突的原因可能包括:
- 哈希函数的设计不够合理:哈希函数可能不足以将不同的键均匀分布到哈希表中,导致多个键被映射到同一个位置。
- 有限的哈希表大小:哈希表的大小是有限的,而键的数量可能远远超过哈希表的大小,因此必然会有多个键映射到同一个桶(bucket)。
哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常见的哈希函数 :
- 直接定址法--(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况 - 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p (p<=m) ,将关键码转换成哈希地址。
(可以不用取质数作为除数,stl中使用质数大小的哈希表,但VS中没有使用质数大小的哈希表) - 平方取中法--(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
3.5 哈希冲突解决
解决哈希冲突两种常见的方法是:闭散列和开散列
3.5.1 闭散列
闭散列也叫开放地址法,当冲突发生时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,可以使用某种探测技术在散列表中形成一个探测序列。
沿此序列逐个位置地查找,直到找到冲突位置中的“下一个” 空位置,将key存放到该位置。
按照形成探查序列的方法不同,可将开放定址法区分为线性探测法、二次探测法等。
- 线性探测法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。插入:
1.通过哈希函数获取待插入元素在哈希表中的位置
2.如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。
删除:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
删除状态的意义:
1、再插入,这个位置可以覆盖值
2、防止后面冲突的值,出现找不到的情况。遇到删除状态,还是继续往后找。查找一个值,如果找不到,则找到空位置结束。
线性探测法的实现:
//仿函数,求key对应的数字
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//模板特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& str)
{
size_t ret = 0;
for (auto& e : str)
{
ret *= 31; //降低str总和相同的概率
ret += e;
}
return ret;
}
};
namespace closed_hash //闭散列、开放定址法、线性探测
{
enum status {
Empty,
Exist,
Delete
};
template<class K, class V>
struct HashData
{
pair<K,V> _kv;
status _status;
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
HashTable()
{
_table.resize(10);
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first)) return false;
//设负载因子为0.7
//负载因子太大,冲突可以会剧增,冲突增加,效率降低
//负载因子太小,冲突降低,但是空间利用率就低了
if (_count * 10 / _table.size() == 7)
{
HashTable<K, V> newhash;
newhash._table.resize(_table.size() * 2);
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._status == Exist)
newhash.Insert(_table[i]._kv);
}
_table.swap(newhash._table);
}
Hash hf;
int hashi = hf(kv.first) % _table.size();
while (_table[hashi]._status == Exist)
{
hashi++;
hashi %= _table.size();
}
_table[hashi]._kv = kv;
_table[hashi]._status = Exist;
_count++;
return true;
}
HashData<K,V>* Find(const K& key)
{
Hash hf;
size_t hashi = hf(key) % _table.size();
while (_table[hashi]._status != Empty) //连续查找,伪删除的节点保证连续
{
if (_table[hashi]._status == Exist
&& _table[hashi]._kv.first == key)
return &_table[hashi];
else hashi = (hashi + 1) % _table.size();
}
return nullptr;
}
//伪删除
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret == nullptr) return false;
ret->_status = Delete;
_count--;
return true;
}
void Print()
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._status == Exist)
{
//printf("[%d]:%d\n", i, _table[i]._kv.first);
cout << "[" << i << "]:" << _table[i]._kv.first << "->" << _table[i]._kv.second << endl;
}
else if (_table[i]._status == Empty)
{
printf("[%d]:\n", i);
}
else
{
printf("[%d]:D\n", i);
}
}
}
private:
vector<HashData<K,V>> _table;
size_t _count = 0; // 存储的关键字的个数
};
}
思考:哈希表什么情况下进行扩容?如何扩容?
在哈希表中,当元素的数量超过一定的阈值时,通常会进行扩容(resize)操作。这个阈值通常是由哈希表的负载因子(load factor)决定的。负载因子定义为填入表中的元素个数与散列表长度的比值,即 α=填入表中的元素个数 / 哈希表的长度
α是哈希表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,哈希表的平均查找长度是负载因子α的函数,只是不同处理冲突的方法有不同的函数。
对于开放定址法,负载因子是特别重要因素,应严格限制在0.7-0.8。负载因子太大,冲突的可能性增加,会导致查找效率下降。负载因子太小,冲突的可能性降低,但空间利用率也就降低了。因此,一些采用开放定址法的hash库,如Java的系统库限制了负载因子为0.75,超过此值将resize哈希表。
扩容的具体步骤如下:1. 创建一个新的哈希表,其大小是原始哈希表大小的两倍或多倍(具体倍数取决于哈希表的实现)。
2. 将原始哈希表中的所有元素重新哈希(rehash)到新的哈希表中。这通常涉及到计算每个元素的新的哈希值,并将其存储在新的哈希表的相应位置。
3. 释放原始哈希表的内存。
4. 将新哈希表设置为当前的哈希表。
注:拷贝时直接用新哈希表调用Insert函数,此时可以减少代码量。但该方法不是递归,因为使用者不是同一个对象。
扩容是一个相对耗时的操作,因为它涉及到重新哈希所有元素。因此,通常在哈希表的大小增长到一定程度时才会触发扩容,以避免频繁的扩容操作。
在实际应用中,不同的哈希表实现可能会有不同的扩容策略和负载因子阈值。例如,Java 中的哈希表默认负载因子是 0.75,超过这个值就会进行扩容。而C++中的'unordered_map'和 'unordered_set'的默认负载因子是 1,这意味着它们会在哈希表大小达到两倍时进行扩容。用户可以通过构造函数或成员函数 'reserve' 来调整负载因子,以控制扩容的时机。
线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。
- 二次探测
二次探测的步骤如下:
- 计算初始位置:首先,使用哈希函数计算元素的初始位置。
- 检查初始位置:如果初始位置是空的,或者可以安全地插入元素(例如,对于只存储键的集合,新元素与初始位置的元素具有相同的哈希值),则将元素插入到该位置。
- 二次探测:如果初始位置被占用,则计算下一个位置。这个位置是通过以下公式计算的:下一个位置 = (初始位置 + i^2) % 哈希表大小
其中,i 是一个整数,通常从 1 开始,每次增加 1,直到找到一个空闲的位置或者达到哈希表的大小。- 重复步骤:重复步骤 2 和 3,直到找到一个空闲的位置或者达到哈希表的大小。
- 插入元素:将元素插入到找到的空闲位置
二次探测法也会导致数据“堆积”现象,导致搜索效率降低。
3.5.2 开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列把发生哈希冲突的元素都同一个桶中。
开散列的实现:
namespace Hash_Bucket
{
enum status {
Empty,
Exist,
Delete
};
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode* _next;
HashNode(const pair<K,V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
{
_table.resize(10);
}
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first)) return false;
Hash hf;
//设平衡因子为1
if (_count == _table.size())
{
vector<Node*> newtable;
newtable.resize(_table.size() * 2);
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
Node* next;
while (cur)
{
next = cur->_next;
size_t hashi = hf(cur->_kv.first) % newtable.size();
cur->_next = newtable[hashi];
newtable[hashi] = cur;
cur = next;
}
}
_table.swap(newtable);
}
//新节点直接头插
int hashi = hf(kv.first) % _table.size();
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_count++;
return true;
}
Node* Find(const K& key)
{
Hash hf;
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
//伪删除
bool Erase(const K& key)
{
Hash hf;
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
_count--;
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
private:
vector<Node*> _table;
size_t _count = 0; // 存储的关键字的个数
};
}
发生哈希冲突时,向桶中插入元素,可以选择头插或尾插,或者其它方式,在此我选择的是头插。
析构哈希桶时,要将桶内的每一个元素都清除掉。
开散列增容
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可
能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,
再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可
以给哈希表增容。即负载因子为1。如果像之前一样,直接将原来的元素拷贝到新的哈希桶中,则会增加拷贝和删除的时间,为了解决这种问题,采用的方式是:将原来的节点根据新哈希桶的映射关系,链入到新的哈希桶
//设平衡因子为1 if (_count == _table.size()) { vector<Node*> newtable; newtable.resize(_table.size() * 2); for (size_t i = 0; i < _table.size(); i++) { Node* cur = _table[i]; Node* next; while (cur) { next = cur->_next; size_t hashi = hf(cur->_kv.first) % newtable.size(); cur->_next = newtable[hashi]; newtable[hashi] = cur; cur = next; } }
3.5.3 思考
1. 哈希函数采用处留余数法,被模的key必须要为整形才可以处理,下面的方法将key转化为
整形。
//仿函数,求key对应的数字
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//模板特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& str)
{
size_t ret = 0;
for (auto& e : str)
{
ret *= 31; //降低str总和相同的概率
ret += e;
}
return ret;
}
};
第一个模板特化用于任何类型的 K,它简单地将键值直接转换为其整数值。这意味着如果 K 是一个整数,那么这个哈希函数将直接返回该整数作为哈希值。
第二个模板特化专门用于字符串类型,它计算字符串的哈希值。这个哈希函数使用了一个简单的哈希算法,它将字符串中的每个字符转换为其整数值,并将这些值相加,然后乘以一个质数(在这个例子中是 31),最后返回结果。
如果key是abc或acb或aad,它们对应ASCII码总和相同,为了区分,将ret的每一次结果乘以31,来降低它们冲突的概率。
要注意的是,这种冲突无法避免,例如仅仅10个字符,对应的情况就有26^10,而整数的范围是2^32,即相对而言,整数是有限的,而字符串情况是无限的,所以冲突是无法避免的。
2. 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
前面讲到,stl中使用的哈希表是素数大小,它是将一组素数存储在一个数组中,要扩容时,就在找数组中找到比哈希表size 大的下一个素数。
3. 闭散列和开散列的比较
开散列:
- 开散列是指在哈希表中插入元素时,如果发生冲突,直接在表中查找下一个空闲位置,并将元素存储在那里。
- 开散列不需要额外的存储空间来存储链表,因为它直接在表中处理冲突。
- 开散列通常要求保持较高的空闲空间,以避免冲突导致的性能下降。需要使用负载因子进行控制。
- 开散列的优点是实现简单,不需要额外的内存开销,但在最坏情况下,查找、插入和删除操作的时间复杂度可能会退化到 O(n)。
闭散列:
- 闭散列是指在哈希表中插入元素时,如果发生冲突,使用链表将所有冲突的元素存储在一起。
- 闭散列需要额外的存储空间来存储链表结构,因为每个冲突的元素都需要一个指针指向链表中的下一个元素。
- 闭散列不需要保持大量的空闲空间,因为它通过链表解决了冲突,即使哈希表中有很多元素,只要链表长度合理,查找效率也不会下降。
- 闭散列的优点是在最坏情况下,查找、插入和删除操作的时间复杂度仍然是 O(1),因为它避免了在表中查找空闲位置的开销。
由于开放地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=
0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。
四、模拟实现
在C++中,std::unordered_map 和 std::unordered_set 默认使用开散列方法来解决哈希冲突。
4.1 模板参数列表的改造
unodered_set和unodered_map的元素为K和pair<K,V>,为了确定是哪种类型,直接将HashNode的类型改为:
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
为了确定T的关键字,HashTable需要添加一个模板参数 KeyOfT。
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
//...
};
HashFunc来将关键字转换成整数,通过HashFunc和KeyOfT 结合使用,可以查找到元素在HashTable中的索引(下标)。
4.2 增加迭代器
为了实现简单,在哈希桶的迭代器类中需要用到HashBucket本身。
迭代器的模板参数要有Ref 和 Ptr ,用来实现普通迭代器和 const 迭代器。
typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
//前置声明
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 __HTIterator
{
typedef HashNode<T> Node;
typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
Node* _pnode;
const HashTable<K, T, KeyOfT, Hash>* _pht;
size_t _hashi; //定位哈希表中的位置,使得++可以到下一个桶
__HTIterator(Node* pnode, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
:_pnode(pnode)
, _pht(pht)
, _hashi(hashi)
{}
Self& operator++()
{
if (_pnode->_next)
{
_pnode = _pnode->_next;
}
else
{
_hashi++;
while (_hashi < _pht->_table.size())
{
if (_pht->_table[_hashi]) break;
else _hashi++;
}
if (_hashi == _pht->_table.size()) _pnode = nullptr;
else _pnode = _pht->_table[_hashi];
}
return *this;
}
Ref operator*()
{
return _pnode->_data;
}
Ptr operator->()
{
return &_pnode->_data;
}
bool operator!=(const Self& other)
{
return _pnode != other._pnode;
}
bool operator ==(const Self& other)
{
return _pnode == other._pnode;
}
};
实现operator++时,当前迭代器所指节点后还有节点时直接取其下一个节点,如果该节点是当前桶的最后一个节点,需要找到下一个不空的桶,返回该桶中第一个节点。
4.3 模拟实现HashTable
//unordered_set -> HsahTable<K, K,...>
//unordered_set -> HsahTable<K, pair<const K,V>,...>
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
typedef HashNode<T> Node;
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct __HTIterator;
public:
typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
//构造函数格式
//__HTIterator(Node* pnode, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
iterator begin()
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]) return iterator(_table[i], this, i);
}
return end();
}
const_iterator begin()const
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]) return const_iterator(_table[i], this, i);
}
return end();
}
iterator end()
{
return iterator(nullptr, this, -1);
}
const_iterator end()const
{
return const_iterator(nullptr, this, -1);
}
HashTable()
{
_table.resize(10);
}
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
pair<iterator, bool> Insert(const T& data)
{
KeyOfT kot;
iterator it = Find(kot(data));
if (it != end()) return make_pair(it,false);
Hash hf;
//设平衡因子为1
if (_count == _table.size())
{
vector<Node*> newtable;
newtable.resize(_table.size() * 2);
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
Node* next;
while (cur)
{
next = cur->_next;
size_t hashi = hf(kot(cur->_data)) % newtable.size();
cur->_next = newtable[hashi];
newtable[hashi] = cur;
cur = next;
}
}
_table.swap(newtable);
}
//新节点直接头插
int hashi = hf(kot(data)) % _table.size();
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_count++;
return make_pair(iterator(newnode, this, hashi), true);
}
iterator Find(const K& key)
{
Hash hf;
KeyOfT kot;
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur,this,hashi);
}
else
{
cur = cur->_next;
}
}
return end();
}
//伪删除
bool Erase(const T& data)
{
KeyOfT kot;
Hash hf;
size_t hashi = hf(kot(data)) % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_data == data)
{
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
_count--;
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
void Print()
{
for (size_t i = 0; i < _table.size(); i++)
{
cout << "[" << i << "]: ";
Node* cur = _table[i];
while (cur)
{
cout << cur->_data << "->" << cur->_data << " ";
cur = cur->_next;
}
cout << endl;
}
}
private:
vector<Node*> _table;
size_t _count = 0; // 存储的关键字的个数
};
4.4 模拟实现 unodered_set
要注意的是,之前在模拟实现实现map和set时,可以直接将成员调用Insert的返回值,传给map、set的insert返回值进行拷贝构造。但是此处不行,因为上面的返回值虽然传递的pair<iterator, bool>,但是它的iterator类型是Node*,可以直接构造成iterator。
此处的iterator包含三个成员:
__HTIterator(Node* pnode, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
:_pnode(pnode)
, _pht(pht)
, _hashi(hashi)
{}
不能再简单的拷贝构造,需要自己写一个构造函数。
template<class K, class Hash = HashFunc<K>>
class myUnorderedSet
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename Hash_Bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
typedef typename Hash_Bucket::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<const_iterator, bool> insert(const K& key)
{
auto ret = _ht.Insert(key);
return pair<const_iterator, bool>(const_iterator(ret.first._pnode, ret.first._pht, ret.first._hashi), ret.second);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
iterator find(const K& key)
{
return _ht.Find(key);
}
void Print()
{
_ht.Print();
}
private:
Hash_Bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
};
4.5 模拟实现 unodered_map
template<class K, class V, class Hash = HashFunc<K>>
class myUnorderedMap
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename Hash_Bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
typedef typename Hash_Bucket::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<const K, V>& data)
{
auto ret = _ht.Insert(data);
return pair<iterator, bool>(iterator(ret.first._pnode, ret.first._pht, ret.first._hashi), ret.second);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
const V& operator[](const K& key)const
{
pair<iterator, bool> ret = _ht.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:
Hash_Bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};