目录
前言
我们在上一篇博客 数据结构与算法:哈希表-CSDN博客 中学习了哈希表这个数据结构,也对哈希桶有了一定的了解,学习这个数据结构的本质是为了对STL容器unordered_set和unordered_map进行深入的学习
我们在C知道中搜索“unordered_set和set 区别”,发现了这两个容器对应着不同的场景,为了更好的了解这个两个容器我们开始学习如何实现“unordered_set和map”把!!!
unordered_set和set是C++标准库中的两个容器,它们都用于存储一组唯一的元素。它们的主要区别在于底层实现和性能特点。
底层实现:
- set是基于红黑树实现的有序容器,它可以保持元素的有序性。
- unordered_set是基于哈希表实现的无序容器,它不会保持元素的有序性。
查找效率:
- set的查找效率较高,时间复杂度为O(log n),因为它使用了红黑树作为底层数据结构。
- unordered_set的查找效率更高,平均情况下时间复杂度为O(1),最坏情况下为O(n),因为它使用了哈希表作为底层数据结构。
插入和删除操作:
- set的插入和删除操作相对较慢,时间复杂度为O(log n),因为需要维护红黑树的平衡性。
- unordered_set的插入和删除操作相对较快,平均情况下时间复杂度为O(1),最坏情况下为O(n),因为需要处理哈希冲突。
元素顺序:
- set中的元素按照键值自动排序,因此可以通过迭代器按照顺序访问元素。
- unordered_set中的元素没有特定的顺序,因此无法通过迭代器按照顺序访问元素。
总结一下: set适用于需要保持元素有序性的场景,而unordered_set适用于对查找操作有较高要求的场景。选择哪个容器取决于具体的需求和性能要求
1.哈希表的结构
古语云::合抱之木,生于毫末,九层之台,起于累土。对于unordered_set和map的初步模拟是一个很复杂的封装过程,就像是摆在你面前的高楼一般,我们知道很宏伟,但是更加知道这座大厦是有无数个小砖块堆砌而成的,我们实现这个STL容器也是如此,我们需要一步一步,先了解框架,然后一撮一撮土合为一块砖,然后再不断地,最终建成九层之台......
如图:实现这个哈希表我们需要
- 定义哈希节点,并且需要实现桶内的节点“增删查”三个函数
- 实现一个迭代器,实现const对象和普通对象的调用
1.1.哈希节点
对于set和map的主要区别是:set传入的是key类型,map传入的是pair类型,所以第一步我们节点存放的数据就需要是一个多参数类型,因而需要引入模版参数。另外我们在哈希桶部分知道,桶的本质是一个链表所以这里我们也需要一个指针变量。这样子一个哈希节点就被抽象出来了!!
template<class T>
struct HashNode
{
HashNode<T>* _next;
// T可为key 也可为pair
T _data;
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
};
因为增和查需要实现迭代器后才能实现,但是具体的内容我们已经在讲述哈希桶的那篇博客中进行了讲解,这里不在赘述。
1.2.迭代器的结构
- 完成一个普通迭代器,实现迭代器的基本功能,操作符重载,还有用什么结构实现
- 通过模版参数的设置,实现const迭代器
迭代器的部分我们由浅到深,先实现一个能用的普通迭代器,接着再实现const迭代器
1.2.1.普通迭代器
实现一个迭代器,我们首先要知道什么是迭代器,迭代器是干什么的?
首先,迭代器是一个类似于指针的结构,对于每一个哈希节点都会对应这维护一个迭代器节点,迭代器是用来遍历节点,并且访问节点的一个结构。
那么我们如何实现这个迭代器的结构呢?我们在上一篇博客中清楚地知道,遍历哈希表我们需要知道哈希表的size()和它的哈希桶位置Hash_i,所以我们需要传入哈希表来获得size和维护一个Hash_i来定位当前迭代器在哪一个哈希桶中。
template<class K, class T, class KeyOfT, class Hash>
struct _HTIterator
{
typedef HashNode<T> HashNode;
typedef _HTIterator<K, T,KeyOfT, Hash> Self;
typedef HashTable<K, T, KeyOfT, Hash> HashTable;
// 结构
HashNode* _node;
size_t _Hash_i;
const HashTable* _pht;
// 这里
_HTIterator(HashNode* node, HashTable* pht, size_t Hash_i)
:_node(node)
, _pht(pht)
, _Hash_i(Hash_i)
{ }
Self& operator++()
{
if (_node->_next != nullptr)
{
_node = _node->_next;
}
else
{
// 如果为空需要往下一个哈希桶寻找
++_Hash_i;
// 不断的找非空桶
while (_Hash_i < _pht->_table.size())
{
if (_pht->_table[_Hash_i] != nullptr)
{
_node = _pht->_table[_Hash_i];
break;
}
_Hash_i++;
}
// 都是空桶,当前节点即为最后一个桶的最后一个节点
if (_Hash_i == _pht->_table.size())
{
_node = nullptr;
}
}
return *this;
}
T& operator*() { return _node->_data; }
bool operator!=(const Self& s) { return _node != s._node; }
};
代码讲解:
- 首先我们模版参数的设置,设置class KeyOfT和class Hash,两个模版参数均是为了实现仿函数内容,因为对于set和map,我们不知道传入哈希表的参数是key还是pair,对于set我们需要控制传入的参数仅仅为key,map为pair,于是乎我们可以在外部的set.h和map.h中各自实现一个仿函数来进行“各回各家,各找各妈”。Hash我们也是为了映射不同的变量类型,string和int这样子
- 对于迭代器++操作就是向后遍历,我们知道哈希桶的结构是存储链表头结点指针数组+哈希节点链表两个一体的,当我们向后遍历时会出现两种情况:1.当前所在节点的下一个节点不为空,也就是仍在原链表向下找。2.当前节点的下一个节点为空,当前节点为某个桶的尾节点,这时候我们需要向下一个桶遍历,走到下一个桶头节点处。这时如果我们更新后的当前桶下标小于哈希表的长度,如果这个桶为空就向后走找不为空的桶,如果桶不为空就维护数据然后break出去。
另外:这里我们访问了哈希表内的private对象_table,这里后面会在哈希表这个类中设置友元函数
到了这里一个普通的迭代器就实现好了,其实也不“难”(真的不难)。
1.2.2.const迭代器的实现
我们还是从为什么需要const和怎么实现const迭代器来进行学习,对于迭代器来说我们可以修改迭代器对应哈希节点的数据,而在使用者的角度,有时我们只是想要遍历这个哈希桶,而不希望修改数据,所以开发者往往就会开发一份const迭代器来满足只写的需求,普通迭代器实现可读可写。
在哈希表结构中,const迭代器的具体体现,需要实现数据的指针、和引用不能修改
所以我们需要增加模版参数
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
增加对数据的指针和引用,接下来我们就是实现一个const迭代器的构造函数
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct _HTIterator
{
typedef HashNode<T> HashNode;
typedef _HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
typedef HashTable<K, T, KeyOfT, Hash> HashTable;
// 结构
HashNode* _node;
size_t _Hash_i;
const HashTable* _pht;
// 普通迭代器的构造
_HTIterator(HashNode* node, HashTable* pht, size_t Hash_i)
:_node(node)
, _pht(pht)
, _Hash_i(Hash_i)
{ }
// const迭代器的构造
_HTIterator(HashNode* node, const HashTable* pht, size_t Hash_i)
:_node(node)
, _pht(pht)
, _Hash_i(Hash_i)
{ }
// 实现Insert的pair类型转换的拷贝构造
_HTIterator(const _HTIterator<K, T, T&, T*, KeyOfT, Hash>& s)
:_node(s._node)
, _pht(s._pht)
, _Hash_i(s._Hash_i)
{ }
Self& operator++()
{
if (_node->_next != nullptr)
{
_node = _node->_next;
}
else
{
// 如果为空需要往下一个哈希桶寻找
++_Hash_i;
while (_Hash_i < _pht->_table.size())
{
if (_pht->_table[_Hash_i] != nullptr)
{
_node = _pht->_table[_Hash_i];
break;
}
_Hash_i++;
}
if (_Hash_i == _pht->_table.size())
{
_node = nullptr;
}
}
return *this;
}
Ptr operator->() { return &_node->_data; }
Ref operator*() { return _node->_data; }
bool operator!=(const Self& s) { return _node != s._node; }
};
实现const迭代器不是主要,主要的是适配unordered_set和unordered_map这两个容器。
1.3.哈希表的实现
基本的架构“增删查”、构造和析构函数我们在上一个博客也是讲过了的,不再赘述,而迭代器我们维护时,通过它的默认构造来实现。
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
typedef HashNode<T> HashNode;
// 定义友元函数 提供HashTable的_table
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct _HTIterator;
public:
// 普通迭代器和const迭代器
typedef _HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
typedef _HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
// 定义默认空间
HashTable() { _table.resize(10); }
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
HashNode* current = _table[i];
HashNode* next = nullptr;
while (current != nullptr)
{
next = current->_next;
delete current;
current = next;
}
_table[i] = nullptr;
}
}
// 迭代器
iterator begin()
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i] != nullptr)
{
return iterator(_table[i], this, i);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this, -1);
}
const_iterator begin() const
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i] != nullptr)
{
return const_iterator(_table[i], this, i);
}
}
return end();
}
const_iterator end() const
{
return const_iterator(nullptr, this, -1);
}
// 增
pair<iterator, bool> Insert(const T& data)
{
Hash hs_func;
KeyOfT kot_func;
iterator it = Find(kot_func(data));
if (it != end())
return make_pair(it, false);
// 当负载因子为1时才进行扩容
if (_num == _table.size())
{
vector<HashNode*> newT;
newT.resize(_table.size() * 2);
for (size_t i = 0; i < _table.size(); i++)
{
HashNode* current = _table[i];
HashNode* next = nullptr;
// 将旧表的内容插入进新表
while (current != nullptr)
{
next = current->_next;
// 头插逻辑
size_t Hash_i = hs_func(kot_func(current->_data)) % newT.size();
current->_next = newT[Hash_i];
current = next;
}
_table[i] = nullptr;
}
_table.swap(newT);
}
size_t Hash_i = hs_func(kot_func(data)) % _table.size();
HashNode* newNode = new HashNode(data);
// 头插(针对哈希桶)
// 新节点的下一个节点就是原本的头节点
newNode->_next = _table[Hash_i];
_table[Hash_i] = newNode;
_num++;
return make_pair(iterator(newNode, this, Hash_i), true);
}
// 删
bool Erase(const K& key)
{
Hash hs_func;
KeyOfT kot_func;
size_t Hash_i = hs_func(key) % _table.size();
HashNode* current = _table[Hash_i];
HashNode* prev = nullptr;
while (current != nullptr)
{
if (current->_kv.first == key)
{
if (prev == nullptr)
{
_table[Hash_i] = current->_next;
}
else
{
prev->_next = current->_next;
}
delete current;
return true;
}
prev = current;
current = current->_next;
}
return false;
}
// 查
iterator Find(const K& key)
{
Hash hs_func;
KeyOfT kot_func;
size_t Hash_i = hs_func(key) % _table.size();
HashNode* current = _table[Hash_i];
while (current != nullptr)
{
if (kot_func(current->_data) == key)
return iterator(current, this, Hash_i);
current = current->_next;
}
return end();
}
private:
// 指针数组存储哈希桶的首元素地址
vector<HashNode*> _table;
// 存入的key的数目
size_t _num = 0;
};
需要注意的是:我们会发现Hash_i这个值的计算有时候不同,这个是根据实际的场景,来进行判断的
到了这里哈希表的结构我们就初步完成了,代码会在后续模块统一给出!!!
2.如何封装哈希表实现个性化的容器
我们在前言中讲了我们学习哈希表就是为了实现并适配unordered_set和unordered_map这两个容器,也就是通过一份相同的哈希表代码来实现不同逻辑的unordered_set和unordered_map。废话不多说,我们开始吧
2.1.unordered_set的封装
如图unordered_set的封装所示,接下来我们解析一下原理:
- unordered_set的底层是哈希表,所以需要维护一个哈希表对象,这跟我们之前实现set封装需要维护一个红黑树对象一致
- 对于unordered_set来说,它维护的数据为key类型,并且这个key不可被修改,所以我们对于迭代器的使用默认就为const迭代器
- 对于大部分的函数我们只需要按照unordered_set的需求进行复用哈希表的原生提供的函数即可
- 值得注意的是当我们需要使用pair<iterator, bool>类型时,我们需要在迭代器中实现一个拷贝构造函数,让普通迭代器转化为const迭代器,实现权限的放大
- 对于仿函数struct SetKeyOfT我们让他只接受key类型的数据,也就是当为key时才进入这个仿函数,并返回内容
2.2.unordered_map的封装
这一部分我们讲一下这两个容器的区别与实现:
- 首先unordered_map这个容器的key也是不允许修改的,value允许修改,当处于普通迭代器时,我们通过将pair类型的first设置为const类型,并且将对应的迭代器的pair参数也设置为const类型
- 在这里我们可以设置const迭代器来实现只读操作,防止value值被误修改。
- 对于哈希表的维护我们的参数为pair类型和set进行区别
- 最重要的一点我们的struct MapKeyOfT面对的是pair类型,这里也是封装精髓的体现,通过泛型编程来实现同一个底层结构实现不同原理的容器
- 这里还涉及[ ]重载具体看测试函数
可能看到这里大家会疑惑为什么直接给我看了结构,并没有教学如何实现?这个地方呢,因为主要是通过研究这两个容器的结构进行学习,讲起来需要长篇大论,我们这样子知道整体架构,理解为什么和怎么做即可。
3.以上内容的代码实现
3.1.HashTable.h
#pragma once
#include<iostream>
#include<vector>
#include<string>
using namespace std;
// 整体实现的哈希函数
struct HashFunc
{
size_t operator()(const size_t& key)
{
return (size_t)key;
}
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
hash *= 31;
hash += e;
}
return hash;
}
// 也可以通过函数重载来实现,不过需要设置多种类型
};
namespace hash_bucket
{
template<class T>
struct HashNode
{
HashNode<T>* _next;
// T可为key 也可为pair
T _data;
// STATUS _status = EMPTY;
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
};
// 前置声明一下哈希表
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> HashNode;
typedef _HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
typedef HashTable<K, T, KeyOfT, Hash> HashTable;
// 结构
HashNode* _node;
size_t _Hash_i;
const HashTable* _pht;
// const HashTable* _pht_const;
// 这里
_HTIterator(HashNode* node, HashTable* pht, size_t Hash_i)
:_node(node)
,_pht(pht)
,_Hash_i(Hash_i)
{ }
_HTIterator(HashNode* node, const HashTable* pht, size_t Hash_i)
:_node(node)
, _pht(pht)
, _Hash_i(Hash_i)
{ }
_HTIterator(const _HTIterator<K, T, T&, T*, KeyOfT, Hash>& s)
:_node(s._node)
, _pht(s._pht)
, _Hash_i(s._Hash_i)
{ }
Self& operator++()
{
if (_node->_next != nullptr)
{
_node = _node->_next;
}
else
{
++_Hash_i;
while (_Hash_i < _pht->_table.size())
{
if (_pht->_table[_Hash_i] != nullptr)
{
_node = _pht->_table[_Hash_i];
break;
}
_Hash_i++;
}
if (_Hash_i == _pht->_table.size())
{
_node = nullptr;
}
}
return *this;
}
Ptr operator->() { return &_node->_data; }
Ref operator*() { return _node->_data; }
bool operator!=(const Self& s) { return _node != s._node; }
};
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
typedef HashNode<T> HashNode;
// 定义友元函数 提供HashTable的_table
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;
// 定义默认空间
HashTable() { _table.resize(10); }
HashTable(const HashTable& ht)
{
_table = new vector<HashNode*>(ht._table);
_num = ht._num;
}
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
HashNode* current = _table[i];
HashNode* next = nullptr;
while (current != nullptr)
{
next = current->_next;
delete current;
current = next;
}
_table[i] = nullptr;
}
}
// 迭代器
iterator begin()
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i] != nullptr)
{
return iterator(_table[i], this, i);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this, -1);
}
const_iterator begin() const
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i] != nullptr)
{
return const_iterator(_table[i], this, i);
}
}
return end();
}
const_iterator end() const
{
return const_iterator(nullptr, this, -1);
}
// 增
pair<iterator, bool> Insert(const T& data)
{
Hash hs_func;
KeyOfT kot_func;
iterator it = Find(kot_func(data));
if (it != end())
return make_pair(it, false);
// 当负载因子为1时才进行扩容
if (_num == _table.size())
{
vector<HashNode*> newT;
newT.resize(_table.size() * 2);
for (size_t i = 0; i < _table.size(); i++)
{
HashNode* current = _table[i];
HashNode* next = nullptr;
// 将旧表的内容插入进新表
while (current != nullptr)
{
next = current->_next;
// 头插逻辑
size_t Hash_i = hs_func(kot_func(current->_data)) % newT.size();
current->_next = newT[Hash_i];
current = next;
}
_table[i] = nullptr;
}
_table.swap(newT);
}
size_t Hash_i = hs_func(kot_func(data)) % _table.size();
HashNode* newNode = new HashNode(data);
// 头插(针对哈希桶)
// 新节点的下一个节点就是原本的头节点
newNode->_next = _table[Hash_i];
_table[Hash_i] = newNode;
_num++;
return make_pair(iterator(newNode, this, Hash_i), true);
}
// 删
bool Erase(const K& key)
{
Hash hs_func;
KeyOfT kot_func;
size_t Hash_i = hs_func(key) % _table.size();
HashNode* current = _table[Hash_i];
HashNode* prev = nullptr;
while (current != nullptr)
{
if (current->_kv.first == key)
{
if (prev == nullptr)
{
_table[Hash_i] = current->_next;
}
else
{
prev->_next = current->_next;
}
delete current;
return true;
}
prev = current;
current = current->_next;
}
return false;
}
// 查
iterator Find(const K& key)
{
Hash hs_func;
KeyOfT kot_func;
size_t Hash_i = hs_func(key) % _table.size();
HashNode* current = _table[Hash_i];
while (current != nullptr)
{
if (kot_func(current->_data) == key)
return iterator(current, this, Hash_i);
current = current->_next;
}
return end();
}
// 对于哈希映射值的分析
// 因为插入值时,需要知道 位置 和 类型,1.string还是int进行映射找到位置 2.为key还是kv结构,所以需要两个仿函数
// size_t Hash_i = hs_func(kot_func(key)) % _table.size();
// 当查找和删除时,我们只需要知道 位置即可,类型我们通过后续判断
// size_t Hash_i = hs_func(key) % _table.size();
private:
// 指针数组存储哈希桶的首元素地址
vector<HashNode*> _table;
// 节点数
size_t _num = 0;
};
}
3.2.unordered_set.h
#pragma once
#include"HashTable.h"
namespace zhong
{
template<class K, class Hash = HashFunc>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key) { return key; }
};
public:
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, HashFunc>::const_iterator iterator;
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, HashFunc>::const_iterator const_iterator;
pair<iterator, bool> insert(const K& key) { return _pht.Insert(key); }
bool erase(const K& key) { return _pht.Erase(key); }
iterator find(const K& key) { return _pht.Find(key); }
// iterator begin() { return _pht.begin(); }
// iterator end() { return _pht.end(); }
const_iterator begin() const { return _pht.begin(); }
const_iterator end() const { return _pht.end(); }
private:
hash_bucket::HashTable<K, K, SetKeyOfT, HashFunc> _pht;
};
// 测试函数
void test1()
{
unordered_set<int> us;
us.insert(1);
us.insert(3);
us.insert(5);
unordered_set<int>::iterator it = us.begin();
while (it != us.end())
{
// *it = 5;
cout << *it << endl;
++it;
}
cout << endl;
}
}
3.3.unordered_map.h
#pragma once
#include"HashTable.h"
namespace zhong
{
template<class K, class V, class Hash = HashFunc>
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, HashFunc>::iterator iterator;
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, HashFunc>::const_iterator const_iterator;
V& operator[](const K& key)
{
pair<iterator, bool> ret = _pht.Insert(make_pair(key, V()));
return ret.first->second;
}
const V& operator[](const K& key) const
{
pair<iterator, bool> ret = _pht.Insert(make_pair(key, V()));
return ret.first->second;
}
// 迭代器
iterator begin() { return _pht.begin(); }
iterator end() { return _pht.end(); }
const_iterator begin() const { return _pht.begin(); }
const_iterator end() const { return _pht.end(); }
// 增删查
pair<iterator, bool> insert(const pair<K, V>& kv) { return _pht.Insert(kv); }
bool erase(const K& key) { return _pht.Erase(key); }
iterator find(const K& key) { return _pht.Find(key); }
private:
hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, HashFunc> _pht;
};
// 测试函数
void test2()
{
unordered_map<string, string> mss;
mss.insert(make_pair("qq", "11"));
mss.insert(make_pair("qa", "22"));
mss.insert(make_pair("qw", "33"));
unordered_map<string, string>::iterator its = mss.begin();
while (its != mss.end())
{
cout << (*its).first << " " << (*its).second << endl;
++its;
}
cout << endl;
unordered_map<int, int> ms;
ms.insert(make_pair(1, 1));
ms.insert(make_pair(2, 2));
ms.insert(make_pair(3, 3));
unordered_map<int, int>::const_iterator it = ms.begin();
while (it != ms.end())
{
// (*it).first = 1;
// (*it).second = 0;
cout << (*it).second << endl;
++it;
}
cout << endl;
}
void test3()
{
string arr[] = { "apple", "xiaomi", "xiaomi", "huawei", "huawei" ,"huawei" };
unordered_map<string, int> count_map;
for (auto& e : arr)
{
count_map[e]++;
}
for (auto& kv : count_map)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
}