一、TreeMap
TreeMap是有序的,可以实现自定义的比较器来决定升序还是降序。存储key-value,内部使用红黑树。
强烈建议先看之前的二叉搜索树和平衡二叉树,了解二叉搜索树和平衡二叉树再看这篇,否则红黑树有点难理解
二、TreeMap常用方法
2.1 属性
实现了NavigableMap接口,
而NavigableMap又继承于SortedMap,又此可知,这个map是有序的。因为大部分加入的都是对象,所以一般都要实现一个比较器来进行排序。
而NavigableMap会有一些导航方法,比如返回最接近的大于等于key的值,返回最接近的key的值等等
剩下的Cloneable和序列化接口就不解释了
2.2 属性
//定制排序规则,为null则是自然排序
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;
/**
* 给父节点构造一个子节点,默认黑色
*/
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
/**
* Returns the key.
*
* @return the key
*/
public K getKey() {
return key;
}
/**
* Returns the value associated with the key.
*
* @return the value associated with the key
*/
public V getValue() {
return value;
}
/**
* Replaces the value currently associated with the key with the given
* value.
*
* @return the value associated with the key before this method was
* called
*/
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
}
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
}
public String toString() {
return key + "=" + value;
}
}
保存了键值,以及其他节点的引用信息,还有默认颜色为黑色
2.3 红黑树的介绍
红黑树和平衡二叉树非常相似,但是平衡二叉树的条件非常苛刻,而起插入后需要旋转的次数是未知。而红黑树追求大致平衡,时间复杂度与平衡二叉树相近的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现比较简单。
红黑树除了二叉查找树的一般要求外,还需要以下要求:
- 节点是红色或黑色
- 根节点是黑色
- 每个叶子节点都是黑色的空节点(NIL节点)
- 每个红色节点的两个子节点都是黑色(从每个叶子到跟的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
这些约束强制了红黑树的关键性质:从跟到叶子的最长的可能路径不多于最短的可能路径的两倍长
如下所示就是一颗红黑树:
为了保持红黑树的以上特性,红黑树在加入元素时会有三个行动来保持自己:
左旋、右旋、着色
例如:加入元素14,破坏了红黑树的严格要求
注意刚加入的元素是红色,破坏了红黑树的要求4,因此需要先进行变色。为什么新插入的节点不是黑色,看源码。。。源码中新加入的节点都是红色
对元素15进行颜色变化,红色变为黑色,但是元素15和16都是黑色,还是破坏了要求5
后面继续进行颜色变化,就不解释了,看图一步步
暂时完成了颜色转化,但是元素13和元素16仍然出现了要求的破坏
此时树的整体是这样的
此时变色已经无法改变了,因此需要进行左旋转
进行左旋转
但是红黑树要求根节点必须为黑色
于是对根节点进行变色
对左子树的节点进行多次变色
但是还不满足要求5,因此此时还要进行旋转
先左旋
在右旋
旋转后,再将左子树进行变色
经过了变色-左旋转-变色-LR平衡旋转-变色后,终于成为了符合红黑树要求的红黑树
大致就是这样的过程,过程很复杂,不要过多纠结
2.4 构造方法
无参构造方法
创建一个空的treemap,比较器设置为null,使用自然排序
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);
}
传入SortedMap集合的构造方法
使用传入的SortedMap的比较器
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值必须实现比较器
2.5 常用方法
2.5.1 增加节点
public V put(K key, V value) {
//根节点
Entry<K,V> t = root;
//如果根节点为空,传入key-value添加至根节点
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
//cmp纪录比较结果
int cmp;
//设置双亲节点
Entry<K,V> parent;
// split comparator and comparable paths
//使用设置的比较器
Comparator<? super K> cpr = comparator;
//如果比较器不为空,开始查找要插入的节点
if (cpr != null) {
do {
//纪录当前节点,是要加入新节点的父节点
parent = t;
//比较新加入的key和当前节点的key值大小
cmp = cpr.compare(key, t.key);
if (cmp < 0)
//新插入的key小于当前节点,则作为当前节点的左孩子
t = t.left;
else if (cmp > 0)
//新插入的key大于当前节点,则作为当前节点的右孩子
t = t.right;
else
//当前节点的key和新插入的key相等的话,覆盖value,并返回旧value
return t.setValue(value);
} while (t != null);
}
//如果比较器为空
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);
}
//parent由上面找到,是要新插入节点的双亲节点
Entry<K,V> e = new Entry<>(key, value, parent);
//如果新插入的节点值小于双亲节点,则插在parent左边
if (cmp < 0)
parent.left = e;
else
//大于,插在右边
parent.right = e;
//调整红黑树
fixAfterInsertion(e);
//元素个数+1
size++;
modCount++;
return null;
}
调整红黑树
private void fixAfterInsertion(Entry<K,V> x) {
//新插入节点设置为红色
x.color = RED;
//确认节点x不为null,x不是根节点,x的双亲节点是红色,则需要调整
while (x != null && x != root && x.parent.color == RED) {
//以下是颜色转换
if (parentOf(x) == leftOf(parentOf(parentOf(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 {
Entry<K,V> y = leftOf(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 == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
//左单旋转
rotateLeft(parentOf(parentOf(x)));
}
}
}
//根节点必须为黑色
root.color = BLACK;
}
2.5.2 删除节点
之前在二叉搜索树中讲到,删除节点有3中情况,这里也是的,不多重复,可以去看二叉搜索树了解3种情况。当时是自己写的代码,比较粗糙,这里的源码写的比较好
这里删除的节点还会有红黑颜色的影响,删除红色节点无影响,删除黑色节点则会影响路径上总的黑色节点数量
public V remove(Object key) {
//根据k获得节点
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
//获得旧值
V oldValue = p.value;
//删除
deleteEntry(p);
//返回旧值
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// If strictly internal, copy successor's element to p and then make p
// point to successor.
//如果被删除的节点p有左右孩子
if (p.left != null && p.right != null) {
//找到p的替代节点s
Entry<K,V> s = successor(p);
//将s的key、value值赋给p
p.key = s.key;
p.value = s.value;
//p指向s节点
p = s;
} // p has 2 children
//找到s节点(p已经指向了s)的左孩子或右孩子
//二叉搜索树中说过,情况3现在已经被转化为了情况1或者情况2
// Start fixup at replacement node, if it exists.
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
//情况2:只有一个孩子
if (replacement != null) {
// Link replacement to parent
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;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
//调整红黑树
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else { // No children. Use self as phantom replacement and unlink.
//情况1:属于叶子节点
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;
}
}
}
//二叉搜索树中删除元素的情况3,详见那节的内容,这里不细说了
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
其实是很复杂的,建议从二叉搜索树、二叉平衡树,再看到红黑树,会比较容易理解。代码比较复杂,当时看懂了,过几天可能就忘了,理解了就好吧
2.5.3 查询
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
//如果比较器不为空,查找匀元素位置,返回节点
return getEntryUsingComparator(key);
//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;
}
TreeMap的方法基本上就讲这些了
三、总结
- TreeMap的底层实现是红黑树,他可以按照key值有序插入,查找比较快。
- key值必须实现comparable接口或者在TreeMap中传入自定义的比较器
- 非线程安全
- key值和value值不能为null
- 同HashMap和HashSet的关系一样,也有一个TreeSet,其底层是使用TreeMap实现的,这里就不说TreeMap了,可以参考之前讲过的HashSet