关于红黑树(RBT)

本文内容主要是在看完wiki后写的,所以可以直接在wiki中找到关于红黑树的讲解,传送

这里不罗嗦了,直接开始。

首先,红黑树是一个满足如下属性的二叉查找树(不知道二叉查找树的朋友先去补一下相关知识):

  1. 每个节点非黑即红。
  2. 根节点为黑。有时可忽略这条规则,因为根节点总是可以由红变黑,但反过来可未必如此,这条规则的影响非常小。
  3. 所有叶子节点(NIL)都为黑。叶子节点无值。
  4. 如果一个节点为红,则它的两个子节点必为黑。
  5. 从一个给定节点到其任一子孙叶子节点(即为NIL的叶子节点,分清内部节点和叶子节点的概念)的所有路径上,所包含的黑色节点数都相同。

以上5条属性就是衡量红黑树的标准,包括插入,删除节点导致的红黑树重新平衡等。

黑色深度表示根节点到一个节点的黑色节点数;黑色高度表示从根节点到叶子节点的所有路径上的统一黑色节点数。

下图就是一个红黑树
来源:wiki

红黑树的结构和order为4的B树相似
在这里插入图片描述

关于后续操作的一些函数方法(C代码)

struct node* parent(struct node* n) {
 return n->parent; // NULL for root node
}

struct node* grandparent(struct node* n) {
 struct node* p = parent(n);
 if (p == NULL)
  return NULL; // No parent means no grandparent
 return parent(p); // NULL if parent is root
}

struct node* sibling(struct node* n) {
 struct node* p = parent(n);
 if (p == NULL)
  return NULL; // No parent means no sibling
 if (n == p->left)
  return p->right;
 else
  return p->left;
}

struct node* uncle(struct node* n) {
 struct node* p = parent(n);
 struct node* g = grandparent(n);
 if (g == NULL)
  return NULL; // No grandparent means no uncle
 return sibling(p);
}

void rotate_left(struct node* n) {
 struct node* nnew = n->right;
 struct node* p = parent(n);
 assert(nnew != LEAF); // since the leaves of a red-black tree are empty, they cannot become internal nodes
 n->right = nnew->left; 
 nnew->left = n;
 n->parent = nnew;
 // handle other child/parent pointers
 if (n->right != NULL)
  n->right->parent = n;
 if (p != NULL) // initially n could be the root
 {
  if (n == p->left)
   p->left = nnew;
  else if (n == p->right) // if (...) is excessive
   p->right = nnew;
 }
 nnew->parent = p;
}

void rotate_right(struct node* n) {
 struct node* nnew = n->left;
 struct node* p = parent(n);
 assert(nnew != LEAF); // since the leaves of a red-black tree are empty, they cannot become internal nodes
 n->left = nnew->right;
 nnew->right = n;
 n->parent = nnew;
 // handle other child/parent pointers
 if (n->left != NULL)
  n->left->parent = n;
 if (p != NULL) // initially n could be the root
 {
  if (n == p->left)
   p->left = nnew;
  else if (n == p->right) // if (...) is excessive
   p->right = nnew;
 }
 nnew->parent = p;
}

基本都是些常规操作(如左旋,右旋等)。

后续的图解中,使用N表示当前操作的节点;P表示N的父节点;G表示N的祖父节点;S表示N的兄弟节点;U表示N的叔伯节点(即PS节点)。三角形表示不确定深度子树;上面有黑色圆圈的三角形表示其黑色高度比不带的多1.

插入操作

struct node *insert(struct node* root, struct node* n) {
 // insert new node into the current tree
 insert_recurse(root, n);

 // repair the tree in case any of the red-black properties have been violated
 insert_repair_tree(n);

 // find the new root to return
 root = n;
 while (parent(root) != NULL)
  root = parent(root);
 return root;
}

void insert_recurse(struct node* root, struct node* n) {
 // recursively descend the tree until a leaf is found
 if (root != NULL && n->key < root->key) {
  if (root->left != LEAF) {
   insert_recurse(root->left, n);
   return;
  }
  else
   root->left = n;
 } else if (root != NULL) {
  if (root->right != LEAF){
   insert_recurse(root->right, n);
   return;
  }
  else
   root->right = n;
 }

 // insert new node n
 n->parent = root;
 n->left = LEAF;
 n->right = LEAF;
 n->color = RED;
}

