【C++进阶】---- 红黑树实现
1.红黑树概念
红⿊树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。 通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路 径⻓出2倍,因⽽是接近平衡的。
1.1红黑树规则
1. 每个结点不是红⾊就是⿊⾊
2. 根结点是⿊⾊的
3. 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的,也就是说任意⼀条路径不会有连续的 红⾊结点。即红节点的孩子只能是黑节点,黑节点的孩子可以是红节点也可以是黑节点
4. 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的⿊⾊结点。即从根节点到NULL节点的所有路劲上的黑色节点个数相同
说明:《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是⿊⾊的规则。他这⾥所指的叶⼦结点 不是传统的意义上的叶⼦结点,⽽是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了 ⽅便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道 ⼀下这个概念即可。
1.2思考⼀下,红⿊树如何确保最⻓路径不超过最短路径的2倍的?
• 由规则4可知,从根到NULL结点的每条路径都有相同数量的⿊⾊结点,所以极端场景下,最短路径 就就是全是⿊⾊结点的路径,假设最短路径⻓度为bh(black height)。即最短路径就是该路径上全是黑色节点,设黑色节点个数为bh
• 由规则2和规则3可知,任意⼀条路径不会有连续的红⾊结点,所以极端场景下,最⻓的路径就是⼀ ⿊⼀红间隔组成,那么最⻓路径的⻓度为2*bh。即最长路径就是一黑一红,因为黑色个数是bh,则最长路径长度是2*bh
• 综合红⿊树的4点规则⽽⾔,理论上的全⿊最短路径和⼀⿊⼀红的最⻓路径并不是在每棵红⿊树都 存在的。假设任意⼀条从根到NULL结点路径的⻓度为x,那么bh<=h<=2*bh
1.3红⿊树的效率:
假设N是红⿊树树中结点数量,h最短路径的⻓度,那么 2^h-1<=N<=2^(2*h)-1,由此推出 h≈logN ,也就是意味着红⿊树增删查改最坏也就是⾛最⻓路径2*logN ,那么时间复杂度还是O(logN) 。
红⿊树的表达相对AVL树要抽象⼀些,AVL树通过⾼度差直观的控制了平衡。红⿊树通过4条规则的颜 ⾊约束,间接的实现了近似平衡,他们效率都是同⼀档次,但是相对⽽⾔,插⼊相同数量的结点,红⿊树的旋转次数是更少的,因为他对平衡的控制没那么严格。
2.红黑树的实现
2.1红黑树的结构
// 枚举红黑树的颜色
enum Color
{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;Color _col;// 默认构造RBTreeNode(const pair<K,V>& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr){}
};// 红黑树结构
template<class K,class V>
class RBTree
{typedef RBTreeNode<K, V> Node;
public:
private:Node* _root = nullptr;
};
2.2红⿊树的插⼊
2.2.1红⿊树树插⼊⼀个值的⼤概过程
1. 插⼊⼀个值按⼆叉搜索树规则进⾏插⼊,插⼊后我们只需要观察是否符合红⿊树的4条规则。
2. 如果是空树插⼊,新增结点是⿊⾊结点。如果是⾮空树插⼊,新增结点必须红⾊结点,因为⾮空树 插⼊,新增⿊⾊结点就破坏了规则4,规则4是很难维护的。新增节点必须是红色节点,新增节点是黑色节点一定会改变某一条路径上黑色节点的个数,即每次插入都会破坏规则4。
3. ⾮空树插⼊后,新增结点必须红⾊结点,如果⽗亲结点是⿊⾊的,则没有违反任何规则,插⼊结束。因为插入的是红节点,不会改变某一条路径上黑色节点的个数,只要其父亲不是红色节点不破坏规则3,这个数就是红黑树。
4. ⾮空树插⼊后,新增结点必须红⾊结点,如果⽗亲结点是红⾊的,则违反规则3。进⼀步分析,c(cur插入节点)是 红⾊,p(父亲节点)为红,g(父亲的父亲)必为⿊,因为如果父亲也为红,插入c节点之前这棵树就违反红黑树的规则了。这三个颜⾊都固定了,关键的变化看u的情况,需要根据u分为以下⼏种 情况分别处理。
说明:下图中假设我们把新增结点标识为c(cur),c的⽗亲标识为p(parent),p的⽗亲标识为 g(grandfather),p的兄弟标识为u(uncle)。
2.2.2情况1:变⾊
c为红,p为红,g为⿊,u存在且为红,则将p和u变⿊,g变红。在把g当做新的c,继续往上更新。
分析:因为p和u都是红⾊,g是⿊⾊,把p和u变⿊,左边⼦树路径各增加⼀个⿊⾊结点,g再变红,相 当于保持g所在⼦树的⿊⾊结点的数量不变,同时解决了c和p连续红⾊结点的问题,需要继续往上更新 是因为,g是红⾊,如果g的⽗亲还是红⾊,那么就还需要继续处理;如果g的⽗亲是⿊⾊,则处理结束 了;如果g就是整棵树的根,再把g变回⿊⾊。
情况1只变⾊,不旋转。所以⽆论c是p的左还是右,p是g的左还是右,都是上⾯的变⾊处理⽅式。
即
1.如果叔叔存在且为红,将父亲和叔叔变成黑色,再把爷爷变红。
2.将父亲变黑是为了不违反规则3(红色节点的孩子必须是黑色节点)。将叔叔也变黑,再把爷爷变红是为了不违法规则4(每条路径上黑色节点的个数相同)。
3.把g(爷爷)当成新的c(插入节点)是因为爷爷原来是黑的,他的父亲可以是红也可以是黑,如果是红就又违反规则3了,需要继续处理。
• 跟AVL树类似,上图我们展⽰了⼀种具体情况,但是实际中需要这样处理的有很多种情况。
• 图1将以上类似的处理进⾏了抽象表达,d/e/f代表每条路径拥有hb个⿊⾊结点的⼦树,a/b代表每 条路径拥有hb-1个⿊⾊结点的根为红的⼦树,hb>=0。
• 图2/图3/图4,分别展⽰了bh==0/bh==1/bh==2的具体情况组合分析,当bh等于2时,这⾥组合 情况上百亿种,这些样例是帮助我们理解,不论情况多少种,多么复杂,处理⽅式⼀样的,变⾊再 继续往上处理即可,所以我们只需要看抽象图即可。
2.2.3情况2:单旋+变⾊
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上 来的。因为如果u存在且为黑,以g为根的左右路径上的黑色节点的个数不同,违反规则4。
分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解 决问题,需要旋转+变⾊。
下面这种情况需要以p为旋转点进行右单旋,然后p变成黑色节点,g变成红色节点。
g p
p u -> c g
c u
分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解 决问题,需要旋转+变⾊。
下面这种情况需要以p为旋转点进行左单旋,然后p变成黑色节点,g变成红色节点。
g p
u p -> g c
c u
p变成这颗树新的根,g再变成红色,这样⼦树⿊⾊结点的数量不变,且没有连续的红⾊结点了,就不需要继续往上更新了,因为p的⽗亲是⿊⾊还是红⾊或者空都不违反规则。
2.2.4情况3:双旋+变⾊
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上 来的。因为如果u存在且为黑,以g为根的左右路径上的黑色节点的个数不同,违反规则4。
1.如果p是g的左,c是p的右,那么先以p为旋转点进⾏左单旋,再以g为旋转点进⾏右单旋,再把c变 ⿊,g变红即可。c变成这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要继续往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则。
g g c
p u -> c u -> p g
c p u
2.如果p是g的右,c是p的左,那么先以p为旋转点进⾏右单旋,再以g为旋转点进⾏左单旋,再把c变 ⿊,g变红即可。c变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且 不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则。
g g c
u p -> u c -> g p
c p u
2.3红⿊树的插⼊代码实现
// 红黑树的插入
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->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}// 不允许插入相同key值else{return false;}}// 新增节点为红色节点cur = new Node(kv);cur->_col = RED;if (parent->_kv.first > kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;// 如果父亲节点也是红,则需要特殊处理,最坏情况是处理至根节点,最后将根节点变成黑色节点即可while (parent && parent->_col == RED){Node* grandfather = parent->_parent;// g// p uif (parent == grandfather->_left){Node* uncle = grandfather->_right;// 叔叔存在且为红if (uncle && uncle->_col == RED){// 父亲和叔叔变黑,爷爷变红parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 变色后继续向上处理cur = grandfather;parent = cur->_parent;}// 叔叔不存在或者叔叔为黑else{// 以p为旋转点进行右单旋,然后p变黑,g变红,停止更新// g// p u// cif (parent->_left == cur){RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}// g// p u// celse{// 先以p为旋转点进行左单旋,再以g节点进行右单旋// 然后c变成黑色,g变成红色,停止更新RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}// g// u pelse{Node* uncle = grandfather->_left;// 叔叔存在且为红if (uncle && uncle->_col == RED){// 父亲和叔叔变黑,爷爷变红parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 变色后继续向上处理cur = grandfather;parent = cur->_parent;}// 叔叔不存在或者叔叔为黑else{// 以p为旋转点进行左单旋,然后p变黑,g变红,停止更新// g// u p// cif (parent->_right == cur){RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}// g// u p// celse{// 先以p为旋转点进行右单旋,再以g节点进行右单旋// 然后c变成黑色,g变成红色,停止更新RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK;return true;
}
2.4旋转代码实现
// 旋转代码的实现跟AVL树是⼀样的,只是不需要更新平衡因⼦
// 左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;Node* pParent = parent->_parent;// 把parent变成sunR的左孩子,sunRL变成parent的右孩子subR->_left = parent;parent->_right = subRL;// 改变parent、subR、subRL的_parentparent->_parent = subR;subR->_parent = pParent;if (subRL)subRL->_parent = parent;// parent有可能是整棵树的根,也可能是局部的子树// 如果是整棵树的根,要修改_root// 如果是局部的子树,要跟上一层连接if (pParent){if (pParent->_left == parent)pParent->_left = subR;elsepParent->_right = subR;}else_root = subR;
}// 右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;Node* pParent = parent->_parent;// 将parent变成subL的右边,subLR变成parent的左边subL->_right = parent;parent->_left = subLR;// 改变parent、subL、subLR的_parentparent->_parent = subL;subL->_parent = pParent;if (subLR)subLR->_parent = parent;// parent有可能是整棵树的根,也可能是局部的子树// 如果是整棵树的根,要修改_root// 如果是局部的子树,要跟上一层连接if (pParent){if (pParent->_left == parent)pParent->_left = subL;elsepParent->_right = subL;}else_root = subL;
}
2.5红⿊树的验证
这⾥获取最⻓路径和最短路径,检查最⻓路径不超过最短路径的2倍是不可⾏的,因为就算满⾜这个条件,红⿊树也可能颜⾊不满⾜规则,当前暂时没出问题,后续继续插⼊还是会出问题的。所以我们还是去检查4点规则,满⾜这4点规则,⼀定能保证最⻓路径不超过最短路径的2倍。
1. 规则1枚举颜⾊类型,天然实现保证了颜⾊不是⿊⾊就是红⾊。 不需要验证。
2. 规则2直接检查根即可。
3. 规则3前序遍历检查,遇到红⾊结点查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲的颜⾊就⽅便多了。即如果当前节点是红色,检查其父亲是否也是红色节点,如果是则返回false。
4. 规则4前序遍历,遍历过程中⽤形参记录跟到当前结点的blackNum(⿊⾊结点数量),前序遍历遇到 ⿊⾊结点就++blackNum,⾛到空就计算出了⼀条路径的⿊⾊结点数量。再任意⼀条路径⿊⾊结点数量refNum作为参考值,依次⽐较即可。我们可以先算出最左路径上黑色节点的个数作为参考值。
// 红黑树的检查
bool Check(Node* root, int blackNum, const int refNum)
{// 前序遍历,检查每条路径上黑色节点的个数,并检查其红色节点的父亲节点是否是红色if (root == nullptr){if (blackNum != refNum){cout << "存在黑色节点个数不相同的路径" << endl;return false;}elsereturn 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;// 找一个参考值refNum,即最左路径上黑色节点的个数int refNum = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK)refNum++;cur = cur->_left;}return Check(_root, 0, refNum);
}