570 likes | 679 Views
数据结构( Java 语言版). —— 第 3 章 线性表. 【 教学提示 】 本章共设 8 个学时,理论 4 学时,实验 4 学时 采用理论联系实践、配合实例的方式进行讲解,重点讲解线性表的逻辑结构、存储结构及相关应用,体会数据结构中逻辑结构与存储结构之间的对应关系。 双向链表、循环链表等内容可作为选学内容。. 【 内容简介 】 线性表是数据结构中最基本的结构之一,其应用非常广泛,在高级语言程序设计中经常遇到的数组就是线性表的应用。按线性表的存储结构,可将其分为顺序表和链表,通过对其操作可以解决许多生活中的实际问题。. 【 知识要点 】 线性表的概述和基本概念;
E N D
数据结构(Java语言版) ——第3章 线性表
【教学提示】 • 本章共设8个学时,理论4学时,实验4学时 • 采用理论联系实践、配合实例的方式进行讲解,重点讲解线性表的逻辑结构、存储结构及相关应用,体会数据结构中逻辑结构与存储结构之间的对应关系。 • 双向链表、循环链表等内容可作为选学内容。
【内容简介】 • 线性表是数据结构中最基本的结构之一,其应用非常广泛,在高级语言程序设计中经常遇到的数组就是线性表的应用。按线性表的存储结构,可将其分为顺序表和链表,通过对其操作可以解决许多生活中的实际问题。
【知识要点】 • 线性表的概述和基本概念; • 顺序表的基本操作及综合应用; • 链表的基本操作及综合应用; • 实例应用。
第一节 • 3.1 实例引入 • 【学习任务】 通过实例初步了解线性表的特征,从感性上认识线性表及其简单操作。 • 【例3.1】 对于初学计算机者,会遇到计算机打字练习,目前有很多指法练习软件,其中打字游戏集娱乐和学习于一体,是计算机初学者比较喜欢使用的打字练习软件。图3.1所示为打字游戏界面。 • 对于如图3.1所示的英文打字软件,是对单个字符进行练习的,如图3.2所示的界面是对多个英文字符进行练习,练习时,若输入正确的字母,以蓝色显示;若输入错误的字母,会给出相应提示或显示其他颜色(假设为红色)。
在如图3.2所示的提示信息中,给出的字母就是按照顺序排列的,其中包含了字母、空格等符号,用户可以按照字母出现的先后顺序进行操作,根据这样的提示输入的信息就是典型的基于前后关系的线性结构。在如图3.2所示的提示信息中,给出的字母就是按照顺序排列的,其中包含了字母、空格等符号,用户可以按照字母出现的先后顺序进行操作,根据这样的提示输入的信息就是典型的基于前后关系的线性结构。 • 【例3.2】 假设现在有一篇英文文章,某用户想在其中查找某单词,则首先要将这些单词以前后出现的顺序存放在一张表中,假设为A=(a1,a2,…,ai−1,ai,ai+1,…,an),要在其中查找某单词时,可按顺序从a1开始,一直找到最后,如果有匹配的单词存在,则查找成功,否则查找失败。如果查找的过程是在类似于以字母为顺序的字典中进行的,则可以先确定该单词的第一个字母所在位置,然后进一步查找单词具体位置,这些具体操作是基于表A的建立方式不同而不同的。这里表A也是一个线性表的实例。
第二节 • 3.2 线性表的概述 • 【学习任务】 理解线性表的含义,熟练掌握线性表的相关概念,重点理解线性表中逻辑结构、存储结构和相应操作之间的关系。 • 3.2.1 线性表的概念 • 通过前面的例子可以看出,线性表实际上是基于前面元素和后面元素之间的一种相邻关系的结构。 • 1.线性表的定义 • 线性表是将多个具有相同类型的数据元素放在一起构成一组有限序列的结构,通常记为 • A =(a1,a2,…,ai−1,ai,ai+1,…,an)
2.线性表的相关概念 • 在上述线性表的定义中,相关概念如下。 • (1)A代表一个线性表。 • (2)ai(1≤i≤n)称为线性表的元素,i为元素的下标,表示该元素在线性表中的位置。 • (3)线性表中n为表长,其中,n≥0,当n=0时称该表为空表。 • (4)线性表是一种基于相邻数据元素之间的对应关系,将元素ai−1称为元素ai的直接前趋,将元素ai+1称为元素ai的直接后继。通过定义可知,在线性表中,a1是表中第一个元素,它没有前趋,an是最后一个元素,没有后继。 • (5)线性表中的元素ai既可以是一个单个元素,也可以是一个数据元素,即由多个数据项构成的元素。例如,在第1章中所介绍的图书信息表中,一个记录即为一个元素,它包括多个数据项(图书编号、书名、数量、价格)。
因此,线性表的定义也可描述如下:线性表是第一个元素无前驱,最后一个元素无后继,而其他元素都有唯一直接前驱和直接后继的表结构。因此,线性表的定义也可描述如下:线性表是第一个元素无前驱,最后一个元素无后继,而其他元素都有唯一直接前驱和直接后继的表结构。 • 例如,A=(1,4,7,…,49)是一个线性表,每个数值就是一个元素。 • B=((2001,计算机应用基础,10,22),(2003,大学英语,30,14),(2005,机械制图,70,30),(2007,数据结构,35,24))也是一个线性表,每个元素是由4个数据项(图书编号、书名、数量、价格)构成的。
3.2.2 线性表的存储结构及操作 • 1.线性表的逻辑结构与存储结构 • 由线性表的定义可知线性表的逻辑结构,即相临元素之间所满足的前驱和后继的逻辑关系。 • 如果要在计算机中实现对线性表的各种操作,必须了解线性表在计算机中的存储形式(即存储结构)。一种逻辑结构可对应多种存储结构,而每种存储结构又有自己的存储特点和操作方式。
2.线性表的操作 • 线性表的常见操作有:建表(初始化)、求表长、查找、插入、删除等。线性表的操作是在其逻辑结构和存储结构的共同支持下完成的。 • 值得注意的是,每种数据结构的相关操作一定不能脱离其逻辑结构和存储结构而独立存在。 • 3.线性表的分类 • 线性表的存储结构可分为顺序存储结构和链式存储结构两种,因此可将线性表分为顺序表和链表两大类。下面分别介绍顺序表和链表的特点及操作。
第三节 • 3.3 顺序表的基本操作及实现 • 【学习任务】 理解线性表在顺序存储结构下的特点,掌握顺序表的表示、相关算法及程序实现。
3.3.1 顺序表的概述 • 1.定义 • 顺序表是指线性表在顺序存储形式下构成的表 • 2.存储特点 • 顺序表的存储是指在内存中,在一段连续的存储单元中存储线性表。 • 其特点为,线性表逻辑上相邻的数据元素(直接前驱和直接后继)在存储位置(或物理位置)上也相邻,如图3.3所示。
图3.3 线性表的逻辑结构和存储结构对应图 在上述表示中,m为该顺序表存储的首地址,d为每个元素占用的存储空间,因此在顺序表的存储结构中,有如下的对应关系。 设a1的存储地址为m,每个数据元素占d个存储单元,则第i 个数据元素的地址为 Loc(ai)=m+(i−1)*d 1≤i≤n
即只要知道顺序表的首地址和每个数据元素所占的字节数,就可求出第i个数据元素的地址,这也是顺序表具有按数据元素的序号随机存取的特性。即只要知道顺序表的首地址和每个数据元素所占的字节数,就可求出第i个数据元素的地址,这也是顺序表具有按数据元素的序号随机存取的特性。 • 通过前面的介绍,可知顺序表非常适合用高级语言中的数组去实现。值得注意的是,探讨某种数据的操作一定是在一定的逻辑结构和存储结构之上进行的操作。
3.3.2 顺序表的基本操作及实现 • 顺序表的基本操作包括建立、插入、删除、查找等,下面就重点说明插入、删除等操作的实现算法。 • 1.顺序表的表示 • 为实现顺序表的各种操作,首先必须了解顺序表的表示方法,顺序表由两个量表示,即表示同种数据类型的数组和表示数组长度的整型变量。
【例3.3】 以整型数组的表示为例,类如下: • public class LineList { • private int[] data; • private int length; • public LineList() { • } • public void setData(int[] data) { • this.data = data; • } • public void setLength(int length) { • this.length = length; • } • public int[] getData() { • return (this.data); • } • public int getLength() { • return (this.length); • } • } • 说明: • ① 该类定义的是整型数组,在实际应用中,可根据具体情况而定; • ② 类中的成员设置为private的,对外不可见。这就是类的封装性。
2.插入操作及程序实现 • 顺序表的插入是指在现有表结构的基础上,在规定位置插入和顺序表相同性质的元素,插入操作具体过程及结果为:首先确定插入位置i,按照an~ai的顺序由后至前依次将各元素向后移动,为新元素让出位置,将新元素x插入已经让出的第i个位置,结束插入操作,结果如图3.4所示。
【算法3.1】 在整型顺序表的某位置上插入一个值为 a 的整型元素。 • public class LineList { • ... • public boolean insert(int i, int a){ • int j; • if(length >= data.length){ //表空间已经满,不能插入 • System.out.println("The table is overflow."); • return false; • } • if(i<0||i>length){ //插入位置是否正确 • System.out.println("The position is mistake. "+i); • return false; • } • for(j=length-1; j>=i; j--) • data[j+1]=data[j]; • data[i] = a; • length ++; • return true; • } • }
说明: • ① 顺序表算法的实现是通过移动元素实现的,如果插入位置比较靠前,则需要移动大量元素; • ② 顺序表插入操作要注意数组的长度和插入位置的判断,否则会出现错误。 • 3.删除操作及程序实现 • 顺序表的删除是指在现有表的基础上,在规定位置删除某元素,删除操作的具体过程及结果为如下: • 首先确定删除位置i,按照ai+1~an的顺序依次将各元素向前移动,将元素ai删除,结束删除操作,结果如图3.5所示。
【算法3.2】 在整型顺序表中删除某位置上的元素。 • public class LineList { • ... • public boolean delete(int i){ • int j; • if(i<0||i>=length){ //检查删除位置是否存在 • System.out.println("The position is mistake."); • return false; • } • for(j=i;j<length;j++){ • data[j] = data[j+1]; • length --; • return true; • } • } • }
4.顺序表的其他操作及实现 • 对于顺序表来说,除了插入和删除操作外,还有许多操作,如顺序表的建立、顺序表中固定元素的查找、顺序表中固定元素的替换等操作,这些操作的原理和插入、删除操作基本相同,例如顺序表的建立操作实际上就是由一个空的顺序表不断在表的末端进行元素插入,顺序表中固定元素的替换也和插入操作基本一样,找到元素后不需要移动元素,而是将该位置的元素用新的元素替换,这里不再赘述,请读者自行编程实现或参考后面的实例。
5.顺序表算法的效率分析 • 根据前面讲过的顺序表的算法实现,可知其操作大部分是在移动元素的基础上完成的。插入算法需要移动的元素为n−i+1(ai~an),而删除元素需要移动的元素个数为n−i(ai+1~an)个,这里以删除元素为例,分析其效率。 • (1)删除操作的时间性能分析 • 顺序表的删除操作的执行时间主要消耗在移动元素上,假设删除第i个元素时,就需要移动n−i个元素(即第i个元素后面的ai+1~an都要向前移动一个位置),因此,该算法的时间复杂度为O(n)。 • (2)删除操作的空间性能分析
顺序表的删除操作需要使用一个变量存储将要被删除的元素,因此其空间复杂度为O(1),但是由于顺序表存储需要事先规定连续的存储空间,即使被删除的元素实际上也在占用空间,因此其运算的空间复杂度虽然小,但是对空间利用率不是最好的。顺序表的删除操作需要使用一个变量存储将要被删除的元素,因此其空间复杂度为O(1),但是由于顺序表存储需要事先规定连续的存储空间,即使被删除的元素实际上也在占用空间,因此其运算的空间复杂度虽然小,但是对空间利用率不是最好的。 • 6.小结 • 顺序表的存储特点是利用一段连续的存储空间存储该顺序表中的元素,这样可以简化算法操作。但实现时,需要大量移动元素,使程序执行时间变长,尤其是当n很大时,其时间复杂度将会很大,通过上面的分析,该存储结构对空间效率不是最优的。也就是说,顺序表的各种算法实现对时间和空间要求都很高,如何提高算法效率将是一个重要的问题。
第四节 • 3.4 链表的基本操作及实现 • 【学习任务】 理解线性表在链式存储结构下的特点,掌握链表的表示、相关算法及程序实现。1.链表的定义 • 链表也是一种有顺序的表,其内容可以存储在一组任意的存储单元中,所谓任意的存储单元,即这组存储单元可以是连续的,也可以是不连续的,这就需要在存储元素本身信息的同时,还要存储下一个元素的位置,由此构成一个链状结构,称其为链表,如图3.6所示。
图3.6 带头指针头结点的链表示意图 2.链表的相关概念 将表示数据元素和下一个元素位置的结构称为链表的结点。若第一个结点只表示整个链表的起始位置,而无任何信息,称其为头结点。对于最后一个结点,后面无任何元素,其表示元素位置的地址用“∧”来表示,称其为尾结点,程序实现中用“null”来表示,如图3.6所示。
3.链表的表示 • 链表中结点的表示必须要用到两个区域,其中一个存放数据元素自身的信息ai,称为数据域;另一个存放下一个元素的地址或位置,以保证表的连续性,称为指针域。 • 链表中结点的表示如下: • 在C/C++等语言中,提供指针以表示某元素的地址,但是这 样可能会造成比较大的风险。因此,Java语言提出了利用java.util.LinkedList的类库提供的链表类供编程者使用. • 用户可以通过该类库简单地实现指针操作本章通过另外的方式来实现链表的存储,即利用数组的方式实现链表的存储以及算法的实现,为此定义如下结构的类:
public class LinkNode { • private int data = -1; • private LinkNode next = null; • public void setData(int data) { • this.data = data; • } • public void setNext(LinkNode next) { • this.next = next; • } • public int getData() { • return (this.data); • } • public LinkNode getNext() { • return (this.next); • } • } • 设该链表中存储的内容为字符串“study”,链表表示如图3.6所示。
【例3.4】 假设有如下逻辑结构的线性表A=(a1,a2,…,a9),用数组表示的链表对应关系如图3.7所示。 图3.7 数组表示的链表之间对应关系示意图 第1个数组存放的是链表表示的数据,第2个数组存放的是第一个数组中数据之间的前后关系。例如链表从数组下标为7的元素a1开始,由第2个数组对应的下标为7的元素数值,得出下一个元素a2所在的位置为3(即第4个元素),然后继续从第2个数组a2的数值为6(即第7个元素),……,以此类推,最终得出全部元素。这样可用两个数组配合来表示链表。当用链表表示线性表时,由于不是按照元素的顺序进行存储的,因此一定要知道第一个元素的位置(即链表的首位置,用Head表示),同时还要知道最后一个元素的结束标志(即链表的末位置,用∧表示)。
3.4.2 链表的分类 • 按照链表的组成方式,可将链表分为单链表、双向链表、循环链表等。下面以单链表为例进行介绍,其他链表以此类推即可。 • 1.单链表 • 单链表是链表中最常用的一种,也是结构比较简单的一种,是由第1个元素到最后一个元素构成的一个链,其特点从第1个元素(可能有头指针和头结点)到最后一个元素(结束标志为∧)构成的一个链,称为单链表,如图3.6所示。 • 单链表的特点是通过前一个元素的指针域可以顺序找到后面元素所在的位置,因此所有操作全部是从第1个元素(头指针或头结点)开始的。
2.循环链表 • 在单链表中,最后一个元素的存储区域是∧,如果将它指向第1个元素(头结点)位置,就构成了循环链表。 • 如链表中存储的内容为字符串"study",其循环链表表示如图3.8所示。 图3.8 带头指针头结点的循环链表示意图 循环链表的特点是在所有元素之间构成一个环,从任何一个元素出发,都可以查找其他所有的元素,同时还充分地利用了空间。
3.双向链表 • 前面两种链表的查找方式都是沿着一个方向进行的,对于元素比较少的链表进行操作时比较方便;但当元素数量非常多时,若只沿着一个方向进行操作,有时操作不是很方便,因此,若每个元素都可以向两个方向进行查找元素,就大大提高了操作效率,这就构成了双向链表。 • 假设,该链表中存储的内容为字符串"study",其双向循环链表表示如图3.9所示。
该链表的特点是可以从任何一个元素出发,向两个方向分别查找相应元素,可提高操作效率。该链表的特点是可以从任何一个元素出发,向两个方向分别查找相应元素,可提高操作效率。 图3.9 带头指针头结点的双向链表示意图 实际上,链表还可以构成更多的种类,如双向循环链表等,通过前面对链表分类的介绍,应该清楚,无论使用哪种形式的链表,其操作都是相通的。
3.4.3 单链表的基本运算及实现 • 和顺序表的操作类似,单链表的操作也有很多,如单链表的建立、插入、删除,元素的查找、替换等。下面以单链表的插入、删除操作为例进行讲解。 • 1.在单链表的指定位置进行插入操作 • 【例3.5】 建立一个单链表,将字符串"STUDY"存储在该链表中。 • 根据题意,其操作可分为如下步骤。
(1)创建单链表并进行初始化操作 • 建立一个头结点(Head),为其申请空间,如图3.10所示。 图3.10 建立头结点Head (2)在单链表的表头位置插入元素 以如图3.10所示的单链表为基础,依次将元素插入到表头位置,实现将"STUDY"倒置存放到该单链表中,如图3.11所示,表示已经将"TUDY"存放到单链表中。 图3.11 插入元素"TUDY"后的单链表
最后,将"S"插入到如图3.11所示的单链表中,先为元素"S"申请一个结点,用new表示,将其插入到如图3.11所示的单链表中,实现语句为最后,将"S"插入到如图3.11所示的单链表中,先为元素"S"申请一个结点,用new表示,将其插入到如图3.11所示的单链表中,实现语句为 • inp=new OnelinkNode(2); • inp.next=Head; • Head=inp; • 最后,得到如图3.12所示的单链表 图3.12 插入结点S后的单链表
(3)在单链表的中间位置插入元素 • 设在如图3.11所示的单链表中,插入一个元素"R",使其变成"STUDRY",其操作如下: • 首先,确定插入位置,用指针minp来表示; • 然后执行如下语句,完成插入操作。 • inp=new OnelinkNode(3); • inp.next=minp.next; • minp.next=inp; • 其结果如图3.13所示。 图3.13 中间插入元素后的单链表
2.在单链表中进行删除操作 • 【例3.6】 对于用单链表表示的"STUDY",若将"U"删除,应如何操作。 • 首先,确定删除元素的位置用delq表示,将该元素前一个位置用delp表示; • 然后执行如下语句。 • delp.next=delq.next; • 或者delp.next=(delp.next).next; • 结果如图3.14所示。 图3.14 在单链表中删除元素"U"
3.4.4 其他形式的链表的相关运算 • 对于双向链表和循环链表,其操作和单链表非常类似,首先确定操作位置,在操作时,要注意在指针变化过程中,要满足先连线,再改变指针位置的操作顺序,否则就会出现结点丢失问题。
3.4.5 算法实例 • 【算法3.3】 在链表的头部插入结点。 • public class LinkNode { • private int data = -1; • private LinkNode next = null; • public void setData(int data) { • this.data = data; • } • public void setNext(LinkNode next) { • this.next = next; • } • public int getData() { • return (this.data); • } • public LinkNode getNext() { • return (this.next); • } • }
public class LinkTable { • private LinkNode head = null; • private int counts = 0; • public void insert(int d){ • if(head == null){ • head = new LinkNode(); • } • LinkNode n = new LinkNode();//定义新的链表结点,并将数据赋给新结点 • n.setData(d); • if(head.getNext() == null){//如果头结点的后继无结点,注意头结点中无数据 • head.setNext(n); • } • else{ • n.setNext(head.getNext()); //如果头结点的后继结点存在 • head.setNext(n); • } • counts ++; //结点总数增加 • } • public void print(){ • LinkNode n = head.getNext(); • int iCounter = 1; • while(n != null){ • System.out.print(n.getData() + " "); • n = n.getNext(); • iCounter ++;
} • } • public int size(){ • return this.counts; • } • public static void main(String[] args){ • LinkTable linkTable = new LinkTable(); • linkTable.insert(30); • linkTable.insert(23); • linkTable.insert(12); • linkTable.insert(50); • linkTable.insert(36); • linkTable.insert(77); • linkTable.insert(37); • linkTable.print(); • } • } • 程序运行的结果为 • 37 77 36 50 12 23 30
第五节 • 3.5 线性表的应用 • 【学习任务】 在学习线性表基础知识的前提下,掌握线性表在顺序存储结构和链式存储结构下的应用实例及程序实现。 • 3.5.1 顺序表的连接 • 有两个顺序表A和B,其元素均按从小到大的升序排列,编写一个算法将它们合并成一个顺序表C,要求C的元素也是按从小到大的升序排列。 • 算法思路:依次扫描通过A和B的元素,比较当前的元素的值,将较小值的元素赋给C,直到一个线性表扫描完毕,然后将未完的那个顺序表中余下的部分赋给C。C的容量要能够大于A、B两个线性表相加的长度即可。具体程序如下:
public class Combinate{ • void merge(int[] a,int[] b) { • int i,j,k; • i=0;j=0;k=0; • int alength=a.length; • int blength=b.length; • int clength=alength+blength; • int[] c=new int[clength]; • while(i<alength && j<blength) • if(a[i]<b[j]) • c[k++]=a[i++]; • else • c[k++]=b[j++]; • while(i<alength) • c[k++]=a[i++]; • while(j<blength) • c[k++]=b[j++]; • System.out.println();
System.out.print("排序好的是:"); • for(int l=0;l<clength;l++) • System.out.print(" "+c[l]); • } • public static void main(String[] args) { • Combinate a1=new Combinate(); • int[] a={2,5,7}; • int[] b={3,4,8,9}; • System.out.print("a数组:"); • for(int i=0;i<a.length;i++) • System.out.print(" "+a[i]); • System.out.println(); • System.out.print("b数组:"); • for(int j=0;j<b.length;j++) • System.out.print(" "+b[j]); • a1.merge(a,b); • } • }
程序运行的结果为 • a数组:2 5 7 • b数组:3 4 8 9 • 排序好的是:2 3 4 5 7 8 9 3.5.2字符串的逆转算法 假设有如下字符串“STUDY”,利用单链表存储该字符串,并实现将其逆转,即原字符串变为“YDUTS”,如图3.15所示。 分析: 根据单链表的插入和删除操作,其实现过程可以以H1为基础,将其第1个元素S作为单链表的末尾元素插入进来(这个过程实际已经完成);将第2个元素T从原来的H1中删除,然后插入到以H1为头,S为尾的单链表中去;然后按照这个过程依次将U、D、Y从原来的链表中删除,依次插入到原链表的首部,这样就实现了逆转。