目录
一、unordered_map
unordered_map其实就是与map相对应的一个容器。学过map就知道,map的底层是一个红黑树,通过中序遍历的方式可以以有序的方式遍历整棵树。而unordered_map,正如它的名字一样,它的数据存储其实是无序的,这也和它底层所使用的哈希结构有关。而在其他功能上,unordered_map和map基本上就是一致的。
1.1、unordered_map的特点
(1)unordered_map是用于存储<key, value>键值对的关联式容器,它允许通过key快速的索引到对应的value。
(2)在内部,unorder_map没有对<key, value>按照任何特定的顺序排序,为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的<key, value>键值对放在相同的桶中。
(3)unordered_map容器的搜索效率比map快,但它在遍历元素自己的范围迭代方面效率就比较低。
(4)它的迭代器只能向前迭代,不支持反向迭代。
1.2、unordered_map和map的区别
从大结构上看,unordered_map和map的模板其实没有太大差距。学习了map和set我们就应该知道,map是通过T来告诉红黑树要构造的树的存储数据类型的,unordered_map也是一样的,但是它的参数中多了Hash和Pred两个参数,这两个参数都传了仿函数,主要和哈希结构有关。这里先不过多讲解
二、unordered_set
unordered_set其实也是一样的,从功能上来看和set并没有什么区别,只是由于地层数据结构的不同,导致unordered_set的数据是无序的,但是查找效率非常高。
2.1、unordered_set的特点
无序性:unordered_set中的元素没有特定的顺序,不会根据插入的顺序或者元素的值进行排序。
唯一性:unordered_set中的元素是唯一的,不允许重复的元素。
快速查找:unordered_set使用哈希表实现,可以在平均常数时间内进行查找操作,即使在大型数据集上也能保持高效。
插入和删除效率高:unordered_set的插入和删除操作的平均时间复杂度为常数时间,即O(1)。
高效的空间利用:unordered_set使用哈希表来存储元素,不会浪费额外的空间。
不支持修改操作:由于unordered_set中的元素是唯一的,不支持直接修改元素的值,需要先删除旧值,再插入新值。
迭代器失效:在进行插入和删除
2.2、unordered_set和set的区别
很明显,unordered_set相较于set,多了Hash和Pred两个参数。这两个参数都是传了仿函数,和unordered_map与map之间的关系都是一样的,这里先不过多讲解。
三、哈系桶的改造
3.1 结构设置
首先,这里实现的哈希桶需要能够同时支持unordered_map和unordered_set的调用。因此,在传入的数据中就需要有一个T,来标识传入的数据类型。同时,还需要有Hash函数和KeyOfT来分别对传入的数据转换为整形和获取传入数据的key值,主要是提供给使用了KV模型的数据。
我们还要知道,哈希桶其实是保存在一个顺序表中的,每个下标对应的位置上都是桶的头节点,每个桶中的数据以单链表的方式链接起来。因此,我们就需要一个vector来存储结构体指针,这个结构体中包含了当前节点存储的数据和下一个节点的位置。当然,还有有一个_n来记录顺序表中数据的个数。
namespace BucketHash//哈希桶
{
//哈希桶内的每个节点/
template<class T>
struct HashNode
{
T _data;//存储数据
HashNode<T>* _next;//指向下一个节点
HashNode(const T& data)
: _data(data)
, _next(nullptr)
{}
};
template<class K, class T, class Hash, class KeyOfT>
class HashBucket
{
template<class K, class T, class Hash, class KeyOfT>
friend struct HashIterator;//友元声明,让迭代器可以访问哈希桶的私有成员
typedef HashNode<T> Node;
public:
typedef HashIterator<K, T, Hash, KeyOfT> iterator;
private:
vector<Node*> _bucket;//存储数据的指针数组
size_t _n;
};
}
在类的模板参数中,Hash为将数据转化为整形的仿函数,KeyOfT是返回键值的仿函数。
注意,上面的代码中有一个迭代器的重命名和迭代器结构体的友元声明。重命名是为了方便后续的使用。友元声明则和迭代器的实现有关,这里先不过多讲解。
3.2 构造函数和析构函数
注意,这里没有实现拷贝构造。并不是不需要实现,实际上,虽然哈希桶内的成员是自定义类型的,会去调自己的拷贝构造,但是这里只会进行浅拷贝,不满足需要。如果有需要,可以自己实现拷贝构造,实现起来也很简单。
HashBucket()
:_n(0)
{
_bucket.resize(10);//构建时默认开10个空间
}
~HashBucket()//析构函数
{
for (auto& cur : _bucket)
{
while (cur)
{
Node* prev = cur;
cur = cur->_next;
delete prev;
prev = nullptr;
}
}
}
3.3 数据插入
insert()函数返回的是pair<iterator, bool>,这是为了后续实现 [ ] 重载做准备。
在插入时,首先要先查看哈希桶中是否存在相同键值,存在就直接返回当前位置。第二步就是要查看哈希桶中的元素个数与哈希桶的容量之间的负载因子,如果等于1,就需要进行扩容。第三步则是开始插入节点。先找到映射位置,然后新建一个节点连接到对应的数组下标的空间中即可
pair<iterator, bool> insert(const T& data)//插入数据
{
KeyOfT kt;
iterator it = find(kt(data));
if (it != end())//不允许数据重复,找到相同的返回
return make_pair(it, false);
if (_bucket.size() == _n)//负载因子设置为1,超过就扩容
{
vector<Node*> newbucket;//这种方式就无需再开空间拷贝节点
newbucket.resize(NextPrime(_bucket.size()));//开一个素数大