红黑树的定义
它或者是一颗空树,或者是具有一下性质的二叉查找树:
1.节点非红即黑。2.根节点是黑色。
3.所有NULL结点称为叶子节点,且认为颜色为黑。
4.所有红节点的子节点都为黑色。
5.从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点。
看完定义有点晕吧,或许看看说明会好些:
说明:
1.第3条,显然这里的叶子节点不是平常我们所说的叶子节点,如图标有NIL的为叶子节点,为什么不按常规出牌,因为按一般的叶子节点也行,但会使算法更复杂;
2.第4条,即该树上决不允许存在两个连续的红节点;
3.所有性质1-5合起来约束了该树的平衡性能–即该树上的最长路径不可能会大于2倍最短路径。为什么?因为第1条该树上的节点非红即黑,由于第4条该树上不允许存在两个连续的红节点,那么对于从一个节点到其叶子节点的一条最长的路径一定是红黑交错的,那么最短路径一定是纯黑色的节点;而又第5条从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点,这么来说最长路径上的黑节点的数目和最短路径上的黑节点的数目相等!而又第2条根结点为黑、第3条叶子节点是黑,那么可知:最长路径<=2*最短路径。一颗二叉树的平衡性能越好,那么它的效率越高!红黑树的效率一般为O(logN)左右吧。
总结来看,就是:根节点和叶子结点(NIL)均为黑色 不允许连续两个红结点出现 从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点,总结起来就这三条吧!
上边的性质看个10遍,看懂看透彻再看操作,特别是第5条!
插入操作
(个人感悟:其实不管是平衡二叉树也好,红黑树也好,插入删除操作的本质上就是排列组合,将所有情况排一遍就算完事,所以研究一下这两个最常的数据结构非常训练逻辑,而且编程这个活说到底不就是个逻辑嘛!好吧,不罗嗦了,继续来分析研究吧……)
由于性质的约束:插入点不能为黑节点,应插入红节点。假如插入的是黑结点,一定会破坏性质5,很简单因为之前是平衡的呀。所以在插入黑结点后第一步要解决问题肯定是看先把它涂成红结点看看行不行(因为如果这样行的话最省力),到最后还是回到红结点上来了,所以不如每次插入红结点,然后再看情况调整。
当然插入红结点后也不会就那么一帆风顺了,比如若他的父节点也为红,那岂不是破坏了性质4?所以要分以下情况做一些“旋转”和一些节点的变色!
另为叙述方便我们给要插入的节点标为N(红色),父节点为P,祖父节点为G,叔节点为U,兄弟节点S。在一一列出以下情况之前先分析一下N节点的插入会因为那些节点而要进行不同的分类,S不影响,剩下的P G U都会影响的。
下边将一一列出所有插入时遇到的情况:
情形1:该树为空树,直接插入根结点的位置,违反性质1,把节点颜色有红改为黑即可。
情形2:插入节点N的父节点P为黑色,不违反任何性质,无需做任何修改。
以上两种情形非常简单,但P为红就不一样了,下边是P为红的各种情况,也是真正要学的地方!
情形3:P为红,(祖节点一定存在,且为黑,下边同理)U也为红,这里不论P是G的左孩子,还是右孩子;不论N是P的左孩子,还是右孩子。
如何做:如图把P、U改为黑色,G改为红色,然后根据情况往上递归。N、P都为红,违反性质4;若把P改为黑,符合性质4,但违反性质5;所以我们把G,U都改为相反色,这样一来通过G的路径的黑节点数目没变,即符合4、5,但是G变红了,若G的父节点又是红的不就有违反了4,是这样,所以经过上边操作后未结束,需把G作为起始点,即把G看做一个插入的红节点继续向上递归检索—-属于哪种情况,按那种情况操作如果G已经是根节点了,那就把他变为黑色吧。
情形4:P为红,U为黑,P为G的左孩子,N为P的左孩子(或者P为G的右孩子,N为P的左孩子;反正就是同向的)。
如何做:如图P、G变色,P、G变换即左左单旋(或者右右单旋),结束。要知道经过P、G变换(旋转),变换后P的位置就是当年G的位置,所以红P变为黑,而黑G变为红都是为了不违反性质5,而维持到达叶节点所包含的黑节点的数目不变!还可以理解为:也就是相当于(只是相当于,并不是实事,只是为了更好理解;)把红N头上的红节点移到对面黑U的头上;这样即符合了性质4也不违反性质5,这样就结束了。
情形5:P为红,U为黑,P为G的左孩子,N为P的右孩子(或者P为G的右孩子,N为P的左孩子;反正两方向相反)。
如何做:需要进行两次变换(旋转),图中只显示了一次变换—–首先P、N变换,颜色不变;然后就变成了情形4的情况,按照情况4操作,结束。 由于P、N都为红,经变换,不违反性质5;然后就变成4的情形,此时G与G现在的左孩子变色,并变换,结束。
到此,插入的所有情况分析完了,下面分析删除操作。
删除操作
我们知道删除需先找到“替代点”来替代删除点而被删除,也就是删除的是替代点,而替代点N的至少有一个子节点为NULL,那么,
若N为红色,则两个子节点一定都为NULL(必须地),那么直接把N删了,不违反任何性质,ok,结束了;
若N为黑色,另一个子节点M不为NULL,则另一个节点M一定是红色的,且M的子节点都为NULL(按性质来的,不明白,自己分析一下)那么把N删掉,M占到N的位置,并改为黑色,不违反任何性质,ok,结束了;
若N为黑色,另一个子节点也为NULL,则把N删掉,该位置置为NULL,显然这个黑节点被删除了,破坏了性质5,那么要以N节点为起始点检索看看属于那种情况,并作相应的操作,另还需说明P为父节点,S为兄弟节点,分为以下5中情况:
情形1:S为红色(那么父节点P一定是黑,子节点一定是黑),N是P的左孩子(或者N是P的右孩子)。
怎么删:P、S变色,并进行AVL中的右右旋转即以P为中心S向左旋(或者是AVL中的左左中的旋转),未结束。我们知道P的左边少了一个黑节点,这样操作相当于在N头上又加了一个红节点—-不违反任何性质,但是到通过N的路径仍少了一个黑节点,需要再把对N进行一次检索,并作相应的操作才可以平衡(暂且不管往下看)。
情形2:P、S及S的孩子们都为黑。
怎么删:S改为红色,未结束。S变为红色后经过S节点的路径的黑节点数目也减少了1,那个从P出发到其叶子节点到所有路径所包含的黑节点数目(记为num)相等了。但是这个num比之前少了1,因为左右子树中的黑节点数目都减少了!一般地,P是他父节点G的一个孩子,那么由G到其叶子节点的黑节点数目就不相等了,所以说没有结束,需把P当做新的起始点开始向上递归检索。
情形3:P为红(S一定为黑),S的孩子们都为黑。
怎么删:P改为黑,S改为红,结束。这种情况最简单了,既然N这边少了一个黑节点,那么就把S涂成红色,S这个分支上也少了一个黑节点,P的两个分支下的黑节点数仍然相等。然后再将P涂为黑色,这样经过P分支的黑节点数仍然没少,处理的多么精妙啊!
情形4:P任意色,S为黑,N是P的左孩子,S的右孩子SR为红,S的左孩子任意(或者是N是P的右孩子,S的左孩子为红,S的右孩子任意)。
怎么删:SR(SL)改为黑,P改为黑,S改为P的颜色,P、S变换–这里相对应于AVL中的右右中的旋转(或者是AVL中的左左旋转),结束。P、S旋转有变色,等于给N这边加了一个黑节点,P位置(是位置而不是P)的颜色不变,S这边少了一个黑节点;SR有红变黑,S这边又增加了一个黑节点;这样一来又恢复了平衡,结束。
情形5:P任意色,S为黑,N是P的左孩子,S的左孩子SL为红,S的右孩子SR为黑(或者N是P的有孩子,S的右孩子为红,S的左孩子为黑)。
怎么删:SL(或SR)改为黑,S改为红,SL(SR)、S变换;此时就回到了情形4,SL(SR)变成了黑S,S变成了红SR(SL),做情形4的操作即可,这两次变换,其实就是对应AVL的右左的两次旋转(或者是AVL的左右的两次旋转)。这种情况如果你按情形4的操作的话,由于SR本来就是黑色,你无法弥补由于P、S的变换(旋转)给S这边造成的损失!所以先对S、SL进行变换之后就变为情形4的情况了,然后…(确实为上策)。
好了,删除的情况也都讨论完了,其实要注意的就是哪些分方向的情况,每个分方向的情形就两种情况,不要搞迷就行!后来我检查的时候还以为P红S黑SL和SR均红这种情况没有考虑到呢,实际上在情形4中就已经包含这种情况了。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int key_t;
typedef int data_t;
typedef enum color_t
{
RED = 0,
BLACK =1
}color_t;
typedef struct rb_node_t
{
struct rb_node_t *left, *right, *parent;
key_t key; //这个是关键值
data_t data; //这个是标号
color_t color;
}rb_node_t;
/*forward declaration */
/*插入结点*/
rb_node_t *rb_insert(key_t key, data_t data, rb_node_t *root);
/*查找结点*/
rb_node_t *rb_search(key_t key, rb_node_t *root);
/*释放结点*/
rb_node_t *rb_erase(key_t key, rb_node_t *root);
int main()
{
int i, count = 7;
key_t key;
rb_node_t *root = NULL;
rb_node_t *node =NULL;
srand(time(NULL));
for(i = 1; i < count; ++i)
{
key = rand() % count;
if((root = rb_insert(key , i, root)))
{
printf("[i = %d] insert key %d success!\\n", i, key);
}
else
{
printf("[i = %d] insert key %d error!\\n", i, key);
exit(-1);
}
if((node = rb_search(key,root)))
{
printf("[i = %d] search key %d success!\\n",i, key);
}
else
{
printf("[i = %d] search key %d error!\\n",i, key);
exit(-1);
}
if (!(i % 5))
{
if((root = rb_erase(key,root)))
{
printf("[i = %d] erase key %d success\\n", i, key);
}
else
{
printf("[i = %d] erase key %d error\\n", i, key);
}
}
}
return 0;
}
static rb_node_t *rb_new_node(key_t key, data_t data)
{
rb_node_t *node = (rb_node_t*)malloc(sizeof(struct rb_node_t));
if(!node)
{
printf("malloc error!\\n");
exit(-1);
}
node->key = key;
node->data = data;
return node;
}
/*-----------------------------------------------------------
| node right
| /\\ ==> /\\
| a right node y
| /\\ /\\
| b y a b //原程序给的这丁点注释错了,我已修正。
-----------------------------------------------------------*/
/* 左旋转代码 */
static rb_node_t *rb_rotate_left(rb_node_t *node, rb_node_t *root)
{
rb_node_t *right = node->right; //指定指针指向right
if((node->right = right->left)) //将right的左孩子结点 挂接 给node的右孩子
{
right->left->parent = node; //node成为right左孩子的父母结点
}
right->left= node; //node成为right的左孩子
if((right->parent = node->parent)) //判断node不为根结点
{
if(node == node->parent->right) //node为父亲结点的右孩子
{
node->parent->right = right;
}
else //node为父亲结点的左孩子
{
node->parent->left = right;
}
}
else
{
root = right; //如果node为根结点
}
node->parent = right; //right成为node的父母
return root;
}
/*-----------------------------------------------------------
| node left
| /\\ /\\
| left y ==> a node
| /\\ /\\
| a b b y //右旋与左旋差不多,分析略过
-----------------------------------------------------------*/
/* 右旋代码 */
static rb_node_t *rb_rotate_right(rb_node_t *node, rb_node_t *root)
{
rb_node_t *left = node->left; //指定指针指向left
if ((node->left = left->right)) //将left的右孩子赋值给node的左孩子,且left的右孩子不为空
{
left->right->parent = node; //node成为left右孩子的父亲结点
}
left->right = node; //node成为left的右孩子
if((left->parent = node->parent)) //将left的父结点指向node的父结点,并且此时,node不为根结点
{
if(node == node->parent->right) //node为其父结点的右孩子
{
node->parent->right = left;
}
else //node为其父结点的左孩子
{
node->parent->left = left;
}
}
else //node为根结点
{
root = left;
}
node->parent = left; //left成为node的父结点
return root;
}
//红黑树查找结点
//-------------------------------------------------------------
//rb_search_auxiliary:查找
//rb_node_t * rb_search:返回找到的结点
//-------------------------------------------------------------
static rb_node_t *rb_search_auxiliary(key_t key,rb_node_t *root,rb_node_t **save)
{
rb_node_t *node = root, *parent = NULL;
int ret;
while(node)
{
parent = node;
ret = node->key - key;
if (0 < ret) //key 小于当前结点的key,所以,往其左子树查找
{
node = node->left;
}
else if (0 > ret) //key 大于当前结点的key,所以,往其右子树查找
{
node = node->right;
}
else
{
return node; //查找成功,返回
}
}
if(save) //
{
*save= parent;
}
return NULL;
}
//返回上述rb_search_auxiliary查找结构
rb_node_t *rb_search(key_t key, rb_node_t *root)
{
return rb_search_auxiliary(key, root, NULL);
}
/* 插入结点之后,重新调整红黑树的结构 */
//插入的三种情况,用z表示当前结点,p[z]表示父母,p[p[z]]表示祖父,y表示叔叔
static rb_node_t *rb_insert_rebalance(rb_node_t *node, rb_node_t *root)
{
rb_node_t *parent, *gparent, *uncle, *tmp;
//循环条件:parent为node的父结点且不为空,另外,node的父结点的颜色为红色时
while((parent = node->parent) && (parent->color == RED))
{
gparent = parent->parent;
if(parent == gparent->left) //当祖父的左孩子为父结点时
{ //其实上述几行语句,无非就是理顺孩子、父母、祖父的关系
uncle = gparent->right; //定义叔叔的概念,叔叔y就是父母的右孩子
if(uncle && uncle->color == RED) //情况1:z的叔叔结点y是红色的(叔叔结点为红色,必须保证叔叔y不为NULL)
{
uncle->color = BLACK; //对策如下:a、b、c
parent->color = BLACK; //a. 把node的父结点和叔叔结点涂黑
gparent->color = RED; //b. 把祖父结点涂红
node = gparent; //c. 把当前结点指向祖父结点,即指针z在树中上移了两层哦
//上述情况1只考虑了z的父结点为祖父的左孩子的情况
}
else //情况2:z的叔叔结点y是黑色的
{
if(parent->right == node) //且z为右孩子
{ //对策如下:
root = rb_rotate_left(parent, root); //当前结点的父结点作为新的当前结点
tmp = parent; //以新的当前结点为支点左旋
parent = node;
node = tmp;
}
//情况3:z的叔叔y是黑色的,此时z成为了左孩子
//注意:1、情况3是由上述情况2变化而来的
//......2、z的叔叔总是黑色的,否则就是情况1了
//对策如下:a,b,c
parent->color = BLACK; //a. 父结点图为黑色
gparent->color = RED; //b. 祖父结点变为红色
root = rb_rotate_right(gparent, root); //c. 以祖父结点为支点右旋
}
}
else //if(parent == gparent->right),即当父结点为祖父结点的右孩子时
{
uncle = gparent->left; //祖父结点的左孩子作为叔叔结点
if(uncle && uncle->color == RED) //情况1:z的叔叔y是红色的
{
uncle->color = BLACK;
parent->color = BLACK;
gparent->color = RED;
node = gparent;
}
else //情况2:z的叔叔y是黑色的
{
if(parent->left == node) //且z为左孩子
{
root = rb_rotate_right(parent, root); //右旋
tmp = parent;
parent = node;
node = tmp; //互换角色
}
//经过情况2的变化,成为了情况3.
parent->color = BLACK;
gparent->color = RED;
root = rb_rotate_left(gparent, root);
}
}
}
root->color = BLACK; //根结点,不论怎么样,都得置为黑色
return root; //返回根结点
}
//红黑树插入结点
rb_node_t *rb_insert(key_t key, data_t data, rb_node_t *root)
{
rb_node_t *parent = NULL, *node;
parent = NULL;
if((node = rb_search_auxiliary(key, root, &parent))) //调用rb_search_auxiliary查找到插入结点的地方
{
return root;
}
node = rb_new_node(key, data); //分配结点
node->parent = parent; //初始化
node->left = node->right = NULL;
node->color = RED;
if(parent) //parent保存的是要插入结点的地方
{
if(parent->key > key)
{
parent->left = node;
}
else
{
parent->right = node;
}
}
else
{
root = node;
}
return rb_insert_rebalance(node, root); //插入结点后,调用rb_search_rebalance修复红黑树
}
//红黑树的4种删除情况
/* 删除结点之后,重新调整红黑树的结构 */
//x表示要删除的结点,*other、w表示兄弟结点
static rb_node_t *rb_erase_rebalance(rb_node_t *node, rb_node_t *parent, rb_node_t *root)
{
rb_node_t *other, *o_left, *o_right; //x的兄弟*other, 兄弟左孩子*o_left, 右孩子*o_right
while((!node || node->color == BLACK) && node != root)
{
if(parent->left == node)
{
other = parent->right;
if(other->color == RED) //情况1:x的兄弟结点w是红色的
{ //对策如下
other->color = BLACK; //a. 把兄弟结点染成黑色
parent->color = RED; //b. 把父结点染成红色
root = rb_rotate_left(parent, root); //对p[x]进行左旋
other = parent->right; //x的新兄弟结点new w 是旋转之前w的某个孩子。其实是左旋后的效果
}
//情况2:x的兄弟结点w是黑色,且w的两个孩子全为黑色
if((!other->left || other->left->color == BLACK) && (!other->right || other->right->color == BLACK))
{
//对策:把当前结点和兄弟结点中抽取出一重黑色追加到父结点上,把父结点当成新的当前结点
other->color = RED; //a. 兄弟结点为红色
node = parent; //b. 把父结点当作新的当前结点
parent = node->parent;
}
else
{
//情况3:x的兄弟结点w是黑色的,且w的左孩子是红色,右孩子为黑色
if(!other->right || other->right->color == BLACK)
{
if((o_left = other->left)) //w和其左孩子left[w],颜色互换
{
o_left->color = BLACK; //w的左孩子颜色由红->黑
}
other->color = RED; //兄弟结点w由黑->红
root = rb_rotate_right(other, root); //对w进行右旋,从而红黑性质恢复
other = parent->right; //变化后,父结点的右孩子,作为新的兄弟结点
}
//情况4:x的兄弟结点w是黑色的,兄弟结点的右孩子是红色
other->color = parent->color; //a. 把兄弟结点染成当前结点父结点的颜色
parent->color = BLACK; //b. 把当前结点的父结点染成黑色
if(other->right)
{
other->right->color = BLACK; //c. 兄弟结点的右孩子染成黑色
}
root = rb_rotate_left(parent, root); //d. 以当前结点的父结点为支点进行左旋
node =root; //e. 把当前结点x置为根root
//此时算法结束,红黑树所有性质调转正确
break;
}
}
else //下面情况与上述情况,原理一致。分析略
{
other = parent->left;
if(other->color == RED)
{
other->color = BLACK;
parent->color = RED;
root = rb_rotate_right(parent, root);
other = parent->left;
}
if(( !other->left || other->left->color == BLACK) && ( !other->right || other->right->color == BLACK))
{
other->color = RED;
node = parent;
parent = node->parent;
}
else
{
if( !other->left || other->left->color == BLACK)
{
if((o_right = other->right))
{
o_right->color = BLACK;
}
other->color = RED;
root = rb_rotate_left(other, root);
other = parent->left;
}
other->color = parent->color;
parent->color = BLACK;
if(other->left)
{
other->left->color = BLACK;
}
root = rb_rotate_right(parent, root);
node = root;
break;
}
}
}
if(node)
{
node->color =BLACK;
}
return root;
}
//红黑树的删除结点
rb_node_t *rb_erase(key_t key, rb_node_t *root)
{
rb_node_t *child, *parent, *old, *left, *node;
color_t color;
if(!(node = rb_search_auxiliary(key, root, NULL))) //调用rb_search_auxiliary查找要删除的结点
{
printf("key %d is not exist!\\n");
return root;
}
old = node;
//删除结点有两个孩子结点
//策略:可以选择把当前删除结点左子树中的最大元素或者是右子树中的最小元素放到放到待删除结点的位置
if(node->left && node->right)
{
node = node->right;
while((left = node->left) != NULL) //找到右子树的最小元素结点,保存到node中
{
node =left;
}
child = node->right; //child为node的右儿子,即node左儿子的兄弟结点
parent = node->parent; //parent为node结点的父结点
color = node->color; //color保存的是node结点的color
if(child) //node的右儿子不为空,则把其祖父结点作为父结点
{
child->parent = parent;
}
if(parent) //如果node结点的父结点不为空,即不为根结点吧?
{
if(parent->left == node) //进行结点的移动,即将node的孩子结点挂接到node的父结点上
{
parent->left = child;
}
else
{
parent->right = child;
} //同上
}
else //node的父结点为根结点
{
root = child;
}
if(node->parent == old) //node的父结点等于要删除的结点
{
parent = node;
}
node->parent = old->parent; //进行把右子树最小元素结点放到当前删除结点处的操作
node->color = old->color;
node->right = old->right;
node->left = old->left;
if(old->parent) //如果当前删除结点的父结点存在,即old不为根结点
{
if(old->parent->left == old) //当前删除结点是其父结点的左孩子
{
old->parent->left = node;
}
else
{
old->parent->right = node; //当前删除结点是其父结点的右孩子
}
}
else
{
root = node; //当前删除结点为根结点
}
old->left->parent = node;
if(old->right)
{
old->right->parent = node;
}
}//if(node->left && node->right)
else //此else语句处理的是:删除结点没有儿子、只有一个儿子时的情况
{
if(!node->left) //删除结点
{
child = node->right;
}
else if(!node->right)
{
child = node->left;
}
parent = node->parent;
color = node->color;
if(child)
{
child->parent = parent;
}
if(parent)
{
if(parent->left == node)
{
parent->left = child;
}
else
{
parent->right = child;
}
}
else
{
root = child;
}
}
free(old);
if(color == BLACK)
{
root= rb_erase_rebalance(child, parent, root); //调用rb_erase_rebalance来恢复红黑树性质
}
return root;
}