Skip to content

Latest commit

 

History

History
208 lines (104 loc) · 9.45 KB

Hashmap.md

File metadata and controls

208 lines (104 loc) · 9.45 KB

https://zhuanlan.zhihu.com/p/76735726

https://juejin.im/post/6844903921190699022#heading-0

https://tech.meituan.com/2016/06/24/java-hashmap.html

1. HashMap原理

jdk8后采用数组+链表+红黑树的数据结构,利用元素的key的hash值对数组长度取模得到在数组上的位置。当出现hash值一样的情形,就在数组上的对应位置形成一条链表。据碰撞越来越多大于8的时候,就会把链表转换成红黑树。

2. HashMap中put()如何实现的

https://blog.csdn.net/qq_38182963/article/details/78942764

1.Key.hashCode和无符号右移16位做异或运算得到hash值,取模运算计算下标index

对key的hashCode 和右移16位做异或运算,之后hash(key) & (capacity - 1)做按位与运算得到下标。

2.下标的位置没有元素说明没有发生碰撞,直接添加元素到散列表中去

3.如果发生了碰撞(hash值相同),进行三种判断

4.1:若key地址相同或equals相同,则替换旧值

4.2:key不相等,如果是红黑树结构,就调用树的插入方法

4.3:key不相等,也不是红黑树,循环遍历直到链表中某个节点为空,用尾插法(1.8)/头插法(1.7)创建新结点插入到链表中,遍历到有节点哈希值相同则覆盖,如果,链表的长度大于等于8了,则将链表改为红黑树。

4.如果桶满了大于阀值,则resize进行扩容

3. HashMap中get()如何实现的

1.Key.hashCode的高16位做异或运算得到hash值,取模运算计算下标index

2.找到所在的链表的头结点,遍历链表,如果key值相等,返回对应的value值,否则返回null

4.为什么HashMap线程不安全

1.多线程put的时候可能导致元素丢失

2.put非null元素后get出来的却是null

3.多线程扩容,引起的死循环问题

1.put的时候会根据tab[index]是否为空执行直接插入还是走链表红黑树逻辑, 并发时,如果两个put 的key发生了碰撞,同时执行判断tab[index]是否为空,两个都是空,会同时插入,就会导致其中一个线程的 put 的数据被覆盖。

2.元素个数超出threshold扩容会创建一个新hash表,最后将旧hash表中元素rehash到新的hash表中, 将旧数组中的元素置null。线程1执行put时,线程2执行去访问原table,get为null。

3.在扩容的时候可能会让链表形成环路。原因是会重新计算元素在新的table里面桶的位置,而且还会将链表翻转过来。 多线程并发resize扩容,头插造成了逆序A-B 变成了C-B ,t1执行e为A,next为B挂起, t2执行完毕导致B指向A,继续执行t1,他继续先头插eA,再头插nextB, 由于t2程导致B后面有A,所以继续头插, A插到B前面,出现环状链表。 get一个在这个链表中不存在的key时,就会出现死循环了。

https://juejin.im/post/6844903796225605640#heading-5

https://coolshell.cn/articles/9606.html/comment-page-3#comments

https://www.iteye.com/blog/firezhfox-2241043

5.HashMap1.7和1.8有哪些区别

参考: https://blog.csdn.net/qq_36520235/article/details/82417949

由数组+链表的结构改为数组+链表+红黑树。

优化了高位运算的hash算法:h^(h>>>16)

扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。

头插改为尾插

(1)由 数组+链表 的结构改为 数组+链表+红黑树 。

拉链过长会严重影响hashmap的性能, 在链表元素数量超过8时改为红黑树,少于6时改为链表,中间7不改是避免频繁转换降低性能。

(2) 优化了高位运算的hash算法

h^(h>>>16)将hashcode无符号右移16位,让高16位和低16位进行异或。

(3)扩容 扩容后数据存储位置的计算方式也不一样

1.7是直接用hash值和需要扩容的二进制数进行与操作,1.8(n-1)&hash,位运算省去了重新计算hash,只需要判断hash值新增的位是0还是1,0的话索引没变,1的话索引变为原索引加原来的数组长度 ,且链表顺序不变。

(4)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法。

因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序链表形成环路导致死循环问题。 但是在JDK1.8之后是因为加入了红黑树,使用尾插法,能够避免出现逆序且链表死循环的问题。

