首先先看TreeMap的继承关系:

继承了抽象类AbstractMap,实现了NavigableMap(SortedMap)、Cloneable、Serializable三个接口

  • NavigableMap(SortedMap):使Key有序,可以获取头尾K-V对,或者获取指定范围内的SubMap
  • Cloneable:支持clone方法
  • Serializable:支持序列化

基于红黑树实现:

    // Red-black mechanicsprivate static final boolean RED   = false;private static final boolean BLACK = true;/*** Node in the Tree.  Doubles as a means to pass key-value pairs back to* user (see Map.Entry).*/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;……}

红黑树特点:

  • 根节点必为黑色
  • 叶节点可红可黑
  • 每条路径上黑节点数量(即黑深度)相等
  • 父子节点不能同时为红色
  • 最长路径的长度不超过最短路径长度的2倍即 

相比于AVL树,红黑树插入时旋转次数基本一致,但是回溯步长为2,耗时更短;删除时旋转次数最多3次,AVL树最多O(logn)次,红黑树性能更好。但是红黑树一般更高(更不平衡)

TreeMap要求,要么节点的Key值实现了Comparable接口,要么传入一个合适的Comparator对象:

    final int compare(Object k1, Object k2) {return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2): comparator.compare((K)k1, (K)k2);}

并且优先使用Comparator进行比较

因此不需要重写Key的hashCode方法和equals方法。因为根本没用到这两个方法,像HashMap就会对key值进行比较:

    //HashMap类,putVal方法if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;

如果两个要求都没有达成,则抛出ClasscastException

节点插入:put方法

先贴源码:

    public V put(K key, V value) {//先把root赋给当前节点,如果为空,代表是棵空树,则直接插入一个新节点Entry<K,V> t = root;if (t == null) {//用来检测Key是否实现了Comparable或TreeMap是否传入了Comparatorcompare(key, key); // type (and possibly null) checkroot = new Entry<>(key, value, null);size = 1;modCount++;return null;}int cmp;Entry<K,V> parent;Comparator<? super K> cpr = comparator;if (cpr != null) {//不断地将传入的key值与当前节点的key值进行比较//如果传入的key值更大,则向右走,更小则向左,相等就直接进行值覆盖do {parent = t;cmp = cpr.compare(key, t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}else {if (key == null)throw new NullPointerException();@SuppressWarnings("unchecked")Comparable<? super K> k = (Comparable<? super K>) key;//这里的过程和if分支的一样do {parent = t;cmp = k.compareTo(t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}//找到父节点以后,就构建节点,并且视情况称为左子结点或右子节点//直到此时还没开始进行红黑树的调整Entry<K,V> e = new Entry<>(key, value, parent);if (cmp < 0)parent.left = e;elseparent.right = e;//fixAfterInsertion方法进行红黑树调整fixAfterInsertion(e);size++;modCount++;return null;}

可以看到,整个插入操作都没有涉及树的调整,直到插入完成才会进行

调整包括重新着色和左右旋转

    private void fixAfterInsertion(Entry<K,V> x) {//先把新插入节点设为红色,内部类Entry的默认值为BLACKx.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 {//以下跟if分支类似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;elsep.parent.right = r; //然后自己变成自己儿子的儿子(贵圈真乱)r.left = p;p.parent = r;}}

左旋的过程就是右儿子当爹,自己变成左儿子,自己的孙节点变成右子节点;右旋反之

以书上的例子说明:按照 :插入 55 56 57 58 83 ,删除57 ,插入59 的顺序建树

图直接从书上拍照过来了:

55直接插入、染黑就行,56染红,57插入时,出现连续红节点,由于默认null节点是黑色,于是发生左旋

插入58时,又出现连续红色,此时父叔节点都是红色,则仅触发重新着色,不进行旋转,56从红变黑是因为根节点每次调整后都会染黑

插入83时再次需要调整,此时情况和57插入时类似,发生了左旋

57因为是叶节点,又是红色,因此可以直接删除

59插入时,再次进行调整,叔节点黑色,自己是左子结点,于是先右旋,右旋之后又触发了左旋条件,于是进行左旋

书上的说明到此就结束了

节点删除:实际调用deleteEntry方法

先贴源码:

    private void deleteEntry(Entry<K,V> p) {modCount++;size--;// 如果p不是叶节点,就选一个继任者接替p,然后让p指向这个接任者if (p.left != null && p.right != null) {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);//如果p至少有一个儿子if (replacement != null) {// 让p的儿子认爹replacement.parent = p.parent;if (p.parent == null)root = replacement;//让p的爹认儿子else if (p == p.parent.left)p.parent.left  = replacement;elsep.parent.right = replacement;// 其他人都有儿子和爹了,p没有作用,因此可以孤立出来了(删除)p.left = p.right = p.parent = null;// 树结构调整if (p.color == BLACK)fixAfterDeletion(replacement);} else if (p.parent == null) { // 如果p没儿子也没爹,说明它是唯一节点(根节点),那么只要把整个树置空即可root = null;} else { //  如果p没儿子但是有爹//  那就先进行重新着色和旋转if (p.color == BLACK)fixAfterDeletion(p);// 如果调整完p还不是根节点,那么此时p肯定是叶节点,直接删除就好if (p.parent != null) {if (p == p.parent.left)p.parent.left = null;else if (p == p.parent.right)p.parent.right = null;p.parent = null;}}}

主要分三种情况:

  • p是叶节点:

    • p是黑色:先进行树调整,再删除
    • p是红色:直接删
  • p不是叶节点:
    • 如果p只有一个子节点,用该子节点替换p
    • 如果p有两个子节点,先找p后继结点(假设为s)代替p(仅替换key、value),然后转变为删除s
    • 以上操作完成后,假如p是黑色,需要进行树结构调整

successor是找后继结点的方法:

    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {//如果是空节点,那就没有后继的,直接退出if (t == null)return null;//如果有右子树,则存在后继节点else if (t.right != null) {Entry<K,V> p = t.right;while (p.left != null)p = p.left;return p;} else {//没有右子树,向上回溯寻找后继节点Entry<K,V> p = t.parent;Entry<K,V> ch = t;while (p != null && ch == p.right) {ch = p;p = p.parent;}return p;}}

根据二叉查找树的性质,p的右子树的最左一个叶节点就是p的后继结点,否则向上回溯寻找祖先节点作为后继

fixAfterDeletion和插入的fixAfterInsertion方法类似,也是进行树结构调整的:

    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);setColor(parentOf(x), RED);rotateLeft(parentOf(x));//然后让新爹的右儿子(以下称为右儿子)继续参与循环sib = rightOf(parentOf(x));}//如果右儿子的子节点全是黑的,那就把它自己染红,然后让当前节点的父节点参与下一轮循环if (colorOf(leftOf(sib))  == BLACK &&colorOf(rightOf(sib)) == BLACK) {setColor(sib, RED);x = parentOf(x);} else {//如果右儿子的右儿子是黑色,那就把它的左儿子变黑,再把右儿子染红并右旋if (colorOf(rightOf(sib)) == BLACK) {setColor(leftOf(sib), BLACK);setColor(sib, RED);rotateRight(sib);//再取新爹的右儿子继续参与循环sib = rightOf(parentOf(x));}//让儿子和爹颜色相同setColor(sib, colorOf(parentOf(x)));//让爹变黑setColor(parentOf(x), BLACK);//让右儿子的右儿子变黑setColor(rightOf(sib), BLACK);//左旋rotateLeft(parentOf(x));//取root,退出循环x = root;}} else { ……//代码差不多,就是把left和right对调一下}}//最后确保root是黑色setColor(x, BLACK);}

这个调整相当绕人

还是用相同的例子,假设现在要删掉59:

根据代码,会找83代替59,之后只要把多余的83叶节点删除即可,由于该节点是红色,删除也不会违反红黑树特性,因此可以直接删除

《码出高效》学习:TreeMap与红黑树相关推荐

  1. 两位阿里大牛联合敬献,码出高效的Java学习笔记,你值得拥有

    写在前面 成长并没有直线式的捷径,"不走弯路就是捷径" 这个观点未必正确.弯路是成长的必经之路,我们在成长的路上需要注意的是保证弯路的前进大方向与直线的行进方向基本一致. 南辕北辙 ...

  2. 死磕 java集合之TreeMap源码分析(一)- 内含红黑树分析全过程

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 TreeMap使用红黑树存储元素,可以保证元素按key值的大小进行遍历. 继承体系 Tr ...

  3. 《码出高效:Java 开发手册》正式发布,83行代码计划启动

    可爱的Java开发者们,让你们久等了! 9月22日杭州云栖大会,众所期待的新书<码出高效:Java 开发手册>正式发布,并宣布将所有图书收益捐赠于技术公益项目. 本次新书发布,邀请了来自阿 ...

  4. 让你久等了!《码出高效:Java 开发手册》正式发布

    可爱的Java开发者们,让你们久等了! 9月22日杭州云栖大会,众所期待的新书<码出高效:Java 开发手册>正式发布,并宣布将所有图书收益捐赠于公益项目. 此书从立意到付梓,历时超过两年 ...

  5. 《码出高效:Java开发手册》背后的故事

    2018年12月22日,由博文视点组织的<码出高效:Java开发手册>作者见面会暨签售仪式在北京举行,InfoQ对书籍作者孤尽(杨冠宝).鸣莎(高海慧)进行了采访,了解了此书出版背后的一些 ...

  6. 码出高效:Java开发手册笔记(线程池及其源码)

    码出高效:Java开发手册笔记(线程池及其源码) 码出高效:Java开发手册笔记(线程池及其源码) 码出高效:Java开发手册笔记(线程池及其源码) 前言 一.线程池的作用 线程的生命周期 二.线程池 ...

  7. 码出高效:java开发手册_Java 11手册:最聪明的技巧来简化Java 11导航

    码出高效:java开发手册 Java 11:提示和技巧,日常陷阱及更多 为了庆祝Java 11的发布,我们邀请了八位Java专家与他们分享最新版本的最佳和最差体验. 由于本系列旨在作为Java 11的 ...

  8. 码出高效:Java开发手册PDF

    <码出高效:Java 开发手册>源于影响了全球250万名开发工程师的<阿里巴巴Java开发手册>,作者静心沉淀,对Java规约的来龙去脉进行了全面而彻底的内容梳理.<码出 ...

  9. 阿里巴巴Java 开发手册 码出高效,码出质量 1.4.0

    前言 <阿里巴巴Java 开发手册>是阿里巴巴集团技术团队的集体智慧结晶和经验总结,经历了多次大规模一线实战的检验及不断完善,系统化地整理成册,回馈给广大开发者.现代软件行业的高速发展对开 ...

  10. 码出高效:java开发手册_Java 11手册:Java专家分享他们在Java 11方面的最佳和最差的经验

    码出高效:java开发手册 Java 10标志着Java生态系统新时代的开始,但最新版本证明仍有一些里程碑可言. Java 11是Oracle新的六个月周期中的第一个LTS版本. 您可以在此处下载Ja ...

最新文章

  1. Ios应用网络安全之https
  2. poj1466(二分图最大独立集)
  3. DevExpress v15.1:WPF控件升级(四)
  4. spring-boot启动源码学习-1
  5. mysql两条记录合成一条数据_踩坑记录之csv数据导入MySQL
  6. 使用cython加密python代码
  7. th:each嵌套_难题:嵌套的computeIfAbsent
  8. 多个redistemplate_Spring boot 使用多个RedisTemplate
  9. iOS开发之网络编程--获取文件的MIMEType
  10. 成都信息工程大学c语言题库,成都信息工程学院C语言考试题及答案.docx
  11. Hadoop中Writable类
  12. 每天坚持跑步到底会不会瘦呢?
  13. 子网掩码掩码计算器_Javascript加载掩码
  14. 【软件后门】qq足迹 - qq应用授权管理
  15. Cybertec PostgreSQL透明加密解析
  16. linux嵌入式reboot不生效,Embeded linux之reboot
  17. 转-iOS- GPUImage README.md
  18. 数据赋能,助力新零售数字化突围
  19. 阿里云服务器搭载code-server
  20. Mac Android studio插件GsonFormat的使用

热门文章

  1. 用Windows自带的画图软件拼接(合成)多张图片
  2. java商品详情页设计_java高并发秒杀系统3-2节商品详情页上.mp4
  3. 阿里云服务器修改主机名
  4. 有鱼上钩!修改游戏数据前的准备
  5. 安卓简易音乐播放器实现
  6. uniapp全局修改字体
  7. 台式计算机的配置清单表格,电脑配置清单表格,为你分别介绍不同价位的三种配置清单...
  8. Linux 30岁,这些年经历了什么?
  9. ajax技术的实质是什么意思,什么是Ajax?Ajax的原理是什么?Ajax的核心技术是什么?Ajax的优缺点是什么?...
  10. 关于高通平台下camera一些参数的设置