数据结构-红黑树

本文由@呆代待殆原创,转载请注明出处:http://www.cnblogs.com/coffeeSS/

 

学习红黑树的前置技能:二叉搜索树http://www.cnblogs.com/coffeeSS/p/5452719.html

 

红黑树简介

红黑树是一种自平衡(平衡指所有叶子的深度趋于相等)二叉查找树,它是在1972年由1972年由鲁道夫·贝尔发明,这个数据结构对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保(也就是说同样都在最坏情况下,他的速度比一般的树的速度要快)无论是查找,插入还是删除都可以在O(lgn)内完成,所以很适合用在时间敏感的场合中。

 

红黑树的性质

红黑树就是有颜色的二叉查找树,并且要满足以下5个约束:

1,节点不是红色就是黑色。

2,根节点是黑色。

3,所有叶子节点都是黑色(这里所有叶子节点指的是NIL节点)。

4,每个红色节点的两个孩子都是黑色。

5,任意一个节点到其每个叶子的所有简单路径(简单路径指路径中不会出现重复节点)都包含相同数目的黑色节点。

 

红黑树的每个节点至少包含5个属性:color、key、left、right与parent。

如果一个节点没有子节点或者父节点则相应的指针设为null或者指向NIL节点。

根节点是唯一一个parent为NIL的节点。

红黑树的所有叶子节点都是NIL节点,但是在很多实例图中这些NIL节点都会被省略,而且实际实现中,如果真的实现NIL节点,一般只会设置一个NIL节点,让所有指向叶子节点的节点都指向这个节点,根节点的parent指针也会指向这个节点(当然你简单的将这些指向NIL节点的指针都设成null也是可以的,但是本文的代码实现是基于NIL节点的,因为这样代码写起来更自然一点)。

NIL节点的作用是充当哨兵和完善红黑树的结构,它的color属性为black,其他属性都没有实际意义,设为null或者不管都可以。

下面看一张来自WIKI百科的红黑树的图例

An example of a red-black tree
 
 
上面的约束确定了一个红黑树的关键特性:从根到叶子的最长的可能路径不多于最短可能路径的两倍。
 
证明这个特性的正确性:因为由约束4可知,不可能存在两个相邻的红色节点,这个结论再配合约束5可知最短的路径一定是全黑的路径,设这个路径上有n个黑节点,那么最长的路径也一定有n个黑节点,而且最长路径的节点数减去最短路径的节点数就是最长路径中红色节点的数目,那么我们只有在n个黑色节点中尽可能多的插入红色节点就行了,在这5个约束下,我们只能让红黑节点交替出现,而且因为根节点和叶子节点都是黑色的,所以我们最多只能插入n-1个节点,所以当最短路径有n个节点时,最长路径最多只有2n-1个节点,完全满足上述特性。
 
本文红黑树的结构
本文将实现以下包含以下结构和方法的红黑树(C++)
 1 enum Color{black,red};
 2 
 3 struct Node{
 4     Node(){ color = black; }
 5     Node(int k) :key(k){ color = red; }
 6     Node* parent = nullptr;
 7     Node* left = nullptr;
 8     Node* right = nullptr;
 9     Color color;
10     int key;
11 };
12 
13 class MyRBT{
14     private:
15         Node* nil=new Node();
16         Node* root=nil;
17         void fix_color(Node* n);//插入过程中有可能递归的部分
18         Node* getUncle(Node* n);//返回n的叔节点
19         void left_rotate(Node* n);//左旋
20         void right_rotate(Node* n);//右旋
21         Node*& parentToN(Node* n);//获得n的父节点里指向n的那个指针
22         Node* getSibling(Node* n);//返回n的兄弟节点
23         bool isParentLeftChild(Node* n);//判断n是不是父节点的左孩子
24         void delete_fix_color(Node* n);//删除过程中有可能递归的部分。
25     public:
26         MyRBT(){};
27         MyRBT(vector<int> v);
28         Node* getRoot(){ return root; }
29         void insertNode(int k);
30         void deleteNode(int k);
31         Node* findByKey(int k);
32         Node* findSuccessor(int k);
33         Node* findPredecessor(int k);
34         void traversal(Node* root, void(*f)(int k) = [](int k)->void{cout << k << " "; });//遍历输出所有节点
35         void insertNode(Node* n);
36         void deleteNode(Node* m);
37         Node* findSuccessor(Node* n);//找n的后继
38         Node* findPredecessor(Node* n);//找n的前驱
39 };

 

