C++ —— 红黑树
红黑树的介绍
红黑树是一种自平衡二叉搜索树,它在插入和删除操作后,仍能保持树的平衡,从而保证了在最坏情况下,基本动态集合操作的时间复杂度为
O
(
l
o
g
n
)
O(log n)
O(logn)。这种高效性使得红黑树在许多应用中得到了广泛使用,例如C++的标准模板库(STL)中的map
和set
。
红黑树的性质
红黑树有以下五个性质:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点(NIL节点,表示空节点)是黑色。
- 如果一个节点是红色,则它的两个子节点都是黑色。(即不存在两个相连的红色节点)
- 对每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
这些性质确保了红黑树在插入和删除操作后仍然保持平衡,从而保证了高效的操作时间复杂度。
红黑树结点的定义
enum Colour
{
RED,
BLACK
};
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)
{}
};
这里通过枚举常量来控制红黑树每个节点的颜色。
红黑树的插入
在红黑树中插入节点后,可能会破坏红黑树的性质。为了修复树,需要进行以下调整步骤:
- 插入节点并着色:
- 新插入的节点默认着色为红色。这是因为插入红色节点比插入黑色节点更少可能违反红黑树的性质。
- 复红黑树的性质:
- 从新插入的节点开始,通过向上检查和调整来维持红黑树的性质。
- 调整策略:
- 情况 1 :新插入的节点是根节点:
- 接将根节点着色为黑色以保持根节点为黑色。
- 情况 2 :新插入的节点的父节点是黑色:
- 入操作不会破坏红黑树的性质,不需要进行任何操作。
- 情况 3 :新插入的节点的父节点是红色:
- 由于红色父节点意味着违反了红黑树的性质 (不能有两个连续的红色节点) ,需要进行调整。
- 情况 3.1 :叔叔节点是红色:
- 叔叔节点的颜色影响了树的调整。叔叔节点为红色意味着叔叔节点和父节点都为红色,祖父节点必须为黑色。
- 父节点和叔叔节点着色为黑色,将祖父节点着色为红色,并将当前节点移至祖父节点继续检查。这是因为此时祖父节点变成了红色节点,如果祖父节点的父节点也是红色,则可能再次违反红黑树的性质。
- 情况 3.2 :叔叔节点是黑色或NIL:
- 这种情况就得需要进行旋转操作来修复树。
- 情况 3.2.1 :当前节点是右孩子且父节点是左孩子(即左右) :
- 进行左旋,以便转化为左-左情况,再进行右旋。
- 情况 3.2.2 :当前节点是左孩子且父节点是右孩子(即右左) :
- 进行右旋,以便转化为右-右情况,再进行左旋。
- 情况 3.2.3 :当前节点是左孩子且父节点是左孩子(即左左) :
- 祖父节点进行右旋,并调整颜色。
- 情况 3.2.4 :当前节点是右孩子且父节点是右孩子(即右右) :
- 对祖父节点进行左旋,并调整颜色。
- 情况 1 :新插入的节点是根节点:
为什么要看叔叔节点:
叔叔节点的颜色决定了如何调整树。如果叔叔是红色,我们需要重新着色并向上移动;如果叔叔是黑色,我们需要进行旋转操作。
叔叔节点为红色和黑色或NIL的不同处理:
叔叔节点为红色:
- 说明父节点和叔叔节点都是红色,这违反了红黑树的性质。通过将父节点和叔叔节点着色为黑色,并将祖父节点着色为红色,我们可以消除冲突,并将问题向上移动到祖父节点。
叔叔节点为黑色或NIL:
- 这种情况下,需要通过旋转操作来修复树的平衡。根据当前节点、父节点和祖父节点的位置,可能需要单旋转或双旋转。
情况1 情况2
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->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
//新插入的节点,初始化颜色为红
cur = new Node(kv);
cur->_col = RED;
if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
如果插入的是root
或者父亲是黑色节点则不会破坏树的稳定,则直接插入即可。
情况3
由于红色父节点意味着违反了红黑树的性质(不能有两个连续的红色节点),需要进行调整。
情况3.1
如果叔叔是红色的,则只需要更改叔叔,父亲和祖父的颜色即可。
因为要满足即不能有连续的红色节点并且每条路的黑色节点必须相同这两个条件,所以我们可以在叔叔是红色的情况下 ,适当的增加黑色节点的个数已达平衡,如下图:
父节点和叔叔节点着色为黑色,将祖父节点着色为红色,并将当前节点移至祖父节点继续检查。
情况3.2
该情况为叔节点是黑色或NIL,此时就需要旋转操作才能来修复树。
此情况又分为以下四种情况:
情况 | 旋转 |
---|---|
父节点是左孩子且节点是右孩子(即左右) | 进行左旋,以便转化为左-左情况,再进行右旋 |
父节点是右孩子且节点是左孩子(即右左) | 进行右旋,以便转化为右-右情况,再进行左旋 |
父节点是左孩子且节点是左孩子(即左左) | 父节点进行右旋,并调整颜色 |
父节点是右孩子且节点是右孩子(即右右) | 对祖父节点进行左旋,并调整颜色 |
3.2.1父节点是左孩子且节点是右孩子(即左右)
进行左旋,以便转化为左-左情况,再进行右旋。
3.2.2父节点是右孩子且节点是左孩子(即右左)
进行右旋,以便转化为右-右情况,再进行左旋。
3.2.3 父节点是左孩子且节点是左孩子(即左左)
对祖父节点进行右旋,并调整颜色。
3.2.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->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
//新插入的节点,初始化颜色为红
cur = new Node(kv);
cur->_col = RED;
if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//如果父亲的颜色是红色,违反规则
while (parent && parent->_col == RED)
{
//分情况探讨,首先判断父亲和叔叔的相对位置
Node* grandfather = parent->_parent;
//父亲在左边
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
//如果叔叔为红色,则变色继续上调
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
//继续上调
cur = grandfather;
parent = cur->_parent;
}
else
//叔叔是黑色或者空
{
//左左情况
// g
// p u
// c
if (cur == parent->_left)
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
//左右情况
// g
// p u
// c
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else
//父亲在右边
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
//叔叔是红
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
//叔叔黑或空
{
if (cur == parent->_right)
{
//右右情况
// g
// u p
// c
RotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else
{
//右左情况
// g
// u p
// c
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
void RotateR(Node* parent)
{
Node* SubL = parent->_left;
Node* SubLR = SubL->_right;
parent->_left = SubLR;
if (SubLR)
{
SubLR->_parent = parent;
}
SubL->_right = parent;
Node* ppNode = parent->_parent;
parent->_parent = SubL;
if (parent == _root)
{
_root = SubL;
SubL->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = SubL;
}
else
{
ppNode->_right = SubL;
}
SubL->_parent = ppNode;
}
}
void RotateL(Node* parent)
{
Node* SubR = parent->_right;
Node* SubRL = SubR->_left;
parent->_right = SubRL;
if (SubRL)
{
SubRL->_parent = parent;
}
Node* ppNode = parent->_parent;
parent->_parent = SubR;
SubR->_left = parent;
if (parent == _root)
{
_root = SubR;
_root->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = SubR;
}
else
{
ppNode->_right = SubR;
}
SubR->_parent = ppNode;
}
}
红黑树的验证
因为红黑树是一种特殊的二叉搜索树,所以我们可以先获取二叉树的中序遍历序列,来判断该二叉树是否满足二叉搜索树的性质。
//中序遍历
void Inorder()
{
_Inorder(_root);
}
//中序遍历子函数
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << " ";
_Inorder(root->_right);
}
再后续通过 check 和 IsBalance
函数检查是否平衡
bool IsBalance()
{
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);
}
bool Check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
//cout << blackNum << endl;
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);
}
红黑树与AVL树的比较
红黑树和AVL树都是高效的自平衡二叉搜索树,增删查改的时间复杂度都是 O ( l o g n ) O(log n) O(logn)。它们在保持树的平衡方面采取了不同的方法:
AVL树
- 平衡方式:AVL树通过控制每个节点的左右子树高度差不超过1来保持平衡。这种方式实现了二叉树的严格平衡。
- 优点:由于严格的平衡,AVL树在查找操作上的性能非常好,适合查找较多的场景。
- 缺点:由于严格的平衡性要求,在插入和删除操作时可能需要进行较多的旋转操作,性能会稍逊于红黑树。
红黑树
- 平衡方式:红黑树通过控制节点的颜色,使得最长可能路径不超过最短可能路径的2倍来保持近似平衡。
- 优点:红黑树在插入和删除操作时所需的旋转次数较少,适合插入和删除操作较多的场景。因此,红黑树在实际应用中更为常见。
- 缺点:相较于AVL树,红黑树的查找操作性能稍逊,但整体仍然保持在 O ( l o g n ) O(log n) O(logn)的复杂度。
总结:
- AVL树:通过控制高度差实现严格平衡,查找性能优异,但插入和删除操作较频繁时,旋转次数较多。
- 红黑树:通过节点颜色控制实现近似平衡,插入和删除操作较频繁时,旋转次数较少,性能更优。
在实际应用中,红黑树由于在插入和删除操作上的性能优势,被广泛应用于如C++ STL中的map和set等场景。