算法导论学习---红黑树详解之插入(C语言实现)

前面我们学习二叉搜索树的时候发现在一些情况下其高度不是很均匀,甚至有时候会退化成一条长链,所以我们引用一些”平衡”的二叉搜索树。红黑树就是一种”平衡”的二叉搜索树,它通过在每个结点附加颜色位和路径上的一些约束条件可以保证在最坏的情况下基本动态集合操作的时间复杂度为O(nlgn).下面会总结红黑树的性质,然后分析红黑树的插入操作,并给出一份完整代码。

先给出红黑树的结点定义:

#define RED 1
#define BLACK 0

///红黑树结点定义,与普通的二叉树搜索树相比多了一个颜色域
typedef struct node
{
      int key,color; ///定义1为红,0为黑
      node *p,*left,*right;
      node(){
         color=BLACK; ///默认结点颜色为黑
      }
}*RBTree;

一.从红黑树的性质讲起

红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或BLACK。通过堆任何一条从根到叶子结点的简单路径上各个结点颜色进行约束,红黑树确保没有一条路径会比其它路径要长出两倍,因而是近似于平衡的。下面给出红黑树的五条”红黑性质”,这几条性质是后面插入和删除的基础。

1).每个结点或是红色或是黑色。

2).根结点是黑色的。

3).每个叶结点是黑色的

4).如果一个结点是红色的,那么它的两个子节点都是黑色的。

5).对于每个结点,从该结点出发到其所有后代叶结点的简单路径上均包含相同数目的黑结点。

我们设从某个结点x出发(不含该结点)到达一个叶结点的一条简单路径上的黑结点个数为该结点的黑高,记为bh(x)。显然根据性质5,每个结点的黑高都是唯一的。下面我们来证明:一棵含有n个内部结点的红黑树的高度至多为2lg(n+1).

先证明以任一结点x为根的子树中至少包含2^bh(x)-1个内部结点。我们采用数学归纳法,对x的黑高进行归纳。如果x的黑高高度为0即x为叶子结点,则显然正确。x的黑高为bh(x),则其两棵子树的黑高为bh(x)或bh(x)-1(取决与它们本身的颜色),则可归纳出对应的内部结点至少为2^(bh(x)-1)-1,即x包含的内部结点至少为2^(bh(x)-1)-1+2^(bh(x)-1)-1+1=2^bh(x)-1,因此得证。然后现在我们设含n个结点的树高为h,然后根据性质4,可以知道从根到叶结点(不包括根结点)的任一条简单路径上都至少有一半的结点为黑色。因此树的黑高至少为h/2;然后有上面的结论得n>=2^(h/2)-1.解得h<=2lg(n+1)。于是我们就证明了动态集合操作Search,Minimum,Successor等都可以在O(lgn)时间复杂度内完成了。

下面给一张红黑树的图片。

二.旋转操作

后面的插入和删除过程中都会有破坏红黑树性质的情况发送,为了维护这些性质,必须要改变树中某些结点的颜色和指针结构。指针结构的修改是通过旋转来完成的,旋转被分为左旋和右旋;在插入和删除操作中这两个操作会多次出现,我们先来分析一下这两个操作。

这两个操作对应的图像如下:

从图像上可以看出,左旋和右旋操作并不是很复杂。下面我们以左旋为例进行解析:如图我们假设结点x的右儿子为y。左旋的前提是:结点必须要有右儿子。左旋的结果是:y代替x,x成为y的左儿子,x成为y的左儿子,y的左儿子成为x的右儿子。下面是具体的代码实现:

///左旋:对具有任意具有右孩子的结点可以进行
///传入要选择的结点指针的引用
///旋转以后x的右孩子y取代了x,而x成为y的左孩子,y的左孩子成为x的右孩子
///下面的代码就是完成这三个部分
void LeftRotate(RBTree x)
{
      RBTree y=x->right; ///y表示x的右孩子
      x->right=y->left; ///第一步:将x的右孩子设为y的左孩子
      if(y->left!=Nul)
            y->left->p=x;

     y->p=x->p;  ///更改y的父结点为x的父结点
      if(x->p==Nul)    ///第二步:y替代x,需要分情况讨论
            rt=y; ///x原来是根结点,则设y为根结点
      else if(x==x->p->left)
           x->p->left=y;   ///更改y为x父结点的左孩子
      else
           x->p->right=y; ///更改y为x父结点的右孩子

      y->left=x; ///第三步:将x设为y的左孩子
      x->p=y;
}

右旋与左旋是类似的,代码如下:

///右旋:对任何具有左孩子的结点可以进行
///传入要右旋的结点指针的引用
///旋转以后结点x被左孩子y替换,x成为y的右儿子,y的右孩子成为x的左孩子
void RightRotate(RBTree x)
{
        RBTree y=x->left; ///y表示x的左孩子
        x->left=y->right; ///第一步:x的左孩子更改为y的右孩子
        if(y->right!=Nul)
            y->right->p=x;

        y->p=x->p;  ///更改y的父结点为x的父结点
        if(x->p==Nul) ///第二步:y替代x,需要分情况讨论
            rt=y;  ///x原来为根结点,指定y为新根结点
        else if(x==x->p->left)
            x->p->left=y;  ///更改x父结点的左孩子为y
        else
            x->p->right=y; ///更改x父结点的右孩子为y

       y->right=x; ///第三步:更改y的右结点为x
       x->p=y;
}

三. 插入详解

理解红黑树插入和删除操作的难点在于:在我们改变一个结点以后,产生的情况太多。相应的我们也就得到了基本的讨论方法:将所有的情况进行分类讨论,然后就可以理解代码为什么要这样写了。实际上只要把《算法导论》上的那些Case理解清楚,红黑树的操作也就不难懂了。下面应用分类讨论的思想来理解红黑树的插入操作。

首先下面我们先给出插入部分的代码:

///红黑树的插入
///RB插入函数与普通的BST的插入函数只是稍微有点不同
///我们将原来的null换成了Nul结点,然后对新加入的结点,染成红色
///然后调用RBInsertFixUp函数进行调整,使得红黑树的性质不被破坏
void RBInsert(int key)
{
       RBTree z=new node;
       z->color=RED;
       z->key=key;
       z->p=z->left=z->right=Nul;
       RBTree y=Nul;
       RBTree x=rt;
       while(x!=Nul) ///按照二叉搜索树的性质寻找z的插入点
       {
            y=x;
            if(z->key<x->key)
                x=x->left;
            else
                x=x->right;
       }
       z->p=y;
       if(y==Nul)///插入的是根节点
            rt=z;
       else if(z->key<y->key)
            y->left=z;
      else
           y->right=z;
      RBInsertFixUp(z); ///插入红色结点可能违反了红黑树的某些性质,调用调整函数进行调整
}

从代码上看,这里的插入和二叉搜索树的插入并没有什么太大的不同。只是在最后加了一个插入调整函数:RBInsertFixUp()。之所以要加入调整函数是因为我们插入一个结点并将其染成红色导致红黑树的某条性质被违反了;所以我们需要着重的讨论到底会违反那条性质,然后我们这么样在调整函数中就这种情况进行性质的恢复。

首先我们设插入的结点为z,插入以后z会被染成红色。如果z的父结点是黑色的,则不会违反任何性质,不需要调整。所以我们下面只需要讨论z的父结点为红色的情况。然后在这种情况下,只有性质4是肯定会被违反的,然后我们再按z的父结点是其祖父结点的左儿子还是右儿子分类(这两种情况没有本质性的区别,只是在编码上稍微有点不同而已),下面我们只讨论z的父结点是其祖父结点的左儿子的情况。然后我们设z的叔结点(即z父结点的兄弟结点)结点为y。在这些前提下我们再进行分类讨论:

1)情况1:y的颜色为红色

这时其祖父结点一定是黑色的(因为在插入这个结点之前红黑树没有性质被违反)。这种情况下,我们可以将z的父结点染成黑色,z的叔结点染成黑色,z的祖父结点染成红色;这样染色以后,z的祖父结点以下的子树肯定是合法的红黑树,但是z的祖父结点可能违反性质4了,相当于将z在树中上升了两层(z=z->p->p)。这种情况下如果新z的父结点为黑就会退出循环了(如果新z为根节点就一定会退出,这时候根节点有可能会被染成红色,所以在退出循环后需要再将根节点染成黑色)。

这种情况的示意图如下:

2):y的颜色为黑

在y的颜色为黑的情况下我们在按z是其父结点的左孩子还是右孩子进行分类

(1).z是其父结点的左孩子

这时我们可以通过将z的父结点染成黑色,z的祖父结点染成红色,然后对z的祖父结点进行一次右旋恢复性质4,并且不违反其它的性质(对于这一点我们只要自己画一下图就可以很清楚的看到了)。

(2).z是其父结点的右孩子

这种情况显然是可以通过将z指向z的父结点,然后对z进行一次左旋就可以变成情况(1)处理了。

这两种情况的示意图如下:其中图片上的情况二对应我们这里的(2),情况三即为我们这里的(1)

总的来讲只要y为黑,我们就可以退出循环了;上面的两种情况的划分不过是内部结构的一些小转变而已。

