【C++】哈希
- unordered系列关联式容器
- 底层结构
- 模拟实现
-
- 哈希桶的改造
- [unordered_map 的封装](#unordered_map 的封装)
-
- 模板参数
- 成员变量
- 默认成员函数
- 迭代器
- [begin(), end()](#begin(), end())
- insert、find、erase
- operator[]
- 代码
- 测试
- [unordered_set 的封装](#unordered_set 的封装)
unordered系列关联式容器
map 和 set 底层使用的结构是树形结构,也就是红黑树,查找数据的效率可以达到O(log n),即树的高度次,这个效率已经很高了,但是当树的节点非常多时,效率就不是很理想了
在 C++11 中,STL增添了四个 unordered 系列的关联式容器,它们的底层是哈希结构,采用哈希表来实现。在理想情况下,查询效率可以接近 O(1)
这四个容器是:unordered_map、unordered_set、unordered_multimap 和 unordered_multiset,它们的使用和 map/set 的使用类似,只是底层结构不同,所以这里只稍微讲解一下 unordered_map 的使用
unordered_map
相关文档:unordered_map
- unordered_map是存储 <key, value>键值对 的关联式容器,其允许通过 keys 快速的索引到与其对应的value
- 在unordered_map中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同,例如<int, int>,<string, int>
- 在内部,unordered_map 存储<kye, value>是无序 的,这一点与红黑树不同, 为了能在常数范围内找到key所对应的value,unordered_map 将相同哈希值 的键值对放在相同的哈希桶中
- unordered_map 容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭代方面效率较低
- unordered_maps 实现了直接访问操作符 operator[],它允许使用key作为参数直接访问value:当键值对存在时,会返回 key 对应的 value;不存在,就插入新的键值对 <key, 默认value>,并返回 value
- 它的迭代器是单向迭代器:迭代器只能++,且没有反向迭代器
哈希桶:
接口
构造
函数声明 | 功能介绍 |
---|---|
unordered_map | 构造不同格式的 unordered_map 对象 |
容量
函数声明 | 功能介绍 |
---|---|
bool empty() const | 检测 unordered_map 是否为空,如果容器中没有元素,则返回 true;否则返回 false |
size_t size() const | 获取 unordered_map 的有效元素个数,返回容器中存储的键值对的数量 |
迭代器
函数声明 | 功能介绍 |
---|---|
begin() | 返回 unordered_map 第一个元素的迭代器 |
end() | 返回 unordered_map 最后一个元素下一个位置的迭代器 |
元素访问
函数声明 | 功能介绍 |
---|---|
operator[] | 返回与key对应的value,键值对不存在就返回一个默认值 |
注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,将key对应的value返回
查询
函数声明 | 功能介绍 |
---|---|
iterator find(const K& key) | 返回给定 key 在哈希桶中的位置的迭代器。如果 key 不存在于容器中,则返回容器的 end()迭代器。 |
size_t count(const K& key) | 返回哈希桶中关键码为 key 的键值对的个数。对于 unordered_map,由于不允许重复的键,所以该函数通常返回 0 或 1。 |
修改
函数声明 | 功能介绍 |
---|---|
insert | 向容器中插入键值对 |
erase | 删除容器中的键值对 |
void clear() | 清空容器中有效元素个数 |
void swap(unordered_map&) | 交换两个容器中的元素 |
桶操作
函数声明 | 功能介绍 |
---|---|
size_t bucket_count() const | 返回哈希桶中桶的总个数。这个值表示哈希表被划分成的桶的数量。不同的哈希函数和负载因子会影响桶的数量 |
size_t bucket_size(size_t n) const | 返回指定编号 n 的桶中有效元素的总个数 |
size_t bucket(const K& key) | 返回给定元素 key 所在的桶号。这个函数可以确定特定元素在哈希表中被存储到哪个桶中 |
底层结构
哈希概念
哈希(又被称为散列),是一种思想,而 哈希表(散列表) 是利用哈希思想创建出来的一种数据结构。那么什么是哈希呢?
简单来说,哈希就是一种映射 :在值与值之间建立一种关联,这种关联可以是1对1 ,也可以是1对多
例如这道算法题:387. 字符串中的第一个唯一字符 - 力扣(LeetCode)
- 根据哈希思想,我们可以建立一个映射 <char,int>,建立一个哈希表,用于记录每个字符的频次,遍历一遍字符串即可统计出来。
- 再遍历字符串,在哈希表中查看每个字符的频次,当出现频次为1的字符时,即可返回
上面这种哈希表是建立 字符-次数 的映射,而本章要讲的哈希表是建立key-存储位置 的映射关系。这样,通过哈希函数 使元素的 key 与 存储位置建立一一对应的映射关系,就可以很快地找到相应元素
- 插入元素 根据元素的 key ,通过哈希函数计算出对应的存储位置,将元素存放
- 查找元素 根据要查找元素的 key,计算出元素的存储位置,再去对应的位置取出相应的元素,如果 key 相等,则查找成功
例如,数据集合{1, 37, 26, 4, 15, 99}
- 哈希函数:hash(key) = key % size(),size为哈希表的大小
思考:如果此时再插入一个 14,会发生什么?
14 计算出的哈希地址为 4,可是下标为 4 的空间已经被占用了
哈希冲突
对于两个不同的 key,计算出相同的哈希地址,这种现象叫做哈希冲突/哈希碰撞
把具有不同 key 而具有相同哈希地址的元素成为同义词
哈希函数
引起哈希冲突的一个重要原因:哈希函数设计不合理
哈希函数设计原则:
- 哈希函数的定义域必须包括所有需要存储的 key,如果哈希表有 m 个地址,那么其值域必须位于 0~m-1
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常用哈希函数:
- 直接定址法
取 key 的某个线性函数为哈希函数,hash(key) = A*key + B
这种方法适用于事先知道 key 分布情况的场景,如果 key 非常不连续,就会导致哈希表特别大,可能会浪费空间。例如数据集合{1, 999, 8},key 非常分散,只为了三个数开出很多空间,非常浪费
- 除留余数法
假设哈希表空间的大小为 m,那么就取一个小于 m,但是最接近或者等于 m 的数作为除数 p
hash(key) = key % p
这种方法就解决了 key 很分散的问题,再大的数,通过哈希函数计算得出的地址,都在哈希表范围内
哈希冲突解决
解决哈希冲突的两种方法:闭散列 和开散列
闭散列
闭散列又被称为开放地址法,当发生哈希冲突时,如果哈希表还有未被占用的位置,就到后面去寻找,将元素存入。
如何向后寻找?------线性探测法 :从发生冲突的位置开始,依次向后寻找 ,直到找到下一个空位置 。插入 和查找元素都可以用线性探测法
在查找元素时,如果没有对应元素,那么查找终止条件是什么呢?直到找到空位置就停止吗?如果是下面这种情况,就会有问题:删除 37 后,将该位置设为空,再寻找 14 就会提前终止,找不到14
所以要有一个枚举变量 表示数据的状态:存在,空,删除。这样在查找元素时,只有遇到空时才停止,遇到存在或者删除就继续向后查找
下面先实现一个闭散列的哈希表结构
闭散列的哈希表结构
首先定义一个类 HashData,用来存储键值对 元素。此外还要有一个枚举变量表示数据的状态:存在,空,删除
cpp
enum STATE
{
EMPTY,
EXIST,
DELETE
};
template <class K, class V>
struct HashData
{
pair<K, V> _kv;
STATE _sta = EMPTY;
};
HashTable 的底层容器是一个 vector,其中存的数据类型就是 HashData;此外还有一个成员变量表示表中的有效元素数量
cpp
template <class K, class V>
class HashTable
{
public:
HashTable()
{
_table.resize(10); // 哈希表初始大小
}
private:
vector<HashData<K, V>> _table; // 哈希表
size_t _n = 0; // 有效元素的数量
};
insert------线性探测法
插入数据时可能会发生冲突,发生哈希冲突时,闭散列的思路就是:我的位置被占用,那我就去占用别人的位置。
- 除留余数法计算元素的插入位置hashi。注意:计算 hashi 时,是
%size
,而不是%capacity
,用 capacity 可能会得到 size 之外的位置,在 vector 中要求空间连续使用,这样是不规范的
- 进行线性检测,如果当前位置的数据状态是 EXIST,就往后走
- 每次hashi都取模,防止越界
cpp
bool insert(const pair<K, V>& kv)
{
// 插入,除留余数法计算哈希地址
size_t hashi = kv.first % _table.size();
// 线性探测
while (_table[hashi]._sta == EXIST)
{
++hashi;
// 防止越界,要进行%操作,在哈希表范围之内寻找
hashi %= _table.size();
}
// 找到的位置是删除状态或者是空状态,插入元素
_table[hashi]._kv = kv;
_table[hashi]._sta = EXIST;
++_n;
return true;
}
载荷因子 && 扩容
上面的代码还有一个bug:如果哈希表满了,就会一直进行线性探测,陷入死循环。所以在插入之前,应该进行扩容检测
那扩容条件应该怎么设置呢?满了才扩容吗?哈希表的载荷因子:α = 表中元素/哈希表长度
α越大,说明插入元素越多,空位置越少,这种情况下就很容易发生哈希冲突,并且每次线性探测的消耗很高;α 越小,插入元素少,空位置多,自然发生冲入的可能性就越小,线性探测消耗也越小
所以,为了保证哈希表的效率,不能满了再扩容,那么达成什么条件才扩容呢?
一般当 α == 0.7 时,就进行扩容
cpp
_n/_table.size() == 0.7
// 两个整型相除不会有小数点,所以两边同时*10
_n*10/_table.size() == 7
哈希表扩容后,数据的 key 和存储位置要重新映射,因为哈希表的 size 变了 。而且不能在旧表的基础上重新映射,数据重新映射后可能会把还未重新映射的数据 覆盖,所以要创建一个新的哈希表,将旧表的数据荷载到新表,这里规定新表的大小是旧表的2倍
创建新表还有一个好处:不用我们手动把旧表数据插入到新表,直接调用新表的insert
函数,将旧表数据直接映射到新表,最后交换旧表和新表的 _table 即可
cpp
bool insert(const pair<K, V>& kv)
{
// 扩容
if (_n * 10 / _table.size() == 7)
{
HashTable<K, V> newHT;
newHT._table.resize(_table.size() * 2);
for (int i = 0; i < _table.size(); i++)
{
if (_table[i]._sta == EXIST)
{
newHT.insert(_table[i]._kv);
}
}
_table.swap(newHT._table);
}
// 插入
}
测试:
cpp
int arr[] = { 1, 37, 26, 4, 15, 99, 44, 12};
// 插入12,发生扩容
HashTable<int, int> ht;
for (auto e : arr)
ht.insert(make_pair(e, e));
find
- 计算 key 的存储位置,从存储位置开始寻找数据
- 找到,返回 HashData 指针;找不到,返回 nullptr
- 数据状态不为空,一直寻找
- 当找到相应的 key 而且数据状态存在时,返回
cpp
HashData<K, V>* find(const K& key)
{
size_t hashi = key % _table.size();
// 线性探测
while (_table[hashi]._sta != EMPTY)
{
// 元素存在,且 key 与要找的 key 相同
if (_table[hashi]._sta == EXIST && _table[hashi]._kv.first == key)
return &_table[hashi];
++hashi;
hashi %= _table.size();
}
// 找不到
return nullptr;
}
测试:
insert完善:在插入数据之前,可以使用find查看key是否已经存在,防止出现重复key
cpp
bool insert(const pair<K, V>& kv)
{
// 检查键是否已经存在
if (find(kv.first))
return false;
}
erase
- 要删除哈希表中的数据,不需要真的删除,只需要将那个位置的数据状态改为 DELETE 即可
- 可以用 find 检查要删除的数据是否存在,不存在就返回
- 存在,find 返回的是数据的指针,通过指针修改数据状态
cpp
bool erase(const K& key)
{
HashData<K, V>* ret = find(key);
if (ret == nullptr) // 数据不存在
return false;
// 存在,删除
ret->_sta = DELETE;
--_n;
return true;
}
测试:
将非整型key转换为整型
在上面的哈希表中,我们采用除留余数法来建立 key 与储存位置的映射关系,这样有一个问题:被模的 key 只能是整型,而且不能是负数,如果是其他整型 key 怎么办?
像 float、double、char 等类型都是整型家族的,可以强转为无符号整型;但是如果 key 是自定义类型,例如 string 等,不可以强转,该怎么办呢?------这时就又轮到仿函数出场了,我们需要根据 key 的类型,提供相应的转换方法,然后将仿函数作为模板传递给哈希表 HashTable
写一个 HashFunc,将可以转换为整形的类型转换(例如 float),HashFunc 作为默认模板参数,这样就不用每次都传参了
cpp
template <class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{}
字符串哈希
如果哈希表中的 key 类型是 string,那我们还可以写一个 StringHashFunc,作为模板参数传递
cpp
struct StringHashFunc
{
size_t operator()(const string& s)
{}
};
那么问题来了,该如何将 string 转换为无符号整型呢?------没有硬性规定,只要能通过一定的操作,使得转换出来的数据重复率比较低即可
- 例如,可以根据字符串首字符的ASCII码值,来当作结果。但是这样非常容易重复,例如"abcd"->97,"add"->97。key 一旦重复,就容易发生哈希冲突
- 或者可以把字符串中所有字符的ASCII码值加起来,作为结果。这样虽然可以减少一些重复,但是效果不太理想。例如"abab"和"aabb",他们两个转换结果相同
这里不再列举其他方法,字符串哈希的算法可以参考这篇博客:各种字符串哈希函数
这里选取上述博客中的BKDRHash算法作为字符串哈希函数
cpp
struct StringHashFunc
{
size_t operator()(const string& s)
{
size_t ret = 0;
for (auto& e : s)
{
ret = ret * 131 + e;
}
return ret;
}
};
仿函数Hash的使用
在使用 key 进行除留余数法进行运算之前,需要将 key 转换为整型
- 可以先创建一个 Hash 有名对象 hs,然后通过 hs 来转换
cpp
// 插入,除留余数法计算哈希地址
Hash hs;
size_t hashi = hs(kv.first) % _table.size();
- 也可以直接使用匿名对象
cpp
// 插入,除留余数法计算哈希地址
size_t hashi = Hash()(kv.first) % _table.size();
需要将用到 key 进行模运算的地方全部替换掉,这里就不贴代码了
测试:
仿函数特化
虽然现在可以转换string为整型,但是每次都得传递仿函数,而使用 unordered_map 时,似乎不需要传递仿函数,也可以使用 string 作为 key 的类型
这是因为,string 作为 key 是很常见的,所以库对默认的 HashFunc 进行了特化,特化版本会对 string 类型的 key 进行转换
我们也可以将自己写的 StringHashFunc 进行特化
cpp
// 转换 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& s)
{
size_t ret = 0;
for (auto& e : s)
{
ret = ret * 131 + e;
}
return ret;
}
};
这样使用 string 作为 key 的类型时,就不用每次都传仿函数了
闭散列代码
cpp
// 转换 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& s)
{
size_t ret = 0;
for (auto& e : s)
{
ret = ret * 131 + e;
}
return ret;
}
};
namespace OpenAddress
{
enum STATE
{
EMPTY,
EXIST,
DELETE
};
template <class K, class V>
struct HashData
{
pair<K, V> _kv;
STATE _sta = EMPTY; // 数据的状态
};
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;
// 扩容
if (_n * 10 / _table.size() == 7)
{
HashTable<K, V, Hash> newHT;
newHT._table.resize(_table.size() * 2);
for (int i = 0; i < _table.size(); i++)
{
if (_table[i]._sta == EXIST)
{
newHT.insert(_table[i]._kv);
}
}
_table.swap(newHT._table);
}
// 插入,除留余数法计算哈希地址
size_t hashi = Hash()(kv.first) % _table.size();
//线性探测
while (_table[hashi]._sta == EXIST)
{
++hashi;
// 防止越界,要进行%操作,在哈希表范围之内寻找
hashi %= _table.size();
}
// 找到的位置是删除状态或者是空状态,插入元素
_table[hashi]._kv = kv;
_table[hashi]._sta = EXIST;
++_n;
return true;
}
HashData<K, V>* find(const K& key)
{
size_t hashi = Hash()(key) % _table.size();
// 线性探测
while (_table[hashi]._sta != EMPTY)
{
// 元素存在,且 key 与要找的 key 相同
if (_table[hashi]._sta == EXIST && _table[hashi]._kv.first == key)
return &_table[hashi];
++hashi;
hashi %= _table.size();
}
// 找不到
return nullptr;
}
bool erase(const K& key)
{
HashData<K, V>* ret = find(key);
if (ret == nullptr) // 数据不存在
return false;
// 存在,删除
ret->_sta = DELETE;
--_n;
return true;
}
private:
vector<HashData<K, V>> _table; // 哈希表
size_t _n = 0; // 有效元素的数量
};
}
下面我们来看解决哈希冲突的另一种方法:开散列
开散列(哈希桶)
开散列法也被称为链地址法(开链法):对 key 通过哈希函数计算哈希地址,具有相同哈希地址的元素存放于同一子集合中,每个子集合被称为桶,每个桶中的元素通过一个单链表链接起来,每个单链表的头节点地址存放于哈希表中
每个桶中存放的都是发生哈希冲突的数据。下面我们来实现一个开散列的哈希表
开散列的哈希表结构
用于存储哈希数据的结构是一个个节点 HashNode
cpp
template <class K, class V>
struct HashNode
{
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{}
pair<K, V> _kv;
HashNode<K, V>* _next;
};
哈希表 HashTable 中存的是节点指针,用来指向每个桶
cpp
template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
{
_table.resize(10, nullptr); // 初始给10个空间
}
~HashTable()
{
// 删除每个桶中的节点
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
private:
vector<Node*> _table; // 存放结点指针
size_t _n = 0; // 有效元素数量
};
insert
- 计算出 key 的哈希地址,new出一个新节点来存放插入的数据
- 直接将新节点头插到对应的哈希桶中,然后将新节点的值存入哈希表
- 头插的好处就是不用管桶是否为空
cpp
bool insert(const pair<K, V>& kv)
{
// 计算哈希地址
size_t hashi= Hash()(kv.first) % _table.size();
// new 新节点,头插
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return true;
}
扩容
理论情况下,每次插入数据插入都会到相应的桶中,可以不进行扩容吗?随着节点的不断增加,极端情况下,桶中的链表节点非常多,导致查找效率下降,影响哈希表性能,所以需要进行扩容
最理想的情况就是每个桶只有一个节点 ,这样下次插入时一定会发生哈希冲突,所以扩容条件就是元素个数等于哈希表大小
那么具体怎么扩容呢?还是像闭散列那样,创建一个新的哈希表,然后调用新表的insert
吗?
可以是可以,但是有不必要的损耗:insert是会new新节点的,也就是将旧表的节点重新new一遍,然后将旧表的节点释放掉,先new一遍再释放一遍,这不就是没有意义的操作吗,还会增加性能开销
所以我们要发挥链表的优势 ,将旧表的节点一一链接 到新表,只不过要对每个节点重新映射,因为新表的size改变了
例如,旧表的 size == 10,那么 12,22是在同一桶中的;而新表的 size == 20,重新映射后,12和22就不在同一桶中了
cpp
bool insert(const pair<K, V>& kv)
{
// 扩容
if (_n == _table.size())
{
// 创建新表
vector<Node*> newHT(_table.size()*2, nullptr);
// 将旧表节点重新映射
for (int i = 0; i < _table.size(); i++)
{
// 遍历桶
Node* cur = _table[i];
while (cur)
{
// 记录旧表当前桶中下一个节点
Node* next = cur->_next;
// 将当前节点映射到新表
size_t hashi = Hash()(cur->_kv.first) % newHT.size();
cur->_next = newHT[hashi];
newHT[hashi] = cur;
// 更新cur,旧表当前桶的下一节点
cur = next;
}
// 旧表置空
_table[i] = nullptr;
}
// 新表映射完成,新旧表交换
_table.swap(newHT);
}
// 插入
}
find
- 根据 key 计算出哈希地址,确定目标数据在哪个桶中
- 遍历对应的桶,当元素的 key 等于要找到 key 时,返回节点指针
- 遍历完桶后还没找到,返回 nullptr
cpp
Node* find(const K& key)
{
// 确定桶
size_t hashi = Hash()(key) % _table.size();
Node* cur = _table[hashi];
// 遍历桶
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
// 找不到
return nullptr;
}
同样,有了 find 后,就可以完善 insert:在插入之前,查看 key 是否已存在,不允许插入已存在 key
cpp
bool insert(const pair<K, V>& kv)
{
// 检查 key 是否重复
if (find(kv.first))
return false;
}
erase
- 计算哈希地址,确定目标在那个桶
- 遍历桶,找到目标cur后,有两种情况
- cur 是桶的头节点,将next节点存入哈希表,删除cur
- cur 不是头节点,需要将cur的前一节点和后一节点链接,删除cur
- 遍历完桶还找不到,返回false
cpp
bool erase(const K& key)
{
// 计算哈希地址
size_t hashi = Hash()(key) % _table.size();
// 遍历桶
Node* cur = _table[hashi];
Node* prev = nullptr; // 前一个节点
while (cur)
{
if (cur->_kv.first != key)
{
prev = cur;
cur = cur->_next;
}
else // 找到目标
{
// 情况1,删除的是头节点
if (prev == nullptr)
_table[hashi] = cur->_next;
// 情况2,删除中间节点
else
prev->_next = cur->_next;
delete cur;
return true;
}
}
return false;
}
测试:
因为后面我们要使用哈希桶模拟实现 unordered_map 和 unordered_set,所以下面完善一下拷贝相关接口
拷贝构造
- 拷贝构造 h2(h1),不需要重新映射,只需要将h1的节点完全拷贝一遍即可,h1的元素映射关系就是h2的
- 提前将 h2 的大小调整到与 h1 一致
- 对每个桶进行拷贝链表的操作,将拷贝后的链表的头节点地址存入 h2。拷贝链表时是尾插,所以提前开一个哨兵位,这样操作起来就很舒服
- 拷贝链表时可以设置一个哨兵位节点,这样操作起来更加简单,不用判空等操作
cpp
HashTable(const HashTable<K, V, Hash>& ht)
{
// 调整大小
_table.resize(ht._table.size());
// 拷贝链表
Node* copyHead = new Node(pair<K, V>()); // 拷贝链表的哨兵位
for (int i = 0; i < ht._table.size(); i++)
{
Node* cur = ht._table[i]; // 遍历桶
Node* copyCur = copyHead; // 遍历拷贝链表
// 遍历,拷贝
while (cur)
{
// 拷贝
copyCur->_next = new Node(cur->_kv);
// 向下走
cur = cur->_next;
copyCur = copyCur->_next;
}
// 将拷贝的桶的头节点放到 h2
_table[i] = copyHead->_next;
copyHead->_next = nullptr;
}
_n = ht._n;
delete copyHead;
}
测试:
赋值重载operator=
- 传值传参,调用拷贝构造,构造一个临时对象t
- 将 t 的表和 this 的表交换
- 出了函数,t会自动销毁
cpp
HashTable<K, V, Hash>& operator=(HashTable<K, V, Hash> t)
{
_table.swap(t._table);
_n = t._n;
return *this;
}
代码
cpp
// 2.哈希桶
namespace hash_bucket
{
template <class K, class V>
struct HashNode
{
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{}
pair<K, V> _kv;
HashNode<K, V>* _next;
};
template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
{
_table.resize(10, nullptr);
}
~HashTable()
{
// 删除每个桶中的节点
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
// h2(h1)
HashTable(const HashTable<K, V, Hash>& ht)
{
// 调整大小
_table.resize(ht._table.size());
// 拷贝链表
Node* copyHead = new Node(pair<K, V>()); // 拷贝链表的哨兵位
for (int i = 0; i < ht._table.size(); i++)
{
Node* cur = ht._table[i]; // 遍历桶
Node* copyCur = copyHead; // 遍历拷贝链表
// 遍历,拷贝
while (cur)
{
// 拷贝
copyCur->_next = new Node(cur->_kv);
// 向下走
cur = cur->_next;
copyCur = copyCur->_next;
}
// 将拷贝的桶的头节点放到 h2
_table[i] = copyHead->_next;
copyHead->_next = nullptr;
}
_n = ht._n;
delete copyHead;
}
// h2 = h1
HashTable<K, V, Hash>& operator=(HashTable<K, V, Hash> t)
{
_table.swap(t._table);
_n = t._n;
return *this;
}
bool insert(const pair<K, V>& kv)
{
// 检查 key 是否重复
if (find(kv.first))
return false;
// 扩容
if (_n == _table.size())
{
// 创建新表
vector<Node*> newHT(_table.size()*2, nullptr);
// 将旧表节点重新映射
for (int i = 0; i < _table.size(); i++)
{
// 遍历桶
Node* cur = _table[i];
while (cur)
{
// 记录旧表当前桶中下一个节点
Node* next = cur->_next;
// 将当前节点映射到新表
size_t hashi = Hash()(cur->_kv.first) % newHT.size();
cur->_next = newHT[hashi];
newHT[hashi] = cur;
// 更新cur,旧表当前桶的下一节点
cur = next;
}
// 旧表置空
_table[i] = nullptr;
}
// 新表映射完成,新旧表交换
_table.swap(newHT);
}
// 插入
// 计算哈希地址
size_t hashi= Hash()(kv.first) % _table.size();
// new 新节点,头插
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return true;
}
Node* find(const K& key)
{
// 确定桶
size_t hashi = Hash()(key) % _table.size();
Node* cur = _table[hashi];
// 遍历桶
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
// 找不到
return nullptr;
}
bool erase(const K& key)
{
// 计算哈希地址
size_t hashi = Hash()(key) % _table.size();
// 遍历桶
Node* cur = _table[hashi];
Node* prev = nullptr; // 前一个节点
while (cur)
{
if (cur->_kv.first != key)
{
prev = cur;
cur = cur->_next;
}
else // 找到目标
{
// 情况1,删除的是头节点
if (prev == nullptr)
_table[hashi] = cur->_next;
// 情况2,删除中间节点
else
prev->_next = cur->_next;
delete cur;
return true;
}
}
return false;
}
private:
vector<Node*> _table;
size_t _n = 0;
};
}
模拟实现
使用哈希桶来模拟实现 unordered_map/set,在那之前我们需要对上面写的哈希桶做一些改造
哈希桶的改造
模板参数列表
- K:key 的类型,这里的 K 是给 find 等函数用的。取 key 需要到 T 中取,而不是用这里的 K
- T:value 的类型,unordered_map 的T类型是 pair,unordered_set 的T类型是 K
- KeyOfT:仿函数对象类型,取出 T 的key,不同的 T 取 key 的方法也不同。
- Hash:仿函数对象类型,将 key 转化为整数。哈希函数使用除留余数法,需要将 key 转化为整型数字才能取模。这里的Hash 由封装后的 unordered_map/set 提供,不再使用默认
cpp
template <class K, class T, class KeyOfT, class Hash>
class HashTable
{
typedef HashTable<K, T, KeyOfT, Hash> Self;
typedef HashNode<T> Node;
};
因为 HashTable 模板参数发生改变,用于存储数据的 HashNode 的成员变量也要相应改变,值类型由 pair<K, V> 改为T,这样既可以储存 pair,又可以储存 K
cpp
template <class T>
struct HashNode
{
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
T _data;
HashNode<T>* _next;
};
增加迭代器
因为哈希桶的底层是单链表,所以使用原生节点指针 就可以当作迭代器,但是原生指针的行为满足不了哈希桶。例如,当迭代器处于桶的最后一个节点,++操作需要到下一个桶的第一个节点,原生指针的++做不到,所以需要将原生指针封装为一个迭代器类 ,重载运算符。也因为底层是单链表,所以迭代器只支持单向迭代器
另外,迭代器内部只有节点指针,它如何知道下一个桶是什么呢?所以迭代器内部还会用到HashTable 本身 ,将 HashTable 指针作为迭代器类的另一个成员变量
同时,为了实现 const 迭代器,还需要两个模板参数:
- Ref:代表 T& 或者 const T&
- Ptr:代表 T* 或者 constT*
最终,模板参数就变成了这么长:
cpp
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;
typedef HashTable<K, T, KeyOfT, Hash> HashTable;
__HTIterator(Node* node, const HashTable* pht) // 注意const
:_node(node)
, _pht(pht)
{}
Node* _node;
const HashTable* _pht;
};
注意!HashTable*
一定要加 const,防止权限放大。如果不加 const,那么当哈希表是 const 对象时,将 const 指针 赋值给非const指针 ,就会导致权限放大。一开始我忘了加 const,报错找bug找了好久(😓
最后的最后,由于 __HTIterator 和 HashTable 这两个类互相需要 ,所以需要在第一个类前面声明另外一个类,要不然第一个类找不到第二个类;而且 __HTIterator 会用到 HashTable 的私有****成员变量 ,所以需要将__HTIterator 设置为 HashTable 的友元类
cpp
// 声明
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
{};
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 __HTIterator;
};
如果不想要上述的一系列操作,可以把迭代器写成 HashTable 的内部类
cpp
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
public:
// 内部类
template<class Ptr, class Ref>
struct __HTIterator
{
typedef HashNode<T> Node;
typedef __HTIterator Self;
}
};
但是 C++ 不爱用内部类,所以这里还是使用声明+友元的方式
operator* 和 operator->
*
获取 T 的引用,这样就可以直接修改元素了,与原生指针功能类似->
获取 T 的指针 ,使用时大概就是这样it.operator->()->成员
,虽然看起来很麻烦但是可以直接这样使用it->成员
配合模板使用可以将迭代器实例化成普通迭代器或者const迭代器:
- Ref:可以传递 T& 或者 const T&
- Ptr:可以传递 T* 或者 const T*
cpp
//*it
Ref operator*()
{
return _node->_data;
}
// it->
Ptr operator->()
{
return &_node->_data;
}
!= 和 ==
直接判断两个迭代器内部的节点指针即可
cpp
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
operator++
迭代器有两种情况:
- 迭代器指向当前桶的中间节点,直接跳到 next 即可
- 迭代器指向当前桶的最后一个节点,此时需要借助哈希表,找到下一个不为空的桶,并指向此桶的第一个节点,如果找不到,说明当前桶迭代完毕,返回空迭代器即可
cpp
Self& operator++()
{
// 1.中间节点
if (_node->_next)
{
_node = _node->_next;
}
// 2. 最后节点
else
{
// 计算当前桶位置
KeyOfT kot;
size_t hashi = Hash()(kot(_node->_data)) % _pht->_table.size();
++hashi;
// 向后寻找非空桶
for (; hashi < _pht->_table.size(); hashi++)
{
if (_pht->_table[hashi])
break;
}
// 判断是走完没找到,还是找到后break了
if (hashi == _pht->_table.size())
_node = nullptr; // 走完
else
_node = _pht->_table[hashi]; // 找到
}
return *this;
}
哈希桶的迭代器接口
迭代器类完成后,就可以给哈希桶添加迭代器接口了
cpp
template <class K, class T, class KeyOfT, class Hash>
class HashTable
{
public:
typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
}
begin()
- 返回哈希表的第一个非空桶的第一个节点
- 如果哈希表为空,则返回 end
- 构造迭代器的哈希表地址可以使用 this 指针
cpp
iterator begin()
{
// 寻找第一个非空桶
for (int i = 0; i < _table.size(); i++)
if (_table[i])
return iterator(_table[i], this);
// 找不到
return end();
}
const_iterator begin() const
{
// 寻找第一个非空桶
for (int i = 0; i < _table.size(); i++)
if (_table[i])
return iterator(_table[i], this);
// 找不到
return end();
}
end()
- end() 指向哈希表最后一个节点的下一个位置,理论上为空
cpp
iterator end()
{
return iterator(nullptr, this);
}
const_iterator end() const
{
return iterator(nullptr, this);
}
修改find
- 之前的 find 返回类型是 Node*,有了迭代器之后就可以返回迭代器了
- 找到返回节点迭代器,找不到返回 end()
cpp
iterator find(const K& key)
{
KeyOfT kot;
// 确定桶
size_t hashi = Hash()(key) % _table.size();
Node* cur = _table[hashi];
// 遍历桶
while (cur)
{
if (kot(cur->_data) == key)
return iterator(cur, this);
cur = cur->_next;
}
// 找不到
return end();
}
修改insert
- insert 返回类型是一个 pair<iterator, bool>
- 数据不存在,插入成功,返回 pair<插入节点的迭代器, true>
- 数据已存在,插入失败,返回 pair<已存在节点的迭代器, false>
cpp
pair<iterator, bool> insert(const T& data)
{
KeyOfT kot;
// 检查 key 是否重复
iterator ret = find(kot(data));
if (ret != end())
return make_pair(ret, false);
// 扩容
if (_n == _table.size())
{
// 创建新表
vector<Node*> newHT(_table.size()*2, nullptr);
// 将旧表节点重新映射
for (int i = 0; i < _table.size(); i++)
{
// 遍历桶
Node* cur = _table[i];
while (cur)
{
// 记录旧表当前桶中下一个节点
Node* next = cur->_next;
// 将当前节点映射到新表
size_t hashi = Hash()(kot(cur->_data)) % newHT.size();
cur->_next = newHT[hashi];
newHT[hashi] = cur;
// 更新cur,旧表当前桶的下一节点
cur = next;
}
// 旧表置空
_table[i] = nullptr;
}
// 新表映射完成,新旧表交换
_table.swap(newHT);
}
// 插入
// 计算哈希地址
size_t hashi= Hash()(kot(data)) % _table.size();
// new 新节点,头插
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return make_pair(iterator(newnode, this), true);
}
代码
cpp
// HashTable.h
namespace hash_bucket
{
template <class T>
struct HashNode
{
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
T _data;
HashNode<T>* _next;
};
// 声明
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;
typedef HashTable<K, T, KeyOfT, Hash> HashTable;
__HTIterator(Node* node, const HashTable* pht)
:_node(node)
, _pht(pht)
{}
Node* _node;
const HashTable* _pht;
//*it
Ref operator*()
{
return _node->_data;
}
// it->
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
Self& operator++()
{
// 1.中间节点
if (_node->_next)
{
_node = _node->_next;
}
// 2. 最后节点
else
{
// 计算当前桶位置
KeyOfT kot;
size_t hashi = Hash()(kot(_node->_data)) % _pht->_table.size();
++hashi;
// 向后寻找非空桶
for (; hashi < _pht->_table.size(); hashi++)
{
if (_pht->_table[hashi])
break;
}
// 判断是走完没找到,还是找到后break了
if (hashi == _pht->_table.size())
_node = nullptr; // 走完
else
_node = _pht->_table[hashi]; // 找到
}
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 __HTIterator;
typedef HashTable<K, T, KeyOfT, Hash> Self;
typedef HashNode<T> Node;
public:
typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
iterator begin()
{
// 寻找第一个非空桶
for (int i = 0; i < _table.size(); i++)
if (_table[i])
return iterator(_table[i], this);
// 找不到
return end();
}
const_iterator begin() const
{
// 寻找第一个非空桶
for (int i = 0; i < _table.size(); i++)
if (_table[i])
return iterator(_table[i], this);
// 找不到
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
const_iterator end() const
{
return iterator(nullptr, this);
}
HashTable()
{
_table.resize(10, nullptr);
}
~HashTable()
{
// 删除每个桶中的节点
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
// h2(h1)
HashTable(const Self& ht)
{
// 调整大小
_table.resize(ht._table.size());
// 拷贝链表
Node* copyHead = new Node(T()); // 拷贝链表的哨兵位
for (int i = 0; i < ht._table.size(); i++)
{
Node* cur = ht._table[i]; // 遍历桶
Node* copyCur = copyHead; // 遍历拷贝链表
// 遍历,拷贝
while (cur)
{
// 拷贝
copyCur->_next = new Node(cur->_data);
// 向下走
cur = cur->_next;
copyCur = copyCur->_next;
}
// 将拷贝的桶的头节点放到 h2
_table[i] = copyHead->_next;
copyHead->_next = nullptr;
}
_n = ht._n;
delete copyHead;
}
// h2 = h1
Self& operator=(Self t)
{
_table.swap(t._table);
_n = t._n;
return *this;
}
pair<iterator, bool> insert(const T& data)
{
KeyOfT kot;
// 检查 key 是否重复
iterator ret = find(kot(data));
if (ret != end())
return make_pair(ret, false);
// 扩容
if (_n == _table.size())
{
// 创建新表
vector<Node*> newHT(_table.size()*2, nullptr);
// 将旧表节点重新映射
for (int i = 0; i < _table.size(); i++)
{
// 遍历桶
Node* cur = _table[i];
while (cur)
{
// 记录旧表当前桶中下一个节点
Node* next = cur->_next;
// 将当前节点映射到新表
size_t hashi = Hash()(kot(cur->_data)) % newHT.size();
cur->_next = newHT[hashi];
newHT[hashi] = cur;
// 更新cur,旧表当前桶的下一节点
cur = next;
}
// 旧表置空
_table[i] = nullptr;
}
// 新表映射完成,新旧表交换
_table.swap(newHT);
}
// 插入
// 计算哈希地址
size_t hashi= Hash()(kot(data)) % _table.size();
// new 新节点,头插
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return make_pair(iterator(newnode, this), true);
}
iterator find(const K& key)
{
KeyOfT kot;
// 确定桶
size_t hashi = Hash()(key) % _table.size();
Node* cur = _table[hashi];
// 遍历桶
while (cur)
{
if (kot(cur->_data) == key)
return iterator(cur, this);
cur = cur->_next;
}
// 找不到
return end();
}
bool erase(const K& key)
{
// 计算哈希地址
size_t hashi = Hash()(key) % _table.size();
// 遍历桶
KeyOfT kot;
Node* cur = _table[hashi];
Node* prev = nullptr; // 前一个节点
while (cur)
{
if (kot(cur->_data) != key)
{
prev = cur;
cur = cur->_next;
}
else // 找到目标
{
// 情况1,删除的是头节点
if (prev == nullptr)
_table[hashi] = cur->_next;
// 情况2,删除中间节点
else
prev->_next = cur->_next;
delete cur;
return true;
}
}
return false;
}
private:
vector<Node*> _table;
size_t _n = 0;
};
}
unordered_map 的封装
模板参数
- K:key 的类型
- V:value 的类型
- Hash:仿函数对象类型,将 key 转化为整数。哈希函数使用除留余数法,需要将 key 转化为整型数字才能取模
cpp
template <class K, class V, class Hash = HashFunc<K>>
class unordered_map
{};
成员变量
unordered_map 的底层结构就是一个哈希桶
- 给 HashTable 传递 pair<const K, V> 当作 T,const 是指 key 不可修改
- 写一个仿函数 MapKeyOfT,通过仿函数可以取出键值对中的 key
cpp
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::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
默认成员函数
因为 unordered_map 只是套了一层壳的哈希表,所以默认成员函数例如构造、析构等,可以直接使用哈希表的,不用我们手动写
迭代器
可以直接用哈希表的迭代器
cpp
template <class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
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;
};
begin(), end()
直接使用哈希表的 begin() 和 end()
cpp
iterator begin()
{
return _ht.begin();
}
const_iterator begin() const
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator end() const
{
return _ht.end();
}
insert、find、erase
同理,这三个接口也可以直接使用哈希表的
cpp
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _ht.insert(kv);
}
iterator find(const K& key)
{
return _ht.find(key);
}
bool erase(const K& key)
{
return _ht.erase(key);
}
operator[]
- 通过 key 访问 value
- 直接尝试插入,插入键值对 pair<key, V()>,V()是 value 的默认值。V 是 int,默认值为0;V 是 string,默认值为
""
- key不存在,插入成功,返回 pair<插入节点的迭代器, true>
- key已存在,插入失败,返回 pair<已存在节点的迭代器, false>
- 通过迭代器就可以取到 value 并返回
cpp
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
代码
cpp
#pragma once
#include "HashTable.h"
namespace myUM
{
// 转换 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& s)
{
size_t ret = 0;
for (auto& e : s)
{
ret = ret * 131 + e;
}
return ret;
}
};
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;
}
};
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();
}
const_iterator begin() const
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator end() const
{
return _ht.end();
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _ht.insert(kv);
}
iterator find(const K& key)
{
return _ht.find(key);
}
bool erase(const K& key)
{
return _ht.erase(key);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
private:
hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
}
测试
insert、find、erase
cpp
void test_unordered_map1()
{
// insert
unordered_map<string, int> ht;
ht.insert(make_pair("hash", 1));
ht.insert(make_pair("map", 2));
ht.insert(make_pair("table", 3));
// find
auto ret = ht.find("hash");
cout << ret->first << ":" << ret->second << endl;
// erase
ht.erase("hash");
cout << "erase:hash" << endl;
ret = ht.find("hash");
if (ret == ht.end())
cout << "hash 已删除" << endl;
}
operator[]+迭代器+范围for
cpp
void test_unordered_map2()
{
// operator[]
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
unordered_map<string, int> countMap;
for (auto& e : arr)
{
countMap[e]++;
}
// 迭代器
unordered_map<string, int>::iterator it = countMap.begin();
while (it != countMap.end())
{
//it->first += 'x'; // key不能修改
it->second += 1; // value可以修改
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
// 范围 for
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
unordered_set 的封装
unordered_set 除了不支持 operator[] 外,其他都与 unordered_map 类似,这里就不赘述了
代码
cpp
#pragma once
#include "HashTable.h"
namespace myUS
{
// 转换 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& s)
{
size_t ret = 0;
for (auto& e : s)
{
ret = ret * 131 + e;
}
return ret;
}
};
template <class K, class Hash = HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;
typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::const_iterator const_iterator;
iterator begin()
{
return _ht.begin();
}
const_iterator begin() const
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
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:
hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
};
}
测试
insert、find、erase
cpp
void test_unordered_set1()
{
// insert
unordered_set<int> s;
s.insert(31);
s.insert(11);
s.insert(5);
s.insert(15);
s.insert(25);
// find
auto ret = s.find(31);
cout << *ret << endl;
// erase
s.erase(31);
cout << "erase:31" << endl;
ret = s.find(31);
if (ret == s.end())
cout << "31 已删除" << endl;
}
迭代器
cpp
void test_unordered_set2()
{
unordered_set<int> s;
s.insert(31);
s.insert(11);
s.insert(5);
s.insert(15);
s.insert(25);
unordered_set<int>::iterator it = s.begin();
while (it != s.end())
{
//*it = 1;
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}