红黑树概念、红黑树的插入及旋转操作详细解读

红黑树的一些基本概念

在讲TreeMap前还是先说一下红黑树的一些基本概念,这样可以更好地理解之后TreeMap的源代码。

二叉查找树是在生成的时候是非常容易失衡的,造成的最坏情况就是一边倒(即只有左子树/右子树),这样会导致树检索的效率大大降低。(关于树和二叉查找树可以看我之前写的一篇文章树型结构)

红黑树是为了维护二叉查找树的平衡而产生的一种树,根据维基百科的定义,红黑树有五个特性,但我觉得讲得不太易懂,我自己总结一下,红黑树的特性大致有三个(换句话说,插入、删除节点后整个红黑树也必须满足下面的三个性质,如果不满足则必须进行旋转):

  1. 根节点与叶节点都是黑色节点,其中叶节点为Null节点
  2. 每个红色节点的两个子节点都是黑色节点,换句话说就是不能有连续两个红色节点
  3. 从根节点到所有叶子节点上的黑色节点数量是相同的

上述的性质约束了红黑树的关键:从根到叶子的最长可能路径不多于最短可能路径的两倍长。得到这个结论的理由是:

  1. 红黑树中最短的可能路径是全部为黑色节点的路径
  2. 红黑树中最长的可能路径是红黑相间的路径

此时(2)正好是(1)的两倍长。结果就是这个树大致上是平衡的,因为比如插入、删除和查找某个值这样的操作最坏情况都要求与树的高度成比例,这个高度的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树,最终保证了红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除

下面展示一张红黑树的实例图:

红黑树概念、红黑树的插入及旋转操作详细解读_第1张图片

可以看到根节点到所有NULL LEAF节点(即叶子节点)所经过的黑色节点都是2个。

另外从这张图上我们还能得到一个结论:红黑树并不是高度的平衡树。所谓平衡树指的是一棵空树或它的左右两个子树的高度差的绝对值不超过1,但是我们看:

  • 最左边的路径0026-->0017-->0012-->0010-->0003-->NULL LEAF,它的高度为5
  • 最后边的路径0026-->0041-->0047-->NULL LEAF,它的高度为3

左右子树的高度差值为2,因此红黑树并不是高度平衡的,它放弃了高度平衡的特性而只追求部分平衡,这种特性降低了插入、删除时对树旋转的要求,从而提升了树的整体性能。而其他平衡树比如AVL树虽然查找性能为性能是O(logn),但是为了维护其平衡特性,可能要在插入、删除操作时进行多次的旋转,产生比较大的消耗。

四个关注点在TreeMap上的答案

关 注 点 结  论
TreeMap是否允许键值对为空 Key不允许为空,Value允许为空 
TreeMap是否允许重复数据 Key重复会覆盖,Value允许重复 
TreeMap是否有序 按照Key的自然顺序排序或者Comparator接口指定的排序算法进行排序 
TreeMap是否线程安全  非线程安全

TreeMap基本数据结构

TreeMap基于红黑树实现,既然是红黑树,那么每个节点中除了Key-->Value映射之外,必然存储了红黑树节点特有的一些内容,它们是:

  1. 父节点引用
  2. 左子节点引用
  3. 右子节点引用
  4. 节点颜色

TreeMap的节点Java代码定义为:

static final class Entry implements Map.Entry {
        K key;
        V value;
        Entry left = null;
        Entry right = null;
        Entry parent;
        boolean color = BLACK;
        ...
}

由于颜色只有红色和黑色两种,因此颜色可以使用布尔类型(boolean)来表示,黑色表示为true,红色为false。

TreeMap添加数据流程总结

首先看一下TreeMap如何添加数据,测试代码为:

public class MapTest {

