欢迎关注个人公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一块儿畅游源码的海洋。java
TreeMap使用红黑树存储元素,能够保证元素按key值的大小进行遍历。git
TreeMap实现了Map、SortedMap、NavigableMap、Cloneable、Serializable等接口。app
SortedMap规定了元素能够按key的大小来遍历,它定义了一些返回部分map的方法。ui
public interface SortedMap<K,V> extends Map<K,V> {
// key的比较器
Comparator<? super K> comparator();
// 返回fromKey(包含)到toKey(不包含)之间的元素组成的子map
SortedMap<K,V> subMap(K fromKey, K toKey);
// 返回小于toKey(不包含)的子map
SortedMap<K,V> headMap(K toKey);
// 返回大于等于fromKey(包含)的子map
SortedMap<K,V> tailMap(K fromKey);
// 返回最小的key
K firstKey();
// 返回最大的key
K lastKey();
// 返回key集合
Set<K> keySet();
// 返回value集合
Collection<V> values();
// 返回节点集合
Set<Map.Entry<K, V>> entrySet();
}
复制代码
NavigableMap是对SortedMap的加强,定义了一些返回离目标key最近的元素的方法。this
public interface NavigableMap<K,V> extends SortedMap<K,V> {
// 小于给定key的最大节点
Map.Entry<K,V> lowerEntry(K key);
// 小于给定key的最大key
K lowerKey(K key);
// 小于等于给定key的最大节点
Map.Entry<K,V> floorEntry(K key);
// 小于等于给定key的最大key
K floorKey(K key);
// 大于等于给定key的最小节点
Map.Entry<K,V> ceilingEntry(K key);
// 大于等于给定key的最小key
K ceilingKey(K key);
// 大于给定key的最小节点
Map.Entry<K,V> higherEntry(K key);
// 大于给定key的最小key
K higherKey(K key);
// 最小的节点
Map.Entry<K,V> firstEntry();
// 最大的节点
Map.Entry<K,V> lastEntry();
// 弹出最小的节点
Map.Entry<K,V> pollFirstEntry();
// 弹出最大的节点
Map.Entry<K,V> pollLastEntry();
// 返回倒序的map
NavigableMap<K,V> descendingMap();
// 返回有序的key集合
NavigableSet<K> navigableKeySet();
// 返回倒序的key集合
NavigableSet<K> descendingKeySet();
// 返回从fromKey到toKey的子map,是否包含起止元素能够本身决定
NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive);
// 返回小于toKey的子map,是否包含toKey本身决定
NavigableMap<K,V> headMap(K toKey, boolean inclusive);
// 返回大于fromKey的子map,是否包含fromKey本身决定
NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);
// 等价于subMap(fromKey, true, toKey, false)
SortedMap<K,V> subMap(K fromKey, K toKey);
// 等价于headMap(toKey, false)
SortedMap<K,V> headMap(K toKey);
// 等价于tailMap(fromKey, true)
SortedMap<K,V> tailMap(K fromKey);
}
复制代码
TreeMap只使用到了红黑树,因此它的时间复杂度为O(log n),咱们再来回顾一下红黑树的特性。spa
(1)每一个节点或者是黑色,或者是红色。指针
(2)根节点是黑色。code
(3)每一个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)排序
(4)若是一个节点是红色的,则它的子节点必须是黑色的。继承
(5)从一个节点到该节点的子孙节点的全部路径上包含相同数目的黑节点。
/** * 比较器,若是没传则key要实现Comparable接口 */
private final Comparator<? super K> comparator;
/** * 根节点 */
private transient Entry<K,V> root;
/** * 元素个数 */
private transient int size = 0;
/** * 修改次数 */
private transient int modCount = 0;
复制代码
(1)comparator
按key的大小排序有两种方式,一种是key实现Comparable接口,一种方式经过构造方法传入比较器。
(2)root
根节点,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;
}
复制代码
/** * 默认构造方法,key必须实现Comparable接口 */
public TreeMap() {
comparator = null;
}
/** * 使用传入的comparator比较两个key的大小 */
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
/** * key必须实现Comparable接口,把传入map中的全部元素保存到新的TreeMap中 */
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
/** * 使用传入map的比较器,并把传入map中的全部元素保存到新的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) {
}
}
复制代码
构造方法主要分红两类,一类是使用comparator比较器,一类是key必须实现Comparable接口。
其实,笔者认为这两种比较方式能够合并成一种,当没有传comparator的时候,能够用如下方式来给comparator赋值,这样后续全部的比较操做均可以使用同样的逻辑处理了,而不用每次都检查comparator为空的时候又用Comparable来实现一遍逻辑。
// 若是comparator为空,则key必须实现Comparable接口,因此这里确定能够强转
// 这样在构造方法中统一替换掉,后续的逻辑就都一致了
comparator = (k1, k2) -> ((Comparable<? super K>)k1).compareTo(k2);
复制代码
获取元素,典型的二叉查找树的查找方法。
public V get(Object key) {
// 根据key查找元素
Entry<K,V> p = getEntry(key);
// 找到了返回value值,没找到返回null
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// 若是comparator不为空,使用comparator的版本获取元素
if (comparator != null)
return getEntryUsingComparator(key);
// 若是key为空返回空指针异常
if (key == null)
throw new NullPointerException();
// 将key强转为Comparable
@SuppressWarnings("unchecked")
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)
// 若是小于0从左子树查找
p = p.left;
else if (cmp > 0)
// 若是大于0从右子树查找
p = p.right;
else
// 若是相等说明找到了直接返回
return p;
}
// 没找到返回null
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)
// 若是小于0从左子树查找
p = p.left;
else if (cmp > 0)
// 若是大于0从右子树查找
p = p.right;
else
// 若是相等说明找到了直接返回
return p;
}
}
// 没找到返回null
return null;
}
复制代码
(1)从root遍历整个树;
(2)若是待查找的key比当前遍历的key小,则在其左子树中查找;
(3)若是待查找的key比当前遍历的key大,则在其右子树中查找;
(4)若是待查找的key与当前遍历的key相等,则找到了该元素,直接返回;
(5)从这里能够看出是否有comparator分化成了两个方法,可是内部逻辑如出一辙,所以可见笔者comparator = (k1, k2) -> ((Comparable<? super K>)k1).compareTo(k2);
这种改造的必要性。
我是一条美丽的分割线,前方高能,请作好准备。
(1)每一个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每一个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)
(4)若是一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的全部路径上包含相同数目的黑节点。
左旋,就是以某个节点为支点向左旋转。
整个左旋过程以下:
(1)将 y的左节点 设为 x的右节点,即将 β 设为 x的右节点;
(2)将 x 设为 y的左节点的父节点,即将 β的父节点 设为 x;
(3)将 x的父节点 设为 y的父节点;
(4)若是 x的父节点 为空节点,则将y设置为根节点;若是x是它父节点的左(右)节点,则将y设置为x父节点的左(右)节点;
(5)将 x 设为 y的左节点;
(6)将 x的父节点 设为 y;
让咱们来看看TreeMap中的实现:
/** * 以p为支点进行左旋 * 假设p为图中的x */
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
// p的右节点,即y
Entry<K,V> r = p.right;
// (1)将 y的左节点 设为 x的右节点
p.right = r.left;
// (2)将 x 设为 y的左节点的父节点(若是y的左节点存在的话)
if (r.left != null)
r.left.parent = p;
// (3)将 x的父节点 设为 y的父节点
r.parent = p.parent;
// (4)...
if (p.parent == null)
// 若是 x的父节点 为空,则将y设置为根节点
root = r;
else if (p.parent.left == p)
// 若是x是它父节点的左节点,则将y设置为x父节点的左节点
p.parent.left = r;
else
// 若是x是它父节点的右节点,则将y设置为x父节点的右节点
p.parent.right = r;
// (5)将 x 设为 y的左节点
r.left = p;
// (6)将 x的父节点 设为 y
p.parent = r;
}
}
复制代码
右旋,就是以某个节点为支点向右旋转。
整个右旋过程以下:
(1)将 x的右节点 设为 y的左节点,即 将 β 设为 y的左节点;
(2)将 y 设为 x的右节点的父节点,即 将 β的父节点 设为 y;
(3)将 y的父节点 设为 x的父节点;
(4)若是 y的父节点 是 空节点,则将x设为根节点;若是y是它父节点的左(右)节点,则将x设为y的父节点的左(右)节点;
(5)将 y 设为 x的右节点;
(6)将 y的父节点 设为 x;
让咱们来看看TreeMap中的实现:
/** * 以p为支点进行右旋 * 假设p为图中的y */
private void rotateRight(Entry<K,V> p) {
if (p != null) {
// p的左节点,即x
Entry<K,V> l = p.left;
// (1)将 x的右节点 设为 y的左节点
p.left = l.right;
// (2)将 y 设为 x的右节点的父节点(若是x有右节点的话)
if (l.right != null) l.right.parent = p;
// (3)将 y的父节点 设为 x的父节点
l.parent = p.parent;
// (4)...
if (p.parent == null)
// 若是 y的父节点 是 空节点,则将x设为根节点
root = l;
else if (p.parent.right == p)
// 若是y是它父节点的右节点,则将x设为y的父节点的右节点
p.parent.right = l;
else
// 若是y是它父节点的左节点,则将x设为y的父节点的左节点
p.parent.left = l;
// (5)将 y 设为 x的右节点
l.right = p;
// (6)将 y的父节点 设为 x
p.parent = l;
}
}
复制代码
未完待续,下一节咱们一块儿探讨红黑树插入元素的操做。
如今公众号文章没办法留言了,若是有什么疑问或者建议请直接在公众号给我留言。
欢迎关注个人公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一块儿畅游源码的海洋。