insert_recurse方法为递归插入方法,插入节点最后一定是末端节点(有两个NIL叶子节点),和二叉查找树的插入相同;insert_recurse方法为调整插入后的树的方法,使其满足红黑树的条件,同样为递归方法,且最后插入节点的颜色一定是红色(为了满足第五条要求,所有路径的黑色节点数相同,所以插入红色节点时,一定满足该条件)。

接下来讨论插入后会出现的几种情况:

  1. N为根节点。
  2. N的父节点P为黑色。
  3. P为红色(即不是树的根节点),且U(叔伯节点)为红色。
  4. P为红色且U为黑色。
void insert_repair_tree(struct node* n) {
 if (parent(n) == NULL) {
  insert_case1(n);
 } else if (parent(n)->color == BLACK) {
  insert_case2(n);
 } else if (uncle(n)->color == RED) {
  insert_case3(n);
 } else {
  insert_case4(n);
 }
}
  • 要求1(节点非红即黑)和要求3(所有叶子节点为黑)永远满足。
  • 要求2(根节点为黑)在第一种情况中得到满足。
  • 要求4(红色节点只有黑色子节点)只有在插入红色节点时会被破坏,需要将节点由黑变红,或者进行旋转。(这句话可能现在有点难以理解,先往下看)
  • 要求5(所有路径下黑色节点数相同)只有在加入黑色节点时受到破坏,节点需要变色或旋转。(插入节点不是一定是红色吗?为什么会这么说呢?先往下看)
Case 1

N为根节点,直接变色为黑。

void insert_case1(struct node* n)
{
 if (parent(n) == NULL)
  n->color = BLACK;
}
Case 2

P为黑,则要求4(红色节点的子节点都为黑)并没有被破坏。要求5(所有路径黑色节点数都相同)也没有被破坏,因为现在N有两个黑色叶子节点,且N为红色,所以经过N的路径上并没有新增黑色节点数,在N插入前是1,插入后,两个叶子节点还是1.
如下图
在这里插入图片描述

Case 3

PU都为红色,则它们都被变为黑色,而G变为红色,这样才能满足要求5。因为经过P或者U的路径必定经过G,经过这样变色后,这些路径的黑色节点数没有发生变化。然而,如果G是根节点时不满足要求2,或者它的父节点为红色时不满足要求4,所以需要让G再执行insert_repair_tree方法。
此种情况图示如下
在这里插入图片描述
相关代码如下:

void insert_case3(struct node* n)
{
 parent(n)->color = BLACK;
 uncle(n)->color = BLACK;
 grandparent(n)->color = RED;
 insert_repair_tree(grandparent(n));
}
Case 4

P红,U黑。我们最终的目标是将当前插入的节点N旋转到祖父节点G的位置,但是如果NG子树的“内侧”时(NG左子树的右侧子节点或右子树的左侧子节点)无法直接一步旋转。故

第一步
如下图
在这里插入图片描述
P进行左旋(如果NG的右子树的左侧子结点时,则进行右旋)。旋转后会导致要求4被破坏,但要求5未被破坏。

void insert_case4(struct node* n)
{
 struct node* p = parent(n);
 struct node* g = grandparent(n);

 if (n == g->left->right) {
  rotate_left(p);
  n = n->left;
 } else if (n == g->right->left) {
  rotate_right(p);
  n = n->right; 
 }

 insert_case4step2(n);
}

第二步
当前节点NG子树的“外侧”(其实就是Case 4的第二种情况,我们上面第一步讨论的是第一种情况)。这种情况下G满足右旋条件,旋转后PNG的父节点。由于G为黑色,在不破坏要求4的情况下,P不能再为红色,因此为了满足要求4,PG的颜色互换。此时要求5依然满足,之前经过节点G的路径现在经过节点P

注意,如果是从第一步转移到这一步时,当前图示中的N为第一步中的PP为第一步中的N

图示如下
在这里插入图片描述
代码如下

void insert_case4step2(struct node* n)
{
 struct node* p = parent(n);
 struct node* g = grandparent(n);

 if (n == p->left)
  rotate_right(g);
 else
  rotate_left(g);
 p->color = BLACK;
 g->color = RED;
}

到此所有插入情况讨论完毕。在上述算法中,除了Case3会以G节点递归调用Case1,其他情况只会调用一次。由于每次修复树会向上两级,因此插入操作修复树的函数调用次数最大为h/2次。因为升级的概论随着每次迭代呈指数下降,所以平均插入成本实际上是恒定的。

删除操作

