rbtree原理及应用--插入

9 篇文章 1 订阅

插入


参考结点为何要选择叔结点?



[
      整个红黑操作过程中,涉及到的结点包括:当前结点、兄弟结点、父结点、叔结点、祖父结点。
      当前结点即是新插入结点,新插入的结点被默认设置成红色。根据当前结点可以确定的条件只剩下一个了,就是当前结点是左孩子或右孩子。插入红色结点,不会影响性质5,但是可能会影响性质4、性质2。对于性质2,只有是插入结点是根结点的时候,才有影响。对于性质4,父结点是红色的时候,才会有影响。所以,以当前结点作为参考结点,其作为左右孩子并不会对红黑性质产生影响,进行处理的参考点递推到了父结点身上,故,当前结点不宜作为参考结点。
      那么以兄弟结点作为参考结点会有什么效果呢?首先,可影响的因素有:结点的位置和结点的颜色。对于结点颜色来说,若兄弟结点是红色,即与插入结点的颜色相同,不会对红黑性质产生影响,这一点用兄弟结点作为参考结点,没有疑问。但若兄弟结点是黑色,那能说明哪些红黑性质遭到破坏呢?如果不看父结点,这一点无法回答。若父结点是黑色,则没有红黑性质遭到破坏,若父结点是红色,则性质4遭到破坏。所以,从这一点来说,对于以兄弟结点作为参考结点的情形来说,还是归结到父结点的身上。结点位置的影响,就不用看了,因为兄弟结点已经无法独立判断出对红黑性质的影响了。
      前面两个似乎都归结到了父结点身上,那以父结点作为参考结点,真的可以吗?
      还是按照上面的内容来看,影响的因素还是两个:位置和颜色,而红黑树更看重颜色,仍旧先从颜色着手进行分析。若父结点是黑色,直接插入,没有红黑性质遭到破坏。若父结点是红色呢?很明显,性质4被破坏了。那么,怎么处理才能满足红黑性质呢?将插入结点着黑色?若插入结点存在兄弟结点(此时,该兄弟结点一定是黑色,否则,性质4得不到满足),对插入结点着黑色,没有问题。但,此时问题来了,对插入结点进行着色处理,并不单单依靠父结点的颜色判断,还需要判断兄弟结点(若存在的话)的颜色,这也就是说明,单单的父结点的颜色这一个内容,无法确定具体的操作,还得依赖于其他结点(本例中使用的是插入结点的兄弟结点)的颜色来进行辅助操作。所以,父结点也不能独立完成操作的逻辑分类工作。
      现在来看看祖父结点。
      仍旧是从颜色入手。祖父结点是红色,插入结点是红色,这个没有问题,因为父结点和叔结点(若存在的话)一定是黑色,也不会引起红黑性质的变化。但若祖父结点是黑色呢?父辈结点可以是红色,也可以是黑色的。如果在插入之前,父辈结点是黑色的,当前结点插入没有问题。分析到此为止,因为已经说明,单靠祖父结点的颜色并不能说明哪一条红黑性质被破坏了,还是得依赖于其他结点(父结点和叔结点)的颜色才能判断出来。
      那看来只有叔结点能够完成这项工作了,事实上,也正是如此。 
]

新插入点的颜色选择



目前没有答案



插入过程分析


情况1:z的叔结点y是红色的。
情况2:z的叔结点y是黑色的,且z是一个右孩子。
情况3:z的叔结点y是黑色的,且z是一个左孩子。


      为了理解RB-INSERT-FIXUP的工作过程,将代码分为三个主要的步骤。首先,要确定结点z被插入并着为红色后,红黑树性质中有哪些不能继续保持。其次,应分析第1~15行中while循环的总目标。最后,要分析while循环体中的三种情况,看看它们是如何完成目标的。图13-4给出一个范例,显示在一棵红黑树上,RB-INSERT-FIXUP的操作过程。
(a) 
 
(b) 
 
(c) 
 
