Java TreeMap 源码解析

简介:

上篇文章介绍完了HashMap,这篇文章开始介绍Map系列另一个比较重要的类TreeMap。 大家也许能感觉到,网络上介绍HashMap的文章比较多,但是介绍TreeMap反而不那么多,这里面是有原因:一方面HashMap的使用场景比较多;二是相对于HashMap来说,TreeMap所用到的数据结构更为复杂。 废话不多说,进入正题。

签名(signature)

public class TreeMap<K,V>
       extends AbstractMap<K,V>
       implements NavigableMap<K,V>, Cloneable, java.io.Serializable

可以看到,相比HashMap来说,TreeMap多继承了一个接口NavigableMap,也就是这个接口,决定了TreeMap与HashMap的不同:

HashMap的key是无序的,TreeMap的key是有序的

接口NavigableMap

首先看下NavigableMap的签名

public interface NavigableMap<K,V> extends SortedMap<K,V>

发现NavigableMap继承了SortedMap,再看SortedMap的签名

SortedMap

public interface SortedMap<K,V> extends Map<K,V>

SortedMap就像其名字那样,说明这个Map是有序的。这个顺序一般是指由Comparable接口提供的keys的自然序(natural ordering),或者也可以在创建SortedMap实例时,指定一个Comparator来 决定。 当我们在用集合视角(collection views,与HashMap一样,也是由entrySet、keySet与values方法提供)来迭代(iterate)一个SortedMap实例 时会体现出key的顺序。 这里引申下关于Comparable与Comparator的区别(参考这里):

  • Comparable一般表示类的自然序,比如定义一个Student类,学号为默认排序

  • Comparator一般表示类在某种场合下的特殊分类,需要定制化排序。比如现在想按照Student类的age来排序

插入SortedMap中的key的类类都必须继承Comparable类(或指定一个comparator),这样才能确定如何比较(通过k1.compareTo(k2)comparator.compare(k1, k2))两个key,否则,在插入时,会报ClassCastException的异常。 此为,SortedMap中key的顺序性应该与equals方法保持一致。也就是说k1.compareTo(k2)comparator.compare(k1, k2)为true时,k1.equals(k2)也 应该为true。 介绍完了SortedMap,再来回到我们的NavigableMap上面来。 NavigableMap是JDK1.6新增的,在SortedMap的基础上,增加了一些“导航方法”(navigation methods)来返回与搜索目标最近的元素。例如下面这些方法:

  • lowerEntry,返回所有比给定Map.Entry小的元素

  • floorEntry,返回所有比给定Map.Entry小或相等的元素

  • ceilingEntry,返回所有比给定Map.Entry大或相等的元素

  • higherEntry,返回所有比给定Map.Entry大的元素

设计理念(design concept)

红黑树(Red–black tree)

TreeMap是用红黑树作为基础实现的,红黑树是一种二叉搜索树,让我们在一起回忆下二叉搜索树的一些性质

二叉搜索树

先看看二叉搜索树(binary search tree,BST)长什么样呢?

Java TreeMap 源码解析
二叉搜索树

相信大家对这个图都不陌生,关键点是:

左子树的值小于根节点,右子树的值大于根节点。

二叉搜索树的优势在于每进行一次判断就是能将问题的规模减少一半,所以如果二叉搜索树是平衡的话,查找元素的时间复杂度为log(n),也就是树的高度。 我这里想到一个比较严肃的问题,如果说二叉搜索树将问题规模减少了一半,那么三叉搜索树不就将问题规模减少了三分之二,这不是更好嘛,以此类推,我们还可以有四叉搜索树,五叉搜索树……对于更一般的情况:

n个元素,K叉树搜索树的K为多少时效率是最好的?K=2时吗?

K 叉搜索树

如果大家按照我上面分析,很可能也陷入一个误区,就是

三叉搜索树在将问题规模减少三分之二时,所需比较操作的次数是两次(二叉搜索树再将问题规模减少一半时,只需要一次比较操作)

我们不能把这两次给忽略了,对于更一般的情况:

n个元素,K叉树搜索树需要的平均比较次数为k*log(n/k)

对于极端情况k=n时,K叉树就转化为了线性表了,复杂度也就是O(n)了,如果用数学角度来解这个问题,相当于:

n为固定值时,k取何值时,k*log(n/k)的取值最小?

k*log(n/k)根据对数的运算规则可以转化为ln(n)*k/ln(k)ln(n)为常数,所以相当于取k/ln(k)的极小值。这个问题对于大一刚学高数的人来说再简单不过了,我们这里直接看结果

当k=e时,k/ln(k)取最小值。

