目录:
- 前言
- 红黑树的概念
- 红黑树的性质
- 插入过程遇到的情况
- 情况1 -- 根节点
- 情况2 -- parent为黑色
- 情况3 -- parent为红色
- uncle为红色
- uncle为黑色
- uncle不存在
- 插入过程代码实现
- 分析红黑树是否构建成功
- 总结
前言
打怪升级:第88天 |
---|
红黑树,可以说是树中的绝对大佬了,它和我们前一篇讲解的avl树一样,都属于二叉排序树,
avl树中我们通过记录平衡因子以及旋转来保证一棵树的绝对平衡,而今天所讲的红黑树则是通过给各个节点添加颜色属性来保证一棵树的平衡,那么下面我们就一起揭开红黑树神奇的面纱吧~。
注:三叉树的旋转操作在avl树中进行了讲解,此篇文章不再赘述,有需要的朋友可以跳转观看。
红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
红黑树的性质
- 每个节点不是红色就是黑色;
- 根节点是黑色的;
- 红色节点的两个孩子都是黑色;
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点;
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点);
通过上面的五条性质我们可以得到那些信息呢?
- 红色节点的孩子只能是黑色的,黑色节点的孩子可以是红色也可以是黑色
- 从一个节点到其所有后代节点的路径上,均包含相同数目的黑色节点
从这里我们是否可以得出最长路径与最短路径?
– 答案是可以的,
最短路径:全部都是黑色节点
最长路径:每个黑色节点之间插入一个红色节点
那么从这里我们就论证了红黑树的概念描述:没有一条路径会比其余路径长出两倍。
- 红黑树节点的定义
enum Color // 颜色枚举
{
RED,
BLACK
};
// 1. 节点 -- 三叉链
template<class T>
struct _RBTree_Node
{
_RBTree_Node(const T& val)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _val(val)
, _col(RED)
{}
typedef _RBTree_Node<T> Node;
Node* _left;
Node* _right;
Node* _parent;
T _val;
Color _col;
};
提一个小问题:节点初始化时为什么默认是红色的,黑色的可以吗?
– 要回答这个问题,我们就需要从五条性质出发来对比初始化为红色和黑色的影响;
首先我们看到,对于性质1,2,5都是没有任何影响的,
那么对于性质3:默认为红色,如果新插入节点的父节点是黑色,这次就插入成功了,但是如果新插入节点的父亲也是红色就会产生冲突,我们需要改变这两个节点的颜色,具体的下面我们会分析;
默认为黑色则不会出现两个节点都为红色的情况;
对于性质4:默认为红色,不会影响黑色节点的数量;
默认为黑色,那么每次新增节点都会影响这个分支上的黑色节点个数,我们每次都需要进行调整,使得各个支路黑色节点个数相同。
由此我们决定选择调整次数少的情况 – 也就是默认为红色节点的初始化方案。
插入过程遇到的情况
情况1 – 根节点
此时是一个空树,插入节点就是这棵树的根节点,插入结束后就要更改节点的颜色,保证根节点为黑色。
情况2 – parent为黑色
情况3 – parent为红色
当parent为红色的时候我们就要更改新增节点(cur)或者parent的颜色,以满足红黑树的性质3,
那么,我们到底是改变cur的颜色还是改变parent的颜色呢?
假设我们改变cur,那么此时和我们直接将节点初始化为黑色岂不是一样的,我们初始化为红色就没有意义了,
因此我们要来改变parent的颜色,把它变为黑色后这个分支好像又多了一个黑色节点,
那么我们想要让它减少一个黑色节点我们是不是就要进行往上调节啊,
我们再把节点(爷爷 ,grand)改为红色,
如果这样改过之后该分支黑色节点该好了,但是,grand的另外一棵子树上就都少了一个黑色节点,
现在,我们还要让叔叔节点(父亲的兄弟 , uncle)变为黑色即可,
说着很简单,但是谁说叔叔节点就一定是红色的,而且,叔叔存不存在还两说呢;
所以在这里就分出了很多中情况;
–
我们来理一理上面的思路:
1.parent为红色,那么我们一定有grand,因为根节点一定为黑色,parent为红色就一定不是根节点,所以一定存在祖先,并且祖先一定是黑色节点;
2.但是我们不一定有uncle,因此这里需要判断的就是nucle不存在、nucle为红色、以及uncle为黑色三种情况;
下面我们逐个分析。
uncle为红色
变色放案:新增节点不变色,parent与uncle变为黑色,grand变为红色;
上方变色方案走过一轮后grand的左右子树都调整结束了,但是grand变成了红色,我们就需要判断grand的parent是否为红色,继续往上走。。。
注:下方示例特殊了一些,往上迭代时uncle不一定还是红色的。
uncle为黑色
我们的变色规律为:parent、uncle变为黑色,uncle变为红色,当uncle也为红色的情况下我们可以保证各个分支的黑色节点个数不变,
但是,当uncle为黑色时,再进行变色就会使得nucle分支的黑色节点个数减少,如下图第二次变色:
那么为了使parent与uncle分支节点个数相同我们就需要保持parent不变,uncle分支黑色加一,
或者uncle分支不变,parent黑色节点个数减一,
但是我们就是从第二种情况下变换过来的,因此第二种丢弃,选择使uncle分支黑色节点加一,
这里我们就需要用到旋转,由于左边黑色节点个数多,我们就进行右旋。
情况1:
上方,我们之所以进行右旋,是因为左边的黑色节点个数多,因此我们就需要判断到底是哪个子树黑色节点多,也就是parent到底在grand的左边还是右边;
情况2:
那么下方这种情况,我们看到parent在grand的右边,右边黑色节点多,因此我们进行一次左旋,
但是左旋之后我们并没有得到预期的结果,如果大家继续按照“变色,旋转”的方法往后画会发现这是一个死循环,这里就不再一一画出。
那么为什么会出现这种情况,这里和上方第一种情况有何不同呢? – cur 与parent的相对位置,与 parent与grand的相对位置不同!
第一种:cur为parent的左,parent为grand的左,因此进行旋转后parent变为新的根,cur与grand两个红色节点分别在他两侧;
上方这种:cur为parent的左,parent为grand的右,经过旋转后parent变为了新的根,但是cur和grand两个红色节点又连在了一起,
那么我们就需要避免这种情况的发生,如果我们将这种情况转换为第一种情况就可以将两者分开了,所以我们要对parent进行一次右旋,使得新的cur是parent的右,新的parent成为grand的右。
如图:
当出现双旋的情况时我们就要改变变色方案了,至于怎么更改就需要我们旋转之后往可以达到颜色平衡的情况下改变。
下方为单旋的详细流程图:
uncle不存在
uncle不存在的情况和uncle为黑的分析情况完全相同 – 变色方案是要将uncle变为黑色,而uncle不存在与uncle本就为黑时uncle都不需要改变,都会使得uncle分支少一个黑色节点。
插入过程代码实现
// 插入
bool Insert(const T& val)
{
// 空树
if (_root == nullptr)
{
_root = new Node(val);
_root->_col = BLACK; // 根节点为黑色
++_node_const;
return true;
}
// 查找是否存在
Node* parent = nullptr;
Node* cur = _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 (parent->_val < val)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
++_node_const;
// 调整节点
while (parent && parent->_col == RED)
{
Node* grand = parent->_parent;
Node* uncle = nullptr;
if (parent == grand->_left)
uncle = grand->_right;
else
uncle = grand->_left;
if (uncle && uncle->_col == RED) // uncle 存在且为红色,uncle与parent变为黑色,grand变为红色
{
// 更新颜色
uncle->_col = parent->_col = BLACK;
grand->_col = RED;
// 继续向上更新节点
cur = grand;
parent = grand->_parent;
}
else // uncle不存在 或者 uncle为黑色 ,依然采用上方的变色方案,特殊的是要进行旋转 -- 因为uncle分支黑色节点减少了
{
// 判断旋转策略
if (cur == parent->_left)
{
if (parent == grand->_left) // 右旋
{
// g
// p u
// c
RotateR(grand);
parent->_col = BLACK;
grand->_col = RED;
}
else // 右左双旋
{
// g
// u p
// c
RotateR(parent);
RotateL(grand);
cur->_col = BLACK;
//parent->_col = BLACK;
grand->_col = RED;
}
}
else // cur == parent->_right
{
if (parent == grand->_right) // 左旋
{
// g
// u p
// c
RotateL(grand);
parent->_col = BLACK;
grand->_col = RED;
}
else // 左右双旋
{
// g
// p u
// c
RotateL(parent);
RotateR(grand);
cur->_col = BLACK;
//parent->_col = BLACK;
grand->_col = RED;
}
}
}
}
// 根节点为黑色
_root->_col = BLACK;
return true;
}
// 旋转
void RotateL(Node* cur) // 左旋
{
// 三组关系,四个节点
Node* curR = cur->_right;
Node* curRL = curR->_left;
Node* curP = cur->_parent;
curR->_left = cur;
cur->_parent = curR;
cur->_right = curRL;
if (curRL) curRL->_parent = cur;
if(curP)
{
if (cur == curP->_left)
curP->_left = curR;
else
curP->_right = curR;
}
else
{
_root = curR;
}
curR->_parent = curP;
}
void RotateR(Node* cur) // 右旋
{
// 三组关系,四个节点
Node* curL = cur->_left;
Node* curLR = curL->_right;
Node* curP = cur->_parent;
curL->_right = cur;
cur->_parent = curL;
cur->_left = curLR;
if (curLR) curLR->_parent = cur;
if (curP)
{
if (cur == curP->_left)
curP->_left = curL;
else
curP->_right = curL;
}
else
{
_root = curL;
}
curL->_parent = curP;
}
分析红黑树是否构建成功
- 中序遍历是否为排序树
void InOrder()
{
_InOrder(_root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == nullptr) return;
_InOrder(root->_left);
cout << root->_val << ' ';
_InOrder(root->_right);
}
- 判断是否符合红黑树性质
void IsRBTree() // 判断是否符合红黑树的条件
{
if (_root->_col == RED) // 跟节点为黑色
cerr << "_root color error" << endl;
_IsRBTree(_root);
}
void _IsRBTree(Node* root) // 检测是否红黑树
{
if (root == nullptr) return;
// 1. 左右子树高度差 -- 2倍
int lenL = Height(root->_left);
int lenR = Height(root->_right);
if (lenL < lenR) swap(lenL, lenR);
if (lenL > lenR * 2) cerr <<"节点" << root->_val << ", height error" << endl;
// 2. 红色节点的孩子不能为红色
TowRed(root);
// 3. 黑色节点个数相同
BlackCnt(root);
_IsRBTree(root->_left);
_IsRBTree(root->_right);
}
int Height(Node* root)
{
if (root == nullptr) return 1; // 空节点算作黑节点
int lenL = Height(root->_left);
int lenR = Height(root->_right);
return (lenL > lenR ? lenL : lenR) + 1;
}
void TowRed(Node* root)
{
if (root->_col == RED)
{
if (root->_left && root->_left->_col == RED)
cerr << "节点" << root->_val << ", left color error" << endl;
if (root->_right && root->_right->_col == RED)
cerr << "节点" << root->_val << ", right color error" << endl;
}
}
void BlackCnt(Node* root)
{
Node* lnode = root->_left;
Node* rnode = root->_right;
int lcnt = 0;
int rcnt = 0;
while (lnode)
{
if (lnode->_col == BLACK) ++lcnt;
lnode = lnode->_left;
}
while (rnode)
{
if (rnode->_col == BLACK) ++rcnt;
rnode = rnode->_right;
}
if (lcnt != rcnt)
cerr << "节点" << root->_val << ", black count error" << endl
<< lcnt << " " << rcnt << endl;
}
- 测试
void Test_rbTree1()
{
srand((unsigned int)time(0));
kz::RBTree<int> t;
for (int i = 0; i < 20; ++i)
{
t.Insert(rand() % 100);
}
t.InOrder(); // 中序遍历打印结果
t.IsRBTree(); // 判断是否为红黑树
}
void Test_rbTree2()
{
srand(time(0));
kz::RBTree<int> t;
for (int i = 0; i < 10000; ++i)
{
t.Insert(rand());
}
t.IsRBTree();
}
int main()
{
Test_rbTree1();
cout << "------------------ - " << endl;
Test_rbTree2();
return 0;
}
总结
以上就是我们关于红黑树概念以及变色、旋转的全部内容,红黑树的旋转和avl的旋转都是一样的,不一样的一点就是红黑树旋转更新节点颜色,avl旋转更新节点的平衡因子。
红黑树是近似平衡,avl则是绝对平衡,而为了达到绝对的平衡自然就需要进行更多的旋转操作,所以avl在插入过程中会消耗大量的时间,
而因为是绝对平衡的,树的高度接近完全二叉树,查找的效率自然就会提高很多,
不过由于二叉树查找效率为logN,
在10亿个数据中进行查找也只需要查找30次,由于红黑树允许1倍左右的高度差,因此10亿个数据最多可能需要查找60次,
而这中间的30次查找对于cpu来说简直可以忽略不计,由于绝对平衡调整时间消耗大而且查找优势也并不明显,因此我们平时很少使用avl,而更多的使用红黑树。
- 红黑树的应用
- C++ STL库 – map/set、mutil_map/mutil_set
- Java 库
- linux内核
- 其他一些库