其中除了插入和删除操作,其他操作和二叉搜索树没什么区别,只是这里的版本都是基于NIL节点实现的,而之前的二叉搜索树那篇是基于nullptr实现的,本质上没有任何区别,所以本文重点介绍插入删除操作。

 
红黑树的操作
红黑树的只读操作和普通的二叉查找树没有区别(只读操作包括查找,遍历等),但是插入和删除操作和普通的二叉搜索树是不一样的,因为红黑树要始终保持上述5个约束不被破坏,如果在插入或者删除某个节点的时候破坏了红黑树的约束,我们可以通过旋转操作来调整这棵树让它重新满足这5个约束,那么再我们介绍插入和删除前先让我们来看一看什么是旋转操作吧。
 
旋转操作
如图是一张来自《算法导论》的图示,从上面可以直接看出,旋转操作分为左旋和右旋,并且他们为互逆的操作。
左旋(left_rotate):在x节点上进行左旋操作时假设他的右孩子(y节点)不是NIL节点,并且x节点也不是NIL节点,左旋结束后,如图,x节点成为y节点的做孩子,y节点取代了x节点的位置,其实仔细看一下就知道,左旋就是原本指向x节点的指针指向了x节点的右孩子,相当于整个以x为根节点的子树向左转动了一下而已,是一个相当简单的操作。
右旋(right_rotate):假设右旋节点的左孩子和这个节点不为NIL节点,其他和上述左旋类似,不再赘述。
 
 
再给出左旋,右旋的代码前,我们先给出这个方法的代码 Node*& parentToN(Node* n);//获得n的父节点里指向n的那个指针 
取得n节点的父节点比较容易,但是再从父节点取到n节点需要确定n节点是父节点的左孩子还是右孩子,因为我们经常要让n节点的父节点指向n的指针指向一个新的节点,所以我们封装一下这个方法比较好,这个方法返回的是左值,可以直接对其赋值。
1  Node*& MyRBT::parentToN(Node* n){
2      if (n->parent == nil) return nil;
3      if (n->parent->left == n)
4          return n->parent->left;
5      else return n->parent->right;
6 }

 然后我们给出左旋和右旋操作的代码,结合上面的图来理解比较好。

 1 void MyRBT::left_rotate(Node* n){
 2     if (n->right == nil)//设n的右孩子为y  
 3         cout << "left_rotate error" << endl; 
 4     parentToN(n) = n->right;//修改n的父节点指向y
 5     n->right->parent = n->parent;//修改y的parent指针     改y->parent 为 n->parent
 6     n->parent = n->right;//修改n的parent指针             改n->parent 为 n->right
 7     n->right = n->right->left;//将y的左子树变成n的右子树  改n->right 为 y-left
 8     n->parent->left = n;//将n改成y的左孩子 ,这个时候取原来n的右孩子不能通过n->right,因为上一句中已经改变了r->right,但是看到上上一句就知道n->parent现在指向n的原先的右孩子               改y->left 为 n
 9 }
10 void MyRBT::right_rotate(Node* n){
11     if (n->left == nil)
12         cout << "right_rotate error" << endl;
13     parentToN(n) = n->left;
14     n->left->parent = n->parent;
15     n->parent = n->left;
16     n->left = n->left->right;
17     n->parent->right = n;
18 }

 

插入操作
给红黑树插入节点时,插入的节点会标为红色(因为如果你插入的是黑色节点,你就会破坏约束5,直接插入红色节点可以避免破坏这个约束,减小了破坏约束的概率),但是插入后,仍然有其他约束可能被破坏,归纳一下有5种情况需要我们来处理
注:插入节点我们用n来指代,其父节点用p来指代,叔节点用u来指代,祖父节点用g来表示。
1,n是根节点(破坏了约束2).
2,n的p节点是黑色。
3,n的p节点和u节点都是红色。
4,n是g节点的左孩子的右孩子,或者是g节点的右孩子的左孩子。(p是红的,而u是黑的)
5,n是g节点的左孩子的左孩子,或者是g节点的右孩子的右孩子。(p是红的,而u是黑的)
 
对于这五种情况需要依次判断。
 
情况一:这种情况很简单,只要把n染成黑色的就解问题了。
情况二:这种情况下,没有任何规则被违反,不需要处理。
情况三:这种情况下,我们会违反约束4,所以我们要将p与u染成黑色,并且将g染成红色,这样就解决了局部问题,但是这样也许会导致g节点违反约束,此时的g节点就像是刚插入的新节点一样需要从情况一开始判断是否违反约束。
情况四:如果n是g节点的左孩子的右孩子,那么我们对n进行左旋,如果n节点是g节点的右孩子的左孩子,那么我们对p进行右旋,这是情况四会转换成情况五,然后我们就用情况五去解决剩下的问题(因为此时约束4仍然不满足)
情况五:如果n是g节点的左孩子的左孩子,那么我们对g节点右旋,如果n节点是g节点的右孩子的右孩子,那么我们对g节点左旋,然后将此时的根节点染成黑色(如果是红色会违反约束四),将根节点的两个孩子染成红色,此时树就被修复了。
 
这就是插入新节点需要考虑的问题。那么我们接下来集中解决一些相关问题。
 
问题一:为什么只有这些情况(注意插入的操作前半部分是和二叉搜索树一样的)。
答:首先一个节点插入要么他在根上,那么就是情况一;要么他的深度为2,只有p节点(此时p是跟节点只能是黑的),没有g节点,这是情况二;如果深度大于二,那么首先考虑父节点那一层,如果都是黑的,那么可以当做情况二来处理,如果p是黑的,u是红的,也可以当做情况二处理;如果p是红的u是黑的那么进入情况四与情况五,如果p与u都是红色的,这是情况三。
 
问题二:为什么分情况的时候不考虑g上面的其他节点呢?
答:太上面的节点我们已经是规范的了,如果我们能解决g节点一下的不规范,g节点以上是不用管的,但是如果我们改变了g节点的颜色,我们就要对g节点进行上述判断,这是情况三中的一部分。
 
问题三:为什么不考虑g节点的颜色呢?
答:g节点的颜色我们不需判断(g一定是黑的,因为p是红的),理由如下,涉及到g节点的过程情况是三到五,在情况三中,我们一定会把g节点染成红色并把它当成刚插入的节点来重新判断,所以g节点原本是什么颜色不重要;如果判断流程进入情况四,那么一定会进入情况五,而情况四种没有针对g的操作,所以g的颜色对过程四也不重要,情况五中,g的孩子节点一定会取代g节点的位置,而且这个新的根节点会被染成黑色,之后g节点也一定会被我们染成红色,所以g的颜色对情况2五也不重要。
 
问题四:情况五中,为什么我们要将新的根节点染黑而不通过调整子节点的颜色来规范红黑树呢?
答:因为之前的根节点一定是黑色,如新的根节点颜色改变,那么我们不得不对新的节点重新判断,看它是否也破坏了红黑树到的约束,所以将新的根节点涂成黑色,可以将矛盾控制在内部,方便解决。
 
问题五:有全黑的红黑树么?
答:有啊,只有一个根节点的时候,其他情况,你可以自己试试看= =
 
下面给出插入方法的实现,插入的过程和二叉搜索树差不多,不再写注释,不同的部分在于用NIL节点代替了nullptr,并增加了修正颜色用的方法 void fix_color(Node* n);//插入过程中修正颜色的方法 对这个方法的理解是重点所在
 
首先再给出一个辅助方法 Node* getUncle(Node* n);//返回n的叔节点 
1 Node* MyRBT::getUncle(Node* n){
2     if (n->parent != nil&&n->parent->parent != nil){
3         if (n->parent->parent->left == n->parent)
4             return n->parent->parent->right;
5         else return n->parent->parent->left;
6     }
7     return nullptr;
8 }

 

