图解:什么是哈希?

本文详细介绍了哈希算法的原理和应用,包括哈希表、哈希函数、哈希碰撞及其解决方案(链地址法、开放地址法、再哈希)。哈希算法在数据结构和算法中起着关键作用,可以实现高效的插入、查找和删除操作。通过实例和分析,展示了如何使用哈希解决冲突和优化性能。文章最后探讨了哈希在现实生活中的应用,如信息摘要、密码校验和负载均衡等。

为什么要有哈希?

假设我们要设计一个系统来存储将员工手机号作为主键的员工记录,并希望高效地执行以下操作:

  1. 插入电话号码和相应的信息。(插入)

  2. 搜索电话号码并获取信息。(查找)

  3. 删除电话号码及相关信息。(删除)

我们可以考虑使用以下数据结构来维护不同电话所对应的信息:

  1. 数组

  2. 链表

  3. 平衡二叉树(红黑树等等)

  4. 直接访问表

对于数组和链表而言,我们需要以线性的方式进行查找,这在实际应用中代价太大;如果我们使用数组且保证数组中电话号码有序排列,那么使用二分查找可将查找电话号码的时间复杂度降到 ,但同时由于要维持数组的有序性,插入和删除操作的代价将变大

对于平衡二叉查找树而言,插入、查找和删除操作的时间复杂度均为 ,这似乎已经看着很不错了,那么是否有更好的数据结构呢?

再来看直接访问表的方式,首先创建一个大数组(至少能够用电话号码作为数组下标),如果电话号码没有出现在数组当中,就在相应下标填充 NULL ;如果电话号码出现了,则下标中数据填充该电话号码关联的记录的地址。

这样一来,插入、查找和删除的时间复杂度将降为 ,比如插入一条记录(15002629900,0xFF0A “可爱的读者”),只需要将手机号码 15002629900 当做下标,然后在该下标的位置填充记录的地址,即 arr[15002629900] = 0xFF0A;

但是直接访问表有实践上的限制。首先需要申请额外的存储空间,并且存在大量的空间浪费。比如对于一个含有 n 位的电话号码,我们需要 的空间复杂度,其中 m 表示数据本身所占用的空间。另外一个问题是,编程语言本身提供的整型无法表示电话号码。

由于上述限制,使用直接访问表并不是最明智的方法。而哈希则是解决以上问题的最好数据结构,并且与上面所提到的数据结构(数组,链表,AVL树)在实践中相比,性能非常好。通过哈希,可以在 的时间复杂度内实现插入、查找和删除操作(在合理的假设下),最坏情况下为 的时间复杂度。

哈希是对直接访问表的改进。使用哈希函数将给定的电话号码或任何其他键转换为较小的数字,将该较小的数字称为哈希表的索引(哈希值)

什么是哈希表?

哈希表和直接访问表很类似,同样是一个用于存储指向给定电话号码对应记录的指针的数组,只不过,此时的数组下标不再是电话号码,而是经过哈希函数映射后的输出值。

什么是哈希函数?

哈希函数用于将一个大数(手机号码)或字符串映射为一个可以作为哈希表索引的较小整数的函数。比如活动开发中经常使用的 MD5 和 SHA 都是历史悠久的Hash算法。

[lovefxs@localhost ~]$ echo -n  "I love J"  | openssl md5 
(stdin)= ef821d9b424fd5f0aac7faf029152e04

一个好的哈希函数应该满足四个条件:

  1. 执行效率要高(效率高

对于一段很长的字符串或者二进制文本也能快速计算出哈希值。

  1. 散列结果应当具有同一性(输出值尽量均匀,越均匀冲突就越少)。(同一性

例如对于电话号码而言,一个好的哈希函数应该考虑电话号码的后四位,而一个糟糕的哈希算法可能考虑电话号码的前四位,因为后四位更有区分性,相当于输入更加分散,那么输出也可能更加均匀,当然这两种选择方式都不是什么好办法,只是希望大家理解同一性原理。

  1. 雪崩效应(微小的输入值变化使得输出值发生巨大的变化)。

[lovefxs@localhost ~]$ echo -n  "I love J"  | openssl md5 
(stdin)= ef821d9b424fd5f0aac7faf029152e04
[lovefxs@localhost ~]$ echo -n  "I love Y"  | openssl md5 
(stdin)= fb49af24bebae07658650ae8eb1c0f5b

其中输入 I love JI love Y 只改变了一个字母,输出值却千差万别。

  1. 从哈希函数的输出值不可反向推导出原始的数据。(不可反向推导

比如上面的原始数据 I love J  与经过 MD5 算法映射后的输出值之间没有对应关系。

什么是 Hash 碰撞?

由于哈希函数的原理是将输入空间的一个较大的值映射成 hash 空间内一个较小的值,那么就会出现两个不同的输入值被映射到了同一个较小的输出值。当一个新插入的值被哈希函数映射到了哈希表中一个已经被占用的槽,就认为产生了 Hash 碰撞(冲突)。

那么这种冲突是否可以避免呢?

答案是只能缓解,不可避免。

由于哈希函数的原理是将输入空间一个较大的值映射到一个较小的 Hash 空间内,而 Hash空间一般远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成同一输出的情况。

何为抽屉原理?

桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是“抽屉原理”。抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原理

知道了为什么哈希碰撞不可避免,那哈希碰撞该如何缓解呢?

Hash 碰撞的解决方案?
  1. 链地址法

  2. 开放地址法

  3. 再哈希

链地址法

链地址法的思想就是将所有发生碰撞的元素用一个单链表串起来。

我们以一个简单的哈希函数 H(key) = key MOD 7 (除数取余法)对一组元素 [50, 700, 76, 85, 92, 73, 101] 进行映射,来理解链地址法处理 Hash 碰撞。

除数为 7 ,初始化一个大小为 7 的空 Hash 表:

然后插入元素 50 ,首先对 50 % 7 = 1 ,得到其哈希值 1 ,在下标为 1 的位置插入 50 :

然后计算 700 % 7 = 076 % 7 = 6 ,得到的哈希值均未发生碰撞,填入相应位置:

然后计算 85 % 7 = 1 , 但是下标为 1 的位置已经有元素 50 ,发生了碰撞,所以使用单链表将 8550 链接起来:

以同样的方式插入所有元素得到如下的哈希表。链地址法解决冲突的方式与图的邻接表存储方式在样式上很相似,思想还是蛮简单,发生冲突,就用单链表组织起来。

链地址法实现

首先创建一个空的哈希表,哈希表的表长为

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值