由浅入深一步步了解什么是哈希(概念向)

文章目录

  • 什么是哈希
  • 哈希函数
    • 直接定址法
    • 除留余数法
  • 哈希冲突
    • 闭散列
      • 线性探测法
      • 二次探测法
      • 负载因子和闭散列的扩容
    • 开散列
      • 开散列的扩容
  • 非整形关键码

什么是哈希

我们来重新认识一下数据查找的过程:

在顺序结构以及平衡树中,记录的关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。

顺序结构: 指的是顺序表、链表等线性数据结构,具体在C++中表现为像vector、list这样的容器;

平衡树: 指的是AVL树、红黑树等树形数据结构,具体在C++中表现为map、set这样的容器;

记录: 指的是容器或数据结构中存储的元素(或者说数据),为了方便后面表述什么哈希的相关知识,特地用这个名词来指代;

关键码: 它是一个记录的唯一标识,可能是记录本身或者记录中的某一项。举个例子,假如说记录是一个整数 or 字符串,记录的关键码就是记录本身,假设记录是一个键值对,记录的关键码就是键值对中的key;假设记录是某个类对象,记录的关键码就是对象中的某几个成员变量;

存储位置: 顾名思义,就是一个记录在 容器 or 数据结构 之中的存储位置。

这里有一组水果相关的英文单词,它们存储在不同容器之中,可以看到记录的关键码与其存储位置之间没有对应的关系
在这里插入图片描述

正因为没有关系,假设我现在查找的目标记录是"watermelon",就只能从起点开始,挨个地比较每个记录的关键码和目标记录的关键码的值是 “ = ” 还是 “ ≠ ”,直到出现相等或者找完才算是有结果,所以才说,元素的查找效率取决于关键码的比较次数

那么有没有一种理想化的状态:查找的过程中,可以不通过任何的比较,而是让记录的关键码和记录的存储位置通过某种手段建立起一种一对一的映射关系,通过关键码直接就可以找到目标记录。

而达成这种理想化的查找状态的方法就是 “ 哈希 ”(或者说 “ 散列 ”),通过这个方法实现的存储结构,我们称之为 “ 哈希表 ”(或者说 “ 散列表 ”),记录的关键码和记录的存储位置建立映射关系的手段我们称之为 “ 哈希函数 ”(或者说 “ 散列函数 ”),哈希函数的作用是将记录的关键码转换成记录在哈希表的地址,对于这个地址我们一般称之为 “ 哈希值 ”(或者 “ 哈希地址 ”)。

" 哈希 "一词源自于英文单词 " hash ",而 " 散列 " 则是 " hash " 的中文翻译。最初,这两个术语可能在不同的语境中出现,但随着时间的推移,它们逐渐成为了同义词,并在计算机科学领域中得到广泛使用。哈希是直接音译,散列则是意译。

哈希表的插入操作大致为,“ 使用哈希函数计算出待插入记录的关键码的哈希值,即记录插入在哈希表的位置 ,然后插入 ” ;查找操作大致为,“ 使用哈希函数计算出待插入记录的关键码的哈希值,在哈希表中按此位置取元素比较,若关键码相等,则查找成功 ” 。

哈希函数

从上面来看,哈希函数可以说是哈希这个思想的关键,所以我们就来看看常用的哈希函数都有哪些。

直接定址法

直接定址法的做法是直接取记录的关键码的某个线性函数值来作为哈希地址

哈希函数的公式:
Hash(Key) = A × Key + B \text{Hash(Key)} = A \times \text{Key} + B Hash(Key)=A×Key+B

我们来看下面这两个例子(例子来自《大话数据结构》,因为比较好懂,我就直接拿来借用一下)。
在这里插入图片描述

这个的哈希函数的优点就是简单,但它只适合记录关键码分布范围较小且数据重复度高度的场景,在某些极端场景下可能会造成极大的空间浪费。

在这里插入图片描述

因此,直接定址法虽然因为简单而常见但是却不使用,真正实用的哈希函数还得是接下来讲的除留余数法。

除留余数法

