学习Android Binder机制的时候,看到“宿主进程使用一个红黑树来维护它内部所有的Binder实体对象”这句话。本着对数据结构极大的热情,所以我决定学习一下Linux内核中红黑树的实现,增加一下自己的逼格。
学习过程中看了很多博客,基本上全是照抄算法导论的伪代码,我先献上自己深深的鄙视,这种贴伪代码的同学估计很难真正的搞懂红黑树。OK,鄙视过后,我们也按照介绍红黑树的基本思路,先从二叉查找树开始show代码,讲解原理。
show代码之前,我先定义一下接下来会用到的节点定义:
由于红黑树本质上就是一棵二叉查找树,所以在了解红黑树之前,我们先来学习一下二叉查找树。二叉查找树(Binary Search Tree),是指一棵空树或者具有下列性质的二叉树:
接下来,我们介绍一下二叉查找树的基本操作。
插入的过程如下:
实现代码如下:
void insert(struct node *root, const int value)
{
struct node *iNode = (struct node *)malloc(sizeof(struct node));
iNode->left = iNode->right = iNode->parent = NULL;
iNode->value = value;
if (root == NULL) {
root = iNode;
return;
}
struct node *p = root;
while (p) {
// 不插入已有的节点
if (value == p->value) {
return;
}
if (value < p->value) {
if (p->left == NULL) {
iNode->parent = p;
p->left = iNode;
break;
}
p = p->left;
} else {
if (p->right == NULL) {
iNode->parent = p;
p->right = iNode;
}
p = p->right;
}
}
}
删除二叉查找树中一个节点的方法如下:
实现代码如下:
struct node* find_replace(struct node* node)
{
struct node *p = node;
if (p->left) {
p = p->left;
while (p->right) {
p = p->right;
}
} else {
p = p->right;
while (p->left) {
p = p->left;
}
}
return p;
}
void delete(struct node *root, const int value)
{
struct node *p = root;
while (p) {
if (p->value < value) {
p = p->right;
} else if (p->value > value) {
p = p->left;
} else {
printf("Hit and Removed this value %d\n", value);
if (p->left == NULL && p->right == NULL) {
// p的左右孩子均为NULL
if (p->value > p->parent->value) {
p->parent->right = NULL;
} else {
p->parent->left = NULL;
}
} else {
struct node *tmp = find_replace(p);
p->value = tmp->value;
}
}
}
}
因为,一棵由n个节点,随机构造的二叉查找树的高度为logn,所以,一般二叉查找树的时间复杂度为O(logn)。但是,在最坏的情况下,例如二叉查找树只有左子树或者只有右子树的情况,这时,二叉查找树就退化为线性表,则操作的时间复杂度变为了O(n)。因此,红黑树是在二叉查找树的基础上,通过一些操作使得树相对平衡,不会出现最差时间复杂度的情况。
红黑树通过在二叉查找树的基础上增加着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(logn)。但是,它是如何保证一棵n个节点的红黑树的高度始终保持在h=logn的呢?
这就引出了红黑树的5条性质:
结合图示,解释一下上述性质。一棵参考的红黑树如下图所示:
通过图示,我们可以看到,节点只有红和黑两种颜色,并且根节点是黑色的。这就解释了性质1和2。
至于性质3,可以看到6这个节点的两个nil节点都是黑色的,其他节点的nil节点我为了省事,就不画了。
至于性质5,我们可以看到:13->8->11和13->17->25->27有相同的黑色节点数目,均为3。
当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。为了继续保持红黑树的性质,我们可以通过对结点进行重新着色,以及对树进行相关的旋转操作,即修改树中某些节点的颜色及指针结构,来达到对红黑树进行插入或删除节点等操作后,继续保持它的性质或平衡。
参考的是Linux kernel中红黑树的实现,红黑树节点定义如下:
#define RB_RED 0
#define RB_BLACK 1
struct rb_node
{
struct rb_node *left;
struct rb_node *right;
struct rb_node *parent;
int value;
int color;
};
struct rbtree {
struct rb_node *root;
};
红黑树左旋的图示如下:
当在某个节点pivot上,做左旋操作时,我们假设它的右孩子y不是NIL[T],pivot可以为任何不是NIL[T]的左孩子节点。左旋以pivot到y之间的链为”支轴”进行,它使y成为孩子树新的根,而y的左孩子b则成为pivot的右孩子。
左旋操作的参考代码如下所示:
void replace_node(struct rbtree *t, struct rb_node *o, struct rb_node *n)
{
if (o->parent == NULL) {
t->root = n;
} else {
if (o == o->parent->left) {
o->parent->left = n;
} else {
o->parent->right = n;
}
}
if (n != NULL) {
n->parent = o->parent;
}
}
void rotate_left(struct rbtree *t, struct rb_node *pnode)
{
struct rb_node *rnode = pnode->right;
replace_node(t, pnode, rnode);
pnode->right = rnode->left;
if (rnode->left != NULL) {
rnode->left->parent = pnode;
}
rnode->left = pnode;
pnode->parent = rnode;
}
红黑树的右旋图示如下:
当在某个结点pivot上,做右旋操作时,我们假设它的左孩子y不是NIL[T],pivot可以为任何不为NIL[T]的右孩子节点。右旋以pivot到y之间链为“支轴”进行,使得y为孩子树的新根,y的右节点为pivot的左节点,并且将y的右节点重新赋值为pivot。
右旋操作的参考代码如下所示:
void replace_node(struct rbtree *t, strct rb_node *o, struct rb_node *n)
{
if (o->parent == NULL) {
t->root = n;
} else {
if (o == o->parent->left) {
o->parent->left = n;
} else {
o->parent->right = n;
}
}
if (n != NULL) {
n->parent = o->parent;
}
}
void rotate_right(struct rbtree *t, struct rb_node *pnode)
{
struct rb_node *lnode = pnode->left;
replace_node(t, pnode, lnode);
pnode->left = lnode->right;
if (lnode->right != NULL) {
lnode->right->parent = pnode;
}
lnode->right = pnode;
pnode->parent = lnode;
}
对于树的旋转,能保持不变的只有原树的搜索性质,而原树的红黑性质则不能保持,在红黑树的数据插入和删除后可利用旋转和颜色重涂来恢复树的红黑性质。
红黑树的插入和删除操作都非常复杂,我们先来学习一下红黑树的插入操作。红黑树插入操作的具体执行步骤为:
在插入后的平衡过程中,分为如下几种情况:
新节点位于树的根节点。这种情况比较简单,直接将根节点从红色变为黑色即可。如下图所示:
实现代码如下:
void insert_case_1(struct rbtree *t, struct node *n)
{
if (n->parent == NULL) {
n->color = RB_BLACK;
} else {
insert_case_2(t, n);
}
}
如果新插入节点的父节点是黑色,则新插入节点后没有违反红黑树的性质,仍是一棵合法的红黑树。因为新节点是红色的,在任何一条路径上都没有增加黑色节点的个数。如下图所示:
实现代码如下:
void insert_case_2(struct rbtree *t, struct rb_node *n)
{
if (n->parent->color == RB_BLACK) {
return;
} else {
insert_case_3(struct rbtree *t, struct rb_node *n)
}
}
如果新节点的父节点是红色,如上图中要插入一个4节点,并且4节点的父节点3也是红色,就违反了红黑树的性质,必须重新绘制(ps:违反的是红黑树的性质4:如果一个节点是红的,那么它的两个儿子节点都是黑色的)。重新绘制也分为下面几种情况。
如果父节点p和叔叔节点u都是红色,则:
如下图:
这个时候祖父节点g就变为红色了。祖父节点变为红色之后,可能也会遇到上述的各种问题,所以我们需要把祖父节点看成是新插入的节点,重新执行上述情况一、二、三。
实现代码如下:
void insert_case_3(struct rbtree *t, struct node *n)
{
if (uncle(n)->color == RB_RED) {
n->parent->color = RB_BLACK;
uncle(n)->color = RB_BLACK;
n->parent->parent->color = RB_RED;
insert_case_1(t, n->parent->parent);
} else {
insert_case_4(t, n);
}
}
如果父节点p是红色的,但是叔叔节点是黑色或者没有,而且插入节点和父节点不在一条直线上,则:
如下图所示:
实现代码如下:
void insert_case_4(struct rbtree *t, struct rb_node *n)
{
if (n == n->parent->right && n->parent == n->parent->parent->left) {
rotate_left(t, n->parent);
n = n->left;
} else if (n == n->parent->left && n->parent == n->parent->parent->right) {
rotate_right(t, n->parent);
n = n->right;
}
insert_case_5(t, n);
}
上述操作之后,只是让新插入节点和父节点在一条直线上,并没有解决违反红黑树性质的问题,接下来,我们讨论新插入节点和父节点在同一条直线上,且均为红色的解决方案。
这种情况需要做如下操作:
旋转完毕后,需要切换父节点和祖父节点的颜色。
如下图所示:
实现代码如下:
void insert_case_5(struct rbtree *t, struct rb_node *n)
{
n->parent->color = RB_BLACK;
n->parent->parent->color = RB_RED;
if (n == n->parent->left && n->parent == n->parent->parent->left) {
rotate_right(t, n->parent->parent);
} else {
rotate_left(t, n->parent->parent);
}
}
接下来,我们来学习红黑树的删除操作。在学习红黑树的删除操作之前,我们先来学习一个重要函数:replace_node,实现代码如下:
void replace_node(struct rbtree *t, struct rb_node *p, struct rb_node *n)
{
if (p->parent == NULL) {
t->root = n;
} else {
if (p == p->parent->left) {
p->parent->left = n;
} else {
p->parent->right = n;
}
}
if (n != NULL) {
n->parent = p->parent;
}
}
红黑树的删除也分为6种情况,我们分别来分析。(注意,这里的情况指的是节点n删除后,如何根据这六种情况进行调整,重新成为一棵合法的红黑树。节点删除的操作这里就不讲了)
如果要删除的节点是根节点,则什么也不做,否则执行删除情况2。
实现代码如下:
void delete_case_1(struct rbtree *t, strct rb_node *n)
{
if (n->parent == NULL) {
return;
} else {
delete_case_2(t, n);
}
}
如果要删除节点的兄弟节点b的颜色为红色,那么把父节点p的颜色变为红色,同时把兄弟节点S的颜色变为黑色。
如下图所示:
实现代码如下:
void delete_case_2(struct rbtree *t, struct rb_node *n)
{
if (brother(n)->color == RB_RED) {
n->parent->color = RB_RED;
brother(n)->color = RB_BLACK;
if (n == n->parent->left) {
rotate_left(t, n->parent);
} else {
rotate_right(t, n->parent);
}
}
delete_case_3(t, n);
}
n的父节点p、兄弟节点b和b的儿子节点均为黑色。
这种情况下,只需要把兄弟节点的颜色变为红色,然后将指针指向父节点p,并进入第一种删除情况。因为兄弟节点变为红色之后,通过他的路径都少了一个黑色节点。
如下图所示:
实现代码如下:
void delete_case_3(struct rbtree *t, struct rb_node *n)
{
if (n->parent->color == RB_BLACK &&
brother(n)->color == RB_BLACK &&
brother(n)->left->color == RB_BLACK &&
brother(n)->right->color == RB_BLACK)
{
brother(n)->color = RB_RED;
delete_case_1(t, n->parent);
} else {
delete_case_4(t, n->parent);
}
}
如果满足上述情况,则把n->parent作为新删除节点来重新进行判断。如果不满足,则直接进入情况四。
兄弟节点b和b的儿子都是黑色,但父节点是红色。
这种情况我们只需要简单的交换父节点和兄弟节点的颜色即可。
如下图所示:
实现代码如下:
void delete_case_4(struct rbtree *t, struct rb_node *n)
{
if (n->parent->color == RB_RED &&
brother(n)->color == RB_BLACK &&
brother(n)->left->color == RB_BLACK &&
brother(n)->right->color == RB_BLACK)
{
n->parent->color = RB_BLACK;
brother(n)->color = RB_RED;
} else {
delete_case_5(t, n);
}
}
如果不满足上述情况,则进入情况五。
情况5又分为两种情况:
旋转结束之后,还有节点颜色的变化:
如下图所示:
实现代码如下:
void delete_case_5(struct rbtree *t, struct rb_node *n)
{
if (n == n->parent->left &&
brother(n)->color == RB_BLACK &&
brother(n)->left->color == RB_RED &&
brother(n)->right->color == RB_BLACK)
{
brother(n)->color = RB_RED;
brother(n)->left->color == RB_BLACK;
rotate_right(t, brother(n));
}
else if ( n == n->parent->right &&
brother(n)->color == RB_BLACK &&
brother(n)->right->color == RB_RED &&
brother(n)->left->color == RB_BLACK
)
{
brother(n)->color = RB_RED;
brother(n)->right->color = RB_BLACK;
rotate_left(t, brother(n));
} else
{
delete_case_6(t, n);
}
}
终于到了删除的最后一种情况,赶紧搞起。
旋转之前,还有节点颜色的变化:
如下图所示:
实现代码如下:
void delete_case_6(struct rbtree *t, struct rb_node *n)
{
brother(n)->color = n->parent->color;
n->parent->color = RB_BLACK;
if (n == n->parent->left) {
brother(n)->right->color = RB_BLACK;
rotate_left(t, n->parent);
} else {
brother(n)->left->color = RB_BLACK;
rotate_right(t, n->parent);
}
}
好了,到这里红黑树的删除操作也结束了。接下去,我会带大家深入到Linux内核,看一下Linux内核中的红黑树实现。