1.概念与规则
(1)概念
首先红黑树是一种近似平衡的二叉搜索树
他在每个节点存储中多存储了一个颜色变量,颜色只能是红色或黑色。然后通过规则的约束,使得红黑树能够满足最长的路径不大于最短路径的两倍,从而达到近似平衡
红黑树与AVL树的关系:
红黑树:二叉搜索树+四条红黑树维护规则(近似平衡)
AVL树:二叉搜索树+平衡因子(高度差小于等于1)
(2)四条规则
1.节点颜色只有黑色和红色
2.根节点是黑色的
3.红色节点的直接子节点必须是黑色的
4.所有路径上的黑色节点数量相同
图示:
左下角的红黑树是错误的:因为第三条路径只有一个黑色节点,其他路径都有两个黑色节点,违反了所有路径黑色节点数量一致的规则
右下角的红黑树也是错误的:因为有红色节点的子节点不是黑色节点,违反了所有红色节点的子节点必须是黑色的规则
思考:红黑树是如何保证最长的路径不长于最短路径的两倍的?
由规则四可知:最短路径的极限情况就是节点全黑由规则四与规则三可知:最长路径的极限情况就是一红一黑交替出现,刚好为最短路径的两倍
即:maxsize == 2 * minsize
而实际情况中,最短路径会更长一点,最长路径会更短一点
即:smallsize >= minsize bigsize <= maxsize
综上:bigsize <= 2*smallsize
2.红黑树的实现
2.1结构搭建
(1)创建树节点
由于我们引入了颜色这个概念,所以这里我们用枚举常量来表示red与black
(2)创建红黑树类
2.2插入
我们先对插入做一个大致分析
(1)当树为空时:
直接插入黑色节点,并让_root指向它
(2)当树非空时:
只能插入红色节点,因为规则四要求每条路径上黑色节点数量要一样,如果我们在这里改变当前路径的黑色节点数,就需要去改变其他路径的黑色节点数,这是很难达到的。
若parent为黑色节点,那插入后可以直接退出了
若parent为红色节点,就违反了红色节点的直接子节点是黑色的规则,那么我们就需要分情况进行调整
首先我们已经确定的信息是:cur,parent都是红色节点,grandfather为黑色节点。
不确定的是parent的兄弟节点uncle是红/黑/nullptr,所以我们的情况分类的关键就是unlce的颜色状态
总结:
1.当uncle为红,变色
2.当uncle不为红,变色+旋转
(1)若parent为左孩子,cur也为左孩子:右旋+变色
(2)若parent为右孩子,cur也为右孩子:左旋+变色
(3)若parent为左孩子,cur为右孩子:左旋加右旋+变色
(4)若parent为右孩子,cur为左孩子:右旋加左旋+变色
2.2.1情况1:变色(nucle为红)
在树为全局情况:即grandfather为根节点时
uncle为红色,我们只要将parent与unlce都变为黑色,将grandfather变为红色
在树为局部情况:即grandfather不为根节点
uncle为红色,我们将parent与uncle变为黑,grandfather变为红色后,还需要判断grandfather的parent节点的颜色
若为黑,结束
若为红,将grandfather变成新的cur,新的parent/unlce/grandfather也是更新,然后下一个循环中继续判断新的情况
图示分析:![]()
(1)当前树为全局树,那么grandfather就是根节点,我们变色之后g的所有子节点都可以满足要求,过g变成了红色节点,违反了规则。
所以后续我们会有一个统一的操作:把根节点置为黑。这样子就可以很好的维护红黑树的根节点为黑的规则
(2)局部:爷爷父节点为黑
这种情况我们变色后也是直接结束
(3)局部:爷爷的父节点为红
此时更新完第一次仍然会违反规则,而我们可以把此时的g当成新的c,并更新出新的u,p,g节点,然后重复进行更新,直到更新成功或者进入旋转加变色
疑问:为什么可以这样变色?
因为我们要让cur节点合法,就必须让parent变为黑色,而它变为黑色就相当于该路径凭空多了一个黑色,这是不允许的,但是我们可以从前面的黑色节点中调度黑色过来,从而维持黑色节点数量不变。
而unlce为红的情况,parent和uncle都从grandfather中提取黑色节点,恰好可以达到uncle和parent路径的黑色节点数量不变,且parent也变为黑色节点。此时我们就可以确定grandfather后面的所有节点都是符合红黑树规则的
2.2.2情况2:变色+旋转(uncle为空或黑)
当parent在grandfather的左侧,且cur在parent左侧时:
以grandfather为中心进行右旋,然后把grandfather变红,把parent变黑
当parent在grandfather的右侧,且cur在parent右侧时:
以grandfather为中心进行左旋,然后把grandfather变红,parent变黑
注意:
若uncle为空,那么cur一定是新增节点:因为若是变色更新上来的节点,那么从grandfather开始分支,parent路径有两个黑色节点,而unlce只有一个黑色节点,违反规则
若unlce为黑,那么cur一定不是新增节点,同理,也会违反规则
疑问1:为什么要旋转?
若不进行旋转,单纯变色是无法不违反规则的。
图示1:uncle为空
此时若让p变为黑色,就会导致p路径黑色节点多一个。即使把g变为红,让p路径满足规则,此时u为空,u路径又少了一个黑色节点,违反规则
图示2:uncle为黑
unlce为黑也是同理
疑问2:为什么旋转后是这样变色?
因为这样变色可以保证规则不被违反。
图示:
c和u后面可能会接其他节点,但是一定是满足规则的,所以我们不用考虑进来
初始情况:左路径黑色节点为1,右路径黑色节点为2
旋转后:左路径无黑色节点,右路径黑色节点为2
所以我们要在右路径黑色节点数量不变的情况下让左路径多一个黑色节点
那么只有一个方案是最优的:将p变黑,g变红
这种方案的局部根节点变黑保证了不用向上更新,u仍为黑保证了u的子树仍满足红黑树规则
疑问3:为什么旋转变色之后就可以直接结束了?不用管grandfather的parent是什么吗?
grandfather只有三种情况:空/黑/红
旋转变色可以保证除了grandfather的其他节点不违反红黑树规则,所以我们只要看grandfather即可
首先看看为空的情况,由于现在的局部根节点为黑色,如果grandfather的parent为空,那么我们的局部根节点就是全局根节点,没有违反规则
然后是为黑的情况,黑节点可以接红色/黑色节点,没问题
最后是为红,红色节点必须接黑色节点,而我们的grandfather就是黑的,没问题
综上,grandfather也没有违反规则,故结束正确
2.2.3情况三:变色+双旋转
当parent在grandfather的左侧,且cur在parent的右侧时:
先以parent为中心进行左旋,然后以grandfather为中心进行右旋,最后把grandfather变红,把parent变黑当parent在grandfather的右侧,且cur在parent左侧时:
先以parent为中心进行右旋,然后以grandfather为中心进行左旋,最后把grandfather变红,把parent变黑
疑问:为什么要进行双旋?
因为单旋后无法进行有效变色,从而无法符合所有规则,我们先旋转一次将问题转换成单旋+变色
2.2.4插入代码实现
层级结构
疑问:为什么这样划分?
我们对于层级的划分一般是先从大范围开始区分,而树是否存在是最大的范围。
然后我们插入节点后,若父节点为黑或空,直接就结束了,为红才继续,所以第二层是以父节点的颜色区分
然后因为旋转+变色的情况是根据父节点和舅节点的相对位置,以及父节点和cur节点的相对位置共同决定的,所以我们第三层就用父节点和舅节点的相对位置划分
层级1:树为空
为空我们直接插入黑色节点然后退出即可
层级1:树非空
(1)查找并链接cur节点
查找的逻辑和二叉搜索树一样。
新插入节点置为红,避免改变黑色节点数量,然后让parent与cur互相指向
(2) 层级2:parent为红 + 层级3:p左u右
因为只有变色部分有可能循环进行,所以我们在旋转变色部分结束后要break跳出循环
旋转函数与AVL树的旋转基本一致,不过要将平衡因子部分全部删除
(3)层级2:parent为红 + 层级3:p右u左
(4)根节点颜色维护
完整代码:
//左旋 template <class K, class V> void RBtree<K,V>::RotateL(Node* parent) { Node* subr = parent->_right; Node* subrl = subr->_left; //更新节点指向 subr->_left = parent; parent->_right = subrl; //更新_parent Node* parentParent = parent->_parent; if (parentParent)//局部树 { if (parentParent->_left == parent) { parentParent->_left = subr; } else { parentParent->_right = subr; } subr->_parent = parentParent; } else//全局树 { subr->_parent = nullptr; _root = subr; } parent->_parent = subr; if (subrl)//sublr可能为空 subrl->_parent = parent; } //右旋 template <class K, class V> void RBtree<K,V>::RotateR(Node* parent) { Node* subl = parent->_left; Node* sublr = subl->_right; //更新节点指向 parent->_left = sublr; subl->_right = parent; //更新_parent Node* parentParent = parent->_parent; if (parentParent)//局部树 { if (parentParent->_left == parent) { parentParent->_left = subl; } else { parentParent->_right = subl; } subl->_parent = parentParent; } else//全局树 { subl->_parent = nullptr; _root = subl; } parent->_parent = subl; if (sublr)//sublr可能为空 sublr->_parent = parent; } //左右双旋 template <class K, class V> void RBtree<K,V>::RotateLR(Node* parent) { Node* subl = parent->_left; Node* sublr = subl->_right; //进行旋转改变节点指向 RotateL(subl); RotateR(parent); } //右左双旋 template <class K, class V> void RBtree<K,V>::RotateRL(Node* parent) { Node* subr = parent->_right; Node* subrl = subr->_left; //进行旋转改变节点指向 RotateR(subr); RotateL(parent); } //红黑树插入 template <class K, class V> bool RBtree<K,V>::insert(const pair<K, V>& kv) { if (_root == nullptr)//树为空 { _root = new Node(kv); _root->_col = BLACK; return true;//插入完成返回 } else//树非空 { //查找插入节点 Node* parent = nullptr; Node* cur = _root; 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->_kv.first > kv.first) { parent->_left = cur; } else { parent->_right = cur; } cur->_parent = parent; while (parent && parent->_col == RED)//祖先节点存在且为红才继续向上更新 { Node* grandfather = parent->_parent; if (grandfather->_left == parent)//父节点在左 { Node* uncle = grandfather->_right; if (uncle != nullptr && uncle->_col == RED)//变色 { grandfather->_col = RED; parent->_col = BLACK; uncle->_col = BLACK; cur = grandfather; parent = cur->_parent; } else//旋转+变色 { //右单旋+变色 if (cur == parent->_left) { RotateR(grandfather); grandfather->_col = RED; parent->_col = BLACK; } else//左右双旋 { RotateLR(grandfather); grandfather->_col = RED; cur->_col = BLACK; } break; } } else//父节点在右 { Node* uncle = grandfather->_left; if (uncle != nullptr && uncle->_col == RED)//变色 { grandfather->_col = RED; parent->_col = BLACK; uncle->_col = BLACK; cur = grandfather; parent = cur->_parent; } else//旋转+变色 { //左单旋+变色 if (cur == parent->_right) { RotateL(grandfather); grandfather->_col = RED; parent->_col = BLACK; } else//右左双旋 { RotateRL(grandfather); grandfather->_col = RED; cur->_col = BLACK; } break; } } } _root->_col = BLACK; return true; } }
2.3查找
红黑树的查找与搜索树逻辑一致
//查找插入节点 { Node* cur = _root; while (cur) { if (cur->_kv.first < kv.first) { cur = cur->_right; } else if (cur->_kv.first > kv.first) { cur = cur->_left; } else { return true; } } return false; }
需要注意的是这里不需要父节点指针,因为只需要查找而不需要链接
2.4验证
由于红黑树是围绕四个规则维护的,所以我们只要满足四个规则就可以保证他是红黑树
规则一:我们利用的是枚举类型,直接保证了红黑树的节点颜色只有红和黑
规则二:直接检查根节点
规则三:我们检查红节点的子节点是否为黑需要查左右,而如果检查红色节点的父节点是否为黑只需要检查一次,所以我们遍历红黑树,遇到红色节点就检查它的父节点是否为黑,若不是说明有连续红色节点,返回false
规则四:利用前序遍历搜索红黑树,遇到黑色节点就让形参blacknum++,直到遇到空节点,说明一条路径黑色节点数量计算完毕。最后随便用一条路径的黑色节点数和其他节点依次比较即可
(1)判断函数
前面两个if语句负责判断根节点颜色是否为黑
后面的while语句负责搜索一条路径黑色节点数,并记录到refnum参考值中,然后传到check前序遍历函数中用于与blacknum比较
(2)check函数
check函数有两个作用:
其一:检查是否有连续红节点
其二:记录并判断所有路径的黑色节点数是否相同