死磕 java集合之TreeMap源码分析(三)- 内含红黑树分析全过程

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。


删除元素

删除元素本身比较简单,就是采用二叉树的删除规则。

(1)如果删除的位置有两个叶子节点,则从其右子树中取最小的元素放到删除的位置,然后把删除位置移到替代元素的位置,进入下一步。

(2)如果删除的位置只有一个叶子节点(有可能是经过第一步转换后的删除位置),则把那个叶子节点作为替代元素,放到删除的位置,然后把这个叶子节点删除。

(3)如果删除的位置没有叶子节点,则直接把这个删除位置的元素删除即可。

(4)针对红黑树,如果删除位置是黑色节点,还需要做再平衡。

(5)如果有替代元素,则以替代元素作为当前节点进入再平衡。

(6)如果没有替代元素,则以删除的位置的元素作为当前节点进入再平衡,平衡之后再删除这个节点。

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

    V oldValue = p.value;
    // 删除节点
    deleteEntry(p);
    // 返回删除的value
    return oldValue;
}

private void deleteEntry(Entry<K,V> p) {
    // 修改次数加1
    modCount++;
    // 元素个数减1
    size--;

    if (p.left != null && p.right != null) {
        // 如果当前节点既有左子节点,又有右子节点
        // 取其右子树中最小的节点
        Entry<K,V> s = successor(p);
        // 用右子树中最小节点的值替换当前节点的值
        p.key = s.key;
        p.value = s.value;
        // 把右子树中最小节点设为当前节点
        p = s;
        // 这种情况实际上并没有删除p节点,而是把p节点的值改了,实际删除的是p的后继节点
    }

    // 如果原来的当前节点(p)有2个子节点,则当前节点已经变成原来p的右子树中的最小节点了,也就是说其没有左子节点了
    // 到这一步,p肯定只有一个子节点了
    // 如果当前节点有子节点,则用子节点替换当前节点
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    if (replacement != null) {
        // 把替换节点直接放到当前节点的位置上(相当于删除了p,并把替换节点移动过来了)
        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;

        // 将p的各项属性都设为空
        p.left = p.right = p.parent = null;

        // 如果p是黑节点,则需要再平衡
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) {
        // 如果当前节点就是根节点,则直接将根节点设为空即可
        root = null;
    } else {
        // 如果当前节点没有子节点且其为黑节点,则把自己当作虚拟的替换节点进行再平衡
        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;
        }
    }
}

删除再平衡

经过上面的处理,真正删除的肯定是黑色节点才会进入到再平衡阶段。

因为删除的是黑色节点,导致整颗树不平衡了,所以这里我们假设把删除的黑色赋予当前节点,这样当前节点除了它自已的颜色还多了一个黑色,那么:

(1)如果当前节点是根节点,则直接涂黑即可,不需要再平衡;

(2)如果当前节点是红+黑节点,则直接涂黑即可,不需要平衡;

(3)如果当前节点是黑+黑节点,则我们只要通过旋转把这个多出来的黑色不断的向上传递到一个红色节点即可,这又可能会出现以下四种情况:

(假设当前节点为父节点的左子节点)

情况 策略
1)x是黑+黑节点,x的兄弟是红节点 (1)将兄弟节点设为黑色;
(2)将父节点设为红色;
(3)以父节点为支点进行左旋;
(4)重新设置x的兄弟节点,进入下一步;
2)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的两个子节点都是黑色 (1)将兄弟节点设置为红色;
(2)将x的父节点作为新的当前节点,进入下一次循环;
3)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的右子节点为黑色,左子节点为红色 (1)将兄弟节点的左子节点设为黑色;
(2)将兄弟节点设为红色;
(3)以兄弟节点为支点进行右旋;
(4)重新设置x的兄弟节点,进入下一步;
3)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的右子节点为红色,左子节点任意颜色 (1)将兄弟节点的颜色设为父节点的颜色;
(2)将父节点设为黑色;
(3)将兄弟节点的右子节点设为黑色;
(4)以父节点为支点进行左旋;
(5)将root作为新的当前节点(退出循环);

(假设当前节点为父节点的右子节点,正好反过来)

情况 策略
1)x是黑+黑节点,x的兄弟是红节点 (1)将兄弟节点设为黑色;
(2)将父节点设为红色;
(3)以父节点为支点进行右旋;
(4)重新设置x的兄弟节点,进入下一步;
2)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的两个子节点都是黑色 (1)将兄弟节点设置为红色;
(2)将x的父节点作为新的当前节点,进入下一次循环;
3)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的左子节点为黑色,右子节点为红色 (1)将兄弟节点的右子节点设为黑色;
(2)将兄弟节点设为红色;
(3)以兄弟节点为支点进行左旋;
(4)重新设置x的兄弟节点,进入下一步;
3)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的左子节点为红色,右子节点任意颜色 (1)将兄弟节点的颜色设为父节点的颜色;
(2)将父节点设为黑色;
(3)将兄弟节点的左子节点设为黑色;
(4)以父节点为支点进行右旋;
(5)将root作为新的当前节点(退出循环);

