前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Java】基础篇- TreeMap

【Java】基础篇- TreeMap

作者头像
haoming1100
发布2019-06-19 19:05:11
6570
发布2019-06-19 19:05:11
举报
文章被收录于专栏:步履前行步履前行步履前行

大家好久不见,我们今天来讲一下 Map 类的另一个重要实现 -- TreeMap。可能大家有的人会问道,我知道 Java 中有 HashMap ,我会用它就行了啊,我还学这个 TreeMap 做啥,其实 HashMap 有个很重要的问题,就是不能排序,或者说它的键值对不能按照特定的顺序排序。所以就引入了我们今天的 TreeMap。(记住 TreeMap 是按照键来进行排序的)而 TreeMap 的实现基础就是我们之前的上一篇文章提到的 排序二叉树,没有看的童鞋请移步:。

TreeMap的实现结构

 A Red-Black tree based {@link NavigableMap} implementation.
 * The map is sorted according to the {@linkplain Comparable natural
 * ordering} of its keys, or by a {@link Comparator} provided at map
 * creation time, depending on which constructor is used.
 ...

上面是类源码的注释之一,之所以给大家看这个,是因为提到很重要的一个概念-底层实现是红黑树,并且内部排序是根据传入的Comparable。

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable {}
  • TreeMap 继承了 AbstractMap,而 AbstractMap 是个抽象类,我们在前面的文章中说过,这里不再叙述了
  • NavigableMap 是 1.6 才提供的,主要拓展了 SortedMap。针对给定搜索目标返回最接近匹配项的导航方法
    • 方法 lowerEntry、floorEntry、ceilingEntry 和 higherEntry 返回与 <、<=、>=、> 给定键的键关联的 Map.Entry ,如果不存在,则返回 null。方法 lowerKey、floorKey、ceilingKey 和 higherKey 只返回关联的键。所有这些方法是为查找条目而不是遍历条目而设计的。
  • Cloneable 和 Serializable 只是一个标记接口

重要组成

    /**
     * The comparator used to maintain order in this tree map, or
     * null if it uses the natural ordering of its keys.
     *
     * 这是 TreeMap 的内部比较器
     */
    private final Comparator<? super K> comparator;
    
    // 内部的存储结构
    private transient Entry<K,V> root;

    /**
     * 节点数量,
     */
    private transient int size = 0;

    /**
     * 树的变更操作次数
     */
    private transient int modCount = 0;

上面的代码就是整个 TreeMap 的核心方法了,接下来的所有操作都是围绕这几个变量的,其中我们要注意下:

  • size : 查看元素的数量的时间复杂度是 O(1),这个时间复杂度实际上被平摊给了每次操作,表面上给我们的映像是 O(1)罢了。
  • modCount: 这个变量就是每次元素的变更次数,将要在 迭代的时候进行比较,如果是非预期的变更,那么就会抛出 ConcurrentModificationException 异常。
  • Entry<K,V> root:这是 TreeMap 的内部物理组成元素,它的结构如下:
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;                                 
}

可以看出,很明显的 红黑树了。

基本用法

构造函数

TreeMap 有 4个 构造函数,分别是:

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

接下来我们一个一个看


1.
 public TreeMap() {
        comparator = null;
 }   

该构造函数使用默认的排序算法,要求 实现 Comparabe 接口,TreeMap 内部进行比较时,会调用 Comparabe 接口 的 compareTo 方法 ,比如

public class MapTest {

    public static void main(String[] args) {
        TreeMap<Item,String> treeMap = new TreeMap<>();
        Item item = new Item();
        item.setAge(1);
        treeMap.put(item, "1");
        
    }
}
class Item {
    private Integer age;
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
}

因为 Item 没有实现 Comparabe 接口,所以运行的时候会抛出异常:

Exception in thread "main" java.lang.ClassCastException: Item cannot be cast to java.lang.Comparable
    at java.util.TreeMap.compare(TreeMap.java:1294)
    at java.util.TreeMap.put(TreeMap.java:538)
    at MapTest.main(MapTest.java:9)

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

该构造函数接受一个比较器对象,如果传入的 comparator 不为 null,TreeMap 内部比较的时候会使用 该 comparator 进行比较,并且 key 也不用再实现 Comparabe 接口,如下

public class MapTest {

    public static void main(String[] args) {
        TreeMap<Item,String> treeMap = new TreeMap<>((Comparator) (o1, o2) -> 0);
        Item item = new Item();
        item.setAge(1);
        treeMap.put(item, "1");
    }
}

可以正常的执行


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

