西风的数据结构教程(4)——hash表

2024-02-23 13:18

本文主要是介绍西风的数据结构教程(4)——hash表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

今天是教师节,感恩老师们的辛勤工作,让我们不断成长。

今天制作的这份教程十分重要,是我们高中时必会,而且是计算机体系中占有关键地位的一种数据结构——hash表。
要说hash表为何常用,为何厉害,那么我们先来考虑一个问题。

为什么说hash算法重要

计算机中,经常用到很多数学模型,例如,线性表 就是一种非常常用的数学模型,这里,不是在说他的数据结构,而是说,线性存储的这种方式,在很多情况下都用的到。

而另外一种重要的模型,就是 key-value键值对 模型,hash算法,重在构建一个高效的key-value模型。例如,我们有很多网页要浏览,而刚刚浏览过的网页再次打开,一般会快很多,因为浏览器帮我们做了缓存,然而我们细想,如何缓存呢?我们怎么才能知道这个网页我们刚刚访问过么?
那么我们可以建立一个key-value的模型,如果刚刚访问过,我们就将网页内容存放到value处,而key就存放网页的url。

例如我们的手机应用,希望快速的查找一个联系人的相关信息,那么遍历所有人员恐怕不是一个好想法,而用key-value模型根据名字索引找到则更方便。

hash算法除了可以构建key-value模型,更可以作为数据校验和数据加密的重要算法。

hash函数是一种单向不可逆函数,MD5是流行的数据校验用hash算法,下载下来的数据是否和网络上的一致,用hash计算一下结果,然后比较网络上的hash值和本地计算的hash值,一致的话就说明数据不存在异常。

sha系列hash算法,在网站密码的加密上有着重要的作用。一般开发网站的朋友都了解,网站用户的密码是不能明文保存的,因为万一数据泄露后,会对用户隐私造成重大威胁。那么用什么来判断密码是否正确呢?将hash后的密码作为值存储起来,判断密码正确前,也将用户的输入hash一次,这样就可以在不知道用户密码的前提下,判断是否为同一用户。

详解hash表

hash表又称散列表,这个名字就是在说,要将数据分散开存放到一些位置上。一般我们有一个很大的名字空间,例如,200位的英文字母组成的字符串,当然,我们不会全部使用,名字太多了,所以我们使用的,只会是其中的一小部分。同时,每一个名字还对应一条数据,往往名字只是标示,而其中的数据,才是我们最关注的。

而我们需要的是,尽快根据一个名字索引到对应的数据。有人说,简单啊,排序一下,然后根据二分查找的思想,对应判断就可以了。没错,二分查找是一种好思想,但也不是完全没有问题,如果我们的系统是动态的话,就很困难了,先存入两个数据,再删除一条,如果真想按照刚才的思想做,就必须做一棵高效平衡二叉树,普通的排序树有风险,可能退化的。而这样就太复杂了,hash表则提供了一种快速简单的思路,实现这个效果。

简单的链地址法的hash表:

这里写图片描述

hash表的结构其实很简单,就是一个较大的线性表,然后你去将key用一个函数计算一下,让结果分布到整个线性表中。由于hash函数一般都没有什么实际含义,一般希望 分布均匀,具有雪崩效应 (输入变化小,输出变化大),这样的映射函数就比较好了。

字符串hash——Times33

hash算法的基础算法之一,将字符串作为索引,存储key-value型数据。

而在字符串hash中, Times33 算法是一种著名的算法。

算法很简单,就是在不断的×33

int hash(String str)
{int hash_ans = 0;for (int i = 0; i< str.length; ++i)hash_ans = hash_ans*33 + str[i];return hash_ans;
}

一般来说,这个算法并没有什么特别深的理论依据,主要是经验性质的判断,对小心英文序列,33这个系数比较合适,对于大小写混合的,65比较好,还有使用31和131作为系数的版本,也都比较常见。

字符串hash——ELFhash算法

