死磕 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;
        modCount++;
        return null;
    }
    // key比较的结果
    int cmp;
    // 用来寻找待插入节点的父节点
    Entry<K,V> parent;
    // 根据是否有comparator使用不同的分支
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        // 如果使用的是comparator方式,key值可以为null,只要在comparator.compare()中允许即可
        // 从根节点开始遍历寻找
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                // 如果小于0从左子树寻找
                t = t.left;
            else if (cmp > 0)
                // 如果大于0从右子树寻找
                t = t.right;
            else
                // 如果等于0,说明插入的节点已经存在了,直接更换其value值并返回旧值
                return t.setValue(value);
        } while (t != null);
    }
    else {
        // 如果使用的是Comparable方式,key不能为null
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        // 从根节点开始遍历寻找
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                // 如果小于0从左子树寻找
                t = t.left;
            else if (cmp > 0)
                // 如果大于0从右子树寻找
                t = t.right;
            else
                // 如果等于0,说明插入的节点已经存在了,直接更换其value值并返回旧值
                return t.setValue(value);
        } while (t != null);
    }
    // 如果没找到,那么新建一个节点,并插入到树中
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        // 如果小于0插入到左子节点
        parent.left = e;
    else
        // 如果大于0插入到右子节点
        parent.right = e;

    // 插入之后的平衡
    fixAfterInsertion(e);
    // 元素个数加1(不需要扩容)
    size++;
    // 修改次数加1
    modCount++;
    // 如果插入了新节点返回空
    return null;
}

插入再平衡

插入的元素默认都是红色,因为插入红色元素只违背了第4条特性,那么我们只要根据这个特性来平衡就容易多了。

根据不同的情况有以下几种处理方式:

  1. 插入的元素如果是根节点,则直接涂成黑色即可,不用平衡;
  2. 插入的元素的父节点如果为黑色,不需要平衡;
  3. 插入的元素的父节点如果为红色,则违背了特性4,需要平衡,平衡时又分成下面三种情况:

(如果父节点是祖父节点的左节点)

情况 策略
1)父节点为红色,叔叔节点也为红色 (1)将父节点设为黑色;<br>(2)将叔叔节点设为黑色;<br>(3)将祖父节点设为红色;<br>(4)将祖父节点设为新的当前节点,进入下一次循环判断;
2)父节点为红色,叔叔节点为黑色,且当前节点是其父节点的右节点 (1)将父节点作为新的当前节点;<br>(2)以新当节点为支点进行左旋,进入情况3);
3)父节点为红色,叔叔节点为黑色,且当前节点是其父节点的左节点 (1)将父节点设为黑色;<br>(2)将祖父节点设为红色;<br>(3)以祖父节点为支点进行右旋,进入下一次循环判断;

(如果父节点是祖父节点的右节点,则正好与上面反过来)

情况 策略
1)父节点为红色,叔叔节点也为红色 (1)将父节点设为黑色;<br>(2)将叔叔节点设为黑色;<br>(3)将祖父节点设为红色;<br>(4)将祖父节点设为新的当前节点,进入下一次循环判断;
2)父节点为红色,叔叔节点为黑色,且当前节点是其父节点的左节点 (1)将父节点作为新的当前节点;<br>(2)以新当节点为支点进行右旋;
3)父节点为红色,叔叔节点为黑色,且当前节点是其父节点的右节点 (1)将父节点设为黑色;<br>(2)将祖父节点设为红色;<br>(3)以祖父节点为支点进行左旋,进入下一次循环判断;

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

/**
 * 插入再平衡
 *(1)每个节点或者是黑色,或者是红色。
 *(2)根节点是黑色。
 *(3)每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)
 *(4)如果一个节点是红色的,则它的子节点必须是黑色的。
 *(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
 */
private void fixAfterInsertion(Entry<K,V> x) {
    // 插入的节点为红节点,x为当前节点
    x.color = RED;

    // 只有当插入节点不是根节点且其父节点为红色时才需要平衡(违背了特性4)
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            // a)如果父节点是祖父节点的左节点
            // y为叔叔节点
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                // 情况1)如果叔叔节点为红色
                // (1)将父节点设为黑色
                setColor(parentOf(x), BLACK);
                // (2)将叔叔节点设为黑色
                setColor(y, BLACK);
                // (3)将祖父节点设为红色
                setColor(parentOf(parentOf(x)), RED);
                // (4)将祖父节点设为新的当前节点
                x = parentOf(parentOf(x));
            } else {
                // 如果叔叔节点为黑色
                // 情况2)如果当前节点为其父节点的右节点
                if (x == rightOf(parentOf(x))) {
                    // (1)将父节点设为当前节点
                    x = parentOf(x);
                    // (2)以新当前节点左旋
                    rotateLeft(x);
                }
                // 情况3)如果当前节点为其父节点的左节点(如果是情况2)则左旋之后新当前节点正好为其父节点的左节点了)
                // (1)将父节点设为黑色
                setColor(parentOf(x), BLACK);
                // (2)将祖父节点设为红色
                setColor(parentOf(parentOf(x)), RED);
                // (3)以祖父节点为支点进行右旋
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            // b)如果父节点是祖父节点的右节点
            // y是叔叔节点
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                // 情况1)如果叔叔节点为红色
                // (1)将父节点设为黑色
                setColor(parentOf(x), BLACK);
                // (2)将叔叔节点设为黑色
                setColor(y, BLACK);
                // (3)将祖父节点设为红色
                setColor(parentOf(parentOf(x)), RED);
                // (4)将祖父节点设为新的当前节点
                x = parentOf(parentOf(x));
            } else {
                // 如果叔叔节点为黑色
                // 情况2)如果当前节点为其父节点的左节点
                if (x == leftOf(parentOf(x))) {
                    // (1)将父节点设为当前节点
                    x = parentOf(x);
                    // (2)以新当前节点右旋
                    rotateRight(x);
                }
                // 情况3)如果当前节点为其父节点的右节点(如果是情况2)则右旋之后新当前节点正好为其父节点的右节点了)
                // (1)将父节点设为黑色
                setColor(parentOf(x), BLACK);
                // (2)将祖父节点设为红色
                setColor(parentOf(parentOf(x)), RED);
                // (3)以祖父节点为支点进行左旋
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    // 平衡完成后将根节点设为黑色
    root.color = BLACK;
}

插入元素举例

我们依次向红黑树中插入 4、2、3 三个元素,来一起看看整个红黑树平衡的过程。

三个元素都插入完成后,符合父节点是祖父节点的左节点,叔叔节点为黑色,且当前节点是其父节点的右节点,即情况2)。

情况2)需要做以下两步处理:

(1)将父节点作为新的当前节点;

(2)以新当节点为支点进行左旋,进入情况3);

情况3)需要做以下三步处理:

(1)将父节点设为黑色;

(2)将祖父节点设为红色;

(3)以祖父节点为支点进行右旋,进入下一次循环判断;

下一次循环不符合父节点为红色了,退出循环,插入再平衡完成。



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



原文地址:https://blog.51cto.com/14267003/2374447

时间: 2024-11-03 18:20:37

死磕 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源码分析(三)- 内含红黑树分析全过程

欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 删除元素 删除元素本身比较简单,就是采用二叉树的删除规则. (1)如果删除的位置有两个叶子节点,则从其右子树中取最小的元素放到删除的位置,然后把删除位置移到替代元素的位置,进入下一步. (2)如果删除的位置只有一个叶子节点(有可能是经过第一步转换后的删除位置),则把那个叶子节点作为替代元素,放到删除的位置,然后把这个叶子节点删除. (3)如果删除的位置没有叶子节点,则直接把这个删除位置的元素删除即可.

死磕 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