初识Hash算法

定义

  • Hash,也称“哈希”或者“散列”,是一种重要的存储方式,也是一种常见的检索方法。散列方法的主要思想是根据结点的关键码值来确定其存储地址:以关键码值K为自变量,通过一定的函数关系h(K)(称为散列函数),计算出对应的函数值来,把这个值解释为结点的存储地址,将结点存入到此存储单元中。检索时,用同样的方法计算地址,然后到相应的单元里去取要找的结点。

为什么需要使用Hash?

  • 通过散列方法可以对结点进行快速检索。Hash算法较多的应用在数据存储和查找领域,最经典的就是Hash表,它的查询效率非常之高,哈希算法如果设计的比较出色的话,Hash表的数据查询时间复杂度可以接近于O(1)

初步理解Hash算法由来

需求:提供一组数据 1,5,7,6,3,4,8,对这组数据进行存储,然后随便给定一个数n,请你判断n是否存在 于刚才的数据集中?

  • 最简单就能想到可以弄一个List,然后遍历进行判断,如下
public static void main(String[] args) {
        int n = 7;
        List<Integer> list = Arrays.asList(1, 5, 7, 6, 3, 4, 8);
        for (Integer num : list) {
            if (n == num) {
                System.out.println(n + "在集合中");
            }
        }
    }
  • 以上这种方法叫做顺序查找法 :这种方式我们是通过循环来完成,比较原始,效率也不高
  • 进一步优化可以用 二分查找:排序之后折半查找,相对于顺序查找法会提高一些效率,但是效率也并不是特别好
  • 可不可以不循环!不二分!而是通过一次查询就把数据n从数据集中查询出来,下面改变一下思路

在这里插入图片描述

  • 如上图定义一个数组,数组⻓度大于等于数据集⻓度,此处⻓度为9,数据1就存储在下标为1的位置,3就存储 在下标为3的元素位置,,,依次类推。这个时候,我想看下5存在与否,只需要判断list.get(5)是否为空,如果为空,代表5不存在于数据集,如果不为空代表5在数据集当中,通过一次查找就达到了目的,时间复杂度为O(1)。

  • 这种方式叫做“直接寻址法”:直接把数据和数组的下标绑定到一起,查找的时候,直接array[n]就取出了数据,但是这种方式有如下优缺点
    优点:速度快,一次查找得到结果
    缺点:

    • 浪费空间,比如 1,5,7,6,3,4,8,12306 ,最大值12306 ,按照上述方式需要定义一个比如⻓度为12307的数组,但是只存储零星的几个数据,其他位置空间都浪费着,
    • 数据如:1,5,7,6,3,4,8,12,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2最大值12,比如开辟13个空间,存储不了这么多内容
  • 现在,换一种设计,如果数据是3,5,7,12306,一共4个数据,我们开辟任意个空间,比如5个,那 么具体数据存储到哪个位置呢,我们可以对数据进行求模(对空间位置数5),根据求模余数确定存储 位置的下标,比如3%5=3,就可以把3这个数据放到下标为3的位置上,12306%5=1,就把12306这个 数据存储到下标为1的位置上
    在这里插入图片描述

  • 上面对数据求模 (数据%空间位置数) 就是一个hash算法,只不过这是一种比较普通又简单的hash 算法,这种构造Hash算法的方式叫做除留余数法

  • 如果数据是1,6,7,8,把这4个数据存储到上面的数组中
    在这里插入图片描述

如何解决冲突?
  1. 开放寻址法:1放进去了,6再来的时候,向前或者向后找空闲位置存放,不好的地方,如果数组⻓度定义好了比如10,⻓度不能扩展,来了11个数据,不管Hash冲突不冲突,肯定存不下这么多数据
  2. 拉链法:在数组元素存储位置放置一个链表,如下
    在这里插入图片描述
  • 拉链法同样有一个缺点,如果hash算法设计不合理的话,所有元素都放到了一个链表中,查询效率就会降低为O(n),如下,
    在这里插入图片描述
Hash算法种类
  1. 除余法:

