Java集合类——数据结构入门
要谈集合类,那必然绕不开数据结构。像ArrayList底层由数组实现,使用的是线性表的顺序存储结构;LinkedList使用的是线性表的链式存储结构;而HashMap则使用了散列存储结构......,等等这些,不一而足。可见集合类和数据结构之间关系之紧密。
很明显,想要深入集合类的源码,必须具备一定程度的数据结构经验,这样才能起到事半功倍效果
一、数据结构
数据结构是相互之间存在一种或多种特定关系的数据元素的集合(这是严蔚敏版《数据结构》给出的经典定义)。简单来说,只要存在一堆数据,这些数据就属于某一种数据结构了。可以根据这些数据之间的关系不同,把它们分成如下几种不同结构类型:
其中线性结构和树形结构是需要重点关注的,集合类中常用的也是这两种数据结构。
上述所说的数据结构(包括那个定义)我觉得更准确的描述应该是"数据间的结构",它更多的是从数据间的关系来阐述的(相当于后面叙述的逻辑结构)。但是从计算机的角度来说,能让计算机使用的数据才是有用的,否则都只能是空中楼阁。所以从数据结构这门学科来说,从广义的概念来说,数据结构往下可以分成两个大类:分别是"逻辑结构"和"存储结构"。
逻辑结构描述的是数据之间的关系,与其如何存储无关。比如就队列这个概念来说,它的定义是先进先出的线性表。只要某数据集合中的数据符合该条件,那么它们的逻辑结构就是队列,与这些数据怎么存、存在哪都没有关系。
存储结构描述的显然就应该是数据如何进行存储了。比如单向链表就是一个存储结构。单向链表一般由多个节点链接组成,每个节点包含两个域(数据域和指针域),前一个节点的指针域指向后一个节点,既前一个节点的指针域的值就是后一个节点的地址。这个概念中用到了地址,那么很明显单向链表是教我们如何存储数据的,所以单向链表是一种存储结构。
在《数据结构》一书中出现了很多的概念,其中有些属于逻辑结构,有些属于存储结构,这些概念往往混在一起,让人无法区分。这里可以描述一个简单的区分方式:如果一个概念中包含了地址、引用地址计算、对存储位置有一定的要求等等这些,那么它描述的就是存储结构。因为这明显是在说应该怎样存储数据,把数据放到哪一个指定的位置(也就是地址)上更合适;如果某概念是以数据的特征、数据间的关系、数据可以进行的操作来做定义,定义中压根没有提到应该如何存放,那么它表就是一个逻辑结构的定义。
下面简单举几个例子。
第一个是数组。数组其实就是一堆有先后顺序的同类型的数据元素的集合。那显然它就是一个逻辑结构的概念。在绝大多数语言中,创建数组的时候,总会使用一片地址连续的存储空间来存放数组,但这并不意味着创建数组就一定要使用连续的地址空间。当然使用连续的地址是这些语言中设定好的规则(不像队列这种需要自己定义其数据类型,实现各种操作),这是由更加底层的代码来实现的,我们无法在编程中进行修改。不过如果语言的创建者愿意的话,我相信数组也是可以使用链式存储来实现的。综上用一句话来概括,就是数组这种逻辑结构是用顺序存储结构来实现的。
第二个是循环队列。这是一个很特殊的概念。队列本身属于逻辑结构,但是循环队列却属于存储结构。了解这个概念的人都知道,想要构建循环队列,对存储形式和地址是有一定的要求的。首先要求得使用顺序存储结构,另外也不支持数组长度的动态扩展,而且还需要设置两个指针来判断队列是否已满(在C语言中这两个指针应该指向地址,java中一般指向引用的对象)。可以看到循环队列对如何存储做出了要求,并对数据地址也会有一定的判断,那么它就是一个存储结构。这是个较特殊的概念,如果考试此处会有坑。
说了这么多,下面来看下常用的概念中哪些是逻辑结构,哪些是存储结构。
二、逻辑结构和存储结构的关系
再来应该谈下逻辑结构和存储结构的关系。逻辑结构是人们想出来的数据之间的关系,其本质和计算机无关,然后又按照不同的类型被分成了如上图所示的几大类。当某些数据之间的关系符合上面某种类型定义的时候,我们就会说这些数据应该符合某种逻辑结构。
比如我们的数据是军队的层级关系,那么就符合树形结构。一个军长下面会有多个师长,一个师长下面又有多个旅长这样。再比如数据是家庭成员关系,因为一个人可能有多个角色,比如是儿子,又是爸爸,和不同的人进行连线会有不同的关系。很显然这是一个多对多的关系,那么应该符合图状结构或网状结构。
从这里也可以看出,逻辑结构的概念是与计算机无关的。
但当我们想把这些数据,这种关系在计算机中表现出来的时候,那么就要和存储结构打交道了。因为存储结构能告诉我们如何把这些数据合理的存储到计算机中。通俗的来说,就是使用计算机语言(各种编程语言)把这些数据和逻辑关系表达出来。
可以看出,对于计算机来说,特别是对于编程来说,逻辑结构和存储结构是相互依存的关系,它们紧密相连,缺一不可。所以我们经常会有这样的表述:"xx逻辑结构的xx存储结构的表示和实现",或者说"使用xx存储结构实现了xx逻辑结构"。比如"线性表的链式表示和实现";"线性表的顺序表示和实现";"使用循环队列实现了队列";"使用顺序存储实现了队列","使用顺序存储实现了数组"等等......
三、逻辑结构和存储结构对程序的影响
下面说说这两种结构对程序的影响。谈影响,那么就不得不提到两个概念:时间复杂度和空间复杂度。这两个概念讲起来非常简单。
时间复杂度代表运行这段程序需要多长时间,很明显越短越好;空间复杂度代表运行这段程序需要多大的存储空间,自然是越小越好。不过这两者一般来说就像是鱼和熊掌不可兼得。要不空间换时间,要不时间换空间,除此无他。在硬件容量越来越大,也越来越便宜的当下,可以说99%以上的算法都是仅考虑时间复杂度,不考虑空间复杂度。
为什么上面不说100%,那是因为不是所有的硬件都便宜,有些特殊情况下工作的硬件很贵,比如飞出地球的那种,那种需要防辐射抗干扰,一颗CPU造价就高达十几万,频率还低,内存啥的自然也不便宜。这些情况暂时不谈。
一个算法花费时间的长短,和它采用的逻辑结构还有存储结构都密切相关。比如我们要查找数组中的数据,在提供数组下标的情况下,时间复杂度为O(1),表示一次查找就可以了。但是当我们要删除一个数据的时候,时间复杂度就变成了O(n),这里的n指代数组中的数据量。因为我们删除一个元素之后,在该元素之后的所有元素都需要往前移动一位,很明显这个操作和数组本身的数据量有关,数据越多越慢。这个特性就是由数组所采用的存储结构决定的。顺序存储结构的特点就是查找快(有下标查找)、增删慢。这也是ArrayList的性质。
可以说逻辑结构和存储结构加在一起,基本上就决定了程序的时间复杂度。剩下的一点点因素一般可以忽略不计,那就是计算机的硬件。比如CPU贼慢,1s计算1次这种,此时什么样的数据结构来也没用。当然现实来看这是不可能的。
后面学习java中的集合类都是围绕这两大块进行的,从逻辑结构和存储结构的角度来看java中的集合类到底是如何实现的。