java容器类---TreeMap、TreeSet

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)




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值