首页 > 代码库 > 深入理解红黑树

深入理解红黑树

 红黑树是平衡树的一种,保证最坏情况下操作时间复杂度为O(lgo(n))。红黑树的应用比较广泛,比如作为C++中STL的set和map的底层数据结构,Java集合中TreeSet和TreeMap的底层数据结构等。学习红黑树,可以把二叉查找树作为参考,这样有助于加深理解。红黑树的操作主要包括节点旋转、插入、删除等操作,下面咱们就一一来看:

1、红黑树性质

  1. 每个节点是红色的,或者是黑色的
  2. 根节点是黑色的
  3. 每个叶节点(nil)是黑色的
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的
  5. 对每个节点,从该节点到其后代叶节点的简单路径上,均包含相同数目的黑色节点

红黑树整体节点图示如下:

技术分享

2、旋转

  旋转是保持二叉搜索树(红黑树)局部性质的操作,包括左旋和右旋。当在节点x上做左旋时,假设它的右孩子为y而不是T.nil,X可为其右孩子不是T.nil的任何节点,同理,X做右旋也是一样的。

技术分享

伪代码如下(左旋)

技术分享

左旋Java示例代码:

/**
 * 左旋操作
 */
private void leftRotate(Node x) {
    Node y = x.right;

    x.right = y.left;
    if (y.left != nil) {
        y.left.parent = x;
    }
    y.parent = x.parent;
    if (x.parent == nil) {
        root = y;
    } else if (x == x.parent.left) {
        x.parent.left = y;
    } else {
        x.parent.right = y;
    }
    
    x.parent = y;
    y.left = x;
}

3、插入

  在O(log(n))时间内插入一个节点,RB-INSERT过程完成该操作,像一个普通的二叉搜索树插入操作一样,然后将要插入的节点Z着为红色。为了保持红黑树的性质,调用一个辅助函数RB-INSERT-FIXUP来对节点重新着色并旋转。待插入节点Z已保存了数据项。

技术分享

RB-INSERT-FIXUP过程伪代码

技术分享
技术分享

  注意:Case1属于if判断条件,Case2和Case3属于else语句,Case2是else语句中的if判断语句的。具体可以看代码。

插入过程Java代码示例:

/**
 * 往红黑树中插入一个元素
 * @param data
 */
public void insert(int data) {
    Node y = nil;
    Node x = root;
    
    if (root == nil) {
        root = newNode(data);
        root.color = Node.BLACK;
    } else {
        while (x != nil) {
            if (data < x.data) {
                y = x;
                x = x.left;
            } else if (data > x.data) {
                y = x;
                x = x.right;
            } else {
                return;
            }
        }
        
        Node z = newNode(data);
        z.parent = y;
        if (data < y.data) {
            y.left = z;
        } else {
            y.right = z;
        }
        if (y.color == Node.RED) {
            insertFixup(z);
        }
    }
}

RB-INSERT-FIXUP过程分析

  第1-15行的while循环在每次迭代的开始始终保持以下3个条件是不变的

  1. 节点z是红节点
  2. 如果z.p是根节点,则z.p是黑节点
  3. 如果有任何红黑树性质贝破坏,则至多有一条被破坏,或是性质2,或是性质4(各个性质见红黑树性质总结)。如果性质2被破坏,则因为z是根节点且是红节点。性质4被破坏则是因为z和z.p都是红节点。

循环终止条件:

  循环终止因为z.p是黑色的(如果z是根节点,则z.p是黑色哨兵节点),这样,在循环终止时并没有违反性质4,唯一可能不成立的就是性质2,不过第16行(伪代码中的行)恢复了这个性质。

循环保持条件:

  循环保持有6中条件,其中3种和另外3种是对称的,这取决于z的父节点z.p是z的祖父节点z.p.p的左孩子还是右孩子。情况1、2、3的区别就在于z 的父节点的兄弟节点(z的叔节点)的颜色不同,加入y指向z的叔节点,如果y的颜色是红色的,则执行情况1,否则转向情况2和3。在所有的3种情况中,z 的祖父节点z.p.p是黑色的,因为他的父节点z.p是红色的,故性质4只有在z和z.p之间被破坏了。

