TreeMap的基本用法&实现

TreeMap是使用红黑树实现的,他是按键有序的。
红黑树:从根到叶子节点的路径,没有任何一条路径的长度会比其他路径长过两倍。红黑树把每个节点进行重色,对节点颜色有一些约束。它确保树是大致平衡的。

基本构造方法:

/**
* 该方法要求Map中的键必须实现Comparable接口,TreeMap进行各种个比较时会调用键的Comparable接口中的compareTo方法
*/
	public TreeMap()

	/**
	* 这个构造方法要传入一个comparator比较器对象,如果comparator不为null,在TreeMap内部进行比较时会调用这个comparator的comapre方法,而不再调用键
	* 的compareTo方法,也不要求键实现Comparable接口
	*/
	public TreeMap(Comparator<? super K> comparator)

内部组成

    /**
     * The comparator used to maintain order in this tree map, or
     * null if it uses the natural ordering of its keys.
     *这个 comparator用于维护树的排序,如果使用key的默认排序则comparator为null
     * @serial
     */
    private final Comparator<? super K> comparator;

    private transient Entry<K,V> root; // 指向树的根节点

    /**
     * The number of entries in the tree
     * 树中键值对的个数
     */
    private transient int size = 0;

Entry(节点)为树的内部类:

    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;

        /**
         * Make a new cell with given key, value, and parent, and with
         * {@code null} child links, and BLACK color.
         */
        Entry(K key, V value, Entry<K,V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }
    }

每个节点除了键和值之外,还有三个引用,分别为左、右孩子,以及父节点。
color表示颜色,默认为黑。

方法详解

1.put

    /**
     * Associates the specified value with the specified key in this map.
     * 将制定的值与指定的键在map中做关联
     * If the map previously contained a mapping for the key, the old
     * 如果以前这个key在map中存在,那么旧值会被取代
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     *
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}.
     *         (A {@code null} return can also indicate that the map
     *         previously associated {@code null} with {@code key}.)
     * @throws ClassCastException if the specified key cannot be compared
     *         with the keys currently in the map
     * @throws NullPointerException if the specified key is null
     *         and this map uses natural ordering, or its comparator
     *         does not permit null keys
     */
    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;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> 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();
            @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);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

代码比较长,我们分段来看:

  1. 第一次添加的判断:
  public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check : 类型和null检查

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }

首先判断根节点是否为null,如果为空,说明是第一次添加,则把新传入的键值节点设为根节点。然后size+1,modCount
用于记录修改次数
使用modCount属性的全是线程不安全的,关于modCount的解释可以参考这篇博文modCount到底是干什么的呢

其中比较奇怪的是一段类型和null检查的代码:compare(key, key),比较key和key有什么意思呢?


    /**
     * Compares two keys using the correct comparison method for this TreeMap.
     * 使用这个TreeMap的比较方法比较两个key
     */
    @SuppressWarnings("unchecked")
    final int compare(Object k1, Object k2) {
    // 如果comparator对象为null则使用key的compareTo方法进行比较,不为null则使用comparator的compare比较哦
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }

还是不太懂这段代码是什么意思,注意put方法中调用compare方法时的注释:类型和null检查;
这里主要是为了检查key是否实现Comparable接口或者是否为null——即检查key是否符合TreeMap的要求

  1. 寻找新put键值对的父节点
    寻找键值对父节点的时候分两种情况,使用key自身的比较器或者使用初始化TreeMap时传入的比较器
    2.1. 设置了comparator比较器:
		int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t; // 第一次执行这一步时,t是根节点
                cmp = cpr.compare(key, t.key);
                if (cmp < 0) // key小于根节点的
                    t = t.left; // 把t改为当前节点的左孩子,因为左孩子小于父节点
                else if (cmp > 0) // key大于根节点
                    t = t.right; // 把t改为当前节点的右孩子
                else // 说明跟当前节点key值,相等
                    return t.setValue(value);
            } while (t != null); // 当退出循环时parent指向待插入节点的父节点
        }
2.2. 没有设置comparator比较器
	        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key; // 如果key
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0) // key小于当前节点
                    t = t.left;
                else if (cmp > 0) // key大于当前节点
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }

Comparable可以认为是一个内比较器,实现了Comparable接口的类有一个特点,就是这些类是可以和自己比较的,至于具体和另一个实现了Comparable接口的类如何比较,则依赖compareTo方法的实现,compareTo方法也被称为自然比较方法。
Comparator可以认为是是一个外比较器,个人认为有两种情况可以使用实现Comparator接口的方式:
1、一个对象不支持自己和自己比较(没有实现Comparable接口),但是又想对两个对象进行比较
2、一个对象实现了Comparable接口,但是开发者认为compareTo方法中的比较方式并不是自己想要的那种比较方式
引用自:Comparable和Comparator的区别

  1. 新建节点
    找到父节点后,新建一个节点挂在父节点上,挂之前判断是左孩子还是右孩子,并增加size和modCount
		Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

其中fixAfterInsertion方法,调整树的结构,使之符合红黑树约束,保持大致平衡,这里就不深入探讨了。

2. get

上代码:

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