6.解决hash冲突的时候,为什么用红黑树

链表取元素是从头结点一直遍历到对应的结点,这个过程的复杂度是O(N) , 而红黑树基于二叉树的结构,查找元素的复杂度为O(logN) , 所以,当元素个数过多时,用红黑树存储可以提高搜索的效率。

7.红黑树的效率高,为什么一开始就用红黑树存储呢?

红黑树虽然查询效率比链表高,但是结点占用的空间大,treenodes的大小大约是常规节点的两倍 只有达到一定的数目才有树化的意义,这是基于时间和空间的平衡考虑。 如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

8.不用红黑树,用二叉查找树可以不

https://blog.csdn.net/T_yoo_csdn/article/details/87163439

但是二叉查找树在特殊情况下会变成一条线性结构

如果构建根节点以后插入的数据是有序的,那么构造出来的二叉搜索树就不是平衡树,而是一个链表,它的时间复杂度就是 O(n),遍历查找会非常慢。

红黑树,每次更新数据以后再进行平衡,以此来保证其查找效率。

9.为什么阀值是8才转为红黑树

容器中节点分布在hash桶中的频率遵循泊松分布

各个长度的命中概率依次递减,源码注释中给我们展示了1-8长度的具体命中概率。

当长度为8的时候,概率概率仅为0.00000006,这么小的概率,大于上千万个数据时HashMap的红黑树转换几乎不会发生。

10.为什么退化为链表的阈值是6

主要是一个过渡,避免链表和红黑树之间频繁的转换。 如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊, 就会频繁的发生树转链表、链表转树,效率会很低。

11.hash冲突你还知道哪些解决办法?

(1)开放定址法 (2)链地址法 (3)再哈希法 (4)公共溢出区域法

12.HashMap在什么条件下扩容

如果bucket满了(超过load factor*current capacity),就要resize。

为什么负载因子是0.75 小于0.5,空着一半就扩容了, 如果是0.5 , 那么每次达到容量的一半就进行扩容,默认容量是16, 达到8就扩容成32,达到16就扩容成64, 最终使用空间和未使用空间的差值会逐渐增加,空间利用率低下。

当负载因子是1.0的时候, 出现大量的Hash的冲突时,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。

是0.75的时候

空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。

13.HashMap中hash函数怎么实现的?还有哪些hash函数的实现方式?

对key的hashCode 和右移16位做异或运算,之后hash(key) & (capacity - 1)做按位与运算得到下标。

Hash函数是指把一个大范围映射到一个小范围。把大范围映射到一个小范围的目的往往是为了节省空间,使得数据容易保存。

如果不同的输入得到了同一个哈希值,就发生了"哈希碰撞"(collision)。

比较出名的有MurmurHash、MD4、MD5等等。

14.为什么不直接将hashcode作为哈希值去做取模,而是要先高16位异或低16位?

均匀散列表的下标,降低hash冲突的几率。 不融合高低位,hashcode返回的值都是高位的变动的话,造成散列的值都是同一个。 融合后,高位的数据会影响到 index 的变换,依然可以保持散列的随机性。 打个比方,当我们的length为16的时候,哈希码(字符串“abcabcabcabcabc”的key对应的哈希码)对(16-1)与操作,对于多个key生成的hashCode,只要哈希码的后4位为0,不论不论高位怎么变化,最终的结果均为0。 扰动函数优化后:减少了碰撞的几率。

15.为什么扩容是2的次幂?

%运算不如位移运算快

在 B 是 2 的幂情况下:A % B = A & (B - 1)

和这个(n - 1) & hash的计算方法有着千丝万缕的关系 按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞。

而长度为5的时候,3&(5-1)=0 2&(5-1)=0,都在0上,出现碰撞了

16.链表的查找的时间复杂度是多少?

HashMap 如果完全不存在冲突则 通过 key 获取 value 的时间复杂度就是 O(1), 如果出现哈希碰撞,HashMap 里面每一个数组(桶)里面存的其实是一个链表,这时候再通过 key 获取 value 的时候时间复杂度就变成了 O(n), HashMap 当一个 key 碰撞次数超过8 的时候就会把链表转换成红黑树,使得查询的时间复杂度变成了O(logN)。 通过高16位异或低16位运算降低hash冲突几率。

17.红黑树