在常规二叉查找树的节点(有两个非叶子子节点)删除操作中,我们寻找该节点左子树的最大元素或者右子树的最小元素,并将该元素的值赋给该节点,并删除对应元素的节点,即可完成删除操作。而我们删除的那个元素节点,其必须拥有少于两个非叶子子节点。(在红黑树中,其末端节点必定有两个值为空的黑色子节点)
因为仅仅是复制值不会破坏红黑树的任何要求,所以这就减少了删除那种至多有一个非叶子子节点的节点的问题。一旦我们解决了这个问题,解决方法同样适用删除拥有至少有两个非叶子子节点的节点。

因此,剩下的部分,我们解决了删除最多有一个非叶子项的节点问题。我们用M表示要删除的节点;C将表示M的一个被选定的子节点,我们也称为“its child”。如果M确实有一个非叶子项,则将其称为C;否则,选择其中一个叶子作为其子项C

如果M为红色节点,我们只需将其替换为它的子项C,因为根据要求4,C必定为黑色。(这仅发生在M有两个叶子节点,如果M有一个非叶子项为黑色,则其替换掉M后会导致要求5被破坏,另一个路径的黑色子节点树+1)。由于通过被删除节点M的路径仅仅是少了一个红色节点,其父节点和子节点都必定是黑色,所以要求3和4不会被破坏。

另一种简单的情况是M为黑色,C为红色。仅是移除黑色节点会破坏要求4和5,但如果我们将C变为黑色,则不会破坏这些规则。

复杂的情况是MC都为黑色。(这只会发生在删除带有两个叶子项节点时,因为如果节点M有一个黑色非叶子项在一边,但另一边时叶子节点,则路径上的黑色节点数不同,破坏要求5)
我们开始用C替换MC就是M的其中一个叶子项)。我们将这个C标注为N,它的兄弟节点标注为S。(S为之前的M的兄弟节点)
在下图中,我们用P表示N的新父节点(M的父节点),SL表示S的左子项,SR表示S的右子项。(S不能为叶子节点,因为如果MC都是黑色,则P的一个包含M的子树拥有两个黑色高度,因此S的子树的黑色高度必须为2,所以S不能为叶子节点)

接下来,我们开始看代码实现。

void replace_node(struct node* n, struct node* child){
    child->parent = n->parent;
    if (n == n->parent->left)
        n->parent->left = child;
    else
        n->parent->right = child;
}

void delete_one_child(struct node* n)
{
 /*
  * Precondition: n has at most one non-leaf child.
  */
 struct node* child = is_leaf(n->right) ? n->left : n->right;

 replace_node(n, child);
 if (n->color == BLACK) {
  if (child->color == RED)
   child->color = BLACK;
  else
   delete_case1(child);
 }
 free(n);
}

replace_node用来将C替换掉M;我们讨论删除delete_one_child的前提是N必须有至少一个非叶子项。

如果N和它的父节点都是黑色,则删除父节点会导致经过N的路径的黑色节点数-1,破坏要求5,因此必须重新平衡。以下是我们需要考虑的几种情况:(以下讨论都是删除节点后进行重新平衡红黑树的算法)

Case 1

N是新的根节点,这种情况下,我们就完成了操作,我们在每个路径上都移除了一个黑色节点,并且新的根节点是黑色的。(这种情况就是,初始红黑树只有一个黑色根节点,然后删除了该根节点,N就是NIL)

void delete_case1(struct node* n)
{
 if (n->parent != NULL)
  delete_case2(n);
}

在case 2,5,6中,我们假定NP的左子项。

Case 2

S是红色的。这种情况下,我们交换PS的颜色,然后左旋P,让S成为N的祖父节点。注意P必须是拥有一个红色子项的黑色节点。因为结果子树有一个路径的黑色节点少1(经过N的那个路径,因为之前删除了一个黑色节点)所以我们还没有完成。现在N有一个黑色兄弟节点及一个红色父节点,满足接下来的4,5,6。之后的case中,我们将N的新兄弟节点标识为S

图示如下
在这里插入图片描述

代码如下:

void delete_case2(struct node* n)
{
 struct node* s = sibling(n);

 if (s->color == RED) {
  n->parent->color = RED;
  s->color = BLACK;
  if (n == n->parent->left)
   rotate_left(n->parent);
  else
   rotate_right(n->parent);
 }
 delete_case3(n);
}
Case 3

PSS的子节点都为黑色。这种情况下,我们只需将S变为红色。通过S的所有路径,这些路径之前都不通过N,都少了一个黑色节点。这样,通过P的所有路径的黑色节点数就都相同了,但是都比原来少1,所以需要对P进行重新平衡,进入case1的情况。

图示如下
在这里插入图片描述

