前言
代码部分基于JDK1.8,ConcurrentHashMap中红黑树的实现。分析红黑树插入节点和删除节点。
红黑树的性质
红黑树的几个原则:
- (颜色属性)性质1:节点非黑即红
- (根属性)性质2:根节点一定是黑色
- (叶子属性)性质3:叶子节点(NIL)一定是黑色
- (红色属性)性质4:每个红色节点的两个子节点,都为黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- (黑色属性)性质5: 从任一节点到其每个叶子的所有路径,都包含相同数目的黑色节点。
黑色属性,可以理解为平衡特征, 如果满足不了平衡特征,就要进行平衡操作。
左旋分析
将p左旋(左下沉),即将p的右节点r替换它位置,然后p成为r的左子树,如果r也有左子树rl,rl成为p的右子树。可以看到左旋可以使右子树的高度减一,左子树高度+1。
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
//rl 成为p的右子树
if ((rl = p.right = r.left) != null)
rl.parent = p;
//r和p交换
if ((pp = r.parent = p.parent) == null)
//如果p是root节点 pr交换那么需要修改root 且r设置为黑色
(root = r).red = false;
//rp 交换显然需要更新pp的引用 即根据对应位置调整引用
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
//p成为 r的左子树
r.left = p;
p.parent = r;
}
return root;
}
右旋分析
将p右旋(右下沉),即将p的左节点l替换它位置,然后p成为l的右子树,如果l也有右子树lr,lr成为p的左子树。可以看到右旋可以使左子树的高度减一,右子树高度加1。
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
// lr 成为p的左子树
if ((lr = p.left = l.right) != null)
lr.parent = p;
//lp 交换
if ((pp = l.parent = p.parent) == null)
// 同理如果p是root节点 p 交换那么需要修改root 且l设置为黑色
(root = l).red = false;
//lp 交换显然需要更新pp的引用 即根据对应位置调整引用
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
//p成为 l的右子树
l.right = p;
p.parent = l;
}
return root;
}
红黑树插入
具体逻辑是根据比较规则对红黑树进行查询(二叉树搜索),如果查询到有对应的节点则返回节点,否则需要在插入位置进行插入并进行平衡。分以下情况:
-
树为空
直接创建新的节点赋值为root
-
存在对应的节点
类型于搜索二叉树那样查询对应的节点,然后返回那个节点
-
不存在对应的节点
搜索直到下一个节点为null,证明时是节点需要插入
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
//dir 搜索的方向 ph,pk当前节点hash和key
int dir, ph; K pk;
//树为空的情况
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
//hash比较 定位搜索方向
else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
//p何插入节点的key相同
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
//直接返回
return p;
//这里表示通过 hash无法判断顺序(hash相等) ,且无法通过compareAble进行比较
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//没有搜索过 则优先搜索下
if (!searched) {
TreeNode<K,V> q, ch;
//搜索标记
searched = true;
//在子树查找对应的节点 查询到则直接返回那节点 此时无法判断左右
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
//通过类名和hashCode 比较
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
// p 赋值对应查询方向的子节点 如果为null 当当前节点是插入节点的父节点进行插入
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K,V> x, f = first;
first = x = new TreeNode<K,V>(h, k, v, f, xp);
//记录插入顺序
if (f != null)
f.prev = x;
//根据插入方向放置插入节点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//1、情况1 父亲节点是黑色
if (!xp.red)
//那么直接将插入节点设置为红色即可,(插入节点节点是红色的)
x.red = true;
else {
//2、父亲节点为红色 违法红黑树 不能有两个连续的红色节点
lockRoot();
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
key怎么比较顺序
首先ConcurrentHashMap扰动之后的hash进行比较,如果相同则判断key是不是Comparable,是用使用compareTo方法,否则就是通过类名比较,类名相同则使用hashCode进行比较。比较规则顺序如下:
-
key.hash即内部hash进行比较//内部hash计算 static final int spread(int h) { //h和高16位进行扰动 并设置符合位为0 保证为正数 return (h ^ (h >>> 16)) & HASH_BITS; } -
Comparable比较 -
类名比较
-
hashCode比较System.identityHashCode实际调用就是hashCode()方法
//判断搜索方向的代码 dir为方向
//这里时比较hash
else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if ((kc == null && (kc = comparableClassFor(k)) == null)
//基于Comparable比较 ==0表示相等 所以通过tieBreakOrder 比较对象的hashCode和类名
|| (dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&(q = ch.findTreeNode(h, k, kc)) != null)
||((ch = p.right) != null &&(q = ch.findTreeNode(h, k, kc)) != null)){
return q;
}
dir = tieBreakOrder(k, pk);
}
//基于Comparable比较
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
// a ,b 为两个key 基于类名和HashCode比较
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
//类名比较
(d = a.getClass().getName().compareTo(b.getClass().getName())) == 0)
//直接用identityHashCode 比较
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?-1 : 1);
return d;
}
balanceInsertion-平衡插入
当插入节点的父节点为红色时需要进行平衡。根据父亲节点在左还是右以及叔叔节点的颜色分为四种情况。其他情况则不需要通过旋转进行平衡-父节点为null或者父亲节点为黑色。
1 不需要平衡情况-循环结束条件
不需要通过旋转即可保证平衡
1.1 父节点为null,设置为黑色结束
插入节点设置为黑色即为root结束,一般出现的情况是一直平衡到了父节点
1.2 其父亲节点为黑色
插入为红色节点,而父亲节点为黑色,此时不破化红黑树的平衡
2 需要平衡的情况-父亲节点为红色
2.1 父节点在左
插入节点的父亲节点是在其父左边,也就是在祖父节点在左子树
2.1.1 叔叔节点为红色
将父亲和叔叔节点变为黑色,祖父节点设置为红色,
这样会导致路径上黑色节点树+1所以需要将祖父节点设置为红色
以祖父节点当做插入位置继续平衡。
祖父节点设置为红色后,可能破化平衡,所以需要继续
2.1.2 叔叔节点为黑色,插入在左- LL型
变色-先将父节点变成黑色,祖父节点变成红色
变色后叔叔节点所在路径因为祖父节点变红而黑色节点树少1,不符合红黑树的定义
所以需要讲变为黑的父节点上去即需要右旋(在左边)
旋转-祖父节点进行右旋
右旋把祖父节点的左子树提上来,然后祖父节点变成左子树的右子树,两边路径刚好黑色节点个数相同
2.1.3 叔叔节点为黑色,插入在右-LR型
左旋-先对父节点进行左旋变成LL型
然后和LL型一样先变色后右旋
2.2 父亲节点在右-对称操作
插入节点的父亲节点是在其父右边,也就是在祖父节点的右子树
2.2.1 叔叔节点为红色
将父亲和叔叔节点变为黑色,祖父节点设置为红色,
这样会导致路径上黑色节点树+1所以需要将祖父节点设置为红色,然后以祖父节点为当前节点继续平衡
以祖父节点当做插入位置继续平衡。
祖父节点设置为红色后,可能破化平衡,所以需要继续
2.2.2 叔叔节点为黑色,插入右边-RR型
变色-先将父节点变成黑色,祖父节点变成红色
变色后叔叔节点所在路径因为祖父节点变红而黑色节点树少1,不符合红黑树的定义
所以需要讲变为黑的父节点上去即需要左旋(在右边)
旋转-祖父节点进行左旋
左旋把祖父节点的右子树提上来,然后祖父节点变成右子树的左子树,两边路径刚好黑色节点个数相同
2.2.3 叔叔节点为黑色,插入左边-RL型
右旋-先对父节点进行右旋变成RR型
然后和RR型一样先变色后左旋
代码分析如下:
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
//新插入的节点为红色
x.red = true;
//旋
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//1.1 当前节点父节点为空
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//1.2 相对于当前节点父亲节点为黑色 或当前只有1节点目的是赋值 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) {
//将xp左旋 xp变成x的左子树 那么变成LL信息
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//插入节点是在左侧 LL型
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 {
// 插入节点是在左侧 RL型
if (x == xp.left) {
//先右旋Xp(Xp成为x的右子树)变成 RR型
root = rotateRight(root, x = xp);
//
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//插入在右侧RR型
if (xp != null) {
//父节点变成黑
xp.red = false;
if (xpp != null) {
//祖父节点变成红色
xpp.red = true;
//以祖父节点进行左旋
root = rotateLeft(root, xpp);
}
}
}
}
}
}
红黑树删除
ConcurrentHashMap删除逻辑
ConcurrentHashMap删除时,先通过hash定槽,如果槽中节点时红黑树,则需要查询红黑树,若节点存在则进行红黑树的删除操作。删除后如果红黑树中的节点少,还需将红黑树转换为链表。
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//对应槽为null 或者 map为未初始化完成 则表示节点不存在
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
else if ((fh = f.hash) == MOVED) // 发现在扩容中则辅助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
//锁住对应的槽节点
synchronized (f) {
//double check 避免 加锁前已经被其他线程修改
//所以要重新判断
if (tabAt(tab, i) == f) {
//链表节点删除 大等于0表示正常链表节点 其他想树节点、转发节点、占位节点 fh<0
if (fh >= 0) {
//省略链表查询遍历删除
}
//如果是红黑树
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
//查询红黑树是否存在节点
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
//覆盖值为null 或者覆盖值等于原始值
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
//删除树形节点 并返回是否要进行逆转树为链表
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
//需要检测数量
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
removeTreeNode-删除节点
删除逻辑主要实现是,选择一个节点来替换删除节点。如果删除节点是黑色则需要进行平衡。具体替换节点情况分以下:
1 删除节点无子树
这种情况替换节点就是本身
2 删除无左子树
那么以右节点作为替换节点
3 删除节点无右子树
那么以左节点为替换节点
4 删除节点左右子树都有
则需要在右子树中找一个后继(中序后继)节点来交换删除节点,进行交换后如果交换的节点是有右子树所以需要那么就是以右子树为替换节点,否则就是本身
交换后变成 情况1和情况2
当替换节点不是本身时需要删除p在进行平衡,否则就需要先平衡后删除
final boolean removeTreeNode(TreeNode<K,V> p) {
TreeNode<K,V> next = (TreeNode<K,V>)p.next;
TreeNode<K,V> pred = p.prev; // unlink traversal pointers
TreeNode<K,V> r, rl;
if (pred == null)
first = next;
else
pred.next = next;
if (next != null)
next.prev = pred;
// 上面是解除 prev的连接 prev作用是什么?
//空树
if (first == null) {
root = null;
return true;
}
//空数或者只有一个节点或者2个节点
if ((r = root) == null || r.right == null || // too small
(rl = r.left) == null || rl.left == null)
return true;
lockRoot();
try {
//找到的替换节点
TreeNode<K,V> replacement;
TreeNode<K,V> pl = p.left;
TreeNode<K,V> pr = p.right;
//1. 左右子树都不为空 则需要交换后继节点 然后变成情况2 or 3
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
// 查询删除后继 中序遍历后继 即在右子树的左边
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red;
//----------------交换p和s(p的后继)这两个节点-----------------------------
//和删除节点交换颜色
s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
// 更新parent 引用
if (s == pr) { // p was s's direct parent
//找到的后继就行右节点 那么其左子树为空
//s 和 p进行交换
p.parent = s;
s.right = p;
}
else {
//交换 p和s
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
//更新子节点引用
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
r = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
//后继有 右子树 那么即是sr替换删除节点
if (sr != null)
replacement = sr;
else
//否则就是直接删除那种
replacement = p;
//----------------交换完成------------------
}
//2左子树不会空 右子树为空 则用左子树替换
else if (pl != null)
replacement = pl;
else if (pr != null)
//3.相反则用左子树替换
replacement = pr;
else
//4.没有子节点
replacement = p;
//是用子节点切换 更新 pp和替换节点引用 且删除p节点
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
r = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
//清空删除节点引用
p.left = p.right = p.parent = null;
}
//p 是黑色的 那么会影响平衡 即需要平衡
root = (p.red) ? r : balanceDeletion(r, replacement);
//替换节点是本身 则需要断开和父节点引用 其他情况是直接断开了
//
if (p == replacement) { // detach pointers
TreeNode<K,V> pp;
if ((pp = p.parent) != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
p.parent = null;
}
}
} finally {
unlockRoot();
}
assert checkInvariants(root);
return false;
}
balanceDeletion-平衡删除
直接看代码晕了,建议看图解:红黑树删除篇(一文读懂),看代码可知取决于替换节点的兄弟节点和兄弟节点子节点的情况,需要旋转平衡按左右对称去除后情况分为4种,下面以替换节点X在左子树为例:
x为处理平衡节点
xp为x的父节点
xpr为x的兄弟节点
1 兄弟节点为黑色
兄弟节点为黑色,需要根据其子节点情况进行区分
1.1 兄弟右子红
当其右孩子为红色,先将xpr变成xp的颜色(xp为null则设置为黑色),随后将xp变黑色然后对xp进行左旋。这样变成完成平衡,具体如下:
假设xp改成黑色,或者sl变成红色带进去会发现结果是一样的,取决于兄弟节点的颜色和左孩子的颜色
1.2 兄弟右子黑
这个情况将xpr变成红色,随后对xpr进行右旋变成上面1.1右孩子为红色的情况。那么这种情况就需要旋转2两次。
1.3 兄弟双子为黑
直接把xpr(兄弟节点)变成红色,以xp作为下次平衡的节点。
假设xp为红,那么下次平衡时直接设置为黑色完成平衡。
如果xp为黑色,那么又根据最终其兄弟节点的颜色和子节点情况进行区分。转移为其他的情况不需要旋转,只是变色所以不会影响旋转次数。
2 兄弟节点为红色
首先将xp变红色,xpr(兄弟节点)变黑色,xp变成红色,然后左旋xp。这样会根据之前xpr成为x的兄弟节点,而xpr子节点必定为黑色(因为xpr为红色,而红黑树不存在连续的两个红色),这样将状态转换为上面兄弟节点黑色的情况。而上面的情况是最多旋转两次,所以该情况最多旋转3次。
//x为替换节点 或者x为p
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
if (x == null || x == root)
return root;
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
else if (x.red) {
x.red = false;
return root;
}
//2 x在左子树
else if ((xpl = xp.left) == x) {
//2.1 兄弟节点存在且为红色
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
//2.2 兄弟节点为null
if (xpr == null)
//改为从父节点进行平衡
x = xp;
else {
//2.3 兄弟节点为黑色
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
//2.3.1 兄弟节点的两个字节节点为黑
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
//兄弟节点变成红色
xpr.red = true;
//以父亲节点作为替换节点继续
x = xp;
}
else {
//2.3.2 兄弟节点右孩子为黑色
if (sr == null || !sr.red) {
//设置左孩子为黑
if (sl != null)
sl.red = false;
//右孩子为红
xpr.red = true;
//以右孩子进行右旋
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
//兄弟节点不为空
if (xpr != null) {
//则设置为xp的颜色
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
//2.3.3 兄弟右孩子为红色
if (xp != null) {
//父亲节点变黑
xp.red = false;
//以父亲节点进行左旋
root = rotateLeft(root, xp);
}
//将root作为替换节点继续平衡
x = root;
}
}
}
else { // symmetric 对称
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
3 结束条件
3.1 当前平衡节点为root
设置为黑色结束,root节点一定为黑色。
3.2 当前平衡节点为红色
设置为黑色结束。删除之后黑色节点少1,替换节点是红色那么改成黑色即可保证红黑树的黑色平衡
总结
时间复杂度
无聊是查询还是删除或者是插入,都没有引入额外结构空间复杂度为O(1)
因为每个节点都记录parent节点,都是通过循环实现递归逻辑
查询复杂度
红黑树所有根节点到叶子节点的黑色节点个数相同,在查询的时间复杂度和二叉平衡树类似O(logn)。
插入复杂度
在插入时也需要通过O(logn)查询到插入位置,然后通过变色和常数级旋转操作(插入时最多两次)维持平衡。
删除复杂度
删除时也需要通过O(logn)查询到删除节点,然后通过变色和常数级旋转操作(插入时最多三次)维持平衡。
对比AVL优势
AVL的左右子树高度差不能超过1,每次进行插入/删除操作时,几乎都需要通过旋转操作保持平衡。
为了严格平衡需要进行自底向上不断旋转来保证平衡
在频繁进行插入/删除的场景中,频繁的旋转操作使得AVL的性能大打折扣
红黑树通过牺牲严格的平衡,换取插入/删除时少量的旋转操作,整体性能优于AVL
- 红黑树插入时的不平衡,不超过两次旋转就可以解决;删除时的不平衡,不超过三次旋转就能解决
- 红黑树的红黑规则,保证最坏的情况下,也能在O ( log n)时间内完成查找操作。