红黑树之插入节点
红黑树的性质
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
- 节点是红色或黑色。
- 根节点是黑色。
- 每个叶节点(这里的叶节点是指NULL节点,在《算法导论》中这个节点叫哨兵节点,除了颜色属性外,其他属性值都为任意。为了和以前的叶子节点做区分,原来的叶子节点还叫叶子节点,这个节点就叫他NULL节点吧)是黑色的。
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点,或者理解为红节点不能有红孩子)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(黑节点的数目称为黑高black-height)。
正是红黑树的这5条性质,使一棵n个结点的红黑树始终保持了logn的高度,从而也就解释了上面所说的“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论成立的原因
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接*衡的。
红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。
红黑树的插入操作
红黑树首先是一棵二叉排序树,所以它的插入操作要遵循二叉排序树的插入原则。
接下来我们先考虑为待插入的节点涂上颜色
如果将待插入的节点涂成黑色,则新的树必然违反了性质5 ,而这个时候我们让树重新成为红黑树的方法,似乎只有将刚刚插入的节点的颜色改成红色(之所以用似乎,是因为我只想到了这种方法,也许有其他方法,或者说,这种方法是效率最高的,毕竟之所以要建立红黑树,而不是普通的二叉排序树,看中的就是红黑树的效率),这样不就多此一举了吗,我们直接把待插入的节点涂成红色不就得了。
新插入节点后会使红黑树的哪些性质遭到破坏?
上面我们已经确定了,待插入的节点要被涂成红色。那么插入一个红色节点会破坏哪些性质呢?首先1和3一定不会被破坏(很显然了),此外5也不会被破坏,因为红节点代替了原来的NULL节点(黑色),但是红节点自身也有两个NULL节点,所以插入一个红色节点,路径上的黑节点数目并不会发生改变。所以,插入一个节点影响的只能是性质2和性质4,而且是可能影响:当插入的节点是根节点的时候,性质2就遭到破坏;当插入的节点的父亲节点为红色的时候,性质4就遭到破坏。明确了哪种性质会被破坏,再牢记红黑树的5个性质,我们就有了调整的依据。
如何调整插入节点后带来的影响?
首先,性质2遭到破坏的情况调整起来比较简单,就是将节点的颜色改成黑色就好啦!
对于性质4遭到破坏的情况,比较复杂,是我们要重点关注的。
现在我们关注的情况是:插入的节点为红色,其父亲节点也为红色的情况。这种情况要根据其叔叔节点的颜色,再细分成两种情况:
叔叔节点的颜色为红色:(这个时候,它的祖父节点必为黑色)
如上图,现在我们插入21
这个时候我们采取的办法是,将25节点(也就是祖父节点)涂成红色,然后将父亲节点和叔叔节点涂成黑色。如下图:
这个时候又出现了新的问题,就是17和25节点又违法了性质4,这个时候我们看到25的叔叔节点,也就是8节点也为红色,那么情况和上一次调整是一样的,我们就再做一次和上次一样的调整即可。
再次调整后,我们发现,现在违反的只有性质2了,这个时候我们就把根节点改成黑色就行啦!
所以,总结一下,就是,当叔叔节点为红色的时候调整的过程是:将新插入的节点作为起始点,然后将父亲节点和叔叔节点涂成黑色,将祖父节点涂成红色,然后以祖父节点为新的起始点再次进行同样的操作(如果新的起点的叔叔节点也为红色)。这样直到新的起始点为根节点为止,最后把根节点涂成黑色即可。
但是问题来了,如果叔叔节点是黑色要怎么办,或者在上面的过程中,某个新的起始点的叔叔节点是黑色的怎么办。这就是下面我们要讨论的第二种情况。
叔叔节点为黑色:(这个时候,祖父节点也必定为黑色,因为父亲节点时红色的)
如图,我们插入的是7节点
这个时候我们采取的调整方法为:我们将以祖父节点(1节点)为根的子树看成是RR型,然后进行RR型的调整(就是AVL树种的RR型),最后将新的根节点涂成黑色,将其两个孩子中不是红色的节点的颜色改成红色。
如下图:
这个时候,我们发现整棵树已经是红黑树了,不需要向情况1一样一直调整到根部了。
有RR型,自然也有LL型,LR型和RL型。如下图:
我们可以发现,新的树根总是黑色的,它的两个孩子总是红色的。
对于AVL树的四种类型:LL、LR、RR、RL的类型判断和调整,请看我之前的文章:http://www.cnblogs.com/qingergege/p/7294892.html
代码如何写:
有了上面的理论知识,我们现在要看代码怎么写
首先先给出节点的数据结构:
//定义节点的颜色
enum color{
BLACK,
RED
};
//节点的数据结构
typedef struct b_node{
int value;//节点的值
enum color color;//树的深度
struct b_node *l_tree;//左子树
struct b_node *r_tree;//右子树
struct b_node *parent;//父亲节点
} BNode,*PBNode;
/**
* 分配一个节点
* */
PBNode allocate_node()
{
PBNode node = NULL;
node = (PBNode)malloc(sizeof(struct b_node));
if(node == NULL)
return NULL;
memset(node,0,sizeof(struct b_node));
return node;
}
/**
* 设置一个节点的值
* */
void set_value(PBNode node,int value)
{
if(node == NULL)
return;
node->value = value;
node->color = RED;
node->l_tree = NULL;
node->r_tree = NULL;
node->parent = NULL;
}
红黑树的插入,首先要遵循二叉排序树的插入,所以我们先给出二叉排序树的插入代码(这里因为要涉及父亲节点的指向,所以用非递归的方法创建),由于根节点的调整情况比较简单,只需将颜色改成黑色即可,所以我们捎带加上一句即可。
/**
* 向二叉查找树中添加一个节点,使得新的二叉树依然时二叉查找树
* 非递归方法实现
* */
void insert_node(PBNode *root,int value)
{
if(*root == NULL)
{
*root = allocate_node();
set_value(*root,value);
(*root)->color = BLACK;
}
else
{
PBNode p = *root;
PBNode pp = NULL;//保存父亲节点
bool is_left = false;
while(p != NULL)
{
pp = p;
is_left = false;
if(value < p->value)
{
is_left = true;
p = p->l_tree;
}
else if(value > p->value)
{
p = p->r_tree;
}
}
PBNode node = allocate_node();
set_value(node,value);
node->parent = pp;//填父亲节点
if(is_left)
{
pp->l_tree = node;
}
else
{
pp->r_tree = node;
}
}
}
接下来就该讨论调整部分的代码了。
首先,我们从小到大,先从小的模块开始。上面提到,当叔叔节点为黑色的时候,除了颜色的改变之外,还需要进行RR、RL、LL、LR四种类型的变换操作,所以我们可以先定义两个功能函数,一个用于判断子树为哪种类型,另一个函数对子树,根据类型进行调整,捎带把颜色也变了。(四种类型的确定和调整,不懂的可以看我之前的文章,http://www.cnblogs.com/qingergege/p/7294892.html)
/**
* 根据子树类型进行调整,顺便把颜色也调整了
* ch_root为待调整的子树的树根,也就是插入新插入节点的祖父节点
* type为子树的类型
* root为整棵树的树根,因为这个过程中,整棵树的树根可能都在随时变换
* */
void case2_adjust(PBNode *root,PBNode ch_root,enum unbalance_type type)
{
int t = type;
PBNode small;
PBNode middle;
PBNode big;
switch (t)
{
case TYPE_LL:
{
//确定small、middle、big三个节点
big = ch_root;
middle = ch_root->l_tree;
small = ch_root->l_tree->l_tree;
//分配middle节点的孩子,给small和big
big->l_tree = middle->r_tree;
//别忘了该父亲节点!!!!!!!!!
if(middle->r_tree != NULL)
middle->r_tree->parent = big;
//将small和big作为midlle的左子和右子
middle->r_tree = big;
break;
}
case TYPE_LR:
{
//确定small、middle、big三个节点
big = ch_root;
small = ch_root->l_tree;
middle = ch_root->l_tree->r_tree;
//分配middle节点的孩子,给small和big
small->r_tree = middle->l_tree;
big->l_tree = middle->r_tree;
//别忘了该父亲节点!!!!!!!!!
if(middle->l_tree != NULL)
middle->l_tree->parent = small;
if(middle->r_tree != NULL)
middle->r_tree->parent = big;
//将small和big作为midlle的左子和右子
middle->l_tree = small;
middle->r_tree = big;
break;
}
case TYPE_RL:
{
//确定small、middle、big三个节点
small = ch_root;
big = ch_root->r_tree;
middle = ch_root->r_tree->l_tree;
//分配middle节点的孩子,给small和big
small->r_tree = middle->l_tree;
big->l_tree = middle->r_tree;
//别忘了该父亲节点!!!!!!!!!
if(middle->l_tree != NULL)
middle->l_tree->parent = small;
if(middle->r_tree != NULL)
middle->r_tree->parent = big;
//将small和big作为midlle的左子和右子
middle->l_tree = small;
middle->r_tree = big;
break;
}
case TYPE_RR:
{
//确定small、middle、big三个节点
small =ch_root;
middle = ch_root->r_tree;
big = ch_root->r_tree->r_tree;
//分配middle节点的孩子,给small和big
small->r_tree = middle->l_tree;
//别忘了该父亲节点!!!!!!!!!
if(middle->l_tree != NULL)
middle->l_tree->parent = small;
//将small和big作为midlle的左子和右子
middle->l_tree = small;
break;
}
}
//将子树的父亲节点的子节点指向middle(也就是将middle,调整后的子树的根结点)
if(ch_root->parent == NULL) //说明子树的根节点就是整棵树的根结点
{
*root = middle;
}
else if(ch_root->parent->l_tree == ch_root)//根是父亲的左孩子
{
ch_root->parent->l_tree = middle;
}
else if(ch_root->parent->r_tree == ch_root)//根是父亲的右孩子
{
ch_root->parent->r_tree = middle;
}
if(ch_root->parent != NULL)
//更改small、middle、big的父亲节点
middle->parent = ch_root->parent;
big->parent = middle;
small->parent = middle;
if(ch_root->parent != NULL)
//更改节点的颜色
middle->color = BLACK;//根节点为黑色
big->color = RED;//孩子为红色
small->color = RED;//孩子为红色
}
对于情况1,也就是叔叔节点为红色的情况,只涉及颜色的调整,比较简单,但是我们也可以将其操作封装在函数中,这样更加模块化,代码也更加清晰。
/**
* 对情况1,也就是叔叔节点为红色的情况的调整(这种情况,只涉及颜色的调整)
* 其中begin为调整的起始点,对于第一次调整,这个点就是刚刚插入的节点
* 返回值是下一轮调整(如果需要)的起始节点
* */
PBNode case1_adjust(PBNode begin)
{
PBNode parent = begin->parent;//父亲节点
PBNode grand = parent->parent;//祖父节点
PBNode uncle = grand->l_tree == parent ? grand->r_tree : grand->l_tree;//叔叔节点
//颜色调整
grand->color = RED;
parent->color = BLACK;
uncle->color = BLACK;
//返回下一次迭代的起始点
return grand;
}
接下来就是将上面的模块进行串联了。由于情况1,也就是叔叔为红色的情况可能要迭代,所以,串联要在循环中进行。
首先我们看循环结束的条件,如果一直迭代下去,最后要到根节点,如果迭代的图中,起始点的父亲节点的颜色为黑色则调整结束,如果迭代过程中遇到情况2,也就是叔叔为黑色的情况,那么进行情况2的调整后,也达到了红黑树的要求,这个时候循环也要结束。所以我们可以这样:while循环中条件为非根节点,如果遇到起始点颜色为黑色或者处理了情况2,那么就用break结束循环。代码如下:
/**
* 串连函数,负责将两种情况的调整函数串连起来
* 参数root为整棵树的根节点,node为刚刚插入的节点
**/
void insert_adjust(PBNode *root,PBNode node)
{
PBNode begin = node;
while(begin != *root)
{
if(begin->parent->color == BLACK)//如果父亲节点为黑色,则不用调整
{
break;
}
//反之,如果父亲节点为红色则需要调整
PBNode parent = begin->parent;//获得父亲节点
PBNode grand = parent->parent;//获得祖父节点
PBNode uncle = grand->l_tree == parent ? grand->r_tree: grand->l_tree;//叔叔节点
//注意节点为空的时候,C语言逻辑与和逻辑或都为短路操作,也就是一点能判断整个表达式的值,就不再进行后面的判断
if(uncle == NULL || uncle->color == BLACK )//如果叔叔节点为黑色,则进行情况2的调整,调整后,整棵树就满足红黑树了,则退出循环
{
enum unbalance_type type = get_type(grand,begin->value);//获得子树的类型
case2_adjust(root,grand,type);
break;//退出循环
}
//反之,如果叔叔节点为红色,则进行情况1的调整,返回值为下一次迭代的起始节点
begin = case1_adjust(begin);
}
if(begin == *root)
{
(*root)->color = BLACK;
}
}
最后附上word文件和源代码文件
链接:http://pan.baidu.com/s/1nvQI2iX 密码:16nd
参考资料:
http://blog.csdn.net/v_JULY_v/article/details/6105630
http://blog.csdn.net/dreamclr/article/details/50962566
http://blog.csdn.net/goodluckwhh/article/details/12718233
http://www.cnblogs.com/sandy2013/p/3270999.html