该构造方法也是使用默认的排序算法,其中 putAll() 就是把 m 放入当前 Map 中,注意,这里的 m 有 2 种情况

  • 是 SortedMap 的子类,直接进行存放
  • 非 SortedMap 的子类
    • 调用子类的 putAll 进行存放
    public void putAll(Map<? extends K, ? extends V> map) {
        int mapSize = map.size();
        // 是 SortedMap 的子类,TreeMap 实现的 NavigableMap 就是 SortedMap的子类
        if (size==0 && mapSize!=0 && map instanceof SortedMap) {
            Comparator<?> c = ((SortedMap<?,?>)map).comparator();
            if (c == comparator || (c != null && c.equals(comparator))) {
                ++modCount;
                try {
                    buildFromSorted(mapSize, map.entrySet().iterator(),
                                    null, null);
                } catch (java.io.IOException cannotHappen) {
                } catch (ClassNotFoundException cannotHappen) {
                }
                return;
            }
        }
        super.putAll(map);
    }

这里有给有趣的地方大家注意下:ut 方法中对每次判断传入的 key check,key 也需要实现 Comparator Map<String,String> map = new HashMap<>(); map.put("key", "value"); TreeMap<String,String> treeMap = new TreeMap<>(map); 比如 我们传入的 是一个 HashMap,那么 super.putAll() 方法,这个 putAll 方法调用的是 AbstractMap 的putAll 方法,而在 putAll 方法中,实际调用的是各个子类的 put 方法,千万不要搞混了 public void putAll(Map<? extends K, ? extends V> m) { for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) //调用子类的 put 方法,AbstractMap 的 put 方法 默认抛出异常 put(e.getKey(), e.getValue()); }

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

该构造方法是从 传入的 SortedMap 构造一个 Comparator 并且把 m 的元素放入 当前Map 中,

在 3 和 4 的构造方法中,有一个 公共的方法,buildFromSorted,这个方法是干什么的呢?

buildFromSorted: 构建红黑树的方法.

buildFromSorted 有序构建红黑树
    // str 和 defaultVal 通常是 null
    // it 是 map.entrySet().iterator()
    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);
    }

其中 buildFromSorted 如下

/**
     * @param level 是当前树的高度,默认是 0 
     * @param lo 第一个子树的元素索引,默认是 0
     * @param hi 最后一个子树的元素索引,默认是 size-1
     * @param redLevel 节点应为红树的高度 对于此大小的树,必须等于computeRedLevel
                computeRedLevel 计算出的 红树的高度
     * @param it 迭代器,如果不为空,则从迭代器中读取元素
     * @param str 对象输入流,如果不为空,则从流中读取键值对
     * @param defaultVal value默认值
     *      
*/
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 {
    /**
    * 根 root 是中间的元素。为了实现它,我们首先递归地构造整个左子树,获取它的所有元素。然后我们可以继续      使用右子树
    */
    // 如果 右子树都比左子树小了 ,返回 空
    if (hi < lo) return null;
    // 计算中间值 即 root
    int mid = (lo + hi) >>> 1;
    Entry<K,V> left  = null;
    if (lo < mid)
        //  递归构建左子树,由于已经有根节点了,所以level+1
        left = buildFromSorted(level+1, lo, mid - 1, redLevel,
                               it, str, defaultVal);

    // e从迭代器或流中提取键和/或值
    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 { // 迭代器为空,对象流不为空,从对象流中读取
        key = (K) str.readObject();
        value = (defaultVal != null ? defaultVal : (V) str.readObject());
    }

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

    // 非完整最底层红色的颜色节点
    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;
}

而关于红黑树的染色 则是使用 之前的 computeRedLevel 来确定:

    // 通过给到的树中节点数,计算红色节点高度
    private static int computeRedLevel(int sz) {
        int level = 0;
        for (int m = sz - 1; m >= 0; m = m / 2 - 1)
            level++;
        return level;
    }

比如当前节点数是 5 ,那么红色节点的高度就是 2

buildFromSorted 看起来很复杂,其实就是下面几步:

  • 检查。如果传入的右树的值比左树的值小,返回null
  • 递归构建左子树
    • 如果当前树的高度与红树的高度相等,那么设置为红树(节点默认是黑树)
  • 递归构建右子树

put 方法

    public V put(K key, V value) {
        Entry<K,V> t = root;
        // 1
        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;
        //2
        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 {
            //3
            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);
        }
        //4
        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;
    }

这是一个 put 方法,由于代码太长,我们拆分成 4 段进行分析

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

在我们上面经常提到的 TreeMap 的 key 要实现 Comparable,其中的校验就是 compare 方法,如下:

   final int compare(Object k1, Object k2) {
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }
2.
        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);
        }

在有设置比较器的情况下,其中就是根据比较器然后判断将要放入的 key - value 要放入左子树还是右子树。

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

这段是在没有设置比较器的情况下,使用 key 默认的比较器进行比较,原理和 2 相同

4.
        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 方法 --— 更新红黑颜色

