前言 TreeMap是基于红黑树结构实现的一种Map,要分析TreeMap的实现首先就要对红黑树有所了解。
构造图如下: 蓝色线条:继承 绿色线条:接口实现
红黑树简介 TreeMap底层是基于红黑树(Red-Black tree) 实现,所以在学习TreeMap之前我们先来了解下红黑树。
红黑树又称红-黑二叉树,它首先是一颗二叉树,它具有二叉树所有的特性。同时红黑树更是一棵自平衡的排序二叉树。
我们知道一颗基本的二叉排序树他们都需要满足一个基本性质–即树中的任何节点的值大于它的左子节点,且小于它的右子节点。按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉排序树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树),这样势必会导致二叉树的检索效率大大降低(O(n)),所以为了维持二叉排序树的平衡,大牛们提出了各种实现的算法,如:AVL,SBT,伸展树,TREAP,红黑树等等。
平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个等等子节点,其左右子树的高度都相近。
红黑树顾名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树而言我们必须增加如下规则:
每个节点都只能是红色或者黑色
根节点是黑色
每个叶节点(NIL节点,空节点)是黑色的
如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
红黑树示意图如下:
但是在在添加或删除节点后,红黑树就发生了变化,可能不再满足上面的5个特性,为了保持红黑树的以上特性,就有了三个动作:左旋、右旋、着色。
下面来看下什么是红黑树的左旋和右旋:
对x进行左旋,意味着”将x变成一个左节点”。
对y进行右旋,意味着”将y变成一个右节点”。
如果还是没看明白,下面找了两张左旋和右旋的动态图:
TreeMap简介 定义 1 2 3 public class TreeMap <K ,V > extends AbstractMap <K ,V > implements NavigableMap <K ,V >, Cloneable , java .io .Serializable
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
TreeMap 实现了Cloneable接口,意味着它能被克隆。
TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。
TreeMap基于红黑树(Red-Black tree)实现 。该映射根据其键的自然顺序进行排序 ,或者根据创建映射时提供的Comparator进行排序 ,具体取决于使用的构造方法。
TreeMap的基本操作containsKey、get、put和remove的时间复杂度是log(n)。
另外,TreeMap是非同步 的。它的iterator方法返回的迭代器是fail-fast 的。
TreeMap属性 1 2 3 4 5 6 7 8 9 10 11 private final Comparator<? super K> comparator;private transient Entry<K,V> root;private transient int size = 0 ;private transient int modCount = 0 ;
TreeMap的本质是R-B Tree(红黑树),它包含几个重要的成员变量:root, size, comparator。
root是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。
红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的,默认按照key的自然顺序排序。
size是红黑数中节点的个数。
Entry是树的节点类,我们来看一下Entry的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 static final class Entry <K ,V > implements Map .Entry <K ,V > { K key; V value; Entry<K,V> left; Entry<K,V> right; 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类理解起来比较简单(因为我们前面看过很多的Entry类了),主要是定义了树的孩子和父亲节点引用,和红黑颜色属性,并对equals和hashCode进行重写,以利于比较是否相等。
TreeMap构造函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public TreeMap () { comparator = null ; } public TreeMap (Comparator<? super K> comparator) { this .comparator = comparator; } public TreeMap (Map<? extends K, ? extends V> m) { comparator = null ; putAll(m); } 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) { } }
从构造方法中可以看出,要创建一个红黑树实现的TreeMap必须要有一个用于比较大小的比较器,因为只有能够比较大小才能实现红黑树的左孩子 < 树根 < 右孩子
的特点。
源码解析 红黑树的添加原理及TreeMap的put实现 将一个节点添加到红黑树中,通常需要下面几个步骤:
1.将红黑树当成一颗二叉查找树,将节点插入。 这一步比较简单,就上开始我们自己写的二叉查找树的操作一样,至于为什么可以这样插入,是因为红黑树本身就是一个二叉查找树。 2.将新插入的节点设置为红色。 有没有疑问,为什么新插入的节点一定要是红色的,因为新插入节点为红色,不会违背红黑规则第5条,少违背一条就少处理一种情况。 3.通过旋转和着色,使它恢复平衡,重新变成一颗符合规则的红黑树。 要想知道怎么样进行左旋和右旋,首先就要知道为什么要进行左旋和右旋。
我们来对比下红黑树的规则和新插入节点后的情况,看下新插入节点会违背哪些规则。
① 节点是红色或黑色。 这一点肯定是不会违背的了。 ② 根节点是黑色。 这一点也不会违背了,如果是根节点,只需将根节点插入就好了,因为默认是黑色。 ③ 每个叶节点(NIL节点,空节点)是黑色的。 这一点也不会违背的,我们插入的是非空的节点,不会影响空节点。 ④ 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)这一点是有可能违背的,我们将新插入的节点都设置成红色,如果其父节点也是红色的话,那就产生冲突了。 ⑤ 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 这一点也不会违背,因为我们将新插入的节点都设置成红色。
了解了红黑树左旋和右旋操作,以及新插入节点主要是可能会违背红黑树的规则④后,我们来分析下,添加新节点的过程有哪几种情况:
新插入节点为根节点。这种情况直接将新插入节点设置为根节点即可,无需进行后续的旋转和着色处理。
新插入节点的父节点是黑色。这种情况直接将新节点插入即可,不会违背规则④。
新插入节点的父节点是红色。这种情况会违背规则④,而这种情况又分为了以下几种,下面进行图解:
(1) 新插入节点N的父节点P和叔叔节点U都是红色。方法是:将祖父节点G设置为红色,父节点P和叔叔节点U设置为黑色 ,这时候就看似平衡了。但是,如果祖父节点G的父节点也是红色,这时候又违背规则④了,怎么办,方法是:将GPUN这一组看成一个新的节点,按照前面的方案递归;又但是根节点为红就违反规则②了,怎么办,方法是直接将根节点设置为黑色(两个连续黑色是没问题的)。
(2) 新插入节点N的父节点P是红色,叔叔节点U是黑色或者缺少,且新节点N是P的右孩子。方法是:左旋父节点P 。左旋后N和P角色互换,但是P和N还是连续的两个红色节点,还没有平衡,怎么办,看第三种情况。
(3) 新插入节点N的父节点P是红色,叔叔节点U是黑色或者缺少,且新节点N是P的左孩子。方法是:右旋祖父节点G,然后将P设置为黑色,G设置为红色,达到平衡 。此时父节点P是黑色,所以不用担心P的父节点是红色。
当然上面说的三种情况都是基于一个前提:新插入节点N的父节点P是祖父节点G的左孩子 ,如果P是G的右孩子又是什么情况呢?其实情况和上面是相似的,只需要调整左旋还是右旋,这里就不细说了。
下面看一下TreeMap的put实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 public V put (K key, V value) { Entry<K,V> t = root; if (t == null ) { compare(key, key); 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 { 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 ; } 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; } 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; } } private void rotateRight (Entry<K,V> p) { if (p != null ) { Entry<K,V> l = p.left; p.left = l.right; if (l.right != null ) l.right.parent = p; l.parent = p.parent; if (p.parent == null ) root = l; else if (p.parent.right == p) p.parent.right = l; else p.parent.left = l; l.right = p; p.parent = l; } }
红黑树的删除原理及TreeMap的remove实现 TODO