目录
🥇红黑树的概念
红黑树也是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子节点的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
(也就是说:假设一棵红黑树的最短路径长度是h,那么这棵红黑树的其他路径长度区间为[h,2h])
🥇红黑树的性质
红黑树的性质简单来说有以下几个:
①每个节点不是黑色就是红色;
②根节点是黑色;
③如果一个节点是红色的,那么它的两个孩子节点必须是黑色的,即:没有连续的红色节点;
④每条路径都包含相同数量的黑色节点;
⑤每个叶子节点都是黑色的(此处的叶子节点指空节点)。
为什么满足以上条件之后,红黑树就能保证其最长路径中节点个数不会超过最短路径节点个数的2倍呢?
这是因为黑色节点可以连续,红色节点不能连续,而树中每条路径的黑色节点数目又是相同的;
例如树中每条路径的黑色节点个数为4,那么最短路径可以是连续的4个黑色节点,最长路径则就是黑红交替场景,此场景下一共有8个节点,自然而然满足了“最长路径不会超过最短路径的2倍”。
🥇红黑树的实现
🥈红黑树节点的定义
在定义红黑树的节点之前,先思考这样一个问题:既然红黑树的节点有红色和黑色之分,那么每次新插入的节点是黑色还是红色呢?
我们要先明白:红黑树中每个路径的黑色节点个数都是相同的,如果在某一个路径下我们新插入了黑色节点,那么必会导致该路径下的黑色节点比其他路径多一个,显然不符合红黑树的规则,自然而然新插入节点是要选择红色节点插入。
因此红黑树节点的定义为:
//节点的颜色
enum Colour
{
red,
black
};
//节点
template<class K, class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;//节点的左孩子
RBTreeNode<K, V>* _right;//节点的右孩子
RBTreeNode<K, V>* _parent;//节点的父亲
pair<K, V> _kv;//节点的值域
Colour _col;//节点的颜色
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(red)//红色节点
{}
};
🥈红黑树的插入
上面提到:新插入的节点必须是红色节点,可其父亲如果是黑色节点还好,但其父亲如果是红色节点,这显然违反“红色节点不能连续”的规则了呀!此时就需进一步自下而上地对树的节点进行调整,而父亲是红色节点涉及调整时又分多种情况,需分情况讨论,如下图:
情况1:较为简单,四种情况无论add节点与叔叔节点如何分布,都只需按部就班变换节点颜色即可
情况2:叔叔不存在或叔叔存在且为黑色
当自下而上调整节点颜色出现叔叔不存在或叔叔存在且为黑色的情况时,因为叔叔已经是黑色了,此时满足红黑树“树中每条路径的黑色节点个数相同”,如果我们再继续将父亲的颜色变黑,那必将导致此路径下的黑色节点比其他路径多,不符合红黑树的规则,所以万万不能将父亲变黑了!那该怎么办呢?
事情发展到现在,我们也应该敏锐地知晓:此时的红黑树已经出现了最长路径大于了最短路径的2倍,也就是说,从这个add节点要在这里插入之初,整个红黑树就已经不符合“红黑树的最长路径不超过最短路径的2倍”这条基本属性了!
此时我们联想到:当AVL树的平衡因子不符合规定时,利用旋转就解决问题了,那么红黑树可不可以也利用旋转解决这个问题呢?答案是肯定的。
情况2如图:
与AVL树类似,当add节点和叔叔节点的分布情况不同时,旋转方法也不相同,上图只演示当叔叔是爷爷的右孩子和c是左孩子时的情况,其他情况也是相同的道理。
具体代码如下:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)//树本来就是空树,直接新增节点
{
_root = new Node(kv);
_root->_col = black;//头节点是黑色
return true;
}
//先按二叉搜索树的规则先找到要插入的位置
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)//大于,往右插入
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)//小于,往左插入
{
parent = cur;
cur = cur->_left;
}
else
{
return false;//等于,无法插入
}
}
//此时新节点要插入的位置已找到
//接着开始将节点插入到该位置
cur = new Node(kv); // 红色的 前面已经说过,新插入节点需是红色
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//为满足红黑树“黑色节点个数相同”
while (parent && parent->_col == red)//当头节点存在或头节点变黑时,结束循环
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
// 情况一:叔叔存在且为红
if (uncle && uncle->_col == red)
{
// 变色
parent->_col = uncle->_col = black;
grandfather->_col = red;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
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)
{
// 变色
parent->_col = uncle->_col = black;
grandfather->_col = red;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else
{
// 情况二:叔叔不存在或者存在且为黑
// 旋转+变色
// g
// u p
// c
if (cur == parent->_right)
{
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;
}
//①右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;// subL: parent的左孩子
Node* subLR = subL->_right;// subLR: parent的左孩子的右孩子(可能为空)
//①subLR变为parent的左孩子
parent->_left = subLR;
//②subLR可能不存在,如果存在,则更新subLR的父亲:subLR的父亲从subL变为parent
if (subLR)
{
subLR->_parent = parent;
}
//③parent从subL的父亲变为subL的右孩子
subL->_right = parent;
//④由于parent可能是棵子树,因此在更新父亲之前必须先保存parent的父亲
Node* pparent = parent->_parent;
//⑤更新parent的父亲:subL从parent儿子的身份变为parent的父亲
parent->_parent = subL;
//⑥如果parent是根节点,更新指向根节点的指针
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
//如果parent是子树,其可能是父亲的左子树,也可能是右子树
if (pparent->_left == parent)
{
pparent->_left = subL;
}
else
{
pparent->_right = subL;
}
//⑦更新subL的父亲:subL的父亲从parent变为pparent
subL->_parent = pparent;
}
//⑧旋转结束,更新平衡因子
subL->_bf = 0;
parent->_bf = 0;
}
//②左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;// subR: parent的右孩子
Node* subRL = subR->_left;// subRL: parent的右孩子的左孩子(可能为空)
//①subRL变为parent的右孩子
parent->_right = subRL;
//②subRL可能不存在,如果存在,则更新subRL的父亲:subRL的父亲从subR变为parent
if (subRL)
{
subRL->_parent = parent;
}
//③parent从subR的父亲变为subR的左孩子
subR->_left = parent;
//④由于parent可能是棵子树,因此在更新父亲之前必须先保存parent的父亲
Node* pparent = parent->_parent;
//⑤更新parent的父亲:subR从parent儿子的身份变为parent的父亲
parent->_parent = subR;
//⑥如果parent是根节点,更新指向根节点的指针
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
//如果parent是子树,其可能是父亲的左子树,也可能是右子树
if (pparent->_left == parent)
{
pparent->_left = subR;
}
else
{
pparent->_right = subR;
}
//⑦更新subR的父亲:subR的父亲从parent变为pparent
subR->_parent = pparent;
}
//⑧旋转结束,更新平衡因子
parent->_bf = 0;
subR->_bf = 0;
}
🥈红黑树的验证
根据红黑树的性质,在验证红黑树时,我们需要验证:
①根节点必须为黑色;②没有连续的红色节点;③每条路径下黑色节点个数相同
又因为红黑树也是二叉搜索树,所以还需验证中序序列是否有序。
具体代码如下:
//中序遍历
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
}
//验证红黑树
bool Check(Node* cur, int blackNum, int refBlackNum)
{
if (cur == nullptr)
{
if (refBlackNum != blackNum)
{
cout << "黑色节点的个数不同" << endl;
return false;
}
return true;
}
if (cur->_col == red && cur->_parent->_col == red)
{
cout << cur->_kv.first << "存在连续的红色节点" << endl;
return false;
}
if (cur->_col == black)//计算出每条路径下的黑色节点个数与参考值比较
{
++blackNum;
}
return Check(cur->_left, blackNum, refBlackNum) && Check(cur->_right, blackNum, refBlackNum);
}
bool IsBalance()
{
if (_root && _root->_col == red)
{
cout << "根是红色" << endl;
return false;
}
int refBlackNum = 0;
Node* cur = _root;
while (cur)
{
//计算从红黑树最左边路径的黑色节点个数作为参考值
if (cur->_col == black)
{
refBlackNum++;
}
cur = cur->_left;
}
return Check(_root, 0, refBlackNum);
}
测试
void TestRBTree()
{
int a[] = { 30, 23, 17, 11, 3, 20, 18, 14, 15 };
//int a[] = { 14, 21, 16, 11, 13, 5, 15, 7, 6, 14 };
RBTree<int, int> t;
for (auto e : a)
{
if (e == 14)
{
int x = 0;
}
t.Insert(make_pair(e, e));
//看是插入谁导致出现的问题
cout << e << "->" << t.IsBalance() << endl;
}
t.InOrder();
cout << t.IsBalance() << endl;
}
int main()
{
TestRBTree();
return 0;
}
*🥇红黑树与AVL树的性能比较
//高度
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
int Height()
{
return _Height(_root);
}
void Compare()
{
const int N = 10000000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (int i = 0; i < N; i++)
{
v.push_back(rand() + i);
}
RBTree<int, int> t1;
size_t begin1 = clock();
for (auto e : v)
{
t1.Insert(make_pair(e, e));
}
size_t end1 = clock();
cout << "红黑树插入时间:" << end1 - begin1 << endl;
AVLTree<int, int> t2;
size_t begin2 = clock();
for (auto e : v)
{
t2.Insert(make_pair(e, e));
}
size_t end2 = clock();
cout << "AVL树插入时间:" << end2 - begin2 << endl;
cout << "红黑树高度:" << t1.Height() << endl;
cout << "AVL树高度:" << t2.Height() << endl;
}
结果如图:
总结:
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log₂N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数, 所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。