哈希封装unordered_map和unordered_set的模拟实现

文章目录

  • (一)认识unordered_map和unordered_set
  • (二)模拟实现unordered_map和unordered_set
    • 2.1 实现出复用哈希表的框架
    • 2.2 迭代器iterator的实现思路分析
    • 2.3 unordered_map支持[]
  • (三)结束语

(一)认识unordered_map和unordered_set

unordered_map和unordered_set这两个容器是C++11之后才更新的。unordered_map的底层复用了哈希表来实现key/value的结构,而unordered_set的底层也是复用了哈希表,但实现的是key的结构。这两个容器的使用其实跟map和set的使用是一致的,只是map和set的底层复用的是红黑树,unordered_set和unordered_map这两个容器的使用如下代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>using namespace std;int main()
{//unordered_mapunordered_map<int, int> myunmap = { {4,4},{2,2},{8,8},{11,11},{2,2} };for (auto& e : myunmap){cout << e.first  << ":" << e.second << endl;}//unordered_setunordered_set<int> myunset = { 3,4,2,1,7,0,5,6,9,8,3,3 };for (auto& e : myunset){cout << e << " ";}cout << endl << endl;//mapmap<int, int> mymap = { {4,4},{2,2},{8,8},{11,11},{2,2} };for (auto& e : mymap){cout << e.first << ":" << e.second << endl;}//setset<int> myset = { 3,4,2,1,7,0,5,6,9,8,3,3 };for (auto& e : myset){cout << e << " ";}cout << endl;return 0;
}

打印结果:
在这里插入图片描述
从代码和打印结果来看,unordered_map和unordered_set这两个容器跟map、set一样都支持initializer_list初始化,但是从打印结果来看,unordered_map和unordered_set支持去重+不排序,而map和set支持去重+排序,这就是它们底层复用的封装不同所导致的,map和set的底层是红黑树,迭代器在遍历时走中序遍历,所以打印出来的数据是有序的,而unordered_map和unordered_set底层是哈希表,哈希表是通过哈希桶来将值一个个存储进去的,迭代器遍历时是遍历一个个哈希桶,所以底层复用的不同,实现出来的效果也略有不同,但是这几个容器的使用方式完全是类似的。

(二)模拟实现unordered_map和unordered_set

2.1 实现出复用哈希表的框架

  • 首先我们得知道,哈希表既能被unordered_map复用又能被unordered_set复用,而unordered_map的数据结构是pair<key,value>,而unordered_set的数据结构就是一个key,由于这两个容器的存储的数据结构不同,所以哈希表在实现时应该使用泛型参数T来接收unordered_map和unordered_set传过来的数据结构的类型,才能实例化出不同的哈希表,提供给这两个容器使用
  • 哈希表在实现时还需要使用一个K参数来接收unordered_map和unordered_set的关键字,因为这两个容器的find/erase等一些接口都是通过关键字来实现的。unordered_set传给泛型T的就是关键字,但还是需要再传一遍,就是为了要与unordered_map保持兼容,unordered_map传给泛型T的是一堆pair值,实现find/erase等接口时就不方便,所以为了保持兼容,哈希表在实现时还需要使用一个K参数来接收unordered_map和unordered_set的关键字
  • 因为哈希表实现了泛型T,不知道传过来的数据是k,还是pair<k,v>,那么insert内部进行插入时要用k对象转换成整型取模和比较相等,因为pair的value不参与计算取模,且默认支持的是key和value一起比较相等,所以在任何时候只需要比较k对象,那么我们就需要在unordered_map和unordered_set层分别实现一个MapOfKey和SetOfKey的仿函数来传给哈希表中的KeyOfT,然后在哈希表中通过KeyOfT仿函数取出T类型对象中的k对象,再转换成整型取模和k比较相等

代码实现如下:
步骤1:实现哈希表

