作者:小树苗渴望变成参天大树
作者宣言:认真写好每一篇博客
作者gitee:gitee✨
作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
今天我们终于来到了我们以前经常提到的一个比较难的数据结构–红黑树,这个数比较抽象,但是我们上一篇学过了AVL数,我觉得在复杂度上红黑树还是相比较而言简单一些,他的分类情况比AVL树少,但是前面的内容要理解好才行,所以前面我会尽量把具体图画的详细一点,然后再带大家来看抽象图。前面的性质我也会给大家铺垫好,大家一定瑶一步步的看下去,跳着是看不懂的,话不多说,我们开始进入红黑树进行讲解
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或
Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路
径会比其他路径长出俩倍,因而是接近平衡的。
为什么要有红黑树?
我们上面的概念第一句话就说这是一个二叉搜索树,通过AVL树的讲解,他是优化二叉搜索树的最差情况的,使得复杂度接近满二叉树,效率已经非常高的了,为什么还要有红黑树呢??他其实也是来改善二叉搜索树的最差情况的,相比较而言AVL是一个绝对平衡的树,而红黑树是一个近似平衡的树,这样就导致红黑树的高度会比AVL树的高度深,AVL中不合理的条件在红黑树中合理,导致红黑树的旋转次数少。AVL树的搜索性能可以说是最好的了,但是他的插入或者删除需要进行大量的旋转带来的消耗是很多的,所以有了红黑树,他虽然高度比AVL深一点,但是旋转次数少很多,对于大量数据红黑树的整体性能可能比AVL好。因为搜索的时候多出的高度也就是几十次的查找而已,对于cpu来说忽略不计,所以说红黑树减少的旋转带来的消耗,来增加的高度,而增加的高度对性能的影响忽略不计。这也就是我们学习红黑树的原因(对于10亿数据,顶多多了十几层,但是旋转次数可能少了好几万次,等这篇博客结束,我会带大家来对比AVL和红黑树的高度以及旋转次数的对比)
提示,我们破坏规则3不会影响其他路径上的规则,破坏规则四会影响所有路径,所以修改颜色的时候不方便,这个一会再详细说下,大家先看看能不能理解我说的
enum color
{
RED,
BLACK
};
template<class T>
struct RBTNode
{
T _val;
RBTNode<T>* _left;
RBTNode<T>* _right;
RBTNode<T>* _parent;
color _col;
RBTNode(const T&val=T())
:_val(val)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_col(RED)
{}
};
因为对节点有颜色的属性,所以定义一个枚举。因为要涉及到旋转,所以还是需要三叉链的结构。
红黑树本质还是二叉搜索树,按照前面的插入方式先找到插入位置。
template<class T>
class RBTree
{
typedef RBTNode<T> Node;
public:
RBTree() {}
bool insert(const T& val = T())
{
if (_root == nullptr)
{
_root = new Node(val);
_root->_col = BLACK;
return true;
}
Node* cur = _root;
Node* parent = _root;
while (cur)
{
if (cur->_val < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_val > val)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(val);
if (cur->_val < parent->_val)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//控制颜色和旋转
return true;
}
我们来看看红黑树有几种情况:
我们来看看先插入的节点到底是什么颜色最好,此数再插入节点之前肯定就是符合红黑树的,如果插入黑节点,那么就会导致新增节点这条路径的黑色节点比其他任何一条路径的黑色节点都多,如果想调整节点个数,要涉及到所有路径,这个非常不好控制,如果新增节点为红色,插入肯能会破坏第三点规则,但是影响的只是此节点路径上,并不会影响其他路径的规则,这个相对来说好控制。所以我们先插入的结点默认为红色。
后面涉及到一条路径上颜色的修改都是两层两层的进行修改,这样就控制第四点规则不会被破坏。一会就可以体会到。
第一种: 是新增结点是根节点
第三种:新增结点的父亲是红色,并且叔叔结点为红色
第四种: 新增结点的父亲为红色,并且叔叔不存在
第五种: 新增结点的父亲为红色,并且叔叔为黑色
第四种和第五种都还有两种情况,看父亲是祖父的哪个孩子。而且这两种情况的操作方式是一样的,所以可以合并为一种。
通过这三者的位置的关系,AVL是通过平衡因子来判断,而且这三者位置非常的清晰,所以就可以使用位置来进行判断选用那种旋转,可以直接复用AVL树的旋转,但是要把平衡因子去掉
我们来看一下代码:
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
// p 右 c g
//c
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//双旋
{
// g g c
// p 左 c 右 p g
// c p
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
{
if (cur == parent->_right)
{
// g p
// p 右 g c
// c
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g g c
// p 右 c 左 g p
// c p
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;//暴力法改根节点的颜色
}
通过上面的代码相信大家对红黑树的操作应该已经了解了,接下来我在带大家通过一般情况来分析二红黑树,这样大家会更加的理解的。
一般情况:
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何
性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连
在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
情况一: cur为红,p为红,g为黑,u存在且为红(原因是p为黑色没必要讨论,g为红,在插入之前就已经不是红黑树了,所以这三个颜色是固定的)
解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整
此时大家对于红黑树的理解是不是又透彻了一些。
接下来带大家验证红黑树
对于AVL树的验证我们通过算出的高度差和平衡因子对比,来判断对不对,而红黑树的验证条件比较多。
他的几个规则都需要成立,才可以,所以来看代码:
bool CheckColour(Node* root, int blacknum, int benchmark)
{
if (root == nullptr)
{
if (blacknum != benchmark)//为空的时候和基准值比较
return false;
return true;
}
if (root->_col == BLACK)//统计每条路径的结点个数
{
++blacknum;
}
if (root->_col == RED && root->_parent && root->_parent->_col == RED)//第三点规则
{
cout << root->_val << "出现连续红色节点" << endl;
return false;
}
return CheckColour(root->_left, blacknum, benchmark)
&& CheckColour(root->_right, blacknum, benchmark);
}
bool Isbalance()
{
return _Isbalance(_root);
}
bool _Isbalance(Node* root)
{
if (root == nullptr)
{
return true;
}
if (root->_col != BLACK)
{
return false;
}
return CheckColour(root, 0, benchmark(root));
}
int benchmark(Node* root)//计算最左边路径上黑色结点的个数作为基准值
{
if (root == nullptr)
{
return 0;
}
Node* cur = root;
int count=0;
while (cur)
{
if (cur->_col == BLACK)
{
++count;
}
cur = cur->_left;
}
return count;
}
我们上一些随机值来验证一下:
int main()
{
const int N = 10;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand()+i);
}
RBTree<int> rbt;
for (auto e : v)
{
rbt.insert(e);
//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
}
cout << rbt.Isbalance() << endl;
return 0;
}
结果是正确的,大家也可以测试一下数据少的时候,把监视窗口打开,画出来红黑树,看看是不是符合要求
我们来看测试代码:
int main()
{
const int N = 10000000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand()+i);
}
RBTree<int> rbt;
for (auto e : v)
{
rbt.insert(e);
//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
}
cout << rbt.Isbalance() << endl;
cout << rbt.Height() << endl;
cout << rbt.rotatecount << endl;
AVLTree<int> avlt;
for (auto e : v)
{
avlt.insert(e);
//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
}
cout << avlt.isbalance() << endl;
cout << avlt.height() << endl;
cout << avlt.rotatecount << endl;
return 0;
}
大家应该可以看出来红黑树的高度虽然多了基层但是旋转次数少了一百多万,大大提高了性能。这时候大家应该理解我前言说的内容了吧
(1)升序
(2)降序
(3)随机
红黑树是一个我们值得学的一种数据结构,他的作用非常广泛,虽然AVL1也非常好,AVL相当于9。5分,红黑树相当于10分,人们还是更愿意使用10分的东西。
C++ STL库 – map/set、mutil_map/mutil_set
Java 库
linux内核
其他一些库
所以他很重要,我们学习这个还要为下一步的模拟实现set和map做铺垫,希望大家可以认真学好这篇,下一篇的难度是比较大,但是大家认真去学都不是问题。我们下篇再见