结合数据结构系统细节Java集合框架 —— 下
上期回顾
常见集合框架图
我们上期介绍了Java常用集合框架中的Collection集合中的List、Queue和Set集合的一部分。在上期结尾,我为各位读者留下了一道思考题——***对于TreeSet集合,它的底层数据结构是什么,又是怎样储存数据的呢?还有神秘的Map集合,它又是怎样的呢?***现在,让我们一起揭开它们神秘的面纱吧!
TreeSet的底层数据结构
上期我们说到,TreeSet的特点是:无序性、元素不重复、无索引。要清楚其特点并熟练运用TreeSet集合提供的各种API,我们有必要了解一下TreeSet底层的数据结构——红黑树。
顾名思义,红黑树可以分解成:“红黑”和“树”这两个部分。其中,“树”表示红黑树这种数据结构是一种“树”,"红黑"表示红黑树这种数据结构具有“红黑”的特点或者说是和其他“树”对比不同的地方。简而言之,红黑树就是一种树状的数据结构,它具有别的树状的数据结构所没有的特点。
因此,要想了解红黑树,必须先了解**“树”**是一种怎样的数据结构。
数据结构——树
“树”其实就是类似这样的数据结构,它由很多节点构成。关于“树”我们要了解一些用来描述“树”的名词。
-
根节点:根节点就是一棵树的顶层节点。如上图所示,这就是一颗倒着画的树
-
左子节点:一个节点左下方和其连接的第一个节点。例如:22节点的左子节点就是18
-
右子节点:一个节点右下方和其连接的第一个节点。例如:22节点的右子节点就是26
-
左子树:一个节点左下方所有节点组成的子树。如上图所示,左边蓝色框框住的就是根节点22的左子树。
-
右子树:一个节点右下方所有节点组成的子树。如上图所示,右边蓝色框框住的就是根结点22的右子树。
-
树高:树的总层数。如上图所示,这棵树有4层,树高为4
-
度:任意节点的子节点个数
树的节点
上期我们介绍过链表的节点,通过观察可以发现,树的节点比链表的节点就多了一个**“父节点的地址”,左子节点和右子节点的地址其实就可以类比成链表的前一个节点的地址和后一个节点**的地址。
数据结构——二叉树查找树
了解完树这种数据结构之后,相信大家就可以对红黑树有初步的理解。了解完红黑树作为“树”所具有的树的一般特征外,我们开始介绍红黑树的特别之处。在正式介绍红黑树的特征之前,我们先了解一下红黑树的由来。这就引出了我们下面要介绍的另一种树状的数据结构——“二叉树查找树”。
在介绍二叉查找树之前,我们先介绍什么是二叉树。
-
二叉树的特点:度(任意节点的子节点个数) <= 2
简单来说,二叉树就是任意节点的分叉不能大于两个。
-
二叉树查找树的特点:
-
度(任意节点的子节点个数) <= 2
-
任意节点的左子树上的值都小于当前节点的值
-
任意节点的右子树上的值都大于当前节点的值
如上图所示,这个就是一个二叉树查找树,它在二叉树的基础上增加了两个要求。我们发现其任意节点的子节点的个数都在两个及两个以内;任意节点的左边的节点都比它小;任意节点的右边的节点都比它大。之所以称它为二叉查找树,是因为这种结构本身就带有二分查找的性质。例如:我们想要查找19这个节点。我们会先用19节点和根节点22比较,发现比根节点小。按照二叉查找树的特点——任意节点的左子树上的值都小于当前节点的值。因此,就可以确定19节点一定在22节点的左子树上。接着,我们用19节点和22节点的左子节点18进行对比,明显19大。再次利用二叉查找树的特点——任意节点的右子树上的值都大于当前节点的值。因此19就一定在18节点的右子树上。最后利用19节点和18节点的右子节点20比较,发现19节点小。因此19节点就一定在20节点的左子树上,这时候20节点的左子树只有一个节点,这个节点正好就是19,查找完毕。
-
-
二叉查找树的弊端:
虽然二叉查找树具有二分的性质,查询的效率比较好。但是,我们如果遇到一些特殊情况,这种数据结构的查询的效率将会大打折扣。
例如:我们想要将1、3、5、7、9这5个节点添加进二叉查找树中。根据二叉查找树的特点,完成的样子如下图。
观察上图,相信看过上期的读者可以发现,这不就是我们介绍过的数据结构——链表?没错,这个时候的二叉查找树和链表几乎完全一样。而链表的缺点就是查询效率慢,因为这时我们想要查询任意一个节点都需要从根节点开始,一条路往下找。这种查找最坏的情况就是O(n)——从根节点一个一个往下找,直到找到最底层的子节点。例如:我们要查找9这个节点,就需要从1节点开始,一直找到树的最底层。
那我们怎么解决这个弊端呢?这个结构之所以查询效率慢的原因就是它的树的层数比较大,导致我们往下查找的时候要花费比较多的时间。既然如此,我们就想办法来控制树的层数。由此,我们引出了下面要介绍的另一种树状的数据结构——平衡二叉树。
数据结构——平衡二叉树
- 特点:任意节点的左右子树的**树高 **(树的总层数) 的差不超过1
观察上图,我们发现每一个节点的左右子树的树高的差值都不超过1。例如,7的左子树的树高为2,7的右子树的树高为2,因此树高的差值为0。根据这种判断方法,我们就会发现这棵树上的任意节点的左右节点的树高的差值都 <= 1。如果一个二叉查找树的任意节点的左右子树的树高的高度差不超过1,我们就是这棵树是平衡的,由此得出了一种新的数据结构——平衡二叉树。
至于当二叉查找树不平衡的时候是怎样变成平衡二叉树的,感兴趣的读者可以去查阅相关资料 —— “平衡二叉树的旋转机制”。
虽然二叉查找树可以通过旋转机制变成一个平衡二叉树,但是这个旋转是需要时间的。特别当某个节点添加进二叉查找树后,需要经过多次旋转才能变成平衡二叉树。因此,存不存在一种数据结构,既可以拥有二叉查找树的性质,又不需要花费多次旋转所消耗的时间。答案是肯定的,而这种数据结构本期我们的主角——红黑树。
数据结构——红黑树
-
红黑树的特点:
- 红黑树不是平衡二叉树,它的“平衡”是通过特有的**“红黑规则”**实现的,因此它的任意节点的左右子树的树高的差值可能大于1
- 红黑树是二叉查找树,遵循二叉查找树的条件。
- 红黑树中的每一个节点都拥有一个颜色,这个颜色可以是红色或者黑色。另外,根节点一定要是黑色。
-
红黑树例子
上图为红黑树的例子,我们可以发现根节点是黑色,然后每一个节点都是红色或者黑色,并且拥有二叉查找树的特点。
注:图中的“Nil”节点被称为叶节点。叶节点:当一个节点的子节点为空的时候,就称这个空的节点为叶节点。
-
红黑树的节点
观察上图,我们发现,红黑树的节点中相比于普通树的节点就多了一个储存颜色的空间,这个空间可以储存红色或 者黑色的值。在Java中红色或者黑色这两个都被定义为boolean类型的常量。其中,RED的值为false,BLACK的值 为true。
- 红黑规则
- 每一个节点必须是红色或者黑色的
- 根节点是黑色的
- 叶节点是黑色的
- 两个红色的节点不能直接相连
- 任意节点到所有后代节点的简单路径上,黑色节点的数量相同
-
红黑树的调整规则
上方为红黑树必须遵守的规则,如果添加一个节点后违反了这些规则,那么就要进行调整。注:红黑树在进行节点的添加时,默认是红色的,因为添加红色后红黑树需要调整的次数比黑色少。
TreeSet的排序方法
在了解完成红黑树的特点后,我们就可以理解TreeSet的特点了。这个结构没有维护添加时数据的顺序,因此是无序的。但是,由于红黑树是二叉查找树,因此再添加数据的时候必须要进行比较,以确定当前节点应该添加在红黑树中节点的左边还是右边。所以,使用TreeSet的时候一定要指定比较的规则。正是因为红黑树**“左边小,右边大”的特点,它经常被用来对数据进行排序**。
TreeSet的排序方法一
添加数据时,用数据所属的类实现Comparable接口,并重写compareTo( )方法。注:Java原生类:如String,Integer等,默认已经实现了Comparable接口并且重写了CompareTo( )方法。因此,对于这种原生类,我们不能用此方法改变TreeSet对于String、Integer等类型的比较规则。而对于我们自己定义的类,则一定要实现Compareble接口并重写里面的方法,如果不实现可能会报错。
- String类的默认比较方式是从第一个字符开始,比较字符在Unicode编码表的值,那个值大就说明这个字符串大,如果相等就继续比较第二个字符,依次类推。利用这个值就进行比较,如果要添加的节点的值大,此节点就存右边;小则存左边;相等则不存,这就解释了TreeSet的不重复性的实现方式。
附:String源码
- Integer类的默认比较方式是比较整形数据的值,**和String类一样,大存右边,小存左边,相等则不存。
附:Integer类源码
- 自定义类并指定排序方法
例:TreeSet的排序方法一
我们可以看到,我们在Student类中实现了Comparable接口并重写了compareTo方法,指定了排序的规则为:对象的name成员的字符串长度。我们添加了四个对象,分别为James、Jordan、Durant、Mike。观察右侧控制台的输出结果,可以看到Durant和Jordan长度一样,因此后添加的Durant就没有被添加进集合,然后按照name字符串的长度进行从小到大的排序(如果想要从大到小,可以颠倒“this”和“o”的位置)。
TreeSet的排序方法二
如果没有利用类实现Comparable接口,就要在创建TreeSet对象的时候必须在构造方法里面传递比较器Comparator接口的对象。
注:TreeSet类里面有一个成员变量,专门用来存储传递过来的构造器对象。当TreeSet里面的数据所属的类同时实现了 Comparable接口和传递了比较器Comparator的对象,那么将会优先以Comparator接口中被重写的方法——compare( )作为添加数据时比较的对象,这时就不再需要实现Comparable接口。
例:TreeSet的排序方法二
上图中,Student类实现了Comparable接口的同时,又在创建TreeSet对象时利用匿名内部类的形式传递了Comparator的实现类对象并重写compare( )方法,指定比较规则为:比较首字母的Unicode值。通过右侧的控制台的输出,可以知道确实是按照首字母的Unicode值类比较的。其中,因为James和Jordan首字母都为“J”,因此后添加的Jordan就不存,然后再按照从小到大的顺序进行获取数据并打印出来(如果从大到小,就颠倒o1和o2的位置)。
TreeSet总结和Map的引入
到此为止,TreeSet我们已经介绍完了,现在让我们来总结一下。TreeSet的不重复性是由于红黑树数据这种数据结构的要求,同样的数据时不存进红黑树的:无序性是因为红黑树不能维护数据添加时候的顺序;无索引是一因为红黑树的数据本身就没有索引,二是Set接口类的集合不要求实现类提供索引操作。
接下来,我们正式开始Map的介绍。介绍之前,我希望大家能记住一句话——Set类集合的底层就是Map集合。了解这句话后,下面的Map集合就比较容易理解了。
Map
概念
前面我们说过,Collection集合是单列集合,集合里面的每个元素只能存一个单一的有效的值。Map和Collection是相对的,Map集合的每一个元素可以存一个键值对对象,而一个键值对对象里面可以存在两个有效的值—— 一个为key,另一个为value。
Entry对象
其实,Set和Map集合中的每一个元素都是一个Entry对象(也称为**“键值对对象”**)。键值对就是通过一个键来映射出一个值。例如:“爱好 :编程” 、”年龄 : 20“,前面的”爱好“、”姓名“称为键,”编程“、”20“就是键对应的值。这种通过一个键对应一个值的对象,就是键值对对象。
-
TreeMap的每个节点的源码
观察上图,我们可以看到,在TreeMap内部有一个内部类Entry。这个Entry的对象就表示红黑树中的每一个节点。其中,key就代表键,value就代表值。那这里大家可能会发现一个问题:Set集合不是单列集合吗?为什么Set集合里面的每一个元素也是键值对对象呢?确实,由于Set集合的底层就是Map集合,每一个元素是一个键值对对象。但是,在它的键值对对象中,只有key(键)的位置存在真实的值,vaule(值)的位置是一个固定的Object类型的常量“PRESENT”**,不是一个真实有效的值。
Map的特点
Map的特点和Set类似,要求元素是不能重复的,在这里要求的是每一个元素中,键(key)位置的值不能重复,值(vaule)位置的值可以重复。如果添加的时候键重复了,默认后面添加的元素会覆盖前面的元素。Map不提供操作索引的方法,不要求元素一定是有序的。
补充: 如果不想进行覆盖,可以使用putIfAbsent( )方法——这个方法只能在集合中不存在要添加键的时候才进行添加成功。也就是说,如果键相同,将不会进行覆盖,直接舍弃不存。
Map集合的底层数据结构
-
HashMap
HashMap的底层数据结构和HashSet一样,也是哈希表。不同点就是HashMap的vaule位置不是常量PRESENT,而是一个真实有效的值。
-
LinkedHashMap
LinkedHashMap的底层数据结构也和LinkedHashSet一样,也是一个哈希表,然后利用一个双向的链表维护和保存添加数据时候的顺序,因此它也是有序的。不同点就是LinikedHashMap的vaule位置不是常量PRESENT,而是一个真实有效的值。
-
TreeMap
TreeMap的底层数据结构和TreeSet一样,也是红黑树,不同点就是TreeMap的vaule位置不是常量PRESENT,而是一个真实有效的值。
注意:使用Map集合的时候要谨记,Map集合的特点是对于键来说的。例如:在使用HashMap重写hashCode()和equals()方法的时候,要在键位置数据所属的类里面进行重写、在使用TreeMap的时候,指定的是键位置数据的比较方法。因此,要在键数据所属的类上进行实现Comparable接口的操作…
思考题
在我们介绍的诸多集合中,到底该如何选择适合我们的集合呢?又如何在尽量提高效率的情况选择合适的集合呢?