TreeMap分析(上)

因为TreeMap的相关知识较多,故TreeMap的分析将会分成三篇文章来写,望大家谅解。

本篇文章先给大家介绍一下红黑树基本概念,并分析一下在红黑树中查找某个结点的相关源码实现。

TreeMap是啥

从TreeMap的类名上就能知道它的底层存储结构其实是红黑树。首先简单介绍一下红黑树的相关知识,以便理解后续内容。

什么是红黑树?先放一张红黑树的示意图看看:

注:图片出处为 博客园 —— 五月的仓颉

简单解释一下树的相关术语的含义:

1.根结点(即0026结点):整个树结构图中最上面的一个结点,其他所有的结点都是由该结点延伸出去的。

2.父结点:在树的上下结构中,有连接关系并处于上方的结点。比如0026是0017和0041的父结点,0017是0012和0021的父结点等等。

3.子结点:在树的上下结构中,有连接关系并处于下方的结点。比如0017是0026的左子结点,0041是0026的右子结点;0030是0041的左子结点,0047是0041的右子结点。

 并且左子节点是小于父结点的,而右子节点一般是大于父结点的。比如0012比0017小,而0010比0012小;同样0041比0026大,并且0047也比0041大。

4.兄弟结点:属于同一个父结点的结点互相称为兄弟结点。比如0017和0041同属于0026的子结点,则0017和0041就是兄弟节点。

5.叶子结点:没有任何子结点的结点称为叶子节点。如上图中所有的NULL LEAF结点都是叶子结点。

6.二叉树:每个节点最多只有两个子结点的树结构称为二叉树。如上图就是一种二叉树的数据结构。

树的一些基本术语就介绍到这里,其他的请同学们自行查阅资料学习吧,这里就不再多说了 : )

什么是红黑树

上面已经说了TreeMap是基于红黑树的存储结构,那什么是红黑树呢?

红黑树是一种特殊的二叉树,它的每个结点都额外有一个颜色的属性,颜色只有两种:红色和黑色。

关于红黑树有以下几个规定:

1.首先当前树必须为二叉树的结构。

2.每个结点都有一种颜色,且颜色只能为红色或黑色

3.根结点必须是黑色。

4.叶子结点必须是黑色。

5.不能同时出现父结点和子结点都是红色的情况。即如果一个结点是父结点且为红色,则它的子结点必须全部为黑色。

6.从根结点到每个叶子结点经过的路程中,黑色结点的数量必须是一样的。

比如  0026 -> 0017 -> 0012 -> 0010 -> 0003 -> 叶子结点,这条路径上共有4个黑色结点;

0026 -> 0041 -> 0030 -> 0038 -> 叶子结点,这条路径上也有4个黑色结点。

只有以上6个条件全部满足的树,我们才称其为红黑树。比如上图的中的树结构就是一个标准的红黑树。

TreeMap源码中的数据结构及相关属性

 1     /**
 2      * 红黑树每个结点对象的数据结构
 3      * 4      *
 5      * @param <K>  key
 6      * @param <V>  value
 7      */
 8     static final class Entry<K,V> implements Map.Entry<K,V> {
 9         K key;   //键
10         V value; //值
11         Entry<K,V> left;  //左子结点引用
12         Entry<K,V> right;  //右子结点引用
13         Entry<K,V> parent;  //父结点引用
14         boolean color = BLACK;  //结点默认为黑色
15
16         /**
17          * 构造一个叶子结点,默认无左右子结点,颜色为黑色
18          */
19         Entry(K key, V value, Entry<K,V> parent) {
20             this.key = key;
21             this.value = value;
22             this.parent = parent;
23         }
24
25         /**
26          * 返回key
27          *
28          * @return the key
29          */
30         public K getKey() {
31             return key;
32         }
33
34         /**
35          * 返回key对应的value
36          *
37          * @return the value associated with the key
38          */
39         public V getValue() {
40             return value;
41         }
42
43         /**
44          * 替换对应的值
45          *
46          * @return 原先被替换的值
47          */
48         public V setValue(V value) {
49             V oldValue = this.value;
50             this.value = value;
51             return oldValue;
52         }
53
54         /**
55          * 重写结点的equals()方法,比较结点的大小时使用
56          */
57         public boolean equals(Object o) {
58             if (!(o instanceof Map.Entry))
59                 return false;
60             Map.Entry<?,?> e = (Map.Entry<?,?>)o;
61             //必须key和value都相同才算相等
62             return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
63         }
64
65         /**
66          * 重写结点的hashCode()方法
67          */
68         public int hashCode() {
69             int keyHash = (key==null ? 0 : key.hashCode());
70             int valueHash = (value==null ? 0 : value.hashCode());
71             return keyHash ^ valueHash;  //key的hashCode 异或  value的hashCode
72         }
73
74         /**
75          * 重写toString()方法
76          */
77         public String toString() {
78             return key + "=" + value;
79         }
80     }
81
82     /** 用户自定义的比较器 */
83     private final Comparator<? super K> comparator;
84
85     /** 红黑树根结点  */
86     private transient Entry<K,V> root;
87
88     /** 红黑树总结点个数  */
89     private transient int size = 0;

