当前位置: 首页 > news >正文

【C++】封装unordered_map和unordered_set(用哈希桶实现)

前言:
       前面我们学习了unordered_map和unordered_set容器,比较了他们和map、set的查找效率,我们发现他们的效率比map、set高,进而我们研究他们的底层是由哈希实现。哈希是一种直接映射的方式,所以查找的效率很快。与学习红黑树和map、set的思路一样,我们现在学完了unordered_map和unordered_set,本章将模拟实现底层结构来封装该容器!

    作者建议在阅读本章前,可以先去看一下前面的红黑树封装map和set——红黑树封装map和set

这两篇文章都重在强调泛型编程的思想,上一篇由于是初认识,作者讲解的会更详细一点~

目录

(一)如何复用一个哈希桶

1、结点的定义:

2、两个容器各自的模板参数类型​编辑

3、改造哈希桶

(二)哈希桶的迭代器的模拟实现

1、begin()和end()的模拟实现

2、operator*和operator->及operator!=和operator==的模拟实现 

3、operator ++的模拟实现

(三)迭代器和改造哈希桶的总代码

(四)封装unordered_map和unordered_set


(一)如何复用一个哈希桶

我们学习过知道,unordered_map和unordered_set容器存放的结点并不一样,为了让它得到复用我们就需要对哈希桶进行改造,将哈希桶改造的更加泛型一点,既符合Key模型,也符合Key_Value模型。

1、结点的定义:

 所以我们这里还是和封装map和set时一样,无论是Key还是Key_Value,都用一个类型T来接收,这里高维度的泛型哈希表中,实现还是用的是Kye_Value模型,K是不能省略的,同样的查找和删除要用,故我们可以引出两个容器各自模板参数类型。


2、两个容器各自的模板参数类型

如何取到想要的数据:

  • 我们给每个容器配一个仿函数
  • 各传不同的仿函数,拿到想要的不同的数据

同时我们再给每个容器配一个哈希函数。

3、改造哈希桶

通过上面1和2,我们可以把各自存放的数据泛化成data:

这样我们哈希桶的模板参数算是完成了

  • 哈希函数我们可以自由选择并传
  • 仿函数在各自容器的封装中实现,用于比较时我们可以取出各自容器想要的数据

我们把上一篇文章封装的哈希桶拿来改造:

//K --> 键值Key,T --> 数据
//unordered_map ->HashTable<K, pair<K, V>, MapKeyOfT> _ht;
//unordered_set ->HashTable<K, K, SetKeyOfT> _ht;
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable
{template<class K, class T, class KeyOfT, class HashFunc>friend class __HTIterator;typedef HashNode<T> Node;
public:typedef __HTIterator<K, T, KeyOfT, HashFunc> iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){return iterator(cur, this);}}return end();}iterator end(){return iterator(nullptr, this);}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}size_t GetNextPrime(size_t prime){const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53,         97,         193,       389,       769,1543,       3079,       6151,      12289,     24593,49157,      98317,      196613,    393241,    786433,1572869,    3145739,    6291469,   12582917,  25165843,50331653,   100663319,  201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};//获取比prime大那一个素数size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){if (primeList[i] > prime)return primeList[i];}return primeList[i];}pair<iterator, bool> Insert(const T& data){HashFunc hf;KeyOfT kot;iterator pos = Find(kot(data));if (pos != end()){return make_pair(pos, false);}//负载因子 == 1 扩容 -- 平均每个桶挂一个结点if (_tables.size() == _n){//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;size_t newSize = GetNextPrime(_tables.size());if (newSize != _tables.size()){vector<Node*> newTable;newTable.resize(newSize, nullptr);//遍历旧表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];//再对每个桶挨个遍历while (cur){Node* next = cur->_next;size_t hashi = hf(kot(cur->_data)) % newSize;//转移到新的表中cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}//将原表置空_tables[i] = nullptr;}newTable.swap(_tables);}}size_t hashi = hf(kot(data));hashi %= _tables.size();//头插到对应的桶即可Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;//有效数据加一_n++;return make_pair(iterator(newnode, this), true);}iterator Find(const K& key){if (_tables.size() == 0){return iterator(nullptr, this);}KeyOfT kot;HashFunc hf;size_t hashi = hf(key);//size_t hashi = HashFunc()(key);hashi %= _tables.size();Node* cur = _tables[hashi];//找到指定的桶之后,顺着单链表挨个找while (cur){if (kot(cur->_data) == key){return iterator(cur, this);}cur = cur->_next;}//没找到返回空return iterator(nullptr, this);}bool Erase(const K& key){if (_tables.size() == 0){return false;}HashFunc hf;KeyOfT kot;size_t hashi = hf(key);hashi %= _tables.size();//单链表删除结点Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){//头删if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->_next;}return false;}
private://指针数组vector<Node*> _tables;size_t _n = 0;
};

主要改造的地方就是上述所注意的地方:

  • 比较时需要调用各自的仿函数
  • 调用外部传的哈希函数

还有对扩容的二次思考:

研究表明:除留余数法,最好模一个素数

  • 通过查STL官方库我们也发现,其提供了一个取素数的函数
  • 所以我们也提供了一个,直接拷贝过来
    • 这样我们在扩容时就可以每次给素数个桶
    • 在扩容时加了一条判断语句是为了防止素数值太大,过分扩容容易直接把空间(堆)干崩了

(二)哈希桶的迭代器的模拟实现

1、begin()和end()的模拟实现

  • 以第一个桶中第一个不为空的结点为整个哈希桶的开始结点
  • 以空结点为哈希桶的结束结点

2、operator*和operator->及operator!=和operator==的模拟实现 

这两组和之前实现的一模一样,大家自行理解。

3、operator ++的模拟实现

注:

  • 这里要在哈希桶的类外面访问其私有成员
  • 我们要搞一个友元类
  • 迭代器类是哈希桶类的朋友
  • 这样就可以访问了

 

思路:

  • 判断一个桶中的数据是否遍历完
  • 如果所在的桶没有遍历完,在该桶中返回下一个结点指针
  • 如果所在的桶遍历完了,进入下一个桶
  • 判断下一个桶是否为空
  • 非空返回桶中第一个节点
  • 空的话就遍历一个桶
  • 后置++和之前一眼老套路,不赘述

注意:

unordered_map和unordered_set是不支持反向迭代器的,从底层结构我们也能很好的理解(单链表找不了前驱)所以不支持实现迭代器的operator- -

最后注意一点,我们需要知道哈希桶大小,所以不仅要传结点地址,还要传一个哈希桶,这样才能知道其大小,除此,由于哈希桶改造在后面,所以我们要在前面声明一下:

(三)迭代器和改造哈希桶的总代码

#include<vector>
#include<string>
#include<iostream>
using namespace std;template<class K>
struct DefaultHash
{size_t operator()(const K& key){return (size_t)key;}
};template<>
struct DefaultHash<string>
{size_t operator()(const string& key){//BKDRsize_t hash = 0;for (auto ch : key){hash = hash * 131 + ch;}return hash;}
};namespace Bucket
{template<class T>struct HashNode{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data), _next(nullptr){}};template<class K, class T, class KeyOfT, class HashFunc>class HashTable;//哈希桶的迭代器template<class K, class T, class KeyOfT, class HashFunc>class __HTIterator{typedef HashNode<T> Node;typedef __HTIterator<K, T, KeyOfT, HashFunc> Self;public:Node* _node;__HTIterator() {};//编译器的原则是向上查找(定义必须在前面,否则必须先声明)HashTable<K, T, KeyOfT, HashFunc>* _pht;__HTIterator(Node* node, HashTable<K, T, KeyOfT, HashFunc>* pht):_node(node), _pht(pht){}Self& operator++(){if (_node->_next){_node = _node->_next;}else//当前桶已经走完了,要走下一个桶{KeyOfT kot;HashFunc hf;size_t hashi = hf(kot(_node->_data)) % _pht->_tables.size();hashi++;//找下一个不为空的桶 -- 访问到了哈希表中私有的成员(友元)for (; hashi < _pht->_tables.size(); hashi++){if (_pht->_tables[hashi]){_node = _pht->_tables[hashi];break;}}//没有找到不为空的桶,用nullptr去做end标识if (hashi == _pht->_tables.size()){_node = nullptr;}}return *this;}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}};//K --> 键值Key,T --> 数据//unordered_map ->HashTable<K, pair<K, V>, MapKeyOfT> _ht;//unordered_set ->HashTable<K, K, SetKeyOfT> _ht;template<class K, class T, class KeyOfT, class HashFunc>class HashTable{template<class K, class T, class KeyOfT, class HashFunc>friend class __HTIterator;typedef HashNode<T> Node;public:typedef __HTIterator<K, T, KeyOfT, HashFunc> iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){return iterator(cur, this);}}return end();}iterator end(){return iterator(nullptr, this);}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}size_t GetNextPrime(size_t prime){const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53,         97,         193,       389,       769,1543,       3079,       6151,      12289,     24593,49157,      98317,      196613,    393241,    786433,1572869,    3145739,    6291469,   12582917,  25165843,50331653,   100663319,  201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};//获取比prime大那一个素数size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){if (primeList[i] > prime)return primeList[i];}return primeList[i];}pair<iterator, bool> Insert(const T& data){HashFunc hf;KeyOfT kot;iterator pos = Find(kot(data));if (pos != end()){return make_pair(pos, false);}//负载因子 == 1 扩容 -- 平均每个桶挂一个结点if (_tables.size() == _n){//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;size_t newSize = GetNextPrime(_tables.size());if (newSize != _tables.size()){vector<Node*> newTable;newTable.resize(newSize, nullptr);//遍历旧表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];//再对每个桶挨个遍历while (cur){Node* next = cur->_next;size_t hashi = hf(kot(cur->_data)) % newSize;//转移到新的表中cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}//将原表置空_tables[i] = nullptr;}newTable.swap(_tables);}}size_t hashi = hf(kot(data));hashi %= _tables.size();//头插到对应的桶即可Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;//有效数据加一_n++;return make_pair(iterator(newnode, this), true);}iterator Find(const K& key){if (_tables.size() == 0){return iterator(nullptr, this);}KeyOfT kot;HashFunc hf;size_t hashi = hf(key);//size_t hashi = HashFunc()(key);hashi %= _tables.size();Node* cur = _tables[hashi];//找到指定的桶之后,顺着单链表挨个找while (cur){if (kot(cur->_data) == key){return iterator(cur, this);}cur = cur->_next;}//没找到返回空return iterator(nullptr, this);}bool Erase(const K& key){if (_tables.size() == 0){return false;}HashFunc hf;KeyOfT kot;size_t hashi = hf(key);hashi %= _tables.size();//单链表删除结点Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){//头删if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->_next;}return false;}private://指针数组vector<Node*> _tables;size_t _n = 0;};
}

