红黑树的实现
本章目标
1.红黑树实现
1.红黑树实现
1.1红黑树的概念
红黑树也是一颗二叉搜索树,与AVL树一样,它也是平衡二叉搜索树,但是它并不依靠平衡因子控制平衡,而是通过颜色控制平衡,可以是红色或者黑色,通过4条互斥的规则去控制每条路径上结点的个数,但是是与AVL树不同的是,它的平衡时近似平衡,它只能保证每条路径上的结点不超过其他路径上的结点二倍.
1.2红黑树的规则
1.它的根节点时黑色的
2.它的每个结点不是黑的就是红的
3.每个红结点接下来必须是黑色的结点,同时也就是说明一个问题,它不存在一条路径上存在两个连续的红色结点
4.对于任意的结点来说,它到nullptr结点上的每条路径上的黑色结点的个数是相同的
5.在算法导论等书籍上可能存在一个NIL结点(叶子结点都是黑色的),它的叶子结点指的是我们所说的空结点,在这里我们了解这个概念即可,我们后续实现的阶段,不引入这条规则
1.3证明红黑树最长路径不超过最短路径的2倍
1.根据规则4,每条路径上有相同的黑色结点,也就是说在最极端的情况下,它的最短路径这条路径上的结点上的颜色都是黑色的,我们,假设最短路径的高度为black height,我们简称黑高(bh).
2.根据规则2,3我们可以推算出最长路径的长度的极端情况,保持黑色结点一致的情况下,就是这个结点上的颜色是一黑一红依此排序,也是说它的这条路径上的长度是黑高的2倍.而正常情况下,最长路径不一定是我们在这里说的极端情况,它可能出现连续的黑色结点
3.总结一下所有路径的长度就是在二者之间,bh<=所有路径的长度<=2*bh
1.4红黑树的效率
我们假设红黑树的结点个数为N,最短路径的长度为h,2的h次方-1<=N<=2的2h次方-1
我们可以推算出这个h是在两个logN量级的数之间的,同时也就推算出,对于一颗树来说,它的时间复杂度就是它的高度,O(logN).
与AVL树不同,它控制颜色不同去控制高度,形成近似平衡,总的来说,在插入相同结点的情况下,红黑树的旋转的情况的是更少的
1.5红黑树结点的结构
我们在这里通过一个枚举去控制红黑树的颜色,它的总体的结构上就是在二叉搜索树的基础上增加了颜色,而在AVL树中则是平衡因子
enum colour
{Black,Red
};
template<class K,class V>
struct RBTree_Node
{typedef RBTree_Node<K, V> Node;Node* parents;Node* left;Node* right;pair<K, V> data;colour col;RBTree_Node(const pair<K,V>& data):parents(nullptr),left(nullptr),right(nullptr),col(Red),data(data){}
};
在红黑树结点的构造函数中,我们给的结点颜色是红色,我们在这里可以随便给,后面我们实现的时候它也是要进行修改的.
1.6红黑树的插入
对于红黑树的插入是在二叉搜索树的基础上而来的,它只是在它的基础上去维护颜色不破坏规则从而保持平衡.
1.空树进行插入的时候,我们保持新增结点的颜色为黑色去保持规则1不被破坏,
非空树进行插入的时候,我们保持新增结点的颜色为红色,因为在这里我们插入黑色结点时很难保持每条路径上的黑色结点个数时相同的.
3.非空树进行插入的时候,因为新增结点的颜色为红色,我们要通过parents结点去判断,如果parents结点为黑色,则没有违反任何规则,插入结束
4.非空树进行插入的时候,因为新增结点为红色,parents的颜色也为红色,而parents结点的parents结点,我们管他叫grandfather结点一定为黑色.在这三个结点的颜色确定之后,我们要根据uncle结点也是grandfather结点除parents结点的另一个结点,我们要通过uncle结点是否存在或者它的颜色具体分析.在这里总体的先概况一下,大致分为两类,变色或者旋转+变色.
1.6.1变色
对于这种情况是在叔叔结点为红色的情况下,这个条件的出现情况一共有两种,第一种是新增增结点为红,第二种是在grandfather结点变上来的为红.出现这种情况我们进行的操作是将parents结点以及uncle结点的颜色变为黑色,然后将grandfather的结点颜色变为红色,继续向上更新,在这种情况下无论uncle是parents的左边还是右边的操作是一样的.
1.6.2单旋转+变色
在这里的情况情况主要分为两大种,第一种是uncle结点是黑色,第二种情况是uncle结点不存在.
第一种情况cur一定不是新增结点,该结点一定是从下面的结点变上来的,当前结点的颜色是从黑色到红色,新增结点是在c的子树中进行插入的.该情况是符合我们的1.6.1的情况的.
第二种情况cur一定是新增结点,我们连续的插入都是越来越大或者越来越小的结点.
要解决连续的结点为红色的情况,parents结点是一定要变黑的.这个时候就要进行旋转去降低它的高度.这两种的情况的解决方式是一致的
在这里我们分别对两种单旋情况进行分析
在这种左边的左边高的情况,我们进行右单旋,在这里grandther是黑色的,uncle是黑色的,其余结点皆为红色.我们以grandther为结点进行旋转让grandfather成为parents的右,parents成为这颗树的根,在旋转完之后将parents的颜色变为黑色在将grandfather的颜色变为红色.
在这种右边高的右边高,我们进行左单旋,在这里grandfather是黑色的,uncle是黑色的,其余结点皆为红色,我们以grandfather为结点进行做左单旋,让grandfather成为parents的左,parents成为这颗树的根,在旋转完之后将parents的颜色变为黑色,再将grandfather的颜色变为红色.
在这两种情况进行调整之后,均不需要继续向上调整,因为无论parents的结点是黑是红或者是不存在是没有破坏规则的.
1.6.3双旋转加变色
如果它的插入形状是一条折线而非直线的话,我们进行单旋转是无法解决问题的.需要进行双旋转解决,具体原因在AVL树的博客中已经有了细致的介绍,在这里就不进行赘叙
在这里我们对两种双旋转的情况进行讨论
在这种情况,左边高的右边高,我们要进行左右双旋,在旋转之后要改变它的颜色.在这里我们的grandfather结点以及uncle结点的颜色均为黑色. 其余结点皆为红色.我们先以parents结点为轴点进行左单旋再以grandfather结点进行右单旋.在经过两次旋转之后,cur成为整颗树的根,parents和grandfather分别成为cur的左右结点,此时将cur的颜色变为黑色在将grandfather的颜色变为红色即可.
在这种情况右边高的左边高,我们需要进行右左双旋去控制整颗树的高度,然后进行颜色的改变,在这里grandfather以及uncle两个结点的颜色为黑色,其他结点的颜色均为红色.我们先以parents为轴点进行右单旋,再以grandfather结点为轴点进行左单旋.
再这时cur成为整颗树的根,grandfather以及parents分别为cur的左右结点,将cur的颜色变为黑色,再将grandfather的颜色变为红色.
在这两种情况下,当cur成为整颗树新的根之后,无论cur的结点的父亲是黑是红都不会印象规则的.
bool insert(const pair<K, V>& data)
{if (root == nullptr){root = new Node(data);root->col = Black;return true;}Node* cur = root;Node* parents = nullptr;while (cur){if (cur->data.first < data.first){parents = cur;cur = cur->right;}else if (cur->data.first > data.first){parents = cur;cur = cur->left;}else{return false;}}cur = new Node(data);cur->col = Red;if (data.first < parents->data.first){parents->left = cur;}else{parents->right = cur;}cur->parents= parents;//检查颜色while (parents && parents->col == Red){Node* grandfather = parents->parents;if (parents == grandfather->left){Node* uncle = grandfather->right;if (uncle && uncle->col == Red){//变色&&继续处理uncle->col = parents->col = Black;grandfather->col = Red;cur = grandfather;parents = cur->parents;}else{// g(B)// p(R) u(B)//c(R)// p(B)// c(R) g(R)// u(B)if (cur == parents->left){RotateR(grandfather);parents->col = Black;grandfather->col = Red;}// g(B)// p(R) u(B)// c(R)// c(B)// p(R) g(R)// u(B)// else{RotateL(parents);RotateR(grandfather);cur->col = Black;grandfather->col = Red;}break;}}else // parents == grandfather->right){Node* uncle = grandfather->left;if (uncle && uncle->col == Red){//变色&&继续处理uncle->col = parents->col = Black;grandfather->col = Red;cur = grandfather;parents = cur->parents;}else{//叔叔不存在或者叔叔存在为黑// g(B)//u(B) p(R)// c(R)// p(B)// g(R) c(R)//u(B)if (cur == parents->right){RotateL(grandfather);parents->col = Black;grandfather->col = Red;}// g(B)// u(B) p(R) // c(R) // c(B) // g(R) p(R)//u(B) // else{RotateR(parents);RotateL(grandfather);cur->col = Black;grandfather->col = Red;}break;}}}root->col = Black;return true;
}
具体实现代码,旋转的代码是与AVL树一致的,只需要将平衡因子部分的调整删掉即可,我们在这里需要两个单旋即可,AVL树分别对两种双选的情况进行封装是要根据它的新增结点平衡因子进行调整新的树的平衡因子.
1.7平衡性的验证
在AVL树阶段我们通过对两颗子树的高度进行相减进行递归来去算它的高度差是否符合平衡因子来进行验证平衡性.
而在红黑树的平衡性验证中,我们在实现红黑树的时候是通过几条互斥的规则来控制平衡的,在这里我们只需要对这几条环境进行取反,如果取反之后的条件有有一个为真,则该红黑树都是不平衡的或者根本不是红黑树
在这里我们最难控制的规则是最后一个去控制黑色结点个数的数量,在这里我们采用在二叉树的阶段,我们算它的高度时采用传值传参的方法,并且提前算出一条路径上的黑色结点用来进行参考.
具体实现如下
bool check(Node* root, int blacknum, int refnum){if (root == nullptr){if (refnum != blacknum){//当当前路线递归结束,黑色结点不一致cout << "黑色结点不一致" << endl;return false;}return true;}//孩子为红,并且父亲为红,连续红结点,直接返回if (root->col == Red && root->parents->col == Red){cout << "连续红色结点" << endl;return false;}//黑色结点增加blacknumif (root->col == Black){blacknum++;}return check(root->left, blacknum, refnum) && check(root->right, blacknum, refnum);}bool IsbalaceTree(){if (root == nullptr){return true;}if (root->col == Red){return false;}Node* cur = root;int refnum = 0;while (cur){if(cur->col==Black)refnum += 1;cur = cur->left;}return check(root, 0, refnum);}
1.8其他
因为红黑树也是一颗二叉搜索树,它的查找是与我们前面实现的是一致的,有关它的删除可以参考算法导论这本书.这里面提供了删除的逻辑以及伪代码