(d) 
      图13-4 RB-INSERT-FIXUP操作。(a)插入后的结点z。由于z和它的父结点z.p都是红色的,所以违反了性质4。由于z的叔结点y是红色的,可以应用程序中的情况1。结点被重新着色,并且指针z沿树上升,所得的树如(b)所示。再一次z及其父结点又都为红色,但z的叔结点y是黑色的。因为z是z.p的右孩子,可以应用情况2。在执行1次左旋之后,所得结果树见(c)。现在,z是其父结点的左孩子,可以应用情况3。重新着色并执行一次右旋后得(d)中的树,它是一棵合法的红黑树。
在调用RB-INSERT-FIXUP操作时,哪些红黑性质可能会被破坏呢?性质1和性质3继续成立,因为新插入的红结点的两个子结点都是哨兵T.nil。性质5,即从一个指定结点开始的每条简单路径上的黑结点的个数都是相等的,也会成立,因为结点z代替了(黑色)哨兵,并且结点z本身是有哨兵孩子的红结点。这样看来,仅可能被破坏的就是性质2和性质4,即根结点需要为黑色以及一个红结点不能有红孩子。这两个性质可能被破坏是因为z被着成红色。如果z是根结点,则破坏了性质2;如果z的父结点是红结点,则破坏了性质4.γ图13-4(a)显示在插入结点之后性质4被破坏的情况。

      在调用RB-INSERT-FIXUP操作时,哪些红黑性质可能会被破坏呢?性质1和性质3继续成立,因为新插入的红结点的两个子结点都是哨兵T.nil。性质5,即从一个指定结点开始的每条简单路径上的黑结点的个数都是相等的,也会成立,因为结点z代替了(黑色)哨兵,并且结点z本身是有哨兵孩子的红结点。这样看来,仅可能被破坏的就是性质2和性质4,即根结点需要为黑色以及一个红结点不能有红孩子。这两个性质可能被破坏是因为z被着成红色。如果z是根结点,则破坏了性质2;如果z的父结点是红结点,则破坏了性质4.γ图13-4(a)显示在插入结点之后性质4被破坏的情况。
情况1:z的叔结点y是红色的。
[
      此时,如果祖父结点是红色,则不满足性质4(如果一个结点是红色,则它的两个子结点都必须是黑色),所以,祖父结点必须是黑色。如果父结点是黑色,则从祖父结点到父结点和叔结点的简单路径上的黑结点数量不同,所以,父结点必须是红色。所以,情况1发生时,父结点和叔结点必须是红色,祖父结点必须是黑色
]

      此时,若插入的结点是红色的,则在当前子树中,并不影响性质5,对于性质1、性质2和性质3也都满足,此时,唯一影响的就是性质4了。此时,对父结点、叔结点和祖父结点的颜色重新进行着色,则此时,性质4得以满足。
      但重新着色之后,可能会对性质2产生影响。在新结点为根结点时,性质2得到破坏。性质2破坏之后,将根结点颜色重新修改为黑色即可满足。
但是若不修改祖父结点的颜色,则由于修改了父结点的颜色,最终可能会导致曾祖父的性质5得不到满足,此时,修改祖父结点的颜色。将新结点移动到原新结点的祖父结点的位置,进入下一次循环。
      图13-5显示了情况1(第5~8行)的情形,这种情况在z.p和y都是红色时发生。因为z.p.p是黑色的,所以将z.p和y都着为黑色,以此解决z和z.p都是红色的问题,将z.p.p着为红色以保持性质5。然后,把z.p.p作为新结点z来重复while循环。指针z在树中上移两层。
(a)   
(b)   
      图13-5 过程RB-INSERT-FIXUP中的情况1。
      性质4被违反,因为z和它的父结点z.p都是红色的。无论z是一个右孩子(图(a))还是一个左孩子(图(b)),都同样处理。每一棵子树α、β、γ、δ和ε都有一个黑色根结点,而且具有相同的黑高。情况1的代码改变了某些结点的颜色,但保持了性质5:从一个结点向下到一个叶结点的所有简单路径都有相同数目的黑结点。while循环将结点z的祖父z.p.p作为新的z以继续迭代。现在性质4的破坏只可能发生在新的红色结点z和它的父结点之间,条件是如果父结点也是红色的。