技术分享

 

情况1:z的叔节点y是红色的

       此时对应Case1,将z.p和y都着成黑色,解决z和z.p都是红色的问题,将z.p.p着成红色保持性质5,然后把z.p.p作为新节点z来继续进行while循环,指针z上移两层

技术分享

情况2:z的叔节点y是黑色的且z是一个右孩子

情况3:z的叔节点y是黑色的且z是一个左孩子

       两种情况中,y都是黑色的,通过z是左孩子还是右孩子来区分,情况2中可以通过一个左旋来转化为情况3,此时z为左孩子。因为z和z.p都是红节点,对黑 高(从某个节点出发,不含该节点,到达一个叶节点的任意一条简答路径上的黑色节点数为该节点的黑高)和性质5都无影响。在情况3中,改变某些节点颜色并做 一次右旋,保持性质5,这样不在有两个红色节点相邻,处理完毕,此时z.p是黑色的,所以无需再执行while循环了。

技术分享

RB-INSERT-FIXUP的Java代码实现

/**
 * 插入节点后不满足红黑树条件时来修复
 * @param z 待插入的节点
 */
private void insertFixup(Node z) {
    // y为z节点的叔叔节点
    Node y = null;
    
    while (z.parent.color == Node.RED) {
        if (z.parent == z.parent.parent.left) {
            y = z.parent.parent.right;
            if (y.color == Node.RED) {
                z.parent.color = Node.BLACK;
                y.color = Node.BLACK;
                z.parent.parent.color = Node.RED;
                z = z.parent.parent;
            } else {
                if (z == z.parent.right) {
                    z = z.parent;
                    leftRotate(z);
                }
                
                z.parent.color = Node.BLACK;
                z.parent.parent.color = Node.RED;
                rightRotate(z.parent.parent);
            }
        } else {
            y = z.parent.parent.left;
            if (y.color == Node.RED) {
                z.parent.color = Node.BLACK;
                y.color = Node.BLACK;
                z.parent.parent.color = Node.RED;
                z = z.parent.parent;
            } else {
                if (z == z.parent.left) {
                    z = z.parent;
                    rightRotate(z);
                }
                
                z.parent.color = Node.BLACK;
                z.parent.parent.color = Node.RED;
                leftRotate(z.parent.parent);
            }
        }
    }
    
    root.color = Node.BLACK;
}

4、删除

  删除操作与插入操作相比,略显复杂,与插入操作一样,也要花费O(log(n))时间。从红黑树中删除节点,需设计一个供TREE-DELETE调用的子过程TRANSPLANT,并应用到红黑树中,TRANSPLANT过程用来调整两个节点的关系,其中的一个节点要替换掉另一个节点

TRANSPLANT过程

  u节点表示将要被替换掉的节点,v节点是准备替换u节点的

技术分享

RB-TRANSPLANT的Java代码实现

/**
 * 两个节点的替换,newNode替换oldNode
 * @param oldNode
 * @param newNode
 */
private void transplant(Node oldNode, Node newNode) {
    if (oldNode.parent == nil) {
        root = newNode;
    } else if (oldNode == oldNode.parent.left) {
        oldNode.parent.left = newNode;
    } else {
        oldNode.parent.right = newNode;
    }
    
    newNode.parent = oldNode.parent;
}

RB-DELETE过程

  RB-DELETE中,z节点是要被删除的节点,其中记录了节点y的踪迹,y有可能导致红黑树性质破坏,当想删除节点z,且z的子节点少于2个时,z从书中删除,并让y称为z。当z有两个子节点时,y应该是z的后继,并且将y移到z的位置。在节点被删除或者移动时,必须记住y的颜色,并且记录节点x的踪迹,将x移到y的原来的位置,因为节点x可能引起红黑树性质破坏。删除节点z后,RB-DELETE-FIXUP过程通过改变颜色和执行旋转来恢复红黑树性质。

