关于查找我已经讲了三种了,一种是线性表的查找,一种是树的,查找树,下面我们讲另外一种结构,它是哈希表
前面的我们查找不管你是顺序查找,还是折半查找,还是树里面查找,都要做一个操作,都需要进行比较,拿着我要找的关键字,
跟表里面或者树里面进行比较,看是不是相同,那既然要比较的话,就涉及到比较的效率,理想的方法是,不需要进行比较,就可以
直接找到我们要找的内容,有人说这怎么可能呢,有没有这样的可能,是有的,对于我们的数组来说,如果我们要按照索引来说的话,
索引查询,我要查询第5个,我要查询第10个,是不是可以直接定位,直接定位,只要计算一次公式,他有数组的首地址,有每个元素的
长度,只要套一个公式,既可以直接定位到索引是5索引是10的元素,找第5个,找第10个,找第1个,找第1万个,花的时间都是一样的,
并且一次计算就可以得到,效率会特别的快,但是我们如果按照内容来找的话,我不是找5个元素,也不是找第10个元素,我是找值是
10的元素,那他就不是索引的值了,是内容,那我们就会逐个的比对,顺序查找效率是比较低的,折半查找要限制条件,要求是有序的,
我们按照内容查找的时候也能和按照索引查找这么高的效率,不需要比较,直接定位,这就是我们哈希表要做的一件事情,所以我们
就知道哈希表他有多重要了,有多神奇了,那怎么来理解哈希表呢,哈希表底层它是采用一个什么样的结构,结构和他的神奇是
有一定的关系,光有这个结构还不行,还有他具体的算法,我们看一下哈希表是怎么添加数据的,和哈希表是怎么查询数据的,
通过这两个具体的操作,知道明白哈希表为什么神奇,它是怎么快的,最后我们还有一些细节的内容,讲里面的一些细节,比如说怎么
减少冲突啊,怎么构建哈希码啊,在JAVA里面有两个重要的方法,使用哈希表的话有两个重要的方法,这两个方法到底有什么样的
作用,那我们就开始了
1. 先来看哈希表的结构和特点:
哈希表英文单词是这个单词hashtable,也按照英译的话也是哈希表,按照意义译的话也是散列表
他有个特点,快,非常快,神奇的快,他底层是什么结构啊,底层有多种结构,多种实现方式,下面我给出的是最容易理解的,最常用的
一种方式,就是顺序表加链表,他的主结构大家看,主结构它是一张顺序表,我们你这里从0到12一共有13个数,那这个13个不存放
具体的内容,每个后面会引一个链表,会引链表的,主结构是一个顺序表,每个顺序表的节点,它是用来引链表的,如果我有一个值是
11,我有一个值是11,那经过我的运算之后,11的位置不往数组里面放,后面拉一个链表,数组里面的指针指向他就行了,这是我们
哈希表的主结构,哈希表最容易理解的主结构是一个数组,每个元素可以拉链表,这是一个,那下面我们来看一下哈下表是怎么来
添加数据的,那我们这里有一个哈希表初始操作状态,我们来理解一下,这上面是什么,上面就是我们要添加的数据,23,36,一直到
47,我们要加这些数,而下面我们这一块就是我们的哈希表,我们是不是创建了一个哈希表他的主结构它是多少啊,是不是11,从0
到10,一共有11个元素,注意画的这个是什么啊,只不过这个是纵向画的,我们这边是横向来画的,注意就是这条线往下,就是哈希表
的主结构了,目前都是空的,刚开始的情况下你可以认为有没有值啊,有,都是null,这个地方都是null,没有什么值的,我们就不再
一个一个标记了,那下边我们就要看,我们就要把这些数放在哈希表里面,怎么快的,首先你想一下数组里面要想加一个数,你如果
不是加到最后,加到中间的话,就要移动,我们这会不需要移动,你看是怎么来做的,我们要加这个23,这一列是是什么啊,这是我们
要加入的内容,加入的数据,只不过这一列比较特殊,正好是整数,不管是什么数据,到最后你都得生成一个哈希码,生成一个哈希码
它是一个整数类型,而对于整数来说,哈希码就取他自己就可以了,所以这个X就是hashcode就是哈希码,不是那个函数哈希码,他就
代表一个哈希码,对于整数来说,哈希码取他自己就可以了,整数的哈希码取自身即可,因为它本身也是一个整数,所以下面一大堆就
在这里解决了,每个哈希码我们又写了一遍他自己,然后y=k(x),它是一个哈希函数,这是需要一个哈希函数的,比如我们往哈希表里
放数据,他不需要比较,但是他需要计算,X是谁啊,X是那个哈希码,y是什么意思啊,y就是要存放的地址,只要计算这一次,马上就能
找到地址,你把这个数放到这个地址,就可以了,不需要大量的比较,也不需要移动,那我们制定的这个函数是什么意思,让这个哈希码
除以11,取余数,为什么我们这里除以11,因为我们哈希表的主结构,长度是11,如果你对11取余数的话,不正好是0到10吗,那正好落在
我们指定的下标位置,那实际上我们这么来做,所以大家初步已经知道了,哈希码不需要比较,不需要移动,只需要计算,计算得到地址
哪有这么快,我们按照索引在数据里面找元素也要计算,也要定位,再往下我们挨个来看,我们现在解决第一个问题,看这儿了,第一步
2. 哈希表是如何添加数据的?
第一步是计算哈希码,整数的哈希码是他自己
第二步是套到哈希函数里面去,得到地址
第三步然后存到哈希表:
一次添加成功,一次就成功了,什么是一次就成功了,我现在加23呗,23除以11取余数,后面的余数已经给大家写好了,告诉我23该怎么存,
23余数不是1吗,要不要把23擦掉把23存到这里,要不要,不要,这个地方不写值,这个地方写什么呢,我们要这么来写,这个引入一个链表,
引入一个节点,当然这个节点里面是分两部分的,这里面就放了一个23,放地址后面还有没有元素,后面是null了,后面没有节点了,如果
这个节点是0X1012的话,实际上我们是要把这个地址放到这儿的,0X1012,这个大家应该是没有任何问题的,链表直接加到这里就可以了,
23就加到这里,得到哈希码计算哈希函数,得到结果是1呗,写到这里就成了,添加这么做,36依次类似吗,余数是3,那你就在这又创建一个
节点,再创建一个节点,那这儿的值是多少,这儿的值是36,后面的值是null,同样,我们不再写具体的地址了,是这么来指的,48与此类似,
我们在这又创建一个,再创建一个啊,到这儿来,我们把这个复制一份吧,一会直接用它就可以了,我们在这放一个48,这边有一个null,
同样指向他,这么来指向,就可以了,77和他是一样的,余数是0,放到这儿来,这边放的是谁,这边放的是77,这边要写上一个null,把这个null
要擦掉,不是null了,指向他,还有吗,86,86的余数是9,选择一下,把它拖过来,9在这儿,那这边写的是86,我想大家已经感觉到他添加的
一个快速性了,添加基本都是常数级别的,直接一步两步三步,就到位了,特别的快,这是一个操作,再往下还有别的吗,76也类似,我们先
跳过去,76在这儿呢,这个67我们先把他放一下,这三个先排除,我们先把其他的一次性的写完,76在哪儿呢,76在这儿呢,76写到这,这边
写一个null,然后再画一个指针,指向他就可以了,其他的都是有冲突的呢,我们来看67吧,67该怎么办,我们讲的一个操作,一次添加成功
特别快,但是有时候你会发现需要多次添加成功,什么叫多次添加成功,67除以11余数是1,1的话怎么办,直接写到这儿,结果往这里写的
时候发现这里已经不是空了,不是空意味着下面已经有值了,有值了怎么办,那我们就在上面,就在这一块,把这个拿过来,拖到这儿,
一会可能还会再拖,我们就先来写一下,先拖到这儿,这边改写谁了,这边叫67,这边还是null,那这个就不是null,这么来指向他,
这个效率就有点降低了,第一次添加不成功,有冲突的,关键之不同但是得到的地址是一样的,当我们再来放一个23,23是另外一种情况,
23在这里画一下,现在来看56,56怎么了,56的余数还是1,那就应该往这里放,有值了,往这儿走,这儿也不是空,往下走,他这儿是空,
意味着它是这里的最后一个节点,然后我们在这里来写,写一个56,然后把这个内容去掉,去掉之后再拉一个链条,56这个呢现在是末
节点,它是null的,还有一个78,78怎么办,78余数还是1,那我们就得顺着这个位置一直往下找,往下列表找不行,我们这个78是23吗,
不是,是67吗,不是,是56吗,不是,那我们一直往下加,最后把78加到这儿,我们再连接起来最后一个状态,到这儿来,把这个去了,
那我们对添加操作就写到这儿,发现冲突是绝对避免不了的,难免会有冲突,如果冲突的概率比较低的话,最终他整体的速度还是可以的
那我们树的加载操作还差最后一步,23我们该怎么加,这我们已经讲了几种情况了,一次添加成功的,多次添加成功,多次添加成功就
出现了冲突,就会拿出我们要加的值,现有的值调用equals方法,进行比较,到最后也会相等,那就创建个新节点,存储数据,加到链表的
后面就行了,可是我们现在加的比较特殊,这是个23,23怎么办,之前已经加过23了,这个时候怎么办,23除以11的余数是1,往这儿一放,
要拿这个23和这个23进行比较,比较啊,从JAVA的角度来说是一个Integer对象,他最终比较是要调用equals来进行比较的,结果那这个
23和这个23进行一比,相同,或者你往里边加的是67,这里边是不是也有了,那怎么办,已经有重复的数据,那就不加了,所以最终导致我们
的哈希表里有没有重复的数据,没,是没有重复数据的,所以不添加,出现冲突,调用equals方法比较,有重复的就不添加了,通过这个添加的
过程,我们应该得到一个结论,整体的速度还是比较快的,计算就得到了位置,大家想一下数组里面是怎么添加的,他加在某个位置要大量的
移动,他的效率也要比较高,添加的时候要得出一个结论,添加的时候要快的,第二,这里面有没有重复的数据,没,有重复的就不加了,
并请问这里面的数据有没有顺序,无序的,这真的是没有顺序的,23,67,56,有什么顺序,没有顺序,所以我们要得到这三个结论,哈希表
添加数据特别的快,如果不考虑冲突的话,3步就可以了,常数级别的,数据元素是唯一的,不会重复,然后结论他还是无序的,哈希表的原理就
讲到这
下面我们来看,添加快了,我们来看一下哈希表是如何来查询数据的?
查询数据又分为3种情况:
1. 查询数据和添加数据过程是相同的,基本上相同的,但是又有不同之处,怎么不同啊,添加可能一次添加成功,也有可能多次添加成功,
我们的查询可能一次找到,有可能多次找到,但是我们添加的时候,如果有重复的,就不添加了,你查询的时候可能查询哈希表里面就没有的
值,我在这里找100,就没有100,根本就没有,我们看这个查询是怎么来实现的,我们举个例子来说,我要找23,我要在这里面找23,怎么办,
和刚才添加的过程是一样的,首先我要找到整数23的哈希码,整数的哈希码就是他自己,然后套到这个公式里面,余数是1,索引是1的位置找,
一下子就找到了23,你要这个48,计算哈希码就是他自己,套哈希函数得到结果4,来4这个位置直接找48,一次就找到了,也非常快,那我们
再来一个,再来一个多次找到的,找一个67,67是怎么找到的,67得到他的哈希码还是67,套入哈希函数,余数是1,那就来这个位置找67了,
一看这个值是67吗,不是,再往下找,67,你看,找到了,多次找到的,78和这个一样,只不过他要多比对,多比对几次,或者你把这个冲突
尽量避免的话,他的效率还是比较高的,再找一个100,100可怎么办,这里面有没有100,没有,我现在要在这里面找一个100了,100除以
11余数是几,是1吧,99加1,100怎么办,1来这个位置找,不是100,不是100,不是100,不是100,再往后没有了,100如果存在就肯定在这个
链表里了,而这个链表从头找到最后,是不是也没有找到100,那说明什么,那说明100是不存在的,我现在画的这个表,根现在这个表,
你看结构是一样的,只不过一个是横着画一个竖着画,通过我们刚才查找的这个过程,大家应该知道,得出什么结论,第一个哈希表的
查询顺序是比较快的,跟添加速度是一样的,查询到之后就可以删除了,比如我想把56删除了怎么办,我想把这个56删了,那你就直接
改指针就行了,改一下指针指向他就行了,更新就要看更新什么了,我找到67想把它改成77,那不能这么改的,为什么啊,当你把它改成
77的话,他是不是排在这个位置了,那不能随便改的,一改的话下次再改就找不着了,但是我们现在存着整数来说,可能这个哈希码
我们要看,我们要根据情况来看,如果一改这个值影响存储位置,那你就要考虑其他方法了,比如先删除再添加,也相当于是更新了,
其他的算法了,会有这种解决方案,更新就要好好考虑了,更新就影响哈希码,影响哈希码就要采用其他方法来解决了,但是你即使
先删除再添加,那速度也不慢,因为它引的是链表,便于删除也便于添加,这个就讲到这里了
讲到这里哈希表的主题内容就已经讲了,如果问到哈希表就从这三步来说,首先要说哈希表的特点是什么,特点是快,为什么快,
跟他的结构有关,然后再讲一下它是怎么添加的,把这个说明白,说明白之后得到这个结论,是非常快的,然后再讲一下它是怎么
查询的,和添加的过程是基本相同的,然后逐个来说明就可以了,讲到这哈希表就可以了,我们再讲一些细节,请问hashcode这个
方法是做什么的,JAVA里面有这个方法,是计算哈希码的,它是计算哈希码的,我们打开JAVA,hashcode是每个类都要有的一个方法,
它是从Object继承过来的,找一个方法,hashcode方法有没有,整数的哈希码就直接返回哈希码这个数,public int hashCode()
return Integer.hashCode(value);public static int hashCode(int value),return value;整数的哈希码取他自身
就可以了
不同数的哈希码肯定是不一样的,哈希码是通过一个计算得到一个整数,如果我们这里面存的是字符串,存的是学生,你必须把
字符串和学生变成一个整数值,算法放到hashcode里边就可以了,我们得到一个整数,这是他的一个内容,再往下看还有什么,这是
我们hashcode的一个作用,equals是干什么的,就是看这个图,hashcode是在这里使用的,equals是怎么用的,当你往这里放的时候,
里边出现了冲突,可能是添加,可能是查询,一般出现冲突之后需要比对了,你找的是67,那要和每个逐个比较看是不是这个内容,
那通过equals来比较,equals是什么,equals是出现了冲突之后,通过equals来比较,判断内容是否相同,这是一个内容
下面再讲一个内容,各种类型的哈希码应该如何获取?
1. 简单一句话就是调用hashcode方法,关键是我们使用者是调用hashcode方法,就得到他的哈希码了,这个hashcode的开发者呢,
在里面写什么代码,得到一个整数,那我们说了,如果是整数,取自身
2. double该怎么办,如果里面存的是double的话,那有人说那不简单,要整数取整呗,3.14,3.15,
3.145,我们都取整,取整不是找冲突吗,为什么,3.14,3.15,3.145一取整,是不是都是3,不同的数哈希码一样,那最终都得
存到一个位置,那明显是冲突的,所以如果你这么来设计hashcode的算法,那以后这种算法是很失败的,会导致冲突,
double肯定不能这么来写,那怎么来写呢,这我们不用操心,我们知道就可以了,好多数学家就是做这个的,学数学的,
学二进制的,他就是用来解决这些问题的,我们来看Double底层的
哈希码是怎么来做的,return Double.hashCode(value);这什么意思,再来看,public static int hashCode(double value),
long bits = doubleToLongBits(value);这什么意思,value就是3.14,先要把double变成一个long类,然后要得到一个long数,
return (int)(bits ^ (bits >>> 32));然后这个数要做什么,是不是麻烦,先要向右移动32位,然后要与原来的bits做异或运算,
这个里面就不看了,比较复杂,他做这么复杂的一个目的是什么,目的就是很简单,我这边不同的double数,经过你这个运算之后,
得到一个整数,这个整数尽量要是不一样的,他就是为了这样的一个目的,为什么整数这么简单,其自身不同就是不同被
3. 如果是一个String的话,字符串的哈希码该怎么办,java,oracle,这跟整数也没关系,这个时候该怎么办,有人说我是有办法的,
比如说JAVA怎么办,JAVA是不是由4个字母组成的,那我就把4个字母的编码值都有它的unicode编码吗,它的编码不就是整数吗,
相加不就可以吗,也能得到一个整数,那也是一个很糟的方案,比如说,abc中国农业银行,cba中国篮球联赛,bac是什么,
我知道bat是什么意思,请问如果按照我们刚才的算法的话,a的编码是97,b的编码是98,c的编码是99,我们这三个一求,
把他们的编码相加,结果都是一样的,结果你这三个不同的字符串,他们的哈希码又是一样的,那一存又存在一个位置了,
又出现冲突了,这种方案又是不好的,那不好我们怎么办,比如我们这么来做,你不是顺序不同吗,abc 1*97+2*98+3*99,
abc这么来的,cba 1*99+2*98+3*97,这么一来数就不一样了,应该就不一样了,这是一种思路,但这是不是最好的思路,
我觉得最好的思路基本上就在这儿,我就看JAVA底层是怎么来实现的,人家肯定是采用非常好的算法,JAVA的hashcode
是怎么来做的,你看什么意思,JAVA的hashcode,int h = hash;这是一个h的值,if (h == 0 && value.length > 0),
value字符串底层是一个字符数组,取他的长度,把每个字符都取出来,char val[] = value;
for (int i = 0; i < value.length; i++),h = 31 * h + val[i];
把每个字符取出来,然后乘以31,再加上value[i],就是h是谁,h是他的哈希值,总之他又是一套的算法,他的算法是这么来实现的,
保证不同的字符串生成的哈希码肯定是整数,并且值是不一样的,这大家明确了,下边怎么办,我要在这里来了一个类,
整数Double都是基本类型,我还来个学生
最后我们来看如何减少冲突?
冲突不能够百分百的避免,肯定会有冲突,没有谁说设计哈希表的时候不冲突的,我们只能减少这个冲突,所以我们来看,如何减少这个冲突,
第一个大家想一下,现在这个长度是不是11,现在这个读取的长度是不是11,我要往里面存20个数,有没有冲突,肯定没有冲突的,你存的数
比哈希表的长度还要长呢,那肯定是有冲突的,那如何避免,哈希表的长度和表中记录数的长度,概念叫填充因子,表的记录数,如果我要
往里面放20个数据,但是我哈希表的长度是11,这个一除大于1的,那肯定有冲突,肯定是有冲突的,那这个比例至少是小于1的,
等于1也是有冲突的,光小于1也不行,根据实际文献证明,填充因子在0.5左右的时候,性能是比较好的,也就是你的长度是11的话,
怎么办呢,你这里面的数最好不要超过5,6个,那这时候就会降低冲突,当然反过来说你降低冲突会浪费空间,那就会浪费空间,
所以大家大概知道这样一个理论,装填因子=表中的记录数/哈希表的长度,经验值是0.5,稍微高一些也可以,这是一个内容了,
冲突减少了,哈希表的总长度和表中要放的记录数是有关系的,这是一个了,哈希函数的选择,因为选一个比较好的哈希函数,
这个从理论上面有很多的方法,直接定址法,折叠法,除留余数法,大家可以去查询相关的资料,怎么去取一个好的哈希函数,
那我们这里使用的哈希函数是y=x%11,这个就是除留取余法,这个大家明确一下,我们的哈希函数是使用这种来做的
还有一个我们怎么来处理冲突,减少冲突,如果你能够把这个冲突处理好,可能能够进一步的减少这个冲突,该如何来处理这个冲突啊,
好多方法,链地址法,开放地址法,再散列法,建立一个公共的溢出区,把公共的数据放在溢出区里面,那我们刚才使用的是哪一种方法,
冲突了就往链表后面加节点,我们用的就是链地址法,我们刚才处理冲突的方法叫链地址法,讲到这里我们就把哈希表相关的理论就讲了
总结一下:
如果问到哈希表了,哈希表最大的特点就是快,怎么快啦,添加快,查询快,怎么达到这一点的呢,跟他的结构有关,说一下他最流行,
最简单的结构,然后来讲一下它是如何来添加的,再讲一下查询的,讲查询和添加的时候要贯穿一条主线,就是快,几步就可以了,
要贯穿这条主线,讲到这基本上就可以结尾了,如果下面再继续交流的话,我们就可以把后面的这些内容说一下,我建议大家
再加上一句话,那句话啊,我们JAVA里面有一个类,叫HashSet,是不是还有一个叫HashMap,还有一个过时了,叫Hashtable,
他们底层用的是什么啊,都是哈希表,只要遇到Hash这四个单词,这4个字母哈希的意思,他的底层结构就是哈希,
关于哈希的理论和算法呢,就都给大家讲了
package com.learn.search;
/**
* 如果我要往哈希表里面存学生的话
* 他的哈希码该怎么办,
* 这是一个复杂类型,
* 但是他的属性是基本类型,
* Student比较复杂,
* 但是你这里不是有这么多的属性吗
* 我得到你每个属性的基本哈希码,
* 然后按照某种算法添加啊,
* 不是可以得到一个整数了吗
* 你的Class不是比较复杂吗
*
* 学生的哈希码又该怎么办
* 我来了一个学生的姓名,年龄都不一样的
* @author Leon.Sun
*
*/
public class Student {
private int id;
private String name;
private int age;
/**
* 因为我们这里写的是小写的double,
* 如果是大写的Double他就会直接调用它里面的hashCode方法
*/
private double score;
/**
* 但是还有一种情况,
* 班级Class,
* 这个clazz当然不存在
*/
private Clazz clazz;
/**
* 我们来产生以下它的hashCode方法
* 好复杂啊,
* 最终还是得到一个整数,
* 返回就可以了
* 我们又给大家回答了一个问题,
* 什么问题呢,
* 各种数据类型的哈希码该如何获取
* 这个理论大家知道
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
/**
* 年龄
*/
result = prime * result + age;
/**
* 班级直接调用hashCode方法得到结果就行了
*/
result = prime * result + ((clazz == null) ? 0 : clazz.hashCode());
/**
* id
* 整数直接取自身
*/
result = prime * result + id;
/**
* 姓名
* 字符串的话就直接调用hashCode
*/
result = prime * result + ((name == null) ? 0 : name.hashCode());
long temp;
/**
* 实际上和我们开始见到的double处理机制是一样的
*/
temp = Double.doubleToLongBits(score);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (age != other.age)
return false;
if (clazz == null) {
if (other.clazz != null)
return false;
} else if (!clazz.equals(other.clazz))
return false;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (Double.doubleToLongBits(score) != Double.doubleToLongBits(other.score))
return false;
return true;
}
}
package com.learn.search;
/**
* Clazz里面还是基本类型的
* 可以得到一个哈希码
* 最终按照某个机制来就可以了
* 我们哈希表里既用到hashcode也用到equals
* equals我们经常用,
* 所以我们同时来实现hashcode和equals
* @author Leon.Sun
*
*/
public class Clazz {
/**
* 这里面可能比较简单
*/
private int id;
private String name;
/**
* 仔细看一下他的hashCode是怎么来的,
* 最终保证不同
*/
@Override
public int hashCode() {
/**
* 还是一个31
*/
final int prime = 31;
int result = 1;
/**
* 哈希码的整数就是他自己
*/
result = prime * result + id;
/**
* 字符串的哈希码就是调用它的hashCode方法
*/
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Clazz other = (Clazz) obj;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}