情况2:z的叔结点y是黑色的,且z是一个右孩子。
情况3:z的叔结点y是黑色的,且z是一个左孩子。
[
      z的叔结点是黑色的,父结点是什么颜色呢?祖父结点是什么颜色呢?
      z的叔结点存在,说明祖父结点一定存在,否则,哪来的叔结点呢?
      下面来独立分析一下父结点和祖父结点的颜色。
      z的叔结点是黑色的,能断定父结点的颜色么?应该不能判定。若父结点是红色的,则z结点一定存在兄弟结点,并且,兄弟结点一定是黑色的。否则,祖父结点的性质5不会被满足。若父结点是黑色的,则父结点与叔结点的颜色相同,祖父结点的性质5没有受到影响,新加入的结点保持红色,对父结点、祖父结点的性质5均不会产生影响,此时,性质1—满足,性质2—不影响,性质3—不影响,性质4—满足。
      那,祖父结点是什么颜色呢?
      叔结点是黑色的,若父结点是红色的,则祖父结点必须是黑色的,否则,性质4将会被破坏。如果父结点是黑色的,则祖父结点没有限制,祖父结点可以是红色的,也可以是黑色的。
      那么,在插入过程中,会不会造成父结点、叔结点、祖父结点三个结点均是黑色的呢?完全可能的,下面的顺序插入234,0,28,32,在插入这4个结点后,在0结点的左右孩子处,在234结点的左孩子处插入一个新结点,就会是这种情况,而此时,只需要在目标位置插入结点即可,不需要其他额外的操作。也就是说,此时,情况2和情况3,可以认为是不满足的,因为循环可以直接退出,不需应用情况2和情况3的具体操作内容。
      对于父结点是黑色,祖父结点是红色,又需要操作什么呢?答案是,直接插入。因为插入的结点是红色结点,不会影响父结点和祖父结点的性质5的变化,对于性质4,也是不影响的。
      所以,具体来说,父结点是黑色的情形下,情况2和情况3是不需要进行处理的。
      那么,情况2和情况3处理的情形就只剩下父结点是红色的情况了。
      而父结点是红色,只能导致,祖父结点是黑色这一种情形,再加上性质5,z结点一定存在一个黑色的兄弟结点。
]

      情况2和情况3中,z的叔结点y是黑色的。通过z是z.p的右孩子还是左孩子来区别这两种情况。在情况2中,结点z是它的父结点的右孩子,可以立即使用一个左旋将此情形转变为情况3,此时结点z为左孩子。因为z和z.p都是红色的,所以,该旋转对结点的黑高和性质5都无影响。无论是直接进入情况2,还是通过情况3进入情况2,z的叔结点y总是黑色的,否则就要执行情况1。此外,结点z.p.p存在。在情况3中,改变某些结点的颜色并做一次右旋,以保持性质5。这样,由于在一行中不再有两个红色结点,所有的处理到此完毕。因为此时z.p是黑色的,所以无需再执行一次while循环。
 
      图13-6 过程RB-INSERT-FIXUP中的情况2和情况3。如同情况1,由于z和它的父结点z.p都是红色的,性质4在情况2或情况3中会被破坏。每一棵子树α、β、γ和δ都有一个黑色根结点(α、β、γ是由性质4而来,δ也有黑色根结点,因为否则将导致情况1),而且具有相同的黑高。通过左旋将情况2转变为情况3,以保持性质5:从一个结点向下到一个叶结点的所有简单路径都有相同数目的黑结点。情况3引起某些结点颜色的改变,以及一个同样为了保持性质5的右旋。然后while循环终止,因为性质4已经得到了满足:一行中不再有两个红色结点。


      综上分析,Case1做的动作就是染色,染父辈(包括父结点和叔结点)的颜色和祖父的颜色,不会进行旋转操作。
      Case2可以认为是Case3的中间状态,所以,Case2做的动作就是旋转,而且是左旋。
      Case3所做的动作稍微复杂一些,是右旋和染色,这两个步骤必须要做,但是没有前后依赖关系。如果先染色,即将父结点和祖父结点的颜色变更,则性质5被破坏,右旋,解决性质5的问题。若先右旋,性质5和性质4同时会被破坏,通过变更叔结点和父结点(此时是旋转之后的状态)的颜色,可以解决性质5的问题。


