红黑树学习笔记:红黑树学习
HashMap 介绍
hashmap支持null键值
jdk1.8前hashmap由数组加链表组成,数组是hashmap主体,链表是为了解决hash冲突。
jdk1.8之后引入了红黑树,当链表长度大于阈值(默认为8)并且当前数组长度大于64时,此时索引位置上的数组会转化为红黑树,但是如果阈值大于8,但是数组长度小于64,并不会转换成红黑树,而是进行数组扩容。(阈值大于8且长度大于64后转红黑树效率会变快)
HashMap存储逻辑
根据key值的hashCode()值进行无符号右移(>>>),按位异或(^),按位与(&)计算出索引。如果索引处已经有值,判断两个key的hashCode()是否相等,不相等则在该索引创建链表向下存储,如果hash值相等,则发生了hash碰撞,需要判断两个key的值是否相同如果相同则覆盖,不相同则顺着链表依次向下执行以上逻辑。
1.HashMap中Hash函数怎么实现的?
对key的hashCode值做hash操作,无符号右移然后做异或运算。
还有伪随机数法和取余数法,位运算是效率最高的
2.HashCode值相等怎么办?
判断两个key的值是否相等,如果相等则覆盖value,否则顺着链表向下依次对比,如果链表长度超过8且数组长度超过64则转为红黑树
3.HashMap扩容逻辑?
当存储的元素个数超出临界值时进行扩容,长度为原数组的2倍,并且复制原来的数组。
4.为什么要添加红黑树?
当存储数据量较大时,可能一个索引下存储了很多个数据,这些数据都在一个链表上,此时读取数据的时间复杂度是O(n),而如果转换成红黑树,那么时间复杂度为O(logn),读取效率变高。当红黑树内元素被删除的少于6位后,会重新变成链表。
5.为什么HashMap的长度为啥必须是2的n次幂
为了减少hash碰撞。为了让hashMap存储高效,最好能把数据比较均匀的存放在每一个索引上,而比较好的办法就是取余,hash%length,而hash%length==hash&(length-1)(前提是length=2的n次幂)
6.怎么做到长度自动变为2的n次幂的?
数组长度一定是2的n次幂,如果指定长度不是2的n次幂,则通过无符号右移,按位或运算,算出大于指定值的最小的2的n次幂
7.为什么当链表长度超过8才转红黑树?
因为在随机哈希值的概率下,理论上随机到每个桶的概率是满足泊松分布的。泊松分布理论中,一个链表长度达到8的概率为0.00000006,而又因为红黑树虽然查询效率高,但是占用的空间是链表的2倍,所以其实java还是不太希望链表的长度转化为红黑树的,因此阈值设置为8 。
8.为什么链表长度<=6之后变回链表?
变回链表是因为链表长度小于6的时候和红黑树的查询速度差距是很小的,而设置为6的原因是防止频繁切换链表和红黑树,比如如果小于8就转换,则如果红黑树删除一个元素变为7,转换为链表,如果这时又添加了一个元素,则又变为红黑树,影响性能,因此中间长度为7做过度。
HashMap比较重要的常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认数组长度
static final int MAXIMUM_CAPACITY = 1 << 30; //最大数组长度
static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子
static final int TREEIFY_THRESHOLD = 8; //链表转换为红黑树的阈值
static final int UNTREEIFY_THRESHOLD = 6; //红黑树转换为链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64; //数组长度大于这个值链表才 (要同时满足MIN_TREEIFY_CAPACITY和TREEIFY_THRESHOLD)
int threshold; //边界值,当数组内的元素大于该值就扩容,=数组长度*负载因子
HashMap构造方法
public HashMap(int initialCapacity, float loadFactor) {
//initialCapacity指定长度,loadFactor负载因子默认0.75
if (initialCapacity < 0)
//长度小于0就报错
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//长度大于最大值,则将最大值赋值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断负载因子是否为数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//负载因子
this.loadFactor = loadFactor;
//计算大于指定长度的最小的2的n次幂
this.threshold = tableSizeFor(initialCapacity);
}
//返回大于指定长度的最小的2的n次幂
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
this.threshold = tableSizeFor(initialCapacity);构造方法中直接将计算的数组长度赋值给了边界值字段,这是因为jdk1.8之后不在构造方法中初始化table了,而是推迟到put方法中初始化table,会在table中重新计算边界值
成员方法
- put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(h = key.hashCode()) ^ (h >>> 16);
目的是为了减少hash碰撞,增加性能。
向右移16位做异或运算,是为了保留高16位的特征,如果直接与,那么高16位的特征大概率直接丢失,而先向右移16位做异或运算,可以将低16位和高16位的特征进行混合,从而能更好的保留高位和地位的特征。
如果是用|或者&?
如果是|,则运算结果会偏向1.
如果是&,运算结果会偏向0.
HashMap扩容
结论:jdk8之后,HashMap扩容不需要rehash,原数组中的元素位置要么还是原索引,要么是索引+原数组长度。比如数组长度16,两个元素索引都为5,那么扩容之后,如果元素的bit以0开头 则位置不变,以1开头索引变为5+16=21
链表部分扩容源码解析
//Node<K,V> loHead = null, loTail = null 低位链,即和旧数组长度进行与运算为0的,在新数组中索引不变。
//Node<K,V> hiHead = null, hiTail = null 高位链,hash值和旧数组长度进行与运算结果不为0,在新数组中下标=旧索引+旧数组长度。
//原理:
//旧数组长度为16,设下标为15的桶中有n个元素,那么这些元素的hash值后4位一定都是1111
//因为索引是根据(length-1)& hash 计算得出的,15的二进制为1111,与运算是有0为0,那么hash值和((length-1)=1111)进行与运算得出1111,hash的后四位也只能是1111。
//而扩容之后,此链表内的元素重新计算索引,要么是原位置不变,要么是原索引+原数组长度。
//例:
//hash后5位->: 11111
//(newLength-1) = 31 -> 11111
//与运算结果-> 11111 -> 31
//
//hash后5位->: 01111
//(newLength-1) = 31 -> 11111
//与运算结果-> 01111 -> 15
//从上面运算可以得出 扩容后的链表索引如果hash第1位是0则在原位,是1则等于原索引+旧索引长度
//而源码中是和旧数组长度进行与运算,如下
//(e.hash & oldCap),hash值和旧数组长度进行与运算,旧数组为16->10000,那么hash值只有后5位会参与运算,而后4位都是1,那么与运算的结果只能是00000或者10000
// ?1111
//& 10000
// ?0000 ?=0或1
//可以得出 当(e.hash & oldCap)=0,则说明hash值后五位的首位一定是0,否则是1,再根据上面的规律,那么首位为0 则属于低位链,为1则属于高位链。
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
//这部分代码困惑了我好几天,现在终于明白了。
//第一次进来loTail = null , loHead = e , loTail = e 此时 loHead=loTail
//第二次进来 loTail.next = e,那么loHead.next=e ,然后 loTail = e , 此时 loTail指向的位置是loTail的子节点,即loTail=loHead.next
//第三次进来 loTail指向的是loHead的子节点, loTail.next = e ,那么就相当于loHead.next.next=e , loTail = e , 此时 loTail又指向loHead.next.next
//以此类推,loTail一直指向的是loHead的尾结点。
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//此时高位链和低位链已经分配完毕,但是loTail.next可能还有值,并且可能是高位链的节点,所以需要将子节点赋值为null。
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}