1.根节点为黑色
2.节点不是红色就是黑色
3.虚拟叶子节点为黑色
4.红色节点的子节点必须为黑色
这个可以推导出:红色节点的父只能是黑色
所以对于红色节点来说,父子都必须为黑色
5.任意一个节点到虚拟叶子节点经历的黑色节点个数相同
补充说明:
1.性质3中:虚拟叶子结点是假想出来的,实际并不存在;它的作用是给我们用作条件判断
2.这五条性质并不是保证红黑树中平衡因子像AVL树一样严格<=1,只是保证树大致平衡
3.红黑树并没有对黑色节点的子节点做要求,黑色节点的子节点可以是黑色,也可以是红色;
从5条性质分析
:
1.根节点为黑色
2.节点有黑有红
3.虚拟黑色叶子节点不用管
4.红色节点子节点为黑色
5.不满足性质5
55到38.right 和 55到25.left 经历的黑色节点个数不同
4阶B树的性质
1.节点元素个数:[1,3]
2.节点的子节点个数:节点元素个数+1
这两个性质,约束了4阶B树:
红黑树转换为4阶B树的方式
(1)每个black节点等效为4阶B树一个节点中的父元素
(2)black节点和他的所有红色子节点融合,形成1个B树节点
(1)红黑树的black节点个数和4阶B树节点总个数是一样的
(2)等效4阶B树中一个节点中的父元素永远是红黑树的black节点
这两个可以推导出红黑树转为4阶B树时,B树节点的4种情况,如上图右上角所示:
红黑树等价转换的4阶B树中节点的四种情况:
1)度为2的black节点和red子节点融合,构成3个元素的节点
2)度为1的black节点和red子节点融合(分左右子节点)
3)度为0,自己为一个节点
因此一个节点最多有3个元素,最少1个节点,因此和4阶B树对应上了。
说明
对于AVL树,是通过平衡因子来维护树的平衡。而对于红黑树来说,就是通过在二叉树的增删过程中维护节点颜色来保证满足红黑树5个性质,来维护树的平衡,因此对于红黑树的节点来说只有颜色,不讲究高度、平衡因子
红黑树与4阶B树在逻辑上是完美等价的(符合任何情况)
逻辑上等价成4阶B树的目的:在增删节点时维护红黑树节点颜色的同时,也要保证4阶B树的性质;
2-3树无法和红黑树做等价变换,因此二者比较没有意义
Uncle节点:和parent共一个
parent的兄弟节点叫uncle节点,比如图中的50的uncle节点,只有25是
(1) RBTree也是搜索树,因此和AVLTree一样,继承BinarySearchTree,由于搜索树必须提供比较,所以仍然需要带有comparator的构造器
import java.util.Comparator;
public class _04_RBTree<E> extends _02_BinarySearchTree<E>{
public _04_RBTree(){}
public _04_RBTree(Comparator comparator){
super(comparator);
}
}
RBTree的Node需要有color属性,因此继承Node,增加color属性;
color只有红色和黑色因此可以设置为布尔值;
在RBTree内部定义私有静态常量来规定RED和Black对应的布尔值:
public class _04_RBTree<E> extends _05_BinaryBalancedSearchTree<E> {
private static final boolean RED=false;
private static final boolean BLACK=true;
public _04_RBTree() {
}
public _04_RBTree(Comparator comparator) {
super(comparator);
}
private static class RBNode<E> extends Node<E> {
boolean color = RED;
public RBNode(E element, Node<E> parent) {
this.element = element;
this.parent = parent;
}
@Override
public String toString() {
String str = "";
if(color==RED){
str = "R_";
}
return str + element.toString();
}
}
(1)RBTree中添加
//1.todo 给节点染色
//返回值为node 返回染完色的node
private Node<E> color(Node<E> node,boolean color){
if(node == null) return node;
((RBNode<E>)node).color = color;
return node;
}
private Node<E> red(Node<E> node){
return color(node,RED);
}
private Node<E> black(Node<E> node){
return color(node,BLACK);
}
//todo2. 获取node的颜色
private boolean colorOf(Node<E> node){
if(node == null) return BLACK;//空节点在红黑树中往往是叶子结点,因此返回黑色
return ((RBNode<E>)node).color;
}
private boolean isBlack(Node<E> node){
return colorOf(node) == BLACK;
}
(2)在BinaryTree的Node中添加:
public Node<E> sibling() { // 红黑树中用到, 返回兄弟节点
if (isLeftChild()) {
return parent.right;
}
if (isRightChild()) {
return parent.left;
}
return null;
}
由于红黑树可以完美等价于4阶B树,因此一定要时时刻刻联想到B树(心里有B树):
(1) B树中,新添加进来的元素必然是添加到叶子结点中
(2) 4阶B树,无论是根节点还是非根节点,元素个数都满足:[1,3]
建议:
将新添加进来的节点默认设置为RED,这样能够让红黑树尽快的满足红黑树的性质。所以在上面写的构造方法中,设置color默认为RED
原因分析:
假设往一个红黑树中添加进一个红色节点,那么满足的性质有:
红黑树所有的叶子结点类型如下所示:
红黑树和AVL树不同在于,AVL是通过维护平衡因子来保证搜索树的平衡,因此每个节点都要维护高度。但是红黑树不是通过平衡因子,是通过保障红黑树的5个性质来保证树的平衡,因此对于红黑树,在增删操作中,只要保证满足5个性质,即可。
为什么5个性质能保证平衡,在学完红黑树增删节点之后就能理解了。
有8种添加会导致红黑树不满足性质4,现在我们将这8种情况更细一步拆解。
如上图所示,46-50-52 和 76-72-60两种 红色节点的子节点为红色,因此不满足红黑树性质四;我们可以将这两种情况定性为RR和LL 失衡;
定性的原则是 以B树角度来看,当前叶子节点中两个红色元素和黑色父元素的关系
从红黑树等价转换为4阶B树的四种叶子节点情况的角度来进行修复:
RR:46-50-52
(1)当前节点有三个元素,那么只能是红黑红的情况;
(2)黑色元素必须为当前节点的根元素
由这两个条件,可以推断出应该将此节点修复为:
46(RED) <- 50(BLACK) -> 50(RED)
LL:60-72-76
同理修复为:
60(RED) <- 72(BLACK) -> 76(RED)
这两种情况的判定条件
没有uncle节点(或者说uncle节点为黑色(nil))
我们先不处理25上溢的问题,先处理这个叶子节点
当插入一个新节点10:
(1)先处理构成的子树: 17子树和33子树
17和33都要变为黑色=>意味着新节点的父节点和Uncle节点要转为黑色
(2)再处理25: 25作为上溢节点
对于父节点38 和 55 来说也是插入新的元素,因此先将25节点染红,然后套用12种情况作为新元素添加
(3)连续上溢到根节点
如果持续上溢到根节点,树会长高,根节点只有一个新的元素,此时只需要将新的根节点染成黑色
添加一共有个12种情况:
(1)父节点是黑色
占了四种情况,这四种情况不需要做任何处理
(2)Uncle节点是黑色
占了四种情况,这四种情况又分为:LL LR RR RL
由于Uncle为黑色的情况下也涉及到旋转,那么和AVL树的旋转是一样的,这样有公共代码,就可以抽象到一个公共类中;原本的继承结构如下:
公共类
package _02_二叉树;
import java.util.Comparator;
public class _05_BinaryBalancedSearchTree<E> extends _02_BinarySearchTree<E>{
public _05_BinaryBalancedSearchTree() {
}
public _05_BinaryBalancedSearchTree(Comparator<E> comparator) {
super(comparator);
}
//旋转
protected void rotateLeft(Node<E> node) {
Node<E> parent = node.right;
Node<E> child = parent.left;
//1.更改左右子节点
parent.left = node;
node.right = child;
afterRotate(node,parent,child);
//更新高度 更新高度只有AVL树需要做
// updateHeight(node);
// updateHeight(p);
}
protected void rotateRight(Node<E> node){
Node<E> parent = node.left;
Node<E> child = parent.right;
parent.right = node;
node.left = child;
afterRotate(node,parent,child);
//更新高度 更新高度只有AVL树需要做
//updateHeight(node);
//updateHeight(p);
}
//重新规划parent
protected void afterRotate(Node<E> grand,Node<E> parent,Node<E> child){
parent.parent = grand.parent;
if(grand.isLeftChild()){
grand.parent.left = parent;
}else if(grand.isRightChild()){
grand.parent.right = parent;
}else{
root = parent;
}
}
//统一旋转操作
protected void rotate(
Node<E> r, // 子树的根节点
Node<E> b, Node<E> c,
Node<E> d,
Node<E> e, Node<E> f) {
// 让d成为这颗子树的根结点
d.parent = r.parent;
if (r.isLeftChild()) {
r.parent.left = d;
} else if (r.isRightChild()) {
r.parent.right = d;
} else {
root = d;
}
// b-c
b.right = c;
if (c != null) {
c.parent = b;
}
//updateHeight(b);
// e-f
f.left = e;
if (e != null) {
e.parent = f;
}
//updateHeight(f);
// b-d-f
d.left = b;
d.right = f;
b.parent = d;
f.parent = d;
//updateHeight(d);
//AVL树中需要更新节点高度
//注意:更新高度的顺序必须是先更新低的节点
}
}
AVLTree
AVLTree种重写旋转后的操作,要更新高度
@Override
protected void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) {
super.afterRotate(grand, parent, child);
updateHeight(grand);
updateHeight(parent);
}
@Override
protected void rotate(Node<E> r, Node<E> b, Node<E> c, Node<E> d, Node<E> e, Node<E> f) {
super.rotate(r, b, c, d, e, f);
updateHeight(b);
updateHeight(f);
updateHeight(d);
}
//添加后处理
@Override
protected void afterAdd(Node<E> node) {
Node<E> parent = node.parent;
//todo case1:父节点是black
//如果parent==null,意味着新添加的节点为root,此时将此节点染成黑色
//注意:这里也解决了上溢到根节点,根节点溢出新的根节点的问题
//新溢出的根节点要染成黑色
if(parent==null){
black(node);
return;
}
//如果parent是黑色节点,那么直接返回即可,不需要做多的操作
if(isBlack(parent)){
return;
}
//todo case2:parent为红色,并且Unlce节点为红色
Node<E> uncle = parent.sibling();
// 1.parent 和 Uncle染成black
// 2.grand上溢 : 将grand染成红色,然后当做新节点添加到
Node<E> grand = parent.parent;
if(isRed(uncle)){
black(parent);
black(uncle);
Node<E> newGrand = red(grand);//将grand染成红色
afterAdd(newGrand);//当做一个新的节点添加,注意染成红色后就是添加后的状态了,只需要做afterADd处理
return;
}
//todo case3: Uncle节点不是红色
// Uncle节点不是红色还有四种情况 LL LR RR RL
if(parent.isLeftChild()){//L
if(node.isLeftChild()){//LL
//parent染成黑色
//grand染成红色 并且单旋操作
black(parent);
red(grand);
rotateRight(grand);
}else{ //LR
//自己染成黑色,grand染成红色
black(node);
red(grand);
rotateLeft(parent);
rotateRight(grand);
}
}else{ //R
if(node.isLeftChild()){//RL
black(node);
red(grand);
rotateRight(parent);
rotateLeft(grand);
}else{ //RR
black(parent);
red(grand);
rotateLeft(grand);
}
}
}
补充
上面的代码中,除了父节点为黑色意外,grand都要染成红色,因此在获取grand节点的时候可以直接染红
前提
:红黑树可以等价于4阶B树来说,对B树来说,真正被删除的元素只会再叶子节点上;
从上图看,删了红色节点不影响红黑树性质;也不影响B树性质
因此可以直接删除,不需要做任何操作
判断条件
isRed(node) == true
处理方式
return;
BST中,删除度为2的节点,实际上删除的是前继节点,并值进行了替换,效果如下:
判断条件
isRed(node) node是实际被删除的节点
处理方式
return;
度为1的黑色节点被删除后:
从B树角度出发:仍然满足B树性质,因此不需要做下溢操作
从红黑树出发:将子元素染成黑色即可
度为0的黑色节点被删除后,那么B树就缺了一根胳膊,B树结构肯定要调整;
所以第一时间是看兄弟节点能否借元素。
兄弟节点能借,前提是兄弟节点是黑色,如果是红色,那兄弟节点是B树中父节点的元素
88节点被删除后,父节点有两个元素,因此对于B树来说必须要有三个指针,因此造成下溢;
B树节点下溢的处理方式:看兄弟能否借元素;当兄弟节点减少一个元素,仍然能满足节点中元素个数为[1,3]那么就可以借
从4阶B树来看,兄弟节点元素个数>=2才可以借
下图三种情况,兄弟节点可以借元素
1.条件
(1) 兄弟节点(兄弟元素)必须是黑色
如果兄弟节点是红色,那么这个红色节点在4阶B树的视角来看,
是父节点的元素,无法借元素过来。
(2) 兄弟节点有1或者2个红色子节点
2.处理方式
LR 先对兄弟节点左旋转,再对父节点进行右旋转;
旋转后染色:
1.新的父节点继承 parent的颜色
2.旋转后,左右子节点都为Black
类比LL的处理方式,兄弟有元素的三种情况如下图总结:
上图第三种情况,LL和LR处理都可以。
如果是第一种情况,对brother进行左旋转之后,也变成了LL型,因此三种情况都需要对parent做右旋转
第一种情况的判决条件是 brother.left 是黑色的
if(isBlack(brother.left)){
rotateLeft(brother);
brother = brother.parent;
}
//旋转brother之后,统一变为LL的情况 先染色 再旋转
//(这里brother旋转之后是没有变的,因此要在上面LR的情况修改一下brother)
color(brother,colorOf(parent));
black(brother.left);
black(parent);
rotateRight(parent);
从4阶B树来看兄弟节点不能借,兄弟节点元素<=1,那么兄弟节点要么为黑色节点要么没有兄弟节点。
兄弟节点不能借,在4阶B树中,是父节点下来和左右子树合并。
1.条件
兄弟节点没有红色子元素
也有两种情况:
(1)有黑色兄弟节点,父节点为红色
那么80下来和76合并。需要更换颜色,因为当前节点的父元素肯定为黑色。
处理如下:
(2)有黑色兄弟节点,父节点为黑色
88删除后,调用了afterRemove(88),父节点下溢会导致父节点不满足条件,导致祖父节点需要下溢,因此需要再次对parent调用一次afterremove()即可。
处理方式
删除black叶子结点-兄弟节点为红色
处理方式
兄弟节点染成黑色
parent染成红色
对parent进行旋转
if(isRed(brother)){
black(brother);
red(parent);
rotateRight(parent);
brother = parent.left;
}
注意:
兄弟节点55的下面绝对是两个节点,否则不满足B树性质
所以旋转后的兄弟节点76是必定存在的
在判断被删除的黑色节点是否有红色子节点的时候,我们需要获取子节点的信息,子节点就是被删除节点;因此我们在BST的搜索操作上添加如下删除后修补的方法:
protected void afterRemove(Node<E> node,Node<E> replacement){}
replacement是被删除元素的子元素;
private void remove(Node<E> node){
if(node == null) return;
//先处理度为2的节点,因为也是删除度为1的节点
Node<E> del = null;
if(node.left != null && node.right != null){
del = predesessor(node);//前继节点
node.element = del.element;
}else{
del = node;
}
//del要么度为1,要那么度为0
//child为del不为空的子节点。如果child为null,则del为叶子结点。
Node<E> child = del.left == null ? del.right:del.left;
if(child != null){ //del是度为1 的节点 需要做这一步
child.parent = del.parent;
}
if(del.parent == null){
root = child;
}else{
if(del == del.parent.left){
del.parent.left = child;
}else{
del.parent.right = child;
}
}
//节点真正被删除的时候删除
afterRemove(del,child);
size--;
}
1.明确被删除的元素只能再B树的叶子节点
叶子结点4种情况: 红黑红 黑红 红黑 黑
2.删除红色元素:直接删除,不做任何修复操作
3.删除黑色元素
(1)红黑红:BST中删除的是前继或者后继,删除的还是红色节点;不做任何修复操作
(2)黑红和红黑:BST中的做法是将parent的child指向为红色子元素,因此需要对红色子元素进行染黑处理
(3)黑色光棍:BST的做法是直接删除,因此会产生下溢
3.1.兄弟能借
条件:兄弟节点是黑色,而且还有红色子节点
3.2兄弟不能借
兄弟节点<=1,兄弟要么是黑色光棍,要么是红色节点
3.2.1兄弟也是黑色光棍
3.2.1.1 父节点是红色
直接下来,合并在一起;
3.2.1.2 父节点是黑色
父节点和子节点合并;
兄弟节点染红;
父节点也造成下溢,进行afterRemove()处理
3.2.2 兄弟是红色节点
旋转,让兄弟节点变成黑色
到这里,删除分析结束;
@Override
protected void afterRemove(Node<E> node, Node<E> replacement) {
//todo case1.被删除的节点为红色
//包含两种情况:1.直接删除的红色节点 2.删除的是度为2的黑色节点 红色是替死鬼
if (isRed(node)) {
return;
}
//todo 到这里,剩下的被删除的节点都是黑色 要么度为1 要么度为0
//todo case2.处理度为1的黑色节点 (条件:被删除的节点的替代节点为红色 )
if (isRed(replacement)) {
black(replacement); //将染成黑色即可
return;
}
//todo case3.处理度为0的black节点
// todo 3.1 只有1个节点的红黑树
Node<E> parent = node.parent;
if (parent == null) return;
// todo 3.2 找到兄弟节点 判断兄弟节点能不能借元素
//Node brother = node.sibling();
// 注意:这里获取brother节点不能这么获取,因为在afterRemove()方法中,node是已经被删除的节点
// 这意味着:如果node是左子节点,那么parent.left==null
// public Node sibling() { // 红黑树中用到, 返回兄弟节点
// if (isLeftChild()) {
// return parent.right;
// }
//
// if (isRightChild()) {
// return parent.left;
// }
// return null;
// }
// public boolean isLeftChild(){ // 判断自己是不是左子树
// return parent!=null && this==parent.left;
// }
// 由于aftrerRemove()方法是在remove()方法之后调用的,
// 那么remove()方法中,node节点被删除后:
// if(del == del.parent.left){
// del.parent.left = child;
// }else{
// del.parent.right = child;
// }
// del就是被删除节点node,这里 node就是度为0的black节点,child=null
// 因此调用remove()后 parent.left == null
// 那么在这里调用node.sibling() 会调用 node.isLeftChild()
// 如果ndoe是左子节点,那么node.parent.left 已经为null了,右子节点同理
// 因此node调用isLeftChild的时候,praent.left==null了,this不为null,所以判断会有问题。
// 无法获取isLeftChild(),也就无法获取sibling()了
//这里处理的方法是:如果node.parent.left == null 那么node就是左子节点
//为什么能这么判断?
//因为红黑树等效为4阶B树
//4阶B树不可能只有一个子节点
//还用node.isLeftChild()判断是当父节点只有一个黑色元素的时候,父节点下溢,黑色元素下溢和子节点合并,但是并不是真正
//的删除这个黑色元素,也就是说这个黑色元素的parent.left或者parent.right 仍然等于这个黑色元素
boolean left = parent.left == null || node.isLeftChild();
Node<E> brother = left ? parent.right : parent.left;
//todo 3.3 判断父节点的颜色
boolean parentColor = colorOf(parent);
//3.2 兄弟节点可以借 兄弟节点为黑色,且有子节点。
if (left) { //被删除节点在左边
if (isRed(brother)) {
black(brother);
red(parent);
rotateLeft(parent);
//更换兄弟 兄弟是黑色节点
brother = parent.right;
}
//到这里了,brother肯定为黑色
if (isBlack(brother.left) && isBlack(brother.right)) {
//brother是光杆司令,父节点要和子节点合并=> 父节点染黑,兄弟节点染红 这么处理完后相当于删除了父节点
boolean parentBlack = isBlack(parent);
black(parent);
red(brother);
if (parentBlack) {
//父节点为黑色,父节点向下融合后就为空了,因此相当于删除掉一个叶子结点,左删除后处理。
afterRemove(parent, null);
}
} else {
//兄弟节点至少有一个红色
if (isBlack(brother.right)) {
rotateRight(brother);
brother = brother.parent;
}
//旋转brother之后,统一变为LL的情况 先染色 再旋转
//(这里brother旋转之后是没有变的,因此要在上面LR的情况修改一下brother)
color(brother, colorOf(parent));
black(brother.right);
black(parent);
rotateLeft(parent);
}
} else {
//1.step1 如果被删除节点是右子节点
//2. step2.兄弟节点是红色,先将兄弟节点变成黑色,再接下来统一处理兄弟为黑色的情况
if (isRed(brother)) {
black(brother);
red(parent);
rotateRight(parent);
//更换兄弟 兄弟是黑色节点
brother = parent.left;
}
//3. step3.到这里了,brother肯定为黑色
//4. step4.处理黑兄弟没有红色子节点的情况 (不能借元素)
// 这里brother不会为空,
if (isBlack(brother.left) && isBlack(brother.right)) {
//step 4.1 brother是光杆司令,父节点要和子节点合并 父节点染黑,兄弟节点染红 这么处理完后相当于删除了父节点
boolean parentBlack = isBlack(parent);
black(parent);
red(brother);
// step 4.2 如果父节点是黑色,那么B树角度看,父节点空了,造成连锁下溢
if (parentBlack) {
afterRemove(parent, null);
}
} else {
//5. step5.兄弟节点至少有一个红色 找兄弟节点借元素
//6. step6.先处理LR的情况,转变为LL,在后面做统一处理
if (isBlack(brother.left)) {
rotateLeft(brother);
brother = brother.parent;
}
//7. step7. 旋转brother之后,统一变为LL的情况 先染色 再旋转
//(这里brother旋转之后是没有变的,因此要在上面LR的情况修改一下brother)
color(brother, colorOf(parent));
black(brother.left);
black(parent);
rotateRight(parent);
}
}
}
一点都不复杂,真的。
1.红黑树的平衡标准
:没有一条路径会使其他路径的2倍,是一种弱平衡策略
AVL树靠的是平衡因子来保证平衡的。红黑树的平衡是靠保证5条性质从而保证红黑树等价于4阶B树;那么四阶B树的平衡,是因为一个节点能够存储多个元素,因此树矮,索引次数少,但是按照红黑树进行展开会发现,树并不矮,并且平衡因子会>1,为什么用红黑树呢、。?
因为红黑树保证的是平衡因子不会过高,不会退化为链表。
最短路径为全是黑色节点。最长路径为黑节点*2; 最长路径就是每个黑色后面跟一个红色
在只看黑色节点的情况下,树的高度是满足AVL的平衡标准的;
1.二叉搜索树
添加、删除、搜索都和树的高度有关 O(logN)
2.红黑树
树高级别也是O(logN)
添加、删除、搜索都和树的高度有关 O(logN)
AVL树和红黑树对比:
红黑树和AVL树都是在二叉搜索树添加、删除节点之后的基础上再做的额外操作;
对于红黑树来说,添加和删除的额外修复操作都是O(1)级别的旋转操作
对于AVL树来说,删除的时候可能会造成链式反应,造成O(logN)级别的旋转操作
因此从这方面红黑树的性能优于AVL树
红黑树旋转操作分析:
只有当uncle节点不是红色的情况下,也就是没有uncle节点,这种情况下需要进行旋转操作。会发现最多旋转两次,因此旋转次数为O(1)
而链式反应种,不需要做旋转操作;最多做一次,就是遇到了uncle不是红色的时候