这是另外一种流行的hash算法,转自网友 阳光四季 的博文:http://blog.csdn.net/zhccl/article/details/7826137/

    // ELF Hash Function  unsigned int ELFHash(char *str)  {  unsigned int hash = 0;  unsigned int x = 0;  while (*str)  {  hash = (hash << 4) + (*str++);//hash左移4位,把当前字符ASCII存入hash低四位。   if ((x = hash & 0xF0000000L) != 0)  {  //如果最高的四位不为0,则说明字符多余7个,现在正在存第7个字符,如果不处理,再加下一个字符时,第一个字符会被移出,因此要有如下处理。  //该处理,如果最高位为0,就会仅仅影响5-8位,否则会影响5-31位,因为C语言使用的算数移位  //因为1-4位刚刚存储了新加入到字符,所以不能>>28  hash ^= (x >> 24);  //上面这行代码并不会对X有影响,本身X和hash的高4位相同,下面这行代码&~即对28-31(高4位)位清零。  hash &= ~x;  }  }  //返回一个符号位为0的数,即丢弃最高位,以免函数外产生影响。(我们可以考虑,如果只有字符,符号位不可能为负)  return (hash & 0x7FFFFFFF);  }  

hash几乎都会有冲突

除了放少量几个元素外,大部分哈希函数都不能保障,两个不一样的输入,得出相同的hash值,虽然这个概率很小,但毕竟名称空间很大,难免冲突。

所以hash表重要的一环就是冲突的解决。

开地址法

这个方法是一种非常简单的方法,例如下面这个例子,Tom和Lucy两个字符串经过hash后(这里只是假设)冲突了,他们的hash值恰好都是3,那么开地址法怎么做呢?先存放的就先占上这个位置,后放的Lucy发现没地方了,就一直往后找,找的第一个空位,就坐下了,占上了4这个位置。注意一点,开地址法也必须要把原来的key存放起来。

这里写图片描述

如果又有一个hash为4的元素来了怎么办?继续后移啊,要是到了末尾还没发现空位,就再回到头开始找,但如果找了一圈都没发现空位,那么说明表满了。

查找时,算出对应的位置,然后从这个位置一直往后找,直到找到这个元素为止,或者碰到空停下,或者全满时绕了一圈发现没有。

这种方法虽然简单,但效率确实不高,在不存在删除的情况下还比较好,但如果存在删除的情况时,那删除时,就必须向后找到最后一个对应元素,然后将其挪到当前位置上,否则,这个寻找的链就因为删除时的空断开了。

这个方法被成为 开地址法 ,也叫 线性探测法

新地址: Hn=(H+1)
H 是当前地址, Hn是下一次要探测的新地址

开地址法的改进——二次探测法和随机探测法

由于开地址法存在一种情况,就是本来没冲突的两个元素,有可能碰撞在一起,这种情况叫做 堆积 。为了避免堆积情况的产生,有人采用了这样的算法,往后找平方序列个元素,并且是往后找一次,往前找一次,跳跃检测的步数序列是这样的:1, -1, 4, -4, 9, -9……

新地址: Hn=(Hkey+d) d=1, -1, 4, -4, 9, -9……

这样往后看一下,往前看一下,再往后,再往前,确实要比线下查找分布均匀一些,但计算依旧比较复杂。

随机探测法,就是说这的d序列,是一个随机序列,虽说是随机序列,但这随机序列的种子我们是知道的,所以可以确定好这个hash表中所使用的序列是什么,这种方法就是随机探测。

链地址法

这也是最简单的方法之一,也很常用,如果有冲突,那么就在这个位置挂一个链表,一直顺着节点往下找。当然,这个链表查找时的复杂度就是线性的了。注意一点,链表中节点的头存放的都是原来的key值,用来判断是否为同一元素用的。

这里写图片描述

这样做的好处,其实显而易见,相比开地址法,这种挂链表的方式虽然更加繁琐,需要添加链表的操作,但也更加稳妥,效率较为稳定,各个地址间不会相互干扰,删除也十分方便。

hash桶

链表最大的问题可能在于随机访问速度较慢,如果想优化一下,那么可以使用类似块状链表的思路来实现。
块状链表我们第一节就有提到,块状链表能大大提升随机访问速度,是链表和线性表的混合产物,hash桶就是这种块状实现,用一个节点中放入几条记录的方式,就能处理有限条冲突,如果冲突实在太多,再想后面挂链表节点,再开空间。

