C++ 模拟实现 unordered_map 与 unordered_set
一、哈希表相关概念与特性总结1. 什么是哈希表哈希表散列表通过哈希函数把关键字 key 映射到数组下标位置直接访问元素理想情况下查找、插入、删除时间复杂度接近 \(O(1)\)。数组叫做哈希桶数组数组每个位置叫桶 (bucket)。 公式hash(key) 下标2. 哈希冲突不同的 key 经过哈希函数计算得到同一个数组下标就是哈希冲突。 不可能设计完美哈希函数冲突不可避免。常见解决冲突方案开放定址法冲突就找下一个空位置线性探测、二次探测。缺点删除麻烦容易堆积冲突。链地址法 (哈希桶)STLunordered_xx使用的方案。 每个桶里面挂一条链表冲突的元素直接链在同一个桶的链表上。3. 负载因子 load_factor\(负载因子 \frac{有效元素个数}{桶数组总大小}\)负载因子越大冲突概率越高链表越长效率下降。STL 中unordered容器负载因子阈值默认是1超过阈值就要扩容rehash。rehash 扩容做两件事1. 开辟更大的桶数组一般找下一个质数质数降低冲突 2. 遍历旧表全部元素重新计算哈希值搬迁到新数组释放旧空间。注意rehash 之后元素迭代器会失效。4. 哈希函数与仿函数C 内置类型int、stringSTL 提供默认哈希仿函数hashT。 自定义类型必须自己提供哈希仿函数把对象转成 size_t 整数。5.unordered_set 与 unordered_map 对比表格容器存储内容key 是否唯一key 是否可修改unordered_set只存 keykey 唯一key 不能修改unordered_map存 pairKey,Tkey 唯一key 不能修改value 可修改unordered_set相当于unordered_map只使用 keyvalue 无意义。所以 STL 内部unordered_set和unordered_map共用同一套哈希表底层只是传入的数据类型不同。6. 特性总结无序元素不排序遍历顺序和插入顺序无关key 不允许重复底层链地址哈希桶平均\(O(1)\)增删查最坏退化\(O(n)\)全部冲突挂一条链表迭代器不是双向迭代器不支持/--的有序遍历不支持lower_boundrehash 会导致迭代器失效二、模拟实现整体思路STL 源码思路封装一个通用哈希表HashTable。unordered_setK底层哈希表存Kvalue 就是 keyunordered_mapK,V底层哈希表存pairconst K,V核心组件哈希节点链表节点存数据 next 指针哈希桶数组vectorNode*仿函数 1取 key 仿函数 —— 从存储的元素中取出 keyset 取本身map 取 pair.first仿函数 2哈希函数仿函数 —— key 转 size_t 下标仿函数 3相等比较仿函数 —— 判断两个 key 是否相等哈希值相等不代表 key 相等必须判等核心接口insert、find、erase、rehash、operator []map 独有1. 哈希节点定义链地址templateclass T struct HashNode { T _data; HashNode* _next; HashNode(const T data) :_data(data), _next(nullptr) {} };T对于 set 就是K对于 map 就是pairconst K,V。2. 三个关键仿函数模拟实现重点① 获取 key 仿函数GetKey同一个 HashTable 要同时给 set 和 map 用存储的数据类型不一样需要从存储对象拿到 key。// unordered_set数据就是key直接返回 templateclass K struct SetGetKey { const K operator()(const K key) { return key; } }; // unordered_map存储pair返回pair.first templateclass K,class V struct MapGetKey { const K operator()(const pairconst K,V kv) { return kv.first; } };② HashFunc 哈希仿函数key 转 size_t 下标内置类型直接强转字符串需要特殊处理。// 针对int templateclass K struct HashFunc { size_t operator()(const K key) { return (size_t)key; } }; // string特化哈希 template struct HashFuncstring { size_t operator()(const string s) { size_t hash 0; for(auto ch : s) { hash hash * 131 ch; } return hash; } };自定义类型使用者需要自己写 HashFunc 仿函数。③ equal 相等比较哈希值相同 ≠ key 相同。同一个桶链表遍历的时候必须用 key 判断是否相等不能只对比哈希值。3. 通用哈希表模板设计templateclass T, class KeyOfT, class Hash class HashTable { public: typedef HashNodeT Node; private: vectorNode* _buckets; //哈希桶数组 size_t _size; //有效元素个数 //...接口 };T容器存储元素set 为 Kmap 为 pairconst K,VKeyOfT仿函数从 T 取出 keyHash哈希仿函数key→size_t4. 核心接口要点分析(1) insert 插入步骤先调用find(key)key 已经存在直接返回key 唯一判断负载因子_size / _buckets.size() 1执行 rehash 扩容使用哈希函数算出哈希值对桶数组大小取模得到桶下标size_t idx Hash()(key) % _buckets.size();new 新节点头插法插入该桶的链表STL 旧版本头插新版本有些改动_size注意扩容要处理空桶桶数组不能为 0初始给一个最小容量。(2) rehash 扩容要点开辟新的 vector 桶数组容量选质数减少冲突遍历旧桶数组每一个桶遍历桶内整条链表每个旧节点用新桶大小重新计算下标摘节点头插到新桶旧 vector 生命周期结束自动释放不要重新 new 节点直接搬迁节点不拷贝对象移动节点指针效率高。❗坑不要重新 new 节点搬迁原有节点否则拷贝代价巨大。(3) find 查找key 计算下标找到对应的桶遍历该桶下链表调用KeyOfT()拿到节点数据的 key和目标 key 比较找到返回节点指针找不到返回 nullptr只在同一个桶内遍历不用遍历整个哈希表。(4) erase 删除find 找到节点同时保存前驱节点链表删除节点释放节点_size--链地址法删除简单不需要像开放定址法做标记。(5) operator [] 仅 unordered_map 拥有map[key]语义key 存在返回 value 引用不存在就插入默认构造的 pair。 底层调用insert拿到迭代器返回it-second引用。 set 不需要operator[]因为没有 value。5. 模拟实现 unordered_set包装层内部聚合 HashTable把参数传递给底层哈希表。templateclass K, class Hash HashFuncK class MyUnorderedSet { public: //底层哈希表存储类型TK取key仿函数SetGetKey bool insert(const K k) { return _ht.Insert(k); } bool find(const K k) { return _ht.Find(k) ! nullptr; } bool erase(const K k) { return _ht.Erase(k); } private: HashTableK, SetGetKeyK, Hash _ht; };6. 模拟实现 unordered_maptemplateclass K, class V, class Hash HashFuncK class MyUnorderedMap { public: bool insert(const pairconst K,V kv) { return _ht.Insert(kv); } V operator[](const K key) { pairdecltype(_ht.begin()),bool ret _ht.Insert({key,V()}); return ret.first-_data.second; } bool erase(const K key) { return _ht.Erase(key); } private: HashTablepairconst K,V, MapGetKeyK,V, Hash _ht; };关键点map 存储pairconst K,Vkey 是 const防止用户修改 key一旦修改 key 哈希值失效整个哈希表结构错乱。三、重点难点梳理博客重点1. 为什么 unordered_set 和 unordered_map 可以复用同一个 HashTable存储的数据类型 T 不同set 存 Kmap 存 pair。通过仿函数 KeyOfT 做解耦统一从存储对象拿到 key不需要写两份哈希表代码。这是 STL 泛型编程精髓。2. 哈希冲突处理链地址法优缺点✅优点删除简单没有堆积问题rehash 直接搬迁节点不需要拷贝数据。 ❌缺点需要额外开辟节点有指针开销极端冲突退化成链表查找\(O(n)\)。3. 仿函数三处用途总结KeyOfT从存储的 T 类型提取 key隔离 set/map 存储差异。Hash把任意 key 转成 size_t 哈希整数自定义类型必须提供。equal 比较哈希值相同不等于 key 相同链表遍历必须真实 key 判等。4.rehash 容易踩坑不是简单扩大两倍STL 选用质数做桶大小降低冲突概率。rehash 是移动节点不是拷贝数据避免拷贝大对象。rehash 后迭代器全部失效因为节点被搬到不同桶。5.key 不能修改的原因哈希表所有位置依靠 key 的哈希值定位。 一旦 key 被修改节点实际存储位置和计算出的下标不匹配find 永远找不到元素。 所以unordered_map的 pair 的 first 是const Kset 的 key 也是不可修改。四、完整可运行简易代码汇总#includeiostream #includevector #includestring using namespace std; //哈希节点 templateclass T struct HashNode { T _data; HashNode* _next; HashNode(const T data):_data(data),_next(nullptr){} }; //set取key仿函数 templateclass K struct SetGetKey { const K operator()(const K k){return k;} }; //map取key仿函数 templateclass K,class V struct MapGetKey { const K operator()(const pairconst K,V kv){return kv.first;} }; //哈希仿函数 templateclass K struct HashFunc { size_t operator()(const K key){return (size_t)key;} }; template struct HashFuncstring { size_t operator()(const string s) { size_t hash0; for(auto c:s) hashhash*131c; return hash; } }; //通用哈希表 templateclass T,class KeyOfT,class Hash class HashTable { public: typedef HashNodeT Node; HashTable():_size(0) { _buckets.resize(10,nullptr); } Node* Find(const KeyOfT key) { Hash hf; KeyOfT getkey; size_t idxhf(key)%_buckets.size(); Node* cur_buckets[idx]; while(cur) { if(getkey(cur-_data)key) return cur; curcur-_next; } return nullptr; } bool Insert(const T data) { KeyOfT getkey; auto keygetkey(data); if(Find(key)) return false; //负载因子1就扩容 if(_size _buckets.size()) { size_t newCap _buckets.size()*2; vectorNode* newBuckets(newCap,nullptr); for(size_t i0;i_buckets.size();i) { Node* cur_buckets[i]; while(cur) { Node* nextcur-_next; size_t idxHash()(getkey(cur-_data))%newCap; cur-_nextnewBuckets[idx]; newBuckets[idx]cur; curnext; } } _buckets.swap(newBuckets); } size_t idxHash()(key)%_buckets.size(); Node* newNodenew Node(data); newNode-_next_buckets[idx]; _buckets[idx]newNode; _size; return true; } bool Erase(const KeyOfT key) { Hash hf; KeyOfT getkey; size_t idxhf(key)%_buckets.size(); Node* cur_buckets[idx]; Node* prevnullptr; while(cur) { if(getkey(cur-_data)key) { if(prevnullptr) { _buckets[idx]cur-_next; } else { prev-_nextcur-_next; } delete cur; _size--; return true; } prevcur; curcur-_next; } return false; } private: vectorNode* _buckets; size_t _size; }; //模拟unordered_set templateclass K,class HashHashFuncK class MyUnorderedSet { public: bool insert(const K k) { return _ht.Insert(k); } bool find(const K k) { return _ht.Find(k)!nullptr; } bool erase(const K k) { return _ht.Erase(k); } private: HashTableK,SetGetKeyK,Hash _ht; }; //模拟unordered_map templateclass K,class V,class HashHashFuncK class MyUnorderedMap { public: bool insert(const pairconst K,V kv) { return _ht.Insert(kv); } V operator[](const K key) { _ht.Insert({key,V()}); Node* node_ht.Find(key); return node-_data.second; } bool erase(const K key) { return _ht.Erase(key); } private: typedef HashNodepairconst K,V Node; HashTablepairconst K,V,MapGetKeyK,V,Hash _ht; }; //测试 int main() { MyUnorderedSetint s; s.insert(1); s.insert(3); couts.find(3)endl; s.erase(3); couts.find(3)endl; MyUnorderedMapstring,int mp; mp[apple]100; coutmp[apple]endl; return 0; }五、博客小结unordered_set、unordered_map底层是链地址法哈希表核心解决哈希冲突。STL 使用泛型 仿函数实现代码复用同一个 HashTable 支撑 set 与 map。三个仿函数分工提取 key、哈希转换、key 判等是模拟实现的核心难点。rehash 扩容、负载因子、key 禁止修改都是高频考点。

相关新闻

AI产品工程实践:从快速迭代到全盘规划的策略选择

AI产品工程实践:从快速迭代到全盘规划的策略选择

1. 先想清楚,你的AI产品到底要解决什么问题做AI产品,最怕一上来就纠结“快速迭代”还是“全盘规划”。这就像盖房子,还没想好是盖个临时工棚还是百年住宅,就开始争论是用预制板快,还是打地基稳。方向错了,跑…

2026/8/16 2:58:50 阅读更多 →
CSS padding属性深度解析:从盒模型原理到响应式布局实战

CSS padding属性深度解析:从盒模型原理到响应式布局实战

1. 项目概述:为什么我们总在“盒子里”纠结 padding?如果你写过 CSS,那你一定和padding这个属性打过交道。它看起来很简单,不就是给元素加点内边距嘛。但在我十多年的前端开发生涯里,见过太多因为对padding理解不透彻而…

2026/8/16 2:58:50 阅读更多 →
运维转 Agent:Demo 跑通后,权限和日志才是真正翻车点

运维转 Agent:Demo 跑通后,权限和日志才是真正翻车点

聊《我用运维经验做了次 AI 项目,最先失效的是旧方法》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。摘要摘要:从自动化脚本到 AIOps Agent,很多运维工程师以为换个工具就能上手…

