红黑树的插入与删除(一)

    xiaoxiao2025-06-16  4

    红黑树定义

    红黑树作为同样提供O(log n)查询的树结构(其实准确点说不是log n,红黑树虽然是平衡树,但是没有AVL树那么平衡),通常用于关联数组的使用中,相对于AVL树,在插入和删除操作中能够以更少的调节操作来位置平衡,AVL树的平衡唯一参考标准就是其高度,在插入和删除节点时需要检查调整在搜索路径上造成的“影响”,可能需要多个调整步骤来维持树的平衡。

    红黑树的节点具有一个颜色属性,平衡是根据其定义的五个特性来维持的:

    1.节点颜色为红或者为黑

    2.根节点为黑

    3.叶子节点为黑(叶子作为结束标志,可以以空表示或者自定义实体节点作为结束标志,后面树中没有画出来)

    4.红色节点必须有两个黑色子节点(子节点可以为结束标志)

    5.任一节点出发到叶节点路径上包含相同数量的黑色节点

    提前预告:最重要的就是特性4,5,因为后续的插入节点操作主要依靠特性4来调整平衡,删除节点操作主要依靠特性5来调整平衡。整个后面的操作都是依赖这5条要求,所以一定要记熟了(下面会反复提,帮助记忆)。

    调整概念

    由这5个特性要求可以看出,红黑树的平衡调节操作有两种类型:

    1.节点变色

    2.子树旋转

    相对于AVL树,调节操作较少,能够提供良好的统计性能(插入最多旋转两次,删除最多旋转三次,节点变色不提)。因为保证的是更大范围上的平衡(颜色+高度),AVL树的平衡(高度),所以需要考虑的情况就相对比较多,不只是AVL树中的左旋,右旋。

    (1) 关于颜色要求,之前特性4说过,红不能与红相邻,意思就是红色节点父节点只能为黑,子节点只能为黑。也就是说红黑树中存在节点为红时,则父节点必存在且为黑,子节点若存在则必为黑,不存在也可,因为叶子节点也是黑色。

    (2) 关于高度要求,能够保证一条路径上的高度不能超过另一条路径高度的两倍。两倍的情况:一个为纯黑路线,一个为黑红交替路线,有图有真相:

    左边属于纯黑路线,两个节点,右边属于黑红交替路线,四个节点,叶子节点不计算在内。

    树的节点插入

    节点插入操作相对于删除操作来讲,考虑的情况比较少,所以操作比较简单

    情景演示

    在正式插入节点之前,先预演一下经常使用的变换,口说无凭,有图有真相

    从左往右是黑色上提,从右往左是黑色下方

    在树的局部子树中存在节点颜色不一致情况,如上图所示时,根据特性5,黑色和红色的向上变换或者向下变换,该子树对外呈现的结果都是满足特性5的。

    至于因为颜色变换导致特性4的冲突,在当前步骤不关心。

    节点插入分析

    在节点的插入时,将待插入的节点创建为红色节点,目的当然是避免在每一次插入都导致特性5不符,需要进行平衡调节。

    插入节点会遇到的情形

    情形1:当前判断的节点为根节点,节点直接将当前节点变色为黑即可。

    当前节点如果为新插入的节点,即树为空,此时直接变色为黑即可;如果是迭代调整后的当前节点为根节点,即已经“上诉”,将调整情况反馈到根节点,则此时只需要直接将节点颜色变为黑即可满足平衡。

    情形2:树不为空,待插入节点的父节点为黑,则直接插入,特性4不会不符,无需调整。

    情形3:树不为空,带插入节点的父节点为红,特性4不符:

    情形3.1:父节点为红,叔节点为红。因为父节点为红,则祖父节点为黑,此时情形如下图所示:

    f:父节点,u:叔节点,g:祖父节点,n:新插入节点

    此时的情形如上面提到的情景演示中的颜色变换场景,所以此时的调节操作为:

    将父节点和叔节点的颜色变为黑,祖父节点颜色变为红,将祖父节点作为新节点n,进行迭代平衡调节,则在新一轮的判断中,n节点属于根节点,直接颜色变为黑即可。

    所以在该情形中不需要考虑新插入节点n是左节点还是右节点。

    代码演示:

    叔节点为红色:

    /* check_case1解决问题,父节点红色,则祖父节点为黑色,叔节点为红色的情况 */ void check_case1(node t){//叔节点为红 node uncle=uncle(t); t.parent.color=false;//父节点变黑 uncle.color=false;//叔节点变黑 uncle.parent.color=true;//祖父节点变红 insert_check(uncle.parent);//以祖父节点作为新的检查点 }

    情形3.2:父节点为红,叔节点为叶子节点或者叔节点为黑色节点。如图所示:

    叔节点为叶子节点或者黑色节点的时候,为了保持平衡,解决特性4的冲突问题,由AVL树得来的经验可知,此时需要旋转了,因为节点包含颜色属性,所以在旋转时需要更改颜色。

    在旋转之前,先讲明该情形的由来。如果n为新插入节点,则由特性5知,叔节点u为叶子节点;如果n为由情形3.1中的祖父节点g更换而来的节点,则此时的叔节点u为黑色节点。

    不过既然叔节点u不为红色节点,则不需要考虑u到底是哪种情形,无关紧要。相比较与AVL树的节点结构,此时的节点包含了指向父节点的parent属性,而且红黑树的旋转操作属于局部操作,所以插入或者删除函数不必以递归的方式进行(不需要如此大的消耗运算)。

    代码演示:

    旋转操作:

    node left_rotate(node t){//以t为根节点,左旋 node par=t.parent; node t_rig=t.right; boolean l=false; if(par!=null){ l=check_for_side(t); } t.right=t_rig.left; if(t_rig.left!=null){ t_rig.left.parent=t; } t_rig.left=t; t.parent=t_rig; t_rig.parent=par; if(par!=null){ if(l){ par.left=t_rig; }else{ par.right=t_rig; } } if(t_rig.parent==null){ root=t_rig; } return t_rig; } node right_rotate(node t){//以t为根节点,右旋 node par=t.parent; node t_lef=t.left; boolean l=false; if(par!=null){ l=check_for_side(t); } t.left=t_lef.right; if(t_lef.right!=null){ t_lef.right.parent=t; } t_lef.right=t; t.parent=t_lef; t_lef.parent=par; if(par!=null){ if(l){ par.left=t_lef; }else{ par.right=t_lef; } } if(t_lef.parent==null){ root=t_lef; } return t_lef; } 情形3.2.1:n节点为f节点左孩子,f为g节点左孩子,或者n为f节点右孩子,f为g节点右孩子,即红色节点都在一根绳上,以左边为例,旋转操作如下:

    更改父节点f和祖父节点g的颜色,以g节点为根右旋,旋转操作在AVL树中已经说过,此时父节点f的右孩子没有画出来。

    情形3.2.2:n节点为f节点的左孩子,f节点为g节点的右孩子,或者n为f节点的右孩子,f为g节点的左孩子,即红色节点存在“拐弯”情景,以“右拐弯”为例,此时的旋转操作如下:

    该选择操作之后,以f节点为新的n节点,进行迭代平衡调节,则进入情形3.2.1所描述的场景。

    代码演示:

    叔节点不为红色:

    /* check_case2解决问题,父节点红色,则祖父节点为黑色,叔节点不为红色的情况 直接插入新节点不会出现这种情况,因为祖父节点到叔节点和到父节点情况,违背红黑树特性5 所以这种情况是由check_case1变化颜色导致的 */ void check_case2(node t){//叔节点为黑 if(t==t.parent.right&&t.parent==grandparent(t).left){//左右拐弯 t=left_rotate(t.parent);//以父节点为根左旋,将新的父节点返回 t.color=false; t.parent.color=true; t=right_rotate(t.parent);//以父节点为根右旋,将新的父节点返回 }else if(t==t.parent.left&&t.parent==grandparent(t).right){//右左拐弯 t=right_rotate(t.parent);//以父节点为根右旋,将新的父节点返回 t.color=false; t.parent.color=true; t=left_rotate(t.parent);//以父节点为根左旋,将新的父节点返回 }else if(t==t.parent.left&&t.parent==grandparent(t).left){//左边一条道 t.parent.color=false; t.parent.parent.color=true; t=right_rotate(grandparent(t));//以祖父节点为根右旋 }else if(t==t.parent.right&&t.parent==grandparent(t).right){ t.parent.color=false; t.parent.parent.color=true; t=left_rotate(grandparent(t));//以祖父节点为根左旋 } }

    这里其实正规来讲应该根据f,n先进行一次旋转,然后以f作为新的n节点,再进行一次平衡调节(这种写法才规范),不过这里顺便就直接两次旋转结束了。

    由以上插入过程可以看出来,插入节点最多进行两次旋转操作即可达成平衡状态,变色操作不计入。主要的操作都是通过对叔节点的判断来进行选择执行的。

    代码演示:

    插入节点:

    void insert_check(node t){//插入检查 if(t.parent==null){//如果问题已经上升到根节点,则就是check的最后一步 t.color=false;//下层都已经整理好,直接改根节点为黑色 root=t; }else{ if(t.parent.color){//父节点存在,且为红 if(uncle(t)!=null&&uncle(t).color){//如果叔节点存在且为红 check_case1(t);//叔节点为红 }else{ check_case2(t);//叔节点为黑 } } } }

    删除节点操作放在下一篇。

    转载请注明原文地址: https://ju.6miu.com/read-1300010.html
    最新回复(0)