下面是插入算法本体和fix_color方法
 1 void MyRBT::insertNode(int k){
 2     insertNode(new Node(k));
 3 }
 4 
 5 void MyRBT::insertNode(Node* n){
 6     Node* temp = root;
 7     if (temp == nil){
 8         root = n;
 9         root->parent = nil;//root的父节点指向nil
10         root->left = nil;
11         root->right = nil;
12         fix_color(n);
13         return;
14     }
15     while (true){
16         if (temp->key > n->key){
17             if (temp->left != nil)
18                 temp = temp->left;
19             else{
20                 temp->left = n;
21                 n->parent = temp;
22                 n->right = nil;
23                 n->left = nil;
24                 fix_color(n);
25                 return;
26             }
27         }
28         else{
29             if (temp->right != nil)
30                 temp = temp->right;
31             else{
32                 temp->right = n;
33                 n->parent = temp;
34                 n->right = nil;
35                 n->left = nil;
36                 fix_color(n);
37                 return;
38             }
39         }
40     }
41 }
42 
43 //nil节点的存在是为了保证数结构的完整性和规范性
44 
45 void MyRBT::fix_color(Node* n){
46     if (n->parent == nil)//情况一
47         n->color = black;
48     else if (n->parent->color == black){//情况二
49         //无需任何处理
50     }
51     else if (getUncle(n)->color == red){//情况三
52         n->parent->color = black;
53         getUncle(n)->color = black;
54         n->parent->parent->color = red;
55         fix_color(n->parent->parent);
56     }
57     else {
58         if (n == n->parent->parent->left->right){//    头两个if是同时进行了情况四和情况五,这样写是因为旋转操作次数不
59             left_rotate(n->parent);              //一样,各个节点的位置关系不一样,不能把这两个过程完全分开,看看后面的
60             right_rotate(n->parent);             //颜色处理就会明白。
61             n->color = black;
62             n->left->color = red;
63             n->right->color = red;
64         }
65         else if (n == n->parent->parent->right->left){
66             right_rotate(n->parent);
67             left_rotate(n->parent);
68             n->color = black;
69             n->left->color = red;
70             n->right->color = red;
71         }
72         else if (n == n->parent->parent->left->left){//这两个if是没有经过情况四直接进行情况五的时候。
73             right_rotate(n->parent->parent);
74             n->parent->color = black;
75             n->parent->left->color = red;
76             n->parent->right->color = red;
77         }
78         else {
79             left_rotate(n->parent->parent);
80             n->parent->color = black;
81             n->parent->left->color = red;
82             n->parent->right->color = red;
83         }
84     }
85 }

 

插入算法的特点:
1,旋转操作不会超过两次。
2,插入算法的递归次数不会大于此节点深度的二分之一。
3,插入算法的最坏时间复杂度为O(lgn)
 
以上就是插入的所有问题。
下面让我们来看看删除的时候会碰到的问题。
     
 
删除操作
删除算法比较复杂,我们来慢慢分析,请保持耐心。
首先当被删除的节点M有两个非NIL孩子的时候,我们会像二叉搜索树那样找M节点的(前驱/后继)来代替M(代替M时,我们只是改变了M的key值,并没有改变任何节点的颜色,所以没有破坏红黑树的性质),然后删除这个(前驱/后继)(这时红黑树的性质可能被破坏),这时,我们删除的这个(前驱/后继)节点是只有一个非NIL孩子的,所以删除有两个非NIL孩子节点的问题可以当成删除有一个非NIL孩子节点的问题。
 
那么接下来我们的讨论都会针对最多只有一个非NIL孩子的节点
 
我们称被删除的节点为M,如果M有一个非NIL节点的孩子,那么我们叫这个节点C,如果M只有两个NIL节点孩子,随便选一个叫C就好。
 
情况一:M是红节点时,此时M一定有两个NIL节点的孩子(证明:考虑如果有一个非NIL节点的孩子C,那么C一定是黑色的,这时不管C节点后面都有什么节点,经过C节点的路径的黑色节点的个数一定比到达M节点另一个孩子节点的路径多,这时违反红黑树规定的,M为红色时,只可能有两个NIL孩子节点),此时我们只要用C节点替换M节点就行了。
 
情况二:M是黑色节点,且有一个红色节点的孩子C,这时情况也很简单,我们只需要删掉M并用C节点代替它,再把C节点染成黑色就好了,从颜色上看,我们只是减少了一个红色节点,不会破坏任何性质,从结构上看,我们成功删除了M节点,达到了目的,也很好操作。
 
情况三:当M与C都是黑色节点时,情况就比较复杂了(这种情况只可能时M有两个NIL孩子节点时,假设其中有一个是非NIL孩子节点那么它的子树中一定有黑色节点,这就会导致不是每条路径的黑色节点数相同,所以M与C都是黑色时只能是M有两个NIL孩子节点时)这时我们先用任意一个M的孩子当做C节点来进行接下来的操作。
 
