【夜深人静写数据结构与算法 | 第八篇】哈希算法与哈希表

目录

前言:

哈希: 

哈希表:

哈希表组成:

哈希表实例:

哈希函数:

 TIPS:

总结


前言:

        如果此时我要你默写一个有一百位的数字,你要如何做才能保证不会漏写呢?我们有一种方法很好用:直接数我们写的数字有没有100个就好了,而这种把长的信息串压缩起来的方法就叫做哈希。

哈希: 

        哈希算法是一种将任意长度的信息压缩到一个固定长度的摘要信息的算法。哈希算法将原始数据映射成一段长度固定、内容任意的二进制字符串,该字符串通常称为哈希值、消息摘要或数字指纹。

注意点:有人看到了哈希可以转换信息的优点,便认为哈希就是加密其实这种思想是错误的

因为加密是可逆的,而哈希是不可逆的,我们可以通过加密过的文件解密得到本体,但是无法通过哈希值得到本体。 

举例:

        网站账户的密码存储就是一个典型的哈希过程。当我们创建一个网站账户时,通常会设置一个密码。为确保安全性,网站通常会先使用哈希算法将密码字符串转换成一个哈希值,并将这个哈希值存储在服务器上。这样,即使黑客攻击了网站,也无法得到我们的明文密码。我们输入密码登录时,网站会再次对我们输入的密码进行哈希,然后将结果与存储的哈希值进行比较。如果结果匹配,则认为密码正确,允许我们登录。

举个简单的例子,假设我们在某个网站注册时,设置的密码为 “password123”,网站使用SHA256算法将其哈希成 “d3c59d25033dbf980d29554025c23a75” 并存储在服务器上。当我们登录时,我们输入密码 “password123”,网站再次使用SHA256算法将其哈希成 “d3c59d25033dbf980d29554025c23a75”,并发现与之前存储的哈希值相同,于是认为我们输入的密码是正确的,允许我们登录。由于哈希算法的不可逆性,黑客即使获取了存储的哈希值,也无法反向推导得到我们的密码。

哈希表:

而我们基于哈希这种算法,设计了一种数据结构:哈希表(散列表)

        哈希表是一种基于哈希算法实现的数据结构,也称为散列表。哈希表通过将元素的关键字映射到表中一个位置来支持高效的元素查找、插入和删除操作。哈希表通常由数组和哈希函数两部分组成。数组用于存储元素,哈希函数则将元素的关键字映射到数组中一个位置。

哈希表的查找操作非常快速,因为不需要对所有元素进行遍历,只需要先通过哈希函数计算出元素在数组中的位置,然后直接访问该位置即可。哈希表支持的插入和删除操作也非常高效,通常只需要做一次哈希计算和一次数组访问。

        然而,哈希表的性能也受到哈希冲突(多个元素被映射到同一个位置)的影响。为解决哈希冲突,通常会使用一些常用的解决方法,比如开放地址法和链表法。

        哈希表在现实中有广泛的应用,比如编译器符号表、字典、缓存、数据库索引等。哈希表不仅具有高效的数据操作特性,还具有空间效率高、易于扩展、支持动态增删的优点。

哈希表组成:

哈希表由两部分组成:数组和哈希函数。

  • 1. 数组:哈希表通常由一个数组来实现,它用于存储元素。数组的大小取决于哈希表需要存储多少元素。
  • 2. 哈希函数:哈希函数是将元素关键字转换为数组下标的映射函数。哈希函数的设计非常关键,因为它的好坏直接影响哈希表的性能。哈希函数应该能够将关键字均匀地散布到数组中,并且产生最小的冲突。

除了数组和哈希函数,哈希表还可能包含链表(或其他数据结构),用于解决哈希冲突。基于链表的哈希表通常称为哈希链表,当多个元素被哈希到同一个数组下标时,它们可以被放置在同一个链表中。

需要注意的是,哈希表并不保证元素的顺序,因此在使用哈希表时,不能依赖元素的顺序,而是要根据元素的关键字来进行数据操作。

哈希表实例:

在这里我们用实图解决一下什么是用链表解决的哈希值:
假设我们需要存储各种动物下的蛋需要我们归类,我们一般会想直接用数组存储:

 但是这就有一个问题:每一次寻找蛋都需要我们遍历数组,因为我们并不知道哪一个下标存储的是哪一个蛋。

这个时候有的聪明的人就提出了:那我记忆一下不就好了,0是鸡蛋,1是乌龟蛋,2是鸵鸟蛋。。。。。我们下次找蛋的时候直接查询一下这个记录,不就不用遍历数组了,也就是我下次要找乌龟蛋,我就直接找乌龟蛋的记录:乌龟蛋的对应的下标是1,那我直接提取数组中下标是1的蛋不就好了。

而这种把一个文本转化成为另外一个文本,方便我们使用的过程,就是哈希

但是如果再下一个鸡蛋呢?它不就与第一个位置的鸡蛋冲突了?我们把这种冲突叫做哈希冲突(哈希冲突是指多个不同的元素被哈希函数映射到同一个数组下标的情况)