顾名思义,除余法就是用关键码x除以M(往往取散列表长度),并取余数作为散列地址。除余法几乎是最简单的散列方法,
散列函数为: h(x) = x mod M。

  1. 乘余取整法:

使用此方法时,先让关键码key乘上一个常数A (0< A < 1),提取乘积的小数部分。然后,再用整数n乘以这个值,对结果向下取整,把它做为散列的地址。散列函数为: hash ( key ) = _LOW( n × ( A × key % 1 ) )。 其中,“A × key % 1”表示取 A × key 小数部分,即: A × key % 1 = A × key - _LOW(A × key), 而_LOW(X)是表示对X取下整

  1. 平方取中法

由于整数相除的运行速度通常比相乘要慢,所以有意识地避免使用除余法运算可以提高散列算法的运行时间。平方取中法的具体实现是:先通过求关键码的平方值,从而扩大相近数的差别,然后根据表长度取中间的几位数(往往取二进制的比特位)作为散列函数值。因为一个乘积的中间几位数与乘数的每一数位都相关,所以由此产生的散列地址较为均匀

关键字关键字的平方哈希函数值
12341522756227
21434592449924
413217073424734
321410329796297
  1. 数字分析法

假设关键字集合中的每个关键字都是由 s 位数字组成 (u1, u2, …, us),分析关键字集中的全体,并从中提取分布均匀的若干位或它们的组合作为地址。数字分析法是取数据元素关键字中某些取值较均匀的数字位作为哈希地址的方法。即当关键字的位数很多时,可以通过对关键字的各位进行分析,丢掉分布不均匀的位,作为哈希值。它只适合于所有关键字值已知的情况。通过分析分布情况把关键字取值区间转化为一个较小的关键字取值区间。
举例:要构造一个数据元素个数n=80,哈希长度m=100的哈希表。不失一般性,我们这里只给出其中8个关键字进行分析,8个关键字如下所示:
K1=61317602 K2=61326875 K3=62739628 K4=61343634
K5=62706815 K6=62774638 K7=61381262 K8=61394220
分析上述8个关键字可知,关键字从左到右的第1、2、3、6位取值比较集中,不宜作为哈希地址,剩余的第4、5、7、8位取值较均匀,可选取其中的两位作为哈希地址。设选取最后两位作为哈希地址,则这8个关键字的哈希地址分别为:2,75,28,34,15,38,62,20。
此法适于:能预先估计出全体关键字的每一位上各种数字出现的频度。

  1. 基数转换法

将关键码值看成另一种进制的数再转换成原来进制的数,然后选其中几位作为散列地址。
例Hash(80127429)=(80127429)13=8137+0136+1135+2134+7133+4132+2*131+9=(502432641)10如果取中间三位作为哈希值,得Hash(80127429)=432为了获得良好的哈希函数,可以将几种方法联合起来使用,比如先变基,再折叠或平方取中等等,只要散列均匀,就可以随意拼凑。

  1. 折叠法

有时关键码所含的位数很多,采用平方取中法计算太复杂,则可将关键码分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址,这方法称为折叠法。

  • 移位叠加:将分割后的几部分低位对齐相加。
  • 边界叠加:从一端沿分割界来回折叠,然后对齐相加
冲突解决方式

尽管散列函数的目标是使得冲突最少,但实际上冲突是无法避免的。因此,我们必须研究冲突解决策略。冲突解决技术可以分为类:

  • 开散列方法( open hashing,也称为拉链法,separate chaining )
  • 闭散列方法( closed hashing,也称为开地址方法,open addressing )。

这两种方法的不同之处在于:开散列法把发生冲突的关键码存储在散列表主表之外,而闭散列法把发生冲突的关键码存储在表中另一个槽内。

  1. 分离链表法(拉链法)

开散列方法的一种简单形式是把散列表中的每个槽定义为一个链表的表头。散列到一个特定槽的所有记录都放到这个槽的链表中。下图说明了一个开散列的散列表,这个表中每一个槽存储一个记录和一个指向链表其余部分的指针。这7个数存储在有11个槽的散列表中,使用的散列函数是h(K) = K mod 11。数的插入顺序是77、7、110、95、14、75和62。有2个值散列到第0个槽,1个值散列到第3个槽,3个值散列到第7个槽,1个值散列到第9个槽。

