HashMap中初始化大小为什么是16? 为什么链表的长度为8是变成红黑树?为什么为6时又变成链表?
一 HashMap中初始化大小为什么是16?
首先我们看hashMap的源码可知当新put一个数据时会进行计算位于table数组(也称为桶)中的下标:
int index =key.hashCode()&(length-1);
hahmap每次扩容都是以 2的整数次幂进行扩容
比如:
十进制: 201314
二进制: 11 0001 0010 0110 0010
假设初始化大小为16
15转化为二进制: 1111
index : 11 0001 0010 0110 0010 & 1111 =0010 为 3
假设初始化大小为10
10转化为二进制: 1010
index: 11 0001 0010 0110 0010 & 1010=0010 为 3
因为是将二进制进行按位于,(16-1) 是 1111,末位是1,这样也能保证计算后的index既可以是奇数也可以是偶数,并且只要传进来的key足够分散,均匀那么按位于的时候获得的index就会减少重复,这样也就减少了hash的碰撞以及hashMap的查询效率。
那么到了这里你也许会问? 那么就然16可以,是不是只要是2的整数次幂就可以呢?
答案是肯定的。那为什么不是8,4呢? 因为是8或者4的话很容易导致map扩容影响性能,如果分配的太大的话又会浪费资源,所以就使用16作为初始大小
总结: 1 减少hash碰撞
2 提高map查询效率
3 分配过小防止频繁扩容
4 分配过大浪费资源
二 为什么链表的长度为8是变成红黑树?为什么为6时又变成链表?
因为,大部分的文章都是分析链表是怎么转换成红黑树的,但是并没有说明为什么当链表长度为8的时候才做转换动作。本人 第 一反应也是一样,只能初略的猜测是因为时间和空间的权衡
首先当链表长度为6时 查询的平均长度为 n/2=3
红黑树为 log(6)=2.6
为8时 : 链表 8/2=4
红黑树 log(8)=3
根据两者的函数图也可以知道随着bin中的数量越多那么红黑树花的时间远远比链表少,所以我觉得这也是原因之一。为7的时候两者应该是 链表花的时间小于红黑树的,但是为什么不是在7的时候转成链表呢,我觉得可能是因为把7当做一个链表和红黑树的过渡点。
事实上真的是因为考虑到时间复杂度所以才把是在8的时候进行转成红黑树吗?其实这并不是真正的原因
至于为什么阈值是8,我想,去源码中找寻答案应该是最可靠的途径。
8这个阈值定义在HashMap中,如下所示,这段注释只说明了8是bin(bin就是bucket,即HashMap中hashCode值一样的元素保存的地方)从链表转成树的阈值,但是并没有说明为什么是8:
/**
- The bin count threshold for using a tree rather than list for a
- bin. Bins are converted to trees when adding an element to a
- bin with at least this many nodes. The value must be greater
- than 2 and should be at least 8 to mesh with assumptions in
- tree removal about conversion back to plain bins upon shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
我们继续往下看,在HashMap中有一段Implementation notes,笔者摘录了几段重要的描述,第一段如下所示,大概含义是当bin变得很大的时候,就会被转换成TreeNodes中的bin,其结构和TreeMap相似,也就是红黑树:
This map usually acts as a binned (bucketed) hash table, but
when bins get too large, they are transformed into bins of TreeNodes,
each structured similarly to those in java.util.TreeMap
继续往下看,TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin。
这样就解析了为什么不是一开始就将其转换为TreeNodes,而是需要一定节点数才转为TreeNodes,说白了就是trade-off,空间和时间的权衡:
Because TreeNodes are about twice the size of regular nodes, we
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due to
removal or resizing) they are converted back to plain bins. In
usages with well-distributed user hashCodes, tree bins are
rarely used. Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5)*pow(0.5, k)/factorial(k)).
The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
这段内容还说到:当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。
通俗点将就是put进去的key进行计算hashCode时 只要选择计算hash值的算法足够好(hash碰撞率极低),从而遵循泊松分布,使得桶中挂载的bin的数量等于8的概率非常小,从而转换为红黑树的概率也小,反之则概率大。
所以,之所以选择8,不是拍脑袋决定的,而是根据概率统计决定的。由此可见,发展30年的Java每一项改动和优化都是非常严谨和科学的。
一下提供一个测试链表转红黑树以及扩容的例子:
package com.zjh.map;
import java.util.regex.Pattern;
public class MapKey {
private static final String REG = "[0-9]+";
private String key;
public MapKey(String key) {
this.key = key;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MapKey mapKey = (MapKey) o;
return !(key != null ? !key.equals(mapKey.key) : mapKey.key != null);
}
/*
* 确保每次key的hashCode都相同
*/
@Override
public int hashCode() {
if (key == null)
return 0;
Pattern pattern = Pattern.compile(REG);
if (pattern.matcher(key).matches())
return 1;
else
return 2;
}
@Override
public String toString() {
return key;
}
}
MapKey 用于重写了hashCode使得hashCode碰撞极高,可以看到链表转红黑树的过程
package com.zjh.map;
import java.util.HashMap;
import java.util.Map;
/**
* 创建时间: 2019/6/17 23:19
* 创建人: zjh
* 描述: 用于测试map 从链表转变成红黑树的过程
*/
public class MapTest {
public static void main(String[] args){
Map<MapKey,String> map = new HashMap<MapKey, String>();
//第一阶段
for (int i = 0; i < 6; i++) {
map.put(new MapKey(String.valueOf(i)), "A");
}
//第二阶段
for (int i = 0; i < 10; i++) {
map.put(new MapKey(String.valueOf(i)), "A");
}
//第三阶段
for (int i = 0; i < 50; i++) {
map.put(new MapKey(String.valueOf(i)), "A");
}
//第四阶段
map.put(new MapKey("Z"), "B");
map.put(new MapKey("J"), "B");
map.put(new MapKey("F"), "B");
System.out.println(map);
}
}
下面逐个阶段通过debug,查看map中的数据。
注意,在使用IDEA查看map的数据时,要设置view as Object。如下图所示:
第一阶段
这个时候桶中bin的数量小于TREEIFY_THRESHOLD 。
Debug如下所示:
第二阶段:
此时还是链表的数据结构
第三阶段
这个时候桶中bin的数量大于了TREEIFY_THRESHOLD 且 capacity大于了MIN_TREEIFY_CAPACITY ,因此,会树化。
对这个输出map的值,可以看到是乱序的,因为是使用树形结构进行存储的。
第四阶段
这个阶段主要是测试,如果一个桶采用了树形结构存储,其他桶是不是也采用树形结构存储。结论是,如果其他桶中bin的数量没有超过TREEIFY_THRESHOLD,则用链表存储,如果超过TREEIFY_THRESHOLD ,则用树形存储。
此时table已中第一个是红黑树,第二个依然是链表
所以由链表变成红黑树也只是当前桶挂载的bin会进行转换,不会影响其它桶的数据结构*
阿里的人问 数组的时间复杂度是多少,链表的是多少,hashmap的时间复杂度是多少。
后来才知道,时间复杂度是要区分 增删改查的。主要看查询的时间复杂度;
1、数组 查询的时间复杂度 O(n)
2、链表 查询的时间复杂度 O(n)
3、hashmap 查询的时间复杂度 O(1)
数组 查询的时间复杂度 O(n)
建议看一下下面的博客:
hashSet,hashtable,hashMap 都是基于散列函数, 时间复杂度 O(1) 但是如果太差的话是O(n)
TreeSet==>O(log(n))==> 基于树的搜索,只需要搜索一半即可
O⑴的原因是离散后,下标对应关键字
hash就是散列,甚至再散列。但是我一直对hash表的时间复杂度有个疑问。一个需要存储的字符串,通过hash函数散列到一个相对较短的索引,使得存取速度加快。但为什么存取的时间复杂度能达到常量级O(1)呢?? 查找时搜索索引不需要费时间吗?为什么不是O(n)呢? n是hash表的长度,
如果对Hashtable的构造有很深的理解的话,就知道了,Hashtable 其实是综合了数组和链表的优点,当Hashtable对数值进行搜索的时候,首先用该数值与Hashtable的长度做了取模的操作,得到的数字直接作为hashtable中entry数组的index,因为hashtable是由entry数组组成的,因此,可以直接定位到指定的位置,不需要搜索,当然,这里还有个问题,每个entry其实是链表,如果entry有很多值的话,还是需要挨个遍历的,因此可以这样讲Hashtable的时间复杂度最好是O(1)但是最差是 O(n) 最差的时候也就是hashtable中所有的值的hash值都一样,都分配在一个entry里面,当然这个概率跟中1亿彩票的概率相差不大。
如果还不理解可以参考专门的博客:
关于HashMap的:http://www.cnblogs.com/aspirant/p/8908399.html
关于Hashtable的:http://www.cnblogs.com/aspirant/p/8906018.html
在看起来就是对Entry链表的循环的时间复杂度影响最大,链表查找的时间复杂度为O(n),与链表长度有关。我们要保证那个链表长度为1,才可以说时间复杂度能满足O(1)。但这么说来只有那个hash算法尽量减少冲突,才能使链表长度尽可能短,理想状态为1。因此可以得出结论:HashMap的查找时间复杂度只有在最理想的情况下才会为O(1),最差是O(n),而要保证这个理想状态不是我们开发者控制的。
常用数据结构的时间复杂度 常用数据结构的时间复杂度
如何选择数据结构
Array (T[])
当元素的数量是固定的,并且需要使用下标时。
Linked list (LinkedList)
当元素需要能够在列表的两端添加时。否则使用 List。
Resizable array list (List)
当元素的数量不是固定的,并且需要使用下标时。
Stack (Stack)
当需要实现 LIFO(Last In First Out)时。
Queue (Queue)
当需要实现 FIFO(First In First Out)时。
Hash table (Dictionary<K,T>)
当需要使用键值对(Key-Value)来快速添加和查找,并且元素没有特定的顺序时。
Tree-based dictionary (SortedDictionary<K,T>)
当需要使用价值对(Key-Value)来快速添加和查找,并且元素根据 Key 来排序时。
Hash table based set (HashSet)
当需要保存一组唯一的值,并且元素没有特定顺序时。
Tree based set (SortedSet)
当需要保存一组唯一的值,并且元素需要排序时。