算法导论真的好(厚)啊,才看到红黑树

207 阅读2分钟
  • 棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或BLACK。

  • 树中每个结点包含5个属性:color、key、left、right和p。

  • 如果一个结点没有子节点或父节点,则该节点相应指针属性值为NIL。可将这些NIL视为指向二叉搜索树的叶节点(外部结点)的指针,而把带关键字的结点视为树的内部节点。

红黑树的性质

  1. 每个结点或是红色,或是黑色。
  2. 根节点是黑色的
  3. 每个叶节点(NIL)是黑色的
  4. 如果一个结点是红色的,则它的两个子节点都是黑色的。(一个红结点的父节点和子节点都是黑色)
  5. 对每个结点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色结点。

为了便于处理,使用一个哨兵来代表NIL。对于红黑树T,T.nil是一个空节点,其color属性为黑色,其他属性为任意值。所有指向NIL的指针均使用哨兵T.nil替换。

一棵有n个内部节点的红黑树的高度至多为2lg(n+1)。

红黑树的初始化

  • 红黑树

    public class RedBlackTree {
        RedBlackTreeNode root;
        RedBlackTreeNode nil;
        
        public RedBlackTree() {}
        public RedBlackTree(RedBlackTreeNode root) {
            this.root = root;
        }
    }
    
  • 树节点

    public class RedBlackTreeNode {
        String color;
        int key;
        RedBlackTreeNode left;
        RedBlackTreeNode right;
        RedBlackTreeNode p;
    
        public RedBlackTreeNode() {}
        public RedBlackTreeNode(String color, int key, RedBlackTreeNode left, RedBlackTreeNode right, RedBlackTreeNode p) {
            this.color = color;
            this.key = key;
            this.left = left;
            this.right = right;
            this.p = p;
        }
    }
    

红黑树的常规操作

指针结构的修改:旋转

这是一种能保持二叉搜索树性质的搜索树局部操作。

  • 左旋

    public static void leftRotate(RedBlackTree T,RedBlackTreeNode x) {
        RedBlackTreeNode y = x.right;
        //把x结点和其新子节点都连接好
        x.right = y.left;
        if (y.left != T.nil) {
            y.left.p = x;
        }
        //把x的根节点和y连接好
        y.p = x.p;
        if (x.p == T.nil) {
            T.root = y;
        } else if (x == x.p.left) {
            x.p.left = y;
        } else {
            x.p.right = y;
        }
        //把y和x连接好
        y.left = x;
        x.p = y;
    }
    
  • 右旋

    public static void rightRotate(RedBlackTree T,RedBlackTreeNode x) {
        RedBlackTreeNode y = x.left;
        //把y结点和其新子节点都连接好
        x.left = y.right;
        if (y.right != T.nil) {
            y.right.p = x;
        }
        //把y的根节点和x连接好
        y.p = x.p;
        if (x.p == T.nil) {
            T.root = y;
        } else if (x == x.p.left) {
            x.p.left = y;
        } else {
            x.p.right = y;
        }
        //把x和y连接好
        y.right = x;
        x.p = y;
    }
    

左旋和右旋的时间复杂度均为O(1)。在旋转操作中,只有指针改变,其他所有属性均保持不变。