假设散列表的长度为 c a p a i c t y capaicty capaicty p p p 是一个不大于 c a p a i c t y capaicty capaicty,但最接近或者等于 c a p a i c t y capaicty capaicty 的质数,除留余数法的公式如下:
Hash(Key) = Key % p , ( p ≤ capacity ) \text{Hash(Key)} = \text{Key} \% p, \quad (p \leq \text{capacity}) Hash(Key)=Key%p,(pcapacity)

在这里插入图片描述

从这个例子中我们能看到,哪怕最大值和最小值之间相差了999998,进行取模运算之后,我们也可以在表中找到一个位置存储记录。

然而,除留余数法还有一个致命的问题,假设我们再往表里插入记录 48 48 48 时,此时就会出现 H a s h ( 4 ) Hash(4) Hash(4) H a s h ( 48 ) Hash(48) Hash(48) 的哈希值都是 4 4 4 的现象,像这样的,当不同关键码通过相同哈希哈函数数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

而关键码不同、哈希地址相同的记录,我们称为 “ 同义词 ” 。

发生哈希冲突该如何处理呢?

哈希冲突

解决哈希冲突两种常见的方法是:闭散列开散列

闭散列

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

线性探测法

线性探测的做法是,从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
哈希函数的公式为: Hash(Key) = ( Hash(key) + d i ) % p , ( d i = 1 , 2 , 3 , … , p − 1 ) \text{Hash(Key)} = (\text{Hash(key)} + d_i) \% p, \quad (d_i = 1, 2, 3, \dots, p-1) Hash(Key)=(Hash(key)+di)%p,(di=1,2,3,,p1)

在这里插入图片描述

假设哈希地址为 8 8 8 10 10 10 的位置已经存储有记录,这时候要 “ 回头 ” 找空位置。

在这里插入图片描述

从上面的插入例子,我们能看到:

  • 线性探测优点:实现非常简单。

  • 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低(就像哈希值为 8 8 8 10 10 10 的位置存在记录时插入记录 44 44 44 一样)。

二次探测法

为了避免线性探测法产生 “ 堆积 ” 现象,还有一种找空位置的方法叫做二次探测法,与线性探测法不同的是,二次探测法使用的增量序列是 d i = 1 2 , − 1 2 , 2 2 , − 2 2 , … , q 2 , − q 2 , ( q ≤ p 2 ) d_i = 1^2, -1^2, 2^2, -2^2, \dots, q^2, -q^2, (q \leq \frac{p}{2}) di=12,12,22,22,,q2,q2,(q2p),这样的增量序列会让记录更加均匀的分布,从而达到降低哈希碰撞的概率。

二次探测法的哈希函数公式:
Hash(Key) = ( Hash(key) + d i ) % p , ( d i = 1 2 , − 1 2 , 2 2 , − 2 2 , … , q 2 , − q 2 , q ≤ p 2 ) \text{Hash(Key)} = (\text{Hash(key)} + d_i) \% p, \quad (d_i = 1^2, -1^2, 2^2, -2^2, \dots, q^2, -q^2, q \leq \frac{p}{2}) Hash(Key)=(Hash(key)+di)%p,(di=12,12,22,22,,q2,q2,q2p)

在这里插入图片描述

从上面的例子看到,用二次探测法来处理线性探测法的残局还是很有效的。

负载因子和闭散列的扩容

哈希表中还有一个叫做 “ 负载因子 ” 的概念,它的定义为:
负载因子 = 当前哈希表记录个数 哈希表的长度 \text{负载因子} = \frac{\text{当前哈希表记录个数}}{\text{哈希表的长度}} 负载因子=哈希表的长度当前哈希表记录个数

由于表的长度是一个定值,负载因子与 “ 填入表中的记录个数 ” 成正比,所以,当负载因子越大,表明填入表中的记录就越多,产生哈希冲突的可能性就越大,而哈希表的查找效率与哈希冲突的息息相关,在闭散列不可避免会产生哈希冲突的情况下,我们应当尽量降低哈希冲突的可能性。

存在研究表明:

当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次,如果插入过程中超过0.5就考虑对哈希表进行扩容,这种方法虽然查找效率极高,但是空间浪费也很严重。

而当负载因子超过0.8时,哈希表的空间利用率虽然提高了,但是查表时的CPU缓存不命中率次数会按照按照指数曲线上升,查找效率反而急速下降。

