Java|Java 7之集合类型第5篇 - TreeMap

转载请注明出处:http://blog.csdn.net/mazhimazh/article/details/19028311



TreeMap 的实现使用了红黑树数据结构,也就是一棵自平衡的排序二叉树,这样就可以保证快速检索指定节点。对于 TreeMap 而言,它采用一种被称为“红黑树”的排序二叉树来保存 Map 中每个 Entry —— 每个 Entry 都被当成“红黑树”的一个节点对待。举例:


public class TreeMapTest{ public static void main(String[] args) { TreeMap map =new TreeMap(); map.put("ccc" , 89.0); map.put("aaa" , 80.0); map.put("zzz" , 80.0); map.put("bbb" , 89.0); System.out.println(map); } }

当程序执行 map.put("ccc" , 89.0); 时,系统将直接把 "ccc"-89.0 这个 Entry 放入 Map 中,这个 Entry 就是该“红黑树”的根节点。接着程序执行 map.put("aaa" , 80.0); 时,程序会将 "aaa"-80.0 作为新节点添加到已有的红黑树中。
以后每向 TreeMap 中放入一个 key-value 对,系统都需要将该 Entry 当成一个新节点,添加成已有红黑树中,通过这种方式就可保证 TreeMap 中所有 key 总是按某种规则排列。例如我们输出上面程序,将看到如下结果(所有 key 由小到大地排列):
{aaa=80.0, bbb=89.0, ccc=89.0, zzz=80.0}


(1)节点插入操作

红黑树是一种自平衡排序二叉树,树中每个节点的值,都大于或等于在它的左子树中的所有节点的值,并且小于或等于在它的右子树中的所有节点的值,这确保红黑树运行时可以快速地在树中查找和定位所需节点。
缺点:
对于 TreeMap 而言,由于它底层采用一棵“红黑树”来保存集合中的 Entry,这意味这 TreeMap 添加元素、取出元素的性能都比 HashMap 低:

  • 当 TreeMap 添加元素时,需要通过循环找到新增 Entry 的插入位置,因此比较耗性能
  • 当从 TreeMap 中取出元素时,需要通过循环才能找到合适的 Entry,也比较耗性能
优点:
但 TreeMap、TreeSet 比 HashMap、HashSet 的优势在于:TreeMap 中的所有 Entry 总是按 key 根据指定排序规则保持有序状态,TreeSet 中所有元素总是根据指定排序规则保持有序状态。

TreeMap 添加节点(TreeMap 中使用 Entry 内部类代表节点)的实现,TreeMap 集合的 put(K key, V value) 方法实现了将 Entry 放入排序二叉树中,下面是该方法的源代码:

