C++修炼:unordered_map和unordered_set的使用和封装

article/2025/6/18 2:09:31

        Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客:<但凡.

我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》

欢迎点赞,关注!

        这一期我们来讲解并模拟实现unordered_map和unordered_set。有了前面的铺垫,这一篇就要相对简单很多了。

 

1、unordered_set和unordered_map的使用

        在C++标准模板库(STL)中,unordered_setunordered_map是两种非常重要的关联容器,它们基于哈希表实现,提供了高效的查找、插入和删除操作。本文将详细讲解这两种容器的特性、用法、内部实现原理。

      1.1、unordered_set的使用

        unordered_set是一个存储唯一元素的容器,其中每个元素的值同时也是它的键。

        我们把unordered_set的使用快速过一遍。因为在日常使用中,unordered_set的使用和set的使用很相似。接口都是类似的。

#include <unordered_set>
#include <iostream>int main() {// 创建unordered_setstd::unordered_set<int> numbers = {1, 2, 3, 4, 5};// 插入元素numbers.insert(6);numbers.emplace(7);  // 更高效的插入方式//下两篇会详细讲解emplace接口// 查找元素if (numbers.find(3) != numbers.end()) {std::cout << "3 found in the set\n";}// 删除元素numbers.erase(4);// 遍历元素for (const auto& num : numbers) {std::cout << num << " ";}std::cout << "\n";// 获取大小std::cout << "Size: " << numbers.size() << "\n";return 0;
}

         unordered_set的主要特性:

                唯一性:容器中不允许有重复的元素

                无序性:元素不以任何特定顺序存储

                哈希实现:基于哈希表实现,平均情况下提供O(1)的查找复杂度

                动态大小:容器可以根据需要动态扩展或收缩

        我们可以把unordered_set理解成不能排序的set,并且unordered_set的插入,删除,查找平均时间复杂度都是O(1),效率比set要高。

         1.2、unordered_map的使用

        unordered_map是存储键值对的关联容器,其中每个键都是唯一的。与map不同unordered_map中的元素不以任何特定顺序排序。

        操作我们也不细讲了,因为常用接口和map的接口都差不多。

#include <unordered_map>
#include <iostream>
#include <string>int main() {// 创建unordered_mapstd::unordered_map<std::string, int> word_counts = {{"apple", 5},{"banana", 3}};// 插入元素word_counts.insert({"orange", 2});word_counts["pear"] = 4;  // 使用下标操作符插入// 访问元素std::cout << "apple count: " << word_counts["apple"] << "\n";// 查找元素auto it = word_counts.find("banana");if (it != word_counts.end()) {std::cout << "banana count: " << it->second << "\n";}// 删除元素word_counts.erase("orange");// 遍历元素for (const auto& pair : word_counts) {std::cout << pair.first << ": " << pair.second << "\n";}// 获取大小std::cout << "Size: " << word_counts.size() << "\n";return 0;
}

        unordered_map的主要特性:

                键唯一性:每个键只能在容器中出现一次

                无序性:键值对不以任何特定顺序存储

                哈希实现:基于哈希表实现,平均情况下提供O(1)的访问复杂度

                快速访问:通过键可以直接访问对应的值

               如果我们想要支持自定义类型的比较和存储,需要让自定义类型支持哈希映射和==的比较。unordered_map和unordered_set同理。

#include<iostream>
#include<unordered_map>
#include<unordered_set>
using namespace std;struct node
{int _a;int _b;bool operator==(const node& x) const{return (_a == x._a) && (_b == x._b);}
};class A
{
public://需要注意仿函数的重载()一定是public:size_t operator()(const node& x) const{int ret = x._a;ret *= 131;ret += x._b;ret *= 131;return ret;}
};int main()
{unordered_set<node, A> s;unordered_map<node, string, A> mp;s.insert({ 1,1 });s.insert({ 2,2 });node x = { 1,1 };mp[{1, 2}] = "11";node x1 = { 2,3 };mp.insert({x1, "ss"});//传入pair(key,value)return 0;
}

 2、unordered_map和unordered_set的封装

        基于我们上节课实现的哈希表,我们对unordered_set和unordered_map进行封装。需要注意的是我们使用的是链地址法进行封装,因为stl中的unordered_map和unordered_set也是链地址法。

        2.1、KeyOfT

        上期我们的哈希表查找key值,我们是通过直接访问kv.first来实现的。但是由于封装的时候我们想让unordered_set和unordered_map用同一个哈希表,所以我们对哈希表的模板参数中传入KeyOfT,然后在访问key值时使用KeyOfT的方式:

template<class K,class T,class KeyOfT,class Hash>

        这个KeyOfT是跟着unordered_set和unordered_map的时候写的,所以等我们进一步封装的时候再去实现KeyOfT。 

         2.2、迭代器

        我们对上一次的哈希表进行升级,是他支持迭代器:

template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>
struct HTIterator
{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;Node* _node;const HT* _pht;HTIterator(Node* node,const HT* pht):_node(node),_pht(pht){}Self& operator++(){if (_node->_next){_node = _node->_next;}else{//根据哈希表找到下一个下挂节点的地方KeyOfT kot;Hash hs;size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();hashi++;while (hashi < _pht->_tables.size()){if (_pht->_tables[hashi]){_node = _pht->_tables[hashi];break;}//这个地方和线性探测不一样,不要让他成环++hashi;}if (hashi == _pht->_tables.size()){_node = nullptr;//如果跳出循环发现这个位置根本不存在说明没有下一个节点了}//注意这是前置++return *this;}}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const Self& s) const{return  _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;//比较地址}
};

        我们模板参数有6个,K,T是存储类型不多说了,和我们哈希表的一样,ref是引用,ptr是指针,大家应该也都知道,keyOfT是提取key值,hash是哈希映射函数

        对于operator++,首先如果这个节点是链中的一个节点,并且他还有下一个节点,那我们直接加加就可以了。如果没有,那么我们就去找下一个链。首先我们通过哈希哈希函数找到当前节点在的链子,记作hashi。然后把hashi加加跳过当前的链子,之后一直往后遍历知道找到下一个链子的位置。

        接下来我们让哈希表支持迭代器。

	template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>friend struct HTIterator;typedef HashNode<T> Node;
public:typedef HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;typedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;Iterator Begin(){if (_n == 0)return End();for (size_t i = 0;i < _tables.size();i++){if (_tables[i]){return Iterator(_tables[i],this);}}return End();}Iterator End(){return Iterator(nullptr,this);}ConstIterator Begin() const{if (_n == 0)return End();for (size_t i = 0;i < _tables.size();i++){if (_tables[i]){return ConstIterator(_tables[i], this);}}return End();}ConstIterator End() const{return ConstIterator(nullptr, this);}

        因为我们在写迭代器类的时候用了hashTable中的私有成员变量,所以要在hashTable中对迭代器类进行友元声明。 

        对于begin来说,我们就通过遍历,找到第一个出现值的位置就行了,如果走到最后了都没出现值,那说明我们当前哈希表是空的,直接返回end(),而end()实际上是返回一个nullptr构造的迭代器。

      2.3、插入

        我们对插入操作改造升级,使他和源码的返回值相同:

pair<Iterator, bool> Insert(const T& data)
{KeyOfT kot;Iterator it = Find(kot(data));if (it != End())return { it,false };Hash hs;if (_n == _tables.size()){//扩容//HashTable<K, V> newht(__stl_next_prime(_tables.size() + 1));遍历旧表,将旧表的数据全部重新映射到新表//for (size_t i = 0; i < _tables.size(); i++)//{//	Node* cur = _tables[i];//	while (cur)//	{//		newht.Insert(cur->_kv);//		cur = cur->_next;//	}//}//_tables.swap(newht._tables);//我们不采用上面的方式,因为上面的方式每个节点都得拷贝一次vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);for (size_t 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);//浅交换}size_t hashi = hs(kot(data)) % _tables.size();Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return { Iterator(newnode,this),true };
}

        这段内容没什么难点,大家看看就好了。 

         2.4、unordered_map的封装

        现在我们新建一个头文件unordered_map.h,然后写一下unordered_map的类:

#include"HashTable.h"template<class K,class V,class hash=HashFunc<K>>
class unordered_map
{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv) const //仿函数记得加const{return kv.first;}};
public:typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, hash>::Iterator iterator;typedef typename hash_bucket::HashTable< K, pair<const K, V>, MapKeyOfT, hash>::ConstIterator const_iterator;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){return _ht.Find(key);}bool erase(const K& key){return _ht.Erase(key);}V& operator[](const K& key){pair<iterator, bool> ret = insert({ key,V() });return ret.first->second;}
private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, hash> _ht;
};

        由于我们要保证unordered_map中存储的值的key值不被修改,所以哈希表的私有变量我们第二个传入的模版参数是pair<const K,V>。

        2.5、unordered_set的封装