可以看到TreeMap因为实现了Map的结点的数据结构,所以同样有key和value两个属性。并且因为是红黑树,所以还有父结点、左右子结点的引用以及结点颜色这些属性。

我们看到第83行有个 comparator 属性,它表示当前TreeMap使用的是哪种比较器。

大家都知道TreeMap是支持结点排序的,它有两种排序方式:

1.按照结点key的自然顺序维护结点的顺序。比如有两个结点 A Entry<1,"11"> 和  B Entry<2,"222">,以这种key的自然排序来讲,如果先插入A后插入B,则A为B父结点,B为A的右子结点;如果先插入B后插入A,那么B为A的父结点,A为B的左子结点。

2.另一种排序是用户自定义的排序方式。在构造方法中传入一个自定义的比较器,则TreeMap会根据用户自己定义的结点对象比较大小的方法来维护结点的顺序。

TreeMap构造方法

 1     /**
 2      * 无参数构造方法,使用key的自然比较顺序来维护树中结点的顺序
 3      */
 4     public TreeMap() {
 5         comparator = null;
 6     }
 7
 8     /**
 9      * 带参数构造方法,使用用户传入的比较器来维护树中结点的顺序
10      * @param 用户自定义比较器,如果为空,则该Map会使用key的自然排序维护树的顺序
11      */
12     public TreeMap(Comparator<? super K> comparator) {
13         this.comparator = comparator;
14     }

TreeMap获取某个结点的源码分析

 1     /**
 2      * 获取节点的操作
 3      * 如果在map中找到了这个键,则返回键对应的值;找不到对应的键或者键指向为null,否则返回null
 4      *
 5      * @throws ClassCastException 用户传入的key和当前map中的key无法比较(不是同一类型),则报该异常
 6      * @throws NullPointerException 使用了自然排序但传入的key为null,或者比较器不支持key为null的情况,则报此异常
 7      *
 8      */
 9     public V get(Object key) {
10         Entry<K,V> p = getEntry(key);   //可以看到实际上是调用getEntry()这个方法来获取节点的
11         return (p==null ? null : p.value);
12     }
13
14
15     /**
16      * 返回通过传入的键在map中找到的相应entry,若未找到则返回null
17      *
18      * @throws ClassCastException 用户传入的key和当前map中的key无法比较(不是同一类型),则报该异常
19      * @throws NullPointerException 使用了自然排序但传入的key为null,或者比较器不支持key为null的情况,则报此异常
20      */
21     final Entry<K,V> getEntry(Object key) {
22         // 首先要区分是使用map键的自然排序查找还是使用用户自定义的比较器来进行查找
23         if (comparator != null)  //如果用户传入了自定义比较器
24             return getEntryUsingComparator(key); //调用getEntryUsingComparator()方法,通过用户自定义比较器的compare()方法查找entry
25         if (key == null)  //如果key为null,报空指针异常
26             throw new NullPointerException();
27         Comparable<? super K> k = (Comparable<? super K>) key;  //没有传入自定义比较器,使用key的自然排序比较传入的key和map中的key
28         Entry<K,V> p = root;  //从根节点开始比较
29         while (p != null) {  //如果节点不是空,则一直循环遍历比较
30             int cmp = k.compareTo(p.key); //获取传入的key和当前节点key的比较结果,使用自然排序Comparable实现的compareTo()方法进行比较
31             if (cmp < 0)  //结果<0,说明传入的key比当前节点的key小
32                 p = p.left; //将下次比较的节点变更为当前节点的左子节点
33             else if (cmp > 0)  //结果>0,说明传入的key比当前节点的key大
34                 p = p.right; //将下次比较的节点变更为当前节点的右子节点
35             else  //结果相等,则该节点就是要查找的节点
36                 return p;  //直接返回节点对应的Entry
37         }
38         return null;   //遍历完整个树也没找到,则返回null
39     }
40
41
42
43     /**
44      * 使用用户自定义比较器的比较方法来查找节点
45      * 逻辑与通过自然排序查找类似,不再赘述
46      */
47     final Entry<K,V> getEntryUsingComparator(Object key) {
48         @SuppressWarnings("unchecked")
49             K k = (K) key;
50         Comparator<? super K> cpr = comparator;
51         if (cpr != null) {
52             Entry<K,V> p = root;
53             while (p != null) {
54                 int cmp = cpr.compare(k, p.key);  //仅仅这里和getEntry()方法不同,使用自定义比较器的compare()方法进行比较
55                 if (cmp < 0)
56                     p = p.left;
57                 else if (cmp > 0)
58                     p = p.right;
59                 else
60                     return p;
61             }
62         }
63         return null;
64     }

可以看到查找其实很简单,就是从根结点开始往下遍历比较。如果要查找的节点比较小,则继续往左子结点比较;反之则往右子结点比较;如果相等,则当前结点就是要查找的结点。

要注意的只有一点,即TreeMap使用的是key的自然排序还是用户自定义的排序。

key自然排序使用的是实现了Comparable接口的compareTo()方法来进行比较,而用户自定义排序则使用的实现Comparator接口的compare()方法来比较。

这里延伸一个经典的面试题:Comparable接口和Comparator接口有啥相同和不同之处?

相同点:两个接口都是用于支持对象进行比较大小所定义的。

不同点:

1.Comparable具体实现比较大小的方法是CompareTo(),而Comparator具体实现比较大小的方法是Compare()。

2.Comparable一般强调相同类型的对象进行比较(比如小明和小红都是人类,那他们之间的大小就是按年龄大小来进行比较的,那么CompareTo()方法中其实就是两人的age进行比较);

而相对的Comparator一般强调不同类型的对象进行比较(比如小华是人类,旺财是狗,而人类要和狗进行比较大小,则必须由用户自己制定一个比较规则来确定谁大谁小,那么compare()方法中其实就是自定义的两个类型的不同属性进行比较)。

明确这两个接口的区别后,上面查询结点的方法也就不难理解啦  : )

本篇文章到此结束,内容还是偏基础,希望大家不吝赐教。

那么下篇文章我会和大家一起分析下在插入节点后,红黑树内部数据结构是怎样一步步变化的,敬请期待哟~

原文地址:https://www.cnblogs.com/smartchen/p/9032266.html

时间: 2024-10-30 22:13:26

TreeMap分析(上)的相关文章

深入理解 hash 函数、HashMap、LinkedHashMap、TreeMap 【上】

前言 Map 是非常常用的一种数据接口.在 Java 中,提供了成熟的 Map 实现. 图 1 最主要的实现类有 Hashtable.HashMap.LinkedHashMap和 TreeMap.在 HashTable 的子类中,还有 Properties的实现.Properties 是专门读取配置文件的类,我们会在稍后介绍.这里首先值得关注的是 HashMap 和 HashTable 两套不同的实现,两者都实现了 Map 接口.从表面上看,并没有多大差别,但是在内部实现上却有些微小的细节. 首

TreeMap分析(下)

