左倾红黑树

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

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

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

1. 2-3查找树

2-3查找树
尽管我们可以使用不同的数据类型表达2节点和3节点并写出变换所需要的代码,但是这种直白的表示方法实现大多数的操作并不方便,因为需要处理的情况实在太多:我们需要维护两种类型的节点,将被查找的键和节点中的每个键进行比较,将链接和其他信息从一个节点复制到另一个节点,将节点从一种数据类型转变为另一种数据类型;

实现上面的操作不仅需要大量的代码,而且转换所产生的额外消耗可能会使算法比标准二叉查找树更慢;

平衡一棵树的初衷是为了消除最坏情况,但是我们希望这种保障所需的代码能越少越好,幸运的是,我们只需要一点点的代价就能用一种统一方式完成所有变换(红黑树)

2. 红黑二叉查找树

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

2.1 替换3-节点

红黑二叉查找树背后的思想是用标准的二叉查找树(完全由2节点构成)和一些额外的信息(替换3节点)来表示2-3树

我们将树中的链接分为两种类型:

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

所以,我们只需要将3-结点还原成由红链接(左倾的)连接的2个2-结点,即可去掉3-结点!而对于3-结点的子结点,则分配给两个2-结点来完成。

这样表示的优点是,我们无需修改就可以直接使用标准二叉查找树的get()方法,对于任意2-3树,只需要对节点进行转换就能立刻派生出一棵对应的二叉查找树

在23树中,我们定义了2结点和3结点,而真正使它平衡的原因在于3结点,它多存储了一个结点信息,使得新结点的插入能够停留,而此时,23树有了重新分配结点,动态改造树结构的能力。所以说,多一个状态对树的平衡起了决定性的作用。现在来看看红黑树,是否就能理解它为什么多了一个红色状态呢
在这里插入图片描述
红色加粗现表示了什么?它的意思就是说,结点a和结点b【看上去】是不可分割的一个整体。可以对比下23树,所以说,【a+红线+b】等于23树中的【3结点】

此时,对比下红黑树的结点图,你会发现在a和b之间做了一个标记【红色】,这模拟了23树中3结点的特性,因为我们知道,单纯的树在做插入时,如果对key没有分配额外的空间,即不是Key[] keys= new Key[2];的形式,那么新插入的结点无法停留,唯一的办法就是把它分配在它应有的位置,即普通二叉搜索树的做法。可如何表示就近相邻的两个结点看上去是一个【3结点】呢?好吧,最直观的就是在链接上做操作,标一个状态即可,然后对它做些约束就好了。这有两个好处,第一,它还是二叉树的形式,即之前的get()查找操作是兼容这种新结构的。其次,【结点的停留】可以用一个状态来表示,这就自然能够使得红黑树有权力去动态平衡。

2.2 等价定义

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

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

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

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

③ 如果去除颜色标记,它并不能算平衡树,把红色链接铺平来看,平衡二叉树是黑色平衡的,所以我们红黑树也是完美黑色平衡的,不计入红色为树的高度

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

2.3 结点构成及颜色表示

因为每个节点都只会有一条指向自己的链接(从他的父节点指向它),我们将链接的颜色保存在表示节点的Node数据类型的布尔变量color中,如果指向他的链接为红色,那么变量为true,否则为false, 我们约定空连接为黑色
在这里插入图片描述

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.4 旋转

在我们实现某些操作的时候可能会出现红色右链接或者两条连续的红链接,但在操作完成前这些情况都会被旋转修复
旋转操作会改变红链接的指向
旋转操作是红黑树动态平衡的真正核心,插入旋转操作,完全可以类比23树的向上分裂操作

旋转可以保证红黑树的两个重要性质:有序性和完美平衡性

① 左旋:可以将红链从右边移至左边。
它对应的方法接受一条指向红黑树中某个节点的链接为参数, 假设被指向的节点的右链接时红色的,这个方法会对树进行必要的调整并返回一个指向包含同一组键的子树且其左连接为红色的根节点链接,根据返回值重置父节点中的相应链接

这个方法多数用于删除操作和调整树的平衡,以满足我们的等价定义。
在这里插入图片描述
在这里插入图片描述

  • 旋转后需要重置父结点的链接。
  • 旋转后需要调整结点的大小,因为结点的高度变化了。
    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.5 插入

规定:插入的新结点的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 可以根据其他情况进行继续变换。
    在这里插入图片描述
    在这里插入图片描述
    你根据23树细品这个操作,是不是就是在上图,最初A和E是一个三节点,我要新插入S,那么S要置为红色(因为根据23树中可知,是先构成4节点,再进行分裂),好,那这个时候,AES就是一个四节点,我们要进行分裂,对于23树中就是把中间的节点提起来,这也就说明了为什么E要变为红色,因为提起啦放到上一层是变成3节点嘛
    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.的情况。
在这里插入图片描述
其实这里的右旋不就是把4节点(CBA)进行向上分裂么(根据23树你细品)

③ 新插入的键大小在两个键之间:

插入左子结点的右结点 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.6 删除 delete:

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

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

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

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

2.6.1 删除最小值 deleteMin

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

因为最小键一定是在根结点的左侧的左侧的左侧…所以,我们只要判断两种情况如果,最左侧的结点是3结点,那么可以直接删除,不影响树的平衡性;

如果删除的最小键是2结点,那就从它的亲兄弟那里借一个过来,合并成3结点,那如果亲兄弟也是2结点呢?没办法了,只能从父亲结点那里坑一个过来,但坑一个过来的结果是,我必须把近亲兄弟的唯一结点也得拿过来,这就变成了一个4结点,我靠问题复杂了。但没关系,它是临时4结点,此时我们可以删除那个想要的键了,一删不就回归到了3结点么,完美解决

当我们进行删除末结点的时候,让我们先回归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.6.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.6.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的简易示意图
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
左倾和右倾都是的变种,它们都是为了避免在插入元素时出现右旋操作而设计的。具体特点和区别如下: 左倾(Left-Leaning Red-Black Tree): 1. 根节点是色的。 2. 所有色节点都是向左倾斜的。 3. 任意一个节点的左子中的色节点个数不会超过右子色节点的个数。 4. 没有两个连续的色节点,即不存在色节点的父节点为色节点。 5. 插入操作只需要进行左旋转,不需要进行右旋转。 右倾(Right-Leaning Red-Black Tree): 1. 根节点是色的。 2. 所有色节点都是向右倾斜的。 3. 任意一个节点的右子中的色节点个数不会超过左子色节点的个数。 4. 没有两个连续的色节点,即不存在色节点的父节点为色节点。 5. 插入操作只需要进行右旋转,不需要进行左旋转。 两者区别: 左倾和右倾除了色节点的倾斜方向不同外,其它特点基本相同。它们的区别主要在于插入操作时需要进行的旋转方式不同,这也决定了它们在一些场景下的性能表现不同。一般来说,左倾更适合进行插入操作,因为插入时只需要进行左旋转,而左旋转操作是比较轻量级的;而右倾更适合进行查找操作,因为查找时只需要进行右旋转,而右旋转操作也比较轻量级。但是,具体应用场景还需要根据实际情况来决定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值