fixAfterInsertion
private void fixAfterInsertion(Entry<K,V> x) {
    //插入的节点颜色强制设置为红色
    x.color = RED;
    //插入的节点不允许是 null,并且不能是 根节点,并且父节点的颜色是红色,才执行颜色 fix
    //比如我们在有一个元素是根节点的情况下,再插入的时候,他的父节点的颜色是黑色,不满足 while 语句,那么直接跳过 fix
    while (x != null && x != root && x.parent.color == RED) {
        // 如果X的父节点(P)是其叔父节点
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            // 获取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 {
            // 如果X的父节点不是其叔父节点
            //获取X的叔父节点
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            // 如果X的叔节点 为红色
            if (colorOf(y) == RED) {
                //设置父节点是黑色
                setColor(parentOf(x), BLACK);
                //设置叔父节点是黑色
                setColor(y, BLACK);
                // 设置父节点的父节点是红色
                setColor(parentOf(parentOf(x)), RED);
                //获取到父节点的父节点
                x = parentOf(parentOf(x));
            } else {
                // 如果X的叔节点 为黑色
                // 获取到x 的叔父节点
                if (x == leftOf(parentOf(x))) {
                    // 获取到 x 的父节点
                    x = parentOf(x);
                    // 进行右旋染色
                    rotateRight(x);
                }
                //设置父节点为黑色
                setColor(parentOf(x), BLACK);
                //设置父节点的父节点为红色
                setColor(parentOf(parentOf(x)), RED);
                //左旋染色
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //根节点的颜色强制设置为 黑色
    root.color = BLACK;
}

上面就是整个染色的内容了,其中有重要的左旋和右旋翻转,我们这里就不再叙述了,在上篇 排序二叉树中已经提到过相关算法,大家可以去查看

get 方法

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

get 方法相比较 put 方法,简直简单的不要不要的。其中有个核心方法,getEntry(),如下:

final Entry<K,V> getEntry(Object key) {
        // 如果比较器不为空,则按照指定的比较器进行查找
        if (comparator != null)
            return getEntryUsingComparator(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;
    }

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

上面的 remove 的灵魂就是 deleteEntry 方法,这个方法是删除一个节点,并且重新调整红黑树,我们下面来研究下:

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

        // 如果被删除的节点 p 的左子树 和右子树都不为空,那么就要在 p 的后继节点中寻找一个节点来代替 待删除的节点 p
        // 寻找 代替节点的方法是 successor() ,具体逻辑是 右分支最左边的节点,或者 左分支最右边的节点
        if (p.left != null && p.right != null) {
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } 
        //替换之后  
        // replacement 为替换的节点的左子节点的判断(如果左子节点存在,那么就用左节点,反之用右节点)
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
        // 如果要替换的 节点不为空
        if (replacement != null) {
            // Link replacement to parent
            replacement.parent = p.parent;
            // 如果没有父节点,则把 replacement 变为父节点
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left) // 如果 p 是左节点,那么用 replacement 代替左节点
                p.parent.left  = replacement;
            else // 如果p 是右节点,那么用 replacement 代替右节点
                p.parent.right = replacement;

            // 将 P 节点删除
            p.left = p.right = p.parent = null;

            // 如果 p 的颜色是黑色,那么需要保持红黑树的平衡
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { // p 的父节点为 null,代表的是根节点,直接删除
            root = null;
        } else {// p 没有子节点,直接删除
            if (p.color == BLACK)
                // 如果 p 的颜色是黑色,则进行调整,和 put 方法的调整类似
                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;
            }
        }
    }

上面的内容概括如下:

  • 寻找代替节点
  • 替换节点存在,用找到的替换节点代替 p,如果当前节点是黑色节点,进行红黑调整
  • 如果是根节点,直接删除
  • 如果没有子节点,并且当前是黑色节点,进行红黑调整,然后进行删除

clear 方法

clear 方法很简单,就是吧 root 节点删除即可

public void clear() {
        modCount++;
        size = 0;
        root = null;
    }

containsXXX 方法

contains 方法有 2 个,分别是 containsKey 和 containsValue 。

   public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }
public boolean containsValue(Object value) {
        for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e))
            if (valEquals(value, e.value))
                return true;
        return false;
    }

上面的的 2个方法很简单,我们就不再叙述了。

firstEntry & lastEntry

    public Map.Entry<K,V> firstEntry() {
        return exportEntry(getFirstEntry());
    }

    public Map.Entry<K,V> lastEntry() {
        return exportEntry(getLastEntry());
    }

firstEntry 和 lastEntry 相对来说比较简单,其中调用的是内部的 getFirstEntry 和 getLastEntry 方法。

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

这里大家需要注意下,firstEntry() 和 lastEntry() 都调用了 exportEntry()。

 static <K,V> Map.Entry<K,V> exportEntry(TreeMap.Entry<K,V> e) {
        return (e == null) ? null :
            new AbstractMap.SimpleImmutableEntry<>(e);
    }

返回的是 SimpleImmutableEntry, 不可变的 Entry。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-06-18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • TreeMap的实现结构
  • 重要组成
  • 基本用法
    • 构造函数
      • put 方法
        • get 方法
          • remove 方法
            • clear 方法
              • containsXXX 方法
                • firstEntry & lastEntry
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档