【C++游记】红黑树
枫の个人主页
你不能改变过去,但你可以改变未来
算法/C++/数据结构/C
Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记】
话不多说,让我们一起进入今天的学习吧~~~
目录
一、红黑树的概念
1.1 红黑树的规则
1.2 红黑树如何确保最长路径不超过最短路径的 2 倍?
1.3 红黑树的效率
二、红黑树的实现
2.1 红黑树的结构
2.2 红黑树的插入
插入过程概述
情况 1:变色处理
情况 2:单旋 + 变色
情况 3:双旋 + 变色
插入代码实现
2.3 红黑树的查找
2.4 红黑树的验证
三、总结
四、结语
一、红黑树的概念
红黑树是一种二叉搜索树,它的每个结点增加了一个存储位来表示结点的颜色(红色或黑色)。通过对从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出 2 倍,从而实现了接近平衡的状态。
1.1 红黑树的规则
- 每个结点不是红色就是黑色
- 根结点是黑色的
- 如果一个结点是红色的,则它的两个孩子结点必须是黑色的(即任意一条路径不会有连续的红色结点)
- 对于任意一个结点,从该结点到其所有 NULL 结点的简单路径上,均包含相同数量的黑色结点
说明:《算法导论》等书籍上补充了一条规则:每个叶子结点 (NIL) 都是黑色的。这里所指的叶子结点不是传统意义上的叶子结点,而是空结点,有些书籍上也把 NIL 叫做外部结点。NIL 是为了方便准确地标识出所有路径,在实际实现中有时会被忽略,大家了解这个概念即可。
1.2 红黑树如何确保最长路径不超过最短路径的 2 倍?
由规则 4 可知,从根到 NULL 结点的每条路径都有相同数量的黑色结点。我们将这个数量称为黑色高度(bh)。
- 最短路径就是全是黑色结点的路径,长度为 bh
- 由规则 2 和规则 3 可知,任意一条路径不会有连续的红色结点,因此最长的路径就是一黑一红间隔组成,长度为 2*bh
- 综合来看,任意一条从根到 NULL 结点路径的长度 h 满足:bh≤h≤2∗bh
1.3 红黑树的效率
假设红黑树中结点数量为 N,最短路径的长度为 h,那么有:2^h−1≤N<2^(2*h)−1,由此可推出h≈logN。这意味着红黑树的增删查改操作最坏情况下的时间复杂度为O(logN)。
与 AVL 树相比,红黑树的表达相对抽象一些。AVL 树通过高度差直观地控制平衡,而红黑树通过颜色约束间接实现近似平衡。两者效率处于同一档次,但插入相同数量的结点时,红黑树的旋转次数更少,因为它对平衡的控制没那么严格。
二、红黑树的实现
2.1 红黑树的结构
首先定义结点的颜色和结构:
// 枚举值表示颜色
enum Colour
{RED,BLACK
};// 按key/value结构实现
template<class K, class V>
struct RBTreeNode
{// 包含parent指针用于平衡控制pair<K, V> _kv;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;Colour _col;RBTreeNode(const pair<K, V>& kv): _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) // 新增节点默认为红色{}
};template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;public:// 成员函数将在后面实现
private:Node* _root = nullptr;
};
2.2 红黑树的插入
插入过程概述
- 按二叉搜索树规则插入新结点
- 若为空树,新增结点为黑色;若非空树,新增结点为红色(避免破坏规则 4)
- 若父亲结点是黑色,插入结束;若父亲结点是红色(违反规则 3),则需要根据叔叔结点的情况进行处理
说明:新增结点标识为 c (cur),c 的父亲标识为 p (parent),p 的父亲标识为 g (grandfather),p 的兄弟标识为 u (uncle)
情况 1:变色处理
当 c 为红,p 为红,g 为黑,u 存在且为红时:
- 将 p 和 u 变黑,g 变红
- 把 g 当做新的 c,继续往上更新
分析:p 和 u 都是红色,g 是黑色,将 p 和 u 变黑使左边子树路径各增加一个黑色结点,g 变红保持了 g 所在子树的黑色结点数量不变,同时解决了 c 和 p 连续红色的问题。需要继续往上更新是因为 g 变为红色后,若 g 的父亲也是红色,则仍违反规则 3。
这种情况只需要变色,不需要旋转,无论 c 是 p 的左孩子还是右孩子,p 是 g 的左孩子还是右孩子,处理方式都相同。
情况 2:单旋 + 变色
当 c 为红,p 为红,g 为黑,u 不存在或 u 存在且为黑时:
- 若 p 是 g 的左孩子,c 是 p 的左孩子:以 g 为旋转点进行右单旋,然后将 p 变黑,g 变红
- 若 p 是 g 的右孩子,c 是 p 的右孩子:以 g 为旋转点进行左单旋,然后将 p 变黑,g 变红
分析:此时单纯变色无法解决问题,需要旋转 + 变色。旋转后 p 成为新的根,子树黑色结点的数量不变,且没有连续的红色结点,不需要继续往上更新。
情况 3:双旋 + 变色
当 c 为红,p 为红,g 为黑,u 不存在或 u 存在且为黑时:
- 若 p 是 g 的左孩子,c 是 p 的右孩子:先以 p 为旋转点进行左单旋,再以 g 为旋转点进行右单旋,然后将 c 变黑,g 变红
- 若 p 是 g 的右孩子,c 是 p 的左孩子:先以 p 为旋转点进行右单旋,再以 g 为旋转点进行左单旋,然后将 c 变黑,g 变红
分析:这种情况是 c 和 p 不在同一条直线上,需要先通过一次旋转将其变为直线,再进行另一次旋转,最后进行变色处理。处理后 c 成为新的根,子树黑色结点的数量不变,且没有连续的红色结点,不需要继续往上更新。
插入代码实现
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{// 键已存在,插入失败return false;}}// 插入新节点cur = new Node(kv);cur->_col = RED; // 新增节点为红色if (parent->_kv.first < 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){// 情况1:叔叔存在且为红,变色处理parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续往上处理cur = grandfather;parent = cur->_parent;}else{// 情况2和3:叔叔不存在或为黑,旋转+变色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{Node* uncle = grandfather->_left;if (uncle && uncle->_col == RED){// 情况1:叔叔存在且为红,变色处理parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续往上处理cur = grandfather;parent = cur->_parent;}else{// 情况2和3:叔叔不存在或为黑,旋转+变色if (cur == parent->_right){// 单旋(左单旋)RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{// 双旋(先右后左)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* pParent = parent->_parent;parent->_parent = subL;if (pParent == nullptr){_root = subL;}else{if (pParent->_left == parent)pParent->_left = subL;elsepParent->_right = subL;}subL->_parent = pParent;
}// 左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;Node* pParent = parent->_parent;parent->_parent = subR;if (pParent == nullptr){_root = subR;}else{if (pParent->_left == parent)pParent->_left = subR;elsepParent->_right = subR;}subR->_parent = pParent;
}
2.3 红黑树的查找
红黑树的查找操作与普通二叉搜索树相同,时间复杂度为O(logN):
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;
}
2.4 红黑树的验证
红黑树的验证需要检查是否满足前面提到的 4 条规则:
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);
}
三、总结
红黑树是一种高效的自平衡二叉搜索树,通过颜色约束实现了近似平衡,确保了增删查改操作的时间复杂度为O(logN)。与 AVL 树相比,红黑树在插入操作时旋转次数更少,因此在频繁插入的场景下表现更优。
红黑树的核心是维护其四条规则,通过变色和旋转两种操作来实现。插入操作需要根据叔叔节点的情况分为三种处理方式,而验证操作则需要检查是否满足所有规则。
四、结语
今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~