浅尝红黑树
目录
目录
红黑树的优势
- 一棵有n个内部结点的红黑树的高度最多是2lg(n+1)
- 进行基本动态集合操作如查找、插入、删除等的时间复杂度最坏为O(log n)
红黑树的应用
- C++的STL中的set和map
- Linux的虚拟内存管理
- 关联数组的实现
- 等等
五条红黑性质
- 每个结点不是红就是黑(非红即黑)
- 根结点是黑的(根黑)
- 每个叶结点:指树尾端NIL指针或NULL结点,是黑的(NIL/NULL黑)
- 如果一个结点是红的,那么它的俩个儿子都是黑的,即从每个叶子到根的所有路径上都不能有两个连续的红色结点(红父黑孩)
- 对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点(路径黑同数)
树的旋转知识
左旋
- 当前结点的右孩子y成为该孩子树新的根,y的左孩子变为当前结点的右孩子
左旋伪代码:
Left-Rotate(T, x){
y = x.right //定义y是x的右孩子
x.right = y.left //y的左孩子成为x的右孩子
if(y.left != T.nil) y.left.p = x //如果y左孩子不是nil
y.p = x.p //x结点成为y的父结点
if(x.p == T.nil) T.root = y //如果x是根结点,y成为根结点
else if(x == x.p.left) x.p.left = y //确定y新成为左孩子还是右孩子
else x.p.right = y
y.left = x //x成为y的左孩子
x.p = y
}
对左旋的理解
- 通过以上代码,可以看到被旋转的结点会变成一个左结点,如果被旋转结点的右孩子拥有左孩子,那就被继承到被旋转结点的右孩子那了
右旋
- 当前结点的左孩子y成为该孩子树新的根,y的右孩子变为当前结点的左孩子
右旋伪代码
Right-Rotate(T,x){
y = x.left //定义y是x的左孩子
x.left = y.right //y的右孩子成为x的左孩子
if(y.right != T.nil) y.left.p = x
y.p = x.p //x结点成为y的父结点
if(x.p == T.nil) T.root = y //如果x是根结点,y成为根结点
else if(x == x.p.left) x.p.left = y //确定y新成为左孩子还是右孩子
else x.p.right = y
y.right = x //x成为y的左孩子
x.p = y
}
对右旋的理解
- 通过以上代码,可以看到被旋转的结点会变成一个右结点,如果被旋转结点的左孩子拥有右孩子,那就被继承到被旋转结点的左孩子那了
红黑树的插入
插入伪代码
RB-Insert(T,z){
y = nil
x = T.root
while(x != T.nil){
y = x //找出z的父结点y的位置
if(z.key < x.key) x = x.left
else x = x.right
}
z.p = y
if(y == nil[T]) T.root = z//判断z要插入的位置
else if(z.key < y.key) y.left = z
else y.right = z
z.left = T.nil
z.right = T.nil
z.color = RED
RB-Insert_Fixup(T,z)
}
为什么插入的结点要染成红色呢?
- 我想的是,不管是染成红还是黑,性质1,2,3都是满足的
- 试想我们从头构建一棵红黑树,当只有一个黑色的根结点的时候,如果染成黑色就不满足性质5了,推广到新插入结点的父结点都是黑色的话,染成黑色必定是不满足性质5的了
- 而染成红色可以在一些情况下少违背一条性质
插入修复伪代码
RB-Insert-Fixup(T,z){
while(z.p.color == RED){//所有修复情况父结点都是红色
if(z.p == z.p.p.left){
y = z.p.p.right
if(y.color == RED){//情况1:叔叔结点是红色
z.p.color = BLACK//父变黑
y.color = BLACK//叔变黑
z.p.p.color = RED//祖变红
z = z.p.p//将祖父当做新增结点z,z上移两个指针且为红色
}
else {//叔叔结点是黑色
if(z == z.p.right){//情况2:当前结点是父结点的右子
z = z.p//父与子角色互换
Left-Rotate(T,z)//左旋
}//情况3:当前结点是父结点的左子
z.p.color = BLACK//父变黑
z.p.p.color = RED//祖变红
Right-Rotate(T,z.p.p)//祖右旋
}
}
else{
y = z.p.p.left
if(y && y.color == RED){
z.p.color = BLACK
y.color = BLACK
z.p.p.color = RED
z = z.p.p
}
else{
if(z == z.p.left){
z = z.p
Right-Rotate(T,z)
}
z.p.color = BLACK
z.p.p.color = RED
Left-Rotate(T,z.p.p)
}
}
}
T.root.color = BLACK//根肯定是黑的
return root
}
(特此说明:当前代指当前结点,父代指父结点,叔代指父的兄弟结点,祖代指祖父结点,数字1,2,3,4,5代指上述对应性质)
不用修复的情况:
插入的是根结点
- 直接染黑当前
插入的结点的父结点是黑色
- 红黑树没有被破坏,所以什么也不用做。
插入修复情况1
- 分析:父和叔都是红
- 对策:父和叔染黑,祖染红,祖为新当前,从新当前重新分析执行
为什么这么做?
- 当前与父都是红,违背4,染黑父
- 染黑父之后,包含父的路径违背5,染红祖
- 染红祖之后,包含叔的路径违背5 ,染黑叔
- 祖不一定满足4,若祖为根,染黑祖
- 若祖不为根,设置祖为当前,重新分析执行
插入修复情况2
- 分析:父为红,叔为黑,当前是右子
- 对策:父作新当前,新当前左旋
为什么这么做?
- 父为新当前,因为左旋可以使儿子上移
- 左旋后,若儿子为根,染黑;不为根,重新分析父
- 为何设置父为新当前?处理需要从下到上,从叶到根处理
- 通过这么做之后,某些情况下便到达了情况3的地步
插入修复情况3
- 分析:父为红,叔为黑,当前是左子
- 对策:父染黑,祖染红,祖右旋
为什么这么做?
- 当前和父为红,违背4,染黑父
- 染黑父,违背5,染红祖,以祖旋转
红黑树的删除
删除伪代码
RB-Transplant(T,x,y){
//找到安放y的位置
//用y顶替x的位置
if(x.p == T.nil) T.root = y
else if(x == x.p.left) x.p.left = y
else x.p.right = y
y.p = x.p
}
RB-Delete(T,z){
y = z //记录要删除结点的原信息
y-origial-color = y.color
if(z.left == T.nil){//被删除结点只有一个孩子的情况
x = z.right
RB-Transplant(T,z,z.right)
}
else if(z.right == T.nil){//被删除结点只有一个孩子的情况
x = z.left
RB-Transplant(T,z,z.left)
}
else{//y是z的后继结点
y = Tree-Minium(z.right) //这里也可以是Tree-Maxmun(z.left),不过下面要修改
y-origial-color = y.color
x = y.right
if(y.p == z) x.p = y//如果y是z的直系孩子,z右边只有一个孩子的情况,绑定x和y
else{
RB-Transplant(T,y,y.right)//用y.right顶替y的位置
y.right = z.right//y接管z的右孩子
y.right.p = y
}
RB-Transplant(T,z,y)//用y顶替z的位置
y.left = z.left//y接管z的左孩子
y.left.p = y
y.color = z.color//y接管z的颜色
}//如果y原来是黑色的,进行修复
if(y-origial-color == BLACK) RB-Delete-Fixup(T,x)
}
删除修复伪代码
RB-Delete-Fixup(T,z){
while(z != T.root && z.color == BLACK){//z一直往上移
if(z == z.p.left){
w = z.p.right//w是z的兄弟结点
if(w.color == RED){//情况1:w是红色
w.color = BLACK //兄变黑
z.p.color = RED //父变红
Left-Rotate(T,z.p) //父左旋
w = z.p.right //重置兄
}
if(w.left.color == BLACK && w.right.color == BLACK){//情况2:兄和其两孩子都是黑
w.color = RED //兄变红
z = z.p //父为新当前
}
else{
if(w.right.color == BLACK){ //情况3:兄的孩左红右黑
w.left.color = BLACK //兄左孩变黑
w.color = RED //兄变红
Right-Rotate(T,w) //兄右旋
w = z.p.right //重置兄
} //情况4:兄的孩左右皆红
w.color = z.p.color //兄颜色变为父
z.p.color = BLACK //父变黑
w.right.color = BLACK //兄右孩变黑
Left-Rotate(T,z.p) //父左旋
z = T.root //当前为根
}
}
else{
w = z.p.left//w是z的兄弟结点
if(w.color == RED){//情况1:w是红色
w.color = BLACK //兄变黑
z.p.color = RED //父变红
Right-Rotate(T,z.p) //父右旋
w = z.p.left //重置兄
}
if(w.right.color == BLACK && w.left.color == BLACK){//情况2:兄和其两孩子都是黑
w.color = RED //兄变红
z = z.p //父为新当前
}
else{
if(w.left.color == BLACK){ //情况3:兄的孩右红左黑
w.right.color = BLACK //兄右孩变黑
w.color = RED //兄变红
Left-Rotate(T,w) //兄左旋
w = z.p.left //重置兄
} //情况4:兄的孩左右皆红
w.color = z.p.color //兄颜色变为父
z.p.color = BLACK //父变黑
w.left.color = BLACK //兄左孩变黑
Right-Rotate(T,z.p) //父右旋
z = T.root //当前为根
}
}
}
z.color = BLACK
}
理解修复的准备
- 假设被删除结点的后继结点包含一个额外的黑色,也保留原来的颜色
- 原因:将红黑树按二叉查找树的方法删除结点后,可能会违反2,4,5,将其包含额外的黑色即可保持5
- 因为引入额外颜色,违反1
修复思路
- 将包含额外黑色的结点不断上移
- 当结点为红+黑,直接染黑
- 当结点指向根,直接染黑
- 其它情况如下所列,转化为以上情况
删除修复情况1
- 分析:当前为黑+黑,兄为红,父为黑,兄的孩子都是黑
- 对策:兄染黑,父染红,父左旋,重置兄
问一句为什么?
- 为了将其转化为其它三种情况
- 父左旋提升了兄,为保持已有状态,需父变红,兄变黑,因为兄被提升,需重新设定兄弟关系
删除修复情况2
- 分析:当前为黑+黑,兄和其两孩子都是黑
- 解法:兄染红,父为新当前
问一句为什么?
- 根据思路,将额外的黑色上移,在这既转移给父
- 导致经过兄的路径的黑色数都增加一,违反5
- 将兄染成红即可
- 此时若父为红+黑,直接染黑
- 若父为黑+黑,则继续处理
删除修复情况3
- 分析:当前为黑+黑,兄为黑,兄的孩子左红右黑
- 解法:兄的左孩子染黑,兄染红,兄右旋,后重置当前的兄弟节点
问一句为什么?
- 将其转换为情况4
- 为此需右旋兄,直接右旋违反4
- 先将兄的左孩子染黑,兄染红
- 兄发生了变化,需重置当前的兄弟节点
删除修复情况4
- 分析:当前为黑+黑,兄为黑,兄的右子为红,兄的左子随意
- 解法:将兄染成父,父染黑,兄的右子染黑,父左旋,设当前为根节点
问一句为什么?
- 目的:去除当前身上的额外黑色
- 若直接将父左旋,违背4,为此染黑父
- 但染黑父后左旋,将导致:
- 经过当前和根的路径违背5,所以可直接去除当前身上额外的黑色
- 经过根和兄左子的路径违背5,所以可互换父和兄的颜色,因为父已染黑,亦可将兄染成父
- 经过根和兄右子的路径违背5,所以可在满足上一条件时,将兄右子染黑
后记
- 因理解尚浅,尚有表达错误或修改之处,劳烦指出
- 需要图画的可查看书本或引用,在此不表,请谅解
- 后续可能会尝试实现,望与君共勉
- 若需转载,麻烦注明博客园-AnnsShadoW