一般来说,负载因子控制在0.5到0.7之间空间利用率和操作效率之间取得较好的平衡。

哈希表的扩容一般是1.5倍扩容或者是2倍扩容,对哈希表进行扩容操作之后有一个点要处理,除留余数法的的操作让关键码除以表长后的余数作为哈希值,因此需要重新计算所有记录的哈希值,并将它们重新分配到新的哈希表中。

这个过程涉及以下几个步骤:

  1. 创建一个新的、更大容量的哈希表。
  2. 将旧哈希表中的所有键值对重新计算哈希值,并根据新的表长,将它们插入到新的哈希表中的相应位置。
  3. 销毁旧的哈希表,释放内存空间。

开散列

闭散列处理哈希冲突的思路是,这个位置有 “ 人 ” 了,我就找一个新的位置,但其实思路还可以再换一换,为了有冲突就一定得换地方呢?我们直接就在原地想办法不可以吗?

于是就有了这里的开散列法。

开散列法,又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同哈希地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

在这里插入图片描述

像上图那样,已经不存在什么冲突换地址的问题了,无论来多少个冲突的记录,都只是在当前位置给单链表增加结点的问题。

开散列对于可能会造成很多冲突的哈希函数来说,提供了绝对不会出现找不到地址的保障,但是这也并不是没有代价的,单链表来存储冲突记录就以为着需要遍历单链表的性能损耗。

开散列的扩容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,,那该条件怎么确认呢?

对于开散列来说最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在记录个数刚好等于桶的个数时,即负载因子为 1 1 1 时,可以给哈希表增容。

扩容的过程为如下几个步骤:

  1. 创建一个新的、更大容量的哈希表。
  2. 遍历旧哈希表中的每个哈希桶,将其中的记录的关键码对重新计算哈希值,并将其挪到新的哈希表中的对应位置。
  3. 释放旧哈希表的内存空间。

非整形关键码

除留余数法中,% 运算符已经规定了左右操作数是整形,右边的运算符是表长,它本身就是一个整数,不用过多考虑,关键是记录的关键码,假如说记录的关键码是一个字符串而不是一个整数时,我们又该怎么处理呢?

其实就是一句话,关键码不是整形,那就转换成整型!

方法一:直接转换

通过观察我们发现,所谓字符串其实就是多个字符的组合,而字符的本质其实是ASCII码,也是一个整型值,最简单的处理我们可以考虑将一个字符串中所有字符的ASCII码加起来作为记录的关键码,比如说字符串 "hello",ASCII 码表中 'h' 的值是 104,'e' 的值是 101,'l' 的值是 108,'o' 的值是 111,那么, 关键码 = 104 + 101 + 108 + 108 + 111 = 532 关键码 = 104 + 101 + 108 + 108 + 111 = 532 关键码=104+101+108+108+111=532

但是,这个方法也有很大的缺陷,容易引发哈希冲突,假设字符串是 "olleh",它转换处理出来的关键码同样也是 532,这必然会导致哈希冲突。

方法二:加权转换

为了减少哈希冲突,可以为每个字符指定一个权值,然后将每个字符的ASCII码值乘以对应的权值再相加,得到一个关键码。这种方法可以根据实际情况调整权重,以尽可能地减少哈希冲突。

同样以字符串 "hello" 为例,给定一个权值数组,例如 [1, 3, 5, 7, 11] hello 的关键码 = 104 × 1 + 101 × 3 + 108 × 5 + 108 × 7 + 111 × 11 = 2924 \text{hello 的关键码} = 104 \times 1 + 101 \times 3 + 108 \times 5 + 108 \times 7 + 111 \times 11 = 2924 hello 的关键码=104×1+101×3+108×5+108×7+111×11=2924

假设 "olleh" 的权值数组也是 [1, 3, 5, 7, 11],但是转换后的关键码,却不是一样的, olleh 的关键码 = 111 × 1 + 108 × 3 + 108 × 5 + 105 × 7 + 104 × 11 = 2854 \text{olleh 的关键码} = 111 \times 1 + 108 \times 3 + 108 \times 5 + 105 \times 7 + 104 \times 11 = 2854 olleh 的关键码=111×1+108×3+108×5+105×7+104×11=2854

