手撕TreeMap

TreeMap 的排序可以通过键(实现 Comparable 接口),或者在创建 Map 时,提供的 Comparator 实现。

文档中提到有趣的一点,containsKeygetputremove 操作提供了 log(n) 时间成本,所以带着这个问题去看。

初始化

TreeMap 的构造器有如下几个:

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

	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) {
        }
    }

我们可以对比它与 HashMap 的构造函数,就知道它的底层数据结构已经变了。提供比较器,是因为 TreeMap 是支持自定义排序的,而不像 LinkedHashMap 仅仅只能按照插入顺序或者访问顺序进行排序。

查看 buildFromSorted 方法的实现:

	private void buildFromSorted(int size, Iterator<?> it,
                                 java.io.ObjectInputStream str,
                                 V defaultVal)
        throws  java.io.IOException, ClassNotFoundException {
        this.size = size;
        root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
                               it, str, defaultVal);
    }

	private final Entry<K,V> buildFromSorted(int level, int lo, int hi,
                                             int redLevel,
                                             Iterator<?> it,
                                             java.io.ObjectInputStream str,
                                             V defaultVal)
        throws  java.io.IOException, ClassNotFoundException {
        
        if (hi < lo) return null;

        int mid = (lo + hi) >>> 1;

        Entry<K,V> left  = null;
        if (lo < mid)
            // 递归构建左子树
            left = buildFromSorted(level+1, lo, mid - 1, redLevel,
                                   it, str, defaultVal);

        // extract key and/or value from iterator or stream
        K key;
        V value;
        if (it != null) {
            if (defaultVal==null) {
                Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
                key = (K)entry.getKey();
                value = (V)entry.getValue();
            } else {
                key = (K)it.next();
                value = defaultVal;
            }
        } else { // use stream
            key = (K) str.readObject();
            value = (defaultVal != null ? defaultVal : (V) str.readObject());
        }

        Entry<K,V> middle =  new Entry<>(key, value, null);

        // color nodes in non-full bottommost level red
        if (level == redLevel)
            middle.color = RED;

        if (left != null) {
            middle.left = left;
            left.parent = middle;
        }

        if (mid < hi) {
            // 递归构建右子树
            Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
                                               it, str, defaultVal);
            middle.right = right;
            right.parent = middle;
        }

        return middle;
    }

代码过程比较抽象,转换成图过程说明:
例子树
上面树结构的迭代顺序为 【1,2,3,4,5,6,7,8,9】,采用了中序遍历,这很重要,因为代码中构建树的数据来源就是迭代器,也就是我们只能按照这样的顺序重新构造树节点。

结合上面代码,分析图:

例子树执行流程分析

带有彩色的数字,代表执行顺序;节点真正的创建时机,结合代码分析可知,在递归返回时,开始创建节点。以上是先递归构造左子树,然后递归构造右子树。可以发现上面节点创建时机,将完全符合迭代器中的顺序。

注意,TreeMap 可能不会构造成这样一模一样的树,在这里只是借此说明代码的执行过程。

这里思考一个问题,既然是递归,那么就有可能出现栈溢出,对此,做了测试,结果总是抛出 OutOfMemoryError

存取操作

put 操作将存储新的映射,如果当前 key 之前并未存在的话;如果存在,则覆盖,并返回旧值。

	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;
        // 1. 比较器存在的情况下
        if (cpr != null) {
            // 1.1. 循环查找父节点
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                // 1.1.1 找到相等的节点,直接替代,然后返回
                else
                    return t.setValue(value);
            } while (t != null);
        }
        // 2. 比较器不存在的情况下
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            // 2.1 循环查找父节点
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                // 2.1.1 找到相等节点,直接替代返回
                else
                    return t.setValue(value);
            } while (t != null);
        }
        // 3. 创建当前节点,并根据比较值,判断是为父类的左子节点,还是右子节点
        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;
    }

有趣的问题:

TreeMap 的 key 是否能为 null?( value 能够为 null)

其实这与你的构造有关,本质上 TreeMap 需要排序,所以如果你提交了 Comparator ,并且它的实现不会抛出 null,那么 key 就可以为 null。但如果你依赖 key 实现 Comparable 接口来排序,那么 key 无论如何都不能为 null 了。

		Map<String, String> baseMap = new TreeMap<>((o1, o2) -> 0);
		// Map<String, String> baseMap = new TreeMap<>(); 会导致 put 时 抛出 NPE
        baseMap.put(null, "1");

下面再来看 get 操作:

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

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

get 取值其实是一个在树中进行查找的操作,不过因为比较节点相等的方法不同(依赖比较器,或者实现了 Comparable 的 key),略有区分。

TreeMap 要么在构造时添加比较器,要么其 key 实现 Comparable 接口


删除操作

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

	// 删除节点,重新平衡树
	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.
        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

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

上面的删除节点代码,在理解了红黑树的删除实现之后,再来看会好懂一点。文末参考博文中会给出几篇比较好的学习红黑树算法的文章。


迭代器

迭代器的主要实现如下:

	abstract class PrivateEntryIterator<T> implements Iterator<T> {
        Entry<K,V> next;
        Entry<K,V> lastReturned;
        int expectedModCount;

        PrivateEntryIterator(Entry<K,V> first) {
            expectedModCount = modCount;
            lastReturned = null;
            next = first;
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Entry<K,V> nextEntry() {
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            // 找到后继(中序遍历)
            next = successor(e);
            lastReturned = e;
            return e;
        }

        final Entry<K,V> prevEntry() {
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            next = predecessor(e);
            lastReturned = e;
            return e;
        }

        public void remove() {
            if (lastReturned == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            // deleted entries are replaced by their successors
            if (lastReturned.left != null && lastReturned.right != null)
                next = lastReturned;
            deleteEntry(lastReturned);
            expectedModCount = modCount;
            lastReturned = null;
        }
    }

	// 中序遍历:左、中、右
	static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        // t 刚好为 中 ,往右遍历
        else if (t.right != null) {
            Entry<K,V> p = t.right;        
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            // t 刚好为 左,右,则往上
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

复杂的在 successor 方法中,我们需要明白,它要做的事是中序遍历这颗树。


总结

TreeMap 是可排序的,它的排序方式有两种:

  1. 根据 Comparator 排序:如果比较器实现中,key 为 null 不会抛出异常,那么,TreeMap 中就可以存放 key 为 null 的映射。
  2. 根据 key 实现的 Comparable 接口:说明如果不提供比较器,则 key 必须实现 Comparable 接口,并且在这样的比较下,key 是否为 null 的。

迭代器的设计与 HashMapLinkedHashMap 一样,并且,都是属于快速失败的,在并发修改的时候,能够尽最大努力抛出 ConcurrentModificationException

TreeMap 完全是基于红黑树实现的(算法导论中的描述,略有修剪),文中并没有就红黑树再做出具体分析。

TreeMap 还实现了 NavigableMap 接口,该接口提供了一系列可导航的方法。


推荐博文


手撕Java类HashMap

手撕HashMap迭代器

手撕HashMap红黑树

手撕LinkedHashMap


参考博文


红黑树原理和算法介绍

自平衡二叉树和红黑树

30张图带你彻底理解红黑树

数据结构可视化


我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值