三种情况中的颜色操作分析

      仔细观察这三种情况,不难发现,在着色过程中,操作是尽可能的保持住叶结点的颜色为红色,对父结点和祖父结点的颜色进行重绘。这样做,可以最大可能的保持住祖父以外的其他结点的颜色满足红黑性质,将插入导致的红黑性质破坏尽可能地保持在当前结点到祖父结点的小局部范围内。
      保持插入结点颜色为红色,也就保持住性质5不会被破坏。反观其他性质,对于性质1,始终满足,对于性质2,不论是插入点颜色为红色或是黑色,均有可能在递归若干次之后遭到破坏,所以,插入点颜色为红色和黑色的效果,对于性质2来说,等价。对于性质3,同样也不会受到影响,对于性质4,如果插入点是黑色,不会遭到破坏,如果插入点是红色,可能会遭到破坏。但是对比性质4和性质5,应该选择插入点的颜色是红色还是黑色更好一些?


插入时间复杂度分析


代码执行分析


      插入代码的执行过程,参照linux-3.10.44中提供的功能进行。
      [K lib/rbtree.c]
385 
386 void rb_insert_color(struct rb_node *node, struct rb_root *root)
387 {
388         __rb_insert(node, root, dummy_rotate);
389 }
390 EXPORT_SYMBOL(rb_insert_color);
      rb_insert_color()方法主要完成了在红黑树root中对于插入node结点后的自平衡处理。其调用__rb_insert()方法完成这个功能。
         [K lib/rbtree.c]
 71 
 72 static __always_inline void
 73 __rb_insert(struct rb_node *node, struct rb_root *root,
 74             void (*augment_rotate)(struct rb_node *old, struct rb_node *new))
 75 {
 76         struct rb_node *parent = rb_red_parent(node), *gparent, *tmp;
 77 
 78         while (true) {
 79                 /*
 80                  * Loop invariant: node is red
 81                  *
 82                  * If there is a black parent, we are done.
 83                  * Otherwise, take some corrective action as we don't
 84                  * want a red root or two consecutive red nodes.
 85                  */
 86                 if (!parent) {
 87                         rb_set_parent_color(node, NULL, RB_BLACK);
 88                         break;
 89                 } else if (rb_is_black(parent))
 90                         break;   
 91
      在__rb_insert()中的主体是一个while循环,循环的终止条件在循环体内部。
      76行获得node结点的父结点。86行,就首先判断父结点是否存在,若父结点不存在,则当前node结点指向了树根部分。而对于树根的操作,做的动作就是将其设置成黑色,然后,循环终止。当结点指向树根的时候,红黑性质中,唯一能够遭到破坏的就只有性质2了。
      在89行,当父结点的颜色是黑色时,循环也终止了。这个比较容易理解,不看父结点一下做了哪些操作,从父结点开始,父结点和其兄弟结点一定满足上一次循环(或原始树)的祖父结点的性质5。因为父结点是黑色结点,祖父结点的颜色选择余地就大的多了,但是,仍旧满足性质4,否则,上一次插入时候生成的红黑树,性质4同样不会满足。其他的性质,就更显而易见了。
      接下来的代码中,首先获得祖父结点,然后根据当前父结点是否是祖父结点的左孩子进行分类处理。为什么要这么处理呢?
      [K lib/rbtree.c] 
 91 
 92                 gparent = rb_red_parent(parent);
 93 
 94                 tmp = gparent->rb_right;
 95                 if (parent != tmp) {    /* parent == gparent->rb_left */
……
159                 } else {
……
193                 }
      首先看看父结点是祖父的左孩子,叔结点保存在tmp中。
        [K lib/rbtree.c]
 95                 if (parent != tmp) {    /* parent == gparent->rb_left */
 96                         if (tmp && rb_is_red(tmp)) {
 97                                 /*
 98                                  * Case 1 - color flips
 99                                  *
100                                  *       G            g
101                                  *      / \          / \
102                                  *     p   u  -->   P   U
103                                  *    /            /
104                                  *   n            N
105                                  *
106                                  * However, since g's parent might be red, and
107                                  * 4) does not allow this, we need to recurse
108                                  * at g.
109                                  */
110                                 rb_set_parent_color(tmp, gparent, RB_BLACK);
111                                 rb_set_parent_color(parent, gparent, RB_BLACK);
112                                 node = gparent;
113                                 parent = rb_parent(node);
114                                 rb_set_parent_color(node, parent, RB_RED);
115                                 continue;
116                         }
117
      这里,首选检查是否满足情况1,即叔结点是否存在,并且叔结点是否是红色的。若是,则将父辈结点(父结点和叔结点)染成黑色,将祖父结点染成红色(注意,祖父结点染成红色后,可能会导致性质2被破坏,破坏的条件就是祖父结点是根结点,此时只需在下次循环的时候,将根结点设置成黑色即满足性质2了)。在染色的过程中,将结点指针上移了2个。终止本次循环。
      若不满足情形1,则检查情形2。注意,对于情形2来说,将其看做是情况3的一个中间状态,即,经过情况2处理之后,在按照情况3进行操作,此次红黑操作过程就应该结束了。
      [K lib/rbtree.c]
117 
118                         tmp = parent->rb_right;
119                         if (node == tmp) {
120                                 /*
121                                  * Case 2 - left rotate at parent
122                                  *
123                                  *      G             G
124                                  *     / \           / \
125                                  *    p   U  -->    n   U
126                                  *     \           /
127                                  *      n         p
128                                  *
129                                  * This still leaves us in violation of 4), the
130                                  * continuation into Case 3 will fix that.
131                                  */
132                                 parent->rb_right = tmp = node->rb_left;
133                                 node->rb_left = parent;
134                                 if (tmp)
135                                         rb_set_parent_color(tmp, parent,
136                                                             RB_BLACK);
137                                 rb_set_parent_color(parent, node, RB_RED);
138                                 augment_rotate(parent, node);
139                                 parent = node;
140                                 tmp = node->rb_right;
141                         }
142
      case1不满足时,才检查case2的情况。
      118行首先取出父结点的右孩子,若自己是父结点的右孩子,就满足case2的情形,否则,就是case3的情形。在case2中,132行和133行完成父结点-当前结点轴的“部分”左旋,此时,祖父结点的指针没有发生更改(有必要修改祖父结点的指针么?接下来的case3中的右旋同样需要对祖父进行操作,在后续一并修改就可以了)。
      接下来,修改parent和tmp的指针指向, parent指向了tmp结点的父结点(也就是执行case2左旋之前的node结点),在这段代码中,tmp被定义为当前结点的叔结点或兄弟结点,而在此时,由于在case2中执行了左旋动作,左旋之前的parent变成了此前node结点的左孩子,所以,tmp指向左旋之前node结点的右孩子。
        [K lib/rbtree.c]
142 
143                         /*
144                          * Case 3 - right rotate at gparent
145                          *
146                          *        G           P
147                          *       / \         / \
148                          *      p   U  -->  n   g
149                          *     /                 \
150                          *    n                   U
151                          */
152                         gparent->rb_left = tmp;  /* == parent->rb_right */
153                         parent->rb_right = gparent;
154                         if (tmp)
155                                 rb_set_parent_color(tmp, gparent, RB_BLACK);
156                         __rb_rotate_set_parents(gparent, parent, root, RB_RED);
157                         augment_rotate(gparent, parent);
158                         break;
      在case3中,以p-G为轴,完成右旋动作。
      首先,152行和153行完成这一右旋动作。在完成旋转之后,若p结点的右孩子不为空,则需要变更其父结点和颜色(那么,需要将该结点染成什么颜色呢?观察此前对case3的执行操作的分析发现,最终,需要将原来的祖父结点染成红色,由于性质4的限制,原祖父结点不能有红色的孩子,所以,此时只能将该结点染成黑色)。
      接下来,在156行调用__rb_rotate_set_parents()方法重新设置旋转之后的相关结点的颜色和父结点。
      [K lib/rbtree.c] 
 56 
 57 /*
 58  * Helper function for rotations:
 59  * - old's parent and color get assigned to new
 60  * - old gets assigned new as a parent and 'color' as a color.
 61  */
 62 static inline void
 63 __rb_rotate_set_parents(struct rb_node *old, struct rb_node *new,                                                                                     
 64                         struct rb_root *root, int color)
 65 {
 66         struct rb_node *parent = rb_parent(old);
 67         new->__rb_parent_color = old->__rb_parent_color;
 68         rb_set_parent_color(old, new, color);
 69         __rb_change_child(old, new, parent, root);
 70 }
 71
      注意,此时的参数中,new是old的父结点,操作之前的情况是:old是new的父结点。在此方法中,需要完成相关结点的链接重新设置和对应的颜色设置。66行取出执行case3操作之前的红黑树的祖父结点的父结点,由于在case3中执行了右旋动作,old结点的位置被new结点替代,所以,new结点的父结点和颜色只需要与old结点的父结点和颜色相同即可,67行完成了这个功能。在68行,调用rb_set_parent_color()方法,设置old结点的父结点和颜色:父结点当然是new结点了,而对于执行case3的插入来说,最终情况是将old结点设置成红色,所以,此时,color的值是红色。
      69行的__rb_change_child()方法定义在rbtree_augmented.h中,先看看这个inline方法的具体内容:
      [K include/linux/rbtree_augmented.h]
109 
110 static inline void
111 __rb_change_child(struct rb_node *old, struct rb_node *new,
112                   struct rb_node *parent, struct rb_root *root)
113 {
114         if (parent) {
115                 if (parent->rb_left == old)
116                         parent->rb_left = new;
117                 else
118                         parent->rb_right = new;
119         } else
120                 root->rb_node = new;
121 }
122
      这段代码的意图很明显,即在父结点的左右孩子关系上,old结点完全被new结点替代。
      回到lib/rbtree.c +69行的位置。经过__rb_rotate_set_parents()方法后,case3右旋后的着色和指针修改工作完全完成了。
      继续回退到lib/rbtree.c +158,这里只有一个break,必须break吗?在case3中,插入点的叔结点是黑色的,父结点是红色的,而在整个操作过程中,叔结点(最终,叔结点会变成当前结点的非直系后代)的颜色并没有发生变更,旋转之前,祖父-叔父轴上的黑色结点数目与旋转、着色之后的祖父-“叔父”轴上的黑色结点数目相同,即没有变化。另外一个颜色变化是旋转前自己的兄弟结点(原父结点的右孩子)变成了原祖父结点的左孩子,并对其进行着黑色操作(存在的情况下),注意一下原兄弟结点的位置,最终原兄弟结点是与原叔父结点做兄弟,而原叔结点的颜色是黑色,并且在整个case3操作过程中,原叔结点的颜色并没有发生变化,所以,原兄弟结点着黑色后,在这个子树中,并没有影响到其他性质的破坏,所以,此时,循环可以终止。


      对于循环体的另一半,也就是父结点是祖父的右孩子的情况,代码的主体逻辑是“一致”的,只不过存在一点“对称性”,将上面的左和右,在这部分中对称过去,即是这部分的处理过程。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值