由于hash表设计的好时,冲突本身就不多,那么hash桶实际上是一种高效优雅的解决方案。

可扩展hash表

一般情况,hash表的大小要开最大数据量的两倍左右,这样才能尽量避免冲突。
但由于有些情况,我们不知道hash表要存储的数据最大量,所以我们需要一种更灵活的方式来处理。

当我们发现hash表空间不够时,那么我们就将hash表增大一倍,方法同线性表,然后新进入的元素映射到新的空间,如果再不够,再将表扩大一倍,然后将元素映射到新空间。

这里写图片描述

查找时,先找最新的空间有没有,如果没找到,再递归往左半边找,期间不断变化对应的hash函数即可。
注意一点,插入时,不能直接插入,需要先向左半边查找,确认元素不存在时,再插入右半边。

这篇关于西风的数据结构教程(4)——hash表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/738766

相关文章

C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)

《C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)》本文主要介绍了C#集成DeepSeek模型实现AI私有化的方法,包括搭建基础环境,如安装Ollama和下载DeepS... 目录前言搭建基础环境1、安装 Ollama2、下载 DeepSeek R1 模型客户端 ChatBo

JAVA集成本地部署的DeepSeek的图文教程

《JAVA集成本地部署的DeepSeek的图文教程》本文主要介绍了JAVA集成本地部署的DeepSeek的图文教程,包含配置环境变量及下载DeepSeek-R1模型并启动,具有一定的参考价值,感兴趣的... 目录一、下载部署DeepSeek1.下载ollama2.下载DeepSeek-R1模型并启动 二、J

Vue项目的甘特图组件之dhtmlx-gantt使用教程和实现效果展示(推荐)

《Vue项目的甘特图组件之dhtmlx-gantt使用教程和实现效果展示(推荐)》文章介绍了如何使用dhtmlx-gantt组件来实现公司的甘特图需求,并提供了一个简单的Vue组件示例,文章还分享了一... 目录一、首先 npm 安装插件二、创建一个vue组件三、业务页面内 引用自定义组件:四、dhtmlx

MySQL zip安装包配置教程

《MySQLzip安装包配置教程》这篇文章详细介绍了如何使用zip安装包在Windows11上安装MySQL8.0,包括下载、解压、配置环境变量、初始化数据库、安装服务以及更改密码等步骤,感兴趣的朋... 目录mysql zip安装包配置教程1、下载zip安装包:2、安装2.1 解压zip包到安装目录2.2

Java使用Tesseract-OCR实战教程

《Java使用Tesseract-OCR实战教程》本文介绍了如何在Java中使用Tesseract-OCR进行文本提取,包括Tesseract-OCR的安装、中文训练库的配置、依赖库的引入以及具体的代... 目录Java使用Tesseract-OCRTesseract-OCR安装配置中文训练库引入依赖代码实

Ubuntu固定虚拟机ip地址的方法教程

《Ubuntu固定虚拟机ip地址的方法教程》本文详细介绍了如何在Ubuntu虚拟机中固定IP地址,包括检查和编辑`/etc/apt/sources.list`文件、更新网络配置文件以及使用Networ... 1、由于虚拟机网络是桥接,所以ip地址会不停地变化,接下来我们就讲述ip如何固定 2、如果apt安

PyCharm 接入 DeepSeek最新完整教程

《PyCharm接入DeepSeek最新完整教程》文章介绍了DeepSeek-V3模型的性能提升以及如何在PyCharm中接入和使用DeepSeek进行代码开发,本文通过图文并茂的形式给大家介绍的... 目录DeepSeek-V3效果演示创建API Key在PyCharm中下载Continue插件配置Con

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

在不同系统间迁移Python程序的方法与教程

《在不同系统间迁移Python程序的方法与教程》本文介绍了几种将Windows上编写的Python程序迁移到Linux服务器上的方法,包括使用虚拟环境和依赖冻结、容器化技术(如Docker)、使用An... 目录使用虚拟环境和依赖冻结1. 创建虚拟环境2. 冻结依赖使用容器化技术(如 docker)1. 创

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要