红黑树的介绍
红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树。
红黑树是特殊的二叉查找树,意味着它满足二叉查找树的特征:任意一个节点的值,大于等于左孩子的值,小于等于右孩子的值。
除了具备该特性之外,红黑树还包括许多额外的信息。
红黑树顾名思义就是给节点加入了颜色,每个节点上都有存储位表示节点的颜色,颜色是红(Red)或黑(Black)。
特点
1.红黑树不是一颗平衡树
2.红黑树节点左右子树高度差长的不能超过短的二倍 红黑树牺牲了一些平衡,使得插入操作更快 (主要原因是减少了旋转的次数,但有了节点的重新着色;这就使得红黑树的增删查效率 远大于 AVL树)
3.红黑树最多做两次旋转,删除最多旋转三次
红黑树的五个性质
1.每个节点都有颜色,不是黑色,就是红色
2.叶子结点(left和right)是黑色 (叶子节点,是只为空(NIL或null)的节点)
3.根节点root必须是黑色
4.如果一个节点是红色的,则它的子节点必须是黑色的,不能出现连续的红色节点
5. 从根节点root到达每一个叶子结点的路径上,黑色节点的数量都是相同的
图解
红黑树节点左右子树高度差长的不能超过短的二倍,这就意味着红黑树一颗近似平衡的二叉树,红黑树对插入时间、删除时间和查找时间
提供了最好可能的最坏情况担保,效率极大提高,因此红黑树的使用十分广泛,在计算几何中使用的很多数据结构都可以基于红黑树。
红黑树的实现(代码说明)
1.基本定义
/**
* 枚举 红黑树节点颜色定义
*/
enum Color{
BLACK, RED;
}
/**
* 红黑树节点类型定义
* @param <T>
*/
class RBNode<T extends Comparable<T>>{
private T data; //关键字(键值)
private RBNode<T> left; //左孩子
private RBNode<T> right; //右孩子
private RBNode<T> parent; //父节点
private Color color; //颜色
public RBNode(T data, RBNode<T> left, RBNode<T> right, RBNode<T> parent, Color color) {
this.data = data;
this.left = left;
this.right = right;
this.parent = parent;
this.color = color;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public RBNode<T> getLeft() {
return left;
}
public void setLeft(RBNode<T> left) {
this.left = left;
}
public RBNode<T> getRight() {
return right;
}
public void setRight(RBNode<T> right) {
this.right = right;
}
public RBNode<T> getParent() {
return parent;
}
public void setParent(RBNode<T> parent) {
this.parent = parent;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
}
/**
* 红黑树定义
* @param <T>
*/
class RBTree<T extends Comparable<T>>{
private RBNode<T> root;
private Color color(RBNode<T> node){
return node == null ? Color.BLACK : node.getColor();
}
private void setColor(RBNode<T> node, Color color){
node.setColor(color);
}
private RBNode<T> parent(RBNode<T> node){
return node.getParent();
}
private RBNode<T> left(RBNode<T> node){
return node.getLeft();
}
private RBNode<T> right(RBNode<T> node){
return node.getRight();
}
}
2.左旋转、右旋转
红黑树的旋转操作比AVL少,主要分为左旋和右旋操作,由于红黑树在调整节点颜色的时候,需要经常访问父节点,祖先节点,叔叔节点,兄弟节点,因此每一个节点都记录了它的parent,用于从当前节点向上访问节点,如下:
左旋操作:
private void leftRotate(RBNode<T> node){
RBNode<T> child = node.getRight();
child.setParent(node.getParent());
if(node.getParent() == null){
this.root = child;
} else {
if(node.getParent().getLeft() == node){
node.getParent().setLeft(child);
} else {
node.getParent().setRight(child);
}
}
node.setRight(child.getLeft());
if(child.getLeft() != null){
child.getLeft().setParent(node);
}
child.setLeft(node);
node.setParent(child);
}
右旋操作:
private void rightRotate(RBNode<T> node){
RBNode<T> child = node.getLeft ();
child.setParent (node.getParent ());
if(node.getParent () == null){
this.root= child;
} else {
if(node.getParent().getLeft() == node){
node.getParent().setLeft(child);
} else {
node.getParent().setRight(child);
}
}
node.setLeft (child.getRight ());
if(child.getRight () != null){
child.getRight ().setParent (node);
}
node.setRight (node);
node.setParent (child);
}
3.插入
红黑树的根节点是黑色,其它新插入的节点都是红色节点,因为红色节点不影响红黑树的性质,不会影响某一个路径上黑色节点的数量:
树是空的,第一个插入的根节点是黑色
树不是空的,新插入的节点颜色为红色,然后向上检查父节点,父节点为黑色,插入完成;父节点为红色(隐含祖先节点为黑色),需要做插入调整(红黑树的插入调整,需要看叔叔的颜色),以下为三种调整情况:
1.父节点为红色,叔叔节点也是红色
a.把父节点和叔叔节点都设置成黑色
b.把祖父节点设置成红色
c.指针指向祖先节点
d.然后从祖父节点开始继续向上回溯,判断当前指向节点的父节点是否为红色,如果为红色重复操作,反之插入成功,代码结束
2.父节点为红色,叔叔节点是黑色,当前节点和父节点,祖父节点在一侧(一条线上)
a.将父节点颜色置为黑色
b.将祖先节点颜色置为红色
c.进行右旋操作(这里我们都是以插入节点在左子树上为例,若是在右子树上则进行左旋操作)
3.父节点为红色,叔叔节点是黑色,当前节点和父节点,祖父节点不在同一侧
a.以父亲节点为根节点进行左旋,使当前节点和父节点,祖先节点在同一侧
b.执行情况2操作
只有将所有的情况都判断,才能完成红黑树的插入,代码如下:
public void insert(T data){
if(this.root == null){
this.root = new RBNode<> (data,null,null,null,Color.BLACK);
return;
}
RBNode<T> parent = null;
RBNode<T> cur = this.root;
while (cur != null){
parent = cur;
if(cur.getData ().compareTo (data) > 0){
cur = cur.getLeft ();
}else if(cur.getData ().compareTo (data) < 0){
cur = cur.getRight ();
}else {
return;
}
}
//判断应该插在叶子结点的哪边
RBNode<T> node = new RBNode<> (data,null,null,null,Color.RED);
if(parent.getData ().compareTo (data) > 0){
parent.setLeft (node);
}else {
parent.setRight (node);
}
//如果新插入节点的父节点是红色,需要做插入调整
if (color (parent(node)) == Color.RED){
fixAfterInsert(node);
}
}
private void fixAfterInsert(RBNode<T> node) {
while(color(parent(node)) == Color.RED){
if(left(parent(parent(node))) == parent(node)){
// 当前节点,父节点在祖先节点的左子树
RBNode<T> uncle = right(parent(parent(node)));
if(color(uncle) == Color.RED){ // 插入情况1
setColor(parent(node), Color.BLACK);
setColor(uncle, Color.BLACK);
setColor(parent(parent(node)), Color.RED);
node = parent(parent(node));
} else {
if(node == right(parent(node))){ // 插入情况3前半段
node = parent(node);
leftRotate(node);
}
setColor(parent(node), Color.BLACK); // 插入情况3后半段和情况2合并
setColor(parent(parent(node)), Color.RED);
rightRotate(parent(parent(node)));
break;
}
} else {
// 当前节点,父节点在祖先节点的右树
RBNode<T> uncle = left(parent(parent(node)));
if(color(uncle) == Color.RED){ // 插入情况1
setColor(parent(node), Color.BLACK);
setColor(uncle, Color.BLACK);
setColor(parent(parent(node)), Color.RED);
node = parent(parent(node));
} else {
if(node == left(parent(node))){ // 插入情况3前半段
node = parent(node);
rightRotate(node);
}
setColor(parent(node), Color.BLACK); // 插入情况3后半段和情况2合并
setColor(parent(parent(node)), Color.RED);
leftRotate(parent(parent(node)));
break;
}
}
}
// 有可能向上回溯到根节点跳出,把根节点置成黑色
setColor(this.root, Color.BLACK);
}
测试代码
public class RBTreeTestUnit {
public static void main(String[] args) {
RBTree<Integer> avl = new RBTree<> ();
for (int i = 1; i <= 10; i++) {
avl.insert (i);
}
System.out.println(avl);
}
}
1~10十个数字,使用红黑树插入图解:
由于夜太深,本博客就先介绍到这,我们将在下一篇博客中继续介绍红黑树的删除操作(较为复杂)