Java集合系列源码分析(八)-- TreeMap(红黑树)

本文深入解析Java中TreeMap的数据结构与实现原理,包括红黑树的特性、插入、删除、查询操作及源码分析。TreeMap通过红黑树提供高效的键值对存储,支持自定义比较器,确保数据有序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、TreeMap

TreeMap是有序的,可以实现自定义的比较器来决定升序还是降序。存储key-value,内部使用红黑树。

强烈建议先看之前的二叉搜索树和平衡二叉树,了解二叉搜索树和平衡二叉树再看这篇,否则红黑树有点难理解

二、TreeMap常用方法

2.1 属性

在这里插入图片描述
在这里插入图片描述
实现了NavigableMap接口,
在这里插入图片描述
而NavigableMap又继承于SortedMap,又此可知,这个map是有序的。因为大部分加入的都是对象,所以一般都要实现一个比较器来进行排序。

而NavigableMap会有一些导航方法,比如返回最接近的大于等于key的值,返回最接近的key的值等等

剩下的Cloneable和序列化接口就不解释了

2.2 属性

   //定制排序规则,为null则是自然排序
   private final Comparator<? super K> comparator;
   //红黑树的根节点
    private transient Entry<K,V> root;

    /**
     * 红黑树的节点数量
     */
    private transient int size = 0;

    /**
     * 修改次数
     */
    private transient int modCount = 0;

而节点的属性如下:

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    //左孩子节点
    Entry<K,V> left;
    //右孩子节点
    Entry<K,V> right;
    //父节点
    Entry<K,V> parent;
    //红黑表示节点的颜色属性,默认黑色
    boolean color = BLACK;

    /**
     * 给父节点构造一个子节点,默认黑色
     */
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }

    /**
     * Returns the key.
     *
     * @return the key
     */
    public K getKey() {
        return key;
    }

    /**
     * Returns the value associated with the key.
     *
     * @return the value associated with the key
     */
    public V getValue() {
        return value;
    }

    /**
     * Replaces the value currently associated with the key with the given
     * value.
     *
     * @return the value associated with the key before this method was
     *         called
     */
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    public String toString() {
        return key + "=" + value;
    }
}

保存了键值,以及其他节点的引用信息,还有默认颜色为黑色

2.3 红黑树的介绍

红黑树和平衡二叉树非常相似,但是平衡二叉树的条件非常苛刻,而起插入后需要旋转的次数是未知。而红黑树追求大致平衡,时间复杂度与平衡二叉树相近的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现比较简单。

红黑树除了二叉查找树的一般要求外,还需要以下要求:

  1. 节点是红色或黑色
  2. 根节点是黑色
  3. 每个叶子节点都是黑色的空节点(NIL节点)
  4. 每个红色节点的两个子节点都是黑色(从每个叶子到跟的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

这些约束强制了红黑树的关键性质:从跟到叶子的最长的可能路径不多于最短的可能路径的两倍长

如下所示就是一颗红黑树:
在这里插入图片描述
为了保持红黑树的以上特性,红黑树在加入元素时会有三个行动来保持自己:
左旋、右旋、着色

例如:加入元素14,破坏了红黑树的严格要求
在这里插入图片描述
注意刚加入的元素是红色,破坏了红黑树的要求4,因此需要先进行变色。为什么新插入的节点不是黑色,看源码。。。源码中新加入的节点都是红色

在这里插入图片描述
对元素15进行颜色变化,红色变为黑色,但是元素15和16都是黑色,还是破坏了要求5

后面继续进行颜色变化,就不解释了,看图一步步

在这里插入图片描述
在这里插入图片描述
暂时完成了颜色转化,但是元素13和元素16仍然出现了要求的破坏

此时树的整体是这样的
在这里插入图片描述
此时变色已经无法改变了,因此需要进行左旋转
在这里插入图片描述
进行左旋转
在这里插入图片描述
但是红黑树要求根节点必须为黑色

于是对根节点进行变色
在这里插入图片描述
对左子树的节点进行多次变色

在这里插入图片描述
但是还不满足要求5,因此此时还要进行旋转
在这里插入图片描述
先左旋
在这里插入图片描述
在右旋

在这里插入图片描述
旋转后,再将左子树进行变色
在这里插入图片描述
经过了变色-左旋转-变色-LR平衡旋转-变色后,终于成为了符合红黑树要求的红黑树

大致就是这样的过程,过程很复杂,不要过多纠结

2.4 构造方法
在这里插入图片描述
无参构造方法

创建一个空的treemap,比较器设置为null,使用自然排序

public TreeMap() {
    comparator = null;
}

带比较器参数方法

传入比较器

public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

传入比较器和集合构造方法

public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}

传入SortedMap集合的构造方法

使用传入的SortedMap的比较器

public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

由构造方法可见,加入的元素的key值必须实现比较器

2.5 常用方法

2.5.1 增加节点

 public V put(K key, V value) {
 		//根节点
        Entry<K,V> t = root;
        //如果根节点为空,传入key-value添加至根节点
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        //cmp纪录比较结果
        int cmp;
        //设置双亲节点
        Entry<K,V> parent;
        // split comparator and comparable paths
        //使用设置的比较器
        Comparator<? super K> cpr = comparator;
        //如果比较器不为空,开始查找要插入的节点
        if (cpr != null) {
            do {
            	//纪录当前节点,是要加入新节点的父节点
                parent = t;
                //比较新加入的key和当前节点的key值大小
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                	//新插入的key小于当前节点,则作为当前节点的左孩子
                    t = t.left;
                else if (cmp > 0)
                	//新插入的key大于当前节点,则作为当前节点的右孩子
                    t = t.right;
                else
                	//当前节点的key和新插入的key相等的话,覆盖value,并返回旧value
                    return t.setValue(value);
            } while (t != null);
        }
        //如果比较器为空
        else {
        	//如果key值为空,则抛出异常,且key必须实现Comparable接口
            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)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        //parent由上面找到,是要新插入节点的双亲节点
        Entry<K,V> e = new Entry<>(key, value, parent);
        //如果新插入的节点值小于双亲节点,则插在parent左边
        if (cmp < 0)
            parent.left = e;
        else
        	//大于,插在右边
            parent.right = e;
        //调整红黑树
        fixAfterInsertion(e);
        //元素个数+1
        size++;
        modCount++;
        return null;
    }

调整红黑树

   private void fixAfterInsertion(Entry<K,V> x) {
   		//新插入节点设置为红色
        x.color = RED;

		//确认节点x不为null,x不是根节点,x的双亲节点是红色,则需要调整
        while (x != null && x != root && x.parent.color == RED) {
        	//以下是颜色转换
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                Entry<K,V> 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<K,V> 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.5.2 删除节点

之前在二叉搜索树中讲到,删除节点有3中情况,这里也是的,不多重复,可以去看二叉搜索树了解3种情况。当时是自己写的代码,比较粗糙,这里的源码写的比较好

这里删除的节点还会有红黑颜色的影响,删除红色节点无影响,删除黑色节点则会影响路径上总的黑色节点数量

    public V remove(Object key) {
    	//根据k获得节点
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;
		//获得旧值
        V oldValue = p.value;
        //删除
        deleteEntry(p);
        //返回旧值
        return oldValue;
    }

   private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        // If strictly internal, copy successor's element to p and then make p
        // point to successor.
        //如果被删除的节点p有左右孩子
        if (p.left != null && p.right != null) {
        	//找到p的替代节点s
            Entry<K,V> s = successor(p);
            //将s的key、value值赋给p
            p.key = s.key;
            p.value = s.value;
            //p指向s节点
            p = s;
        } // p has 2 children

		//找到s节点(p已经指向了s)的左孩子或右孩子
		//二叉搜索树中说过,情况3现在已经被转化为了情况1或者情况2
        // Start fixup at replacement node, if it exists.
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);

		//情况2:只有一个孩子
        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.
        	//情况1:属于叶子节点
            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;
            }
        }
    }

	//二叉搜索树中删除元素的情况3,详见那节的内容,这里不细说了
    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

其实是很复杂的,建议从二叉搜索树、二叉平衡树,再看到红黑树,会比较容易理解。代码比较复杂,当时看懂了,过几天可能就忘了,理解了就好吧

2.5.3 查询

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}

    final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
        	//如果比较器不为空,查找匀元素位置,返回节点
            return getEntryUsingComparator(key);
        //key为空,抛出异常
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        	//如果未传入比较器,采用key值实现的比较器
            Comparable<? super K> k = (Comparable<? super K>) key;
        //从根节点开始查找
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            //小于,转向左子树
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
            	//大于,转向右子树
                p = p.right;
            else
            	//等于,找到了返回
                return p;
        }
        //找不到
        return null;
    }

	//获取传入的比较器,利用传入的比较器找到节点,同上操作
    final Entry<K,V> getEntryUsingComparator(Object key) {
        @SuppressWarnings("unchecked")
            K k = (K) key;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            Entry<K,V> p = root;
            while (p != null) {
                int cmp = cpr.compare(k, p.key);
                if (cmp < 0)
                    p = p.left;
                else if (cmp > 0)
                    p = p.right;
                else
                    return p;
            }
        }
        return null;
    }

TreeMap的方法基本上就讲这些了

三、总结

  1. TreeMap的底层实现是红黑树,他可以按照key值有序插入,查找比较快。
  2. key值必须实现comparable接口或者在TreeMap中传入自定义的比较器
  3. 非线程安全
  4. key值和value值不能为null
  5. 同HashMap和HashSet的关系一样,也有一个TreeSet,其底层是使用TreeMap实现的,这里就不说TreeMap了,可以参考之前讲过的HashSet
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值