一、摘要

在集合系列的第一章,咱们了解到,Map的实现类有HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties等等。

本文主要从数据结构和算法层面,探讨TreeMap的实现。

二、简介

Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。

TreeMap底层通过红黑树(Red-Black tree)实现,所以要了解TreeMap就必须对红黑树有一定的了解,在《集合系列》文章中,如果你已经读过红黑树的讲解,其实本文要讲解的TreeMap,跟其大同小异。

红黑树又称红-黑二叉树,它首先是一颗二叉树,它具有二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树。

对于一棵有效的红黑树二叉树,主要有以下规则:

  • 1、每个节点要么是红色,要么是黑色,但根节点永远是黑色的;
  • 2、每个红色节点的两个子节点一定都是黑色;
  • 3、红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色);
  • 4、从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;
  • 5、所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);

这些约束强制了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n)。下图为一颗典型的红黑二叉树。

在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的条件。

调整方式主要有:左旋、右旋和颜色转换!

2.1、左旋

左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

2.2、右旋

右旋的过程是将x的左子树绕x顺时针旋转,使得x的左子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

2.3、颜色转换

颜色转换的过程是将红色节点转换为黑色节点,或者将黑色节点转换为红色节点,以满足红黑树二叉树的规则!

三、常用方法介绍

3.1、get方法

get方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.value

算法思想是根据key的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0entry

源码如下:

final Entry<K,V> getEntry(Object key) {//如果传入比较器,通过getEntryUsingComparator方法获取元素if (comparator != null)return getEntryUsingComparator(key);//不允许key值为nullif (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;elsereturn p;}return null;
}

如果传入比较器,通过getEntryUsingComparator方法获取元素

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;elsereturn p;}}return null;
}

测试用例:

public static void main(String[] args) {Map initMap = new TreeMap();initMap.put("4", "d");initMap.put("3", "c");initMap.put("1", "a");initMap.put("2", "b");//默认自然排序,key为升序System.out.println("默认 排序结果:" + initMap.toString());//自定义排序,在TreeMap初始化阶段传入Comparator 内部对象Map comparatorMap = new TreeMap<String, String>(new Comparator<String>() {@Overridepublic int compare(String o1, String o2){//根据key比较大小,采用倒叙,以大到小排序return o2.compareTo(o1);}});comparatorMap.put("4", "d");comparatorMap.put("3", "c");comparatorMap.put("1", "a");comparatorMap.put("2", "b");System.out.println("自定义 排序结果:" + comparatorMap.toString());
}

输出结果:

默认 排序结果:{1=a, 2=b, 3=c, 4=d}
自定义 排序结果:{4=d, 3=c, 2=b, 1=a}

3.2、put方法

put方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到则会在红黑树中插入新的entry,如果插入之后破坏了红黑树的约束,还需要进行调整(旋转,改变某些节点的颜色)。

源码如下:

public V put(K key, V value) {Entry<K,V> t = root;//如果红黑树根部为空,直接插入if (t == null) {compare(key, key); // type (and possibly null) checkroot = new Entry<>(key, value, null);size = 1;modCount++;return null;}int cmp;Entry<K,V> parent;// split comparator and comparable pathsComparator<? 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;elsereturn 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;elsereturn t.setValue(value);} while (t != null);}//创建并插入新的entryEntry<K,V> e = new Entry<>(key, value, parent);if (cmp < 0)parent.left = e;elseparent.right = e;//红黑树调整函数fixAfterInsertion(e);size++;modCount++;return null;
}

红黑树调整函数fixAfterInsertion(Entry<K,V> x)

