HashMap结构
HashMap是一个用来存放Key-Value键值对的集合,每一个键值对是一个Entry对象(Entry对象结构下面详细介绍),Entry对的数组组成了HashMap的主干。HashMap数据结构是数组加链表,主干是一个数组,在每个产生哈希冲突的位置是一个链表。
先看下java中的其他数据结构,再解释HashMap为什么是数据加链表的数据结构。
数组:一组内存连续的存储单元存储数据。
通过下标查找时间复杂度是o(1),因为直接就能计算出内存地址
对于给定值查找,需要遍历数组对比,因此时间复杂度为o(n)
对于有序数组,可以通过二分查找,时间复杂度为o(log)
对于插入和删除操作,由于要将删除或插入下标位置之后的全部数组元素移位,因此效率不高
链表:内存上不连续的,链式结构
对于查找,因为内存不连续(每个元素仅知道与之相关联的元素的内存地址(单向只知道后驱元素,双向知道前驱和后驱),无法知道每个对象的内存地址,只能从链表的一头逐个检查每个元素,),因此要遍历数组逐一查找,时间复杂度为o(n)
对于插入或删除操作,时间复杂度为o(1),因为只需在找到操作后置后,改变节点间的引用即可。
数组和链表都有插入或查找时效率低的问题,所以HashMap使用了数组加链表加上hash函数寻址的方式解决这个问题。
如果不考虑hash冲突的情况下,查找、删除、插入操作的时间复杂度都是o(1)
原因在于,hash表查找数组位置的过程
1.根据要操作元素的关键值调用hashcode方法得到一个int类型的hash值
2.根据之前得到的hash值 通过一个hash函数得到数组下标
因此,仅需一次操作就能完成
如果一个元素,通过哈希运算定位到数组中同一个位置发现该位置已经存在元素了,称为hash冲突或hash碰撞。所以hash函数的设计应尽量避免hash碰撞。
put方法
put时,通过对要插入键值对的Key一个hash函数进行求值,求值得到的结果对应数组下标位置。如果该位置没有元素直接插入;如果该位置已经存在元素,那么对该位置的链表进行遍历,不存在相同的key值则插入链表头部,存在相同的key值则覆盖原来的value。
具体如何获取下标位置
先通过一个hash函数对 key的hash值,进行了许多异或和移位操作,以保证尽可能的不产生hash碰撞
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
然后通过位运算,最终获取下标位置 index=h & (length-1
)
static int indexFor(int h, int length) { return h & (length-1); }
当发生hash碰撞时,HashMap是通过数组加链表的数据结构解决的。当一个元素插入时发现数组该位置上已经被占据,则插入该位置上的链表的头.
HashMap的查找,当定位到的数组下标仅存在一个元素时,时间复杂度为o(1),通过hash函数一次查找。当发现该位置上存在链表时,需遍历链表,对链表上的元素的key值逐一对比,时间复杂度为o(n)
hashmap的插入,当定位到数组位置不包含元素时,仅需hash函数一次定位即可。当定位到的位置存在元素,需逐个遍历链表,存在相同key则覆盖,不存在相同key则添加
为什么要计算Key的下标位置要用Key的hash值和数组长度进行位运算?
HashMap规定数组长度必须是2的幂次,目的也是为了让计算下标时尽可能的分布均匀,以减少hash冲突。假如length是16,
那么length的后四位是1111(只要是2的幂次-1,那么所有位都是1),这样可以实现尽可能的实现分布均匀。下面举例说明
1.假如一个key的经过hash函数后计算出的二进制值是101110001110101110 1001
2.16-1=15,二进制值是1111
3.进行与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。
所以下标位置完全取决于Key经过hash函数后值的最后几位。
当发生hash碰撞时,HashMap是通过数组加链表的数据结构解决的。当一个元素插入时发现数组该位置上已经被占据,则插入该位置上的链表的头.
HashMap的查找,当定位到的数组下标仅存在一个元素时,时间复杂度为o(1),通过hash函数一次查找。当发现该位置上存在链表时,需遍历链表,对链表上的元素的key值逐一对比,时间复杂度为o(n)
hashmap的插入,当定位到数组位置不包含元素时,仅需hash函数一次定位即可。当定位到的位置存在元素,需逐个遍历链表,存在相同key则覆盖,不存在相同key则添加
那么如果数组长度不是2的幂次会产生什么情况?
假如HashMap数组的长度是10,二进制的值为1001进行位运算101110001110101110 1001 & 1001=1001
后四位变一下再看
进行位运算101110001110101110 1011 & 1001=1001
再变一下
进行位运算101110001110101110 1111 & 1001=1001
所以当key经过hash函数之后的值改变时,结果也很可能不会变,这样就增加了hash冲突的可能性,而且有些值必然不会出现(2、3位
为1的值),那么也就是数组某些位置会一直空着。与数组尽可能分布均匀的思想显然时相悖的。如果数组长度是2的幂次,后几位全部是1
那么与运算的结果就完全由key的hash值去确定了
总结
1.调用key的hashcode方法
2.对key的hashcode值进行hash函数运算,得到一个int值h
3.对 上面得到h和数组长度减一的值进行与运算,得到的int值index就是应插入的数组下标位置
4.将元素插入该index位置,如果该位置无其他元素直接插入
5.如果该位置有其他元素,插入链表头