java数据结构和算法09(哈希表)

  树的结构说得差很少了,如今咱们来讲说一种数据结构叫作哈希表(hash table),哈希表有是干什么用的呢?咱们知道树的操做的时间复杂度一般为O(logN),那有没有更快的数据结构?固然有,那就是哈希表;java

 

1.哈希表简介算法

  哈希表(hash table)是一种数据结构,提供很快速的插入和查找操做(有的时候甚至删除操做也是),时间复杂度为O(1),对比时间复杂度就能够知道哈希表比树的效率快得多,而且哈希表的实现也相对容易,然而没有任何一种数据结构是完美的,哈希表也是;哈希表最大的缺陷就是基于数组,由于数组初始化的时候大小是肯定的,数组建立后扩展起来比较困难;数组

  当哈希表装满了以后,就要把数据转移到一个更大的哈希表中,这会很费时间,并且哈希表不支持有顺序的遍历,由于从哈希表中遍历数据是随机的;因此咱们使用哈希表的前提是:不须要有序的遍历数据,能够大概知道数据量的多少;知足这两点就能够用哈希表;数据结构

  那有人就要问了,说得这么厉害,哈希表究竟是什么样子的啊?下面就随便说两个吧。。。函数

  很经典的例子就是英语字典,咱们查字典的时候能够根据这个单词就能够找到第xxx页,在这里该单词和页数就对应起来了,这能够说是一个哈希表;spa

  再举个现实中的例子,在上学的时候每一个人在学校里都会有一个学号,你这我的在学校中就对应这个学号,假如校长手上有一个记录全校学生的表,而后根据学号找一个学生时,就能很快锁定这个学生的姓名,性别,班级等信息;有没有想过假如没有学号的话,校长想找一个学生就只能根据姓名去找,但是同名同姓的人这么多,想找到目标学生不是一件容易的事。。。。。线程

  ok,在这里哈希表能够看做是校长手上的那个表(其实就是一个数组),咱们根据咱们要存的信息生成一个表中的位置的号码(在这里这个号码就是数组的下标),根据这个号码咱们就知道该数据存在数组的哪一个位置,而后将数据保存进去就能够了;假若有个大小为20的数组,我要存“aaa”,咱们能够想个很厉害的办法将这个字符串变成一个比较小的数字,好比是10,那么就把这个字符串存到数组的第10个位置,这样作的好处就是下次若是要从哈希表中查询(或删除)“aaa”这个字符串时,只须要将“aaa”字符串算出那个号码10,而后直接去数组中第10个位置找一个看有没有这个字符串,是否是很简单啊!blog

  因此如今咱们须要解决的就是想个很厉害的办法能够将字符串变成一个比较小的数字(这个过程叫作哈希化),还要保证这个数字不能超过数组的最大边界!字符串

 

2 哈希化hash

  哈希化就是想办法将咱们要保存的数据对应一个数组下标,在数组的该位置下保存数据;咱们能够把这个过程专业一点的说一下:把要保存的数据,经过哈希函数转化为对应的数组下标;如今咱们的目标就是怎么编写一个哈希函数可使得字符串变成数组下标;

  这里咱们能够假设一个字符串t数组的大小为30,String[] str = new String[30];   要存“cats”这个单词,最容易想到的办法就是用ASCII码,可是因为ASCII码太多了很差记,因而咱们能够本身设置一套规则,我就假设a到z分别对应1到26,外加空格对应0,如今一套最简陋的规则就出来了,我那么“cats”这个单词:c = 3,a = 1,t = 20,s = 19,如今“cats”有两种办法变成数组的下标;

  额外补充一下:假如咱们要保存的字符串有50个,那么咱们new的数组大小必定要是它的两倍大,即 new String[100];,后面会说到这个缘由

  2.1哈希函数实现一

  怎么实现比较好呢?别想那么多,直接相加就好,3+1+20+19 = 43,这个时候就有个小问题,咱们的数组的大小为30,也就是说数组下标最大值是29,而这里咱们的数字为43,怎么将43变成29之内的数(包括29)呢?由于任何数除以30的余数只都在0-29之间,因而咱们用43除以30拿到余数13,那么咱们就把”cats“放到数组下标为13的位置,str[13] = "cats";

  这种哈希函数的实现很容易,可是每每越容易的东西缺点就越大,最大的缺陷就是有不少单词变成数字是相同的,好比was,tin,give等100多个单词变成数字后都是43,而后咱们恰巧添加单词的时候就是这些单词,如今问题来了,多个单词最后算出来的数组下标很大几率上是同样的,也就是数组一个位置要放多个数据,怎么解决这个问题呢?咱们能够换一种哈希函数的实现来下降这个几率

  2.2 哈希函数实现二

  由2.1能够知道太多的单词变成数字的结果是同样的,那么咱们就要想办法为每个单词都对应一个独一无二的整数,而后用这个整数除以数组的大小取余数,就能够知道该单词在数组中的存放位置;

  因而啊,咱们能够利用幂的连乘来获得这个独一无二的整数,好比“cats”用这种计算方法:3*273+1*272+20*271+19*270,有点相似二进制变成十进制,经过这个算法,能够获得一个独一无二的整数,其余的任何单词经过这种方法算出来的结果几乎是不可能相等的,有兴趣的能够试试;而后将这个计算结果除以30取余数,就能够获得一个数组的位置,而后将该字符串丢到里面便可;

  不知道你们有没有发现这种方法的一个问题,由于数组的大小是必定的,并且咱们是经过取余数来获得数组的位置,那么问题来了,即便是两个不相同的整数分别除以30,最后的余数是相等的;

  就好比有两个字符串经过幂的连乘最后获得32和62(固然咱们这里确定不会获得这两个整数,为了好理解随便拿两个数),虽然这两个数是独一无二的,可是除以30余数都为2,那么两个数据要保存到哈希表中确定会有冲突,下后面咱们来解决一下这个冲突;

 

  有个简单的哈希函数实现看一下(虽然还能够进行修改一下,可是这个已经差很少了);

  