private void fixAfterInsertion(Entry<K,V> x) {x.color = RED;while (x != null && x != root && x.parent.color == RED) {//判断x是否在树的左边,还是右边if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {Entry<K,V> y = rightOf(parentOf(parentOf(x)));//如果x的父亲的父亲的右子树是红色,违反了红色节点不能连续if (colorOf(y) == RED) {//进行颜色调整,以满足红色节点不能连续规则setColor(parentOf(x), BLACK);setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED);x = parentOf(parentOf(x));} else {//如果x的父亲的右子树等于自己,那么需要进行左旋转if (x == rightOf(parentOf(x))) {x = parentOf(x);rotateLeft(x);}setColor(parentOf(x), BLACK);  setColor(parentOf(parentOf(x)), RED);rotateRight(parentOf(parentOf(x)));}} else {//跟上面的流程正好相反//获取x的父亲的父亲的左子树节点Entry<K,V> y = leftOf(parentOf(parentOf(x)));//如果y是红色节点,违反了红色节点不能连续if (colorOf(y) == RED) {//进行颜色转换setColor(parentOf(x), BLACK);setColor(y, BLACK);setColor(parentOf(parentOf(x)), RED);x = parentOf(parentOf(x));} else {//如果x的父亲的左子树等于自己,那么需要进行右旋转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;
}

上述代码的插入流程:

  • 1、首先在红黑树上找到合适的位置;
  • 2、然后创建新的entry并插入;
  • 3、通过函数fixAfterInsertion(),对某些节点进行旋转、改变某些节点的颜色,进行调整;

调整图解:

3.3、remove方法

remove的作用是删除key值对应的entry,该方法首先通过上文中提到的getEntry(Object key)方法找到 key 值对应的 entry,然后调用deleteEntry(Entry<K,V> entry)删除对应的 entry。由于删除操作会改变红黑树的结构,有可能破坏红黑树的约束,因此有可能要进行调整。

源码如下:

public V remove(Object key) {Entry<K,V> p = getEntry(key);if (p == null)return null;V oldValue = p.value;deleteEntry(p);return oldValue;
}

删除函数 deleteEntry()

private void deleteEntry(Entry<K,V> p) {modCount++;size--;if (p.left != null && p.right != null) {// 删除点p的左右子树都非空。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) {// 删除点p只有一棵子树非空。replacement.parent = p.parent;if (p.parent == null)root = replacement;else if (p == p.parent.left)p.parent.left  = replacement;elsep.parent.right = replacement;p.left = p.right = p.parent = null;if (p.color == BLACK)fixAfterDeletion(replacement);// 调整} else if (p.parent == null) {root = null;} else { //删除点p的左右子树都为空if (p.color == BLACK)fixAfterDeletion(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;}}
}

删除后调整函数fixAfterDeletion()的具体代码如下:

private void fixAfterDeletion(Entry<K,V> x) {while (x != root && colorOf(x) == BLACK) {//判断当前删除的元素,是在x父亲的左边还是右边if (x == leftOf(parentOf(x))) {Entry<K,V> sib = rightOf(parentOf(x));//判断x的父亲的右子树,是红色还是黑色节点if (colorOf(sib) == RED) {//进行颜色转换setColor(sib, BLACK);setColor(parentOf(x), RED);rotateLeft(parentOf(x));sib = rightOf(parentOf(x));}//x的父亲的右子树的左边是黑色节点,右边也是黑色节点if (colorOf(leftOf(sib))  == BLACK &&colorOf(rightOf(sib)) == BLACK) {//设置x的父亲的右子树为红色节点,将x的父亲赋值给xsetColor(sib, RED);x = parentOf(x);} else {//x的父亲的右子树的左边是红色节点,右边也是黑色节点if (colorOf(rightOf(sib)) == BLACK) {//x的父亲的右子树的左边进行颜色调整,右旋调整setColor(leftOf(sib), BLACK);setColor(sib, RED);rotateRight(sib);sib = rightOf(parentOf(x));}//对x进行左旋,颜色调整setColor(sib, colorOf(parentOf(x)));setColor(parentOf(x), BLACK);setColor(rightOf(sib), BLACK);rotateLeft(parentOf(x));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);
}

上述代码的删除流程:

  • 1、首先在红黑树上找到合适的位置;
  • 2、然后删除entry;
  • 3、通过函数fixAfterDeletion(),对某些节点进行旋转、改变某些节点的颜色,进行调整;

四、总结

TreeMap 默认是按键值的升序排序,如果需要自定义排序,可以通过new Comparator构造参数,重写compare方法,进行自定义比较。

以上,主要是对 java 集合中的 TreeMap 做了写讲解,如果有理解不当之处,欢迎指正。

五、参考

1、JDK1.7&JDK1.8 源码

2、博客园 - chenssy - TreeMap分析

2、知乎 - CarpenterLee - TreeMap讲解

深入浅出的分析 TreeMap相关推荐

  1. 深入浅出面向对象分析与设计

    深入浅出面向对象分析与设计书籍 下载位置:http://pan.baidu.com/s/1o7gmmuu 转载于:https://www.cnblogs.com/wlming/p/5160140.ht ...

  2. JDK源码分析-TreeMap(1)

    概述 前面数据结构与算法笔记对红黑树进行了分析,而 TreeMap 内部就是基于红黑树实现的.示意图: 转载于:https://juejin.im/post/5d05a9bd6fb9a07ecb0ba ...

  3. 带你深入浅出的分析 HashTable 源码

    Hashtable 一个元老级的集合类,早在 JDK 1.0 就诞生了,今天小编想和大家一起来揭开它的面纱! 01.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.Linke ...

  4. java TreeMap 源代码分析 平衡二叉树

    TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点. TreeSet 和 TreeMap 的关系 为了让大家了解 TreeMap 和 Tre ...

  5. 通过分析 JDK 源代码研究 TreeMap 红黑树算法实现--转

    TreeMap 和 TreeSet 是 Java Collection Framework 的两个重要成员,其中 TreeMap 是 Map 接口的常用实现类,而 TreeSet 是 Set 接口的常 ...

  6. 干货|深入浅出YOLOv5

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达本文转自|机器学习算法工程师 之前已经对Yolov4的相关基础知识做 ...

  7. 高通量测序技术的原理及各平台优势和实践应用的分析

    高通量测序技术的原理及各平台优势和实践应用的分析 2020.9.01 2060 随着人类基因组计划(human genome project )在2003年顺利完成,基因组测序技术取得了长足的进步,这 ...

  8. Java源码详解三:Hashtable源码分析--openjdk java 11源码

    文章目录 注释 哈希算法与映射 线程安全的实现方法 put 操作 get操作 本系列是Java详解,专栏地址:Java源码分析 Hashtable官方文档:Hashtable (Java Platfo ...

  9. Treemap and Treeset java 实现

    引自:  http://www.ibm.com/developerworks/cn/java/j-lo-tree/(红黑树部分 详见这里) 对于 TreeMap 而言,它采用一种被称为"红黑 ...

最新文章

  1. 不做调参侠,重视数据及处理能力?吴恩达发起的Data-Centric赛事总结!
  2. 今天学会了如何察看SSDT里面的东西、修改里面的地址
  3. 正则至少一个数字_好程序员web前端培训分享JavaScript学习笔记之正则
  4. 用C#(.NET Core) 实现简单工厂和工厂方法设计模式
  5. Python | 字符串isdecimal(),isdigit(),isnumeric()和Methods之间的区别
  6. Linux kernel 同步机制(下篇)
  7. javamail 解码 base64 html格式邮件_python使用QQ邮箱实现自动发送邮件
  8. html三张图片的轮播代码_vue写了个轮播图
  9. git(2)---git 分布式版本控制系统
  10. 看好某一个机会和项目,拥有或掌握某一个资源要素和自然禀赋,寻找并组合志同道合
  11. JXLS 2.4.0学习
  12. Oracle数据库连接、退出缓慢问题查询与处理
  13. 惠普m1216硒鼓清零步骤_惠普m1136打印机怎么清零
  14. 人脸测温门禁 传感器_测温人脸门禁什么牌子好
  15. linux根目录不足,追加空间到根目录
  16. Fragment already added解决
  17. pc计算机含义,pc端游什么意思
  18. 判断点在多边形内的算法(Winding Number详解)
  19. PGP加密技术应用(含安装包)
  20. JavaScript 如何计算两个日期之间的天数

热门文章

  1. 一个关于图片处理的代码
  2. freemaker 前端web教程
  3. GPS从入门到放弃(八) --- GPS卫星速度解算
  4. jstack命令 详解
  5. zabbix模板关联群组
  6. 成本优化之使用P2P的方案的需要了解的本地SDK的背后的原理
  7. 马尔科夫随机场Markov Random Field
  8. 用户的基本组与附加组
  9. Python Pathlib 详解
  10. WPS表格的常用操作技巧大全