TreeMap
TreeMap是基于红黑树结构实现的一种Map。红黑树是一种自平衡二叉查找树。
二叉查找树
- 若左子树不为空,则左子树上所有节点的值均小于它的根节点的值;
- 若右子树不为空,则右子树上所有节点的值均大于它的根节点的值;
- 左、右子树也分别为二叉查找树;
- 没有键值相等的节点。
TreeMap 利用了红黑树左节点小,右节点大的性质,根据 key 进行排序,使每个元素能够插入到红黑树大小适当的位置,维护了 key 的大小关系,适用于 key 需要排序的场景。因为底层使用的是红黑树的结构(平衡树),所以 containsKey、get、put、remove 等方法的时间 复杂度都是 log(n)。
一、源码中常见属性
//自带的比较器,如果外部有传进来比较器,优先用外部的
private final Comparator<? super K> comparator;
//红黑树的根
private transient Entry<K,V> root;
//集合元素数量
private transient int size = 0;
//版本号修改次数
private transient int modCount = 0;
//红黑树的节点
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;
}
二、新增节点
- 判断红黑树的根节点是不是空的,是空的则要创建根节点,并且key不能为null。
- 根据红黑树左小右大的特性,找到新增节点的父节点(这边要判断有没有外部比较器),如果已经存在,就是值相等,就直接覆盖。
- 找到父节点之后就是插入,相等的情况就在第二步中处理。
- 对红黑树进行着色旋转,达到平衡。
public V put(K key, V value) {
Entry<K,V> t = root;
//1、如果根是空的,就new一个出来,size++,修改版本号
if (t == null) {
compare(key, key); // type check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
// 记录比较结果
int cmp;
Entry<K,V> parent;
Comparator<? super K> cpr = comparator;
//如果比较器不为空,就是用指定的比较器来维护TreeMap的元素顺序
if (cpr != null) {
// do while循环,查找key要插入的位置(也就是新节点的父节点是谁)
do {
//一次循环结束时,parent 就是上次比过的对象
parent = t;
// 比较当前节点的key和新插入的key的大小
cmp = cpr.compare(key, t.key);
//新插入的key小的话,则以当前节点的左孩子节点为新的比较节点
if (cmp < 0)
t = t.left;
//新插入的key大的话,则以当前节点的右孩子节点为新的比较节点
else if (cmp > 0)
t = t.right;
//如果当前节点的key和新插入的key相等的话,则覆盖map的value,返回
else
return t.setValue(value);
} while (t != null); // t 为空,说明已经到叶子节点了
}
else {
// 如果比较器为空,则使用key作为比较器进行比较
// 这里要求key不能为空,并且必须实现Comparable接口
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);
}
// 找到新节点的父节点后,创建节点对象
Entry<K,V> e = new Entry<>(key, value, parent);
// 如果新节点key的值小于父节点key的值,则插在父节点的左侧
if (cmp < 0)
parent.left = e;
// 如果新节点key的值大于父节点key的值,则插在父节点的右侧
else
parent.right = e;
// 插入新的节点后,为了保持红黑树平衡,对红黑树进行调整
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
三、红黑树的删除原理及TreeMap的remove实现
相比添加,红黑树的删除显得更加复杂了。看下红黑树的删除需要哪几个步奏:
- 将红黑树当成一颗二叉查找树,将节点删除。
- 通过旋转和着色,使它恢复平衡,重新变成一颗符合规则的红黑树。
删除节点的关键是:
- 如果删除的是红色节点,不会违背红黑树的规则。
- 如果删除的是黑色节点,那么这个路径上就少了一个黑色节点,则违背了红黑树的规则。
来看下红黑树删除节点会有哪几种情况:
- 被删除的节点没有孩子节点,即叶子节点。可直接删除。
- 被删除的节点只有一个孩子节点,那么直接删除该节点,然后用它的孩子节点顶替它的位置。
- 被删除的节点有两个孩子节点。这种情况二叉树的删除有一个技巧,就是查找到要删除的节点X,接着我们找到它左子树的最大元素M,或者它右子树的最小元素M,交换X和M的值,然后删除节点M。
我们得到的结论是红黑树的删除遇到的主要问题就是被删除路径上的黑色节点减少,于是需要进行一系列旋转和着色。
TreeMap的查找与HashMap类似。
四、HashMap,LinkedHashMap、HashTable和TreeMap的区别
Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复会覆盖),但允许值重复。
1. HashMap
Hashmap是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的;
HashMap最多只允许一条记录的键为Null;允许多条记录的值为Null;
HashMap不支持线程的同步(非线程安全),即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致;
同步,可以用Collections的synchronizedMap方法HashMap具有同步的能力,或者使用ConcurrentHashMap。
在Map中插入、删除和定位元素,HashMap是最好的选择。
AbstractMap抽象类,(HashMap继承AbstractMap)覆盖了equals()和hashCode()方法以确保两个相等映射返回相同的哈希码。如果两个映射大小相等、包含同样的键且每个键在这两个映射中对应的值都相同,则这两个映射相等
2. HashTable
HashTable与HashMap类似,它不允许记录的键或者值为空;
支持线程的同步(线程安全),即任一时刻只有一个线程能写HashTable,因此导致了Hashtable在写入时会比较慢。
3. LinkedHashMap
LinkedHashMap是HashMap的一个子类;
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的;
4. TreeMap
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器。
TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
TreeMap基于红黑树实现,是非线程安全