现在我们来确定在情况三下,若是删除了M节点会有什么影响,由于我们用C节点代替了M节点,将M节点移除了,所以经过C节点的路径会比没有经过C节点的路径少一个黑色节点所以只要我们想办法把经过C节点的路径的黑色节点数增加一个就可以了,那么现在我们来进行这个工作,第一步,我们先来做一下相关节点的命名区分。
 
我们定义当C节点替换完了M节点后,我们重新命名这个C节点为N节点(new的意思啦),N的兄弟节点命名为S(也就是原来M的父节点的另一个孩子),N的父节点为P(也就是之前M的父节点)。
 
下面我们就来讨论情况三下,当节点呈现各种状态的时候我们应该如何面对,在这里我们先假定N是p的左孩子(如果N是P的右孩子,下述相关操作的很多左与右的关系会替换,但是过程和思想是一样的)。
 
 
经过前人的归纳,一共将这个修复的过程划分成了6个状态,每一次删除M节点后我们都要对N进行这个六个状态的依次检查,规则如下。
 
开始:首先判断是否符合状态一的描述,符合则进入状态一,不符合则跳过状态一,判断是否满足状态二,不满足就跳过状态二的描述,就根据状态二的后续部分进行接下来的操作。
 

状态一:

描述:N是根节点。

操作:什么都不用做了,因为树空了。

         影响:无。

         后续:退出。

 

状态二:

描述:S节点是红色。

操作:将P和S的颜色交换,然后对P节点进行左旋(到此树的结构还是没有正常之后还要经过4,5 or 6,同样接下来的操作还是通过N节点来定位)。

影响:

结构:因为进行了左旋所以有很多变化详细请看图。

颜色:各个路径上的黑色节点数没有变化,但是S(左旋后的位置)的左子树的红色节点多了一个,而右子树的红色节点少了一个。

后续:判定现在的状态决定进入状态三、四、五或者六中的一个。

 

状态三:

描述:P、S、S的孩子都是黑色。

操作:我们直接把S染成红色。

影响:

         结构:无变化。

         颜色:P的右子树路径上的黑色节点数少了一个,而红色节点多了一个。

后续:这样一来经过P节点的路径就会比不经过P节点的路径少一个黑色节点(P节点以下的颜色矛盾已经解决),P节点现在的处境和之前的N节点一模一样,所以我们对P进行颜色的修正,进行递归调用。

 

 

状态四:

描述:S和S的孩子是黑色的,但是P是红的。

操作:那么我们就将S和P的颜色交。

影响:

结构:无变化

颜色:P的左子树路径黑色节点数量加一,P的右子树路径黑色节点数量不变

后续:状态修复退出。

 

 

状态五:

描述:S是黑色,S的左孩子是红的,S的右孩子是黑的。

操作:我们对S进行右旋,然后交换S和他的新父节点的颜色。

影响:

结构:由于进行了右旋所以结构变化很大,请直接看图。

颜色:就黑色节点而言没有任何一条路径有改变,但是,红色节点的位置发生了变化,N节点现在有一个有红色节点当右孩子的黑色兄弟。

后续:进入状态六。

 

 

状态六:

描述:S是黑色的,S的右节点是红色的。

操作:对P进行左旋,然后交换P和S的颜色,并将S的右孩子变成黑色

影响:

结构:由于进行了左旋所以结构变化很大,请直接看图。

颜色:S(左旋后)的右子树路径的黑色节点的数目不变,左子树路径的黑色节点增加了一个。

后续:状态修复退出。

 

 

这样来看的话我们一共有8条可能的路径来修复(假设1-6代表进行状态1-6对应的操作)

路径:

1(问题解决)

3(解决局部问题,向上传递到状态1)

2,4(问题解决)

2,5,6(问题解决)

2,6(问题解决)

4(问题解决)

5,6(问题解决)

6(问题解决)

 

然后让我们来假设N是父节点的右孩子,那么那些操作和情况会发生变化呢?(下面说的不会变化指的是上面的文字描述不会有变化)

状态一,不会有变化。

状态二,对P节点的左旋会变成右旋,其他操作不变。

状态三,不会有变化。

状态四,不会有变化。

状态五,S的左孩子是黑色,S的右孩子是红色,对S进行左旋,其他不变。

状态六,S的左节点是红色,对P进行右旋,将S的左孩子染成黑色,其他不变。

 

所以我们在写判定条件和操作的时候要记得把N是左右孩子的情况都考虑进去,我们综合一下应该是下面这样。在M与C都是黑色的情况下