让我们来看看TreeMap中的实现:

/**
 * 删除再平衡
 *(1)每个节点或者是黑色,或者是红色。
 *(2)根节点是黑色。
 *(3)每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)
 *(4)如果一个节点是红色的,则它的子节点必须是黑色的。
 *(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
 */
private void fixAfterDeletion(Entry<K,V> x) {
    // 只有当前节点不是根节点且当前节点是黑色时才进入循环
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            // 如果当前节点是其父节点的左子节点
            // sib是当前节点的兄弟节点
            Entry<K,V> sib = rightOf(parentOf(x));

            // 情况1)如果兄弟节点是红色
            if (colorOf(sib) == RED) {
                // (1)将兄弟节点设为黑色
                setColor(sib, BLACK);
                // (2)将父节点设为红色
                setColor(parentOf(x), RED);
                // (3)以父节点为支点进行左旋
                rotateLeft(parentOf(x));
                // (4)重新设置x的兄弟节点,进入下一步
                sib = rightOf(parentOf(x));
            }

            if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                // 情况2)如果兄弟节点的两个子节点都是黑色
                // (1)将兄弟节点设置为红色
                setColor(sib, RED);
                // (2)将x的父节点作为新的当前节点,进入下一次循环
                x = parentOf(x);
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    // 情况3)如果兄弟节点的右子节点为黑色
                    // (1)将兄弟节点的左子节点设为黑色
                    setColor(leftOf(sib), BLACK);
                    // (2)将兄弟节点设为红色
                    setColor(sib, RED);
                    // (3)以兄弟节点为支点进行右旋
                    rotateRight(sib);
                    // (4)重新设置x的兄弟节点
                    sib = rightOf(parentOf(x));
                }
                // 情况4)
                // (1)将兄弟节点的颜色设为父节点的颜色
                setColor(sib, colorOf(parentOf(x)));
                // (2)将父节点设为黑色
                setColor(parentOf(x), BLACK);
                // (3)将兄弟节点的右子节点设为黑色
                setColor(rightOf(sib), BLACK);
                // (4)以父节点为支点进行左旋
                rotateLeft(parentOf(x));
                // (5)将root作为新的当前节点(退出循环)
                x = root;
            }
        } else { // symmetric
            // 如果当前节点是其父节点的右子节点
            // sib是当前节点的兄弟节点
            Entry<K,V> sib = leftOf(parentOf(x));

            // 情况1)如果兄弟节点是红色
            if (colorOf(sib) == RED) {
                // (1)将兄弟节点设为黑色
                setColor(sib, BLACK);
                // (2)将父节点设为红色
                setColor(parentOf(x), RED);
                // (3)以父节点为支点进行右旋
                rotateRight(parentOf(x));
                // (4)重新设置x的兄弟节点
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                    colorOf(leftOf(sib)) == BLACK) {
                // 情况2)如果兄弟节点的两个子节点都是黑色
                // (1)将兄弟节点设置为红色
                setColor(sib, RED);
                // (2)将x的父节点作为新的当前节点,进入下一次循环
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    // 情况3)如果兄弟节点的左子节点为黑色
                    // (1)将兄弟节点的右子节点设为黑色
                    setColor(rightOf(sib), BLACK);
                    // (2)将兄弟节点设为红色
                    setColor(sib, RED);
                    // (3)以兄弟节点为支点进行左旋
                    rotateLeft(sib);
                    // (4)重新设置x的兄弟节点
                    sib = leftOf(parentOf(x));
                }
                // 情况4)
                // (1)将兄弟节点的颜色设为父节点的颜色
                setColor(sib, colorOf(parentOf(x)));
                // (2)将父节点设为黑色
                setColor(parentOf(x), BLACK);
                // (3)将兄弟节点的左子节点设为黑色
                setColor(leftOf(sib), BLACK);
                // (4)以父节点为支点进行右旋
                rotateRight(parentOf(x));
                // (5)将root作为新的当前节点(退出循环)
                x = root;
            }
        }
    }

    // 退出条件为多出来的黑色向上传递到了根节点或者红节点
    // 则将x设为黑色即可满足红黑树规则
    setColor(x, BLACK);
}

删除元素举例

假设我们有下面这样一颗红黑树。

我们删除6号元素,则从右子树中找到了最小元素7,7又没有子节点了,所以把7作为当前节点进行再平衡。

我们看到7是黑节点,且其兄弟为黑节点,且其兄弟的两个子节点都是红色,满足情况4),平衡之后如下图所示。

我们再删除7号元素,则从右子树中找到了最小元素8,8有子节点且为黑色,所以8的子节点9是替代节点,以9为当前节点进行再平衡。

我们发现9是红节点,则直接把它涂成黑色即满足了红黑树的特性,不需要再过多的平衡了。

这次我们来个狠的,把根节点删除,从右子树中找到了最小的元素5,5没有子节点,所以把5作为当前节点进行再平衡。

我们看到5是黑节点,且其兄弟为红色,符合情况1),平衡之后如下图所示,然后进入情况2)。

对情况2)进行再平衡后如下图所示。

然后进入下一次循环,发现不符合循环条件了,直接把x涂为黑色即可,退出这个方法之后会把旧x删除掉(见deleteEntry()方法),最后的结果就是下面这样。



未完待续,下一节我们一起探讨红黑树遍历元素的操作。

现在公众号文章没办法留言了,如果有什么疑问或者建议请直接在公众号给我留言。



欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

原文地址:https://www.cnblogs.com/tong-yuan/p/10657620.html

时间: 2024-10-05 04:09:17

死磕 java集合之TreeMap源码分析(三)- 内含红黑树分析全过程的相关文章

死磕 java集合之TreeMap源码分析(一)

欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 TreeMap使用红黑树存储元素,可以保证元素按key值的大小进行遍历. 继承体系 TreeMap实现了Map.SortedMap.NavigableMap.Cloneable.Serializable等接口. SortedMap规定了元素可以按key的大小来遍历,它定义了一些返回部分map的方法. public interface SortedMap<K,V> extends Ma

死磕 java集合之TreeMap源码分析(四)-内含彩蛋

欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 二叉树的遍历 我们知道二叉查找树的遍历有前序遍历.中序遍历.后序遍历. (1)前序遍历,先遍历我,再遍历我的左子节点,最后遍历我的右子节点: (2)中序遍历,先遍历我的左子节点,再遍历我,最后遍历我的右子节点: (3)后序遍历,先遍历我的左子节点,再遍历我的右子节点,最后遍历我: 这里的前中后都是以"我"的顺序为准的,我在前就是前序遍历,我在中就是中序遍历,我在后就是后序遍历. 下

死磕 java集合之TreeMap源码分析(二)

插入元素 插入元素,如果元素在树中存在,则替换value:如果元素不存在,则插入到对应的位置,再平衡树. public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { // 如果没有根节点,直接插入到根节点 compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1;

死磕 java集合之ConcurrentHashMap源码分析(三)

本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样,都是先找到元素所在的桶,然后采用分段锁的思想锁住整个桶,再进行操作. public V remove(Object key) { // 调用替换节点方法 return replaceNode(key, null, null); } final V replaceNode(Object key, V

死磕 java集合之LinkedHashSet源码分析

问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashSet支持按元素访问顺序排序吗? 简介 上一节我们说HashSet中的元素是无序的,那么有没有什么办法保证Set中的元素是有序的呢? 答案是当然可以. 我们今天的主角LinkedHashSet就有这个功能,它是怎么实现有序的呢?让我们来一起学习吧. 源码分析 LinkedHashSet继承自HashS

死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)CopyOnWriteArraySet以何种方式保证元素不重复? (5)如何比较两个Set中的元素是否完全一致? 简介 CopyOnWriteArraySet底层是使用CopyOnWriteArrayList存储元素的,所以它并不是使用Map来存储元素的. 但是,我们知道CopyOnWriteArra

死磕 java集合之PriorityQueue源码分析

问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合,集合中的每个元素都有一个权重值,每次出队都弹出优先级最大或最小的元素. 一般来说,优先级队列使用堆来实现. 还记得堆的相关知识吗?链接直达[拜托,面试别再问我堆(排序)了!]. 那么Java里面是如何通过"堆"这个数据结构来实现优先级队列的呢? 让我们一起来学习吧. 源码分析 主要属性

死磕 java集合之PriorityBlockingQueue源码分析

问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的? 简介 PriorityBlockingQueue是java并发包下的优先级阻塞队列,它是线程安全的,如果让你来实现你会怎么实现它呢? 还记得我们前面介绍过的PriorityQueue吗?点击链接直达[死磕 java集合之PriorityQueue源码分析] 还记得优先级队列一般使用什么来实现吗?

死磕 java集合之DelayQueue源码分析

问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于实现定时任务. 继承体系 从继承体系可以看到,DelayQueue实现了BlockingQueue,所以它是一个阻塞队列. 另外,DelayQueue还组合了一个叫做Delayed的接口,DelayQueue中存储的所有元素必须实现Delayed接口. 那么,Delayed是什么呢? public