java数据结构与算法之顺序表与链表深入分析.docx
- 文档编号:29371120
- 上传时间:2023-07-22
- 格式:DOCX
- 页数:59
- 大小:781.91KB
java数据结构与算法之顺序表与链表深入分析.docx
《java数据结构与算法之顺序表与链表深入分析.docx》由会员分享,可在线阅读,更多相关《java数据结构与算法之顺序表与链表深入分析.docx(59页珍藏版)》请在冰豆网上搜索。
java数据结构与算法之顺序表与链表深入分析
java数据结构与算法之顺序表与链表深入分析
1.线性表抽象数据类型概述
首先来说明一下什么是抽象数据类型,我们都知道java在默认情况下,所有的基本数据类型(int,float,boolean等)都支持基本运算,如加减法,这是因为系统已帮我们实现了这些基本数据类型的的基本运算。
而对于自定义的数据类型(如类)也需要定义相应的运算,但在实际使用这些自定义的数据类型的运算时需要自己实现相关的运算,也就是说用户自定义的数据类型的运算需要我们自己利用系统提供的基本运算来定义和实现。
这些自定义了数据结构(如自定义类)和包含相关运算组合实现的数据类型就称其为抽象数据类型(ADT,AbstractDataType),因此一个ADT会包含数据声明和运算声明。
常用的ADT包含链表、栈、队列、优先队列、二叉树、散列表、图等,所以接下来我们要分析的顺序表和链表也属于ADT范畴。
下面引用自java数据结构一书对线性表的定义:
线性表是由n(n>=0)个类型相同的数据元素a0,a1,…,an-1组成的有限的序列,在数学中记作(a0,a1,…,an-1),其中ai的数据类型可以是基本数据类型(int,float等)、字符或类。
n代表线性表的元素个数,也称其为长度(Length)。
若n=0,则为空表;若n>0,则ai(0
以上便是对线性表抽象数据类型概述,下面我们开始分别针对顺序表和链表进行深入分析。
2.线性表的顺序存储设计与实现(顺序表)
2.1顺序存储结构的设计原理概要
顺序存储结构底层是利用数组来实现的,而数组可以存储具有相同数据类型的元素集合,如int,float或者自定义类型等,当我们创建一个数组时,计算机操作系统会为该数组分配一块连续的内存块,这也就意味着数组中的每个存储单元的地址都是连续的,因此只要知道了数组的起始内存地址就可以通过简单的乘法和加法计算出数组中第n-1个存储单元的内存地址,就如下图所示:
通过上图可以发现为了访问一个数组元素,该元素的内存地址需要计算其距离数组基地址的偏移量,即用一个乘法计算偏移量然后加上基地址,就可以获得数组中某个元素的内存地址。
其中c代表的是元素数据类型的存储空间大小,而序号则为数组的下标索引。
整个过程需要一次乘法和一次加法运算,因为这两个操作的执行时间是常数时间,所以我们可以认为数组访问操作能再常数时间内完成,即时间复杂度为O
(1),这种存取任何一个元素的时间复杂度为O
(1)的数据结构称之为随机存取结构。
而顺序表的存储原理正如上图所示,因此顺序表的定义如下(引用):
线性表的顺序存储结构称之为顺序表(SequentialList),它使用一维数组依次存放从a0到an-1的数据元素(a0,a1,…,an-1),将ai(0n-1)存放在数组的第i个元素,使得ai与其前驱ai-1及后继ai+1的存储位置相邻,因此数据元素在内存的物理存储次序反映了线性表数据元素之间的逻辑次序。
2.2顺序存储结构的实现分析
接着我们来分析一下顺序表的实现,先声明一个顺序表接口类ISeqList
packagecom.zejian.structures.LinkedList;
/**
*Createdbyzejianon2016/10/30.
*顺序表顶级接口
*/
publicinterfaceISeqList
/**
*判断链表是否为空
*@return
*/
booleanisEmpty();
/**
*链表长度
*@return
*/
intlength();
/**
*获取元素
*@paramindex
*@return
*/
Tget(intindex);
/**
*设置某个元素的值
*@paramindex
*@paramdata
*@return
*/
Tset(intindex,Tdata);
/**
*根据index添加元素
*@paramindex
*@paramdata
*@return
*/
booleanadd(intindex,Tdata);
/**
*添加元素
*@paramdata
*@return
*/
booleanadd(Tdata);
/**
*根据index移除元素
*@paramindex
*@return
*/
Tremove(intindex);
/**
*根据data移除元素
*@paramdata
*@return
*/
booleanremove(Tdata);
/**
*根据data移除元素
*@paramdata
*@return
*/
booleanremoveAll(Tdata);
/**
*清空链表
*/
voidclear();
/**
*是否包含data元素
*@paramdata
*@return
*/
booleancontains(Tdata);
/**
*根据值查询下标
*@paramdata
*@return
*/
intindexOf(Tdata);
/**
*根据data值查询最后一个出现在顺序表中的下标
*@paramdata
*@return
*/
intlastIndexOf(Tdata);
/**
*输出格式
*@return
*/
StringtoString();
}
}
代码中声明了一个Object数组,初始化数组大小默认为64,存储的元素类型为泛型T,length则为顺序表的长度,部分方法实现比较简单,这里不过多分析,我们主要分析get(intindex)、set(intindex,Tdata)、add(intindex,Tdata)、remove(intindex)、removeAll(Tdata)、indexof(Tdata)等方法的实现。
get(intindex)实现分析
从顺序表中获取值是一种相当简单的操作并且效率很高,这是由于顺序表内部采用了数组作为存储数据的容器。
因此只要根据传递的索引值,然后直接获取数组中相对应下标的值即可,代码实现如下:
publicTget(intindex){
if(index>=0&&index return(T)this.table[index]; returnnull; } set(intindex,Tdata)实现分析 在顺序表中替换值也是非常高效和简单的,只要根据传递的索引值index找到需要替换的元素,然后把对应元素值替换成传递的data值即可,代码如下: publicTset(intindex,Tdata){ if(index>=0&&index =null) { Told=(T)this.table[index]; this.table[index]=data; returnold; } returnnull; } add(intindex,Tdata)实现分析 在顺序表中执行插入操作时,如果其内部数组的容量尚未达到最大值时,可以归结为两种情况,一种是在头部插入或者中间插入,这种情况下需要移动数组中的数据元素,效率较低,另一种是在尾部插入,无需移动数组中的元素,效率高。 但是当顺序表内部数组的容量已达到最大值无法插入时,则需要申请另一个更大容量的数组并复制全部数组元素到新的数组,这样的时间和空间开销是比较大的,也就导致了效率更为糟糕了。 因此在插入频繁的场景下,顺序表的插入操作并不是理想的选择。 下面是顺序表在数组容量充足下头部或中间插入操作示意图(尾部插入比较简单就不演示了): 理解了以上几种顺序表的插入操作后,我们通过代码来实现这个插入操作如下,注释很清晰就过多分析了: /** *根据index插入元素 *@paramindex插入位置的下标,0作为起始值 *@paramdata插入的数据 *@return */ publicbooleanadd(intindex,Tdata){ if(data==null) returnfalse; //插入下标的容错判断,插入在最前面 if(index<0) index=0; //插入下标的容错判断,插入在最后面 if(index>this.length) index=this.length; //判断内部数组是否已满 if(this.length==table.length) { //把原数组赋值给临时数组 Object[]temp=this.table; //对原来的数组进行成倍拓容,并把原数组的元素复制到新数组 this.table=newObject[temp.length*2]; //先把原数组下标从0到index-1(即插入位置的前一个位置)复制到新数组 for(inti=0;i this.table[i]=temp[i]; } } //从原数组的最后一个元素开始直到index位置,都往后一个位置 //最终腾出来的位置就是新插入元素的位置了 for(intj=this.length-1;j>=index;j--){ this.table[j+1]=this.table[j]; } //插入新值 this.table[index]=data; //长度加一 this.length++; //插入成功 returntrue; } remove(intindex)实现分析 顺序表的删除操作和前的插入操作情况是类似的,如果是在中间或者头部删除顺序表中的元素,那么在删除位置之后的元素都必须依次往前移动,效率较低,如果是在顺序表的尾部直接删除的话,则无需移动元素,此情况下删除效率高。 如下图所示在顺序表中删除元素ai时,ai之后的元素都依次往前移动: 删除操作的代码实现如下: /** *根据index删除元素 *@paramindex需要删除元素的下标 *@return */ publicTremove(intindex) { if(this.length! =0&&index>=0&&index { //记录删除元素的值并返回 Told=(T)this.table[index]; //从被删除的元素位置开,其后的元素都依次往前移动 for(intj=index;j this.table[j]=this.table[j+1]; } //设置数组元素对象为空 this.table[this.length-1]=null; //顺序表长度减1 this.length--; returnold; } returnnull; } removeAll(Tdata)实现分析 在顺序表中根据数据data找到需要删除的数据元素和前面分析的根据index删除顺序表中的数据元素是一样的道理,因此我们只要通过比较找到与data相等的数据元素并获取其下标,然后调用前面实现的remove(intindex)方法来移除即可。 代码实现如下: @Override publicbooleanremoveAll(Tdata){ booleandone=false; if(this.length! =0&&data! =null) { inti=0; while(i //找出数据相同的选项 if(data.equals(this.table[i])) { this.remove(i);//根据下标删除 done=true; } else i++;//继续查找 } returndone; } indexOf(Tdata)实现分析 要根据data在顺序表中查找第一个出现的数据元素的下标,只需要通过对比数据项是否相等,相等则返回下标,不相等则返回-1,indexOf和lastIndexOf方法实现如下: /** *根据数据查询下标 *@paramdata *@return */ @Override publicintindexOf(Tdata) { if(data! =null) for(inti=0;i //相当则返回下标 if(this.table[i].equals(data)) returni; } return-1; } /** *根据data查询最后一个出现在顺序表中的下标 *@paramdata *@return */ @Override publicintlastIndexOf(Tdata) { if(data! =null) for(inti=this.length-1;i>=0;i--) if(data.equals(this.table[i])) returni; return-1; } 以上便是顺序表的主要的操作方法,当然顺序表中还可以实现其他操作,如在初始化构造函数时传入数组来整体初始化顺序表,比较两个信息表是否相等、是否包含某个数据等。 这里贴一下传入数据构建顺序表构造方法实现,其他实现代码我们这里就不贴了,稍后实现源码都会上传gitHub提供给大家: /** *传入一个数组初始化顺序表 *@paramarray */ publicSeqList(T[]array){ if(array==null){ thrownewNullPointerException("arraycan\'tbeempty! "); } //创建对应容量的数组 this.table=newObject[array.length]; //复制元素 for(inti=0;i this.table[i]=array[i]; } this.length=array.length; } 2.3顺序存储结构的效率分析 通过上述的分析,我们对顺序表的实现已有了比较清晰的认识,接下来看一下顺序表的执行效率问题,主要针对获取、插入、修改、删除等主要操作。 前面分析过,由于顺序表内部采用了数组作为存储容器,而数组又是随机存取结构的容器,也就是说在创建数组时操作系统给数组分配的是一块连续的内存空间,数组中每个存储单元的地址都是连续的,所以在知道数组基地址后可以通过一个简单的乘法和加法运算即可计算出其他存储单元的内存地址(实际上计算机内部也就是这么做的),这两个运算的执行时间是常数时间,因此可以认为数组的访问操作能在常数时间内完成,即顺序表的访问操作(获取和修改元素值)的时间复杂为O (1)。 对于在顺序表中插入或者删除元素,从效率上则显得不太理想了,由于插入或者删除操作是基于位置的,需要移动数组中的其他元素,所以顺序表的插入或删除操作,算法所花费的时间主要是用于移动元素,如在顺序表头部插入或删除时,效率就显得相当糟糕了。 若在最前插入或删除,则需要移动n(这里假设长度为n)个元素;若在最后插入或删除,则需要移动的元素为0。 这里我们假设插入或删除值为第i(0 p1∗(n−1)+p2∗(n−2)+...+pi∗(n−i)+...+pn−1∗1+pn∗0=∑i=1n(pi∗(n−i)) 如果在各个位置插入元素的概率相同即pi=1n+1(n+1个插入位置任意选择一个的概率)则有: ∑i=1n(pi∗(n−i))=1n+1∑i=1n(n−i)=1n+1∗n(n+1)2=n2=O(n) 也就是说,在等概率的情况下,插入或者删除一个顺序表的元素平均需要移动顺序表元素总量的一半,其时间复杂度是O(n)。 当然如果在插入时,内部数组容量不足时,也会造成其他开销,如复制元素的时间开销和新建数组的空间开销。 因此总得来说顺序表有以下优缺点: 优点 使用数组作为内部容器简单且易用 在访问元素方面效率高 数组具有内存空间局部性的特点,由于本身定义为连续的内存块,所以任何元素与其相邻的元素在物理地址上也是相邻的。 缺点 内部数组大小是静态的,在使用前必须指定大小,如果遇到容量不足时,需动态拓展内部数组的大小,会造成额外的时间和空间开销 在内部创建数组时提供的是一块连续的空间块,当规模较大时可能会无法分配数组所需要的内存空间 顺序表的插入和删除是基于位置的操作,如果需要在数组中的指定位置插入或者删除元素,可能需要移动内部数组中的其他元素,这样会造成较大的时间开销,时间复杂度为O(n) 3.线性表的链式存储设计与实现(链表) 3.1链表的链式存储结构设计原理概要 通过前面对线性顺序表的分析,我们知道当创建顺序表时必须分配一块连续的内存存储空间,而当顺序表内部数组的容量不足时,则必须创建一个新的数组,然后把原数组的的元素复制到新的数组中,这将浪费大量的时间。 而在插入或删除元素时,可能需要移动数组中的元素,这也将消耗一定的时间。 鉴于这种种原因,于是链表就出场了,链表在初始化时仅需要分配一个元素的存储空间,并且插入和删除新的元素也相当便捷,同时链表在内存分配上可以是不连续的内存,也不需要做任何内存复制和重新分配的操作,由此看来顺序表的缺点在链表中都变成了优势,实际上也是如此,当然链表也有缺点,主要是在访问单个元素的时间开销上,这个问题留着后面分析,我们先通过一张图来初步认识一下链表的存储结构,如下: 从图可以看出线性链表的存储结构是用若干个地址分散的存储单元存放数据元素的,逻辑上相邻的数据元素在物理位置上不一定相邻,因此每个存储单元中都会有一个地址指向域,这个地址指向域指明其后继元素的位置。 在链表中存储数据的单元称为结点(Node),从图中可以看出一个结点至少包含了数据域和地址域,其中数据域用于存储数据,而地址域用于存储前驱或后继元素的地址。 前面我们说过链表的插入和删除都相当便捷,这是由于链表中的结点的存储空间是在插入或者删除过程中动态申请和释放的,不需要预先给单链表分配存储空间的,从而避免了顺序表因存储空间不足需要扩充空间和复制元素的过程,提高了运行效率和存储空间的利用率。 3.2单链表的储结构实现分析 到此我们已初步了解了链表的概念和存储结构,接下来,开始分析链表的实现,这里先从单链表入手。 同样地,先来定义一个顶级的链表接口: ILinkedList和存储数据的结点类Node,该类是代表一个最基本的存储单元,Node代码如下: /** *Createdbyzejianon2016/10/21. *单向链表节点 */ publicclassNode publicTdata;//数据域 publicNode publicNode(Tdata){ this.data=data; } publicNode(Tdata,Node this.data=data; this.next=next; } } 接着顶级的链表接口ILinkedList,该接口声明了我们所有需要实现的方法。 /** *Createdbyzejianon2016/10/21. *链表顶级接口 */ publicinterfaceILinkedList /** *判断链表是否为空 *@return */ booleanisEmpty(); /** *链表长度 *@return */ intlength(); /** *获取元素 *@paramindex *@return */ Tget(intindex); /** *设置某个结点的的值 *@paramindex *@paramdata *@return */ Tset(intindex,Tdata); /** *根据index添加结点 *@paramindex *@paramdata *@return */ booleanadd(intindex,Tdata); /** *添加结点 *@paramdata *@return */ booleanadd(Tdata); /** *根据index移除结点 *@paramindex *@return */ Tremove(intindex); /** *根据data移除结点 *@paramdata *@return */ booleanremoveAll(Tdata); /** *清空链表 */ voidclear(); /** *是否包含data结点 *@paramdata *@return */ booleancontains(Tdata); /** *输出格式 *@return */ StringtoString(); } 创建一个单链表SingleILinkedList并实现ILinkedList接口,覆盖其所有方法,声明一个单链表的头结点head,代表链表的开始位置,如下: publicclassSingleILinkedList protectedNode publicSingleILinkedList(Node this.headNode=head; } //其他代码先省略 ..... } booleanisEmpty()实现分析 需要判断链表是否为空的依据是头结点head是否为null,当head=null时链表即为空链表,因此我们只需判断头结点是否为
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- java 数据结构 算法 顺序 深入 分析
![提示](https://static.bdocx.com/images/bang_tan.gif)