爱恨交织的红黑树

BST

虐你千万遍,还要待她如初恋的红黑树,是否对她既欢喜又畏惧。别担忧,经过本文讲解,但愿你能有史无前例的感动。java

红黑树也是二叉查找树,但比普通的二叉查找树多一些特性条件限制,每一个结点上都存储有红色或黑色的标记。由于是二叉查找树,因此他拥有二叉查找树的全部特性。红黑树是一种自平衡二叉查找树,在极端数据条件插入时(正序或倒叙)不会退化成类链状数据,能够更高效的在O(log(n))时间内完成查找,插入,删除操做。node

准备

在阅读本文以前,建议先阅读我上篇文章《二叉查找树的解读和实现》,能够更好的帮助你理解红黑树。this

特性

  1. 结点是红色或黑色
  2. 根结点必须为黑色
  3. 叶子结点(约定为null)必定为黑色
  4. 任一结点到叶子结点的每条路径上黑色结点数量都相等
  5. 不容许连续两个结点都为红色,也就是说父结点和子结点不能都为红色

查找

红黑树的查找方式和上篇文章所讲述的原理同样,这里就不从新讲述,以结点[38,20,50,15,27,43,70,60,90]为例,返回一颗红黑树。编码

红黑树

普通操做

红黑树的插入和删除,分为多种状况,相对来讲比较复杂。插入或删除新结点后的树,必需要知足上面五点特性的二叉查找树,因此要经过不一样手段来调整树。但普通操做就是和普通二叉查找树操做同样。
好比普通插入中,由于每一个结点只能是红色或黑色,因此咱们定义新添加的非根结点默认颜色为红色。将新结点定义为红色的缘由是为了知足特性4(任一结点到叶子结点的每条路径上黑色结点数量都相等),不然会多出一个黑色结点打破规则。
如今向树中插入结点10。设计

插入结点10

从图中能够看到,父结点15为黑色结点,插入红色结点10,不会增长黑色结点的数量,其余规则也没有受到影响,因此,当插入结点的父结点为黑色时,直接插入树中,不会破坏原红黑树的规则。
该种状况代码实现:
结点对象code

package com.ytao.rbt;

/**
 * Created by YANGTAO on 2019/11/9 0009.
 */
public class Node {

    public static String RED = "red";
    public static String BLACK = "black";

    public Integer value;

    public String color;

    public Node left;

    public Node right;


    public Node(Integer value, String color, Node left, Node right) {
        this.value = value;
        this.color = color;
        this.left = left;
        this.right = right;
    }

    public Node(int value, String color) {
        this.value = value;
        this.color = color;
    }
}

实现操做对象

public void commonInsert(Node node, Integer newVal){
    if (node == null)
        node = new Node(newVal, Node.BLACK);
    
    while (true){
        if (newVal < node.value){
            if (node.left == null){
                // 若是左树为叶子结点而且父结点为黑色,能够直接插入红色新结点
                if (node.color == Node.BLACK){
                    node.left = new Node(newVal, Node.RED);
                    break;
                }
            }
            node = node.left;
        }else if (newVal > node.value){
            if (node.right == null){
                if (node.color == Node.BLACK){
                    node.right = new Node(newVal, Node.RED);
                    break;
                }
                
            }
            node = node.right;
        }
    }
}

看到这段代码,是否似曾相识的感受,没错,这就是上篇文章的插入操做加了个颜色限制。
一样删除也是如此,这里就不在细述。blog

变色

为了更好分析清楚变色的缘由,咱们将树中的50结点提取出来做为根结点,如图:get

结点50做为根的树

向树中添加结点55,获得树如图:博客

添加结点55

这时55和60都为红色结点,不符合红黑树的特性(不容许连续两个结点都为红色),这时咱们须要调整,就使用到变色。
将父结点60变为黑色,又遇到不符合红黑树特性(任一结点到叶子结点的每条路径上黑色结点数量都相等),由于咱们增长了黑色结点60,多出了一个黑色结点。
这时的结点70必定为黑色,由于本来的父结点60的颜色为红色。将结点70变为红色,知足告终点70的左子树,但右子树受结点70变为红色的影响,少了个黑色结点,恰好结点90为红色,能够将其变为黑色,知足结点70的右子树要求。
该种特殊状况较为简单处理,只需经过变色就能处理。

变色调整后

这种条件结构的红黑树实现:

public void changeColor(Node node, int newVal){
    if (node.left == null || node.right == null)
        return;
    // 经过判断待插入结点的父结点和叔叔结点,是否知足咱们须要的条件
    if (node.left.color == Node.RED && node.right.color == Node.RED){
        // 肯定是更新到左树仍是右树中
        Node base = compare(newVal, node.value) > 0 ? node.right : node.left;
        // 和待插入结点的父结点做比较
        if (newVal < base.value && base.left == null){
            base.left = new Node(newVal, Node.RED);
        }else if (newVal > base.value && base.right == null){
            base.right = new Node(newVal, Node.RED);
        }
    }
    node.color = Node.RED;
    // 经过取反获取插入结点的叔叔结点并将颜色变黑色
    Node uncleNode = compare(newVal, node.value) > 0 ? node.left : node.right;
    uncleNode.color = Node.BLACK;
}

public int compare(int o1, int o2){
    if (o1 == o2)
        return 0;
    return o1 > o2 ? 1 : -1;
}

旋转

当仅仅经过变色没法解决咱们须要知足特性时,咱们就要考虑使用红黑树的旋转。
旋转在插入和删除中,会频繁用到该操做,为了知足咱们的五条特性,经过旋转能够生成一颗新的红黑树,旋转分为左旋转和右旋转。

左旋转

左旋转为逆时针的旋转,相似于把父结点往左边拉(能够这么记忆区分左右旋转的方向),变换如图:

左旋转

右旋转

右旋转与左旋转出方向相反外,其余都同样,变换如图:

右旋转

从图中能够看出,旋转后的父子结点,关系对调了,同时子结点的子结点给了父结点。
若是是左旋转,那么父结点会成为旋转结点的左子结点;子结点的左子结点会成为父结点的右子结点。
若是是右旋转,那么父结点会成为旋转结点的右子结点;子结点的右子结点会成为父结点的左子结点。
听起来比较比较拗口,记住一条规则,左小右大,结合上图两个旋转就比较好理解。
用代码实现旋转以下:

/**
 *
 * @param node 两个旋转结点中的父结点
 * @param value 两个旋转结点中子结点的值,由于在整合旋转的时候,node能够遍历查找出来,value做为须要旋转的标记结点
 */
public void rotate(Node node, int value){
    Node nodeChild = compare(value, node.value) > 0 ? node.right : node.left;
    if (nodeChild != null && value == nodeChild.value){
        Node parent = node;
        // 旋转子结点小于旋转父结点,执行的是右旋转,不然为左旋转
        if (value < node.value){
            rightRotate(parent);
        }else if (value > node.value){
            leftRotate(parent);
        }
    }
}

/**
 * 左旋转
 * @param node 旋转的父结点
 */
public void leftRotate(Node node){
    Node rightNode = node.right;
    // 旋转结点的左子结点给父结点的右子结点
    node.right = rightNode.left;
    // 父结点做为子结点的左子结点
    rightNode.left = node;
}

/**
 * 右旋转
 * @param node
 */
public void rightRotate(Node node){
    Node leftNode = node.left;
    // 旋转结点的右子结点给父结点的左子结点
    node.left = leftNode.right;
    // 父结点做为子结点的右子结点
    leftNode.right = node;
}

旋转变色案例应用

在上面结点为38的红黑中插入结点55。应用前面讲解到的变色后,红黑树结构如图:

此时出现不知足红黑树特性(不容许连续两个结点都为红色),这时须要咱们将结点50和结点38进行左旋转,获得以下图:

根结点50不符合红黑树特性(根结点必须为黑色),因此先将根结点变为黑色后。

如今获得的红黑树,又出现违背(任一结点到叶子结点的每条路径上黑色结点数量都相等)特性,左树比右树多一个黑色结点,此时将38,20,15,27颜色改变。

这里通过变色旋转完成了上面这课树的操做红黑树的调整。

因为代码篇幅较大,并无将所有可能状况都考虑进来。相信理解了红黑树的对编码实现不是太大问题。

总结

红黑树的操做是基于普通二叉查找树上加了红黑树的特性,不论是插入仍是删除操做,也就是在普通红黑树上进行旋转变色调整树结构,因此在理解红黑树的时候,主要把握旋转,变色,利用旋转变色来知足红黑树的特性,这也是红黑树的精华所在。懂得其原理和设计思想的话,应用到实际中解决问题确实是很不错的设计。固然,红黑树在实际的操做过程当中是多变的,复杂的,要彻底掌握仍是要花点时间来研究的。




我的博客: https://ytao.top
个人公众号 ytao
个人公众号

相关文章
相关标签/搜索