2026/8/16 2:58:50 阅读更多 →

最新新闻

检测机构查询小程序众多,哪家才是你的最优之选?

检测机构查询小程序众多,哪家才是你的最优之选?

检测机构查询小程序推荐榜单 在当今工业自动化快速发展的时代,无论是 PLC控制器、伺服驱动 等设备的生产企业,还是从事 智能制造、工业物联网 相关业务的公司,都经常需要查询检测机构的信息。我在电子电气领域摸爬滚打多年,接触过…

2026/8/16 3:56:09 阅读更多 →
一家工程机械厂的AI获客30天实录:从零召回到18次被提到

一家工程机械厂的AI获客30天实录:从零召回到18次被提到

株洲有一家做工程机械零部件外协的工厂,设备投了百来万,干的活不差,客户也稳定。但有个事一直让老板心里不踏实——新客户越来越难找了。项目启动前,团队先做了一次摸底:在四个主流AI工具里,用采购方真实会…

2026/8/16 3:56:09 阅读更多 →
一次请求的前世今生(进阶):底层原理与生产环境那些坑

一次请求的前世今生(进阶):底层原理与生产环境那些坑

一次请求的前世今生(进阶):底层原理与生产环境那些坑 上一篇《Go请求链路(入门)》捋清了主线。这一篇往下挖一层,讲几个生产环境真会踩的坑,以及 c.Next() / c.Abort() 的底层实现。 建议先看完…

2026/8/16 3:56:09 阅读更多 →
用 HackRF + GNU Radio 制作 FM 发射机:从 WAV 到 103 MHz 空中信号

用 HackRF + GNU Radio 制作 FM 发射机:从 WAV 到 103 MHz 空中信号

用 HackRF GNU Radio 制作 FM 发射机:从 WAV 到 103 MHz 空中信号作者:charlie 关键词:HackRF、GNU Radio 3.10、SoapySDR、analog_wfm_tx、FM 广播调制、采样率链摘要 本文记录如何用一块 HackRF One/Pro 配合 GNU Radio,把一个…

2026/8/16 3:56:09 阅读更多 →
C语言函数递归详解:从核心要素到实战案例

C语言函数递归详解:从核心要素到实战案例

1. 什么是递归 递归是编程中的一种技术&#xff0c;指的是一个函数在其定义内部调用自身。递归一定是依赖于函数的。 史上最简单的递归程序&#xff1a; #include <stdio.h> int main() {printf("hehe\n");main(); //main函数自己调用自己return 0; }这个程序是…

2026/8/16 3:56:09 阅读更多 →
LangGraph实战:构建有状态AI智能体工作流,解决复杂流程编排痛点

LangGraph实战:构建有状态AI智能体工作流,解决复杂流程编排痛点

1. 先搞清楚 LangGraph 到底解决了什么 Agent 开发痛点如果你正在用 LangChain 或者类似的框架做 AI 应用&#xff0c;尤其是涉及多步骤、有状态、需要协作的智能体&#xff08;Agent&#xff09;&#xff0c;大概率会遇到几个头疼的问题&#xff1a;任务流程一复杂&#xff0c…

2026/8/16 3:55:09 阅读更多 →

日新闻

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

如果你是一名开发者&#xff0c;最近可能已经感受到了AI大模型正在从“玩具”变成“生产力工具”的强烈信号。从代码补全到智能Agent&#xff0c;从本地部署到云端API&#xff0c;我们正处在一个技术栈快速重构的节点。然而&#xff0c;面对层出不穷的模型、框架和工具&#xf…

2026/8/16 0:00:54 阅读更多 →
工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/16 0:00:55 阅读更多 →
【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。&#x1f34e; 往期回顾关注个人主页&#xff1a;Matlab科研工作室&#x1f447; 关注我领取海量matlab电子书和…

2026/8/16 0:03:55 阅读更多 →

周新闻

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

如果你是一名开发者&#xff0c;最近可能已经感受到了AI大模型正在从“玩具”变成“生产力工具”的强烈信号。从代码补全到智能Agent&#xff0c;从本地部署到云端API&#xff0c;我们正处在一个技术栈快速重构的节点。然而&#xff0c;面对层出不穷的模型、框架和工具&#xf…

2026/8/16 0:00:54 阅读更多 →
工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/16 0:00:55 阅读更多 →
【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。&#x1f34e; 往期回顾关注个人主页&#xff1a;Matlab科研工作室&#x1f447; 关注我领取海量matlab电子书和…

2026/8/16 0:03:55 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速&#xff1a;macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/14 13:40:53 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南&#xff1a;3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗&#xff1f;ncmdump解密工具帮你轻松解决这个困…

2026/8/14 14:06:45 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片&#xff1a;为英语学习 App 打造桌面级学习助手适用平台&#xff1a;HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0&#xff08;API 26 Beta&#xff09;新增了 AgentCard 智能体卡片能力&#xff0c;这是继 HMAF&#xff08;鸿蒙智能体框架&#x…

2026/8/15 2:35:29 阅读更多 →