红黑树原理和实现(1)

来源:互联网 发布:java线程数量 编辑:程序博客网 时间:2024/06/13 23:38
       曾经看Robert Love写的《Linux Kernel Development》第三版中曾提到过Linux内核的程序调度算法是用红黑树(Red-Black Tree,下文简称RBT)实现的,当时找了很多关于RBT的东西,包括博客,数据结构和算法书籍甚至动态图等。由于念书的时候念的不是计算机相关专业,没有数据结构特别是树的基础,根本看不懂。工作两年多之后,由于工作需要接触了nginx,其中的超时部分也是用RBT实现的。所以觉得有必要把它弄明白。为此写博客记下自己的学习过程,以后忘了好马上找到!

       要理解RBT首先要理解二叉查找树(Binary Search Tree,下文简称BST),因为RBT也是BST。BST的基本操作包括插入操作,删除操作,遍历,查找,后继结点查找,前驱结点查找(一般BST删除操作中如果该结点有两个子结点,那么一般找它的后继结点,这是默认做法,当然也可以找它的前驱结点;注意,这里的前驱结点和后继结点是以中序遍历的结点顺序来说的)和移植(transplant)操作。RBT在这些操作的基础上还要加上左旋(left rotate)和右旋(right rotate)操作,这些操作是在进行插入和删除操作(与BST的一样)后进行修正的操作,以维持RBT的性质。下面首先简要介绍BST的一些知识。

一 二叉搜索树(Binary Search Tree)

       BST又称二叉排序树(Sorted(Ordered) Binary Tree),它具有如下的性质:从根结点起,其左子树的所有结点的值(Key)都小于它的值,其右子树的所有结点的值都大于它的值,并且对于子树中的任意非叶子结点也有同样的性质。BST示意图如图1所示,来自维基百科。

图 1 BST示意图

       下面给出我自己参考《Introduction To Algorithms》第三版伪代码后自己实现的代码(C语言实现,我以前听人说数据结构用C++实现是首选,当时不理解,但是看过BST和RBT之后我觉得C++实现是比C更加方便。大概思路:RBT类继承BST类,再添加额外的几个操作,使用模板处理不同结构类型。但是C++基础不太扎实,就没用C++实现)。首先给出头文件bst.h,如下所示:

#ifndef __BST_H__#define __BST_H__typedef struct __bst_node {int key;struct __bst_node *parent;struct __bst_node *left;struct __bst_node *right;}bst_node;bst_node *bst_minimum(bst_node *node);bst_node *bst_maximun(bst_node *node);bst_node *bst_search(bst_node *root, int key);bst_node *bst_transplant(bst_node **root, bst_node *u, bst_node *v);int bst_insert(bst_node **root, int key);void bst_delete(bst_node **root, bst_node *del);void bst_preorder_walk(bst_node *root);void bst_inorder_walk(bst_node *root);void bst_postorder_walk(bst_node *root);#endif

1.1 插入操作

       插入操作是就是插入叶子结点(除了根结点之外):

int bst_insert(bst_node **root, int key){bst_node *ptr = NULL, *new = NULL;bst_node *x = *root;new = (bst_node *)malloc(sizeof(bst_node));if (new == NULL) {printf("malloc error!\n");return -1;}new->key = key;new->parent = NULL;new->left = NULL;new->right = NULL;while (x != NULL) {ptr = x;if (new->key < x->key)x = x->left;elsex = x->right;}// rootif (ptr == NULL)*root = new;else {new->parent = ptr;if (new->key < ptr->key)ptr->left = new;elseptr->right = new;}return 0;}
       简要说明一下:每个结点的结构体包括整形变量key,parent指针,left指针和right指针。

       由于根结点最开始指向NULL,所以采用指针的指针的形式(单指针不能回传地址修改,详见我的博客中C语言指针总结的相关内容)。

       在函数体内分配新的结点的内存空间。

       如果是根结点,那么将它指向*root,否则与当前结点的值比较,如果小于当前结点的值,那么插入的结点应该在当前结点的左子树中,否则应该在右子树中。

       最后,当当前结点已经是叶子结点,那么就可以进行插入操作了,当插入结点的值比当前结点的值小,那么插入结点就作为当前结点的左子结点插入,否则作为当前结点的右子结点插入。

       注意:插入操作容易出错的地方是对parent指针,left指针和right指针的修改,包括插入结点的parent指针修改为指向已存在树的相关结点和已存在树的相关结点的left指针或者right指针修改为指向插入结点。如下图所示:

图2 BST插入结点示意图

       上图是根据图1画出的,原图中没有画出指针的指向关系,这里画出是为了便于理解实现的细节。

1.2 删除操作

       删除操作相对于插入操作来说复杂一些,删除操作分为三种情况:

       <1>要删除的结点没有子结点,即它为叶子结点或者根结点,只需删除即可。

       <2>要删除的结点只有一个子结点,首先用它的子结点替换它,然后再将其删除。

       <3>要删除的结点有两个子结点,这时为了维持BST的性质,首先要从其左子树中找到包含最大值的结点或者从其右子树中找到包含最小值的结点(一般用后者替换要删除的结点),然后替换它,最后将其删除。

       情况<1>是最简单的,情况<2>和<3>需要额外的一些操作,如移植(就是<2>和<3>中提到的替换)和查找子树中包含最大值(最小值)的结点。

1.2.1 查找右子树中包含最小值的结点,即后继结点(左子树中包含最大值的结点,即前驱结点略)

       假设某个结点存在右子树,那么该右子树中包含最小值的结点存在于该右子树的左子树,且为最左边的那个结点。如图1中的10是根节点在右子树中包含最小值的结点,为什么不是13,它“看起来才是最左边”的结点,因为它不在根节点的右子树的左子树中,而是在根结点的右子树的右子树中。实现代码如下:

bst_node *bst_minimum(bst_node *node){    while (node->left != NULL)        node = node->left;    return node;}

1.2.2 移植

       当要删除的结点有至少一个结点时,会涉及到移植。如下图所示,假设要移植的结点分别是10和14:

图3 BST移植结点示意图

       其中虚线是需要执行的操作,如果10的子结点是它的左孩子,操作也类似。实现代码如下,其中考虑了要移植的结点是根结点的情况:

bst_node *bst_transplant(bst_node **root, bst_node *u, bst_node *v){bst_node *d = NULL;// childif (u->parent == NULL) {//u is root itselfd = *root;*root = v;} else {d = u;if (u == u->parent->left)u->parent->left = v;elseu->parent->right = v;}// parentif (v != NULL)v->parent = u->parent;return d;}

       上面代码中的指针d的作用是返回指向被移植结点的地址,从下面的删除操作代码中可以看出来。删除操作的示意图如图4所示,当要删除的结点有两个子结点时,又分为两种情况:

       <1>要删除的结点的后继结点就是它的右孩子(为什么不是左孩子,因为我们选择的是后继结点,不是前驱结点)

       <2>要删除的结点的后继结点不是它的右孩子

                                                            (a)                                                                                                                                                              (b)

图4 BST删除结点示意图

       图4中(a)情况表示要删除的结点是8,它的后继结点就是它的右孩子,那么直接将它替换根结点,并且将原根结点的左孩子指向它的后继结点。

       图4中(b)情况表示要删除的结点是3,这里画得有点复杂,为了更全面表示删除操作,在图1的基础上添加了一个结点5,它是结点4的右孩子。结点4是结点3的后继结点。执行的操作如下:

       <1>移植结点5为结点6的左孩子,如图中红色虚线箭头所示,即结点5替换了结点4原来的位置

       <2>将结点3的右孩子“过继”给结点4,如图中蓝色虚线箭头所示

       <3>将结点4替换结点3,如图中绿色虚线箭头所示

       <4>将结点3的左孩子“过继”给结点4,如图中橙色虚线箭头所示

       下面给出删除结点的代码实现:

/* * note: when root is to be deleted, its pointer will be modified */void bst_delete(bst_node **root, bst_node *del){bst_node *y = NULL, *r = NULL;if (del->left == NULL) {r = bst_transplant(root, del, del->right);bst_node_free(&r);} else if (del->right == NULL) {r = bst_transplant(root, del, del->left);bst_node_free(&r);} else {// search for mini-key node in the right subtreey = bst_minimum(del->right);// mini-key node is not child of the to-be-deleted nodeif (y->parent != del) {/* * do not need to free y 'cause del will be replaced with it */bst_transplant(root, y, y->right);y->right = del->right; // move y to the to-be-deleted nodey->right->parent = y; // the original right child reparented to y}r = bst_transplant(root, del, y);y->left = del->left; // y's left child now is the original left childy->left->parent = y; // the original left child reparented to ybst_node_free(&r);}}

1.3 一些额外的操作

       从上面的代码中可以看出,上文中bst_transplant函数返回的结点有可能是要删除的结点,bst_node_free函数是释放结点操作,它的定义是:

static void bst_node_free(bst_node **node)                                                                                                              {    (*node)->parent = NULL;    (*node)->left = NULL;    (*node)->right = NULL;    free(*node);    *node = NULL;}

       最后给出BST中序遍历的代码作为BST算法实现的结束,前序遍历和后序遍历的代码略。下一篇将介绍RBT的算法实现,可以从下一篇RBT算法中实现中找到很多本文代码的影子,因为是用C实现的,所以代码重复的地方比较多。

void bst_inorder_walk(bst_node *root){    bst_node *y = root;         if (y != NULL) {        bst_inorder_walk(y->left);        printf("%d ", y->key);        bst_inorder_walk(y->right);    }   }


原创粉丝点击