C++数据结构 — 哈希表 (Hash Table)、散列函数 课程设计与应用

C++数据结构 — 哈希表 (Hash Table)、散列函数 课程设计与应用


在这里插入图片描述


🎁个人主页:工藤新一¹

🔍系列专栏:C++面向对象(类和对象篇)

​ 🌟心中的天空之城,终会照亮我前方的路

🎉欢迎大家点赞👍评论📝收藏⭐文章

文章目录

哈希表( Hash Table ) —— 算法数据结构

(一)、哈希表的概念

1.1哈希表的定义

  • 哈希表(hash table),又称散列表,是根据关键字进行访问的数据结构
  • 哈希表建立了一种关键字存储地址之间映射关系,使每个关键字与结构中的唯一存储位置相对应。
在这里插入图片描述
  • 理想情况下,在散列表中进行查找的时间复杂度为O(1),即与表中的元素数量无关。因此哈希表是一种存储和查找非常快的结构。
  • 核心思想:给出关键字,通过映射关系找出存储位置
  • 关键字 ----> 映射关系 ----> 存储位置

在这里插入图片描述
int cnt[26];//创建一个大小为26的数组voidsolve(string& s){for(auto ch : s)//遍历语法糖{ cnt[ch -'a']++;/* 因为'a' - 'a' = 0,'b' - 'a' = 1;将对应的数组索引位置 +1,从而通过下标映射出字符出现的次数。 */}}

  • 流程演示
在这里插入图片描述
"a b c a b c c"//idex ^ 扫描到 a 时,其元素的映射关系的(下标)0 位置进行 ++ 

在这里插入图片描述
"a b c a b c c"//idex ^ 扫描到 b 时,其元素的映射关系(下标)1 位置进行++ 

在这里插入图片描述
"a b c a b c c"//idex ^ 扫描到 c 时,其元素对应位置(下标)2 位置进行++ 
"a b c a b c c"//idex ^ 同理。 

在这里插入图片描述
"a b c a b c c"//idex ^ 

在这里插入图片描述
int cnt[26];voidsolve(string& s){for(auto ch : s){ cnt[ch -'a']++;/* 'b' - 'a' == 1,因此当我们要统计字符 b 出现的次数时,可以通过下标为 1 的位置,来找到字符 b 出现的次数,cnt[1]。 */}}

  • 因此在我们这段编程代码中:
  • 关键字(字符a, b, c) ----> 映射关系(ch - 'a') ----> 存储位置(idex)

1.2哈希函数

  • 定义:将关键字映射成对应地址的函数就是哈希函数
    说白了,哈希函数就是这个映射关系。
    交给哈希函数一个关键字,哈希函数给我们计算出一个存储位置。
  • 哈希函数,也叫做散列函数,记作:Hash( key ) = Adder。
    F ( x ) = K;
  • 比如上个案例中 ch - ‘a’ 就是一个哈希函数,记作:Hash( key ) = key - ‘a’,代入公式:Hash( ch ) = ch - ‘a’。
  • 算法核心:Hash( key ) = Adder。告诉哈希函数( Hash )一个关键字 key ( x ) ,( Hash )会返回关键字 key 所对应的存储位置( Adder )。

1.3哈希冲突

  • 设计哈希函数:Hash( key ) = key % 7;( 为什么 % 7 )
    • 由数组大小可知,sizeof(a) == 7;因此我们 %7 后绝对是可以将数组中的每一个元素映射到大小为 7 的数组中的每一个存储位置上( 数组中的每个元素都会被映射到 idex == 0~6 的位置上)。
    • 因此,我们可以开辟一个大小为7的数组即可。

但此时的问题就会显现:

在这里插入图片描述

哈希函数可能会把两个或两个以上不同的关键字,映射到同一个存储位置上,这种情况被称为哈希冲突,也叫做散列冲突。起冲突的两个不同关键词被称为同义词。

在这里插入图片描述
hash(3)=3;hash(17)=3;/* 当我们统计元素 3 出现的次数时,对应 3 存储位置上的数++; 当我们统计元素 17 出现的次数时,对应 3 存储位置上的数也会进行了++操作 */

如上所见,两个不同的元素却出现在同一个存储位置上!
综上:不同的关键字映射到同一个存储位置上,这种情况我们就成为哈希冲突。


1.3.1对于哈希冲突的认知
  • 哈希冲突不可避免。
  • 我们需要的不是消除哈希冲突,而是设法设计出优秀的哈希函数,并且处理哈希冲突。
  • 由此可见,设计一个优秀的哈希表,不仅需要设计一个优秀的哈希函数,也需要能够处理哈希冲突。那么,学习哈希表的重点就是设计哈希函数和处理哈希冲突。

(二)、常见的哈希函数

2.1直接定址法

  • 哈希表概念篇章中,统计字符串中某字符出现次数的案例所使用的方法,就是直接定址法。
  • 直接定址法:直接取关键字的某个线性函数值散列地址。比如,直接映射"Hash( key ) = key "直接拿关键字的值当作存储地址(Adder)或者拿一个线性的值当作存储地址(Adder)"Hash( key ) = a * key + b "(a,b 是常数) 。这种方式计算较为简单,适合关键字分布基本连续的情况,但关键字分布不连续,会导致空位较多,导致存储空间的大量浪费。
  • 如下情况使用思路一会导致存储空间的浪费:
在这里插入图片描述

2.2除留余数法

  • 如上图哈希冲突例子中的思路二即是典型的除留余数法:拿关键字的值 % 某一个数,映射到下标的某一个位置( 存储位置上 )。
  • **除留余数法:**顾名思义,假设哈希表的大小为 M,那么通过key / M的余数作为映射位置的下标,也就是哈希函数:hash( key ) = key % M。因此这种方法的重点就是选好模数M。
    • 建议 M 取不太接近 2 的整数次幂的一个质数(素数)。( 具体原理可以参考《算法导论》中的证明,选素数可以使分布更均匀从而减少冲突)

(三)、处理哈希冲突

  • 有时候哈希表无论选择什么哈希函数都无法避免哈希冲突的产生,那么插入数据时,如何解决解决哈希冲突呢?主要有两种方法:线性探测法链地址法

3.1线性探测法

  • 线性探测:从发生冲突的位置开始,依次线性向后探测,直到找到下一个没有存储数据的位置为止,若走到哈希表表尾,则回绕到哈希表表头位置。
  • 线性探测法的弊端:当数组对元素的存储过于拥挤时,会导致查找与存储的时间复杂度退化为O(N)。
  • 案例说明:哈希表的作用 — 存储数据
  • INF无穷大是数组中绝对不会出现的值,将数组初始化为INF可以明确表示某个位置尚未被有效赋值。如果数组未初始化,其值是不确定的(可能包含随机垃圾值),将数组初始化为INF可以避免因未定义行为导致的错误。
在这里插入图片描述

h(19)=8(哈希值 || Adder); 因此我们将关键字 19 存放到下标为 8 的存储位置上 
在这里插入图片描述

h(30)=8; 因此我们将关键字 30 存放到下标为 8 的存储位置上 但发现下标为 8 的存储位置上已经存放了其他元素,此时就发生哈希冲突 使用线性探测法,依次线性向后寻找一个没有存储元素的位置 最终我们发现下标为 9 的位置上可以存放关键字 30
在这里插入图片描述

h(5)=5;h(26)=3;h(13)=2; 将关键字 5 放到下标为 5 的存储位置上 将关键字 26 放到下标为 3 的存储位置上 将关键字 13 放到下标为 2 的存储位置上 h(20)=9; 发现下标为 9 的存储位置上存放着其他元素,此时发生了哈希冲突 使用线性探测法,依次线性向后寻找一个没有存储元素的位置 


h(21)=10; 发生哈希冲突,且存储位置在表尾上 接下来再回到表头,进行线性探测 h(12)=1; 发生哈希冲突,存储至下标为 2 的存储位置上 此时数组中的所有的元素,都以线性探测的方式存入哈希标中 
在这里插入图片描述

3.2链地址法

  • 链地址法链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们将这些冲突的数据链接成一个链表挂在哈希表存储位置的下方
在这里插入图片描述
  • 结果如下:
在这里插入图片描述
  • 注意:我们这里的链表执行的插入操作是头插操作
  • 弊端:当数组中所有的元素都存放在同一个存储位置时,时间复杂度就退化成了O(N)。
  • 解决弊端的措施: 不再继续使用链表的方式来穿插哈希表存储位置的下方元素,而是改用红黑树!时间复杂度就变为了O( logN )。

(四)、哈希表的模拟实现

在这里插入图片描述

4.1线性探测法

4.1.1创建
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
#include<iostream>usingnamespace std;#include<cstring>constint N =23;//(随意选取一个质数)//创建哈希表 h,无穷大:0x3f3f3f3f,通常用一个特殊值表示某个位置是空的或未被使用。int h[N], INF =0x3f3f3f3f;//为 h 数组初始化voidinit(){//使用 memset 函数将整个数组 h 初始化为 0x3f3f3f3fmemset(h,0x3f,sizeof h);}intmain(){init();return0;}

4.1.2哈希函数以及处理哈希函数
除留余数法:hash(key) = key % N;

但要注意,key有可能是负数,负数取模后仍是负数

核心重点:

​ eg:hash(-5) = -5(key) % 23(质数) (映射)----> 负数(下标) 大家仔细思考,其所得到这个负数结果,是会被映射到存储位置(下标)上的,但数组中是不会存在负数下标的。因此,如果我们需要映射一个负数存储空间时,那就需要进行补正操作。**负数补正操作:**取模后再加上模数即可。eg:(-5 % 23)+ 23(模数)但正数取模后再加上模数整体就会变大,所以需要统一再次进行取模操作。eg:(5 % 23) + 23 == 28,因此需要统一补正操作。

最终就是:(5 % 23 + 23 ) % 23;

公式:(key % N + N)% N ,简称: 模加模
#include<iostream>usingnamespace std;#include<cstring>constint N =23;int h[N], INF =0x3f3f3f3f;voidinit(){memset(h,0x3f,sizeof h);}//实现哈希函数的功能 - 计算出 x 的存储位置inthash(int x){int id =(x % N + N)% N;//处理哈希冲突 - 线性探测法while(h[id]!= INF && h[id]!= x)//当 id 位置存在其他元素,且h[id] != x;{ id++;if(id == N) id =0;//如果走到尾部,就从头再来}return id;}intmain(){init();return0;}

4.1.3添加元素
通过哈希函数找到合适的位置,将其放入即可。
voidinsert(int x){int idex =hash(x);//找到 x 适合存放的位置 h[idex]= x;}

4.1.4查找元素
通过哈希函数找到映射位置,判断数组中的值是否是 x
boolfind(int x){int idex =hash(x);return h[idex]== x;}

4.1.5测试代码
#include<iostream>usingnamespace std;#include<cstring>constint N =23;int h[N], INF =0x3f3f3f3f;voidinit(){memset(h,0x3f,sizeof h);}inthash(int x){int id =(x % N + N)% N;while(h[id]!= INF && h[id]!= x){ id++;if(id == N) id =0;}return id;}voidinsert(int x){int idex =hash(x); h[idex]= x;}boolfind(int x){int idex =hash(x);return h[idex]== x;}intmain(){init();int n;cin >> n;while(n--){int op, x;cin >> op >> x;if(op ==1){insert(x);}elseif(op ==2){if(find(x)) cout <<"Yes"<< endl;else cout <<"No"<< endl;}}return0;}

4.2连地址法

4.2.1创建
连地址法的实现与 树的存储 - 链式前向星 一模一样

本质也是使用数组模拟链表
在这里插入图片描述
#include<iostream>usingnamespace std;constint N =23;int h[N];//使用数组创建哈希表int e[N], ne[N], id;//e[N] - 数据域,ne[N] - 指针域intmain(){return0;}
4.2.2知识穿插 —树的存储 - 链式前向星
链式前向星是在静态链表链表概念)的基础上完成的。

链式前向星本质是用数组来模拟链表,名字听起来很花哨,但是不要被唬到。链式前向星其实就是用链表存储所有的孩子节点(树中的概念),其中链表是用数组来模拟实现的

4.2.3哈希函数
除留余数法
//哈希函数 - 计算哈希值inthash(int x){return(x % N + N)% N;//确保哈希值非负}

4.2.4插入元素
实质就是把当前这个节点( x )放到当前的哈希值( 存储位置 )所对应的链表

插入方式:头插 - 涉及链表的存储
//插入元素,并且处理哈希冲突voidinsert(int x){int idex =hash(x);//计算哈希值(存储位置)//把 x 头插到 idex 所在的链表在 中 id++;//为元素 x 分配一个存储位置  e[id]= x;//链地址法 ne[id]= h[idex];//新节点的指针指向当前头节点 h[idex]= id;//更新头节点}

4.2.5查找元素
找出哈希值

遍历链表 - 涉及链表的遍历
boolfind(int x){int idex =hash(x);//计算哈希值(存储位置)//遍历链表中是否存在元素 xfor(int i = h[idex]; i; i = ne[i])if(e[i]== x)returntrue;returnfalse;}

4.2.6代码实现
#include<iostream>usingnamespace std;constint N =23;int h[N];int e[N], ne[N], id;inthash(int x){return(x % N + N)% N;}voidinsert(int x){int idex =hash(x); id++; e[id]= x; ne[id]= h[idex]; h[idex]= id;}boolfind(int x){int idex =hash(x);for(int i = h[idex]; i; i = ne[i])if(e[i]== x)returntrue;returnfalse;}intmain(){int n;cin >> n;while(n--){int op, x;cin >> op >> x;if(op ==1){insert(x);}elseif(op ==2){if(find(x)) cout <<"Yes"<< endl;else cout <<"No"<< endl;}}return0;}

(五)、简介STL - unordered_set / unordered_map

1. unordered_set的介绍

unordered_set是一种关联式容器,它具有以下几个特点:

1.基本概念unordered_set是一种关联式容器,不按特定顺序存储键值。元素的值同时也是唯一标识它的key

2.内部存储方式内部元素没有按照特定顺序排序。为快速找到指定key,将相同哈希值的键值放在相同的桶中(开散列)。

3.性能特点通过key访问单个元素比set快。在遍历元素子集的范围迭代方面效率较低,因为数据并不在某个范围,而是无序的。

4.迭代器类型迭代器至少是前向迭代器。

具体可参考官方文档——unordered_set


2.STL容器差异

1.setunordered_set 的区别前者的实现方式是红黑树,后者是哈希表。使用方式完全一样。无非是存储和查找的效率不同。以及前者(set)是有序后者(unordered_set)是无序的。

2.unordered_setunordered_map的区别前者只存储,且键是唯一的,没有,后者存储键值对key-value),是唯一的,但可以重复。

3.unordered_setunordered_multiset区别前者存储唯一的元素,不允许重复,后者允许存储重复的元素。


🌟各位看官好我是工藤新一¹呀~

🌈愿各位心中所想,终有所致!

Read more

Flutter 三方库 async_extension 的鸿蒙化适配指南 - 实现具备高级异步编排算法与流操作扩展的并发工具集、支持端侧复杂业务流的函数式处理实战

Flutter 三方库 async_extension 的鸿蒙化适配指南 - 实现具备高级异步编排算法与流操作扩展的并发工具集、支持端侧复杂业务流的函数式处理实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 async_extension 的鸿蒙化适配指南 - 实现具备高级异步编排算法与流操作扩展的并发工具集、支持端侧复杂业务流的函数式处理实战 前言 在进行 Flutter for OpenHarmony 的大规模异步业务系统(如实时行情刷新、多源数据聚合)开发时,如何更优雅地处理 Future 的超时竞争、Stream 的防抖(Debounce)或复杂的并发队列控制?虽然 Dart async 包提供了基础功能,但 async_extension 进一步扩展了异步编程的边界,提供了更符合函数式范式的工具。本文将探讨如何在鸿蒙端构建极致、高效的异步处理链路。 一、原直观解析 / 概念介绍 1.1 基础原理 该库通过对 Dart 核心异步类的非侵入式扩展(Extensions)

By Ne0inhk
链表进阶核心 | LeetCode 92 区间反转:吃透递归反转与哨兵技巧

链表进阶核心 | LeetCode 92 区间反转:吃透递归反转与哨兵技巧

✨链表进阶核心 | LeetCode 92 区间反转:吃透递归反转与哨兵技巧🎯 * 视频地址 * 🚀 开篇引论:链表反转的进阶之路 * 🔄 基础筑基:链表【前n个节点】递归反转 * 1. 函数定义与核心功能 * 2. 递归实现思路拆解 * 3. 直观调用示例 * 4. 关键代码实现(C++)与详解 * 🎯 实战攻坚:LeetCode 92 链表区间反转 * 1. 题目问题描述 * 2. 神器加持:虚拟头节点(哨兵)技巧 * 3. 整体解题思路 * 4. 完整代码实现(C++)与逐行解析 * 5. 算法复杂度分析 * 📚 算法原理深度剖析 * 1. 递归反转的核心原理 * 2. 虚拟头节点的底层逻辑 * 💡 算法学习核心建议 * 结语 * ✅ 关键点回顾 视频地址

By Ne0inhk
【数据结构-初阶】详解线性表(5)---队列

【数据结构-初阶】详解线性表(5)---队列

🎈主页传送门:良木生香 🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》 🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离 上期回顾:在上一篇文章(【数据结构-初阶】详解栈和队列(1)---栈)中我们讲到了在顺序表与链表之外的另一种线性表---栈,知道了这是一种具有先进后出和后进先出特点的数据结构,既然有先进后出,那么肯定就有先进先出的数据结构,所以这就是我们今天要讲的------队列 一、队列的概念 既然我们想要实现先进先出的效果,那肯定就不像栈那样有一端是堵起来的,想必应该是两端都开口吧。嗯,事实确实如此。 队列:是只允许在一端进行数据的插入操作,在另一端进行数据的删除操作的一种特殊的线性表,其具有先进先出FIFO(first in first out)的结构特点. 入队列:进行插入操作的一端叫做队尾 出队列:进行删除操作的一端叫做队头 下面是队列的示意图: 名字叫做队列,其实就像我们排队一样,先排的人先得服务,后排的人后得到服务,在队列中,先进来的元素先得到操作,

By Ne0inhk
【数据结构与算法】环与相遇:链表带环问题的底层逻辑与工程实现

【数据结构与算法】环与相遇:链表带环问题的底层逻辑与工程实现

🔥小龙报:个人主页 🎬作者简介:C++研发,嵌入式,机器人等方向学习者 ❄️个人专栏:《C语言》《【初阶】数据结构与算法》 ✨ 永远相信美好的事情即将发生 文章目录 * 前言 * 一、带环链表 * 1.1题目 * 1.2 算法原理 * 1.3 代码 * 1.4 数学证明 * 1.4.1 为什么带环slow与fast必定能相遇? * 1.4.2 fast一定只能走2步吗?可以是2步甚至更多吗? * 1.4.2.1 以3步为例 * 1.4.3结论 * 二、环形链表(寻找相遇点) * 2.1 题目

By Ne0inhk