1. 红黑树介绍
红黑树是一棵二叉搜索树。之所以称为红黑树,是因为它增添了一个颜色变量用于描述结点的状态,对于红黑树中的结点,它的颜色不是黑的,就是红的。
红黑树控制平衡的逻辑在于,在红黑树中,最长路径所含的结点数始终不超过最短路径所含结点数的两倍(一条路径是指从根结点不断向下走,直到空为止)。
相比于AVL树,红黑树控制平衡的逻辑没有那么严格,因此在搜索时,整体的效率可能会略低于AVL树,不过无论是AVL树,还是红黑树,搜索的时间复杂度都是O(log(n))级别的。
2. 红黑树规则
由上述介绍我们可知,红黑树控制平衡的关键在于最长路径不超过最短路径长的两倍。
为了使得最长路径和最短路径满足上述关系,红黑树有以下四条规则:
- 每个结点的颜色不是红就是黑
- 红黑树的根结点颜色必为黑
- 在红黑树中,每条路径所含的黑色结点数一定相同
- 整棵红黑树中,红结点的孩子结点一定为黑,即任意一条路径中,不能存在两个连续的红结点
为什么上述规则就能控制最长路径不超过最短路径长的两倍呢?
我们有如下分析:
- 首先由于每条路径所含黑色结点数一定相同,因此我们每条路径所含的黑色结点数为n,那么每条路径的长度一定大于或等于n。
- 由于根结点颜色必为黑,且任意一条路径不存在两个连续的红结点,同时又要控制每条路径的黑色结点数相同,因此一条理论上的最长路径应是黑红黑红… 这样交错的,若每条路径的黑色结点数为n,那么理论上的最长路径长应为2n。
- 所以,对于一棵每条路径所含黑色结点数为n的红黑树而言,理论最短路径长为n,理论最长路径长为2n。
由上述分析可见,这四条规则成功使得红黑树中最长路径长不超过最短路径长的两倍。
3. 红黑树插入的实现
红黑树的插入按如下顺序进行:
- 首先,按照二叉搜索树的插入规则,找到相应的插入位置,进行插入
- 插入的结点必须将颜色设置为红色,因为红黑树必须确保每条路径的黑色结点数相同。如果随意插入黑色结点,将破坏这条规则。
- 插入结点后,看情况进行相应调整。以下将对不同情况下如何进行调整进行详细介绍。
插入结点后,情况可以分为三类:
- 原树为空树,插入结点为根结点。此时不需要做额外调整,仅需将根结点的颜色改为黑色即可。
- 原树不为空树,但插入结点的父结点为黑色。此时直接插入即可,不需要做任何调整。
- 原树不为空树,但插入结点的父结点为红色。这时需要调整,因为红黑树的规则决定了任意一条路径中不能有两个相邻的红色结点。这种情况又可以继续细分为两类。
- 有叔叔结点(父结点的兄弟结点),且叔叔结点的颜色为红色。此时我们将父结点和叔叔结点的颜色均变为黑色,同时将爷爷结点的颜色变为红色;再以爷爷结点作为子结点,如若此时仍满足该种情况的条件,继续重复上述操作,直到根结点为止。需要注意,如果一直重复上述操作直到根结点,会将根结点变红,因此最后我们要将根结点重新置为黑色。
- 有叔叔结点,但叔叔结点的颜色为黑色,或者不存在叔叔结点。此时我们需要进行的操作是**变色+旋转。**对于这种情况,如何变色,如何旋转,博主将具体画图说明。图解说明如下:
需要说明的是,这种情况也会分为两大类,因为新插入结点的父结点,可能是爷爷结点的左结点,也可能是右结点,不过这两种情况是对称的,相应的变色逻辑完全相同,旋转则是刚好相反。因此,下面图解中,博主仅介绍新插入结点的父结点为爷爷结点的左结点的情况,右结点的情况,读者可自行类比推导。另外,由于存在颜色为黑色的叔叔结点与叔叔结点不存在时的处理逻辑是相同的,因此下面图解说明中,以存在颜色为黑色的叔叔结点为例。
变色+右单旋的情况:
这种情况中,我们对爷爷结点G,即最上面结点所在的整棵树,做右旋(旋转的逻辑在此不介绍),然后将原先的父结点P颜色改为黑色,原先的爷爷结点G,颜色改为红色,同时注意更改与更上层结点的父子连接关系(如果有更上层结点的话),其余均不做修改。
变色+左右双旋:
首先,我们对以P为根结点的子树进行左旋,然后再对左旋后的,以G为根结点的树进行右旋,再将C结点变为黑,G结点变为红,同时注意与更上层结点之间的父子连接关系(如果有更上层结点的话),其余不作调整。
将对应的子树进行相应的旋转和变色调整后,可以直接跳出整个不断调整的循环。因为,调整过后,既使得调整的树符合红黑树规则,同时又保证了各个路径上的黑色结点数,在调整前后,保持不变,因此不会对更上层结点(如果有的话)造成影响,因此可以直接break。
我们可以看到,上述两种情况进行相应处理后,最终得到的树的情况是相同的,并且保证了红黑树的规则不被破坏。
4. 红黑树插入的具体代码
enum COLOUR
{
RED,
BLACK
};
template<class K, class V>
struct RBTNode
{
pair<K, V> _kv;
RBTNode<K,V>* _left = nullptr;
RBTNode<K,V>* _right = nullptr;
RBTNode<K,V>* _parent = nullptr;
COLOUR _color;
RBTNode(const K& key = K(), const V& value = V())
:_kv(key, value)
{}
RBTNode(const pair<K, V>& kv)
:_kv(kv)
{}
};
template<class K, class V>
class RBTree
{
public:
typedef RBTNode<K, V> Node;
//右单旋
void RotateR(Node* son)
{
Node* parent = son->_parent;
Node* sonR = son->_right;
Node* gparent = parent->_parent;
son->_right = parent;
parent->_parent = son;
parent->_left = sonR;
if (sonR)
sonR->_parent = parent;
son->_parent = gparent;
if (gparent == nullptr)
{
//说明parent为根结点,旋转后,son为根结点
_root = son;
}
else
{
if (gparent->_left == parent)
gparent->_left = son;
else if (gparent->_right == parent)
gparent->_right = son;
else
assert(false);
}
}
//左单旋
void RotateL(Node* son)
{
Node* parent = son->_parent;
Node* gparent = parent->_parent;
Node* sonL = son->_left;
son->_left = parent;
parent->_parent = son;
parent->_right = sonL;
if (sonL)
sonL->_parent = parent;
son->_parent = gparent;
if (gparent == nullptr)
{
_root = son;
}
else
{
if (gparent->_left == parent)
{
gparent->_left = son;
}
else if (gparent->_right == parent)
{
gparent->_right = son;
}
else
assert(false);
}
}
void Insert(const pair<K, V>& kv)
{
Node* ptr = new Node(kv);
ptr->_color = RED;
if (_root == nullptr)
{
_root = ptr;
_root->_color = BLACK;
}
else
{
//遍历整棵二叉搜索树,找到应处位置
Node* cur = _root;
Node* pre = nullptr;
while (cur)
{
pre = cur;
if (ptr->_kv.first < cur->_kv.first)
{
cur = cur->_left;
}
else if (ptr->_kv.first > cur->_kv.first)
{
cur = cur->_right;
}
else//暂且不处理相等的情况
{
delete ptr;
return;
}
}
ptr->_parent = pre;
if (ptr->_kv.first < pre->_kv.first)
{
pre->_left = ptr;
}
else
{
pre->_right = ptr;
}
}
Node* cur = ptr;
Node* parent = ptr->_parent;
//插入结点,初始化完成,开始变色+旋转
//1.如果parent为空(根结点插入情况)或者parent为黑,不需要处理
//2. 需要处理的是parent不为空并且parent为红的情况
while (parent && parent->_color == RED)
{
Node* grandParent = parent->_parent;
if (parent == grandParent->_left)
{
Node* uncle = grandParent->_right;
//分三种情况,但两种处理来讨论
//1.叔叔存在且为红,不断向上调整,此时仅变色即可
//注意,不能写成循环的形式
if (uncle && uncle->_color == RED)
{
parent->_color = uncle->_color = BLACK;
grandParent->_color = RED;
cur = grandParent;
parent = cur->_parent;
}
//2.叔叔存在且为黑,或叔叔不存在,此时需变色+旋转
//同样内部不能写成循环的形式
else if (uncle == nullptr || uncle && uncle->_color == BLACK)
{
if (cur == parent->_left)
{
//单旋+变色
RotateR(parent);
parent->_color = BLACK;
grandParent->_color = RED;
break;
}
else if (cur == parent->_right)
{
//双旋+变色
RotateL(cur);
RotateR(cur);
cur->_color = BLACK;
grandParent->_color = RED;
break;
}
else
{
assert(false);
}
}
else
{
assert(false);
}
}
else if (parent == grandParent->_right)
{
Node* uncle = grandParent->_left;
//分三种情况,但两种处理来讨论
//1.叔叔存在且为红,不断向上调整,此时仅变色即可
//注意,不能写成循环的形式
if (uncle && uncle->_color == RED)
{
parent->_color = uncle->_color = BLACK;
grandParent->_color = RED;
cur = grandParent;
parent = cur->_parent;
}
//2.叔叔存在且为黑,或叔叔不存在,此时需变色+旋转
//同样内部不能写成循环的形式
else if (uncle == nullptr || uncle && uncle->_color == BLACK)
{
if (cur == parent->_right)
{
//单旋+变色
RotateL(parent);
parent->_color = BLACK;
grandParent->_color = RED;
break;
}
else if (cur == parent->_left)
{
//双旋+变色
RotateR(cur);
RotateL(cur);
cur->_color = BLACK;
grandParent->_color = RED;
break;
}
else
{
assert(false);
}
}
else
{
assert(false);
}
}
else
{
assert(false);
}
}
_root->_color = BLACK;//无脑将根结点的颜色保证为黑色
}
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->_color == RED && root->_parent->_color == RED)
{
cout << root->_kv.first << "存在连续的红色结点" << endl;
return false;
}
if(root->_color == BLACK)
{
blackNum++;
}
return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}
bool IsBalance()
{
if (_root == nullptr)
return true;
if (_root->_color == RED)
return false;
// 参考值
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_color == BLACK)
{
++refNum;
}
cur = cur->_left;
}
return Check(_root, 0, refNum);
}
private:
Node* _root = nullptr;//根结点
};
5. 红黑树的删除调整
红黑树删除结点时的相应调整方法,在本篇博客中暂不做介绍,不过删除的调整操作要比插入调整更为复杂,至多会有三次旋转和log(N)级别的变色次数(N为红黑树中的结点个数)。