java容器类---TreeMap、TreeSet

来源:互联网 发布:hits算法题目 编辑:程序博客网 时间:2024/05/01 23:40

1、TreeMap 简介

TreeMap是基于红黑树实现的,这里只对红黑树做个简单的介绍,红黑树是一种特殊的二叉排序树,关于二叉排序树,红黑树通过一些限制,使其不会出现二叉树排序树中极端的一边倒的情况,相对二叉排序树而言,这自然提高了查询的效率。

红黑树的基本性质如下:
1、每个节点都只能是红色或者黑色
2、根节点是黑色
3、每个叶节点(NULL节点,空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

在介绍TreeMap前先介绍Comparable和Comparator接口。 

Comparable接口:

public interface Comparable<T> {     public int compareTo(T o); }
Comparable接口支持泛型,只有一个方法,该方法返回负数、零、正数分别表示当前对象“小于”、“等于”、“大于”传入对象o。

Comparator接口:

public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); }
compare(T o1,T o2)方法比较o1和o2两个对象,o1“大于”o2,返回正数,相等返回零,“小于”返回负数。

equals(Object obj)返回true的唯一情况是obj也是一个比较器(Comparator)并且比较结果和此比较器的结果的大小次序是一致的。即comp1.equals(comp2)意味着sgn(comp1.compare(o1,o2))==sgn(comp2.compare(o1,o2))。
补充:符号sgn(expression)表示数学上的signum函数,该函数根据expression的值是负数、零或正数,分别返回-1、0或1。

1.1 数据结构

    TreeMap的排序是基于对key的排序实现的,它的每一个Entry代表红黑树的一个节点,Entry的数据结构如下:

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;           // 构造函数         Entry(K key, V value, Entry<K,V> parent) {             this.key = key;             this.value = value;             this.parent = parent;         }      ........  }
1.2 继承关系

public class TreeMap<K,V>     extends AbstractMap<K,V>     implements NavigableMap<K,V>, Cloneable, java.io.Serializable
NavigableMap接口扩展的SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法。方法lowerEntry、floorEntry、ceilingEntry和higherEntry分别返回与小于、小于等于、大于等于、大于给定键的键关联的Map.Entry对象,如果不存在这样的键,则返回null。类似地,方法lowerKey、floorKey、ceilingKey和higherKey只返回关联的键。

1.3 成员变量

   // 用于保持顺序的比较器,如果为空的话使用自然顺保持Key的顺序    private final Comparator<? super K> comparator;    // 根节点    private transient Entry<K,V> root = null;    // 树中的节点数量    private transient int size = 0;    // 多次在集合类中提到了,用于举了结构行的改变次数    private transient int modCount = 0; 
2、TreeMap 构造函数

// 构造方法一,默认的构造方法,comparator为空,即采用自然顺序维持TreeMap中节点的顺序public TreeMap() {    comparator = null;}// 构造方法二,提供指定的比较器public TreeMap(Comparator<? super K> comparator) {    this.comparator = comparator;}// 构造方法三,采用自然序维持TreeMap中节点的顺序,同时将传入的Map中的内容添加到TreeMap中public TreeMap(Map<? extends K, ? extends V> m) {    comparator = null;    putAll(m);}/** *构造方法四,接收SortedMap参数,根据SortedMap的比较器维持TreeMap中的节点顺序,* 同时通过buildFromSorted(int size, Iterator it, java.io.ObjectInputStream str, V defaultVal)方* 法将SortedMap中的内容添加到TreeMap中*/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.compareTo()方法,因此key必须实现Comparable接口,并覆写其中的compareTo方法。
构造方法二采用带比较器的构造方法,这时候,排序依赖该比较器,key可以不用实现Comparable接口。

3、TreeMap 常用方法

3.1 构造TreeMap

// 将map中的全部节点添加到TreeMap中    public void putAll(Map<? extends K, ? extends V> map) {        // 获取map的大小        int mapSize = map.size();        // 如果TreeMap的大小是0,且map的大小不是0,且map是已排序的“key-value对”        if (size==0 && mapSize!=0 && map instanceof SortedMap) {            Comparator c = ((SortedMap)map).comparator();            // 如果TreeMap和map的比较器相等;            // 则将map的元素全部拷贝到TreeMap中,然后返回!            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;            }        }        // 调用AbstractMap中的putAll();        // AbstractMap中的putAll()又会调用到TreeMap的put()        super.putAll(map);    }
显然,如果Map里的元素是排好序的,就调用buildFromSorted方法来拷贝Map中的元素,而如果Map中的元素不是排好序的,就调用AbstractMap的putAll(map)方法,该方法源码如下 

public void putAll(Map<? extends K, ? extends V> m) {        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())            put(e.getKey(), e.getValue());    }
    很明显它是将Map中的元素一个个put(插入)到TreeMap中的,主要因为Map中的元素是无序存放的,因此要一个个插入到红黑树中,使其有序存放,并满足红黑树的性质。  

