继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

红黑树——以无厚入有间(插入)

湖上湖
关注TA
已关注
手记 276
粉丝 85
获赞 377

首先说一下,关于红黑树有一篇很棒的论文《A dichromatic framework for balanced trees》,作者之一的Robert Sedgewick,想必大家不会陌生。如果有兴趣可以仔细研读一下,里面讲了更多细节的分析,而本文也借鉴了其中的一些思路和配图。

 

回顾一下之前的结构分析,经验指出:平均而言红黑树大约和AVL树一样深,如此保证了查找效率接近最优。另外他的优点有两个,插入所需要的开销比之前更低,再有就是实践中发生的旋转相对比较少。

https://img.mukewang.com/5b8bf33d000105be16360582.jpg

而红黑树的具体实现是比较复杂的,这不仅因为可能会发生大量的旋转,还因为一些子树可能是空的(比如10这个节点的右子树),以及处理根的特殊情况(因为没有父节点)。因此我们要用两个标记节点:一个是根;一个是NullNode,类似在伸展树里指示NULL的指针。根标记用来储存−∞−∞和一个真正指向根的右指针。先给出类型声明,因为旋转操作还需要用到,所以继续沿用。

复制代码

 1 #ifndef RB_Tree_h  2 #define RB_Tree_h  3   4 typedef enum{ red,black} Color;  5   6 const int Infinity = 0x3f3f3f3f;  7 const int NegINF = -Infinity -1;  8 struct RedBlackNode;  9 typedef RedBlackNode*  RedBlackTree; 10 typedef RedBlackNode*  Position; 11  12 struct RedBlackNode { 13     int value,Height; 14     RedBlackTree left; 15     RedBlackTree right; 16     Color col; 17 }; 18  19 Position NullNode=NULL; //needs initialization 20 int max(int a,int b){ return a>b?a:b;} 21 RedBlackTree Insert(int Item,RedBlackTree T); 22 RedBlackTree Delete(int Item,RedBlackTree T); 23 #endif /* RB_Tree_h */ 24  25 static int 26 Height( Position P ) 27 { 28     return P==NULL?-1:P->Height; 29 } 30  31 static Position 32 SingleRotateWithLeft( Position p )   //左-左的情况 33 { 34     Position temp; 35      36     temp = p->left; 37     p->left = temp->right; 38      39     temp->right = p; 40      41     p->Height = max( Height( p->left ), Height( p->right ) ) + 1; 42     temp->Height = max( Height( temp->left ), p->Height ) + 1; 43      44     return temp;  /* New root */ 45 } 46  47 static Position 48 SingleRotateWithRight( Position g )   //右-右的情况 49 { 50     Position temp; 51      52     temp = g->right; 53     g->right = temp->left; 54      55     temp->left = g; 56      57     g->Height = max( Height( g->left ), Height( g->right ) ) + 1; 58     temp->Height = max( Height( temp->right ), g->Height ) + 1; 59      60     return temp;  /* New root */ 61 }

复制代码

NullNode标记含义有所变化,为此调整一下打印函数,用一个隐藏的递归过程,这样就很巧妙了,因为不会强迫用户传入T-> right,这留给机器去做。

复制代码

 1 static void DoPrint(RedBlackTree T){  2     if (T != NullNode) {  3         DoPrint(T->left);  4         printf("%d ",T->value);  5         DoPrint(T->right);  6     }  7     printf("\n");  8 }  9  10 void Display(RedBlackTree T){ 11     DoPrint(T->right); 12 }

复制代码

 

除此之外我们还需要通过初始化来指定头节点。构造第一棵树时,初始化程序要为NullNode分配内存,此后的树可以共享NullNode。

