红黑树是一种自平衡二叉搜索树,它通过在每个节点上增加一个“颜色”属性(红色或黑色)来维持树的平衡,从而保证了动态数据集操作的高效性。
1. 红黑树的特性与规则
关键特性:
- 是一种二叉搜索树,具有二叉搜索树的基本性质。
- 平衡性不是完全严格的,但通过颜色规则限制了树的高度,使其最长路径长度最多为最短路径长度的两倍。
红黑树被广泛应用于需要动态维护有序数据的场景,例如 C++ STL 的 map
和 set
实现。
红黑树的规则
红黑树通过以下规则确保平衡性:
- 节点颜色:每个节点要么是红色,要么是黑色。
- 根节点必须是黑色。
- 红色节点的子节点必须是黑色(即红色节点不能连续出现,也称为“红黑交替”)。
- 每个节点到其所有后代叶子节点的路径上,必须包含相同数量的黑色节点(称为“黑高”一致性)。
- 叶子节点(空节点)是黑色的。
时间复杂度
红黑树通过上述规则保证了树的高度在 O(logn)) 的范围内,从而使得常见操作效率始终保持稳定:
- 查找:O(logn)
- 插入:O(logn)
- 删除:O(logn)
空间复杂度
红黑树的空间复杂度与普通二叉搜索树类似,为 O(n),主要用于存储节点和颜色信息。
效率特点
- 查找效率:与AVL树相近,但略逊于AVL树,因为红黑树的平衡性较松。
- 插入与删除效率:由于平衡调整操作较少,红黑树的插入和删除操作比AVL树更快。
与其他树的比较
- 与AVL树:红黑树的旋转操作更少,因此适合频繁插入和删除的场景;而AVL树查找效率稍高,更适合读操作频繁的场景。
- 与普通二叉搜索树:普通二叉搜索树在最坏情况下可能退化为链表,而红黑树通过颜色规则避免了这种退化。
2. 红黑树的结构
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)
{}
};
template <class T, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
//
private:
Node* _root = nullptr;
};
3. 红黑树的插入
3.1 大致插入过程
1. 按二叉搜索树规则插入: 插入操作首先按照二叉搜索树(BST)的规则进行,也就是通过比较键值确定插入节点的位置。插入后,接下来需要检查并确保红黑树的四条平衡规则仍然成立。
2.1 空树插入:如果树是空的,插入的节点为根节点并且必须是黑色节点。根节点是黑色是红黑树规则之一,确保树的平衡性。
2.2 非空树插入:如果插入的是非空树,则新插入的节点必须是红色节点。这是因为,如果插入黑色节点,可能会破坏红黑树的第四条规则(每个节点到其后代叶子节点的路径上必须有相同数目的黑色节点)。红色节点插入后,通常不会违反规则4,且通过旋转和调整可以保证红黑树的平衡性。
3.1 父节点是黑色的情况:如果插入的节点的父节点是黑色的,则没有违反任何红黑树的规则,插入过程结束。此时,插入节点不需要进行任何额外的调整。
3.2 父节点是红色的情况:如果插入的节点的父节点是红色的,则违反了红黑树的第三条规则(红色节点不能有红色子节点)。此时需要进行颜色固定来进一步处理,尤其是处理插入节点、父节点、祖父节点、以及父节点的兄弟节点(即叔叔节点)之间的关系。
颜色固定:
(1)父节点(p
)是红色,祖父节点(g
)必须是黑色,插入节点(c
)的颜色为红色。
(2)根据叔叔节点(u
)的颜色以及是否存在,可以产生下面三种变色和旋转组合的情况
3.2 只变色不旋转

3.2 变色+单旋

3.3 变色+双旋

3.4 代码实现
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 (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 = new Node(kv);
cur->_col = RED;
//与父节点链接
if (cur->_kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else {
parent->_right = cur;
}
cur->_parent = parent;
//变色+旋转。从插入节点向上更新直到符合条件或更新到根节点
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
//1.当u存在且为红色时。只变色不旋转
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
// u存在且为⿊或不存在 -》旋转+变⾊
else
{
//单旋加变色
if (cur == parent->_left)
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
//双选旋加变色
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else
{
// g
// u p
Node* uncle = grandfather->_left;
// 叔叔存在且为红,-》变⾊即可
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else // uncle不存在,或者存在且为⿊
{
// 情况⼆:叔叔不存在或者存在且为⿊
// 旋转+变⾊
// g
// u p
// c
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{ //双旋
// g
// u p
// c
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
4. 红黑树的查找
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
5. 红黑树的验证
这里获取最长路径和最短路径,检查最长路径不超过最短路径的2倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前暂时没出问题,后续继续插入还是会出问题的。所以我们还是去检查4点规则,满足这4点规则,一定能保证最长路径不超过最短路径的2倍。
- 规则1枚举颜色类型,天然实现保证了颜色不是黑色就是红色。
- 规则2直接检查根即可。
- 规则3前序遍历检查,遇到红色节点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了。
- 规则4前序遍历,遍历过程中用形参记录跟到当前节点的blackNum(黑色节点数量),前序遍历遇到黑色节点就++blackNum,走到空就计算出了一条路径的黑色节点数量。再任意一条路径黑色节点数量作为参考值,依次比较即可。
bool Check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
// 前序遍历⾛到空时,意味着⼀条路径⾛完了
if (refNum != blackNum)
{
cout << "存在⿊⾊结点的数量不相等的路径" << endl;
return false;
}
return true;
}
// 检查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲就⽅便多了
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_kv.first << "存在连续的红⾊结点" << endl;
return false;
}
if (root->_col == BLACK)
{
blackNum++;
}
return Check(root->_left, blackNum, refNum)
&& Check(root->_right, blackNum, refNum);
}
bool IsBalance()
{
if (_root == nullptr)
return true;
if (_root->_col == RED)
return false;
// 参考值
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
{
++refNum;
}
cur = cur->_left;
}
return Check(_root, 0, refNum);
}