有兴趣的话,这里推荐一篇文章《各种字符串Hash函数》,里面的内容是关于如何调整权值来最大化的减少哈希冲突发生的可能性,以及各种字符串哈希函数之间的性能对比。

方法三:自定义转换

根据应用场景的特点,设计自定义的转换方法。例如,对于日期类型的关键码,可以将日期转换成天数或秒数作为整数哈希码;对于自定义类对象,可以根据对象的属性值计算出一个整数哈希码,这个就不好距离了,得根据实际需求来定。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/767441.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【自然语言处理】统计中文分词技术(一):1、分词与频度统计

文章目录 一、词与分词1、词 vs 词素2、世界语言分类 二、分词的原因与基本原因1、为什么要分词2、分词规范3、分词的主要难点-切分歧义如何排除切分歧义利用词法信息利用句法信息利用语义信息利用语用、语境信息 4、分词的主要难点-未登录词未登录词如何识别未登录词 三、分词…

Docker入门到实践之环境配置

Docker入门到实践之环境配置 docker 环境安装 Ubuntu/Debian: sudo apt update sudo apt install docker.ioCentOS/RHEL: sudo yum install dockerArch Linux: sudo pacman -S docker如果未安装成功,或者env的path未设置成功,运行时会报错 Bash: Do…

[HackMyVM]靶场 Slowman

kali:192.168.56.104 靶机:192.168.56.132 端口扫描 # nmap 192.168.56.132 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-03-24 15:28 CST Nmap scan report for 192.168.56.132 Host is up (0.00066s latency). Not shown: 995 filtered tcp ports (no-response) …

NX二次开发-调内部函数创建进度条MT_create_progress_bar

一、概述 最近学习NX二次开发&#xff0c;看到NX打开装配模型或者加载模型时会显示进度条的问题&#xff0c;个人觉得很有意思&#xff0c;然后参考阿飞2018中的文章进行学习。 二、代码解析 //User Defined Header File#include <uf.h>#include <uf_ui.h>#includ…

使用 React antd 的ProFormSelect组件 搜索查询 多选的写法

使用 React antd 的ProFormSelect组件 搜索查询 多选的写法 需求&#xff1a;需要一个搜索框&#xff0c;可以选择员工&#xff0c;&#xff08;员工人数多无法一次性获取&#xff0c;全部放入options中&#xff09;&#xff0c;所以需要使用搜索功能&#xff0c;而且是可以多…

HarmonyOS NEXT应用开发之ArkWeb同层渲染

介绍 该方案展示了ArkWeb同层渲染&#xff1a;将系统原生组件直接渲染到前端H5页面上&#xff0c;原生组件不仅可以提供H5组件无法实现的一些功能&#xff0c;还能提升用户体验的流畅度 效果图预览 使用说明 进入页面即可看到同层渲染效果&#xff0c;Text&#xff0c;searc…

数据库系统概论(超详解!!!) 第四节 关系数据库标准语言SQL(Ⅰ)

1.SQL概述 SQL&#xff08;Structured Query Language&#xff09;结构化查询语言&#xff0c;是关系数据库的标准语言 SQL是一个通用的、功能极强的关系数据库语言 SQL的动词 基本概念 基本表 &#xff1a;本身独立存在的表&#xff1b; SQL中一个关系就对应一个基本表&am…

SecureCRT:高效安全的远程连接工具

SecureCRT是一款功能强大的终端仿真工具&#xff0c;主要用于连接和运行包括Windows、UNIX和VMS在内的远程系统。它支持多种协议&#xff0c;如SSH1、SSH2、Telnet、SFTP、Rlogin、Serial、SCP等&#xff0c;确保用户与目标设备之间的通信安全&#xff0c;并防止网络攻击和窥探…

HTTP系列之HTTP缓存 —— 强缓存和协商缓存

文章目录 HTTP缓存强缓存协商缓存状态码区别缓存优先级如何设置强缓存和协商缓存使用场景 HTTP缓存 HTTP缓存时利用HTTP响应头将所请求的资源在浏览器进行缓存&#xff0c;缓存方式分两种&#xff1a;强缓存和协商缓存。 浏览器缓存是指将之前请求过的资源在浏览器进行缓存&am…