public V put(K key, V value) { Entry t = root; // 先以 t 保存链表的 root 节点,树是由链表来实现的 if (t == null) {// 如果 t==null,表明是一个空链表,即该 TreeMap 里没有任何 Entry root = new Entry(key, value, null); // 使用key-value 创建一个新Entry,并将该 Entry 作为 root size = 1; // 设置该 Map 集合的 size 为 1 modCount++; // 记录修改次数为 1 return null; } int cmp; Entry parent; // 记录新插入节点的父节点 Comparator cpr = comparator; if (cpr != null){// 使用自定义排序 do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0)// 如果新插入的 key 小于 t 的 key,插入到t的左边 t = t.left; else if (cmp > 0) // 如果新插入的 key 大于 t 的 key,插入到t的右边 t = t.right; else// 如果两个 key 相等,新的 value 覆盖原有的 value,并返回原有的 value return t.setValue(value); } while (t != null); } else{ if (key == null)// 不支持key为NULL值 throw new NullPointerException(); Comparable k = (Comparable) key; // 实现了Comparable接口的K do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0)// 如果新插入的 key 小于 t 的 key,插入左边 t = t.left; else if (cmp > 0) // 如果新插入的 key 大于 t 的 key,插入右边 t = t.right; else// 如果两个 key 相等,新的 value 覆盖原有的 value,并返回原有的 value return t.setValue(value); } while (t != null); } // 将新插入的节点作为 parent 节点的子节点 Entry e = new Entry(key, value, parent); if (cmp < 0) // 如果新插入 key 小于 parent 的 key,则 e 作为 parent 的左子节点 parent.left = e; else// 如果新插入 key 小于 parent 的 key,则 e 作为 parent 的右子节点 parent.right = e; fixAfterInsertion(e); // 修复红黑树 size++; modCount++; return null; }


如上创建的过程相当于不断地向排序二叉树添加节点的过程,向排序二叉树添加节点的步骤如下:

  1. 以根节点当前节点开始搜索。
  2. 拿新节点的key值和当前节点的key值比较。
  3. 如果新节点的key值更大,则以当前节点的右子节点作为新的当前节点;如果新节点的key值更小,则以当前节点的左子节点作为新的当前节点。
  4. 重复 2、3 两个步骤,直到搜索到合适的叶子节点为止。
  5. 将新节点(e)添加为第 4 步找到的叶子节点(parent)的子节点;如果新节点更大,则添加为右子节点;否则添加为左子节点。

如果关于排序的Comparator和Comparable类还不是很了解,可以参考如下文章:
传送门:


(2)节点删除操作

TreeMap 删除节点的方法由如下方法实现:

private void deleteEntry(Entry p) { modCount++; size--; if (p.left != null && p.right != null){ // 如果被删除节点的左子树、右子树都不为空 Entry s = successor (p); // 用 p 节点的中序后继节点代替 p 节点 // 如下的两句代码表示p被替换为s节点 p.key = s.key; p.value = https://www.it610.com/article/s.value; p = s; // p的引用被替换为s的引用,下面的p全部指带s节点 } /* *注意这里的p指的是s节点,由于s节点可能存在左子树或右子树(左右子树不可能同时存在),也可能为空 *replacement 代表这个子树,也可能为空 */ Entry replacement = (p.left != null ? p.left : p.right); if (replacement != null){ /* * 要修改两处: * 1、修改替代节点的父节点为被删除节点的父节点 * 2、修改被删除节点父节点下的子节点引用为被替代节点 */ replacement.parent = p.parent; if (p.parent == null)// 如果 p没有父节点,则 replacemment 变成父节点 root = replacement; else if (p == p.parent.left) // 如果 p节点是其父节点的左子节点 p.parent.left= replacement; else// 如果 p 节点是其父节点的右子节点 p.parent.right = replacement; p.left = p.right = p.parent = null; // 释放这个节点,帮助GC回收// 修复红黑树 if (p.color == BLACK) fixAfterDeletion(replacement); // ① } /* *如果被替代的子节点没有父节点,那么删除的一定是仅有根节点的树了 */ else if (p.parent == null) { root = null; } /* * 代码执行到如下部分说明: * 1、这个替代的节点肯定是叶子节点 * 2、这个树中至少含有两个节点 */ else{ if (p.color == BLACK) fixAfterDeletion(p); // 修复红黑树② if (p.parent != null){ if (p == p.parent.left)// 如果 p 是其父节点的左子节点 p.parent.left = null; else if (p == p.parent.right)// 如果 p 是其父节点的右子节点 p.parent.right = null; p.parent = null; } } }


排序二叉树虽然可以快速检索,但在最坏的情况下:如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到 的排序二叉树将变成链表:所有节点只有左节点(如果插入节点集本身是大到小排列);或所有节点只有右节点(如果插入节点集本身是小到大排列)。在这种情况 下,排序二叉树就变成了普通链表,其检索效率就会很差。
红黑树是一个更高效的检索二叉树,因此常常用来实现关联数组。典型地,JDK 提供的集合类 TreeMap 本身就是一个红黑树的实现。红黑树在原有的排序二叉树增加了如下几个要求:
下面的性质 3 中指定红黑树的每个叶子节点都是空节点,而且叶子节点都是黑色。但 Java 实现的红黑树将使用 null 来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。

  • · 性质 1:每个节点要么是红色,要么是黑色。
  • · 性质 2:根节点永远是黑色的。
  • · 性质 3:所有的叶节点都是空节点(即 null),并且是黑色的。
  • · 性质 4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
  • · 性质 5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

Java 中实现的红黑树可能有如图 6 所示结构:
图 6. Java 红黑树的示意



备注:本文中所有关于红黑树中的示意图采用白色代表红色。黑色节点还是采用了黑色表示。

Java|Java 7之集合类型第5篇 - TreeMap
文章图片





红黑树从根节点到每个叶子节点的路径都包含相同数量的黑色节点,因此从根节点到叶子节点的路径中包含的黑色节点数被称为树的“黑色高度(black-height)”。
对于给定的黑色高度为 N 的红黑树,从根到叶子节点的最短路径长度为 N-1,最长路径长度为 2 * (N-1)。
假如有一棵黑色高度为 3 的红黑树:从根节点到叶节点的最短路径长度是 2,该路径上全是黑色节点(黑节点 - 黑节点 - 黑节点)。最长路径也只可能为 4,在每个黑色节点之间插入一个红色节点(黑节点 - 红节点 - 黑节点 - 红节点 - 黑节点),性质 4 保证绝不可能插入更多的红色节点。由此可见,红黑树中最长路径就是一条红黑交替的路径。


由此我们可以得出结论:
排序二叉树的深度直接影响了检索的性能,当插入节点本身就是由小到大排列时,排序二叉树将变成一个链表,这种排序二叉树的检索性能最低:N 个节点的二叉树深度就是 N-1。但是红黑树通过上面这种限制来保证它大致是平衡的——因为红黑树的高度不会无限增高,这样保证红黑树在最坏情况下都是高效的,不会出现普通排序二叉树的情况。
由于红黑树只是一个特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的只读操作完全相同,只是红黑树保持了大致平衡,因此检索性能比排序二叉树要好很多。
但在红黑树上进行插入操作和删除操作会导致树不再符合红黑树的特征,因此插入操作和删除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树。



添加节点后的修复
上面 put(K key, V value) 方法中①号代码处使用fixAfterInsertion(e) 方法来修复红黑树——因此每次插入节点后必须进行简单修复,使该排序二叉树满足红黑树的要求。
插入操作按如下步骤进行:
(1)以排序二叉树的方法插入新节点,并将它设为红色。
(2)进行颜色调换和树旋转。
插入后的修复
在插入操作中,红黑树的性质 1 和性质 3 两个永远不会发生改变,因此无需考虑红黑树的这两个特性。
这种颜色调用和树旋转就比较复杂了,下面将分情况进行介绍。在介绍中,我们把新插入的节点定义为 N 节点,N 节点的父节点定义为 P 节点,P 节点的兄弟节点定义为 U 节点,P 节点父节点定义为 G 节点。
下面分成不同情形来分析插入操作
情形 1:新节点 N 是树的根节点,没有父节点
在这种情形下,直接将它设置为黑色以满足性质 2。
情形 2:新节点的父节点 P 是黑色
在这种情况下,新插入的节点是红色的,因此依然满足性质 4。而且因为新节点 N 有两个黑色叶子节点;但是由于新节点 N 是红色,通过它的每个子节点的路径依然保持相同的黑色节点数,因此依然满足性质 5。
情形 3:如果父节点 P 和父节点的兄弟节点 U 都是红色
在这种情况下,程序应该将 P 节点、U 节点都设置为黑色,并将 P 节点的父节点设为红色(用来保持性质 5)。现在新节点 N 有了一个黑色的父节点 P。由于从 P 节点、U 节点到根节点的任何路径都必须通过 G 节点,在这些路径上的黑节点数目没有改变(原来有叶子和 G 节点两个黑色节点,现在有叶子和 P 两个黑色节点)。
经过上面处理后,红色的 G 节点的父节点也有可能是红色的,这就违反了性质 4,因此还需要对 G 节点递归地进行整个过程(把 G 当成是新插入的节点进行处理即可)。
图 7 显示了这种处理过程:
Java|Java 7之集合类型第5篇 - TreeMap
文章图片


备注:虽然图 11.28 绘制的是新节点 N 作为父节点 P 左子节点的情形,其实新节点 N 作为父节点 P 右子节点的情况与图 11.28 完全相同。
情形 4:父节点 P 是红色、而其兄弟节点 U 是黑色或缺少;且新节点 N 是父节点 P 的右子节点,而父节点 P 又是其父节点 G 的左子节点。
在这种情形下,我们进行一次左旋转对新节点和其父节点进行,接着按情形 5 处理以前的父节点 P(也就是把 P 当成新插入的节点即可)。这导致某些路径通过它们以前不通过的新节点 N 或父节点 P 的其中之一,但是这两个节点都是红色的,因此不会影响性质 5。
图 8 显示了对情形 4 的处理:

图 8. 插入节点后的树旋转

Java|Java 7之集合类型第5篇 - TreeMap
文章图片


备注:图 11.29 中 P 节点是 G 节点的左子节点,如果 P 节点是其父节点 G 节点的右子节点,那么上 面的处理情况应该左、右对调一下。
情形 5:父节点 P 是红色、而其兄弟节点 U 是黑色或缺少;且新节点 N 是其父节点的左子节点,而父节点 P 又是其父节点 G 的左子节点。
在这种情形下,需要对节点 G 的一次右旋转,在旋转产生的树中,以前的父节点 P 现在是新节点 N 和节点 G 的父节点。由于以前的节点 G 是黑色,否则父节点 P 就不可能是红色,我们切换以前的父节点 P 和节点 G 的颜色,使之满足性质 4,性质 5 也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过节点 G,现在它们都通过以前的父节点 P。在各自的情形下,这都是三个节点中唯一的黑色节点。
图 9 显示了情形 5 的处理过程:

图 9. 插入节点后的颜色调整、树旋转

Java|Java 7之集合类型第5篇 - TreeMap
文章图片


备注:图 11.30 中 P 节点是 G 节点的左子节点,如果 P 节点是其父节点 G 节点的右子节点,那么上面的处理情况应该左、右对调一下。
TreeMap 为插入节点后的修复操作由 fixAfterInsertion(Entry x) 方法提供,该方法的源代码如下:
// 插入节点后修复红黑树 private void fixAfterInsertion(Entry x) { x.color = RED; // 直到 x 节点的父节点不是根,且 x 的父节点不是红色 while (x != null && x != root && x.parent.color == RED) { // 如果 x 的父节点是其父节点的左子节点 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // 获取 x 的父节点的兄弟节点 Entry y = rightOf(parentOf(parentOf(x))); // 如果 x 的父节点的兄弟节点是红色 if (colorOf(y) == RED) { // 将 x 的父节点设为黑色 setColor(parentOf(x), BLACK); // 将 x 的父节点的兄弟节点设为黑色 setColor(y, BLACK); // 将 x 的父节点的父节点设为红色 setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } // 如果 x 的父节点的兄弟节点是黑色 else { // 如果 x 是其父节点的右子节点 if (x == rightOf(parentOf(x))) { // 将 x 的父节点设为 x x = parentOf(x); rotateLeft(x); } // 把 x 的父节点设为黑色 setColor(parentOf(x), BLACK); // 把 x 的父节点的父节点设为红色 setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } // 如果 x 的父节点是其父节点的右子节点 else { // 获取 x 的父节点的兄弟节点 Entry y = leftOf(parentOf(parentOf(x))); // 如果 x 的父节点的兄弟节点是红色 if (colorOf(y) == RED) { // 将 x 的父节点设为黑色。 setColor(parentOf(x), BLACK); // 将 x 的父节点的兄弟节点设为黑色 setColor(y, BLACK); // 将 x 的父节点的父节点设为红色 setColor(parentOf(parentOf(x)), RED); // 将 x 设为 x 的父节点的节点 x = parentOf(parentOf(x)); } // 如果 x 的父节点的兄弟节点是黑色 else { // 如果 x 是其父节点的左子节点 if (x == leftOf(parentOf(x))) { // 将 x 的父节点设为 x x = parentOf(x); rotateRight(x); } // 把 x 的父节点设为黑色 setColor(parentOf(x), BLACK); // 把 x 的父节点的父节点设为红色 setColor(parentOf(parentOf(x)), RED); rotateLeft(parentOf(parentOf(x))); } } } // 将根节点设为黑色 root.color = BLACK; }

删除节点后的修复
与添加节点之后的修复类似的是,TreeMap 删除节点之后也需要进行类似的修复操作,通过这种修复来保证该排序二叉树依然满足红黑树特征。大家可以参考插入节点之后的修复来分析删除之后的修复。 TreeMap 在删除之后的修复操作由 fixAfterDeletion(Entry x) 方法提供,该方法源代码如下:
// 删除节点后修复红黑树 private void fixAfterDeletion(Entry x) { // 直到 x 不是根节点,且 x 的颜色是黑色 while (x != root && colorOf(x) == BLACK) { // 如果 x 是其父节点的左子节点 if (x == leftOf(parentOf(x))) { // 获取 x 节点的兄弟节点 Entry sib = rightOf(parentOf(x)); // 如果 sib 节点是红色 if (colorOf(sib) == RED) { // 将 sib 节点设为黑色 setColor(sib, BLACK); // 将 x 的父节点设为红色 setColor(parentOf(x), RED); rotateLeft(parentOf(x)); // 再次将 sib 设为 x 的父节点的右子节点 sib = rightOf(parentOf(x)); } // 如果 sib 的两个子节点都是黑色 if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { // 将 sib 设为红色 setColor(sib, RED); // 让 x 等于 x 的父节点 x = parentOf(x); } else { // 如果 sib 的只有右子节点是黑色 if (colorOf(rightOf(sib)) == BLACK) { // 将 sib 的左子节点也设为黑色 setColor(leftOf(sib), BLACK); // 将 sib 设为红色 setColor(sib, RED); rotateRight(sib); sib = rightOf(parentOf(x)); } // 设置 sib 的颜色与 x 的父节点的颜色相同 setColor(sib, colorOf(parentOf(x))); // 将 x 的父节点设为黑色 setColor(parentOf(x), BLACK); // 将 sib 的右子节点设为黑色 setColor(rightOf(sib), BLACK); rotateLeft(parentOf(x)); x = root; } } // 如果 x 是其父节点的右子节点 else { // 获取 x 节点的兄弟节点 Entry sib = leftOf(parentOf(x)); // 如果 sib 的颜色是红色 if (colorOf(sib) == RED) { // 将 sib 的颜色设为黑色 setColor(sib, BLACK); // 将 sib 的父节点设为红色 setColor(parentOf(x), RED); rotateRight(parentOf(x)); sib = leftOf(parentOf(x)); } // 如果 sib 的两个子节点都是黑色 if (colorOf(rightOf(sib)) == BLACK && colorOf(leftOf(sib)) == BLACK) { // 将 sib 设为红色 setColor(sib, RED); // 让 x 等于 x 的父节点 x = parentOf(x); } else { // 如果 sib 只有左子节点是黑色 if (colorOf(leftOf(sib)) == BLACK) { // 将 sib 的右子节点也设为黑色 setColor(rightOf(sib), BLACK); // 将 sib 设为红色 setColor(sib, RED); rotateLeft(sib); sib = leftOf(parentOf(x)); } // 将 sib 的颜色设为与 x 的父节点颜色相同 setColor(sib, colorOf(parentOf(x))); // 将 x 的父节点设为黑色 setColor(parentOf(x), BLACK); // 将 sib 的左子节点设为黑色 setColor(leftOf(sib), BLACK); rotateRight(parentOf(x)); x = root; } } } setColor(x, BLACK); }


(3)检索节点

当 TreeMap 根据 key 来取出 value 时,TreeMap 对应的方法如下:
public V get(Object key) { // 根据指定 key 取出对应的 Entry Entry>K,V< p = getEntry(key); // 返回该 Entry 所包含的 value return (p==null ? null : p.value); }

从上面程序的粗体字代码可以看出,get(Object key) 方法实质是由于 getEntry() 方法实现的,这个 getEntry() 方法的代码如下:
final Entry getEntry(Object key) { // 如果 comparator 不为 null,表明程序采用定制排序 if (comparator != null) // 调用 getEntryUsingComparator 方法来取出对应的 key return getEntryUsingComparator(key); // 如果 key 形参的值为 null,抛出 NullPointerException 异常 if (key == null) throw new NullPointerException(); // 将 key 强制类型转换为 Comparable 实例 Comparable k = (Comparable) key; // 从树的根节点开始 Entry p = root; while (p != null) { // 拿 key 与当前节点的 key 进行比较 int cmp = k.compareTo(p.key); // 如果 key 小于当前节点的 key,向“左子树”搜索 if (cmp < 0) p = p.left; // 如果 key 大于当前节点的 key,向“右子树”搜索 else if (cmp > 0) p = p.right; // 不大于、不小于,就是找到了目标 Entry else return p; } return null; }

上面的 getEntry(Object obj) 方法也是充分利用排序二叉树的特征来搜索目标 Entry,程序依然从二叉树的根节点开始,如果被搜索节点大于当前节点,程序向“右子树”搜索;如果被搜索节点小于当前节点,程序向“左子树”搜索;如 果相等,那就是找到了指定节点。
当 TreeMap 里的 comparator != null 即表明该 TreeMap 采用了定制排序,在采用定制排序的方式下,TreeMap 采用 getEntryUsingComparator(key) 方法来根据 key 获取 Entry。下面是该方法的代码:
final Entry getEntryUsingComparator(Object key) { K k = (K) key; // 获取该 TreeMap 的 comparator Comparator cpr = comparator; if (cpr != null) { // 从根节点开始 Entry p = root; while (p != null) { // 拿 key 与当前节点的 key 进行比较 int cmp = cpr.compare(k, p.key); // 如果 key 小于当前节点的 key,向“左子树”搜索 if (cmp < 0) p = p.left; // 如果 key 大于当前节点的 key,向“右子树”搜索 else if (cmp > 0) p = p.right; // 不大于、不小于,就是找到了目标 Entry else return p; } } return null; }

其实 getEntry、getEntryUsingComparator 两个方法的实现思路完全类似,只是前者对自然排序的 TreeMap 获取有效,后者对定制排序的 TreeMap 有效。
通过上面源代码的分析不难看出,TreeMap 这个工具类的实现其实很简单。或者说:从内部结构来看,TreeMap 本质上就是一棵“红黑树”,而 TreeMap 的每个 Entry 就是该红黑树的一个节点。
























【Java|Java 7之集合类型第5篇 - TreeMap】


    推荐阅读