继上一篇文章hashmap源码剖析的第一部分,由于树化的过程较为复杂,放到此篇来进行详细详解,熟读此篇文章,可以同时掌握TreeMap和LinkedList等数据结构;同样,论述方式按照上一篇文章的风格,注:代码均来自jdk8的hashmap源码
1,详尽注释源码部分;
2,选取部分复杂流程作图解释;
3,总结成述;
从treeifyBin(tab, hash);作为入口开始分析:
// 入参 tab hash桶数组,该hash值所对应的位置需要树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
// n用来表示数组长度,index表示位置,e用来保存首节点引用
int n, index; Node<K,V> e;
// 如果数组为空,或者数组的长度小于最小树化容量,
// 都只进行扩容优化不进行结构优化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 参照上一篇文章的详解
resize();
// 数组长度和当前hash值计算出数组位置,判断该位置是否为空
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 树节点hd用来表示头指针,tl表示尾指针;
TreeNode<K,V> hd = null, tl = null;
do {
// 这里的replacementTreeNode()方法,
// 点进去之后是根据当前节点e创建一个树形节点p
TreeNode<K,V> p = replacementTreeNode(e, null);
// 第一次判断尾指针为空
if (tl == null)
// 将当前第一个节点首节点p,作为头指针的引用,hd指向p
hd = p;
else {
/* 第二次遍历,尾部指针指向尾部的p此时不为空
将当前节点的前置指针指向上一个循环上一个节点尾部节点,
同时将尾节点的后继指针指向当前节点
*/
p.prev = tl;
tl.next = p;
}
// 将尾部指针tl指向当前节点p;
// 第二轮循环,继续将尾部指针移动到当前节点
tl = p;
// e继续向后移动
} while ((e = e.next) != null);
// 循环结束,原先的单链表,变成了头指针hd,尾部指针tl的双向链表,
if ((tab[index] = hd) != null)
// 此时开始进行由双向链表进行树化
hd.treeify(tab);
}
}
接下来,我们继续分析treeify的源码细节;
final void treeify(Node<K,V>[] tab) {
// 声明根节点root
TreeNode<K,V> root = null;
// 初始化x为当前的树形节点,双向链表首节点hd
for (TreeNode<K,V> x = this, next;
x != null; x = next) {
// 保存首节点的后继节点至next节点
next = (TreeNode<K,V>)x.next;
// 当前节点x的左右孩子置为空
x.left = x.right = null;
// 根为null进入判断
if (root == null) {
/* 当前节点的父节点置为null,
且节点颜色置为黑色,x赋值给根节点 */
x.parent = null;
x.red = false;
root = x;
}
else {
// 第二次循环进入,获取当前节点的key值
K k = x.key;
// 获取当前节点的hash值
int h = x.hash;
// 比较器类型kc为null
Class<?> kc = null;
// 从根节点开始遍历
for (TreeNode<K,V> p = root;;) {
// dir来表示key值比较结果;
// ph表示当前节点p的hash值;
int dir, ph;
// 获取遍历到当前节点p的key值
K pk = p.key;
// 将当前节点的hash值赋值给ph,如果大于即将插入的
// 节点x对应的hash值,那么比较结果为-1
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
// 如果小于则为1
dir = 1;
/* 如果比较器为null且比较器返回类型为null
或者,计算p节点的key值和x的key的比较结果为0
*/
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 则直接比较两个节点key的hashcode值,得到结果
dir = tieBreakOrder(k, pk);
// 声明xp节点指向当前p节点
TreeNode<K,V> xp = p;
/* 如果比较结果小于0,那么将当前节点p的左孩子赋值给p
否则将右孩子赋值给p,如果p为空,那么待插入的节点父节点
直接指向xp节点(p节点赋值之前的节点)
*/
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
/* 如果比较结果小于0,那么说明key值的比较结果小于当前p节点,
那么将该节点置于xp节点的左孩子 */
if (dir <= 0)
xp.left = x;
else
// 反之,置于xp的节点的右孩子
xp.right = x;
// 插入完成后平衡插入,因为红黑树的特点,插入节点后
// 要进行高度调整和颜色调整,满足红黑树规则
// 下面将继续讨论平衡插入的过程
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
接下来,我们开始对balanceInsertion(root, x)的过程进行源码分析:
关于红黑树的调整方式,一些前置知识阅读下面的内容,暂时推荐两篇文章
一、红黑树基础描述
二、红黑树调整插入调整
注:第二篇文章有些笔误部分,阅读的时候需要注意下!!!
后续有时间的化,会把红黑树的数据结构以更简化的方式写出来
// 入参1:root为根节点的树,入参2:x插入的节点
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
// 新插入的节点的颜色必须是红色(红黑树插入规则)
x.red = true;
// 循环遍历,xp,xpp,xppl,xppr依次表示为x节点的父亲,爷爷,爷爷的左孩子,爷爷的右孩子
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 将x的父亲节点赋值给xp节点,如果为null的化,说明x为根节点
// 那么直接将x节点的颜色变为黑色(红黑树规则),然后返回根节点
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 如果父节点是黑色,或者
// 将父节点的父节点赋值给爷爷节点,爷爷节点为空的化,
// 那么,直接返回root根节点,即不需要调整树结构
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 没有走else if逻辑代表此时xp的颜色已经是红色了
// 将爷爷节点的左孩子赋值给xppl,如果x的父节点是爷爷节点的左孩子
// 即x节点的父节点xp是作为其父节点xpp的左孩子
if (xp == (xppl = xpp.left)) {
// 此时x的父亲节点已经是左孩子,那么继续判断,x的叔叔节点
// 不为空并且x的叔叔节点xppr的颜色为红色的化
if ((xppr = xpp.right) != null && xppr.red) {
// 此时的情况总结为,x为父亲为红色左孩子,x的叔叔为红色右孩子
// 且插入的x的颜色为红色
// 将叔叔的颜色变为黑色,将父亲的颜色变黑色,将当前x的节点
// 继续往上跳到爷爷节点,继续向上调整爷爷的颜色,颜色调整是一个自底向上不断调整的过程
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
// 进入else逻辑,代表叔叔的节点为黑色,即父亲红,叔叔黑的情况
if (x == xp.right) {
/* 如果此时,x节点本身是父亲节点的右孩子,
那么右父亲为爷爷节点的左孩子,
x为父亲节点的有孩子,即左右插入情况 */
// 那么进行一次左旋操作,同时将x的节点赋值为父亲节点
// 因为左旋之后,x节点和xp节点身份互换,x将成为父节点
// xp成为子节点,所以将x赋值成父节点
root = rotateLeft(root, x = xp);
// 重新将xp的值赋值为x节点的父节点,如果父节点为null
// 那么将爷爷节点置为null,否则指向父节点xp的父节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 如果父亲节点不为null,先将父节点颜色变黑
if (xp != null) {
xp.red = false;
// 如果爷爷的节点不为空
if (xpp != null) {
// 将爷爷的颜色变成红色,
xpp.red = true;
// 然后以爷爷节点开始进行右旋操作,结束第一次遍历调整
root = rotateRight(root, xpp);
}
}
}
}
else {
// 此处过程与x的父亲节点为左孩子的处理规则类似,读者可以结合
// if逻辑的处理结合判断,加深印象并且做相应的总结
// 此处,x的叔叔不为null,且左孩子的颜色为红色,即x的父亲为右孩子,颜色为红色
if (xppl != null && xppl.red) {
// 此时将叔叔的颜色变黑,父亲的颜色变黑,与上面父亲为左孩子的处理逻辑一样
xppl.red = false;
xp.red = false;
// 爷爷的颜色变红,
xpp.red = true;
// x继续往上跳到爷爷,接着自底向上开始遍历调整
x = xpp;
}
else {
// 此时,父亲为右孩子,如果叔叔为null或者叔叔的颜色为黑色
// 如果x为父亲的左孩子,即父右子左,右左插入
if (x == xp.left) {
// 那么第一步先将x与父亲节点互换,因为右旋会导致身份互换
root = rotateRight(root, x = xp);
// 右旋之后,将x的节点父亲重新赋值给xp,完成身份互换
// 如果父亲节点为null,那爷爷节点为null,否则爷爷节点为父节点的父节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 如果x的父亲节点不为null
if (xp != null) {
// 那么将父亲节点颜色变为黑色
xp.red = false;
// 如果爷爷节点存在
if (xpp != null) {
// 那么将爷爷节点颜色变为红,然后以爷爷节点为中心左旋
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
接下来,将代码里的部分方法的实现继续深入分析,首先是左旋操作rotateLeft(root, xpp),右旋操作与此类似不作分析,如下:
// 入参1:待旋转操作的树 入参2:旋转中心点
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
// 声明树节点r表示旋转点的右孩子,pp代表
TreeNode<K,V> r, pp, rl;
// 旋转点p不为null,且p的右孩子赋值给r也不为null
if (p != null && (r = p.right) != null) {
// 将旋转点p的右孩子的左孩子赋值给旋转点p的右孩子赋值给rl,rl不为空
if ((rl = p.right = r.left) != null)
// 那么将p节点的右孩子的左孩子rl的父节点指向旋转点p
rl.parent = p;
// 如果旋转点的父节点赋值给 右孩子的父节点(旋转后r将成为父节点,要保留旋转前p节点的父节点引用)
// 然后将旋转前的p节点的父节点应用保存到pp,如果pp节点为null
if ((pp = r.parent = p.parent) == null)
// 说明旋转点为根节点,那么将右孩子赋值给root根节点
// 且颜色变为黑色(红黑树规则)
(root = r).red = false;
// 如果不为null,且旋转点为左孩子
else if (pp.left == p)
// 那么将旋转之后的r作为pp节点的左孩子
pp.left = r;
else
// 反之,将旋转之后的r作为pp节点的右孩子
pp.right = r;
// 旋转之后,将原先的旋转点p作为新的中心点r的左孩子
r.left = p;
// 并且将左孩子p的父节点指向r
p.parent = r;
}
// 返回旋转后的root
return root;
}
左旋转小结,红黑树里面节点,每个节点保留着对父节点,和左右孩子的引用,左旋的时候,将旋转点的右孩子置为中心点,将右孩子的左孩子作为旋转点的右孩子,将旋转点作为中心点的左孩子,同时更改各个节点的父节点以及子节点的引用。下面以简单的图作为示例进行展示:
分析完balanceInsertion()方法,继续回到treeify方法的部分,最后一个moveRootToFront(tab, root);该方法的实现比较简单,即保证形成的红黑树的根节点为hashmap数组的该位置的首节点,即tab[i]与root相等;
总结:
1、hashmap的树化过程,借用来红黑树,此篇大致介绍来红黑树的插入调整和颜色调整规则;
2、插入调整的流程可以总结为:
2-1、父节点黑,直接插入不需要调整;
2-2、父节点为红色,以父亲节点左子节点分析
1). 叔叔节点为红色,那么只需要通过变色来调整即可完成红黑树规则调整,此时将父亲和叔叔变黑,然后跳至爷爷节点,继续向上遍历调整;
2). 叔叔节点为黑色
2-2-1、如果给插入节点为左子节点,那么将父亲变黑,爷爷变红然后以爷爷节点进行右旋操作;
2-2-2、如果插入节点为父亲节点右子节点,那么先左旋,然后再将父亲变黑,爷爷变红,以爷爷节点进行右旋操作;
3、父亲节点为红色,父亲节点为右子节点,与第二种情况类似,调整方向相反,读者可以自行分析,以加深印象;