JDK1.8HashMap之红黑树学习
红黑树出现的意义
一般的二叉查找树理想情况下时间复杂度是O(lgN),但是有退化成链表的时候,这时的二叉查找树的时间复杂度可变为O(N).所以出现了B-树,B+树,AVL树,红黑树等这类型需要维护树的平衡甚至是树的高度的数据结构,我们本篇文章主要是讲红黑树。
红黑树的定义:
1.红黑树中的节点非黑即红。
2.根节点是黑色的。
3.从根节点出发到叶子节点的路径中,不能出现两个连续的红点,反之不一定。
4.从根节点出发到叶子节点的所有路径中,黑色节点的个数相等。
5.新插入的节点颜色是红色的。
JDK 1.8中HashMap的红黑色树平衡操作
本文将以往红黑树插入元素的一个真实过程展示红黑树的变化,并结合这个过程一步步分析源码的流程。
插入100,是根节点,涂黑返回。图一:
插入41,200,因为父节点100是黑色的,无需任何平衡操作,都是返回。图二:
插入40,父亲节点41和叔叔节点200都是红色的,只需将父亲节点和叔叔节点涂黑,将爷爷节点100涂红,将爷爷设置为当前节点x,进入下一轮循环,因为爷爷节点是根节点,所以下一循环中又被涂黑了,结束。图三:
插入60,父节点41是黑色,无需平衡操作,返回。图四:
插入50,父节点60和叔叔节点40都是红色,只需将父节点和叔叔节点涂黑,爷爷节点41涂红设置为当前节点x,进入下一轮循环,因41的父节点是黑色的,返回结束。图五:
插入70,父节点60是黑色,无需平衡操作,返回。图六:
插入79,父节点70和叔叔节点50都是红色,将父节点和叔叔节点涂黑,爷爷节点60涂红,并设置为60当前节点x,进入下一轮循环,未结束。图七:
当前节点x=60,它的父节点xp=41不是根节点,也不是黑色的(违背了规则3不能连续出现两个红点),且xp=41是它父亲的左孩子,当前节点x=60是它父节点41的右孩子并且叔叔节点200不是红色的,这时候要以父节点41为支点进行左旋操作,未结束。图八:
经过左旋之后,若爷爷节点存在,都要紧接着以当前节点的爷爷节点为支点做一次右旋操作,当前节点x=41,父节点xp=60,爷爷节点xpp=100,右旋前需要将xp=60涂黑,xpp=100涂红,以xpp为支点,进行右旋,结束。图九:
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
//默认插入的节点颜色是红色的
x.red = true;
//无限循环直到return根节点
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//如果是根节点的插入,直接涂黑返回(图一)
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果插入节点的父节点是黑色的,无需做任何操作,直接返回根节点;(图二)
//或者父节点是红色的,给xpp这个引用赋值为爷爷节点,这里写得有点迷惑性
//父节点为红色的了,怎么可能还会是根节点?难道只是为了为xpp赋值?
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//如果父节点是爷爷的左孩子(图三)
if (xp == (xppl = xpp.left)) {
//如果叔叔节点不为空且是红色的,只需将父节点和叔叔节点涂黑,爷爷节点涂红,将当前节点的引用指向爷爷节点,继续下一循环的平衡操作
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
//走到这里说明叔叔节点为空或者不为红色。
//且当前节点是父节点的右孩子
if (x == xp.right) {
//需要进行左旋(图八左旋)
root = rotateLeft(root, x = xp);
//左旋完之后给爷爷节点xpp,父节点xp重新赋值
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//如果父节点不为空(图九)
if (xp != null) {
//涂黑
xp.red = false;
if (xpp != null) {
//涂红
xpp.red = true;
//右旋
root = rotateRight(root, xpp);
}
}
}
}
else {
//走到这里说明父节点是爷爷节点的右孩子
if (xppl != null && xppl.red) {
//同理,如果左叔叔不为空并且是红色的,将父节点和叔叔节点涂黑,爷爷节点涂红,并将当前节点指向爷爷节点
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
//走到这里说明左叔叔为空或者节点颜色不为红色。且如果当前节点是父节点的左孩子
if (x == xp.left) {
//右旋
root = rotateRight(root, x = xp);
//左旋完之后给爷爷节点xpp,父节点xp重新赋值
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//如果父节点不为空
if (xp != null) {
//将父节点涂黑
xp.red = false;
if (xpp != null) {
//爷爷节点涂红
xpp.red = true;
//左旋
root = rotateLeft(root, xpp);
}
}
}
}
}
}
左旋和右旋
左旋和右旋是一个相对称的操作,我们使用左旋来进行分析:
/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
//p是旋转的支点,也就是r的父节点,当旋转的时候p变成r的左孩子,r变成p的父亲,左旋前,如果r的孩子存在,旋转后因为p变成了r的左孩子,所以要将r之前的左孩子作为p的右孩子。
if (p != null && (r = p.right) != null) {
//将rl,p.right指向r的左孩子
if ((rl = p.right = r.left) != null)
//如果r的左孩子不为空,将它的父亲指向p
rl.parent = p;
//将r的父亲指向p的父亲,同时赋值给pp
if ((pp = r.parent = p.parent) == null)
//如果p刚好是根节点,涂黑,r变成根节点
(root = r).red = false;
//走到这里说明p不是根节点,如果p旋转前是父亲的左孩子,那么r将取代p变成左孩子
else if (pp.left == p)
pp.left = r;
else//否则r取代p变成右孩子
pp.right = r;
//将p作为r的左孩子
r.left = p;
//将r作为p的父亲
p.parent = r;
}
return root;
}
总结:
1.插入的节点是第一个节点,即是根节点,涂黑返回。
2.当前插入节点的父节点是黑色的,无需做任何改变。
3.当前插入节点的父节点是红色的,且是爷爷节点的左孩子,(1)如果存在叔叔节点且是红色的,将父节点和叔叔节点涂黑,将爷爷节点涂红,并将爷爷节点设置为当前节点。(2)如果不存在叔叔节点或者叔叔节点是黑色的,且当前节点是父节点的右孩子,先进行左旋,再进行右旋。否则是父节点的左孩子直接右旋(上面的图示包含了这个过程,第4点虽然没有包含,但也是同样的道理)
4.当插入节点的父节点是红色的,且是爷爷节点的右孩子,(1)如果存在叔叔节点且是红色的,将父节点和叔叔节点涂黑,将爷爷节点涂红,并将爷爷节点设置为当前节点。(2)如果不存在叔叔节点或者叔叔节点是黑色的,且当前节点是父节点的左孩子,先进行右旋,再进行左旋。否则是父节点的右孩子直接左旋。和3是相对称的操作。
附上一个数据结构可视化的网站链接。
链接: link.