#pragma once
#include <iostream>
#include <vector>
using namespace std;//将关键字转成可以取模,如string本身取不了模,利用ASCII码值即可,用仿函数实现
template<class K>
class HashFunc
{
public://全部转成无符号的整型,这样才能取模const size_t operator()(const K& key){return (size_t)key;}
};
//使用库里面的unordered_map和unordered_set时,我们不用给string的取模进行仿函数的实现,因为库里面为string的仿函数进行了特化
template<>
class HashFunc<string>
{
public:size_t operator()(const string& str){size_t ret = 0;for (auto& s : str){ret += s;ret *= 131;}return ret;}
};namespace li //模拟实现时防止与库里面的冲突,用了命名空间
{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 Hash>class HashTable{typedef HashNode<T> Node;public:inline unsigned long _stl_next_prime(unsigned long n){static const int _stl_num_primes = 28;static const unsigned long _stl_primes_list[_stl_num_primes] ={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};const unsigned long* first = _stl_primes_list;const unsigned long* last = _stl_primes_list + _stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);//lower_bound()在迭代器的范围内取>=n的最小值return pos == last ? *(pos - 1) : *(pos);}HashTable(){//提前开好第一个质数大小的空间_tables.resize(_stl_next_prime(1),nullptr);}~HashTable(){for (int i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}bool insert(const T& data){Hash hs;KeyOfT kot;if(find(kot(data)))return false;//负载因子为1就扩容if (_n == _tables.size()){unsigned long newsize = _stl_next_prime(_tables.size() + 1);vector<Node*> newtables(newsize, nullptr);for (int i = 0; i < _tables.size(); i++){//遍历旧表,将数据挪到新表Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = hs(kot(cur->_data)) % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}//算出key在哈希表中映射的存储空间size_t hashi = hs(kot(data)) % _tables.size();//头插Node* cur = new Node(data);cur->_next = _tables[hashi];_tables[hashi] = cur;++_n;return true;}bool find(const K& key){Hash hs;KeyOfT kot;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (hs(kot(cur->_data)) == hs(key)){return true;}cur = cur->_next;}return false;}bool erase(const K& key){Hash hs;KeyOfT kot;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (hs(kot(cur->_data)) == hs(key)){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_n;return true;}prev = cur;cur = cur->_next;}return false;}private:vector<Node*> _tables;size_t _n = 0;};
}

步骤2:封装unordered_map和unordered_set的框架,解决获取关键字(KeyOfT)的问题

//unordered_map.h
#pragma once
#include "HashTable.h"
namespace Unordered_map
{template<class K, class V,class Hash = HashFunc<K>>class unordered_map{struct MapOfKey{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:bool insert(const pair<K, V>& kv){return _hash.insert(kv);}bool find(const K& key){return _hash.find(key);}bool erase(const K& key){return _hash.erase(key);}private:li::HashTable<K, pair<const K, V>,MapOfKey,Hash> _hash;};
}
//unordered_set.h
#pragma once
#include "HashTable.h"
namespace Unordered_set
{template<class K,class Hash = HashFunc<K>>class unordered_set{struct SetOfKey{const K& operator()(const K& key){return key;}};public:bool insert(const K& key){return _hash.insert(key);}bool find(const K& key){return _hash.find(key);}bool erase(const K& key){return _hash.erase(key);}private:li::HashTable<K,const K,SetOfKey,Hash> _hash;};
}

2.2 迭代器iterator的实现思路分析

  • iterator的实现就是用一个类型去封装结点的哈希结点的指针,再通过重载运算符实现迭代器像指针一样访问的行为,这里哈希表的迭代器是一个单项迭代器
  • 重载运算符中,operator++如何实现呢,iterator中有一个结点的指针,该指针指向哈希桶里的一个结点,若桶下面还有结点,则将迭代器指向下一个结点即可。若桶的下面没有结点,则需要找到哈希桶,为了能找到下一个桶,我们需要在迭代器中增加一个哈希表指针,让该指针指向哈希表,这样的话若当前桶走完了,要找到下一个桶就方便了,直接用key值计算出当前桶的位置,依次往后找不为空的桶即可,若往后找到的桶都为空,遍历到哈希尾,就可返回一个nullptr

代码如下:
步骤1:实现哈希表的迭代器,重载运算符

template<class K, class T, class KeyOfT, class Hash>
class HashTable;template<class K, class T, class KeyOfT, class Hash>
struct HashTableIterator
{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> ht;typedef HashTableIterator<K, T, KeyOfT, Hash> Self;Node* _node; //结点的指针const ht* _ht; //哈希表指针HashTableIterator(Node* node,const ht* ht):_node(node),_ht(ht){ }T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}Self operator++(){if (_node->_next){//说明桶的下面还有结点_node = _node->_next;}else{KeyOfT kot;Hash hs;size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();hashi++;//找下一个不为空的桶while (hashi < _ht->_tables.size()){if (_ht->_tables[hashi]){_node = _ht->_tables[hashi];break;}else{hashi++;}}if (hashi == _ht->_tables.size()){//没有不为空的桶_node = nullptr;}}return *this;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}
};

步骤2:begin()返回第一个桶中第一个结点指针构造的迭代器,end()返回的迭代器可以用空来表示

template<class K,class T,class KeyOfT,class Hash>
class HashTable
{typedef HashNode<T> Node;template<class K, class T, class KeyOfT, class Hash>friend struct HashTableIterator; //友元声明,方便迭代器访问哈希表中的私有成员public:typedef HashTableIterator<K, T, KeyOfT, Hash> Iterator;Iterator Begin(){for (int i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){return Iterator(cur, this);}}return End();}Iterator End(){return Iterator(nullptr, this);}
};

上面两个步骤实现的迭代器是可以修改的,我们知道unordered_map和unordered_set的关键字是不可以被修改的,所以我们需要把unordered_set的第二个模板参数改成const K,unordered_map的第二个模板参数改成pair<const K,V>,代码如下:

步骤3:封装哈希迭代器实现unordered_map和unordered_set的迭代器

//unordered_map.h
template<class K, class V,class Hash = HashFunc<K>>
class unordered_map
{
public:typedef typename li::HashTable<K, pair<const K, V>, MapOfKey, Hash>::Iterator iterator;iterator begin(){return _hash.Begin();}iterator end(){return _hash.End();}
private:li::HashTable<K, pair<const K, V>,MapOfKey,Hash> _hash;
};
//unordered_set.h
template<class K,class Hash = HashFunc<K>>
class unordered_set
{
public:typedef typename li::HashTable<K,const K, SetOfKey, Hash>::Iterator iterator;iterator begin(){return _hash.Begin();}iterator end(){return _hash.End();}
private:li::HashTable<K,const K,SetOfKey,Hash> _hash;
};
li::HashTable<K, pair<const K, V>,MapOfKey,Hash> _hash; //unordered_map
li::HashTable<K,const K,SetOfKey,Hash> _hash; // unordered_set

步骤4:实现const迭代器
增加迭代器的模板参数

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 HashTableIterator
{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> ht;typedef HashTableIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;Node* _node; //结点的指针const ht* _ht; //哈希表指针HashTableIterator(Node* node,const ht* ht):_node(node),_ht(ht){ }Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}
};

步骤5:实现const Begin()和const End()

template<class K,class T,class KeyOfT,class Hash>
class HashTable
{typedef HashNode<T> Node;template<class K, class T, class KeyOfT, class Hash>friend struct HashTableIterator; //友元声明,方便迭代器访问哈希表中的私有成员public:typedef HashTableIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;typedef HashTableIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;ConstIterator Begin() const{for (int i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){return ConstIterator(cur, this);}}return End();}ConstIterator End() const{return ConstIterator(nullptr, this);}
};

步骤6:封装哈希const_iterator实现unordered_map和unordered_set的const_iterator

//unordered_map.h
template<class K, class V,class Hash = HashFunc<K>>
class unordered_map
{
public:typedef typename li::HashTable<K, pair<const K, V>, MapOfKey, Hash>::ConstIterator const_iterator;const_iterator begin() const{return _hash.Begin();}const_iterator end() const{return _hash.End();}
private:li::HashTable<K, pair<const K, V>,MapOfKey,Hash> _hash;
};
//unordered_set.h
template<class K,class Hash = HashFunc<K>>
class unordered_set
{
public:typedef typename li::HashTable<K,const K, SetOfKey, Hash>::ConstIterator const_iterator;const_iterator begin() const{return _hash.Begin();}const_iterator end() const{return _hash.End();}
private:li::HashTable<K,const K,SetOfKey,Hash> _hash;
};

2.3 unordered_map支持[]

unordered_map要支持[]主要需要修改insert返回值支持,修改HashTable中insert的返回值为pair<Iterator,bool> insert(const T& data),有了insert支持unordered_map的[]就很好实现了

代码如下:

pair<Iterator,bool> insert(const T& data)
{Hash hs;KeyOfT kot;Iterator it = find(kot(data)); //find函数的返回值也要进行修改,修改成Iteratorif (it != End())return { it,false };//负载因子为1就扩容if (_n == _tables.size()){unsigned long newsize = _stl_next_prime(_tables.size() + 1);vector<Node*> newtables(newsize, nullptr);for (int i = 0; i < _tables.size(); i++){//遍历旧表,将数据挪到新表Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = hs(kot(cur->_data)) % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}
//unordered_map.h
V& operator[](const K& key)
{pair<iterator, bool> ret = _hash.insert(make_pair(key, V()));return ret.first->second;
}

既然哈希表中的inser的返回值修改了,那么对应的unordered_map和unordered_set的insert函数的返回值也要进行修改

(三)结束语

用哈希表来模拟实现这两个容器,就得先学习底层的哈希表,不了解哈希表的友友可以看我的上一篇文章https://blog.csdn.net/muzi_liii/article/details/147519118?spm=1001.2014.3001.5501。该模拟实现简易的unordered_map和unordered_set就完成了。一起学习吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/80690.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java学习-Java基础

1.重写与重载的区别 重写发生在父子类之间,重载发生在同类之间构造方法不能重写,只能重载重写的方法返回值,参数列表,方法名必须相同重载的方法名相同,参数列表必须不同重写的方法的访问权限不能比父类方法的访问权限更低 2.接口和抽象类的区别 接口是interface,抽象类是abs…

BG开发者日志0427:故事的起点

1、4月26日晚上&#xff0c;BG项目的gameplay部分开发完毕&#xff0c;后续是细节以及试玩版优化。 开发重心转移到story部分&#xff0c;目前刚开始&#xff0c; 确切地说以前是长期搁置状态&#xff0c;因为过去的四个月中gameplay部分优先开发。 --- 2、BG这个项目的起点…

头歌实训之游标触发器

&#x1f31f; 各位看官好&#xff0c;我是maomi_9526&#xff01; &#x1f30d; 种一棵树最好是十年前&#xff0c;其次是现在&#xff01; &#x1f680; 今天来学习C语言的相关知识。 &#x1f44d; 如果觉得这篇文章有帮助&#xff0c;欢迎您一键三连&#xff0c;分享给更…

【深度学习】多头注意力机制的实现|pytorch

博主简介&#xff1a;努力学习的22级计算机科学与技术本科生一枚&#x1f338;博主主页&#xff1a; Yaoyao2024往期回顾&#xff1a;【深度学习】注意力机制| 基于“上下文”进行编码,用更聪明的矩阵乘法替代笨重的全连接每日一言&#x1f33c;: 路漫漫其修远兮&#xff0c;吾…

java16

1.API续集 可以导入别人写好的clone的jar包 注意&#xff1a;方法要有调用者&#xff0c;如果调用者是null就会报错 2.如何导入别人写好的jar包 复制jar包然后粘贴在lib里面&#xff0c;然后右键点击jar包再点击下面的add 3.关于打印java中的引用数据类型

PostgreSQL的扩展 credcheck

PostgreSQL的扩展 credcheck credcheck 是 PostgreSQL 的一个安全扩展&#xff0c;专门用于强制实施密码策略和凭证检查&#xff0c;特别适合需要符合安全合规要求的数据库环境。 一、扩展概述 1. 主要功能 强制密码复杂度要求防止使用常见弱密码密码过期策略实施密码重复使…

MyBatis中的@Param注解-如何传入多个不同类型的参数

mybatis中参数识别规则 默认情况下,MyBatis 会按照参数位置自动分配名称:param1, param2, param3, ...或者 arg0, arg1。 // Mapper 接口方法 User getUserByIdAndName(Integer id, String name); 以上接口在XML中只能通过param1或者arg0这样的方式来引用,可读性差。 &l…

DIFY教程第一集:安装Dify配置环境

一、Dify的介绍 https://dify.ai/ Dify 是一款创新的智能生活助手应用&#xff0c;旨在为您提供便捷、高效的服务。通过人工智能技术&#xff0c; Dify 可以实现语音 助手、智能家居控制、日程管理等功能&#xff0c;助您轻松应对生活琐事&#xff0c;享受智慧生活。简约的…

5、Rag基础:RAG 专题

RAG 简介 什么是检索增强生成? 检索增强生成(RAG)是指对大型语言模型输出进行优化,使其能够在生成响应之前引用训练数据来源之外的权威知识库。大型语言模型(LLM)用海量数据进行训练,使用数十亿个参数为回答问题、翻译语言和完成句子等任务生成原始输出。在 LLM 本就强…

GAMES202-高质量实时渲染(homework1)

目录 Homework1shadow MapPCF(Percentage Closer Filter)PCSS(Percentage Closer Soft Shadow) GitHub主页&#xff1a;https://github.com/sdpyy1 作业实现:https://github.com/sdpyy1/CppLearn/tree/main/games202 Homework1 shadow Map 首先需要完成MVP矩阵的构造&#xf…

JDK(Ubuntu 18.04.6 LTS)安装笔记

一、前言 本文与【MySQL 8&#xff08;Ubuntu 18.04.6 LTS&#xff09;安装笔记】同批次&#xff1a;先搭建数据库&#xff0c;再安装JDK&#xff0c;后面肯定就是部署Web应用&#xff1a;典型的单机部署。“麻雀虽小五脏俱全”&#xff0c;善始善终&#xff0c;还是记下来吧。…

软件测试之接口测试常见面试题

一、什么是(软件)接口测试? 接口测试&#xff1a;是测试系统组件间接口的一种测试方法 接口测试的重点&#xff1a;检查数据的交换&#xff0c;数据传递的正确性&#xff0c;以及接口间的逻辑依赖关系 接口测试的意义&#xff1a;在较早期开展&#xff0c;在软件开发的同时…

Lua 第11部分 小插曲:出现频率最高的单词

在本章中&#xff0c;我们要开发一个读取并输出一段文本中出现频率最高的单词的程序。像之前的小插曲一样&#xff0c;本章的程序也十分简单但是也使用了诸如迭代器和匿名函数这样的高级特性。 该程序的主要数据结构是一个记录文本中出现的每一个单词及其出现次数之间关系的表。…

软件项目进度管理活动详解

目录 1. 活动定义&#xff08;Activity Definition&#xff09; 2. 活动排序&#xff08;Activity Sequencing&#xff09; 3. 活动资源估算&#xff08;Activity Resource Estimating&#xff09; 4. 活动历时估算&#xff08;Activity Duration Estimating&#xff09; …

docker 国内源和常用命令

Ubuntu | Docker Docs 参考docker官方安装docker # Add Dockers official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt…

身份与访问管理(IAM):零信任架构下的认证授权技术与实战

身份与访问管理&#xff08;IAM&#xff09;&#xff1a;零信任架构下的认证授权技术与实战 在网络安全防御体系中&#xff0c;身份与访问管理&#xff08;Identity and Access Management, IAM&#xff09;是守护数字资产的“数字门禁系统”。随着远程办公和多云架构的普及&a…

Maven进阶知识

一、Maven 坐标 &#xff08;一&#xff09;概念 在 Maven 中坐标是构件的唯一标识&#xff0c;其元素包括 groupId、artifactId、version、packaging、classifier。其中 groupId、artifactId、version 是必定义项&#xff0c;packaging 默认为 jar。 &#xff08;二&#x…

网络原理 ——TCP 协议

TCP 报文结构 TCP 头部 20字节&#xff08;无选项&#xff09;&#xff0c;关键字段&#xff1a; 字段长度&#xff08;bit&#xff09;说明源端口16发送方端口目的端口16接收方端口序列号&#xff08;seq&#xff09;32数据字节的编号确认号&#xff08;ack&#xff09;32期…

C#使用sftp远程拷贝文件

需要下载 的包&#xff1a;Core.Renci.SshNet 下载依赖包的时候需要注意版本&#xff0c;高版本的.net环境不支持会用不了&#xff0c;我用的.net5,所以下载的2021.10.2 功能的核心式创建一个SftpClient&#xff0c;并传入所需要的参数&#xff1a;远程IP地址&#xff0c;端口…

文本预处理(NLTK)

1. 自然语言处理基础概念 1.1 什么是自然语言处理 自然语言处理( Natural Language Processing, NLP)是计算机科学领域与人工智能领域中的一个重要方向。它研究能实现人与计算机之间用自然语言进行有效通信的各种理论和方法。自然语言处理是一门融语言学、计算机科学、数学于…