【 C++ 】红黑树
目录
1、红黑树的概念
2、红黑树的性质
3、红黑树节点的定义
4、红黑树结构
5、红黑树的插入操作
6、红黑树的验证
7、红黑树的删除
8、红黑树与AVL树的比较
9、红黑树的应用
10、源码链接
1、红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍(最长路径不超过最短路径的2倍),因而是接近平衡的。
同是二叉搜索平衡树,但是AVL树控制的比红黑树严格的多,AVL树要是每个节点的平衡因子绝对值不超过1,就会导致不断的去旋转调整,付出相对较高的代价,而这里红黑树更像是一种近似平衡,条件没有这么苛刻。
如下一棵树,站在红黑树的角度看是平衡的,站在AVL树的角度看就是不平衡的,需要旋转调整:
但是从搜索效率的角度看AVL树还是好一点,因为它的平衡标准高,就导致其更加平衡,相同数量的节点情况下AVL树的高度会更低,加上存100w个数据,AVL树大概有20层(log100w),而红黑树最坏就能达到40层,显然AVL树的搜索效率高。但是在内存里找20次和找40次没有什么区别,因为CPU足够的快,这里简单提一下。
2、红黑树的性质
- 1、每个结点不是红色就是黑色
- 2、根节点必须是黑色的
- 3、如果一个节点是红色的,则它的两个孩子结点是黑色的(没有连续的红色节点)
- 4、对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点(每条路径都包含相同数量的黑色节点)
- 5、每个叶子结点都是黑色的(此处的叶子结点指的是空结点 -》NIL节点)
根据这些规则,红黑树是如何保证最长路径不超过最短路径的2倍的呢?
首先我们根据规则分析得知,我们假设一条路径的黑色节点的个数为N个,则最长路径和最短路径的情况如下:
- 最短路径:全黑
- 最长路径:一黑一红间隔
而这里一黑一红间隔的原因在于红黑树不允许出现连续的红节点,为了能最大程度的保证最长节点数,唯有一黑一红间隔的方式才能达到最长,综上当黑节点个数固定为N时,最短路径节点个数为N,最长路径节点个数为2N
3、红黑树节点的定义
这里节点的实现相较于AVL树我们依旧是创建成KV模型、三叉链结构,唯一有所改变的是这里要通过枚举的方式把红色和黑色定义好,并在节点类内部定义变量_col表示节点颜色,最后记得写上构造函数。
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){} };
为什么插入的节点在构造函数这里要处理成红色?
- 如果处理成黑色,则一定导致新插入节点的那条路径多出一个黑色节点,不再满足各个路径黑色节点个数相同的性质,一定破坏性质4,此时很难维护。
- 如果处理成红色,则可能父亲节点也是红色,此时就出现了连续的红色节点,破坏性质3,不过此时我们向上调整即可,但如果父亲节点是黑色,那就无需操作了,不违反任何性质。
综合利弊,插入黑色节点一定会破坏性质4,而插入红色节点可能破坏性质3,因此处理成红色为宜。
4、红黑树类的基本框架
此模板类主要是用于红黑树的插入、旋转、调整、验证等等操作,基本框架如下:
//红黑树的类 template <class K, class V> class RBTree {typedef RBTreeNode<K, V> Node; public://…… private:Node* _root = nullptr; };
5、红黑树的插入操作
红黑树的插入操作主要分为这几大步骤:
- 1、一开始为空树,直接new新节点
- 2、一开始非空树,寻找插入的合适位置
- 3、找到插入的合适位置后,进行父亲与孩子的双向链接
- 4、检测新节点插入后,红黑树的性质是否造到破坏
接下来对其进行逐个分析:
- 1、一开始为空树,直接new新节点:
因为树为空的,所以直接new一个新插入的节点,将其作为根_root即可,接着更新颜色_col为黑色。
- 2、一开始非空树,寻找插入的合适位置:
这里和二叉搜索树的寻找合适的插入位置的思想一样,都要遵循以下几步:
- 插入的值 > 节点的值,更新到右子树查找
- 插入的值 < 节点的值,更新到左子树查找
- 插入的值 = 节点的值,数据冗余插入失败,返回false
当循环结束的时候,就说明已经找到插入的合适位置,即可进行下一步链接。
- 3、找到插入的合适位置后,进行父亲与孩子的双向链接:
注意这里节点的构成为三叉链,因此最后链接后端孩子和父亲是双向链接,具体操作如下:
- 插入的值 > 父亲的值,把插入的值链接在父亲的右边
- 插入的值 < 父亲的值,把插入的值链接在父亲的左边
- 因为是三叉连,插入后记得双向链接(孩子链接父亲)
走到这,说明节点已经插入完毕,接下来就要对红黑树的颜色进行调整了
- 4、检测新节点插入后,红黑树的性质是否造到破坏:
不是所有的情况都是需要进行调整的,当插入节点的父亲为黑色(新节点的默认颜色是红色),那么就不需要进行调整,因为没有破坏红黑树的任何一条性质。
只有当插入节点的父亲为红色时(新节点的默认颜色也是是红色),才需要进行调整,因为此时插入的节点和父亲都是红色节点,但是红黑树不允许出现连续的红色节点,此时就要进行调整。
注意这里既然插入节点cur的父亲p是红色,那么根据红黑树的性质(根结点是黑色的),其父亲的父亲g也就是祖父必然存在且一定是黑色,那么其父亲的兄弟节点u(可能不存在)也就是新插入节点cur的叔叔。因此我们约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点。
这里调整的办法主要是看叔叔节点的颜色如何,叔叔节点的不同,会导致三种不同的情况需要调整:
- 情况一:cur为红,p为红,g为黑,u存在且为红
- 情况二:cur为红,p为红,g为黑,u不存在
- 情况三:cur为红,p为红,g为黑,u存在且为黑
接下来分别进行讨论:
- 情况一:cur为红,p为红,g为黑,u存在且为红
为了避免出现连续的红色节点,我们可以把父节点p变黑,但是为了保证每条路径的黑色节点个数相同,我们需要把祖父节g点变红(不影响其它路径黑节点的个数),再把叔叔节点u变黑。
调整并未结束,此时祖父节点g为红色,但是如果这棵树本就是一颗完整的树呢?也就是g为根节点,那么只需要把节点g变成黑色即可。
如果这棵树是一棵树的子树,那么刚好把祖父节点g作为新插入的节点cur向上继续调整(继续判断父亲、叔叔如何……),直至调整结束。
- 补充:情况一不关心左右关系,只变色不旋转,所以 p、u是g的左或右是无所谓的,cur是p的左或右也是无所谓的。
接下来分析情况2:
- 情况二:cur为红,p为红,g为黑,u不存在
如果节点u不存在,则cur一定是新插入节点,因为如果cur不是新插入节点,则cur和p一定有一个节点的颜色是黑色,就不满足性质4:每条路径黑色节点个数相同。
此时就是一个很经典的右单旋结构(新节点插入较高左子树的左侧)我们可以先对其进行一个右单旋,再来更新颜色。具体步骤如下:
- 让祖父g变成父亲p的右子树
- 父亲p作为根节点
- 更新父亲节点p为黑色
- 更新祖父g为红色
- 补充:
如若p为g的右孩子,cur为p的右孩子,则针对p做左单旋转,示例:
如若祖孙三代的关系是折线(cur、parent、grandfather这三个结点为一条折现),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理。示例:
综上:
- p为g的左,cur为p的左,则进行右单旋 + p变黑,g变红
- p为g的右,cur为p的右,则进行左单旋 + p变黑,g变红
- p是g的左,cur是p的右,则进行左右双旋 + cur变黑, g变红
- p是g的右,cur是p的左,则进行右左双旋 + cur变黑, g变红
下面进入情况三
- 情况三:cur为红,p为红,g为黑,u存在且为黑
此情况绝非单独存在,绝不可能是真的新节点cur插入,然后还会出现p为红,g为黑,u存在且为黑的情况,如果存在,那么只能说明先前插入节点或者构造函数就有问题,因为插入前就不符合红黑树的性质啊(每个路径的黑节点个数均相同)
既然情况三出现了,那么一定是合理的,它就是建立在情况一的基础上继续往上调整从而出现的一种特殊情况,具体咱就是画图演示:
此时就是很明显的一个情况3了,cur为红,pp为红,gg为黑,u存在且为黑,由此证明,情况三是通过情况一向上继续调整演化出来的。并且此新节点一定是从p和x任意一颗左右子树插入或演化上来的,才引发后续的cur从黑变红。
此时就是一个很经典的右单旋结构(cur在较高左子树的左侧)我们可以先对其进行一个右单旋,再来更新颜色。具体步骤如下:
- 让p的右子树变成g的左子树
- 让p变成根节点位置
- p的右子树指向g
- 更新p的颜色为黑色
- 更新g的颜色为红色
- 补充:
如若p为g的右孩子,cur为p的右孩子,则进行左单旋 + 调色,示例:
若祖孙三代的关系是折现(cur、parent、grandfather这三个结点为一条折线),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理。示例:
综上:
- p为g的左,cur为p的左,则进行右单旋 + p变黑,g变红
- p为g的右,cur为p的右,则进行左单旋 + p变黑,g变红
- p是g的左,cur是p的右,则进行左右双旋 + cur变黑, g变红
- p是g的右,cur是p的左,则进行右左双旋 + cur变黑, g变红
情况二和情况三旋转 + 变色后,这颗子树不违反红黑树规则,相比插入前,且黑色节点的数量不变,不会影响上层,处理结束了。
代码如下:
bool Insert(const pair<K, V>& kv) {//1、一开始为空树,直接new新节点if (_root == nullptr){_root = new Node(kv);_root->_col = Black;//新插入的节点处理成黑色return true;}//2、寻找插入的合适位置Node* cur = _root;Node* parent = nullptr;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;//插入的值 = 节点的值,数据冗余插入失败,返回false}}//3、找到了插入的位置,进行父亲与插入节点的链接cur = new Node(kv);cur->_col = Red;//插入的节点处理成红色if (parent->_kv.first < kv.first){parent->_right = cur;//插入的值 > 父亲的值,链接在父亲的右边}else{parent->_left = cur;//插入的值 < 父亲的值,链接在父亲的左边}cur->_parent = parent;//三叉链,要双向链接//4、检测新节点插入后,红黑树的性质是否造到破坏while (parent && parent->_col == Red)//存在连续的红色节点{Node* grandfather = parent->_parent;assert(grandfather);//先确保叔叔的位置if (grandfather->_left == parent){Node* uncle = grandfather->_right;//情况一:cur为红,p为红,g为黑,u存在且为红if (uncle && uncle->_col == Red){//变色parent->_col = uncle->_col = Black;grandfather->_col = Red;//继续往上处理cur = grandfather;parent = cur->_parent;}//情况二+情况三:叔叔不存在,或者叔叔存在且为黑else{if (cur == parent->_left)//p为g的左,cur为p的左,则进行右单旋 + p变黑,g变红{// g// p// curRotateR(grandfather);parent->_col = Black;grandfather->_col = Red;}else//p是g的左,cur是p的右,则进行左右双旋 + cur变黑, g变红{// g// p// curRotateLR(grandfather);cur->_col = Black;grandfather->_col = Red;}break;}}else//grandfather->_right == parent{Node* uncle = grandfather->_left;//情况一:cur为红,p为红,g为黑,u存在且为红if (uncle && uncle->_col == Red){//变色parent->_col = uncle->_col = Black;grandfather->_col = Red;//继续往上处理cur = grandfather;parent = cur->_parent;}//情况二+情况三:叔叔不存在,或者叔叔存在且为黑else{if (cur == parent->_right)//p为g的右,cur为p的右,则进行左单旋 + p变黑,g变红{// g// p// curRotateL(grandfather);parent->_col = Black;grandfather->_col = Red;}else//p是g的右,cur是p的左,则进行右左双旋 + cur变黑, g变红{// g// p// curRotateRL(grandfather);cur->_col = Black;grandfather->_col = Red;}break;}}}_root->_col = Black;//暴力处理把根变成黑色return true; } //1、左单旋 void RotateL(Node* parent) {Node* subR = parent->_right;Node* subRL = subR->_left;Node* ppNode = parent->_parent;//提前保持parent的父亲//1、建立parent和subRL之间的关系parent->_right = subRL;if (subRL)//防止subRL为空{subRL->_parent = parent;}//2、建立subR和parent之间的关系subR->_left = parent;parent->_parent = subR;//3、建立ppNode和subR之间的关系if (parent == _root){_root = subR;_root->_parent = nullptr;}else{if (parent == ppNode->_left){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;//三叉链双向链接关系} } //2、右单旋 void RotateR(Node* parent) {Node* subL = parent->_left;Node* subLR = subL->_right;Node* ppNode = parent->_parent;//1、建立parent和subLR之间的关系parent->_left = subLR;if (subLR){subLR->_parent = parent;}//2、建立subL和parent之间的关系subL->_right = parent;parent->_parent = subL;//3、建立ppNode和subL的关系if (parent == _root){_root = subL;_root->_parent = nullptr;}else{if (parent == ppNode->_left){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;//三叉链双向关系} } //3、左右双旋 void RotateLR(Node* parent) {RotateL(parent->_left);RotateR(parent); } //4、右左双旋 void RotateRL(Node* parent) {RotateR(parent->_right);RotateL(parent); }
6、红黑树的验证
红黑树的验证主要分为两大步骤:
- 1、检测其是否满足二叉搜索树(中序遍历是否为有序序列)
- 2、检测其是否满足红黑树的性质
接下来分别演示:
- 1、检测其是否满足二叉搜索树(中序遍历是否为有序序列):
这里只需要递归写一个中序遍历,并判断测试用例的结果是否为一个有序序列即可判断二叉搜索树:
//验证是否为一颗搜索二叉树 void InOrder() {_InOrder(_root);//调用中序遍历子树cout << endl; } //中序遍历的子树 void _InOrder(Node* root) {if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right); }
- 2、检测其是否满足红黑树的性质:
这里只要判断是否满足红黑树的5大规则即可,具体操作如下:
- 1、根节点是否为黑色
- 2、任意一条路径黑色节点数是否相同(递归每一条和确定的一条比较是否相同)
- 3、递归检测是否违反性质三从而出现连续的红节点
bool IsBalanceTree() {Node* pRoot = _root;// 空树也是红黑树if (pRoot == nullptr)return true;// 检测根节点是否满足情况if (pRoot->_col != Black){cout << "违反红黑树性质二:根节点必须为黑色" << endl;return false;}// 获取任意一条路径中黑色节点的个数-->拿最左路径作为比较基准值size_t blackCount = 0;Node* pCur = pRoot;while (pCur){if (pCur->_col == Black)blackCount++;pCur = pCur->_left;}// 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数size_t k = 0;return _IsValidRBTree(pRoot, k, blackCount); } bool _IsValidRBTree(Node* pRoot, size_t k, const size_t blackCount) {//走到null之后,判断k和black是否相等if (pRoot == nullptr){if (k != blackCount){cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;return false;}return true;}// 统计黑色节点的个数if (pRoot->_col == Black)k++;// 检测当前节点与其双亲是否都为红色Node* pParent = pRoot->_parent;if (pParent && pParent->_col == Red && pRoot->_col == Red){cout << "违反性质三:没有连在一起的红色节点,而这里出现了" << endl;return false;}return _IsValidRBTree(pRoot->_left, k, blackCount) && _IsValidRBTree(pRoot->_right, k, blackCount); }
7、红黑树的删除
红黑树的删除这里和AVL树一样就不做过多演示了,具体可参考《算法导论》或者《STL源码剖析》,也可参考此大佬的博文:红黑树的插入删除操作
8、红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
9、红黑树的应用
- 1、C++ STL库 -- map/set、mutil_map/mutil_set
- 2、Java 库
- 3、linux内核
- 4、其他一些库
10、源码链接
链接直达:红黑树的模拟实现完整版
【 C++ 】红黑树相关推荐
- AVL树、splay树(伸展树)和红黑树比较
AVL树.splay树(伸展树)和红黑树比较 一.AVL树: 优点:查找.插入和删除,最坏复杂度均为O(logN).实现操作简单 如过是随机插入或者删除,其理论上可以得到O(logN)的复杂度,但是实 ...
- PAT (Advanced Level) 1132~1135:1132 模拟 1133模拟(易超时!) 1134图 1135红黑树
1132 Cut Integer(20 分) 题意:将一个含K(K为偶数)个数字的整数Z割分为A和B两部分,若Z能被A*B整除,则输出Yes,否则输出No. 分析:当A*B为0的时候,不能被Z整除,输 ...
- 数据结构Java版之红黑树(八)
红黑树是一种自动平衡的二叉查找树,因为存在红黑规则,所以有效的防止了二叉树退化成了链表,且查找和删除的速度都很快,时间复杂度为log(n). 什么是红黑规则? 1.根节点必须是黑色的. 2.节点颜色要 ...
- 算法基础知识科普:8大搜索算法之红黑树(下)
这是介绍红黑树的最后一部分,令y为要删除结点,n为要删除结点的子结点(子结点最多有1个),w为y的兄弟结点,删除操作的重点是使红黑树删除结点并通过调整后仍满足自身是搜索二叉树和设定的三点规则.删除操作 ...
- 算法基础知识科普:8大搜索算法之红黑树(中)
红黑树也是一种特殊形式的二叉搜索树,通过结点的颜色以及三条规则来保证二叉搜索树的平衡.规则1:根结点的颜色是黑色,规则2:叶子结点到根结点路径上遇到的黑色结点数目相同,规则3:叶子结点到根结点路径上无 ...
- 算法基础知识科普:8大搜索算法之红黑树(上)
平衡二叉树(AVL)是一种特殊的二叉搜索树(BST),即每个结点的值都大于其左子树且小于其右子树的值(若存在),并通过引入平衡因子的概念来保持树的平衡.平衡二叉树算法的重点是在插入.删除结点时,如何保 ...
- 美团实习面试:熟悉红黑树是吧?能不能写一下?
点击关注公众号,Java干货及时送达 手写红黑树确实有点过分了,但我觉得写不出来也正常,只要理解就行 红黑树是数据结构中比较复杂的一种,最近与它交集颇多,于是花了一周的空闲时间跟它死磕,终于弄明白并实 ...
- 美团实习面试:熟悉红黑树?能不能手写一下?
点击关注公众号,Java干货及时送达 来源:https://zhenbianshu.github.io 图片 手写红黑树确实有点过分了,但我觉得写不出来也正常,只要理解就行 红黑树是数据结构中比较复 ...
- 浅谈树形结构的特性和应用(上):多叉树,红黑树,堆,Trie树,B树,B+树......
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 上篇文章我们主要介绍了线性数据结构,本篇233酱带大家看看 无所不 ...
- 面试问你红黑树,你都懂了吗
(给视学算法加星标,修炼编程内功) 作者:Sun_TTTT https://blog.csdn.net/sun_tttt/article/details/65445754 红黑树是一个平衡的二叉树,但 ...
最新文章
- oracle win10家庭版,Windows10远程报错:由于CredSSP加密Oracle修正(ps:Win10家庭版)
- 渐变色 + 屏幕缩小自动产生滚动条
- JS中使用正则表达式封装的一些常用的格式验证的方法-是否外部url、是否小写、邮箱格式、是否字符、是否数组
- 学生用计算机中sto,STO 文件扩展名: 它是什么以及如何打开它?
- html三元运算符 模板,AngularJS模板中的三元运算符
- CSS 二十年发展简史
- lucene-SpanNotQuery和SpanOrQuery交迭与全局跨度
- linux 远程桌面 命令,linux 命令 远程连接
- 数据结构上机实践第14周项目1(3) - 验证算法(二叉排序树)
- 自动化运维工具SaltStack
- java参数传入数组_java传入数组参数
- 计算机控制总线传输的是,总线,地址总线,数据总线和控制总线
- 解决报错:ssh: Could not resolve hostname c: Temporary failure in name resolutionlost connection
- html怎么把正方形改成圆形,css怎样让div变成圆的?
- poi导出excel在单元格内画斜线
- ESLint语法检查
- js实现汉字转拼音(解决首字母排序问题)
- 2022年最新美股上市SaaS公司前50名排行榜单
- 需求分析和数据分析那些事。
- Excel2019关闭时无响应
热门文章
- 计算机专业要学视频剪辑吗,想要成为入门剪辑师?必须做到这五点,才能坚持下去...
- 乖离 暗机器人_乖离性百万亚瑟王黑暗机器人打法技巧 暗马桶攻略
- 【物联网】windows环境 配置mqtt服务器
- 使用VMware备份操作系统
- 经典前缀和+差分问题之小明的彩灯(c++)
- [XCTF-Reverse] 入门1-6
- python 论文写作_论文编辑神器Sublime Text,让论文写作过程事半功倍
- Jupyter Notebooks 入门
- 微软一个罕为人知的无敌命令
- ~1 ccf 2022-06-2 寻宝!大冒险!