通过上篇文章,大家已经能够清楚的了解到treeMap插入结点的过程,那么本篇文章就来分析下TreeMap删除一个结点时,内部数据结构发生了怎样的变化. TreeMap删除某个结点的源码分析 1 /** 2 * 删除节点,并平衡红黑树的操作 3 * 4 * @Param Entry<K,V> p 要删除的节点Entry 5 */ 6 private void deleteEntry(Entry<K,V> p) { 7 modCount++; 8 size--; //节点总数-1 9

hadoop、spark、hive、solr、es与YDB在车辆即席分析上的对比分析

自2012年以来,公安部交通管理局在全国范围内推广了机动车缉查布控系统(简称卡口系统),通过整合共享各地车辆智能监测记录等信息资源,建立了横向联网.纵向贯通的全国机动车缉查布控系统,实现了大范围车辆缉查布控和预警拦截.车辆轨迹.交通流量分析研判.重点车辆布控.交通违法行为甄别查处及侦破涉车案件等应用.在侦破肇事逃逸案件.查处涉车违法行为.治安防控以及反恐维稳等方面发挥着重要作用. 随着联网单位和接入卡口的不断增加,各省市区部署的机动车缉查布控系统积聚了海量的过车数据.截至目前,全国32个省(区.

用sparkR, 分析上亿条订单数据的脚本。(增长黑客实践)

上周我们这个10人的小团队开发的推荐拉新系统, 日拉新人数已接近4万人.过去几个月这个系统从无到有, 拉新从日增几千稳步增长到日增几万, 同事们几个月来,每天工作13个小时以上,洗澡时间都没有, 有时就住在公司, 回家怕吵到家人,只能睡客厅地板, 周日也不能保证休息. 大家的全力投入,不懈努力才能有这个结果. 非常感慨团队产生的的化学反应, 和惊人的生产效率. 产品稳定后,最近全面转入大数据分析, 和机器学习阶段, 开始做真正的增长黑客实践. spark, R, scala都是刚刚开始深入地学习

练手之经典病毒熊猫烧香分析(上)

熊猫烧香病毒在当年可是火的一塌糊涂,感染非常迅速,算是病毒史上比较经典的案例.不过已经比较老了,基本上没啥危害,其中的技术也都过时了.作为练手项目,开始对熊猫烧香病毒进行分析.首先准备好病毒样本(看雪论坛有),VM虚拟机和Xp Sp3系统.样本参数如下: 病毒名称:panda.exe 文件大小:61952 bytes MD5值:3520D3565273E41C9EEB04675D05DCA8 SHA1值:BB1D8FA7EE4E59844C1FEB7B27A73F9B47D36A0A CRC32

Hadoop-2.4.1学习之Map任务源码分析(上)

众所周知,Mapper是MapReduce编程模式中最重要的环节之一(另一个当然是Reducer了).在Hadoop-2.x版本中虽然不再有JobTracker和TaskTracker,但Mapper任务的功能却没有变化,本篇文章将结合源代码深入分析Mapper任务时如何执行的,包括处理InputSplit,mapper的输出.对输出分类等.在进行分析之前先明确几个概念:作业.任务.任务的阶段和任务的状态,可以将作业理解为要最终实现的功能或目的,比如统计单词的数量,而任务就是对该作业的拆分,只负

用户空间缺页异常pte_handle_fault()分析--(上)【转】

转自:http://blog.csdn.net/vanbreaker/article/details/7881206 版权声明:本文为博主原创文章,未经博主允许不得转载. 前面简单的分析了内核处理用户空间缺页异常的流程,进入到了handle_mm_fault()函数,该函数为触发缺页异常的地址address分配各级的页目录,也就是说现在已经拥有了一个和address配对的pte了,但是这个pte如何去映射物理页框,内核又得根据pte的状态进行分类和判断,而这个过程又会牵扯出一些其他的概念……这也

memcached学习笔记——存储命令源码分析上

原创文章,转载请标明,谢谢. 上一篇分析过memcached的连接模型,了解memcached是如何高效处理客户端连接,这一篇分析memcached源码中的process_update_command函数,探究memcached客户端的set命令,解读memcached是如何解析客户端文本命令,剖析memcached的内存管理,LRU算法是如何工作等等. 解析客户端文本命令 客户端向memcached server发出set操作,memcached server读取客户端的命令,客户端的连接状态

axios 源码分析(上) 使用方法

axios是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,它可以在浏览器和node环境下运行,在github上已经有六七万个星了,axios使用很方便,很多人在使用他,vue官方也推荐使用axios了,技术这东西还是随主流吧,大家都用肯定有它的特长所在. axios现在最新的版本的是v0.19.0,实现代码也很好理解.我们本节先说一下它的使用方法,然后来分析一下它的实现源码 我们可以使用两种方式来创建一个axios实例: ·一种是直接调用axios(config) ·