概念:哈希即可以是一种数据结构,也可以是一种函数概念

通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

哈希算法不过是一个均匀的运算,它的输入可以是字符串,可以是数据,可以是任何文件,经过哈希运算后,变成一个固定长度的输出, 该输出就是哈希值。但是哈希算法有一个很大的特点,就是你不能从结果推算出输入,所以又称为不可逆的算法

哈希的特性

  • 不可逆 : 就如同你可以通过x*y=z得到z,但你不能确定z=x*y,xy一定刚刚的数
     
  • 运算快:20G高清电影和一个5K文本文件复杂度相同,计算量都极小。越巧妙的hash函数碰撞越少,空间利用率越高
  • 结果均匀:哈希函数计算出来的地址能均匀分布在整个空间中,这时hash函数的设计原则

常见哈希函数

直接定制法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B优点:简单、均匀缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况。

除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

哈希冲突

哈希冲突是不可避免的

当计算出的hash值相同时,就会发生哈希冲突。常用的解决哈希冲突的方法有两种:

1.闭散列(开放定址法)

当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

其中最简单的就是线性探测

  • 若没有冲突则直接插入值
  • 若有冲突则向后查找至空位插入值

 但这么做有弊端:

一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”(查找时会多次重复比较,大大降低查找效率)即“踩踏效应”

二次探测线性探测的缺陷是产生冲突的数据堆积在一块,这和找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,他不再是挨着找下一个空位置,而是平方式的跳跃找下一个空位置,这样冲突就不会堆积在一片,而是会相对散开一些。

一个新的知识点:载荷因子

α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小
通常,只要a取的合适(一般取0.7-0.8之间),哈希表的平均查找长度就会是常数也就是O(1)级别的。

 Golang语言中map结构的载荷因子为6.5 但长度为8

闭散列(开放定址法)的删除

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索

好比上面图中的数字11,删除11之后,查找5却找到了11的位置,查询11没有数字,就会误报。

闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷

2.开散列   又名链地址法(开链法)

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

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

GO的map底层bucket桶即使用这个方法 

那如果就是出现了极端的情况,所有的数此时都冲突到一个桶中,那么这个桶中的数据就会太多了,应该怎么办?

  1. 将此时的链表改换挂红黑树
  2. 多阶哈希(不常使用)

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