左倾红黑树

佐手、 提交于 2020-01-31 14:07:19

普通红黑树:允许一个节点有两个红色的子节点,对应2-3-4树
左倾红黑树:一个节点只能有一个红色子节点,并且是左节点,对应2-3树

在学习完红黑树之后我完全不理解是怎么想到红黑树这一种数据结构的,所以我又去看了算法第四版,明白了当红黑二叉树是由2-3查找树(B-树)推导来的

接下来学习红黑二叉查找树(原理、实现)对相应知识进行总结

1. 2-3查找树

2-3查找树

2. 红黑二叉查找树

这里所探究的是左倾红黑树

红黑树就是利用标准二叉树中结点的额外信息表示2-3树,额外信息,其实就是指的(结点)链接的颜色

  • 黑链接是2-3树中的普通链接,也是标准二叉树中的链接。
  • 红链接是将两个2-结点,变为一个3-结点。

所以,我们只需要将3-结点还原成由红链接连接的2个2-结点,即可去掉3-结点!而对于3-结点的子结点,则分配给两个2-结点来完成。
在这里插入图片描述

2.1 等价定义

一个静态的红黑树:(不需要再变换的、操作完成后的)

  • 所有红链接均为左链接
  • 没有任何一个结点与两个红链接相连
    -该树是完美黑色平衡的,即任意空结点到根结点的路径上黑链接数量相同!(黑链数目既是树的高度)

① 对于第一点,对于红黑树红链接为右链接是可以的,但是这里为了统一和美观,以及减少讨论的情况和复杂度,我们统一规定左边,也就是左倾红黑树

② 为啥不能两个红链接相连?两个红链接相连其实就产生了4-结点,这在2-3树中是会被转化的,而我们红黑树是由2-3树演变而来,所以应该通过相应的转化变换解决这个问题

③ 完美平衡二叉树是黑色平衡的,所以我们红黑也是完美黑色平衡的,不计入红色为树的高度

我们将红黑树中,所有的红链接都横过来画(或者说,将2-3树中所有3-结点内部都加上红链接),即显示了为何红黑树也是2-3树,并且红黑树的高度就是2-3树的高度,也就是空结点到根节点路径上普通黑链接的数量。
在这里插入图片描述

2.2 结点构成及颜色表示

我们在二叉树的基础上,加入了结点的颜色来表示指向当前结点的链接颜色。
在这里插入图片描述

    private class Node {
        private Node left;
        private Node right;
        private boolean color = BLACK;
        private Key key;
        private Value value;
        private int size;

        public Node(Key key, Value value, int size, boolean color) {
            this.key = key;
            this.value = value;
            this.size = size;
            this.color = color;
        }
    }
    
    private boolean isRed(Node h) {
        if (h == null) return false;
        return h.color;
    }

2.3 旋转

在我们实现某些操作的时候可能会出现红色右链接或者两条连续的红链接,但在操作完成前这些情况都会被旋转修复
旋转操作会改变红链接的指向

① 左旋:可以将红链从右边移至左边。
这个方法多数用于删除操作和调整树的平衡,以满足我们的等价定义。
在这里插入图片描述
在这里插入图片描述

  • 旋转后需要重置父结点的链接。
  • 旋转后需要调整结点的大小,因为结点的高度变化了。
    private Node rotateLeft(Node h) {
        if (h == null) throw new IllegalArgumentException();
        Node x = h.right;
        h.right = x.left;
        x.left = h;
        x.color = x.left.color;//x.color = h.color
        x.left.color = RED;
        x.size = h.size;
        h.size = 1 + size(h.left) + size(h.right);
        return x;
    }

② 右旋:可以将红链从左边移至右边
这个方法多数临时用于删除操作中,后面我们会介绍删除操作
在这里插入图片描述
在这里插入图片描述

    private Node rotateRight(Node h) {
        if (h == null) throw new IllegalArgumentException();
        Node x = h.left;
        h.left = x.right;
        x.right = h;
        x.color = x.right.color;//x.color = h.color
        x.right.color = RED;
        x.size = h.size;
        h.size = 1 + size(h.left) + size(h.right);
        return x;
    }

2.4 插入

规定:插入的新结点的color都是RED
这个只要想想2-3树中插入就明白了。

1. 向单个2-结点中插入新键:
如果一个红黑树只有一个2-结点,即根节点root,那么插入的时候会有三种情况:

① key == root,替换root 的值。
② key < root,则插入左子结点,此时不需要调整。
③ key > root,则插入右子结点,此时为了满足等价定义,rotateLeft一下就好了。

2. 向树底部的2-结点插入新键:
和上面三个情况差不多,只要保证我们的等价定义,以及二叉树的基本定义(x.left < x < x.right),调整并更新父链接就好啦。

3. 向一个双键树(3-结点)插入新键:

① 新插入的键最大:

