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