Hashmap
感谢黑马
快速查找
hashmap如何进行快速查找:将原始值按照一定的规律进行编码,从而获得该Key所在得位置(数组得索引值)。在进行查找得时候,将上述过程反着进行就可以快速查找了。
过程:
- 通过计算原始hash 二次hash 以及桶下标,获得当前Key值所对应的数组的索引,以便后续快速查找。
- 但是,有些Key值会拥有相同得桶下标。因此,将具有相同桶下标得Key值通过链表得方式存在该索引下.
- 当链表长度过长时,hashmap得查询性能会下降。如何优化查询性能?
1)缩小链表长度–>扩容。
如何扩容
首先,数组得索引值通过下式取得
Index =hash2(hash1(key))%capacity
Index =hash2(hash1(key))&(capacity-1)
可以看出,数组的容量可以改变key值的索引,因此,扩容是一个解决办法。
**什么时候扩容** hashmap中得factor就是一个判断是否扩容得参数(默认0.75)。
if(元素个数>factor*capacity)
capacity = 2*capacity;
但是当key值的原始hash值都一样时,扩容之后也不会改变链表长度。
2)将链表转换为红黑树–>树化(空间换时间)
红黑树的特性
父节点左侧比他小,右侧比他大。
红黑树Vs链表
红黑树的搜索时间复杂度o(log2(n))
链表的搜索时间复杂度o(n)
红黑树的底层数据结构是treenode
链表的底层数据结构是Node
红黑树的空间复杂度大于链表的空间复杂度
树化的条件
链表长度大于8,且数组容量大于等于64
文章目录
一、经典面试题
1. HashMap底层的数据结构
1)1.7 数组加链表 , 1.8 数据+(链表或红黑树)
2)链表长度超过一个阈值8之后,会转为红黑树(且数组长度大于等于64)
2. 为何要用红黑树,为什么一上来不树化,树化的阈值为什么是8,什么时候会树化,什么时候退化为链表?
1) 红黑树是用来避免DoS攻击,防止链表超长时性能下降,树化应当是偶然情况。
2) 选择8是为了让树化的机率足够小(树化是一种以空间换时间的方案,权衡时间和空间的选择)(当链表长度达到一定长度后,查询的效率变低;而红黑树虽然查询效率高于链表,但他的空间复杂度更高,效率更低(红黑树需要左旋、右旋、变色来保持平衡))
3) 树化的两个条件 1:链表长度大于8且数组容量>=64
4) 退化的情况1:在扩容时如果拆分树时,树元素个数<=6则会退化链表
5) 退化情况2:remove树节点时,若root|root.left|root.right|root.left.left有一个为null,则会退化为链表。(没懂)
3. 索引如何计算?HashCode都有了,为何还要提供hash()方法?数组容量为何是2的n次幂?
一些别的hash()实现方法:平方取中法,伪随机数法、取余数法。当数组容量是在2^n时,取余数法等价于&(capacity-1)
1) 计算对象的hashCode(),在进行调用HashMap的hash()方法进行二次哈希,最后&(capacity-1)得到索引
2)二次hash()是为了综合高位数据,让哈希分布更均匀(减少哈希碰撞)
3)计算索引时,如果是2的n次幂可以实现位与运算代替取模,效率更高;扩容时hash&oldCap==0的元素留在原来的位置,否则新位置=旧位置+oldCap
但1) 2) 3) 都是为了配合容量为2的n次幂时的优化手段,例如Hashtable的容量就不是2的n次幂,并不能说那种设计更优,应该是设计者综合了各种因素,最终选择了使用2的n次幂作为容量。
4. 介绍一下put方法流程,1.7和1.8有何不同
1) HashMap是懒惰创建数组的,首次使用才能创建数组
2)计算索引(桶下标)
3)如果桶下标还没人占用,创建Node占位返回
4)如果桶下标已经有人占用
a. 已经是TreeNode 走红黑树的添加或更新逻辑
b. 是普通Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
5)返回前检查容量是否超过阈值,一旦超过进行扩容。
6)不同
a. 链表插入节点时,1.7是头插法,1.8是尾插法
b. 1.7是大于等于阈值且没有空位时才扩容,而1.8是大于阈值就扩容。
c. 1.8在扩容计算Node索引时,会优化
5.加载因子为何默认是0.75f
注:0.75f是指capacity的0.75倍,即当容量为16时,元素超过12就会扩容
- 在空间占用和查询时间之间取得较好的权衡
2)大于这个值,空间节省了,单链表就会比较长影响性能
3)小于这个值,冲突减少了,单扩容就会更频繁,空间占用多。
6.多线程下HashMap绘有什么问题
1) 扩容死链(1.7)
扩容时,在链表的迁移(头插法)过程中容易发生死链(死循环)
具体的,比如,线程2扩容后,由于头插法,链表顺序颠倒,但线程1的临时遍历e和next还引用了这两节点(a,b的地址没变),因此导致死循环
2) 数据错乱 (1.7,1.8)
7.Key能否为null,作为key的对象有什么要求?
1) HashMap的key可以为null,但Map的其他实现则不然
2) 作为key的对象,必须实现hashCode和equals,并且key的内容不能修改(不可变)
8.String对象的hashCode()如何设计的,为啥每次乘31
目的:达到较为均匀的散列效果,每个字符串的hashCode足够独特
1)字符串中的每个字符都可以表现为一个数字,称为S_i,其中i的范围为0~n-1
2)散列公式如下所示:
3)31带入公式有较好的散列特性,并且31*h可以被优化为
a. 即32 * h-h
b. 即2^5-h
c. 即h<<5-h

提示:以下是本篇文章正文内容,下面案例可供参考
二、Hashmap内部结构
数组加链表/红黑树
1. 哈希码
数组+链表或红黑树
- 原始hash h= hashCode(Key)
- 二次hash h1 = h&(h>>16)
- 桶下标 buket = h1&(capacity-1)
存入key值需要经过上面三个过程,以遍后续更加快速的对key值进行遍历
为什么要二次hash?为了综合高位数据,让hash分布更加均匀
数组容量为何是2^n,
2. 树化–链表转化为红黑树
树化的条件(需要同时满足):
1. 链表长度大于8
2. 容量大于64
三、HashMap应用
1.HashMap排序
代码如下(示例):
2.HashMap其他函数应用
四、HashMap与HashSet的区别
1.HashMap
当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。头插法
在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
2.HashSet
HashSet的内部使用HashMap实现的
public boolean add(Object o){
return map.put(o,PRESENT)==null;
}
3.区别
- HashMap实现了Map接口,而HashSet实现了Set接口
- HashMap用于存储键值对,而HashSet用于存储对象
- HashMap不允许有重复复的键(value可以相同)。HashSet不允许有重复元素。(元素是指?)
- HashMap中null可以作为键Key,可以有多个键值value为null。HashSet允许有一个空值。
- HashMap中使用put()将元素加入map中,而HashSet使用add()将元素放入Set中 。
- HashMap比较快,因为其使用唯一的键来获取对象。
总结
.......
本文详细探讨了HashMap的面试知识点,包括HashMap的内部结构,如哈希码计算、链表与红黑树的转换,以及HashMap在不同版本中的优化策略。此外,还讨论了HashMap的应用场景,与HashSet的区别,以及树化条件和扩容机制。

255

被折叠的 条评论
为什么被折叠?



