📚 博主的专栏
上篇文章:AVL树
下篇文章:红黑树模拟实现STL库 map_set
目录
解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
a/b/c/d/e每条路径有x个黑色节点,红黑子树 x >=0。
情况二: cur为红,p为红,g为黑,u存在且为黑、u不存在
情况二.5:cur为红,p为红,g为黑,u不存在/u存在且为黑
红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。红黑树能比AVL树略高,旋转比AVL树更少一些。最长路径 <= 最短路径*2
AVL树:是一个严格平衡因子
红黑树:近似平衡++
红黑树的性质
- 1. 每个结点不是红色就是黑色
- 2. 根节点是黑色的
- 3. 如果一个节点是红色的,则它的两个孩子结点是黑色的(不存在连续的红色节点)
- 4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。(每条路径都存在相同数量的黑色节点)
- 5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点(NIL节点)
如何做到最长路径 <= 最短路径*2
极端场景:
最长路径:一黑一红间隔
最短路径:全黑
控制红黑树
enum Colour
{
BLACK,
RED
};
template<class K, class V>
struct RBTreeNode
{
//在K_V添加了parent
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Colour _col;
};
红黑树的插入
红黑树的插入操作
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
1. 按照二叉搜索的树规则插入新节点
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
void insert();
private:
Node* _root = nullptr;
size_t _size = 0;
};
检测新节点插入后,红黑树的性质是否造到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
新插入节点,必须插入红色(可能违反规则3),如果插入黑色,就会违反规则4:每条路径都存在相同数量的黑色节点。
实际上就是对uncle的三种情况进行讨论,分析。u是否存在,存在是什么颜色?
情况一: cur为红,p为红,g为黑,u存在且为红
cur和p均为红,违反了性质三,此处能否将p直接改为黑?
解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
g是否可以不变红,不行,g所在的这棵树,可能是整棵树的子树,不变红,子树路径的黑色节点数量都+1,破坏了规则4
红黑树的原则:永远不能破坏规则4(每条路径都存在相同数量的黑色节点)
如果g是根:就将g变黑
如果g不是根,就将g当成cur,继续向上调整,如果遇到父亲是黑色,那就结束了。
为了保证黑色节点的数量不变,以及补救插入红色节点导致的连续红色节点。
a/b/c/d/e每条路径有x个黑色节点,红黑子树 x >=0。
x == 1
这张图中,如果a\b、g的父亲是黑色的,就结束了,如果a\b、g的父亲是红色的就还需要继续处理
一颗红黑树的黑色节点是由我们插入红节点后向上变色所增加的
也就是这样一个变化过程
计算子树x=1的红黑树一共会有多少种情况:
x == 1,子树有m/n/p/q四种情况,c/d/e是m/n/p/q四种情况中的任意一个,组合4^3 = 64。
新增节点插入位置是a或者b的孩子 2 + 2 = 4种情况,合计组合:64 * 4 = 256种
抽象图:
情况二: cur为红,p为红,g为黑,u存在且为黑、u不存在
解决方式:
p为g的左孩子,cur为p的左孩子,则进行右单旋转;
相反, p为g的右孩子,cur为p的右孩子,则进行左单旋转
p、g变色--p变黑,g变红
1.u不存在
cur是新插入的节点
需要将cur的父亲变黑,爷爷变红,右旋:
树的高度不变,和上级链接的节点也本就是黑色,因此,不需要再向上变色
2.u存在
如图: 之前一定是黑色的,保证不违反4
x == 1:
相反,这里还存在着一种情况,如果p连着的子树在左,u在右,最后就是左单旋
情况二.5:cur为红,p为红,g为黑,u不存在/u存在且为黑
解决方式:
p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;
相反, p为g的右孩子,cur为p的左孩子,则针对p做右单旋转
则转换成了类似情况2
insert代码编写
其中的旋转函数直接参考上篇文章AVL中的旋转函数
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; //往右节点走之前,先存下parent
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else//默认定义(90%的情况下),搜索树不允许冗余,因此若值相等就不插入
{
return false;
}
}
cur = new Node(kv);
cur->_col = RED;//新增节点给红色
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
// p u c g
//c u
RotateR(grandfather);
//将父亲变黑,爷爷变红
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// 双旋
// g g c
// p u ——> c u ——> p g
// c p u
//以parent为旋转点进行左单旋,再以grandfather进行右旋转,cur->black
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else if (parent == grandfather->_right)
{
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
// u p ——> g c
// c u
RotateL(grandfather);
//将父亲变黑,爷爷变红
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g g c
// u p ——> u c ——> g p
// c p u
//以parent为旋转点进行左单旋,再以grandfather进行右旋转,cur->black
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
//无论循环是怎么处理的,根始终都要为黑色
_root->_col = BLACK;
return true;
}
红黑树的验证
红黑树的检测分为两步:
1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
2. 检测其是否满足红黑树的性质
- a、性质三遇到红色就看父亲是否是黑色
- b、计算出每条路径黑色节点的数量(DFS深度遍历,前序遍历就是一种DFS)
- 每个节点都记录一个黑色节点的数量,遇到黑色节点就+1记录后,走到空后再往回走传给根。
- 添加一个形参。
- 比较每条路径的个数
- 把每次的值都放到一个vector中,然后把vector里的值进行比对
- 先任意计算一条路径作为参考值,走最左或者最右路径
- 或者增加一个prev变量,记录上一条路径的值,用引用来比较
bool IsBalance()
{
if (_root->_col == RED) //2.根必须是黑色
return false;
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
{
++refNum;
}
cur = cur->_left;
}
return Check(_root, 0, refNum);//用0来表示黑色节点初始值
}
private:
bool Check(Node* root, int blackNum, const int refNum)
{
//根据红色找父亲,遍历整棵树
if (root == nullptr)
{
cout << blackNum << endl;
if (refNum != blackNum)
{
cout << "存在黑色节点数量不相等的路径" << endl;
return false;
}
return true;
}
//3.不能存在连续的红色节点
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_kv.first << "->存在连续的红色节点" << endl;
return false;
}
//4.每条路径都存在相同数量的黑色节点————>深度遍历
if(root->_col == BLACK)
{
blackNum++;
}
return Check(root->_left, blackNum, refNum)
&& Check(root->_right, blackNum, refNum);
}
测试代码:
void TestRBTree1()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13,8, 3, 1, 10, 6, 4, 7, 14, 13 };
//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14,4, 2, 6, 1, 3, 5, 15, 7, 16, 14, };
RBTree<int, int> t1;
for (auto e : a)
{
t1.Insert({e, e});
}
t1.InOrder();
cout << t1.IsBalance() << endl;
}
void TestRBTree2()
{
cout << "Starting TestAVLTree2..." << endl; // 添加调试输出
const int N = 10000;
vector<int> v;
v.reserve(N);
srand(rand());
for (size_t i = 0; i < N; i++)
{
v.push_back(rand());
//cout << v.back() << endl;
}
size_t begin2 = clock();
RBTree<int, int> t;
for (auto e : v)
{
t.Insert(make_pair(e, e));
//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
}
size_t end2 = clock();
cout << "Insert:" << end2 - begin2 << endl;
cout << t.IsBalance() << endl;
//cout << "Height:" << t.Height() << endl;
}
红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O($log_2 N$),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
红黑树的应用
1. C++ STL库 -- map/set、mutil_map/mutil_set
2. Java 库
3. linux内核
4. 其他一些库
结语:
随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。