技术分享

  我们保存节点x的踪迹,使它移至节点y的原来位置。第4、7和11行的赋值语句令x或指向y的唯一子节点或指向y哨兵T.nil(y没有子节点的话)。  第5、8或14行调用RB-TRANSPLANT时,传递的第2个参数与x相同

  如果y是黑色的,则有可能引入一个或多个破坏红黑色的情况,所以在第22行调用RB-TRANSPLANT来恢复红黑树性质。如果y是红色的,当y被删除或移动时,红黑树性质依然成立,因为(1) 树中黑高没有变化 (2) 不存在两个相邻的红节点,因为y在树中占据了z的位置,z的位置肯定不会违反红黑树性质的。另外,如果y是z的右孩子,则y的原右孩子x代替y,如果y是红色的,则x一定是黑色的,一次用x替代y不会使两个红节点相邻。 (3) 如果y是红色的,就不会是根节点,所以根节点仍然是黑色的。

y为黑节点情况分析

技术分享

RB-DELETE的Java代码实现

/**
 * 从红黑树中移除一个元素
 * @param data 待移除的元素
 */
public void remove(int data) {
    Node z = contains(data);
    if (z == null) {
        return;
    }
    
    Node x;
    Node y = z;
    int yOldColor = y.color;
    if (z.left == nil) {
        x = z.right;
        transplant(z, x);
    } else if (z.right == nil) {
        x = z.left;
        transplant(z,  x);
    } else {
        y = z.right;
        while (y.left != nil) {
            y = y.left;
        }
        yOldColor = y.color;
        x = y.right;
        
        if (y.parent == z) {
            x.parent = y;
        } else {
            transplant(y, y.right);
            y.right = z.right;
            y.right.right = y;
        }
        
        transplant(z, y);
        y.left = z.left;
        y.left.parent = y;
        y.color = z.color;
    }
    
    if (yOldColor == Node.BLACK) {
        removeFixup(x);
    }
}

RB-DELETE-FIXUP过程

技术分享

ps:图片上的情况层次关系对应不是很齐,可以看码来的清楚

4种情况的转换关系

  • 1 -> 2、3、4
  • 2 -> 1、2、3、4、修复 (只有Case2是节点上移,所有有可能会使x=root)
  • 3 -> 4
  • 4 -> 修复

  无论哪个Case,要么是为了到达平衡两个节点,路径上黑色数目相同的目的,要么是通过变形间接达到这个目的。

while循环的目标是将额外的黑色沿树上移,直到:

  1. x指向红黑节点,此时在第23行处将x着为(单个)黑色
  2. x指向根节点,此时可以简单地移除额外的黑色
  3. 执行适当的旋转和重新着色

技术分享

代码中的4种情况图示

技术分享

       上图给出了代码中出现的四种情况,再具体研究每一个情况时,先看看如何证实每种情况中的变换保证性质5。关键思想是在每种情况下,从子树的跟(包括跟)到每棵子树之间的黑色节点个数(包括x的额外黑色)并不被变换改变,因此,如果性质5在变换之前成立,则在变换之后也成立。比如:在情况1中,在变换前后,根节点到子树a或b之间的黑色节点数是3(因为x增加了一层黑色),类似的,在变换前后根节点到其余的4个子树的叶节点中的任何一个的黑节点数是2。在图13-7(b)中,计数是还要包括所示子树的根节点的color属性的值,它是RED或是BLACK,如果定义count(RED)=0以及count(BLACK)=1,那么变换前后根节点至a的黑节点都为2+count(c)。在此情况下,变换之后新节点x具有color属性的c,但是这个节点的颜色是红黑(如果c=RED)或者双重黑色的(如果c=BLACK)。其他情况可以类似加以验证。

情况1:x的兄弟节点w是红色的

  情况1(见RB-DELETE-FIXUP的第5-8行和图13-7(a))发生在节点x的兄弟节点w为红色时。因为w必须有黑色子节点,所有可以改变w的x.p的颜色,然后对x.p做一次左旋而不违反红黑树的任何性质。现在x的新兄弟节点是旋转之前w的某个子节点,其颜色为黑色。这样,就将情况1转变为了情况2、3或者4。

  当节点w为黑色时,属于情况2、3或者4;这些情况有w的子节点颜色来区分了。

