红黑树的例子
在线例子网址
https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
内部节点及类定义
Get/Set方法省略
static class RBNode<K extends Comparable<K>, V> {
private RBNode parent;
private RBNode left;
private RBNode right;
private boolean color;
private K key;
private V value;
}
记录根节点的信息以及定义红黑
public class RBTree<K extends Comparable<K>, V> {
private static final boolean RED = false;
private static final boolean BLACK = true;
private RBNode root;
}
左旋右旋
和avl树没有区别
所谓的左旋,指的是向左方向旋,那么应该是右边比较多的情况,也就是RightRight的情况
private void leftRotate(RBNode p) {
if (p != null) {
RBNode r = p.right;
p.right = r.left;
if (r.left != null) {
r.left.parent = p;
}
r.parent = p.parent;
if (p.parent == null) {
root = r;
} else if (p.parent.left == p) {
p.parent.left = r;
} else {
p.parent.right = r;
}
r.left = p;
p.parent = r;
}
}
相应的右旋
private void rightRotate(RBNode p) {
if (p != null) {
RBNode l = p.left;
p.left = l.right;
if (l.right != null) {
l.right.parent = p;
}
l.parent = p.parent;
if (p.parent == null) {
root = l;
} else if (p.parent.right == p) {
p.parent.right = l;
} else {
p.parent.left = l;
}
l.right = p;
p.parent = l;
}
}
其他操作
返回节点的颜色
注意,当节点为null时返回的是黑色
private boolean colorOf(RBNode node) {
return node == null ? BLACK : node.color;
}
返回父节点
private RBNode parentOf(RBNode node) {
return node != null ? node.parent : null;
}
返回左右节点
private RBNode leftOf(RBNode node) {
return node != null ? node.left : null;
}
private RBNode rightOf(RBNode node) {
return node != null ? node.right : null;
}
给节点设置颜色
private void setColor(RBNode node, boolean color) {
if (node != null) {
node.color = color;
}
}
查找前驱以及后继
前驱
如果有左孩子,那么前驱一定在左孩子的最右边。
如果没有左孩子,在红黑树对应的234树中一定处于叶子节点,所对应的红黑树中一定位于底节点或者倒数第二层,直接看代码:
private RBNode predecessor(RBNode node) {
if (node == null) {
return null;
} else if (node.left != null) {//如果有左儿子
RBNode p = node.left;
while (p.right != null) {
p = p.right;
}
return p;
} else if (node.parent != null) { //如果没有左儿子,但是有祖先
RBNode p = node.parent;
RBNode c = node;
while (p != null && p.left == c) {
c = p;
p = p.parent;
}
return p;
}
//如果没有左儿子,没有祖先,那么相当于没有前驱
return null;
}
后继
TreeMap里删除使用的是后继。
private RBNode successor(RBNode node) {
if (node == null) {
return null;
} else if (node.right != null) {
RBNode p = node.right;
while (p.left != null) {
p = p.left;
}
return p;
} else if (node.parent != null) {
RBNode p = node.parent;
RBNode c = node;
while (p != null && p.right == c) {
c = p;
p = p.parent;
}
return p;
}
return null;
}
插入操作
先插入再调整,插入的节点统一为红色,之后再进行调整,根节点必黑。
插入根节点必黑,允许黑连黑,不允许红连红,新增红色,爸叔通红(爸叔)就变黑,爷爷节点再变红之后递归。爸红叔黑就旋转,哪黑往哪旋。如果插入父节点为黑,那么就不需要调整。
注意,如果插入的是root节点,也就是说一开始没有节点,那么直接变黑就行。
因为规定不能红色连红色,而调整本质上是为了让树的黑色节点平衡,也就是说如果没有插入节点的时候,爸和爷本质上是平衡的,现在尽量是在爸叔爷和已经新插入的节点的层面上完成平衡,所以需要进行旋转。
注意,不管是左旋还是右旋,统一旋转节点中辈分最大的节点。
public void put(K key, V value) {
RBNode t = this.root;
//如果是根节点
if (t == null) {
root = new RBNode<>(key, value == null ? key : value, null);
return;
}
int cmp;
//寻找插入位置
//定义一个双亲指针
RBNode parent;
if (key == null) {
throw new NullPointerException();
}
//沿着跟节点寻找插入位置
do {
parent = t;
cmp = key.compareTo((K) t.key);
if (cmp < 0) {
t = t.left;
} else if (cmp > 0) {
t = t.right;
} else {
t.setValue(value == null ? key : value);
return;
}
} while (t != null);
RBNode<K, Object> e = new RBNode<>(key, value == null ? key : value, parent);
//如果比较最终落在左子树,则直接将父节点左指针指向e
if (cmp < 0) {
parent.left = e;
}
//如果比较最终落在右子树,则直接将父节点右指针指向e
else {
parent.right = e;
}
//调整
fixAfterPut(e);
}
调整操作
如果插入节点的父节点是黑色,那么不用调整。
如果插入节点的父节点是红色,且父节点的兄弟(叔)也为红色,为了平衡黑色,那么在爷爸和新插入节点这个层面下进行调整:将爸叔变为黑色,同时爷变成红色,因为爷变成了红色,对于上层的节点有影响,所以把x赋值成爷循环调整。
如果插入节点的父节点是红色,同时父节点的兄弟为黑色(不存在也是黑色),那么为了保持平衡,需要进行旋转,此时分成两种情况:
1)如果父节点是爷节点的右,插入节点是父节点的左,那么先要把父节点右旋,也就是把父节点和插入节点位置换一下,此时爷,父,子都在一条靠右的线上(如果插入节点是父节点的右则不用操作)。将爷染成红,父染成黑之后,将爷左旋,ok。
2)如果父节点是爷节点的左,插入节点是父节点的右,那么先要把父节点左旋,也就是把父节点和插入节点位置换一下,此时爷,父,子都在一条靠右的线上(如果插入节点是父节点的左则不用操作)。将爷染成红,父染成黑之后,将爷右旋,ok。
private void fixAfterPut(RBNode x) {
x.color = RED;
//本质上就是父节点是黑色就不需要调整
while (x != null && x != root && x.parent.color == RED) {
//1、x的父节点是爷爷的左孩子(左3)
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//叔叔节点
RBNode y = rightOf(parentOf(parentOf(x)));
//第3种情况
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
//爷爷节点递归
x = parentOf(parentOf(x));
}
//第2种情况
else {
//如果插入节点是父节点的右,统一弄到左边
//旋转过后父亲节点下去了成了子节点
//如果不旋的话,那么子节点调整过后会连接在现在爷节点的左边,那么还是不平衡
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
leftRotate(x);
}
//父亲变黑
setColor(parentOf(x), BLACK);
//爷爷变红
setColor(parentOf(parentOf(x)), RED);
//根据爷爷节点右旋转
rightRotate(parentOf(parentOf(x)));
}
}
//2、跟第一种情况相反操作
else {
//右3
//叔叔节点
RBNode y = leftOf(parentOf(parentOf(x)));
//第3种情况
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
//爷爷节点递归
x = parentOf(parentOf(x));
}
//第2种情况
else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rightRotate(x);
}
//父亲变黑
setColor(parentOf(x), BLACK);
//爷爷变红
setColor(parentOf(parentOf(x)), RED);
//根据爷爷节点右旋转
leftRotate(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
删除操作
先要通过key指找到节点,这一部分简单
public V remove(K key) {
RBNode node = getNode(key);
if (node == null) {
return null;
}
V oldValue = (V) node.value;
deleteNode(node);
return oldValue;
}
private RBNode getNode(K key) {
RBNode node = this.root;
while (node != null) {
int cmp = key.compareTo((K) node.key);
if (cmp < 0) {
node = node.left;
} else if (cmp > 0) {
node = node.right;
} else
return node;
}
return null;
}
找到节点后执行删除,还是先删除再调整
如果要删除的是红色节点,直接删除即可
删除的时候要明确一点就是:真正删除的节点永远对应234树的叶子节点。
TreeMap里删除是找后继节点,如果没有后继节点,那么这个节点一定在234树的叶子节点上。(因为234树的特性,转换成红黑树后,如果找不到后继节点,那么必是叶子节点或者是三节点裂变的上黑下红,而234树非叶子节点的上黑下红裂变必然左右两边都挂着子节点,所以只有可能在叶子节点),如果有后继节点,那么这个后继节点必然在叶子节点上,所以234树永远删除的是叶子节点。
所以,步骤是:
1.先判断是不是同时有左右孩子(判断是不是同时都有,因为先判断这个可以将其转换成下面的情况),如果满足就找后继(后继有可能是红黑树的叶子或者有一个孩子),找到后继之后,将后继的值赋给待删除节点,现在的目标就转换成了删除这个后继节点了。
2.判断待删除节点是不是只有一个孩子(和上面一脉相承),如果是,直接拿孩子替换,此时必然是待删除节点是黑,孩子是红(因为只有一个孩子,必然是3节点分裂的),所以将孩子染黑代替父节点就完了。如果不是,如果待删除节点是红色的,替换的孩子就不用变色(因为不破坏黑色节点相等的条件),如果待删除节点是黑色的,替换的孩子节点必须染成黑色。
3.现在只剩下是待删除节点是叶子节点的情况了,如果叶子节点是红色的,直接删。如果是黑色的,那么有点复杂(注意这里是先调整再删除):
因为要尽量保证删除节点之后树的平衡,所以尽量在待删除节点以他的父节点为根的那一颗子树上保持平衡,所以优先找兄弟节点借。
如果待删除叶子黑节点是父节点的左:
此时找父节点的右,也就是兄弟节点,如果兄弟节点为红的话,那么可以肯定,兄弟节点是由234树的父节点裂变的,且兄弟节点下面必有其他节点(因为如果是3节点的话,肯定是由底下的节点因为满了而往上跑的),因为这个兄弟节点是红色的,而平衡的节点必须要是黑色,兄弟节点必须是黑色才能接下来的操作(产生兄弟节点是红色的原因是因为3节点裂变的的时候有两个方向都可以,导致的不一致),所以只需要旋转回去就行了(这种情况可以百分之一百肯定父节点是黑色),将父节点左旋就行,此时兄弟节点必黑。再判断兄弟节点的右是不是存在也就是是否为黑~~(为什么不判断左是不是有节点,因为要左旋需要保证兄弟节点的右节点为黑,这样旋转之后才能平衡)~~(好吧我也不清楚为什么不判断左节点是否存在,可能的原因是左右节点有可能都存在,记住一个结论:在这种情况下优先判断是否为空),如果为黑色,那么先需要右旋,保证右节点上必须有值(那么问题来了,为什么兄弟节点必有孩子呢?因为前面讲兄弟节点没有孩子情况排除了,这一点稍后再写),最后左旋。
在上面操作还要插入一行,要判断兄弟节点是否有孩子,这样才能借(因为兄弟节点这个时候必然是黑色,如果是红色234树会不成立),如果兄弟节点也是叶子节点的话,那么就将兄弟节点染成红色(不能借),同时将待调整的节点赋给父节点,因为以父节点为根的这颗子树来说,无论怎么调整,已经不能保证平衡了,所以相当于直接减少了支路的黑色节点,由父节点再往上进行调整看看能不能补回。
调整完之后再删除
/**
* 删除操作:
* 1、删除叶子节点,直接删除
* 2、删除的节点有一个子节点,那么用子节点来替代
* 3、如果删除的节点有2个子节点,此时需要找到前驱节点或者后继节点来替代
*
* @param node
*/
private void deleteNode(RBNode node) {
//3、node节点有2个孩子
if (node.left != null && node.right != null) {
/**
* 这里要注意,如果使用下面这个网站演示的话,此网站用的是前驱节点替代
* 下面代码里我使用的是后继节点替代,删除节点后显示可能会和该网站不一致,
* 但是这两种方法红黑树删除都是合法的
* (可以自行把前驱节点替代方案屏蔽放开,后继节点替代方案注释掉测试下)
*
* https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
*/
//后继节点替代
RBNode rep = successor(node);
//前驱节点替代
// RBNode rep= predecessor(node);
node.key = rep.key;
node.value = rep.value;
node = rep;
}
//到这里时,node也就是要删除的节点只可能是只有一个孩子或者没有孩子的了。
//也就是说,只有可能是对应234树的非叶子节点。
RBNode replacement = node.left != null ? node.left : node.right;
//2、替代节点不为空
if (replacement != null) {
//替代者的父指针指向的原来node的父亲
replacement.parent = node.parent;
//node是根节点
if (node.parent == null) {
root = replacement;
}
//node是左孩子,所以替代者依然是左孩子
else if (node == node.parent.left) {
node.parent.left = replacement;
}
//node是右孩子,所以替代者依然是右孩子
else {
node.parent.right = replacement;
}
//将node的左右孩子指针和父指针都指向null(此时node处于游离状态,等待垃圾回收)
node.left = node.right = node.parent = null;
//替换完之后需要调整平衡
if (node.color == BLACK) {
//需要调整,这种情况一定是红色(替代节点一定是红色,此时只要变色)
fixAfterRemove(replacement);
}
}
//删除节点就是根节点,且没有替代节点
else if (node.parent == null) {
root = null;
}
//1、node节点是叶子节点,replacement为null
else {
//先调整
if (node.color == BLACK) {
fixAfterRemove(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;
}
}
}
/**
* 删除后调整
*
* @param x
*/
private void fixAfterRemove(RBNode x) {
while (x != root && colorOf(x) == BLACK) {
//x是左孩子的情况
if (x == leftOf(parentOf(x))) {
//兄弟节点
RBNode rnode = rightOf(parentOf(x));
//判断此时兄弟节点是否是真正的兄弟节点
if (colorOf(rnode) == RED) {
setColor(rnode, BLACK);
setColor(parentOf(x), RED);
leftRotate(parentOf(x));
//找到真正的兄弟节点
rnode = rightOf(parentOf(x));
}
//情况三,找兄弟借,兄弟没得借,这里不是指的为黑色,而是指的空,因为colorOf在为空的时候返回的是黑
if (colorOf(leftOf(rnode)) == BLACK && colorOf(rightOf(rnode)) == BLACK) {
//这种情况的时候,在父节点以下已经无法调整,所以把其兄弟变为红色。
//此时将矛盾转到父节点,也就是看看从父节点开始还能不能调整。
setColor(rnode, RED);
x = parentOf(x);
}
//情况二,找兄弟借,兄弟有的借
else {
//分2种小情况:兄弟节点本来是3节点或者是4节点的情况
if (colorOf(rightOf(rnode)) == BLACK) {
setColor(leftOf(rnode), BLACK);
setColor(rnode, RED);
rightRotate(rnode);
rnode = rightOf(parentOf(x));
}
setColor(rnode, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(rnode), BLACK);
leftRotate(parentOf(x));
//表示直接跳出循环,把x赋给root,顺便在最后一步的时候把root变黑
x = root;
}
}
//x是右孩子的情况
else {
//兄弟节点
RBNode rnode = leftOf(parentOf(x));
//判断此时兄弟节点是否是真正的兄弟节点
if (colorOf(rnode) == RED) {
setColor(rnode, BLACK);
setColor(parentOf(x), RED);
rightRotate(parentOf(x));
//找到真正的兄弟节点
rnode = leftOf(parentOf(x));
}
//情况三,找兄弟借,兄弟没得借
if (colorOf(rightOf(rnode)) == BLACK && colorOf(leftOf(rnode)) == BLACK) {
//情况复杂,暂时不写
setColor(rnode, RED);
x = parentOf(x);
}
//情况二,找兄弟借,兄弟有的借
else {
//分2种小情况:兄弟节点本来是3节点或者是4节点的情况
if (colorOf(leftOf(rnode)) == BLACK) {
setColor(rightOf(rnode), BLACK);
setColor(rnode, RED);
leftRotate(rnode);
rnode = leftOf(parentOf(x));
}
setColor(rnode, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(rnode), BLACK);
rightRotate(parentOf(x));
x = root;
}
}
}
//情况一、替代节点是红色,则直接染黑,补偿删除的黑色节点,这样红黑树依然保持平衡
setColor(x, BLACK);
}