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

news2024/12/29 13:48:51

文章目录

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

什么是哈希

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

在顺序结构以及平衡树中,记录的关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为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.coloradmin.cn/o/1541057.html

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

相关文章

基因在各个细胞系表达情况

从CCLE下载数据得到基因在每个细胞系中的 现在从DepMap: The Cancer Dependency Map Project at Broad Institute 需要先选择Custom Downloads 就可以下载数据进行处理了&#xff1a; rm(list ls()) library(tidyverse) library(ggpubr) rt <- data.table::fread("…

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

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

SpringBoot 3整合Elasticsearch 8

这里写自定义目录标题 版本说明spring boot POM依赖application.yml配置新建模型映射Repository简单测试完整项目文件目录结构windows下elasticsearch安装配置 版本说明 官网说明 本文使用最新的版本 springboot: 3.2.3 spring-data elasticsearch: 5.2.3 elasticsearch: 8.1…

Unity Mesh简化为Cube mesh

Mesh简化为Cube mesh &#x1f373;食用&#x1f959;子物体独立生成CubeMesh&#x1f96a;合并成一个CubeMesh&#x1f32d;Demo &#x1f373;食用 下载并导入插件&#x1f448;即可在代码中调用。 &#x1f959;子物体独立生成CubeMesh gameObject.ToCubeMesh_Invidual()…

计算机基础系列 —— 汇编语言

Same hardware can run many different programs(Software) 文中提到的所有实现都可以参考&#xff1a;nand2tetris_sol&#xff0c;但是最好还是自己学习课程实现一遍&#xff0c;理解更深刻。 我们在之前的文章里&#xff0c;构建了 Register、RAM 和 ALU&#xff0c;使得我…

前端基础篇-前端工程化 Vue 项目开发流程(环境准备、Element 组件库、Vue 路由、项目打包部署)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 环境准备 1.1 安装 NodeJs 1.2 验证 NodeJs 环境变量 1.3 配置 npm 的全局安装路径 1.4 切换 npm 的淘宝镜像( npm 使用国内淘宝镜像的方法(最新) ) 1.5 查看镜像…

QGIS编译(跨平台编译)056:PDAL编译(Windows、Linux、MacOS环境下编译)

点击查看专栏目录 文章目录 1、PDAL介绍2、PDAL下载3、Windows下编译4、linux下编译5、MacOS下编译1、PDAL介绍 PDAL(Point Data Abstraction Library)是一个开源的地理空间数据处理库,它专注于点云数据的获取、处理和分析。PDAL 提供了丰富的工具和库,用于处理激光扫描仪、…

Winform数据绑定

简介# 在C#中提起控件绑定数据&#xff0c;大部分人首先想到的是WPF&#xff0c;其实Winform也支持控件和数据的绑定。 Winform中的数据绑定按控件类型可以分为以下几种&#xff1a; 简单控件绑定列表控件绑定表格控件绑定 绑定基类# 绑定数据类必须实现INotifyPropertyChanged…

Docker 安装 Nginx 容器,反向代理

Docker官方镜像https://hub.docker.com/ 寻找Nginx镜像 下载Nginx镜像 docker pull nginx #下载最新版Nginx镜像 (其实此命令就等同于 : docker pull nginx:latest ) docker pull nginx:xxx #下载指定版本的Nginx镜像 (xxx指具体版本号)检查当前所有Docker下载的镜像 docker…

Linux 创建交换空间

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 仓库主页&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 欢迎点赞…

【C++】使用cppcheck检查C++代码

Cppcheck 是 C/C 代码的静态分析工具。它提供独特的代码分析来检测错误&#xff0c;并专注于检测未定义的行为和 危险的编码结构&#xff0c;即使它具有非标准语法&#xff08;在嵌入式项目中很常见&#xff09;。 关于静态分析 通过静态分析可以发现的错误类型包括&#xff…

Zabbix使用TimescaleDB数据库

一、前言 Zabbix 6.0 已发布很久&#xff0c;下个季度7.0应该会正式发布&#xff0c;但6.0也有许多新功能和新特性&#xff0c;这里介绍 6.0 配置 TimescaleDB&#xff0c;此安装配置方法可基本通用与其他版本。 二、TimescaleDB TimescaleDB 基于 PostgreSQL 数据库打造的一…

【Leetcode】2549. 统计桌面上的不同数字

文章目录 题目思路代码复杂度分析时间复杂度空间复杂度 结果总结 题目 题目链接&#x1f517; 给你一个正整数 n n n &#xff0c;开始时&#xff0c;它放在桌面上。在 1 0 9 10^9 109 天内&#xff0c;每天都要执行下述步骤&#xff1a; 对于出现在桌面上的每个数字 x &am…

Programming Abstractions in C阅读笔记:p331-p337

《Programming Abstractions in C》学习第79天&#xff0c;p331-p337&#xff0c;总计7页。 一、技术总结 /** File: stack.h* -------------* This interface defines an abstraction for stacks. In any* single application that uses this interface, the values in* the…

2024/3/24 LED点阵屏

显示原理&#xff1a; 类似矩阵键盘&#xff0c;逐行or逐列扫描 74HC595是串行 寄存器 感觉就是三转八寄存器 并行&#xff1a;同时输出&#xff1b;串行&#xff1a;一位一位输出 先配置74HC595&#xff0c;重新进行位声明 sbit RCKP3^5; //RCLK sbit SCKP3^6; …

ai问答机器人是什么?介绍这几款实用ai问答机器人

ai问答机器人是什么&#xff1f;随着人工智能技术的飞速发展&#xff0c;AI问答机器人已成为我们生活中不可或缺的一部分。它们能够智能地解答各种问题&#xff0c;提供便捷的服务&#xff0c;极大地提升了用户体验。本文将带你了解AI问答机器人的基本概念&#xff0c;并介绍几…

30-函数(上)

30-1 函数是什么 在计算机科学中&#xff0c;子程序是一个大型程序中的某部分代码&#xff0c;由一个或多个语句块组成。它负责完成某项特定任务&#xff0c;而且相较于其他代码&#xff0c;具备相对的独立性。 一般会有输入参数并有返回值&#xff0c;提供对过程的封装和细节…

jenkins配置源码管理的git地址时,怎么使用不了 credential凭证信息

前提 Jenkins使用docker部署 问题 &#xff08;在jenlins中设置凭证的方式&#xff09;在Jenkins的任务重配置Git地址&#xff0c;并且设置了git凭证,但是验证不通过&#xff0c;报错; 无法连接仓库&#xff1a;Command "git ls-remote -h -- http://192.1XX.0.98:X02/…

梅林生态第一个Defi借贷协议零撸教程

简介&#xff1a;Avalon Finance是第一个基于Merlin Chain的去中心化借贷协议。它包括三个关键组成部分&#xff1a;超额抵押借贷、与借贷相关的衍生品交易和基于借贷的算法稳定币。 相关概念&#xff1a;梅林生态、Defi 融资信息&#xff1a;项目于3月15日完成种子轮融资&am…

IntelliJ IDEA集成git配置账号密码

1 背景说明 刚使用IDEA,本地也安装Git,在提交和拉取代码的时候,总提示登录框,而且登录框还不能输入账号密码,只能输入登录Token。如下: 从而无法正常使用IDEA的Git功能,很苦恼。 2 解决方法 2.1 安装Git 进入官网地址 https://git-scm.com/,点击下载: 浏览器直接…