在这里插入图片描述

  1. 闭散列方法(开放地址法)
  • 闭散列方法把所有记录直接存储在散列表中。每个记录关键码key有一个由散列函数计算出来的基位置,即h(key)。如果要插入一个关键码,而另一个记录已经占据了R的基位置(发生碰撞),那么就把R存储在表中的其它地址内,由冲突解决策略确定是哪个地址。
  • 闭散列表解决冲突的基本思想是:当冲突发生时,使用某种方法为关键码K生成一个散列地址序列d0,d1,d2,… di ,…dm-1。其中d0=h(K)称为K的基地址地置( home position );所有di(0< i< m)是后继散列地址。当插入K时,若基地址上的结点已被别的数据元素占用,则按上述地址序列依次探查,将找到的第一个开放的空闲位置di作为K的存储位置;若所有后继散列地址都不空闲,说明该闭散列表已满,报告溢出。相应地,检索K时,将按同值的后继地址序列依次查找,检索成功时返回该位置di ;如果沿着探查序列检索时,遇到了开放的空闲地址,则说明表中没有待查的关键码。删除K时,也按同值的后继地址序列依次查找,查找到某个位置di具有该K值,则删除该位置di上的数据元素(删除操作实际上只是对该结点加以删除标记);如果遇到了开放的空闲地址,则说明表中没有待删除的关键码。因此,对于闭散列表来说,构造后继散列地址序列的方法,也就是处理冲突的方法。
  • 形成探查的方法不同,所得到的解决冲突的方法也不同。下面是几种常见的构造方法。
  • 1. 线性探测法

将散列表看成是一个环形表,若在基地址d(即h(K)=d)发生冲突,则依次探查下述地址单元:d+1,d+2,…,M-1,0,1,…,d-1直到找到一个空闲地址或查找到关键码为key的结点为止。当然,若沿着该探查序列检索一遍之后,又回到了地址d,则无论是做插入操作还是做检索操作,都意味着失败。 用于简单线性探查的探查函数是: p(K,i) = i

  • 2. 二次探查法

二次探查法的基本思想是:生成的后继散列地址不是连续的,而是跳跃式的,以便为后续数据元素留下空间从而减少聚集。二次探查法的探查序列依次为:12,-12,22 ,-22,…等,也就是说,发生冲突时,将同义词来回散列在第一个地址的两端。求下一个开放地址的公式为:

在这里插入图片描述

  • 3.随机探查法
  • 理想的探查函数应当在探查序列中随机地从未访问过的槽中选择下一个位置,即探查序列应当是散列表位置的一个随机排列。但是,我们实际上不能随机地从探查序列中选择一个位置,因为在检索关键码的时候不能建立起同样的探查序列。然而,我们可以做一些类似于伪随机探查( pseudo-random probing )的事情。在伪随机探查中,探查序列中的第i个槽是(h(K) + ri) mod M,其中ri是1到M - 1之间数的“随机”数序列。所有插入和检索都使用相同的“随机”数。探查函数将是 p(K,i) = perm[i - 1], 这里perm是一个长度为M - 1的数组,它包含值从1到M – 1的随机序列。
  • 例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元,参图8.26 (a)。如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元,参图8.26 (b)。如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,………,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元,参考下图。

在这里插入图片描述

  • 4. 双散列探查法
  • 伪随机探查和二次探查都能消除基本聚集——即基地址不同的关键码,其探查序列的某些段重叠在一起——的问题。然而,如果两个关键码散列到同一个基地址,那么采用这两种方法还是得到同样的探查序列,仍然会产生聚集。这是因为伪随机探查和二次探查产生的探查序列只是基地址的函数,而不是原来关键码值的函数。这个问题称为二级聚集( secondary clustering )。
  • 为了避免二级聚集,我们需要使得探查序列是原来关键码值的函数,而不是基位置的函数。双散列探查法利用第二个散列函数作为常数,每次跳过常数项,做线性探查。
  • 以上内容部分出自:https://www.jianshu.com/p/f9239c9377c5
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值