1、二叉查找树的不足
二叉查找树的基本操作包括搜索、插入、删除、取最大和最小值等都能够在O(h)(h为树的高度)时间复杂度内实现,因此能在期望时间O(lgn)下实现,但是二叉查找树的平衡性在这些操作中并没有得到维护,其高度可能会变得很高,当其高度较高时,二叉查找树的性能就未必比链表好了,所以二叉查找树的集合操作是期望时间O(lgn),最坏情况下为O(n)。由于二叉查找树的高度问题,因而出现了红黑树,红黑树能够保证在最坏情况下的集合操作时间都是O(lgn),红黑树因其性能较好,有很多的应用,例如在C++STL中的一些性能较好的容器(如:set,map)都是利用红黑树作为底层数据结构来支撑的。
2、红黑树
红黑树也是一种二叉查找树,它拥有二叉查找树的性质,同时红黑树还有其它一些特殊性质,这使得红黑树的动态集合基本操作在最坏情况下也为O(lgn),红黑树通过给节点增加颜色和其它限制条件,使得红黑树中的某一节点到叶节点的任一路径不会比其它路径长两倍,因此可以保持树的平衡性。红黑树中节点包括五个域:颜色、关键字、左节点、右节点和父节点,并将没有子节点或父节点的指针指向一个 nil 哨兵节点,这个nil节点称着叶节点,也是外节点,其它包含关键字的节点叫着内节点。之所以要增加这样一个为空的外节点,是为了方便红黑树上的一些边界操作,特别是在删除节点的时候,这个nil节点就很有用了。
红黑树的节点定义为:
color | key | left | right | parent |
2.1、红黑树的性质
红黑树的五个性质:
1、每个节点或者是红的,或者是黑的。
2、根节点是黑的。
3、叶节点(nil节点)是黑的。
4、如果一个节点是红的,则其孩子节点都是黑的。
5、对于任意节点,从该节点到其子孙叶节点(nil节点)的所有路径上包含相同数目的黑节点。
如下图:
那么为什么红黑树要定义这五个性质呢?其根本原因就是为了让红黑树能够自平衡,保证红黑树的高度为O(lgn),下面分别解释一下这五条性质:
性质1:定义了节点的颜色,是为后面的性质做铺垫的,不用细说。
性质2:一定要定义根节点为黑色,因为后面有性质4,定义根节点为黑色,则根节点的子节点的颜色就可以为红,也可以为黑,没有受到限制,否则如果定义根节点为红,则其子节点就只能为黑,就是实说红黑树的上两层节点颜色就定死了,这样显然让树不够灵活。
性质3:定义外节点nil节点为黑色,这时为红黑树操作边界情况时提供方便,大家在看到后面的删除节点的时候就会发现,如果定义为红色,若删除的节点在树的最底层,边界处理就能回考虑得更麻烦。
性质4:很重要,它说明了红黑树中如果有红节点,其子孙一定为黑节点,但是有黑节点,不一定要出现红节点,也就保证了整个红黑树中的任意路径上黑节点的个数至少是路径上节点总个数的一半,这为保证红黑树的高度任何时刻都为O(lgn)提供了重要依据。
性质5:就是为了让红黑树保持平衡,不出现普通二叉查找树可能出现的一边倒的情况。
因此红黑树的每个性质都是有特别意义的。
黑高度:从某个节点(不包括该节点)出发到达叶节点(nil节点)的任意一条路径上,黑节点的个数称为该节点的黑高度bh(x)。红黑树的黑高度就定义为根节点的黑高度。
引理:一颗有n个内节点的红黑树的高度至多为2lg(n+1)(可以利用节点的黑高度来证明),这也就保证了最坏情况下查找操作 O(lgn) 的时间复杂度。
这个引理很重要,它的得出就是由上面的五条性质支持的。下面我给出简单的证明:
设根节点为x,其黑高度为bh(x),则这棵红黑树节点最少的情况就是:高度h = bh(x) - 1,因为 bh(x) 包含了nil节点(为黑),所以路径上黑节点的实际数目应该是bh(x) - 1 ,在最长路径上没有红节点,全是黑节点,这时的节点总数是2^(h+1) - 1,也就是2^bh(x) - 1,所以红黑树的总结点数 n >= 2^bh(x) - 1,又因为红黑树的任意路径上黑节点个数至少是总结点个数的一半,所以bh(x) >= h/2(h为树的实际高度),所以n >= 2^(h/2) - 1,从而有h <= 2*lg(n+1),所以h = O(lgn)。
当然,这个引理也可以通过数学归纳法来证明,算法导论中就是如此。
红黑树在执行一些搜索,取最大最小值,取前驱后继节点等操作没有修改红黑树,因此跟二叉查找树上的操作相同,但是对于插入和删除操作,红黑树由于被修改了,必须执行一些额外的操作来维护其红黑树的性质,实际上这些额外的操作也不会改变插入和删除操作的时间复杂度,仍然为O(lgn)。
2.2、红黑树的旋转
为了维护红黑树的性质,需要执行一些红黑树特有的操作:旋转,包括左旋和右旋。
左旋:对节点x做左旋,必须保证它的右孩子y 不为nil节点,旋转以x到y之间的支轴进行,让y取代x的位置成为该子树新的根,而x成为y的左孩子,y原来的左孩子就成为x的右孩 子。时间复杂度为O(1)。
右旋:与左旋相反,对节点x做右旋,必须保证它的左孩子y不为nil节点,旋转以x到y的支轴进行,让y取代x的位置成为该子树新的根,而x成为y的右孩子,y原来的右孩子就成为 x的左孩子。时间复杂度为O(1)。
2.3、红黑树的插入和删除
在对红黑树执行插入和删除之后,需要执行额外的维护操作,这些操作就是建立在旋转之上的。下面分析一下在红黑树中插入和删除节点之后可能出现的问题,及解决思路:在插入或者删除节点之后,维护红黑树的性质的时候,都是只有一种情形可以直接通过变换来保持红黑树的性质,其它情形就必须通过变换转化到这一种可以直接解决的出口情形。
1、插入节点
向红黑树中插入一个新的节点,为了不增加黑节点的个数(从而影响黑高度),我们将新插入的节点涂为红色,这样插入之后它可能会违反性质2和4,在未付性质2和4的同时还得注意不能违反性质5,主要考虑这三个性质即可。因此维护的主要任务就是通过修改红黑树中节点的结构和颜色让根节点为黑色,或者让插入的红节点的父节点变成黑色(如果不是黑色)。
1)将新插入节点定为红色(因为这样不会增加黑节点,不会违反性质5),若违反红黑树的其它性质,则执行变换。
2)如果插入节点为根节点,则直接将其变为黑色即可;若不是根节点,则逐步修正,将违反规则的位置上移直到变成出口情形来直接解决。共分为三种情况:
因为当前插入节点为红,若父节点也为红,则违反性质4
(1)当前节点的叔叔节点为红,则祖父节点必然为黑色,可以将父节点和叔叔节点都涂黑,祖父节点涂红,然后将祖父节点变成当前节点,这样违规位置就上升了,循环算法。(祖父节点必须涂红,是为了保证黑高度保持不变,否则对上面的节点会产生影响)
(2)当前节点的叔叔节点为黑
(2.1)当前节点为其父节点的左孩子(出口情形)
可以将父节点涂黑,祖父节点涂红,然后沿祖父节点右旋,则当前节点的父节点变成了黑色,性质4满足了,同时性质5没有被破坏,维护完成,算法结束。
(2.2)当前节点为其父节点的右孩子
这种情形需要通过变换转换成情形2.1,以方便解决。即:将当前节点的父节点作为新的当前节点,以新的当前节点为支点左旋,从而转换成了情形2.1。
实际上,维护插入节点后红黑树的性质共分为三种情形:主要考虑插入节点的叔叔节点,如果叔叔节点为红,则通过不断上移违规位置直到叔叔节点为黑来解决,或者直接变成了根节点,根节点直接就涂黑了;如果叔叔节点为黑,这时候又分两种情形,如果当前节点是其父节点的左孩子,则可以直接解决,如果是右孩子,则变换成是左孩子的情形在解决。
2、删除节点
从红黑树中删除一个节点之后,如果这个节点是红的,则红黑性质没有被破坏,因为被删除的是红节点,所以不是根节点,性质2没问题,同时黑节点没有变,所以性质5没问题,因为被删除节点的父节点是黑,所以无论替补上去的节点是什么颜色都不会破坏性质4,性质1和3没有被影响,所以所有性质都没有破坏。如果被删除的节点是黑色的,则节点的黑高度变化了,性质5被破坏了,维护的主要任务就是通过修改节点的结构和颜色从而增加该子树一个黑高度。
在删除节点之后,主要考虑替补节点的兄弟节点,把替补节点作为当前节点,如果当前节点为红,则直接涂黑即可保持性质;如果当前节点为黑色,若被删除的节点为黑色,则当前节点所在子树的黑高度少了1,如果当前节点为根节点,则整个树的黑高度少1,没有影响,否则就要变换调整,同插入修正一样,将黑高度少1的违规位置上升,直到进入出口情形,直接解决。共有4种情形。如果删除的节点没有孩子,则替补节点就是nil外节点,保证了替补节点不为NULL,同时为黑色,方便操作。
1)当前节点为黑,兄弟节点为红
将这种情形转换成兄弟节点为黑的情形来处理,即:将当前节点的父节点涂红,兄弟节点涂黑,,以父节点为支点左旋,则当前节点的兄弟节点就为原来兄弟节点的左黑孩子,变成了兄弟节点为黑的情形。
2)当前节点的兄弟节点为黑
(2.1)兄弟节点的两个孩子全为黑
把兄弟节点涂红,让父节点成为新的当前节点,使使父节点为根的整个子树黑高度少1,违规位置上移。(蓝色代表任意颜色)
(2.2)兄弟节点的左孩子为红,右孩子为黑
将这种情形转换成兄弟节点的右孩子为红的情形来解决,即:将兄弟节点涂红,并将其左孩子涂黑,然后以兄弟节点为支点右旋,则当前节点的新的兄弟节点为元兄弟节点的左孩子,且想在兄弟节点的右孩子为红了。
(2.3)兄弟节点的右孩子为红,左孩子任意(出口情形)
这时出口情形,可以直接解决,可以先把兄弟节点涂成父节点的颜色,再把父节点和兄弟节点的右孩子涂黑,然后以父节点为支点左旋,此时所有性质都满足了;因为原本是当前节点的黑高度比兄弟节点少1,只与兄弟节点的孩子相同,经过这次变换,当前节点所在的位置变成了原来的黑父节点,黑高度增加了1,所以保持了性质5,而其它节点的黑高度也没有受到影响,依然没变。所以维护完成,算法结束。
/*
* 算法导论 第十三章 红黑树
*/
#include <iostream>
using namespace std;
//定义颜色
enum Color
{
BLACK = 0,
RED = 1
};
//定义红黑树节点
typedef struct RBTNode
{
Color color;
int key;
RBTNode *left, *right, *parent;
RBTNode()
{
this->color = Color::RED;
this->key = 0;
this->left = this->right = this->parent = NULL;
}
RBTNode(Color c, int k, RBTNode* l, RBTNode* r, RBTNode* p)
{
this->color = c;
this->key = k;
this->left = l;
this->parent = p;
this->right = r;
}
}RBTNode, RBTree;
//定义一个哨兵nilNode,以方便处理边界问题
//特别是在删除红黑树元素时很有用
// 如果删除节点没有孩子,nilNode作为其孩子处理起来就方便多了
RBTNode* nilNode = NULL;
/*
- 中序遍历
- 递归
/
void inOrderTreeWalkRecursion(RBTree tree)
{
if (tree && tree != nilNode)
{
inOrderTreeWalkRecursion(tree->left);
cout<<tree->key<<" ";
inOrderTreeWalkRecursion(tree->right);
}
}
/*
-
查找二叉排序树中的最小元素
-
即为最左元素
-
时间复杂度为O(lgn)
/
RBTNode rbTreeMinimum(RBTree* tree)
{
if (! tree || tree == nilNode)
return NULL;while (tree->left != nilNode)
{
tree = tree->left;
}return tree;
}
/*
-
求二叉排序树中指定节点node的中序遍历后继
-
如果node右子树不为空,则后继则为node右子树中的最小节点
-
否则node右子树为空,必须向上回溯找到第一个节点:该节点为其父节点的左孩子
-
后继即为其父节点,如果不存在这样的节点,说明node为最右节点,后继为空
-
时间复杂度为O(lgn)
/
RBTNode rbTreeSuccessor(RBTNode* node)
{
if (! node || node == nilNode)
return NULL;if (node->right != nilNode)
{
return rbTreeMinimum(node->right);
}RBTNode* temp = node->parent;
while (temp != nilNode && node == temp->right)
{
node = temp;
temp = node->parent;
}
return temp;
}
/*
- 红黑树节点左旋
/
void leftRotate(RBTree &tree, RBTNode* node)
{
//如果node为空或者其右节点为空,就返回
if (! node || node == nilNode || node->right == nilNode)
return;
//让node右节点的左孩子成为node的右孩子
RBTNode* temp = node->right;
node->right = temp->left;
if (temp->left != nilNode)
{
temp->left->parent = node;
}
//让node的右孩子取代node的位置
temp->parent = node->parent;
if (node->parent == nilNode)
{//说明node是根节点,修改根节点的指针
tree = temp;
} else {
if (node == node->parent->left)
{
node->parent->left = temp;
} else {
node->parent->right = temp;
}
}
//让node成为其右孩子的左孩子
temp->left = node;
node->parent = temp;
}
/*
- 红黑树节点右旋
/
void rightRotate(RBTree &tree, RBTNode* node)
{
//如果node为空或者其左节点为空,就返回
if (! node || node == nilNode || node->left == nilNode)
return;
//让node左节点的右孩子成为node的左孩子
RBTNode* temp = node->left;
node->left = temp->right;
if (temp->right != nilNode)
{
temp->right->parent = node;
}
//让node的右左孩子取代node的位置
temp->parent = node->parent;
if (node->parent == nilNode)
{//说明node是根节点,修改根节点的指针
tree = temp;
} else {
if (node == node->parent->left)
{
node->parent->left = temp;
} else {
node->parent->right = temp;
}
}
//让node成为其左孩子的右孩子
temp->right = node;
node->parent = temp;
}
/*
-
红黑树插入元素后,对树进行调整以保持红黑树的性质
-
主要耗时在将违规的位置上升,因为高度为O(lgn),所以其时间复杂度依然为O(lgn)
-
而旋转一共不超过2次,每次时间复杂度为O(1)
-
所以总的时间复杂度为O(lgn)
/
void rbInsertFixup(RBTree &tree, RBTNode* node)
{
if (! tree || ! node)
return;// 因为新插入的node颜色为红,只可能违反性质2(根节点为黑色)和性质4(红节点的孩子节点只能为黑)
// 所以如果node的父节点不是nilNode,则只有在其父节点也为红的时候才违反红黑树的性质,需要调整
// 否则不用调整
while (node->parent->color == Color::RED)
{ //主要考虑node的叔叔节点
RBTNode* uncleNode = NULL;
if(node->parent == node->parent->parent->left)
{
uncleNode = node->parent->parent->right;if (uncleNode->color == Color::RED) { // 情形1:node的叔叔节点为红,则将违规位置上移 // 因为node的父节点和叔叔节点均为红,则node的祖父节点必然为黑 // 因此可以将node的父节点和叔叔节点均变成黑,而祖父节点变成红 // 这样依然保持了整体的黑高度没变,保持了性质5(任意节点到其子孙节点的路径上黑节点个数相同) node->parent->color = Color::BLACK; uncleNode->color = Color::BLACK; node->parent->parent->color = Color::RED; node = node->parent->parent; } else { // 情形2:node的叔叔节点为黑 // 情形2.1:node为其父节点的右孩子,将情形2.1转化成2.2以便处理,因为node和其父节点都是红的,所以演父节点旋转没有<span style="white-space:pre"> </span> 影响 if (node == node->parent->right) { node = node->parent; leftRotate(tree, node);//从2.1变成了2.2 } // 情形2.2:node为其父节点的左孩子,这种情形为出口情形 // 因为父节点为红,叔叔节点为黑,则祖父节点一定为黑,所以将父节点变为黑 // 祖父节点变为红,再沿祖父节点右旋则黑高度没变,同时node的父节点变黑了 // 达到主要目的:让node的父节点变成黑色,以保持性质4 // 在这个过程中又不能改变黑高度,因为要保持性质5 node->parent->color = Color::BLACK; node->parent->parent->color = Color::RED; rightRotate(tree, node->parent->parent); } } else { //叔叔节点的位置相反,处理方法同上,只是旋转时候的方向相反 uncleNode = node->parent->parent->left; if (uncleNode->color == Color::RED) { // 情形1:node的叔叔节点为红,则将违规位置上移 node->parent->color = Color::BLACK; uncleNode->color = Color::BLACK; node->parent->parent->color = Color::RED; node = node->parent->parent; } else { // 情形2:node的叔叔节点为黑 // 情形2.1:node为其父节点的左孩子 if (node == node->parent->left) { node = node->parent; rightRotate(tree, node);//从2.1变成了2.2 } // 情形2.2:node为其父节点的右孩子,这种情形为出口情形 node->parent->color = Color::BLACK; node->parent->parent->color = Color::RED; leftRotate(tree, node->parent->parent); } }
}
//保持性质2,让根节点为黑
tree->color = Color::BLACK;
}
/*
-
插入元素
-
搜索插入点时间为O(lgn)
-
维护红黑树的性质时间为O(lgn)
-
所以总的时间复杂度为O(lgn)
/
void rbInsert(RBTree &tree, RBTNode* node)
{
if (! tree || ! node)
return;RBTNode* posNode = nilNode;
RBTNode* t = tree;
while (t != nilNode)
{
posNode = t;
if (node->key < t->key)
{
t = t->left;
} else {
t = t->right;
}
}node->parent = posNode;
if (posNode == nilNode)
{
tree = node;
} else {
if (node->key < posNode->key)
{
posNode->left = node;
} else {
posNode->right = node;
}
}
//不同于二叉排序树的地方
node->left = node->right = nilNode;
node->color = Color::RED;//插入红节点以保证黑高度不变
//维护红黑树性质
rbInsertFixup(tree, node);
}
/*
-
删除红黑树节点后维护红黑树的性质
-
主要耗时仍然是将违规位置上升,时间复杂度为O(lgn)
-
旋转最多3次,共O(1)
-
所以时间复杂度为O(lgn)
/
void rbDeleteFixup(RBTree &tree, RBTNode* node)
{
//如果node是根节点,则整个树的黑高度都减一,没有影响
//如果node的颜色是红色,则直接涂黑就可以了
// 否则就要维护了,红黑树节点删除之后,维护性质主要与node的兄弟节点有关
while (node != tree && node->color == Color::BLACK)
{
//当前节点node为非根黑节点,考虑其兄弟节点
RBTNode* brotherNode = NULL;
if (node == node->parent->left)
{
brotherNode = node->parent->right;
//情形1:兄弟节点为红,将其转换成兄弟节点为黑的情形2
//应为node节点为黑,兄弟节点为红,则其父节点必然为黑
// 且兄弟节点的子节点必然为黑,因此可以将父节点变红,兄弟节点变黑
// 然后沿父节点左旋,则黑高度没有被影响,同时node的新的兄弟节点变成了原兄弟节点的左黑孩子了
// 因此node的兄弟节点变为黑节点了
if (brotherNode->color == Color::RED)
{
node->parent->color = Color::RED;
brotherNode->color = Color::BLACK;
leftRotate(tree, node->parent);
brotherNode = node->parent->right;
}//情形2:兄弟节点为黑 if (brotherNode->right->color == Color::BLACK && brotherNode->left->color == Color::BLACK) { //情形2.1:兄弟节点的右孩子为黑,且其左孩子为黑 //这时node为黑,其兄弟节点和两个孩子节点也均为黑 //因为此时node子树比其兄弟节点的子树黑高度小1,所以可以把兄弟节点变红,从而二者黑高度相同 // 从而让node的父节点子树整个黑高度降低1(违反规则5),让node的父节点变成新的node,使违规的位置上升 // 继续循环 brotherNode->color = Color::RED; node = node->parent; } else { if (brotherNode->right->color == Color::BLACK) { //情形2.2:兄弟节点的右孩子为黑,且其左孩子为红,将这种情形转换成2.3 //因为兄弟节点为黑,其左孩子为红,右孩子为黑,因此可以将兄弟节点的左孩子变黑 //兄弟节点变红,然后沿兄弟节点右旋,从而进入情形2.3,且为改变黑高度 brotherNode->left->color = Color::BLACK; brotherNode->color = Color::RED; rightRotate(tree, brotherNode); brotherNode = node->parent->right; } //情形2.3:兄弟节点的右孩子为红,左孩子任意,这时出口情形,经过以下变换,算法结束 // 因为兄弟节点为黑,但其右孩子为红,可以把兄弟节点变成父节点的颜色,把其右孩子和node父节点变黑 // 然后沿node父节点左旋,则node的兄弟节点的位置仍然是黑色(为brother的右孩子),该子树的黑高度不变 // 而node节点所在子树因为增加了node父节点这个黑节点而黑高度增加1,所以两边的黑高度相同了 // (原来node子树比其兄弟子树黑高度少1),从而保持了性质5 brotherNode->color = node->parent->color; node->parent->color = Color::BLACK; brotherNode->right->color = Color::BLACK; leftRotate(tree, node->parent); //问题已解决,让node指向根节点,从而退出循环 node = tree; } } else {//兄弟节点的位置相反,原理同上 brotherNode = node->parent->left; //情形1:兄弟节点为红,将其转换成兄弟节点为黑的情形2 if (brotherNode->color == Color::RED) { node->parent->color = Color::RED; brotherNode->color = Color::BLACK; rightRotate(tree, node->parent); brotherNode = node->parent->left; } //情形2:兄弟节点为黑 if (brotherNode->left->color == Color::BLACK && brotherNode->right->color == Color::BLACK) { //情形2.1:兄弟节点的左孩子为黑,且其右孩子为黑 brotherNode->color = Color::RED; node = node->parent; } else { if (brotherNode->left->color == Color::BLACK) { //情形2.2:兄弟节点的左孩子为黑,且其右孩子为红,将这种情形转换成2.3 brotherNode->right->color = Color::BLACK; brotherNode->color = Color::RED; leftRotate(tree, brotherNode); brotherNode = node->parent->left; } //情形2.3:兄弟节点的左孩子为红,右孩子任意,这时出口情形,经过以下变换,算法结束 brotherNode->color = node->parent->color; node->parent->color = Color::BLACK; brotherNode->left->color = Color::BLACK; rightRotate(tree, node->parent); //问题已解决,让node指向根节点,从而退出循环 node = tree; } }
}
node->color = Color::BLACK;
}
/*
-
删除红黑树节点
-
主要耗时是在获取删除节点的中序遍历后继节点,时间复杂度为O(lgn)
-
而维护红黑树性质的时间复杂度为O(lgn)
-
所以总的时间复杂度为O(lgn)
/
RBTNode rbDelete(RBTree* &tree, RBTNode* node)
{
if (! tree || ! node)
return;RBTNode* delNode = NULL;
if (node->left == nilNode || node->right == nilNode)
{
delNode = node;
} else {
delNode = rbTreeSuccessor(node);
}RBTNode* fillNode = NULL;
if (delNode->left != nilNode)
{
fillNode = delNode->left;
} else {
fillNode = delNode->right;
}fillNode->parent = delNode->parent;
if (delNode->parent == nilNode)
{
tree = fillNode;
} else {
if (fillNode == fillNode->parent->left)
{
fillNode->parent->left = fillNode;
} else {
fillNode->parent->right = fillNode;
}
}if (delNode != node)
{
node->key = delNode->key;
}//如果被删除节点是黑色,则树中某些的黑高度被减一,必须维护性质5
//将该节点相关路径上增加一个黑节点
if (delNode->color == Color::BLACK)
{
rbDeleteFixup(tree, fillNode);
}return delNode;
}