3.2 插入元素

插入操作即对应TreeMap的put方法,put操作实际上只需按照二叉排序树的插入步骤来操作即可,插入到指定位置后,再做调整,使其保持红黑树的特性。put源码的实现:

public V put(K key, V value) {        Entry<K,V> t = root;        if (t == null) {        //如果根节点为null,将传入的键值对构造成根节点(根节点没有父节点,所以传入的父节点为null)            root = new Entry<K,V>(key, value, null);            size = 1;            modCount++;            return null;        }        // 记录比较结果        int cmp;        Entry<K,V> parent;        // 分割比较器和可比较接口的处理        Comparator<? super K> cpr = comparator;        // 有比较器的处理        if (cpr != null) {            // do while实现在root为根节点移动寻找传入键值对需要插入的位置            do {                // 记录将要被掺入新的键值对将要节点(即新节点的父节点)                parent = t;                // 使用比较器比较父节点和插入键值对的key值的大小                cmp = cpr.compare(key, t.key);                // 插入的key较大                if (cmp < 0)                    t = t.left;                // 插入的key较小                else if (cmp > 0)                    t = t.right;                // key值相等,替换并返回t节点的value(put方法结束)                else                    return t.setValue(value);            } while (t != null);        }        // 没有比较器的处理        else {            // key为null抛出NullPointerException异常            if (key == null)                throw new NullPointerException();            Comparable<? super K> k = (Comparable<? super K>) key;            // 与if中的do while类似,只是比较的方式不同            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相同的节点才会有下面的操作        // 根据传入的键值对和找到的“父节点”创建新节点        Entry<K,V> e = new Entry<K,V>(key, value, parent);        // 根据最后一次的判断结果确认新节点是“父节点”的左孩子还是又孩子        if (cmp < 0)            parent.left = e;        else            parent.right = e;        // 对加入新节点的树进行调整        fixAfterInsertion(e);        // 记录size和modCount        size++;        modCount++;        // 因为是插入新节点,所以返回的是null        return null;    }
这里的fixAfterInsertion便是节点插入后对树进行调整的方法,这里不做介绍。
3.3 查找元素

final Entry<K,V> getEntry(Object key) {    // 如果有比较器,返回getEntryUsingComparator(Object key)的结果    if (comparator != null)        return getEntryUsingComparator(key);    // 查找的key为null,抛出NullPointerException    if (key == null)        throw new NullPointerException();    // 如果没有比较器,而是实现了可比较接口    Comparable<? super K> k = (Comparable<? super K>) key;    // 获取根节点    Entry<K,V> p = root;    // 对树进行遍历查找节点    while (p != null) {        // 把key和当前节点的key进行比较        int cmp = k.compareTo(p.key);        // key小于当前节点的key        if (cmp < 0)            // p “移动”到左节点上            p = p.left;        // key大于当前节点的key        else if (cmp > 0)            // p “移动”到右节点上p = p.right;        // key值相等则当前节点就是要找的节点        else            // 返回找到的节点            return p;        }    // 没找到则返回null    return null;}
3.4 删除元素
删除操作及对应TreeMap的deleteEntry方法,deleteEntry方法同样也只需按照二叉排序树的操作步骤实现即可,删除指定节点后,再对树进行调整即可。deleteEntry方法的实现源码如下:

// 删除“红黑树的节点p”    private void deleteEntry(Entry<K,V> p) {        modCount++;        size--;          if (p.left != null && p.right != null) {            Entry<K,V> s = successor (p);            p.key = s.key;            p.value = s.value;            p = s;        }         Entry<K,V> replacement = (p.left != null ? p.left : p.right);          if (replacement != null) {            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;              p.left = p.right = p.parent = null;              if (p.color == BLACK)                fixAfterDeletion(replacement);        } else if (p.parent == null) {           root = null;        } else {          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;            }        }    }

4、总结

TreeMap用的没有HashMap那么多,我们有个宏观上的把我和比较即可。


1、TreeMap是根据key进行排序的,它的排序和定位需要依赖比较器或覆写Comparable接口,也因此不需要key覆写hashCode方法和equals方法,就可以排除掉重复的key,而HashMap的key则需要通过覆写hashCode方法和equals方法来确保没有重复的key。

2、TreeMap的查询、插入、删除效率均没有HashMap高,一般只有要对key排序时才使用TreeMap

3、TreeMap的key不能为null,而HashMap的key可以为null。

5、TreeSet

   TreeSet是基于TreeMap实现的,只是对应的节点中只有key,而没有value,因此对TreeMap比较了解的话,对TreeSet的理解就会非常容易。


参考来源:
【Java集合源码剖析】TreeMap源码剖析

TreeMap源码分析——基础分析(基于JDK1.6)




0 0