红黑树的性质
红黑树和平衡二叉树类似,最大的区别是红黑树为节点赋予了黑色和红色两种属性。
- 红黑树的节点只有两种颜色,红色和黑色。
- 根节点为黑色。
- 叶子节点为黑色,值为NULL。
- 没有两个连续的红色节点。
- 从一个节点到该节点的子孙节点的所有路径包含相同数目的黑色节点。
平衡树的旋转
红黑树和AVL树的平衡条件类似,当插入节点之后,可能会导致树的某节点左右子树高度差大于1。这时候我们需要左旋转或者右旋转来实现平衡的维持。
1、左旋:
2、右旋:
旋转后我们可以发现有节点交换了连接位置,在左旋中,右侧最小的节点转换到了左侧的最大处;在右旋中,左侧的最大节点转换到了右侧的最小处。在旋转前后依然满足平衡树的条件。
红黑树的基本操作
在对红黑树做插入、删除,或是旋转等操作时,红黑树的五条性质都需要满足,我们不仅需要在适当的时候做一些旋转,还需要对节点的颜色做一些适当的修改和调整。下面我们先来看看插入操作,假设插入的节点为红色,插入分为几种情况:
- 如果当前没有节点,那么插入的节点是根节点,直接涂上黑色。
- 如果插入的节点是其双亲节点的左子树,若双亲节点和叔叔节点都是黑色,那么可以直接插入,不用调整。
- 如果插入的节点是其双亲节点的左子树,但双亲节点和叔叔节点都是红色,那么要把双亲节点和叔叔节点都涂黑,然后继续向上调整颜色,直到满足性质四。
- 如果插入的节点是其双亲节点的左子树,双亲节点为红,叔叔节点为黑色,那么我们把双亲节点右旋,再调整颜色,即可得到一颗平衡的红黑树。
- 如果插入的节点是其双亲节点的右子树,双亲节点为红,叔叔节点为黑色,我们先把双亲节点左旋,再把新的“双亲节点”右旋,调整颜色,即可得到平衡的红黑树。
- 若如4和5中,把双亲节点和叔叔节点红黑调换,类似操作也可达到目的。
部分示意图:
配图展示了变色,左旋,右旋变色操作,大家可以在大脑里模拟一下旋转变色的动态过程,也建议动手画画,以促进理解。
红黑树的删除我们这里不细说,原理和插入类似,最重要的是我们要灵活运用旋转和变色的方法,时刻保证红黑树满足上述五条性质。
红黑树的定义(java实现)
这段代码展现了红黑树节点,颜色,指针等参数的定义。其中值得一提的是T参数的运用。我们知道,红黑树实现的是基本的二叉查找树,不论插入还是删除,我们都需要寻找到相应位置,在这过程中我们需要不断和节点值比较,确定索引的方向。程序中我用参数T继承Comparable接口,把T作为键值,我们便可以作比较了。其中<? super T>用什么作用呢?T extends Comparable表示T类型继承此接口(但此接口只能被继承一次),可用来比较,也许我们在这个程序中把这部分换为T也没关系,但如果需要比较的类型复杂起来,有了继承关系,那么可能会导致程序报错,如果换为<? super T>便允许有继承关系,比如Animal extends Comparable成立的情况下我们还可以让Dog继承Animal并实现Dog extends Comparable,具体实现大家可以动手敲一下,便能看出区别了。
public class RBTree<T extends Comparable<? super T>>{
private RBTree<T> mroot; //根节点
private static final boolean RED = false; //红色
private static final boolean BLACK= true; //黑色
public class RBTNode<T extends Comparable<? super T>>{ //T类型实现比较接口
boolean color; //颜色属性
T key; //键值,可比较
RBTNode<T> left; //左子节点
RBTNode<T> right; //右子节点
RBTNode<T> parent; //双亲节点
public RBTNode(boolean color, T key, RBTNode<T> left, RBTNode<T> right,RBTNode<T> parent) {
this.color = color;
this.key = key;
this.left = left;
this.right = right;
this.parent = parent;
}
}
}
红黑树的操作(java实现)
1、左旋:
private void leftRotate(RBTNode<T> x){ //左旋的方法
RBTNode<T> y = x.right; //把x的右子节点设为y节点
x.right=y.left; //x指向ly
if(y.left!=null)
y.left.parent=x; //ly指向lx节点
y.parent=x.parent; //y指向px
if(x.parent==null){
this.mRoot = y; //把y设为根节点
}else{
x.parent.left=y; //px指向y
}
x.parent=y;
y.left=x;
}
2、右旋:
private void rightRotate(RBTNode<T> y){ //右旋的方法
RBTNode<T> x= y.left;
x.parent=y.parent;
if(y.parent==null){
this.mRoot=x;
}else{
y.parent.left=x;
}
y.left=x.right;
if(x.right!=null)
x.right.parent=y;
x.right = y;
y.parent= x;
}
关于旋转操作的代码实现,我们简单介绍一下思路和写代码的过程中可能需要注意的问题:我们首先需要画出旋转前后的示意图,这有助于我们观察判断指针的变化。连接两个节点一般需要两步,既要把前一节点指向后一节点,也要把后一节点的双亲节点指针指向前一节点,主要有两种操作:1、类似这种指针的赋值:y.left=x.right;2、指针指向节点:y.parent.left=x;我们只要在需要连接的节点之间重新建立连接即可完成节点的调整,这便是旋转。我们需要注意到,如果涉及到类似这种操作:if(x.right!=null) x.right.parent=y;我们不要忘记加上非空判断,防止出现对象空指针异常。
3、查找:
通过结点的key值比较来判断是接下来查找左子树还是右子树。直到找到null位置,才停止,等待插入:
while (x != null) {
y=x;
cmp = node.key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else
x = x.right;
}
4、插入:
获取之前查找的null位置的父节点的位置,和其键值比较,判断插入左侧还是右侧,如果y为null,则插入为根节点。
node.parent = y;
if (y!=null) {
cmp = node.key.compareTo(y.key);
if (cmp < 0)
y.left = node;
else
y.right = node;
} else {
this.mRoot = node;
}
5、设置颜色,树平衡修正(平衡修正的方法即根据上方介绍的几种插入情况做分类讨论):
// 设置节点的颜色为红色
node.color = RED;
// 将它重新修正为一颗二叉查找树
insertFixUp(node);
6、输入需要插入的键值,创建节点:
public void insert(T key) {
RBTNode<T> node=new RBTNode<T>(key,BLACK,null,null,null);
// 如果新建结点失败,则返回。
if (node != null)
insert(node); //调用方法,找到位置,插入节点
}
我们在实现红黑树的旋转,插入,删除等方法后,便可以通过调用这些方法,来实现综合性的操作。所有红黑树的操作,都要围绕红黑树的五条基本性质来实现。