版权声明:原创不易,转载请注明转自weewqrer 红黑树
红黑树简介
首先红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或者BLACK。通过对一条从根节点到NIL叶节点(指空结点或者下面说的哨兵)的简单路径上各个结点在颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是近似平衡的。
用途
红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。对于查找、插入、删除、最大、最小等动态操作的时间复杂度为O(lgn).常见的用途有以下几种:
- STL(标准模板库)中在set map是基于红黑树实现的。
- Java中在TreeMap使用的也是红黑树。
- epoll在内核中的实现,用红黑树管理事件块。
- linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块
红黑树 VS AVL树
常见的平衡树有红黑树和AVL平衡树,为什么STL和linux都使用红黑树作为平衡树的实现?大概有以下几个原因:
从实现细节上来讲,如果插入一个结点引起了树的不平衡,AVL树和红黑树都最多需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而RB-Tree最多只需3次旋转,只需要O(1)的复杂度
从两种平衡树对平衡的要求来讲,AVL的结构相较RB-Tree来说更为平衡,在插入和删除node更容易引起Tree的unbalance,因此在大量数据需要插入或者删除时,AVL需要rebalance的频率会更高。因此,RB-Tree在需要大量插入和删除node的场景下,效率更高。自然,由于AVL高度平衡,因此AVL的search效率更高。
总体来说,RB-tree的统计性能是高于AVL的。
关于参考书
《算法导论》和《数据结构与算法分析》是大家常用的两本算法书,针对红黑树这一章,这两本书上也都有,但是二者从数据结构到使用的方法上都不一样,这里我推荐使用《算法导论》。有以下几个原因:
- 《数据结构与算法分析》中使用的数据结构没有保存父亲结点,所以在调用旋转函数时需要用函数返回值来保持上下结点的连接,这在avl树中显得很简洁,因为在avl中的情况比较简单,但是在红黑树中涉及到了祖祖父、祖父、父亲、儿子、四代结点,按照这本书上的方法得保存GGP、GP、P、X四个全局变量的值,在更新它们的指向时非常容易搞混。
- 《数据结构与算法分析》没有实现删除操作,只是描述了实现的方法,按照这上面的方法我也做了实现,最后发现代码非常长,至少150+,不如《算法导论》中的简洁。
- 另外,《算法导论》中描述的方法比较完整,思路严谨。
红黑树详解
红黑树性质
红黑树是每个结点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
- 列表项结点是红色或黑色。
- 根是黑色。
- 所有叶子都是黑色(叶子是NIL结点)。
- 每个红色结点必须有两个黑色的子结点。(从每个叶子到根的所有路径上不能有两个连续的红色结点。)
- 从任一结点到其每个叶子的所有简单路径都包含相同数目的黑色结点。
为了便于处理红黑树中的边界情况,使用一个哨兵来代表所有的NIL结点,也就是说所有指向NIL的指针都指向哨兵T.nil。
红黑树数据结构
typedef enum ColorType {RED, BLACK} ColorType;
typedef struct rbt_t{
int key;
rbt_t * left;
rbt_t * right;
rbt_t * p;
ColorType color;
}rbt_t;
typedef struct rbt_root_t{
rbt_t* root;
rbt_t* nil;
}rbt_root_t;
/*
*@brief rbt_init 初始化
*/
rbt_root_t* rbt_init(void){
rbt_root_t* T;
T = (rbt_root_t*)malloc(sizeof(rbt_root_t));
assert( NULL != T);
//用一个哨兵代表NIL。
T->nil = (rbt_t*)malloc(sizeof(rbt_t));
assert(NULL != T->nil);
T->nil->color = BLACK;
T->nil->left = T->nil->right = NULL;
T->nil->p = NULL;
T->root = T->nil;
return T;
}
红黑树旋转
搜索树操作inert和delete在含有n个关键字的红黑树上,运行花费时间为 O(lgn) 。由于这两个操作对树做了修改,所以有可能违反上面的性质,所以需要改变树中某些结点的颜色以及指针的结构。
指针结构的修改是通过旋转来完成的,这是一种能保持二叉搜索树性质的局部操作。一种有两种旋转操作,如下图所示,都在O(1)时间内完成。
这里要求x,y都不是T.nil。
c代码:
/*
*@brief rbt_left_rotate
*@param[in] T 树根
*@param[in] x 要进行旋转的节点
*/
void rbt_left_rotate( rbt_root_t* T, rbt_t* x){
rbt_t* y = x->right;
x->right = y->left;
if(x->right != T->nil)//更新某结点的父亲时,要确定此结点不是T.nil
x->right->p = x;
y->p = x->p;
if(x->p == T->nil){
//如果x以前是树根,那么现在树根易主了
T->root = y;
}else if(y->key < y->p->key)
y->p->left = y;
else
y->p->right = y;
y->left = x;
x->p = y;
}
/*
*@brief rbt_right_rotate
*@param[in] 树根
*@param[in] 要进行旋转的节点
*/
void rbt_right_rotate( rbt_root_t* T, rbt_t* x){
rbt_t * y = x->left;
x->left = y->right;
if(T->nil != x->left)
x->left->p = x;
y->p = x->p;
if(y->p == T->nil)
T->root = y;
else if(y->key < y->p->key)
y->p->left= y;
else
y->p->right = y;
y->right = x;
x->p = y;
}
红黑树插入
在红黑树中插入一个元素,跟在二叉搜索树中插入一个元素一样,只是插入一个元素之后,有可能使得这个树不再平衡,所以要再处理一下,使之重新回到平衡状态。
图中N为新插入的结点,U为它的叔叔。
插入操作的关键在于以下几点:
- 新插入的节点一定是红色的。(如果是黑色的,会破坏条件5)
- 如果新插入的节点的父亲是黑色的,则没有破坏

409

被折叠的 条评论
为什么被折叠?



