算法导论 第十三章:红黑树 笔记(红黑树的性质、旋转、插入、删除)

版权声明:站在巨人的肩膀上学习。 https://blog.csdn.net/zgcr654321/article/details/83119533

红黑树(red-black tree) 是许多“平衡的”查找树中的一种,它能保证在最坏情况下,基本的动态集合操作的时间为O(lgn) 。

红黑树的性质:

红黑树是一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是RED或BLACK 。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。

树中每个结点包含五个域: color, key, left, right和p。如果某结点没有一个子结点或父结点,则该结点相应的指针(p)域包含值NIL。我们将把这些NIL视为指向二叉查找树的外结点(叶子)的指针,而把带关键字的结点视为树的内结点。

红黑树有以下五点性质: 

1、每个节点或是红色的,或是黑色的; 

2、根节点是黑色的; 

3、每个叶节点(NIL)是黑色的; 

4、如果一个节点是红色的,则其两个子节点都是黑色的; 

5、对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。 

当满足以上5个性质的红黑树,就能保证没有一条路径会比其他路径长2倍以上,因而可看作近似平衡

黑高度

从某个节点x出发到大一个叶节点的任意一条路径上,黑色节点的个数称为该节点的黑高度,用bh(x)表示。

以任一结点x为根的子树至少包含2的bh(x)次方−1个内部结点。

红黑树是一种好的二叉查找树,对一棵有n个内节点的红黑树的高度至多为2lg(n+1),STL中的set和map就是用红黑树实现的。红黑树的动态集合操作SEARCH, MINIMUM, MAXIMUM, SUCCESSOR, PREDECESSOR与二叉查找树的对应操作的实现一样,且它们的时间复杂度都是O(lgn)

为了便于处理红黑树代码中的边界条件,我们采用一个哨兵来代表NIL。对一棵红黑树T 来说,哨兵nil[T] 是一个与树内普通结点有相同域的对象。它的color 域为BLACK, 而它的其他域p, left, right 和key可以设置成任意允许的值。如下图所示,所有指向NIL的指针都被替换成指向哨兵nil[T]的指针。

使用哨兵后,就可以将结点x的NIL孩子视为一个其父结点为x的普通结点。虽然我们可以在树内的每一个NIL 上新增一个不同的哨兵结点,来让每个NIL的父结点都有这样的定义,但是这种做法会浪费空间。

我们的做法是采用一个哨兵nil[T]来代表所有的NIL——所有的叶子以及根部的父结点。哨兵的域p, left, right 以及key 的取值如何并不重要,为了方便起见,也可以在程序中设定它们。

在本章中,我们使用忽略了所有叶子与根部的父节点的红黑树。

如:

旋转:

两种旋转: 左旋和右旋。

如:

当在某个结点x上做左旋时,我们假设它的右孩子y不是nil[T]; x可以为树内任意右孩子不是nil[T]的结点。

左旋过后,使y的父结点从x变为x的父结点,x变为y的右子结点,y的左子结点变为x的右子结点。

伪代码:

LEFT-ROTATE(T,x)
    y <- right[x]
    right[x] <- left[y]
    if left[y]!= nil[T]
        p[left[y]] <- x
    p[y] <- p[x]
    if p[x] == nil[T]
        then root[T] <- y
        else if x == left[p[x]]
                then left[p[x]] <- y
                else right[p[x]] <- y
    left[y] <- x
    p[x] <- y

RIGHT-ROTATE(T,x)
    y <- left[x]
    left[x] <- right[y]
    if right[y] != nil[T]
        p[right[y]] <- x
    p[y] <- p[x]
    if p[x] == nil[T]
        then root[T] <- y
        else if x == left[p[x]]
                then left[p[x]] <- y
                else right[p[x]] <- y
    right[y] <- x
    p[x] <- y

下面举一个例子:

插入:

向一棵含n 个结点的红黑树中插入一个新结点的操作可在O(lgn) 时间内完成。

我们利用TREE-INSERT 过程的一个略作修改的版本,来将结点z插入树T内,就好像T是一棵普通的二叉查找树一样,然后将z着为红色。为保证红黑性质能继续保持,我们调用一个辅助程序RB-INSERT-FIXUP 来对结点重新着色并旋转。

红黑树有以下五点性质: 

1、每个节点或是红色的,或是黑色的; 

2、根节点是黑色的; 

3、每个叶节点(NIL)是黑色的; 

4、如果一个节点是红色的,则其两个子节点都是黑色的; 

5、对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。 

z插入后有以下几种情况:

z作为根节点:(破坏了性质2)修改根节点为黑色即可;

z的父节点为黑色: 不做操作;

z的父节点为红色:(破坏了性质4)又分成三种情况:
    1、z的叔结点y是红色;
    2、z的叔结点y是黑色且z是一个右孩子;
    3、z的叔结点y是黑色且z是一个左孩子。

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

如:

如果z的叔节点是红色,则z结点的祖父结点是黑色,父结点是红色(父结点和本身都是红色才会违背性质4)。我们只需要将父、叔结点涂成黑色,祖父结点涂成红色,而本身颜色不需要改变。

这样一来只是维护好了自己的“小家庭”,包括父、叔结点和祖父节点。而且保证了性质⑤:红变黑、黑变红,并不会增减某一条路径中黑结点的个数。至于祖父节点的变色可能会导致它和它父亲结点的冲突,所以我们就将“当前结点”这个“不合群”的帽子扣在了祖父结点,让它继续地被维护。

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

将z的父结点做为新的当前结点,将新的当前结点做左旋。这样就将情况2转到情况3来处理。

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

如:



如图,情况2中将z的父结点做为新的当前结点,将新的当前结点做左旋。这样就变成了情况3。再将此时的z父结点做为新的当前结点,将新的当前结点涂成黑色(之前是红色),新的当前节点的父节点涂成红色,再对新的当前结点的父结点做右旋。这样就保证了性质5,同时修正了性质4,中间并没有破坏任何其他性质,结束循环判断。

伪代码:

RB-INSERT(T,x)
    y <- nil[T]
    x <- root[T]
    while x != nil[T]
        do y <- x
        if key[z] < key[x]
            then x <- left[x]
            else x <- right[x]
    p[z] <- y
    if y == nil[T]
        then root[T] <- z
        else if key[z] < key[y]
            then left[y] <- z
            else right[y] <- z
    left[z] <- nil[T]
    right[z] <- nil[T]
    color[z] <- RED
    RB-INSERT-FIXUP(T,z)

RB-INSERT-FIXUP(T,z)
while color[p[z]] == RED
    do if p[z] == left[p[p[z]]]
        then y <- right[p[p[z]]]
            if color[y] == RED
                then color[p[z]] <- BLACK
                     color[y] <- BLACK
                     color[p[p[z]]] <- RED
                     z <- p[p[z]]
            else if z == right[[p[z]]
                then z <- p[z]
                     LEFT-ROTATE(T,x)
                color[p[z]] <- BLACK
                color[p[p[z]]] <- RED
                RIGHT-ROTATE(T,z.p.p)
            else(same as then clause with "right" and "left" exchanged)
    color[root[T]] <- BLACK

删除:

和n个结点的红黑树上的其他基本操作一样,对一个结点的删除要花O(lgn) 时间。

程序RB-DELETE是对TREE-DELETE程序略作修改得来的。在删除一个结点后,该程序就调用一个辅助程序RB-DELETE-FIXUP,用来改变结点的颜色并做旋转,从而保持红黑树性质。

删除操作也分几个步骤:

首先,按照搜索二叉树的删除操作删除要删的结点,然后针对每种情况做换色和旋转操作,使其恢复红黑树的性质。

红黑树有以下五点性质: 

1、每个节点或是红色的,或是黑色的; 

2、根节点是黑色的; 

3、每个叶节点(NIL)是黑色的; 

4、如果一个节点是红色的,则其两个子节点都是黑色的; 

5、对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。 

换色和旋转的维护工作分为4种情况考虑(以下情况是x作为其父节点的左孩子的情况,右孩子的情况对称处理):

1、待删结点x的兄弟结点w是红色:

如图,x的兄弟结点w是红色,所以它一定有两个黑色的子结点。我们只需改变待删的兄弟结点为黑色,改变父结点的颜色为红色,然后再对父结点做一次左旋操作,所以待删结点的兄弟结点一定是黑色。这样就将情况1转为情况2、3、4之一去处理。

2、待删结点x的兄弟结点w是黑色,且w的两个子结点都是黑色:

如图,x的兄弟节点w是黑色,且w的两个子节点都是黑色。这种情况下B左边黑色高度比右边的少1,所以只需将w变成红色就满足性质5。

如果父节点B本身是黑色的,这时父节点B具有黑黑的属性,继续当做x迭代处理!

如果父节点B本身是红色的,这时父节点B具有红黑的属性,直接退出循环,把父节点B涂成黑色即可!

3、待删结点x的兄弟结点w是黑色,且w的左孩子是红色,右孩子是黑色:

我们需要交换w和其左孩子的颜色,即C、D颜色互换。然后对w进行右旋,使得待删结点x的新兄弟结点new w是一个有红色右孩子的黑色结点,这样将情况3转为情况4去处理。

4、待删结点x的兄弟结点w是黑色,且w的右孩子是红色:

此时我们可以将兄弟结点D染成当前父结点B的颜色,把当前父结点B的颜色染成黑色,兄弟结点的右子结点E染成黑色。然后对当前父结点B做一次左旋即可。

伪代码:

RB-TRANSPLANT(t,u,v)
    if u.p == T.nil
        T.root = v
    else if u == u.p.left
        u.p.left = v
    else u.p.right = v
    v.p = u.p

RB-DELETE(T,z)
    y = z
    y-original-color = y.color
    if z.left == T.nil
        x = z.right
        RB-TRANSPLANT(T,z,x)
    else if z.right == T.nil
        x = z.left
        RB-TRANSPLANT(T,z,x)
    else y = TREE-MINIMUM(z.right)
        y-original-color = y.color
        x = y.right
        if y.p == z
            x.p = y
        else 
            RB-TRANSPLANT(T,y,x)
            y.right = z.right
            z.right.p = y
        RB-TRANSPLANT(T,z,y)
        y.left = z.left
        y.left.p = y
        y.color = z.color
    if y-original-color == BLACK
        RB-DELETE-FIXUP(T,x)

RB-DELETE-FIXUP(T,x)
    while x != T.root && x.color == BLACK
        if x == x.p.left
            w = x.p.right
            if w.color == RED
                w.color = BLACK
                x.p.color = RED
                LEFT_ROTATE(T,x.p)
                w = x.p.right
            if w.left.color == BLACK && w.right.color == BLACK
                w.color = RED
                x = x.p
            else 
                if w.right.color == BLACK
                    w.left.color = BLACK
                    w.color = RED
                    RIGHT-ROTATE(T,w)
                    w = x.p.right
                w.color = x.p.color
                x.p.color = BLACK
                w.right.color = BLACK
                LEFT_ROTATE(T,x.p)
                x = T.root
        else(same as then clause with "right" and "left" exchanged)
    T.root.color = BLACK

猜你喜欢

转载自blog.csdn.net/zgcr654321/article/details/83119533