安卓findViewById 的优化方案:ViewBinding与ButterKnife(一)

好多小伙伴现在还用findViewById来获取控件的id, 在这里提供俩种替代方案&#xff1a;ViewBinding与ButterKnife&#xff1b; 先来说说ButterKnife ButterKnife ButterKnife是一个专注于Android系统的View注入框架&#xff0c;在过去的项目中总是需要很多的findViewById来查…

java 实现发送邮件功能

今天分享一篇 java 发送 QQ 邮件的功能 环境&#xff1a; jdk 1.8 springboot 2.6.3 maven 3.9.6 邮件功能依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId>&…

idea打包war包部署到tomcat以及访问路径问题

idea将web项目打包成war最重要的是配置atrificats。 首先打开file -》 project structure 创建之后&#xff0c;output directory即为输出war包的路径。Name可以随意&#xff0c;之后点击绿色&#xff0c;打开directory content 选择webapp目录&#xff0c;记得勾选include in…

(C语言)浮点数在内存中的存储详解

1. 浮点数 常见的浮点数&#xff1a;3.14159、 1E10等 &#xff0c;浮点数家族包括&#xff1a; float、double、long double 类型。 浮点数表示的范围&#xff1a; float.h 中定义. 2. 浮点数的存储 我们先来看一串代码&#xff1a; int main() {int n 9;float* pFloa…

安全工具介绍 SCNR/Arachni

关于SCNR 原来叫Arachni 是开源的&#xff0c;现在是SCNR&#xff0c;商用工具了 可试用一个月 Arachni Web Application Security Scanner Framework 看名字就知道了&#xff0c;针对web app 的安全工具&#xff0c;DASTIAST吧 安装 安装之前先 sudo apt-get update sudo…

嵌入式数据库--SQLite

目录 1. SQLite数据库简介 2. SQLite数据库的安装 方式一&#xff1a; 方式二&#xff1a; 3. SQLite的命令用法 1.创建一个数据库 2.创建一张表 3.删除表 4.插入数据 5. 查询数据 6.删除表内一条数据 7.修改表中的数据 8.增加一列也就是增加一个字段 1. SQLite数据库…

LeetCode每日一题——统计桌面上的不同数字

统计桌面上的不同数字OJ链接&#xff1a;2549. 统计桌面上的不同数字 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 思路&#xff1a; 这是一个很简单的数学问题&#xff1a; 当n 5时&#xff0c;因为n % 4 1&#xff0c;所以下一天4一定会被放上桌面 当n 4…

Linux系统 安装docker

安装&#xff1a; 1、Docker要求CentOS系统的内核版本高于 3.10 &#xff0c;通过 uname -r 命令查看你当前的内核版本是否支持安账docker 2、更新yum包&#xff1a; sudo yum -y update 3、安装需要的软件包&#xff0c;yum-util 提供yum-config-manager功能&#xff0c;另外…

kvm虚拟化

kvm虚拟化 1. 虚拟化介绍 虚拟化是云计算的基础。简单的说&#xff0c;虚拟化使得在一台物理的服务器上可以跑多台虚拟机&#xff0c;虚拟机共享物理机的 CPU、内存、IO 硬件资源&#xff0c;但逻辑上虚拟机之间是相互隔离的。 物理机我们一般称为宿主机&#xff08;Host&…

深度学习pytorch——多层感知机反向传播(持续更新)

在讲解多层感知机反向传播之前&#xff0c;先来回顾一下多输出感知机的问题&#xff0c;下图是一个多输出感知机模型&#xff1a; 课时44 反向传播算法-1_哔哩哔哩_bilibili 根据上一次的分析深度学习pytorch——感知机&#xff08;Perceptron&#xff09;&#xff08;持续更新…

数学(算法竞赛、蓝桥杯)--快速幂

1、B站视频链接&#xff1a;G01 快速幂_哔哩哔哩_bilibili 题目链接&#xff1a;P1226 【模板】快速幂 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) #include <bits/stdc.h> using namespace std; typedef long long LL; int a,b,p; int quickpow(LL a,int n,int p){…