红黑树详解
一.红黑树介绍
1.红黑树的概念
红黑树是一颗二叉搜索树,增加了一个储存位来表示颜色,可以是红色或者黑色。通过对从根到叶子(指的是nullptr节点)的路径上每个节点颜色的约束,红黑树确保了没有一条路径会超出其他路径长度的两倍,因而接近平衡。
2.红黑树的规则
每条路径的结尾都是NIL节点(传统的叶节点,而是空节点,为了方便准确标识所有路径):
1.红黑树节点不是黑色就是红色;
2.整棵树的根节点一定是黑色;
3.如果一个节点是红色的,那么它的两个孩子必然是黑色的,也就是说,在红黑树的任意路径上没有连续的两个红节点;
4.对于任意节点,从该节点到其所有NULL节点的路径上,黑节点的数量相同。
注意:有些树上会补充NIL节点都是黑色的规则。后面讲解的细节中会忽略NIL。
为什么红黑树确保了没有一条路径会超出其他路径长度的两倍?
1.由规则3可得,最短路径(极端情况)的节点全是黑节点,设其高度为bh(black height);
2.为了让路径更长那么就得让路径上的红节点多一点,但不允许存在连续的红节点,那么路径最长时(极端情况)就是每一个黑节点接一个红节点,路径长为2 * bh;
3.综合而言,设红黑树的每条路径的长度为x,bh<= x <= 2 * bh;
3.红黑树的效率
设红黑树的节点个数为N,最短路径长为h,那么有2^h - 1 <= N < 2^(2 * h) - 1,所以h约等于logN,也就是说增删查改最多要走2*logN,故其效率是O(logN)。
相比于AVL树:
红黑树的表达相对于AVL树要抽象一点,AVL树通过对高度差的观察控制了平衡;而红黑树是通过四条规则约束,间接实现了近似平衡。它们的效率是同一档次的,但插入相同的数据时红黑树的旋转次数要少一点,因为它对平衡的控制相对轻松,而旋转消耗比较大,所以实际应用中红黑树是使用更多的。
二.红黑树的实现
1.红黑树的结构
enumColor{RED,BLACK};template<classk,classv>structRBTreeNode{typedefRBTreeNode<k,v>Node;RBTreeNode(constpair<k,v>&data=pair<k,v>()):_kv(data),_left(nullptr),_parent(nullptr),_right(nullptr),_col(BLACK){}pair<k,v>_kv;Node*_parent;Node*_left;Node*_right;Color _col;};template<classk,classv>classRBTree{typedefRBTreeNode<k,v>Node;public:private:Node*_root=nullptr;};2.红黑树的插入
1.插入的大概过程
1.按照二叉搜索树的插入规则,插入后我们只需要观察它是否符合红黑树的四大规则;
2.如果是空树插入,那么插入的节点为根节点,且颜色为black;如果是非空树插入,那么插入的节点必须为red,因为插入black节点后会破坏规则4,它是很难维护的;
3.非空树插入新节点后,新节点为红色,如果其parent节点为black,则没有违反规则,插入结束;
4.非空树插入新节点后,新节点为红色,如果其parent节点为red,则会违反规则3,需要进一步分析。
重要节点说明:新插入的节点为cur( c ),其父节点为parent( p ),祖父节点为grandparent( g ),叔节点为uncle( u )。
新插入节点后,如果要进一步分析,那么四个重要节点中会有三个节点的颜色是知道的:
cur和parent都是red(它们导致要进一步分析),grandparent为black,只有uncle未知。以下的分析就是围绕uncle的不同来分类的。
设bh为每条路径黑节点的数量。
2.情况一:变色
uncle为红色:
分析:p、u都是红色,此时把它们变黑,g变红,这样解决了p、c连续红节点的问题,也没有改变以g为根的两条路径的bh,使得以g为根的子树符合红黑树的规则。但要注意的是,g变红了,如果g的parent是黑色,那么插入结束;如果是红色,那么就要让g变成c继续向上更新。
抽象图理解:这里只展示了p为g的左孩子的情况,插入右子树也差不多。
一直更新到g为根节点或它的oarent为black为止。
单变色要的条件:
uncle存在且为red;
boolinsert(constpair<k,v>data){//先普通的插入if(!_root){_root=newNode(data);returntrue;}Node*cur=_root;Node*parent=nullptr;while(cur){if(data.first<cur->_kv.first){parent=cur;cur=cur->_left;}elseif(data.first>cur->_kv.first){parent=cur;cur=cur->_right;}elsereturnfalse;}cur=newNode(data);cur->_col=RED;cur->_parent=parent;if(data.first<parent->_kv.first)parent->_left=cur;elseparent->_right=cur;//调整节点的colwhile(parent&&parent->_col==RED)//更新到cur为根节点会自动跳出{Node*grandparent=parent->_parent;if(parent==grandparent->_left)//cur插入在左子树的情况{Node*uncle=grandparent->_right;if(uncle&&uncle->_col==RED)//情况1//uncle为红的情况,将parent和uncle置为黑,gp置为红{parent->_col=BLACK;uncle->_col=BLACK;grandparent->_col=RED;cur=grandparent;parent=cur->_parent;}}else//cur插入在右子树的情况{Node*uncle=grandparent->_left;if(uncle&&uncle->_col==RED){parent->_col=BLACK;uncle->_col=BLACK;grandparent->_col=RED;cur=grandparent;parent=cur->_parent;}}}_root->_col=BLACK;//保存根节点为黑returntrue;}3.情况2:单旋+变色
当uncle为空或其_col为black时,单靠变色时无法解决问题的,还得加上旋转。
当u不存在时,cur一定时新插入的节点:
如图假设u不存在时,cur不是新插入节点,因为都更新到这一步了,ABC肯定是存在黑节点的,但g的右子树没有黑节点,所以这种情况不存在,u为空,cur只能是新插入的节点。
当u存在且颜色为黑时,cur一定不是新插入节点:
假设cur为新插入节点:
明显可见,在插入cur前,以g为根的树的左右子树黑接待你个数就不同,所以u为黑,cur必不可能是新插入节点。
抽象图分析:这里分析p为g的右孩子。
可见,如果p是g的右孩子,cur是p的右孩子,只需要以g为旋点左旋,最后
p:red->black
g:black->red
即可。
单旋 + 变色要的条件:
1.uncle不存在或存在且为black;
2.当p为g的左/右孩子时,cur为p的左/右孩子。
while(parent&&parent->_col==RED){Node*grandparent=parent->_parent;if(parent==grandparent->_left){Node*uncle=grandparent->_right;if(uncle&&uncle->_col==RED)//uncle为红的情况,将parent和uncle置为黑,gp置为红{parent->_col=BLACK;uncle->_col=BLACK;grandparent->_col=RED;cur=grandparent;parent=cur->_parent;}elseif(uncle==nullptr||uncle->_col==BLACK){if(cur==parent->_left)//单旋接变色//情况2{// g// p u//cRotateR(grandparent);parent->_col=BLACK;grandparent->_col=RED;}break;}}else//p == gp->r{Node*uncle=grandparent->_left;if(uncle&&uncle->_col==RED){parent->_col=BLACK;uncle->_col=BLACK;grandparent->_col=RED;cur=grandparent;parent=cur->_parent;}elseif(uncle==nullptr||uncle->_col==BLACK){if(cur==parent->_right)//单旋接变色{// g// u p// cRotateL(grandparent);parent->_col=BLACK;grandparent->_col=RED;}break;}}}//左右单旋:voidRotateR(Node*parent){Node*subL=parent->_left;Node*subLR=subL->_right;Node*Pparent=parent->_parent;parent->_left=subLR;parent->_parent=subL;if(subLR)subLR->_parent=parent;if(parent==_root)_root=subL;subL->_right=parent;subL->_parent=Pparent;if(Pparent&&Pparent->_left==parent)Pparent->_left=subL;elseif(Pparent)Pparent->_right=subL;}voidRotateL(Node*parent){Node*subR=parent->_right;Node*subRL=subR->_left;Node*Pparent=parent->_parent;parent->_right=subRL;parent->_parent=subR;if(subRL)subRL->_parent=parent;if(parent==_root)_root=subR;subR->_left=parent;subR->_parent=Pparent;if(Pparent&&Pparent->_left==parent)Pparent->_left=subR;elseif(Pparent)Pparent->_right=subR;}4.情况3:双旋+变色
抽象图分析:p为g的右孩子。
可见当p为g的右孩子,cur为p的左孩子时,得先以p为旋点右旋,在以p为旋点左旋,最后进行变色处理:
g:black->red;
c:red->black;
双旋 + 变色的条件:
1.uncle不存在/存在且为黑;
2.p为g的右/左孩子时,c为p的左/右孩子;
while(parent&&parent->_col==RED){Node*grandparent=parent->_parent;if(parent==grandparent->_left){Node*uncle=grandparent->_right;if(uncle&&uncle->_col==RED)//uncle为红的情况,将parent和uncle置为黑,gp置为红{parent->_col=BLACK;uncle->_col=BLACK;grandparent->_col=RED;cur=grandparent;parent=cur->_parent;}elseif(uncle==nullptr||uncle->_col==BLACK){if(cur==parent->_left)//单旋接变色{// g// p u//cRotateR(grandparent);parent->_col=BLACK;grandparent->_col=RED;}else//双旋接变色{// g// p u// cRotateL(parent);RotateR(grandparent);cur->_col=BLACK;grandparent->_col=RED;}break;}}else//p == gp->r{Node*uncle=grandparent->_left;if(uncle&&uncle->_col==RED){parent->_col=BLACK;uncle->_col=BLACK;grandparent->_col=RED;cur=grandparent;parent=cur->_parent;}elseif(uncle==nullptr||uncle->_col==BLACK){if(cur==parent->_right)//单旋接变色{// g// u p// cRotateL(grandparent);parent->_col=BLACK;grandparent->_col=RED;}else//双旋接变色{// g// u p// cRotateR(parent);RotateL(grandparent);cur->_col=BLACK;grandparent->_col=RED;}break;}}}5.插入的完整代码
boolinsert(constpair<k,v>data){//先普通的插入if(!_root){_root=newNode(data);returntrue;}Node*cur=_root;Node*parent=nullptr;while(cur){if(data.first<cur->_kv.first){parent=cur;cur=cur->_left;}elseif(data.first>cur->_kv.first){parent=cur;cur=cur->_right;}elsereturnfalse;}cur=newNode(data);cur->_col=RED;cur->_parent=parent;if(data.first<parent->_kv.first)parent->_left=cur;elseparent->_right=cur;//调整节点的colwhile(parent&&parent->_col==RED){Node*grandparent=parent->_parent;if(parent==grandparent->_left){Node*uncle=grandparent->_right;if(uncle&&uncle->_col==RED)//uncle为红的情况,将parent和uncle置为黑,gp置为红{parent->_col=BLACK;uncle->_col=BLACK;grandparent->_col=RED;cur=grandparent;parent=cur->_parent;}elseif(uncle==nullptr||uncle->_col==BLACK){if(cur==parent->_left)//单旋接变色{// g// p u//cRotateR(grandparent);parent->_col=BLACK;grandparent->_col=RED;}else//双旋接变色{// g// p u// cRotateL(parent);RotateR(grandparent);cur->_col=BLACK;grandparent->_col=RED;}break;}}else//p == gp->r{Node*uncle=grandparent->_left;if(uncle&&uncle->_col==RED){parent->_col=BLACK;uncle->_col=BLACK;grandparent->_col=RED;cur=grandparent;parent=cur->_parent;}elseif(uncle==nullptr||uncle->_col==BLACK){if(cur==parent->_right)//单旋接变色{// g// u p// cRotateL(grandparent);parent->_col=BLACK;grandparent->_col=RED;}else//双旋接变色{// g// u p// cRotateR(parent);RotateL(grandparent);cur->_col=BLACK;grandparent->_col=RED;}break;}}}_root->_col=BLACK;//保存根节点为黑returntrue;}在插入过程中,红黑树靠什么增加bh大小的?
单看情况1、2、3每执行一次bh的大小时不变的。在插入过程中,其大小的变化只会由一种情况引起,那就是在变色环节:while (parent && parent->_col == RED)中一直更新到根节点,且当g为根节点时,u为red,这是会按情况1执行,让根变为red后跳出循环,再在循环外让根变黑,这时bh就增加了1。
三.红黑树的检验
判断一棵树是否为红黑树只要看它是否满足红黑树的四条规则就好了:
template<classk,classv>classRBTree{typedefRBTreeNode<k,v>Node;public:boolisbalance(){if(_root==nullptr)returntrue;if(_root->_col==RED)returnfalse;intway=0;Node*cur=_root;while(cur)//获取单条路径的bh{if(cur->_col==BLACK)++way;cur=cur->_left;}returncheck(_root,0,way);}boolcheck(Node*root,intblacknum,constintrefnum)//refnum为每条路径的黑节点个数{if(root==nullptr){if(blacknum==refnum)//代码走到着时就说明一条路走完了returntrue;else{cout<<"有路径的黑色节点数不相等"<<endl;returnfalse;}}if(root->_col==BLACK)blacknum++;else//r->c = red//检查子树不太方便,因为有两个,而且还不一定存在,所以检查parent{if(root->_parent&&root->_parent->_col==RED)returnfalse;}returncheck(root->_left,blacknum,refnum)&&check(root->_right,blacknum,refnum);}