萌新学习数据结构挺久的了,经常使用数据结构均可以手撕,而平衡树只是了解原理,撕不出来,看各类博客文章也看得晕头转向的。
以前看《算法》红皮书学习了左偏红黑树,此次从JDK的TreeMap来分析下常规红黑树。 阅读须要有二叉查找树的知识背景java
出自《算法导论》node
由于TreeMap中有不少集合相关的操做,原代码长度上千行,看得眼花了。因此这里把其中和红黑树相关的部分提取出来分析。算法
这里为了方便,把其中的泛型部分简化成int编程
private static class Node implements Comparable<Node> {
int key;
int val;
boolean color = BLACK;
Node left, right, parent;
Node(int key, int val, Node parent) {
this.key = key;
this.val = val;
this.parent = parent;
}
@Override
public int compareTo(Node o) {
return this.key - o.key;
}
}
复制代码
//红黑树
public class RedBlackTree{
//常量定义
private static final boolean RED = false;
private static final boolean BLACK = true;
private Node root;
private int size;
// 以前的节点类
private static class Node implements Comparable<Node> {...}
}
复制代码
private static boolean colorOf(Node p) {
return (p == null ? BLACK : p.color);
}
private static void setColor(Node p, boolean c) {
if (p != null)
p.color = c;
}
复制代码
private static Node parentOf(Node p) {
return (p == null ? null : p.parent);
}
private static Node leftOf(Node p) {
return (p == null) ? null : p.left;
}
private static Node rightOf(Node p) {
return (p == null) ? null : p.right;
}
private Node successor(Node tmp) {
//后继节点的查找
if (tmp == null) {
return null;
}
if (tmp.right != null) {
Node p = tmp.right;
while (p.left != null) {
p = p.left;
}
return p;
} else {
Node p = tmp.parent;
Node ch = tmp;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
复制代码
所谓左旋,就是对原节点N,让N的右子节点R代替N的位置,
N成为R的左子节点,至于他们的其余子节点,看着办就好。
从观感上有N下位成左子节点,N的右子节点上位之感,故谓之左旋。
右旋反之。
很是简单的操做,无须多言。数据结构
private void rotateLeft(Node tmp) {
if (tmp == null) {
return;
}
Node r = tmp.right;
if (r == null) {
return;
}
tmp.right = r.left;
if (r.left != null) {
r.left.parent = tmp;
}
r.parent = tmp.parent;
if (tmp.parent == null) {
root = r;
} else if (tmp.parent.left == tmp) {
tmp.parent.left = r;
} else {
tmp.parent.right = r;
}
r.left = tmp;
tmp.parent = r;
}
private void rotateRight(Node tmp) {
if (tmp == null) {
return;
}
Node l = tmp.left;
if (l == null) {
return;
}
tmp.left = l.right;
if (l.right != null) {
l.parent = tmp;
}
l.parent = tmp.parent;
if (tmp.parent == null) {
root = l;
} else if (tmp == tmp.parent.left) {
tmp.parent.left = l;
} else {
tmp.parent.right = l;
}
l.right = tmp;
tmp.parent = l;
}
复制代码
public void put(int key, int val) {
if (this.root == null) {
//对于空树,咱们直接插入就行了,知足RBT的各类性质
this.root = new Node(key, val, null);
size = 1;
} else {
Node newNode = insert(root, key, val);
//newNode为空,就是key存在,这时候没有插入新节点
//非空的话,插入新节点,须要考虑修复被破坏的RBT性质
if (newNode != null) {
fixAfterInsertion(newNode);
}
}
}
复制代码
其实与常规二叉树的操做同样的ide
private Node insert(Node root, int key, int val) {
Node tmp = root, parent = tmp;
while (tmp != null) {
parent = tmp;
int cmp = tmp.key - key;
if (cmp < 0) {
tmp = tmp.left;
} else if (cmp > 0) {
tmp = tmp.right;
} else {
// key已经存在,直接修改val后返回就行了
tmp.val = val;
return null;
}
}
//tmp为null, parent是上一轮的tmp
//只须要把节点插入到这个parent下面
int cmp = parent.key - key;
Node newNode = new Node(key, val, parent);
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
size++;
return newNode;
}
复制代码
方法名分析:插入以后的修复。修复必然是由于某些性质被破坏,这里须要经过一些操做来还RBT的性质。工具
几个关键点学习
这个操做,能够参考算法红皮书中的flip方法this
private void fixAfterInsertion(Node insertNode) {
setColor(insertNode, RED);
Node tmp = insertNode;
while (tmp != null && tmp != root && tmp.parent.color == RED) {
//第一个if
if (parentOf(tmp) == leftOf(parentOf(parentOf(tmp)))) {
Node uncle = rightOf(parentOf(parentOf(tmp)));//tmp的叔节点
if (colorOf(uncle) == RED) {
//第二个if
//可参考红皮书的翻转操做
setColor(parentOf(tmp), BLACK);
setColor(uncle, BLACK);
setColor(parentOf(uncle), RED);
tmp = parentOf(uncle);
} else {//第二个else,叔节点为黑色或不存在
if (tmp == rightOf(parentOf(tmp))) {//第三个if
//若tmp为右节点,那么经过旋转操做,使tmp指向左子节点,方便下面的统一操做
tmp = parentOf(tmp);
rotateLeft(tmp);
}
setColor(parentOf(tmp), BLACK);
setColor(parentOf(parentOf(tmp)), RED);
rotateRight(parentOf(parentOf(tmp)));
}
}
//else中的内容为第一个if的镜像
else {
Node uncle = leftOf(parentOf(parentOf(tmp)));//tmp的左叔节点
if (colorOf(uncle) == RED) {
setColor(parentOf(tmp), BLACK);
setColor(uncle, BLACK);
setColor(parentOf(uncle), RED);
tmp = parentOf(uncle);
} else {
if (tmp == leftOf(parentOf(tmp))) {
tmp = parentOf(tmp);
rotateRight(tmp);
}
setColor(parentOf(tmp), BLACK);
setColor(parentOf(parentOf(tmp)), RED);
rotateLeft(parentOf(parentOf(tmp)));
}
}
}
setColor(root, BLACK);
}
复制代码
从编程的角度理解,这里循环的做用,是把“新加了一个红色节点”这一事件逐层向上传递。
在传递过程当中,可能某一层能够处理这一事件,那么他就处理,而后终止这一事件的传递。若是处理不了这一事件,他就经过一系列转换,把这个事件转换成上层要处理的问题。
这样递归到root就行了。spa
其实与常规二叉树查找同样
public Node get(int key) {
Node tmp = root;
while (tmp != null) {
int cmp = tmp.key - key;
if (cmp > 0) {
tmp = tmp.right;
} else if (cmp < 0) {
tmp = tmp.left;
} else {
return tmp;
}
}
return null;
}
复制代码
public void delete(int key) {
Node node = get(key);
if (node != null) {
size--;
deleteNode(node);
}
}
复制代码
关键点分析:
删除红色节点不会破坏RBT的性质,可是删除黑色节点会破坏性质5,因此删除黑色节点后须要调用fixAfterDeletion方法来修复性质5,具体后面分析
先是各类非空判断。
语句1:对于要删除有两个子节点的节点Tmp,咱们先找的其后继节点s,而后把s提到Tmp的位置,转为删除s就行了。
由于Tmp有两个子节点,可知s一定存在于Tmp的右子树中,而且s没有子节点或者只有一个子节点。
这样咱们就把全部的删除操做都概括到删除叶子节点或者只有一个子节点两种状况下了。
对于只有一个子节点的状况,咱们在语句2中处理。 主要步骤就是找到这个子节点,而后子节点登基大宝,原节点被忘却。
可是原节点若为黑色,那么这条路径下全部节点的路径都少了一个黑色节点,不符合性质5了,因此要进行修复。
对于删除叶子节点的状况,咱们在语句3中处理。 该节点为红色,咱们就直接断开各类链接就行了; 若该节点为黑色,咱们还须要先进行fixAfterDeletion。
private void deleteNode(Node node) {
if (node == null) {
return;
}
if (node.parent == null) {
root = null;
return;
}
//语句1
if (node.left != null && node.right != null) {
Node s = successor(node);
node.key = s.key;
node.val = s.val;
node = s;
}
//语句2
if (node.left != null || node.right != null) {
//找到这个惟一的子节点
Node replacement = node.left == null ? node.right : node.left;
//把这个子节点顶上去,原节点的各类链接都被这个子节点所占据
replacement.parent = node.parent;
if (node.parent == null) {
root = replacement;
} else if (node == node.parent.left) {
node.parent.left = replacement;
} else {
node.parent.right = replacement;
}
node.left = null;
node.right = null;
node.parent = null;
//老节点若为黑色,须要修复
if (node.color == BLACK) {
fixAfterDeletion(replacement);
}
} else {//语句3
if (node.parent == null) {
root = null;
} else {
if (node.color == BLACK) {
fixAfterDeletion(node);
}
//断开链接
if (node.parent != null) {
if (node == node.parent.left) {
node.parent.left = null;
} else if (node == node.parent.right) {
node.parent.right = null;
}
node.parent = null;
}
}
}
}
复制代码
若是删除体现了“新王上位老王败溃”的无情,那么修复则体现了“兄弟就是拿来坑的”的暖暖亲情。
删除以后,若不知足RBT的性质,只会是不知足性质5。即和其兄弟比起来,在路径上少了一个黑色节点。
因此这个修复的关键是想办法从兄弟那里找一个红色节点变成黑色,经过parent传递过来,从而达到平衡。实在搞不到了,那么就把兄弟也减小一个黑色节点(黑变红),而后把问题交给parent去处理,充分体现了高超的甩锅水平。
关键点分析:
private void fixAfterDeletion(Node tmp) {
while (tmp != root && colorOf(tmp) == BLACK) {
if (tmp == leftOf(parentOf(tmp))) {
//获取兄弟节点
Node bro = rightOf(parentOf(tmp));
//代码1,把兄弟变成黑色
if (colorOf(bro) == RED) {
//parent必定为黑
setColor(bro, BLACK);
setColor(parentOf(tmp), RED);
rotateLeft(parentOf(tmp));
bro = rightOf(parentOf(tmp));
}
//代码2,bro的颜色一定为黑色
if (colorOf(leftOf(bro)) == BLACK && colorOf(rightOf(bro)) == BLACK) {
setColor(bro, RED);
tmp = parentOf(tmp);//向上一层传递事件
}
else {
//代码3,bro为黑色,而且bro至少有一个红子节点
if (colorOf(rightOf(bro)) == BLACK) {
setColor(leftOf(bro), BLACK);//这种状况想bro的左子节点一定为RED
setColor(bro, RED);
rotateRight(bro);
bro = rightOf(parentOf(tmp));
//成功把bro的right转成了红色
}
//代码4这一步的做用是从bro那里借来一个黑色节点
setColor(bro, colorOf(parentOf(tmp)));
setColor(parentOf(tmp), BLACK);
setColor(rightOf(bro), BLACK);
rotateLeft(parentOf(tmp));
tmp = root;//退出
}
} else {//镜像操做,没啥好说的
Node bro = leftOf(parentOf(tmp));
if (colorOf(bro) == RED) {
setColor(bro, BLACK);
setColor(parentOf(tmp), RED);
rotateRight(parentOf(tmp));
bro = leftOf(parentOf(tmp));
}
if (colorOf(rightOf(bro)) == BLACK && colorOf(leftOf(bro)) == BLACK) {
setColor(bro, RED);
tmp = parentOf(tmp);
} else {
if (colorOf(leftOf(bro)) == BLACK) {
setColor(rightOf(bro), BLACK);
setColor(bro, RED);
rotateLeft(bro);
bro = leftOf(parentOf(tmp));
}
setColor(bro, colorOf(parentOf(tmp)));
setColor(parentOf(tmp), BLACK);
setColor(leftOf(bro), BLACK);
rotateRight(parentOf(tmp));
tmp = root;
}
}
}
setColor(tmp, BLACK);
}
复制代码
理解这一步的关键就是:经过循环,把少了一个黑色节点这一事件逐层向上传递,直到被某一层处理。具体处理方法呢?就是在bro的子节点里找到一个RED。
疑问为何不在bro为RED的时候直接把bro变成BLACK,经过parent传递过来呢?由于bro的子节点的黑色路径数目会变,以下图
问题的等效转换
初看可能会以为,各类状况极其复杂,然而他们经过等效变换(换色+旋转,保持各个节点路径的黑色节点数目不变),将各类复杂状况概括为两三种简单状况,对这两三种简单状况,又分为在本层解决,或者在本层局部解决把问题推到上一层这两种处理方式,从而保持或者修复RBT的各类性质。 插入总结:
删除总结