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源码剖析