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

这篇具有很好参考价值的文章主要介绍了由浅入深一步步了解什么是哈希(概念向)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

什么是哈希

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

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

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

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

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

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

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

这里有一组水果相关的英文单词,它们存储在不同容器之中,可以看到记录的关键码与其存储位置之间没有对应的关系
由浅入深一步步了解什么是哈希(概念向),ljh的数据结构笔记,哈希算法,算法,数据结构

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

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

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

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

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

哈希函数

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

直接定址法

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

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

我们来看下面这两个例子(例子来自《大话数据结构》,因为比较好懂,我就直接拿来借用一下)。
由浅入深一步步了解什么是哈希(概念向),ljh的数据结构笔记,哈希算法,算法,数据结构

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

由浅入深一步步了解什么是哈希(概念向),ljh的数据结构笔记,哈希算法,算法,数据结构

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

除留余数法

假设散列表的长度为 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)

由浅入深一步步了解什么是哈希(概念向),ljh的数据结构笔记,哈希算法,算法,数据结构

从这个例子中我们能看到,哪怕最大值和最小值之间相差了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)

由浅入深一步步了解什么是哈希(概念向),ljh的数据结构笔记,哈希算法,算法,数据结构

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

由浅入深一步步了解什么是哈希(概念向),ljh的数据结构笔记,哈希算法,算法,数据结构

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

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

  • 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低(就像哈希值为 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)

由浅入深一步步了解什么是哈希(概念向),ljh的数据结构笔记,哈希算法,算法,数据结构

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

负载因子和闭散列的扩容

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

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

存在研究表明:

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

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

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

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

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

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

开散列

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

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

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

由浅入深一步步了解什么是哈希(概念向),ljh的数据结构笔记,哈希算法,算法,数据结构

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

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

开散列的扩容

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

对于开散列来说最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在记录个数刚好等于桶的个数时,即负载因子为 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函数》,里面的内容是关于如何调整权值来最大化的减少哈希冲突发生的可能性,以及各种字符串哈希函数之间的性能对比。

方法三:自定义转换

根据应用场景的特点,设计自定义的转换方法。例如,对于日期类型的关键码,可以将日期转换成天数或秒数作为整数哈希码;对于自定义类对象,可以根据对象的属性值计算出一个整数哈希码,这个就不好距离了,得根据实际需求来定。文章来源地址https://www.toymoban.com/news/detail-852788.html

到了这里,关于由浅入深一步步了解什么是哈希(概念向)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 什么是感知机——图文并茂,由浅入深

    生活中常常伴随着各种各样的逻辑判断,比如看到远方天空中飘来乌云,打开手机看到天气预报说1小时后40%的概率下雨,此时时候我们常常会做出等会下雨,出门带伞的判断。 上述思考过程可以抽象为一个”与“的”神经逻辑“。当”看到乌云“和”天气预报40%下雨“同时

    2023年04月20日
    浏览(41)
  • Docker由浅入深(一)

    容器化技术介绍 介绍容器化之前,我们得先知道,为什么会出现容器化,容器化之前都经历了什么 物理机时代 部署非常慢 成功很高 浪费资源 难于扩展与迁移 受制于硬件 虚拟化时代 在同一个物理机上安装多个虚拟机,每个虚拟机安装操作系统和应用, 虚拟机之间物理资源

    2024年02月03日
    浏览(55)
  • React - redux 使用(由浅入深)

    中文文档: http://www.redux.org.cn/ 英文文档: https://redux.js.org/ Github: https://github.com/reactjs/redux 可直接参照 目录十 进行使用 react-redux redux 是一个专门用于做状态管理的JS库(不是react插件库)。 它可以用在 react, angular, vue 等项目中, 但基本与 react 配合使用。 作用: 集中式管理 re

    2024年02月07日
    浏览(53)
  • 由浅入深Netty代码调优

    序列化,反序列化主要用在消息正文的转换上 序列化时,需要将 Java 对象变为要传输的数据(可以是 byte[],或 json 等,最终都需要变成 byte[]) 反序列化时,需要将传入的正文数据还原成 Java 对象,便于处理 目前的代码仅支持 Java 自带的序列化,反序列化机制,核心代码如

    2024年02月05日
    浏览(46)
  • 【个人笔记】由浅入深分析 ClickHouse

    项目中不少地方使用到ClickHouse,就对它做了一个相对深入一点的了解和研究。并对各种知识点及整理过程中的一些理解心得进行了汇总并分享出来,希望对其他同学能有帮助。 本文主要讲解ClickHouse的特点、读写过程、存储形式、索引、引擎、物化视图等特性。 适合 入门和

    2024年01月20日
    浏览(47)
  • 【由浅入深学习MySQL】之索引进阶

    本系列为:MySQL数据库详解,为千锋资深教学老师独家创作 致力于为大家讲解清晰MySQL数据库相关知识点,含有丰富的代码案例及讲解。如果感觉对大家有帮助的话,可以【关注】持续追更~ 文末有本文重点总结,技术类问题,也欢迎大家和我们沟通交流! 从今天开始本系列

    2024年02月05日
    浏览(45)
  • 手拉手Vue组件由浅入深

    组件 (Component) 是 Vue.js 最强大的功能之一,它是html、css、js等的一个聚合体,封装性和隔离性非常强。 组件化开发:     1、将一个具备完整功能的项目的一部分分割多处使用     2、加快项目的进度     3、可以进行项目的复用 组件注册分为:全局注册和局部注册 目录

    2024年01月18日
    浏览(46)
  • 由浅入深介绍 Python Websocket 编程

    1.1 websocket 协议简介 Websocket协议是对http的改进,可以实现client 与 server之间的双向通信; websocket连接一旦建立就始终保持,直到client或server 中断连接,弥补了http无法保持长连接的不足,方便了客户端应用与服务器之间实时通信。 适用场景 html页面实时更新, 客户端的html页面

    2024年02月03日
    浏览(42)
  • Springboot3+EasyExcel由浅入深

    环境介绍 技术栈 springboot3+easyexcel 软件 版本 IDEA IntelliJ IDEA 2022.2.1 JDK 17 Spring Boot 3 EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。 他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。 官网https://easyexcel.opensource.ali

    2024年01月16日
    浏览(47)
  • 由浅入深理解C#中的事件

    本文较长,给大家提供了目录,可以直接看自己感兴趣的部分。 前面介绍了C#中的委托,事件的很多部分都与委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托,事件包含了一个私有的委托,如下图所示: 有关事件的私有委托需要了解的重要事项如下: 1、事

    2024年02月03日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包