插入

  • RB-Insert(在红黑树T内插入结点z)

    public static void RBInsert(RedBlackTree T,RedBlackTreeNode z) {
        RedBlackTreeNode y = T.nil;
        RedBlackTreeNode x = T.root;
        while (x != T.nil) {
            y = x; //y用来记录x的父节点
            if (z.key < x.key) {
                x = x.left;
            } else {
                x = x.right;
            }
        }
        z.p = y;
        if (y == T.nil) {
            T.root = 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";
        RBInsertFixup(T,z)//对结点重新着色并旋转
    }
    

    仅插入的时间复杂度为O(lgn)。

    插入新节点z的时候,将着为红色,原因是因为结点z代替了黑色的哨兵,若z是黑色则必然导致其所有祖先结点的黑高度发生变化。而z是红结点时,其两个子节点都是哨兵T.nil,可使黑高度不变,即性质5仍然成立。

    此时,插入操作仅可能破坏的性质就是性质2(根节点需要为黑色)和性质4(一个红结点不能有哄孩子)。

    • 如果违反了性质2,则红色根节点一定是新增节点z,它是树中唯一的内部节点。此时不会违反性质4。
    • 如果违反了性质4,必然是因为z和z.p都是红色的。
  • RB-Insert-Fixup(对结点重新着色并旋转)

    public static void RBInsertFixup(RedBlackTree T,RedBlackTreeNode z) {
        while (z.p.color == "RED") {	//z.p是红色的
            if (z.p == z.p.p.left) {
                RedBlackTreeNode 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;
                } else if (z == z.p.right) {	//情况2
                    z = z.p;
                    leftRotate(T,z);
                } else {				//情况3
                    z.p.color = "BLACK";
                    z.p.p.color = "RED";
                    rightRotate(T,z.p.p);
                }
    
            } else {
                RedBlackTreeNode y = z.p.p.left;
                if (y.color == "RED") {		//情况1
                    z.p.color = "BLACK";
                    y.color = "BLACK";
                    z.p.p.color = "RED";
                    z = z.p.p;
                } else if (z == z.p.left) {		//情况2
                    z = z.p;
                    rightRotate(T,z);
                } else {					//情况3
                    z.p.color = "BLACK";
                    z.p.p.color = "RED";
                    leftRotate(T,z.p.p);
                }
            }
        }
        T.root.color = "BLACK";
    }
    

    分为三种情况:区别在于z父亲的兄弟结点(或称为叔结点)的颜色不同

    • 情况1:z的叔结点y是红色的。

      因为z.p.p的颜色为黑色,故将z.p和y都着为黑色,以此解决z和z.p都是红色的问题,将z.p.p着为红色以保持性质5。把z.p.p作为新节点z来重复while循环。

    • 情况2:z的叔结点y是黑色的且z是一个右孩子

    • 情况3:z的叔结点y是黑色的且z是一个左孩子

      情况2可以通过左旋变为情况3,因为z和z.p均为红色,该旋转不会影响其黑高和性质5。情况3中改变某些结点的颜色并做一次右旋,以保持性质5。

    仅当情况1发生,然后指针沿着树上升两层,while循环才会重复执行,故while循环可能被执行的总次数为O(lgn)。其中旋转从不超过2次,因为只要执行了情况2或情况3,while循环就结束了。

整个插入操作(RBInsert和RBInsertFixup)的时间复杂度为O(lgn)。

删除

  • RB-Delete

    public static void RBDelete(RedBlackTree T,RedBlackTreeNode z) {
        RedBlackTreeNode y = z;
        RedBlackTreeNode x; //x指向y的唯一子节点或指向哨兵T.nil,  x.p总是指向y父节点的原始位置
        String y_original_color = y.color; //发生改变前的y颜色
        if (z.left == T.nil) {
            x = z.right;
            RBTransplant(T,z,z.right);
        } else if (z.right == T.nil) {
            x = z.left;
            RBTransplant(T,z,z.left);
        } else {
            y = treeMinimum(T,z.right);
    
            y_original_color = y.color;
            x = y.right;
            if (y.p == z) {
                x.p = y;
            } else {
                RBTransplant(T,y,y.right);
                y.right = z.right;
                y.right.p = y;
            }
            RBTransplant(T,z,y);
            y.left = z.left;
            y.left.p = y;
            y.color = z.color;
        }
        if (y_original_color == "BLACK") { //如果y是黑色就有可能引入一个或多个红黑性质被破坏的情况
            RBDeleteFixup(T,x);//恢复红黑性质
        }
    }
    
    public static void RBTransplant(RedBlackTree T,RedBlackTreeNode u,RedBlackTreeNode v) {
        if (u.p == T.nil) {
            T.root = v;
        } else if (u == u.p.left) {
            u.p.left = v;
        } else {
            u.p.right = v;
        }
        v.p = u.p;
    }
    
    public static RedBlackTreeNode treeMinimum(RedBlackTree T,RedBlackTreeNode x) {
        while (x.left != T.nil) {
            x = x.left;
        }
        return x;
    }
    

