问题总结
-
HashMap的特点
Map<K,V>,以key–value键值对的形式存储数据。
key不重复,元素的存储位置由key决定,即可以通过key寻找键值对的位置,从而得到value的值。 -
HashMap如何计算key–Value结果的存储位置?
计算key的hashcode,然后对hashcode进行扰动处理,最后和table.length - 1进行按位与处理,得到index(下标)。 -
解决哈希冲突的方法
哈希冲突:对hashcode按table.length-1按位与后的下标值相等。- 链地址法:
jdk1.7中,采用数组+链表的结构。
jdk1.8中,当一个链表上的节点大于8个时,改用数组+红黑树的结构。
因为红黑树的时间复杂度(O(log2N))相比起链表(O(n))要低,查找效率高。
适用于经常进行插入和删除的情况。 - 开放定址法
- 线性探测再散列
冲突发生时,顺序查看表中下一单元,知道找出一个空单元或遍历全表。 - 二次探测再散列
di = 1²,-1²,2²,-2²,…,k²,-k²(k<=m/2)
冲突发生时,在表的组偶有进行跳跃式探测,比较灵活。 - 伪随机探测再散列:建立一个伪随机数生成器,冲突发生时,给定一个随机数做起点。
- 线性探测再散列
- 再哈希法
同时构造多个不同的哈希函数。当地一个哈希函数发生冲突时,再计算下一个哈希函数,知道冲突不再产生。这种方法不易产生聚集,但增加了计算时间。 - 建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。当发生冲突的元素意义不大时,可以使用这个方法。
- 链地址法:
-
HashMap get方法的时间复杂度
通过key计算index,再遍历链表。
时间复杂度近似于O(1),由于可能存在冲突所以不一定能达到O(1)。 -
为什么hashcode要进行扰动处理
h ^= (h >>> 20) ^ (h >>> 12);
降低hashcode的重复率,降低index的重复率。 -
为什么hashmap的容量要保持2的幂次
在计算table的index下标时,因为模运算的消耗大,效率低,所以进行的是hash & (length - 1)这个按位与算数运算操作。为了适应这个操作,数组长度必须是2的幂次,比如16,hash和1111按位与时,这样可以使下标充分地散列,使添加的元素均匀分布在数组上,减少了hash冲突。如果length是15,hash和1110按位与时,最后一位永远是0,会导致0001,0011,0101,1001,1011,0111,1101这几个位置不能存放元素,空间浪费很大,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了冲突的几率,减慢了查询的效率。
-
如何判断添加了重复的key
- key如果为空, 在以数组0号下标为头节点的链表中查找key为null的节点。
- key不为空,则先判断hashcode,哈希值不一样key一定不相同。再判断引用地址,如果一样,key一定是一样的。最后比较key。这样做提高了效率,前两步判断速度快,且可以筛选掉大部分比较。直接对象比较的话速度较慢
if (e.hash == hash && (e.key == key || key.equals(k)))
- 如果自己指定HashMap的初始容量和加载因子,那么容量的大小和加载因子对HashMap有什么影响?
初始容量:向上取整2的幂次,越小越容易发生哈希冲突。
加载因子:默认为0.75f。加载因子越小扩容越早,哈希冲突概率越小,空间利用率越小。加载因子越大扩容越晚,哈希冲突概率越大,空间利用率更大。 - 为什么HashMap需要扩容
数据量增多时,哈希冲突增多,链表变长,查找元素的效率降低。因此需要扩容。扩容需要计算每一个节点对应的index,哈希冲突几率降低。 - HashMap的使用场景
HashMap查询效率比较高,并且可以通过key去查询对应value。
自定义user类(账户,密码,性别。年龄)
HashMap<String,user>
map.put(“张三”,user1);
map.get(“张三”); - HashMap的优缺点
优点:查询效率高,近似于O(1)。
缺点:默认初始容量和加载因子下,浪费空间。