C++进阶--红黑树的实现
文章目录
- 红黑树的实现
- 红黑树的概念
- 红黑树的规则
- 红黑树的效率
- 红黑树的实现
- 红黑树的结构
- 红黑树的插入
- 变色+单旋(变色)+双旋(变色)
- 红黑树的查找
- 红黑树的验证
- 总结:
- 结语
很高兴和大家见面,给生活加点impetus!!开启今天的变成之路!!
今天我们来学习红黑树,重点了解是如何进行旋转的,即红黑树的插入接口
作者:٩( ‘ω’ )و260
我的专栏:C++进阶,C++初阶,数据结构初阶,题海探骊,c语言
欢迎点赞,关注!!
红黑树的实现
红黑树的概念
红黑树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表示结点的颜⾊,可以是红色或者黑色。通过对任何⼀条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有⼀条路径会比其他路径长出2倍,因而是接近平衡的
红黑树的规则
红黑树的规则主要是有四点:
每个结点不是红色就是黑色
根结点一定是黑色的
如果一个结点是红色的,那么他的孩子结点必须是黑色的,即一条路径上不会有连续的红结点
对于红黑树任意一个路径上,黑结点的个数是相同的
下面我们来看几个红黑树:
这几个二叉搜索树都是满足这四个规律的,这时,就为红黑树。
细节说明:第四点:每一条路径,这个路径并非到达叶子结点,即一条路径只要能够到达nullptr的话,这就算作是一条路径了
来看下图:
这个点稍不注意其实就会看错的,为了避免这种情况,在《算法导论》中提出了⼀条每个叶子结点(NIL)都是黑色的规则。这里的叶子结点并非叶子结点,而是空结点。NIL的存在是为了标明每一条路径,来看下图:
这里再来提问一个问题,红黑树是如何保证最长路径不超过最短路径的二倍的?
由规则4可知,从根到NULL结点的每条路径都有相同数量的黑色结点,所以极端场景下,最短路径就就是全是黑色结点的路径,假设最短路径长度为bh(black height)
由规则2和规则3可知,任意⼀条路径不会有连续的红色结点,所以极端场景下,最长的路径就是一黑一红间隔组成,那么最长路径的长度为2bh
综合红黑树的4点规则而言,理论上的全黑最短路径和一黑一红的最长路径并不是在每棵红黑树都存在的。假设任意⼀条从根到NULL结点路径的长度为x,那么bh<=h<=2bh。
综上:只要我们满足了红黑树的四点规则,就能够满足最长路径不超过最短路径的二倍
红黑树的效率
我们假设红黑树的结点个数为N,h为最短路径的长度那么2 h- 1 < = N < 22h -1。因为红黑树具备二叉搜索树的性质,也就是意味着红黑树增删查改最坏也就是走最长路径2*logN,所以红黑树的时间复杂度为O(logN)
红黑树与AVL树的区别:红黑树是通过结点颜色近似平衡,AVL树是通过高度差严格控制平衡。AVL的旋转次数肯定是多于红黑树的,而红黑树的高度肯定是多于AVL树。
解释:为什么这里h的最短路径长度算出来是这个呢?
当我们是最短路径的时候,我们假设这个为完全二叉树,当为最长路径的时候,也同理,这样就能够使用二叉树方面的知识了(即二叉树高度与结点个数的关系:N=2h -1)
来看下图:
红黑树的实现
首先红黑树是具备二叉搜索树的性质的,而且与AVL树的差别就是控制树的平衡的方式不同,所以在代码部分与AVL树的部分代码类似:
红黑树的结构
我们来定义红黑树的结构:
enum Colour{RED,BLACK
};
template<class K,class V>
struct RBTreeNode{//红黑树结点结构pair<K,V> _kv;RBTreeNode<K,v> _left;RBTreeNode<K,v> _right;RBTreeNode<K,v> _parent;//主要是旋转的时候需要使用parent指针Colour _col;RBTreeNode(const pair<K,V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_col(RED),_kv(kv){}
tmplate<class K,class V>
class RBTree{//红黑树结构
public:typedef RBTreeNode<K,V> Node;
private:Node* _root=nullptr;
};
}
细节:为什么我们要将结点颜色初始化为红色?
当我们插入一个值,肯定是需要将这个值转换成一个结点,插入结点,如果插入一个黑色结点,就会影响红黑树的其他所有的分支,因为必须保证红黑树中每一条路径的黑色结点数量相同。所以为了不保证影响其他路径,插入的结点我们选择为红色!!
红黑树的插入
红黑树的插入的大致过程:
1:插入⼀个值按二叉搜索树规则进行插入,插入后我们只需要观察是否符合红黑树的4条规则
2:如果是空树插入,新增结点是黑色结点。如果是非空树插⼊,新增结点必须红色结点,因为非空树插⼊,新增黑色结点就破坏了规则4,规则4是很难维护的
3:非空树插⼊后,新增结点必须红色结点,如果父亲结点是黑色的,则没有违反任何规则,插入结束,因为此时并没有违反这四条规则
4:非空树插⼊后,新增结点必须红色结点,如果⽗亲结点是红色的,则违反规则3
此时我们应该怎么办呢?
主要分为一下几种方法来处理违法规则三的情况。
变色+单旋(变色)+双旋(变色)
我们再来分一下类别,其实标题的这三种方法还可以分类:
具体的处理方法要看uncle的情况:
uncle有三种情况:
1:存在且为红
2:存在且为黑
3:不存在
而2,3点可以归为一类,1归为一类具体来看下面的图示:
1:叔叔存在且为红
下图中假设我们把新增结点标识为c(cur),c的父亲标识为p(parent),p的父亲标识为g(grandfather),p的兄弟标识为u(uncle)。
当叔叔(uncle)存在且为红的时候,我们直接将p和u变黑,g变红,此时仍然满足红黑树的性质。
那么cur和parent分不分插入的位置呢?其实是不分的,即这种情况cur在左还是在右,parent在左还是在右,处理方法都是相同的。
来看下图:
下面这四种情况使用这一种方法都是可以解决的。
一种情况是叔叔存在且为红
c为红,p为红,g为黑,u存在且为红,则将p和u变黑,g变红。在把g当做新的c,继续往上更新。直到我们更新到根结点的位置。
只变色,不旋转。所以无论c是p的左还是右,p是g的左还是右,都是上面的变色处理方式。
分析:因为p和u都是红色,g是黑色,把p和u变黑,左右子树路径各增加⼀个黑色结点,g再变红,相当于保持g所在子树的黑色结点的数量不变,同时解决了c和p连续红色结点的问题,需要继续往上更新是因为,g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的父亲是黑色,则处理结束了;如果g就是整棵树的根,再把g变回黑色。因为一棵树的根结点必须是黑色。
这里我们来画出抽象展开图:
2:叔叔不存在或者说是叔叔存在且为黑
先来看图示:
我们先来总结一下:当叔叔存在且为黑或者叔叔不存在时,此时需要旋转,旋转就分为单旋或双旋:如果此时高的一遍呈现直线(单旋),如果高的一遍呈现曲线(双旋),即一种是左边高的左边高,一般是左边高的右边高(一种是一个方向一高到底,一种是两边都出现)
c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c一定是新增结点,u存在且为黑,则c⼀定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。
p必须变黑,才能解决,连续红色结点的问题,u不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转+变色
如果p是g的左,c是p的左,那么以g为旋转点进行右单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样⼦树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红⾊或者空都不违反规则
反之:
如果p是g的右,c是p的右,那么以g为旋转点进行左单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则
注意:这里我们都是一边高,接下来我们来看双旋的情形:
如果p是g的左,c是p的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。c变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红是或者空都不违反规则。
反之:
如果p是g的右,c是p的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可。c变成课这颗树新的根,这样⼦树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的⽗亲是黑色还是红色或者空都不违反规则
总结:如果是第一种情况(叔叔存在且为红),是需要继续向上更新的,但是第二种情况,可以直接结束循环,因为只要旋转之后,我们新的根一定是黑色的。
接下来我们来进行代码实现:
bool Insert(const pair<K,V>& kv)
{if(_root==nullptr){_root=new Node(kv);_root->_col=BLACK;//根结点一定是黑色的}//循环找到合适的位置放kvNode*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;//实现去重的功能}}//此时找到了合适的位置,分清是左边放,还是右边放cur=new Node(kv);cur->_col=RED;//插入的结点必须是红结点if(parent->_left==cur){parent->_left=cur;cur->_parent=parent;}else{parent->_right=cur;cur->_parent=parent;}//此时需要看违背了红黑树的四条规则没,违反了就要进行处理操作while(parent&&parent->_col)//因为插入的结点是红色,连续红结点的话parent也是红色{Node*grandfather=parent->_parent;if(grandfather->_left==parent){Node*uncle=grandfather->_right;if(uncle&&uncle->_col==RED){uncle->_col=parent->_col=BLACK;grandfather->_col=RED;//继续往上遍历cur=grandfather;parent=cur->_parent;}else{//分单旋和双旋的情况if(cur==parent->_left)//单旋+变色{RotateR(grandfather);parent->_col=BLACK;//新的根grandfather->_col=RED;}else if(cur==parent->_right)//双旋+变色{RoteteL(parent);RotateR(grandfather);cur->_col=BLACK;//新的根grandfather->_col=RED;}break;//涉及的旋转,就要退出循环}}else//grandfather->_right==parent{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)//单旋+变色{RotateR(grandfather);parent->_col=BLACK;//新的根grandfather->_col=RED;}else if(cur==parent->_left)//双旋+变色{RoteteL(parent);RotateR(grandfather);cur->_col=BLACK;//新的根grandfather->_col=RED;}break;//涉及的旋转,就要退出循环}}}_root->_col=BALCK;//让新的根重新为黑结点return true;
}
到这里,二叉树的插入操作就讲解完啦,有关左旋与右旋的代码,在以后会出一个章节来讲解哦。
红黑树的查找
我们直接按照二叉搜索树的查找规则来进行即可,因为红黑树也是二叉搜索树。
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;}
这里的代码简单,不做过多讲解
红黑树的验证
红黑树的检验:我们只需要检查是否满足红黑树的四点要求即可。
关键是每一条路径上的黑结点相同,这个该怎么来检验呢?
我们可以先来遍历任意一条路径,拿这条参考路径上的黑结点与任意路径上的黑结点个数进行比较,我们不用改变链表结构,只用使用局部变量来传递一个递归参数即可
来看代码:
bool IsBalanceTree()
{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)
}
bool check(Node*root,int blackNum,int refNum)
{if(root==nullptr)//说明此时走完了一个路径{if(blackNum!=refNum) return false;//路径上的黑结点个数不同return true;}if(root->_col==RED){if(root->parent->_col==RED) return false;//连续的红结点}if(root->_col==BLACK){refNum++;}return check(root->_left,blackNum,refNum)&&check(root->_right,blackNum,refNum);
}
这里我们需要注意一个点,当我们遍历到红结点的时候,我们不用去管他的孩子,因为孩子有三种(不存在,存在且为红,存在且为黑),不好判断,我们直接判断他的父亲即可
红黑树的删除这里我们不做讲解。
总结:
今天学习了红黑树,从概念,规则,实现角度出发,核心是插入接口的实现,其次就是验证红黑树的思路,是通过叔叔的角度来分析是否旋转,还是说只是变色即可,旋转的时候需要分清位置关系,而且是单旋还是双旋。
结语
今天的内容就分享到这里,不足之处欢迎留言指正,感谢大家的支持!!
古之成大事者,不惟有超世之才,亦必有坚忍不拔之志!加油!!