综上,其实红黑树的插入操作主要是违反了性质4,调整函数的过程就是分了两类情况对性质4进行调整的过程,并不是很复杂。上面给出的是z的父结点为其祖父结点左儿子的情况,其实对于右儿子的情况基本上是一样的(具体的见代码)。

还有一个令人困惑的地方在于红黑树的代码中大量的使用了z->p->p这种形式,但是这真的不会导致空指针异常吗?对此算法导论上进行了比较详细的论证。在这里我简单的说一下我的看法:首先,在插入根节点的时候,是不会进入循环中的,所以就不会引用z->p->p。然后在插入根节点以后,除根结点外每个结点的祖父结点都是一定存在的,所以第一次引用不会出问题。问题只有可能出现在z在树中上移的情况下(对应情况1),可能会上移到某个位置,而这个位置的父结点不存在父结点,这时就会导致空指针的危险了;但是我们要注意到,这个位置只有可能是根结点,而如果z移动到了根节点,那么就会因为根节点的父结点是黑色而退出循环了!根本就不会再出现z->p->p这种形式的引用。所以不需要担心z->p->p的引用会出现空指针异常。

下面给出红黑的的插入调整函数RBInsertFixUp()函数的代码,其对应我们上面讨论的三种情况:


///红黑树插入调整函数
///我们将插入结点染成红色,可能违反了性质4,所以要进行调整
///调整的过程其实就是根据不同的情况进行分类讨论,不断转换的过程
///最后转成可以通过染色和旋转恢复性质的情况
void RBInsertFixUp(RBTree z)
{
    ///在下面的代码中z结点总是违反性质4的那个结点
     while(z->p->color==RED) ///x是红色,它的父结点也是红色就说明性质4被违反,要持续调整
     {
          ///下面的过程按x->p是其祖父结点的左孩子还是右儿子进行分类讨论
           if(z->p==z->p->p->left) ///父结点是其祖父结点的左孩子
           {
                 RBTree y=z->p->p->right;  ///表示z的叔结点

                 ///下面按y的颜色进行分类讨论
                 if(y->color==RED)
                 {///如果y是红色并z的祖父结点一定是黑色的,这时我们通过下面的染色过程
                   ///在保证黑高不变的情况下(性质5),将z在树中上移两层,z=z->p->p
                        z->p->color=BLACK;
                        y->color=BLACK;
                        z->p->p->color=RED;
                        z=z->p->p;///如果上移到根节点或某个父结点不为红的结点就可以结束循环了
                 }
                 else   ///叔结点为黑色
                 { ///此时要根据z是其父结点的左孩子还是右孩子进行分类讨论
                   ///如果z是左孩子则可以直接可以通过染色和右旋来恢复性质
                   ///如果z是右孩子则可以先左旋来转成右孩子的情况

                      if(z==z->p->right)
                      {
                            z=z->p;
                            LeftRotate(z); ///直接左旋
                      }
                      ///重新染色,再右旋就可以恢复性质
                      z->p->color=BLACK;
                      z->p->p->color=RED;
                      RightRotate(z->p->p);
                 }
           }
           else///父结点是祖父结点的右孩子
           {
                  RBTree y=z->p->p->left;  ///叔结点
                  if(y->color==RED)
                  {
                         z->p->color=BLACK;
                         y->color=BLACK;
                         z->p->p->color=RED;
                         z=z->p->p;
                  }
                  else
                  {///右儿子的时候可以直接左旋,重新调色恢复性质
                   ///左儿子可以先右旋成右儿子再处理
                         if(z==z->p->left)
                         {
                              z=z->p;
                              RightRotate(z);
                         }
                         z->p->color=BLACK;
                         z->p->p->color=RED;
                         LeftRotate(z->p->p);
                  }
           }
     }
     ///将根节点染成黑色,是必要的步骤;处理两种情况
     ///1.第一次插入根结点被染成红色的情况
     ///2.和在上面的循环中根节点可能被染成红色的情况
     rt->color=BLACK;
}

下一篇:算法导论学习–红黑树详解之删除

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-05 04:45:35

算法导论学习---红黑树详解之插入(C语言实现)的相关文章

算法导论学习---红黑树具体解释之插入(C语言实现)

前面我们学习二叉搜索树的时候发如今一些情况下其高度不是非常均匀,甚至有时候会退化成一条长链,所以我们引用一些"平衡"的二叉搜索树.红黑树就是一种"平衡"的二叉搜索树,它通过在每一个结点附加颜色位和路径上的一些约束条件能够保证在最坏的情况下基本动态集合操作的时间复杂度为O(nlgn).以下会总结红黑树的性质,然后分析红黑树的插入操作,并给出一份完整代码. 先给出红黑树的结点定义: #define RED 1 #define BLACK 0 ///红黑树结点定义,与普通