自然数e的取值大约为2.718左右,可以看到二叉树基本上就是这样最优解了。在Nodejs的REPL中进行下面的操作


 
 
  1. function foo(k) {return k/Math.log(k);} 
  2. > foo(2
  3. 2.8853900817779268 
  4. > foo(3
  5. 2.730717679880512 
  6. > foo(4
  7. 2.8853900817779268 
  8. > foo(5
  9. 3.1066746727980594 

貌似k=3时比k=2时得到的结果还要小,那也就是说三叉搜索树应该比二叉搜索树更好些呀,但是为什么二叉树更流行呢?后来在万能的stackoverflow上找到了答案,主旨如下:

现在的CPU可以针对二重逻辑(binary logic)的代码做优化,三重逻辑会被分解为多个二重逻辑。

这样也就大概能理解为什么二叉树这么流行了,就是因为进行一次比较操作,我们最多可以将问题规模减少一半。 好了这里扯的有点远了,我们再回到红黑树上来。

红黑树性质

先看看红黑树的样子:

Java TreeMap 源码解析
红黑树示例

上图是从wiki截来的,需要说明的一点是:

叶子节点为上图中的NIL节点,国内一些教材中没有这个NIL节点,我们在画图时有时也会省略这些NIL节点,但是我们需要明确,当我们说叶子节点时,指的就是这些NIL节点。

红黑树通过下面5条规则,保证了树是平衡的:

  1. 树的节点只有红与黑两种颜色

  2. 根节点为黑色的

  3. 叶子节点为黑色的

  4. 红色节点的字节点必定是黑色的

  5. 从任意一节点出发,到其后继的叶子节点的路径中,黑色节点的数目相同

满足了上面5个条件后,就能够保证:根节点到叶子节点的最长路径不会大于根节点到叶子最短路径的2倍。 其实这个很好理解,主要是用了性质4与5,这里简单说下:

假设根节点到叶子节点最短的路径中,黑色节点数目为B,那么根据性质5,根节点到叶子节点的最长路径中,黑色节点数目也是B,最长的情况就是每两个黑色节点中间有个红色节点(也就是红黑相间的情况),所以红色节点最多为B-1个。这样就能证明上面的结论了。

红黑树操作

Java TreeMap 源码解析
红黑树旋转示例(没有画出NIL节点)

关于红黑树的插入、删除、左旋、右旋这些操作,我觉得最好可以做到可视化,文字表达比较繁琐,我这里就不在献丑了,网上能找到的也比较多,像v_July_v的《教你透彻了解红黑树》。我这里推荐个swf教学视频(视频为英文,大家不要害怕,重点是看图??),7分钟左右,大家可以参考。 这里还有个交互式红黑树的可视化网页,大家可以上去自己操作操作,插入几个节点,删除几个节点玩玩,看看左旋右旋是怎么玩的。

源码剖析

由于红黑树的操作我这里不说了,所以这里基本上也就没什么源码可以讲了,因为这里面重要的算法都是From CLR,这里的CLR是指Cormen, Leiserson, Rivest,他们是算法导论的作者,也就是说TreeMap里面算法都是参照算法导论的伪代码。 因为红黑树是平衡的二叉搜索树,所以其put(包含update操作)、get、remove的时间复杂度都为log(n)

总结

到目前为止,TreeMap与HashMap的的实现算是都介绍完了,可以看到它们实现的不同,决定了它们应用场景的不同:

  • TreeMap的key是有序的,增删改查操作的时间复杂度为O(log(n)),为了保证红黑树平衡,在必要时会进行旋转
  • HashMap的key是无序的,增删改查操作的时间复杂度为O(1),为了做到动态扩容,在必要时会进行resize。

另外,我这里没有解释具体代码,难免有些标题党了,请大家见谅,后面理解的更深刻了再来填坑。


来源:51CTO

相关文章
|
1月前
|
机器学习/深度学习 JSON Java
Java调用Python的5种实用方案:从简单到进阶的全场景解析
在机器学习与大数据融合背景下,Java与Python协同开发成为企业常见需求。本文通过真实案例解析5种主流调用方案,涵盖脚本调用到微服务架构,助力开发者根据业务场景选择最优方案,提升开发效率与系统性能。
341 0
|
1月前
|
Java
Java的CAS机制深度解析
CAS(Compare-And-Swap)是并发编程中的原子操作,用于实现多线程环境下的无锁数据同步。它通过比较内存值与预期值,决定是否更新值,从而避免锁的使用。CAS广泛应用于Java的原子类和并发包中,如AtomicInteger和ConcurrentHashMap,提升了并发性能。尽管CAS具有高性能、无死锁等优点,但也存在ABA问题、循环开销大及仅支持单变量原子操作等缺点。合理使用CAS,结合实际场景选择同步机制,能有效提升程序性能。
|
1月前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
354 100
|
2月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
26天前
|
Java 开发者
Java 函数式编程全解析:静态方法引用、实例方法引用、特定类型方法引用与构造器引用实战教程
本文介绍Java 8函数式编程中的四种方法引用:静态、实例、特定类型及构造器引用,通过简洁示例演示其用法,帮助开发者提升代码可读性与简洁性。
|
1月前
|
安全 Java API
Java SE 与 Java EE 区别解析及应用场景对比
在Java编程世界中,Java SE(Java Standard Edition)和Java EE(Java Enterprise Edition)是两个重要的平台版本,它们各自有着独特的定位和应用场景。理解它们之间的差异,对于开发者选择合适的技术栈进行项目开发至关重要。
153 1
|
1月前
|
存储 小程序 Java
热门小程序源码合集:微信抖音小程序源码支持PHP/Java/uni-app完整项目实践指南
小程序已成为企业获客与开发者创业的重要载体。本文详解PHP、Java、uni-app三大技术栈在电商、工具、服务类小程序中的源码应用,提供从开发到部署的全流程指南,并分享选型避坑与商业化落地策略,助力开发者高效构建稳定可扩展项目。
|
7月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
689 29
|
7月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
208 4
|
7月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

推荐镜像

更多
  • DNS