(四)封装unordered_map和unordered_set

有了上面的哈希桶的改装,我们这里的对map和set的封装就显得很得心应手了。

unordered_map的封装:

#include "HashTable.h"namespace zc
{template<class K, class V, class HashFunc = DefaultHash<K>>class unordered_map{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename Bucket::HashTable<K, pair<K, V>, MapKeyOfT, HashFunc>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){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:Bucket::HashTable<K, pair<K, V>, MapKeyOfT, HashFunc> _ht;};void test_map(){unordered_map<string, string> dict;dict.insert(make_pair("sort", "排序"));dict.insert(make_pair("left", "左边"));dict.insert(make_pair("left", "下面"));dict["string"];dict["left"] = "上面";dict["string"] = "字符串";unordered_map<string, string>::iterator it = dict.begin();while (it != dict.end()){cout << it->first << " " << it->second << endl;++it;}cout << endl;for (auto e : dict){cout << e.first << " " << e.second << endl;}}}

这里unordered_map中的operator[ ]我们知道其原理之后,模拟实现就非常方便,直接调用插入函数,控制好参数和返回值即可。

对unordered_set的封装:

#include "HashTable.h"#include "HashTable.h"namespace zc
{template<class K, class HashFunc = DefaultHash<K>>class unordered_set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public://2.48typedef typename Bucket::HashTable<K, K, SetKeyOfT, HashFunc>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){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:Bucket::HashTable<K, K, SetKeyOfT, HashFunc> _ht;};struct Date{Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}bool operator==(const Date& d) const{return _year == d._year&& _month == d._month&& _day == d._day;}int _year;int _month;int _day;};struct DateHash{size_t operator()(const Date& d){//return d._year + d._month + d._day;size_t hash = 0;hash += d._year;hash *= 131;hash += d._month;hash *= 1313;hash += d._day;//cout << hash << endl;return hash;}};void test_set(){unordered_set<int> s;//set<int> s;s.insert(2);s.insert(3);s.insert(1);s.insert(2);s.insert(5);s.insert(12);unordered_set<int>::iterator it = s.begin();//auto it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;for (auto e : s){cout << e << " ";}cout << endl;unordered_set<Date, DateHash> sd;sd.insert(Date(2022, 3, 4));sd.insert(Date(2022, 4, 3));}
}

最后大家可以利用代码中给的测试函数进行测试!

感谢你的阅读!

http://www.lryc.cn/news/165418.html

相关文章:

  • 面试问题回忆
  • 更多场景、更多选择,Milvus 新消息队列 NATS 了解一下
  • 如何通过python实现一个web自动化测试框架?
  • Linux —— 信号阻塞
  • 【【萌新编写riscV之计算机体系结构之CPU 总二】】
  • error:03000086:digital envelope routines::initialization error
  • 暴涨130万粉仅用3个月,一招转型成B站热门UP主
  • 【Linus】vim的使用:命令模式、底行模式、插入模式、视图模式、替换模式的常用操作介绍
  • leetcode第362场周赛补题
  • SpringMvc 之crud增删改查应用
  • 【业务功能109】微服务-springcloud-springboot-Skywalking-链路追踪-监控
  • 《向量数据库指南》——AI原生向量数据库Milvus Cloud 2.3架构升级
  • Flutter中实现交互式Webview的方法
  • 【Java Web】用Redis优化登陆模块
  • 华为云云耀云服务器L实例评测|docker私有仓库部署手册
  • JAVA-3DES对称加解密工具(不依赖第三方库)
  • 基于Matlab卡尔曼滤波的IMU和GPS组合导航数据融合(附上源码+数据)
  • net自动排课系统完整源码(适合智慧校园)
  • Matlab匿名函数教程
  • 【Vue】一文让你进入Vue的大门
  • Linux mmap读/写触发共享文件页生命周期
  • linux 用户、组操作
  • MySQL报错this is incompatible withsal mode=only full group by处理办法
  • Mybatis 动态语言 - mybatis-freemarker
  • 软件源码开发,网络中的“摄像头”:运维监控系统
  • ping命令
  • MFC:程序的托盘显示
  • AI绘画:StableDiffusion实操教程-斗破苍穹-云韵-婚服(附高清图下载)
  • JS装饰器的介绍
  • 微信小程序(原生)使用Swiper实现(商品详情)视频和图片轮播(仿京东/淘宝商品详情头部视频+图片轮播)