情况2:x的兄弟节点w是黑色的,而且w的两个子节点都是黑色的

  在情况2(见RB-DELETE-FIXUP的第10-11行和图13-7(b)),w的两个子节点都是黑色的,因为w也是黑色的,所以从x和w上去掉一重黑色,使得x只有一重黑色而w为红色。为了补偿从x和w中去掉的一重黑色,在原来是红色或黑色的x.p上增加一重额外的黑色。通过将x.p作为新节点x来重复while循环。注意到,如果通过情况1进入到情况2,则新节点x就是红黑色的,因为原来的x.p是红色的,因此,新节点x的color属性值为RED,并且在测试循环条件后循环终止。然后,在第23行处将新节点x着为(单一)黑色。

情况3:x的兄弟节点w是黑色的,w的左孩子是红色的,w的右孩子是黑色的

  情况3(见RB-DELETE-FIXUP的第13-16行和图13-7(c))发生在w为黑色且其左孩子为红色,右孩子为黑色时。可以交换w和其左孩子w.left的颜色,然后对w进行右旋而不违反红黑树的性质。现在x的新节点w是一个有红色右孩子的黑色节点,这样我们就从情况3转换成了情况4。

情况4:x的兄弟节点w是黑色的,且w的右孩子是红色的

  情况4(见RB-DELETE-FIXUP的第17-21行和图13-7(d))发生在节点x的兄弟节点w为黑色且w的右孩子为红色时,通过进行某些颜色修改并对x.p做一次左旋,可以去掉x的额外黑色,从而使它变为单重黑色,而且不破坏红黑树性质。将x设为设为根root后,当while循环测试其循环条件时,循环终止。

RB-DELETE-FIXUP的Java代码实现

/**
 * 移除一个元素后红黑树不符合要求时的修复方法
 * @param x
 */
private void removeFixup(Node x) {
    Node y;
    
    while (x != root && x.color == Node.BLACK) {
        if (x == x.parent.left) {
            y = x.parent.right;
            if (y.color == Node.RED) { //情况1:x的兄弟节点w是红色
                y.color = Node.BLACK;
                x.parent.color = Node.RED;
                leftRotate(x.parent);
                y = x.parent.right;
            }
            
            if (y.left.color == Node.BLACK && y.right.color == Node.BLACK) { //情况2:x的兄弟节点w是黑色,而且w的两个子节点都是黑色的
                y.color = Node.RED;
                x = x.parent;
            } else {
                if (y.right.color == Node.BLACK) { //情况3:x的兄弟节点w是黑色的,w的左孩子是红色的,w的右孩子是黑色的
                    y.left.color = Node.BLACK;
                    y.color = Node.RED;
                    rightRotate(y);
                    y = x.parent.right;
                }
                
                y.color = x.parent.color; //情况4:x的兄弟节点w是黑色的,且w的右孩子是红色的
                x.parent.color = Node.BLACK;
                y.right.color = Node.BLACK;
                leftRotate(x.parent);
                x = root;
            }
        } else {
            y = x.parent.left;
            if (y.color == Node.RED) {
                y.color = Node.BLACK;
                x.parent.color = Node.RED;
                rightRotate(x.parent);
                y = x.parent.left;
            }
            
            if (y.left.color == Node.BLACK && y.right.color == Node.BLACK) {
                y.color = Node.RED;
                x = x.parent;
            } else {
                if (y.left.color == Node.BLACK) {
                    y.right.color = Node.BLACK;
                    y.color = Node.RED;
                    leftRotate(y);
                    y = x.parent.left;
                }
                
                y.color = x.parent.color;
                x.parent.color = Node.BLACK;
                y.left.color = Node.BLACK;
                rightRotate(x.parent);
                x = root;
            }
        }
    }
    x.color = Node.BLACK;
}

删除分析:

技术分享

5、完整程序Java代码实现

红黑树Rbtre.java文件:

技术分享 View Code

测试用例程序Main.java文件

技术分享 View Code

深入理解红黑树