红黑树详细讲解(多文整合)

红黑树是一种二叉查找树,优于平衡2叉树,其高度不会超过左右子树的2倍。本文整合了多个来源,详细讲解了红黑树的基本操作、插入、删除等,并提供了代码示例。红黑树在JDK的TreeMap及Linux内核调度中都有应用。插入时,新节点为红色,可能需要通过3种情况调整保持红黑树性质。删除时,根据节点颜色和子树情况,进行相应的旋转和颜色调整以保持平衡。
摘要由CSDN通过智能技术生成


本文是笔者在翻了网上各篇关于红黑树的文章,最终学懂红黑树后,将令笔者有所收获的几篇文章,整合在一起,并加上自身的修改和总结,希望能够让读者更容易理解红黑树。其中,基本操作、插入和部分简介转载自http://blog.chinaunix.net/uid-27767798-id-3339483.html,删除转载自http://blog.csdn.net/spch2008/article/details/9338923,部分简介转载自http://blog.csdn.net/eric491179912/article/details/6179908 。 代码中的红黑树类转载自一篇转载文,原作者不详。笔者已在OJ上通过了该代码。(笔者自己也写了个红黑树类并通过了OJ,但是没它详细,就贴了这篇 ^-^)
对原作者一并表示感谢!


1. 简介

红黑树是一种二叉查找树,它是在1972年由Rudolf Bayer发明的,它的性能优于平衡2叉树(avl树),因为avl树过分追求平衡,avl树要求任何节点的左右子树高度之差不能大于1,而红黑树做到的是任何节点的左右子树高度差不会超过2倍(左子树的高度不会大于右子树高度的2倍,或者右子树的高度不会大于左子树的高度的2倍),由此看出avl树如果要保持平衡需要付出更多的旋转(左旋,右旋),avl更平衡意味着avl树比红黑树的高度更低,查询时更快一些,但是过多旋转的时间代价大于查询带来的优势。红黑树的应用:jdk中的treeMap,内核中CFS调度根据vruntime(虚拟运行时间),来为进程建立红黑树结构,等等

红黑树满足以下5个性质:

  1. 每个结点的颜色只能是红色或黑色。
  2. 根结点是黑色的。
  3. 每个叶子结点都带有两个空的黑色结点(被称为黑哨兵),如果一个结点n的只有一个左孩子,那么n的右孩子是一个黑哨兵;如果结点n只有一个右孩子,那么n的左孩子是一个黑哨兵。
  4. 如果一个结点是红的,则它的两个儿子都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
  5. 对于每个结点来说,从该结点到其子孙叶结点的所有路径上包含相同数目的黑结点。

红黑树的这5个性质中,第3点是比较难理解的,但它却非常有必要。我们看图1中的左边这张图,如果不使用黑哨兵,它完全满足红黑树性质,结点50到两个叶结点8和叶结点82路径上的黑色结点数都为2个。但如果加入黑哨兵后(如图1右图中的小黑圆点),叶结点的个数变为8个黑哨兵,根结点50到这8个叶结点路径上的黑高度就不一样了,所以它并不是一棵红黑树。


2. 基本操作

2.1 基本旋转操作:

(调节平衡时会用到)

右旋操作围绕4节点旋转,代码如下:
 void rotateRight(node *target){    //如上图4节点就是参数target
     node *left=target->left;       //left节点是2节点
     node *parent=target->parent;   //parentNode是target的父节点
     if(parent!=NULL) { 
         left->parent= parent;      //如果父节点不为空,设置父节点的父子关系
         if(parent->left==target)
              parent->left=left;    //设置父节点到子节点的关系
         else
             parent->right=left;    //设置父节点到子节点的关系
       } 
      node *move=left->right;       //move节点代表3节点
      left->parent=target->parent;                               
      target->parent=left;          //设置target节点到新父节点2的关系
      left->right=target;           //设置left节点到target节点的关系
      if(move!=NULL){               //设置move节点(3节点的父子关系)
          target->left=move;        //target的左节点是3节点
          move->parent=target;      //3节点的父节点target节点
       }
      if(target==root)
         root=left;                 //如果旋转的节点是跟节点,需要更新跟节点引用
 }

void rotateLeft(node *target){        //如上图5节点就是参数target
    node * right =target->right;      //right节点是7节点
    node *parent=target->parent;      //parentNode是target的父节点
    if(parent!=NULL) { 
        right ->parent= parent;       //如果父节点不为空,设置父节点的父子关系
        if(parent-> right ==target)
             parent-> right = right;  //设置父节点到子节点的关系
        else
             parent->left= right;     //设置父节点到子节点的关系
     } 
     node *move= right ->left;        //move节点代表7节点
     right->parent=target->parent;
     target->parent= right;           //设置target节点到新父节点7的关系
     right ->left=target;             //设置left节点到target节点的关系
     if(move!=NULL){                  //设置move节点(6节点的父子关系)
        target->right=move;           //target的左节点是3节点
        move->parent=target;          //6节点的父节点target节点
      }
     if(target==root)
        root=right;                   //如果旋转的节点是跟节点,需要更新跟节点引用

 }

2.2 求节点的后继节点:

(节点删除时会用到)

node* successor(node *target){
   node* temp;
   if(target->right!=NULL){ //case1 当target节点有右孩子时,返回右子树中最小的节点,即7节点           
      temp=target->right; 
      while(temp->left!=NULL)
         temp=temp->left;
      return temp;
   }
   while(temp->parent!=NULL&&temp==temp->parent->right){ 
   //case 2 当左子树为空时,可以理解为比7节点 小的,但是小节点中最大的节点,这个节点就应该是7节点的左子树中最大的节点,即6节点。
      temp=temp->parent;
   }
   return temp->parent;
}

3. 插入

红黑树的节点的插入过程,和普通的二叉查找树的插入过程类似。只是每个节点多了一个color域,代表节点的颜色(红色,黑色),新插入的节点的颜色是红色的。每个节点插入之后需要看一下当前插入节点的parent节点是否为红色,如果为黑色,则2叉树继续保持红黑树性质4,5,如果为红色,破坏了红黑树性质4,这时需要调整一下节点节点的颜色。所以当插入节点的父节点为红色时,插入后的节点调整需要分为3个case:

case1:第一种情况的条件是uncle节点不为空,并且uncle节点为红色节点。target节点是parent节点的左孩子或者右孩子,插入target节点之前,会保证数据结构中没有相邻的红色节点,且到叶子节点的黑色数目相同,这时插入target节点,只需要把parent节点,uncle节点变成黑色,grand节点变为红色即可,这样把grand节点的黑色下降到了孩子节点上(parent,uncle),保持了没有相邻的红色节点,且到叶子节点黑色数目相同,但是这样把grand节点变成了红色,可能会影响grand的父节点的红黑树性质(如果grand->parent节点为红色),所以需要把target指针指向grand节点,继续递归下去。

case2:是个过渡阶段,目的是让target节点为parent节点的左孩子,这样在后面的右旋时,target节点才不会成为grand的左孩子,正确的做法是交换target和parent节点,然后左旋target节点,令target指针指向parent节点,并对它进行左旋,进入case3,结果如右图(注,右图中target节点就是左图中parent节点,右图中parent节点就是左图中target节点)。反之如果在case2中直接右旋grand节点,(目的是保持没有相邻的红色节点,同时黑色节点数量保持一致)会出现下面几种情况:

第一种情况错误的旋转,交换parent节点和grand结果的颜色,显然这样的结果违反了不能出现两个连续的红色节点的性质

第二种情况错误的旋转,交换uncle节点和parent节点的颜色,同时uncle节点为红色,这样会导致uncle左右子树可能出现连续两个红色节点,剩下的错误旋转情况都是显而易见的,不是黑色节点的个数多了就是违反了红色节点不能相邻。

case3:情况是插入的target节点是parent节点左孩子,或是右孩子通过case2的操作变成了左孩子,这种情况直接右旋grand节点,并且交换parent节点和grand节点的颜色即可,这种情况不用在递归parent节点的上层数据结构了因为从grand节点的父节点看到的子节点就是黑色的,case3转换完毕后子节点还是黑色的,并且左右子树黑节点的数量维持不变,所以这种情况不用递归父节点的数据结构了。

插入过程的最后需要将root节点置为黑色,这是因为,case1中有可能grand节点就是root节点,case1的最后将root置为了红色,这时root节点没有父节点了,而需要保持红黑树的性质,需将root节点置为黑色。

node* insert(node *parent,int data){
      if(parent->value>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值