先看看TreeMap的继承关系:
TreeMap是一个基于“红黑树”实现的导航映射(NavigableMap
)。其映射的Key根据自然顺序排列,或者根据映射创建时提供的比较器排列。
由于TreeMap继承了SortedMap,这要求TreeMap的key必须是可比较的:要么提供compareTo()
方法(实现Comparable
)接口,要么在映射初始化的时候提供比较器。
主意,TreeMap时线程不安全的,但可以使用包装方法得到线程安全的TreeMap实例:
SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));
红黑树
二叉树
二叉树是树的特殊一种,具有如下特点
- 每个结点最多有两颗子树
- 左子树和右子树是有顺序的,次序不能颠倒
即使某结点只有一个子树,也要区分左右子树
二叉树遍历:从树的根节点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问仅且一次。
这里有两个关键词:访问和次序。
二叉树常见的访问方式:
二叉树的前序遍历
基本思想:先访问根结点,再先序遍历左子树,最后再先序遍历右子树即:根->左->右。
上图中的的元素访问顺序: 1 -> 2 -> 4 -> 5 -> 7 -> 8 -> 3 -> 6。
二叉树的中序遍历
基本思想:先中序遍历左子树,然后再访问根结点,最后再中序遍历右子树即:左 -> 根 -> 右。
上图中的二叉树安中序遍历的访问顺序是: 4 -> 2 -> 7 -> 8 -> 5 -> 1 -> 3 -> 6。
二叉树的后序遍历
基本思想:先后序遍历左子树,然后再后序遍历右子树,最后再访问根结点即:左 -> 右 -> 根。
上图中的二叉树按后序遍历的结果是:4 -> 8 -> 7 -> 5 -> 2 -> 6 -> 3 -> 1。
红黑树的概念
R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的特性:
- 每个节点或者是黑色,或者是红色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色,注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点。
- 如果一个节点是红色的,则它的子节点必须是黑色的。
从一个节点到该节点子树包含的的叶子节点的所有路径上包含相同数目的黑节点。
如下图示:
红黑树的时间复杂度为O(lgn),因此应用非常广泛,如本文中的TreeMap。
红黑树的基本操作
红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。
旋转包括两种:左旋 和 右旋。
左旋与右旋
当在某个节点x进行左旋时,假设它的右孩子节点y不是T.nil,x可以为其右孩子不是T.nil节点的树内任意节点,左旋x到y的链,使得y成为该子树新的根节点,x成为y的左孩子,y的左孩子成为x的右孩子,x的左孩子仍旧为x的左孩子。
左旋的伪代码:
y = x.right // set y
x.right = y.left // 将y的左孩子设置为x的右孩子
if y.left != T.nil
y.left.parent = x
y.parent = x.parent
if x.parent == T.nil
T.root = y
else if x == x.parent.left
x.parent.left = y
else
x.parent.right = y
y.left = x
x.parent = y
对x进行左旋,意味着,将“x的右孩子”设为“x的父亲节点”;即,将 x变成了一个左节点。 因此,左旋中的“左”,意味着“被旋转的节点将变成一个左节点”。
对x进行右旋,意味着,将“x的左孩子”设为“x的父亲节点”;即,将 x变成了一个右节点。因此,右旋中的“右”,意味着“被旋转的节点将变成一个右节点”。
插入
将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。详细描述如下:
第一步: 将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
第二步:将插入的节点着色为”红色”。
将插入的节点着色为红色,不会违背”特性(5)”。少违背一条特性,就意味着我们需要处理的情况越少。
第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
第二步中,将插入节点着色为”红色”之后,不会违背”特性(5)”。那它到底会违背哪些特性呢?
对于”特性(1)”,显然不会违背了。因为我们已经将它涂成红色了。
对于”特性(2)”,显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。
对于”特性(3)”,显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
对于”特性(4)”,是有可能违背的!
那接下来,想办法使之”满足特性(4)”,就可以将树重新构造成红黑树了。
其它操作
待续
TreeMap实现介绍
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;
/**
* Make a new cell with given key, value, and parent, and with
* {@code null} child links, and BLACK color.
*/
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;
}
}
TreeMap中红黑树的左旋与右旋
/** From CLR */
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}
/** From CLR */
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left;
p.left = l.right;
if (l.right != null) l.right.parent = p;
l.parent = p.parent;
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else p.parent.left = l;
l.right = p;
p.parent = l;
}
}
应用示例
示例1:
public class ScatterChartSerializer extends JsonSerializer<ScatterChart> {
@Override
public void serialize(ScatterChart scatterChart, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeStartObject();
//construct Treemap instance with an comparator
Map<String, Integer> rangeMap = new TreeMap<>(Comparator.comparingInt(OrderedElapsedRanger::getOrder).reversed());
List<ScatterData> scatterDataList = scatterChart.getScatterData();
if (null != scatterDataList && !scatterDataList.isEmpty()) {
jsonGenerator.writeFieldName("data");
jsonGenerator.writeStartArray();
for (ScatterData scatterData : scatterDataList) {
Map<String, ScatterAggregation> scatterMap = scatterData.getRangeMap();
if (null == scatterMap || scatterMap.isEmpty()) {
continue;
}
for (Map.Entry<String, ScatterAggregation> entry : scatterMap.entrySet()) {
jsonGenerator.writeStartArray();
//do something serializing metadata
Integer count = rangeMap.get(entry.getKey());
// insert kv map and sort them
if (count == null) {
rangeMap.put(entry.getKey(), entry.getValue().getCount());
} else {
rangeMap.put(entry.getKey(), count + entry.getValue().getCount());
}
jsonGenerator.writeEndArray();
}
}
jsonGenerator.writeEndArray();
jsonGenerator.writeFieldName("range");
jsonGenerator.writeStartArray();
//encounter sorted kv map
for (Map.Entry<String, Integer> entry : rangeMap.entrySet()) {
//do something serializing elapsed range
}
jsonGenerator.writeEndArray();
}
jsonGenerator.writeEndObject();
}
}
示例2
public class TreeMapTest {
public static void main(String[] args) {
Map<Integer, String> map = new TreeMap<>();
map.put(1, "2");
map.put(32, "32");
map.put(22, "22");
map.put(8, "8");
map.put(12, "12");
for (Map.Entry<Integer, String> integerStringEntry : map.entrySet()) {
System.out.println("key = " + integerStringEntry.getKey());
System.out.println("value = " + integerStringEntry.getValue());
}
}
}
输出:
"D:\Program Files\Java\bin\java" ......
key = 1
value = 2
key = 8
value = 8
key = 12
value = 12
key = 22
value = 22
key = 32
value = 32
Process finished with exit code 0