红黑树详解

我们知道,二叉搜索树是个很好的数据结构,可以快速地找到一个给定关键字的数据项,并且可以快速地插入和删除数据项。

但是二叉搜索树有个很麻烦的问题,如果树中插入的是随机数据,则执行效果很好,但如果插入的是有序或者逆序的数据,那么二叉搜索树的执行速度就变得很慢。

因为当插入数值有序时,二叉树就是非平衡的了,排在一条线上,其实就变成了一个链表……它的快速查找、插入和删除指定数据项的能力就丧失了。

为了能以较快的时间O(logN)来搜索一棵树,需要保证树总是平衡的(或者至少大部分是平衡的),这就是说对树中的每个节点在它左边的后代数目和在它右边的后代数目应该大致相等。

红黑树的就是这样的一棵平衡树,对一个要插入的数据项,插入例程要检查会不会破坏树的特征,如果破坏了,程序就会进行纠正,根据需要改变树的结构,从而保持树的平衡。

那么红黑树都有哪些特征呢?

 

 

红黑树的特征

 

它主要有两个特征: 1.节点都有颜色;2.在插入和删除的过程中,要遵循保持这些颜色的不同排列的规则。

首先第一个特征很好解决,在节点类中添加一个数据字段,例如boolean型变量,以此来表示节点的颜色信息。

第二个特征比较复杂,红黑树有它的几个规则,如果遵循这些规则,那么树就是平衡的。红黑树的主要规则如下:

  1. 每个节点不是红色就是黑色的;

  2. 根节点总是黑色的;

  3. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);

  4. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。

在红黑树中插入的节点都是红色的,这不是偶然的,因为插入一个红色节点比插入一个黑色节点违背红黑规则的可能性更小。

原因是:插入黑色节点总会改变黑色高度(违背规则4),但是插入红色节点只有一半的机会会违背规则3。另外违背规则3比违背规则4要更容易修正。当插入一个新的节点时,可能会破坏这种平衡性,那么红黑树是如何修正的呢?

 

 

平衡性的修正

 

红黑树主要通过三种方式对平衡进行修正,改变节点颜色、左旋和右旋。这看起来有点抽象,我们分别来介绍它们。

1、变色

改变节点颜色比较容易理解,因为它违背了规则3。假设现在有个节点E,然后插入节点A和节点S,节点A在左子节点,S在右子节点,目前是平衡的。如果此时再插一个节点,那么就出现了不平衡了,因为红色节点的子节点必须为黑色,但是新插的节点是红色的。所以这时候就必须改变节点颜色了。

所以我们将根的两个子节点从红色变为黑色(至于为什么都要变,下面插入的时候会详细介绍),将父节点会从黑色变成红色。

可以用如下示意图表示一下:

2、左旋

通常左旋操作用于将一个向右倾斜的红色链接旋转为向左链接。示意图如下:

左旋有个很萌萌哒的动态示意图,可以方便理解:

3、右旋

 

右旋和左旋刚好相反,这里不再赘述,直接看示意图:

当然咯,右旋也有个萌萌的动态图:

这里主要介绍了红黑树对平衡的三种修正方式,大家有个感性的认识,那么什么时候该修正呢?什么时候该用哪种修正呢?这将是接下来我们要探讨的问题。

 

 

红黑树的操作

 

红黑树的基本操作是添加、删除和旋转。

对红黑树进行添加或删除后,可能会破坏其平衡性,会用到哪种旋转方式去修正呢?我们首先对红黑树的节点做一介绍,然后分别对左旋和右旋的具体实现做一分析,最后我们探讨下红黑树的具体操作。

1、红黑树的节点

红黑树是对二叉搜索树的改进,所以其节点与二叉搜索树是差不多的,只不过在它基础上增加了一个boolean型变量来表示节点的颜色,具体看RBNode类:

 
  1. public class RBNode<T extends Comparable<T>>{

  2.    boolean color; //颜色

  3.    T key; //关键字(键值)

  4.    RBNode<T> left; //左子节点

  5.    RBNode<T> right; //右子节点

  6.    RBNode<T> parent; //父节点

  7.  

  8.    public RBNode(T key, boolean color, RBNode<T> parent, RBNode<T> left, RBNode<T> right) {

  9.        this.key = key;

  10.        this.color = color;

  11.        this.parent = parent;

  12.        this.left = left;

  13.        this.right = right;

  14.    }

  15.  

  16.    public T getKey() {

  17.        return key;

  18.    }

  19.  

  20.    public String toString() {

  21.        return "" + key + (this.color == RED? "R" : "B");

  22.    }

  23. }

  24.  

2、左旋的具体实现

