Java Collections Framework source code analysis (5.3-TreeMap, deletion of red and black trees)

This article is the last of the source code analysis of TreeMap and red-black tree. This time, we will combine the source code of TreeMap to teach you the algorithm of red-black tree deletion node. The red-black tree deletion algorithm is more complicated than insertion, but don't worry, this article will use a simple and clear explanation, combined with the JDK source code to let you understand the red-black tree deletion algorithm.

Before the text begins, please make sure you understand the knowledge points described in the previous two articles. If you forget something, you may wish to review it quickly again.

Java Collections Framework source code analysis (5.1-Map, TreeMap, Red Black Tree)

Java Collections Framework source code analysis (5.2-TreeMap, insertion of red and black trees)

TreeMap remove method

MapOn the removemethod of action is to remove the key from the inside of the container, we look at TreeMapthe implementation:

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

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}
复制代码

By getEntryacquiring a corresponding method from the red-black tree Entryobject if the corresponding key is Entrynot present directly returns null. Otherwise it will execute deleteEntrythe method, and returns the old value. Let us continue to look down deleteEntryimplementation of the method.

deleteEntry

From deleteEntrythe code point of view divided into two parts:

  1. Remove nodes from the red-black tree (balanced binary tree).
  2. Re-adjust the state of the tree to restore balance.

Let us first understand the algorithm for deleting nodes in a balanced binary tree. The specific algorithm is actually very simple and needs to distinguish between two different states. Let ’s talk about it simply. If the currently deleted node has only one non-empty child node, you only need to delete it directly, that is, establish a connection between your child node and the parent node. When you have two non-empty child nodes, you need to delete them in the following order:

  1. 找到当前删除节点的的 前驱节点或是 后继节点(前驱与后继的概念稍后会说)。
  2. 前驱(或是 后继)节点的值复制给需要删除的节点
  3. 按照第一种情况删除那个 前驱(或是 后继)节点

在详细解释之前,我们先说前驱后继的概念。因为平衡二叉树实质上是一种排序的数据结构,如果把它拉成一条直线,其实就是一个链表。而前驱的意思就是小于且最接近当前节点的节点,相应的后继就是大于且最接近的节点,具体可以看下面的图:

假设图中40(红色)的节点为当前节点,那么35(蓝色)节点为前驱节点45(绿色)节点为后继节点

了解了这些背景知识之后,可以看一下 deleteEntry 的代码了。

// If strictly internal, copy successor's element to p and then make p
// point to successor.
if (p.left != null && p.right != null) {
    Entry<K,V> s = successor(p);
    p.key = s.key;
    p.value = s.value;
    p = s;
// p has 2 children
复制代码

这部分代码按照我们之前所说的算法,先执行了有两个非空子节点情况下的逻辑,同时这里选择的是使用 successor 后继节点进行替换。很容易看出在使用 successor 获得当前节点的后继节点后,将后继节点的值复制给了当前节点,然后将需要删除节点的引用指向了后继节点。

successor 方法我就不在解释了,我建议你可以去看一下,看看是否符合我们之前的定义。相应的,在 TreeMap 的源码中还有一个 predecessor 方法是获取当前节点前驱节点,也值得看一下。

然后我们接着往下看:

// Start fixup at replacement node, if it exists.
Entry<K,V> 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);
}
复制代码

这里是进行节点删除的具体代码,此时需要删除的 P 节点有可能已经是指向后继节点了,但是无论如何,删除的逻辑都是一样的,都是重新建立父节点与子节点之间的关联,并移除与要删除节点间的关联。至此第一步删除节点的操作已经完成了,接下来就是要对树进行重新平衡,以符合红黑树的要求。

fixAfterDeletion

从上面代码片段中可以看出,在进行节点删除之后,调用了 fixAfterDeletion 方法,还记得上一篇中有个类似的 fixAfterInsertion 吗?不难猜出,这个 fixAfterDeletion 就是在删除节点后对二叉树重新平衡的方法,让我们先参考 wiki 上的算法定义。

相对插入而言删除的算法稍微更复杂些,需要执行 6 步操作。但也不用慌,耐心往下看(算法描述依然采用之前的 N,P 等缩写)。

  1. N 是否为根节点,如果是,那么就直接终止,否则执行第 2 步操作。
  2. S 节点,如果 S 节点颜色为红色:
    1. P 改为红色
    2. S 改为黑色
    3. N 是否为 P 的左节点
      1. 是:将 P 左转
      2. 否:将 P 右转 执行第 3 步操作
  3. P,S,S.left,S.right 这些节点的颜色都为黑色:
    1. S 改为红色
    2. 将 P 作为参数,从第 1 步开始重新执行 否则执行第 4 步
  4. P 为红色,S,S.left,S.right 都为黑色:
    1. S 改为红色
    2. P 改为黑色
    3. 终止操作 否则执行第 5 步
  5. S 的颜色为黑色
    1. N 为 P 的左节点,S.right 为黑色,S.left 为红色
      1. S 改为红色
      2. S.left 改为黑色
      3. 将 S 右转
    2. N 为 P 的右节点,S.right 为红色,S.left 为黑色
      1. S 改为红色
      2. S.right 改为黑色
      3. 将 S 左转 执行第 6 步操作
  6. 将 S 的颜色改为 P 的颜色 P 改为黑色
    1. N 为 P 的左节点
      1. S.right 改为黑色
      2. 将 P 左转
    2. N 为 P 的右节点
      1. S.left 改为黑色
      2. 将 P 右转

上手一看算法逻辑可能很繁琐,其实仔细看一下,很多都是对称的,你只需要记住一半就行了。现在结合 TreeMap 的代码看一下:

private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Entry<K,V> 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;
            }
        }
......
复制代码

可以看到 fixAfterDeletetion 方法的逻辑与我们描述的算法在顺序上稍有不同,它在一开始的分支条件是区分当前节点是左节点还是右节点。紧接着的:

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

这部分可以看到对应我们算法的第 2 步。需要注意的是 sib = rightOf(parentOf(x));,那是因为发生旋转后,sib 也发生了变化,需要重新获取。接下来的:

if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            }
复制代码

也能看到是对应算法的第 3 步,将 x 设为了 parent,然后重新开始 while 循环。之后的分支也都可以对应到算法的剩余步骤,我把这部分代码解读的工作流给你了,不妨自己拿笔和纸出来,将代码和我所描述的算法一一对应,看看能不能对上。

结语

至此,TreeMap 和红黑树的所有代码都分析完了。最后这篇节点删除算法拖了很久,期间有人也私信问我,为什么要学习红黑树?为什么要学习数据结构?我们工作中就是 CRUD ,什么排序,什么查找,都是用现成的呀,有问题吗?这是个很有趣的问题,在我工作过程中有很多人问过我类似的问题,可以把数据结构和红黑树替换成其他的许多名词,例如操作系统,编译原理,JVM 等等,等等。我想后面会花时间用一篇单独的文章来回答这个,而在这里我只想说对于这些底层知识的掌握,决定了你能力的上限,换而言之也决定了你做什么和不能做什么 。

接下来应该是 Java Collections Framework 最后一部分了,也就是 HashMap 的源码解析,希望你不要错过。

欢迎关注我的微信号「且把金针度与人」,获取更多高质量文章

本文使用 mdnice 排版

Guess you like

Origin juejin.im/post/5e95cbdb518825738f2b299f