状态一:

描述:N是根节点。

操作:什么都不用做了,因为树空了。

状态二:

描述:S节点是红色。

操作:将P和S的颜色交换,然后如果N是P的左孩子则对P节点进行左旋。

                                         如果N是P的右孩子则对P节点进行右旋。

状态三:

描述:P、S、S的孩子都是黑色。

操作:我们直接把S染成红色。

状态四:

描述:S和S的孩子是黑色的,但是P是红的。

操作:那么我们就将S和P的颜色交。

状态五:

描述:S是黑色,S的左孩子是红的,S的右孩子是黑的,N是P的左孩子。

         或者

         S是黑色,S的左孩子是黑的,S的右孩子是红的,N是P的右孩子。

操作:N是P的左孩子我们对S进行右旋。

     或者

     N是P的右孩子我们对S进行左旋。

         然后交换S和他的新父节点的颜色。

状态六:

描述:S是黑色的,S的右节点是红色的,N是P的左孩子。

         或者

        S是黑色的,S的左节点是红色的,N是P的右孩子。

操作:N是P的左孩子我们对P进行左旋,然后交换P和S的颜色,并将S的右孩子变成黑色。

     或者

     N是P的右孩子我们对P进行右旋,然后交换P和S的颜色,并将S的左孩子变成黑色。

        

 

现在让我们来集中解决一些常见问题。

问题一:情况三中S节点不会是NIL节点么?

答:不会,可以用假设法,假设S是NIL节点,那么为了保证每条路径上的黑色节点数相同,那么M节点必须是NIL节点,如果M是非NIL节点,则一定会导致这个非NIL节点的路径上的黑色节点数多于经过S节点的路径,而M在情况三中并不是NIL节点,M的两个孩子才是。

 

问题二:N节点是NIL节点,删除算法中把它当做普通节点对待没有问题么?

答:NIL节点和null值是不同的,NIL节点是一个空的节点,它也具有节点的结构,但是在这里NIL里有意义的数据只有color,其他的数据都是没有意义的,所以只要算法结束后红黑树的结构维持不涉及NIL节点中color以外的属性就没有问题。(如果你想用null来代替NIL节点实现算法也是可以的,但是很多细节需要有相应的修改,博主觉得还是用NIL节点比较方便)。

 

现在让我们来看代码实现,为了使代码更简洁我们再写两个辅助函数

 Node* getSibling(Node* n);//返回n的兄弟节点 

 bool isParentLeftChild(Node* n);//判断n是不是父节点的左孩子 

 1 bool MyRBT::isParentLeftChild(Node* n){
 2     if (n->parent == nil){
 3         cout << "isParentLeftChild error" << endl;
 4         exit(0);
 5     }
 6     if (n->parent->left == n) return true;
 7     else return false;
 8 }
 9 
10 Node* MyRBT::getSibling(Node* n){
11     if (n->parent == nil||n==nullptr) return nil;
12     if (n->parent->left == n)
13         return n->parent->right;
14     else return n->parent->left;
15 }

 

因为删除算法涉及到查找前驱和后继,所以这个代码也贴在这里

 1 Node* MyRBT::findSuccessor(int k){
 2     return findSuccessor(findByKey(k));
 3 }
 4 Node* MyRBT::findSuccessor(Node* n){
 5     if (n->right != nil){
 6         n = n->right;
 7         while (n->left != nil)
 8             n = n->left;
 9         return n;
10     }
11     while (n->parent != nil&&n->parent->right == n)
12         n = n->parent;
13     return n->parent;
14 }
15 
16 Node* MyRBT::findPredecessor(int k){
17     return findPredecessor(findByKey(k));
18 }
19 Node* MyRBT::findPredecessor(Node* n){
20     if (n->left != nil){
21         n = n->left;
22         while (n->right != nil)
23             n = n->right;
24         return n;
25     }
26     while (n->parent != nil&&n->parent->left == n)
27         n = n->parent;
28     return n->parent;
29 }

 