插入右子结点x.right。则形成4-结点,此时需要进行变换。这里介绍一个flipColors方法,用来分解4-结点:

  • 一个键h 的左右子结点都是红色,h 为黑色。
  • 将h 的左右子结点都变为黑色,h 变为红色。
  • 此时树的高度+1。
  • 调整后的h 可以根据其他情况进行继续变换。
    在这里插入图片描述
    在这里插入图片描述
    private Node flipColors(Node h) {
        if (h == null || h.left == null || h.right == null) throw new IllegalArgumentException();
        h.color = !h.color;
        h.left.color = !h.left.color;
        h.right.color = !h.right.color;
        return h;
    }

② 新插入的键最小:

插入左子结点的子结点 x.left.left,则此时形成连续的左链接都是红色(A 插入 B-C ,形成 A-B-C)。
这时候需要我们先对C进行右旋 rotateRight©,然后就形成了上面1.的情况。
在这里插入图片描述
③ 新插入的键大小在两个键之间:

插入左子结点的右结点 x.left.right,这种情况最为复杂。例如B插入A-C
此时需要先对A进行左旋 rotateLeft(A),然后就形成了上面2.的情况。
在这里插入图片描述

由此我们可以看出,插入总是在“ 情况3 -> 情况2 -> 情况1 ”之间转化。

根节点总是黑色
当我们进行插入后,根节点有时候会变为红色,此时当根节点由红色转为黑色时,树的高度+1

代码实现:

    public void put(Key key, Value value) {
        root = put(root, key, value);
        root.color = BLACK;//根节点总是黑色
    }

    private Node put(Node h, Key key, Value value) {
        if (key == null) throw new IllegalArgumentException();
        //创建新子结点,颜色为红色
        if (h == null) return new Node(key, value, 1, RED);

        int comp = key.compareTo(h.key);
        if (comp > 0) h.right = put(h.right, key, value);
        else if (comp < 0) h.left = put(h.left, key, value);
        else h.value = value;

        //插入完成后重新调整树以满足等价定义
        if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);//情况3 -> 情况2
        if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);//情况2
        if (isRed(h.left) && isRed(h.right)) h = flipColors(h);//情况1

        //调整完成后需要重新计算树的高度
        h.size = size(h.left) + size(h.right) + 1;
        return h;
    }

2.5 删除 delete:

删除任意一个键,是红黑树中最为复杂的算法。而在删除任意结点的时候,我们先想想二叉树中是如何进行删除操作的。

当x不是树的末结点,若直接删除会造成空缺,树不连续,我们需要变换一下。 则当x不是树的末结点且x有两个子结点时:
删除x后,需要寻找x的右子结点中最小的(或者x的左子结点中最大的)来代替x的位置,保证二叉树的性质“每个结点的键都大于任意左子节点而小于任意右子节点”。

故我们可以这样理解,最复杂的情况下,假设x不是树的末结点且x有两个子结点:
如果我们将x先和右结点最小的(或者左结点最大的)进行交换,然后就将x变为树的末结点,此时即可直接删除。

所以,删除的操作可以简化为 删除最小值 或 删除最大值 的操作。

2.5.1 删除最小值 deleteMin:

由二叉树性质可知,最小键一定是在树的最左边。并且由等价定义可知,最小值一定是在树的末端最左边。

当我们进行删除末结点的时候,让我们先回归2-3树,并且稍微允许临时4-结点的存在。

  • 有时候为了让父结点变为红色,需要临时合并为4-结点。
  • 如果删除的末结点是3-结点,则直接删除即可。
  • 如果删除的末结点是2-结点,直接删除会破坏树的完美平衡。所以此时我们需要进行变换。

分以下几种情况:

  1. 如果此时要删除的结点,它的父结点、兄弟结点(父结点的右结点)都是2-结点,则可以将这三个结点flipColors,还原为一个临时的4-结点。这样在我们删除后,依然是3-结点,不会破坏完美平衡,高度-1。
    在这里插入图片描述
    如果它的父结点不是红色,则想办法变为红色。

  2. 如果此时要删除的结点,它的父结点是2-结点,而它的兄弟结点不是2-结点,则此时需要向兄弟结点借一个结点过来,形成3-结点。
    从兄弟结点中借一个结点,e可有可无

  3. 如果此时要删除的结点,它的父结点不是2-结点,那么从父结点借一个结点过来,形成3-结点。
    两种情况,分别是兄弟结点是否为2-结点
    若兄弟结点不是2-结点,则从父结点借一个结点后,兄弟结点可以补给父结点一个最小的结点,保持树的完美平衡性。
    若兄弟结点是2-结点,则父结点中最小的结点向下合并,形成临时4-结点。

待删除完成后,需要自下而上重新整理树的结构,将所有的临时4-结点分解,从而满足我们的等价定义。

代码实现:

沿着树的最左路径,一路向下的过程中,当遇到2-结点,实现一些变换moveRedLeft(),从而保证当前结点不是2-结点。
这里也就是想办法变红色,从而能够删除键而不破坏树的完美平衡。

private Node moveRedLeft(Node h) {
    //假设h为红色,h.left 和h.left.left都是黑色(h.left和h.left.left都是2-结点)
    //将h.left 或h.left 的子结点之一变红(想办法变红,变为3-结点)
    flipColors(h);//这个方法可以在拆分4-结点和组合4-结点之间变换。

    if (isRed(h.right.left)) {
    //兄弟结点为非2-结点,此时经旋转,将红键从右往左传递。见下图。
        h.right = rotateRight(h.right);
        h = rotateLeft(h);
    }
    return h;
}

moveRedLeft 过程

   public void deleteMin() {
        if (isEmpty()) throw new NoSuchElementException();
        if (!isRed(root.left) && !isRed(root.right))
            root.color = RED;
        root = deleteMin(root);
        if (!isEmpty()) root.color = BLACK;
    }

    private Node deleteMin(Node h) {
        if (h.left == null) return null;
        if (!isRed(h.left) && !isRed(h.left.left))
            h = moveRedLeft(h);
        h.left = deleteMin(h.left);
        return balance(h);//自下而上重新整理树的结构。
    }

一开始,如果根节点的两个子键都没有红键,则需要我们临时将根节点变红,从而可以拆分出红键。
而在删除完成最后,如果树还有结点,则要将根节点还原为黑色,以满足根节点总是黑色。
其中balance()方法与之前我们进行put后的操作类似,不再赘述。

   private Node balance(Node h) {
        if (!isRed(h.left) && isRed(h.right)) h = rotateLeft(h);
        if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
        if (isRed(h.left) && isRed(h.right)) flipColors(h);
        h.size = size(h.left) + size(h.right) + 1;
        return h;
    }

2.5.2 删除最大值 deleteMax:

由于我们的红链都是左链,所以这里与deleteMin稍有不同。

沿着树的最右路径,一路向下的过程中,当遇到2-结点,实现变换moveRedRight(),从而保证当前结点不是2-结点。

private Node moveRedRight(Node h) {
    //假设h为红色,h.right 和h.right.left都是黑色(h.right和h.right.left都是2-结点)
    //将h.right 或h.right 的子结点之一变红(想办法变红,变为3-结点)
    flipColors(h);
    if (!isRed(h.left.left))//两个子结点均为2-结点
        h = rotateRight(h);
    return h;
}

两个子结点都是2-结点的删除示意
如图示不难理解,由于我们删除的是最大值,所以键一定在右子结点中,故要将红键从左往右传递。其与操作与deleteMin差不多,就不赘述了。但是要记住,我们这些局部的旋转和移动都不会改变树的组成(见2-3树性质)。

public void deleteMax() {
    if (isEmpty()) throw new NoSuchElementException();
    if (!isRed(root.left) && !isRed(root.right))
        root.color = RED;
    root = deleteMax(root);
    if (!isEmpty()) root.color = BLACK;
}

private Node deleteMax(Node h) {
    if (isRed(h.left))//由于我们删除的键总在右子结点中
        h = rotateRight(h);
    if (h.right == null) return null;

    if (!isRed(h.right) && !isRed(h.right.left))//h.right是个2-结点
        h = moveRedRight(h);//此时将红键从左向右传递
    h.right = deleteMax(h.right);
    return balance(h);
}

2.5.3 删除任意结点的实现:

删除任意结点就是转为为删除最小值和删除最大值的操作。

public void delete(Key key) {
    if (key == null) throw new IllegalArgumentException();
    if (isEmpty()) throw new NoSuchElementException();
    if (!isRed(root.left) && !isRed(root.right))
        root.color = RED;
    root = delete(root, key);
    if (!isEmpty()) root.color = BLACK;
}

上面这段代码就不解释了,和之前deleteMin的原因一样。我们主要看下面的具体实现。

  private Node delete(Node h, Key key) {
        if (key.compareTo(h.key) < 0) {
            if (!isRed(h.left) && !isRed(h.left.left))
                h = moveRedLeft(h);
            h.left = delete(h.left, key);
        } else {
            if (isRed(h.left))
                h = rotateRight(h);
            if (key.compareTo(h.key) == 0 && h.right == null)
                return null;
            if (!isRed(h.right) && !isRed(h.right.left))
                h = moveRedRight(h);
            if (key.compareTo(h.key) == 0) {
                h.value = get(h.right, min(h.right).key);
                h.key = min(h.right).key;
                h.right = deleteMin(h.right);
            } else h.right = delete(h.right, key);
        }
        return balance(h);
    }
  • 如果寻找的键在左边,则消除左边路径上的2-结点,参考deleteMin。
  • 如果寻找的键在右边,则消除右边路径上的2-结点,参考deleteMax。
  • 我们在这里主要采用的是寻找右子键中最小值来交换自己的位置,此时待删除结点就从树的 中间部分被交换到了树的末端,从而删除右子键中的最小值,简化为删除最小值的问题。
  • 最后也要记得自下而上整理整个树的结构,满足我们的等价定义。
    删除B的简易示意图
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!