红黑树的本质上也是一棵二叉搜索树,它给每个结点都增加了颜色用来标记结点,颜色可以是红色也可以是黑的;通过对颜色的限定,红黑 树可以实现从根结点开始到任意一个空结点的位置都不会超过其他路径的两倍。也就是说红黑树不像AVL树那样要求绝对的平衡,但是通过一些规则的限定,可以实现这棵树里最长路径不会超过最短路径的两倍(最短路径和最长路径不一定存在)。
一、红黑树的规则
1、每个结点的颜色不是黑色就是红色。
2、根结点一定是黑色的。
3、如果一个结点的颜色是红色的,那么它的两个孩子结点的颜色必须是黑色,也就是说,在任意一条路径内,不能出现连续的红色结点。
4、对于任意一个结点,从该结点到其他所有NULL结点的路径上,都包含相同数量的黑色结点。
由规则4可知,从根结点到任意一个NULL结点的路径上都有相同数量的黑色结点,所以在极端的情况下,这条路径中的所有结点都是黑色结点,此时这条路径就是最短路径,假设这条路径的长度为bh。
由规则2、3可知,一棵树根结点一定是黑色的,并且不会出现连续的红色结点,那么在极端情况下,这棵树里最长的路径的组成就是一黑一红,由上一点我们可以知道黑色结点的个数是bh个,那么最长路径的长度就是2*bh。
所以综合红黑树的四点规则来看,理论上最短路径的长度是bh,最长路径的长度是2*bh,并且并不是所有的红黑树都存在最短路径和最长路径,设根结点开始到任意一个NULL结点位置的长度x,那么bh <= x <= 2*bh。
二、红黑树的效率
设在一个有N个结点的红黑树中最短路路径的长度为h,则有 由此可以推出 也就是说,在这棵红黑树中增删查改的最坏结果需要走过的路径也就只是2*logN,这样红黑树的时间复杂度还是O(logN)。
红黑树虽然相较于AVL来说没有那么绝对平衡,但是在效率上,两者都属于同一个档次,也是因为红黑树的平衡控制的没有那么严格,所以在插入数据时,红黑树需要旋转的次数更少,在实际中应用的更多。
三、红黑树的结构
红黑树的结点和AVL树的结点在结构上只是把AVL树的平衡因子替换成了用来标记结点的颜色,颜色我们也用一个枚举来表示,方便后续的区分和操作。
cpp
enum Color
{
BLACK,
RED
};
template<class T>
struct RBTreeNode
{
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
Color _col;
T _key;
RBTreeNode(const T& key = T())
:_key(key)
,_parent(nullptr)
,_left(nullptr)
,_right(nullptr)
,_col(RED)
{}
};
和所有的树形结构一样,我们都只需要一个根节点就能找到这一整棵树,所以红黑树的成员变量也就只有一个根结点。
cpp
template<class T>
class RBTree
{
typedef RBTreeNode<T> Node;
public:
RBTree()
{
}
private:
Node* _root = nullptr;
};
四、插入数据
这是红黑树里最重要也是比较难懂的部分,但是我们已经讲解过AVL树的插入数据,那么这里的难度就下降了很多。首先我们要确定插入新的结点时,这个结点应该是红色还是黑色的。
首先如果这是一个空树,那么插入的第一个结点就是根结点,红黑树规定了根结点的颜色一定是黑色,所以插入的第一个结点一定是黑色的;在后续插入的结点中,假设我们新插入的结点的颜色是黑色,那在插入第二个结点时,如果想要满足规则四,任意一个结点到任意NULL的路径上的黑色结点的数量都相同的话,只能把根结点变成红色,但是我们也规定了根结点一定要是黑色的,所以如果插入新的结点是黑色的话,第二个结点就会完全破坏红黑树的性质,并且无法同时满足四条规则;同时如果在一棵已有的红黑树中插入黑色结点的话,那么从任意结点到新结点的路径上就会多出一个黑色结点,那么就要进行调整,也就是说每次插入新的结点都要进行调整,这样的效率是很低的。因此我们插入的新的结点的颜色一定是红色的,当插入的结点是根结点时,我们进行特殊处理就好了。
当我们插入一个数据时,这个新结点的颜色是红色的,由规则3我们可以知道,如果插入新结点的父亲结点是黑色时,插入新结点是不需要调整的,因为新插入的结点不会违反红黑树的四条规则,当插入的结点的父亲结点是红色时,此时出现了连续的红色结点,我们就要调整了。
首先寻找插入数据的方式和所有的搜索二叉树都是一样的,通过比较结点的_key值找到待插入的位置,如果待插入数据已经存在,则返回false。
这里我先解释一下几个变量的意思,cur就是新插入的结点,parent结点就是新插入结点的父亲结点,uncle结点就是新插入结点父亲的亲兄弟结点,我们用叔叔(u)代称,grandFather就是新插入结点父亲的的父亲,我们用祖父(g)代称。
情况一:u存在且为红
此时我们可以确定的是p和u的颜色一定是红色,g的颜色一定是黑色,因为如果p是黑色,此时插入新结点就不需要进行调整,同时如果g的颜色也是红色,那么在插入p的时候就要进行调整了,而u的颜色是情况一限定的,所以此时我们要做的第一件事肯定是把p的颜色变成黑色,这样才能消除连续的红色结点,但是如果只把p变成黑色的话,此时从根结点出发到新插入结点出的NULL路径上的黑色结点就会比其他路径上的多出来一个,所以我们还需要改变g的颜色,但是改变了g的颜色以后,从根结点出发经过u结点路径上的黑色结点数量又会少一个,所以我们还要把u结点的颜色改成黑色,此时路径上的黑色结点数量就符合要求了,但是我们不能确定g的父亲结点的颜色,所以还要继续把g结点当成是新插入的结点,继续向上查看,如果g的父亲结点是黑色,那么就不需要调整,如果g的父亲结点是红色,那么就继续进行调整即可。
总结来说这种情况就是:把父亲结点和叔叔结点变成黑色,再把祖父结点变成红色,同时再把祖父结点当成是新插入的结点继续向上调整。
情况二:c的方向与p的方向一致同时u不存在或者u存在颜色为黑
此时能确定是c是红色,p是红色,g是黑色,此时如果u不存在,那么c一定是新插入的结点,因为此时如果c的下方还有结点的话,那么c的下方就一定还要黑色结点,那么此时从根结点出发经过c到达NULL的路径和从根结点出发到到u位置NULL的路径上的黑色结点的数量是不一样的,所以此时c一定是新插入的结点;如果u存在且为黑,同理可以退出c的下方一定还有其他的黑色结点,因此c一定不是新插入的结点。
处理这种情况首先也一定要把p变成黑色,此时u不存在或者是黑色时,就不能采用情况一的方式来解决,因为不能通过改变u来解决,但是是此时我们可以通过旋转g来重新符合规则,此时只要把g旋转下来以后,此时经过c的路径上黑色结点的数量就恢复到正常了,但是经过g节点的路径黑色结点的数量还是多一个,我们再把g变成红色,因为u可能不存在或者u存在为黑,并不会影响到红黑树的性质,此时整棵树内黑色结点的数量并没有发生改变,所以此时就不需要继续向上调整了。
情况三:c的方向与p的方向不一致同时u不存在或者u存在颜色为黑
和情况二能够确定的一样,如果u不存在,那么c一定是新插入的结点,如果u存在且颜色为黑,那么c一定不是新插入的结点。
此时的处理方式和上面有区别,此时和AVL树的双旋有些类似,进行了双旋以后,c结点就替代了g的位置,此时p和g就变成了c的两个孩子,此时把c变成黑色,g变成红色就能完成调整了,这种情况下黑色结点的数量同样没有增加,所以也不需要继续向上调整了。
此时还需要注意一点,如果一直是情况一一直向上调整,那么最后会把根结点也改成红色,为了处理这种情况,我们可以在插入了每个结点时,不论有没有改变到根结点,都把根结点的颜色设置为黑色,这样就会永远符合规则。、
cpp
bool Insert(const T& data)
{
//插入前为空树
if (_root == nullptr)
{
_root = new Node(data);
root->_col = BLACK;
return true;
}
else
{
//寻找待插入的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
parent = cur;
if (cur->_key < data)
cur = cur->_right;
else if (cur->_key > data)
cur = cur->_left;
//待插入数据已经存在
else
return false;
}
cur = new Node(data);
if (data > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
while (parent->_col == RED)
{
Node* grandFather = parent->_parent;
if (parent == grandFather->_left)
{
Node* uncle = grandFather->_right;
//情况一:u存在 且u为红
//把p和u变黑 g变红 然后把g当成新的c继续向上处理
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandFather->_col = RED;
cur = grandFather;
parent = cur->_parent;
}
//情况二、三:u不存在 或者u存在但是为黑
//旋转 + 变色
else
{
if (cur == parent->_left)
{
//情况二: g 单旋 + 变色
// p u
// c
RotateR(grandFather);
parent->_col = BLACK;
grandFather->_col = RED;
}
else
{
//情况三: g 双旋 + 变色
// p u
// c
RotateL(parent);
RotateR(grandFather);
cur->_col = BLACK;
grandFather->_col = RED;
}
//旋转以后最上方的结点就变成了黑色 因此它上方的结点是红色还是黑色都不会违反规则
//就不需要继续向上更新了
break;
}
}
//与上面的情况相反
else
{
Node* uncle = grandFather->_left;
//上述情况一
if (uncle && uncle->_col == RED)
{
grandFather->_col = RED;
parent->_col = BLACK;
uncle->_col = BLACK;
cur = grandFather;
parent = cur->_parent;
}
//u不存在或者u存在为黑
else
{
if (cur == parent->_right)
{
//上述情况二: g 单旋 + 变色
// u p
// c
RotateL(grandFather);
parent->_col = BLACK;
grandFather->_col = RED;
}
else
{
//上述情况三: g 双旋 + 变色
// u p
// c
RotateR(parent);
RotateL(grandFather);
cur->_col = BLACK;
grandFather->_col = RED;
}
break;
}
}
}
//保证根为黑 同时代价很小
root->_col = BLACK;
return true;
}
}
上面代码中的旋转操作和AVL树里的是一样的,只需要把AVL树里旋转对平衡因子的操作删除掉就行了,具体代码可以参考一下我上一篇关于AVL树的博客,这里也贴一下地址:AVL树的模拟实现