数据结构之红黑树——BST的变种2

2024-05-04 00:48

本文主要是介绍数据结构之红黑树——BST的变种2,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


转自:http://hxraid.iteye.com/blog/611816

感谢原文作者的分享


红黑树的性质与定义

红黑树(red-black tree) 是一棵满足下述性质的二叉查找树:

1. 每一个结点要么是红色,要么是黑色。

2. 根结点是黑色的。

3. 所有叶子结点都是黑色的(实际上都是Null指针,下图用NIL表示)。叶子结点不包含任何关键字信息,所有查询关键字都在非终结点上。

4. 每个红色结点的两个子节点必须是黑色的。换句话说:从每个叶子到根的所有路径上不能有两个连续的红色结点

5. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点

 

 

黑深度 ——从某个结点x出发(不包括结点x本身)到叶结点(包括叶子结点)的路径上的黑结点个数,称为该结点x的黑深度,记为bd(x),根结点的黑深度就是该红黑树的黑深度。叶子结点的黑深度为0。比如:上图bd(13)=2,bd(8)=2,bd(1)=1

内部结点 —— 红黑树的非终结点

外部节点 —— 红黑树的叶子结点

 

红黑树相关定理

1. 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。

      根据上面的性质5我们知道上图的红黑树每条路径上都是3个黑结点。因此最短路径长度为2(没有红结点的路径)。再根据性质4(两个红结点不能相连)和性质1,2(叶子和根必须是黑结点)。那么我们可以得出:一条具有3个黑结点的路径上最多只能有2个红结点(红黑间隔存在)。也就是说黑深度为2(根结点也是黑色)的红黑树最长路径为4,最短路径为2。从这一点我们可以看出红黑树是 大致平衡的。 (当然比平衡二叉树要差一些,AVL的平衡因子最多为1)

 

2. 红黑树的树高(h)不大于两倍的红黑树的黑深度(bd),即h<=2bd

      根据定理1,我们不难说明这一点。bd是红黑树的最短路径长度。而可能的最长路径长度(树高的最大值)就是红黑相间的路径,等于2bd。因此h<=2bd。

 

3. 一棵拥有n个内部结点(不包括叶子结点)的红黑树的树高h<=2log(n+1)

      下面我们首先证明一颗有n个内部结点的红黑树满足n>=2^bd-1。这可以用数学归纳法证明,施归纳于树高h。当h=0时,这相当于是一个叶结点,黑高度bd为0,而内部结点数量n为0,此时0>=2^0-1成立。假设树高h<=t时,n>=2^bd-1成立,我们记一颗树高 为t+1的红黑树的根结点的左子树的内部结点数量为nl,右子树的内部结点数量为nr,记这两颗子树的黑高度为bd'(注意这两颗子树的黑高度必然一 样),显然这两颗子树的树高<=t,于是有nl>=2^bd'-1以及nr>=2^bd'-1,将这两个不等式相加有nl+nr>=2^(bd'+1)-2,将该不等式左右加1,得到n>=2^(bd'+1)-1,很显然bd'+1>=bd,于是前面的不等式可以 变为n>=2^bd-1,这样就证明了一颗有n个内部结点的红黑树满足n>=2^bd-1。

        在根据定理2,h<=2bd。即n>=2^(h/2)-1,那么h<=2log(n+1)

        从这里我们能够看出,红黑树的查找长度最多不超过2log(n+1),因此其查找时间复杂度也是O(log N)级别的。

 

红黑树的操作

 

因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的查找操作与普通二叉查找树上的查找操作相同。然而,在红黑树上进行插入操作和删除操作会导致不 再符合红黑树的性质。恢复红黑树的属性需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。 虽然插入和删除很复杂,但操作时间仍可以保持为 O(log n) 次 。

 

插入操作

我们首先以二叉查找树的方法增加节点并标记它为红色。  如果设为黑色,就会导致根到叶子的路径上有一条路上,多一个额外的黑节点,这个是很难调整的。但是设为红色节点后,可能会导致出现两个连续红色节点的冲突,那么可以通过颜色调换(color flips)和树旋转来调整。) 下面要进行什么操作取决于其他临近节点的颜色。同人类的家族树中一样,我们将使用术语叔父节点来指一个节点的父节点的兄弟节点。

 

