红黑树的插入
性质
性质1 每个节点要么是红色的要么是黑色的
性质2 根节点一定是黑色的
性质3 每个叶子节点肯定是黑色的
性质4 一个红色节点的父亲节点一定不是红色的
性质5 任何一个节点到叶子节点的简单路径上所包含的黑色节点的数量是相同的
左旋 右旋
接下来我们试着把上面的图片(图片针对叶子节点进行了隐藏,文章之后的图片也一样,故以后不再说明)中的x节点进行左旋
void leftRotate(T,x){
//x的右孩子
y = x.right;
//y的左孩子变为x的右孩子
x.right = y.left;
//假如y的左孩子不为空
if(y.left != NULL){
//y左孩子的父亲节点变为x
y.left.p = x;
}
//y的父亲节点变为x的父亲节点
y.p = x.p;
//假如x为其父亲节点的左孩子
if(x == x.p.left){
//y = x父亲的左孩子
y = x.p.left;
}
//x为其父亲节点的右孩子
else if(x == x.p.right){
//y = x.p.right
y = x.p.right;
}
//x的父亲节点为空即x为根节点
else
{
//y设置为根节点
T.root = y;
}
//x设为y的左孩子
y.left = x;
//y = x.p;
x.p = y;
}
至于右旋这里就不写了,只需要将上面代码中的left与right互换即可。
为什么一定插入红色节点
在插入之前我们必须要思考一个问题,假如我们在插入某个节点后,要给这个新的节点染色。我们会将这个新的节点染为什么颜色?相信很多人都会选择红色,为什么?插入红色节点可能大大的减少我们的工作量,假如我们插入红色节点的父亲节点是黑色节点,那么在插入红色节点后的这棵树是满足红黑树的的各项性质的,所以我们接下来什么也不用干。假如我们此时插入的是黑色节点,那么我们肯定是违背上面我们所提到的红黑树的性质5。所以插入红色节点我们有可能得到一颗新的红黑树,但是插入黑色节点我们得到的一定不是一颗红黑树。所以综上所诉,我们选择插入红色节点。
插入
我们这里所选择的策略是先依据二叉排序树进行插入,然后再进行调整
redBlackTreeInsert(T,z){
//T.root
x = T.root;
//记录插入位置的父亲节点
y = NULL;
//根节点为空
if(x == NULL)
{
//z = 根节点
T.root = z;
}
//根节点不为空
while(x != NULL){
//记录插入位置的父亲节点
y = x;
//当前节点小于z节点的值的大小
if(x.key < z.key)
{
x = z.left;
}
//当前节点大于z节点的值
else
{
x = z.right;
}
}
//将z插入
z.p = y;
//z为y的左孩子
if(z.key < y.key)
{
y.left = z;
}
//z为y的右孩子
else
{
y.right =z;
}
//z的左右孩子为空
z.left = NULL;
z.right = NULL;
//z的颜色为红色
z.color = red;
//对插入后的红黑树进行调整
redBlackTreeInsertFixup(T,z);
}
对插入新节点后的二叉排序树进行调整,重新成为红黑树
我们在原来红黑树的基础上插入一个红色的节点z后,假如这个节点z的父亲节点是黑色的那么这颗新的树是一定符合红黑树的各项性质的,所以这种情况我们是不需要进行任何调整的。假如节点z的父亲节点要是红色的话,那么这颗新树是一定违反了上面红黑树性质中的性质4,这时候我们可能会想在父亲节点是红色的情况下,新插入节点z是左孩子还是右孩子呢?我们是否需要根据z的位置来进行分类讨论呢?答案是不用的。因为假如节点z是右孩子的话我们是可以将其父亲节点左旋来将它变成情况1的。所以我们这里只讨论新插入节点z是左孩子的情况。比如下面这张图所展示的情况1:
面对这种情况,我们为了使得插入节点后的树重新满足红黑树的各项性质。可以采取措施是,将新插入的节点z的父亲节点y以及叔叔节点u变为黑色(为了满足性质4),再将z的祖父节点变为红色。这里有人肯定会有疑问,明明在将z的父亲节点y以及叔叔节点u都变为黑色就已经修复了之前所违背的性质4,为何又要将z的祖父节点从黑色变为红色。我们这里要明确的是图片中所画出来的只是整棵树中的一小部分,如果我们只是将z的父亲节点以及叔叔节点变黑,却不改变z的祖父节点x的颜色,那我们将视野扩大到整棵树就会发现,无论x的父亲节点是红色还是黑色,这种做法接下来肯定会违背性质5,而我们将z的祖父节点变为红色,同样将视野扩大到整棵树,我们会发现这种做法接下来未必就会违背性质4,假如x的父亲节点是黑色情况下我们对整棵树的修改就到此为止,此时的树就是一颗红黑树。
那情况1经过上面所说的操作接下来该怎么办呢?接下来,假如x的父亲节点是黑色的那么我们接下来是不需要进行任何操作的,整棵树此时已经满足红黑树的所有性质。假如x节点是根节点,那么我们需要将节点x染为黑色。但是假如x的父亲节点是红色的话,比如下面这种情况2:
从图片中我们可以看到情况2中的节点与其父亲节点v违背了红黑树的性质4,那这里有人会问假如节点x不是父亲节点v的左孩子而是右孩子这种情况该怎么办?在情况1中我们就已经讲到遇到假如x是右孩子完全是可以将其父亲节点左旋来变为情况2这种情况的。所以这里我们只讨论上面这种情况2.对于情况2,我们是否可以向对待情况1那样将x的父亲节点以及叔叔节点染成黑色,然后将祖父节点染为红色?如果x的叔叔节点s是红色的,那么没错我们完全可以采取与情况1相同的措施,但是这里x的叔叔节点是黑色,我们假如完全照搬情况1的措施,那我们肯定会违背性质5,如果x的祖父节点w的父亲节点万一是红色的话,我们就会违背性质5和性质4这两条性质,那我们有没有其他更好的选择?针对情况2我们是有更好的措施的:我们可以将x的父亲节点v染成黑色,祖父节点w变为红色,然后将x的祖父节点w右旋,变为下面这种情况3:
这时候我们会发现情况3中关于红黑树的每一条性质我们都没有违背。而且接下来假如节点v的父亲节点无论是黑色还是红色,我们都不会违背性质4,整棵树的修改也可以到此为止。
最后肯定有人会发现无论是情况1中z的父亲节点y还是情况2中的x的父亲节点v都是左孩子,那么假如是右孩子那又该怎么办?可以通过左旋来转变为情况1或者情况2来解决吗?显然是不可以的,但是我们也可以发现假如情况1中的z的父亲节y或者情况2后再难过x的父亲节点v是右孩子的话与我们上面所讲的情况1和情况2是对称的,在具体的算法实现的时候我们只需要将left与right互换即可。
调整的代码实现
redBlackTreeInsertFixup(T,z){
//当父亲节点是红色的时候进行调整
while(z.p.color == red){
//情况1
if(z.p == z.p.p.left)
{
//父亲节点以及叔叔节点涂黑
z.p.color = black;
z.p.p.right.color = black;
//祖父节点变红
z.p.p.color = red;
//指针上移 为了接下来判断祖父节点的父亲节点是否是红色节点
z = z.p.p;
}
//假如自身节点为右孩子节点
if(z.p == z.p.right){
//将父亲节点左旋来变为情况1或者情况2
leftRotate(T,z.p);
}
//情况3
if(z.p.p.right == black)
{
//父亲节点变黑
z.p.color = black;
//祖父节点变红
z.p.p.color = red;
//右旋
rightRotate(T,z.p.p);
}
//父亲节点为右孩子
else
{
//将上方相关代码中的left与right进行互换
}
}
//节点z为根节点 需要将根节点变为黑色
T.root.color = black;
}
参考资料
(美)ThomasH.Cormen,CharlesE.Leiserson,RonaldL.Rivest,CliffordStein著;王刚,邹恒明,殷建平,王宏志等译. 算法导论 原书第3版. 北京:机械工业出版社, 2013.01.