HashMap源码阅读——红黑树
上节我们提到了jdk1.8中引入了红黑树来解决一个桶下链表过长的问题。
关键参数
HashMap中有三个关于红黑树的关键参数
//一个桶的树化阈值
//当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
//这个值必须为 8,要不然频繁转换效率也不高
static final int TREEIFY_THRESHOLD = 8;
//一个树的链表还原阈值
//当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构
//这个值应该比上面那个小,至少为 6,避免频繁转换
static final int UNTREEIFY_THRESHOLD = 6;
//哈希表的最小树形化容量
//当哈希表中的容量大于这个值时,表中的桶才能进行树形化
//否则桶内元素太多时会扩容,而不是树形化
//为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
红黑树定义
- 红黑树的每个节点的颜色非红即黑
- 根节点是黑色的
- 空叶子节点是黑色的
- 如果一个节点是红色的,那么它的子节点必须是黑色的
- 从任意节点都该节点的子孙节点的所有路径上包含相同数目的黑点树
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 O(logn)
treeifyBin
HashMap中树形化最重要的一个方法treeifyBin() 即树形化。在一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是8),就使用红黑树来替换链表。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果hash表为空或者hash表的容量小于MIN_TREEIFY_CAPACITY(64),那么就去新建或者扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//新建一个树形节点,内容和当前链表节点一致
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p; //头节点
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
//之前得到的只是一个链表状的二叉树,下一步格式化红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
红黑树的具体格式化过程
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) { //根节点
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) { //遍历已经生成的红黑树
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)//这里的hash是经过扰动函数之后得到的hash
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null && //hash一样且(key值不可比较或者两者key值相等)
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root); //把根节点放到桶中
}
可以看到,将二叉树变为红黑树时,需要保证有序。这里有个双重循环,拿树中的所有节点和当前节点的哈希值进行对比(如果哈希值相等,就对比键,这里不用完全有序,只是保证rebalance时的插入一致性)
参考文献
Java 集合深入理解(17):HashMap 在 JDK 1.8 后新增的红黑树结构
重温数据结构:深入理解红黑树
红黑树代码实现