假设新加入的结点为N,父亲结点为P,叔父结点为Ui(叔父结点就是一些列P的兄弟结点),祖父结点G(父亲结点P的父亲)。下面会给出每一种情况,我们将使用C示例代码来展示。通过下列函数,可以找到一个节点的叔父和祖父节点:  

C代码   收藏代码
  1. node grandparent(node n) {  
  2.      return n->parent->parent;  
  3.  }  
  4.    
  5. node uncle(node n) {  
  6.      if (n->parent == grandparent(n)->left)  
  7.          return grandparent(n)->right;  
  8.      else  
  9.          return grandparent(n)->left;  
  10. }  

 

情况1. 当前红黑树为空,新结点N位于树的根上,没有父结点。

 

       此时很简单,我们将直接插入一个黑结点N(满足性质2),其他情况下插入的N为红色(原因在前面提到了)。

C代码   收藏代码
  1. void insert_case1(node n) {  
  2.     if (n->parent == NULL)  
  3.         n->color = BLACK;  
  4.     else  
  5.         insert_case2(n); //插入情况2  
  6. }  

情况2. 新结点N的父结点P是黑色。

 

       在这种情况下,我们插入一个红色结点N(满足性质5)。

Java代码   收藏代码
  1. void insert_case2(node n) {  
  2.     if (n->parent->color == BLACK)  
  3.         return// 树仍旧有效  
  4.     else  
  5.         insert_case3(n); //插入情况3  
  6. }  

 

注意:在情况3,4,5下,我们假定新节点有祖父节点,因为父节点是红色;并且如果它是根,它就应当是黑色。所以新节点总有一个叔父节点,尽管在情形4和5下它可能是叶子。

 

情况3.如果父节点P和叔父节点U二者都是红色。

 

        如下图,因为新加入的N结点必须为红色,那么我们可以将父结点P(保证性质4),以及N的叔父结点U(保证性质5)重新绘制成黑色。如果此时祖父结点G是根,则结束变化。如果不是根,则祖父结点重绘为红色(保证性质5)。但是,G的父亲也可能是红色的,为了保证性质4。我们把G递归当做新加入的结点N在进行各种情况的重新检查。

      

C代码   收藏代码
  1. void insert_case3(node n) {  
  2.     if (uncle(n) != NULL && uncle(n)->color == RED) {  
  3.         n->parent->color = BLACK;  
  4.         uncle(n)->color = BLACK;  
  5.         grandparent(n)->color = RED;  
  6.         insert_case1(grandparent(n));  
  7.     }  
  8.     else  
  9.         insert_case4(n);  
  10. }  

 

注意:在情形4和5下,我们假定父节点P 是祖父结点G 的左子节点。如果它是右子节点,情形4和情形5中的左和右应当对调。

 

情况4. 父节点P是红色而叔父节点U是黑色或缺少; 另外,新节点N是其父节点P的右子节点,而父节点P又是祖父结点G的左子节点。

 

       如下图, 在这种情形下,我们进行一次左旋转调换新节点和其父节点的角色(与AVL树的左旋转相同); 这导致某些路径通过它们以前不通过的新节点N或父节点P中的一个,但是这两个节点都是红色的,所以性质5没有失效。但目前情况将违反性质4,所以接着,我们按下面的情况5继续处理以前的父节点P。

 