get方法就不多说了,是老铁都明白。
getEntry方法:

    
final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        // 同样分是否传入了comparator对象两种情况
        if (comparator != null) 
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            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;
    }

跟put方法很类似,如果comparator不为空,则调用单独的方法getEntryUsingComparator,否则假定key实现了Comparable接口,使用compareTo进行比较
找的逻辑跟put一样,从根开始找,比根小,往左找;比根大,往右找,找到为止,如果没有找到,返回null;。getEntryUsingComparator方法的逻辑类似,就不多说了。

3. containsValue

TreeMap可以根据键高效的进行查找,但是如果根据值进行查找,则需要遍历,上代码:

    public boolean containsValue(Object value) {
    // getFirstEntry获取第一个点(key最小)
    // successor获取给定节点的后继节点
        for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e))
            if (valEquals(value, e.value)) // valEquals比较值是否相等
                return true;
        return false;
    }

getFirstEntry:

    final Entry<K,V> getFirstEntry() {
        Entry<K,V> p = root;
        if (p != null)
            while (p.left != null)
                p = p.left;
        return p;
    }

successor:

    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {  // 右子节点不为null则找右子节点下的最左侧节点
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else { // 右子节点为null
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) { // 父节点不为null且当前节点是父节点的右孩子
                ch = p;
                p = p.parent;
            }
            return p; // 如果当前节点父节点是左孩子或者为null,则返回父节点
        }
    }

其实只要知道二叉树是如何找后继节点,这段代码就很好懂;
这里画蛇添足增加一下文案描述:

  1. 如果当前节点有右节点则找到右子树中最小的节点(最左下的节点)
  2. 如果当前节点没有右节点,那么其后继节有三种情况
    2.1 :当前节点是父节点的右孩子,那么后继节点一定是当前节点的某一个祖先节点;一直往上找父,直到父是爷爷的左子树,那么返回爷爷,比较抽象,建议大家画个图,一目了然。
    2.2 :当前节点是父节点的左孩子,那么后继节点就是父节点
    2.3 :当前节点无后继节点,返回null

4. remove

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

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

调用getEntry获取要删除的节点;
如果存在那么调用deleteEntry方法删除,返回节点值。
deleteEntry:

   /**
     * Delete node p, and then rebalance the tree.
     * 删掉p,然后平衡树 
     */
    private void deleteEntry(Entry<K,V> p) {
        modCount++; // 变更记录+1
        size--; // 大小-1

        // 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指向后继节点
        } // p has 2 children

        // 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);
        } 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;
            }
        }
    }

删除这部分不太好理解,建议我们先在纸上自己画一下二叉树删除的几种情况:

  1. 叶子节点,这个容易处理,直接修改父节点的对应引用位置为null即可
  2. 只有一个孩子:在父节点和孩子节点之间建立连接。
  3. 有两个孩子:这个就复杂一点了,首先,找到后继节点,找到后替换当前节点为后继节点,然后再删除后继节点,因为后继节点肯定没有左孩子,这样就把两个孩子的情况转换为了前面两种情况(可以理解为把要删除的节点替换为后继节点,然后再处理后继节点下的树)

我们拆开来看:画图更好理解!!

  1. 两个子节点的情况, 把要删除的节点替换为后继节点,然后删除后继节点,转换为前两种情况
    private void deleteEntry(Entry<K,V> p) {
        modCount++; // 变更记录+1
        size--; // 大小-1

        // 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

  1. 一个子节点的情况:
        // Start fixup at replacement node, if it exists.
        Entry<K,V> replacement = (p.left != null ? p.left : p.right); // p为待删除节点,找到替换p节点的孩子节点,找不到为null

        if (replacement != null) { // 孩子节点不为null
            // Link replacement to parent
            replacement.parent = p.parent; // 把孩子节点的父节点改为要删除节点的父节点
            if (p.parent == null) // 如果要删除节点的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.
            // 将要删除节点的左右和父都职位null,然后使用平衡方法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.——没有孩子节点也没有父节点
  1. 最后一段: 无子节点——叶子节点的情况
        } 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) // 如果要删除的节点为父的左节点,设置父的左孩子为null
                    p.parent.left = null;
                else if (p == p.parent.right) 如果要删除的节点为父的右节点,设置父的右孩子为null
                    p.parent.right = null;
                p.parent = null; // 因为是叶子节点,只把要删除节点的父置为null;
            }
        }
    }

小结

与HashMap相比,TreeMap同样实现了Map接口,但内部使用红黑树实现。红黑树是统计效率比较高的大致平衡二叉树,这决定了它有如下特点:

  1. 按键有序,TreeMap同样实现了NavigableMap接口(该接口实现了SortedMap接口)可以方便地根据键的顺序进行查找,如第一个、最后一个、某一范围的键、邻近键等。
  2. 为了按键有序,TreeMap要求键实现Comparable接口或通过构造方法提供一个Comparator对象。
  3. 根据键保存、查找、删除的效率比较高,为O(h),h为树的高度,在树平衡的情况下,h为log(N),N为节点数。

应该使用TreeMap还是HashMap呢?不要求排序优先考虑HashMap,要求排序,考虑TreeMap。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值