前些天在网上偶然 间看到一篇关于java 8的 HashMap的分析文章,其对java7进行了大量改进,核心是引进了红黑树,提高了hashcode碰撞严重时的查找性能。这一点引发了我对红黑树的兴趣。小白的我表示之前对2叉树也是一之半解,更不要提复杂的红黑树了。因此下定决心分析下红黑树。
首先介绍几个我在这周过程中在网上看到的一些比较好的文章如下:
关于红黑树:
http://www.kuqin.com/shuoit/20160630/352539.html 对插入和删除分析的比较到位
http://blog.csdn.net/v_july_v/article/details/6105630 比较详细的介绍了红黑树的相关知
http://blog.csdn.net/v_JULY_v/article/details/6284050 july的系列文章,
http://www.cs.usfca.edu/~galles/visualization/Algorithms.html 算法的动态过程
java 中的数据结构分析HashMap TreeMap
http://tech.meituan.com/java-hashmap.html 美团 分析 的重新认识HashMap
http://www.importnew.com/21818.html TreeMap 实现
https://www.ibm.com/developerworks/cn/java/j-lo-tree/ TreeMap
http://yemengying.com/2016/05/07/threadsafe-hashmap/ HashMap并发性问题
接下来,对我这周的分析进行总结:
红黑树
首先介绍下红黑树的相关知识,其是一颗平衡的二叉树。其在二叉查找树的基础上增加了红黑色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。
具体的5个性质简单要介绍,1)结点要么为黑要么为红、2)根结点为黑色、3)每个叶节点(包括null节点)为黑色、4)红色结点的叶结点必须为黑色、5)所有的节点到叶节点的黑结点数相同。
从上面五个性质,我们可以得出如下 结点可以是连续黑色的,n个结点的高度为logN。
红黑树的插入和删除是通过结点的旋转(左右旋转),节点变色来重新平衡。接下来重点介绍下插入的恢复过程:
插入必为红色,其叔叔为红色,将叔,父变黑,祖父变红。叔叔为黑色,根据其父是左结点还是右结点,进行相应的左旋与右旋。而后将父节点变黑,祖父变红。如此直到根结点。
TreeMap 与HashMap
之前 我就在想,hashMap如果用到了红黑树,可以直接引用treemap中的红黑树算法,但实际是HashMap中也自己实现了一套HashMap。首先分析下treeMap的算法实现:
TreeMap在构造有一定要求,如果在构造时没有比较器。则在put时,如果key没有实现comparable接口,则会抛出异常。因此要么在构造时指定,要么key实现comparable接口。
TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
接着就是通过一个标准的2叉树查找实现,找到插入点。
Entry<K,V> t = root; 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);
查到插入点parent 后,插入节点,而后通过fixAfterInsertion(e)自修复
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;
修复过程如下:
首先将节点变为红色,
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 { 与上面类似,只是方向不同,这里的逻辑对应的是父节点是祖父节点的右孩子, 所以如果当前节点是左孩子,先要右旋。形成两个连续的红色,接着祖父变红,父变黑,因为之前是右孩子,左旋。 }} root.color = Baclk; }
最后当前结点为root时,将root结点改为Black,插入结束。
删除时类似,先找到节点,删除后恢复,类似。
HashMap
HashMap 在1.8 之前 ,由一个链表的数组。不是一个单纯的树。而java8 后,同样还是数组,但是数组中的值有了变化,并一定是链表,默认的设置当前超过8时,链表转换成红黑树的结构,但是还是会保持一个以红黑树的root的节点为链表头的链表。
其中treeMap中与另外一个与TreeMap中不同的是,key不要求实现comparable,或者指定比较器,而是用hashcode值 的大小来比较,注意不是hashcode的 转换后,比数据长度&后的 值 ,因为同一个链表中的该值是相等的,而hashcode不一定一样,但是如果 相等,则会通过 object对象的,如下方法,如果没有重写hashcode,则默认用如下方法
System.identityHashCode(a)
我们重点介绍下数据节点是红黑树的插入情况,
调用put 方法时,先将hashcode的高16位与低16位异或,让高位也参与 位置的计算,减小hash冲突,而后与数组的大小进去位与运算,而数组的大小是2的幂,这样的目的是为了在扩容时,原来在在同一个数组的位置,要么在原来的位置,要么在原来位置的两倍。减小hash运算。
接着如果计算出来的位置的节点 当前是红黑树,则就是红黑树的插入过程,但是不同的时,需要将计算出来的root节点,移动到链表的头部。
而如果大小等于8时,就需要将链表转换成结点,先要将每个结点转换成红黑树节点 ,而后依次插入,形成红黑树。删除8时,需要重新转换为链表。
还有一个与java7 最大的不同就是 resize的过程。 java7 的实现 方式,重新遍历每个节点,重新计算插入扩容后的数组中,而java8 是没有重新计算hashcode,而是将原hashcode后的hash值 与扩容后的那个增加位进行与运算,决定 数组的位置。
先介绍到这里,后面在补充。