C代码   收藏代码
  1. void insert_case4(node n) {  
  2.      
  3.       if (n == n->parent->right && n->parent == grandparent(n)->left) {  
  4.         rotate_left(n->parent);  
  5.         n = n->left;  
  6.     } else if (n == n->parent->left && n->parent == grandparent(n)->right) {  
  7.         rotate_right(n->parent);  
  8.         n = n->right;  
  9.     }  
  10.     insert_case5(n)  
  11. }  
   

情况5. 父节点P是红色而叔父节点U 是黑色或缺少,新节点N 是其父节点的左子节点,而父节点P又是祖父结点的G的左子节点。

 

       如下图: 在这种情形下,我们进行针对祖父节点P 的一次右旋转; 在旋转产生的树中,以前的父节点P现在是新节点N和以前的祖父节点G 的父节点。我们知道以前的祖父节点G是黑色,否则父节点P就不可能是红色。我们切换以前的父节点P和祖父节点G的颜色,结果的树满足性质4[3]。性质 5[4]也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过祖父节点G ,现在它们都通过以前的父节点P。在各自的情形下,这都是三个节点中唯一的黑色节点。

         

C代码   收藏代码
  1. void insert_case5(node n) {  
  2.     n->parent->color = BLACK;  
  3.     grandparent(n)->color = RED;  
  4.     if (n == n->parent->left && n->parent == grandparent(n)->left) {  
  5.         rotate_right(grandparent(n));  
  6.     } else {  
  7.         /* Here, n == n->parent->right && n->parent == grandparent(n)->right */  
  8.         rotate_left(grandparent(n));  
  9.     }  
  10. }  
 

删除操作

 

如果需要删除的节点有两个儿子,那么问题可以被转化成删除另一个只有一个儿子的节点的问题(为了表述方便,这里所指的儿子,为非叶子节点的儿子)。 对于二叉查找树,在删除带有两个非叶子儿子的节点的时候,我们找到要么在它的左子树中的最大元素、要么在它的右子树中的最小元素,并把它的值转移到要删除 的节点中(如在这里所展示的那样)。我们接着删除我们从中复制出值的那个节点,它必定有少于两个非叶子的儿子。因为只是复制了一个值而不违反任何属性,这 就把问题简化为如何删除最多有一个儿子的节点的问题。它不关心这个节点是最初要删除的节点还是我们从中复制出值的那个节点。

 

在本文余下的部分中,我们只需要讨论删除只有一个儿子的节点(如果它两个儿子都为空,即均为叶子,我们任意将其中一个看作它的儿子)。如果我们删除一个红色节点,它的父亲和儿子一定是黑色的。所以我们可以简单的用它的黑色儿子替换它,并不会破坏属性3和4。通过被删除节点的所有路径只是少了一个红色 节点,这样可以继续保证属性5。另一种简单情况是在被删除节点是黑色而它的儿子是红色的时候。如果只是去除这个黑色节点,用它的红色儿子顶替上来的话,会 破坏属性4,但是如果我们重绘它的儿子为黑色,则曾经通过它的所有路径将通过它的黑色儿子,这样可以继续保持属性4。

 

需要进一步讨论的是在要删除的节点和它的儿子二者都是黑色的时候,这是一种复杂的情况。我们首先把要删除的节点替换为它的儿子。出于方便,称呼这个儿子为 N,称呼它的兄弟(它父亲的另一个儿子)为S。在下面的示意图中,我们还是使用P称呼N的父亲,SL称呼S的左儿子,SR称呼S的右儿子。我们将使用下述 函数找到兄弟节点:

C代码   收藏代码
  1. struct node * sibling(struct node *n)  
  2. {  
  3.         if (n == n->parent->left)  
  4.                 return n->parent->right;  
  5.         else  
  6.                 return n->parent->left;  
  7. }  

 我们可以使用下列代码进行上述的概要步骤,这里的函数 replace_node 替换 child 到 n 在树中的位置。出于方便,在本章节中的代码将假定空叶子被用不是 NULL 的实际节点对象来表示(在插入章节中的代码可以同任何一种表示一起工作)。