上面对左旋的概念已经有了感性的认识了,这里就不再赘述了,我们从下面的代码中结合上面的示意图,探讨一下左旋的具体实现:

 
  1. /*************对红黑树节点x进行左旋操作 ******************/

  2. /*

  3. * 左旋示意图:对节点x进行左旋

  4. *     p                       p

  5. *    /                       /

  6. *   x                       y

  7. *  /                      /

  8. * lx  y      ----->       x  ry

  9. *    /                  /

  10. *   ly ry               lx ly

  11. * 左旋做了三件事:

  12. * 1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)

  13. * 2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)

  14. * 3. 将y的左子节点设为x,将x的父节点设为y

  15. */

  16. private void leftRotate(RBNode<T> x) {

  17.    //1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)

  18.    RBNode<T> y = x.right;

  19.    x.right = y.left;

  20.  

  21.    if(y.left != null)

  22.        y.left.parent = x;

  23.  

  24.    //2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)

  25.    y.parent = x.parent;

  26.  

  27.    if(x.parent == null) {

  28.        this.root = y; //如果x的父节点为空,则将y设为父节点

  29.    } else {

  30.        if(x == x.parent.left) //如果x是左子节点

  31.            x.parent.left = y; //则也将y设为左子节点

  32.        else

  33.            x.parent.right = y;//否则将y设为右子节点

  34.    }

  35.  

  36.    //3. 将y的左子节点设为x,将x的父节点设为y

  37.    y.left = x;

  38.    x.parent = y;      

  39. }

  40.  

3、右旋具体实现

上面对右旋的概念已经有了感性的认识了,这里也不再赘述了,我们从下面的代码中结合上面的示意图,探讨一下右旋的具体实现:

 
  1. /*************对红黑树节点y进行右旋操作 ******************/

  2. /*

  3. * 左旋示意图:对节点y进行右旋

  4. *        p                   p

  5. *       /                   /

  6. *      y                   x

  7. *     /                  /

  8. *    x  ry   ----->      lx  y

  9. *   /                      /

  10. * lx  rx                   rx ry

  11. * 右旋做了三件事:

  12. * 1. 将x的右子节点赋给y的左子节点,并将y赋给x右子节点的父节点(x右子节点非空时)

  13. * 2. 将y的父节点p(非空时)赋给x的父节点,同时更新p的子节点为x(左或右)

  14. * 3. 将x的右子节点设为y,将y的父节点设为x

  15. */

  16. private void rightRotate(RBNode<T> y) {

  17.    //1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)

  18.    RBNode<T> x = y.left;

  19.    y.left = x.right;

  20.  

  21.    if(x.right != null)

  22.        x.right.parent = y;

  23.  

  24.    //2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)

  25.    x.parent = y.parent;

  26.  

  27.    if(y.parent == null) {

  28.        this.root = x; //如果x的父节点为空,则将y设为父节点

  29.    } else {

  30.        if(y == y.parent.right) //如果x是左子节点

  31.            y.parent.right = x; //则也将y设为左子节点

  32.        else

  33.            y.parent.left = x;//否则将y设为右子节点

  34.    }

  35.  

  36.    //3. 将y的左子节点设为x,将x的父节点设为y

  37.    x.right = y;

  38.    y.parent = x;      

  39. }

  40.  

4、插入操作

分析完了红黑树中主要的旋转操作,接下来我们开始分析常见的插入、删除等操作了。这里先分析插入操作。

由于红黑树是二叉搜索树的改进,所以插入操作的前半工作是相同的,即先找到待插入的位置,再将节点插入,先来看看插入的前半段代码:

 
  1. /*********************** 向红黑树中插入节点 **********************/

  2. public void insert(T key) {

  3.    RBNode<T> node = new RBNode<T>(key, RED, null, null, null);

  4.    if(node != null)

  5.        insert(node);

  6. }

  7.  

  8. //将节点插入到红黑树中,这个过程与二叉搜索树是一样的

  9. private void insert(RBNode<T> node) {

  10.    RBNode<T> current = null; //表示最后node的父节点

  11.    RBNode<T> x = this.root; //用来向下搜索用的

  12.  

  13.    //1. 找到插入的位置

  14.    while(x != null) {

  15.        current = x;

  16.        int cmp = node.key.compareTo(x.key);

  17.        if(cmp < 0)

  18.            x = x.left;

  19.        else

  20.            x = x.right;

  21.    }

  22.    node.parent = current; //找到了位置,将当前current作为node的父节点

  23.  

  24.    //2. 接下来判断node是插在左子节点还是右子节点

  25.    if(current != null) {

  26.        int cmp = node.key.compareTo(current.key);

  27.        if(cmp < 0)

  28.            current.left = node;

  29.        else

  30.            current.right = node;

  31.    } else {

  32.        this.root = node;

  33.    }

  34.  

  35.    //3. 将它重新修整为一颗红黑树

  36.    insertFixUp(node);

  37. }

  38.  

这与二叉搜索树中实现的思路一模一样,这里不再赘述,主要看看方法里面最后一步insertFixUp操作。