解决哈希冲突的方法主要有以下两种:

        1. 链表法:将哈希表中冲突的元素放在同一个桶(数组位置)下,并通过链表(或其他数据结构)进行连接。当查询元素时,首先使用哈希函数计算出元素对应的桶,然后遍历该桶中的链表,找到对应的元素。链表法可以解决冲突,但在链表过长时查找效率会降低。

        2. 开放地址法:在发生哈希冲突时,通过重新计算哈希函数并找到下一个可用的桶。不同的开放地址法有不同的决策方法来决定下一个可用的桶,比如线性探测、二次探测和双重哈希等。开放地址法相对于链表法来说,具有更好的内存缓存性能,但适用于装载因子比较小(大于0.7时,性能开始下降)的哈希表。

一些其他的解决哈希冲突的方法还包括:再哈希法、伪随机序列法、公共溢出区法等。这些方法各有优缺点,选择合适的解决哈希冲突的方法需要根据实际应用场景和数据规模进行权衡。

链表法:
 

但是用链表来解决哈希冲突也有一定的危险性:可能会出现数据都集中在几个链上的情况,那么哈希表快速查找的功能岂不是基本丧失了?因此在实际开发中我们不会使用这种方法进行存储。

 开放地址法:
如果此时在下一个蛋,这个蛋的位置上已经有旧蛋了,那么我们就再找一个空间就好了。

不过这种方法还是会让相同的数据聚集在一起,因此我们的开放地址法这个寻址的过程,我们又设计了二次探测方法。 

以上只是一个演示,让我们更好的理解哈希函数,但实际生活中我们通常不会以汉字这种形式来做哈希函数的结果。

哈希函数:

哈希函数是哈希表存储和查找的核心,它的好坏决定了哈希表的性能。一个好的哈希函数应该具有以下特点:

1. 高效:哈希函数的计算速度应该非常快,否则就会降低哈希表的性能。
2. 均匀:哈希函数应该将元素的关键字均匀地散布到哈希表中不同的数组下标中,以避免哈希冲突。
3. 独特:哈希函数计算出来的哈希值应该尽可能的独特,以防止不同元素在哈希表中映射到同一个数组下标,并且哈希冲突的概率要尽可能小。

如果哈希函数设计得好,就可以在常数时间内计算出对应元素的哈希值,并将元素存储到相应的位置中。而如果哈希函数不好,就可能导致冲突严重、存储效率低下,或者查询、插入或删除元素的性能变得非常低。

在实际应用中,哈希函数也需要考虑到哈希表的负载因子、元素的数据类型和数据分布等因素。一些常见的哈希函数包括:简单取模法、乘法哈希法、SHA哈希法等。

        1. 简单取模法:这是一种最简单的哈希函数,它的计算公式是将元素关键字除以哈希表的大小,然后将余数作为该元素的哈希值。简单取模法的优点是计算速度快,但如果哈希表的大小和元素的数目不匹配,就容易产生哈希冲突。为了减少冲突,在选择哈希表的大小时应该选择一个质数。

        2. 乘法哈希法:这种哈希函数计算元素哈希值的公式是:h(k) = floor(m * (kA mod 1)),其中k是元素关键字,A是一个介于0和1之间的常数,m是哈希表的大小,"floor"表示向下取整。这种方法相对于简单取模法来说,生成的哈希值更加分散,冲突率更低。

        3. SHA哈希法:SHA(Secure Hash Algorithm,安全哈希算法)是一种加密哈希函数,通常用于对数据进行不可逆的加密处理。SHA算法产生的哈希值通常是固定长度的,同时它生成的哈希值是基于输入数据唯一的,即使输入数据稍有变化,也会产生截然不同的哈希值。基于SHA哈希函数的哈希表在安全性和随机性方面都具有很好的性质,但计算相对较慢,不适用于哈希表中元素较多的情况。

 TIPS:

        虽然我们把一个数字经过哈希加密之后无法逆向得到原来的数字,但是我们还是可以通过撞库的方式来得到这个数字(这里我们采用的是SHA256加密):

        撞库这种手法很简单,就是暴力测试,就好比你想蹭到邻居家里的WIFI,那么你就有两种方式:1.直接去要密码。2.暴力测试,尝试所有可能的密码,直到密码正确为止。

而这种暴力测试所有密码,就是撞库。

加密后: 

不断进行撞库: 

 

也就是说虽然我们没有办法通过已经加密后的字符串来解密出数字1234,但是我们可以不断的实验所有的数字,肯定能够找到一个加密串跟这个串相等,那么我们就得到了密码。

总结

哈希算法是一个归类的算法,而由它组建起来的哈希表更是一个极其重要的数据结构,在很多高级语言中都有实现,因此我们要掌握好他的底层原理,才可以更好的运用这种数据结构形式。 

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

猜你喜欢

转载自blog.csdn.net/fckbb/article/details/131338233