目录
一、红黑树的概念:
红黑树也是平衡二叉搜索树的一种,相较于AVL树,红黑树在平衡的控制上就没有那么严格了,也不用“平衡因子”去控制树的高度,而是用红黑两种颜色来控制,如下图:
图中的N为空节点,画出来比较详细一点,而且这里的叶子节点就是空节点N
红黑树的规则就可以简记为一个口诀:左根右 根叶黑 不红红 黑路同
左根右:必须是二叉搜索树(左<根<右)。
根叶黑:根节点和叶子节点都是黑色(注意这里的叶子节点是空节点,也就是图中的N)。
不红红:同一条路径上不能有连续的两个红色节点,换句话说就是每个红色节点的左右孩子都必须得是黑色。
黑路同:任意一个节点到叶子节点的每一条路径上的黑色节点的数量都是相同的(例如从根节点开始,无论哪一条路径,黑色节点的数量都是3个)
二、红黑树的模拟实现:
1、定义树的节点:
跟AVL树一样,用三叉链表链接关系,不同的是不需要平衡因子去控制平衡了,而是改用颜色来控制了,所以我们可以直接枚举颜色(因为就只有红色和黑色)
// 枚举颜色
enum Colour
{
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;
Colour _col; // 初始化时初始为红色
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(RED)
{}
};
2、红黑树的插入操作:
(1)插入节点的颜色选择:
对于新插入的节点,我们会将其颜色设置为红色,为什么不设置为黑色呢?首先我们来看我们的口诀的最后一条:黑路同。也就是每一条路径的黑色节点的数量是相同的,如果我们插入节点为黑色,那么必定破坏这一条规则,就必定要对树做出平衡调整,如果插入节点为红色,那么只有可能破坏根叶黑和不红红这两条规则,相比之下,做出调整的情况就会大大减少。
(2)插入后的平衡调整:
根据上面所说,插入节点为红色,只可能会违反根叶黑,不红红这两条规则,根叶黑好办,如果根节点为红色,只需将根节点颜色变为红色即可,所以我们重点来关注如何调整违反了不红红这条规则的节点。
违反不红红的情况下,我们重点要关注插入节点的叔叔的颜色,如图:
【叔叔节点为红色】:
当插入节点的叔叔为红色时,让叔父爷三个节点变色,然后爷爷节点变为插入节点继续往上判定。如图:(为了方便看图,隐藏的空节点(N)就不显示出来了)
【叔叔节点为黑色】:
当插入节点的叔叔为黑色时,则看一下是LL、RR、LR、RL那种类型,根据类型进行旋转,然后对旋转点和旋转中心进行变色即可。
其他类型的对应的旋转操作和演示均在上一章的【AVL树的模拟实现】中画过详细的图解,有需要的可以去补一下,这里就不再过多的赘述,需要注意的是:旋转中心的选择,LL型和RR型分别只需要旋转点(也就是爷爷节点)进行一次右单旋和左单旋即可,所以旋转中心就是父节点,如图:
而LR型和RL型则都需要进行两次旋转,所以他俩的旋转中心要在旋转完一次后才定,旋转一次后父节点的位置就变成cur了,所以旋转中心是cur,而不是父节点,如图:
(3)代码形式:
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 (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else // cur->_kv.first == kv.first
{
return false;
}
}
cur = new Node(kv);
cur->_col = RED; // 新节点颜色为红
if (parent->_kv.first < kv.first) // 确定cur的位置
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
// 新插入节点后就看父亲的节点,是黑色就结束了,是红色就违反了不红红的规则,需要进行更新
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent; // 定义爷爷节点
Node* uncle; // 定义叔叔节点
if (parent == grandfather->_left)// 也就是叔叔是爷爷节点的右孩子
{
uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
// 叔父爷变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else if (!uncle || uncle->_col == BLACK) // 旋转变色原则是旋转点和旋转中心变色
{
if (cur == parent->_left) // 爷爷右单旋型 LL
{
RotateR(grandfather);
// 变色处理
grandfather->_col = RED;
parent->_col = BLACK;
}
else // LR型,左孩子左旋,爷爷右旋型
{
RotateL(parent);
RotateR(grandfather);
// 变色处理
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
else { assert(false); } // 不可能存在除此以外的情况
}
else // parent == grandfather->_right,也就是叔叔是爷爷节点的左孩子
{
uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
// 叔父爷变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else if (!uncle || uncle->_col == BLACK) // 旋转变色原则是旋转点和旋转中心变色
{
if (cur == parent->_right) // 爷爷左单旋型 RR
{
RotateL(grandfather);
// 变色处理
grandfather->_col = RED;
parent->_col = BLACK;
}
else // RL型,右孩子右旋,爷爷左单旋型 (cur == parent->left)
{
RotateR(parent);
RotateL(grandfather);
// 变色处理
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
else { assert(false); } // 除上面两种情况外没有其他情况了
}
}
_root->_col = BLACK; // 无论怎样,确保最终根节点的颜色为黑色
return true;
}
左单旋和右单旋:
左右单旋的代码可以直接复用AVL树的左右单旋的代码,思想是一样的,只不过不需要平衡因子了,这里步骤的讲解就不再过多赘述,有需要的可以去看AVL树的那一章节哈。
// 右单旋
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;
_root->_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;
subR->_left = parent;
Node* ppNode = parent->_parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (ppNode->_right == parent)
{
ppNode->_right = subR;
}
else
{
ppNode->_left = subR;
}
subR->_parent = ppNode;
}
}
3、红黑树的判平衡:
我们可以来看红黑树的口诀的第四点:黑路同。我们可以知道从根节点开始的任意一条路径上的黑色节点数量都与其他路径上的黑色节点数量是相同的,所以我们可以先求出任意一条路径上的黑色节点,然后再比对一下其他路径上的黑色节点,相同就代表平衡,不同则代表该树不平衡。
代码形式:
// 外部封装:
bool IsBalance()
{
if (_root->_col == RED) { return false; }
// 取一个参考值(任意路径上的黑色节点数量)
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
refNum++;
cur = cur->_left;
}
// 通过Check函数去递归获取从根节点开始的每一条路径上的黑色节点
return Check(_root, 0, refNum);
}
// 核心代码:
bool Check(Node* root, int BlackNum, const int refNum)
{
if (root == nullptr) // 当走到空时判断一下是否相等
{
if (refNum != BlackNum)
{
cout << "存在黑色节点数量不相同的路径!!!" << endl;
return false;
}
return true;
}
// BlackNum 传值的是形参,每一个栈帧中都存在一个属于自己的BlackNum,下面的++不影响上一层。
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);
}
三、完整代码:
// 枚举颜色
enum Colour
{
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;
Colour _col; // 初始化时初始为红色
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(RED)
{}
};
template<class K, class V>
class RBTree
{
public:
typedef RBTreeNode<K, V> Node;
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 (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else // cur->_kv.first == kv.first
{
return false;
}
}
cur = new Node(kv);
cur->_col = RED; // 新节点颜色为红
if (parent->_kv.first < kv.first) // 确定cur的位置
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
// 新插入节点后就看父亲的节点,是黑色就结束了,是红色就违反了不红红的规则,需要进行更新
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent; // 定义爷爷节点
Node* uncle; // 定义叔叔节点
if (parent == grandfather->_left)// 也就是叔叔是爷爷节点的右孩子
{
uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
// 叔父爷变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else if (!uncle || uncle->_col == BLACK) // 旋转变色原则是旋转点和旋转中心变色
{
if (cur == parent->_left) // 爷爷右单旋型 LL
{
RotateR(grandfather);
// 变色处理
grandfather->_col = RED;
parent->_col = BLACK;
}
else // LR型,左孩子左旋,爷爷右旋型
{
RotateL(parent);
RotateR(grandfather);
// 变色处理
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
else { assert(false); } // 不可能存在除此以外的情况
}
else // parent == grandfather->_right,也就是叔叔是爷爷节点的左孩子
{
uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
// 叔父爷变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else if (!uncle || uncle->_col == BLACK) // 旋转变色原则是旋转点和旋转中心变色
{
if (cur == parent->_right) // 爷爷左单旋型 RR
{
RotateL(grandfather);
// 变色处理
grandfather->_col = RED;
parent->_col = BLACK;
}
else // RL型,右孩子右旋,爷爷左单旋型 (cur == parent->left)
{
RotateR(parent);
RotateL(grandfather);
// 变色处理
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
else { assert(false); } // 除上面两种情况外没有其他情况了
}
}
_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;
_root->_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;
subR->_left = parent;
Node* ppNode = parent->_parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (ppNode->_right == parent)
{
ppNode->_right = subR;
}
else
{
ppNode->_left = subR;
}
subR->_parent = ppNode;
}
}
void InOrder() { _InOrder(_root); cout << endl; }
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);
}
private:
void _InOrder(Node* root)
{
if (root == nullptr) { return; }
_InOrder(root->_left);
cout << root->_kv.first << " : " << root->_kv.second << endl;
_InOrder(root->_right);
}
bool Check(Node* root, int BlackNum, const int refNum)
{
if (root == nullptr) // 当走到空时判断一下是否相等
{
if (refNum != BlackNum)
{
cout << "存在黑色节点数量不相同的路径!!!" << endl;
return false;
}
return true;
}
// BlackNum 传值的是形参,每一个栈帧中都存在一个属于自己的BlackNum,下面的++不影响上一层。
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);
}
private:
Node* _root = nullptr;
};
代码测试:
如下:我们随机插入100万个值,再判断一下是否平衡:
四、红黑树的总结:
1、关于红黑树的删除:
对于红黑树的删除,让我们回忆一下二叉搜索树的删除(有三种情况):
1、删除节点没有孩子 -> 直接删除
2、只有左子树 / 右子树 -> 直接代替
3、左右子树都有 -> 直接后继(或前驱)代替值,然后转换为前两种情况
其实红黑树的删除和二叉搜索树的删除差不多,只不过如果破坏了性质,那么就需要进行一些调整
这里先简单提两句,红黑树插入节点容易破坏【不红红】这个性质,删除节点呢,就更容易破坏【黑路同】这个性质,例如:删除的节点是黑色那么就必定会破坏【黑路同】的性质,此时的调整会比较麻烦,这里后面会单独写一篇关于红黑树的删除。
2、红黑树的总结:
红黑树是一种高效且实用的数据结构,适用于需要频繁进行查找、插入和删除操作的动态数据集,在查找、插入和删除操作上的时间复杂度都是O(logN),它的特性使它最长路径不会超过最短路径的两倍。相比于AVL树而言,平衡的调整次数就大大减少,提高了插入删除的效率,这使得红黑树在处理大规模动态数据集时更加高效。