红黑树(Red-Black Tree)
1.前言
- 本文中,我们使用
KV
实现红黑树。- 源码链接放在文章结尾
红黑树是一种自平衡的 二叉搜索树 (Binary Search Tree, BST),通过引入额外的规则(红黑性质)来保证树的高度接近最优,从而在最坏情况下提供O(logn) 的操作复杂度。
红黑树的增删查改都是基于二叉搜索树的,如果不了解二叉搜索树,点我进入二叉搜索树的世界
红黑树的旋转跟AVL树的旋转相同,点我进入AVL树的世界
2.红黑树的特性
红黑树在其节点上引入了红色和黑色的颜色属性,并满足以下红黑性质:
- 节点颜色:每个节点必须是红色或黑色。
- 根节点是黑色:红黑树的根节点必须是黑色。
- 红色节点不能相邻:红色节点的子节点必须是黑色(即不存在连续两个红色节点)。
- 每条路径上的黑高相等:从任意节点到其所有叶子节点(
NIL
或空节点)的路径上,经过的黑色节点数必须相同。 - 叶子节点为黑色:红黑树中的叶子节点是一个虚拟的黑色
NIL
节点。
这些性质共同确保了红黑树的平衡,避免其退化成链表。
3.节点的定义
我们使用枚举来定义红黑树的颜色:
enum Color
{
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;
// 节点颜色
Color _col;
// 构造函数
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED) // 新插入的节点默认是红色
{}
};
思考:为什么要默认插入红色节点?
- 如果插入的是红色节点,特性3红色节点不能相邻会被破坏。
- 如果插入的是黑色节点,特性4每条路径上的黑高相等会被破坏。
- 因为红色节点不能相邻被破环比每条路径上的黑高相等好恢复,所以避难就易,插入红色节点。
4.插入
插入的思路:
- 按照二叉搜索树的规则插入。
- 插入后更新节点颜色,维护红黑树的规则。
更新节点颜色的思路:
- 如果父亲的颜色为黑色,不破坏红黑树性质,不更新节点颜色。
- 如果父亲的颜色为红色:
- 如果叔叔节点存在并且为红色:
- 将祖父节点置为红色,将父亲和叔叔节点置为黑色。
- 向上更新
- 如果叔叔节点不存在,或者存在为黑色:
- 父亲是祖父的左孩子:
- 孩子是父亲的左孩子:
- 右旋祖父
- 祖父节点置为红色,父亲节点置为黑色
- 停止更新
- 孩子是父亲的右孩子:
- 左旋父亲
- 交换父亲和孩子的指针
- 右旋祖父
- 祖父节点置为红色,父亲节点置为黑色
- 停止更新
- 孩子是父亲的左孩子:
- 父亲是祖父的右孩子:
- 孩子是父亲的右孩子:
- 左旋祖父
- 祖父节点置为红色,父亲节点置为黑色
- 停止更新
- 孩子是父亲的左孩子:
- 右旋父亲
- 交换父亲和孩子的指针
- 左旋祖父
- 祖父节点置为红色,父亲节点置为黑色
- 停止更新
- 孩子是父亲的右孩子:
- 父亲是祖父的左孩子:
- 如果叔叔节点存在并且为红色:
//代码实现
template<class K, class V>
class RBTree {
typedef RBTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv) {
// 1. 按二叉搜索树规则插入
if (!_root) {
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
parent = cur;
if (kv.first < cur->_kv.first)
cur = cur->_left;
else if (kv.first > cur->_kv.first)
cur = cur->_right;
else
return false;
}
cur = new Node(kv);
if (kv.first < parent->_kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
// 2. 调整颜色以恢复红黑性质
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent;
if (grandfather->_left == parent) {
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) {
// Case 1: 父节点和叔叔节点为红色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
// Case 2 & 3: 叔叔节点为黑色或不存在
if (cur == parent->_right) {
RotateL(parent);
swap(parent, cur);
}
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break;
}
} else {
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED) {
// Case 1: 父节点和叔叔节点为红色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
// Case 2 & 3: 叔叔节点为黑色或不存在
if (cur == parent->_left) {
RotateR(parent);
swap(parent, cur);
}
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break;
}
}
}
_root->_col = BLACK; // 确保根节点为黑色
return true;
}
private:
void RotateL(Node*& parent) {
Node* rightChild = parent->_right;
parent->_right = rightChild->_left;
if (rightChild->_left)
rightChild->_left->_parent = parent;
rightChild->_parent = parent->_parent;
if (!parent->_parent)
_root = rightChild;
else if (parent == parent->_parent->_left)
parent->_parent->_left = rightChild;
else
parent->_parent->_right = rightChild;
rightChild->_left = parent;
parent->_parent = rightChild;
}
void RotateR(Node*& parent) {
Node* leftChild = parent->_left;
parent->_left = leftChild->_right;
if (leftChild->_right)
leftChild->_right->_parent = parent;
leftChild->_parent = parent->_parent;
if (!parent->_parent)
_root = leftChild;
else if (parent == parent->_parent->_left)
parent->_parent->_left = leftChild;
else
parent->_parent->_right = leftChild;
leftChild->_right = parent;
parent->_parent = leftChild;
}
private:
Node* _root = nullptr;
};
5.AVL树和红黑树的比较
对比项 | 红黑树 | AVL树 |
---|---|---|
树的高度 | 相对较高,但保证 O(logn)O(\log n)O(logn)。 | 更加严格平衡,树的高度更接近最优。 |
插入效率 | 通常更快,最多 O(logn)O(\log n)O(logn) 次旋转,且常数因子较小。 | 较慢,可能需要多次旋转(最多 2 次)。 |
删除效率 | 通常更快,调整操作较少,常数因子较低。 | 较慢,可能需要多次旋转(最多 O(logn)O(\log n)O(logn))。 |
查找效率 | 较慢,因高度可能比 AVL 稍高。 | 较快,因为严格平衡导致树更矮。 |
空间效率 | 较优,因旋转次数少、存储开销小。 | 略差,因频繁旋转需要更多栈调用等开销。 |
适用场景 | 插入、删除频繁的场景(如字典、关联容器)。 | 查找频繁的场景(如数据库索引)。 |
,因为严格平衡导致树更矮。 |
| 空间效率 | 较优,因旋转次数少、存储开销小。 | 略差,因频繁旋转需要更多栈调用等开销。 |
| 适用场景 | 插入、删除频繁的场景(如字典、关联容器)。 | 查找频繁的场景(如数据库索引)。 |