爆款云主机2核4G限时秒杀,88元/年起!
查看详情

活动

天翼云最新优惠活动,涵盖免费试用,产品折扣等,助您降本增效!
热门活动
  • 618智算钜惠季 爆款云主机2核4G限时秒杀,88元/年起!
  • 免费体验DeepSeek,上天翼云息壤 NEW 新老用户均可免费体验2500万Tokens,限时两周
  • 云上钜惠 HOT 爆款云主机全场特惠,更有万元锦鲤券等你来领!
  • 算力套餐 HOT 让算力触手可及
  • 天翼云脑AOne NEW 连接、保护、办公,All-in-One!
  • 中小企业应用上云专场 产品组合下单即享折上9折起,助力企业快速上云
  • 息壤高校钜惠活动 NEW 天翼云息壤杯高校AI大赛,数款产品享受线上订购超值特惠
  • 天翼云电脑专场 HOT 移动办公新选择,爆款4核8G畅享1年3.5折起,快来抢购!
  • 天翼云奖励推广计划 加入成为云推官,推荐新用户注册下单得现金奖励
免费活动
  • 免费试用中心 HOT 多款云产品免费试用,快来开启云上之旅
  • 天翼云用户体验官 NEW 您的洞察,重塑科技边界

智算服务

打造统一的产品能力,实现算网调度、训练推理、技术架构、资源管理一体化智算服务
智算云(DeepSeek专区)
科研助手
  • 算力商城
  • 应用商城
  • 开发机
  • 并行计算
算力互联调度平台
  • 应用市场
  • 算力市场
  • 算力调度推荐
一站式智算服务平台
  • 模型广场
  • 体验中心
  • 服务接入
智算一体机
  • 智算一体机
大模型
  • DeepSeek-R1-昇腾版(671B)
  • DeepSeek-R1-英伟达版(671B)
  • DeepSeek-V3-昇腾版(671B)
  • DeepSeek-R1-Distill-Llama-70B
  • DeepSeek-R1-Distill-Qwen-32B
  • Qwen2-72B-Instruct
  • StableDiffusion-V2.1
  • TeleChat-12B

应用商城

天翼云精选行业优秀合作伙伴及千余款商品,提供一站式云上应用服务
进入甄选商城进入云市场创新解决方案
办公协同
  • WPS云文档
  • 安全邮箱
  • EMM手机管家
  • 智能商业平台
财务管理
  • 工资条
  • 税务风控云
企业应用
  • 翼信息化运维服务
  • 翼视频云归档解决方案
工业能源
  • 智慧工厂_生产流程管理解决方案
  • 智慧工地
建站工具
  • SSL证书
  • 新域名服务
网络工具
  • 翼云加速
灾备迁移
  • 云管家2.0
  • 翼备份
资源管理
  • 全栈混合云敏捷版(软件)
  • 全栈混合云敏捷版(一体机)
行业应用
  • 翼电子教室
  • 翼智慧显示一体化解决方案

合作伙伴

天翼云携手合作伙伴,共创云上生态,合作共赢
天翼云生态合作中心
  • 天翼云生态合作中心
天翼云渠道合作伙伴
  • 天翼云代理渠道合作伙伴
天翼云服务合作伙伴
  • 天翼云集成商交付能力认证
天翼云应用合作伙伴
  • 天翼云云市场合作伙伴
  • 天翼云甄选商城合作伙伴
天翼云技术合作伙伴
  • 天翼云OpenAPI中心
  • 天翼云EasyCoding平台
天翼云培训认证
  • 天翼云学堂
  • 天翼云市场商学院
天翼云合作计划
  • 云汇计划
天翼云东升计划
  • 适配中心
  • 东升计划
  • 适配互认证

开发者

开发者相关功能入口汇聚
技术社区
  • 专栏文章
  • 互动问答
  • 技术视频
资源与工具
  • OpenAPI中心
开放能力
  • EasyCoding敏捷开发平台
培训与认证
  • 天翼云学堂
  • 天翼云认证
魔乐社区
  • 魔乐社区

支持与服务

为您提供全方位支持与服务,全流程技术保障,助您轻松上云,安全无忧
文档与工具
  • 文档中心
  • 新手上云
  • 自助服务
  • OpenAPI中心
定价
  • 价格计算器
  • 定价策略
基础服务
  • 售前咨询
  • 在线支持
  • 在线支持
  • 工单服务
  • 建议与反馈
  • 用户体验官
  • 服务保障
  • 客户公告
  • 会员中心
增值服务
  • 红心服务
  • 首保服务
  • 客户支持计划
  • 专家技术服务
  • 备案管家

了解天翼云

天翼云秉承央企使命,致力于成为数字经济主力军,投身科技强国伟大事业,为用户提供安全、普惠云服务
品牌介绍
  • 关于天翼云
  • 智算云
  • 天翼云4.0
  • 新闻资讯
  • 天翼云APP
基础设施
  • 全球基础设施
  • 信任中心
最佳实践
  • 精选案例
  • 超级探访
  • 云杂志
  • 分析师和白皮书
  • 天翼云·创新直播间
市场活动
  • 2025智能云生态大会
  • 2024智算云生态大会
  • 2023云生态大会
  • 2022云生态大会
  • 天翼云中国行
天翼云
  • 活动
  • 智算服务
  • 产品
  • 解决方案
  • 应用商城
  • 合作伙伴
  • 开发者
  • 支持与服务
  • 了解天翼云
      • 文档
      • 控制中心
      • 备案
      • 管理中心

      STL:哈希表和unordered系列容器的封装

      首页 知识中心 软件开发 文章详情页

      STL:哈希表和unordered系列容器的封装

      2025-01-17 09:05:56 阅读次数:14

      位置,元素,关键字,函数,哈希

      一、unordered系列关联式容器的介绍

               在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是
      其底层结构不同(哈希表)

      1.1 unordered_map

      unordered_map的介绍

      1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
      2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
      3. 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
      4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
      5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
      6. 它的迭代器至少是前向迭代器。

      7.unordered就是无序的意思

      使用细节基本上和map一致


      1.2 unordered_set

      unordered_set的文档说明

      1.3 性能对比

      通过一个测试代码来比较unordered_set 和set的效率

      void testop() //测试  底层是红黑树和哈希表的效率比对    
      {
      	const size_t N = 1000000;
      
      	unordered_set<int> us;
      	set<int> s;
      
      	vector<int> v;
      	v.reserve(N);
      	srand((unsigned int)time(0));
      	for (size_t i = 0; i < N; ++i)
      	{
      		v.push_back(rand());
      		//v.push_back(rand()+i);
      		//v.push_back(i);
      	}
      
      	size_t begin1 = clock();
      	for (auto e : v)
      	{
      		s.insert(e);
      	}
      	size_t end1 = clock();
      	cout << "set insert:" << end1 - begin1 << endl;
      
      	size_t begin2 = clock();
      	for (auto e : v)
      	{
      		us.insert(e);
      	}
      	size_t end2 = clock();
      	cout << "unordered_set insert:" << end2 - begin2 << endl;
      
      
      	size_t begin3 = clock();
      	for (auto e : v)
      	{
      		s.find(e);
      	}
      	size_t end3 = clock();
      	cout << "set find:" << end3 - begin3 << endl;
      
      	size_t begin4 = clock();
      	for (auto e : v)
      	{
      		us.find(e);
      	}
      	size_t end4 = clock();
      	cout << "unordered_set find:" << end4 - begin4 << endl << endl;
      
      	cout << s.size() << endl;
      	cout << us.size() << endl << endl;;
      
      	size_t begin5 = clock();
      	for (auto e : v)
      	{
      		s.erase(e);
      	}
      	size_t end5 = clock();
      	cout << "set erase:" << end5 - begin5 << endl;
      
      	size_t begin6 = clock();
      	for (auto e : v)
      	{
      		us.erase(e);
      	}
      	size_t end6 = clock();
      	cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
      }
      

      STL:哈希表和unordered系列容器的封装

       我们发现,整体而言都是unordered系列更优一点,尤其是find的效率最为突出

      二、哈希表

      unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

      2.1 哈希表的概念

              顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
      O(log2 N)
      ,搜索的效率取决于搜索过程中元素的比较次数
       

      理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc->哈希函数)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素->哈希表

      (1)插入元素
             根据待插入元素的关键码,以此哈希函数计算出该元素的存储位置并按此位置进行存放
      (2)搜索元素
             对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
      取元素比较,若关键码相等,则搜索成功
      (3)删除元素

             对元素的关键码进行同样的计算,找到对应的位置并删除

              该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表
       

      STL:哈希表和unordered系列容器的封装

      2.2 哈希冲突

               不同关键字通过相同哈希哈数计算出相同的哈希地址(不同的值映射到了同一个位置),该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。


      下面我们就会重点介绍什么情况下会出现哈希冲突以及如何解决哈希冲突!!

      2.3 哈希函数

      哈希函数:将任意长度的数据映射到固定长度输出的算法(将键值映射为存储位置)

      哈希函数设计原则:
      (1)哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
      (2)哈希函数计算出来的地址能均匀分布在整个空间中
      (3)哈希函数应该比较简单

      引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
       

      常见的哈希函数:

      (1)直接定址法--(常用)

      取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
      优点:简单、均匀
      缺点:需要事先知道关键字的分布情况
      使用场景:适合查找比较小且连续的情况


      我们的位图其实就是用的直接定址法,可以理解为每个键值都有一个单独的映射位置。

      (2) 除留余数法(常用)

      设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
       

      哈希表的实现就是用的这个方法!!

      (3)平方取中法--(不常用)

      假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
      平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
       

      (4)折叠法--(不常用)

      折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
      折叠法适合:事先不需要知道关键字的分布,适合关键字位数比较多的情况
       

      (5)随机数法--(不常用)

      选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
      通常应用于关键字长度不等时采用此法
       

      (6)数学分析法(不常用)

             设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定
      相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。

      STL:哈希表和unordered系列容器的封装

             假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同
      的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。
                数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
      若干位分布较均匀的情况

       

      哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

       

      接下来我们将会重点讲解哈希表的实现,其底层用的是除留余数法,  解决其哈希冲突的方法有两种,即开放定址法和拉链法。
       

      2.4 开放定址法实现简单哈希表

      闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
           

      那么我们如何寻找下一个位置呢?答案就是线性探测。

      STL:哈希表和unordered系列容器的封装

               比如图中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
              但是线性探测会存在以下两个问题:

      (1)不确定每个位置的初始值放多少,不能是放0,否则该位置如果本来就要放0的话,这个时候我们就没办法区分了!!

      (2)插入的时候线性探测是向后查找直到空停止并放入,所以删除的时候也应该是线性探测向后查找直到空的这段区域必然可以找到这个要删除的元素,但是如上图44的位置依赖于前面4567的定位,假如4-7中的任意一个元素删除了,那么下一次要删44的时候就会找不到44.

       

          为了解决上面的两个问题,我们的策略就是设置3种状态,EMPTY 表示空, EXIST表示有,DELETE表示已删除。 我们给每个位置都存一个状态。

          同时为了防止元素扎堆的情况,可以采用二次线性探测,这样可以减少冲突。

      2.4.1 基本结构

      enum State //设置状态本质上是为了解决两个问题 1,一开始的元素不知道是多少比较合适  如果是0的话  可能加入的数也正好是0  2.如果该位置元素删除了,那么会导致因为该元素占位而到别的位置的元素找不到!!!
      {
      	EMPTY,//空
      	EXIST,//存在
      	DELETE //删除  
      };
      
      template<class K, class V>
      struct HashData
      {
      	pair<K, V> _kv;//会调用自己的默认构造
      	State _state = EMPTY;
      };
      
      
      template<class K, class V>
      class HashTable
      {
      public:
      	
      private:
      	vector<HashData<K, V>> _tables;
      	size_t _n = 0; //记录有效的数据个数
      };
      

      2.4.2 插入

      bool Insert(const pair<K, V>& kv)
      {
      	if (Find(kv.first))  return false;//键值不冗余, 所以如果存在,就没有必要去插入
      
      	//考虑扩容的问题   负载因子为0.7
      	if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
      	{
      	   //利用现代写法 让你去搞  然后窃取革命成果
      		size_t newsize = _tables.size() == 0 ? 10 :_tables.size()*2;
      		HashTable<K, V> newt;
      		newt._tables.resize(newsize);
      	    //遍历旧表 然后重新搞到新表上
      		for (auto& data : _tables)
      			if (data._state == EXIST)
      				newt.Insert(data._kv);
      		_tables.swap(newt._tables);//窃取革命成果
      	}
      
      
      	size_t hashi = kv.first % _tables.size();//%容量是没有用的,%size才是有用的  因为光扩容,也是不敢随机访问的!
      	//进行线性探测 当前如果存在就要往后推
      	size_t i = 1;
      	size_t index = hashi;
      	while (_tables[index]._state == EXIST) //
      	{
      		index = hashi + i; //这样子方便后面修改成二次检测
      		//index可能会越界,可能需要去修正一下
      		index %= _tables.size();
      		++i;
      	}
      	//在相应的位置进行操作
      	_tables[index]._kv = kv;
      	_tables[index]._state = EXIST;
      	++_n; //更新次数
      
      	return true;
      }

        (1) 哈希表在%的时候一定要%size而不是%capacity(可能会存在越界,比如说size只有10,而capacity是20,如果映射到15也是不可以访问的!!),因为我们空间一定是开好了的而不是中间插入进去。 扩容的时候也是一定要用resize而不是reserve。

      (2)哈希表在某些时候需要去扩容,我们设置一个负载因子(假设0.7),超出这个范围我们就是扩容,但是我们不能单纯地去resize,因为映射关系会完全改变。比如说原本10的空间,12是插入在2的位置,但是空间变成20后,12就应该插入到12的位置上。所以我们要创建一个新表,然后遍历旧表拿数据,重新计算映射位置。      每次又要走一次线性探测,为了防止代码冗余,此时有两种方案,一种是将线性探测的代码封装起来,然后复用   另一种方案是现代写法,再创建一个新的对象,然后让该对象去调用insert,最后再利用swap窃取成果。

      2.4.3 查找

      HashData<K, V>* Find(const K& key) //返回值为指针是方便我们在找不到的时候可以返回空
      {
      	if (_tables.size() == 0) return nullptr;
      	//一样要进行线性探测
      	size_t hashi = key % _tables.size();
      	size_t i = 1;
      	size_t index = hashi;
      	while (_tables[index]._state != EMPTY)  //不等于空的时候往后走
      	{
      		if (_tables[index]._state == EXIST && _tables[index]._kv.first == key)  //前面的判断是确保当前位置不为DETELE 状态  因为为我们封装的删除是一个伪删除,并不是真的删除
      			return &_tables[index];
      		//如果找不到,往后继续走
      		index = hashi + i;
      		index %= _tables.size();
      		++i;
      
      		//极端情况    插入一部分 +删除一部分 +插入一部分 正好所有的位置都是存在+删除 此时上面面临着死循环  所以我们堆i进行判断,如果index==hashi说明已经找了一圈了,此时就跳出去
      		if (index == hashi) break;
      	}
      	return nullptr;
      }

              不等于空就可以继续向后线性探测,因为我们设置delete状态就是想实现伪删除,这样在删除某些元素的时候不会影响线性探测。

      极端情况:如果正好插入一部分,删除一部分,再插入一部分,导致所有的位置都是存在+删除,那么循环永远不会停止,这个时候我们就要去判断如果正好走了一圈了,就可以break了。

      2.4.4 删除

      bool Erase(const K& key)
      {
      	HashData<K, V>* ret = Find(key); //复用find
      	if (ret) //如果找到了  将该位置改成删除
      	{
      		ret->_state = DELETE; //伪删除
      		--_n;
      		return true;
      	}
      	return false;
      }

      复用Find即可,然后进行伪删除。 

      2.4.5 整体代码的实现

      	enum State //设置状态本质上是为了解决两个问题 1,一开始的元素不知道是多少比较合适  如果是0的话  可能加入的数也正好是0  2.如果该位置元素删除了,那么会导致因为该元素占位而到别的位置的元素找不到!!!
      	{
      		EMPTY,//空
      		EXIST,//存在
      		DELETE //删除  
      	};
      
      	template<class K, class V>
      	struct HashData
      	{
      		pair<K, V> _kv;//会调用自己的默认构造
      		State _state = EMPTY;
      	};
      
      
      	template<class K, class V>
      	class HashTable
      	{
      	public:
      		bool Insert(const pair<K, V>& kv)
      		{
      			if (Find(kv.first))  return false;//键值不冗余, 所以如果存在,就没有必要去插入
      
      			//考虑扩容的问题   负载因子为0.7
      			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
      			{
      			   //利用现代写法 让你去搞  然后窃取革命成果
      				size_t newsize = _tables.size() == 0 ? 10 :_tables.size()*2;
      				HashTable<K, V> newt;
      				newt._tables.resize(newsize);
      			    //遍历旧表 然后重新搞到新表上
      				for (auto& data : _tables)
      					if (data._state == EXIST)
      						newt.Insert(data._kv);
      				_tables.swap(newt._tables);//窃取革命成果
      			}
      
      
      			size_t hashi = kv.first % _tables.size();//%容量是没有用的,%size才是有用的  因为光扩容,也是不敢随机访问的!
      			//进行线性探测 当前如果存在就要往后推
      			size_t i = 1;
      			size_t index = hashi;
      			while (_tables[index]._state == EXIST) //
      			{
      				index = hashi + i; //这样子方便后面修改成二次检测
      				//index可能会越界,可能需要去修正一下
      				index %= _tables.size();
      				++i;
      			}
      			//在相应的位置进行操作
      			_tables[index]._kv = kv;
      			_tables[index]._state = EXIST;
      			++_n; //更新次数
      
      			return true;
      		}
      
      		HashData<K, V>* Find(const K& key) //返回值为指针是方便我们在找不到的时候可以返回空
      		{
      			if (_tables.size() == 0) return nullptr;
      			//一样要进行线性探测
      			size_t hashi = key % _tables.size();
      			size_t i = 1;
      			size_t index = hashi;
      			while (_tables[index]._state != EMPTY)  //不等于空的时候往后走
      			{
      				if (_tables[index]._state == EXIST && _tables[index]._kv.first == key)  //前面的判断是确保当前位置不为DETELE 状态  因为为我们封装的删除是一个伪删除,并不是真的删除
      					return &_tables[index];
      				//如果找不到,往后继续走
      				index = hashi + i;
      				index %= _tables.size();
      				++i;
      
      				//极端情况    插入一部分 +删除一部分 +插入一部分 正好所有的位置都是存在+删除 此时上面面临着死循环  所以我们堆i进行判断,如果index==hashi说明已经找了一圈了,此时就跳出去
      				if (index == hashi) break;
      			}
      			return nullptr;
      		}
      
      		bool Erase(const K& key)
      		{
      			HashData<K, V>* ret = Find(key); //复用find
      			if (ret) //如果找到了  将该位置改成删除
      			{
      				ret->_state = DELETE; //伪删除
      				--_n;
      				return true;
      			}
      			return false;
      		}
      
      
      	private:
      		vector<HashData<K, V>> _tables;
      		size_t _n = 0; //记录有效的数据个数
      	};

      2.5 拉链法实现简单哈希表

              开放定址法是一个比较粗暴的方式,因为其是一种恶性循环,冲突之后就占别的位置,而后面一旦出现本该放在该位置的元素,也得另外占别的位置。 

             而拉链法相对来说更文明一点,如果发生冲突了,我就跟你挤一挤。接在一起。

            开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶(哈希桶),各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
           STL:哈希表和unordered系列容器的封装

      开散列中每个桶中放的都是发生哈希冲突的元素。

      //因为有扩容(负载因子控制)的存在!!!!,哈希桶增删查改的时间复杂度都是O(1)      虽然插入可能会存在扩容,扩容操作是O(N) 但是整体而言扩容只在那一瞬间 因为我们的哈希表是不断在插入元素的 
                                                         //相当于是一个好学生考试,他大多数情况下都考得好就偶尔一两次考不好  同理,这边插入的时间复杂度大多数情况是O(1) O(N)只在一瞬间
      //跟快排一样,快排也非常难出现最坏情况 因为有了随机取KEY(针对有序) 以及这个三路划分(针对重复)
      namespace HashBucket //哈希桶  拉链法  开散列
      {
      
      	template<class K,class V>
      	struct HashNode
      	{
      		HashNode<K, V>* _next;
      		pair<K, V> _kv;
              
      		HashNode(const pair<K, V>& kv)
      			:_next(nullptr)
      			, _kv(kv)
      		{}
      	};
      
      
      	template<class K>
      	struct HashFunc //默认 int
      	{
      		size_t operator()(const K& key)
      		{
      			return key;
      		}
      	};
      
      	template<>  //因为string 用的比较多, 所以我们可以专门搞一个模板特化版本  专门针对string  有特化走特化,没特化走普通类型
      	struct HashFunc<string>
      	{
      		// 尽量要控制不要重复  1.将字符串的所有ASCII码加起来 2,但是加起来之后,对于字符相同但是顺序不同的字符串 还是不太好区分,所以这里要用以恶搞BKDR哈希算法
      		size_t operator()(const string& s)
      		{
      			size_t hash = 0;
      			for (auto ch : s)  //将字符串转化成整型,通过映射整型去找到字符串对应的位置
      			{
      				hash += ch;  //但是光有这一句代码的话, 应对一些字符相同但是顺序相同的字符串的时候,仍然是不好区分的 
      				hash *= 31;  //BKDR哈希 
      			}
      
      			return hash;
      		}
      	};
      
      
      	template<class K,class V,class Hash= HashFunc<K>>
      	class HashTable
      	{
      		typedef HashNode<K, V> Node;
      	public:
      		~HashTable()
      		{
      			clear();
      		}
      
      		bool Insert(const pair<K, V>& kv)
      		{
      			if (Find(kv.first)) return false;//如果有就不用找了  
      			Hash hash;//通过仿函数控制可以K变成可以取模
      			//负载因子为1时进行扩容
      			if (_n == _tables.size()) //这里不适合用现代写法,因为如果去调用insert的话,会重新申请很多新的结点,然后又要释放很多结点 效率太低了    //解决方案就是  原表的结点重新计算,挪动到新表的位置                                                                  
      			{
      				//size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2; 原来的版本
      
      				size_t newsize = GetNextPrime(_tables.size()); //按照SGI版本的素数表
      				第一种方案  创建一个新的表来完成这个任务
      				//HashTable<K, V> newht;
      				//newht._tables.resize(newsize);
      				//for (auto&cur : _tables)
      				//{
      				//	while (cur)
      				//	{
      				//		newht.Insert(cur->_kv);
      				//		cur = cur->_next;
      				//	}
      				//}
      				//_tables.swap(newht._tables); 
      
      				vector<Node*> newtables(newsize, nullptr);
      				//将结点一个个解下来放到新表中 (复用的话会创建很多新的结点 其实没有什么必要 )
      				for (Node*& cur : _tables)
      					while (cur)
      					{
      						Node* next = cur->_next;
      						size_t hashi = hash(cur->_kv.first) % newtables.size(); //重新计算   
      						//头插到新表
      						cur->_next = newtables[hashi];
      						newtables[hashi] = cur;
      						cur = next;
      					}
      				_tables.swap(newtables);//交换过来 
      			}
      			size_t hashi = hash(kv.first) % _tables.size();  //只有整形是支持取模的,其他的类型是不一定的
      			//执行头插的逻辑
      			Node* newnode = new Node(kv);
      			newnode->_next = _tables[hashi];
      			_tables[hashi] = newnode;//成为新的头
      			++_n;
      			return true;
      		}
      
      			Node* Find(const K&key)
      			{
      				if (_tables.size() == 0)  return nullptr;
      				Hash hash;
      				size_t hashi = hash(key) % _tables.size(); //找到这个点
      				Node* cur = _tables[hashi];
      				while (cur)
      				{
      					if (cur->_kv.first == key) return cur;
      					cur = cur->_next;
      				}
      				return nullptr;
      			}
      
      			bool Erase(const K& key)
      			{
      				Hash hash;
      				//这边就不能直接复用find了,因为只是拿到该结点,但是还得从里面删掉才有用
      				size_t hashi = hash(key) % _tables.size();//找到这个位置
      				Node* prev = nullptr;
      				Node* cur = _tables[hashi];
      				while (cur)
      				{
      					if (cur->_kv.first == 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; //说明找不到
      			}
      
      
      			size_t MaxBucketSize() //统计桶的最大长度并返回  检测查找的效率
      			{
      				size_t max = 0;
      				for (size_t i = 0; i < _tables.size(); ++i)
      				{
      					auto cur = _tables[i];
      					size_t size = 0;
      					while (cur)
      					{
      						++size;
      						cur = cur->_next;
      					}
      					printf("[%zd]->%zd\n", i, size); //打印所有桶的长度 让我们看到具体的分布情况  这样就可以测出查找的时间复杂度
      					if (size > max) 	max = size;
      				}
      				return max;
      			}
      
      			void clear()
      			{
      				for (Node*& cur : _tables)
      				{
      					while (cur)
      					{
      						Node* next = cur->_next;
      						delete cur;
      						cur = next;
      					}
      
      					cur = nullptr;
      				}
      			}
      
      
      			//SGI版本    主要是数学上提到说如果表的长度是素数的话,模的时候更不容易冲突  
      			//但是如果我们正常的扩2倍肯定是难以维持他是素数的,所以就有了这样一个解决方案,专门给了一个素数表 让哈希表的长度按照这个表去更新 这样可以保证长度一直是素数
      			//SGI版本是这么实现的
      			size_t GetNextPrime(size_t prime)
      			{
      				// SGI
      				static const int __stl_num_primes = 28;
      				static const unsigned long __stl_prime_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
      				}; //实际上是不可能扩到最大的 因为 2^32次方  哪怕是char类型都有4G了  指针都有16G了
      				    /*1Byte(字节)=8bit(位)
      				    1 KB = 1024 Byte
      					1 MB = 1024 KB
      					1 GB = 1024 MB
      					1TB = 1024GB*/
      			
      
      				size_t i = 0;
      				for (; i < __stl_num_primes; ++i)
      				{
      					if (__stl_prime_list[i] > prime)
      						return __stl_prime_list[i];
      				}
      
      				return __stl_prime_list[i];  //在表里依次遍历 找到比原先大的那个素数值
      			}
      
      	private:
      		vector<Node*> _tables; //指针数组
      		size_t _n=0;//记录有效元素的个数 
      	};

      三、unordered系列容器的封装

      3.1 改造拉链法哈希表

      //自己实现的时候  一定要一步一步来, 先封装哈希表 然后再封装简单的map和set  然后再封装迭代器让插入跑起来,然后再去考虑其他的一些细节问题, 不要一下子把所有的模板参数都加上 
      //要一步一步来
      
      template<class T>
      struct HashNode
      {
      	HashNode<T>* _next;
      	T _data; //T决定存的是KV 还是K
      
      	HashNode(const T& data)
      		:_next(nullptr)
      		,_data(data)
      	{}
      };
      template<class K>
      struct HashFunc //默认 int
      {
      	size_t operator()(const K& key)
      	{
      		return key;
      	}
      };
      
      template<>  //因为string 用的比较多, 所以我们可以专门搞一个模板特化版本  专门针对string  有特化走特化,没特化走普通类型  默认支持
      struct HashFunc<string>
      {
      	// 尽量要控制不要重复  1.将字符串的所有ASCII码加起来 2,但是加起来之后,对于字符相同但是顺序不同的字符串 还是不太好区分,所以这里要用以恶搞BKDR哈希算法
      	size_t operator()(const string& s)
      	{
      		size_t hash = 0;
      		for (auto ch : s)  //将字符串转化成整型,通过映射整型去找到字符串对应的位置
      		{
      			hash += ch;  //但是光有这一句代码的话, 应对一些字符相同但是顺序相同的字符串的时候,仍然是不好区分的 
      			hash *= 31;  //BKDR哈希 
      		}
      
      		return hash;
      	}
      };
      
      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 _HashIterator //迭代器的封装
      {
      	typedef HashNode<T> Node;//结点
      	typedef HashTable<K, T, KeyOfT,Hash> HT;//处理指向哈希桶的指针
      	typedef _HashIterator<K, T, Ref, Ptr, KeyOfT,Hash> Self;//自己
      	typedef _HashIterator<K, T, T&, T*, KeyOfT, Hash> Iterator; //服务set普通迭代器 
      	//在红黑树中也是按照这个逻辑,key被修改会破坏红黑树的整体结构, 所以我们必须要去控制
          
      	//成员变量 必然要有结点指针以及哈希桶的指针
      	Node* _node;
      	const HT* _ht;
      
      
      	_HashIterator(Node* node,const HT* ht) //这样可以保证const可以接收 普通的也可以接收 因为权限可以缩小
      		:_node(node)
      		, _ht(ht)
      	{}
      
      	_HashIterator(const Iterator& it) //拷贝构造  本质上是为了set去服务的
      		:_node(it._node)   //普通迭代器可以转化成const迭代器 
      		, _ht(it._ht)
      	{}
      
      	Ref operator*()
      	{
      		return _node->_data;
      	}
      
      	Ptr operator->()
      	{
      		return &operator*();
      	}
      
      	bool operator!=(const Self& s)
      	{
      		return _node != s._node;
      	}
      
      	Self& operator++()
      	{
      		if (_node->_next) _node = _node->_next;// 如果结点下一个不为空,就去找下一个
      		else //如果结点的下一个为空,那么就去哈希桶找下一个桶
      		{
      			KeyOfT kot;//控制data取出来的是什么
      			//先算一下自己在当前桶的位置 
      			size_t hashi = _ht->bucket(kot(_node->_data)); //无法访问私有成员 要么用友元 要么封装gettable和getbucketsize这两个函数 
      			++hashi;
      			while (hashi < _ht->_buckets.size())
      			{
      				if (_ht->_buckets[hashi]) //如果找到了
      				{
      					_node = _ht->_buckets[hashi];
      					break;
      				}
      				++hashi; //继续找下一个桶
      			}
                 //此时可能有两种情况  一种是成功找到,一种是走到最后都没找到
      			if (hashi == _ht->_buckets.size()) _node = nullptr;
      		}
      		return *this;
      	}
      
      	Self operator++(int)
      	{
      		Self temp = *this;
      		++*this;
      		return temp;
      	}
      
      };
      
      //    class Pred = equal_to<Key>
      
      template<class K, class T, class KeyOfT,class Hash>
      class HashTable
      {
      	     typedef HashNode<T> Node;
      		 //友元  为了能够让迭代器访问私有成员table  但是这样会破坏封装性 不推荐使用  还有一种方法是封装一个getbucketsize函数和gettables的函数  
      	     template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
      		 friend	struct _HashIterator; 
      public:
      	~HashTable()
      	{
      		clear();
      	}
      
      	typedef _HashIterator<K, T,T&,T*,KeyOfT, Hash>  iterator;
      	typedef _HashIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
      	typedef HashTable<K, T, KeyOfT, Hash>   Self;
      	iterator begin()  //得找到第一个有效的桶
      	{
      		Node* cur = nullptr; //防止全部为空的时候
      		for (size_t i = 0; i < _buckets.size(); ++i)
      		{
      			if (_buckets[i])
      			{
      				cur = _buckets[i];
      				break;
      			}
      		}
      		return iterator(cur, this);  //this指针
      	}
      
      	iterator end()  
      	{
      		return iterator(nullptr, this);  //this指针
      	}
      
      
      	const_iterator begin() const
      	{
      		Node* cur = nullptr;
      		for (size_t i = 0; i < _buckets.size(); ++i)
      		{
      			cur = _buckets[i];
      			if (_buckets[i])
      			{
      				cur = _buckets[i];
      				break;
      			}
      		}
      		return const_iterator(cur, this);
      	}
      
      
      	
      	const_iterator end() const
      	{
      		return const_iterator(nullptr, this);
      	}
      
      
      	pair<iterator, bool> Insert(const T& data)
      	{
      		KeyOfT kot;//控制data取出来的是什么
      		iterator it = Find(kot(data));
      		if (it != end()) return make_pair(it, false);//如果有就不用找了  
      		
      		//负载因子为1时进行扩容
      		if (_n == _buckets.size()) //这里不适合用现代写法,因为如果去调用insert的话,会重新申请很多新的结点,然后又要释放很多结点 效率太低了    //解决方案就是  原表的结点重新计算,挪动到新表的位置                                                                  
      		{
      			//size_t newsize = _buckets.size() == 0 ? 10 : _buckets.size() * 2; 原来的版本
      
      			size_t newsize = GetNextPrime(_buckets.size()); //按照SGI版本的素数表
      			第一种方案  创建一个新的表来完成这个任务
      			//HashTable<K, V> newht;
      			//newht._buckets.resize(newsize);
      			//for (auto&cur : _buckets)
      			//{
      			//	while (cur)
      			//	{
      			//		newht.Insert(cur->_kv);
      			//		cur = cur->_next;
      			//	}
      			//}
      			//_buckets.swap(newht._buckets); 
      
      			vector<Node*> newtables(newsize, nullptr);
      			//将结点一个个解下来放到新表中 (复用的话会创建很多新的结点 其实没有什么必要 ) 所以这里不用第一种方法  
      			for (Node*& cur : _buckets)
      				while (cur)
      				{
      					Node* next = cur->_next;
      					size_t hashi = bucket(kot(cur->_data)); //重新计算   
      					//头插到新表
      					cur->_next = newtables[hashi];
      					newtables[hashi] = cur;
      					cur = next;
      				}
      			_buckets.swap(newtables);//交换过来 
      		}
      		size_t hashi = bucket(kot(data));  //只有整形是支持取模的,其他的类型是不一定的
      		//执行头插的逻辑
      		Node* newnode = new Node(data);
      		newnode->_next = _buckets[hashi];
      		_buckets[hashi] = newnode;//成为新的头
      		++_n;
      		return make_pair(iterator(newnode,this),true);
      	}
      
      	iterator Find(const K& key)
      	{
      		if (empty())  return end(); //为空 就没啥好找的了
      		KeyOfT kot;//控制data取出来的是什么
      		size_t hashi = bucket(key); //找到这个点
      		Node* cur = _buckets[hashi];
      		while (cur)
      		{
      			if (kot(cur->_data) == key) return iterator(cur, this);
      			cur = cur->_next;
      		}
      		return iterator(nullptr, this);
      	}
      
      	bool Erase(const K& key)
      	{
      		Hash hash;//控制模出来的情况 因为可能会是字符串什么的
      		KeyOfT kot;//控制data取出来的是什么
      		//这边就不能直接复用find了,因为只是拿到该结点,但是还得从里面删掉才有用
      		size_t hashi = bucket(key);//找到key对应的桶
      		Node* prev = nullptr;
      		Node* cur = _buckets[hashi];
      		while (cur)
      		{
      			if (kot(cur->_data) == key) //执行删除
      			{
      				if (prev == nullptr)   _buckets[hashi] = cur->_next;         //说明第一个就要删
      				else  prev->_next = cur->_next;
      				delete cur;
      				--_n;
      				return true;
      			}
      			prev = cur;
      			cur = cur->_next;
      		}
      		return false; //说明找不到
      	}
      
      	void clear() //清空桶中的元素
      	{
      		for (Node*& cur : _buckets)
      		{
      			while (cur)
      			{
      				Node* next = cur->_next;
      				delete cur;
      				cur = next;
      			}
      			cur = nullptr;
      		}
      	}
      	
      
      	size_t size() const //哈希表中的元素个数
      	{
      		return _n;
      	}
      
      	bool empty() const
      	{
      		return size() == 0;
      	}
      
      	void swap(Self& s)
      	{
      		_buckets.swap(s._buckets);
      		std::swap(_n, s._n);
      	}
      
      
      	//SGI版本    主要是数学上提到说如果表的长度是素数的话,模的时候更不容易冲突  
      	//但是如果我们正常的扩2倍肯定是难以维持他是素数的,所以就有了这样一个解决方案,专门给了一个素数表 让哈希表的长度按照这个表去更新 这样可以保证长度一直是素数
      	//SGI版本是这么实现的
      	size_t GetNextPrime(size_t prime)
      	{
      		// SGI
      		static const int __stl_num_primes = 28;
      		static const unsigned long __stl_prime_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
      		}; //实际上是不可能扩到最大的 因为 2^32次方  哪怕是char类型都有4G了  指针都有16G了
      		/*1Byte(字节)=8bit(位)
      		1 KB = 1024 Byte
      		1 MB = 1024 KB
      		1 GB = 1024 MB
      		1TB = 1024GB*/
      
      
      		size_t i = 0;
      		for (; i < __stl_num_primes; ++i)
      		{
      			if (__stl_prime_list[i] > prime)
      				return __stl_prime_list[i];
      		}
      
      		return __stl_prime_list[i];  //在表里依次遍历 找到比原先大的那个素数值
      	}
      
      	const size_t MaxBucketSize() const  //统计桶的最大长度并返回  检测查找的效率   测试用的  不是刚需
      	{
      		size_t max = 0;
      		for (size_t i = 0; i < _buckets.size(); ++i)
      		{
      			Node* cur = _buckets[i];
      			size_t size = bucket_size(cur);
      			printf("[%zd]->%zd\n", i, size); //打印所有桶的长度 让我们看到具体的分布情况  这样就可以测出查找的时间复杂度
      			if (size > max) 	max = size;
      		}
      		return max;
      	}
      
      	 size_t bucket(const K& key) const //返回key所在哈希桶的编号
      	{
      		Hash hash;//控制模出来的情况 因为可能会是字符串什么的
      		size_t hashi = hash(key) % _buckets.size(); //找到这个点
      		return hashi;
      	}
      
      	 size_t bucket_size(size_t n) const  //返回这个桶有多少个元素
      	 {
      		 Node* cur = _buckets[n];
      		 size_t size = 0;
      		 while (cur)
      		 {
      			 ++size;
      			 cur = cur->_next;
      		 }
      		 return size;
      	 }
      
      private:
      	vector<Node*> _buckets; //指针数组  哈希桶集合
      	size_t _n = 0;//记录有效元素的个数 
      };

      字符串哈希函数算法

      由于string很常用,所以库里面有默认支持string类型的哈希函数,将哈希函数转化成可以取模的整数

      3.2 unordered_set的封装

      #pragma once
      #include"HashTable.h"
      
      namespace cyx
      {
      	template<class K , class Hash = HashFunc<K>> //不要再Hashtable里面传  会写死 这样自定义类型的key就没有办法解决了
      	class unordered_set
      	{
      	public:
      		struct SetKeyOfT
      		{
      			const K& operator()(const K& key)
      			{
      				return key;
      			}
      		};
      
      		~unordered_set()
      		{
      			 clear();
      		}
      
      		//取类模板的内嵌类型 一定要用typename  不然无法区分这是一个类型还是成员变量 
      		typedef typename HashTable<K, K,SetKeyOfT,Hash> ::const_iterator iterator; //set的迭代器不可被修改
      		typedef typename HashTable<K, K, SetKeyOfT, Hash>::const_iterator const_iterator;
      		iterator begin() //普通迭代器转化const迭代器 就会去调用那个拷贝构造
      		{
      			return _ht.begin();
      		}
      		iterator end()
      		{
      			return _ht.end();
      		}
      
      		const_iterator begin() const
      		{
      			return _ht.begin();
      		}
      
      		const_iterator end() const
      		{
      			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);
      		}
      
      		void clear() 
      		{
      			_ht.clear();
      		}
      
      		bool empty() const
      		{
      			return _ht.empty();
      		}
      
      	private:
      		HashTable<K, K, SetKeyOfT, Hash>  _ht ;
      	};
      }

      3.3 unordered_map的封装

      #pragma once
      #include"HashTable.h"
      
      namespace cyx
      {
      	template<class K,class V,class Hash = HashFunc<K>>
      	class unordered_map
      	{
      	public:
      		struct MapKeyOfT
      		{
      			const K& operator()(const pair<K,V>& kv) //帮助我们拿到key
      			{
      				return kv.first;
      			}
      		};
      
      		 ~unordered_map()
      		{
      			clear();
      		}
      
      		//取类模板的内嵌类型 一定要用typename  不然无法区分这是一个类型还是成员变量 
      		typedef typename HashTable<K, pair<const K, V>, MapKeyOfT,Hash> ::iterator iterator; //pair的key无论什么情况都不能修改 所以我们通过传const K控制
      		typedef typename HashTable<K, pair<const K, V>, MapKeyOfT,Hash>::const_iterator const_iterator;
      		typedef unordered_map<K, V, Hash> Self;
      
      		iterator begin()
      		{
      			return _ht.begin();
      		}
      		iterator end()
      		{
      			return _ht.end();
      		}
      
      		const_iterator begin() const
      		{
      			return _ht.begin();
      		}
      
      		const_iterator end() const
      		{
      			return _ht.end();
      		}
      
      
      		pair<iterator, bool> insert(const pair<K,V>& kv)
      		{
      			return _ht.Insert(kv);
      		}
      
      		iterator find(const K& key) const
      		{
      			return _ht.Find(key);
      		}
      
      		bool erase(const K& key)
      		{
      			return _ht.Erase();
      		}
      
      		V& operator[](const K& key)  //重载方括号
      		{
      			pair<iterator, bool> ret = insert(make_pair(key, V()));
      			return ret.first->second;
      		}
      		
      
      		void clear() 
      		{
      			_ht.clear();
      		}
      
      		bool empty() const
      		{
      			return _ht.empty();
      		}
      
      
      		void swap(Self& s)
      		{
      			_ht.swap(s._ht);
      		}
      	private:
      		HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht; 
      	};
      

       3.4 自定义类型的key

      	class Date
      	{
      		friend struct HashDate; //确保能够访问其私有成员
      	public:
      		Date(int year = 1900, int month = 1, int day = 1)
      			: _year(year)
      			, _month(month)
      			, _day(day)
      		{}
      
      		bool operator<(const Date& d)const
      		{
      			return (_year < d._year) ||
      				(_year == d._year && _month < d._month) ||
      				(_year == d._year && _month == d._month && _day < d._day);
      		}
      
      		bool operator>(const Date& d)const
      		{
      			return (_year > d._year) ||
      				(_year == d._year && _month > d._month) ||
      				(_year == d._year && _month == d._month && _day > d._day);
      		}
      
      		bool operator==(const Date& d) const
      		{
      			return _year == d._year
      				&& _month == d._month
      				&& _day == d._day;
      		}
      
      		friend ostream& operator<<(ostream& _cout, const Date& d);
      	private:
      		int _year;
      		int _month;
      		int _day;
      	};
      
      	ostream& operator<<(ostream& cout, const Date& d)
      	{
      		cout << d._year << "-" << d._month << "-" << d._day;
      		return cout;
      	}
      
      	struct HashDate //自定义类型的key就手动传一个
      	{
      		size_t operator()(const Date& d)
      		{
      			size_t hash = 0;
      			hash += d._year;
      			hash *= 31;
      
      			hash += d._month;
      			hash *= 31;
      
      			hash += d._day;
      			hash *= 31;
      
      			return hash;
      		}
      	};
      
      	// 一个类型要做unordered_map/unordered_set的key,要满足支持转换成取模的整形 和 ==比较
      	
      	// 一个类型要做map/set的key,要满足支持<比较
      	/*if (cur->key < key)
      	{}
      	else if (key < cur->key)
      	{}
      	else
      	{}*/
      
      
      	//如果有些类不是你写的,并不支持上面的东西 但是你还是想要去比较怎么办呢??   其实库里面还提供了一个仿函数  class Pred = equal_to<Key>//判断各个键值对相同的规则
      
      	void test_unordered_map4()
      	{
      		Date d1(2023, 3, 13);
      		Date d2(2023, 3, 13);
      		Date d3(2023, 3, 12);
      		Date d4(2023, 3, 11);
      		Date d5(2023, 3, 12);
      		Date d6(2023, 3, 13);
      
      		Date a[] = { d1, d2, d3, d4, d5, d6 };
      
      		unordered_map<Date, int, HashDate> countMap;
      		for (auto e : a)  ++countMap[e];
      		for (auto& kv : countMap)  cout << kv.first << ":" << kv.second << endl;
      	}

      STL:哈希表和unordered系列容器的封装

      注意:

      1、 一个类型要做unordered_map/unordered_set的key,满足支持转换成 取模的整形 和 ==比较
             一个类型要做map/set的key,要满足支持<比较 

      2、但是如果该类不是你自己写的,我们没有办法直接去访问其私有成员,那就得再加一个模版参数class Pred = equal_to<Key> 。增加判断键值相同的规则。

      版权声明:本文内容来自第三方投稿或授权转载,原文地址:https://blog.csdn.net/weixin_51142926/article/details/138453310,作者:✿༺小陈在拼命༻✿,版权归原作者所有。本网站转在其作品的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如因作品内容、版权等问题需要同本网站联系,请发邮件至ctyunbbs@chinatelecom.cn沟通。

      上一篇:恕我直言你可能真的不会java第10篇-集合元素归约

      下一篇:laravel 源码分析之表单验证不通过的时候是怎么展示的

      相关文章

      2025-05-19 09:04:44

      js小题2:构造函数介绍与普通函数对比

      js小题2:构造函数介绍与普通函数对比

      2025-05-19 09:04:44
      new , 关键字 , 函数 , 对象 , 构造函数
      2025-05-19 09:04:30

      【Canvas技法】辐射式多道光影的实现

      【Canvas技法】辐射式多道光影的实现

      2025-05-19 09:04:30
      代码 , 函数 , 实现
      2025-05-19 09:04:22

      外设驱动库开发笔记54:外设库驱动设计改进的思考

      外设驱动库开发笔记54:外设库驱动设计改进的思考

      2025-05-19 09:04:22
      使用 , 函数 , 初始化 , 定义 , 对象
      2025-05-19 09:04:14

      C语言字符函数和字符串函数--(超全超详细)

      C语言字符函数和字符串函数--(超全超详细)

      2025-05-19 09:04:14
      函数 , 字符 , 字符串
      2025-05-16 09:15:24

      Redis Set集合

      Redis Set集合

      2025-05-16 09:15:24
      set , 个数 , 元素 , 示例 , 集合
      2025-05-16 09:15:24

      Redis Hash哈希

      Redis Hash哈希

      2025-05-16 09:15:24
      field , hash , Redis , value , 哈希
      2025-05-16 09:15:24

      如何将一串数字用函数的方法倒过来(C语言)

      如何将一串数字用函数的方法倒过来(C语言)

      2025-05-16 09:15:24
      函数 , 数字 , 数组
      2025-05-14 10:33:31

      【数据结构】第一章——绪论(2)

      【数据结构】第一章——绪论(2)

      2025-05-14 10:33:31
      函数 , 实现 , 打印 , 理解 , 算法 , 输入 , 输出
      2025-05-14 10:33:31

      【数据结构】详细介绍串的简单模式匹配——朴素模式匹配算法

      【数据结构】详细介绍串的简单模式匹配——朴素模式匹配算法

      2025-05-14 10:33:31
      下标 , 元素 , 匹配 , 子串 , 模式匹配 , 算法
      2025-05-14 10:33:31

      计算机小白的成长历程——数组(1)

      计算机小白的成长历程——数组(1)

      2025-05-14 10:33:31
      strlen , 个数 , 元素 , 内存 , 十六进制 , 地址 , 数组
      查看更多
      推荐标签

      作者介绍

      天翼云小翼
      天翼云用户

      文章

      33561

      阅读量

      5233481

      查看更多

      最新文章

      【Canvas技法】辐射式多道光影的实现

      2025-05-19 09:04:30

      外设驱动库开发笔记54:外设库驱动设计改进的思考

      2025-05-19 09:04:22

      C语言字符函数和字符串函数--(超全超详细)

      2025-05-19 09:04:14

      Redis Set集合

      2025-05-16 09:15:24

      如何将一串数字用函数的方法倒过来(C语言)

      2025-05-16 09:15:24

      【数据结构】详细介绍串的简单模式匹配——朴素模式匹配算法

      2025-05-14 10:33:31

      查看更多

      热门文章

      Python 函数调用父类详解

      2023-04-23 09:44:31

      python学习(6)——列表元素的添加、删除、修改及排序

      2023-05-22 03:00:29

      游戏编程之六 游戏编程的特点

      2024-09-25 10:13:46

      C#8.0新语法

      2023-02-07 10:34:04

      实现远程线程DLL注入

      2023-05-04 08:57:15

      C++ register关键字作用

      2023-03-08 10:27:24

      查看更多

      热门标签

      java Java python 编程开发 代码 开发语言 算法 线程 Python html 数组 C++ 元素 javascript c++
      查看更多

      相关产品

      弹性云主机

      随时自助获取、弹性伸缩的云服务器资源

      天翼云电脑(公众版)

      便捷、安全、高效的云电脑服务

      对象存储

      高品质、低成本的云上存储服务

      云硬盘

      为云上计算资源提供持久性块存储

      查看更多

      随机文章

      文心一言 VS 讯飞星火 VS chatgpt (59)-- 算法导论6.4 3题

      Python之装饰器

      Python 关键字

      文心一言 VS 讯飞星火 VS chatgpt (286)-- 算法导论21.2 5题

      Python算法学习[11]—图像问题&问题描述与实现

      外设驱动库开发笔记54:外设库驱动设计改进的思考

      • 7*24小时售后
      • 无忧退款
      • 免费备案
      • 专家服务
      售前咨询热线
      400-810-9889转1
      关注天翼云
      • 旗舰店
      • 天翼云APP
      • 天翼云微信公众号
      服务与支持
      • 备案中心
      • 售前咨询
      • 智能客服
      • 自助服务
      • 工单管理
      • 客户公告
      • 涉诈举报
      账户管理
      • 管理中心
      • 订单管理
      • 余额管理
      • 发票管理
      • 充值汇款
      • 续费管理
      快速入口
      • 天翼云旗舰店
      • 文档中心
      • 最新活动
      • 免费试用
      • 信任中心
      • 天翼云学堂
      云网生态
      • 甄选商城
      • 渠道合作
      • 云市场合作
      了解天翼云
      • 关于天翼云
      • 天翼云APP
      • 服务案例
      • 新闻资讯
      • 联系我们
      热门产品
      • 云电脑
      • 弹性云主机
      • 云电脑政企版
      • 天翼云手机
      • 云数据库
      • 对象存储
      • 云硬盘
      • Web应用防火墙
      • 服务器安全卫士
      • CDN加速
      热门推荐
      • 云服务备份
      • 边缘安全加速平台
      • 全站加速
      • 安全加速
      • 云服务器
      • 云主机
      • 智能边缘云
      • 应用编排服务
      • 微服务引擎
      • 共享流量包
      更多推荐
      • web应用防火墙
      • 密钥管理
      • 等保咨询
      • 安全专区
      • 应用运维管理
      • 云日志服务
      • 文档数据库服务
      • 云搜索服务
      • 数据湖探索
      • 数据仓库服务
      友情链接
      • 中国电信集团
      • 189邮箱
      • 天翼企业云盘
      • 天翼云盘
      ©2025 天翼云科技有限公司版权所有 增值电信业务经营许可证A2.B1.B2-20090001
      公司地址:北京市东城区青龙胡同甲1号、3号2幢2层205-32室
      • 用户协议
      • 隐私政策
      • 个人信息保护
      • 法律声明
      备案 京公网安备11010802043424号 京ICP备 2021034386号