红黑树 ,是一种 二叉搜索树 ,但 在每个结点上增加一个存储位表示结点的颜色,可以是 Red 或 Black 。 通过对 任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍 ,因而是 接近平衡 的。
请在阅读前文的AVL树相关文章之后学习红黑树:C++ AVLTree-CSDN博客
1.红黑树及其基本规则
1.1 基础规则
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
- 每个叶子结点都是黑色的 ( 此处的叶子结点指的是空结点 )
对于规则3,可以理解为:一条路径中没有连续的红色节点。
对于规则4,可以理解为:每条路径黑色节点数量相等。
从规则中我们也可以看出:
AVL树严格平衡,能保证所有的分支之间高度差小于1
红黑树近似平衡,最长路径不会超过最长路径的两倍。
因为红色不能连续,所以:
最短情况:全黑
最长情况:一黑一红
并且黑色数量都是一样的,所以最长的就是比最短的多一倍节点。
最长和最多只是理想状态,每一棵树不一定有最长的也不一定有最短的。
第五点,也叫NIL(nullptr)节点
其实不考虑NIL节点也可以。NIL节点主要便于计数路径数,比如上图有11个NIL节点,就有11个路径。
1.2 红黑树和AVL树的查找效率
前文说到,AVL高度差不会超过1,红黑是高度不差过一倍。
所以只讨论find函数,AVL树肯定更快
但是对于红黑树,最短路径LogN 最长路径2*LogN,所以其时间复杂度其实都是O(N)
并且因为LogN足够小
所以对cpu的运算速度来说,LogN和2*LogN没区别。因此可以认为红黑树与AVL树的效率是差距不大的。
反而在插入和删除元素时红黑树更有优势,调整起来没有那么麻烦。
1.3 红黑树的节点定义
cpp
enum Color {
RED,
BLACK
};
template<typename k,typename v>
struct RBTreeNode {
typedef RBTreeNode<k, v> Node;
RBTreeNode(const pair<k,v>& kv = make_pair(k(),v()))
:_parent(nullptr)
,_left(nullptr)
,_right(nullptr)
,_kv(kv)
,_color(RED)
{}
Node* _parent;
Node* _left;
Node* _right;
pair<k, v> _kv;
Color _color;
};
template<typename k,typename v>
class RBTree {
public:
typedef RBTreeNode<k, v> Node;
private:
Node* _root = nullptr;
};
2. 红黑树的插入
以此树为例:
这个情况下要添加节点,加红节点还是黑节点呢?
无论怎样加都会破坏规则。
插入一个黑色的,会让插入新节点的路径和其他所有路径都一定矛盾(黑节点数量变了)。
插入一个红色的,如果是在黑色节点下面插入,就无需调整;如果在红色节点下面插入红色,依然存在问题。
插入黑节点一定有问题,插入红节点可能有问题:
所以,要插入新节点时,我们无脑插入红色节点,然后再逐一遍历向上调整。
具体调整分析如下:
红黑树的调整中,多通过观察三代:子cur 父parent 叔叔uncle 爷爷grandfather
不需要调整的一类插入:
直接在parent下面插入一个红色节点。
这是最理想的情况,不需要调整。
++那么我们是否可以把所有需要调整的情况都往这种不需要调整的方向靠拢呢?++
需要调整:
注意:此处看到的所有树都有可能是完整的树或者一棵子树。
第一种 parent和uncle都是红色(改色)
最简单的时候 abcde都是空树 也就是说 cur是我们插入的第一个节点。
解决方法:
需要将p和u都变黑,然后g变红
调整完之后,如果g是根,需要将g改为黑色;如果不是根,需要检查g和他的_parent节点的颜色关系,如果是两个红,需要进入新一轮的调整。
子树有一个黑色节点时:
cde可能是x y z中的一种,此时要在a或b的下面插入一个节点
新增的记做cur 其父节点记作p 父节点因为是红色所以一定还有父节点,记作g
思路不变:父亲和叔叔变黑,爷爷变红,然后爷爷变cur,再往上调整。
至于往上调整时是哪种情况需要重新判断。
其实不管子树有多少层,都可以只看成一种情况。因为我们插入时都是直接插红色,而以上都是调整的部分。cur可以作为新增的元素,也可以在上一轮中被调整的元素,不用可以纠结子树到底长什么样。
第二种 uncle是黑或者不存在(旋转+改色)
叔叔不存在的时候不能贸然的把父节点变黑。单纯变黑不能解决问题(会改变路径上黑节点的数量)
旋转解决
旋转后注意,parent要变黑,grandparent要变红,也就是交换了parent和grandparent的颜色。
先看单旋的情况(cur,p,g成一条直线,并且此处u是空):
再看双旋的情况:
双旋旋转一次就能得到单旋的情况。
例如,需要双旋并且u存在的时候(此时abc必定是有黑色节点的):
(这个情况一定是先经过其他调整才得到的,因为abc中必然有黑色节点)
记忆:旋转时要让g和c变成p的左右节点,所以要让g的颜色变成红色,p变成黑色
旋转一定会存在单旋或者双旋 ,由于前文avl树中有详细解释,此处不再多介绍。
总结:遇到连续的红节点,关键看叔叔。
进一步分析:在以上逻辑中,每一个插入的节点原本都是红色。如果他是黑色,说明这是一个已经经历过调整的节点。
3. 代码实现:插入后的遍历调整
经过上述分析,首先parent对应的节点需要是红色才需要我们进一步调整。并且如果parent是红色,说明parent一定不是根,grandfather一定存在。
为了控制高度差,我们又没有_bf来作为标志,只能先分parent在g的左和右两个大类来讨论
然后再 将grandparent的值赋值给cur 然后parent的值变成cur->_parent
while里面又判断了一下parent是不是为空:
1、parent为空,parent已经不存在了说明cur就是根了,出循环之后处理根的颜色即可
2、parent存在,且为黑。这是最理想的状态,不需要再调节,直接出循环。
3、parent存在且为红,进入新一轮的调整循环。
此时读者容易有的问题:
1、为什么先只写uncle为红的情况?
答:因为uncle为红一定是第一个需要调整的情况
换句话说
这样一个节点中,cur不可能是新增节点。
否则原来的黑色数量就不对。
比如一种会uncle是黑的情况:
第二轮循环才会遇到uncle是黑。
2、出循环之后如何处理根的颜色?答:直接_root->_colour = BLACK;即可
因为根节点的颜色变化是唯一一个不会影响**"所有路径黑色节点数相等"这一条件的**
接着实现uncle是黑的情况:
由AVL树处可知,旋转分为单旋和双旋。单纯的一边高(如上图)是单旋,非单纯的需要采用双旋,因此我们还要继续判断:
cpp
//uncle 为黑或者不存在(旋转+变色)
if (cur = parent->_left) {
// g
// p u
//c
//都是同一边高,采用单旋即可
RotateR(grandfather);
parent->_color = BLACK;
grandfather->_color = RED;
}
else {
// g
// p u
// c 采用双旋
RotateL(parent);
RotateR(grandfather);
cur->_color = BLACK;
grandfather->_color = RED;
}
并且旋转之后都可以直接Break,不用像情况1一样再往上调整。
因为旋转之后的颜色改变让我们目前操作的这课子树的"根"变成了黑色,没有改变任意路径的黑色节点数量,**使该子树与调整之前一样并且还解决了新加入的节点。**无需往上调节。
整体代码:
cpp
while (parent && parent->_color == RED) {
Node* grandfather = parent->_parent;
if (parent == grandfather->_left) {
// g
// p u
Node* uncle = grandfather->_right;
//叔叔为红,改色处理即可
if (uncle && uncle->_color == RED) {
parent->_color = uncle->_color = BLACK;
grandfather->_color = RED;
cur = grandfather;
parent = cur->_parent;//进入新的一轮循环之后,
//如果parent不存在,则cur已经到根了
}
else {
//uncle 为黑或者不存在(旋转+变色)
if (cur == parent->_left) {
// g
// p u
//c
//都是同一边高,采用单旋即可
RotateR(grandfather);
parent->_color = BLACK;
grandfather->_color = RED;
}
else {
// g
// p u
// c 采用双旋
RotateL(parent);
RotateR(grandfather);
cur->_color = BLACK;
grandfather->_color = RED;
}
break;
}
}
else if (parent == grandfather->_right) {
//与上述同理
}
}
_root->_color = BLACK;
return true;
旋转逻辑与AVL树同理:
cpp
void RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) {
subRL->_parent = parent;
}
subR->_left = parent;
Node* parentParent = parent->_parent;
parent->_parent = subR;
if (parentParent == nullptr) {
this->_root = subR;
}
else {
if (parentParent->_left == parent) parentParent->_left = subR;
if (parentParent->_right == parent) parentParent->_right = subR;
}
subR->_parent = parentParent;
}
void RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {
subLR->_parent = parent;
}
Node* parentParent = parent->_parent;
parent->_parent = subL;
subL->_right = parent;
if (parentParent==nullptr) {
this->_root = subL;
}
else {
if (parentParent->_left == parent) parentParent->_left = subL;
if (parentParent->_right == parent) parentParent->_right = subL;
}
subL->_parent = parentParent;
}
实现几个简单接口(能坚持到这里并理解红黑树大逻辑的各位应该都能轻松搞定下列接口了吧):
因为 外部不能掉_root,所以包一层。
4. 检测红黑树
1.中序判断是否是有序的。
2.遇到红节点就检查其父亲是否是红,判断是否有红色连续。
这个不难解决,遍历的时候检查就行。
- 黑色节点的数量。
遍历每个节点时记下每个节点的到根节点的路径上有多少黑色节点。
思路:遍历一条路径获得一个基准值。之后每一条路径遇到空的时候都与基准值做比较
递归中我们加入一个blackNum 作为形参,每一层栈帧中都带上blackNum,不需要使用引用或者指针或者容器
还可以顺带检查下有无连续红色节点。
cpp
bool IsBalance() {
if (_root == nullptr) {
return true;
}
if (_root->_color == RED) {
return false;
}
int refnum = 0;//作为比较的标准值
Node* pnode = _root;
while (pnode) {
if (pnode->_color == BLACK)refnum++;
pnode = pnode->_left;
}
return check(_root, 0, refnum);
}
bool check(Node* cur, int BlackNum, const int refnum) {
if (cur == nullptr) {
//如果走到头了
if (BlackNum == refnum) {
return true;
}
else {
cout << "黑色节点数量不一致" << endl;
return false;
}
}
else {
//如果没走到头
if (cur->_color == RED) {
if (cur->_parent->_color == RED) {
cout << "有连续红色节点" << endl;
return false;
}
}
else {
BlackNum++;
}
}
return check(cur->_left, BlackNum, refnum) &&
check(cur->_right, BlackNum, refnum);
}
效率比较:
同时拿很多数据插入AVL和RB树,RB确实会高一点,但是旋转次数也少一点。
红黑树和 AVL 树都是高效的平衡二叉树,增删改查的时间复杂度都是 O(logN) ,红黑树不追 求绝对平衡,其只需保证最长路径不超过最短路径的 2 倍,相对而言,降低了插入和旋转的次数, 所以在经常进行增删的结构中性能比 AVL 树更优,而且红黑树实现比较简单, 所以实际运用中红黑树更多。