3.冲突

  冲突的缘由就是两个独一无二的整数除以数组的大小,取余数是相等的,而数组中一个位置只能存一个数据,这就致使了冲突,解决冲突的办法有两种;

  3.1 解决方法一(开放地址法)

  还记得前面说过数组的大小要为实际数量的两倍吗?就是为了这个时候用的,假如一个单词已经放在了数组的第15个位置那里,另一个单词原本也要放在第15的位置,因为这个位置已经被别人占了。那就放在数组的另一个位置上,反正还有不少数组比较大,这种方式叫作------开放地址法

  3.2 解决方法二(链地址法 )

  既然有两个数据都要放在数组的一个位置上,那就想办法把第二个数据连在第一个数据后面,经过第一个数据能够找到第二个数据,而数组中只保存第一个数据的地址;其实就是一句话,数组中每一个位置放一个链表;

  这种方法的好处很明显,完美解决上述冲突,不须要用什么花里胡哨的操做;缺陷就是当链表太长了,咱们要查询这个链表的最后面的数据,只能慢慢遍历这个链表,而咱们知道,链表的优点是插入和删除,而对于查询这种操做是比较坑爹的,而咱们前面用了红黑树这样的结构来完美解决链表的缺点;最后,咱们就差很少想到了一个比较实用的方法:数组的每一个位置都存放一个链表,当链表的节点不多的时候,那就用链表吧!可是当链表慢慢的变长,当节点数目到达一个界限的时候,咱们就把这个链表变成一个红黑树,比较完美的方案,这也叫作------链地址法 

  顺便一提,jdk7的HashMap就是数组中放链表,即便链表很长也不会变红黑树;jdk8中的HashMap才增长了变红黑树这个操做

 

4.开放地址法

  所谓的开放地址法就是:根据咱们要保存的数据计算出来的数组下标的那个位置已经存放了数据,这个时候咱们就要再找一个空位置,而后将要保存的数据丢进去便可,那么怎么找比较好呢?这里提供三种方式,线性探测,二次探测和再哈希法,下面就看看这三种方式究竟是怎么工做的;

  4.1 线程探测

  看名字线性就知道是从前日后寻找空的位置,举个很简单的例子,当一个字符串通过运算对应于数组下标为52,然而此时52这个位置上已经有了数据,那么就尝试放到53的位置,假如53的位置也已经放了数据,那就放到54位置,就这样一直日后慢慢找,直到找到一个空的位置就把数据放进去;而此时找的次数越多,假如已经找到56的位置,那么从53到56这么多位置叫作填充序列,当填充序列很长的时候,咱们就称为原始汇集,下图所示:

  这里填充序列的中有5个填充单元,咱们也能够说步数为1,每次探测都是前进一步;咱们能够知道当探测的次数越多的时候,说明汇集越严重,下一次再想添加到这个位置的数据的效率就越低;

  还有就是当哈希表填充得越满,效率也就越低,因此当哈希表快满了以后就要扩展,而java中数组是不能直接进行扩展的,须要再新建一个数组,而后想办法将这个哈希表中的数据复制到新的数组中,注意,这里不能直接复制,由于新的数组的容量和原来的数组不同,那么原来哈希表中全部的数据必需要从新哈希化,而后放入到新的数组中,很是耗时....

  4.2 二次探测

  根据前面咱们的线性探测能够知道,假如通过哈希函数计算出来的原始数组下标为x,那么线性探测的位置是x+1,x+2,x+3,x+4.....,;那么 进行二次探测找的位置就是x+12,x+22,x+32,x+42.....其实就是按照步数的平方进行探测看里面有没有数据,没有的话才放进去新的数据,二次探测能够防止汇集太长所致使的效率降低问题;

  对于二次探测来讲,若是当前计算出来的位置为x,首先会探测x后面一个位置,若是这个位置有数据,那就多日后4个位置看有没有数据,假如仍是有数据,那么二次探测可能会以为你这个汇集特别长,因而此次跳得更远的位置,当前位置后面的16的位置等等,直到最后跳过整个数组, 这样能够避免一个一个的位置慢慢探测的底下效率,二次探测下图所示:

  二次探测也有点问题,会致使二次汇集,那什么又是二次汇集呢?其实跟原始汇集差很少吧!好比184,302,420,544这几个整数都要放到哈希表中,并且这几个数通过哈希算法算出来的数组下标都为7,302须要以1步长进行探测,而420要先以1为步长,而后以4步长进行探测,而544要先以1为步长,而后以4为步长,最后以16步长进行探测,假如后面还有数据对应的数组下标为7,那么仍是要重复这个步骤,并且是愈来愈长....这也是一种汇集,我的感受从某种意义来讲和原始汇集性质差很少吧!

    二次探测不经常使用,由于有更好的办法解决,就是再哈希法;

  4.3 再哈希法

  用再哈希法能够消除原始汇集和二次汇集,那么什么是再哈希法呢?咱们能够知道产生原始汇集和二次汇集的缘由其实差很少,都是因为多个数据添加到哈希表中的同一个位置,而后根据步长一个一个位置的探测,直到找到一个空的位置,若是须要找的位置特别多,那么这就是汇集,添加的效率的就会大幅度下降; 

  那么咱们就要想一种方法即便多个数据要放在哈希表的同一个位置,可是不须要从头开始一个一个位置的探测,若是每一个数据均可以产生一个独一无二的步长那不就行了么!而后直接根据这个步长探测该位置将数据丢进去就ok了;

  因而咱们准备了两个哈希函数,一个哈希函数就是咱们上面说到的能够产生对应的数组下标,另一个哈希函数能够产生步长,其实就是多个数据放在同一个位置产发生冲突,就用这个哈希函数再次哈希化产生一个步长,根据这个步长进行探测就能够了,而不用每次都从第一个步长开始;好比下面就有一个产生步长的哈希函数,咱们能够知道步长的范围是1-constant,注意步长不能为0,不然就原地踏步了。。。

  上图中,假如咱们往哈希表中添加的数据是数字,那就直接将数据和数组大小取余获得数组下标,这里的key就是咱们的数据,constant只要是小于数组容量的一个质数,随便什么均可以

  顺便一提:再哈希法使用的前提必须保证数组的容量为一个质数,由于这样才能使得全部位置都被探测到;能够试试假如数组容量为15,步长为5,一个数据通过计算获得额数组下标为0,那么探测的位置应该为:(0+5)%15 = 5,、(5+5)%15 = 10,(10+5)%15 = 0,只会探测0、五、10这三个位置;可是若是数组容量为质数13,步长为5,第一个数据下标仍是0,那么探测位置为:(0+5)%13 = 5,、(5+5)%13 = 10,(10+5)%13 = 二、(2+5)%13 = 7,(7+5)%13 = 12,(12+5)%13 = 4,(4+5)%13 = 9等等,能够看到每次探测的位置都不同,能够探测到数组中全部位置只要有空的就把数据当进去便可;

  假如使用的是开放地址法,那么探测序列就用这个再哈希法生成,其实很容易!

 

5.链地址法 

  能够看到上面的开放地址法有点麻烦,须要找到探测序列真的是日了狗了,麻烦的我都不想看了,若是能够不用这么麻烦那该多好呀,ok,那就用链地址法吧!就相似下面这样的结构,原始的数组中不直接保存数据,每一个位置只是保存第一个数据的引用,经过该位置第一个引用就能够取到后面全部的数据!若是链表太长遍历起来就比较费劲,能够转为红黑树效率就高了不少;

 

  这里其实没什么好说的,由于数组和链表的使用很熟悉了,没什么特别难的东西,基本逻辑:只须要新建一个MyHashTable的类,这个类中有几个属性:一个数组,一个int类型的属性标识数组真实容量的大小;最好有个节点类为静态内部类,这个静态内部类中实现了对链表的增删改查的操做;而后在MyHashTable类中写一个哈希函数的方法,根据这个哈希函数得出来的数组下标,最后对数组的增删改查了!

 

6.总结

  哈希表其实还能够用在外部存储中,也就是硬盘中,有兴趣的能够看看,不过我感受到这里就差很少了!其实哈希表的内容没多少吧,最主要的就是哈希函数的选取,选择一个好的哈希函数可使得咱们的哈希表的效率更高!而后就是数组中存数据的方式,能够直接在数组中存数据,也能够在数组中存节点的引用,其实吧,知不知道二维数组?在咱们这个数组中每一个位置存的是另一个数组的引用,这样其实也行,因为扩展起来很困难,使用链表比使用二维数组好。。。

相关文章
相关标签/搜索