目录
前言
1.哈希表的结构
1.1.哈希节点
1.2.迭代器的结构
1.2.1.普通迭代器
1.2.2.const迭代器的实现
1.3.哈希表的实现
2.如何封装哈希表实现个性化的容器
2.1.unordered_set的封装
2.2.unordered_map的封装
3.以上内容的代码实现
3.1.HashTable.h
3.2.unordered_set.h
3.3.unordered_map.h
前言
我们在上一篇博客 数据结构与算法:哈希表-CSDN博客 中学习了哈希表这个数据结构,也对哈希桶有了一定的了解,学习这个数据结构的本质是为了对STL容器unordered_set和unordered_map进行深入的学习
我们在C知道中搜索“unordered_set和set 区别”,发现了这两个容器对应着不同的场景,为了更好的了解这个两个容器我们开始学习如何实现“unordered_set和map”把!!!
C++学习进阶:map和set的实现-CSDN博客
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 也可为pairT _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的_tabletemplate<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 也可为pairT _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的_tabletemplate<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;}}