数据结构与算法(三)线性表
线性表定义
线性表(List):零个或多个数据元素的有限序列。
首先它是一个序列,其次,线性表强调是有限的。
前驱元素:若A元素在B元素的前面,则称A为B的前驱元素。
后继元素:若B元素在A元素的后面,则称B为A的后继元素。线性表的特征
数据元素之间具有一种一对一的逻辑关系。第一个数据元素没有前驱,这个数据元素被称为头结点;最后一个数据元素没有后继,这个数据元素被称为尾结点;除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。
如果把线性表用数学语言来定义,则可以表示为(a1,。。。ai1,ai,ai1,。。。an),ai1领先于ai,ai领先于ai1,称ai1是ai的前驱元素,ai1是ai的后继元素。
线性表的分类
线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。顺序表
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素、使得线性表中再逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
顺序表设计
类名
SequenceList
构造方法
SequenceList(intcapacity):创建容量为capacity的SequenceList对象
成员方法publicvoidclear():空置线性表
publicbooleanisEmpty():判断线性表是否为空,是返回true,否返回false
publicintlength():获取线性表中元素的个数
publicTget(inti):读取并返回线性表中的第i个元素的值
publicvoidinsert(inti,Ee):在线性表的第i个元素之前插入一个值为e的数据元素。
publicvoidinsert(Ee):向线性表中添加一个元素e
publicTremove(inti):删除并返回线性表中第i个数据元素。
publicintindexOf(Ee):返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回1。
成员变量privateT〔〕elements:存储元素的数组
privateintn:当前线性表的长度顺序表代码实现SuppressWarnings(unchecked)publicclassSequenceListTimplementsIterableT{存储元素的数组privateT〔〕elements;记录当前顺序表中的元素个数privateintn;创建容量为capacity的SequenceList对象publicSequenceList(intcapacity){初始化数组this。elements(T〔〕)newObject〔capacity〕;初始化长度n0;}空置线性表publicvoidclear(){n0;}判断线性表是否为空,是返回true,否返回falsepublicbooleanisEmpty(){returnn0;}获取线性表中元素的个数publicintlength(){returnn;}读取并返回线性表中的第i个元素的值publicTget(inti){if(i0in){thrownewRuntimeException(当前元素不存在!);}returnelements〔i〕;}在线性表的第i个元素之前插入一个值为t的数据元素publicvoidinsert(inti,Ee){if(nelements。length){thrownewRuntimeException(当前表已满);}先把i索引处的元素及其后面的元素依次向后移动一位for(intjn;ji;j){elements〔j〕elements〔j1〕;}再把t元素放到i索引处elements〔i〕t;n;}向线性表中添加一个元素tpublicvoidinsert(Ee){if(ielements。length){thrownewRuntimeException(当前表已满);}if(i0in){thrownewRuntimeException(插入的位置不合法);}elements〔n〕t;}删除并返回线性表中第i个数据元素publicTremove(inti){if(i0in1){thrownewRuntimeException(当前要删除的元素不存在);}记录i索引处的值Tcurrentelements〔i〕;索引i后面元素依次向前移动一位for(intji;jn1;j){elements〔j〕elements〔j1〕;}元素个数1n;returncurrent;}返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回1。publicintindexOf(Ee){if(tnull){thrownewRuntimeException(查找的元素不合法);}for(inti0;in;i){if(Objects。equals(elements〔i〕,t)){returni;}}return1;}}顺序表的遍历
一般作为容器存储数据,都需要向外部提供遍历的方式,因此我们需要给顺序表提供遍历方式。
在Java中,遍历集合的方式一般都是用的是forEach循环,如果想让我们的SequenceList也能支持forEach循环,则需要做如下操作:让SequenceList实现Iterable接口,重写iterator方法;在SequenceList内部提供一个内部类SIterator,实现Iterator接口,重写hasNext方法和next方法;publicclassSequenceListTimplementsIterableT{。。。OverridepublicIteratorTiterator(){returnnewSIterator();}privateclassSIteratorimplementsIteratorT{privateintcursor;publicSIterator(){this。cursor0;}OverridepublicbooleanhasNext(){returncursorn;}OverridepublicTnext(){returnelements〔cursor〕;}}}顺序表容量可变
在之前的实现中,当我们使用SequenceList时,先newSequenceList(5)创建一个对象,创建对象时就需要指定容器的大小,初始化指定大小的数组来存储元素,当我们插入元素时,如果已经插入了5个元素,还要继续插入数据,则会报错,就不能插入了。这种设计不符合容器的设计理念,因此我们在设计顺序表时,应该考虑它的容量的伸缩性。
考虑容器的容量伸缩性,其实就是改变存储数据元素的数组的大小,那我们需要考虑什么时候需要改变数组的大小?添加元素时扩容
添加元素时,应该检查当前数组的大小是否能容纳新的元素,如果不能容纳,则需要创建新的容量更大的数组,我们这里创建一个是原数组两倍容量的新数组存储元素。
移除元素时缩容
移除元素时,应该检查当前数组的大小是否太大,比如正在用100个容量的数组存储10个元素,这样就会造成内存空间的浪费,应该创建一个容量更小的数组存储元素。如果我们发现数据元素的数量不足数组容量的14,则创建一个是原数组容量的12的新数组存储元素。
SuppressWarnings(unchecked)publicclassSequenceListT{向线性表中添加一个元素tpublicvoidinsert(Ee){如果当前容量已满,那么扩容2倍if(nelements。length){resize(2elements。length);}。。。}删除并返回线性表中第i个数据元素publicTremove(inti){。。。如果当前元素数量小于容量的14,那么缩容为12if(nelements。length4){resize(elements。length2);}returncurrent;}根据newSize,重置elements的大小publicvoidresize(intnewSize){定义一个临时数组,指向原数组T〔〕tempelements;创建新数组elements(T〔〕)newObject〔newSize〕;System。arraycopy(temp,0,elements,0,temp。length);}}
扩缩容的原理很简单,是创建一个具有指定新容量的新数组,然后把原来的数据拷贝到新数组。顺序表的时间复杂度get(i):不论数据元素量n有多大,只需要一次elements〔i〕就可以获取到对应的元素,所以时间复杂度为O(1)。我们通常把具有这一特点的存储结构称为随机存取结构。insert(inti,Ee):每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为O(n);remove(inti):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n);
由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显。顺序表的优缺点优点无需为表示表中元素之间的逻辑关系而增加额外的存储空间;可以快速地存取表中任意位置的元素。缺点插入和删除操作需要移动大量元素;当线性表长度变化较大时,难以确定存储空间的容量;造成存储空间的碎片。链表
虽然顺序表的查询很快,时间复杂度为O(1),但是增删的效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。这个问题有没有解决方案呢?
有,我们可以使用另外一种存储结构实现线性表,链式存储结构。
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。
链表节点设计
类名
Node
构造方法
Node(Ee,Nodenext):创建Node对象
成员变量Eitem:存储数据
Nodenext:指向下一个结点NoArgsConstructorAllArgsConstructorDatapublicclassNodeE{存储元素privateEitem;指向下一个节点privateNodeEnext;}单向链表
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
单向链表设计
类名
LinkList
构造方法
LinkList():创建LinkList对象
成员方法publicvoidclear():空置线性表
publicbooleanisEmpty():判断线性表是否为空,是返回true,否返回false
publicintlength():获取线性表中元素的个数
publicEget(inti):读取并返回线性表中的第i个元素的值
publicvoidinsert(Ee):往线性表中添加一个元素;
publicvoidinsert(inti,Ee):在线性表的第i个元素之前插入一个值为t的数据元素。
publicEremove(inti):删除并返回线性表中第i个数据元素。
publicintindexOf(Ee):返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回1。
成员变量privateNodehead:记录首结点
privateintn:记录链表的长度单向链表代码实现publicclassLinkListEimplementsIterableE{记录首节点privatefinalNodeEhead;记录链表的长度privateintn;publicLinkList(){初始化头结点headnewNode(null,null);初始化元素个数n0;}空置线性表publicvoidclear(){head。nextnull;n0;}判断线性表是否为空,是返回true,否返回falsepublicbooleanisEmpty(){returnn0;}获取线性表中元素的个数publicintlength(){returnn;}读取并返回线性表中的第i个元素的值publicEget(inti){if(i0in){thrownewRuntimeException(位置不合法!);}通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素NodeEnhead。next;for(intindex0;indexi;index){nn。next;}returnn。item;}往线性表中添加一个元素publicvoidinsert(Ee){找到当前最后一个节点NodeEnhead;头节点不存储数据,所以不能算作第一个元素while(n。next!null){nn。next;}创建新节点,保存元素t让当前最后一个元素指向新节点n。nextnewNode(e,null);元素个数1this。n;}在线性表的第i个元素之前插入一个值为t的数据元素publicvoidinsert(inti,Ee){if(i0in){thrownewRuntimeException(位置不合法!);}找到i位置前一个节点NodeEprehead;头节点不存储数据,所以不能算作第一个元素for(intindex0;indexi;index){prepre。next;}找到i位置的节点NodeEcurrentpre。next;创建新节点,并且新节点需要指向原来i位置的节点原来i位置的前一个节点指向新节点pre。nextnewNode(e,current);元素个数1n;}删除并返回线性表中第i个数据元素publicEremove(inti){if(i0in){thrownewRuntimeException(位置不合法);}找到i位置前一个节点NodeEprehead;for(intindex0;indexi;index){prepre。next;}找到i位置的节点NodeEcurrentpre。next;找到i位置的下一个节点前一个节点指向下一个节点pre。nextcurrent。next;元素个数1n;returncurrent。item;}返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回1publicintindexOf(Ee){从头结点开始,依次找到每一个节点,取出item,和t比较,如果相同,就返回NodeEnhead;for(inti0;n。next!null;i){nn。next;if(n。item。equals(e)){returni;}}return1;}OverridepublicIteratorEiterator(){returnnewLIterator();}privatestaticclassNodeT{存储元素Titem;指向下一个节点NodeTnext;Node(Titem,NodeTnext){this。itemitem;this。nextnext;}}}循环链表
对于单向链表,由于每个结点只存储了向后的指针,到了尾标志就停止了向后链的操作,这样,当中某一结点就无法找到它的前驱结点了。
比如,你是一业务员,家在上海,需要经常出差,行程就是上海到北京一路上的城市,找客户谈生意或分公司办理业务。你从上海出发,乘火车路经多个城市停留后,再乘飞机返回上海,以后,每隔一段时间,你基本还要按照这样的行程开展业务,如图所示:
有一次,你先到南京开会,接下来要对以上的城市走一遍,此时有人对你说,不行,你得从上海开始,因为上海是第一站。你会对这人说什么?神经病。哪有这么傻的,直接回上海根本没有必要,你可以从南京开始,下一站蚌埠,直到北京,之后再考虑走完上海及苏南的几个城市。
显然这表示你是从当中一结点开始遍历整个链表,这都是原来的单链表结构解决不了的问题。事实上,把北京和上海之间连起来,形成一个环就解决了前面所面临的困难。这就是循环链表。
从刚才的例子,可以总结出,循环链表解决了一个很麻烦的问题。如何从当中一个结点出发,访问到链表的全部结点。
在单向链表中,最后一个节点的指针为NULL,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可。
如果链表中没有元素,那么头结点也需要指向自己,从而形成环。
循环链表代码实现
代码实现和单向链表基本一致,只需要在插入尾结点的时候,将尾结点指向头结点即可。publicvoidinsert(Ee){找到当前最后一个节点头节点不存储数据,所以不能算作第一个元素varnhead;while(n。next!null){nn。next;}创建新节点,保存元素t,让当前最后一个元素指向新节点循环链表,最后一个元素指向头结点n。nextnewNode(e,head);元素个数1this。n;}
同时,在构造和清空链表时,让头结点指向自己publicCycleLinkList(){初始化头结点headnewNode(null,null);head。nexthead;初始化元素个数n0;}清空线性表publicvoidclear(){head。nexthead;n0;}双向链表
继续刚才的例子,你平时都是从上海一路停留到北京的,可是这一次,你得先到北京开会,谁叫北京是首都呢,会就是多。开完会后,你需要例行公事,走访各个城市,此时你怎么办?
有人又出主意了,你可以先飞回上海,一路再乘火车走遍这几个城市,到了北京后,你再飞回上海。你会感慨,人生中为什么总会有这样出馊主意的人存在呢?真要气死人才行。哪来这么麻烦,我一路从北京坐火车或汽车回去不就完了吗。
我们的单链表,总是从头到尾找结点,难道就不可以正反遍历都可以吗?当然可以,只不过需要加点东西而已。
双向链表:在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。双向链表结点设计
类名
Node
构造方法
Node(Ee,Nodepre,Nodenext):创建Node对象
成员变量Eitem:存储数据
Nodenext:指向下一个结点
Nodepre:指向上一个结点AllArgsConstructorprivatestaticclassNodeE{存储元素Eitem;指向上一个节点NodeEpre;指向下一个节点NodeEnext;}双向链表设计
类名
DoubleLinkList
构造方法
DoubleLinkList():创建DoubleLinkList对象
成员方法publicvoidclear():空置线性表
publicbooleanisEmpty():判断线性表是否为空,是返回true,否返回false
publicintlength():获取线性表中元素的个数
publicTget(inti):读取并返回线性表中的第i个元素的值
publicvoidinsert(Ee):往线性表中添加一个元素;
publicvoidinsert(inti,Ee):在线性表的第i个元素之前插入一个值为t的数据元素。
publicTremove(inti):删除并返回线性表中第i个数据元素。
publicintindexOf(Ee):返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回1。
publicTgetFirst():获取第一个元素
publicTgetLast():获取最后一个元素
成员变量privateNodefirst:记录首结点
privateNodelast:记录尾结点
privateintn:记录链表的长度双向链表代码实现publicclassDoubleLinkListEimplementsIterableE{记录首节点privatefinalNodeEhead;记录尾节点privateNodeElast;记录链表的长度privateintn;publicDoubleLinkList(){初始化头结点headnewNode(null,null,null);初始化尾节点lastnull;初始化元素个数n0;}空置线性表publicvoidclear(){head。nextnull;lastnull;n0;}判断线性表是否为空,是返回true,否返回falsepublicbooleanisEmpty(){returnn0;}获取线性表中元素的个数publicintlength(){returnn;}获取头结点publicEgetFirst(){if(isEmpty()){returnnull;}returnhead。next。item;}获取尾节点publicEgetLast(){if(isEmpty()){returnnull;}returnlast。item;}读取并返回线性表中的第i个元素的值publicEget(inti){if(i0in){thrownewRuntimeException(位置不合法!);}通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素NodeEnhead。next;for(intindex0;indexi;index){nn。next;}returnn。item;}往线性表中添加一个元素publicvoidinsert(Ee){if(isEmpty()){如果链表为空创建新的节点NodeEnewNodenewNode(e,head,null);让新节点成为尾节点lastnewNode;让头结点指向尾节点head。nextlast;}else{如果链表不为空NodeEtempLastlast;创建新的节点NodeEnewNodenewNode(e,tempLast,null);当前的尾节点指向新节点tempLast。nextnewNode;让新节点成为尾节点lastnewNode;}元素个数1n;}在线性表的第i个元素之前插入一个值为t的数据元素publicvoidinsert(inti,Ee){if(i0in){thrownewRuntimeException(位置不合法!);}找到i位置的前一个节点NodeEprehead;for(intindex0;indexi;index){prepre。next;}找到i位置的节点NodeEcurrentpre。next;创建新节点NodeEnewNodenewNode(e,pre,current);让i位置的前一个节点指向新节点pre。nextnewNode;让i位置的前一个节点变为新节点current。prenewNode;元素个数1n;}删除并返回线性表中第i个数据元素publicEremove(inti){if(i0in){thrownewRuntimeException(位置不合法);}找到i位置前一个节点NodeEprehead;for(intindex0;indexi;index){prepre。next;}找到i位置的节点NodeEcurrentpre。next;找到i位置的下一个节点NodeEnextcurrent。next;i位置的前一个节点的下一个节点指向i位置的下一个节点pre。nextnext;i位置的下一个节点的前一个节点指向i位置的前一个节点next。prepre;元素个数1n;returncurrent。item;}返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回1publicintindexOf(Ee){从头结点开始,依次找到每一个节点,取出item,和t比较,如果相同,就返回NodeEnhead;for(inti0;n。next!null;i){nn。next;if(n。item。equals(e)){returni;}}return1;}OverridepublicIteratorEiterator(){returnnewLIterator();}AllArgsConstructorprivatestaticclassNodeE{存储元素Eitem;指向上一个节点NodeEpre;指向下一个节点NodeEnext;}privateclassLIteratorimplementsIteratorE{privateNodeEnhead;OverridepublicbooleanhasNext(){returnn。next!null;}OverridepublicEnext(){nn。next;returnn。item;}}}链表的时间复杂度get(inti):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为O(n);insert(inti,Ee):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n);remove(inti):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n)。
相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,并且在存储过程中不涉及到扩容等操作,同时它并没有涉及的元素的交换。
相比较顺序表,链表的查询操作性能会比较低。
因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。
华为造车关键一战绘图杨佳近日,中建集团相关信息显示,中建六局联合体中标安徽肥西新能源汽车智能产业园EPC项目,该项目建成后将用于华为与江淮汽车共同开发新一代高端智能电动汽车。华为方面回应称将与江淮
粤港澳大湾区诞生首个绿色贸易标准化技术委员会近日,经广东省市场监督管理局批准,广东省绿色贸易标准化技术委员会(以下简称绿色贸易标委会)成立大会在广州召开。绿色贸易标委会由广东省商务厅中国国际贸易促进委员会广东省委员会联合进行
Java单元测试浅析(JUnitMockito)作者京东物流秦彪1。什么是单元测试(1)单元测试环节测试过程按照阶段划分分为单元测试集成测试系统测试验收测试等。相关含义如下1)单元测试针对计算机程序模块进行输出正确性检验工作。2
探索激光技术从发明到应用的全方位介绍激光的发现与激光器的发明激光的发明可以追溯到20世纪50年代。激光的理论原理早在1916年已被爱因斯坦发现,1958年,物理学家DicksonDyson首次提出激光的概念,并建议用
ETF观察丨东数西算概念获主力加仓近24亿元,人工智能ETF(515980)等涨超1今日(2月24日)午后,东数西算概念活跃,获主力资金净流入近24亿元。个股方面,中科曙光涨停,美利云午后直线封板,而后打开涨停。截至发稿,曙光数创涨超10,证通电子中科曙光拓维信息
广东省拥有7个三线城市汕头第一,6个四线城市,1个五线城市广东省,已经连续34年蝉联我国第一,综合实力更是全国第一,是我国对外的重要窗口。在2022年,广东省的GDP总量达到了129118。58亿元,领先江苏6242。98亿元,高居第一。
写给孩子的话如果我离开了人在死亡的时候,一定会有很多话说。毕竟,我是个这么话唠的人。我能想象,我的孩子长大以后,可能嫌我唠叨,可能嫌我没有给我更好的家庭,但我孩子一定不会嫌我,不够爱他。可能是我生了一场病
孩子感冒发烧咳嗽丨煮一碗甜水,让烧走的津液自己补回去!在孩子感冒发烧的时候,孩子身体需要应对多个挑战,首当其冲的就是津液的损耗。津液在我们的身体中扮演着重要的角色,我们的心肝脾肺肾等重要器官都需要津液的支持,比如帮助消化维持体温保持皮
带孩子需注意的危险物品孩子们几乎会把任何东西放进嘴里,很容易误吞异物。将小的东西吸入喉部或气管中导致窒息,窒息后4分钟内就会造成脑损伤,甚至死亡,监护人应提高防范意识,让儿童远离危险物品,案例小朋友们玩
35岁的孩子适合读哪些名著?这篇文章告诉你答案想让孩子早点接触名著,却不知道如何切入?孩子对名著不感兴趣怎么办?为什么这么小的孩子要读名著?35岁的孩子适合读哪些名著?橙子从三岁开始听西游记,到现在四岁半,陆续听完了西游记地心
女子怀孕37周每天坚持撸铁健身是否合理?医生撸铁可能导致流产常言道,十月怀胎,一朝分娩。但怀胎十月,准妈妈们不得不面对各种各样的状况,不管是心理方面和身体方面,健康的准妈妈对于生产是有极大益处的。对于产前的休养问题,很多医生都一致认为,分娩