因为插入后可能会导致树的不平衡,insertFixUp方法里主要是分情况讨论,分析何时变色,何时左旋,何时右旋。我们先从理论上分析具体的情况,然后再看insertFixUp方法的具体实现。

如果是第一次插入,由于原树为空,所以只会违反红黑树的规则2,所以只要把根节点涂黑即可;如果插入节点的父节点是黑色的,那不会违背红黑树的规则,什么也不需要做;但是遇到如下三种情况时,我们就要开始变色和旋转了:

  1. 插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的;

  2. 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;

  3. 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。

下面我们先挨个分析这三种情况都需要如何操作,然后再给出实现代码。

对于情况1:插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的。

此时,肯定存在祖父节点,但是不知道父节点是其左子节点还是右子节点,但是由于对称性,我们只要讨论出一边的情况,另一种情况自然也与之对应。这里考虑父节点是祖父节点的左子节点的情况,如下左图所示:

 

对于这种情况,我们要做的操作有:将当前节点(4)的父节点(5)和叔叔节点(8)涂黑,将祖父节点(7)涂红,变成上右图所示的情况。

再将当前节点指向其祖父节点,再次从新的当前节点开始算法(具体等下看下面的程序)。这样上右图就变成了情况2了。

对于情况2:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点。

我们要做的操作有:将当前节点(7)的父节点(2)作为新的节点,以新的当前节点为支点做左旋操作。完成后如左下图所示,这样左下图就变成情况3了。

 

对于情况3:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。

我们要做的操作有:将当前节点的父节点(7)涂黑,将祖父节点(11)涂红,在祖父节点为支点做右旋操作。最后把根节点涂黑,整个红黑树重新恢复了平衡,如右上图所示。至此,插入操作完成!

我们可以看出,如果是从情况1开始发生的,必然会走完情况2和3,也就是说这是一整个流程,当然,实际中可能不一定会从情况1发生,如果从情况2开始发生,那再走个情况3即可完成调整,如果直接只要调整情况3,那么前两种情况均不需要调整了。

故变色和旋转之间的先后关系可以表示为:变色->左旋->右旋。

至此,我们完成了全部的插入操作。下面我们看看insertFixUp方法中的具体实现(可以结合上面的分析图,更加利于理解):

 
  1. private void insertFixUp(RBNode<T> node) {  

  2.    RBNode<T> parent, gparent; //定义父节点和祖父节点  

  3.  

  4.    //需要修整的条件:父节点存在,且父节点的颜色是红色  

  5.    while(((parent = parentOf(node)) != null) && isRed(parent)) {  

  6.        gparent = parentOf(parent);//获得祖父节点  

  7.  

  8.        //若父节点是祖父节点的左子节点,下面else与其相反  

  9.        if(parent == gparent.left) {                  

  10.            RBNode<T> uncle = gparent.right; //获得叔叔节点  

  11.  

  12.            //case1: 叔叔节点也是红色  

  13.            if(uncle != null && isRed(uncle)) {  

  14.                setBlack(parent); //把父节点和叔叔节点涂黑  

  15.                setBlack(uncle);  

  16.                setRed(gparent); //把祖父节点涂红  

  17.                node = gparent; //将位置放到祖父节点处  

  18.                continue; //继续while,重新判断  

  19.            }  

  20.  

  21.            //case2: 叔叔节点是黑色,且当前节点是右子节点  

  22.            if(node == parent.right) {  

  23.                leftRotate(parent); //从父节点处左旋  

  24.                RBNode<T> tmp = parent; //然后将父节点和自己调换一下,为下面右旋做准备  

  25.                parent = node;  

  26.                node = tmp;  

  27.            }  

  28.  

  29.            //case3: 叔叔节点是黑色,且当前节点是左子节点  

  30.            setBlack(parent);  

  31.            setRed(gparent);  

  32.            rightRotate(gparent);  

  33.        } else { //若父节点是祖父节点的右子节点,与上面的完全相反,本质一样的  

  34.            RBNode<T> uncle = gparent.left;  

  35.  

  36.            //case1: 叔叔节点也是红色  

  37.            if(uncle != null & isRed(uncle)) {  

  38.                setBlack(parent);  

  39.                setBlack(uncle);  

  40.                setRed(gparent);  

  41.                node = gparent;  

  42.                continue;  

  43.            }  

  44.  

  45.            //case2: 叔叔节点是黑色的,且当前节点是左子节点  

  46.            if(node == parent.left) {  

  47.                rightRotate(parent);  

  48.                RBNode<T> tmp = parent;  

  49.                parent = node;  

  50.                node = tmp;  

  51.            }  

  52.  

  53.            //case3: 叔叔节点是黑色的,且当前节点是右子节点  

  54.            setBlack(parent);  

  55.            setRed(gparent);  

  56.            leftRotate(gparent);  

  57.        }  

  58.    }  

  59.  

  60.    //将根节点设置为黑色  

  61.    setBlack(this.root);  

  62.  

    }  

     

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值