红黑树

2022-09-03 21:09:51 浏览数 (1)

历史上AVL树流行的另一变种是红黑树(red black tree)。对于红黑树的操作在最坏情形下花费O(log N)时间,而且我们将看到,(对于插入操作的)一种慎重的非递归实现可以相对容易地完成(与AVL树相比)。

红黑树是具有下列着色性质的二叉查找树:

1、每一个节点或者红色,或者黑色。

2、根是黑色的。

3、如果一个节点是红色的,那么它的子节点必须是黑色的。

4、从一个节点到一个NULL指针的每一条路径必须包含相同数目的黑色节点。

着色法则的一个推论是:红黑树的高度最多是2log(N 1)。因此,保证查找是一种对数的操作。和通常一样,困难在于将一个新项插入到树中。通常把新项作为树叶放到树中。如果我们把该项涂成黑色,那么我们肯定违反条件4,因为将会建立一条更长的黑节点的路径。因此,这一项必须涂成黑色。如果它的父节点是黑的,我们插入完成。如果它的父节点已经是红色的,那么我们得到连续红色节点,这就违反了条件3。在这种情况下,我们必须调整该树以确保条件3满足(且又不引起条件4被破坏)。用于完成这项工作的基本操作是颜色的改变和树的旋转。

1、自底向上输入

我们已将注意到,如果新插入的项的定义是黑色的,那么插入完成。如果父节点是红色的,那么会有几种情形(每种都有一个镜像对称)需要考虑。首先,假设这个父节点的兄弟是黑色的(我们采纳约定:NULL节点都是黑色的)。对于插入3或8是适用的,但对插入99不适用。令X是新加的树叶,P是它的父节点,S是该父节点的兄弟(若存在),G是祖父节点。这种情形只有X和P是红的,G是黑的,因为否则就会在插入前有两个相连的红色节点,违反了红黑树的法则。采用伸展树的术语,X、P和G可以形成一个一字形链或之字形链(两个方向中的任一个方向)。

第一种情形对应P和G之间的单旋转,而第二种情形对应双旋转,该双旋转首先在X和P间进行,然后在X和G之间进行。当编写程序的时候,我们必须记录父节点、祖父节点,以及为了重新连接还要记录曾祖节点。在两种情形下,子树的新根被涂成黑色,因此,即使原来的曾祖是红的,我们也排除了两个相邻红节点的可能性。同样中要的是,这些旋转的结果是通向A、B和C诸路径上的黑节点数保持不变。

到现在为止一切顺利。但是,如果S是红色的,那么会发生什么情况呢?在这种情况下,初始时从子树的根到C的路径上有一个黑色节点。在旋转之后,一定仍然还是只有一个黑色节点。但在两种情况下,在通向C的路径上都有三个节点(新的根,S和G)。由于只有一个可能是黑的,又由于我们不能有连续的红色节点,于是我们必须把S的子树和新根都涂成红色,而把G(以及第四个节点)涂成黑色。这很好,可是,如果曾祖也是红色的那么又会怎样呢?此时,我们可以将这个过程朝着根的方向上滤,就像对B树和二叉堆所做的那样,直到我们不再有两个相连的红色节点或者到达根(它将被重新涂成黑色)处为止。

2、自顶向下红黑树

上滤的实现需要用一个栈或用一些父指针保存路径。我们看到,如果我们使用一个自顶向下的过程,实际上是对红黑树应用从顶向下保证S不会是红的过程,则伸展树会更有效。这个过程在概念上是容易的。在向下的过程中当我们看到一个节点X有两个红儿子的时候,我们让X成为红的而让它的两个儿子是黑的。如果X的父节点的兄弟是红的会如何?这种可能性已经被从顶向下过程中的行动所排除,因此X的父节点的兄弟不可能是红的!特别地,如果在沿树向下的过程中我们看到一个节点Y由两个红儿子,那么我们知道Y的孙子必须是黑的,由于Y的儿子也要变成黑的,甚至可能发生旋转之后,因此我们将不会看到两层上另外的红节点。这样,当我们看到X,若的父节点是红的,则X的父节点的兄弟不可能也是红的。

红黑树的具体实现是复杂的,这不仅因为有大量的可能的旋转,而且还因为一些子树可能是空的,以及处理根的特殊的情况(尤其是根没有父亲)。因此,我们使用两个标记节点:一个是为根,一个是NullNode,它的作用像在伸展树中那样指示一个NULL指针。根标记将在存储关键字

和一个指向真正的根的右指针。为此,查找和打印需要调整。递归的例程都很巧妙。我们使用一个隐藏的递归过程,并不强迫用户传递T-->Right。因此用户不必关系头结点。下面指出如何重新编写中序遍历。

代码语言:javascript复制
/*Print the tree, watch our for NullNode*/
/*and skip header */

static void
{
   if( T! != NullNode )
   {   DoPrint( T->Left );
       Output( T->Element );
       DoPrint( T->Right );
   }
}


void
PrintTree(RedBlackTree T)
{
    DoPrint( T->Right );
}

