Hash算法初学习

关于学习目的

(每日一图)
在这里插入图片描述

在算法题的学习和,发现很多类型的问题都可以用哈希算法来解决,而且更快,所以决心学习一下。当然呢,在学习分过程中我也意识到了一些基础的算法分析的重要性,所以我决定下一步就转向数据结构与算法分析的同步学习。

概念

哈希表就是一种以键 – 值(key-indexed)存储数据的结构,输入key即可查找到其对应的值。
正如我在第一篇里面简单应用时所列举出来的一个简单的表结构:

学号姓名储存位置
202101小红1
202102小明2
202103小智3

也就是储存位置=学号-202100,也就是对应的键值为1时,对应的值就为202100+1=202101。当然如果所有的问题都如此简单就好了。
由于在不同情况下,不同的标准下,我们遇到的并不只是这种单一的排序方法,比如,小红同学去参加了一个全国性的比赛,里面有很多来自其他地区的同学。然后用的排号标准是,以学号的个位数作为哈希表的键值。来参赛的有

学号参赛人员
202101小红
201904小芳
202003小a
202004小b
201804小c

向哈希表中存时,会出现:

学号key
2021011
02
2019033
2020033
2020044
05
2018055

因为哈希表是将元素数据储存在一段有限的连续空间里面,所以哈希表的key值按顺序往下排,但是按照上面的逻辑,在输入key值之后,无法准确的找出对应的值,这就是哈希冲突。
综上所述,哈希算法的思路十分简单,对于都是整数的键值,就可以用一个简单的无序数组来实现,即将键作为索引,值为其对应的值,这样就可以快速访问任意键的值。
(以下内容来自博主@汤高)
使用哈希查找有两个步骤:

  1. 使用哈希函数将被查找的键转换为数组的索引。在理想的情况下,不同的键会被转换为不同的索引值,但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况。所以哈希查找的第二个步骤就是处理冲突
  2. 处理哈希碰撞冲突。有很多处理哈希碰撞冲突的方法,本文后面会介绍拉链法和线性探测法。

哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。

处理哈希冲突

对于哈希算法已经有一个初步的了解了,那么该如何处理哈希冲突呢?现在主流的方法有两种:开放寻址法和拉链法。

开放寻址法:

首先是开放寻址法:先用一下那个例图

学号key
2021011
02
2019033
2020033
2020044
05
2018055

用开放寻址法怎么解决呢?既然位置被占了,那就换一个位置呗,因此利用这个方法,我们就可以得出下面这个表:

学号key
2021011
02
2019033
2020034
2020045
06
2018057

这就是开放寻址法,也就是Java中的ThreadLocal方法。
开放寻址法的做法是,当冲突发生时,使用某种探测算法在散列表中寻找下一个空的的散列地址,只要散列地址足够大空的散列地址总能被找到。一般呢又将开放寻址法分为线性探查法,二次探查法,双重散列法等

为了初步展示一下三种方法的效果,此处以一个以9为模的哈希表为例,采取除数留余法,向表中插入三个关键字分别为29,39,50.
插入后为:
(没错,我用word打的)
在这里插入图片描述
接着向其中插入数据39,那么此时就产生了冲突,因为按照道理,39因该分配在3的位置里,但此时3的位置已经被30占据了位置,那么针对这个地址冲突的问题,有以下三种探测方法。

(1)线性探查法
f[i]= (fkey+i) % m, 0 =<i=<m<=m-1

此时探查时从地址d开始进行探查,即首先探查T[d],接着是探查T[d+1],然后依次探查到T[m-1],然后再循环到T[0],T[1],…知道探查到有空余地址或者直到T[d-1]为止。
那么此时,39就会被插入到地址为4 的位置
在这里插入图片描述

其逻辑也十分容易理解,在这里不做赘述,假使说4,5位置都被占据,那么仍然按照往后移的规则,直至找到一个空位,将数据存放进去。

当然,这个方法的缺点也是很明显的,那就是:
需要不断的处理冲突(比如后续插入31,那么就会出现原本4的位置被39补位,导致31仍然需要被处理,包括后续查找时,也需要不断的去排除冲突产生的移位情况),存入和查找效率大大下降。

(2)二次探查法(也可以叫做平方探测法)
f[i]=(fkey+d[i])%m,0=<i<=m-1

探查地址时从d入手,首先探查T[d],然后依次探查T[d+d[i]],d[i]为增量序列1^2,-1^2,2^2,-2^2,……q^2,-q^2(q<=1/2(m-1)),直到探查到有空余地址或者直到T[d-1]为止。
在这里插入图片描述
那么插入39时,探查到3的位置被占据,即和3的位置产生冲突,那么开始进行二次探查
(以下计算中理论上应为(3+1)%9,但是这样写有些过于麻烦,且对结果和叙述影响不大,故省略)
3+1=4,与第4位数字冲突,pass
3-1=2,与第二位数字冲突,pass
3+4=7,无冲突,填入。
计算结束。
在这里插入图片描述
这就是二次探查法。

(3)伪随机探测法

di=伪随机数数列,具体实现时,建立一个伪随机数发生器,(如i = (1+p)%m),生成一个伪随机序列,并给定一个随机数做起点,每次加上这个伪随机数进行自增就可以了。(具体原理呢,和上个差不多)

拉链法(链地址法)

这种方法的关键是,把同一个散列槽(也就是数组中的每一个槽)中的所有元素放到一个链表中。具体解释就是,把具有相同散列地址的关键字(同义词)放入到一个单列表中,称为同义词链表,有m个散列地址就有m个链表,同时用指针数组T[0,M-1]存放各个链表的头指针,凡是散列地址为i的记录都以结点方式插入到T[i]为指针的单链表中,T中各分量的初值应该为初指针。
其实在Java中不管是HashMap还是HashSet都是采用拉链发来解决哈希冲突的。,就是在每个位桶是实现的时候,我们用链表的数据结构去存取产生哈希冲突的输入域的关键字(也就是被哈希函数映射到同一个桶位上的关键字)一般使用拉链法解决哈希冲突有以下几种方法。

①插入操作:在发生哈希冲突的时候,我们输入域的关键字去映射到位桶(实际上是实现位桶的这个数据结构,链表或者红黑树)中去的时候,我们先检查带插入元素x是否出现在表中,很明显,这个查找所用的次数不会超过装载因子(n/m:n为输入域的关键字个数,m为位桶的数目),它是个常数,所以插入操作的最坏时间复杂度为O(1)的。

②查询操作:和①一样,在发生哈希冲突的时候,我们去检索的时间复杂度不会超过装载因子,也就是检索数据的时间复杂度也是O(1)的

③删除操作:如果在拉链法中我们想要使用链表这种数据结构来实现位桶,那么这个链表一定是双向链表,因为在删除一个元素x的时候,需要更改x的前驱元素的next指针的属性,把x从链表中删除。这个操作的时间复杂度也是O(1)的。

与开放定址法相比,拉链法有如下几个优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
当然,这种方法也有其缺点,缺点如下:
拉链法的缺点
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

使用例子:HashMap
(好了,初步学习就先到这里,果然计划赶不上变化,突然多了好多事,把我原本的规划搞得乱七八糟,所以哈希算法的初步学习在断断续续一周后终于结束了,下一步呢就是在Java中先实现一下,然后继续补全)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值