zoukankan      html  css  js  c++  java
  • 浅尝红黑树

    目录


    红黑树的优势

    • 一棵有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

    引用参考

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

  • 相关阅读:
    CAP概述与技术选型
    maven基础命令
    那就从头开始吧,哈哈。
    react 小细节
    二分查找法,折半查找原理
    心态很重要
    apache 软件基金会分发目录。
    jquery的基础知识复习()
    jquery的基础知识复习(基础选择器,属性选择器,层级选择器)
    CPP函数类型转换
  • 原文地址:https://www.cnblogs.com/annsshadow/p/5170141.html
Copyright © 2011-2022 走看看