现在让我们来看真正的删除算法吧
 1 void MyRBT::deleteNode(int k){
 2     deleteNode(findByKey(k));
 3 }
 4 
 5 void MyRBT::deleteNode(Node* m){
 6     while(m->left != nil&&m->right != nil){//这个循环就是不断的找前驱(找后继也是可以的)来代替要被删除的节点,直达某个前驱最多只有一个非NIL节点的孩子
 7         Node* temp = findPredecessor(m);
 8         m->key = temp->key;
 9         m = temp;
10     }
11     if (m->color == red) //情况一
12         parentToN(m) = nil;
13     else if (m->left->color == red || m->right->color == red){//情况二
14         if (m->left != nil){
15             m->left->parent = m->parent;
16             parentToN(m) = m->left;
17             m->left->color = black;
18         }
19         else{
20             m->right->parent = m->parent;
21             parentToN(m) = m->right;
22             m->right->color = black;
23         }
24     }
25     else delete_fix_color(m);//进入最复杂的情况三
26     delete m;//处理完释放被删除的节点的空间
27     return;
28 }
29 
30 void MyRBT::delete_fix_color(Node* n){
31     Node* s = getSibling(n);
32     Node* p = n->parent;
33     parentToN(n) = nil;
34     nil->parent = n->parent;
35     n = nil;
36     if (n->parent == nil){//状态一
37         //什么都不用做
38     }
39     else if (p->color == black&&s->color == black&&s->left->color == black&&s->right->color == black){//状态三
40         s->color = red;
41         delete_fix_color(p);
42     }
43     else {
44         if (s->color == red){//状态二
45             s->color = black;
46             p->color = red;
47             if (isParentLeftChild(n)) left_rotate(p);
48             else right_rotate(p);
49         }
50         if (s->color == black&&s->left->color == black&&s->right->color == black&&p->color == red){//状态四
51             s->color = red;
52             p->color = black;
53             return;
54         }
55         else{//状态五
56             if (s->color == black&&s->left->color == red&&s->right->color == black&&isParentLeftChild(n)){
57                 right_rotate(s);
58                 s->color = red;
59                 s->parent->color = black;
60             }
61             else if (s->color == black&&s->left->color == black&&s->right->color == red && (!isParentLeftChild(n))){
62                 left_rotate(s);
63                 s->color = red;
64                 s->parent->color = black;
65             }
66             //状态六
67             if (s->color == black&&s->right->color == red&&isParentLeftChild(n)){
68                 left_rotate(p);
69                 s->color = p->color;
70                 p->color = black;
71                 s->right->color = black;
72             }
73             else if (s->color == black&&s->left->color == red && (!isParentLeftChild(n))){
74                 right_rotate(p);
75                 s->color = p->color;
76                 p->color = black;
77                 s->left->color = black;
78             }
79         }
80     }
81 }

 

删除算法的特点:
1,旋转操作不会超过三次。
2,插入算法的递归次数不会大于此节点的深度。
3,插入算法的最坏时间复杂度为O(lgn)

删除操作到此结束。

 

其他操作只读操作的代码我也贴在这里,供大家参考(详细的说明请参考二叉搜索树那一篇)
 
构造方法
1 MyRBT::MyRBT(vector<int> v):MyRBT(){
2     for (auto n : v){
3         insertNode(n);
4     }
5 }

通过key值查找特定节点

 1 Node* MyRBT::findByKey(int k){
 2     Node* temp = root;
 3     while (temp != nil){
 4         if (k == temp->key)
 5             return temp;
 6         temp = k < temp->key ? temp->left : temp->right;
 7     }
 8     cout << "can't find" << endl;
 9     return nullptr;
10 }

遍历算法

1 void MyRBT::traversal(Node* root, void(*f)(int k)){
2     if (root==nil)//当树为空的时候root为nullptr
3         return;
4     traversal(root->left);
5     cout << root->key << " " << root->color << endl;
6     traversal(root->right);
7 }

 

 
终于写完了,撒花❀❀❀,好长啊= =,如果觉得还是不够清楚可以看看wiki百科上的解释,链接在参考资料里,讲的很好,一定要看英文的,中文版的翻译的不全,省略了很多说明性的东西,反而更难懂了_(:зゝ∠)_。
祝各位学习愉快♪(^∇^*)~~~~。
 
 
 

参考资料:

1,《算法导论 中文版》(英文版第三版)(美)ThomasH.Cormen,CharlesE.Leiserson,RonaldL.Rivest,CliffordStein 著;王刚,邹恒明,殷建平,王宏志等译。

2,WIKI百科https://en.wikipedia.org/wiki/Red%E2%80%93black_tree

posted @ 2016-05-07 23:30  呆代待殆  阅读(495)  评论(0编辑  收藏  举报