浅尝红黑树

目录


红黑树的优势

  • 一棵有n个内部结点的红黑树的高度最多是2lg(n+1)
  • 进行基本动态集合操作如查找、插入、删除等的时间复杂度最坏为O(log n)

红黑树的应用

  • C++的STL中的set和map
  • Linux的虚拟内存管理
  • 关联数组的实现
  • 等等

五条红黑性质

  1. 每个结点不是红就是黑(非红即黑)
  2. 根结点是黑的(根黑)
  3. 每个叶结点:指树尾端NIL指针或NULL结点,是黑的(NIL/NULL黑)
  4. 如果一个结点是红的,那么它的俩个儿子都是黑的,即从每个叶子到根的所有路径上都不能有两个连续的红色结点(红父黑孩)
  5. 对于任一结点而言,其到叶结点树尾端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

引用参考

《算法导论》中文第三版
百度百科-红黑树
教你透彻了解红黑树

posted @ 2016-01-29 21:18  AnnsShadoW  阅读(570)  评论(0编辑  收藏  举报