🌈欢迎来到数据结构专栏~~手撕红黑树
- (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
- 目前状态:大三非科班啃C++中
- 🌍博客主页:张小姐的猫~江湖背景
- 快上车🚘,握好方向盘跟我有一起打天下嘞!
- 送给自己的一句鸡汤🤔:
- 🔥真正的大师永远怀着一颗学徒的心
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
- 🎉🎉欢迎持续关注!
文章目录
- 🌈欢迎来到数据结构专栏~~手撕红黑树
- 一. 红黑树的概念😎
- 二. 五大特性
- 三. 节点的定义
- 四. 红黑树插入
- ⚡模型
- 🥑情况一:u 存在且为红
- 🥑情况二:
- 💥具体情况1️⃣:u不存在
- 💥具体情况2️⃣:u存在且为黑
- 💥双旋是怎么样产生的?
- 大总结
- 五. 验证红黑树
- 六. 红黑树的性能
- 七. 红黑树的性能
一. 红黑树的概念😎
红黑树也是一种二叉搜索树,但是每个节点都存储了一个颜色,该颜色可以为黑可以为红,因此也叫红黑树
红黑树和AVL树的区别就是:红黑树是近似平衡,但不是完全平衡,没有和AVL树一样 的通过平衡因子来控制高度差,而是通过每条路径上对红黑节点的控制,来达到确保最长路径长度不超过最短路径长度的 2 倍。
二. 五大特性
- 根节点一定为黑色
- 每个节点只能是 红或黑
- 节点为红色,则该节点的两个子节点都为黑色(也就是树中没有连续的红色节点)
- 对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点(每条路径的黑色节点相等)
- 每个叶子结点都是黑色(此处的叶子节点指的是空节点:
NIL
节点)
如果是空树,刚好是空节点为黑色,也符合第一条规则
那么问题来了,仅仅依靠这五大特性是如何确保最长路径长度 <= 最短路径长度的 2 倍?
根据红黑树的性质3可以得出,红黑树当中不会出现连续的红色结点,而根据性质4又可以得出,从某一结点到其后代叶子结点的所有路径上包含的黑色结点的数目是相同的
所以我们不妨假设极端场景,最短的路径无非就是全黑的情况了,假设此时有 n
个节点,长度就为 n
而最长路径就是:一红一黑排列的,如果有 n 个黑色节点,那么红色节点数目与黑色相同,则长度为 2n,所以不超过两倍!
三. 节点的定义
cv工程师登场!此处我们还是定义成三叉链,只不过把平衡因子替换成了节点颜色,因为节点的颜色就两种,直接枚举就好了
//枚举颜色
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)
{}
};
此处有个问题:为什么构造结点时,默认将结点的颜色设置为红色?
此处我们知道插入黑色的节点是一定违反了性质4;某条路径的黑色节点一定会增加一个,那为了维护结构,我们岂不是要在其他的路径是不是也要增加一个呢?
但此时如果新增的是一个红色的节点呢;如果根节点为红色,那么又会破坏性质 3 出现了连续的红色节点,但是如果根节点为黑色,就不需要进行调整。 也就是说新增red节点不一定会破坏结构,但新增Black节点就一定会破坏。
四. 红黑树插入
此处插入的前半部分和AVL树的插入一样:
- 按二叉搜索树的插入方法,找到待插入位置
- 将待插入结点插入到树中
- 若插入结点的父结点是红色的,则需要对红黑树进行调整
红黑树的关键就在这第三步中!大有来头
⚡模型
我们先给出一个基本模型:(可以是子树也可以是一棵完整的树)
cur :当前节点
p:parent,父节点
g:grandfather,祖父节点
u:uncle,叔叔节点
a,b,c,d,e:子树
顺便科普一下树的路径是从根节点一路走到空节点(NIL
节点)才算一条路径,而不是走到叶子就停下来了
所以上面这棵树的有效路径有9条,而不是4条
🥑情况一:u 存在且为红
cur 为红,p 为红,g 为黑, u 存在且为红
首先我们知道红黑树的关键是看叔叔uncle
;节点的颜色是固定为黑色的;因为不能有两个相同的红色节点,所以我们开始调整!首先将parent
变成黑色;又为了不违反性质4,所以uncle的节点也要变成黑色;同时也要将grandparent
节点变红,不要忘了这可能只是一颗子树;为了维持每条路径上黑色节点的数量;祖父必须变红,不然会多出一个黑色节点。
最后不要忘了将祖父当成 cur 节点继续向上调整,直到g是根,最后才将变成黑色!
具体代码:
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
assert(grandfather);
assert(grandfather->_col == BLACK);
//关键看叔叔 ~ 判断叔叔的位置
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
//情况1:uncle存在且为红 + 继续往上处理
if (uncle && uncle->_col = RED)
{
//变色:p和u变黑,g变红
parent->_col = uncle ->_col = Black;
grandfather->_col = RED;
//继续往上调整
cur = grandfather;
parent = cur->_parent;
}
else //情况2
{}
}
else //parent == grandfather->_right
{
Node* uncle = grandfather->_left;
//情况1:uncle存在且为红 + 继续往上处理
if (uncle&& uncle->_col = RED)
{
//变色:p和u变黑,g变红
parent->_col = uncle->_col = Black;
grandfather->_col = RED;
//继续往上调整
cur = grandfather;
parent = cur->_parent;
}
else //情况2
{}
}
}
_root->_col = BLACK;//不管什么,最后根要变黑
return true;
}
🥑情况二:
💥具体情况1️⃣:u不存在
cur 为红,p 为红,g 为黑, u 不存在
如果叔叔不存在,那么a/b/c/d/e都是空树,cur是新增节点!因为叔叔不存在的话,parent下也不可能挂上黑节点!
此处也违背了性质4,所谓单纯的变色也无法处理了,那就旋转试试看吧,
祖孙三代在一条直线上偏左,一波右单旋安排,接着根节点变成 parent 后调整颜色,父亲变黑,祖父变红,一波操作下来黑节点数目没边,根节点也是黑色不用再向上调整了
💥具体情况2️⃣:u存在且为黑
cur 为红,p 为红,g 为黑, u 存在且为黑
b和c可以是空树或者是一个红节点,a可以是根也可以是黑节点的子树,下面四种的的任意一种
以下的情况一定是由情况一往上调整过程中才会出现的!即这种情况下的cur结点一定不是新插入的结点,而是上一次情况一调整过程中的祖父结点,如下图:
此上的情况必定是左边的的多一个黑色节点,如上图所示, 假设祖父上面有 x 个黑节点,叔叔下有y个节点,那么左子树(含祖父)现在是 x +1 个,右子树(含祖父)是 x + 2 + y 个,很明显 x + 2 + y > x + 1,因此在插入结点前就已经不满足要求了,所以说叔叔结点存在且为黑这种情况,一定是由情况一往上调整过程中才会出现的!
此处单纯的变色已经无法处理,况且还违背了性质4;这时我们需要进行旋转+变色处理
如果是祖父、父亲、cur 都在同一条直线上,那么只需要单旋即可
💥双旋是怎么样产生的?
若祖孙三代的关系是折线(cur、parent、grandfather这三个结点为一条折现),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理
抽象图如下:
以上情况的完整代码:
//如果插入节点的父节点是红色的,则需要对红黑树进行操作
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
assert(grandfather);
assert(grandfather->_col == BLACK);
//关键看叔叔 ~ 判断叔叔的位置
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
//情况1:uncle存在且为红 + 继续往上处理
if (uncle && uncle->_col == RED)
{
//变色:p和u变黑,g变红
parent->_col = uncle ->_col = BLACK;
grandfather->_col = RED;
//继续往上调整
cur = grandfather;
parent = cur->_parent;
}
else //情况2 + 情况3:uncle不存在 + uncle存在且为黑
{
//情况二:单旋 + 变色
// g
// p u
//c
if (cur = parent->_left)
{
RotateR(grandfather);//右旋
//颜色调整
parent->_col = BLACK;
grandfather->_col = RED;
}
else//cur == parent->_right
{
//情况三:左右双旋 + 变色
// g
// p u
// c
RotateL(parent);
RotateR(grandfather);
//调整颜色
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else //parent == grandfather->_right
{
Node* uncle = grandfather->_left;
//情况1:uncle存在且为红 + 继续往上处理
if (uncle&& uncle->_col == RED)
{
//变色:p和u变黑,g变红
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//继续往上调整
cur = grandfather;
parent = cur->_parent;
}
else //情况2 + 情况3:uncle不存在 + uncle存在且为黑
{
//情况二:单旋 + 变色
// g
// u p
// c
if (cur = parent->_right)
{
RotateL(grandfather);//左单 旋
//颜色调整
parent->_col = BLACK;
grandfather->_col = RED;
}
else//cur == parent->_left
{
//情况三:右左双旋 + 变色
// g
// u p
// c
RotateR(parent);
RotateL(grandfather);
//调整颜色
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
大总结
无论是情况一还是情况二,cur为红,p为红,g为黑这三个条件是固定的
-
情况一:叔叔存在且为红:
- 1️⃣单纯的变色:p和u变黑,g变红
- 2️⃣把g继续当成cur,g不是根继续往上处理(继续往上处理有可能变成情况2);g是根就变成黑色
-
情况二:叔叔不存在 or 叔叔存在且为黑
- 1️⃣旋转:具体情况来判断是什么旋转(祖孙三代是折线关系就是双旋,直线就是单旋)
- 2️⃣变色:p变黑, g变红
五. 验证红黑树
还是老套路中序遍历来验证:
void Inorder()
{
_Inorder(_root);
}
void _Inorder(Node* root)
{
if (root == nullptr)//空树也是红黑树
return;
_Inorder(root->_left);
cout << root->_kv.first << " ";
_Inorder(root->_right);
}
那怎么样验证它是红黑树呢?从五大特性下手!
bool IsBalance()
{
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)
{
cout << "根节点不是黑色" << endl;
return false;
}
// 黑色节点数量基准值
int benchmark = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
++benchmark;
cur = cur->_left;//以最左的路径进行
}
return PrevCheck(_root, 0, benchmark);
}
bool PrevCheck(Node* root, int blackNum, int& benchmark)
{
if (root == nullptr)
{
//cout << blackNum << endl;
//return;
if (benchmark == 0)
{
benchmark = blackNum;
return true;
}
if (blackNum != benchmark)
{
cout << "某条黑色节点的数量不相等" << endl;
return false;
}
else
{
return true;
}
}
if (root->_col == BLACK)
{
++blackNum;
}
//检测当前节点以及父亲
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "存在连续的红色节点" << endl;
return false;
}
return PrevCheck(root->_left, blackNum, benchmark)
&& PrevCheck(root->_right, blackNum, benchmark);
}
六. 红黑树的性能
红黑树的删除本节不做讲解,有兴趣的可参考:《算法导论》或者《STL源码剖析》
参考地址
七. 红黑树的性能
AVL 树和红黑树都是平衡二叉树的两个老大哥,他们增删查改的时间复杂度都O(logN)
,到底谁更技高一筹?
其实在大数据的场景下,比如百万级量化数据,AVL 需要构建大约 20 多层,同时红黑树需要构建大约 40 多层,毕竟红黑树是近似平衡的二叉搜索树。
但是我们知道 20 和 40 在 CPU 运算速度面前并没有什么差别,虽然 AVL 树在效率上会略胜红黑树一点点,但是生活中红黑树的运用却比 AVL 树更为广泛,因为 AVL 树的效率是有代价的,是充分牺牲结构进行不断旋转得到的,而红黑树大大降低了旋转次数会更安全因此,换来了更优的性能