复制代码

 1 RedBlackTree Initialize()  2 {  3     RedBlackTree T;  4     if (NullNode == NULL) { //如果NullNode不存在,就让它成为一个空闲仓库,并且保证合乎红黑树规则  5         NullNode=(Position)malloc(sizeof(struct RedBlackNode));  6         NullNode->left=NullNode->right=NullNode;  7         NullNode->col=black;  8         NullNode->value=Infinity;  9     } 10      11     //Create the header node 12     T=(Position)malloc(sizeof(struct RedBlackNode)); 13     T->value=NegINF; 14     T->left=T->right=NullNode; 15     T->col=black; 16      17     return T; 18 }

复制代码

首先是插入操作,与理解红黑树的定义一样,这里我们也必须借助B树的模型才能更好地了解相关算法的原理和过程。也就是说在我们考察每一棵红黑树的时候,在脑海中总是要有一棵对应的B树如影随形,本来红黑树就是4阶B-树嘛。就像“降临:暴君巴拉克”一样2333

https://img1.mukewang.com/5b8bf3490001806d07440646.jpg

 

与所有的BBST一样,在经过了动态变化之后,红黑树的组成成员不仅发生了变化,而且它们之间的拓扑连接关系也可能发生变化。然而这种变化并不容易直接理解,为此我们需要借助B树的影子。就像理解红黑树的定义一样,我们会发现,红黑树与其对应的影子B树之间的关系非常好理解,而且反之亦然。而更重要的是,站在新的视角来看,前后两棵影子B树之间的关系也将变得一目了然。这样一种理解的方式,看似迂回,但很快就会感受到它的效率反而是最高的。

https://img.mukewang.com/5b8bf35200016b0c17261460.jpg 

假设我们要插入e这个数,不妨先用BST常规插入算法看看,然后予以改进。插入后相应的会生成一个新的末端节点x,平凡情况的树根就不考虑了。这意味着 它的父亲必然是存在的,接下来将x染为红色(除非是根)。这样做的好处是:红黑树的各条规则能够尽量满足。来逐条考察一下

  1. 树根是黑色的,叶子(即使是NULL)也是黑色的

  2. 除了root和leaf,每一个节点或红或黑

  3. 如果一个节点是红色的,那子节点必须是黑色

  4. 从某个节点到叶子的路径上必须包含相同数目的黑色节点

https://img.mukewang.com/5b8bf35c00011bd101960214.jpg

树根节点和所有的外部节点依然是黑的,在通往各个外部节点的路径上,黑节点的数目因为没有变化,我们染的红色,所以依然保持全局的一致性。然而第三条规则却未必满足。考察这个新插入的红色节点x,作为末端的叶节点,它的2个孩子都是外部节点,所以都是黑的。然而父节点颜色不确定,此时p就是这样一个可黑可红的节点,如果它的确是黑的那第三条规则也同时满足,整个插入就成功返回。然而问题在于p的确可能原本就是红的,比如这样: 

https://img1.mukewang.com/5b8bf3650001a8a604120662.jpg

关于边的虚实,作如下约定:凡是指向黑色节点或者颜色不定的节点的边,都用实线来表示。所有指向红色节点的边,都用虚线表示。这种方式可以更好地帮助我们思考和分析,因为这类虚边在经过提升变换之后都会变成是水平方向。新插入的节点x与它的父亲p如果同时为红色,是红黑树规则所禁止的,这样一种非法的情况也因此称作双红缺陷。如何修复呢?我们首先要考察x的祖父节点g,注意,此时的g必然存在,否则作为树根的节点p是不可能为红色的。然后,作为红色节点p的父亲,节点g也必然是黑色的。此外我们还需要考查g的另一个孩子u(uncle of x),当然节点u的颜色也是不定的。因此以下就根据这个节点u的颜色分2种情况分别处理。

首先假设u是黑色(而且约定NULL都是黑色)。举个例子,还是那幅图,插入3 or 8这种情况是允许的,而插入99就不行了(双红)。

https://img4.mukewang.com/5b8bf36d00013b0615240548.jpg

现在抽象地考虑,我们分别有Grandparent,Parent,Sibling和待插入节点X,用伸展树中的术语,他们之间可以构成一字链or之字链,可以采用旋转解决。

一字形分布,采用单旋转

 

https://img.mukewang.com/5b8bf3730001646f09860212.jpg

 

之字形分布,双旋转。(这两个图待会讲u是红色的时候还会提到,记得翻回来) 

https://img2.mukewang.com/5b8bf3790001737e09800260.jpg

 

以上两种情况还分别有另外的镜像对称情形,分别是右右,和右左。用原论文中的图表示如下,不难理解:

https://img4.mukewang.com/5b8bf381000100bb11041000.jpg

而他们4种都说明了如果u(或者说是下图的S)是黑色,那么可以旋转解决。而编写程序时我们必须记录父节点,祖父节点以及为了重新连接而记录的曾祖父(Great-GrandParent)节点。在这两大种情况下,子树的新根(P,X)都被涂成黑色,因此即使原来的曾祖父是red,我们也排除了两个相邻节点都是red的可能。最重要的是,经过旋转后保持了路径上黑节点数目不变。

所以旋转微调和总体双红修复的步骤如下:

复制代码

//在X处执行旋转 static Position Rotate(int Item,Position par) {     if (Item < par->value)         return par->left = Item < par->left->value ?         SingleRotateWithLeft(par->left) :         SingleRotateWithRight(par->left);          else         return par->right = Item < par->right->value ?         SingleRotateWithLeft(par->right) :         SingleRotateWithRight(par->right); } //双红修复 static Position X,P,GP,GGP; void HandleReorient(int Item,RedBlackTree T) {     X->col=red;  //默认染红,下面孩子还是得遵守规则,保证是黑     X->left->col=black;     X->right->col=black;          if (P->col == red) {  //引发双红缺陷         GP->col=red;         if ((Item < GP->value) != (Item < P->value)) //两者异或为真,意味着是之字形,要在父节点多做一次旋转,否则以曾祖父为轴做单旋转             P = Rotate(Item, GP);         X = Rotate(Item, GGP);         X->col=black;     }     T->right->col=black; //让新根染黑 }

复制代码

 

为了进一步加深理解,我们从B树角度看一看两种情况: 

https://img.mukewang.com/5b8bf38a00014efd04860688.jpg

 此时的x p g下属应该共有4个子树,尽管它们都有可能是外部节点(NULL),但是根据红黑树红色节点只能有黑色孩子的规则——包括u在内,它们都必然是黑的。而且既然在此前,这是一棵合法的红黑树,那这4个黑节点的黑高度也应该是一样的。现在借助提升变换,将此前指向红色节点的所有虚边都收缩起来,局部的这祖孙三代节点就会合并为一个4阶B树中的内部节点。貌似这样的超级节点并没有违规?因为它们下属的分支都不超过4阶B树的上限。确切的说,唯一的缺陷是在每个超级节点居中的这个关键码不是黑色。因此从B树的角度看这种调整就很简明了:并不需要调整B树的拓扑结构,而只需在违规的超级节点中对关键码重新的染色。a的情况,只需交换p和g的颜色。

 

https://img3.mukewang.com/5b8bf39300011f0007961156.jpg

而b的情况只需交换x和g的颜色。

 

整个调整过程以及效果从B树的角度来看,是非常清晰的。双红缺陷之所以是非法的,从B树的角度可以认为是,因为在某个原本是3叉的节点中,插入了一个红色的关键码,从而使得原先的黑关键码不再居中。对照所有的4种情况不难验证这点。而调整之后的效果相当于,B树的拓扑结构不变,而在对应的4叉节点中3个关键码的颜色已经改为合法的红黑红模式。请要注意的是在这种情况下,即使红黑树的拓扑结构有所调整,但也仅限于局部。而更重要的是,这种调整是一步到位的,不用后续其他的调整了。因此就全树的拓扑连接关系的变化量而言,必然是不超过O(1)O(1)。

到这一步一切顺利,下面考虑u是红色的情况,比如说在下图插入79。

https://img1.mukewang.com/5b8bf39b00013b0615240548.jpg

这种情况下从子树的根到C的路径上有一个黑色节点(翻到上面旋转的例图)。旋转之后必然还是只有一个,但两种情况下,通向C的路径上都有三个节点(新根,G,S),这里面只有一个可能是黑的,为了保证不能有连续的红色节点,就必须把S和子树的新根都涂成红色,而把G和GGP都涂成黑色。

这么说我都感觉有点绕,现在从B树的角度再考虑一下。这里也只给出了两种情况,忽略掉对称的另两种情况。 

https://img4.mukewang.com/5b8bf3a2000137f304840614.jpg

           图 M

 

https://img2.mukewang.com/5b8bf3a90001f58005200648.jpg

            图  N

 

同样借助提升变换,将所有指向红色节点的虚边收缩起来。于是从B树的角度来看 局部的这4个节点将合并为一个包含4个关键码的内部节点。4个关键码,对应于5个分支。这无论是a,b哪种情况,这样的内部节点在4阶B树中都是非法的。而用B树的语言来说,它们之所以非法,是因为上溢。因此,与其说我们是在红黑树中修复双红缺陷,不如说是在对应的4阶B树中修复上溢缺陷,这二者完全是一回事。那B树中如何修复上溢?回忆一下。

https://img2.mukewang.com/5b8bf3b200018c9818040504.jpg

需要在出现问题节点中找到居中的那个关键码,并且以它为界将原先的大节点分裂为左右两个新的节点,而居中分界的这个关键码则应被取出来,上移并插入到父节点中的适当位置,这样一个转换,只是B树中的基本操作,很好理解。

因此总体看来,将此前的红黑树转换为对应的4阶B树,从提升变换的角度来看 也非常好理解。从变换之后的B树到红黑树通过提升变换,也非常易于理解,这样迂回的过程,比我们试图直接去理解红黑树的调整过程反过来更为简明。也印证了前面的那幅示意图。 

https://img2.mukewang.com/5b8bf3bd0001c19018441256.jpg

从红黑树的角度对于这种情况,只需将节点p由红转黑,同时节点g,u由黑转红。而从B树的角度来看,等效于对一个刚刚发生上溢的节点实施一次分裂操作,同时居中的关键码被提升并加入到父节点之后将转为红色。当然在g的左或右至少应该有一个黑色的关键码,如果有红色,就再次发生了双红缺陷,没关系,继续套用这个方法即可。因为这个过程是逐层向上蔓延,因此顶多复杂度是$O\left( h \right)$。最后需要强调的一点是:尽管调整过程从B树的角度来看,发生了拓扑结构的变化。但是从红黑树的角度来看,除了节点的颜色会变,全树的拓扑连接关系并没有任何变化。也就是说,尽管重染色操作的次数可能会高达O(logn)O(log⁡n)次,但拓扑结构的变化却依然控制在常数的范围。

概括来讲,自顶向下的过程中,当我们看到一个节点X有两个红色儿子,就让X成为红色,而两个儿子变成黑色。

当叔父节点u为红色时,修正双红缺陷导致的红黑树拓扑结构没有变化。

综上所述,旋转和旋转和结构调整的步骤如下:

复制代码

 1 RedBlackTree  2 Insert(int Item,RedBlackTree T)  3 {  4     X=P=GP=T;  5     NullNode->value=Item;  //此时经过初始化,NullNode已经是一个空闲节点,存入指定的数值  6     while (X->value != Item)    //自树根拾级而下,扫除一切uncle为红的情况  7   8     {  9         GGP=GP; GP=P; P=X; 10         if(Item < X->value ) 11             X=X->left; 12         else 13             X=X->right; 14         //节点的两个孩子都是红色,转换成B树意味着内部节点上溢,如图M,N,需要进行修复 15         if( X->left->col == red && X->right->col == red ) 16             HandleReorient(Item, T); 17     } 18     //走到这一步时,X->value已经是期望的Item了 19     if(X != NullNode) 20         return NullNode; //重复 21      22     //否则就创建新的节点接入 23     X=(Position)malloc(sizeof(struct RedBlackNode)); 24     X->value=Item; 25     X->left = X->right = NullNode; 26      27     if(Item < P->value) 28         P->left=X; 29     else 30         P->right=X; 31     HandleReorient(Item, T); 32      33     return T; 34 }

复制代码

 

最后分析一下插入的复杂度。首先这里无非牵涉到两种基本的操作:旋转和对节点颜色的重新定义。二者都是局部的基本操作,单次只需O(1)O(1),因此我们只需统计在整个的修正过程中,二者各自总共执行了多少次。这是整个修正算法的流程图:

https://img3.mukewang.com/5b8bf3c80001fd7317281466.jpg

可以看到通过判断u节点的颜色,无非两个分支。其中u节点为黑的这个分支相对简单

我们只需做一次局部的旋转调整,再做常数次的染色操作就完成了。也就是说在这种情况下,旋转至多一轮2次。而染色呢?至多牵涉到两个节点,都是常数。当然u节点为红色的情况比较复杂,因为尽管在每一个节点处我们只需做常数次的重新染色,但是事情未必彻底解决,因为由此可能导致在更高的节点处进而出现双红缺陷,此时我们还需要重新回到算法的入口,再来一次。在最坏的情况下,这种情形有可能会出现多达O(logn)O(log⁡n)次。总结如下:

 https://img2.mukewang.com/5b8bf3d00001e66d06620284.jpg

要注意的是,在右半侧循环中,我们只需做重新染色,而不用做结构调整

https://img4.mukewang.com/5b8bf3d80001360809480810.jpg

而另外半边,一旦做过结构的调整,整个算法就会随即结束。因此总体而言,整个修复过程中可能会执行很多次染色操作,但ReOrient在整个修复过程中至多只会执行O(1)O(1)次。还记得吧,上一篇讲过我们为什么会更加在意拓扑重构——因为这类操作对于可持久化结构而言是至关重要的。当然对于插入操作的这些性能要求,AVL树同样满足,然而正如我们在前文所指出的,AVL的删除操作却不具有这样的性能。那么红黑树呢?让我们拭目以待。

原文出处:https://www.cnblogs.com/hongshijie/p/9575289.html

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP