TreeMap
文章目录
TreeMap最早出现在
JDK 1.2中,是 Java 集合框架中比较重要一个的实现。
TreeMap 底层基于红黑树实现,可保证在log(n)时间复杂度内完成 containsKey、get、put 和 remove 操作
另一方面,由于 TreeMap 基于红黑树实现,这为 TreeMap 保持键的有序性打下了基础。
总的来说,TreeMap 的核心是红黑树,其很多方法也是对红黑树增删查基础操作的一个包装。
所以:知道红黑树,就会了TreeMap
一:TreeMap概述
TreeMap继承自AbstractMap,并实现了 NavigableMap接口。
NavigableMap 接口继承了SortedMap接口,SortedMap和AbstractMap 最终继承自Map接口

这里来简单说一下继承体系中不常见的接口NavigableMap和SortedMap,这两个接口见名知意。先说 NavigableMap 接口,NavigableMap 接口声明了一些列具有导航功能的方法,比如:
/**
* 返回红黑树中最小键所对应的 Entry
*/
Map.Entry<K,V> firstEntry();
/**
* 返回最大的键 maxKey,且 maxKey 仅小于参数 key
*/
K lowerKey(K key);
/**
* 返回最小的键 minKey,且 minKey 仅大于参数 key
*/
K higherKey(K key);
// 其他略
通过这些导航方法,我们可以快速定位到目标的 key 或 Entry。
至于 SortedMap 接口,这个接口提供了一些基于有序键的操作,比如
/**
* 返回包含键值在 [minKey, toKey) 范围内的 Map
*/
SortedMap<K,V> headMap(K toKey);();
/**
* 返回包含键值在 [fromKey, toKey) 范围内的 Map
*/
SortedMap<K,V> subMap(K fromKey, K toKey);
// 其他略
以上就是两个接口的介绍,很简单。至于 AbstractMap 和 Map 这里就不说了,可以看下JavaDoc
二:源码分析
在看源码之前,先看几个关键的知识点部分
1:每一个元素的内部有哪些属性
在TreeMap源码类中有一个Entry内部类,这里存储着treeMap中每一个元素的内部是怎样的。

K key; // 键
V value; // 值
Entry<K,V> left; // 记录左子节点的地址值
Entry<K,V> right; // 记录右子节点的地址值
Entry<K,V> parent; // 记录父节点的地址值
boolean color = BLACK; // 记录节点的颜色
在这为什么默认节点是黑色的呢?
先来看上面用 private static final 定义的两个常量:RED、BLACK
此时就发现了,这里的 RED 跟 BLACK 是表示颜色,但是本质上只是常量的颜色而已。
private static final boolean RED = false;
private static final boolean BLACK = true;
如果在赋值的时候赋值 true / false,那就很难理解了,true 表示什么?false 表示什么?一点也不好理解
因此Java就使用了这种常量的方式提高了代码的阅读性。
此时不用看其他地方,直接看 boolean color = BLACK; 这一行代码,我们就知道了,创建一个 Entry对象 的时候,里面节点颜色的初始化值是黑色的,因此它的作用是为了提高代码的阅读性。
2:TreeMap的成员变量
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
/**
* The comparator used to maintain order in this tree map, or
* null if it uses the natural ordering of its keys.
*
* @serial
*/
private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
/**
* The number of entries in the tree
*/
private transient int size = 0;
/**
* The number of structural modifications to the tree.
*/
private transient int modCount = 0;
// other code...
}
可见TreeMap 中一共有三个成员变量需要我们知道。
1、comparator:表示比较的规则。
2、root:记录红黑树根节点的地址值,并且类型是 Entry,因此它就表示根节点。
3、size:表示集合的长度,也表示红黑树中节点的个数。
3:构造方法
// 空参构造非常的简单,它里面没有传递任何的数据,在方法体中将 null 赋值给了 comparator。
// 因此当我们用空参构造来创建对象的时候,这里的 comparator 记录的值就是 null,表示没有比较器对象。
public TreeMap() {
comparator = null;
}
// 带参构造就是我们在调用的时候要传递比较器对象,然后将传递过来的比较器对象赋值给成员变量 comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
// 如果传入的基本Map对象,将比较器置为null,然后调用putAll方法,将m中的数据全部放入
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
// 如果传入的是SortedMap对象,将会使用m所使用的比较器
// 然后尝试使用buildFromSorted方法进行放入到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) {
}
}
4:查找 - get()方法剖析
查找操作的核心逻辑就是getEntry方法中的while循环,具体如下:
public V get(Object key) {
// 调用getEntry方法
Entry<K,V> p = getEntry(key);
// 如果可以找到key对应的值,将返回对应的value,否则返回null
return (p==null ? null : p.value);
}
/**
* 返回此映射中给定键的条目,如果映射中不包含该键的条目,则返回 {@code null}。
* 该方法首先检查是否使用了比较器进行键比较;如果是,则调用另一个方法来处理。
* 如果键为 null,则抛出 NullPointerException。
* 如果键不为 null,则将键转换为 Comparable 类型,并使用二叉搜索方法在映射中查找。
* 如果找到键,则返回对应的条目;否则,返回 null。
*
* @param key 要查找的键
* @return 此映射中给定键的条目,如果映射中不包含该键的条目,则返回 {@code null}
* @throws ClassCastException 如果指定的键无法与映射中的当前键进行比较
* @throws NullPointerException 如果指定的键为 null,并且此映射使用自然排序,或其比较器不允许 null 键
*/
final Entry<K,V> getEntry(Object key) {
// 如果使用了比较器,则调用专门处理比较器的版本以提高性能
if (comparator != null)
return getEntryUsingComparator(key);
// 如果键为 null,则抛出 NullPointerException
if (key == null)
throw new NullPointerException();
// 将键强制转换为 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)
p = p.left;
// 如果键大于当前节点的键,向右子树继续搜索
else if (cmp > 0)
p = p.right;
// 如果键等于当前节点的键,返回当前节点
else
return p;
}
// 如果未找到键,返回 null
return null;
}
5:遍历 - foreach操作剖析
遍历操作也是大家使用频率较高的一个操作,对于TreeMap,使用方式一般如下:
for(Object key : map.keySet()) {
// do something
}
for(Map.Entry entry : map.entrySet()) {
// do something
}
从上面代码片段中可以看出,大家一般都是对 TreeMap 的 key 集合或 Entry 集合进行遍历。
上面代码片段中用 foreach 遍历 keySet 方法产生的集合,在编译时会转换成用迭代器遍历,等价于:
Set keys = map.keySet();
Iterator ite = keys.iterator(); // 拿到对应的迭代器
while (ite.hasNext()) {
Object key = ite.next();
// do something
}
另一方面,TreeMap 有一个特性,即可以保证键的有序性,默认是正序。
所以在遍历过程中会发现 TreeMap 会从小到大输出键的值。
那么,接下来就来分析一下keySet方法,以及在遍历 keySet 方法产生的集合时,TreeMap 是如何保证键的有序性的
public Set<K> keySet() {
return navigableKeySet();
}
public NavigableSet<K> navigableKeySet() {
KeySet<K> nks = navigableKeySet;
return (nks != null) ? nks : (navigableKeySet = new KeySet<>(this));
}
static final class KeySet<E> extends AbstractSet<E> implements NavigableSet<E> {
private final NavigableMap<E, ?> m;
KeySet(NavigableMap<E,?> map) { m = map; }
/**
* 返回集合的迭代器
* 此方法根据m的类型选择合适的迭代器,以确保高效迭代
* 如果m是TreeMap类型,则直接使用TreeMap的keyIterator方法
* 否则,认为m是TreeMap.NavigableSubMap类型,并使用其keyIterator方法
*
* @return 根据m的类型返回TreeMap或NavigableSubMap的键迭代器
*/
public Iterator<E> iterator() {
if (m instanceof TreeMap)
return ((TreeMap<E,?>)m).keyIterator();
else
return ((TreeMap.NavigableSubMap<E,?>)m).keyIterator();
}
// 省略非关键代码
}
Iterator<K> keyIterator() {
return new KeyIterator(getFirstEntry());
}
final class KeyIterator extends PrivateEntryIterator<K> {
KeyIterator(Entry<K,V> first) {
super(first);
}
public K next() {
return nextEntry().key;
}
}
abstract class PrivateEntryIterator<T> implements Iterator<T> {
Entry<K,V> next;
Entry<K,V> lastReturned;
int expectedModCount;
PrivateEntryIterator(Entry<K,V> first) {
expectedModCount = modCount;
lastReturned = null;
next = first;
}
public final boolean hasNext() {
return next != null;
}
/**
* 获取下一个条目
*
* 此方法用于迭代集合中的下一个元素它首先检查是否有下一个元素,如果没有,则抛出异常
* 还检查自迭代开始以来集合是否已被修改,如果是,则抛出异常这可以确保迭代过程的一致性
* 最后,它将下一个要返回的元素设置为当前元素的后继,并更新最后返回的元素
*
* @return 下一个条目
* @throws NoSuchElementException 如果没有下一个元素
* @throws ConcurrentModificationException 如果集合在迭代过程中被修改
*/
final Entry<K,V> nextEntry() {
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
next = successor(e);
lastReturned = e;
return e;
}
// 其他方法省略
}
上面的代码比较多,keySet 涉及的代码还是比较多的,大家可以从上往下看。
从上面源码可以看出 keySet 方法返回的是KeySet类的对象。这个类实现了Iterable接口,可以返回一个迭代器。
该迭代器的具体实现是KeyIterator,而 KeyIterator 类的核心逻辑是在PrivateEntryIterator中实现的。
上面的代码虽多,但核心代码还是 KeySet 类和 PrivateEntryIterator 类的 nextEntry方法。
6:插入 - put()方法剖析
相对于前两个操作,插入操作明显要复杂一些。当往 TreeMap 中放入新的键值对后,可能会破坏红黑树的性质。这里为了描述方便,把 Entry 称为节点。并把新插入的节点称为N,N 的父节点为P。P 的父节点为G,且 P 是 G 的左孩子。P 的兄弟节点为U。在往红黑树中插入新的节点 N 后(新节点为红色),会产生下面5种情况:
- N 是根节点
- N 的父节点是黑色
- N 的父节点是红色,叔叔节点也是红色
- N 的父节点是红色,叔叔节点是黑色,且 N 是 P 的右孩子
- N 的父节点是红色,叔叔节点是黑色,且 N 是 P 的左孩子
上面5中情况中,情况2不会破坏红黑树性质,所以无需处理。情况1 会破坏红黑树性质2(根是黑色),情况3、4、和5会破坏红黑树性质4(每个红色节点必须有两个黑色的子节点)。这个时候就需要进行调整,以使红黑树重新恢复平衡
/**
* 将指定的值与指定的键在此映射中关联如果映射之前包含该键的映射,则旧值将被替换
*
* @param key 要关联指定值的键
* @param value 要与指定键关联的值
*
* @return 之前与键关联的值,如果没有映射该键,则返回{@code null} (一个{@code null}返回值也可以表示映射之前将{@code null}与键关联)
* @throws ClassCastException 如果指定的键不能与当前映射中的键进行比较
* @throws NullPointerException 如果指定的键为null,并且此映射使用自然顺序,或者其比较器不允许null键
*/
public V put(K key, V value) {
Entry<K,V> t = root;
// 如果根节点为空,直接创建新节点作为根节点
if (t == null) {
compare(key, key); // 类型(和可能的空)检查
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// 分割比较器和可比较路径
Comparator<? super K> cpr = comparator;
if (cpr != null) {
// 使用比较器比较键
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
// 使用自然顺序比较键
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);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 如果红黑树的性质被破坏了,将通过这个方法进行旋转恢复
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
/**
* 插入节点后修复红黑树的性质
* 此方法确保红黑树在插入新节点后仍然保持其性质,主要包括确保没有两个红色节点连续以及根节点为黑色
*
* @param x 刚刚插入的节点,该节点在插入时被标记为红色
*/
private void fixAfterInsertion(Entry<K,V> x) {
// 将新插入的节点标记为红色
x.color = RED;
// 当新插入的节点不是根节点且其父节点为红色时,进行修复
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;
}
7:删除 - remove()方法剖析
删除操作是红黑树最复杂的部分,原因是该操作可能会破坏红黑树性质5(从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点),修复性质5要比修复其他性质(性质2和4需修复,性质1和3不用修复)复杂的多。
当删除操作导致性质5被破坏时,会出现8种情况。
为了方便表述,这里还是先做一些假设。我们把最终被删除的节点称为 X,X 的替换节点称为 N。
N 的父节点为P,且 N 是 P 的左孩子。N 的兄弟节点为S,S 的左孩子为 SL,右孩子为 SR。
这里特地强调 X 是 最终被删除 的节点,是原因二叉查找树会把要删除有两个孩子的节点的情况转化为删除只有一个孩子的节点的情况,该节点是欲被删除节点的前驱和后继。
/**
* 从当前的TreeMap中移除指定键的映射关系,如果存在的话。
*
* @param key 需要移除映射关系的键
* @return 返回与指定键关联的前一个值,如果不存在该键的映射关系则返回{@code null}。
* (返回值为{@code null}也可能表示该键之前与{@code null}关联)
* @throws ClassCastException 如果指定的键无法与当前映射中的键进行比较
* @throws NullPointerException 如果指定的键为null,并且此映射使用自然排序,或其比较器不允许null键
*/
public V remove(Object key) {
// 查找指定键对应的条目
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
// 获取并保存旧值
V oldValue = p.value;
// 删除条目
deleteEntry(p);
return oldValue;
}
/**
* 删除节点 p,并重新平衡树。
*
* @param p 要删除的节点
*/
private void deleteEntry(Entry<K,V> p) {
// 增加树的修改计数,表示结构发生了变化
modCount++;
// 减少树的大小,因为一个节点将被移除
size--;
// 如果节点 p 是内部节点(有两个子节点),则将后继节点的值复制到 p 并将 p 指向后继节点
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p 有两个子节点
// 从 p 的左子节点或右子节点中选择一个作为替换节点
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 的链接,以便 fixAfterDeletion 方法可以安全地使用
p.left = p.right = p.parent = null;
// 如果 p 是黑色节点,则需要重新平衡树
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // 如果 p 是唯一的节点,则将根节点设置为 null
root = null;
} else { // 如果 p 没有子节点,则使用自身作为虚拟替换节点并断开链接
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;
}
}
}
/**
* 修复删除操作后的红黑树性质
* 当删除一个节点后,可能会破坏红黑树的性质,此方法用于恢复红黑树的性质
*
* @param x 删除节点后,用于修复的起始节点
*/
private void fixAfterDeletion(Entry<K,V> x) {
// 当x不是根节点且颜色为黑色时,需要进行修复
while (x != root && colorOf(x) == BLACK) {
// 判断x是其父节点的左子节点还是右子节点
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
// 情况1: 兄弟节点sib是红色
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
// 情况2: 兄弟节点sib的两个子节点都是黑色
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
// 情况3: 兄弟节点sib的右子节点是黑色
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
// 情况4: 兄弟节点sib的右子节点是红色
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // 对称情况
Entry<K,V> sib = leftOf(parentOf(x));
// 对称情况1: 兄弟节点sib是红色
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
// 对称情况2: 兄弟节点sib的两个子节点都是黑色
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
// 对称情况3: 兄弟节点sib的左子节点是黑色
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
// 对称情况4: 兄弟节点sib的左子节点是红色
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}
// 最后将x设为黑色,完成修复
setColor(x, BLACK);
}
从源码中可以看出,remove方法只是一个简单的保证
核心实现在deleteEntry方法中。deleteEntry 主要做了这么几件事:
- 如果待删除节点 P 有两个孩子,则先找到 P 的后继 S,然后将 S 中的值拷贝到 P 中,并让 P 指向 S
- 如果最终被删除节点 P(P 现在指向最终被删除节点)的孩子不为空,则用其孩子节点替换掉
- 如果最终被删除的节点是黑色的话,调用 fixAfterDeletion 方法进行修复
上面说的略微啰嗦,如果简单说的话,7个字即可总结:找后继 -> 替换 -> 修复【fixAfterDeletion】

4449

被折叠的 条评论
为什么被折叠?



