【C++进阶】红黑树的介绍及实现
🥕个人主页:开敲🍉
🔥所属专栏:C++🥭
🌼文章目录🌼
1. 红黑树的概念
红黑树是一颗二叉搜索树,他的每个节点增加一个存储位来表示节点颜色,红色或者黑色。通过对任何一条从根到叶子的路径上各个节点的颜色进行约束,红黑树能够确保没有任何一条路比其他任何一条路的路径长2倍以上,因而红黑树非常接近平衡二叉搜索树。
1.1 红黑树的规则
① 每个节点不是红色就是黑色。
② 根节点必须是黑色的。
③ 如果一个节点是红色,则它的两个孩子节点必须是黑色,也就是不能够出现连续的两个红色节点。
④ 对于任意一个节点,从该节点到其所有NULL节点的简单路径上,均包含相同数量的黑色节点。
1.2 红黑树如何确保最长路径不会超过最短路径的2倍?
由规则 ④ 可以知道,从根到NULL节点的每条路径都含有相同数量的黑色节点,因此在极端情况下,最短路径就是全黑节点,记最短路径为 bh (black height);由规则2 和规则3 可以知道,任意一条路径不会有连续的红色节点,因此在极端情况下,最长路径就是一黑一红交替形成,那么最长的路径就是 2*bh。
1.3 红黑树的效率
假设 N 是红黑树中的节点数量,h 是最短路径的长度,那么 2^h - 1 <= N <= 2^2*h - 1,由此推出h ≈ logN,也就意味着红黑树查找时最坏的情况就是走最长路径 2*logN,那么时间复杂度依旧是 O(logN)。
红黑树的表达相对 AVL 树要更加抽象。AVL 树通过平衡因子直观地控制平衡;而红黑树通过4条规则来约束颜色从而实现近似平衡。它们的效率都在 O(logN) 这一量级,但是相对于 AVL 树,红黑树在插入节点时的旋转次数要少许多,因为红黑树对平衡的控制没有 AVL树 严格。
2. 红黑树的实现
2.1 红黑树的结构
template <class T>
class RBTreeNode
{
public:
T _val;//存储的值
RBTreeNode<T>* _left;//左孩子
RBTreeNode<T>* _right;//右孩子
RBTreeNode<T>* _parent;//父亲
Color _col;//颜色
RBTreeNode(const T& val,Color col = RED)//构造函数,这里默认构造RED节点
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_val(val)
,_col(col)
{}
private:
Node* _root = nullptr;//根节点
};
2.2 红黑树的插入
2.2.1 红黑树插入一个值的大概过程
① 插入一个值按照二叉搜索树的规则插入,插入后我们再根据红黑树的 4条 规则进行 变色或者旋转 操作。
② 如果空树插入,则插入的节点必须是黑色,并且将该节点作为根;如果是非空树插入,新增节点必须是红色节点,否则会破坏规则④,这点很好证明。
③ 新增节点插入后,如果其父亲节点是黑色,则无事发生,插入结束。
④ 新增节点插入后,如果其父亲节点是红色,则违反了规则③。此时需要我们进行变色、旋转操作恢复树的结构。
这里需要我们进一步分析,下图我们用 cur 代表新增节点,parent代表新增节点的父亲,uncle代表父亲的兄弟,grandfather 代表父亲的父亲:
首先需要明白一点:c为红色,p为红色,则grandfather必定为黑色。因为如果grandfather是红色,则 parent 和 grandfather 就已经违反了规则③,根本没机会轮到 cur 和 parent 违反。因此,cur、parent、grandfather的颜色都是确定的,此时只有 uncle 的颜色是未知,因此关键就在于 uncle 的变化,根据 uncle 的变化我们可以分为以下几种情况处理。
注:下面我们用 c 表示 cur,p 表示 parent,u表示uncle,g表示 grandfather。
2.2.2 情况1:变色
c为红,p为红,g为黑,u存在并且u为红,则将 p、u 变黑,g 变红。再把 g 当作新的 c,继续往上更新。
分析:因为p和u都是红色,g是黑色,把p和u变黑,左边子树路径各增加⼀个黑色结点,g再变红,相当于保持g所在⼦树的黑色结点的数量不变,同时解决了c和p连续红色结点的问题,需要继续往上更新是因为,g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的父亲是黑色,则处理结束了;如果g就是整棵树的根,再把g变回黑色。
情况1只变色,不旋转。所以无论c是p的左还是右,p是g的左还是右,都是上面的变色处理方式。
① 跟 AVL 树类似,上图我们展示了一种具体情况,但是实际中需要这样处理的有很多种情况。
② 下图将上面类似的处理进行了抽象表达,d/e/f代表每条路径拥有nb个黑色节点的子树,a/b代表每条路拥有 hb-1 个 黑色节点的根为红的子树,hb>=0。
③ 图1/图2/图3,分别展示了 hb ==0/hb == 1/hb == 2 的具体情况组合分析,当 hb == 2时,组合情况就来到了上百亿种。当然,这些样例并不是想要说明红黑树有多么复杂,恰恰相反,这里想要说明不论情况多么复杂,处理方式都是一样的,变色再继续向上处理,因此我们只需要对着抽象图来操作即可。
2.2.3 情况2:单旋+变色
c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c⼀定是新增结点,u存在且为黑,则c⼀定不是新增,c之前是黑色的,是在c的子树中插⼊,符合情况1,变⾊将c从黑色变成红色,更新上来的。
分析:p必须变黑,才能解决,连续红色结点的问题,u不存在或者是黑色的,这⾥单纯的变色无法解决问题,需要旋转+变色。
如果p是g的左,c是p的左,那么以g为旋转点进行右单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。
如果p是g的右,c是p的右,那么以g为旋转点进行左单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样⼦树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。
2.2.4 情况3:双旋+变色
c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c⼀定是新增结点,u存在且为黑,则c⼀定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。
分析:p必须变黑,才能解决,连续红色结点的问题,u不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转+变色。
如果p是g的左,c是p的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。c变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。
如果p是g的右,c是p的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可。c变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。
2.3 红黑树插入代码实现
bool Insert(const T& data)
{
if (!_root)
{
_root = new Node(data,BLACK);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)//插入节点
{
if (cur->_val > data)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_val < data)
{
parent = cur;
cur = cur->_right;
}
else return false;
}
cur = new Node(data);
cur->_parent = parent;
if (parent->_val > data) parent->_left = cur;
else parent->_right = cur;
//处理两个红连在一起的情况
while (parent && parent->_col == RED)
{
if (parent->_left == cur)//新增节点插入在parent左边的情况
{
Node* grandfather = parent->_parent;
Node* uncle = grandfather->_right;
if (grandfather->_right == parent) uncle = grandfather->_left;
if (uncle && uncle->_col == RED)//uncle存在并且颜色为红
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//uncle不存在或者颜色为黑
{
if (grandfather->_left == parent)
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else//新增节点插入在parent右边的情况
{
Node* grandfather = parent->_parent;
Node* uncle = grandfather->_right;
if (grandfather->_right == parent) uncle = grandfather->_left;
if (uncle && uncle->_col == RED)//uncle存在并且颜色为红
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (grandfather->_right == parent)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
2.4 红黑树的查找
按二叉搜索树的逻辑查找即可,查找效率为 O(logN)。
void Find(Node* cur,const T& data)//查找
{
while (cur)
{
if (cur->_val > data) cur = cur->_left;
else if (cur->_val < data) cur = cur->_right;
else
{
cout << data << "->" << cur->_val << endl;
return;
}
}
cout << "找不到" << endl;
}
2.5 红黑树的验证
这里获取最长路径和最短路径,检查最长路径不超过最短路径的2倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前暂时没出问题,后续继续插入还是会出问题的。所以我们还是去检查4点规则,满足这4点规则,⼀定能保证最长路径不超过最短路径的2倍。
① 规则①:我们实现采用枚举颜色类型,天然实现保证了颜色不是红色就是黑色。
② 规则②:直接检查根即可。
③ 规则③:前序遍历检查,遇到红⾊结点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了。
④ 规则④:前序遍历,遍历过程中用形参记录跟到当前结点的BlackNum(黑色结点数量),前序遍历遇到黑色结点就BlackNum++,走到空就计算出了⼀条路径的黑色结点数量。再任意⼀条路径黑色结点数量作为参考值,依次比较即可。
#include "RBTree.h"
int main()
{
gjk::RBTree<int> rb;
常规测试用例
//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
特殊的带有双旋场景的测试⽤例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
{
rb.Insert(e);
}
rb.printf();
rb.find(4);
rb.find(100);
bool ret = rb.isRBTree();
cout << ret << endl;
int nodesize = rb.nodesize();
cout << nodesize << endl;
int leafsize = rb.leafnodesize();
cout << leafsize << endl;
vector<int> arr;
rb.blacknodenum(arr);
for (auto i : arr) cout << i << " ";
cout << endl;
return 0;
}
创作不易,点个赞呗,蟹蟹啦~