TreeMap
TreeMap底层采用红黑树保存每个Entry对象,红黑树是一种自平衡排序二叉树。
TreeMap添加元素源码:
public V put(K key, V value) {
// 获取红黑树根节点
Entry<K,V> t = root;
// 根节点为null,说明该红黑树为空树
if (t == null) {
// compare方法原意是比较两个key值的大小(源码见本方法后);
// 此处的用意应当是检测当前TreeMap对象是否提供定制排序对象(Comparator),
// 或者key对象类是否实现了Comparable接口;
compare(key, key); // type (and possibly null) check
// 新建Entry对象存储key、value
root = new Entry<>(key, value, null);
// 设置TreeMap对象中节点数目为1
size = 1;
// 增加树结构修改次数
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// 获取当前TreeMap对象的定制排序对象
Comparator<? super K> cpr = comparator;
// 若定制排序对象不为null,表明使用定制排序对象提供的compare()方法进行比较
if (cpr != null) {
do {
// 记录当前节点为下一节点的父节点,若第一次进入循环则当前节点为根节点
parent = t;
// 新插入key值与当前节点key进行比较
cmp = cpr.compare(key, t.key);
// cmp小于0说明新插入key值小于当前节点key值,新节点应该在当前节点的左子树
if (cmp < 0)
t = t.left;
// cmp大于0说明新插入key值大于当前节点key值,新节点应该在当前节点的右子树
else if (cmp > 0)
t = t.right;
// cmp等于0说明新插入key值与当前节点key值相等,新的value值覆盖原有的value
else
return t.setValue(value);
} while (t != null);
}
// 使用key对象实现的compareTo()方法进行比较
else {
// 若新插入的key对象为null,抛出NullPointException异常
if (key == null)
throw new NullPointerException();
// 主要目的是获取compareTo()方法
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
// 循环代码基本同上,只是由compare()方法换位compareTo()方法进行比较
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对象存储key、value值
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;
}
final int compare(Object k1, Object k2) {
return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
: comparator.compare((K)k1, (K)k2);
}
从上面源码我们可以看出:当使用key对象的compareTo()方法时,key值不允许为空值;当使用定制排序对象的compare()方法时,若key对象为null并不影响比较时key值可以为空值(笔者更倾向于这种情况是bug甚至是错误。若key为null且不影响比较,那不就意味着可以存储多个key等于null的键值对吗?这不与key值具有唯一性相悖了吗?)。
TreeMap删除元素源码
TreeMap删除元素代码较为复杂(不得不感叹于Java语言作者们逻辑之缜密、代码之简洁),要理解删除节点时有以3种情况:
- 被删除节点是叶子节点,此时只需要将其从父节点删除即可;
- 被删除节点只有左子树,此时将其父节点的左/右指针指向左子树即可;被删除节点只有右子树,将父节点的左/右指针指向右子树即可;(注意:被删除节点只有左子树不代表被删除节点是其父节点的左子节点)
- 被删除节点左右子树均非空,此时将右子树的最小节点值赋值给被删除节点,并删除右子树的最小节点。(类似与排序数组中删除中间某一元素后,其余元素向前补位,并将最后一位设置为空)
public V remove(Object key) {
// 获取待删除节点
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--;
// 被删除节点左右节点都不为null,节点p有两个子树
if (p.left != null && p.right != null) {
// 获取节点p右子树中的最小值,即中序遍历的后继节点
/*
successor()方法中逻辑较复杂(代码见下),但此时p.left != null && p.right != null,
所以successor(p)一定执行 else if(t.right!=null){……}分支
*/
Entry<K,V> s = successor(p);
// 修改节点p的值,且指向节点s
p.key = s.key;
p.value = s.value;
p = s;
}
/*
1.被删除节点为叶子节点,则replacement必定被赋值为null。若被删除节点为根节点则进入第二个分支,否则进入第三分支;
2.被删除节点只有一个子树,则replacement被赋值为非空子树根节点,进入第一个分支;
3.被删除节点拥有两个子树,则经先前处理此时的节点p已经指向被删除节点的后继节点。
*/
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) {
// 注意:TreeMap类中Entry不仅包含指向左右节点的指针,另有一个指向父节点的指针
// 节点p即将被删除,则其子结点的父节点指针应指向节点p的父节点
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的所有指针置为null
p.left = p.right = p.parent = null;
// 修复红黑树
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // 节点p的父节点为空说明节点p为根节点,且此时要删除节点p
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;
}
}
}
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
// 节点t存在右子树,获取右子树的最小值,即中序遍历的后继节点
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;
}
}
红黑树
排序二叉树虽然可以快速检索,但在最坏的情况下即插入节点集本身就是有序的,排序二叉树就会变为链表,其检索效率变差。
红黑树在排序二叉树的基础增加了如下5个性质:
-
每个节点要么是红色,要么是黑色;
-
根节点永远是黑色的;
-
所有的叶节点都是空节点,并且是黑色的;
-
每个红色节点的两个子结点都是黑色(从每个叶节点到根节点的路径上不会有两个连续的红色节点);
-
从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
TreeSet
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
// 使用NavigableMap的key保存元素
private transient NavigableMap<E,Object> m;
// 使用常量PRESENT填充NavigableMap中的value
private static final Object PRESENT = new Object();
// default访问权限构造器
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet() {
// 创建自然排序的TreeMap,调用上面的构造器
// TreeMap是NavigableMap接口的实现类
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
// 创建定制排序的TreeMap
this(new TreeMap<>(comparator));
}
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
……
}
从上面代码可以看出:TreeSet的所有构造器都是新建一个TreeMap对象作为实际存储Set元素的容器,由此可见,TreeSet底层实际使用的存储容器就是TreeMap。与HashSet完全类似,TreeSet中大部分方法都是直接调用TreeMap的方法实现的。