Java - TreeMap源码解析 + 红黑树
Java提高篇(二七)-----TreeMap
TreeMap的实现是红黑树算法的实现,所以要了解TreeMap就必须对红黑树有一定的了解,其实这篇博文的名字叫做:根据红黑树的算法来分析TreeMap的实现,但是为了与Java提高篇系列博文保持一致还是叫做TreeMap比较好。通过这篇博文你可以获得如下知识点:
1、红黑树的基本概念。
2、红黑树增加节点、删除节点的实现过程。
3、红黑树左旋转、右旋转的复杂过程。
4、Java 中TreeMap是如何通过put、deleteEntry两个来实现红黑树增加、删除节点的。
我想通过这篇博文你对TreeMap一定有了更深的认识。好了,下面先简单普及红黑树知识。
一、红黑树简介
红黑树又称红-黑二叉树,它首先是一颗二叉树,它具体二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树。
我们知道一颗基本的二叉树他们都需要满足一个基本性质--即树中的任何节点的值大于它的左子节点,且小于它的右子节点。按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树),这样势必会导致二叉树的检索效率大大降低(O(n)),所以为了维持二叉树的平衡,大牛们提出了各种实现的算法,如:AVL,SBT,伸展树,TREAP ,红黑树等等。
平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个等等子节点,其左右子树的高度都相近。
红黑树顾名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树二叉树而言我们必须增加如下规则:
1、每个节点都只能是红色或者黑色
2、根节点是黑色
3、每个叶节点(NIL节点,空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n)。下图为一颗典型的红黑二叉树。
对于红黑二叉树而言它主要包括三大基本操作:左旋、右旋、着色。
(图片来自:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html)
本节参考文献:http://baike.baidu.com/view/133754.htm?fr=aladdin-----百度百科
注:由于本文主要是讲解Java中TreeMap,所以并没有对红黑树进行非常深入的了解和研究,如果诸位想对其进行更加深入的研究Lz提供几篇较好的博文:
1、红黑树系列集锦
3、红黑树
二、TreeMap数据结构
>>>>>>回归主角:TreeMap<<<<<<
TreeMap的定义如下:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
TreeMap继承AbstractMap,实现NavigableMap、Cloneable、Serializable三个接口。其中AbstractMap表明TreeMap为一个Map即支持key-value的集合, NavigableMap(更多)则意味着它支持一系列的导航方法,具备针对给定搜索目标返回最接近匹配项的导航方法 。
TreeMap基于红黑树(点击查看树、红黑树相关内容)实现。查看“键”或“键值对”时,它们会被排序(次序由Comparable或Comparator决定)。TreeMap的特点在于,所得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。
Comparable接口:
1 public interface Comparable<T> { 2 public int compareTo(T o); 3 }
Comparable接口支持泛型,只有一个方法,该方法返回负数、零、正数分别表示当前对象“小于”、“等于”、“大于”传入对象o。
Comparamtor接口:
1 public interface Comparator<T> { 2 int compare(T o1, T o2); 3 boolean equals(Object obj); 4 }
compare(T o1,T o2)方法比较o1和o2两个对象,o1“大于”o2,返回正数,相等返回零,“小于”返回负数。
equals(Object obj)返回true的唯一情况是obj也是一个比较器(Comparator)并且比较结果和此比较器的结果的大小次序是一致的。即comp1.equals(comp2)意味着sgn(comp1.compare(o1, * o2))==sgn(comp2.compare(o1, o2))。
补充:符号sgn(expression)表示数学上的signum函数,该函数根据expression的值是负数、零或正数,分别返回-1、0或1。
小结一下,实现Comparable结构的类可以和其他对象进行比较,即实现Comparable可以进行比较的类。而实现Comparator接口的类是比较器,用于比较两个对象的大小。
下面正式分析TreeMap的源码。
既然TreeMap底层使用的是树结构,那么必然有表示节点的对象。下面先看TreeMap中表示节点的内部类Entry。
上面只有一个接口需要说明,那就是NavigableMap接口。
NavigableMap接口扩展的SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法。方法lowerEntry、floorEntry、ceilingEntry和higherEntry分别返回与小于、小于等于、大于等于、大于给定键的键关联的Map.Entry对象,如果不存在这样的键,则返回null。类似地,方法lowerKey、floorKey、ceilingKey和higherKey只返回关联的键。所有这些方法是为查找条目而不是遍历条目而设计的(后面会逐个介绍这些方法)。
TreeMap中同时也包含了如下几个重要的属性:
//比较器,因为TreeMap是有序的,通过comparator接口我们可以对TreeMap的内部排序进行精密的控制 private final Comparator<? super K> comparator; //TreeMap红-黑节点,为TreeMap的内部类 private transient Entry<K,V> root = null; //容器大小 private transient int size = 0; //TreeMap修改次数 private transient int modCount = 0; //红黑树的节点颜色--红色 private static final boolean RED = false; //红黑树的节点颜色--黑色 private static final boolean BLACK = true;
对于叶子节点Entry是TreeMap的内部类,它有几个重要的属性:
//键
K key; //值 V value; //左孩子 Entry<K,V> left = null; //右孩子 Entry<K,V> right = null; //父亲 Entry<K,V> parent; //颜色 boolean color = BLACK;
三、TreeMap put()方法
-
TreeMap put()方法实现分析
在TreeMap的put()的实现方法中主要分为两个步骤,第一:构建排序二叉树,第二:平衡二叉树。
-
对于排序二叉树的创建,其添加节点的过程如下:
-
1、以根节点为初始节点进行检索。
-
2、与当前节点进行比对,若新增节点值较大,则以当前节点的右子节点作为新的当前节点。否则以当前节点的左子节点作为新的当前节点。
-
3、循环递归2步骤知道检索出合适的叶子节点为止。
-
4、将新增节点与3步骤中找到的节点进行比对,如果新增节点较大,则添加为右子节点;否则添加为左子节点。
-
按照这个步骤我们就可以将一个新增节点添加到排序二叉树中合适的位置。如下:
public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { //如果根节点为null,将传入的键值对构造成根节点(根节点没有父节点,所以传入的父节点为null) root = new Entry<K,V>(key, value, null); size = 1; modCount++; return null; } // 记录比较结果 int cmp; Entry<K,V> parent; // 分割比较器和可比较接口的处理 Comparator<? super K> cpr = comparator; // 有比较器的处理 if (cpr != null) { // do while实现在root为根节点移动寻找传入键值对需要插入的位置 do { // 记录将要被掺入新的键值对将要节点(即新节点的父节点) parent = t; // 使用比较器比较父节点和插入键值对的key值的大小 cmp = cpr.compare(key, t.key); // 插入的key较大 if (cmp < 0) t = t.left; // 插入的key较小 else if (cmp > 0) t = t.right; // key值相等,替换并返回t节点的value(put方法结束) else return t.setValue(value); } while (t != null); } // 没有比较器的处理 else { // key为null抛出NullPointerException异常 if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; // 与if中的do while类似,只是比较的方式不同 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); } // 没有找到key相同的节点才会有下面的操作 // 根据传入的键值对和找到的“父节点”创建新节点 Entry<K,V> e = new Entry<K,V>(key, value, parent); // 根据最后一次的判断结果确认新节点是“父节点”的左孩子还是又孩子 if (cmp < 0) parent.left = e; else parent.right = e; // 对加入新节点的树进行调整 fixAfterInsertion(e); // 记录size和modCount size++; modCount++; // 因为是插入新节点,所以返回的是null return null; }
首先一点通性是TreeMap的put方法和其他Map的put方法一样,向Map中加入键值对,若原先“键(key)”已经存在则替换“值(value)”,并返回原先的值。
在put(K key,V value)方法的末尾调用了fixAfterInsertion(Entry<K,V> x)方法,这个方法负责在插入节点后调整树结构和着色,以满足红黑树的要求。
- 每一个节点或者着成红色,或者着成黑色。
- 根是黑色的。
- 如果一个节点是红色的,那么它的子节点必须是黑色的。
- 一个节点到一个null引用的每一条路径必须包含相同数量的黑色节点。
在看fixAfterInsertion(Entry<K,V> x)方法前先看一个红黑树的内容:红黑树不是严格的平衡二叉树,它并不严格的保证左右子树的高度差不超过1,但红黑树高度依然是平均log(n),且最坏情况高度不会超过2log(n),所以它算是平衡树。
fixAfterInsertion(Entry<K,V> x)
private void fixAfterInsertion(Entry<K,V> x) { // 插入节点默认为红色 x.color = RED; // 循环条件是x不为空、不是根节点、父节点的颜色是红色(如果父节点不是红色,则没有连续的红色节点,不再调整) while (x != null && x != root && x.parent.color == RED) { // x节点的父节点p(记作p)是其父节点pp(p的父节点,记作pp)的左孩子(pp的左孩子) if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // 获取pp节点的右孩子r Entry<K,V> y = rightOf(parentOf(parentOf(x))); // pp右孩子的颜色是红色(colorOf(Entry e)方法在e为空时返回BLACK),不需要进行旋转操作(因为红黑树不是严格的平衡二叉树) if (colorOf(y) == RED) { // 将父节点设置为黑色 setColor(parentOf(x), BLACK); // y节点,即r设置成黑色 setColor(y, BLACK); // pp节点设置成红色 setColor(parentOf(parentOf(x)), RED); // x“移动”到pp节点 x = parentOf(parentOf(x)); } else {//父亲的兄弟是黑色的,这时需要进行旋转操作,根据是“内部”还是“外部”的情况决定是双旋转还是单旋转 // x节点是父节点的右孩子(因为上面已近确认p是pp的左孩子,所以这是一个“内部,左-右”插入的情况,需要进行双旋转处理) if (x == rightOf(parentOf(x))) { // x移动到它的父节点 x = parentOf(x); // 左旋操作 rotateLeft(x); } // x的父节点设置成黑色 setColor(parentOf(x), BLACK); // x的父节点的父节点设置成红色 setColor(parentOf(parentOf(x)), RED); // 右旋操作 rotateRight(parentOf(parentOf(x))); } } else { // 获取x的父节点(记作p)的父节点(记作pp)的左孩子 Entry<K,V> y = leftOf(parentOf(parentOf(x))); // y节点是红色的 if (colorOf(y) == RED) { // x的父节点,即p节点,设置成黑色 setColor(parentOf(x), BLACK); // y节点设置成黑色 setColor(y, BLACK); // pp节点设置成红色 setColor(parentOf(parentOf(x)), RED); // x移动到pp节点 x = parentOf(parentOf(x)); } else { // x是父节点的左孩子(因为上面已近确认p是pp的右孩子,所以这是一个“内部,右-左”插入的情况,需要进行双旋转处理), if (x == leftOf(parentOf(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; }
fixAfterInsertion(Entry<K,V> x)方法涉及到了左旋和右旋的操作,下面是左旋的代码及示意图(右旋操作类似,就不给出代码和示意图了)。
// 左旋操作 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; } }
看完put操作,下面来看get操作相关的内容。
get(Object key)
1 public V get(Object key) { 2 Entry<K,V> p = getEntry(key); 3 return (p==null ? null : p.value); 4 }
get(Object key)通过key获取对应的value,它通过调用getEntry(Object key)获取节点,若节点为null则返回null,否则返回节点的value值。下面是getEntry(Object key)的内容,来看它是怎么寻找节点的。
getEntry(Object key)
final Entry<K,V> getEntry(Object key) { // 如果有比较器,返回getEntryUsingComparator(Object key)的结果 if (comparator != null) return getEntryUsingComparator(key); // 查找的key为null,抛出NullPointerException if (key == null) throw new NullPointerException(); // 如果没有比较器,而是实现了可比较接口 Comparable<? super K> k = (Comparable<? super K>) key; // 获取根节点 Entry<K,V> p = root; // 对树进行遍历查找节点 while (p != null) { // 把key和当前节点的key进行比较 int cmp = k.compareTo(p.key); // key小于当前节点的key if (cmp < 0) // p “移动”到左节点上 p = p.left; // key大于当前节点的key else if (cmp > 0) // p “移动”到右节点上 p = p.right; // key值相等则当前节点就是要找的节点 else // 返回找到的节点 return p; } // 没找到则返回null return null; }
上面主要是处理实现了可比较接口的情况,而有比较器的情况在getEntryUsingComparator(Object key)中处理了,下面来看处理的代码。
getEntryUsingComparator(Object key)
final Entry<K,V> getEntryUsingComparator(Object key) { K k = (K) key; // 获取比较器 Comparator<? super K> cpr = comparator; // 其实在调用此方法的get(Object key)中已经对比较器为null的情况进行判断,这里是防御性的判断 if (cpr != null) { // 获取根节点 Entry<K,V> p = root; // 遍历树 while (p != null) { // 获取key和当前节点的key的比较结果 int cmp = cpr.compare(k, p.key); // 查找的key值较小 if (cmp < 0) // p“移动”到左孩子 p = p.left; // 查找的key值较大 else if (cmp > 0) // p“移动”到右节点 p = p.right; // key值相等 else // 返回找到的节点 return p; } } // 没找到key值对应的节点,返回null return null; }
四、TreeMap delete()方法
remove(Object key)
public V remove(Object key) { // 通过getEntry(Object key)获取节点 getEntry(Object key)方法已经在上面介绍过了 Entry<K,V> p = getEntry(key); // 指定key的节点不存在,返回null if (p == null) return null; // 获取节点的value V oldValue = p.value; // 删除节点 deleteEntry(p); // 返回节点的内容 return oldValue; }
真正实现删除节点的内容在deleteEntry(Entry e)中,涉及到树结构的调整等。remove(Object key)只是获取要删除的节点并返回被删除节点的value。下面来看deleteEntry(Entry e)的内容。
deleteEntry(Entry e)
private void deleteEntry(Entry<K,V> p) { // 记录树结构的修改次数 modCount++; // 记录树中节点的个数 size--; // p有左右两个孩子的情况 标记① if (p.left != null && p.right != null) { // 获取继承者节点(有两个孩子的情况下,继承者肯定是右孩子或右孩子的最左子孙) Entry<K,V> s = successor (p); // 使用继承者s替换要被删除的节点p,将继承者的key和value复制到p节点,之后将p指向继承者 p.key = s.key; p.value = s.value; p = s; } // Start fixup at replacement node, if it exists. // 开始修复被移除节点处的树结构 // 如果p有左孩子,取左孩子,否则取右孩子 标记② Entry<K,V> replacement = (p.left != null ? p.left : p.right); if (replacement != null) { // Link replacement to parent replacement.parent = p.parent; // p节点没有父节点,即p节点是根节点 if (p.parent == null) // 将根节点替换为replacement节点 root = replacement; // p是其父节点的左孩子 else if (p == p.parent.left) // 将p的父节点的left引用指向replacement // 这步操作实现了删除p的父节点到p节点的引用 p.parent.left = replacement; else // 如果p是其父节点的右孩子,将父节点的right引用指向replacement p.parent.right = replacement; // 解除p节点到其左右孩子和父节点的引用 p.left = p.right = p.parent = null; if (p.color == BLACK) // 在删除节点后修复红黑树的颜色分配 fixAfterDeletion(replacement); } else if (p.parent == null) { /* 进入这块代码则说明p节点就是根节点(这块比较难理解,如果标记①处p有左右孩子,则找到的继承节点s是p的一个祖先节点或右孩子或右孩子的最左子孙节点,他们要么有孩子节点,要么有父节点,所以如果进入这块代码,则说明标记①除的p节点没有左右两个孩子。没有左右孩子,则有没有孩子、有一个右孩子、有一个左孩子三种情况,三种情况中只有没有孩子的情况会使标记②的if判断不通过,所以p节点只能是没有孩子,加上这里的判断,p没有父节点,所以p是一个独立节点,也是树种的唯一节点……有点难理解,只能解释到这里了,读者只能结合注释慢慢体会了),所以将根节点设置为null即实现了对该节点的删除 */ root = null; } else { /* 标记②的if判断没有通过说明被删除节点没有孩子,或它有两个孩子但它的继承者没有孩子。如果是被删除节点没有孩子,说明p是个叶子节点,则不需要找继承者,直接删除该节点。如果是有两个孩子,那么继承者肯定是右孩子或右孩子的最左子孙 */ if (p.color == BLACK) // 调整树结构 fixAfterDeletion(p); // 这个判断也一定会通过,因为p.parent如果不是null则在上面的else if块中已经被处理 if (p.parent != null) { // p是一个左孩子 if (p == p.parent.left) // 删除父节点对p的引用 p.parent.left = null; else if (p == p.parent.right)// p是一个右孩子 // 删除父节点对p的引用 p.parent.right = null; // 删除p节点对父节点的引用 p.parent = null; } } }
deleteEntry(Entry e)方法中主要有两个方法调用需要分析:successor(Entry<K,V> t)和fixAfterDeletion(Entry<K,V> x)。
successor(Entry<K,V> t)返回指定节点的继承者。分三种情况处理,第一。t节点是个空节点:返回null;第二,t有右孩子:找到t的右孩子中的最左子孙节点,如果右孩子没有左孩子则返回右节点,否则返回找到的最左子孙节点;第三,t没有右孩子:沿着向上(向跟节点方向)找到第一个自身是一个左孩子的节点或根节点,返回找到的节点。下面是具体代码分析的注释。
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { // 如果t本身是一个空节点,返回null if (t == null) return null; // 如果t有右孩子,找到右孩子的最左子孙节点 else if (t.right != null) { Entry<K,V> p = t.right; // 获取p节点最左的子孙节点,如果存在的话 while (p.left != null) p = p.left; // 返回找到的继承节点 return p; } else {//t不为null且没有右孩子 Entry<K,V> p = t.parent; Entry<K,V> ch = t; // // 沿着右孩子向上查找继承者,直到根节点或找到节点ch是其父节点的左孩子的节点 while (p != null && ch == p.right) { ch = p; p = p.parent; } return p; } }
与添加节点之后的修复类似的是,TreeMap 删除节点之后也需要进行类似的修复操作,通过这种修复来保证该排序二叉树依然满足红黑树特征。大家可以参考插入节点之后的修复来分析删除之后的修复。TreeMap 在删除之后的修复操作由 fixAfterDeletion(Entry<K,V> x) 方法提供,该方法源代码如下:
private void fixAfterDeletion(Entry<K,V> x) { // 循环处理,条件为x不是root节点且是黑色的(因为红色不会对红黑树的性质造成破坏,所以不需要调整) while (x != root && colorOf(x) == BLACK) { // x是一个左孩子 if (x == leftOf(parentOf(x))) { // 获取x的兄弟节点sib Entry<K,V> sib = rightOf(parentOf(x)); // sib是红色的 if (colorOf(sib) == RED) { // 将sib设置为黑色 setColor(sib, BLACK); // 将父节点设置成红色 setColor(parentOf(x), RED); // 左旋父节点 rotateLeft(parentOf(x)); // sib移动到旋转后x的父节点p的右孩子(参见左旋示意图,获取的节点是旋转前p的右孩子r的左孩子rl) sib = rightOf(parentOf(x)); } // sib的两个孩子的颜色都是黑色(null返回黑色) if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { // 将sib设置成红色 setColor(sib, RED); // x移动到x的父节点 x = parentOf(x); } else {// sib的左右孩子都是黑色的不成立 // sib的右孩子是黑色的 if (colorOf(rightOf(sib)) == BLACK) { // 将sib的左孩子设置成黑色 setColor(leftOf(sib), BLACK); // sib节点设置成红色 setColor(sib, RED); // 右旋操作 rotateRight(sib); // sib移动到旋转后x父节点的右孩子 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 = root; } } else { // x是一个右孩子 // 获取x的兄弟节点 Entry<K,V> sib = leftOf(parentOf(x)); // 如果sib是红色的 if (colorOf(sib) == RED) { // 将sib设置为黑色 setColor(sib, BLACK); // 将x的父节点设置成红色 setColor(parentOf(x), RED); // 右旋 rotateRight(parentOf(x)); // sib移动到旋转后x父节点的左孩子 sib = leftOf(parentOf(x)); } // sib的两个孩子的颜色都是黑色(null返回黑色) if (colorOf(rightOf(sib)) == BLACK && colorOf(leftOf(sib)) == BLACK) { // sib设置为红色 setColor(sib, RED); // x移动到x的父节点 x = parentOf(x); } else { // sib的两个孩子的颜色都是黑色(null返回黑色)不成立 // sib的左孩子是黑色的,或者没有左孩子 if (colorOf(leftOf(sib)) == BLACK) { // 将sib的右孩子设置成黑色 setColor(rightOf(sib), BLACK); // sib节点设置成红色 setColor(sib, RED); // 左旋 rotateLeft(sib); // sib移动到x父节点的左孩子 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; } } } // 将x设置为黑色 setColor(x, BLACK); }
光看调整的代码,一大堆设置颜色,还有左旋和右旋,非常的抽象,下面是一个构造红黑树的视屏,包括了着色和旋转。
http://v.youku.com/v_show/id_XMjI3NjM0MTgw.html
clear()
1 public void clear() { 2 modCount++; 3 size = 0; 4 root = null; 5 }
clear()方法很简单,只是记录结构修改次数,将size修改为0,将root设置为null,这样就没法通过root访问树的其他节点,所以数的内容会被GC回收。
添加(修改)、获取、删除的原码都已经看了,下面看判断是否包含的方法。
1 public boolean containsKey(Object key) { 2 return getEntry(key) != null; 3 }
这个方法判断获取key对应的节点是否为空,getEntry(Object key)方法已经在上面介绍过了。
contain(Object value)
public boolean containsValue(Object value) { // 通过e = successor(e)实现对树的遍历 for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e)) // 判断节点值是否和value相等 if (valEquals(value, e.value)) return true; // 默认返回false return false; }
contain(Object value)涉及到了getFirstEntry()方法和successor(Entry<K,V> e)。getFirstEntry()是获取第一个节点,successor(Entry<K,V> e)是获取节点e的继承者,在for循环中配合使用getFirstEntry()方法和successor(Entry<K,V> e)及e!=null是遍历树的一种方法。
下面介绍getFirstEntry()方法。
getFirstEntry()
final Entry<K,V> getFirstEntry() { Entry<K,V> p = root; if (p != null) while (p.left != null) p = p.left; return p; }
从名字上看是获取第一个节点,实际是获取的整棵树中“最左”的节点(第一个节点具体指哪一个节点和树的遍历次序有关,如果是先根遍历,则第一个节点是根节点)。又因为红黑树是排序的树,所以“最左”的节点也是值最小的节点。
上面是getFirstEntry()方法,下面介绍getLastEntry()方法。
getLastEntry()
final Entry<K,V> getLastEntry() { Entry<K,V> p = root; if (p != null) while (p.right != null) p = p.right; return p; }
以上内容是TreeMap的基础方法,TreeMap的内部类及涉及到内部类的方法等都将在《TreeMap源码分析——深入分析》中给出。
Java 集合系列12之 TreeMap详细介绍(源码解析)和使用示例
第4部分 TreeMap遍历方式
4.1 遍历TreeMap的键值对
第一步:根据entrySet()获取TreeMap的“键值对”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。
// 假设map是TreeMap对象 // map中的key是String类型,value是Integer类型 Integer integ = null; Iterator iter = map.entrySet().iterator(); while(iter.hasNext()) { Map.Entry entry = (Map.Entry)iter.next(); // 获取key key = (String)entry.getKey(); // 获取value integ = (Integer)entry.getValue(); }
4.2 遍历TreeMap的键
第一步:根据keySet()获取TreeMap的“键”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。
// 假设map是TreeMap对象 // map中的key是String类型,value是Integer类型 String key = null; Integer integ = null; Iterator iter = map.keySet().iterator(); while (iter.hasNext()) { // 获取key key = (String)iter.next(); // 根据key,获取value integ = (Integer)map.get(key); }
4.3 遍历TreeMap的值
第一步:根据value()获取TreeMap的“值”的集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。
// 假设map是TreeMap对象 // map中的key是String类型,value是Integer类型 Integer value = null; Collection c = map.values(); Iterator iter= c.iterator(); while (iter.hasNext()) { value = (Integer)iter.next(); }
TreeMap遍历测试程序如下:
import java.util.Map; import java.util.Random; import java.util.Iterator; import java.util.TreeMap; import java.util.HashSet; import java.util.Map.Entry; import java.util.Collection; /* * @desc 遍历TreeMap的测试程序。 * (01) 通过entrySet()去遍历key、value,参考实现函数: * iteratorTreeMapByEntryset() * (02) 通过keySet()去遍历key、value,参考实现函数: * iteratorTreeMapByKeyset() * (03) 通过values()去遍历value,参考实现函数: * iteratorTreeMapJustValues() * * @author skywang */ public class TreeMapIteratorTest { public static void main(String[] args) { int val = 0; String key = null; Integer value = null; Random r = new Random(); TreeMap map = new TreeMap(); for (int i=0; i<12; i++) { // 随机获取一个[0,100)之间的数字 val = r.nextInt(100); key = String.valueOf(val); value = r.nextInt(5); // 添加到TreeMap中 map.put(key, value); System.out.println(" key:"+key+" value:"+value); } // 通过entrySet()遍历TreeMap的key-value iteratorTreeMapByEntryset(map) ; // 通过keySet()遍历TreeMap的key-value iteratorTreeMapByKeyset(map) ; // 单单遍历TreeMap的value iteratorTreeMapJustValues(map); } /* * 通过entry set遍历TreeMap * 效率高! */ private static void iteratorTreeMapByEntryset(TreeMap map) { if (map == null) return ; System.out.println("\niterator TreeMap By entryset"); String key = null; Integer integ = null; Iterator iter = map.entrySet().iterator(); while(iter.hasNext()) { Map.Entry entry = (Map.Entry)iter.next(); key = (String)entry.getKey(); integ = (Integer)entry.getValue(); System.out.println(key+" -- "+integ.intValue()); } } /* * 通过keyset来遍历TreeMap * 效率低! */ private static void iteratorTreeMapByKeyset(TreeMap map) { if (map == null) return ; System.out.println("\niterator TreeMap By keyset"); String key = null; Integer integ = null; Iterator iter = map.keySet().iterator(); while (iter.hasNext()) { key = (String)iter.next(); integ = (Integer)map.get(key); System.out.println(key+" -- "+integ.intValue()); } } /* * 遍历TreeMap的values */ private static void iteratorTreeMapJustValues(TreeMap map) { if (map == null) return ; Collection c = map.values(); Iterator iter= c.iterator(); while (iter.hasNext()) { System.out.println(iter.next()); } } }
第5部分 TreeMap示例
下面通过实例来学习如何使用TreeMap
import java.util.*; /** * @desc TreeMap测试程序 * * @author skywang */ public class TreeMapTest { public static void main(String[] args) { // 测试常用的API testTreeMapOridinaryAPIs(); // 测试TreeMap的导航函数 //testNavigableMapAPIs(); // 测试TreeMap的子Map函数 //testSubMapAPIs(); } /** * 测试常用的API */ private static void testTreeMapOridinaryAPIs() { // 初始化随机种子 Random r = new Random(); // 新建TreeMap TreeMap tmap = new TreeMap(); // 添加操作 tmap.put("one", r.nextInt(10)); tmap.put("two", r.nextInt(10)); tmap.put("three", r.nextInt(10)); System.out.printf("\n ---- testTreeMapOridinaryAPIs ----\n"); // 打印出TreeMap System.out.printf("%s\n",tmap ); // 通过Iterator遍历key-value Iterator iter = tmap.entrySet().iterator(); while(iter.hasNext()) { Map.Entry entry = (Map.Entry)iter.next(); System.out.printf("next : %s - %s\n", entry.getKey(), entry.getValue()); } // TreeMap的键值对个数 System.out.printf("size: %s\n", tmap.size()); // containsKey(Object key) :是否包含键key System.out.printf("contains key two : %s\n",tmap.containsKey("two")); System.out.printf("contains key five : %s\n",tmap.containsKey("five")); // containsValue(Object value) :是否包含值value System.out.printf("contains value 0 : %s\n",tmap.containsValue(new Integer(0))); // remove(Object key) : 删除键key对应的键值对 tmap.remove("three"); System.out.printf("tmap:%s\n",tmap ); // clear() : 清空TreeMap tmap.clear(); // isEmpty() : TreeMap是否为空 System.out.printf("%s\n", (tmap.isEmpty()?"tmap is empty":"tmap is not empty") ); } /** * 测试TreeMap的子Map函数 */ public static void testSubMapAPIs() { // 新建TreeMap TreeMap tmap = new TreeMap(); // 添加“键值对” tmap.put("a", 101); tmap.put("b", 102); tmap.put("c", 103); tmap.put("d", 104); tmap.put("e", 105); System.out.printf("\n ---- testSubMapAPIs ----\n"); // 打印出TreeMap System.out.printf("tmap:\n\t%s\n", tmap); // 测试 headMap(K toKey) System.out.printf("tmap.headMap(\"c\"):\n\t%s\n", tmap.headMap("c")); // 测试 headMap(K toKey, boolean inclusive) System.out.printf("tmap.headMap(\"c\", true):\n\t%s\n", tmap.headMap("c", true)); System.out.printf("tmap.headMap(\"c\", false):\n\t%s\n", tmap.headMap("c", false)); // 测试 tailMap(K fromKey) System.out.printf("tmap.tailMap(\"c\"):\n\t%s\n", tmap.tailMap("c")); // 测试 tailMap(K fromKey, boolean inclusive) System.out.printf("tmap.tailMap(\"c\", true):\n\t%s\n", tmap.tailMap("c", true)); System.out.printf("tmap.tailMap(\"c\", false):\n\t%s\n", tmap.tailMap("c", false)); // 测试 subMap(K fromKey, K toKey) System.out.printf("tmap.subMap(\"a\", \"c\"):\n\t%s\n", tmap.subMap("a", "c")); // 测试 System.out.printf("tmap.subMap(\"a\", true, \"c\", true):\n\t%s\n", tmap.subMap("a", true, "c", true)); System.out.printf("tmap.subMap(\"a\", true, \"c\", false):\n\t%s\n", tmap.subMap("a", true, "c", false)); System.out.printf("tmap.subMap(\"a\", false, \"c\", true):\n\t%s\n", tmap.subMap("a", false, "c", true)); System.out.printf("tmap.subMap(\"a\", false, \"c\", false):\n\t%s\n", tmap.subMap("a", false, "c", false)); // 测试 navigableKeySet() System.out.printf("tmap.navigableKeySet():\n\t%s\n", tmap.navigableKeySet()); // 测试 descendingKeySet() System.out.printf("tmap.descendingKeySet():\n\t%s\n", tmap.descendingKeySet()); } /** * 测试TreeMap的导航函数 */ public static void testNavigableMapAPIs() { // 新建TreeMap NavigableMap nav = new TreeMap(); // 添加“键值对” nav.put("aaa", 111); nav.put("bbb", 222); nav.put("eee", 333); nav.put("ccc", 555); nav.put("ddd", 444); System.out.printf("\n ---- testNavigableMapAPIs ----\n"); // 打印出TreeMap System.out.printf("Whole list:%s%n", nav); // 获取第一个key、第一个Entry System.out.printf("First key: %s\tFirst entry: %s%n",nav.firstKey(), nav.firstEntry()); // 获取最后一个key、最后一个Entry System.out.printf("Last key: %s\tLast entry: %s%n",nav.lastKey(), nav.lastEntry()); // 获取“小于/等于bbb”的最大键值对 System.out.printf("Key floor before bbb: %s%n",nav.floorKey("bbb")); // 获取“小于bbb”的最大键值对 System.out.printf("Key lower before bbb: %s%n", nav.lowerKey("bbb")); // 获取“大于/等于bbb”的最小键值对 System.out.printf("Key ceiling after ccc: %s%n",nav.ceilingKey("ccc")); // 获取“大于bbb”的最小键值对 System.out.printf("Key higher after ccc: %s%n\n",nav.higherKey("ccc")); } }
运行结果:
{one=8, three=4, two=2} next : one - 8 next : three - 4 next : two - 2 size: 3 contains key two : true contains key five : false contains value 0 : false tmap:{one=8, two=2} tmap is empty