    @Test
    public void testTreeMap() {
        TreeMap treeMap = new TreeMap();
        treeMap.put(10, "10");
        treeMap.put(85, "85");
        treeMap.put(15, "15");
        treeMap.put(70, "70");
        treeMap.put(20, "20");
        treeMap.put(60, "60");
        treeMap.put(30, "30");
        treeMap.put(50, "50");

        for (Map.Entry entry : treeMap.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
    }
    
}

本文接下来的内容会给出插入每条数据之后红黑树的数据结构是什么样子的。首先看一下treeMap的put方法的代码实现:

public V put(K key, V value) {
    Entry t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry parent;
    // split comparator and comparable paths
    Comparator cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        Comparable k = (Comparable) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

从这段代码,先总结一下TreeMap添加数据的几个步骤:

  1. 获取根节点,根节点为空,产生一个根节点,将其着色为黑色,退出余下流程
  2. 获取比较器,如果传入的Comparator接口不为空,使用传入的Comparator接口实现类进行比较;如果传入的Comparator接口为空,将Key强转为Comparable接口进行比较
  3. 从根节点开始逐一依照规定的排序算法进行比较,取比较值cmp,如果cmp=0,表示插入的Key已存在;如果cmp>0,取当前节点的右子节点;如果cmp<0,取当前节点的左子节点
  4. 排除插入的Key已存在的情况,第(3)步的比较一直比较到当前节点t的左子节点或右子节点为null,此时t就是我们寻找到的节点,cmp>0则准备往t的右子节点插入新节点,cmp<0则准备往t的左子节点插入新节点
  5. new出一个新节点,默认为黑色,根据cmp的值向t的左边或者右边进行插入
  6. 插入之后进行修复,包括左旋、右旋、重新着色这些操作,让树保持平衡性

第1~第5步都没有什么问题,红黑树最核心的应当是第6步插入数据之后进行的修复工作,对应的Java代码是TreeMap中的fixAfterInsertion方法,下面看一下put每个数据之后TreeMap都做了什么操作,借此来理清TreeMap的实现原理。

put(10, "10")

首先是put(10, "10"),由于此时TreeMap中没有任何节点,因此10为根且根节点为黑色节点,put(10, "10")之后的数据结构为:

put(85, "85")

接着是put(85, "85"),这一步也不难,85比10大,因此在10的右节点上,但是由于85不是根节点,因此会执行fixAfterInsertion方法进行数据修正,看一下fixAfterInsertion方法代码实现:

private void fixAfterInsertion(Entry x) {
    x.color = RED;

    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            Entry y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}

我们看第2行的代码,它将默认的插入的那个节点着色成为红色,这很好理解:

根据红黑树的性质(3),红黑树要求从根节点到叶子所有叶子节点上经过的黑色节点个数是相同的,因此如果插入的节点着色为黑色,那必然有可能导致某条路径上的黑色节点数量大于其他路径上的黑色节点数量,因此默认插入的节点必须是红色的,以此来维持红黑树的性质(3)

当然插入节点着色为红色节点后,有可能导致的问题是违反性质(2),即出现连续两个红色节点,这就需要通过旋转操作去改变树的结构,解决这个问题。

接着看第4行的判断,前两个条件都满足,但是因为85这个节点的父节点是根节点的,根节点是黑色节点,因此这个条件不满足,while循环不进去,直接执行一次30行的代码给根节点着色为黑色(因为在旋转过程中有可能导致根节点为红色,而红黑树的根节点必须是黑色,因此最后不管根节点是不是黑色,都要重新着色确保根节点是黑色的)。

那么put(85, "85")之后,整个树的结构变为:

红黑树概念、红黑树的插入及旋转操作详细解读_第2张图片

fixAfterInsertion方法流程

在看put(15, "15")之前,必须要先过一下fixAfterInsertion方法。第5行~第21行的代码和第21行~第38行的代码是一样的,无非一个是操作左子树另一个是操作右子树而已,因此就看前一半:

while (x != null && x != root && x.parent.color == RED) {
    if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
        Entry y = rightOf(parentOf(parentOf(x)));
        if (colorOf(y) == RED) {
            setColor(parentOf(x), BLACK);
            setColor(y, BLACK);
            setColor(parentOf(parentOf(x)), RED);
            x = parentOf(parentOf(x));
        } else {
            if (x == rightOf(parentOf(x))) {
                x = parentOf(x);
                rotateLeft(x);
            }
            setColor(parentOf(x), BLACK);
            setColor(parentOf(parentOf(x)), RED);
            rotateRight(parentOf(parentOf(x)));
        }
    }
    ....
}

第2行的判断注意一下,用语言描述出来就是:判断当前节点的父节点与当前节点的父节点的父节点的左子节点是否同一个节点。翻译一下就是:当前节点是否左子节点插入,关于这个不明白的我就不解释了,可以自己多思考一下。对这整段代码我用流程图描述一下:

红黑树概念、红黑树的插入及旋转操作详细解读_第3张图片

这里有一个左子树内侧插入与左子树点外侧插入的概念,我用图表示一下:

红黑树概念、红黑树的插入及旋转操作详细解读_第4张图片

其中左边的是左子树外侧插入,右边的是左子树内侧插入,可以从上面的流程图上看到,对于这两种插入方式的处理是不同的,区别是后者也就是左子树内侧插入多一步左旋操作

能看出,红黑树的插入最多只需要进行两次旋转,至于红黑树的旋转,后面结合代码进行讲解。

put(15, "15")

看完fixAfterInsertion方法流程之后,继续添加数据,这次添加的是put(15, "15"),15比10大且比85小,因此15最终应当是85的左子节点,默认插入的是红色节点,因此首先将15作为红色节点插入85的左子节点后的结构应当是:

红黑树概念、红黑树的插入及旋转操作详细解读_第5张图片

但是显然这里违反了红黑树的性质(2),即连续出现了两个红色节点,因此此时必须进行旋转。回看前面fixAfterInsertion的流程,上面演示的是左子树插入流程,右子树一样,可以看到这是右子树内侧插入,需要进行两次旋转操作:

  1. 对新插入节点的父节点进行一次右旋操作
  2. 新插入节点的父节点着色为黑色,新插入节点的祖父节点着色为红色
  3. 对新插入节点的祖父节点进行一次左旋操作

旋转是红黑树中最难理解也是最核心的操作,右旋和左旋是对称的操作,我个人的理解,以右旋为例,对某个节点x进行右旋,其实质是:

  • 降低左子树的高度,增加右子树的高度
  • 将x变为当前位置的右子节点

左旋是同样的道理,在旋转的时候一定要记住这两句话,这将会帮助我们清楚地知道在不同的场景下旋转如何进行。

先看一下(1)也就是"对新插入节点的父节点进行一次右旋操作",源代码为rotateRight方法:

private void rotateRight(Entry p) {
    if (p != null) {
        Entry l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
           root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

右旋流程用流程图画一下其流程:

红黑树概念、红黑树的插入及旋转操作详细解读_第6张图片

再用一张示例图表示一下右旋各节点的变化,旋转不会改变节点颜色,这里就不区分红色节点和黑色节点了,a是需要进行右旋的节点:

红黑树概念、红黑树的插入及旋转操作详细解读_第7张图片

左旋与右旋是一个对称的操作,大家可以试试看把右图的b节点进行左旋,就变成了左图了。这里多说一句,旋转一定要说明是对哪个节点进行旋转,网上看很多文章讲左旋、右旋都是直接说旋转之后怎么样怎么样,我认为脱离具体的节点讲旋转是没有任何意义的。

这里可能会有的一个问题是:b有左右两个子节点分别为d和e,为什么右旋的时候要将右子节点e拿到a的左子节点而不是b的左子节点d?

一个很简单的解释是:如果将b的左子节点d拿到a的左子节点,那么b右旋后右子节点指向a,b原来的右子节点e就成为了一个游离的节点,游离于整个数据结构之外

回到实际的例子,对85这个节点进行右旋之后还有一次着色操作(2),分别是将x的父节点着色为黑色,将x的祖父节点着色为红色,那么此时的树形结构应当为:

红黑树概念、红黑树的插入及旋转操作详细解读_第8张图片

然后对节点10进行一次左旋操作(3),左旋之后的结构为:

红黑树概念、红黑树的插入及旋转操作详细解读_第9张图片

最后不管根节点是不是黑色,都将根节点着色为黑色,那么插入15之后的数据结构就变为了上图,满足红黑树的三条特性。

put(70, "70")

put(70, "70")就很简单了,70是85的左子节点,由于70的父节点以及叔父节点都是红色节点,因此直接将70的父节点85、将70的叔父节点10着色为黑色即可,70这个节点着色为红色,即满足红黑树的特性,插入70之后的结构图为:

红黑树概念、红黑树的插入及旋转操作详细解读_第10张图片

put(20, "20")

put(20, "20"),插入的位置应当是70的左子节点,默认插入红色,插入之后的结构图为:

红黑树概念、红黑树的插入及旋转操作详细解读_第11张图片

问题很明显,出现了连续两个红色节点,20的插入位置是一种左子树外侧插入的场景,因此只需要进行着色+对节点85进行一次右旋即可,着色+右旋之后数据结构变为:

红黑树概念、红黑树的插入及旋转操作详细解读_第12张图片

put(60, "60")

下面进行put(60, "60")操作,节点60插入的位置是节点20的右子节点,由于节点60的父节点与叔父节点都是红色节点,因此只需要将节点60的父节点与叔父节点着色为黑色,将节点60的组父节点着色为红色即可。

那么put(60, "60")之后的结构为:

红黑树概念、红黑树的插入及旋转操作详细解读_第13张图片

put(30, "30")

put(30, "30"),节点30应当为节点60的左子节点,因此插入节点30之后应该是这样的:

红黑树概念、红黑树的插入及旋转操作详细解读_第14张图片

显然这里违反了红黑树性质(2)即连续出现了两个红色节点,因此这里要进行旋转。

put(30, "30")的操作和put(15, "15")的操作类似,同样是右子树内侧插入的场景,那么需要进行两次旋转:

  1. 对节点30的父节点节点60进行一次右旋
  2. 右旋之后对节点60的祖父节点20进行一次左旋

右旋+着色+左旋之后,put(30, "30")的结果应当为:

红黑树概念、红黑树的插入及旋转操作详细解读_第15张图片

 

put(50, "50")

下一个操作是put(50, "50"),节点50是节点60的左子节点,由于节点50的父亲节点与叔父节点都是红色节点,因此只需要将节点50的父亲节点与叔父节点着色为黑色,将节点50的祖父节点着色为红色即可:

红黑树概念、红黑树的插入及旋转操作详细解读_第16张图片

节点50的父节点与叔父节点都是红色节点(注意不要被上图迷糊了!上图是重新着色之后的结构而不是重新着色之前的结构,重新着色之前的结构为上上图),因此插入节点50只需要进行着色,本身这样的操作是没有任何问题的,但问题的关键在于,着色之后出现了连续的红色节点,即节点30与节点70。这就是为什么fixAfterInsertion方法的方法体是while循环的原因:

private void fixAfterInsertion(Entry x) {
    x.color = RED;

    while (x != null && x != root && x.parent.color == RED) {
    ...
    }
}

因为这种着色方式是将插入节点的祖父节点着色为红色,因此着色之后必须将当前节点指向插入节点的祖父节点,判断祖父节点与父节点是否连续红色的节点,是就进行旋转,重新让红黑树平衡。

接下来的问题就是怎么旋转了。我们可以把节点15-->节点70-->节点30连起来看,是不是很熟悉?这就是上面重复了两次的右子树内侧插入的场景,那么首先对节点70进行右旋,右旋后的结果为:

红黑树概念、红黑树的插入及旋转操作详细解读_第17张图片

下一步,节点70的父节点着色为黑色,节点70的祖父节点着色为红色(这一步不理解或者忘了为什么的,可以去看一下之前对于fixAfterInsertion方法的解读),重新着色后的结构为:

红黑树概念、红黑树的插入及旋转操作详细解读_第18张图片

最后一步,对节点70的父节点节点15进行一次左旋,左旋之后的结构为:

红黑树概念、红黑树的插入及旋转操作详细解读_第19张图片

重新恢复红黑树的性质:

  1. 根节点为黑色节点
  2. 没有连续红色节点
  3. 根节点到所有叶子节点经过的黑色节点都是2个

----------------------------------------------

 

本文先研究一下红黑树的移除操作是如何实现的,移除操作比较复杂,具体移除的操作要进行几次旋转和移除的节点在红黑树中的位置有关,这里也不特意按照旋转次数选择节点了,就找三种位置举例演示红黑树移除操作如何进行:

  • 移除根节点,例子就是移除节点30
  • 移除中间节点,例子就是移除节点70
  • 移除最底下节点,例子就是移除节点85

首先来过一下TreeMap的remove方法:

public V remove(Object key) {
    Entry p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

第2行的代码是获取待移除的节点Entry,做法很简单,拿key与当前节点按指定算法做一个比较获取cmp,cmp=0表示当前节点就是待移除的节点;cmp>0,取右子节点继续比较;cmp<0,取左子节点继续比较。

接着重点跟一下第7行的deleteEntry方法:

private void deleteEntry(Entry p) {
    modCount++;
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    if (p.left != null && p.right != null) {
        Entry s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    Entry replacement = (p.left != null ? p.left : p.right);

    if (replacement != null) {
        // Link replacement to parent
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        p.left = p.right = p.parent = null;

        // Fix replacement
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        if (p.color == BLACK)
            fixAfterDeletion(p);

        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

用流程图整理一下这里的逻辑:

红黑树概念、红黑树的插入及旋转操作详细解读_第20张图片

下面结合实际代码来看下。

移除根节点

根据上面的流程图,根节点30左右子节点不为空,因此要先选择继承者,选择继承者的流程为:

红黑树概念、红黑树的插入及旋转操作详细解读_第21张图片

分点整理一下移除节点30做了什么:

  1. 由于节点30的右子节点不为空,因此从节点70开始不断取左子节点直到取到叶子节点为止,最终取到的节点s为节点50
  2. p的key=s的key即50,p的value=s的value即50,由于此时p指向的是root节点,因此root节点的key和value变化,变为50-->50
  3. p=s,即p原来指向的是root节点,现在p指向s节点,p与s指向同一份内存空间,即节点50
  4. 接着选择replacement,由于p与s指向同一份内存空间,因此replacement判断的是s是否有左子节点,显然s没有,因此replacement为空
  5. replacement为空,但是p有父节点,因此可以判断出来p也就是节点50不是root节点
  6. 接着根据流程图可知,节点p是一个红色节点,这里不需要进行移除数据修正
  7. 最后节点p是其父节点的左子节点,因此节点p的左子节点置为null,再将p的父节点置为null,相当于把节点p移除

经过上述流程,移除根节点30之后的数据结构如下图:

红黑树概念、红黑树的插入及旋转操作详细解读_第22张图片

 

移除中间节点

接着看一下移除中间节点TreeMap是怎么做的,这里以移除节点70为例,继续分点整理一下移除节点70做了什么:

  1. 节点70有左右子节点,因此还是选择继承者,由于节点70的右子节点85没有左子节点,因此选出来的继承者就是节点85
  2. p的key=s的key即85,p的value=s的value即85,此时p指向的是节点70,因此节点70的key与value都变为85
  3. key与value赋值完毕后执行p=s,此时p指向节点85
  4. 接着选择replacement,由于85没有左右子节点,因此replacement为null
  5. replacement为null且节点p即节点85有父节点,根据流程图可知,节点p是一个黑色节点,因此需要进行删除数据修正
  6. 最后节点p是其父节点的右子节点,因此节点p的右子节点置为null,再将p的父节点置为null,相当于把节点p移除

总体流程和移除根节点差不多,唯一的区别是节点85是一个黑色节点,因此需要进行一次删除数据修正操作。删除数据修正实现为fixAfterDeletion方法,它的源码:

private void fixAfterDeletion(Entry x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Entry sib = rightOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }

            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { // symmetric
            Entry sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}

方法第3行~第30行与第30行~第57行是对称的,因此只分析一下前半部分也就是第3行~第30行的代码。第三行的代码"x == leftOf(parentOf(x))"很显然判断的是x是否其父节点的左子节点。其流程图为:

红黑树概念、红黑树的插入及旋转操作详细解读_第23张图片

从上图中,首先可以得出一个重要的结论:红黑树移除节点最多需要三次旋转

先看一下删除数据修正之前的结构图:

红黑树概念、红黑树的插入及旋转操作详细解读_第24张图片

p指向右下角的黑色节点85,对此节点进行修正,上面的流程图是p是父节点的左子节点的流程,这里的p是父节点的右子节点,没太大区别。

sib取父节点的左子节点即节点60,节点60是一个黑色节点,因此这里不需要进行一次旋转。

接着,sib的左右子节点不是黑色节点且sib的左子节点为红色节点,因此这里只需要进行一次旋转的流程:

  1. 将sib着色为它父节点的颜色
  2. p的父节点着色为黑色
  3. sib的左子节点着色为黑色
  4. p的父节点右旋

经过这样四步操作之后,红黑树的结构变为:

红黑树概念、红黑树的插入及旋转操作详细解读_第25张图片

最后一步的操作在fixAfterDeletion方法的外层,节点85的父节点不为空,因此将节点85的父节点置空,最终移除节点70之后的数据结构为:

红黑树概念、红黑树的插入及旋转操作详细解读_第26张图片

 

移除最底下节点

最后看看移除最底下节点的场景,以移除节点85为例,节点85根据代码以节点p称呼。

节点p没有左右子节点,因此节点p不需要进行选择继承者的操作;同样的由于节点p没有左右子节点,因此选择出来的replacement为null。

接着由于replacement为null但是节点p是一个黑色节点,黑色节点需要进行删除修正流程:

  1. 节点p是父节点的右子节点,那么节点sib为父节点的左子节点50
  2. sib是黑色的,因此不需要进行一次右旋
  3. sib的左子节点是红色的,因此这里需要进行的操作是将sib着色为p父节点的颜色红色、将p的父节点着色为黑色、将sib的左子节点着色为黑色、将p的父节点进行一次右旋

这么做之后,树形结构变为:

红黑树概念、红黑树的插入及旋转操作详细解读_第27张图片

最后还是一样,回到fixAfterDeletion方法外层的代码,将p的父节点置为null,即节点p就不在当前数据结构中了,完成移除,红黑树最终的结构为:

红黑树概念、红黑树的插入及旋转操作详细解读_第28张图片

 

你可能感兴趣的:(Java,集合)