C代码   收藏代码
  1. void delete_one_child(struct node *n)  
  2. {  
  3.         /* 
  4.          * Precondition: n has at most one non-null child. 
  5.          */  
  6.         struct node *child = is_leaf(n->right) ? n->left : n->right;  
  7.    
  8.         replace_node(n, child);  
  9.         if (n->color == BLACK) {  
  10.                 if (child->color == RED)  
  11.                         child->color = BLACK;  
  12.                 else  
  13.                         delete_case1(child);  
  14.         }  
  15.         free(n);  
  16. }  

 如果 N 和它初始的父亲是黑色,则删除它的父亲导致通过 N 的路径都比不通过它的路径少了一个黑色节点。因为这违反了属性 4,树需要被重新平衡。有几种情况需要考虑:

 

情况1. N 是新的根。

        在这种情况下,我们就做完了。我们从所有路径去除了一个黑色节点,而新根是黑色的,所以属性都保持着。

C代码   收藏代码
  1. void delete_case1(struct node *n)  
  2. {  
  3.         if (n->parent != NULL)  
  4.                 delete_case2(n);  
  5. }  

 

注意: 在情况2、5和6下,我们假定 N 是它父亲的左儿子。如果它是右儿子,则在这些情况下的左和右应当对调。

 

情况2. S 是红色。

 

        在这种情况下我们在N的父亲上做左旋转,把红色兄弟转换成N的祖父。我们接着对调 N 的父亲和祖父的颜色。尽管所有的路径仍然有相同数目的黑色节点,现在 N 有了一个黑色的兄弟和一个红色的父亲,所以我们可以接下去按 4、5或6情况来处理。(它的新兄弟是黑色因为它是红色S的一个儿子。)

C代码   收藏代码
  1. void delete_case2(struct node *n)  
  2. {  
  3.         struct node *s = sibling(n);  
  4.    
  5.         if (s->color == RED) {  
  6.                 n->parent->color = RED;  
  7.                 s->color = BLACK;  
  8.                 if (n == n->parent->left)  
  9.                         rotate_left(n->parent);  
  10.                 else  
  11.                         rotate_right(n->parent);  
  12.         }  
  13.         delete_case3(n);  
  14. }  

 

情况 3: N 的父亲、S 和 S 的儿子都是黑色的。

 

       在这种情况下,我们简单的重绘 S 为红色。结果是通过S的所有路径, 它们就是以前不通过 N 的那些路径,都少了一个黑色节点。因为删除 N 的初始的父亲使通过 N 的所有路径少了一个黑色节点,这使事情都平衡了起来。但是,通过 P 的所有路径现在比不通过 P 的路径少了一个黑色节点,所以仍然违反属性4。要修正这个问题,我们要从情况 1 开始,在 P 上做重新平衡处理。

 、

C代码   收藏代码
  1. void delete_case3(struct node *n)  
  2. {  
  3.         struct node *s = sibling(n);  
  4.    
  5.         if ((n->parent->color == BLACK) &&  
  6.             (s->color == BLACK) &&  
  7.             (s->left->color == BLACK) &&  
  8.             (s->right->color == BLACK)) {  
  9.                 s->color = RED;  
  10.                 delete_case1(n->parent);  
  11.         } else  
  12.                 delete_case4(n);  
  13. }  

 

情况4. S 和 S 的儿子都是黑色,但是 N 的父亲是红色。

 

       在这种情况下,我们简单的交换 N 的兄弟和父亲的颜色。这不影响不通过 N 的路径的黑色节点的数目,但是它在通过 N 的路径上对黑色节点数目增加了一,添补了在这些路径上删除的黑色节点。

Java代码   收藏代码
  1. void delete_case4(struct node *n)  
  2. {  
  3.         struct node *s = sibling(n);  
  4.    
  5.         if ((n->parent->color == RED) &&  
  6.             (s->color == BLACK) &&  
  7.             (s->left->color == BLACK) &&  
  8.             (s->right->color == BLACK)) {  
  9.                 s->color = RED;  
  10.                 n->parent->color = BLACK;  
  11.         } else  
  12.                 delete_case5(n);  
  13. }  

 

情况5. S 是黑色,S 的左儿子是红色,S 的右儿子是黑色,而 N 是它父亲的左儿子。

 

      在这种情况下我们在 S 上做右旋转,这样 S 的左儿子成为 S 的父亲和 N 的新兄弟。我们接着交换 S 和它的新父亲的颜色。所有路径仍有同样数目的黑色节点,但是现在 N 有了一个右儿子是红色的黑色兄弟,所以我们进入了情况 6。N 和它的父亲都不受这个变换的影响。

C代码   收藏代码
  1. void delete_case5(struct node *n)  
  2. {  
  3.         struct node *s = sibling(n);  
  4.    
  5.         if  (s->color == BLACK)   
  6. /* this if statement is trivial, 
  7. due to Case 2 (even though Case two changed the sibling to a sibling's child, 
  8. the sibling's child can't be red, since no red parent can have a red child). */  
  9.   
  10. // the following statements just force the red to be on the left of the left of the parent,  
  11. // or right of the right, so case six will rotate correctly.  
  12.                 if ((n == n->parent->left) &&  
  13.                     (s->right->color == BLACK) &&  
  14.                     (s->left->color == RED)) { // this last test is trivial too due to cases 2-4.  
  15.                         s->color = RED;  
  16.                         s->left->color = BLACK;  
  17.                         rotate_right(s);  
  18.                 } else if ((n == n->parent->right) &&  
  19.                            (s->left->color == BLACK) &&  
  20.                            (s->right->color == RED)) {// this last test is trivial too due to cases 2-4.  
  21.                         s->color = RED;  
  22.                         s->right->color = BLACK;  
  23.                         rotate_left(s);  
  24.                 }  
  25.         }  
  26.         delete_case6(n);  
  27. }  

 

情况6. S 是黑色,S 的右儿子是红色,而 N 是它父亲的左儿子。

 

       在这种情况下我们在 N 的父亲上做左旋转,这样 S 成为 N 的父亲和 S 的右儿子的父亲。我们接着交换 N 的父亲和 S 的颜色,并使 S 的右儿子为黑色。子树在它的根上的仍是同样的颜色,所以属性 3 没有被违反。但是,N 现在增加了一个黑色祖先: 要么 N 的父亲变成黑色,要么它是黑色而 S 被增加为一个黑色祖父。所以,通过 N 的路径都增加了一个黑色节点。

       此时,如果一个路径不通过 N,则有两种可能性:

      它通过 N 的新兄弟。那么它以前和现在都必定通过 S 和 N 的父亲,而它们只是交换了颜色。所以路径保持了同样数目的黑色节点。 
      它通过 N 的新叔父,S 的右儿子。那么它以前通过 S、S 的父亲和 S 的右儿子,但是现在只通过 S,它被假定为它以前的父亲的颜色,和 S 的右儿子,它被从红色改变为黑色。合成效果是这个路径通过了同样数目的黑色节点。 
      在任何情况下,在这些路径上的黑色节点数目都没有改变。所以我们恢复了属性 4。在示意图中的白色节点可以是红色或黑色,但是在变换前后都必须指定相同的颜色。

C代码   收藏代码
  1. void delete_case6(struct node *n)  
  2. {  
  3.         struct node *s = sibling(n);  
  4.    
  5.         s->color = n->parent->color;  
  6.         n->parent->color = BLACK;  
  7.    
  8.         if (n == n->parent->left) {  
  9.                 s->right->color = BLACK;  
  10.                 rotate_left(n->parent);  
  11.         } else {  
  12.                 s->left->color = BLACK;  
  13.                 rotate_right(n->parent);  
  14.         }  
  15. }  

 

       同样的,函数调用都使用了尾部递归,所以算法是就地的。此外,在旋转之后不再做递归调用,所以进行了恒定数目(最多 3 次)的旋转。

 

 

红黑树的优势

 

红黑树能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。这一点是AVL所不具备的。

 

而且实际应用中,很多语言都实现了红黑树的数据结构。比如 TreeMap, TreeSet(Java )、 STL(C++)等。

 

这篇关于数据结构之红黑树——BST的变种2的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/958008

相关文章

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

《数据结构(C语言版)第二版》第八章-排序(8.3-交换排序、8.4-选择排序)

8.3 交换排序 8.3.1 冒泡排序 【算法特点】 (1) 稳定排序。 (2) 可用于链式存储结构。 (3) 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时, 此算法不宜采用。 #include <stdio.h>#include <stdlib.h>#define MAXSIZE 26typedef int KeyType;typedef char In

【408数据结构】散列 (哈希)知识点集合复习考点题目

苏泽  “弃工从研”的路上很孤独,于是我记下了些许笔记相伴,希望能够帮助到大家    知识点 1. 散列查找 散列查找是一种高效的查找方法,它通过散列函数将关键字映射到数组的一个位置,从而实现快速查找。这种方法的时间复杂度平均为(

浙大数据结构:树的定义与操作

四种遍历 #include<iostream>#include<queue>using namespace std;typedef struct treenode *BinTree;typedef BinTree position;typedef int ElementType;struct treenode{ElementType data;BinTree left;BinTre

Python 内置的一些数据结构

文章目录 1. 列表 (List)2. 元组 (Tuple)3. 字典 (Dictionary)4. 集合 (Set)5. 字符串 (String) Python 提供了几种内置的数据结构来存储和操作数据,每种都有其独特的特点和用途。下面是一些常用的数据结构及其简要说明: 1. 列表 (List) 列表是一种可变的有序集合,可以存放任意类型的数据。列表中的元素可以通过索

浙大数据结构:04-树7 二叉搜索树的操作集

这道题答案都在PPT上,所以先学会再写的话并不难。 1、BinTree Insert( BinTree BST, ElementType X ) 递归实现,小就进左子树,大就进右子树。 为空就新建结点插入。 BinTree Insert( BinTree BST, ElementType X ){if(!BST){BST=(BinTree)malloc(sizeof(struct TNo

【数据结构入门】排序算法之交换排序与归并排序

前言         在前一篇博客,我们学习了排序算法中的插入排序和选择排序,接下来我们将继续探索交换排序与归并排序,这两个排序都是重头戏,让我们接着往下看。  一、交换排序 1.1 冒泡排序 冒泡排序是一种简单的排序算法。 1.1.1 基本思想 它的基本思想是通过相邻元素的比较和交换,让较大的元素逐渐向右移动,从而将最大的元素移动到最右边。 动画演示: 1.1.2 具体步

数据结构:线性表的顺序存储

文章目录 🍊自我介绍🍊线性表的顺序存储介绍概述例子 🍊顺序表的存储类型设计设计思路类型设计 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以:点赞+关注+评论+收藏(一键四连)哦~ 🍊自我介绍   Hello,大家好,我是小珑也要变强(也是小珑),我是易编程·终身成长社群的一名“创始团队·嘉宾” 和“内容共创官” ,现在我来为大家介绍一下有关物联网-嵌入

[数据结构]队列之顺序队列的类模板实现

队列是一种限定存取位置的线性表,允许插入的一端叫做队尾(rear),允许删除的一端叫做队首(front)。 队列具有FIFO的性质 队列的存储表示也有两种方式:基于数组的,基于列表的。基于数组的叫做顺序队列,基于列表的叫做链式队列。 一下是基于动态数组的顺序队列的模板类的实现。 顺序队列的抽象基类如下所示:只提供了接口和显式的默认构造函数和析构函数,在派生类中调用。 #i