仅删除操作的时间复杂度为O(lgn)。

  • RB-Delete-Fixup

    public static void RBDeleteFixup(RedBlackTree T,RedBlackTreeNode x) {
        RedBlackTreeNode w;
        while (x != T.root && x.color == "BLACK") {
            if (x == x.p.left) {
                w = x.p.right;//指向右边兄弟结点
                if (w.color == "RED") {	//情况1
                    w.color = "BLACK";
                    x.p.color = "RED";
                    leftRotate(T,x.p);
                    w = x.p.right;
                }
                if (w.left.color == "BLACK" && w.right.color == "BLACK") {	//情况2
                    w.color = "RED";
                    x = x.p;
                } else if (w.right.color == "BLACK") { 	//情况3
                    w.left.color = "BLACK";
                    w.color = "RED";
                    rightRotate(T,w);
                    w = x.p.right;
                } else {				//情况4
                    w.color = x.p.color;
                    x.p.color = "BLACK";
                    w.right.color = "BLACK";
                    leftRotate(T,x.p);
                    x = T.root;
                }
            } else {
                w = x.p.left;
                if (w.color == "RED") {		//情况1
                    w.color = "BLACK";
                    x.p.color = "RED";
                    rightRotate(T,x.p);
                    w = x.p.left;
                }
                if (w.left.color == "BLACK" && w.right.color == "BLACK") {  //情况2
                    w.color = "RED";
                    x = x.p;
                } else if (w.left.color == "BLACK") {		//情况3
                    w.right.color = "BLACK";
                    w.color = "RED";
                    leftRotate(T,w);
                    w = x.p.left;
                } else {						//情况4
                    w.color = x.p.color;
                    x.p.color = "BLACK";
                    w.left.color = "BLACK";
                    rightRotate(T,x.p);
                    x = T.root;
                }
            }
        }
        x.color = "BLACK";
    }
    

    只有当y为黑色时才会执行RBDeleteFixup。因为如果y为红色,当y被删除或移动时,红黑性质仍然保持。原因如下:

    1. 树中的黑高没有发生变化
    2. 不存在两个相邻的红结点。y的新位置不可能有两个相邻的红结点,因为z本来就是满足性质的。如果y是红色,则x一定是黑色,用x替代y不可能使两个红结点相邻。
    3. 如果y是红色,就不可能是根节点

    如果y是黑色的,会产生三个问题:

    1. 如果y是原来的根节点,而y的一个红色孩子成为新的根节点,违反了性质2.

    2. 如果x和x.p是红色的,违反了性质4.

    3. 在树中移动y将导致先前包含y的任何简单路径上的黑结点个数少1。y的任何祖先都不满足性质5.

      解决办法:

      假设将占有y的x的颜色视为还有一层额外的黑色。这样性质5仍然成立。x此时为红黑色或双重黑色,x的属性仍然为RED或BLACK。

    恢复红黑性质分三种情况:

    • 情况1:x的兄弟结点w是红色的

      将x.p着为红色,w着为黑色,对x.p做一次左旋而不违反红黑树任何性质。现在x的新兄弟结点w是旋转之前w的某个子节点,其颜色为黑色,故将情况1转换为了情况2、3、4。

    • 情况2:x的兄弟结点w是黑色的,而且w的两个子节点都是黑色的

      将x和w去掉一重黑色,则x只有一重黑色,w为红色,为了补偿去掉的这重黑色,将它们的父节点x.p加上一重黑色,并将x.p作为新的x来循环。如果是通过情况1进入情况2,则此时新x红黑色的,因为之前x.p是红色的。新x的color为RED,测试循环条件后循环终止。

    • 情况3:x的兄弟结点w是黑色的,而w的左孩子是红色的,右孩子是黑色的

      交换w和其左孩子的颜色,对w进行右旋而不违反红黑树的任何性质。此时情况3转换为情况4。

    • 情况4:x的兄弟结点w是黑色的,而w的右孩子是红色的

      将w和其父节点的颜色交换,将右孩子的颜色变为黑色,左旋不违法红黑树的任何性质,将x设置为根后,当while测试循环条件时终止。

    在RBDeleteFixup中,情况1、3和4在各执行常数次数的颜色改变和至多三次旋转后便终止。情况2是while循环可以重复执行的唯一情况,然后指针x沿树上升至多O(lgn)次,且不做任何旋转。故RBDeleteFixup要花费O(lgn)时间,做至多3次旋转。

整个删除操作的时间复杂度为O(lgn)。

数据结构的扩张

顺序统计树(order-statistic tree)

顺序统计树T只是简单地在每个结点上存储附加信息的一棵红黑树。

红黑树的结点:

public class RedBlackTreeNode {
    String color;
    int key;
    int size;  //包含以该节点为根的子树(包括该节点)的内结点数,即这棵子树的大小
    RedBlackTreeNode left;
    RedBlackTreeNode right;
    RedBlackTreeNode p;

    public RedBlackTreeNode() {}
    public RedBlackTreeNode(String color, int key, RedBlackTreeNode left, RedBlackTreeNode right, RedBlackTreeNode p) {
        this.color = color;
        this.key = key;
        this.left = left;
        this.right = right;
        this.p = p;
    }
}

对于结点x,x.size = x.left.size + x.right.size + 1;

在一棵顺序统计树中,并不要求关键字各不相同。定义一个元素的秩为在中序遍历时输出的位置。

通过size这个附加信息,可以查找具有给定秩的元素和确定一个元素的秩。

  • 查找具有给定秩的元素

    OS-Select

    public static OS_RedBlackTreeNode OsSelect(OS_RedBlackTreeNode x,int i) {
        int r = x.left.size + 1;
        if (i == r) {
            return x;
        } else if (i < r) {
            return OsSelect(x.left,i);
        } else {
            return OsSelect(x.right,i - r);
        }
    }
    

    每次递归都沿树下降一层,整个运行时间为O(lgn)。

  • 确定一个元素的秩

    OS-Rank

    public static int OsRank(OS_RedBlackTree T,OS_RedBlackTreeNode x) {
        int r = x.left.size + 1;
        OS_RedBlackTreeNode y = x;
        while (y != T.root) {
            if (y == y.p.right) {
                r = r + y.p.left.size + 1;
            }
            y = y.p;
        }
        return r;
    }
    

    返回对T中序遍历对应的线性序中x的位置。

    while的每次迭代沿树上升一层,最坏情况的运行时间为O(lgn)。

对size的维护

插入操作

分两个阶段

  • 第一阶段:维护子树的规模

    对由根至叶子的路径上遍历的每个结点x,都增加x.size的属性。新增结点的size为1。由于一条遍历的路径上共有O(lgn)个结点,故维护size属性的额外代价为O(lgn)。

  • 第二阶段:参加旋转的两个结点的size维护。

    旋转次数至多为2,代价为O(1)。

旋转

  • 左旋

    public static void leftRotate(OS_RedBlackTree T, OS_RedBlackTreeNode x) {
        OS_RedBlackTreeNode y = x.right;
        //把x结点和其新子节点都连接好
        x.right = y.left;
        if (y.left != T.nil) {
            y.left.p = x;
        }
        //把x的根节点和y连接好
        y.p = x.p;
        if (x.p == T.nil) {
            T.root = y;
        } else if (x == x.p.left) {
            x.p.left = y;
        } else {
            x.p.right = y;
        }
        //把y和x连接好
        y.left = x;
        x.p = y;
        
        y.size = x.size;
        x.size = x.left.size + x.right.size + 1;
    }
    
  • 右旋

    public static void rightRotate(OS_RedBlackTree T, OS_RedBlackTreeNode x) {
        OS_RedBlackTreeNode y = x.left;
        //把y结点和其新子节点都连接好
        x.left = y.right;
        if (y.right != T.nil) {
            y.right.p = x;
        }
        //把y的根节点和x连接好
        y.p = x.p;
        if (x.p == T.nil) {
            T.root = y;
        } else if (x == x.p.left) {
            x.p.left = y;
        } else {
            x.p.right = y;
        }
        //把x和y连接好
        y.right = x;
        x.p = y;
        
        y.size = x.size;
        x.size = x.left.size + x.right.size + 1;
    }
    

顺序统计树的插入元素所需要的总时间为O(lgn)。

删除操作

分两个阶段:

  • 第一阶段:维护子树的规模

    遍历一条由结点y(从他的原始位置开始)至根的简单路径,并减少路径上每个结点的size属性的值。代价为O(lgn)。

  • 第二阶段:参加旋转的两个结点的size维护

    至多三次旋转,代价为O(1)。

顺序统计树的删除元素所需要的总时间为O(lgn)。

如何扩张数据结构

分为4个步骤:

  1. 选择一种基本的数据结构
  2. 确定基础数据结构中要维护的附加信息
  3. 检验基础数据结构上的基本修改操作能否维护附加信息
  4. 设计一些新操作

区间树(interval tree)

区间树是一种对动态集合进行维护的红黑树,其中每个元素x都包含一个区间x.int。

两区间i和j,存在三种情况:

  1. 区间i和区间j重叠 (i.low <= j.high 且j.low <= i.high)
  2. i在j的左边 (i.high<j.low)
  3. i在j的右边 (j.high < i.low)
  • 基础数据结构

    选择一棵红黑树,每个结点x包含一个区间属性x.int。且x的关键字为区间低端点x.int.low。该数据结构按中序遍历列出的就是按低端点的次序排列的各区间。

  • 附加信息

    每个结点还包含一个值x.max,它是以x为根的子树中所有区间的端点的最大值。

  • 对信息的维护

    x.max = max(x.int.high,x.left.max,x.right.max)

  • 设计新操作

    Interval-Search,用来找出树T中与区间i重叠的那个结点。

区间树结点

  • public class INT_RedBlackTreeNode {
        String color;
        int key;
        int max;
        Interval i;
        INT_RedBlackTreeNode left;
        INT_RedBlackTreeNode right;
        INT_RedBlackTreeNode p;   
    }
    class Interval {
        int low;
        int high;
    }
    
  • Interval-Search

    public static INT_RedBlackTreeNode intervalSearch(INT_RedBlackTree T,Interval i) {
        INT_RedBlackTreeNode x = T.root;
        while (x != T.nil && (i.high < x.i.low || x.i.high < i.low)) {
            if (x.left != T.nil && x.left.max >= i.low) {
                x = x.left;
            } else {
                x = x.right;
            }
        }
        return x;
    }
    

    intervalSearch耗费O(lgn)时间