我们还需要使用用户调查例程Initialize来指定投节点。如果构造的是第一棵树,那么Initialize应该再为NullNode分配内存(其后的树可以分享NullNode)。因为得到的树必须连接到父节点上,所以Rotate把该父节点作为一个参数。在沿着树下行的时候,我们把Item作为参数传递,而不是跟踪旋转的类型。由于我们希望插入过程中旋转很少,因此这么做实际上不仅更简单,而且还更快。Rotate直接返回执行相应单旋转的结果。例程HandleReorient当我们遇到带有两个红儿子的节点时被调用,在我们插入一片叶子时它也被调用。唯一复杂的部分是,一个双旋转实际上是两个单旋转,而且只有通向X的分支相反方向时才进行。正如我们在较早的讨论中提到的。当沿树向下进行的时候,Inset必须记录父亲、祖父和曾祖父。注意,在一次旋转之后,存储在祖父和曾祖父中的值将不再正确。不过,可以肯定到下一次再需要它们的时候它将被重新存储。

3、自顶向下删除

红黑树中的删除也可以自顶向下进行。每一行工作都归结于能够杉树一片树叶。这是因为,要删除一个带有两个儿子的节点,我们用右子树上的最小节点代替它;该节点必须最多有一个儿子,然后将该节点删除。只有一个右儿子的节点可以用相同的方式删除,而只有一个左儿子的节点通过用其左子树上最大节点替换,然后可以将该节点删除。注意,对于红黑树带有一个儿子的节点的情形,我们不想使用这种方法进行,因为这可能在树的中部连接两个红色节点,为红黑条件的实现增加苦难。

代码语言:javascript复制
typeof enum ColorType { Red, Black } ColorType:

struct RedBlack
{
    ElementType  Element 
    RedBlackTree Left;
    RedBlackTree RIght;
    ColorType Color;
};

Position NullNode = NULL; /* Needs initialization */
RedBlackTree
Initialize( void )
{
         RedBlackTree T;
         if( NullNode = Null)
         {
            NullNode == malloc(sizeof(struct RedBlackNode));
            if( NullNode == NULL )
               FataError("Out of space!!!");
            NullNode -> left = NullNode->Right = NullNode;
            NullNode -> Color = Black;
            NullNode -> Element = Infinity;
           
         }
         /*Create the header node*/
         T = malloc( sizeof( struct RedBlackTree ) );
         if( T==NULL )
             FatalError("Out of sapce!!!")
         T -> Element = NegINfinity;
         T -> Left = T -> Right = NullNode;
         T -> Color = Black;
        
         return T;
}



/* Perform a rotation at node X*/
/* (whose parent is passed as parameter)*/
/* The child is deduced by examing item*/

static Position
Potate( ElementType Item, Position Parnet )
{
     if( Item < Parnet -> Element )
        return Parent -> Left = Item < Parent -> Left -> Element ?
           SingleRotateWithLeft( Parent->Left);
           SingleRotateWithRight( Parent->Left);
     else
        return Parent->Right = Item < Parent->Right->Element ?
             SingleRotateWithLeft( Parent -> Right )
             SingleROtationWithRight( Parent -> Right )

}




static Position X, P, GP, GGP;
static
void HandleReorient( Element Type, RedBlackTree T )
{
    x->Color = Red; /* DO the color flip */
    x->Left->Color = Black;
    x->Right->Color = Black;

    if( P->Color == Red )/*Have to rotate*/
    {
       GP -> Color = Red;
       if((Item < GP->Element) != (Item < P->Element))
           P = Rotate( Item, GP );/* Start double rotation */
       x = Rotate( Item, GGP )
       x->Color = Black;
    }
    T->Right->Color = Black; /*Make root black*/
}
RedBlack
Insert( ElementType Item, RedBlackTree T )
{
       X = P = GP = T;
       NullNode -> Element = Item;
       while(x->Element != Item) /*Descend down the tree*/
       {
          GGP = GP; GP = P; P = X;
          if( Item < X->Elemnt )
              X = X->Left;
          else
              X = X->Right;
          if(x->Left->Color == Red && X->Right->Color == Red)
              HandleReorient( item, T )
       }
       if(X != NullNode)
          return NullNode;/*Duplicate*/

       X = malloc( sizeof( struct RedBlackNode ) )
       if( X == NULL )
         FatalError( "Out of space!!!" )   
       x->Element = item;
       x->Left = X->Right = NULLNode;

       if( Item < P->Element )/*Attach to its parent*/
          P->Left = X;
       else
          P->Right = X;
       HandleReorient( Item, T );/*Color red; maybe rotate*/
         
       return T;
}

当然,我们树叶的删除很简单。然而,如果一片树叶是黑色的,那么删除操作会复杂得多,因为黑色节点的删除将破坏条件4。解决方法是保证从上到下删除期间树叶是红色的。在整个讨论中,令X为当前节点,T是它的兄弟,而P是它们的父亲。开始时我们把树的根涂成红色。当沿树向下遍历时,我们设法保证X是红色的。当我们达到一个新的节点时,我们要确信P是红色的(归纳地按照我们试图保持的这种不变性)并且X和T是黑的(因为我们不能有两个相连的红色节点)。存在两种主要的情形。首先,X有两个黑儿子。此时有三种子情况。如果T也有两个黑儿子,那么我们可以翻转X,T和P的颜色来保持这种不变性。否则,T的儿子之一是红色的。设X的儿子之一是红的。在这种情形下,我们落到下一层上,得到新的X、T和P。如果幸运,X落在红儿子上,则我们可以继续向前进行。如果不是这样,那么我们知道T将是红的,而X和P将是黑的。我们可以旋转T和P,使得X的新父亲是红的;当然X和它将是黑的。此时我们可以回到第一种情况。

0 人点赞