Jdk1.6 Collections Framework源码解析(12)-TreeMap、TreeSet
作者:大飞
功能简介:
- TreeMap是一种有序的Map(K,V)容器,Key在容器中按照某种顺序排列,该顺序由给定的比较器或者Key自身的顺序来决定。
- 在TreeMap内部,所有的Key由红黑树的结构组织而成,对TreeMap的常规操作:查找、插入和删除的平均时间复杂度都是O(log n)。
- TreeMap不允许Key为null,而且要保证要么Key本身可排序,要么指定比较器,否则会发生异常。
注:jdk1.6的TreeMap有一个Bug,当向一个空TreeMap中插入一个Key为null的元素时,并不会报错,而会在后续再次插入任何元素时抛出异常。同样的,向一个没有指定比较器的空TreeMap中插入一个Key不可比较的元素时,也不会报错,而会在后续再次插入任何元素时抛出异常。
- TreeMap支持克隆、序列化,线程不安全。
- TreeSet是基于TreeMap实现的一个有序集合,同样支持克隆、序列化,线程不安全。
在进入源码分析之前,先介绍一些背景知识:(重要)
红黑树是个什么鬼?
简单说红黑树就是个二叉树,它是一种二叉平衡查找树。
一步步来。
什么是二叉树? 这个就不罗嗦了,呵呵哒~~
什么是二叉查找树?不严谨但简单的描述:对于二叉树中任意一个结点,它的左子结点比它小,右子结点比它大。
当然在具体实现中(比如TreeMap)也可以左子结点大,右子结点小,取决于怎么比较。总之目的是保证某种顺序。
看个图:
如图,这就是一棵二叉查找树。
这个二叉查找树是整体有序的,怎么理解呢?如果我们按照中序遍历一下这个二叉树,得到的结果是:
6,10,13,15,17,22,29
也可以用另一个感性的方式认识一下这种顺序性,假设我们用剪刀把这棵二叉树结点之间的连接都剪断:
然后这些货们都掉下来,落到了同一水平线上(此处请脑补):
看到没,和中序遍历一样的结果!
看完了二叉查找树的顺序性,我们来看一下怎么从二叉查找树中找到一个给定的结点。
这个其实很简单,描述下,就是拿给定的元素和根结点比较,如果比根结点小,就继续和根结点的左子结点比较;比根结点大就继续和根结点的右子结点比较...这是一个递归的过程,直到找到和给定元素值相等的结点。(可以看着上面的图,走一下过程)。
需要注意几个地方:
如果要查找的元素确实存在于二叉查找树中,按照上面那个特殊的二叉查找树来看,最多从树根走到某个子结点,就一定能找到给定的元素。
如果要查找的元素不存在于二叉查找树中,那么一定会走到一个子结点,然后走不下去了,说明没找到给定元素。
伪代码如下:
//查找Key为k的结点。 search(node,k){ while(node! = null && node.key != k){ if(k < node.key) node = node.left else node = node.right } return node }
我们会发现查找的过程的步骤取决于树的高度(仅仅根据上面的那个特殊情况来说),那么树的高度是多少呢?
简单证明一下:
假设树有n个结点,有h层,那么会有下面的等式:
20+21+22+…+2h-1 = n
根据等比数列求和公式有:n = 2h-1
计算一下:h = log(n+1)
so,查找操作过程最不济也会在h步完成,所以平均时间复杂度可以认为是O(log n)喽。
当然上面的过程不够严谨,只是针对上面给出的那个特殊的二叉查找树说的,并不是说二叉查找树的查找的时间复查度是O(log n)。
再提一下,回想一下上面我们剪断那个二叉查找树连接的情形,查找给定元素的过程岂不是很类似在一个有序数组中进行二分查找吗?有木有~~!
我们再看一下怎么给二叉查找树中添加一个结点。
简单的描述下,从根结点开始,将给定的结点和根结点进行比较,如果比根结点小,就继续和根结点的左子结点比较;如果比根结点大,就继续和根结点的右子结点比较;直到下列情况发生:
当和某个结点比较,给定结点比这个结点小,但是这个结点没有左子结点,那么给定结点就作为这个结点的左子结点拼入树中;
当和某个结点比较,给定结点比这个结点大,但是这个结点没有右子结点,那么给定结点就作为这个结点的右子结点拼入树中;
伪代码如下:
//插入结点d到树tree中 insert(tree,d){ y = null x = tree.root while(x != null){ y = x if(d.key < x.key) x = x.left else x = x.right } d.parent = y if(y == null){ tree.root = d //说明是空树 }else if(d.key < y.key){ y.left = d }else{ y.right = d } }
可见插入结点的时间复杂度也是O(log n)。
接着看一下从二叉查找树中删除某个给定的结点,假设这个结点为d。
删除结点的过程有一点点小麻烦,需要分几种情况:
1.如果给定的结点d没有任何子结点,那么直接删除它就好了。
2.如果给定的结点d有一个子结点(左子结点或右子结点都行),那么先删除给定结点,然后将它唯一的子结点移动到它的位置上。
3.如果给定的结点d有两个子结点,那么首先需要找到给定结点d的后继结点s,然后又分两种情况:
3.1.如果结点s就是d的右子结点,那么直接把s移动到d的位置上,之前d的左子结点可以作为s的左子结点(因为s一定没有左子结点),然后删除d。
3.2.如果结点s不是d的右子结点,那么需要先用s的右子结点r移动到s的位置上,现在s成了光杆儿司令,直接替换d的位置就可以,然后删除d。
上图便于感性认识,只表示了一种情况。后继结点的查找分两种情况:
1.如果一个结点有右子树,那么这个结点的后继结点一定在这个结点的右子树中,但不一定是其右子结点,且这个后继结点一定没有左子结点。(从上到下的过程)
2.如果一个结点没有右子树,那么这个结点的后继结点一定是这个结点的某个祖先结点a,分两种情况:
2.1.a是这个结点的父结点p。
2.2.从这个结点的父结点p到a的路径上,所有的结点都是其父结点的右子结点(从下到上的过程)。
还记得前面那个剪断结点连接的过程吧,所有结点落到同一水平线上后,一个结点的后继结点一定是它右边紧挨着它的那个结点。
同样的,前驱结点就是和后继结点相对的了,就是比一个结点小的所有结点中最大的结点。
好了,言归正传,看一下二叉查找树中删除结点的伪代码:
//从树tree中删除结点d delete(tree,d){ if(d.left == null){ move(tree,d,d.right) }else if(d.right == null){ move(tree,d,d.left) }else{ s = successor(d) //注意这里d是有右子结点的,所以后继一定在右子树中。 if(s.parent != d){ move(tree,s,s.right) s.right = d.right s.right.parent = s } move(tree,d,s) s.left = d.left s.left.parent = s } } //将结点n移动到结点o的位置上 move(tree,o,n){ if(o.parent == null){ T.root = n }else if(o == o.parent.left){ o.parent.left = n }else{ o.parent.right = n } if(n != null){ n.parent = o.parent } } //获取结点node的后继结点 successor(node){ if(node.right != null){ node = node.right while(node.left != null){ node = node.left } return node } p = node.parent while(p != null && node == p.right){ node = p p = p.parent } return p }
既然我们了解了二叉查找树的插入和删除,那就练练手呗:
针对前面给出的那棵完美的二叉查找树,我们依次做如下操作:删除13,删除17,删除29,删除22,插入5,插入4,插入3,插入2。
得到的树如下:(过程可以自己画图或脑补)
这还是树,只是长歪了,退化成链表状了... 这种极端情况下,之后的插入和删除的时间复杂度就会从O(log n)变成O(n)了。
这里就会反应出一个问题:平衡。
所以二叉查找树,在加上平衡的性质,就变成了二叉平衡查找树。
什么是二叉平衡查找树呢?简单的说就是远远的看,很接近于前面画的那棵完美的二叉树,整体保持平衡,这样才能保证常规操作的时间复杂度在O(log n)左右。
那具体怎么保持平衡呢?
首先,针对不同类型的二叉平衡查找树,都有自己的一些性质,只要能满足这些性质,就能保持平衡。
其次,一些常规操作(插入、删除等)可能会破坏这些性质,破坏了怎么办呢?如果性质被破坏,二叉树需要做一些类似于变形金刚一样的变形,来再次满足对应的性质。
ok,先来介绍两个的变形动作,左旋转和右旋转。这两个基本动作在多种二叉平衡查找树(红黑树,AVL树)变形过程中都会体现。
先看一下左旋转。
对结点23进行左旋转,旋转前:
旋转后:
可见,旋转后,整体的顺序性还是保持不变的。
旋转后:
可见,旋转后,整体的顺序性还是保持不变的。
左旋转是目标旋转结点和其右子结点相互之间进行一个变形,有一种逆时针方向旋转的赶脚。可以根据这个来记忆。
右旋转和左旋转是对称的,这里就不啰嗦了。
看下伪代码:
//对树Tree中的结点进行左旋转 left_rotate(tree,n){ r = n.right n.right = r.left if(r.left != null){ r.left.parent = n } r.parent = n.parent if(n.parent == null){ tree.root = r }else if(n == n.parent.left){ n.parent.left = r }else{ n.parent.right = r } r.left = n n.parent = r }
最后,终于要进入主题了。。。看下啥是红黑树吧:
既然红黑树是一种二叉平衡查找树,那么红黑树有哪些性质来保证平衡呢?如果平衡性质被破坏了,肿么办?
在回答第一个问题之前,先简单介绍下红黑树:
首先,红黑树是一种二叉平衡查找树,每个树结点上都有指向父结点、指向左子结点和右子结点的指针,也包含关键字key;
另外,红黑树的结点上还会带一个color属性,来表示这个结点是红色还是黑色;
最后,如果一个树结点没有任何子结点,那么认为这个树结点仍然指向了两个叶结点(虽然在具体Java代码中没有体现这种结点,其实就是null,但对于分析很重要)。
下面来看下红黑树有哪些性质来保证平衡(这些性质一定要记住,对于理解红黑树至关重要):
1.每个结点要么是红色的,要么是黑色的。
2.根结点是黑色的。
3.叶结点都是黑色的。
4.如果一个结点是红色的,那么它的两个子结点必须是黑色的。
5.对于每个结点,从该结点到其所有后代叶结点的简单路径上,黑色结点的数量是相同的。
这些性质怎么保证查找操作的时间复杂度在O(log n)左右呢?
假设红黑树最高的(最差情况的)高度是H,那么查找操作的最差时间复杂度是O(H),我们看看这个H是多少:
按照上面性质,如果一个红黑树中所有的结点都是黑色的,由于性质5的限制,它一定是一棵完美的二叉查找树!查找的时候时间复杂度妥妥的O(log n)。这样也能看出来,一棵高度为h的这种完美红黑树,它的结点有2h-1个(这个算术题前面提到过...)。
当然这只是极端情况,我们来想一下,一棵正常的接地气的红黑树,它的结点一定比2h-1要多吧(往完美红黑树里多塞几个红结点,不破坏红黑树性质就行),而且上面的那个h其实相当于是黑结点的高度,看看性质4,如果一个结点是红的,它的子结点一定是黑的,那么就说明它实际的高度最多可能是h的两倍,我们就按照最差的情况算,它的实际高度H=2h(注意这个h是黑高),那么可知:
结点数n >= 2h-1
带入最高的H 得到:n >= 2H/2-1
做个算术,得到: H <= 2log(n+1) ,OK的。
至于红黑树的性质被破坏了怎么办,源码分析的时候一起说吧,尼玛,终于能进入正题了!!
源码分析:
- 先看下TreeMap内部结构:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable { private final Comparator<? super K> comparator; private transient Entry<K,V> root = null; private transient int size = 0; private transient int modCount = 0; public TreeMap() { comparator = null; } public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }内部结构很简单:
一个root结点,由一个Entry类表示,Entry后面分析。
size做结点数量记录,modCount来支持快速失败。
还有一个比较器,比较器可以为null,但要求key本身是可比较的,否则会发生错误。
下面看一个Entry这个类的结构:
private static final boolean RED = false; private static final boolean BLACK = true; static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left = null; Entry<K,V> right = null; Entry<K,V> parent; boolean color = BLACK; Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; } public K getKey() { return key; } public V getValue() { return value; } public V setValue(V value) { V oldValue = this.value; this.value = value; return oldValue; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>)o; return valEquals(key,e.getKey()) && valEquals(value,e.getValue()); } public int hashCode() { int keyHash = (key==null ? 0 : key.hashCode()); int valueHash = (value==null ? 0 : value.hashCode()); return keyHash ^ valueHash; } public String toString() { return key + "=" + value; } }
Entry这个内部类的结构也很简单:有K-V,有指向parent、左子结点和右子结点的域,有表示红黑颜色的域(默认是黑色)。
- 看完了内部结构,我们来从查找、插入和删除三个操作作为重点开始分析。
public V get(Object key) { Entry<K,V> p = getEntry(key); return (p==null ? null : p.value); } final Entry<K,V> getEntry(Object key) { // Offload comparator-based version for sake of performance if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); 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; } return null; } final Entry<K,V> getEntryUsingComparator(Object key) { 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) p = p.left; else if (cmp > 0) p = p.right; else return p; } } return null; }
可见查找方法非常简单,以get方法为入口,内部会根据按比较器或者Key自身的可比较性比较来做分支,但具体逻辑都是一样的,和之前介绍二叉查找树时的伪代码基本一致。
public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { // TBD: // 5045147: (coll) Adding null to an empty TreeSet should // throw NullPointerException // // compare(key, key); // type check root = new Entry<K,V>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths 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(); 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<K,V>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); size++; modCount++; return null; }
分析一下:
插入方法中,首先处理空树的情况,空树直接将给定的K-V包装成一个Entry,设置为树根结点,默认就是黑色的。通过里面的注释可也以看到,这里存在一个bug。
如果不是空树的话,接下来会根据比较器或者Key自身的可比较性分成两个分支,里面的逻辑都一样,从树根不断往下找,这个过程中如果找到了Key对应的Entry,那就变成替换操作了,覆盖并返回Entry的旧值,方法结束。
但如果并没有在上面的过程中结束,那么就说明是一个插入操作。接下来会根据给定的K-V和上面过程中得到的parent来构造一个Entry,然后根据上面过程中最后的比较结果cmp,来决定将Entry作为parent的左子结点还是右子结点。
由于这个过程已经破坏了红黑树的性质(前面看到Entry默认的color是黑色,那么做完插入操作,插入结点位置所在的简单路径上,黑结点多出来一个,破坏了性质5),接下来会调用一个fixAfterInsertion方法来恢复红黑树的性质,看下这个方法: 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); //情况1 setColor(y, BLACK); //情况1 setColor(parentOf(parentOf(x)), RED); //情况1 x = parentOf(parentOf(x)); //情况1 } else { if (x == rightOf(parentOf(x))) { x = parentOf(x); //情况2 rotateLeft(x); //情况2 } setColor(parentOf(x), BLACK); //情况3 setColor(parentOf(parentOf(x)), RED); //情况3 rotateRight(parentOf(parentOf(x))); //情况3 } } 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; }
我们先大概浏览一下这个方法的过程:
首先将x的color设置成红色;然后是一个while循环,条件是当x结点不是root结点且x结点的父结点是红色;在while中,可能会改变x的指向。
分析一下:
开始将x的color设置成红色,之后就会有下面两种情况:
1.那么如果x是root结点的话,直接将其设置为黑色就好了,满足所有性质。
2.如果x的父结点是黑色,那么也ok了,满足所有性质。
所以while的出口条件就是上面两种情况。
那么如果x的父结点的颜色是红色的话,就会破坏性质4,怎么修复呢?分为三种情况:
情况1:x的叔结点y(注意这里是叔结点,叔叔的叔.....)是红色。
在情况1下做出的处理过程如图示。我们分析下,处理之前,性质4被破坏。然后做出如下处理:
1.将x的父结点设置为黑色。
2.将x的叔结点y设置为黑色。
3.将x的父结点的父结点设置为红色。
4.将x指向x的父结点的父结点。
这个处理过程首先恢复了性质4,也并没有破坏性质5,而且将x向上(根结点)推进了2层。
注:图中画的是x是右子结点的情况,x是左子结点也做相同的操作,没差别。
做完这个处理后,继续下一轮迭代。
情况2:x的叔结点y是黑色,且x的是一个右子结点。
在情况2下做出的处理过程如图示。处理过程如下:
1.首先将x指向x的父结点。
2.对x做左旋转。
情况2只是个过渡阶段,过渡到情况3。
情况3:x的叔结点y是黑色,且x的是一个左子结点。
在情况3下做出的处理过程如图示。分析下,处理前性质4被破坏,然后做出如下处理:
1.将x的父结点设置为黑色。
2.将x的父结点的父结点设置为红色。
3.对x的父结点的父结点做右旋转。
做完这些处理,会发现恢复了性质4,同时并没有破坏性质5,而且完成后,x的父结点一定是黑色的了,所以下次while会退出,恢复过程结束。
我们对插入结点后的fix做个总体分析,先看一下这三种情况的过程图:
很明显,一旦进入情况2就会接着进入情况3,然后结束;最差的情形就是一直在情况1周旋,但是别忘了情况1处理过程中每次会将x结点向上提升2个层级,所以最坏的情况下也会在O(log n)时间内完成恢复。
由于插入过程的时间复杂度是O(log n),恢复过程的时间复杂度也是O(log n),所以,红黑树插入操作总的时间复杂度是O(log n)。
最后看下删除操作:
public V remove(Object key) { Entry<K,V> p = getEntry(key); if (p == null) return null; V oldValue = p.value; deleteEntry(p); return oldValue; }
方法实现中会首先查找key对应的Entry,如果找不到,返回null;如果找到了,会调用一个deleteEntry方法来删除找到的Entry,然后返回被删除Entry的值。
看下deleteEntry方法的实现: private void deleteEntry(Entry<K,V> p) { modCount++; size--; if (p.left != null && p.right != null) { //条件1 Entry<K,V> s = successor (p); p.key = s.key; p.value = s.value; p = s; } Entry<K,V> replacement = (p.left != null ? p.left : p.right); if (replacement != null) { //条件2 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.left = p.right = p.parent = null; if (p.color == BLACK) fixAfterDeletion(replacement); } else if (p.parent == null) { //条件3 root = null; } else { //条件4 if (p.color == BLACK) fixAfterDeletion(p); if (p.parent != null) { //条件5 if (p == p.parent.left) p.parent.left = null; else if (p == p.parent.right) p.parent.right = null; p.parent = null; } } }
先概括一下,deleteEntry方法要做两件事:
1.删除某个结点p(注意这个p可能不是方法传进来的p)。
2.删除结点p后,检查下p的颜色,如果是黑色,那么p原本所在的简单路径上就会少了一个黑结点,破坏了性质5,需要fix。
详细分析一下:
其实上面方法的过程和前面提到的删除一个二叉查找树的结点的过程是一样的思路,只是代码上的条件搭配有点区别。我们按照之前的思路走一遍看看。
1.假设要删除的结点p没有任何子结点,那么会分两种情况:
1.1.p是根结点,代码会走条件3,然后当前红黑树变成了一棵空树,不需要fix了,方法结束。
1.2.p不是根结点,代码会走条件4,然后判断一下p的颜色,如果是黑色,fix一下。然后代码继续走条件5,切断p和其父结点的关联,删除p。
2.假设要删除的结点p只有左子结点,那么代码会走条件2,然后分种情况:
2.1.p是根结点,那么p的左子结点replacement会代替p成为新的根结点。这种情况下会破坏红黑树性质,因为p必然的黑色的,如果replacement是红色的,那么破坏了性质2;如果replacement是黑色的,那么replacement所在路径上少了一个黑结点,破坏了性质5.所以后面会做一个fix。
3.假设要删除的结点p只有右子结点,和上面是同一种情况。
4.假设要删除的结点p既有左子结点又有右子结点,那么代码会走条件1,先找到p的后继结点s,然后将s的数据(K-V)拷贝到p上,然后将p指向s(注意现在的p是s了),然后又分两种情况:(注意,p没指向s之前,p的后继结点s肯定在p的右子树中,而且s肯定没有左子结点)
4.1.p没有右子结点,那么replacement为null,代码进入条件4,如果p的颜色是黑色,fix一下,然后删除p(注意这里删除的已经不是传进来的p了)。
4.2.p有右子结点replacement,代码进入条件2,用replacement替换p,判断一下p的颜色,如果是黑色,需要fix一下。
好了,过程清晰了,下面看一下比较关键的fix方法-fixAfterDeletion:
private void fixAfterDeletion(Entry<K,V> x) { while (x != root && colorOf(x) == BLACK) { if (x == leftOf(parentOf(x))) { Entry<K,V> sib = rightOf(parentOf(x)); if (colorOf(sib) == RED) { setColor(sib, BLACK); //情况1 setColor(parentOf(x), RED); //情况1 rotateLeft(parentOf(x)); //情况1 sib = rightOf(parentOf(x)); //情况1 } if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { setColor(sib, RED); //情况2 x = parentOf(x); //情况2 } else { if (colorOf(rightOf(sib)) == BLACK) { setColor(leftOf(sib), BLACK); //情况3 setColor(sib, RED); //情况3 rotateRight(sib); //情况3 sib = rightOf(parentOf(x)); //情况3 } setColor(sib, colorOf(parentOf(x)));//情况4 setColor(parentOf(x), BLACK); //情况4 setColor(rightOf(sib), BLACK); //情况4 rotateLeft(parentOf(x)); //情况4 x = root; } } else { //对称过程 Entry<K,V> sib = leftOf(parentOf(x)); if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateRight(parentOf(x)); sib = leftOf(parentOf(x)); } if (colorOf(rightOf(sib)) == BLACK && colorOf(leftOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { if (colorOf(leftOf(sib)) == BLACK) { setColor(rightOf(sib), BLACK); setColor(sib, RED); rotateLeft(sib); sib = leftOf(parentOf(x)); } setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(leftOf(sib), BLACK); rotateRight(parentOf(x)); x = root; } } } setColor(x, BLACK); }
在看这个方法之前,我们在回头看下deleteEntry方法中哪些地方调用了这个方法:条件2和条件4。
对于条件2,要恢复的目标结点x是代替p的新结点replacement,它还是属于红黑树中的结点。那么恢复的话分两种情况:
1.如果replacement是红色的,直接改成黑色就好了,相当于补充了一个黑结点,恢复了在deleteEntry中破坏的性质5。
2.如果replacement是黑色的,那么就要想其他办法补充一个黑结点了。
对于条件4,要恢复的目标结点x是deleteEntry方法中被删除的p结点,它已经不属于红黑树了,但是这里可以将其看成是红黑树的一个叶子结点(还记得性质3么,这个叶子结点是概念上的,其实就是个null)。那么这里要恢复的也是性质5,需要补充一个黑结点。
好了,我们还是概览一下fixAfterDeletion方法。内部还是一个while循环,出口条件是x为根结点或者x的颜色为红色,在while循环内部,可能会向上(根结点)推进x。
那么while循环内部具体是怎么恢复红黑性质的呢?
这里我们假设传入fixAfterDeletion的x结点除了结点上的颜色外,自带一重黑色(天生自带技能)。也就是说,x可能是双重黑色,也可能是红黑色。这样可以保证性质5,但破坏了性质1。我们就围绕这个来分析,看看怎么恢复性质1。
分4种情况:
情况1:x的兄弟结点s是红色的。
在情况1下做出的处理过程如图示。分析下过程:
1.将x的兄弟结点s设置为黑色。
2.将x的父结点设置为红色。
3.对x的父结点进行左旋转。
4.将s指向旋转后的x的兄弟结点。
因为x的的兄弟结点为红色,所以x的父结点一定为黑色。所以上述操作不会破坏红黑树的性质。
经过了上面的操作,情况1就转变成情况2、情况3或者情况4。
情况2:x的兄弟结点s是黑色的,且s的两个子结点都是黑色的。
在情况2下做出的处理过程如图示。分析下过程:
1.将x的兄弟结点s设置为红色。
2.将x的指向x的父结点p。
这个过程的思路其实是将x和s同时去掉一重黑色,然后将p加上一重黑色,来保证性质5不被破坏。因为x自带一层黑色,所以具体实现中,将s变成红色(去掉一重黑色),再将x指向p(相当于之前的x去掉一重黑色,p加上了一重黑色) 。
接下来就会以p作为新的x结点进行下一轮的while循环。这里注意下处理前p结点的颜色,由于x的兄弟结点s的颜色是黑色,所以p结点的颜色可能为红色也可能为黑色。如果p结点本来的颜色为红色,那么下一轮while就会直接退出了,并且在最后将p的颜色设置为黑色,完成修复;如果p结点本来的颜色为黑色,那么就继续下一轮循环,但是!!!x提升了一层。
情况3:x的兄弟结点s是黑色的,s的左子结点是红色的,s的右子结点是黑色的。
在情况3下做出的处理过程如图示。分析下过程:
1.将x的兄弟结点s的左子结点设置为黑色。
2.将x的兄弟结点设置为红色。
3.对x的兄弟结点s进行右旋转。
4.将s指向旋转后的x的兄弟结点。
交换s和其左子结点的颜色,然后对s进行右旋转,不会破坏红黑树的性质。
做完上述操作,会由情况3转变为情况4。
情况4:x的兄弟结点s是黑色的,且s的右子结点是红色的。
在情况4下做出的处理过程如图示。分析下过程:
1.将x的兄弟结点s的设置为x的父结点p的颜色。
2.将x的父结点p设置为黑色。
3.将x的兄弟结点s的右子结点设置为黑色。
4.对x的父结点p进行左旋转操作。
5.将x指向根结点。
分析下,进行了上述1、2、3、4步后,红黑树性质不会被破坏,并且当进行完第4步后,相当于在x所在的路径上增加了一个黑结点,那么x就可以将自带的一重黑色去掉了。最后将x指向根结点,完成修复。
我们对删除结点后的fix做个总体分析,先看一下这四种情况的过程图:
很明显,如果从情况1进入情况2,那么会马上结束;一旦进入情况3,会紧接着进入情况4,然后结束;最差的情形就是一直在情况,2周旋,但是情况2处理过程中每次会将x结点向上提升1个层级,所以最坏的情况下也会在O(log n)时间内完成恢复。
由于删除过程中,查找过程的时间复杂度最坏是O(log n),删除过程中可能存在找后继结点的过程,最坏也是O(log n),恢复过程的时间复杂度也是O(log n),所以,红黑树删除操作总的时间复杂度是O(log n)。
- 再看下TreeMap中其他一些方法。
首先,TreeMap支持从一个Map集合或者一个有序Map集合来构建:
public TreeMap(Map<? extends K, ? extends V> m) { comparator = null; putAll(m); } public void putAll(Map<? extends K, ? extends V> map) { int mapSize = map.size(); if (size==0 && mapSize!=0 && map instanceof SortedMap) { Comparator c = ((SortedMap)map).comparator(); if (c == comparator || (c != null && c.equals(comparator))) { ++modCount; try { buildFromSorted(mapSize, map.entrySet().iterator(), null, null); } catch (java.io.IOException cannotHappen) { } catch (ClassNotFoundException cannotHappen) { } return; } } super.putAll(map); } 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) { } }
如果通过一个Map集合来构建TreeMap,内部会调用putAll方法,putAll方法中,判断一下如果传入的Map是有序Map,那么会通过一个buildFromSorted方法来进行Map构建;
同样在通过一个有序Map集合构建TreeMap时,也会调用buildFromSorted方法。
看下这个方法:
private void buildFromSorted(int size, Iterator it, java.io.ObjectInputStream str, V defaultVal) throws java.io.IOException, ClassNotFoundException { this.size = size; root = buildFromSorted(0, 0, size-1, computeRedLevel(size), it, str, defaultVal); } private final Entry<K,V> buildFromSorted(int level, int lo, int hi, int redLevel, Iterator it, java.io.ObjectInputStream str, V defaultVal) throws java.io.IOException, ClassNotFoundException { if (hi < lo) return null; int mid = (lo + hi) / 2; Entry<K,V> left = null; if (lo < mid) left = buildFromSorted(level+1, lo, mid - 1, redLevel, it, str, defaultVal); // extract key and/or value from iterator or stream K key; V value; if (it != null) { if (defaultVal==null) { Map.Entry<K,V> entry = (Map.Entry<K,V>)it.next(); key = entry.getKey(); value = entry.getValue(); } else { key = (K)it.next(); value = defaultVal; } } else { // use stream key = (K) str.readObject(); value = (defaultVal != null ? defaultVal : (V) str.readObject()); } Entry<K,V> middle = new Entry<K,V>(key, value, null); // color nodes in non-full bottommost level red if (level == redLevel) middle.color = RED; if (left != null) { middle.left = left; left.parent = middle; } if (mid < hi) { Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel, it, str, defaultVal); middle.right = right; right.parent = middle; } return middle; } private static int computeRedLevel(int sz) { int level = 0; for (int m = sz - 1; m >= 0; m = m / 2 - 1) level++; return level; }
看到这个方法,有没有想起来剪断二叉查找树的情形?没错,相当于那个的一个反过程。
buildFromSorted方法内部会先根据给定的元素数量来算一个层级,这个高度总是当前给定的数量对应的最后一层未满的完全二叉树的高度,如果数量达到了满二叉树的数量要求,这个层级会加1,比如元素数量为15,那么层级就变成4了。
然后会调用另一个重载的buildFromSorted方法,第二个buildFromSorted方法会返回root结点;在第二个方法中,会不断的二分给定的元素集合,递归的进行构建(先构建左子树,再构建右子树),最后返回的就是整个树的根结点。构建过程中,如果某个结点是从root往下层级达恰好是之前算出来的那个层级的话,颜色会被设置为红色,其余的都默认为黑色。
注意第二个buildFromSorted方法支持从迭代器或者对象流中获取元素。
public boolean containsValue(Object value) { for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e)) if (valEquals(value, e.value)) return true; return false; } final Entry<K,V> getFirstEntry() { Entry<K,V> p = root; if (p != null) while (p.left != null) p = p.left; return p; }
通过value查找,肯定得遍历了,看看怎么遍历的。首先找到TreeMap中第一个元素(相当于二叉查找树中最小的元素),然后不断的找它的后继结点。
最后我们简单看一下TreeMap对NavigableMap接口的实现。
NavigableMap接口中定义了一些特殊的查找方法,比如lowerKey、floorKey等等,这里挑几个解析一下,剩下的都是类似的逻辑。
看下lowerKey方法: public K lowerKey(K key) { return keyOrNull(getLowerEntry(key)); } final Entry<K,V> getLowerEntry(K key) { Entry<K,V> p = root; while (p != null) { int cmp = compare(key, p.key); if (cmp > 0) { if (p.right != null) p = p.right; else return p; } else { if (p.left != null) { p = p.left; } else { Entry<K,V> parent = p.parent; Entry<K,V> ch = p; while (parent != null && ch == parent.left) { ch = parent; parent = parent.parent; } return parent; } } } return null; } static <K,V> K keyOrNull(TreeMap.Entry<K,V> e) { return e == null? null : e.key; }
首先,lowerKey就是要找比给定key小的最大的key。lowerKey中会先调用一个getLowerEntry方法来获取相应的Entry,getLowerEntry方法中从根结点开始往下比较。
对于树中的某个结点p,如果给定的Key比该结点的key大,那么目标结点lower就可能在p的右子树中,如果p没有右子结点那就是p喽;如果p有右子结点r,但是给定的Key比r的key小,那么目标结点还是p。
对于树中的某个结点p,如果给定的Key比该结点的key小,那么目标结点lower就可能在p的左子树中,如果p没有左子结点,那么相当于没有合适的目标结点,返回null;如果p有左子结点,那么继续迭代。
public Map.Entry<K,V> lowerEntry(K key) { return exportEntry(getLowerEntry(key)); } static <K,V> Map.Entry<K,V> exportEntry(TreeMap.Entry<K,V> e) { return e == null? null : new AbstractMap.SimpleImmutableEntry<K,V>(e); }
内部逻辑和lowerKey一致,只是返回了一个不可变的Entry实例。
public Map.Entry<K,V> lastEntry() { return exportEntry(getLastEntry()); } final Entry<K,V> getLastEntry() { Entry<K,V> p = root; if (p != null) while (p.right != null) p = p.right; return p; }很简单,找最右边的Entry。在看一个相关的pollLastEntry方法:
public Map.Entry<K,V> pollLastEntry() { Entry<K,V> p = getLastEntry(); Map.Entry<K,V> result = exportEntry(p); if (p != null) deleteEntry(p); return result; }
找最右边的Entry,从集合中删除这个Entry,并返回这个Entry的一个不可变实例。
NavigableMap接口中还定义了一系列返回各种视图的操作,来看下TreeMap怎么支持的。
首先TreeMap中定义了一个静态内部类NavigableSubMap作为基类: static abstract class NavigableSubMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, java.io.Serializable { /** * The backing map. */ final TreeMap<K,V> m; final K lo, hi; final boolean fromStart, toEnd; final boolean loInclusive, hiInclusive;
NavigableSubMap基类内部包含了一系列的范围相关的属性,这些属性在构造实例的时候由外部指定。NavigableSubMap中还定义了一些基础方法、工具方法等,这些方法基本上都是基于内部的TreeMap实现的,有了前面的基础,很容易看懂。
TreeMap中提供了AscendingSubMap和DescendingSubMap来支持上面提到的一些视图方法,这两个类继承自NavigableSubMap,区别只是内部比较规则相反。
还有一些方法没提到,不过有了本篇的分析,相信剩余的源码都比较容易看懂了。
最后,TreeSet是基于TreeMap实现的:
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable { private transient NavigableMap<E,Object> m; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); TreeSet(NavigableMap<E,Object> m) { this.m = m; } public TreeSet() { this(new TreeMap<E,Object>()); } public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<E,Object>(comparator)); } ...
相关推荐
7. **集合框架增强**:在JDK1.6中,集合框架进一步优化,如`TreeSet`和`TreeMap`增加了对比较器的支持,`LinkedList`改进了迭代性能,`Collections`工具类提供了丰富的静态方法,如排序和转换。 8. **枚举类型**:...
2. **集合框架**:在1.6版本中,Java的集合框架已经相当成熟,包括List(如ArrayList和LinkedList)、Set(如HashSet和TreeSet)、Map(如HashMap和TreeMap)等接口及其实现,以及实用工具类`Collections`和`Arrays`...
《JDK1.6 API帮助文档》是Java开发者不可或缺的参考资料,它详尽地阐述了JDK1.6版本中的各种类、接口、方法和异常等核心组件。这份API文档以CHM(Microsoft Compiled HTML Help)格式压缩为"JDK1.6 API帮助文档.CHM....
对于 JDK 1.1 用户,本书还讨论了如何使用 Java Collection Framework 的子集。此外,还介绍了一种名为 JGL (Java Generic Library) 的第三方库,该库在 Java Collection Framework 出现之前就已经存在,并且提供了...
Java JDK实例宝典源码是Java开发者的重要参考资料,它涵盖了JDK中的各种核心类库、API及其实现的源代码。这些源码对于深入理解Java语言的底层运作机制、优化代码以及解决实际问题有着不可估量的价值。下面,我们将...
《JDK API 1.6 中文版:深入解析与应用》 JDK(Java Development Kit)是Java编程语言的核心工具集,它包含了编译、运行、调试Java程序所需的所有工具和库。JDK 1.6是Oracle公司发布的一个重要版本,其API...
2. **集合框架**:Java 1.6的集合框架进行了进一步完善,包括List(ArrayList、LinkedList)、Set(HashSet、TreeSet)、Map(HashMap、TreeMap)等接口和实现类。它们为数据存储和检索提供了强大的抽象,支持泛型,...
TreeSet基于TreeMap,元素按自然排序或自定义比较器排序,不允许null元素。 4. **Queue和Deque**:Queue接口表示一个先进先出(FIFO)的数据结构,LinkedList实现了Queue接口。Deque接口扩展了Queue,支持双端队列...
### Java中的Set与Map集合详解 #### 一、Set系列集合概述 Set接口是`java.util.Collection`框架的一部分,它代表一个不允许重复元素的集合。...如果需要对元素进行排序,则应该使用`TreeSet`或`TreeMap`。
**Java JDK API 1.6 中文文档** Java Development Kit (JDK) 是Java编程语言的核心组成部分,它包含了编译器、调试工具、运行时环境(JRE)以及丰富的API库。API(Application Programming Interface)是一组预先...
- 引入`java.util.TreeSet`和`java.util.TreeMap`,它们基于红黑树实现,提供了高效的有序集合操作。 - `java.util.LinkedList`的添加,提供了双端队列(Deque)功能。 8. **垃圾回收器优化** JDK 1.4的垃圾回收...
### Java集合面试题全集解析 #### 一、List、Set、Map的区别 - **List**:有序集合,允许重复元素。典型实现包括`ArrayList`(基于数组)、`LinkedList`(基于双向链表)。适用于频繁的插入和删除操作时选择`...
7. **增强的集合框架**:JDK 1.4对集合框架进行了优化,例如`HashMap`和`HashSet`的性能提升,以及`TreeSet`和`TreeMap`类的稳定排序功能。此外,还引入了`Collections.synchronizedXXX`方法,用于创建线程安全的...
4. **java.util.collections**:集合框架的核心,包括List、Set、Queue等接口,以及它们的实现类,如ArrayList、LinkedList、HashSet、TreeSet等。这些类提供了数据存储和管理的功能。 5. **java.util.Map**:Map...
JDK 1.4进一步完善了集合框架,例如,`TreeSet`和`TreeMap`实现了`NavigableSet`和`NavigableMap`接口,提供了更多的排序和查找功能。此外,`Collections`类增加了更多实用方法,如`copy()`用于复制集合。 6. **...
- 常见的包括排序(Collections.sort())、搜索(Collections.indexOfSubList())、混编(Collections.shuffle())、查找最大/最小值(Collections.max()、Collections.min())等。 5. **底层数据结构**: - **...
描述中提到了一个可能的JDK“BUG”,这可能指的是当尝试用非自然排序的类作为`TreeSet`或`TreeMap`的元素或键时,可能导致预期之外的结果。例如,如果自定义类没有正确的实现`Comparable`接口,或者`Comparator`的...
常见的集合类如 List(ArrayList、LinkedList)、Set(HashSet、TreeSet)和 Map(HashMap、TreeMap、Hashtable)等。 10. **Java 语言优点**: 包括平台无关性、自动内存管理、丰富的类库、强大的异常处理、面向...
`TreeMap`使用红黑树保持键的排序,而`LinkedHashMap`则维护插入顺序或访问顺序。 在JDK 1.8中,集合框架引入了一些优化。例如,`HashMap`在容量达到一定阈值时会进行扩容,新的容量是原容量的1.5倍,这个设计是...