在上一篇AVL树的实现中,学习了平衡二叉树的一种——AVL树;由于AVL树极度追求平衡,因此它的查找效率十分高效;但也正是由于其极度追求平衡的旋转策略,导致其动不动就旋转,因此旋转消耗十分大。在实际中使用的不多。对此,今天学习另一个平衡二叉树——红黑树。
红黑树的介绍
概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
红黑树是平衡二叉搜索树中的一种,红黑树性能优异,广泛用于实践中,比如 Linux
内核中的 CFS
调度器,C++ STL库中的map
和set
的底层就用到了红黑树,由此可见红黑树的重要性。
性质
- 每个结点不是红色就是黑色
- 红黑树特点
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 不红红
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
- 黑路同
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点,也叫
NIL
节点)- 此处黑色仅用于路径判断,不具备其他含义
- 需要十分了解这些性质,特别是性质2,3,4。
上面前四条性质特别重要。同时满足前四条性质,就能保证:
其最长路径中节点个数不会超过最短路径节点个数的两倍
这就是极端情况下,理论上存在的红黑树,也就是红黑树最不平衡的时候。而且只要再插入节点,通过红黑树的调整策略就会尽量平衡树身,所以红黑树的效率还是有保障的。
性质4(黑路同)确保了从根到叶子节点的任何路径上的黑色节点数相同。这意味着所有路径在黑色节点级别上是“等长”的。现在,考虑由于性质3(不红红)的存在,即不允许连续的红色节点,那么任意两个黑色节点之间最多只能插入一个红色节点(如果有的话)。
- 最短路径:最短路径只包含黑色节点,因为不存在连续的红色节点可以缩短路径。
- 最长路径:最长路径包含交替的黑色和红色节点(尽管并非所有黑色节点之间都必须有红色节点)。
- 由于任意两个黑色节点之间最多只能插入一个红色节点,因此在两个黑色节点之间最多可以插入一个红色节点,这使得红色节点的数量在任意两个黑色节点之间都是受限的。
考虑从一个黑色节点到下一个黑色节点(包含这两个黑色节点)的路径上,可能的最长情况就是有一个红色节点。由于性质4,我们知道从根到任意叶子节点的黑色节点数相同,设这个数量为 B。那么,最长路径的长度(节点数)就是 2 B − 1 2B−1 2B−1(每个黑色节点之间最多插入一个红色节点)。最短路径只包含黑色节点,因此长度为 B。
所以,最长路径与最短路径的节点数之比为 ( 2 B − 1 ) / B (2B−1)/B (2B−1)/B,简化后得到 2 − 1 / B 2−1/B 2−1/B。由于 B 总是大于0,因此这个比值总是小于2,且随着 B 的增大而趋近于2。
综上,红黑树确保了从根到叶子的最长路径不会超过最短路径的两倍长。但这只是在规则限定下的理论而言,实际上红黑树的最长,最短路径都不一定会存在。
根据此特性可以得出,红黑树是近似平衡的,因此在查找效率上就没有追求极度平衡的AVL树效率高。
红黑树节点的定义
红黑树可以看作是AVL树的改良版,增加了树节点的颜色。其余的还是保存kv的键值对pair,三叉链。
enum Color
{
RED, BLACK
};
template<class K, class V>
struct RBTreeNode
{
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Color _col;
RBTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
,_col(RED)//默认为红色
{}
};
节点的颜色默认设置为红色,理由如下:
- 当节点颜色为黑色时,完成一次插入后,会发现此时一定违反了红黑树性质中黑路同的原则,因为你只能在树的左右一侧插入,那么插入一个黑色节点后,左右子树的黑色节点数量必然不同,必然需要进行调整。
- 当节点颜色为红色时,不需要关心插入在哪棵子树上,由于节点颜色不是红色就是黑色,当插入节点的父节点为红色时,违反了不红红的性质,需要调整;但当插入节点的父节点为黑色时,没有违反红黑树的任何一条性质,此时不需要进行调整。
也就是说:当节点为黑色时一定会调整,当节点为红色时可能会调整。
综上所述,节点默认为红色的设计更优。
红黑树的插入
红黑树的插入分三步:
-
按搜索二叉树的性质插入
-
通过颜色判断是否需要调整
- 通过性质3(不红红)来判断是否需要进行调整
-
调整
- 分三种状况
第二步为什么通过不红红这一性质判断是否需要调整。
与AVL树通过平衡因子(本质就是高度)来判断是否需要旋转调平衡不同;红黑树这里没有所谓高度一说,而是通过树节点的颜色来控制树身的近似平衡的;
通过不红红这一性质判断是否需要调整是因为:树节点的颜色默认为红色,所以插入节点后如果破坏了红黑树的性质,那么一定是不红红这一性质,此时就需要进行调整。
红黑树的调整
由于新插入的节点默认设为红色,所以红黑树在插入之后不一定需要调整,可分为以下三种情况进行调整。调整策略分为两种:
- 单纯染色
- 旋转加染色
- 这里的旋转和AVL树的旋转是一样的,如果不了解如何进行旋转的话请参考——AVL树的旋转
前面说过,红黑树是AVL树的改良版;由于AVL树追求极度的平衡,所以其旋转次数肯定不会少,红黑树的目的就是达到近似平衡,效率上不会差太多,但是可以减少他的旋转消耗,所以红黑树调整时是不情愿旋转的,能不旋转尽量不旋转,必须的时候才旋转。
对于红黑树的调整,需要关注以下几个节点:
cur
为当前节点parent
为父节点,以下称pgrandfather
为祖父节点,以下称guncle
为叔叔节点,以下称u
以下的三种情况我们以u节点的不同状况来区分。
情况一
cur为红,p为红,g为黑,u存在且为红
- 以下示意图为抽象图;abcde不代表高度,代表从abcde开始的路径上有多少黑色节点。
此时cur与p都为红,违反了不红红的性质,需要进行调整;此时的p,u都为红色,所以为了既要解决不红红的问题,又需要保证黑路同的性质,此时可以将p,u都变为黑色,此时解决了不红红的问题;但是由此时的g开始的左右子树的黑色节点的数量都增加了1;这时候还得根据g是否为根节点继续判断:
- g为根节点,这种情况下由于根开始的左右子树都增加了黑色节点数量,所以不违反黑路同的性质。
- g不是根节点,是一颗子树,因为g不是根节点,只是根节点的左/右子树,这种情况增加黑色节点数量违反了黑路同原则,需要将g变红,保证当前子树的黑色节点数量不变。而这时又会出现情况一的问题,还得需要向上继续更新。
- 情况一需要注意的就是当前的树是否是一颗完整的树还是一棵子树。
- 在实现情况一解决办法时,代码的实现为解决上述第二种情况。
- cur不一定是新插入的,也有可能是变色而来。
注意:情况一中,cur不管是p的左孩子还是右孩子都是一样的
情况二
cur为红,p为红,g为黑,u不存在
出现情况二这种状况说明此时的的树身已经不平衡了,有点单枝树的倾向了;而且此时的cur一定是新插入的节点,由于没有u,所以现在是一个单支的状况只能有g,p两个节点。
此时单独的染色已经无法解决这种极度不平衡的树型了,需要搬出旋转大法;此时按照AVL树的旋转策略旋转调平衡不了解旋转策略的请参考AVL树的旋转
完成旋转后,再进行染色维持红黑树的性质:此时的染色策略为交换两旋转点的颜色,这样操作还是从此时的g是否是整棵树还是子树进行考虑;染色就需要同时确保:根为黑,黑路同,不红红的性质。
- 注意染色是不需要考虑cur是p的左孩子还是有孩子;但是旋转需要根据AVL树的旋转规则来,需要区分cur是p的左还是右孩子。
情况三
cur为红,p为红,g为黑,u存在且为黑
情况三的cur一定是由黑色节点变来的,不可能是新插入的节点;也就是说cur为红色是由于情况一的染色调整变来的;因为g的右子树u已经是黑色节点了,那么g的左子树也必须要有对应数量的黑色节点,那么子树abc就不可能是空,必须要有对应数量的黑色节点,才不会违反红黑树的原则。
调整策略与情况二一致,都是旋转加染色;
可以看到此时的树型会复杂一点,而且需要注意是谁要进行颜色交换,所以还是建议画图,对照着图来一步步实现。
以上的旋转加染色都是单旋加染色;颜色的调整无论左右,但是旋转不仅有单旋,还有双旋,需要知道树型是纯粹的一边高还是呈现出折线的样子。
这里通过cur在较高左子树的右边这一例子展示双旋加染色的策略:这一情况需要进行两次旋转,第一次是为了将树型转化为情况二的树型,第二次旋转才是调平衡。之后将两旋转点进行换色处理。
- 换色是在第二次旋转时才需要,而且双旋要换色的点为cur和g,所以一定要画图看仔细。
下图是我在红黑树学习时对插入操作的总结。
以下时具体实现
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;//根为黑色
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
//到在这里说明找到位置了,开始插入。
//此时cur已经为nullptr,让其成为新节点
cur = new Node(kv);
if (kv.first > parent->_kv.first)
{
parent->_right = cur;
}
else//不存在相等情况
{
parent->_left = cur;
}
//处理三叉链的最后一环
cur->_parent = parent;
//cur插在parent处且默认为红色
while (parent && parent->_col == RED)//当cur为根节点时,parent为空,不会进
{
//p,c颜色为红
Node* grandparent = parent->_parent;
// g
// p u
// c
if (parent == grandparent->_left)//单双旋需要借此区分
{
//两种调整策略
//1:单纯染色
//2:旋转加染色(分单双旋)
Node* uncle = grandparent->_right;
if (uncle && uncle->_col == RED)//策略1
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandparent->_col = RED;
//向上更新一整棵树
cur = grandparent;
parent = cur->_parent;
}
//策略二
else//uncle不存在或者存在且为黑(由上面的情况变来)
{
// g
// p u
// c
//先区分单旋还是双旋
//单旋
if (cur == parent->_left)//纯粹一边高
{
//看图
RotateR(grandparent);
parent->_col = BLACK;
grandparent->_col = RED;
}
else//双旋 插入在较高左子树的右侧
{
// g
// p u
// c
RotateL(parent);
RotateR(grandparent);
cur->_col = BLACK;
grandparent->_col = RED;
}
break;//旋转完就可以退出
}
}
else if(parent == grandparent->_right)
{
// g
// u p
// c
Node* uncle = grandparent->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandparent->_col = RED;
cur = grandparent;
parent = cur->_parent;
}
else//uncle不存在或者存在且为黑(由上面的情况变来)
{
// g
// p u
// c
//单旋
if (cur == parent->_right)
{
RotateL(grandparent);
parent->_col = BLACK;
grandparent->_col = RED;
}
else//双旋
{
// g
// p u
// c
RotateR(parent);
RotateL(grandparent);
cur->_col = BLACK;
grandparent->_col = RED;
}
break;//旋转完就可以退出
}
}
}
_root->_col = BLACK;//暴力处理
return true;
}
红黑树的验证
红黑树的验证分为两步:
- 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
- 检测其是否满足红黑树的性质
- 根为黑
- 不红红
- 黑路同
是否满足二叉搜索树
介绍搜索二插树时详细介绍过了,这里就不介绍了。
public:
void InOrder()
{
//嵌套一层,类外不能访问私有成员
_InOrder(_root);
cout << endl;
}
private:
//嵌套一层
void _InOrder(Node* root)//中序遍历搜索二叉树是有序的
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
是否满足红黑树的性质
检查的点有三:根为黑,不红红,黑路通;采用两个函数来验证这三点:
IsBalanceTree
:检测根是否为黑以及用refNum
记录一条路径上有多少个黑色节点。
Check
:三个参数,该函数验证不红红和黑路同这两个性质。
- 第一个用来接受节点
- 第二个用来接受从到上一层的的黑色节点数量
- 第三个为先前在
IsBalanceTree
计算好的黑色节点数量refNum
public:
bool IsBalanceTree()
{
if (_root == nullptr)
{
return true;//空树也是红黑树
}
if (_root->_col == RED)
{
cout << "err:根为红色" << endl;//根必须为黑色
return false;
}
//验证黑路同原则
int refNum = 0;//记录一条路的黑色节点数量
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)//黑色节点
{
refNum++;//记录黑色个数
}
cur = cur->_left;
}
return Check(_root, 0, refNum);//验证不红红,黑路同原则
}
private:
bool Check(Node* root, int blacknum, const int& refnum)
{
if (root == nullptr)
{
if (blacknum != refnum)//blacknum为遍历完一条路径后的黑色节点个数
{
cout << "相同路径黑色节点个数不同" << endl;
return false;
}
return true;//到这里说明是一棵红黑树
}
//不红红原则:用当前节点与父节点比较
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "连续的相同红色节点" << endl;
return false;
}
if (root->_col == BLACK)//黑色节点
{
blacknum++;
}
return Check(root->_left, blacknum, refnum) && Check(root->_right, blacknum, refnum);//一条一条路径检查,全为真 Check函数才为真
}
红黑树与AVL树的比较
红黑树
和AVL树
都是高效的平衡二叉树,增删改查的时间复杂度都是
O
(
l
o
g
2
N
)
O(log_2 N)
O(log2N) ,红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL
树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。如C++ STL库中的map
和set
的底层就用到了红黑树。之后将进行map和set的模拟实现,学习map
和set
。