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

依托Java和百度地图实现长沙市热门道路与景点实时路况检索的实践探索

依托Java和百度地图实现长沙市热门道路与景点实时路况检索的实践探索

目录 前言 一、实时路况服务简介 1、实时路况服务是什么 2、道路实时路况查询 3、周边实时路况查询 4、返回参数 二、Java响应对象封装 1、响应对象设计 2、响应对象实现 三、UniHttp集成及调用 1、检索接口声明 2、道路实时路况查询 3、周边实时路况查询 四、常见问题 1、道路名称错误 2、中心点坐标位置错误 3、坐标类型错误 4、命名的小插曲 五、总结 前言         在当今数字化时代,交通出行的便捷性与高效性已成为衡量城市智慧化水平的重要指标之一。随着城市化进程的加速,长沙市作为湖南省的省会城市,其交通流量日益复杂,再叠加现在的国庆旅游客流和中秋探亲客流,热门道路与景点的路况信息对于市民日常出行和游客旅游规划至关重要。因此,需要开发一套能够实时检索长沙市热门道路与景点路况的系统。         本实践探索旨在通过Java编程语言调用百度地图的API接口,实现对长沙市热门道路与景点的实时路况检索功能。

By Ne0inhk
java( Java 25 LTS)的下载、安装、配置 (IDEA 2025 为例)

java( Java 25 LTS)的下载、安装、配置 (IDEA 2025 为例)

一、Java 25 LTS 下载 Java 下载 |神谕https://www.oracle.com/java/technologies/downloads/#jdk25-windows 二、安装 2.1Windows 图形安装 首先双击下载的 jdk25.msi 文件,进入安装向导。 选择 Next 进入下一步。修改安装路径(建议 D:\Java\jdk-25)确保路径简洁无中文或空格。 勾选 Generate public JRE 选项,保持默认配置。 点击 Install 开始安装,完成后点击 Finish。 2.2macOS 安装 双击下载的 jdk-25.

By Ne0inhk
2023第十四届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组(真题&题解)(C++/Java题解)

2023第十四届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组(真题&题解)(C++/Java题解)

记录刷题的过程、感悟、题解。 希望能帮到,那些与我一同前行的,来自远方的朋友😉 大纲:  1、日期统计-(解析)-暴力dfs(😉蓝桥专属  2、01串的熵-(解析)-不要chu,认真读题,并且知道log()怎么用就OK  3、冶炼金属-(解析)-其实推理极限,用数学知识就能OK😊  4、飞机降落-(解析)-暴力搜索dfs(😉蓝桥专属  5、接龙数列-(解析)-字典dp(😎就是名字高大上点,只是一道dp  6、岛屿个数-(解析)-bfs+dfs,重点在于会染色+会读题(广搜深搜一起整  7、子串简写-(解析)-一道简单的前缀和  8、整数删除-(解析)

By Ne0inhk
不是付费工具用不起,而是国产 Seedance 2.0 更彻底:做 AI 漫剧 / AI 短剧最高效的 AI 工具,真的来了

不是付费工具用不起,而是国产 Seedance 2.0 更彻底:做 AI 漫剧 / AI 短剧最高效的 AI 工具,真的来了

如果你最近在做 AI 漫剧、AI 短剧,或者哪怕只是刷了几天 AI 视频圈,大概率都会有一个相同的感受: 工具越来越多,但真正“好用”的,并没有变多。 我自己是做内容策划和视频创作的,日常工作离不开短视频、剧情内容、IP 向账号。这一年里,我几乎把市面上能试的 AI 视频工具都试过一遍,国外的、国内的、偏专业的、偏玩具的。 结论很现实: 大多数 AI 视频工具,只适合“玩一次”,不适合“长期创作”。 而真正让我停下来、愿意反复用、甚至开始围绕它重新设计工作流的,是 Seedance 2.0。 一、AI 视频赛道,正在发生一次真正的“大洗牌” 先说一个大的判断。 2024–2025

By Ne0inhk