C++寻位映射的奇幻密码:哈希

C++寻位映射的奇幻密码:哈希

文章目录

哈希,用于将任意大小的数据映射为固定长度的数值(哈希值),这个过程通过哈希函数实现,其核心目标是高效地存储、查找和验证数据

1.什么是哈希?

在这里插入图片描述

在学哈希之前,我们对于数据的查找通常是建立于顺序表,树形结构的基础上进行的查找,但是随着数据量越来越庞大,数据的随机性和容量越发严峻

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

因此就在此基础上发展出了一种平均时间复杂度几乎为 O(1) 的数据查找方式,哈希,也称为散列

2.哈希的常见实现方法

2.1 直接定址法

在这里插入图片描述

对于一段相对集中的数据段,就可以使用直接定址法,这里最大的数是 30,最小的数是15,创建一个大小为 15 的数组,将所有值映射到数组上

优点: 简单、均匀
缺点: 需要事先知道关键字的分布情况
使用场景: 适合查找比较小且连续的情况,数据太分散就不适合了,开的数组会太大,造成空间浪费

2.2 除留余数法

在这里插入图片描述

除留余数法是一种通过固定的哈希函数计算方式将数据放入哈希表的常用方法,设散列表中允许的地址数为 m,取一个不大于 m,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

3.哈希冲突

简单来说,通过除留余数法,将每个进来的值除以哈希表的大小得到的余数,必定是在所开哈希表的容器大小范围内的,但是有可能不同的 key 会除出相同的余数,造成同一位置的冲突,该种现象称为哈希冲突哈希碰撞

4.哈希冲突的解决

4.1 闭散列

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

4.1.1 线性探测

下面将通过对借助哈希表的实现解析线性探测相关的知识:

4.1.1.1 哈希表的基本数据结构
enumSTATE{ EXIST, EMPTY, DELETE };template<classK,classV>structHashData{ pair<K, V> _kv; STATE _state = EMPTY;};template<classK,classV,classHashFunc= DefaultHashFunc<K>>classHashTable{public: HashTable(){ _table.resize(10);}private: vector<HashData<K, V>> _table; size_t _n =0;// 存储有效数据的个数};

设置 _kv 存储实际的键值对数据,_state 跟踪该位置的状态

  • EXIST 表示位置已被占用(存在有效元素)
  • EMPTY 表示位置为空(从未被使用)
  • DELETE 表示位置已删除(曾被占用,现已删除)

为什么要用状态来表示呢?

因为用状态表示是最清晰直接的,有人说用零来表示已经删除的值不就好了,但是万一本身存储的值就是零呢?总而言之用状态表示是最方便的
4.1.1.2 哈希表的key转换
template<classK>structDefaultHashFunc{ size_t operator()(const K& key){return(size_t)key;}};template<>structDefaultHashFunc<string>{ size_t operator()(const string& str){// BKDR size_t hash =0;for(auto ch : str){ hash *=131; hash += ch;}return hash;}};

除留余数法一般是对整数进行操作,但是我们并不能保证 key 一定是整数,有人说直接强转不就好了,但是你能保证强转的数据一定是对的吗?可能是 double,也有可能是string,因此最好的方法是利用我们之前常用的仿函数进行统一操作

整数小数等就走默认的 DefaultHashFunc 类,当 keystring 类型的时候,就走特化的版本 DefaultHashFunc<string>,这里特化是为了统一性,不然你再造一个仿函数就太麻烦了

template<classK,classV,classHashFunc= DefaultHashFunc<K>>

将仿函数设为默认类型,这样子在创建例如 HashTable<string, string> dict 的哈希表的时候就不用显式写仿函数的类型

🔥值得注意的是: 这里的 string 特化版本的仿函数,进行了一些 BKDR 数学上的处理,假设有 "abc""bca" 两个字符串,这两个字符串其实是不一样的数据,如果没有进行 hash *= 131 的数据处理,那么这两个字符串的加和就是一样的,那么使用除留余数法的时候就会发生哈希冲突

具体分析可以看博客园里的大佬的分析:

传送门:字符串Hash函数对比
4.1.1.3 哈希表的插入
boolInsert(const pair<K, V>& kv){// 扩容//if ((double)_n / (double)_table.size() >= 0.7)if(_n *10/ _table.size()>=7){ size_t newSize = _table.size()*2;// 遍历旧表,重新映射到新表 HashTable<K, V, HashFunc> newHT; newHT._table.resize(newSize);// 遍历旧表的数据插入到新表即可for(size_t i =0; i < _table.size(); i++){if(_table[i]._state == EXIST){ newHT.Insert(_table[i]._kv);}} _table.swap(newHT._table);}// 线性探测 HashFunc hf; size_t hashi =hf(kv.first)% _table.size();while(_table[hashi]._state == EXIST){++hashi; hashi %= _table.size();} _table[hashi]._kv = kv; _table[hashi]._state = EXIST;++_n;returntrue;}

这里的插入,解决哈希冲突的方式利用的是线性探测的方式:

先对哈希函数计算值所带入的 key 进行处理,转换为合理的计算值,如果计算得出的位置为 EXIST,就依次往后探测,直到找到空位置为止,然后插入即可

那么哈希表满了之后的扩容是怎么一回事呢?

在这里插入图片描述

我们要知道判断一个哈希表是否应该开始扩容的标准是负载因子,通过 size / capacity 判断哈希表的填充程度,我们这里设置为 0.7 即扩容

既然扩容了,之前的数据就必须重新计算位置放入哈希表,不然关系就全乱了,或许会有人想为什么不直接新创建一个数组来放?而是创建一个新对象 HashTable<K, V, HashFunc> newHT,再来创建新哈希表,这是因为在对象里操作可以使用插入等便捷操作,使得新哈希表的创建更方便

🔥值得注意的是:

  • 计算位置时除 size,而不是 capacity,因为 size 直接反映了数组的有效长度, capacity 只是为创建更大的数组做准备的,[0, table_size-1] 是索引的合法范围
  • hashi %= _table.size() 是为了避免超出数组有效索引范围,只要大于 size 就会被除余回到数组第一个位置
  • 有人担心扩容会影响搜索效率,其实影响并不是很大,每次扩容都为之前的两倍,会比之前大很多,也就碰上扩容那一次效率不太高,整体来讲影响是不大的
4.1.1.4 哈希表的查找
HashData<const K, V>*Find(const K& key){// 线性探测 HashFunc hf; size_t hashi =hf(key)% _table.size();while(_table[hashi]._state != EMPTY){if(_table[hashi]._state == EXIST && _table[hashi]._kv.first == key){return(HashData<const K, V>*)& _table[hashi];}++hashi; hashi %= _table.size();}returnnullptr;}

循环条件为 _table[hashi]._state != EMPTY 是因为在插入的时候为空就必定插入,那么查找的过程中在找到新的空之前必定能找到想要的值(如果正确插入的话),if条件还必须加入 _table[hashi]._state == EXIST 是因为避免查找到的是已删除的值

🔥值得注意的是: 返回的是 HashData<const K, V>*,而不是 HashData< K, V>*,防止 key 被错误修改

4.1.1.5 哈希表的删除
boolErase(const K& key){ HashData<const K, V>* ret =Find(key);if(ret){ ret->_state = DELETE;--_n;returntrue;}returnfalse;}

删除可以说是我们学的那么多个结构里,比插入简单的了,直接删除修改状态即可

4.1.2 二次探测

二次探测的位置计算基于平方序列探测的,下面将给出详细的计算步骤:

  1. 核心计算公式

给定初始哈希位置 h₀ 和探测次数 i,下一个探测位置为:

h(i)=(h₀ + i²)% table_size 
  • h₀:初始哈希值(例如 hash(key) % table_size
  • i:探测次数,从 1 开始递增
  • table_size:哈希表的大小(必须为素数,否则可能无法覆盖所有槽位)
  1. 计算步骤示例

假设哈希表大小 table_size = 7(素数),初始哈希位置 h₀ = 3,插入时发生冲突,则二次探测的位置序列为:

探测次数 i计算公式结果 h(i)
1(3 + 1²) % 74
2(3 + 2²) % 70
3(3 + 3²) % 75
4(3 + 4²) % 72
5(3 + 5²) % 76
6(3 + 6²) % 71

序列:3405261,覆盖所有 7 个槽位

  1. 为什么表大小必须是素数?
    table_size 为合数,可能无法覆盖所有槽位。例如,当 table_size = 4(合数)时:
探测次数 i计算公式结果 h(i)
1(h₀ + 1²) % 4h₀ + 1
2(h₀ + 2²) % 4h₀
3(h₀ + 3²) % 4h₀ + 1

序列:h₀h₀+1h₀h₀+1,只能访问 2 个槽位,导致死循环

  1. 代码实现示例
boolInsert(const pair<K, V>& kv){// 扩容逻辑(略) HashFunc hf; size_t h0 =hf(kv.first)% _table.size();// 二次探测for(size_t i =1; i < _table.size();++i){ size_t hashi =(h0 + i * i)% _table.size();if(_table[hashi]._state != EXIST){ _table[hashi]._kv = kv; _table[hashi]._state = EXIST;++_n;returntrue;}}returnfalse;// 表满(实际不会触发,因提前扩容)}

4.1.3 线性探测和二次探测对比

特性线性探测二次探测
探测序列h₀, h₀+1, h₀+2, ...h₀, h₀+1, h₀+4, h₀+9, ...
聚集问题严重(主聚集)较轻(二次聚集)
空间利用率低(易导致连续槽位被占用)高(更均匀分布)
表满检测遍历全量槽位即可检测需遍历约一半槽位

4.2 开散列

4.2.1 哈希桶

在这里插入图片描述

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷,那么有没有办法不把数据只局限在数组里,有的兄弟有的,可以使用拉链法,也叫哈希桶,将数据以单链表的形式挂起来

4.2.1.1 哈希表的基本数据结构
template<classK,classV>structHashNode{ pair<K, V> _kv; HashNode<K, V>* _next;HashNode(const pair<K, V>& _kv):_kv(kv),_next(nullptr){}};template<classK,classV,classHashFunc= DefaultHashFunc<K>>classHashTable{typedef HashNode<K, V> Node;public:HashTable(){ _table.resize(10,nullptr);}~HashTable(){for(size_t i =0; i < _table.size(); i++){ Node* cur = _table[i];while(cur){ Node* next = cur->_next;delete cur; cur = next;} _table[i]=nullptr;}}private: vector<Node*> _table;// 指针数组 size_t _n =0;// 存储了多少个有效数据};

vector<Node*> 或者 vector<list> 都是可以的,节点都是指针需要释放,析构函数需要自己实现

4.2.1.2 哈希表的插入
boolInsert(const pair<K, V>& kv){if(Find(kv.first)){returnfalse;} HashFunc hf;// 负载因子到1就扩容if(_n == _table.size()){ size_t newSize = _table.size()*2; vector<Node*> newTable; newTable.resize(newSize,nullptr);// 遍历旧表,顺手牵羊,把节点牵下来挂到新表for(size_t i =0; i < _table.size(); i++){ Node* cur = _table[i];while(cur){ Node* next = cur->_next;// 头插到新表 size_t hashi =hf(cur->_kv.first)% newSize; cur->_next = newTable[hashi]; newTable[hashi]= cur; cur = next;} _table[i]=nullptr;} _table.swap(newTable);} size_t hashi =hf(kv.first)% _table.size();// 头插 Node* newnode =newNode(kv); newnode->_next = _table[hashi]; _table[hashi]= newnode;++_n;returntrue;}

由于每个哈希桶可以挂多个数据以节省空间,负载因子可以扩大到 1,平均下来一个桶一个数据

在这里插入图片描述

这里悬挂操作是以如图头插的方式进行的,在扩容时,把原先的桶挂到新表上的时候,由于是头插,原先的单链表会在新表上倒置,但是这不影响查找元素,每条桶的元素还是固定的,只是顺序不一样而已

4.2.1.3 哈希表的查找
Node*Find(const K& key){ HashFunc hf; size_t hashi =hf(key)% _table.size(); Node* cur = _table[hashi];while(cur){if(cur->_kv.first == key){return cur;} cur = cur->_next;}returnnullptr;}

当查找的节点为头节点时,prev为空,

4.2.1.4 哈希表的删除
boolErase(const K& key){ HashFunc hf; size_t hashi =hf(key)% _table.size(); Node* prev =nullptr; Node* cur = _table[hashi];while(cur){if(cur->_kv.first == key){if(prev ==nullptr){ _table[hashi]= cur->_next;}else{ prev->_next = cur->_next;}delete cur;returntrue;} prev = cur; cur = cur->_next;}returnfalse;}

当查找的节点为头节点时,prev 为空,直接让下一个节点成为头节点即可
当查找的节点为其他节点时,让前一个节点和下一个节点链接

记得释放删除的节点

4.3 开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销

事实上: 由于开放地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子 a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk