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

【AI深究】卷积神经网络:CNN深度解析——全网最详细全流程详解与案例(附Python代码演示)|数学表达、主流变体与架构创新、优缺点与工程建议、调优技巧|经典变体:ResNet、DenseNet详解

【AI深究】卷积神经网络:CNN深度解析——全网最详细全流程详解与案例(附Python代码演示)|数学表达、主流变体与架构创新、优缺点与工程建议、调优技巧|经典变体:ResNet、DenseNet详解

大家好,我是爱酱。本篇将会系统梳理卷积神经网络(Convolutional Neural Network, CNN)的原理、结构、数学表达、典型应用、可视化代码示例与工程实践,帮助你全面理解这一深度学习的“感知基石”。 注:本文章含大量数学算式、详细例子说明及大量代码演示,大量干货,建议先收藏再慢慢观看理解。新频道发展不易,你们的每个赞、收藏跟转发都是我继续分享的动力! 注:本文章颇长超过8000字长、以及大量详细、完整的Python代码、非常耗时制作,建议先收藏再慢慢观看。新频道发展不易,你们的每个赞、收藏跟转发都是我继续分享的动力! 一、CNN的核心定义与结构 卷积神经网络(CNN)是一种专为处理具有类似网格结构的数据(如图像、音频、时序信号)而设计的深度神经网络。其核心思想是通过卷积操作自动提取局部特征,实现空间不变性和参数高效性。 * 英文专有名词:Convolutional Neural Network, CNN * 主要结构: * 卷积层(Convolutional

By Ne0inhk
Libvio.link爬虫技术技术

Libvio.link爬虫技术技术

Libvio.link爬虫技术详细解析        先明确核心:Libvio.link本质是一个「网页数据采集工具」(爬虫),和我们平时用浏览器看网页、存内容的逻辑一样,只是它能自动、批量地去访问目标网站,把网站里的内容(比如视频链接、文本、图片)爬下来,整理后展示在自己的平台上,供人直接查看/下载。         全程不用懂复杂代码,重点搞懂「它怎么爬、爬什么、为什么能爬、会遇到什么问题」,看完就能明白Libvio.link爬虫的核心逻辑,也能理解同类爬虫的工作原理。 一、先搞懂:Libvio.link爬虫到底是什么?(通俗比喻)         你想把一个视频网站的所有电影链接都存下来,一个个点开网页、复制链接、粘贴保存,要花几个小时甚至几天;而Libvio.link爬虫,就相当于一个「自动打工的机器人」,你给它设定好要爬的网站(比如某视频站),它就会自动点开每一个网页,自动识别里面的视频链接、标题、简介,自动复制保存,全程不用你动手,

By Ne0inhk

跨境电商 AI 数据中台架构实战:接入卖家精灵 MCP,打通“选品—投放—供应链—合规”的闭环

1. 背景与目标 跨境电商公司做 AI,最容易踩的坑不是模型不够强,而是 数据割裂、口径不统一、工具链不可复用、产出无法闭环。典型现状包括: * 运营数据散落在 Amazon SP-API、ERP/WMS、广告平台、客服工单、第三方选品工具(如卖家精灵)等多个系统; * 业务问题(选品/关键词/广告/补货/合规)彼此耦合,但数据链路却是断的; * LLM 生成内容(Listing、广告词、客服话术)可用性不稳定,缺少可追踪评测与回滚机制; * 文件类知识(SOP、合规条款、合同、类目规范)难以被 AI 高质量引用,导致“幻觉”和合规风险。 本文给出一套“可落地”的

By Ne0inhk
Flutter 组件 spinify 适配鸿蒙 HarmonyOS 实战:实时消息管道,构建全场景高性能 WebSocket 长连接架构

Flutter 组件 spinify 适配鸿蒙 HarmonyOS 实战:实时消息管道,构建全场景高性能 WebSocket 长连接架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 spinify 适配鸿蒙 HarmonyOS 实战:实时消息管道,构建全场景高性能 WebSocket 长连接架构 前言 在鸿蒙(OpenHarmony)生态迈向万物互联、涉及高频实时交互、流式数据同步或多人协同编辑的场景下,如何建立一套稳定、高效且具备自动愈合能力的长连接通道,已成为提升应用实时性体验的“关键枢轴”。在鸿蒙设备这类强调分布式协同与严苛能效管理的移动终端上,如果直接使用原生的 WebSocket 进行裸奔(Bare Metal)开发,由于由于缺乏完善的心跳机制、重连策略与频道管理,极易由于由于网络波动导致连接频繁断档,进而引发业务状态的不一致。 我们需要一种能够深度封装协议细节、支持大规模并发频道订阅且具备毫秒级重连恢复能力的实时通讯引擎。 spinify 为 Flutter 开发者提供了与 Centrifugo(高性能实时消息服务器)交互的高级客户端。它支持全双工通信、自动重连计数与消息序列确认(ACK)。在适配到鸿

By Ne0inhk