5

linux学习22,linux内核中的红黑树是怎样设计和使用C语言实现的?

 3 years ago
source link: https://blog.popkx.com/linux-learning-22-how-is-the-red-black-tree-in-the-linux-kernel-designed-and-implemented-in-c-language/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上一节较为详细的讨论了普通二叉搜索树的局限性,在此基础上引出了红黑树的概念并介绍了其原理。在文章最后提到,为了维护一棵红黑树,在插入或者删除节点后,需要对二叉树做重着色和变换操作。那么,为什么要做重着色和变换操作呢?怎么做呢?本节将结合 linux 内核源代码讨论之。

3dcdb3f8f5f0b900ac7be4991d65fe19.png

为什么红黑树插入或者删除节点时,要重新调整树?

在回答这个问题之前,先来回顾一下红黑树的 5 条规则,要想保持一棵树始终是红黑树,就必须遵守这 5 条规则。

  1. 所有节点要么是红色,要么是黑色
  2. 根节点必须是黑色
  3. 所有叶子(NULL,or NIL)节点都是黑色的
  4. 红色节点的两个子节点必须是黑色(不能有连续的红色节点)
  5. 从根节点到任意叶子节点,黑色节点的数目是相等的

现在设想一下,如果往树中插入的节点是红色的,并且它的父节点也是红色的,那不就与规则4冲突了吗?可能你会说,那我把插入的节点染成黑色,不就没这个问题了吗?的确,这样就不会违反规则4,但是它可能会违反规则5!而对于从树中“删除节点”的操作,只要删除的节点是黑色,它就会违反规则5

721f8a1e9987ec6ea71e0948293ca151.png

因此,在往红黑树中插入新节点,或者从中删除节点时,非常有必要仔细检查一下是否需要对树做重着色和旋转变换操作。那么,如果需要,该怎么对树做“重着色”或者“旋转变换”呢?

先来看看往红黑树中插入新节点的情况

首先应该清楚,红黑树仍然是一棵二叉搜索树,所以往红黑树中插入新节点,把它当做一棵普通的二叉搜索树插入就可以了。然后再判断新树是否违反了红黑树的 5 条性质,是否需要做调整。

普通二叉搜索树节点的插入和删除操作,第 20 节文章已经介绍的比较清楚,感到陌生的朋友可以过去看看。

插入新节点后,首先要为新节点染色(规则1)。Linux 内核默认新节点是红色,为什么不是黑色呢?这是因为新节点是红色时,需要考虑的情况比较少,较容易作调整。

a79906c11a96b41c22030b5d24735cf8.png

我们假设插入的节点不是根节点,那么新插入的红色节点显然不会违反红黑树的规则1、规则2、规则3、规则5,唯一可能会违反的只有规则4

那么,如何作调整,才能确保插入新节点的二叉树仍然是一棵红黑树呢?来看看 Linux 内核源代码是怎么实现的吧。内核的红黑树时用到的数据结构的 C语言代码如下,请看:

    100 struct rb_node      
-   101 {                   
|   102     unsigned long  rb_parent_color;
|   103 #define RB_RED      0
|   104 #define RB_BLACK    1
|   105     struct rb_node *rb_right;
|   106     struct rb_node *rb_left;
|   107 } __attribute__((aligned(sizeof(long))));
    108     /* The alignment might seem pointless, but allegedly CRIS needs it */
    109             
    110 struct rb_root  
-   111 {               
|   112     struct rb_node *rb_node;
|   113 };
f45f6364ee03b7c8307a9c442dec7583.png

其中 rb_parent_color 成员不仅记录节点的颜色,也负责记录父节点地址,请看下面的 C语言代码:
    116 #define rb_parent(r)   ((struct rb_node *)((r)->rb_parent_color & ~3))
    117 #define rb_color(r)   ((r)->rb_parent_color & 1)
    118 #define rb_is_red(r)   (!rb_color(r))
    119 #define rb_is_black(r) rb_color(r)
-   120 #define rb_set_red(r)  do { (r)->rb_parent_color &= ~1; } while (0)
-   121 #define rb_set_black(r)  do { (r)->rb_parent_color |= 1; } while (0)
d62ab9dcee9de66de8028fa1e0a94726.png

显然,rb_parent_color 的最低位记录着节点的颜色,除低 2 位的其他位则负责记录父节点地址。Linux 内核在往红黑树插入新节点后,负责检查,以及做重着色和旋转变换的是 rb_insert_color() 函数。
c4ea5b41b280defa0bfb887c64ba477d.png

下面为了方便,将新插入的节点用 N 表示,它的父节点用 P 表示,祖父节点(父节点的父节点)用 G 表示,叔叔节点(祖父节点的另一个子节点)用 U 表示。P 可能是 G 的左儿子,也有可能是 G 的右儿子,但这两种情况是对称的,下面仅详细分析 P 是 G 的左儿子的情况。

当 U 是红色的时候,如下图(白色表示不关心节点的颜色):

041e77a9b23008cc978f0958f8ad9d9e.png

这时 N 违反了规则4,应想办法解决之。内核的解决办法是:把 U 和 P 染成黑色,再把 G 染成红色。这样虽然在局部解决了问题,但是如果把 G 看作是新插入的节点,它可能也会违反规则,所以要返回到程序开头,重新处理。请看下面的C语言代码:
72ea13742bb969bc8438c3b921eb67df.png

继续分析,如果 U 是黑色并且 N 是 P 的右儿子,如下图:

0b351d274aca3efbe2809e33c5207178.png

这时就没法简单的交换颜色解决冲突了,内核采取的解决办法是先以 P 为指点做左旋转,请看下图C语言代码:
faf2f7ee74a1b6277c978f0d31fff610.png

然后再将 P 染成黑色,G 染成红色,最后再以 G 为支点做右旋转,就解决了所有冲突,如下图,现在红黑树就仍然是一棵红黑树了。
e662fe90762bdc921ba872b19f1e6d01.png

关于“左旋转”和“右旋转”可参考上一节的文章。

需要说明的是,这些操作步骤都是递归的,解决问题的关键思路就是先解决局部问题,让局部称为一棵红黑树,把违反规则的部分逐层往上推,一直推到根节点。到根节点就好办了,直接把它染成黑色即可。
ecf3033e3d303379f20c01427b710260.png

从红黑树删除一个节点,Linux 内核如何做调整,以继续维持一棵红黑树呢?

要从红黑树删除节点,首先仍然是把它当做一棵普通的二叉搜索树。在第 20 节我们详细讨论过,如果要删除的节点至多只有一个子树,那么直接把它的子树挂到它的父节点上就可以了。

如果要删除的节点有两个子树呢?Linux 内核采取的方法和第 20 节介绍的方法是类似的:都是选择一个替代节点。替代节点要么在要删除节点的左子树的最右边,要么在要删除节点右子树的最左边,无论如何,替代节点都至多只有一个子树。这种情况下,使用替代节点的值替换要删除节点的值,实际上,真正要删除的是“替代节点”。

这就清楚了,当从红黑树中删除节点时,Linux 内核采取的方法巧妙的保证了要删除节点至多只有一个子树。为了方便,我们仍然把要删除节点用 N 表示,它的兄弟节点(父节点的另一个子节点)用O表示,父节点用 P 表示,祖父节点用 G 表示。

如果 N 是黑色,那么它显然很可能至少会违反红黑树的规则5。那么怎么解决这种冲突呢?来看看 Linux 内核源代码是怎么解决的。内核中负责检查,以及调整删除节点后的红黑树的是 __rb_erase_color() 函数,它的C语言代码如下:

7a59a1321f8ce0479da0ff2744a5a9be.png

和插入新节点一样,N 可能是 P 的左儿子,也有可能是 P 的右儿子,因为这两种情况完全对称,所以这里只详细分析 N 是 P 左儿子的情况。

如果 O 是红色的,那么 P 必定是黑色的,如下图:

565ec9518840b31bbd003824ad4633f6.png

这种情况 Linux 内核先将 O 染成黑色,然后将 P 染成红色,再以 P 为支点坐旋转,请看C语言代码如下:
9f54b4a2dd3d411fa43c7ca7094ebe61.png

现在 O-P 这条线仍然少了一个黑色节点(违反规则5),所以需要继续变换。

如果 O 的两个儿子都是黑色,如下图:

8d8434cd1d8744cd372a2b8661c7b6e6.png

直接将 O 染成红色,P 染成黑色,就解决了问题。

否则,如果 O 的右儿子是黑色,左儿子时红色,如下图:

25e878feb0105f575359840cded16f91.png

内核首先把 O 的左儿子染成黑色,然后把 O 染成红色,再以 O 为支点右旋,接着把 P 的右儿子当作 O,继续进一步操作。请看C语言代码如下:
a392ab48ca73f2b1a096988a07d4df46.png

接着,内核把 O 染成 P 的颜色,把 P 和 O 的右儿子染成黑色,在以 P 为支点左旋。这样就解决了所有冲突,二叉树就又是红黑树了。

至此,我们就分析完 Linux 内核关于红黑树调整部分的源代码了。可以看出,维护一棵红黑树其实并不是特别难,往树中插入新节点,或者从树中删除节点,完全可以先把其当作是一棵普通的二叉搜索树,之后再重新调整就可以了。这一过程是不是很像魔方?插入和删除操作可能会将魔方打乱,不过之后再变换,就又能把魔方还原了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK