• 白话红黑树系列之二——红黑树的构建


      上一篇写了关于红黑树基本性质的东西,这篇来说一说如何创建一棵红黑树吧。

      如果对红黑树的基本性质还有疑问,请先查看一下我的前一篇:http://www.cnblogs.com/unpolishedgem/archive/2012/05/16/2504311.html

      如果图片打不开的话,就去看我的csdn博客:http://blog.csdn.net/arge129

      红黑树是一种二叉查找树,那么我们可以使用插入的方法来创建一棵红黑树,为此,我们先来介绍关于红黑树的一些基本操作。

      1. 旋转

      旋转是一种能保持二叉查找树性质的查找树局部操作,包括左旋右旋两种操作。

      如下图所示,在x结点上做左旋时,我们假设它的右孩子不是nil[T];x可以是树内任意右孩子不是nil[T]的结点。算法导论里面讲到“左旋以x到y之间的链为支轴进行。”我没太理解这句话,但是我是这么想象的,如下图中的曲线箭头所示,左旋就是x下移,y上移,箭头所示方向为左,右旋就是x上移,y下移,箭头所示方向为右。

     

      值得注意的是,在旋转过程中,只会有指针结构的变化,不会有颜色的变化,因此在上面的图中,我没有画出结点的颜色。

    旋转的伪代码,我就不写了,在算法导论里面都有,下面我把我写的旋转代码给贴过来吧,当然还是Java版的。

     1 /**
     2      * 左旋
     3      * @author Alfred
     4      * @param x 输入结点
     5      */
     6     private void leftRotated(RBTreeNode x){
     7         RBTreeNode y = x.getRight();
     8         //x的右孩子y不能是NIL_T,如果是的话,直接返回。
     9         if(y == NIL_T){
    10             return;
    11         }
    12         //将y的左子树变为x的右子树
    13         //设置x的右子树
    14         x.setRight(y.getLeft());
    15         //设置y的右子树的父结点为x
    16         if(y.getLeft() != NIL_T){
    17             y.getLeft().setParent(x);
    18         }
    19         //将x的父结点设置为y的父结点
    20         y.setParent(x.getParent());
    21         //如果x是根结点,则更换根结点
    22         if(x.getParent() == NIL_T){
    23             rootNode = y;
    24         }else if(x == x.getParent().getLeft()){
    25             //如果x是其父结点的左孩子,则将y设为其父结点的左孩子
    26             x.getParent().setLeft(y);
    27         }else{
    28             //如果x是其父结点的右孩子,则将y设为其父结点的右孩子
    29             x.getParent().setRight(y);
    30         }
    31         //y的左孩子为x
    32         y.setLeft(x);
    33         //x的父结点为y
    34         x.setParent(y);
    35     }
    36     /**
    37      * 右旋
    38      * @author Alfred
    39      * @param y 输入结点
    40      */
    41     private void rightRotated(RBTreeNode y){
    42         RBTreeNode x = y.getLeft();
    43         //y的左孩子x不能是NIL_T,如果是的话,直接返回。
    44         if(x == NIL_T){
    45             return;
    46         }
    47         //将x的右子树变为y的左子树
    48         //设置y的左子树
    49         y.setLeft(x.getRight());
    50         //设置x的右子树的父结点为y
    51         if(x.getRight() != NIL_T){
    52             x.getRight().setParent(y);
    53         }
    54         //将y的父结点设置为x的父结点
    55         x.setParent(y.getParent());
    56         //如果y是根结点,则更换根结点
    57         if(y.getParent() == NIL_T){
    58             rootNode = x;
    59         }else if(y == y.getParent().getLeft()){
    60             //如果y是其父结点的左孩子,则将x设为其父结点的左孩子
    61             y.getParent().setLeft(x);
    62         }else{
    63             //如果y是其父结点的右孩子,则将x设为其父结点的右孩子
    64             y.getParent().setRight(x);
    65         }
    66         //x的右孩子为y
    67         x.setRight(y);
    68         //y的父结点为x
    69         y.setParent(x);
    70     }

      2. 插入

      既然红黑树是一棵二叉查找树,那么我们就可以像二叉查找树那样为红黑树插入一个元素。我们将二叉查找树的插入算法做一个略微的修改,我们将结点z插入到树中,就像树T是一棵普通的二叉查找树一样,然后将z着为红色,为保持红黑树的性质,我们需要对树中的结点进行重新着色并旋转。如果对二叉查找树的插入操作不熟悉,请阅读我之前写过的博客:http://www.cnblogs.com/unpolishedgem/archive/2012/05/10/2494403.html

      我们来分析一下,在插入过程中可能违反的性质有哪几个。为此,我把红黑树的性质再抄写一次。

      一棵二叉查找树如果满足下面的红黑性质,则为一棵红黑树:

      1) 每个结点是或是红的,或是黑的。

      2) 根结点是黑的。

      3) 每个叶结点(nil[T])是黑的。

      4) 如果一个结点是红的,那么它的两个儿子是黑的。

      5) 对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。

      首先,我们插入的结点是红色的,因此不会违反性质1)和性质5),性质3)自然成立。唯一可能被破坏的是2)和4)。而且,2)和4)至多有一个性质被破坏。性质2)被破坏时的修复很简单,只需要将根结点重新着色为黑色即可。而性质4)被破坏的修复则要复杂一些,具体分为三种请况。

      情况1):z的叔叔y是红色的。

      如下图所示,如果z的叔叔y是红色的,将z的父结点和y着色为黑色,然后将z的祖父结点着色为红色,最后将z的祖父结点作为新的z结点进行迭代检查,因为z的祖父结点原来是红色的,被着色为黑色的时候,有可能会引起红黑树性质的破坏。

     

      情况2):z的叔叔y是黑色的,而且z是右孩子。

      情况3):z的叔叔y是黑色的,而且z是左孩子。

      如下图所示,如果是情况2),我们可以立即使用一个左旋变成情况3)。情况3)中,首先交换了B和C的颜色,然后通过一个右旋来使整个树达到了满足性质4)。

      从这三种情况来看,可以发现一个非常有趣的事情,那就是该过程所做的旋转从不超过两次,因为只有情况1)会继续将z上移进行红黑性质检查,而一旦进入了情况2)或者情况3),就不会再进行检查了。

      同样,伪代码就不写了,算法导论上都有,在此只写Java实现代码。

     1 /**
     2      * 插入操作
     3      * @author Alfred
     4      * @param k
     5      */
     6     public void treeInsert(int k){
     7         RBTreeNode z = new RBTreeNode(k, NodeColor.RED);
     8         RBTreeNode y = NIL_T;
     9         RBTreeNode x = rootNode;
    10         //与二叉查找树的插入过程类似
    11         while(x != NIL_T){
    12             y = x;
    13             if(z.getKey() < x.getKey()){
    14                 x = x.getLeft();
    15             }else{
    16                 x = x.getRight();
    17             }
    18         }
    19         z.setParent(y);
    20         if(y == NIL_T){
    21             rootNode = z;
    22         }else if(z.getKey() < y.getKey()){
    23             y.setLeft(z);
    24         }else{
    25             y.setRight(z);
    26         }
    27         z.setLeft(NIL_T);
    28         z.setRight(NIL_T);
    29         //进行修复
    30         rbInsertFixUp(z);
    31     }
    32     /**
    33      * 修复插入操作引起的不满足的红黑性质
    34      * @author Alfred
    35      * @param z 要修复的结点
    36      */
    37     private void rbInsertFixUp(RBTreeNode z){
    38         RBTreeNode y = null;
    39         while(z.getParent().getColor() == NodeColor.RED){
    40             //如果z的父结点是z的祖父结点的左孩子
    41             if(z.getParent() == z.getParent().getParent().getLeft()){
    42                 y = z.getParent().getParent().getRight();
    43                 //情况1),z的叔叔y的颜色是红色的。
    44                 if(y.getColor() == NodeColor.RED){
    45                     z.getParent().setColor(NodeColor.BLACK);
    46                     y.setColor(NodeColor.BLACK);
    47                     z.getParent().getParent().setColor(NodeColor.RED);
    48                     z = z.getParent().getParent();
    49                 }else if(z == z.getParent().getRight()){
    50                     //情况2),z的叔叔y的颜色是黑色的,且z是其父结点的右孩子
    51                     z = z.getParent();
    52                     leftRotated(z);
    53                     //情况2)经过左旋之后变为情况3),z的叔叔y的颜色是黑色的,且z是其父结点的左孩子
    54                     z.getParent().setColor(NodeColor.BLACK);
    55                     z.getParent().getParent().setColor(NodeColor.RED);
    56                     rightRotated(z.getParent().getParent());
    57                 }
    58             }else{
    59                 //与上面情况类似。
    60                 y = z.getParent().getParent().getLeft();
    61                 if(y.getColor() == NodeColor.RED){
    62                     z.getParent().setColor(NodeColor.BLACK);
    63                     y.setColor(NodeColor.BLACK);
    64                     z.getParent().getParent().setColor(NodeColor.RED);
    65                     z = z.getParent().getParent();
    66                 }else if(z == z.getParent().getLeft()){
    67                     z = z.getParent();
    68                     rightRotated(z);
    69                     z.getParent().setColor(NodeColor.BLACK);
    70                     z.getParent().getParent().setColor(NodeColor.RED);
    71                     leftRotated(z.getParent().getParent());
    72                 }
    73                 
    74             }
    75         }
    76         //修复性质2)
    77         rootNode.setColor(NodeColor.BLACK);
    78     }

    ps:写博客很累,转载的朋友请注明出处,谢谢。

  • 相关阅读:
    Collection集合总结
    JAVA泛型通配符T,E,K,V区别,T以及Class,Class的区别【转】
    Runnable和Thread
    jdk自带的函数式接口(配合lambda使用)
    java8-接口的静态方法(static)和默认方法(default)
    Java 8
    java8-Stream流【转】
    java8-lambda表达式
    Oracle:ORA-01461报错
    ASP.NET Core 入门笔记2,建立项目
  • 原文地址:https://www.cnblogs.com/unpolishedgem/p/2504350.html
Copyright © 2020-2023  润新知