红黑树的前世今生

红黑树

引言

了解红黑树之前,先了解一下二叉查找树和2-3树会更好理解一点。因为红黑树就是2-3树的一种实现方式。
二叉查找树参考博客

完美平衡的2-3树

由于二叉查找树的性能不稳定,2-3树就能从理论上解决二叉查找树的缺点,让所有的操作都在O(logN)级别的时间复杂度完成。二叉查找树中的节点成为 2-结点(一个键两个链接),现在2-3树引入 3-结点(两个键,三个链接)。

定义。一棵2-3查找树或为一棵空树,或由以下结点组成:

  • 2-结点,含有一个键(及其对应的值)和两条链接,左链接指向的2-3树中的键都小于该结点,右链接指向的2-3树中的鍵都大于该结点。
  • 3-结点,含有两个键(及其对应的值)和三条链接,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。

2-3査找树如图所示

image-20210608145204640

一颗完美平衡的2-3树,所有的空链接到根节点之间的距离都是相等的。

查找操作

image-2021060814555196

插入操作

插入操作复杂一些,如果键存在即查找命中的话,那么更新键中保存的值即可。如键不存在查找未命中就要分为两种情况了

首先查找结束于一个2-结点的话,就让该结点变成3-结点就可以了。

image-20210608150001093

如果结束于3-节点的话,就会产生一个4-节点,一般是把这个4-节点的中那个键拿出来作为其他两个根的父节点,它分为三种情况

  1. 如果树中只由一个3-结点组成,那么直接将中间那个键提出来作为根结点,其余两个键作为他的子结点,把一个4-结点分解为3个2-结点。

    image-2021060815042709
  2. 如果该3-结点的父节点是2-结点,那么将中间那个键提出来放入它的父节点中,让父节点变为3-结点。把4-结点分为两个2-结点,提取出来的中间结点作为父节点的一个结点。

    image-20210608150718940
  3. 如果该3-结点的父结点也是3-结点,那么根据步骤二,提取的中间结点会让父节点也会变为4-结点,那么还要对父节点进行调节,在把父结点的中间结点提取出来往上传,直到遇到一个父结点为2-结点或者根节点,如果根结点也是3-结点那么久分解根节点,这样会导致树高+1。

    image-20210608151432976

和二叉查找树的由上向下生长不同,2-3树是由下往上生长的,所以就算插入的键是有序的也不会出现二叉查找树那样极端的结果,所以树还是黑色平衡的。

红黑树

简介

2-3树虽然从理论上来说能解决二叉查找树的缺点,但是实现非常麻烦,要同时维护两种数据结构 2-结点 和 3-结点。在两种数据结构之间转换复制信息不仅仅代码复杂而且需要额外的时间和空间开销,最后的结果也没理论那么好。所以提出了红黑树这种数据结构,红黑树的实现就是使用 二叉查找树 来实现 2-3树 的功能。

红黑树在两个2-结点之间的链接颜色来实现2-结点和3-结点,如果链接颜色为红色,那么两个结点就表示一个 3-结点。如果链接颜色为黑色,那么是两个独立的 2-结点。

image-20210608164927969

红黑树的一种定义

  1. 红链接均为做结点。这样定义规范一点,我认为右链接也能为红色,只要不是左右链接都为红色就行,因为左右链接都为红色表示的是一个 4-结点。但是如果左右链接可以任意为红色会增加代码的实现复杂程度。
  2. 没有任何一个结点同时与两条红链接相连。一个结点有三条链接,只有任意两条是红的就相当于一个4-结点。
  3. 任意空链接到根节点路径上的黑链接要相同,即黑色平衡

实现

颜色表示

public class RedBlackBST<Key extends Comparable<Key>, Value> {
    private static final  boolean RED = true; //新建结点默认是红色
    private static final  boolean BLACK = false;
    private Node root;
    /**
     * 红黑树中Node的定义,与BST相比就多了一个color属性
     */
    @Data
    private class Node {
        Key key;
        Value value;
        Node left, right;
        int N;
        boolean color; //color 表示指向自己的链接的颜色, true表示红色, false表示黑色

        public Node(Key key, Value value, int n, boolean color) {
            this.key = key;
            this.value = value;
            N = n;
            this.color = color;
        }
    }

    /**
     * 判断一个节点是不是红节点
     * @param x 指定的节点
     * @return true是红色,false是黑色
     */
    private boolean isRed(Node x) {
        if (x == null)
            return false;
        return x.color == RED;
    }
    //增删改查...
}

旋转

红黑树的黑色平衡是靠改变颜色和旋转来实现的,相对于绝对的平衡二叉树需要调整的频率要少很多,效率更高。

左旋:解决右链接是红色的情况

image-20210608165938556

实现

/**
 * 左旋 -> 该节点的右子树是红链接要转换成该子树的左连接是红色,这个操作不会破坏黑色平衡的
 *
 * @param h 以该节点为根节点进行左旋操作
 * @return 返回新的根节点
 */
Node rotateLeft(Node h) {
    //让x做根节点
    Node x = h.right;
    h.right = x.left;
    x.left = h;
    //更新颜色
    x.color = h.color;//指向自己的链接颜色是不变的,是继承原根节点的颜色
    h.color = RED;
    x.N = h.N;
    h.N = size(h.left) + size(h.right) + 1;
    return x;
}

右旋 出现两个连续的左红链接,通常右旋之后还要进行颜色转换

image-20210608170243080

右旋实现

/**
 * 右旋操作主要是为了解决连续两个左链接都是红色的情况,往往和flipColors配合使用
 *
 * @param h 以该节点为根节点进行右旋操作
 * @return 返回新的根节点
 */
Node rotateRight(Node h) {
    //让x做根节点
    Node x = h.left;
    h.left = x.right;
    x.right = h;
    //更新颜色
    x.color = h.color;
    h.color = RED;
    x.N = h.N;
    h.N = size(h.left) + size(h.right) + 1;
    return x;
}

颜色转换 一个结点的左右链接都是红色时使用:本质也就是2-3树出现了4-结点,要把红色的链接往上传递,直到不出现连续两个左红链接为止。

image-2021060817033923

颜色转换实现

/**
 * 颜色转换 -> 左右节点都是红链接的时候使用,把红链接向上传递,不会破坏红黑树的定义
 * 4-节点 转换成  2-节点
 * 需要注意的是指向root节点的链接总是黑色的
 * @param h
 */
void flipColors(Node h) {
    h.color = RED;
    h.left.color = BLACK;
    h.right.color = BLACK;
}

插入操作

插入操作和2-3树一样会出现很多情况,在查找过程中如果命中就只更新值就行了,如果没有命中那么就要考虑以下几种情况了

如果要插入的结点是2-结点

  1. 向只有一个2-结点的红黑树插入新键
  2. 向树底部的2-结点插入键

image-20210608171615330

如果要插入的结点是3-结点

那么分三种情况

  • 新键是最大的 只需一个颜色转换
  • 新键最小 需要一次右旋哦,再进行一次颜色转换
  • 新键介于两者之间 需要一个左旋再进行一次右旋,最后进行一次颜色转换

image-20210608171952517

将红链接向上传递

插入之后可能会破环红黑树的定义,插入之后进行颜色颜色转换之后,可能会出现两个连续的左红链接,或者出现右红链接。

image-2021060817280460

插入操作看起来情况很多,但是可以分三步来解决插入操作出现的各种情况

  1. 如果出现有链接是红色,进行左旋
  2. 如果连续两个左链接是红色,进行右旋
  3. 如果左右链接都是红色,进行颜色转换

image-20210608173037915

如果红链接向上传递破环了红黑树的定义,再往上进行调节。

插入实现

private Node put(Node x, Key key, Value value) {
    //这一段和BST是一样的
    if (x == null)
        return new Node(key,value,1,RED);
    int cmp = key.compareTo(x.key);
    if (cmp < 0) x.left = put(x.left, key, value);
    else if (cmp > 0) x.right = put(x.right, key, value);
    else x.value = value; //键存在,执行更新操作
    //这一段是为了维护红黑树的定义
    /*
     插入 2-节点 就只需要烤炉左旋,插入 3-节点 情况复杂点
     但插入的所有情况都可以通过一下三步来解决
        如果右子结点是红色的而左子结点是黑色的,进行左旋转;
        如果左子结点是红色的且它的左子结点也是红色的,进行右旋转
        如果左右子结点均为红色,进行颜色转换。
     */
    if (isRed(x.right) && !isRed(x.left)) x = rotateLeft(x);
    if (isRed(x.left) && isRed(x.left.left)) x = rotateRight(x);
    if (isRed(x.left) && isRed(x.right)) flipColors(x);

    x.N = size(x.left) + size(x.right) + 1;
    return x;
}

总结

红黑树的插入实现可以达到2-3树的理论时间复杂度,即所有的操作都可以在O(logN)来实现。但是实现比较复杂,删除操作比插入操作更复杂,这篇文章就不讨论了。主要是了解红黑树的由来和插入的实现。就算输入红黑树的键是升序的,也不会出现二叉查找树高度为N的情况

image-2021060817413425

一颗大小为N的红黑树,高度最高不会超过2logN。因为不能出现4-结点,即不能出现两个连续红色左链接。所以最坏的情况是最左边的路径都是 3-结点,即红黑相隔的,高度约等于2logN,另一边都是2-结点。

性能分析

image-20210608174228379

posted @ 2021-06-08 17:47  ${yogurt}  阅读(102)  评论(0编辑  收藏  举报