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/10/24 13:14:15 阅读更多 →
CSS padding属性深度解析:从盒模型原理到响应式布局实战

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

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

2026/10/25 7:37:48 阅读更多 →
运维转 Agent:Demo 跑通后,权限和日志才是真正翻车点

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

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

2026/10/23 19:50:42 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/26 12:00:21 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/25 22:36:10 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/26 4:52:12 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/26 12:00:21 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/26 5:00:37 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/26 3:16:46 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 12:00:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/25 13:21:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 8:34:37 阅读更多 →