        说实话这也没啥好说的,大家看看就好。


#include"HashTable.h"
template<class K,class Hash=HashFunc<K>>
class unordered_set
{struct SetKeyOfT{const K& operator()(const K& key){return key;}};
public://注意迭代器都是大类里面封装好的迭代器,不能拿最开始定义的那个类的迭代器typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;iterator begin(){return _ht.Begin();}iterator end(){return _ht.Begin();}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);}private:hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;};

3、总结 

        unordered_set经常用于去重以及存在性检查。unordered_map常用于快速键值查找以及频率统计。在后续做算法题时,经常会用到这两个容器。

特性unordered_setunordered_map
存储内容仅 KeyKey-Value 对
哈希冲突处理链地址法链地址法
时间复杂度平均 O(1),最坏 O(n)平均 O(1),最坏 O(n)
动态扩容需要需要
迭代器支持可以自定义可以自定义

        好了,今天的内容就分享到这,我们下期再见! 

 


http://www.hkcw.cn/article/CcFWoUQoPo.shtml

相关文章

Centos环境下安装/重装MySQL完整教程

目录 一、卸载残留的MySQL环境&#xff1a; 二、安装MySQL&#xff1a; 1、下载MySQL官方的yum源&#xff1a; 2、更新系统yum源&#xff1a; 3、确保系统中有了对应的MySQL安装包&#xff1a; 4、安装MySQL服务&#xff1a; 5、密钥问题安装失败解决方法&#xff1a; …

【机器学习基础】机器学习入门核心算法:决策树(Decision Tree)

机器学习入门核心算法&#xff1a;决策树&#xff08;Decision Tree&#xff09; 一、算法逻辑1.1 基本概念1.2 算法流程 二、算法原理与数学推导2.1 特征选择指标信息熵&#xff08;ID3算法&#xff09;信息增益&#xff08;Information Gain&#xff09;信息增益率&#xff0…

基于晶体塑性有限元(CPFEM)的钛合金圆棒拉伸过程模拟

作者&#xff1a;辞殇 关键词&#xff1a;CPFEM&#xff1b;钛合金&#xff1b;单轴拉伸&#xff1b;织构极图&#xff1b;孪晶 晶体塑性有限元是一种结合了晶体塑性理论和有限元方法的数值模拟技术‌。这种方法考虑了晶体材料的各向异性、滑移系统的开动和相互作用、以及变形…

开源是什么?我们为什么要开源?

本片为故事类文章推荐听音频哦 软件自由运动的背景 梦开始的地方 20世纪70年代&#xff0c;软件行业处于早期发展阶段&#xff0c;软件通常与硬件捆绑销售&#xff0c;用户对软件的使用、修改和分发权利非常有限。随着计算机技术的发展和互联网的普及&#xff0c;越来越多的开…

帕金森带来的生活困境

当这种健康状况出现&#xff0c;行动不再自如成为最明显的改变。日常行走时&#xff0c;步伐会逐渐变小、变慢&#xff0c;甚至会出现 “小碎步” 往前冲&#xff0c;难以停下&#xff0c;简单的起身、转身都可能变得艰难。手部也会不受控制地颤抖&#xff0c;拿水杯、系纽扣这…

第3期:PCB设计教程:自动布线与导出制版文件详解

第3期&#xff1a;PCB设计教程&#xff1a;自动布线与导出制版文件详解 一、前言 本篇教程主要聚焦于PCB设计中的自动布线功能及文件导出步骤。通过本教程&#xff0c;您将学习如何&#xff1a; 使用自动布线工具高效完成线路连接处理自动布线失败的情况进行DRC检查确保设计…

NACOS 动态配置

1.引入Nacos 配置中心依赖 <!-- nacso 配置中心--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency> 2.在application.properties 配置…

【清晰教程】查看和修改Git配置情况

目录 查看安装版本 查看特定配置 查看全局配置 查看本地仓库配置 设置或修改配置 查看安装版本 打开命令行工具&#xff0c;通过version命令检查Git版本号。 git --version 如果显示出 Git 的版本号&#xff0c;说明 Git 已经成功安装。 查看特定配置 如果想要查看特定…

C语言 — 动态内存管理

目录 1.malloc和free函数1.1 malloc函数1.2 free函数1.3 malloc函数的使用 2.calloc函数2.1 calloc函数2.2 calloc函数的使用 3.realloc函数3.1 realloc函数3.2 realloc函数的使用 4.动态内存管理笔试题4.1 笔试题&#xff08;1&#xff09;4.2 笔试题&#xff08;2&#xff09…

动态规划算法

简称 DP&#xff0c;是一种求解多阶段决策过程最优化问题的方法。在动态规划中&#xff0c;通过把原问题分解为相对简单的子问题&#xff0c;先求解子问题&#xff0c;再由子问题的解而得到原问题的解。 一、概念 动态规划最早由理查德 贝尔曼于 1957 年在其著作「动态规划&…

Qt -使用OpenCV得到SDF

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【暂无】 欢迎点赞&#x1f44d;收藏⭐关注❤️ 目录 cv::MatdistanceTransform获得SDF 本文的目标&#xff0c; 是简单学习并使用OpenCV的相关函数&#xff0c; 并获得QImage的SDF(Signed Distance Field 有向距离场) 至…

【小米拥抱AI】小米开源 MiMo-7B-RL-0530

更新日志 [2025.05.30] 在强化学习训练过程中&#xff0c;通过持续扩大训练窗口尺寸&#xff08;从32K提升至48K&#xff09;&#xff0c;MiMo-7B-RL-0530模型在AIME24基准测试上的表现持续提升&#xff0c;最终超越DeepSeek R1模型的性能水平。 BenchmarkMiMo-7B-RLMiMo-7B-…

俄布良斯克州桥梁坍塌致列车脱轨事故造成3死28伤

△图片来源:莫斯科交通检察院总台记者当地时间6月1日获悉,据俄罗斯紧急情况部初步统计,布良斯克州桥梁坍塌致火车脱轨事故共造成31人伤亡,其中3人不幸遇难,28人已送往医疗机构救治。此前据俄罗斯BAZA网站报道,事件造成4人死亡,至少44人受伤。俄紧急情况部称,救援人员正…

JDK17 与JDK8 共同存在一个电脑上

官网下载JDK17 官网链接 &#xff1a;https://www.oracle.com/java/technologies/downloads/#java17-windows 下载这个 安装 环境变量设置 因为之前设置过JDK 8这里为了使 两者共存&#xff0c;采用设置变量方式来实现具体操作如下 1、进入高级系统环境设置 1.1先建一个关…

打开NRODIC SDK编译不过怎么处理,keil与segger studio

打开NRODIC SDK编译不过怎么处理,以下是keil处理. 1,如图,不要安装安装也不会过 2. 不要安装点击否 3.点击确定后进来这个样子 4.这里选择这个勾,OK后就不会再有后面的pack_license 5.去掉勾后这里要选择自己SDK对应的pack版本,我的是8.27.0 6.OK后弹出个界面也要反复选择…

每日八股文5.31

每日八股-5.31 Go1.切片是值传递还是引用传递&#xff1f;2.切片的深拷贝与浅拷贝3.切片的底层实现4.切片的扩容机制5.Map是线程安全的吗&#xff1f;6.哪些类型可以作为map的key&#xff1f;7.Map删除一个key内存是否会释放&#xff1f;8.Map为什么是无序的&#xff1f;9.如何…

智能重塑连接:AI原生互联网的范式革命与未来十年

引言:互联网的下一幕——智能涌现与体验重塑 2024年初,OpenAI发布的文生视频模型Sora,以其惊人的逼真度和对物理世界的理解能力,再次将人工智能的魔力推向了全球聚光灯下。这不仅仅是一个技术演示,更像是一个强烈的信号:我们正加速驶向一个由AI深度重塑的未来。回望互联…

【深度学习相关安装及配环境】Anaconda搭建虚拟环境并安装CUDA、cuDVV和对应版本的Pytorch,并在jupyter notebook上部署

目录 1. 查看自己电脑的cuda版本2.安装cuda关于环境变量的配置测试一下&#xff0c;安装完成 3.安装cuDVV环境变量的配置测试一下&#xff0c;安装完成 4.创建虚拟环境先安装镜像源下载3.11版本py 5.在虚拟环境下&#xff0c;下载pytorch6.验证是否安装成功7.在jupyter noteboo…

2. 手写数字预测 gui版

2. 手写数字预测 gui版 背景1.界面绘制2.处理图片3. 加载模型4. 预测5.结果6.一点小问题 背景 做了手写数字预测的模型&#xff0c;但是老是跑模型太无聊了&#xff0c;就配合pyqt做了一个可视化界面出来玩一下 源代码可以去这里https://github.com/Leezed525/pytorch_toy拿 …

用JS实现植物大战僵尸(前端作业)

1. 先搭架子 整体效果&#xff1a; 点击开始后进入主场景 左侧是植物卡片 右上角是游戏的开始和暂停键 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevic…