数据结构之红黑树详解
红黑树原理
什么是红黑树
红黑树是一种特殊的二叉查找树,用来实现对大批量有序数据的管理操作。
二叉查找树,即是特殊的二叉树,树中每个节点的左子树的所有元素都不大于该节点的元素,右子树的所有元素都不小于该节点。
相比普通的二叉查找树,红黑树中的每个节点增加了色域(color field)这一数据项,色域取值可以是红色或者黑色。通过巧妙的设计,红黑树可以保证在任何情况下,对数据的有序插入,查找和删除操作,都能在O(logn)的时间复杂度内完成。
为什么要用红黑树
红黑树是一种性能优异的数据结构,经常用作其他数据结构如map等的底层实现。二叉查找树,包括随机二叉查找树,依然会在某些极端情况下,表现出O(n)的时间复杂度,并不能保证总是高效的处理数据。而红黑树,作为一种平衡查找树,可以保证整棵树的高度不超过2log(n+1),从而保证了时间复杂度限制在O(logn),保证了算法在最坏情况下表现依然能满足我们所期望的效率要求。
红黑树的性质(Red-Black properties)
红黑树的优秀性能,依靠如下性质来保证,满足如下性质的二叉查找树,即是红黑树。
- 每个节点都是红色或者黑色。
- 根节点和叶子节点都是黑色。
- 红色节点的子节点或者父节点都必须是黑色。
- 每个节点到其任一后代叶子节点的所有简单路径上的黑色节点数目相同。
对上述性质进行如下说明:
- 红黑树在二叉树的基础上,将每个叶子节点的空指针替换为nil节点作为新的叶子节点,这些nil节点称为外部节点(我们只关心外部节点的颜色,对他们的其他性质没有要求,因此可以用一个哨兵节点统一代替所有的外部节点)而原有节点称为内部节点。
- 性质3要求红色节点的父节点或者子节点为黑色,即任何一条从根到叶子的简单路径上不能出现两个连续的节点是红色。
- 根节点和叶子节点要求为黑色,而不能为红色,是为了避免给内部节点添加不必要的限制。
- 任一节点到其后代叶子节点的简单路径上的黑色节点数称为黑高(black height),叶子节点的黑高为0,黑高不计入该节点本身的颜色。
通过上述的性质规约,可以证明红黑树的操作时间复杂度为O(logn),n为内部节点的数量(数据的总量)。
首先通过归纳法证明,对任意一节点x,以其为根的子树中至少含有2bh(x)-1个内部节点。bh(x)为黑高。
- 对于叶子节点,bh(x) = 0,2bh(x)-1=0, 条件成立。
- 对任意内部节点x,其子节点的黑高为bh(x)(当子节点是红色节点)或者bh(x)-1(当子节点是黑色节点),则x节点的子节点至少含有2bh(x)-1-1个内部节点,故x为根的子树含有1+2bh(x)-1-1+2bh(x)-1-1=2bh(x)-1个内部节点。
- 设h为树的高度,根据红黑树的性质2,任何一条根到叶子的简单路径上的黑色节点至少有一半,即根的黑高至少为h/2,故有n>=2h/2-1,即h<=2log(n+1)。
而二叉查找树的增删查改时间复杂度均为O(h),从而我们可以保证红黑树在任意情况下都有O(logn)的时间复杂度。
C语言实现
红黑树优良的效率性能。是牺牲了算法上的复杂度。在进行增删操作时,需要对红黑树加以修正来满足红黑树的四条性质。主要通过二叉树的旋转和节点的变色来实现。
同时要注意,在插入新节点的时候,我们默认新节点的颜色为红色,这是因为,插入黑色节点,必然会改变某些简单路径上的黑色节点数目,很有可能使得所有的简单路径黑色节点数目不等,带来额外的操作调整。
在本实现中,采用了哨兵节点来取代所有的NULL指针,简化边界条件。
数据结构定义部分
//头文件部分
#define RED 0 // 红色节点
#define BLACK 1 // 黑色节点
typedef int Type; //支持后续数据类型扩展
// 红黑树的节点
typedef struct RBTreeNode{
unsigned int color; // 颜色(RED 或 BLACK)
Type key; // 关键字(键值)
struct RBTreeNode *left; // 左孩子
struct RBTreeNode *right; // 右孩子
struct RBTreeNode *parent; // 父结点
}Node;
typedef struct {
Node *root; //根节点
Node nil; //哨兵元素
}RBTree;
创建红黑树
RBTree * init_tree()
{
RBTree *tree = (RBTree*)malloc(sizeof(RBTree));
tree->nil.color = BLACK;
tree->root = &tree->nil;
tree->root->parent = &(tree->nil);
return tree;
}
初始化一个空节点
Node* init_node(Type key)
{
Node *bt =(Node*)calloc(1,sizeof(Node));
bt->color = RED;
bt->right = NULL;
bt->left = NULL;
bt->key = key;
bt->parent = NULL;
return bt;
}
节点插入
- 插入过程:
- 节点插从树根开始查找,遇到叶子节点nil后插入‘
- 如果该叶子节点的父节点为nil,说明树为空,将插入的节点设为根节点;
- 通过变色和旋转操作修正红黑树使其满足红黑性质。
插入函数
void insert_node(RBTree *tree, Node *node)
{
Node *p,*n;
p = tree->root->parent; //指示父节点
n = tree->root; //寻找插入位置
while(*n != tree->nil)
{
p = n;
if(node->key < n->key)
{
n = n->left;
}
else
{
n = n->right;
}
}
//循环终止时,n为nil节点,p为n的父节点,用node取代n的位置
node->parent = p;
if(&p == tree->nil)
{
tree->root = node;
}
else
{
if(node->key < p->key)
{
p->left = node;
}
else
{
p->right = node;
}
}
node->color = RED;
node->left