• 【源代码】TreeMap源代码剖析


    注:下面源代码基于jdk1.7.0_11

    之前介绍了一系列Map集合中的详细实现类,包含HashMap,HashTable,LinkedHashMap。这三个类都是基于哈希表实现的,今天我们介绍还有一种Map集合,TreeMap。TreeMap是基于红黑树实现的。
    介绍TreeMap之前,回想下红黑树的性质
    首先,我们要明白,红黑树是一种二叉排序树,并且是平衡二叉树。因而红黑树具有排序树的全部特点,随意结点的左子树(假设有的话)的值比该结点小,右子树(假设有的话)的值比该结点大。二叉排序树各项操作的平均时间复杂度为O(logn),可是最坏情况下,二叉排序树会退化成单链表,此时时间复杂度为O(n),红黑树在二叉排序树的基础上,对其添加了一系列约束,使得其尽可能地平衡,红黑树的查询和更新的时间复杂度为O(logn)。
    红黑树的五条性质:
    1.每一个结点要么是红色,要么是黑色;
    2.根结点为黑色;
    3.叶结点为黑色(空结点);
    4.若一个结点为红色,则其子结点为黑色;
    5.每一个叶结点到根结点的路径中黑色结点的数目一致(黑高度同样)。
    红黑树的查询操作与二叉排序树同样,重点是其插入和删除操作。红黑树的插入和删除操作在二叉排序树的基础上添加了修复操作,由于插入和删除可能会导致树不再满足红黑树性质,这时候会通过着色、旋转操作对其进行修复。

    以下来看TreeMap的实现。
    类声明:
    public class TreeMap<K,V>
        extends AbstractMap<K,V>
        implements NavigableMap<K,V>, Cloneable, java.io.Serializable

    TreeMap相同继承AbstractMap,可是它实现了NavigableMap接口,而NavigableMap接口继承自SortedMap接口。
    TreeMap有四个成员变量,当中root是红黑树的根结点,因为红黑树的查询和更新操作须要比較,故而有个比較器comparator,默认情况下,comparator为空,这就要求我们的键必须实现Comparable接口,以定义比較规则。
     private final Comparator<? super K> comparator;//比較器
     private transient Entry<K,V> root = null;//树根
        /**
         * The number of entries in the tree
         */
     private transient int size = 0;//大小
        /**
         * The number of structural modifications to the tree.
         */
     private transient int modCount = 0;//改动次数

    构造器:
    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) {
            }
        }

    在查看TreeMap的查询和更新操作之前,我们先看下Entry的实现,事实上我们都能够猜到,Entry既然是TreeMap存储的结点,那么其必定包含例如以下几个域:数据(键、值)、父结点、左孩子、右孩子、颜色。事实正是如此:
    static final class Entry<K,V> implements Map.Entry<K,V> {
            K key;//键
            V value;//值
            Entry<K,V> left = null;//左孩子
            Entry<K,V> right = null;//右孩子
            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;
            }
            public K getKey() {
                return key;
            }
            public V getValue() {
                return value;
            }
            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;
            }
        }

    以下来看put方法:
    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 {//假设未定义比較器,那么key必须实现Comparable接口
                if (key == null)//不同意空键
                    throw new NullPointerException();
                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.TreeMap的查询和更新操作都涉及到比較操作,故而TreeMap的键必须实现Comparable接口或者构造时得传入比較器(既实现了Comparable接口又传入了比較器情况下,比較器优先);
    2.put操作不同意null键,可是值(value)同意为null;
    3.键反复的情况下,新值会覆盖掉旧值。

    再看get方法:
     public V get(Object key) {//查询操作
            Entry<K,V> p = getEntry(key);
            return (p==null ? null : p.value);
        }

    调用getEntry方法查询指定键值:
    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();
            Comparable<? super K> k = (Comparable<? super K>) key;
            Entry<K,V> p = root;
            while (p != null) {//依据Comparable接口定义的比較规则查找
                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) {//依据比較器定义的比較规则查找
            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;
        }
    对照HashMap近乎O(1)的查找复杂度,TreeMap显得略有不足。
    再看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) {//p的左右孩子都存在
                Entry<K,V> s = successor(p);//找到p的直接后继(顺着p右子树一直向左)
                p.key = s.key;//用直接后继替代p
                p.value = s.value;
                p = s;
            } // p has 2 children
            //以下操作将释放s结点,并修复红黑树
            // 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;
                }
            }
        }

    successtor函数用于找一个结点的中序后继(參见之前写的一篇怎样得到一个结点的中序后继,算法一致):
    迭代器遍历操作正是基于successtor操作完毕的。所以遍历TreeMap得到的键值对是有序的。
     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;
            }
        }

    对称地,还有个查找直接前驱的函数:
     static <K,V> Entry<K,V> predecessor(Entry<K,V> t) {
            if (t == null)
                return null;
            else if (t.left != null) {//若存在左子树
                Entry<K,V> p = t.left;
                while (p.right != null)//顺着左子树,向右搜索
                    p = p.right;
                return p;
            } else {
                Entry<K,V> p = t.parent;
                Entry<K,V> ch = t;
                while (p != null && ch == p.left) {
                    ch = p;
                    p = p.parent;
                }
                return p;
            }
        }
    注:文章有益忽略了更新操作中涉及到的红黑树修复动作(着色,旋转),此部分内容较为复杂,作者眼下也没有全然吃透。

    总结:
    1.TreeMap的实现基于红黑树;
    2.TreeMap不同意插入null键,但同意null值;
    3.TreeMap线程不安全;
    4.插入结点时,若键反复,则新值会覆盖旧值;
    5.TreeMap要求key必须实现Comparable接口,或者初始化时传入Comparator比較器;
    6.遍历TreeMap得到的结果集是有序的(中序遍历);
    7.TreeMap的各项操作的平均时间复杂度为O(logn).




  • 相关阅读:
    企业应用的互联网化
    企业应用开发平台GAP平台
    技术有什么用?
    Samba服务器配置
    IT从业人员需要知道的10个小秘密
    在雪豹10.6.2(Mac OS X)上安装Oracle10g
    项目经理的职责
    通过堡垒机远程连接Windows Server
    MySQL:Data truncated for column 'last_apply_time' at row 1
    当有莫名其妙的错误里, 可以
  • 原文地址:https://www.cnblogs.com/mfrbuaa/p/3999002.html
Copyright © 2020-2023  润新知