代码如下:

void delete_case3(struct node* n)
{
 struct node* s = sibling(n);

 if ((n->parent->color == BLACK) &&
     (s->color == BLACK) &&
     (s->left->color == BLACK) &&
     (s->right->color == BLACK)) {
  s->color = RED;
  delete_case1(n->parent);
 } else
  delete_case4(n);
}
Case 4

SS的子节点都为黑,但P为红。这种情况,我们只需将SP的颜色交换。这不仅不影响通过S的路径,且通过N路径少1个黑色节点,也因为P的变色给补足回来了。

图示如下
在这里插入图片描述

代码如下:

void delete_case4(struct node* n)
{
 struct node* s = sibling(n);

 if ((n->parent->color == RED) &&
     (s->color == BLACK) &&
     (s->left->color == BLACK) &&
     (s->right->color == BLACK)) {
  s->color = RED;
  n->parent->color = BLACK;
 } else
  delete_case5(n);
}
Case 5

S是黑色,S的右子项为黑色,左子项为红色,并且N是它父节点的左子项。这种情况下,我们对S进行右旋,这样S的左子项成为S的父节点及N的兄弟节点。然后我们交换S和其新父节点的颜色。所有路径仍然拥有相同的黑色节点,但是现在N有一个其右子项为红色的黑色兄弟节点,可以让我们进行case 6。这次转换不会影响N及其父节点。

图示如下
在这里插入图片描述

代码如下:

void delete_case5(struct node* n)
{
 struct node* s = sibling(n);

 if  (s->color == BLACK) { /* this if statement is trivial,
due to case 2 (even though case 2 changed the sibling to a sibling's child,
the sibling's child can't be red, since no red parent can have a red child). */
/* the following statements just force the red to be on the left of the left of the parent,
   or right of the right, so case six will rotate correctly. */
  if ((n == n->parent->left) &&
      (s->right->color == BLACK) &&
      (s->left->color == RED)) { /* this last test is trivial too due to cases 2-4. */
   s->color = RED;
   s->left->color = BLACK;
   rotate_right(s);
  } else if ((n == n->parent->right) &&
             (s->left->color == BLACK) &&
             (s->right->color == RED)) {/* this last test is trivial too due to cases 2-4. */
   s->color = RED;
   s->right->color = BLACK;
   rotate_left(s);
  }
 }
 delete_case6(n);
}
Case 6

S是黑色的,S的右子项为红色,N是其父节点的左子项。这种情况下,我们对P进行左旋,这样S成为PS右子项的父节点。之后我们交换PS的颜色,使S的右子项变为黑色。子树在其根节点还是拥有相同的颜色,所以要求4和5没有被破坏。然而N新增了一个黑色祖先:不论P之前是否为黑色。这样经过N的路径新增了一个黑色节点,弥补了之前被删除的黑色M

同时,不经过N的路径有两种可能:

  1. 经过N的新兄弟节点SL,它是那个标号为3的子树。它一定经过SP,不论之前或现在,并且它们仅仅交换了颜色,所以它的路径黑色节点数不受影响。
  2. 经过N的新叔伯节点SR,之前它经过SS的父节点P,及S的右子项SR,但现在只经过S(颜色和之前的P相同),以及SR(颜色由红变黑),所以最后路径上的黑色节点数同样不受影响。

总之,这些路径上的黑色节点数不受影响。因此我们满足了要求4和5。白色节点的颜色既可以是红,也可以是黑。但必须保证转换前后颜色相同。

图示如下
在这里插入图片描述

代码如下:

void delete_case6(struct node* n)
{
 struct node* s = sibling(n);

 s->color = n->parent->color;
 n->parent->color = BLACK;

 if (n == n->parent->left) {
  s->right->color = BLACK;
  rotate_left(n->parent);
 } else {
  s->left->color = BLACK;
  rotate_right(n->parent);
 }
}

至此,我们讨论完了相关的删除节点的重排情况。上述算法中,除了case3 会递归其父节点到case1。所以case1的循环不会超过h。并且因为升级的概率随着每次迭代呈指数下降,所以平均移除成本是恒定的。

另外,子节点上不会发生尾递归,所以尾递归循环只能从一个孩子移回到它的后继祖先时发生。如果过在case 2中出现旋转(这是在case 1-3的循环内旋转的唯一可能性),之后N的父节点在旋转后变为红色并退出循环。因此,该循环中最多旋转一次。同时在退出循环后最多发生两次额外旋转(case 5,6),所以总计最多发生3次旋转。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值