数组是咱们比较熟悉的一种数据结构:固定大小,索引(下标)对应的槽位用以存储数据:html
咱们要在数组中查找一个值,好比红框圈中的 元素5 ,能够经过遍历或者排序后二分的方式达到目的。没有更快捷的查找方式了吗?显然是有的,好比Map。
咱们对存 / 取动一动脑筋,仍是上图的那些元素,假如咱们这样存:java
此时,想获取 元素5 很容易,直接array[5]就能够,但问题也一样突出,数组的length变得很大。这个例子中,最大的元素是79,还能够接受,若是最大元素是98277呢?更大呢?面试
咱们以取余数的方式做为变通:对于元素集合{8,1,5,6,82,33}
,还将这些元素放入最开始的length为6 的数组中。分别对元素除6取余,计算结果以下:算法
8->2 1->1 5->5 6->0 82->4 33->3
把余数做为下标,存入数组。segmentfault
此时,咱们想在数组中查找是否存在元素5,只需对要查找的值——元素5,按数组的length取余5%6=5
,直接array[5]便可。数组
这里的按数组的length取余,扮演的就是散列函数的角色!数据结构
什么是散列函数?能够理解为,将元素尽量分散的打入到数组中的函数。函数
散列函数有两个特征:优化
5%6
老是等于5。同时也有两个疑问,分别看下:spa
字符串的本质是字符数组,字符在ascii码表上就是数字。
对象是各类属性构成的,这些属性包括基本类型、字符串等等。
固然具体的算法要比取来的复杂,好比String的hashCode算法:
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
没错,各类hashCode()方法,就是咱们一直在聊的散列函数!
Tips:
围绕hashCode有几道经典面试题,正值跳槽季,给你们安利一下: 1.Object的hashCode是否是内存地址? 2.什么状况下会覆写hashCode()方法?你有没有覆写过这个方法? 3.若是对象A equals 对象B,则对象A和对象B的hashCode是否相等?反过来,对象A和对象B的hashCode值相等,equals是否返回true?
到目前为止,一切很顺利。length=6的数组完成了对集合{8,1,5,6,82,33}
全部元素的安置,但这是最简单的状况。若是再增长一个数字,就选西方人认为不怎么吉利的 13 好了,取余计算13%6=1
,本来应该放在索引为1的槽上,而咱们的数组如今已经满员了。
这就是hash冲突的问题,怎么解决?显然直接覆盖并不合理,那样会丢掉原有的元素1。想一想HashMap,若是发生了hash冲突,就丢弃原有值,这种作法使用者确定没法接收。
是时候让另外一种数据结构登场了——链表。
数组占用相邻的整块内存,且固定大小;链表则否则,因为结构上存在指向下一个节点(内存地址)的指针,所以不要求内存地址连续,大小也不固定。
由于结构的缘故,链表在插入、删除方面更有优点,修改指针指向便可;而数组在快速定位某槽位上更具优点,链表只能从头遍历。
加入链表后,散列表升级成这样:
元素13放入时,计算hashCode为1(姑且按取余的方式进行理解)。若是索引为1的槽位为空,直接放入元素;若是索引为1的槽位已经存在元素,将该槽位存储结构变动为链表。
根据Key值,计算hashCode。若是hashCode,也就是索引对应的槽位为空或只有一个元素,直接返回该值;若是hashCode对应的槽位中的数据为链表结构,对链表进行遍历,直到找到与KEY equals的对象。
若是hash冲突比较多,会发生什么状况?
链表的无限扩张,会使得查询变得缓慢,咱们最初不就是想用散列表解决快速查找的问题吗?如上图这种状况,散列表几乎失去了意义,又回到了遍历查找的时代,这也是散列函数尽量将元素均匀分布的缘由。怎么解决?数组快要满时,对其扩容!
HashMap也是这么作的,初始值2^4=16的数组,默认0.75的扩容因子;当元素个数超过阈值,即16*0.75=12的时候,触发resize方法进行扩容。数组大小翻倍,元素rehash后放入相应的槽位。
能够看出,散列表就是HashMap的底层结构。固然了,JDK 1.8版本对其还有红黑树等优化,感兴趣可查阅 Java 8系列之从新认识HashMap
ok,本篇文章到此就告一段落了,下一篇咱们探讨下图的经典问题——最短路径,敬请关注!