【算法导论】红黑树详解之一(插入)

本文地址:http://blog.csdn.net/cyp331203/article/details/42677833 作者:苦_咖啡 欢迎转载,但转载请注明出处,否则将追究相应责任,谢谢!. 红黑树是建立在二叉查找树的基础之上的,关于二叉查找树可以参看[算法导论]二叉搜索树的插入和删除和[算法导论]二叉树的前中后序非递归遍历实现.对于高度为h的二叉查找树而言,它的SEARCH.INSERT.DELETE.MINIMUM.MAXIMUM等操作的时间复杂度均为O(h).所以在二叉查找树的高度较高

算法导论 之 红黑树 - 插入[C语言]

作者:邹祁峰 邮箱:[email protected] 博客:http://blog.csdn.net/qifengzou 日期:2013.12.24 21:00 转载请注明来自"祁峰"的CSDN博客 1 引言 在之前的博文中,本人对平衡二叉树的处理做了较详尽的分析,有兴趣的朋友可以参阅博文<算法导论 之 平衡二叉树 - 创建 插入 搜索 销毁>和<算法导论 之 平衡二叉树 - 删除>.平衡二叉树AVL是严格的平衡树,在增删结点时,其旋转操作的次数较多:而红黑树

算法导论之红黑树的学习

最近学习了二叉搜索树中的红黑树,感觉收获颇丰,在此写一篇文章小结一下学到的知识,顺便手写一下Java代码. 1.引言 先来讲讲什么是二叉搜索树,二叉搜索树有如下特点:他是以一颗二叉树(最多有两个子结点)来组织的,对于树中的某个节点,其左子树的所有元素均小于该节点,其右子树的元素均大于该节点.我们知道一颗有N个节点的二叉树的高度至少为lgN,然后在树上的操作都与其高度有关,因此限制树的高度就显得非常有必要.当一个二叉搜索树的高度是lgN时,在该树上的插入删除搜索等操作均为O(lgN)的时间复杂度,

数据结构-红黑树详解

介绍: 红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组. 它是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees).后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的"红黑树". 红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能. 它虽然

红黑树详解

前言 红黑树的起源,自然是二叉查找树了,这种树结构从根节点开始,左子节点小于它,右子节点大于它.每个节点都符合这个特性,所以易于查找,是一种很好的数据结构.但是它有一个问题,就是容易偏向某一侧,这样就像一个链表结构了,失去了树结构的优点,查找时间会变坏. 所以我们都希望树结构都是矮矮胖胖的,像这样: 而不是像这样: 在这种需求下,平衡树(AVL)的概念就应运而生了. 红黑树就是一种平衡树,它可以保证二叉树基本符合矮矮胖胖的结构,但是理解红黑树之前,必须先了解另一种树,叫2-3树,红黑树背后的逻辑

红黑树详解 原理 史上最强 精华

未经授权,不得私自转载,否则追究法律责任 联系作者[email protected]取得授权 转载请注明作者和出处 网上很多红黑树的讲解都没有分析清楚插入删除各种情况是怎么来的,他们大多把分析图画的很复杂,其实这些情况都是极其简单的,我这里重点推导各种情况是怎么来的,不解释各种情况怎么调整,因为结构很简单调整很容易,且网上很多.红黑树的精髓是明白各种情况是怎么来的,弄清楚这个才是真正掌握红黑树. 五个性质: 1.节点要么红,要么黑 2.根节点为黑 3.NIL(叶节点)为黑,这条没什么卵用 4.红

算法导论 学习资源

学习的过程会遇到些问题,发现了一些比较好的资源,每章都会看下别人写的总结,自己太懒了,先记录下别人写的吧,呵呵. 1  Tanky Woo的,每次差不多都看他的 <算法导论>学习总结 - 1.前言 <算法导论>学习总结 - 2.第一章 && 第二章 && 第三章 <算法导论>学习总结 - 3.第四章 && 第五章 <算法导论>学习总结 - 4.第六章(1) 堆排序 <算法导论>学习总结 - 5.第六

机器学习Spark Mllib算法源码及实战详解进阶与提高视频教程

38套大数据,云计算,架构,数据分析师,Hadoop,Spark,Storm,Kafka,人工智能,机器学习,深度学习,项目实战视频教程 视频课程包含: 38套大数据和人工智能精品高级课包含:大数据,云计算,架构,数据挖掘实战,实时推荐系统实战,电视收视率项目实战,实时流统计项目实战,离线电商分析项目实战,Spark大型项目实战用户分析,智能客户系统项目实战,Linux基础,Hadoop,Spark,Storm,Docker,Mapreduce,Kafka,Flume,OpenStack,Hiv